From 440fc73448ac06ec22dd9381dadeb8a1106736fa Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 27 Apr 2026 16:10:50 +0100 Subject: [PATCH] refactor: extract plugin metadata snapshot --- src/plugins/plugin-lookup-table.test.ts | 45 +++++++ src/plugins/plugin-lookup-table.ts | 160 ++++------------------ src/plugins/plugin-metadata-snapshot.ts | 169 ++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 136 deletions(-) create mode 100644 src/plugins/plugin-metadata-snapshot.ts diff --git a/src/plugins/plugin-lookup-table.test.ts b/src/plugins/plugin-lookup-table.test.ts index 77c209e479c..13e063d77d1 100644 --- a/src/plugins/plugin-lookup-table.test.ts +++ b/src/plugins/plugin-lookup-table.test.ts @@ -184,4 +184,49 @@ describe("loadPluginLookUpTable", () => { expect(table.startup.configuredDeferredChannelPluginIds).toEqual([]); expect(table.startup.pluginIds).toEqual(["telegram"]); }); + + it("derives startup ids from a provided metadata snapshot without reloading manifests", async () => { + const plugins = [ + createManifestRecord({ + id: "telegram", + origin: "bundled", + channels: ["telegram"], + }), + ]; + const index = createIndex(plugins); + const manifestRegistry: PluginManifestRegistry = { + plugins, + diagnostics: [], + }; + loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); + const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); + const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); + + const metadataSnapshot = loadPluginMetadataSnapshot({ + config: { + channels: { + telegram: { token: "configured" }, + }, + } as OpenClawConfig, + env: {}, + index, + }); + loadPluginManifestRegistryForInstalledIndex.mockClear(); + + const table = loadPluginLookUpTable({ + config: { + channels: { + telegram: { token: "configured" }, + }, + } as OpenClawConfig, + env: {}, + metadataSnapshot, + }); + + expect(loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled(); + expect(table.manifestRegistry).toBe(manifestRegistry); + expect(table.startup.pluginIds).toEqual(["telegram"]); + expect(table.metrics.indexPluginCount).toBe(1); + expect(table.metrics.manifestPluginCount).toBe(1); + }); }); diff --git a/src/plugins/plugin-lookup-table.ts b/src/plugins/plugin-lookup-table.ts index adcae71de6c..aaa4d98a85b 100644 --- a/src/plugins/plugin-lookup-table.ts +++ b/src/plugins/plugin-lookup-table.ts @@ -5,26 +5,14 @@ import { resolveGatewayStartupPluginIdsFromRegistry, } from "./channel-plugin-ids.js"; import { hashJson } from "./installed-plugin-index-hash.js"; -import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; -import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; -import type { PluginDiagnostic } from "./manifest-types.js"; -import { createPluginRegistryIdNormalizer } from "./plugin-registry-contributions.js"; import { - loadPluginRegistrySnapshotWithMetadata, - type PluginRegistrySnapshot, - type PluginRegistrySnapshotDiagnostic, -} from "./plugin-registry-snapshot.js"; + loadPluginMetadataSnapshot, + type PluginMetadataSnapshot, + type PluginMetadataSnapshotOwnerMaps, +} from "./plugin-metadata-snapshot.js"; +import type { PluginRegistrySnapshot } from "./plugin-registry-snapshot.js"; -export type PluginLookUpTableOwnerMaps = { - channels: ReadonlyMap; - channelConfigs: ReadonlyMap; - providers: ReadonlyMap; - modelCatalogProviders: ReadonlyMap; - cliBackends: ReadonlyMap; - setupProviders: ReadonlyMap; - commandAliases: ReadonlyMap; - contracts: ReadonlyMap; -}; +export type PluginLookUpTableOwnerMaps = PluginMetadataSnapshotOwnerMaps; export type PluginLookUpTableStartupPlan = { channelPluginIds: readonly string[]; @@ -44,18 +32,14 @@ export type PluginLookUpTableMetrics = { deferredChannelPluginCount: number; }; -export type PluginLookUpTable = { +export type PluginLookUpTable = PluginMetadataSnapshot & { key: string; - index: PluginRegistrySnapshot; - registryDiagnostics: readonly PluginRegistrySnapshotDiagnostic[]; - manifestRegistry: PluginManifestRegistry; - plugins: readonly PluginManifestRecord[]; - diagnostics: readonly PluginDiagnostic[]; - byPluginId: ReadonlyMap; - normalizePluginId: (pluginId: string) => string; - owners: PluginLookUpTableOwnerMaps; startup: PluginLookUpTableStartupPlan; - metrics: PluginLookUpTableMetrics; + metrics: PluginMetadataSnapshot["metrics"] & + Pick< + PluginLookUpTableMetrics, + "startupPlanMs" | "startupPluginCount" | "deferredChannelPluginCount" + >; }; export type LoadPluginLookUpTableParams = { @@ -64,97 +48,19 @@ export type LoadPluginLookUpTableParams = { workspaceDir?: string; env: NodeJS.ProcessEnv; index?: PluginRegistrySnapshot; + metadataSnapshot?: PluginMetadataSnapshot; }; -function appendOwner(owners: Map, ownedId: string, pluginId: string): void { - const existing = owners.get(ownedId); - if (existing) { - existing.push(pluginId); - return; - } - owners.set(ownedId, [pluginId]); -} - -function freezeOwnerMap(owners: Map): ReadonlyMap { - return new Map( - [...owners.entries()].map(([ownedId, pluginIds]) => [ownedId, Object.freeze([...pluginIds])]), - ); -} - -function buildOwnerMaps(plugins: readonly PluginManifestRecord[]): PluginLookUpTableOwnerMaps { - const channels = new Map(); - const channelConfigs = new Map(); - const providers = new Map(); - const modelCatalogProviders = new Map(); - const cliBackends = new Map(); - const setupProviders = new Map(); - const commandAliases = new Map(); - const contracts = new Map(); - - for (const plugin of plugins) { - for (const channelId of plugin.channels) { - appendOwner(channels, channelId, plugin.id); - } - for (const channelId of Object.keys(plugin.channelConfigs ?? {})) { - appendOwner(channelConfigs, channelId, plugin.id); - } - for (const providerId of plugin.providers) { - appendOwner(providers, providerId, plugin.id); - } - for (const providerId of Object.keys(plugin.modelCatalog?.providers ?? {})) { - appendOwner(modelCatalogProviders, providerId, plugin.id); - } - for (const cliBackendId of plugin.cliBackends) { - appendOwner(cliBackends, cliBackendId, plugin.id); - } - for (const cliBackendId of plugin.setup?.cliBackends ?? []) { - appendOwner(cliBackends, cliBackendId, plugin.id); - } - for (const setupProvider of plugin.setup?.providers ?? []) { - appendOwner(setupProviders, setupProvider.id, plugin.id); - } - for (const commandAlias of plugin.commandAliases ?? []) { - appendOwner(commandAliases, commandAlias.name, plugin.id); - } - for (const [contract, values] of Object.entries(plugin.contracts ?? {})) { - if (Array.isArray(values) && values.length > 0) { - appendOwner(contracts, contract, plugin.id); - } - } - } - - return { - channels: freezeOwnerMap(channels), - channelConfigs: freezeOwnerMap(channelConfigs), - providers: freezeOwnerMap(providers), - modelCatalogProviders: freezeOwnerMap(modelCatalogProviders), - cliBackends: freezeOwnerMap(cliBackends), - setupProviders: freezeOwnerMap(setupProviders), - commandAliases: freezeOwnerMap(commandAliases), - contracts: freezeOwnerMap(contracts), - }; -} - export function loadPluginLookUpTable(params: LoadPluginLookUpTableParams): PluginLookUpTable { - const totalStartedAt = performance.now(); - const registryStartedAt = performance.now(); - const registryResult = loadPluginRegistrySnapshotWithMetadata({ - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - ...(params.index ? { index: params.index } : {}), - }); - const registrySnapshotMs = performance.now() - registryStartedAt; - const index = registryResult.snapshot; - const manifestStartedAt = performance.now(); - const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({ - index, - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - includeDisabled: true, - }); - const manifestRegistryMs = performance.now() - manifestStartedAt; + const metadataSnapshot = + params.metadataSnapshot ?? + loadPluginMetadataSnapshot({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + ...(params.index ? { index: params.index } : {}), + }); + const { index, manifestRegistry } = metadataSnapshot; const startupPlanStartedAt = performance.now(); const channelPluginIds = resolveChannelPluginIdsFromRegistry({ manifestRegistry }); const configuredDeferredChannelPluginIds = resolveConfiguredDeferredChannelPluginIdsFromRegistry({ @@ -173,19 +79,14 @@ export function loadPluginLookUpTable(params: LoadPluginLookUpTableParams): Plug manifestRegistry, }); const startupPlanMs = performance.now() - startupPlanStartedAt; - const normalizePluginId = createPluginRegistryIdNormalizer(index, { manifestRegistry }); - const byPluginId = new Map(manifestRegistry.plugins.map((plugin) => [plugin.id, plugin])); - const ownerMapsStartedAt = performance.now(); - const owners = buildOwnerMaps(manifestRegistry.plugins); - const ownerMapsMs = performance.now() - ownerMapsStartedAt; const startup = { channelPluginIds, configuredDeferredChannelPluginIds, pluginIds, }; - const totalMs = performance.now() - totalStartedAt; return { + ...metadataSnapshot, key: hashJson({ policyHash: index.policyHash, generatedAtMs: index.generatedAtMs, @@ -196,23 +97,10 @@ export function loadPluginLookUpTable(params: LoadPluginLookUpTableParams): Plug ]), startup, }), - index, - registryDiagnostics: registryResult.diagnostics, - manifestRegistry, - plugins: manifestRegistry.plugins, - diagnostics: manifestRegistry.diagnostics, - byPluginId, - normalizePluginId, - owners, startup, metrics: { - registrySnapshotMs, - manifestRegistryMs, + ...metadataSnapshot.metrics, startupPlanMs, - ownerMapsMs, - totalMs, - indexPluginCount: index.plugins.length, - manifestPluginCount: manifestRegistry.plugins.length, startupPluginCount: pluginIds.length, deferredChannelPluginCount: configuredDeferredChannelPluginIds.length, }, diff --git a/src/plugins/plugin-metadata-snapshot.ts b/src/plugins/plugin-metadata-snapshot.ts new file mode 100644 index 00000000000..a9ab81f6f8d --- /dev/null +++ b/src/plugins/plugin-metadata-snapshot.ts @@ -0,0 +1,169 @@ +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; +import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; +import type { PluginDiagnostic } from "./manifest-types.js"; +import { createPluginRegistryIdNormalizer } from "./plugin-registry-contributions.js"; +import { + loadPluginRegistrySnapshotWithMetadata, + type PluginRegistrySnapshot, + type PluginRegistrySnapshotDiagnostic, +} from "./plugin-registry-snapshot.js"; + +export type PluginMetadataSnapshotOwnerMaps = { + channels: ReadonlyMap; + channelConfigs: ReadonlyMap; + providers: ReadonlyMap; + modelCatalogProviders: ReadonlyMap; + cliBackends: ReadonlyMap; + setupProviders: ReadonlyMap; + commandAliases: ReadonlyMap; + contracts: ReadonlyMap; +}; + +export type PluginMetadataSnapshotMetrics = { + registrySnapshotMs: number; + manifestRegistryMs: number; + ownerMapsMs: number; + totalMs: number; + indexPluginCount: number; + manifestPluginCount: number; +}; + +export type PluginMetadataSnapshot = { + index: PluginRegistrySnapshot; + registryDiagnostics: readonly PluginRegistrySnapshotDiagnostic[]; + manifestRegistry: PluginManifestRegistry; + plugins: readonly PluginManifestRecord[]; + diagnostics: readonly PluginDiagnostic[]; + byPluginId: ReadonlyMap; + normalizePluginId: (pluginId: string) => string; + owners: PluginMetadataSnapshotOwnerMaps; + metrics: PluginMetadataSnapshotMetrics; +}; + +export type LoadPluginMetadataSnapshotParams = { + config: OpenClawConfig; + workspaceDir?: string; + env: NodeJS.ProcessEnv; + index?: PluginRegistrySnapshot; +}; + +function appendOwner(owners: Map, ownedId: string, pluginId: string): void { + const existing = owners.get(ownedId); + if (existing) { + existing.push(pluginId); + return; + } + owners.set(ownedId, [pluginId]); +} + +function freezeOwnerMap(owners: Map): ReadonlyMap { + return new Map( + [...owners.entries()].map(([ownedId, pluginIds]) => [ownedId, Object.freeze([...pluginIds])]), + ); +} + +export function buildPluginMetadataOwnerMaps( + plugins: readonly PluginManifestRecord[], +): PluginMetadataSnapshotOwnerMaps { + const channels = new Map(); + const channelConfigs = new Map(); + const providers = new Map(); + const modelCatalogProviders = new Map(); + const cliBackends = new Map(); + const setupProviders = new Map(); + const commandAliases = new Map(); + const contracts = new Map(); + + for (const plugin of plugins) { + for (const channelId of plugin.channels) { + appendOwner(channels, channelId, plugin.id); + } + for (const channelId of Object.keys(plugin.channelConfigs ?? {})) { + appendOwner(channelConfigs, channelId, plugin.id); + } + for (const providerId of plugin.providers) { + appendOwner(providers, providerId, plugin.id); + } + for (const providerId of Object.keys(plugin.modelCatalog?.providers ?? {})) { + appendOwner(modelCatalogProviders, providerId, plugin.id); + } + for (const cliBackendId of plugin.cliBackends) { + appendOwner(cliBackends, cliBackendId, plugin.id); + } + for (const cliBackendId of plugin.setup?.cliBackends ?? []) { + appendOwner(cliBackends, cliBackendId, plugin.id); + } + for (const setupProvider of plugin.setup?.providers ?? []) { + appendOwner(setupProviders, setupProvider.id, plugin.id); + } + for (const commandAlias of plugin.commandAliases ?? []) { + appendOwner(commandAliases, commandAlias.name, plugin.id); + } + for (const [contract, values] of Object.entries(plugin.contracts ?? {})) { + if (Array.isArray(values) && values.length > 0) { + appendOwner(contracts, contract, plugin.id); + } + } + } + + return { + channels: freezeOwnerMap(channels), + channelConfigs: freezeOwnerMap(channelConfigs), + providers: freezeOwnerMap(providers), + modelCatalogProviders: freezeOwnerMap(modelCatalogProviders), + cliBackends: freezeOwnerMap(cliBackends), + setupProviders: freezeOwnerMap(setupProviders), + commandAliases: freezeOwnerMap(commandAliases), + contracts: freezeOwnerMap(contracts), + }; +} + +export function loadPluginMetadataSnapshot( + params: LoadPluginMetadataSnapshotParams, +): PluginMetadataSnapshot { + const totalStartedAt = performance.now(); + const registryStartedAt = performance.now(); + const registryResult = loadPluginRegistrySnapshotWithMetadata({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + ...(params.index ? { index: params.index } : {}), + }); + const registrySnapshotMs = performance.now() - registryStartedAt; + const index = registryResult.snapshot; + const manifestStartedAt = performance.now(); + const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({ + index, + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + includeDisabled: true, + }); + const manifestRegistryMs = performance.now() - manifestStartedAt; + const normalizePluginId = createPluginRegistryIdNormalizer(index, { manifestRegistry }); + const byPluginId = new Map(manifestRegistry.plugins.map((plugin) => [plugin.id, plugin])); + const ownerMapsStartedAt = performance.now(); + const owners = buildPluginMetadataOwnerMaps(manifestRegistry.plugins); + const ownerMapsMs = performance.now() - ownerMapsStartedAt; + const totalMs = performance.now() - totalStartedAt; + + return { + index, + registryDiagnostics: registryResult.diagnostics, + manifestRegistry, + plugins: manifestRegistry.plugins, + diagnostics: manifestRegistry.diagnostics, + byPluginId, + normalizePluginId, + owners, + metrics: { + registrySnapshotMs, + manifestRegistryMs, + ownerMapsMs, + totalMs, + indexPluginCount: index.plugins.length, + manifestPluginCount: manifestRegistry.plugins.length, + }, + }; +}