@holo-js/cache 0.1.3 → 0.1.5
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/dist/index.d.ts +2 -2
- package/dist/index.mjs +273 -50
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -123,8 +123,8 @@ type RedisDriverModuleLoader = () => Promise<RedisCacheDriverModule>;
|
|
|
123
123
|
declare function isNormalizedRedisConfig(config: HoloRedisConfig | NormalizedHoloRedisConfig): config is NormalizedHoloRedisConfig;
|
|
124
124
|
declare function normalizeRuntimeRedisConfig(config: HoloRedisConfig | NormalizedHoloRedisConfig | undefined): NormalizedHoloRedisConfig | undefined;
|
|
125
125
|
declare function resolveSharedRedisConnection(redisConfig: NormalizedHoloRedisConfig | undefined, connectionName: string): NormalizedHoloRedisConnectionConfig;
|
|
126
|
-
declare function isModuleNotFoundError(error: unknown): boolean;
|
|
127
|
-
declare function normalizeRedisModuleLoadError(error: unknown): CacheOptionalPackageError | unknown;
|
|
126
|
+
declare function isModuleNotFoundError(error: unknown, expectedSpecifier?: string): boolean;
|
|
127
|
+
declare function normalizeRedisModuleLoadError(error: unknown, expectedSpecifier?: string): CacheOptionalPackageError | unknown;
|
|
128
128
|
declare function loadRedisDriverModule(): Promise<RedisCacheDriverModule>;
|
|
129
129
|
declare function setRedisDriverModuleLoader(loader: RedisDriverModuleLoader): void;
|
|
130
130
|
declare function resetRedisDriverModuleLoader(): void;
|
package/dist/index.mjs
CHANGED
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
} from "@holo-js/config";
|
|
29
29
|
|
|
30
30
|
// src/db.ts
|
|
31
|
+
import { createRequire } from "module";
|
|
32
|
+
import { join } from "path";
|
|
33
|
+
import { pathToFileURL } from "url";
|
|
31
34
|
import {
|
|
32
35
|
normalizeDatabaseConfig
|
|
33
36
|
} from "@holo-js/config";
|
|
@@ -59,8 +62,9 @@ function isModuleNotFoundError(error, expectedSpecifier = "@holo-js/cache-db") {
|
|
|
59
62
|
return false;
|
|
60
63
|
}
|
|
61
64
|
const message = "message" in error && typeof error.message === "string" ? error.message : "";
|
|
65
|
+
const code = "code" in error ? error.code : void 0;
|
|
62
66
|
const escapedSpecifier = escapeRegExp(expectedSpecifier);
|
|
63
|
-
if (
|
|
67
|
+
if ((code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") && [
|
|
64
68
|
new RegExp(`Cannot find package ['"]${escapedSpecifier}['"]`),
|
|
65
69
|
new RegExp(`Cannot find module ['"]${escapedSpecifier}['"]`),
|
|
66
70
|
new RegExp(`Could not resolve ['"]${escapedSpecifier}['"]`),
|
|
@@ -82,12 +86,26 @@ function normalizeDatabaseModuleLoadError(error, expectedSpecifier = "@holo-js/c
|
|
|
82
86
|
}
|
|
83
87
|
return error;
|
|
84
88
|
}
|
|
89
|
+
async function importDatabaseDriverModuleFromProject(specifier) {
|
|
90
|
+
const projectRequire = createRequire(join(process.cwd(), "package.json"));
|
|
91
|
+
return await import(pathToFileURL(projectRequire.resolve(specifier)).href);
|
|
92
|
+
}
|
|
85
93
|
async function loadDatabaseDriverModule() {
|
|
94
|
+
const specifier = "@holo-js/cache-db";
|
|
86
95
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
return await import(
|
|
97
|
+
/* webpackIgnore: true */
|
|
98
|
+
specifier
|
|
99
|
+
);
|
|
89
100
|
} catch (error) {
|
|
90
|
-
|
|
101
|
+
if (!isModuleNotFoundError(error, specifier)) {
|
|
102
|
+
throw normalizeDatabaseModuleLoadError(error, specifier);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return await importDatabaseDriverModuleFromProject(specifier);
|
|
106
|
+
} catch (fallbackError) {
|
|
107
|
+
throw normalizeDatabaseModuleLoadError(fallbackError, specifier);
|
|
108
|
+
}
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
111
|
var databaseDriverModuleLoader = loadDatabaseDriverModule;
|
|
@@ -181,14 +199,24 @@ var cacheDbInternals = {
|
|
|
181
199
|
|
|
182
200
|
// src/file.ts
|
|
183
201
|
import { createHash, randomUUID } from "crypto";
|
|
184
|
-
import { mkdir, open, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
185
|
-
import { dirname, join, resolve } from "path";
|
|
202
|
+
import { mkdir, open, readFile, readdir, rename, rm, rmdir, writeFile } from "fs/promises";
|
|
203
|
+
import { dirname, join as join2, resolve } from "path";
|
|
186
204
|
var MALFORMED_FILE = /* @__PURE__ */ Symbol("MALFORMED_FILE");
|
|
187
205
|
function defaultSleep(milliseconds) {
|
|
188
206
|
return new Promise((resolveDelay) => {
|
|
189
207
|
setTimeout(resolveDelay, milliseconds);
|
|
190
208
|
});
|
|
191
209
|
}
|
|
210
|
+
function validateLockSeconds(seconds) {
|
|
211
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
212
|
+
throw new CacheInvalidTtlError("[@holo-js/cache] Cache lock seconds must be a finite number greater than 0.");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function validateLockWaitSeconds(waitSeconds) {
|
|
216
|
+
if (!Number.isFinite(waitSeconds) || waitSeconds < 0) {
|
|
217
|
+
throw new CacheInvalidTtlError("[@holo-js/cache] Cache lock wait seconds must be a finite number greater than or equal to 0.");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
192
220
|
function hashCacheKey(key) {
|
|
193
221
|
return createHash("sha256").update(key).digest("hex");
|
|
194
222
|
}
|
|
@@ -197,11 +225,14 @@ function resolveDriverRoot(path) {
|
|
|
197
225
|
}
|
|
198
226
|
function resolveEntryFilePath(rootPath, key) {
|
|
199
227
|
const hash = hashCacheKey(key);
|
|
200
|
-
return
|
|
228
|
+
return join2(rootPath, "entries", hash.slice(0, 2), `${hash}.json`);
|
|
201
229
|
}
|
|
202
230
|
function resolveLockFilePath(rootPath, name) {
|
|
203
231
|
const hash = hashCacheKey(name);
|
|
204
|
-
return
|
|
232
|
+
return join2(rootPath, "locks", hash.slice(0, 2), `${hash}.lock`);
|
|
233
|
+
}
|
|
234
|
+
function resolveLockMarkerFilePath(lockPath, owner) {
|
|
235
|
+
return join2(lockPath, `${hashCacheKey(owner)}.json`);
|
|
205
236
|
}
|
|
206
237
|
function isPositiveTimestamp(value) {
|
|
207
238
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
@@ -226,6 +257,16 @@ async function ensureParentDirectory(filePath) {
|
|
|
226
257
|
async function removeFileIfPresent(filePath) {
|
|
227
258
|
await rm(filePath, { force: true });
|
|
228
259
|
}
|
|
260
|
+
async function removeEmptyDirectoryIfPresent(path) {
|
|
261
|
+
try {
|
|
262
|
+
await rmdir(path);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTEMPTY" || error.code === "ENOTDIR")) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
229
270
|
async function readFileIfPresent(filePath) {
|
|
230
271
|
try {
|
|
231
272
|
return await readFile(filePath, "utf8");
|
|
@@ -277,7 +318,7 @@ async function listFiles(rootPath) {
|
|
|
277
318
|
try {
|
|
278
319
|
const entries = await readdir(rootPath, { withFileTypes: true });
|
|
279
320
|
const nested = await Promise.all(entries.map(async (entry) => {
|
|
280
|
-
const entryPath =
|
|
321
|
+
const entryPath = join2(rootPath, entry.name);
|
|
281
322
|
if (entry.isDirectory()) return listFiles(entryPath);
|
|
282
323
|
return [entryPath];
|
|
283
324
|
}));
|
|
@@ -299,6 +340,10 @@ async function removeInvalidFileAndReadMissing(filePath, decoded) {
|
|
|
299
340
|
};
|
|
300
341
|
}
|
|
301
342
|
async function removeScopedCacheFiles(rootPath, prefix, isEnvelope, resolveName) {
|
|
343
|
+
async function removeScopedFile(filePath) {
|
|
344
|
+
await removeFileIfPresent(filePath);
|
|
345
|
+
await removeEmptyDirectoryIfPresent(dirname(filePath));
|
|
346
|
+
}
|
|
302
347
|
if (!prefix) {
|
|
303
348
|
await rm(rootPath, { recursive: true, force: true });
|
|
304
349
|
await mkdir(rootPath, { recursive: true });
|
|
@@ -307,11 +352,11 @@ async function removeScopedCacheFiles(rootPath, prefix, isEnvelope, resolveName)
|
|
|
307
352
|
for (const filePath of await listFiles(rootPath)) {
|
|
308
353
|
const decoded = await readJsonFile(filePath);
|
|
309
354
|
if (!decoded || decoded === MALFORMED_FILE || !isEnvelope(decoded)) {
|
|
310
|
-
await
|
|
355
|
+
await removeScopedFile(filePath);
|
|
311
356
|
continue;
|
|
312
357
|
}
|
|
313
358
|
if (resolveName(decoded).startsWith(prefix)) {
|
|
314
|
-
await
|
|
359
|
+
await removeScopedFile(filePath);
|
|
315
360
|
}
|
|
316
361
|
}
|
|
317
362
|
}
|
|
@@ -332,17 +377,77 @@ async function readEntry(rootPath, key, now) {
|
|
|
332
377
|
}
|
|
333
378
|
async function readLock(rootPath, name, now) {
|
|
334
379
|
const filePath = resolveLockFilePath(rootPath, name);
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
380
|
+
let markerFilePaths;
|
|
381
|
+
try {
|
|
382
|
+
const entries = await readdir(filePath, { withFileTypes: true });
|
|
383
|
+
markerFilePaths = entries.filter((entry) => entry.isFile()).map((entry) => join2(filePath, entry.name));
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
386
|
+
return {
|
|
387
|
+
state: "missing",
|
|
388
|
+
filePath
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOTDIR")) {
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
const decoded = await readJsonFile(filePath);
|
|
395
|
+
if (decoded === MALFORMED_FILE || !isFileCacheLockEnvelope(decoded) || decoded.name !== name) {
|
|
396
|
+
if (typeof decoded !== "undefined") {
|
|
397
|
+
await removeFileIfPresent(filePath);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
state: "missing",
|
|
401
|
+
filePath
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (decoded.expiresAt <= now) {
|
|
405
|
+
await removeFileIfPresent(filePath);
|
|
406
|
+
return {
|
|
407
|
+
state: "missing",
|
|
408
|
+
filePath
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
state: "hit",
|
|
413
|
+
filePath,
|
|
414
|
+
markerFilePath: filePath,
|
|
415
|
+
value: decoded
|
|
416
|
+
};
|
|
338
417
|
}
|
|
339
|
-
if (
|
|
340
|
-
return
|
|
418
|
+
if (markerFilePaths.length === 0) {
|
|
419
|
+
return {
|
|
420
|
+
state: "hit",
|
|
421
|
+
filePath,
|
|
422
|
+
markerFilePath: resolveLockMarkerFilePath(filePath, ""),
|
|
423
|
+
value: {
|
|
424
|
+
name,
|
|
425
|
+
owner: "",
|
|
426
|
+
expiresAt: now + 1
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
for (const markerFilePath of markerFilePaths) {
|
|
431
|
+
const decoded = await readJsonFile(markerFilePath);
|
|
432
|
+
if (decoded === MALFORMED_FILE || !isFileCacheLockEnvelope(decoded) || decoded.name !== name) {
|
|
433
|
+
await removeFileIfPresent(markerFilePath);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (decoded.expiresAt <= now) {
|
|
437
|
+
await removeFileIfPresent(markerFilePath);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
state: "hit",
|
|
442
|
+
filePath,
|
|
443
|
+
markerFilePath,
|
|
444
|
+
value: decoded
|
|
445
|
+
};
|
|
341
446
|
}
|
|
447
|
+
await removeEmptyDirectoryIfPresent(filePath);
|
|
342
448
|
return {
|
|
343
|
-
state: "
|
|
344
|
-
filePath
|
|
345
|
-
value: decoded
|
|
449
|
+
state: "missing",
|
|
450
|
+
filePath
|
|
346
451
|
};
|
|
347
452
|
}
|
|
348
453
|
function createEntryEnvelope(input) {
|
|
@@ -353,7 +458,27 @@ function createEntryEnvelope(input) {
|
|
|
353
458
|
};
|
|
354
459
|
}
|
|
355
460
|
function createFileLock(rootPath, name, seconds, now, sleep, ownerFactory) {
|
|
461
|
+
validateLockSeconds(seconds);
|
|
356
462
|
const owner = ownerFactory();
|
|
463
|
+
async function writeLockDirectory(filePath, envelope) {
|
|
464
|
+
await ensureParentDirectory(filePath);
|
|
465
|
+
try {
|
|
466
|
+
await mkdir(filePath);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
await writeFile(resolveLockMarkerFilePath(filePath, owner), envelope, "utf8");
|
|
475
|
+
} catch (error) {
|
|
476
|
+
await removeFileIfPresent(resolveLockMarkerFilePath(filePath, owner));
|
|
477
|
+
await removeEmptyDirectoryIfPresent(filePath);
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
357
482
|
async function tryAcquire() {
|
|
358
483
|
const filePath = resolveLockFilePath(rootPath, name);
|
|
359
484
|
const envelope = JSON.stringify({
|
|
@@ -361,12 +486,12 @@ function createFileLock(rootPath, name, seconds, now, sleep, ownerFactory) {
|
|
|
361
486
|
owner,
|
|
362
487
|
expiresAt: now() + seconds * 1e3
|
|
363
488
|
});
|
|
364
|
-
if (await
|
|
489
|
+
if (await writeLockDirectory(filePath, envelope)) {
|
|
365
490
|
return true;
|
|
366
491
|
}
|
|
367
492
|
const currentLock = await readLock(rootPath, name, now());
|
|
368
493
|
if (currentLock.state === "missing") {
|
|
369
|
-
return
|
|
494
|
+
return writeLockDirectory(filePath, envelope);
|
|
370
495
|
}
|
|
371
496
|
return false;
|
|
372
497
|
}
|
|
@@ -393,14 +518,12 @@ function createFileLock(rootPath, name, seconds, now, sleep, ownerFactory) {
|
|
|
393
518
|
if (currentLock.state === "missing" || currentLock.value.owner !== owner) {
|
|
394
519
|
return false;
|
|
395
520
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return false;
|
|
399
|
-
}
|
|
400
|
-
await removeFileIfPresent(currentLock.filePath);
|
|
521
|
+
await removeFileIfPresent(currentLock.markerFilePath);
|
|
522
|
+
await removeEmptyDirectoryIfPresent(currentLock.filePath);
|
|
401
523
|
return true;
|
|
402
524
|
},
|
|
403
525
|
async block(waitSeconds, callback) {
|
|
526
|
+
validateLockWaitSeconds(waitSeconds);
|
|
404
527
|
const deadline = now() + waitSeconds * 1e3;
|
|
405
528
|
while (true) {
|
|
406
529
|
if (await tryAcquire()) {
|
|
@@ -502,13 +625,13 @@ function createFileCacheDriver(options) {
|
|
|
502
625
|
},
|
|
503
626
|
async flush() {
|
|
504
627
|
await removeScopedCacheFiles(
|
|
505
|
-
|
|
628
|
+
join2(rootPath, "entries"),
|
|
506
629
|
prefix,
|
|
507
630
|
isFileCacheEntryEnvelope,
|
|
508
631
|
(entry) => entry.key
|
|
509
632
|
);
|
|
510
633
|
await removeScopedCacheFiles(
|
|
511
|
-
|
|
634
|
+
join2(rootPath, "locks"),
|
|
512
635
|
prefix,
|
|
513
636
|
isFileCacheLockEnvelope,
|
|
514
637
|
(lock) => lock.name
|
|
@@ -546,7 +669,18 @@ function defaultSleep2(milliseconds) {
|
|
|
546
669
|
setTimeout(resolve2, milliseconds);
|
|
547
670
|
});
|
|
548
671
|
}
|
|
672
|
+
function validateLockSeconds2(seconds) {
|
|
673
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
674
|
+
throw new CacheInvalidTtlError("[@holo-js/cache] Cache lock seconds must be a finite number greater than 0.");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function validateLockWaitSeconds2(waitSeconds) {
|
|
678
|
+
if (!Number.isFinite(waitSeconds) || waitSeconds < 0) {
|
|
679
|
+
throw new CacheInvalidTtlError("[@holo-js/cache] Cache lock wait seconds must be a finite number greater than or equal to 0.");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
549
682
|
function createMemoryLock(state, name, seconds, now, sleep) {
|
|
683
|
+
validateLockSeconds2(seconds);
|
|
550
684
|
const owner = Symbol(name);
|
|
551
685
|
function clearExpiredLock() {
|
|
552
686
|
const current = state.locks.get(name);
|
|
@@ -594,6 +728,7 @@ function createMemoryLock(state, name, seconds, now, sleep) {
|
|
|
594
728
|
return true;
|
|
595
729
|
},
|
|
596
730
|
async block(waitSeconds, callback) {
|
|
731
|
+
validateLockWaitSeconds2(waitSeconds);
|
|
597
732
|
const deadline = now() + waitSeconds * 1e3;
|
|
598
733
|
while (true) {
|
|
599
734
|
if (tryAcquire()) {
|
|
@@ -710,9 +845,13 @@ function createMemoryCacheDriver(options) {
|
|
|
710
845
|
}
|
|
711
846
|
|
|
712
847
|
// src/redis.ts
|
|
848
|
+
import { createRequire as createRequire2 } from "module";
|
|
849
|
+
import { join as join3 } from "path";
|
|
850
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
713
851
|
import {
|
|
714
852
|
normalizeRedisConfig
|
|
715
853
|
} from "@holo-js/config";
|
|
854
|
+
var CACHE_DRIVER_DISPOSE_SYMBOL = /* @__PURE__ */ Symbol.for("holo.cache.driver.dispose");
|
|
716
855
|
function isNormalizedRedisConfig(config) {
|
|
717
856
|
return typeof config.default === "string" && typeof config.connections === "object" && config.connections !== null && Object.values(config.connections).every((connection) => {
|
|
718
857
|
return typeof connection === "object" && connection !== null && "name" in connection && "host" in connection && "port" in connection && typeof connection.name === "string" && typeof connection.host === "string" && typeof connection.port === "number";
|
|
@@ -722,6 +861,9 @@ function normalizeRuntimeRedisConfig(config) {
|
|
|
722
861
|
if (!config) return void 0;
|
|
723
862
|
return isNormalizedRedisConfig(config) ? config : normalizeRedisConfig(config);
|
|
724
863
|
}
|
|
864
|
+
function escapeRegExp2(value) {
|
|
865
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
866
|
+
}
|
|
725
867
|
function resolveSharedRedisConnection(redisConfig, connectionName) {
|
|
726
868
|
if (!redisConfig) {
|
|
727
869
|
throw new CacheDriverResolutionError(
|
|
@@ -735,11 +877,28 @@ function resolveSharedRedisConnection(redisConfig, connectionName) {
|
|
|
735
877
|
`[@holo-js/cache] Redis cache connection "${connectionName}" was not found in config/redis.ts. Available connections: ${availableConnections.join(", ") || "(none)"}.`
|
|
736
878
|
);
|
|
737
879
|
}
|
|
738
|
-
function isModuleNotFoundError2(error) {
|
|
739
|
-
|
|
880
|
+
function isModuleNotFoundError2(error, expectedSpecifier = "@holo-js/cache-redis") {
|
|
881
|
+
if (!error || typeof error !== "object") {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
const message = "message" in error && typeof error.message === "string" ? error.message : "";
|
|
885
|
+
const code = "code" in error ? error.code : void 0;
|
|
886
|
+
const escapedSpecifier = escapeRegExp2(expectedSpecifier);
|
|
887
|
+
if ((code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") && [
|
|
888
|
+
new RegExp(`Cannot find package ['"]${escapedSpecifier}['"]`),
|
|
889
|
+
new RegExp(`Cannot find module ['"]${escapedSpecifier}['"]`),
|
|
890
|
+
new RegExp(`Could not resolve ['"]${escapedSpecifier}['"]`),
|
|
891
|
+
new RegExp(`Failed to load url\\s+(?:['"\`]${escapedSpecifier}['"\`]|${escapedSpecifier}(?=[\\s(]|$))`)
|
|
892
|
+
].some((pattern) => pattern.test(message))) {
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
if ("cause" in error) {
|
|
896
|
+
return isModuleNotFoundError2(error.cause, expectedSpecifier);
|
|
897
|
+
}
|
|
898
|
+
return false;
|
|
740
899
|
}
|
|
741
|
-
function normalizeRedisModuleLoadError(error) {
|
|
742
|
-
if (isModuleNotFoundError2(error)) {
|
|
900
|
+
function normalizeRedisModuleLoadError(error, expectedSpecifier = "@holo-js/cache-redis") {
|
|
901
|
+
if (isModuleNotFoundError2(error, expectedSpecifier)) {
|
|
743
902
|
return new CacheOptionalPackageError(
|
|
744
903
|
"[@holo-js/cache] Redis cache support requires @holo-js/cache-redis to be installed.",
|
|
745
904
|
{ cause: error }
|
|
@@ -747,12 +906,26 @@ function normalizeRedisModuleLoadError(error) {
|
|
|
747
906
|
}
|
|
748
907
|
return error;
|
|
749
908
|
}
|
|
909
|
+
async function importRedisDriverModuleFromProject(specifier) {
|
|
910
|
+
const projectRequire = createRequire2(join3(process.cwd(), "package.json"));
|
|
911
|
+
return await import(pathToFileURL2(projectRequire.resolve(specifier)).href);
|
|
912
|
+
}
|
|
750
913
|
async function loadRedisDriverModule() {
|
|
914
|
+
const specifier = "@holo-js/cache-redis";
|
|
751
915
|
try {
|
|
752
|
-
|
|
753
|
-
|
|
916
|
+
return await import(
|
|
917
|
+
/* webpackIgnore: true */
|
|
918
|
+
specifier
|
|
919
|
+
);
|
|
754
920
|
} catch (error) {
|
|
755
|
-
|
|
921
|
+
if (!isModuleNotFoundError2(error, specifier)) {
|
|
922
|
+
throw normalizeRedisModuleLoadError(error, specifier);
|
|
923
|
+
}
|
|
924
|
+
try {
|
|
925
|
+
return await importRedisDriverModuleFromProject(specifier);
|
|
926
|
+
} catch (fallbackError) {
|
|
927
|
+
throw normalizeRedisModuleLoadError(fallbackError, specifier);
|
|
928
|
+
}
|
|
756
929
|
}
|
|
757
930
|
}
|
|
758
931
|
var redisDriverModuleLoader = loadRedisDriverModule;
|
|
@@ -769,14 +942,18 @@ var LazyRedisCacheDriver = class {
|
|
|
769
942
|
driver = "redis";
|
|
770
943
|
driverInstance;
|
|
771
944
|
pending;
|
|
945
|
+
disposalGeneration = 0;
|
|
772
946
|
get name() {
|
|
773
947
|
return this.options.name;
|
|
774
948
|
}
|
|
775
949
|
async resolveDriver() {
|
|
776
950
|
if (this.driverInstance) return this.driverInstance;
|
|
951
|
+
const pendingGeneration = this.disposalGeneration;
|
|
777
952
|
this.pending ??= redisDriverModuleLoader().then((module) => {
|
|
778
953
|
const driver = module.createRedisCacheDriver(this.options);
|
|
779
|
-
this.
|
|
954
|
+
if (this.disposalGeneration === pendingGeneration) {
|
|
955
|
+
this.driverInstance = driver;
|
|
956
|
+
}
|
|
780
957
|
return driver;
|
|
781
958
|
}).finally(() => {
|
|
782
959
|
this.pending = void 0;
|
|
@@ -786,6 +963,23 @@ var LazyRedisCacheDriver = class {
|
|
|
786
963
|
async withDriver(callback) {
|
|
787
964
|
return callback(await this.resolveDriver());
|
|
788
965
|
}
|
|
966
|
+
[CACHE_DRIVER_DISPOSE_SYMBOL]() {
|
|
967
|
+
const pending = this.pending;
|
|
968
|
+
const driverInstance = this.driverInstance;
|
|
969
|
+
this.disposalGeneration += 1;
|
|
970
|
+
this.driverInstance = void 0;
|
|
971
|
+
this.pending = void 0;
|
|
972
|
+
if (driverInstance) {
|
|
973
|
+
const disposable = driverInstance;
|
|
974
|
+
disposable[CACHE_DRIVER_DISPOSE_SYMBOL]?.();
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
pending?.then((driver) => {
|
|
978
|
+
const disposable = driver;
|
|
979
|
+
disposable[CACHE_DRIVER_DISPOSE_SYMBOL]?.();
|
|
980
|
+
}).catch(() => {
|
|
981
|
+
});
|
|
982
|
+
}
|
|
789
983
|
createLockProxy(name, seconds) {
|
|
790
984
|
let lockPromise;
|
|
791
985
|
const resolveLock = async () => {
|
|
@@ -845,6 +1039,23 @@ var cacheRedisInternals = {
|
|
|
845
1039
|
};
|
|
846
1040
|
|
|
847
1041
|
// src/runtime-shared.ts
|
|
1042
|
+
var CACHE_DRIVER_DISPOSE_SYMBOL2 = /* @__PURE__ */ Symbol.for("holo.cache.driver.dispose");
|
|
1043
|
+
function disposeDriver(driver) {
|
|
1044
|
+
const disposable = driver;
|
|
1045
|
+
disposable[CACHE_DRIVER_DISPOSE_SYMBOL2]?.();
|
|
1046
|
+
}
|
|
1047
|
+
function disposeCacheRuntimeBindings(bindings) {
|
|
1048
|
+
if (!bindings) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
for (const [driverName, driver] of bindings.drivers.entries()) {
|
|
1052
|
+
try {
|
|
1053
|
+
disposeDriver(driver);
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
console.error(`[@holo-js/cache] Failed to dispose cache driver "${driverName}".`, error);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
848
1059
|
function isNormalizedCacheConfig(config) {
|
|
849
1060
|
return typeof config.default === "string" && typeof config.prefix === "string" && typeof config.drivers === "object" && config.drivers !== null && Object.values(config.drivers).every((driver) => {
|
|
850
1061
|
return typeof driver === "object" && driver !== null && "name" in driver && "prefix" in driver && typeof driver.name === "string" && typeof driver.prefix === "string";
|
|
@@ -1155,9 +1366,10 @@ function createCacheQueryBridge(dependencyIndex = getOrCreateDependencyIndex())
|
|
|
1155
1366
|
await dependencyIndex.removeKey(indexedKey);
|
|
1156
1367
|
return context.driver.forget(resolveNormalizedKey(key, options?.driver));
|
|
1157
1368
|
},
|
|
1158
|
-
async invalidateDependencies(dependencies) {
|
|
1369
|
+
async invalidateDependencies(dependencies, options) {
|
|
1159
1370
|
const invalidatedKeys = /* @__PURE__ */ new Set();
|
|
1160
1371
|
const runtime = getCacheRuntime();
|
|
1372
|
+
const driverName = options?.driver?.trim();
|
|
1161
1373
|
for (const dependency of dependencies) {
|
|
1162
1374
|
const indexedKeys = await dependencyIndex.listKeys(dependency);
|
|
1163
1375
|
for (const indexedKey of indexedKeys) {
|
|
@@ -1166,6 +1378,9 @@ function createCacheQueryBridge(dependencyIndex = getOrCreateDependencyIndex())
|
|
|
1166
1378
|
}
|
|
1167
1379
|
invalidatedKeys.add(indexedKey);
|
|
1168
1380
|
const parsed = parseIndexedKey(indexedKey);
|
|
1381
|
+
if (driverName && parsed.driverName !== driverName) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1169
1384
|
const driver = resolveConfiguredDriver(runtime, parsed.driverName);
|
|
1170
1385
|
await driver.forget(parsed.normalizedKey);
|
|
1171
1386
|
await dependencyIndex.removeKey(indexedKey);
|
|
@@ -1186,6 +1401,7 @@ var cacheQueryBridgeInternals = {
|
|
|
1186
1401
|
|
|
1187
1402
|
// src/runtime.ts
|
|
1188
1403
|
function configureCacheRuntime(bindings) {
|
|
1404
|
+
disposeCacheRuntimeBindings(getCacheRuntimeState().bindings);
|
|
1189
1405
|
if (!bindings) {
|
|
1190
1406
|
getCacheRuntimeState().bindings = void 0;
|
|
1191
1407
|
resetDefaultDependencyIndex();
|
|
@@ -1205,6 +1421,7 @@ function configureCacheRuntime(bindings) {
|
|
|
1205
1421
|
setGlobalDatabaseQueryCacheBridge(queryBridge);
|
|
1206
1422
|
}
|
|
1207
1423
|
function resetCacheRuntime() {
|
|
1424
|
+
disposeCacheRuntimeBindings(getCacheRuntimeState().bindings);
|
|
1208
1425
|
getCacheRuntimeState().bindings = void 0;
|
|
1209
1426
|
resetDefaultDependencyIndex();
|
|
1210
1427
|
setGlobalDatabaseQueryCacheBridge(void 0);
|
|
@@ -1285,7 +1502,13 @@ function createCacheRepository(driverName) {
|
|
|
1285
1502
|
}
|
|
1286
1503
|
async function getCachedValue2(key) {
|
|
1287
1504
|
const payload = await getEntryPayload(key);
|
|
1288
|
-
|
|
1505
|
+
if (typeof payload !== "string") {
|
|
1506
|
+
return Object.freeze({ hit: false });
|
|
1507
|
+
}
|
|
1508
|
+
return Object.freeze({
|
|
1509
|
+
hit: true,
|
|
1510
|
+
value: deserializeCacheValue(payload)
|
|
1511
|
+
});
|
|
1289
1512
|
}
|
|
1290
1513
|
async function putFlexibleEnvelope(key, ttl, value) {
|
|
1291
1514
|
const now = Date.now();
|
|
@@ -1377,8 +1600,8 @@ function createCacheRepository(driverName) {
|
|
|
1377
1600
|
},
|
|
1378
1601
|
async remember(key, ttl, callback) {
|
|
1379
1602
|
const cached = await getCachedValue2(key);
|
|
1380
|
-
if (cached
|
|
1381
|
-
return cached;
|
|
1603
|
+
if (cached.hit) {
|
|
1604
|
+
return cached.value;
|
|
1382
1605
|
}
|
|
1383
1606
|
const value = await resolveValue(callback);
|
|
1384
1607
|
await repository.put(key, value, ttl);
|
|
@@ -1386,8 +1609,8 @@ function createCacheRepository(driverName) {
|
|
|
1386
1609
|
},
|
|
1387
1610
|
async rememberForever(key, callback) {
|
|
1388
1611
|
const cached = await getCachedValue2(key);
|
|
1389
|
-
if (cached
|
|
1390
|
-
return cached;
|
|
1612
|
+
if (cached.hit) {
|
|
1613
|
+
return cached.value;
|
|
1391
1614
|
}
|
|
1392
1615
|
const value = await resolveValue(callback);
|
|
1393
1616
|
await repository.forever(key, value);
|
|
@@ -1397,17 +1620,17 @@ function createCacheRepository(driverName) {
|
|
|
1397
1620
|
const normalizedTtl = normalizeFlexibleTtl2(ttl);
|
|
1398
1621
|
const now = Date.now();
|
|
1399
1622
|
const cached = await getCachedValue2(key);
|
|
1400
|
-
if (isFlexibleEnvelope2(cached)) {
|
|
1401
|
-
if (now <= cached.freshUntil) {
|
|
1402
|
-
return cached.value;
|
|
1623
|
+
if (cached.hit && isFlexibleEnvelope2(cached.value)) {
|
|
1624
|
+
if (now <= cached.value.freshUntil) {
|
|
1625
|
+
return cached.value.value;
|
|
1403
1626
|
}
|
|
1404
|
-
if (now <= cached.staleUntil) {
|
|
1627
|
+
if (now <= cached.value.staleUntil) {
|
|
1405
1628
|
const refreshLock2 = createRefreshLock(key, normalizedTtl.staleSeconds);
|
|
1406
1629
|
void refreshLock2.get(async () => {
|
|
1407
1630
|
await refreshFlexibleValue(key, normalizedTtl, callback);
|
|
1408
1631
|
return true;
|
|
1409
1632
|
}).catch(() => void 0);
|
|
1410
|
-
return cached.value;
|
|
1633
|
+
return cached.value.value;
|
|
1411
1634
|
}
|
|
1412
1635
|
}
|
|
1413
1636
|
const refreshLock = createRefreshLock(key, normalizedTtl.staleSeconds);
|
|
@@ -1422,9 +1645,9 @@ function createCacheRepository(driverName) {
|
|
|
1422
1645
|
return refreshed;
|
|
1423
1646
|
}
|
|
1424
1647
|
const retried = await getCachedValue2(key);
|
|
1425
|
-
if (isFlexibleEnvelope2(retried)) {
|
|
1426
|
-
if (Date.now() <= retried.staleUntil) {
|
|
1427
|
-
return retried.value;
|
|
1648
|
+
if (retried.hit && isFlexibleEnvelope2(retried.value)) {
|
|
1649
|
+
if (Date.now() <= retried.value.staleUntil) {
|
|
1650
|
+
return retried.value.value;
|
|
1428
1651
|
}
|
|
1429
1652
|
}
|
|
1430
1653
|
return refreshFlexibleValue(key, normalizedTtl, callback);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holo-js/cache",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Holo-JS Framework - cache contracts, config helpers, serialization, and runtime seams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"test": "vitest --run"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@holo-js/config": "^0.1.
|
|
31
|
+
"@holo-js/config": "^0.1.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^22.10.2",
|
|
35
35
|
"tsup": "^8.3.5",
|
|
36
36
|
"typescript": "^5.7.2",
|
|
37
|
-
"vitest": "^
|
|
37
|
+
"vitest": "^4.1.5"
|
|
38
38
|
}
|
|
39
39
|
}
|