refactor: extract plugin metadata snapshot

This commit is contained in:
Shakker
2026-04-27 16:10:50 +01:00
parent 04b5dd097d
commit 440fc73448
3 changed files with 238 additions and 136 deletions

View File

@@ -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);
});
});

View File

@@ -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<string, readonly string[]>;
channelConfigs: ReadonlyMap<string, readonly string[]>;
providers: ReadonlyMap<string, readonly string[]>;
modelCatalogProviders: ReadonlyMap<string, readonly string[]>;
cliBackends: ReadonlyMap<string, readonly string[]>;
setupProviders: ReadonlyMap<string, readonly string[]>;
commandAliases: ReadonlyMap<string, readonly string[]>;
contracts: ReadonlyMap<string, readonly string[]>;
};
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<string, PluginManifestRecord>;
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<string, string[]>, ownedId: string, pluginId: string): void {
const existing = owners.get(ownedId);
if (existing) {
existing.push(pluginId);
return;
}
owners.set(ownedId, [pluginId]);
}
function freezeOwnerMap(owners: Map<string, string[]>): ReadonlyMap<string, readonly string[]> {
return new Map(
[...owners.entries()].map(([ownedId, pluginIds]) => [ownedId, Object.freeze([...pluginIds])]),
);
}
function buildOwnerMaps(plugins: readonly PluginManifestRecord[]): PluginLookUpTableOwnerMaps {
const channels = new Map<string, string[]>();
const channelConfigs = new Map<string, string[]>();
const providers = new Map<string, string[]>();
const modelCatalogProviders = new Map<string, string[]>();
const cliBackends = new Map<string, string[]>();
const setupProviders = new Map<string, string[]>();
const commandAliases = new Map<string, string[]>();
const contracts = new Map<string, string[]>();
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,
},

View File

@@ -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<string, readonly string[]>;
channelConfigs: ReadonlyMap<string, readonly string[]>;
providers: ReadonlyMap<string, readonly string[]>;
modelCatalogProviders: ReadonlyMap<string, readonly string[]>;
cliBackends: ReadonlyMap<string, readonly string[]>;
setupProviders: ReadonlyMap<string, readonly string[]>;
commandAliases: ReadonlyMap<string, readonly string[]>;
contracts: ReadonlyMap<string, readonly string[]>;
};
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<string, PluginManifestRecord>;
normalizePluginId: (pluginId: string) => string;
owners: PluginMetadataSnapshotOwnerMaps;
metrics: PluginMetadataSnapshotMetrics;
};
export type LoadPluginMetadataSnapshotParams = {
config: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
index?: PluginRegistrySnapshot;
};
function appendOwner(owners: Map<string, string[]>, ownedId: string, pluginId: string): void {
const existing = owners.get(ownedId);
if (existing) {
existing.push(pluginId);
return;
}
owners.set(ownedId, [pluginId]);
}
function freezeOwnerMap(owners: Map<string, string[]>): ReadonlyMap<string, readonly string[]> {
return new Map(
[...owners.entries()].map(([ownedId, pluginIds]) => [ownedId, Object.freeze([...pluginIds])]),
);
}
export function buildPluginMetadataOwnerMaps(
plugins: readonly PluginManifestRecord[],
): PluginMetadataSnapshotOwnerMaps {
const channels = new Map<string, string[]>();
const channelConfigs = new Map<string, string[]>();
const providers = new Map<string, string[]>();
const modelCatalogProviders = new Map<string, string[]>();
const cliBackends = new Map<string, string[]>();
const setupProviders = new Map<string, string[]>();
const commandAliases = new Map<string, string[]>();
const contracts = new Map<string, string[]>();
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,
},
};
}