Skip to content

Commit 2baa07f

Browse files
committed
refactor: streamline plugin cache helpers
1 parent 127da4c commit 2baa07f

13 files changed

Lines changed: 161 additions & 90 deletions

src/commands/agents.bindings.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ import { normalizeChannelId as normalizeBundledChannelId } from "../channels/reg
66
import { isRouteBinding, listRouteBindings } from "../config/bindings.js";
77
import type { AgentRouteBinding } from "../config/types.js";
88
import type { OpenClawConfig } from "../config/types.openclaw.js";
9-
import {
10-
listPluginContributionIds,
11-
loadPluginRegistrySnapshot,
12-
} from "../plugins/plugin-registry.js";
9+
import { listManifestChannelContributionIds } from "../plugins/manifest-channel-contributions.js";
1310
import { DEFAULT_ACCOUNT_ID, normalizeAgentId } from "../routing/session-key.js";
1411
import { normalizeOptionalString } from "../shared/string-coerce.js";
1512
import { normalizeStringEntries } from "../shared/string-normalization.js";
@@ -220,16 +217,11 @@ function resolveDefaultAccountId(cfg: OpenClawConfig, provider: ChannelId): stri
220217
}
221218

222219
function listManifestChannelIds(config: OpenClawConfig): Set<string> {
223-
const index = loadPluginRegistrySnapshot({
224-
config,
225-
env: process.env,
226-
});
227220
return new Set(
228-
listPluginContributionIds({
229-
index,
230-
contribution: "channels",
221+
listManifestChannelContributionIds({
231222
includeDisabled: true,
232223
config,
224+
env: process.env,
233225
}),
234226
);
235227
}

src/commands/channel-setup/discovery.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ import type { ChannelMeta } from "../../channels/plugins/types.public.js";
88
import { isStaticallyChannelConfigured } from "../../config/channel-configured-shared.js";
99
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
1010
import type { OpenClawConfig } from "../../config/types.openclaw.js";
11-
import {
12-
listPluginContributionIds,
13-
loadPluginRegistrySnapshot,
14-
} from "../../plugins/plugin-registry.js";
11+
import { listManifestChannelContributionIds } from "../../plugins/manifest-channel-contributions.js";
1512
import type { ChannelChoice } from "../onboard-types.js";
1613
import {
1714
listSetupDiscoveryChannelPluginCatalogEntries,
@@ -51,15 +48,8 @@ export function listManifestInstalledChannelIds(params: {
5148
env: params.env ?? process.env,
5249
}).config;
5350
const workspaceDir = resolveWorkspaceDir(resolvedConfig, params.workspaceDir);
54-
const index = loadPluginRegistrySnapshot({
55-
config: resolvedConfig,
56-
workspaceDir,
57-
env: params.env ?? process.env,
58-
});
5951
return new Set(
60-
listPluginContributionIds({
61-
index,
62-
contribution: "channels",
52+
listManifestChannelContributionIds({
6353
config: resolvedConfig,
6454
workspaceDir,
6555
env: params.env ?? process.env,

src/commands/channels/logs.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { normalizeChannelId as normalizeBundledChannelId } from "../../channels/
33
import { getResolvedLoggerSettings } from "../../logging.js";
44
import { resolveLogFile } from "../../logging/log-tail.js";
55
import { parseLogLine } from "../../logging/parse-log-line.js";
6-
import {
7-
listPluginContributionIds,
8-
loadPluginRegistrySnapshot,
9-
} from "../../plugins/plugin-registry.js";
6+
import { listManifestChannelContributionIds } from "../../plugins/manifest-channel-contributions.js";
107
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
118
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
129
import { theme } from "../../terminal/theme.js";
@@ -23,14 +20,10 @@ const DEFAULT_LIMIT = 200;
2320
const MAX_BYTES = 1_000_000;
2421

2522
function listManifestChannelIds(): Set<string> {
26-
const index = loadPluginRegistrySnapshot({
27-
env: process.env,
28-
});
2923
return new Set(
30-
listPluginContributionIds({
31-
index,
32-
contribution: "channels",
24+
listManifestChannelContributionIds({
3325
includeDisabled: true,
26+
env: process.env,
3427
}),
3528
);
3629
}

src/media/document-extractors.runtime.ts

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,20 @@ import type {
44
DocumentExtractionResult,
55
} from "../plugins/document-extractor-types.js";
66
import { resolvePluginDocumentExtractors } from "../plugins/document-extractors.runtime.js";
7+
import { createConfigScopedPromiseLoader } from "../plugins/plugin-cache-primitives.js";
78
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
89

9-
let extractorPromise: Promise<ReturnType<typeof resolvePluginDocumentExtractors>> | undefined;
10-
const extractorPromisesByConfig = new WeakMap<
11-
OpenClawConfig,
12-
Promise<ReturnType<typeof resolvePluginDocumentExtractors>>
13-
>();
14-
15-
async function loadDocumentExtractors(config?: OpenClawConfig) {
16-
if (config) {
17-
const cached = extractorPromisesByConfig.get(config);
18-
if (cached) {
19-
return await cached;
20-
}
21-
const promise = Promise.resolve().then(() => resolvePluginDocumentExtractors({ config }));
22-
extractorPromisesByConfig.set(config, promise);
23-
void promise.catch(() => {
24-
extractorPromisesByConfig.delete(config);
25-
});
26-
return await promise;
27-
}
28-
extractorPromise ??= Promise.resolve(resolvePluginDocumentExtractors());
29-
return await extractorPromise;
30-
}
10+
const documentExtractorLoader = createConfigScopedPromiseLoader((config?: OpenClawConfig) =>
11+
resolvePluginDocumentExtractors(config ? { config } : undefined),
12+
);
3113

3214
export async function extractDocumentContent(
3315
params: DocumentExtractionRequest & {
3416
config?: OpenClawConfig;
3517
},
3618
): Promise<(DocumentExtractionResult & { extractor: string }) | null> {
3719
const mimeType = normalizeLowercaseStringOrEmpty(params.mimeType);
38-
const extractors = await loadDocumentExtractors(params.config);
20+
const extractors = await documentExtractorLoader.load(params.config);
3921
const request: DocumentExtractionRequest = {
4022
buffer: params.buffer,
4123
mimeType: params.mimeType,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { OpenClawConfig } from "../config/types.openclaw.js";
2+
import { listPluginContributionIds, loadPluginRegistrySnapshot } from "./plugin-registry.js";
3+
4+
export function listManifestChannelContributionIds(
5+
params: {
6+
config?: OpenClawConfig;
7+
workspaceDir?: string;
8+
env?: NodeJS.ProcessEnv;
9+
includeDisabled?: boolean;
10+
} = {},
11+
): readonly string[] {
12+
const env = params.env ?? process.env;
13+
const index = loadPluginRegistrySnapshot({
14+
config: params.config,
15+
workspaceDir: params.workspaceDir,
16+
env,
17+
});
18+
return listPluginContributionIds({
19+
index,
20+
contribution: "channels",
21+
config: params.config,
22+
workspaceDir: params.workspaceDir,
23+
env,
24+
includeDisabled: params.includeDisabled,
25+
});
26+
}

src/plugins/plugin-cache-primitives.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
22
import type { OpenClawConfig } from "../config/types.openclaw.js";
33
import {
44
PluginLruCache,
5+
createConfigScopedPromiseLoader,
56
resolveConfigScopedRuntimeCacheValue,
67
type ConfigScopedRuntimeCache,
78
} from "./plugin-cache-primitives.js";
@@ -84,3 +85,64 @@ describe("resolveConfigScopedRuntimeCacheValue", () => {
8485
expect(load).toHaveBeenCalledOnce();
8586
});
8687
});
88+
89+
describe("createConfigScopedPromiseLoader", () => {
90+
it("dedupes concurrent default loads", async () => {
91+
let calls = 0;
92+
const loader = createConfigScopedPromiseLoader(async () => `loaded-${++calls}`);
93+
94+
await expect(Promise.all([loader.load(), loader.load()])).resolves.toEqual([
95+
"loaded-1",
96+
"loaded-1",
97+
]);
98+
await expect(loader.load()).resolves.toBe("loaded-1");
99+
expect(calls).toBe(1);
100+
});
101+
102+
it("caches loads by config object", async () => {
103+
const firstConfig = { plugins: { load: { disabled: true } } } as OpenClawConfig;
104+
const secondConfig = { plugins: { load: { disabled: false } } } as OpenClawConfig;
105+
const load = vi.fn(async (config?: OpenClawConfig) =>
106+
config === firstConfig ? "first" : "second",
107+
);
108+
const loader = createConfigScopedPromiseLoader(load);
109+
110+
await expect(loader.load(firstConfig)).resolves.toBe("first");
111+
await expect(loader.load(firstConfig)).resolves.toBe("first");
112+
await expect(loader.load(secondConfig)).resolves.toBe("second");
113+
114+
expect(load).toHaveBeenCalledTimes(2);
115+
});
116+
117+
it("evicts rejected loads so retries can recover", async () => {
118+
const config = {} as OpenClawConfig;
119+
let calls = 0;
120+
const loader = createConfigScopedPromiseLoader(async () => {
121+
calls += 1;
122+
if (calls === 1) {
123+
throw new Error("transient");
124+
}
125+
return "recovered";
126+
});
127+
128+
await expect(loader.load(config)).rejects.toThrow("transient");
129+
await expect(loader.load(config)).resolves.toBe("recovered");
130+
expect(calls).toBe(2);
131+
});
132+
133+
it("clears default and config-scoped entries", async () => {
134+
const config = {} as OpenClawConfig;
135+
let calls = 0;
136+
const loader = createConfigScopedPromiseLoader(
137+
async (owner?: OpenClawConfig) => `${owner ? "config" : "default"}-${++calls}`,
138+
);
139+
140+
await expect(loader.load()).resolves.toBe("default-1");
141+
await expect(loader.load(config)).resolves.toBe("config-2");
142+
143+
loader.clear();
144+
145+
await expect(loader.load()).resolves.toBe("default-3");
146+
await expect(loader.load(config)).resolves.toBe("config-4");
147+
});
148+
});

src/plugins/plugin-cache-primitives.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export class PluginLruCache<T> {
6868

6969
export type ConfigScopedRuntimeCache<T> = WeakMap<OpenClawConfig, Map<string, T>>;
7070

71+
export type ConfigScopedPromiseLoader<T> = {
72+
load(config?: OpenClawConfig): Promise<T>;
73+
clear(): void;
74+
};
75+
7176
export function resolveConfigScopedRuntimeCacheValue<T>(params: {
7277
cache: ConfigScopedRuntimeCache<T>;
7378
config?: OpenClawConfig;
@@ -94,6 +99,45 @@ export function createPluginCacheKey(parts: readonly unknown[]): string {
9499
return JSON.stringify(parts);
95100
}
96101

102+
export function createConfigScopedPromiseLoader<T>(
103+
load: (config?: OpenClawConfig) => T | Promise<T>,
104+
): ConfigScopedPromiseLoader<T> {
105+
let defaultPromise: Promise<T> | undefined;
106+
let promisesByConfig = new WeakMap<OpenClawConfig, Promise<T>>();
107+
108+
const createPromise = (config?: OpenClawConfig): Promise<T> => {
109+
const promise = Promise.resolve().then(() => load(config));
110+
void promise.catch(() => {
111+
if (config) {
112+
promisesByConfig.delete(config);
113+
} else if (defaultPromise === promise) {
114+
defaultPromise = undefined;
115+
}
116+
});
117+
return promise;
118+
};
119+
120+
return {
121+
async load(config?: OpenClawConfig): Promise<T> {
122+
if (!config) {
123+
defaultPromise ??= createPromise();
124+
return await defaultPromise;
125+
}
126+
const cached = promisesByConfig.get(config);
127+
if (cached) {
128+
return await cached;
129+
}
130+
const promise = createPromise(config);
131+
promisesByConfig.set(config, promise);
132+
return await promise;
133+
},
134+
clear(): void {
135+
defaultPromise = undefined;
136+
promisesByConfig = new WeakMap<OpenClawConfig, Promise<T>>();
137+
},
138+
};
139+
}
140+
97141
function normalizeMaxEntries(value: number, fallback: number): number {
98142
if (!Number.isFinite(value) || value <= 0) {
99143
return fallback;

src/plugins/provider-auth-choices.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js";
22
import type { OpenClawConfig } from "../config/types.openclaw.js";
33
import { sanitizeForLog } from "../terminal/ansi.js";
44
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
5+
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
56
import type { PluginManifestRecord } from "./manifest-registry.js";
6-
import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
77
import type { PluginOrigin } from "./plugin-origin.types.js";
88

99
export type ProviderAuthChoiceMetadata = {
@@ -180,7 +180,7 @@ function resolveManifestProviderAuthChoiceCandidates(params?: {
180180
env?: NodeJS.ProcessEnv;
181181
includeUntrustedWorkspacePlugins?: boolean;
182182
}): ProviderAuthChoiceCandidate[] {
183-
const metadataSnapshot = loadPluginMetadataSnapshot({
183+
const metadataSnapshot = loadManifestMetadataSnapshot({
184184
config: params?.config ?? {},
185185
workspaceDir: params?.workspaceDir,
186186
env: params?.env ?? process.env,

src/plugins/provider-discovery.runtime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { OpenClawConfig } from "../config/types.openclaw.js";
2+
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
23
import type { PluginManifestRecord } from "./manifest-registry.js";
3-
import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
44
import type { PluginMetadataRegistryView } from "./plugin-metadata-snapshot.types.js";
55
import { resolveDiscoveredProviderPluginIds } from "./providers.js";
66
import { resolvePluginProviders } from "./providers.runtime.js";
@@ -80,7 +80,7 @@ function resolveProviderDiscoveryEntryPlugins(params: {
8080
}): ProviderDiscoveryEntryResult {
8181
const metadataSnapshot =
8282
params.pluginMetadataSnapshot ??
83-
loadPluginMetadataSnapshot({
83+
loadManifestMetadataSnapshot({
8484
config: params.config ?? {},
8585
env: params.env ?? process.env,
8686
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),

src/plugins/setup-registry.runtime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createRequire } from "node:module";
22
import { normalizeProviderId } from "../agents/provider-id.js";
33
import { isInstalledPluginEnabled } from "./installed-plugin-index.js";
4-
import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
4+
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
55

66
type SetupRegistryRuntimeModule = Pick<
77
typeof import("./setup-registry.js"),
@@ -30,7 +30,7 @@ export const __testing = {
3030
};
3131

3232
function resolveBundledSetupCliBackends(): SetupCliBackendRuntimeEntry[] {
33-
const snapshot = loadPluginMetadataSnapshot({ config: {}, env: process.env });
33+
const snapshot = loadManifestMetadataSnapshot({ config: {}, env: process.env });
3434
return snapshot.plugins.flatMap((plugin) => {
3535
if (plugin.origin !== "bundled" || !isInstalledPluginEnabled(snapshot.index, plugin.id)) {
3636
return [];

0 commit comments

Comments
 (0)