@dbx-tools/appkit 0.9.117 → 0.9.119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -21
- package/index.ts +4 -1
- package/lib/index.d.ts +4 -1
- package/lib/index.js +4 -2
- package/lib/src/appkit.js +15 -20
- package/lib/src/{_cache-storage.d.ts → cache-storage.d.ts} +28 -16
- package/lib/src/cache-storage.js +276 -0
- package/lib/src/files-cache.d.ts +2 -4
- package/lib/src/files-cache.js +38 -37
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -9
- package/src/appkit.ts +14 -21
- package/src/{_cache-storage.ts → cache-storage.ts} +136 -69
- package/src/files-cache.ts +37 -43
- package/lib/src/_cache-storage.js +0 -235
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@databricks/appkit": "0.81.0",
|
|
23
|
-
"@dbx-tools/projen": "^0.9.
|
|
23
|
+
"@dbx-tools/projen": "^0.9.119",
|
|
24
24
|
"@types/node": "^24.6.0",
|
|
25
25
|
"typescript": "^5.9.3",
|
|
26
26
|
"vitest": "3.2.4"
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@databricks/sdk-experimental": "^0.17.0",
|
|
33
|
-
"@dbx-tools/auth": "^0.9.
|
|
34
|
-
"@dbx-tools/
|
|
35
|
-
"@dbx-tools/
|
|
36
|
-
"@dbx-tools/
|
|
37
|
-
"@dbx-tools/
|
|
38
|
-
"@dbx-tools/shared-
|
|
39
|
-
"
|
|
33
|
+
"@dbx-tools/auth": "^0.9.119",
|
|
34
|
+
"@dbx-tools/cache": "^0.9.119",
|
|
35
|
+
"@dbx-tools/core": "^0.9.119",
|
|
36
|
+
"@dbx-tools/lakebase": "^0.9.119",
|
|
37
|
+
"@dbx-tools/postgres": "^0.9.119",
|
|
38
|
+
"@dbx-tools/shared-core": "^0.9.119",
|
|
39
|
+
"@dbx-tools/shared-fs": "^0.9.119",
|
|
40
40
|
"zod": "4.3.6"
|
|
41
41
|
},
|
|
42
42
|
"main": "./lib/index.js",
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
"types": "./lib/src/bundle.d.ts",
|
|
63
63
|
"default": "./lib/src/bundle.js"
|
|
64
64
|
},
|
|
65
|
+
"./cache-storage": {
|
|
66
|
+
"types": "./lib/src/cache-storage.d.ts",
|
|
67
|
+
"default": "./lib/src/cache-storage.js"
|
|
68
|
+
},
|
|
65
69
|
"./child-process": {
|
|
66
70
|
"types": "./lib/src/child-process.d.ts",
|
|
67
71
|
"default": "./lib/src/child-process.js"
|
|
@@ -125,7 +129,7 @@
|
|
|
125
129
|
"./package.json": "./package.json"
|
|
126
130
|
}
|
|
127
131
|
},
|
|
128
|
-
"version": "0.9.
|
|
132
|
+
"version": "0.9.119",
|
|
129
133
|
"types": "./lib/index.d.ts",
|
|
130
134
|
"dbxToolsConfig": {
|
|
131
135
|
"tags": [
|
|
@@ -150,6 +154,10 @@
|
|
|
150
154
|
"types": "./lib/src/bundle.d.ts",
|
|
151
155
|
"default": "./lib/src/bundle.js"
|
|
152
156
|
},
|
|
157
|
+
"./cache-storage": {
|
|
158
|
+
"types": "./lib/src/cache-storage.d.ts",
|
|
159
|
+
"default": "./lib/src/cache-storage.js"
|
|
160
|
+
},
|
|
153
161
|
"./child-process": {
|
|
154
162
|
"types": "./lib/src/child-process.d.ts",
|
|
155
163
|
"default": "./lib/src/child-process.js"
|
package/src/appkit.ts
CHANGED
|
@@ -39,8 +39,8 @@ import { profile as authProfile } from "@dbx-tools/auth";
|
|
|
39
39
|
import { asyncUtils, environmentUtils, log } from "@dbx-tools/shared-core";
|
|
40
40
|
|
|
41
41
|
import { resolveAutoConfigurePolicy } from "./_auto-configure.ts";
|
|
42
|
-
import { createSoftPersistentStorage } from "./_cache-storage.ts";
|
|
43
42
|
import { loadBrandContext } from "./brand-context.ts";
|
|
43
|
+
import { createAppKitCacheStorage } from "./cache-storage.ts";
|
|
44
44
|
import {
|
|
45
45
|
createInterceptorContext,
|
|
46
46
|
type Interceptor,
|
|
@@ -244,20 +244,12 @@ function applyDefaultProfile(): string | undefined {
|
|
|
244
244
|
return resolved.name;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
/**
|
|
248
|
-
|
|
249
|
-
*
|
|
250
|
-
* Skips when the caller disabled the cache, already supplied a storage
|
|
251
|
-
* backend (including in-memory), or Lakebase is not in play.
|
|
252
|
-
*/
|
|
253
|
-
function shouldInjectSoftPersistentCache(
|
|
254
|
-
config: CreateAppConfig | undefined,
|
|
255
|
-
lakebase: LakebaseConnection | undefined,
|
|
256
|
-
): boolean {
|
|
247
|
+
/** Whether `createApp` should inject the default memory-first cache storage. */
|
|
248
|
+
function shouldInjectCacheStorage(config: CreateAppConfig | undefined): boolean {
|
|
257
249
|
const cache = config?.cache;
|
|
258
250
|
if (cache?.enabled === false) return false;
|
|
259
251
|
if (cache?.storage) return false;
|
|
260
|
-
return
|
|
252
|
+
return true;
|
|
261
253
|
}
|
|
262
254
|
|
|
263
255
|
function redactLakebaseConnection(resolved: LakebaseConnection): Record<string, unknown> {
|
|
@@ -328,15 +320,16 @@ export async function createApp<T extends AppKitPlugins>(
|
|
|
328
320
|
delete appConfig.autoConfigure;
|
|
329
321
|
delete appConfig.interceptor;
|
|
330
322
|
|
|
331
|
-
if (
|
|
332
|
-
logger.debug("createApp: inject
|
|
333
|
-
const storage = await
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
323
|
+
if (shouldInjectCacheStorage(config)) {
|
|
324
|
+
logger.debug("createApp: inject memory-first cache storage");
|
|
325
|
+
const storage = await createAppKitCacheStorage({
|
|
326
|
+
cache: config?.cache,
|
|
327
|
+
preferLakebase: Boolean(lakebase) || usesPlugin(config, LAKEBASE_PLUGIN),
|
|
328
|
+
});
|
|
329
|
+
appConfig.cache = { ...config?.cache, storage };
|
|
330
|
+
logger.debug("createApp: memory-first cache storage ready", {
|
|
331
|
+
persistent: storage.isPersistent(),
|
|
332
|
+
});
|
|
340
333
|
}
|
|
341
334
|
|
|
342
335
|
if (interceptors.length === 0) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Memory-first AppKit cache storage with soft-fail Lakebase persistence.
|
|
3
3
|
*
|
|
4
4
|
* AppKit's `PersistentStorage.initialize()` runs DDL migrations and throws on
|
|
5
5
|
* any step failure. With `cache.strictPersistence: true` that throw makes
|
|
@@ -8,18 +8,25 @@
|
|
|
8
8
|
* usable table keeps serving.
|
|
9
9
|
*
|
|
10
10
|
* `PersistentStorage` is not on AppKit's public export map. The deep load is
|
|
11
|
-
* lazy and best-effort: if the installed AppKit layout changes,
|
|
12
|
-
*
|
|
11
|
+
* lazy and best-effort: if the installed AppKit layout changes, Lakebase is
|
|
12
|
+
* skipped and the adapter keeps its project-scoped disk persistence.
|
|
13
13
|
*
|
|
14
14
|
* @module
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { EventEmitter } from "node:events";
|
|
17
18
|
import { createRequire } from "node:module";
|
|
18
19
|
import { dirname, join } from "node:path";
|
|
19
20
|
import { createLakebasePool, getWorkspaceClient, type CacheConfig } from "@databricks/appkit";
|
|
21
|
+
import {
|
|
22
|
+
closeCache,
|
|
23
|
+
createCache,
|
|
24
|
+
flushCache,
|
|
25
|
+
projectCache,
|
|
26
|
+
type CacheOptions,
|
|
27
|
+
} from "@dbx-tools/cache/cache";
|
|
20
28
|
import { postgresConnectionOptions } from "@dbx-tools/postgres";
|
|
21
29
|
import { errorUtils, hash, log } from "@dbx-tools/shared-core";
|
|
22
|
-
import { LRUCache } from "lru-cache";
|
|
23
30
|
import { handleOwnershipMigrationError } from "./migration.ts";
|
|
24
31
|
|
|
25
32
|
const logger = log.logger("cache-storage");
|
|
@@ -32,9 +39,17 @@ interface CacheEntry<T = unknown> {
|
|
|
32
39
|
value: T;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
/**
|
|
36
|
-
export interface
|
|
37
|
-
|
|
42
|
+
/** Options for AppKit's memory-first cache storage adapter. */
|
|
43
|
+
export interface AppKitCacheStorageOptions {
|
|
44
|
+
/** Native AppKit cache configuration. */
|
|
45
|
+
cache?: CacheConfig;
|
|
46
|
+
/** Whether to attempt AppKit's Lakebase PersistentStorage. Defaults to true. */
|
|
47
|
+
preferLakebase?: boolean;
|
|
48
|
+
/** Portable disk scope used when Lakebase is unavailable. */
|
|
49
|
+
diskScope?: string;
|
|
50
|
+
/** Write-behind coalescing delay in milliseconds. */
|
|
51
|
+
writeDelayMs?: number;
|
|
52
|
+
/** Maximum entries retained in the process-local memory primary. */
|
|
38
53
|
maxEntries?: number;
|
|
39
54
|
}
|
|
40
55
|
|
|
@@ -54,95 +69,126 @@ type PersistentStorageConstructor = new (
|
|
|
54
69
|
|
|
55
70
|
let persistentStorageCtor: PersistentStorageConstructor | undefined | null = null;
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
class AppKitStorageLevel extends EventEmitter {
|
|
73
|
+
readonly opts = {};
|
|
74
|
+
|
|
75
|
+
constructor(private readonly storage: CacheStorage) {
|
|
76
|
+
super();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async get<T>(key: string): Promise<T | undefined> {
|
|
80
|
+
const entry = await this.storage.get<T>(key);
|
|
81
|
+
if (!entry) return undefined;
|
|
82
|
+
if (entry.expiry <= Date.now()) {
|
|
83
|
+
await this.storage.delete(key);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return entry.value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async set(key: string, value: unknown, ttl?: number): Promise<void> {
|
|
90
|
+
await this.storage.set(key, {
|
|
91
|
+
value,
|
|
92
|
+
expiry: Date.now() + (ttl ?? 60_000),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async delete(key: string): Promise<boolean> {
|
|
97
|
+
await this.storage.delete(key);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clear(): Promise<void> {
|
|
102
|
+
return this.storage.clear();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
has(key: string): Promise<boolean> {
|
|
106
|
+
return this.storage.has(key);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
disconnect(): Promise<void> {
|
|
110
|
+
return this.storage.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Memory-first AppKit CacheStorage backed by @dbx-tools/cache. */
|
|
115
|
+
export class AppKitCacheStorage implements CacheStorage {
|
|
116
|
+
private readonly cache: ReturnType<typeof createCache>;
|
|
117
|
+
private readonly keys = new Set<string>();
|
|
118
|
+
private closed = false;
|
|
60
119
|
|
|
61
120
|
constructor(
|
|
62
|
-
|
|
63
|
-
|
|
121
|
+
cache: ReturnType<typeof createCache>,
|
|
122
|
+
private readonly persistent: CacheStorage | undefined,
|
|
64
123
|
) {
|
|
65
|
-
this.
|
|
66
|
-
max: positiveBound(options.maxEntries, 1_000, "maxEntries"),
|
|
67
|
-
maxSize: positiveBound(options.maxBytes, 64 * 1024 * 1024, "maxBytes"),
|
|
68
|
-
sizeCalculation: (entry, key) => retainedSize(key, entry),
|
|
69
|
-
});
|
|
124
|
+
this.cache = cache;
|
|
70
125
|
}
|
|
71
126
|
|
|
72
127
|
async get<T>(key: string): Promise<CacheEntry<T> | null> {
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return persisted;
|
|
128
|
+
const entry = await this.cache.get<CacheEntry<T>>(key);
|
|
129
|
+
if (!entry) return null;
|
|
130
|
+
this.keys.add(key);
|
|
131
|
+
return entry;
|
|
78
132
|
}
|
|
79
133
|
|
|
80
134
|
async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
|
|
81
|
-
|
|
82
|
-
|
|
135
|
+
const ttl = entry.expiry - Date.now();
|
|
136
|
+
if (ttl <= 0) {
|
|
137
|
+
await this.delete(key);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await this.cache.set(key, entry, { ttl });
|
|
141
|
+
this.keys.add(key);
|
|
83
142
|
}
|
|
84
143
|
|
|
85
144
|
async delete(key: string): Promise<void> {
|
|
86
|
-
await this.
|
|
87
|
-
this.
|
|
145
|
+
await this.cache.delete(key);
|
|
146
|
+
this.keys.delete(key);
|
|
88
147
|
}
|
|
89
148
|
|
|
90
149
|
async clear(): Promise<void> {
|
|
91
|
-
await this.
|
|
92
|
-
this.
|
|
150
|
+
await this.cache.clear();
|
|
151
|
+
this.keys.clear();
|
|
93
152
|
}
|
|
94
153
|
|
|
95
154
|
async has(key: string): Promise<boolean> {
|
|
96
|
-
|
|
97
|
-
return this.storage.has(key);
|
|
155
|
+
return this.cache.has(key);
|
|
98
156
|
}
|
|
99
157
|
|
|
100
|
-
size(): Promise<number> {
|
|
101
|
-
return this.
|
|
158
|
+
async size(): Promise<number> {
|
|
159
|
+
return this.persistent ? this.persistent.size() : this.keys.size;
|
|
102
160
|
}
|
|
103
161
|
|
|
104
162
|
isPersistent(): boolean {
|
|
105
|
-
return
|
|
163
|
+
return true;
|
|
106
164
|
}
|
|
107
165
|
|
|
108
|
-
healthCheck(): Promise<boolean> {
|
|
109
|
-
return this.
|
|
166
|
+
async healthCheck(): Promise<boolean> {
|
|
167
|
+
return this.persistent ? this.persistent.healthCheck() : true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Drain tracked write-behind persistence without closing the storage. */
|
|
171
|
+
flush(): Promise<void> {
|
|
172
|
+
return flushCache(this.cache);
|
|
110
173
|
}
|
|
111
174
|
|
|
112
175
|
async close(): Promise<void> {
|
|
113
|
-
this.
|
|
114
|
-
await this.
|
|
176
|
+
if (this.closed) return;
|
|
177
|
+
await closeCache(this.cache);
|
|
178
|
+
this.closed = true;
|
|
179
|
+
this.keys.clear();
|
|
115
180
|
}
|
|
116
181
|
|
|
117
182
|
/** Forward AppKit's runtime-only persistent cleanup hook when available. */
|
|
118
183
|
async cleanupExpired(): Promise<number> {
|
|
119
184
|
const cleanup = (
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
private retain<T>(key: string, entry: CacheEntry<T>): void {
|
|
128
|
-
const ttl = entry.expiry - Date.now();
|
|
129
|
-
if (ttl > 0) this.entries.set(key, entry, { ttl });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function positiveBound(value: number | undefined, fallback: number, name: string): number {
|
|
134
|
-
const resolved = value ?? fallback;
|
|
135
|
-
if (!Number.isFinite(resolved) || resolved <= 0) {
|
|
136
|
-
throw new RangeError(`${name} must be a finite positive number`);
|
|
137
|
-
}
|
|
138
|
-
return Math.floor(resolved);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function retainedSize(key: string, entry: CacheEntry): number {
|
|
142
|
-
try {
|
|
143
|
-
return Buffer.byteLength(key) + Buffer.byteLength(JSON.stringify(entry));
|
|
144
|
-
} catch {
|
|
145
|
-
return Number.MAX_SAFE_INTEGER;
|
|
185
|
+
this.persistent as
|
|
186
|
+
| (CacheStorage & {
|
|
187
|
+
cleanupExpired?: () => Promise<number>;
|
|
188
|
+
})
|
|
189
|
+
| undefined
|
|
190
|
+
)?.cleanupExpired;
|
|
191
|
+
return cleanup && this.persistent ? cleanup.call(this.persistent) : 0;
|
|
146
192
|
}
|
|
147
193
|
}
|
|
148
194
|
|
|
@@ -257,9 +303,9 @@ export async function probeStorage(storage: PersistentStorageBase): Promise<void
|
|
|
257
303
|
/**
|
|
258
304
|
* Build a soft-fail Lakebase cache storage when a pool can be created and
|
|
259
305
|
* AppKit's PersistentStorage can be loaded. Returns `undefined` otherwise so
|
|
260
|
-
*
|
|
306
|
+
* the caller can use portable disk persistence.
|
|
261
307
|
*/
|
|
262
|
-
|
|
308
|
+
async function createPersistentStorage(
|
|
263
309
|
cache: CacheConfig | undefined,
|
|
264
310
|
): Promise<CacheStorage | undefined> {
|
|
265
311
|
const PersistentStorage = loadPersistentStorage();
|
|
@@ -281,10 +327,7 @@ export async function createSoftPersistentStorage(
|
|
|
281
327
|
}
|
|
282
328
|
await storage.initialize();
|
|
283
329
|
await probeStorage(storage);
|
|
284
|
-
return
|
|
285
|
-
maxBytes: cache?.maxBytes,
|
|
286
|
-
maxEntries: cache?.maxSize,
|
|
287
|
-
});
|
|
330
|
+
return storage;
|
|
288
331
|
} catch (err) {
|
|
289
332
|
logger.debug("soft persistent cache unavailable", {
|
|
290
333
|
error: errorUtils.errorMessage(err),
|
|
@@ -295,3 +338,27 @@ export async function createSoftPersistentStorage(
|
|
|
295
338
|
return undefined;
|
|
296
339
|
}
|
|
297
340
|
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Create AppKit CacheStorage with bounded memory first and tracked persistence.
|
|
344
|
+
* Lakebase remains AppKit's persistent owner when available; otherwise the
|
|
345
|
+
* shared project-scoped portable disk level is used.
|
|
346
|
+
*/
|
|
347
|
+
export async function createAppKitCacheStorage(
|
|
348
|
+
options: AppKitCacheStorageOptions = {},
|
|
349
|
+
): Promise<AppKitCacheStorage> {
|
|
350
|
+
const nativeCache = options.cache;
|
|
351
|
+
const persistent =
|
|
352
|
+
options.preferLakebase === false ? undefined : await createPersistentStorage(nativeCache);
|
|
353
|
+
const common: CacheOptions = {
|
|
354
|
+
maxEntries: options.maxEntries ?? nativeCache?.maxSize,
|
|
355
|
+
writeDelayMs: options.writeDelayMs,
|
|
356
|
+
};
|
|
357
|
+
const cache = persistent
|
|
358
|
+
? createCache({
|
|
359
|
+
...common,
|
|
360
|
+
levels: [{ name: "appkit-persistent", store: new AppKitStorageLevel(persistent) }],
|
|
361
|
+
})
|
|
362
|
+
: projectCache(options.diskScope ?? "appkit", common);
|
|
363
|
+
return new AppKitCacheStorage(cache, persistent);
|
|
364
|
+
}
|
package/src/files-cache.ts
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AppKit-global filesystem cache with user-isolated process-local
|
|
2
|
+
* AppKit-global filesystem cache with user-isolated process-local values.
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Plugin, toPlugin, type BasePluginConfig, type PluginManifest } from "@databricks/appkit";
|
|
8
|
+
import { cacheKeys, closeCache, createCache, getOrSet } from "@dbx-tools/cache/cache";
|
|
8
9
|
import { errorUtils, log, object } from "@dbx-tools/shared-core";
|
|
9
10
|
import { posixPath, type CacheValue, type FileSystemCache } from "@dbx-tools/shared-fs";
|
|
10
|
-
import { LRUCache } from "lru-cache";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_TTL_MS = 60_000;
|
|
13
13
|
const DEFAULT_INVALIDATION_BATCH_MS = 25;
|
|
14
14
|
const DEFAULT_MAX_ENTRIES = 1_000;
|
|
15
|
-
const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
|
|
16
15
|
const DEFAULT_MAX_SCOPES = 256;
|
|
17
16
|
const logger = log.logger("files-cache");
|
|
18
17
|
|
|
19
|
-
interface RetainedEntry {
|
|
20
|
-
value: CacheValue;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
18
|
interface ScopeState {
|
|
24
19
|
cache: FileSystemCache;
|
|
25
|
-
entries:
|
|
20
|
+
entries: ReturnType<typeof createCache>;
|
|
26
21
|
group: GroupState;
|
|
27
22
|
scope: ResolvedFilesCacheScope;
|
|
28
23
|
sources: Map<string, Promise<object>>;
|
|
@@ -36,7 +31,6 @@ interface GroupState {
|
|
|
36
31
|
|
|
37
32
|
interface ResolvedFilesCacheConfig {
|
|
38
33
|
invalidationBatchMs: number;
|
|
39
|
-
maxBytes: number;
|
|
40
34
|
maxEntries: number;
|
|
41
35
|
maxScopes: number;
|
|
42
36
|
ttlMs: number;
|
|
@@ -57,8 +51,6 @@ export interface FilesCachePluginConfig extends BasePluginConfig {
|
|
|
57
51
|
invalidationBatchMs?: number;
|
|
58
52
|
/** Maximum entries retained in each user L1. Defaults to 1,000. */
|
|
59
53
|
maxEntries?: number;
|
|
60
|
-
/** Maximum retained bytes in each user L1. Defaults to 64 MiB. */
|
|
61
|
-
maxBytes?: number;
|
|
62
54
|
/** Maximum active user scopes retained by the process. Defaults to 256. */
|
|
63
55
|
maxScopes?: number;
|
|
64
56
|
}
|
|
@@ -101,7 +93,7 @@ export interface FilesCacheExports {
|
|
|
101
93
|
export class FilesCacheManager {
|
|
102
94
|
private readonly config: ResolvedFilesCacheConfig;
|
|
103
95
|
private readonly groups = new Map<string, GroupState>();
|
|
104
|
-
private readonly scopes:
|
|
96
|
+
private readonly scopes: ReturnType<typeof createCache>;
|
|
105
97
|
|
|
106
98
|
constructor(config: FilesCachePluginConfig = {}) {
|
|
107
99
|
this.config = {
|
|
@@ -112,15 +104,9 @@ export class FilesCacheManager {
|
|
|
112
104
|
"invalidationBatchMs",
|
|
113
105
|
),
|
|
114
106
|
maxEntries: positiveInteger(config.maxEntries, DEFAULT_MAX_ENTRIES, "maxEntries"),
|
|
115
|
-
maxBytes: positiveInteger(config.maxBytes, DEFAULT_MAX_BYTES, "maxBytes"),
|
|
116
107
|
maxScopes: positiveInteger(config.maxScopes, DEFAULT_MAX_SCOPES, "maxScopes"),
|
|
117
108
|
};
|
|
118
|
-
this.scopes =
|
|
119
|
-
max: this.config.maxScopes,
|
|
120
|
-
dispose: (state) => {
|
|
121
|
-
state.group.scopes.delete(state);
|
|
122
|
-
},
|
|
123
|
-
});
|
|
109
|
+
this.scopes = createCache({ maxEntries: this.config.maxScopes, useClone: false });
|
|
124
110
|
}
|
|
125
111
|
|
|
126
112
|
/** Return one user-isolated cache within a host/workspace invalidation group. */
|
|
@@ -158,12 +144,23 @@ export class FilesCacheManager {
|
|
|
158
144
|
userKey: scope.userKey,
|
|
159
145
|
workspaceId: scope.workspaceId,
|
|
160
146
|
});
|
|
161
|
-
let state = this.scopes.get(scopeKey);
|
|
147
|
+
let state = await this.scopes.get<ScopeState>(scopeKey);
|
|
162
148
|
if (!state) {
|
|
149
|
+
if ([...cacheKeys(this.scopes)].length >= this.config.maxScopes) {
|
|
150
|
+
const oldestKey = cacheKeys(this.scopes).next().value;
|
|
151
|
+
if (oldestKey) {
|
|
152
|
+
const oldest = await this.scopes.get<ScopeState>(oldestKey);
|
|
153
|
+
if (oldest) {
|
|
154
|
+
oldest.group.scopes.delete(oldest);
|
|
155
|
+
await closeCache(oldest.entries);
|
|
156
|
+
}
|
|
157
|
+
await this.scopes.delete(oldestKey);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
163
160
|
const group = this.group(scope);
|
|
164
161
|
state = this.createScope(scope, group);
|
|
165
162
|
group.scopes.add(state);
|
|
166
|
-
this.scopes.set(scopeKey, state);
|
|
163
|
+
await this.scopes.set(scopeKey, state);
|
|
167
164
|
}
|
|
168
165
|
return state;
|
|
169
166
|
}
|
|
@@ -180,7 +177,11 @@ export class FilesCacheManager {
|
|
|
180
177
|
group.timer = undefined;
|
|
181
178
|
}
|
|
182
179
|
await this.flush();
|
|
183
|
-
this.scopes
|
|
180
|
+
for (const key of cacheKeys(this.scopes)) {
|
|
181
|
+
const state = await this.scopes.get<ScopeState>(key);
|
|
182
|
+
if (state) await closeCache(state.entries);
|
|
183
|
+
}
|
|
184
|
+
await closeCache(this.scopes);
|
|
184
185
|
this.groups.clear();
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -203,11 +204,7 @@ export class FilesCacheManager {
|
|
|
203
204
|
const state = {} as ScopeState;
|
|
204
205
|
state.scope = scope;
|
|
205
206
|
state.group = group;
|
|
206
|
-
state.entries =
|
|
207
|
-
max: this.config.maxEntries,
|
|
208
|
-
maxSize: this.config.maxBytes,
|
|
209
|
-
sizeCalculation: (entry, cacheKey) => retainedSize(cacheKey, entry),
|
|
210
|
-
});
|
|
207
|
+
state.entries = createCache({ maxEntries: this.config.maxEntries, useClone: false });
|
|
211
208
|
state.sources = new Map();
|
|
212
209
|
state.cache = {
|
|
213
210
|
read: <T extends CacheValue>(cacheKey: string, load: () => T | Promise<T>) =>
|
|
@@ -224,16 +221,21 @@ export class FilesCacheManager {
|
|
|
224
221
|
load: () => T | Promise<T>,
|
|
225
222
|
): Promise<T> {
|
|
226
223
|
if (!state.group.pending.has(key)) {
|
|
227
|
-
const retained = state.entries.get(key);
|
|
228
|
-
if (retained) return retained
|
|
224
|
+
const retained = await state.entries.get<T>(key);
|
|
225
|
+
if (retained !== undefined) return retained;
|
|
229
226
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
if (state.group.pending.has(key)) {
|
|
228
|
+
const value = await load();
|
|
229
|
+
await state.entries.set(key, value, { ttl: state.scope.ttlMs });
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
return (await getOrSet(state.entries, key, () => Promise.resolve(load()), {
|
|
233
|
+
ttl: state.scope.ttlMs,
|
|
234
|
+
})) as T;
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
private *keys(state: ScopeState): Iterable<string> {
|
|
236
|
-
for (const key of state.entries
|
|
238
|
+
for (const key of cacheKeys(state.entries)) {
|
|
237
239
|
if (!state.group.pending.has(key)) yield key;
|
|
238
240
|
}
|
|
239
241
|
}
|
|
@@ -259,12 +261,12 @@ export class FilesCacheManager {
|
|
|
259
261
|
const keys = [...group.pending];
|
|
260
262
|
group.pending.clear();
|
|
261
263
|
for (const state of group.scopes) {
|
|
262
|
-
|
|
264
|
+
await Promise.all(keys.map((key) => state.entries.delete(key)));
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
}
|
|
266
268
|
|
|
267
|
-
/** AppKit plugin that owns process-local filesystem
|
|
269
|
+
/** AppKit plugin that owns process-local filesystem caches. */
|
|
268
270
|
export class FilesCachePlugin extends Plugin<FilesCachePluginConfig> {
|
|
269
271
|
static manifest = {
|
|
270
272
|
name: "files-cache",
|
|
@@ -324,11 +326,3 @@ function positiveDuration(value: number | undefined, fallback: number, name: str
|
|
|
324
326
|
function positiveInteger(value: number | undefined, fallback: number, name: string): number {
|
|
325
327
|
return Math.floor(positiveDuration(value, fallback, name));
|
|
326
328
|
}
|
|
327
|
-
|
|
328
|
-
function retainedSize(key: string, entry: RetainedEntry): number {
|
|
329
|
-
try {
|
|
330
|
-
return Buffer.byteLength(key) + Buffer.byteLength(JSON.stringify(entry.value));
|
|
331
|
-
} catch {
|
|
332
|
-
return Number.MAX_SAFE_INTEGER;
|
|
333
|
-
}
|
|
334
|
-
}
|