refactor(plugins): wire PluginHostDepsFactory through DI, drop tags/categories bridges

This commit is contained in:
jubnl
2026-07-26 15:28:55 +02:00
parent 167db04d69
commit 811230fa5d
21 changed files with 197 additions and 226 deletions
@@ -14,7 +14,8 @@ import { decrypt_api_key } from '../../services/apiKeyCrypto';
import { PluginSupervisor, type PluginRouteInfo } from './supervisor/plugin-supervisor';
import fs from 'node:fs';
import path from 'node:path';
import { createRealRpcHost, closePluginDataDb } from './host/create-rpc-host';
import { PluginHostDepsFactory } from './host/plugin-host-deps.factory';
import { closePluginDataDb } from './host/plugin-host-state';
import { ForbiddenResource } from './host/rpc-host';
import { removePluginData } from './host/plugin-data.service';
import { isKnownPermission } from './protocol/envelope';
@@ -130,8 +131,13 @@ export class PluginDependencyError extends Error {
export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy {
// The rpc-host factory is bound to `this` as the inter-plugin router, so a
// plugin's ctx.plugins.call / ctx.events.emit resolve through callPlugin/
// emitPluginEvent below (which own the dependency-edge authorization).
private readonly supervisor = new PluginSupervisor((id, granted) => createRealRpcHost(id, granted, this), {
// emitPluginEvent below (which own the dependency-edge authorization). The
// arrow reads this.hostDeps lazily at spawn time, so the field-initializer
// ordering (it runs before the constructor params are assigned) is safe.
private readonly supervisor = new PluginSupervisor((id, granted) => {
if (!this.hostDeps) throw new Error('PluginHostDepsFactory not provided — tests that activate plugins must pass one');
return this.hostDeps.create(id, granted, this);
}, {
// Both hooks run from child lifecycle EventEmitter callbacks (exit / stderr 'data'),
// so a throw here becomes an uncaughtException that has no host-side handler. During a
// restore the core DB is briefly CLOSED (closeDb → the this.db proxy throws on access), so a
@@ -163,12 +169,13 @@ export class PluginRuntimeService implements OnModuleInit, OnModuleDestroy {
// Coalesces overlapping erasure drains (the sweep and enqueue both trigger one).
private drainInFlight: Promise<void> | null = null;
// The registry stays optional at the type level so tests can construct the
// service without one; Nest always injects the real one (the provider is in
// the module).
// The registry and host-deps factory stay optional at the type level so tests
// can construct the service without them; Nest always injects the real ones
// (both providers are in the module).
constructor(
private readonly dbs: DatabaseService,
private readonly registry?: PluginRegistryService,
private readonly hostDeps?: PluginHostDepsFactory,
) {}
private get db() {
+10 -1
View File
@@ -23,6 +23,12 @@ import { PluginOAuthService } from './plugin-oauth.service';
import { PluginsService } from './plugins.service';
import { PluginRuntimeService } from './plugin-runtime.service';
import { PluginRegistryService } from './registry/registry.service';
import { PluginHostDepsFactory } from './host/plugin-host-deps.factory';
import { TagsModule } from '../tags/tags.module';
import { CategoriesModule } from '../categories/categories.module';
import { BudgetModule } from '../budget/budget.module';
import { ReservationsModule } from '../reservations/reservations.module';
import { TodoModule } from '../todo/todo.module';
/**
* Plugin system (#plugins). M0 read side + M2 isolated runtime + M3 frontend:
@@ -32,8 +38,11 @@ import { PluginRegistryService } from './registry/registry.service';
* widget assets at /plugin-frame/:id/*.
*/
@Module({
// The DI-native domain services the plugin host wiring injects
// (PluginHostDepsFactory); DatabaseModule is @Global, so not listed.
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule],
controllers: [PluginsController, PluginsFeedController, PluginsProxyController, PluginFrameController, PlaceDetailsController, TripWarningsController, ViewContributionsController, TripCardContributionsController, PluginPhotosController, PluginCalendarController, MapMarkersController, MapLayersController, PluginRoutesController, DayScheduleController, PdfSectionsController, AtlasLayersController, JournalEntryRowsController, PluginUserSettingsController, PluginOAuthController, PluginActivityController],
providers: [PluginsService, PluginRuntimeService, PluginRegistryService, PluginOAuthService],
providers: [PluginsService, PluginRuntimeService, PluginRegistryService, PluginOAuthService, PluginHostDepsFactory],
// Exported so the admin addon-toggle handler can cascade-disable plugins whose
// required addon was just turned off (#plugins dependencies).
exports: [PluginRuntimeService],
+1 -1
View File
@@ -6,7 +6,7 @@ import { devLinkEnabled } from './dev-link';
import { maybe_encrypt_api_key, decrypt_api_key } from '../../services/apiKeyCrypto';
import { readAudit } from './host/plugin-audit';
import { keyFingerprint } from './signature-status';
import { pluginBudgetUsage } from './host/create-rpc-host';
import { pluginBudgetUsage } from './host/plugin-host-state';
import { isAddonEnabled } from '../../services/adminService';
import { parseDependencies, disabledRequiredAddons, resolveDependencyState, type PluginDepRow, type PluginDependencies, type VersionMismatch } from './dependencies';
import { hostSatisfies, hostVersion } from './install/host-compat';