Skip to content

Commit 8226238

Browse files
committed
refactor(plugins): share lookup cache eviction
1 parent b68b4b9 commit 8226238

4 files changed

Lines changed: 136 additions & 63 deletions

File tree

src/plugins/loader-cache-state.ts

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { PluginLruCache } from "./plugin-lru-cache.js";
2+
13
export class PluginLoadReentryError extends Error {
24
readonly cacheKey: string;
35

@@ -9,27 +11,20 @@ export class PluginLoadReentryError extends Error {
911
}
1012

1113
export class PluginLoaderCacheState<T> {
12-
readonly #defaultMaxEntries: number;
13-
#maxEntries: number;
14-
readonly #registryCache = new Map<string, T>();
14+
readonly #registryCache: PluginLruCache<T>;
1515
readonly #inFlightLoads = new Set<string>();
1616
readonly #openAllowlistWarningCache = new Set<string>();
1717

1818
constructor(defaultMaxEntries: number) {
19-
this.#defaultMaxEntries = Math.max(1, Math.floor(defaultMaxEntries));
20-
this.#maxEntries = this.#defaultMaxEntries;
19+
this.#registryCache = new PluginLruCache<T>(defaultMaxEntries);
2120
}
2221

2322
get maxEntries(): number {
24-
return this.#maxEntries;
23+
return this.#registryCache.maxEntries;
2524
}
2625

2726
setMaxEntriesForTest(value?: number): void {
28-
this.#maxEntries =
29-
typeof value === "number" && Number.isFinite(value) && value > 0
30-
? Math.max(1, Math.floor(value))
31-
: this.#defaultMaxEntries;
32-
this.#evictOldestEntries();
27+
this.#registryCache.setMaxEntriesForTest(value);
3328
}
3429

3530
clear(): void {
@@ -39,21 +34,11 @@ export class PluginLoaderCacheState<T> {
3934
}
4035

4136
get(cacheKey: string): T | undefined {
42-
const cached = this.#registryCache.get(cacheKey);
43-
if (!cached) {
44-
return undefined;
45-
}
46-
this.#registryCache.delete(cacheKey);
47-
this.#registryCache.set(cacheKey, cached);
48-
return cached;
37+
return this.#registryCache.get(cacheKey);
4938
}
5039

5140
set(cacheKey: string, state: T): void {
52-
if (this.#registryCache.has(cacheKey)) {
53-
this.#registryCache.delete(cacheKey);
54-
}
5541
this.#registryCache.set(cacheKey, state);
56-
this.#evictOldestEntries();
5742
}
5843

5944
isLoadInFlight(cacheKey: string): boolean {
@@ -78,14 +63,4 @@ export class PluginLoaderCacheState<T> {
7863
recordOpenAllowlistWarning(cacheKey: string): void {
7964
this.#openAllowlistWarningCache.add(cacheKey);
8065
}
81-
82-
#evictOldestEntries(): void {
83-
while (this.#registryCache.size > this.#maxEntries) {
84-
const oldestEntry = this.#registryCache.keys().next();
85-
if (oldestEntry.done) {
86-
break;
87-
}
88-
this.#registryCache.delete(oldestEntry.value);
89-
}
90-
}
9166
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, it } from "vitest";
2+
import { PluginLruCache } from "./plugin-lru-cache.js";
3+
4+
describe("PluginLruCache", () => {
5+
it("evicts the least recently used entry", () => {
6+
const cache = new PluginLruCache<string>(2);
7+
8+
cache.set("", "empty");
9+
cache.set("a", "alpha");
10+
cache.set("b", "bravo");
11+
expect(cache.get("a")).toBe("alpha");
12+
13+
cache.set("c", "charlie");
14+
15+
expect(cache.get("b")).toBeUndefined();
16+
expect(cache.get("a")).toBe("alpha");
17+
expect(cache.get("c")).toBe("charlie");
18+
});
19+
20+
it("returns hit state for cached null values", () => {
21+
const cache = new PluginLruCache<string | null>(2);
22+
23+
cache.set("missing", null);
24+
25+
expect(cache.getResult("missing")).toEqual({ hit: true, value: null });
26+
expect(cache.getResult("unknown")).toEqual({ hit: false });
27+
});
28+
29+
it("resizes and falls back to the default max entry count", () => {
30+
const cache = new PluginLruCache<string>(2);
31+
32+
cache.setMaxEntriesForTest(1.9);
33+
cache.set("a", "alpha");
34+
cache.set("b", "bravo");
35+
expect(cache.maxEntries).toBe(1);
36+
expect(cache.size).toBe(1);
37+
expect(cache.get("a")).toBeUndefined();
38+
39+
cache.setMaxEntriesForTest();
40+
expect(cache.maxEntries).toBe(2);
41+
});
42+
});

src/plugins/plugin-lru-cache.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
export type PluginLruCacheResult<T> = { hit: true; value: T } | { hit: false };
2+
3+
export class PluginLruCache<T> {
4+
readonly #defaultMaxEntries: number;
5+
#maxEntries: number;
6+
readonly #entries = new Map<string, T>();
7+
8+
constructor(defaultMaxEntries: number) {
9+
this.#defaultMaxEntries = normalizeMaxEntries(defaultMaxEntries, 1);
10+
this.#maxEntries = this.#defaultMaxEntries;
11+
}
12+
13+
get maxEntries(): number {
14+
return this.#maxEntries;
15+
}
16+
17+
get size(): number {
18+
return this.#entries.size;
19+
}
20+
21+
setMaxEntriesForTest(value?: number): void {
22+
this.#maxEntries =
23+
typeof value === "number"
24+
? normalizeMaxEntries(value, this.#defaultMaxEntries)
25+
: this.#defaultMaxEntries;
26+
this.#evictOldestEntries();
27+
}
28+
29+
clear(): void {
30+
this.#entries.clear();
31+
}
32+
33+
get(cacheKey: string): T | undefined {
34+
const cached = this.getResult(cacheKey);
35+
return cached.hit ? cached.value : undefined;
36+
}
37+
38+
getResult(cacheKey: string): PluginLruCacheResult<T> {
39+
if (!this.#entries.has(cacheKey)) {
40+
return { hit: false };
41+
}
42+
const cached = this.#entries.get(cacheKey) as T;
43+
this.#entries.delete(cacheKey);
44+
this.#entries.set(cacheKey, cached);
45+
return { hit: true, value: cached };
46+
}
47+
48+
set(cacheKey: string, value: T): void {
49+
if (this.#entries.has(cacheKey)) {
50+
this.#entries.delete(cacheKey);
51+
}
52+
this.#entries.set(cacheKey, value);
53+
this.#evictOldestEntries();
54+
}
55+
56+
#evictOldestEntries(): void {
57+
while (this.#entries.size > this.#maxEntries) {
58+
const oldestEntry = this.#entries.keys().next();
59+
if (oldestEntry.done) {
60+
break;
61+
}
62+
this.#entries.delete(oldestEntry.value);
63+
}
64+
}
65+
}
66+
67+
function normalizeMaxEntries(value: number, fallback: number): number {
68+
if (!Number.isFinite(value) || value <= 0) {
69+
return fallback;
70+
}
71+
return Math.max(1, Math.floor(value));
72+
}

src/plugins/setup-registry.ts

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { buildPluginApi } from "./api-builder.js";
77
import { collectPluginConfigContractMatches } from "./config-contracts.js";
88
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
99
import type { PluginManifestRecord } from "./manifest-registry.js";
10+
import { PluginLruCache, type PluginLruCacheResult } from "./plugin-lru-cache.js";
1011
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
1112
import { resolvePluginCacheInputs } from "./roots.js";
1213
import type { PluginRuntime } from "./runtime/types.js";
@@ -86,20 +87,22 @@ const NOOP_LOGGER: PluginLogger = {
8687
const MAX_SETUP_LOOKUP_CACHE_ENTRIES = 128;
8788

8889
const jitiLoaders: PluginJitiLoaderCache = new Map();
89-
const setupRegistryCache = new Map<string, PluginSetupRegistry>();
90-
const setupProviderCache = new Map<string, ProviderPlugin | null>();
91-
const setupCliBackendCache = new Map<string, SetupCliBackendEntry | null>();
92-
let setupLookupCacheEntryCap = MAX_SETUP_LOOKUP_CACHE_ENTRIES;
90+
const setupRegistryCache = new PluginLruCache<PluginSetupRegistry>(MAX_SETUP_LOOKUP_CACHE_ENTRIES);
91+
const setupProviderCache = new PluginLruCache<ProviderPlugin | null>(
92+
MAX_SETUP_LOOKUP_CACHE_ENTRIES,
93+
);
94+
const setupCliBackendCache = new PluginLruCache<SetupCliBackendEntry | null>(
95+
MAX_SETUP_LOOKUP_CACHE_ENTRIES,
96+
);
9397

9498
export const __testing = {
9599
get maxSetupLookupCacheEntries() {
96-
return setupLookupCacheEntryCap;
100+
return setupRegistryCache.maxEntries;
97101
},
98102
setMaxSetupLookupCacheEntriesForTest(value?: number) {
99-
setupLookupCacheEntryCap =
100-
typeof value === "number" && Number.isFinite(value) && value > 0
101-
? Math.max(1, Math.floor(value))
102-
: MAX_SETUP_LOOKUP_CACHE_ENTRIES;
103+
setupRegistryCache.setMaxEntriesForTest(value);
104+
setupProviderCache.setMaxEntriesForTest(value);
105+
setupCliBackendCache.setMaxEntriesForTest(value);
103106
},
104107
getCacheSizes() {
105108
return {
@@ -125,31 +128,12 @@ function getJiti(modulePath: string) {
125128
});
126129
}
127130

128-
function getCachedSetupValue<T>(
129-
cache: Map<string, T>,
130-
key: string,
131-
): { hit: true; value: T } | { hit: false } {
132-
if (!cache.has(key)) {
133-
return { hit: false };
134-
}
135-
const cached = cache.get(key) as T;
136-
cache.delete(key);
137-
cache.set(key, cached);
138-
return { hit: true, value: cached };
131+
function getCachedSetupValue<T>(cache: PluginLruCache<T>, key: string): PluginLruCacheResult<T> {
132+
return cache.getResult(key);
139133
}
140134

141-
function setCachedSetupValue<T>(cache: Map<string, T>, key: string, value: T): void {
142-
if (cache.has(key)) {
143-
cache.delete(key);
144-
}
135+
function setCachedSetupValue<T>(cache: PluginLruCache<T>, key: string, value: T): void {
145136
cache.set(key, value);
146-
while (cache.size > setupLookupCacheEntryCap) {
147-
const oldestKey = cache.keys().next().value;
148-
if (typeof oldestKey !== "string") {
149-
break;
150-
}
151-
cache.delete(oldestKey);
152-
}
153137
}
154138

155139
function buildSetupRegistryCacheKey(params: {

0 commit comments

Comments
 (0)