From f43a184103b68748e4340ac073b7cadb7bcb0332 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 05:01:33 +0100 Subject: [PATCH] refactor: centralize plugin cache primitives --- src/plugins/bundled-capability-runtime.ts | 3 +- .../bundled-channel-config-metadata.ts | 3 +- .../config-scoped-runtime-cache.test.ts | 14 +++ src/plugins/config-scoped-runtime-cache.ts | 30 +---- src/plugins/doctor-contract-registry.ts | 3 +- src/plugins/loader-cache-state.ts | 2 +- src/plugins/loader.ts | 3 +- src/plugins/manifest.ts | 28 ++--- src/plugins/plugin-cache-primitives.ts | 118 ++++++++++++++++++ src/plugins/plugin-lru-cache.ts | 73 +---------- .../plugin-module-loader-cache.test.ts | 33 +++++ src/plugins/plugin-module-loader-cache.ts | 14 ++- src/plugins/provider-hook-runtime.ts | 76 +++++------ src/plugins/public-surface-loader.ts | 3 +- .../runtime/runtime-web-channel-plugin.ts | 7 +- src/plugins/schema-validator.ts | 3 +- src/plugins/sdk-alias.ts | 2 +- src/plugins/setup-registry.ts | 3 +- src/plugins/source-loader.ts | 8 +- 19 files changed, 255 insertions(+), 171 deletions(-) create mode 100644 src/plugins/plugin-cache-primitives.ts diff --git a/src/plugins/bundled-capability-runtime.ts b/src/plugins/bundled-capability-runtime.ts index d6ced577e42..381888c96a0 100644 --- a/src/plugins/bundled-capability-runtime.ts +++ b/src/plugins/bundled-capability-runtime.ts @@ -13,6 +13,7 @@ import type { PluginLoadOptions } from "./loader.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; import { unwrapDefaultModuleExport } from "./module-export.js"; import { + createPluginModuleLoaderCache, getCachedPluginModuleLoader, type PluginModuleLoaderCache, } from "./plugin-module-loader-cache.js"; @@ -196,7 +197,7 @@ export function loadBundledCapabilityRuntimeRegistry(params: { const env = params.env ?? process.env; const pluginIds = new Set(params.pluginIds); const registry = createEmptyPluginRegistry(); - const moduleLoaders: PluginModuleLoaderCache = new Map(); + const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); const getModuleLoader = (modulePath: string) => { const tryNative = diff --git a/src/plugins/bundled-channel-config-metadata.ts b/src/plugins/bundled-channel-config-metadata.ts index 847cc449e1e..cc976a71608 100644 --- a/src/plugins/bundled-channel-config-metadata.ts +++ b/src/plugins/bundled-channel-config-metadata.ts @@ -14,6 +14,7 @@ import type { PluginManifestChannelConfig, } from "./manifest.js"; import { + createPluginModuleLoaderCache, getCachedPluginModuleLoader, type PluginModuleLoaderCache, } from "./plugin-module-loader-cache.js"; @@ -35,7 +36,7 @@ type ChannelConfigSurface = { runtime?: ChannelConfigRuntimeSchema; }; -const moduleLoaders: PluginModuleLoaderCache = new Map(); +const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); function isBuiltChannelConfigSchema(value: unknown): value is ChannelConfigSurface { if (!value || typeof value !== "object") { diff --git a/src/plugins/config-scoped-runtime-cache.test.ts b/src/plugins/config-scoped-runtime-cache.test.ts index aa2aad5f017..523a011fe61 100644 --- a/src/plugins/config-scoped-runtime-cache.test.ts +++ b/src/plugins/config-scoped-runtime-cache.test.ts @@ -28,4 +28,18 @@ describe("resolveConfigScopedRuntimeCacheValue", () => { expect(resolveConfigScopedRuntimeCacheValue({ cache, key: "demo", load })).toBe("loaded"); expect(load).toHaveBeenCalledTimes(2); }); + + it("caches undefined values by key", () => { + const cache: ConfigScopedRuntimeCache = new WeakMap(); + const config = {} as OpenClawConfig; + const load = vi.fn(() => undefined); + + expect(resolveConfigScopedRuntimeCacheValue({ cache, config, key: "missing", load })).toBe( + undefined, + ); + expect(resolveConfigScopedRuntimeCacheValue({ cache, config, key: "missing", load })).toBe( + undefined, + ); + expect(load).toHaveBeenCalledOnce(); + }); }); diff --git a/src/plugins/config-scoped-runtime-cache.ts b/src/plugins/config-scoped-runtime-cache.ts index cdce7d7e5f3..5b5dfc69792 100644 --- a/src/plugins/config-scoped-runtime-cache.ts +++ b/src/plugins/config-scoped-runtime-cache.ts @@ -1,26 +1,4 @@ -import type { OpenClawConfig } from "../config/types.openclaw.js"; - -export type ConfigScopedRuntimeCache = WeakMap>; - -export function resolveConfigScopedRuntimeCacheValue(params: { - cache: ConfigScopedRuntimeCache; - config?: OpenClawConfig; - key: string; - load: () => T; -}): T { - if (!params.config) { - return params.load(); - } - let configCache = params.cache.get(params.config); - if (!configCache) { - configCache = new Map(); - params.cache.set(params.config, configCache); - } - const cached = configCache.get(params.key); - if (cached !== undefined) { - return cached; - } - const loaded = params.load(); - configCache.set(params.key, loaded); - return loaded; -} +export { + resolveConfigScopedRuntimeCacheValue, + type ConfigScopedRuntimeCache, +} from "./plugin-cache-primitives.js"; diff --git a/src/plugins/doctor-contract-registry.ts b/src/plugins/doctor-contract-registry.ts index e17806ccb5f..c182e1cf973 100644 --- a/src/plugins/doctor-contract-registry.ts +++ b/src/plugins/doctor-contract-registry.ts @@ -6,6 +6,7 @@ import type { OpenClawConfig } from "../config/types.js"; import { asNullableRecord } from "../shared/record-coerce.js"; import type { PluginManifestRegistry } from "./manifest-registry.js"; import { + createPluginModuleLoaderCache, getCachedPluginModuleLoader, type PluginModuleLoaderCache, } from "./plugin-module-loader-cache.js"; @@ -39,7 +40,7 @@ type PluginDoctorContractEntry = { type PluginManifestRegistryRecord = PluginManifestRegistry["plugins"][number]; -const moduleLoaders: PluginModuleLoaderCache = new Map(); +const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); function loadPluginDoctorContractModule(modulePath: string): PluginDoctorContractModule { return getCachedPluginModuleLoader({ diff --git a/src/plugins/loader-cache-state.ts b/src/plugins/loader-cache-state.ts index 4714f8ea114..840c0b2f549 100644 --- a/src/plugins/loader-cache-state.ts +++ b/src/plugins/loader-cache-state.ts @@ -1,4 +1,4 @@ -import { PluginLruCache } from "./plugin-lru-cache.js"; +import { PluginLruCache } from "./plugin-cache-primitives.js"; export class PluginLoadReentryError extends Error { readonly cacheKey: string; diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index dbf1845608c..76c565a9052 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -108,6 +108,7 @@ import { } from "./plugin-control-plane-context.js"; import { withProfile } from "./plugin-load-profile.js"; import { + createPluginModuleLoaderCache, getCachedPluginSourceModuleLoader, type PluginModuleLoaderCache, } from "./plugin-module-loader-cache.js"; @@ -463,7 +464,7 @@ function runPluginRegisterSync( } function createPluginModuleLoader(options: Pick) { - const moduleLoaders: PluginModuleLoaderCache = new Map(); + const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); const loadSourceModule = (modulePath: string) => { return getCachedPluginSourceModuleLoader({ cache: moduleLoaders, diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index cd281eaba2e..14127d996f8 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -28,6 +28,7 @@ import { type PluginManifestCommandAlias, } from "./manifest-command-aliases.js"; import type { PluginConfigUiHint } from "./manifest-types.js"; +import { createPluginCacheKey, PluginLruCache } from "./plugin-cache-primitives.js"; import type { PluginKind } from "./plugin-kind.types.js"; export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json"; @@ -42,7 +43,9 @@ type PluginManifestLoadCacheEntry = { ctimeMs: number; }; -const pluginManifestLoadCache = new Map(); +const pluginManifestLoadCache = new PluginLruCache( + MAX_PLUGIN_MANIFEST_LOAD_CACHE_ENTRIES, +); export function clearPluginManifestLoadCache(): void { pluginManifestLoadCache.clear(); @@ -1227,12 +1230,14 @@ function buildPluginManifestLoadCacheKey(params: { rootRealPath?: string; stats: fs.Stats; }): string { - return JSON.stringify([ - path.resolve(params.manifestPath), - params.rejectHardlinks, - params.rootRealPath ?? "", - params.stats.dev, - params.stats.ino, + return createPluginCacheKey([ + [ + path.resolve(params.manifestPath), + params.rejectHardlinks, + params.rootRealPath ?? "", + params.stats.dev, + params.stats.ino, + ], params.stats.size, params.stats.mtimeMs, params.stats.ctimeMs, @@ -1252,8 +1257,6 @@ function getCachedPluginManifestLoadResult( ) { return undefined; } - pluginManifestLoadCache.delete(key); - pluginManifestLoadCache.set(key, entry); return entry.result; } @@ -1268,13 +1271,6 @@ function setCachedPluginManifestLoadResult( mtimeMs: stats.mtimeMs, ctimeMs: stats.ctimeMs, }); - if (pluginManifestLoadCache.size <= MAX_PLUGIN_MANIFEST_LOAD_CACHE_ENTRIES) { - return; - } - const oldestKey = pluginManifestLoadCache.keys().next().value; - if (typeof oldestKey === "string") { - pluginManifestLoadCache.delete(oldestKey); - } } function parsePluginKind(raw: unknown): PluginKind | PluginKind[] | undefined { diff --git a/src/plugins/plugin-cache-primitives.ts b/src/plugins/plugin-cache-primitives.ts new file mode 100644 index 00000000000..fcb17b91454 --- /dev/null +++ b/src/plugins/plugin-cache-primitives.ts @@ -0,0 +1,118 @@ +import type { OpenClawConfig } from "../config/types.openclaw.js"; + +export type PluginLruCacheResult = { hit: true; value: T } | { hit: false }; + +export class PluginLruCache { + readonly #defaultMaxEntries: number; + #maxEntries: number; + readonly #entries = new Map(); + + constructor(defaultMaxEntries: number) { + this.#defaultMaxEntries = normalizeMaxEntries(defaultMaxEntries, 1); + this.#maxEntries = this.#defaultMaxEntries; + } + + get maxEntries(): number { + return this.#maxEntries; + } + + get size(): number { + return this.#entries.size; + } + + setMaxEntriesForTest(value?: number): void { + this.#maxEntries = + typeof value === "number" + ? normalizeMaxEntries(value, this.#defaultMaxEntries) + : this.#defaultMaxEntries; + this.#evictOldestEntries(); + } + + clear(): void { + this.#entries.clear(); + } + + get(cacheKey: string): T | undefined { + const cached = this.getResult(cacheKey); + return cached.hit ? cached.value : undefined; + } + + getResult(cacheKey: string): PluginLruCacheResult { + if (!this.#entries.has(cacheKey)) { + return { hit: false }; + } + const cached = this.#entries.get(cacheKey) as T; + this.#entries.delete(cacheKey); + this.#entries.set(cacheKey, cached); + return { hit: true, value: cached }; + } + + set(cacheKey: string, value: T): void { + if (this.#entries.has(cacheKey)) { + this.#entries.delete(cacheKey); + } + this.#entries.set(cacheKey, value); + this.#evictOldestEntries(); + } + + #evictOldestEntries(): void { + while (this.#entries.size > this.#maxEntries) { + const oldestEntry = this.#entries.keys().next(); + if (oldestEntry.done) { + break; + } + this.#entries.delete(oldestEntry.value); + } + } +} + +export type ConfigScopedRuntimeCache = WeakMap>; + +export function resolveConfigScopedRuntimeCacheValue(params: { + cache: ConfigScopedRuntimeCache; + config?: OpenClawConfig; + key: string; + load: () => T; +}): T { + if (!params.config) { + return params.load(); + } + let configCache = params.cache.get(params.config); + if (!configCache) { + configCache = new Map(); + params.cache.set(params.config, configCache); + } + if (configCache.has(params.key)) { + return configCache.get(params.key) as T; + } + const loaded = params.load(); + configCache.set(params.key, loaded); + return loaded; +} + +export function createPluginCacheKey(parts: readonly unknown[]): string { + return JSON.stringify(parts); +} + +export type FileSystemIdentity = { + path: string; + size: number; + mtimeMs: number; + ctimeMs?: number; +}; + +export function createFileSystemIdentityCacheKey(identity: FileSystemIdentity): string { + return createPluginCacheKey([ + identity.path, + identity.size, + identity.mtimeMs, + identity.ctimeMs ?? null, + ]); +} + +function normalizeMaxEntries(value: number, fallback: number): number { + if (!Number.isFinite(value) || value <= 0) { + return fallback; + } + return Math.max(1, Math.floor(value)); +} diff --git a/src/plugins/plugin-lru-cache.ts b/src/plugins/plugin-lru-cache.ts index 9037735cb34..b67cde936cb 100644 --- a/src/plugins/plugin-lru-cache.ts +++ b/src/plugins/plugin-lru-cache.ts @@ -1,72 +1 @@ -export type PluginLruCacheResult = { hit: true; value: T } | { hit: false }; - -export class PluginLruCache { - readonly #defaultMaxEntries: number; - #maxEntries: number; - readonly #entries = new Map(); - - constructor(defaultMaxEntries: number) { - this.#defaultMaxEntries = normalizeMaxEntries(defaultMaxEntries, 1); - this.#maxEntries = this.#defaultMaxEntries; - } - - get maxEntries(): number { - return this.#maxEntries; - } - - get size(): number { - return this.#entries.size; - } - - setMaxEntriesForTest(value?: number): void { - this.#maxEntries = - typeof value === "number" - ? normalizeMaxEntries(value, this.#defaultMaxEntries) - : this.#defaultMaxEntries; - this.#evictOldestEntries(); - } - - clear(): void { - this.#entries.clear(); - } - - get(cacheKey: string): T | undefined { - const cached = this.getResult(cacheKey); - return cached.hit ? cached.value : undefined; - } - - getResult(cacheKey: string): PluginLruCacheResult { - if (!this.#entries.has(cacheKey)) { - return { hit: false }; - } - const cached = this.#entries.get(cacheKey) as T; - this.#entries.delete(cacheKey); - this.#entries.set(cacheKey, cached); - return { hit: true, value: cached }; - } - - set(cacheKey: string, value: T): void { - if (this.#entries.has(cacheKey)) { - this.#entries.delete(cacheKey); - } - this.#entries.set(cacheKey, value); - this.#evictOldestEntries(); - } - - #evictOldestEntries(): void { - while (this.#entries.size > this.#maxEntries) { - const oldestEntry = this.#entries.keys().next(); - if (oldestEntry.done) { - break; - } - this.#entries.delete(oldestEntry.value); - } - } -} - -function normalizeMaxEntries(value: number, fallback: number): number { - if (!Number.isFinite(value) || value <= 0) { - return fallback; - } - return Math.max(1, Math.floor(value)); -} +export { PluginLruCache, type PluginLruCacheResult } from "./plugin-cache-primitives.js"; diff --git a/src/plugins/plugin-module-loader-cache.test.ts b/src/plugins/plugin-module-loader-cache.test.ts index 506d55f94b9..e37f4134340 100644 --- a/src/plugins/plugin-module-loader-cache.test.ts +++ b/src/plugins/plugin-module-loader-cache.test.ts @@ -48,6 +48,39 @@ describe("getCachedPluginModuleLoader", () => { expect(cache.size).toBe(1); }); + it("creates bounded loader caches", async () => { + const { createJiti, getCachedPluginModuleLoader } = + await loadCachedPluginModuleLoader("bounded-loader-cache"); + const { createPluginModuleLoaderCache } = await importFreshModule< + typeof import("./plugin-module-loader-cache.js") + >(import.meta.url, "./plugin-module-loader-cache.js?scope=bounded-loader-cache-factory"); + + const cache = createPluginModuleLoaderCache(1); + const first = getCachedPluginModuleLoader({ + cache, + modulePath: "/repo/extensions/demo-a/index.ts", + importerUrl: "file:///repo/src/plugins/loader.ts", + loaderFilename: "/repo/extensions/demo-a/index.ts", + }); + getCachedPluginModuleLoader({ + cache, + modulePath: "/repo/extensions/demo-b/index.ts", + importerUrl: "file:///repo/src/plugins/loader.ts", + loaderFilename: "/repo/extensions/demo-b/index.ts", + }); + const reloadedFirst = getCachedPluginModuleLoader({ + cache, + modulePath: "/repo/extensions/demo-a/index.ts", + importerUrl: "file:///repo/src/plugins/loader.ts", + loaderFilename: "/repo/extensions/demo-a/index.ts", + }); + + expect(cache.size).toBe(1); + expect(reloadedFirst).not.toBe(first); + reloadedFirst("/repo/extensions/demo-a/index.ts"); + expect(createJiti).toHaveBeenCalledOnce(); + }); + it("keeps loader caches scoped by loader filename and dist preference", async () => { const { createJiti, getCachedPluginModuleLoader } = await loadCachedPluginModuleLoader("filename-scope"); diff --git a/src/plugins/plugin-module-loader-cache.ts b/src/plugins/plugin-module-loader-cache.ts index ffeb43fa6d8..c172b63661b 100644 --- a/src/plugins/plugin-module-loader-cache.ts +++ b/src/plugins/plugin-module-loader-cache.ts @@ -1,6 +1,7 @@ import { createJiti } from "jiti"; import { toSafeImportPath } from "../shared/import-specifier.js"; import { tryNativeRequireJavaScriptModule } from "./native-module-require.js"; +import { PluginLruCache } from "./plugin-cache-primitives.js"; import { buildPluginLoaderJitiOptions, createPluginLoaderModuleCacheKey, @@ -10,7 +11,18 @@ import { export type PluginModuleLoader = ReturnType; export type PluginModuleLoaderFactory = typeof createJiti; -export type PluginModuleLoaderCache = Map; +export type PluginModuleLoaderCache = Pick< + PluginLruCache, + "clear" | "get" | "set" | "size" +>; + +const DEFAULT_PLUGIN_MODULE_LOADER_CACHE_ENTRIES = 128; + +export function createPluginModuleLoaderCache( + maxEntries = DEFAULT_PLUGIN_MODULE_LOADER_CACHE_ENTRIES, +): PluginModuleLoaderCache { + return new PluginLruCache(maxEntries); +} export function getCachedPluginModuleLoader(params: { cache: PluginModuleLoaderCache; diff --git a/src/plugins/provider-hook-runtime.ts b/src/plugins/provider-hook-runtime.ts index b6d74991236..5f705052609 100644 --- a/src/plugins/provider-hook-runtime.ts +++ b/src/plugins/provider-hook-runtime.ts @@ -1,6 +1,10 @@ import { normalizeProviderId } from "../agents/provider-id.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + resolveConfigScopedRuntimeCacheValue, + type ConfigScopedRuntimeCache, +} from "./config-scoped-runtime-cache.js"; import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js"; import { resolveProviderConfigApiOwnerHint } from "./provider-config-owner.js"; import { isPluginProvidersLoadInFlight, resolvePluginProviders } from "./providers.runtime.js"; @@ -15,10 +19,7 @@ import type { ProviderWrapStreamFnContext, } from "./types.js"; -const providerRuntimePluginCache = new WeakMap< - OpenClawConfig, - Map ->(); +const providerRuntimePluginCache: ConfigScopedRuntimeCache = new WeakMap(); type ProviderRuntimePluginLookupParams = { provider: string; @@ -60,20 +61,6 @@ function resolveProviderRuntimePluginCacheKey(params: ProviderRuntimePluginLooku }); } -function resolveProviderRuntimePluginCache( - params: ProviderRuntimePluginLookupParams, -): Map | undefined { - if (!params.config || (params.env && params.env !== process.env)) { - return undefined; - } - let cache = providerRuntimePluginCache.get(params.config); - if (!cache) { - cache = new Map(); - providerRuntimePluginCache.set(params.config, cache); - } - return cache; -} - function matchesProviderLiteralId(provider: ProviderPlugin, providerId: string): boolean { const normalized = normalizeLowercaseStringOrEmpty(providerId); return !!normalized && normalizeLowercaseStringOrEmpty(provider.id) === normalized; @@ -119,33 +106,38 @@ export function resolveProviderPluginsForHooks(params: { export function resolveProviderRuntimePlugin( params: ProviderRuntimePluginLookupParams, ): ProviderPlugin | undefined { - const cache = resolveProviderRuntimePluginCache(params); - const cacheKey = cache ? resolveProviderRuntimePluginCacheKey(params) : ""; - if (cache?.has(cacheKey)) { - return cache.get(cacheKey) ?? undefined; - } - const apiOwnerHint = resolveProviderConfigApiOwnerHint({ - provider: params.provider, - config: params.config, - }); - const plugin = resolveProviderPluginsForHooks({ - config: params.config, - workspaceDir: params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(), - env: params.env, - providerRefs: apiOwnerHint ? [params.provider, apiOwnerHint] : [params.provider], - applyAutoEnable: params.applyAutoEnable, - bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat, - bundledProviderVitestCompat: params.bundledProviderVitestCompat, - }).find((plugin) => { - if (apiOwnerHint) { + const cacheConfig = params.env && params.env !== process.env ? undefined : params.config; + const plugin = resolveConfigScopedRuntimeCacheValue({ + cache: providerRuntimePluginCache, + config: cacheConfig, + key: resolveProviderRuntimePluginCacheKey(params), + load: () => { + const apiOwnerHint = resolveProviderConfigApiOwnerHint({ + provider: params.provider, + config: params.config, + }); return ( - matchesProviderLiteralId(plugin, params.provider) || matchesProviderId(plugin, apiOwnerHint) + resolveProviderPluginsForHooks({ + config: params.config, + workspaceDir: params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(), + env: params.env, + providerRefs: apiOwnerHint ? [params.provider, apiOwnerHint] : [params.provider], + applyAutoEnable: params.applyAutoEnable, + bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat, + bundledProviderVitestCompat: params.bundledProviderVitestCompat, + }).find((plugin) => { + if (apiOwnerHint) { + return ( + matchesProviderLiteralId(plugin, params.provider) || + matchesProviderId(plugin, apiOwnerHint) + ); + } + return matchesProviderId(plugin, params.provider); + }) ?? null ); - } - return matchesProviderId(plugin, params.provider); + }, }); - cache?.set(cacheKey, plugin ?? null); - return plugin; + return plugin ?? undefined; } export function resolveProviderHookPlugin(params: { diff --git a/src/plugins/public-surface-loader.ts b/src/plugins/public-surface-loader.ts index 38c9914c9af..9ec21204171 100644 --- a/src/plugins/public-surface-loader.ts +++ b/src/plugins/public-surface-loader.ts @@ -6,6 +6,7 @@ import { openBoundaryFileSync } from "../infra/boundary-file-read.js"; import { sameFileIdentity } from "../infra/file-identity.js"; import { resolveBundledPluginsDir } from "./bundled-dir.js"; import { + createPluginModuleLoaderCache, getCachedPluginModuleLoader, type PluginModuleLoaderCache, } from "./plugin-module-loader-cache.js"; @@ -26,7 +27,7 @@ const publicSurfaceLocationCache = new Map< boundaryRoot: string; } | null >(); -const moduleLoaders: PluginModuleLoaderCache = new Map(); +const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); function isSourceArtifactPath(modulePath: string): boolean { switch (path.extname(modulePath).toLowerCase()) { diff --git a/src/plugins/runtime/runtime-web-channel-plugin.ts b/src/plugins/runtime/runtime-web-channel-plugin.ts index 53be5b54fe5..9b5c4781ac7 100644 --- a/src/plugins/runtime/runtime-web-channel-plugin.ts +++ b/src/plugins/runtime/runtime-web-channel-plugin.ts @@ -8,7 +8,10 @@ import { optimizeImageToJpeg as optimizeImageToJpegImpl, } from "../../media/web-media.js"; import type { PollInput } from "../../polls.js"; -import type { PluginModuleLoaderCache } from "../plugin-module-loader-cache.js"; +import { + createPluginModuleLoaderCache, + type PluginModuleLoaderCache, +} from "../plugin-module-loader-cache.js"; import type { PluginOrigin } from "../plugin-origin.types.js"; import { loadPluginBoundaryModule, @@ -115,7 +118,7 @@ const webChannelRuntimeModuleCache = new Map< CachedWebChannelRuntimeModule >(); -const moduleLoaders: PluginModuleLoaderCache = new Map(); +const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); function resolveWebChannelPluginRecord(): WebChannelPluginRecord { return resolvePluginRuntimeRecordByEntryBaseNames(["light-runtime-api", "runtime-api"], () => { diff --git a/src/plugins/schema-validator.ts b/src/plugins/schema-validator.ts index 3246557dc53..eabaae3fb82 100644 --- a/src/plugins/schema-validator.ts +++ b/src/plugins/schema-validator.ts @@ -3,6 +3,7 @@ import type { ErrorObject, ValidateFunction } from "ajv"; import { appendAllowedValuesHint, summarizeAllowedValues } from "../config/allowed-values.js"; import type { JsonSchemaObject } from "../shared/json-schema.types.js"; import { sanitizeTerminalText } from "../terminal/safe-text.js"; +import { PluginLruCache } from "./plugin-cache-primitives.js"; const require = createRequire(import.meta.url); type AjvLike = { @@ -52,7 +53,7 @@ type CachedValidator = { schema: JsonSchemaObject; }; -const schemaCache = new Map(); +const schemaCache = new PluginLruCache(512); function cloneValidationValue(value: T): T { if (value === undefined || value === null) { diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index ff06ce65439..5ba8390db93 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; -import { PluginLruCache } from "./plugin-lru-cache.js"; +import { PluginLruCache } from "./plugin-cache-primitives.js"; type PluginSdkAliasCandidateKind = "dist" | "src"; export type PluginSdkResolutionPreference = "auto" | "dist" | "src"; diff --git a/src/plugins/setup-registry.ts b/src/plugins/setup-registry.ts index d1caef98be9..7f10d44e550 100644 --- a/src/plugins/setup-registry.ts +++ b/src/plugins/setup-registry.ts @@ -7,6 +7,7 @@ import { buildPluginApi } from "./api-builder.js"; import { collectPluginConfigContractMatches } from "./config-contracts.js"; import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; import { + createPluginModuleLoaderCache, getCachedPluginModuleLoader, type PluginModuleLoaderCache, } from "./plugin-module-loader-cache.js"; @@ -85,7 +86,7 @@ const NOOP_LOGGER: PluginLogger = { error() {}, }; -const moduleLoaders: PluginModuleLoaderCache = new Map(); +const moduleLoaders: PluginModuleLoaderCache = createPluginModuleLoaderCache(); export function clearPluginSetupRegistryCache(): void { moduleLoaders.clear(); diff --git a/src/plugins/source-loader.ts b/src/plugins/source-loader.ts index e0f9863ada1..92e0f410158 100644 --- a/src/plugins/source-loader.ts +++ b/src/plugins/source-loader.ts @@ -1,11 +1,13 @@ import { withProfile } from "./plugin-load-profile.js"; -import type { PluginModuleLoaderCache } from "./plugin-module-loader-cache.js"; -import { getCachedPluginSourceModuleLoader } from "./plugin-module-loader-cache.js"; +import { + createPluginModuleLoaderCache, + getCachedPluginSourceModuleLoader, +} from "./plugin-module-loader-cache.js"; export type PluginSourceLoader = (modulePath: string) => unknown; export function createPluginSourceLoader(): PluginSourceLoader { - const loaders: PluginModuleLoaderCache = new Map(); + const loaders = createPluginModuleLoaderCache(); return (modulePath) => { const sourceLoader = getCachedPluginSourceModuleLoader({ cache: loaders,