Skip to content

Commit 9989512

Browse files
committed
refactor: simplify plugin cache boundaries
1 parent 9e9df8f commit 9989512

10 files changed

Lines changed: 218 additions & 134 deletions

src/plugins/capability-provider-runtime.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import {
55
withBundledPluginEnablementCompat,
66
withBundledPluginVitestCompat,
77
} from "./bundled-compat.js";
8-
import {
9-
resolveConfigScopedRuntimeCacheValue,
10-
type ConfigScopedRuntimeCache,
11-
} from "./config-scoped-runtime-cache.js";
128
import {
139
resolvePluginRegistryLoadCacheKey,
1410
resolveRuntimePluginRegistry,
1511
type PluginLoadOptions,
1612
} from "./loader.js";
13+
import {
14+
resolveConfigScopedRuntimeCacheValue,
15+
type ConfigScopedRuntimeCache,
16+
} from "./plugin-cache-primitives.js";
1717
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
1818
import type { PluginRegistry } from "./registry-types.js";
1919

src/plugins/config-scoped-runtime-cache.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/plugins/current-plugin-metadata-snapshot.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,21 @@ import {
55
setCurrentPluginMetadataSnapshotState,
66
} from "./current-plugin-metadata-state.js";
77
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
8-
import { resolvePluginMetadataSnapshotConfigFingerprint } from "./plugin-metadata-config-fingerprint.js";
8+
import {
9+
resolvePluginControlPlaneFingerprint,
10+
type ResolvePluginControlPlaneContextParams,
11+
} from "./plugin-control-plane-context.js";
912
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
10-
export { resolvePluginMetadataSnapshotConfigFingerprint } from "./plugin-metadata-config-fingerprint.js";
13+
14+
export function resolvePluginMetadataSnapshotConfigFingerprint(
15+
config?: OpenClawConfig,
16+
options: Omit<ResolvePluginControlPlaneContextParams, "config"> = {},
17+
): string {
18+
return resolvePluginControlPlaneFingerprint({
19+
config,
20+
...options,
21+
});
22+
}
1123

1224
// Single-slot Gateway-owned handoff. Replace or clear it at lifecycle boundaries;
1325
// never accumulate historical metadata snapshots here.

src/plugins/config-scoped-runtime-cache.test.ts renamed to src/plugins/plugin-cache-primitives.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,50 @@
11
import { describe, expect, it, vi } from "vitest";
22
import type { OpenClawConfig } from "../config/types.openclaw.js";
33
import {
4+
PluginLruCache,
45
resolveConfigScopedRuntimeCacheValue,
56
type ConfigScopedRuntimeCache,
6-
} from "./config-scoped-runtime-cache.js";
7+
} from "./plugin-cache-primitives.js";
8+
9+
describe("PluginLruCache", () => {
10+
it("evicts the least recently used entry", () => {
11+
const cache = new PluginLruCache<string>(2);
12+
13+
cache.set("", "empty");
14+
cache.set("a", "alpha");
15+
cache.set("b", "bravo");
16+
expect(cache.get("a")).toBe("alpha");
17+
18+
cache.set("c", "charlie");
19+
20+
expect(cache.get("b")).toBeUndefined();
21+
expect(cache.get("a")).toBe("alpha");
22+
expect(cache.get("c")).toBe("charlie");
23+
});
24+
25+
it("returns hit state for cached null values", () => {
26+
const cache = new PluginLruCache<string | null>(2);
27+
28+
cache.set("missing", null);
29+
30+
expect(cache.getResult("missing")).toEqual({ hit: true, value: null });
31+
expect(cache.getResult("unknown")).toEqual({ hit: false });
32+
});
33+
34+
it("resizes and falls back to the default max entry count", () => {
35+
const cache = new PluginLruCache<string>(2);
36+
37+
cache.setMaxEntriesForTest(1.9);
38+
cache.set("a", "alpha");
39+
cache.set("b", "bravo");
40+
expect(cache.maxEntries).toBe(1);
41+
expect(cache.size).toBe(1);
42+
expect(cache.get("a")).toBeUndefined();
43+
44+
cache.setMaxEntriesForTest();
45+
expect(cache.maxEntries).toBe(2);
46+
});
47+
});
748

849
describe("resolveConfigScopedRuntimeCacheValue", () => {
950
it("caches values by config object and key", () => {

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

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/plugins/plugin-metadata-config-fingerprint.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/plugins/plugin-metadata-snapshot.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
resolveInstalledManifestRegistryIndexFingerprint,
88
} from "./manifest-registry-installed.js";
99
import type { PluginManifestRecord } from "./manifest-registry.js";
10-
import { resolvePluginMetadataSnapshotConfigFingerprint } from "./plugin-metadata-config-fingerprint.js";
10+
import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js";
1111
import type {
1212
LoadPluginMetadataSnapshotParams,
1313
PluginMetadataSnapshot,
@@ -23,6 +23,15 @@ export type {
2323
PluginMetadataSnapshotRegistryDiagnostic,
2424
} from "./plugin-metadata-snapshot.types.js";
2525

26+
function resolvePluginMetadataSnapshotConfigFingerprint(
27+
params: Pick<LoadPluginMetadataSnapshotParams, "config" | "env" | "workspaceDir"> & {
28+
index?: InstalledPluginIndex;
29+
policyHash?: string;
30+
},
31+
): string {
32+
return resolvePluginControlPlaneFingerprint(params);
33+
}
34+
2635
function indexesMatch(
2736
left: InstalledPluginIndex | undefined,
2837
right: InstalledPluginIndex | undefined,
@@ -51,7 +60,8 @@ export function isPluginMetadataSnapshotCompatible(params: {
5160
params.snapshot.policyHash === resolveInstalledPluginIndexPolicyHash(params.config) &&
5261
(!params.snapshot.configFingerprint ||
5362
params.snapshot.configFingerprint ===
54-
resolvePluginMetadataSnapshotConfigFingerprint(params.config, {
63+
resolvePluginMetadataSnapshotConfigFingerprint({
64+
config: params.config,
5565
env,
5666
index: params.index ?? params.snapshot.index,
5767
policyHash: params.snapshot.policyHash,
@@ -185,7 +195,8 @@ function loadPluginMetadataSnapshotImpl(
185195

186196
return {
187197
policyHash: index.policyHash,
188-
configFingerprint: resolvePluginMetadataSnapshotConfigFingerprint(params.config, {
198+
configFingerprint: resolvePluginMetadataSnapshotConfigFingerprint({
199+
config: params.config,
189200
env: params.env,
190201
index,
191202
policyHash: index.policyHash,

src/plugins/plugin-module-loader-cache.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,63 @@ async function loadCachedPluginModuleLoader(scope: string) {
2626
}
2727

2828
describe("getCachedPluginModuleLoader", () => {
29+
it("resolves deterministic cache entries for equivalent alias maps", async () => {
30+
const { resolvePluginModuleLoaderCacheEntry } = await importFreshModule<
31+
typeof import("./plugin-module-loader-cache.js")
32+
>(import.meta.url, "./plugin-module-loader-cache.js?scope=cache-entry-alias-order");
33+
34+
const first = resolvePluginModuleLoaderCacheEntry({
35+
modulePath: "/repo/extensions/demo/index.ts",
36+
importerUrl: "file:///repo/src/plugins/loader.ts",
37+
loaderFilename: "/repo/src/plugins/loader.ts",
38+
aliasMap: {
39+
alpha: "/repo/alpha.js",
40+
zeta: "/repo/zeta.js",
41+
},
42+
tryNative: false,
43+
});
44+
const second = resolvePluginModuleLoaderCacheEntry({
45+
modulePath: "/repo/extensions/demo/index.ts",
46+
importerUrl: "file:///repo/src/plugins/loader.ts",
47+
loaderFilename: "/repo/src/plugins/loader.ts",
48+
aliasMap: {
49+
zeta: "/repo/zeta.js",
50+
alpha: "/repo/alpha.js",
51+
},
52+
tryNative: false,
53+
});
54+
55+
expect(second.cacheKey).toBe(first.cacheKey);
56+
expect(second.scopedCacheKey).toBe(first.scopedCacheKey);
57+
expect(first.loaderFilename).toBe("/repo/src/plugins/loader.ts");
58+
});
59+
60+
it("keeps explicit shared cache scope keys independent of loader options", async () => {
61+
const { resolvePluginModuleLoaderCacheEntry } = await importFreshModule<
62+
typeof import("./plugin-module-loader-cache.js")
63+
>(import.meta.url, "./plugin-module-loader-cache.js?scope=cache-entry-shared-scope");
64+
65+
const first = resolvePluginModuleLoaderCacheEntry({
66+
modulePath: "/repo/dist/extensions/demo-a/api.js",
67+
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
68+
loaderFilename: "/repo/src/plugins/public-surface-loader.ts",
69+
aliasMap: { demo: "/repo/demo-a.js" },
70+
tryNative: true,
71+
sharedCacheScopeKey: "bundled:native",
72+
});
73+
const second = resolvePluginModuleLoaderCacheEntry({
74+
modulePath: "/repo/dist/extensions/demo-b/api.js",
75+
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
76+
loaderFilename: "/repo/src/plugins/public-surface-loader.ts",
77+
aliasMap: { demo: "/repo/demo-b.js" },
78+
tryNative: false,
79+
sharedCacheScopeKey: "bundled:native",
80+
});
81+
82+
expect(first.cacheKey).not.toBe(second.cacheKey);
83+
expect(first.scopedCacheKey).toBe(second.scopedCacheKey);
84+
});
85+
2986
it("reuses cached loaders for the same module config and filename", async () => {
3087
const { createJiti, getCachedPluginModuleLoader } =
3188
await loadCachedPluginModuleLoader("cached-loader");

0 commit comments

Comments
 (0)