refactor: consolidate plugin cache helpers

This commit is contained in:
Peter Steinberger
2026-05-02 04:46:00 +01:00
parent b08220446a
commit f2e03c15c1
6 changed files with 174 additions and 110 deletions

View File

@@ -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<K extends CapabilityProviderRegistryKey> =
PluginRegistry[K][number] extends { provider: infer T } ? T : never;
type CapabilityProviderEntries = PluginRegistry[CapabilityProviderRegistryKey];
const capabilityProviderSnapshotCache = new WeakMap<
OpenClawConfig,
Map<string, CapabilityProviderEntries>
>();
const capabilityProviderSnapshotCache: ConfigScopedRuntimeCache<CapabilityProviderEntries> =
new WeakMap();
const CAPABILITY_CONTRACT_KEY: Record<CapabilityProviderRegistryKey, CapabilityContractKey> = {
memoryEmbeddingProviders: "memoryEmbeddingProviders",
@@ -140,20 +142,6 @@ function createCapabilityProviderFallbackLoadOptions(params: {
};
}
function resolveCapabilityProviderSnapshotCache(
cfg: OpenClawConfig | undefined,
): Map<string, CapabilityProviderEntries> | 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<K extends CapabilityProviderRegi
compatConfig,
pluginIds,
});
const cache = resolveCapabilityProviderSnapshotCache(params.cfg);
const cacheKey = cache
? resolveCapabilityProviderSnapshotCacheKey({ key: params.key, loadOptions })
: "";
let loadedProviders = cache?.get(cacheKey) as PluginRegistry[K] | undefined;
if (!loadedProviders) {
loadedProviders = loadCapabilityProviderEntries({
key: params.key,
pluginIds,
loadOptions,
requested: new Set([params.providerId.toLowerCase()]),
});
cache?.set(cacheKey, loadedProviders as CapabilityProviderEntries);
}
const loadedProviders = resolveConfigScopedRuntimeCacheValue({
cache: capabilityProviderSnapshotCache,
config: params.cfg,
key: resolveCapabilityProviderSnapshotCacheKey({ key: params.key, loadOptions }),
load: () =>
loadCapabilityProviderEntries({
key: params.key,
pluginIds,
loadOptions,
requested: new Set([params.providerId.toLowerCase()]),
}) as CapabilityProviderEntries,
}) as PluginRegistry[K];
return findProviderById(loadedProviders, params.providerId);
}
function resolveCachedCapabilityProviderEntries<K extends CapabilityProviderRegistryKey>(params: {
key: K;
cfg?: OpenClawConfig;
pluginIds: string[];
loadOptions: PluginLoadOptions;
requested?: Set<string>;
}): 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<K extends CapabilityProviderRegistryKey>(params: {
key: K;
cfg?: OpenClawConfig;
@@ -489,20 +499,13 @@ export function resolvePluginCapabilityProviders<K extends CapabilityProviderReg
compatConfig,
pluginIds,
});
const cache = resolveCapabilityProviderSnapshotCache(params.cfg);
const cacheKey = cache
? resolveCapabilityProviderSnapshotCacheKey({ key: params.key, loadOptions })
: "";
let loadedProviders = cache?.get(cacheKey) as PluginRegistry[K] | undefined;
if (!loadedProviders) {
loadedProviders = loadCapabilityProviderEntries({
key: params.key,
pluginIds,
loadOptions,
requested: requestedSpeechProviders,
});
cache?.set(cacheKey, loadedProviders as CapabilityProviderEntries);
}
const loadedProviders = resolveCachedCapabilityProviderEntries({
key: params.key,
cfg: params.cfg,
pluginIds,
loadOptions,
requested: requestedSpeechProviders,
});
if (params.key !== "memoryEmbeddingProviders") {
const mergeLoadedProviders =
activeProviders.length > 0

View File

@@ -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<string[]> = 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<string> = 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);
});
});

View File

@@ -0,0 +1,26 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
export type ConfigScopedRuntimeCache<T> = WeakMap<OpenClawConfig, Map<string, T>>;
export function resolveConfigScopedRuntimeCacheValue<T>(params: {
cache: ConfigScopedRuntimeCache<T>;
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;
}

View File

@@ -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<string, unknown>();
const publicSurfaceModuleCache = new Map<string, unknown>();
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<T extends object>(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<T extends object>(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();
}

View File

@@ -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<T extends CachedWebChannelRuntimeModule["module"]>(
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<WebChannelLightRuntimeModule>(modulePath, moduleLoaders, {
origin: record.origin,
});
cachedLightModulePath = modulePath;
cachedLightModule = loaded;
return loaded;
return getCachedWebChannelRuntimeModule("light", modulePath, () =>
loadPluginBoundaryModule<WebChannelLightRuntimeModule>(modulePath, moduleLoaders, {
origin: record.origin,
}),
);
}
async function loadWebChannelHeavyModule(): Promise<WebChannelHeavyRuntimeModule> {
const record = resolveWebChannelPluginRecord();
const modulePath = resolveWebChannelRuntimeModulePath(record, "runtime-api");
if (cachedHeavyModule && cachedHeavyModulePath === modulePath) {
return cachedHeavyModule;
}
const loaded = loadPluginBoundaryModule<WebChannelHeavyRuntimeModule>(modulePath, moduleLoaders, {
origin: record.origin,
});
cachedHeavyModulePath = modulePath;
cachedHeavyModule = loaded;
return loaded;
return getCachedWebChannelRuntimeModule("heavy", modulePath, () =>
loadPluginBoundaryModule<WebChannelHeavyRuntimeModule>(modulePath, moduleLoaders, {
origin: record.origin,
}),
);
}
function getLightExport<K extends keyof WebChannelLightRuntimeModule>(

View File

@@ -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<string, string[]>();
const cachedPluginSdkScopedAliasMaps = new Map<string, Record<string, string>>();
const MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES = 512;
const cachedPluginSdkExportedSubpaths = new PluginLruCache<string[]>(
MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES,
);
const cachedPluginSdkScopedAliasMaps = new PluginLruCache<Record<string, string>>(
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<string | undefined>(["/", "\\", undefined]);
@@ -488,30 +493,17 @@ const JITI_ALIAS_ROOT_SENTINELS = new Set<string | undefined>(["/", "\\", 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<string, Record<string, string>>();
const normalizedJitiAliasMapCache = new Map<string, Record<string, string>>();
const pluginLoaderModuleConfigCache = new Map<
string,
{
tryNative: boolean;
aliasMap: Record<string, string>;
cacheKey: string;
}
>();
function setBoundedCacheValue<T>(cache: Map<string, T>, 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<Record<string, string>>(
MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES,
);
const normalizedJitiAliasMapCache = new PluginLruCache<Record<string, string>>(
MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES,
);
const pluginLoaderModuleConfigCache = new PluginLruCache<{
tryNative: boolean;
aliasMap: Record<string, string>;
cacheKey: string;
}>(MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES);
function hasJitiNormalizedAliasMarker(aliasMap: Record<string, string>) {
return Boolean((aliasMap as Record<symbol, unknown>)[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;
}