mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
refactor: consolidate plugin cache helpers
This commit is contained in:
@@ -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
|
||||
|
||||
31
src/plugins/config-scoped-runtime-cache.test.ts
Normal file
31
src/plugins/config-scoped-runtime-cache.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
26
src/plugins/config-scoped-runtime-cache.ts
Normal file
26
src/plugins/config-scoped-runtime-cache.ts
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user