From f2e03c15c16a136adc6745d87ded4b0adf06d913 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 04:46:00 +0100 Subject: [PATCH] refactor: consolidate plugin cache helpers --- src/plugins/capability-provider-runtime.ts | 95 ++++++++++--------- .../config-scoped-runtime-cache.test.ts | 31 ++++++ src/plugins/config-scoped-runtime-cache.ts | 26 +++++ src/plugins/public-surface-loader.ts | 24 ++--- .../runtime/runtime-web-channel-plugin.ts | 56 ++++++----- src/plugins/sdk-alias.ts | 52 +++++----- 6 files changed, 174 insertions(+), 110 deletions(-) create mode 100644 src/plugins/config-scoped-runtime-cache.test.ts create mode 100644 src/plugins/config-scoped-runtime-cache.ts diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index bb4c3679b62..67e8a67eace 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -5,6 +5,10 @@ import { withBundledPluginEnablementCompat, withBundledPluginVitestCompat, } from "./bundled-compat.js"; +import { + resolveConfigScopedRuntimeCacheValue, + type ConfigScopedRuntimeCache, +} from "./config-scoped-runtime-cache.js"; import { resolvePluginRegistryLoadCacheKey, resolveRuntimePluginRegistry, @@ -37,10 +41,8 @@ type CapabilityProviderForKey = PluginRegistry[K][number] extends { provider: infer T } ? T : never; type CapabilityProviderEntries = PluginRegistry[CapabilityProviderRegistryKey]; -const capabilityProviderSnapshotCache = new WeakMap< - OpenClawConfig, - Map ->(); +const capabilityProviderSnapshotCache: ConfigScopedRuntimeCache = + new WeakMap(); const CAPABILITY_CONTRACT_KEY: Record = { memoryEmbeddingProviders: "memoryEmbeddingProviders", @@ -140,20 +142,6 @@ function createCapabilityProviderFallbackLoadOptions(params: { }; } -function resolveCapabilityProviderSnapshotCache( - cfg: OpenClawConfig | undefined, -): Map | undefined { - if (!cfg) { - return undefined; - } - let cache = capabilityProviderSnapshotCache.get(cfg); - if (!cache) { - cache = new Map(); - capabilityProviderSnapshotCache.set(cfg, cache); - } - return cache; -} - function resolveCapabilityProviderSnapshotCacheKey(params: { key: CapabilityProviderRegistryKey; loadOptions: PluginLoadOptions; @@ -424,23 +412,45 @@ export function resolvePluginCapabilityProvider + loadCapabilityProviderEntries({ + key: params.key, + pluginIds, + loadOptions, + requested: new Set([params.providerId.toLowerCase()]), + }) as CapabilityProviderEntries, + }) as PluginRegistry[K]; return findProviderById(loadedProviders, params.providerId); } +function resolveCachedCapabilityProviderEntries(params: { + key: K; + cfg?: OpenClawConfig; + pluginIds: string[]; + loadOptions: PluginLoadOptions; + requested?: Set; +}): PluginRegistry[K] { + return resolveConfigScopedRuntimeCacheValue({ + cache: capabilityProviderSnapshotCache, + config: params.cfg, + key: resolveCapabilityProviderSnapshotCacheKey({ + key: params.key, + loadOptions: params.loadOptions, + }), + load: () => + loadCapabilityProviderEntries({ + key: params.key, + pluginIds: params.pluginIds, + loadOptions: params.loadOptions, + requested: params.requested, + }) as CapabilityProviderEntries, + }) as PluginRegistry[K]; +} + export function resolvePluginCapabilityProviders(params: { key: K; cfg?: OpenClawConfig; @@ -489,20 +499,13 @@ export function resolvePluginCapabilityProviders 0 diff --git a/src/plugins/config-scoped-runtime-cache.test.ts b/src/plugins/config-scoped-runtime-cache.test.ts new file mode 100644 index 00000000000..aa2aad5f017 --- /dev/null +++ b/src/plugins/config-scoped-runtime-cache.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + resolveConfigScopedRuntimeCacheValue, + type ConfigScopedRuntimeCache, +} from "./config-scoped-runtime-cache.js"; + +describe("resolveConfigScopedRuntimeCacheValue", () => { + it("caches values by config object and key", () => { + const cache: ConfigScopedRuntimeCache = new WeakMap(); + const config = {} as OpenClawConfig; + const load = vi.fn(() => ["loaded"]); + + expect(resolveConfigScopedRuntimeCacheValue({ cache, config, key: "demo", load })).toEqual([ + "loaded", + ]); + expect(resolveConfigScopedRuntimeCacheValue({ cache, config, key: "demo", load })).toEqual([ + "loaded", + ]); + expect(load).toHaveBeenCalledOnce(); + }); + + it("does not cache values without a config owner", () => { + const cache: ConfigScopedRuntimeCache = new WeakMap(); + const load = vi.fn(() => "loaded"); + + expect(resolveConfigScopedRuntimeCacheValue({ cache, key: "demo", load })).toBe("loaded"); + expect(resolveConfigScopedRuntimeCacheValue({ cache, key: "demo", load })).toBe("loaded"); + expect(load).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/plugins/config-scoped-runtime-cache.ts b/src/plugins/config-scoped-runtime-cache.ts new file mode 100644 index 00000000000..cdce7d7e5f3 --- /dev/null +++ b/src/plugins/config-scoped-runtime-cache.ts @@ -0,0 +1,26 @@ +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; +} diff --git a/src/plugins/public-surface-loader.ts b/src/plugins/public-surface-loader.ts index 082e64a8d99..38c9914c9af 100644 --- a/src/plugins/public-surface-loader.ts +++ b/src/plugins/public-surface-loader.ts @@ -17,9 +17,9 @@ const OPENCLAW_PACKAGE_ROOT = modulePath: fileURLToPath(import.meta.url), moduleUrl: import.meta.url, }) ?? fileURLToPath(new URL("../..", import.meta.url)); -const loadedPublicSurfaceModules = new Map(); +const publicSurfaceModuleCache = new Map(); const sourceArtifactRequire = createRequire(import.meta.url); -const publicSurfaceLocations = new Map< +const publicSurfaceLocationCache = new Map< string, { modulePath: string; @@ -83,11 +83,11 @@ function resolvePublicSurfaceLocation(params: { artifactBasename: string; }): { modulePath: string; boundaryRoot: string } | null { const key = createResolutionKey(params); - if (publicSurfaceLocations.has(key)) { - return publicSurfaceLocations.get(key) ?? null; + if (publicSurfaceLocationCache.has(key)) { + return publicSurfaceLocationCache.get(key) ?? null; } const resolved = resolvePublicSurfaceLocationUncached(params); - publicSurfaceLocations.set(key, resolved); + publicSurfaceLocationCache.set(key, resolved); return resolved; } @@ -120,7 +120,7 @@ export function loadBundledPluginPublicArtifactModuleSync(para `Unable to resolve bundled plugin public surface ${params.dirName}/${params.artifactBasename}`, ); } - const cached = loadedPublicSurfaceModules.get(location.modulePath); + const cached = publicSurfaceModuleCache.get(location.modulePath); if (cached) { return cached as T; } @@ -150,15 +150,15 @@ export function loadBundledPluginPublicArtifactModuleSync(para } const sentinel = {} as T; - loadedPublicSurfaceModules.set(location.modulePath, sentinel); - loadedPublicSurfaceModules.set(validatedPath, sentinel); + publicSurfaceModuleCache.set(location.modulePath, sentinel); + publicSurfaceModuleCache.set(validatedPath, sentinel); try { const loaded = loadPublicSurfaceModule(validatedPath) as T; Object.assign(sentinel, loaded); return sentinel; } catch (error) { - loadedPublicSurfaceModules.delete(location.modulePath); - loadedPublicSurfaceModules.delete(validatedPath); + publicSurfaceModuleCache.delete(location.modulePath); + publicSurfaceModuleCache.delete(validatedPath); throw error; } } @@ -171,7 +171,7 @@ export function resolveBundledPluginPublicArtifactPath(params: { } export function resetBundledPluginPublicArtifactLoaderForTest(): void { - loadedPublicSurfaceModules.clear(); - publicSurfaceLocations.clear(); + publicSurfaceModuleCache.clear(); + publicSurfaceLocationCache.clear(); moduleLoaders.clear(); } diff --git a/src/plugins/runtime/runtime-web-channel-plugin.ts b/src/plugins/runtime/runtime-web-channel-plugin.ts index 2c9351b4e8b..53be5b54fe5 100644 --- a/src/plugins/runtime/runtime-web-channel-plugin.ts +++ b/src/plugins/runtime/runtime-web-channel-plugin.ts @@ -104,10 +104,16 @@ type WebChannelHeavyRuntimeModule = { resolveHeartbeatRecipients: (...args: unknown[]) => unknown; }; -let cachedHeavyModulePath: string | null = null; -let cachedHeavyModule: WebChannelHeavyRuntimeModule | null = null; -let cachedLightModulePath: string | null = null; -let cachedLightModule: WebChannelLightRuntimeModule | null = null; +type WebChannelRuntimeModuleKind = "heavy" | "light"; +type CachedWebChannelRuntimeModule = { + modulePath: string; + module: WebChannelHeavyRuntimeModule | WebChannelLightRuntimeModule; +}; + +const webChannelRuntimeModuleCache = new Map< + WebChannelRuntimeModuleKind, + CachedWebChannelRuntimeModule +>(); const moduleLoaders: PluginModuleLoaderCache = new Map(); @@ -140,32 +146,38 @@ function loadCurrentHeavyModuleSync(): WebChannelHeavyRuntimeModule { }); } +function getCachedWebChannelRuntimeModule( + kind: WebChannelRuntimeModuleKind, + modulePath: string, + load: () => T, +): T { + const cached = webChannelRuntimeModuleCache.get(kind); + if (cached?.modulePath === modulePath) { + return cached.module as T; + } + const loaded = load(); + webChannelRuntimeModuleCache.set(kind, { modulePath, module: loaded }); + return loaded; +} + function loadWebChannelLightModule(): WebChannelLightRuntimeModule { const record = resolveWebChannelPluginRecord(); const modulePath = resolveWebChannelRuntimeModulePath(record, "light-runtime-api"); - if (cachedLightModule && cachedLightModulePath === modulePath) { - return cachedLightModule; - } - const loaded = loadPluginBoundaryModule(modulePath, moduleLoaders, { - origin: record.origin, - }); - cachedLightModulePath = modulePath; - cachedLightModule = loaded; - return loaded; + return getCachedWebChannelRuntimeModule("light", modulePath, () => + loadPluginBoundaryModule(modulePath, moduleLoaders, { + origin: record.origin, + }), + ); } async function loadWebChannelHeavyModule(): Promise { const record = resolveWebChannelPluginRecord(); const modulePath = resolveWebChannelRuntimeModulePath(record, "runtime-api"); - if (cachedHeavyModule && cachedHeavyModulePath === modulePath) { - return cachedHeavyModule; - } - const loaded = loadPluginBoundaryModule(modulePath, moduleLoaders, { - origin: record.origin, - }); - cachedHeavyModulePath = modulePath; - cachedHeavyModule = loaded; - return loaded; + return getCachedWebChannelRuntimeModule("heavy", modulePath, () => + loadPluginBoundaryModule(modulePath, moduleLoaders, { + origin: record.origin, + }), + ); } function getLightExport( diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 067e748801a..ff06ce65439 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -3,6 +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"; type PluginSdkAliasCandidateKind = "dist" | "src"; export type PluginSdkResolutionPreference = "auto" | "dist" | "src"; @@ -249,8 +250,13 @@ export function resolvePluginSdkAliasFile(params: { return null; } -const cachedPluginSdkExportedSubpaths = new Map(); -const cachedPluginSdkScopedAliasMaps = new Map>(); +const MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES = 512; +const cachedPluginSdkExportedSubpaths = new PluginLruCache( + MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES, +); +const cachedPluginSdkScopedAliasMaps = new PluginLruCache>( + MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES, +); const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const; const PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS = [ ".ts", @@ -480,7 +486,6 @@ export function resolveExtensionApiAlias(params: LoaderModuleResolveParams = {}) return null; } -const MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES = 512; const JITI_NORMALIZED_ALIAS_SYMBOL = Symbol.for("pathe:normalizedAlias"); const JITI_ALIAS_ROOT_SENTINELS = new Set(["/", "\\", undefined]); @@ -488,30 +493,17 @@ const JITI_ALIAS_ROOT_SENTINELS = new Set(["/", "\\", undefi // loader setup avoids rebuilding the same filesystem-derived map and cache key. // Include cwd/env inputs because the fallback root and private QA alias // surfaces depend on them. -const aliasMapCache = new Map>(); -const normalizedJitiAliasMapCache = new Map>(); -const pluginLoaderModuleConfigCache = new Map< - string, - { - tryNative: boolean; - aliasMap: Record; - cacheKey: string; - } ->(); - -function setBoundedCacheValue(cache: Map, key: string, value: T) { - if (cache.has(key)) { - cache.delete(key); - } - cache.set(key, value); - while (cache.size > MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES) { - const oldestKey = cache.keys().next().value; - if (typeof oldestKey !== "string") { - break; - } - cache.delete(oldestKey); - } -} +const aliasMapCache = new PluginLruCache>( + MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES, +); +const normalizedJitiAliasMapCache = new PluginLruCache>( + MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES, +); +const pluginLoaderModuleConfigCache = new PluginLruCache<{ + tryNative: boolean; + aliasMap: Record; + cacheKey: string; +}>(MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES); function hasJitiNormalizedAliasMarker(aliasMap: Record) { return Boolean((aliasMap as Record)[JITI_NORMALIZED_ALIAS_SYMBOL]); @@ -557,7 +549,7 @@ function normalizePluginLoaderAliasMapForJiti( value: true, enumerable: false, }); - setBoundedCacheValue(normalizedJitiAliasMapCache, cacheKey, normalizedAliasMap); + normalizedJitiAliasMapCache.set(cacheKey, normalizedAliasMap); return normalizedAliasMap; } @@ -640,7 +632,7 @@ export function buildPluginLoaderAliasMap( ).map(([key, value]) => [key, normalizeJitiAliasTargetPath(value)]), ), }; - setBoundedCacheValue(aliasMapCache, cacheKey, result); + aliasMapCache.set(cacheKey, result); return result; } @@ -781,7 +773,7 @@ export function resolvePluginLoaderModuleConfig(params: { aliasMap, }), }; - setBoundedCacheValue(pluginLoaderModuleConfigCache, configCacheKey, result); + pluginLoaderModuleConfigCache.set(configCacheKey, result); return result; }