@holo-js/cache 0.1.4 → 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 +265 -48
- 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,15 +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
|
-
const specifier = "@holo-js/cache-db";
|
|
88
96
|
return await import(
|
|
89
97
|
/* webpackIgnore: true */
|
|
90
98
|
specifier
|
|
91
99
|
);
|
|
92
100
|
} catch (error) {
|
|
93
|
-
|
|
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
|
+
}
|
|
94
109
|
}
|
|
95
110
|
}
|
|
96
111
|
var databaseDriverModuleLoader = loadDatabaseDriverModule;
|
|
@@ -184,14 +199,24 @@ var cacheDbInternals = {
|
|
|
184
199
|
|
|
185
200
|
// src/file.ts
|
|
186
201
|
import { createHash, randomUUID } from "crypto";
|
|
187
|
-
import { mkdir, open, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
188
|
-
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";
|
|
189
204
|
var MALFORMED_FILE = /* @__PURE__ */ Symbol("MALFORMED_FILE");
|
|
190
205
|
function defaultSleep(milliseconds) {
|
|
191
206
|
return new Promise((resolveDelay) => {
|
|
192
207
|
setTimeout(resolveDelay, milliseconds);
|
|
193
208
|
});
|
|
194
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
|
+
}
|
|
195
220
|
function hashCacheKey(key) {
|
|
196
221
|
return createHash("sha256").update(key).digest("hex");
|
|
197
222
|
}
|
|
@@ -200,11 +225,14 @@ function resolveDriverRoot(path) {
|
|
|
200
225
|
}
|
|
201
226
|
function resolveEntryFilePath(rootPath, key) {
|
|
202
227
|
const hash = hashCacheKey(key);
|
|
203
|
-
return
|
|
228
|
+
return join2(rootPath, "entries", hash.slice(0, 2), `${hash}.json`);
|
|
204
229
|
}
|
|
205
230
|
function resolveLockFilePath(rootPath, name) {
|
|
206
231
|
const hash = hashCacheKey(name);
|
|
207
|
-
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`);
|
|
208
236
|
}
|
|
209
237
|
function isPositiveTimestamp(value) {
|
|
210
238
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
@@ -229,6 +257,16 @@ async function ensureParentDirectory(filePath) {
|
|
|
229
257
|
async function removeFileIfPresent(filePath) {
|
|
230
258
|
await rm(filePath, { force: true });
|
|
231
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
|
+
}
|
|
232
270
|
async function readFileIfPresent(filePath) {
|
|
233
271
|
try {
|
|
234
272
|
return await readFile(filePath, "utf8");
|
|
@@ -280,7 +318,7 @@ async function listFiles(rootPath) {
|
|
|
280
318
|
try {
|
|
281
319
|
const entries = await readdir(rootPath, { withFileTypes: true });
|
|
282
320
|
const nested = await Promise.all(entries.map(async (entry) => {
|
|
283
|
-
const entryPath =
|
|
321
|
+
const entryPath = join2(rootPath, entry.name);
|
|
284
322
|
if (entry.isDirectory()) return listFiles(entryPath);
|
|
285
323
|
return [entryPath];
|
|
286
324
|
}));
|
|
@@ -302,6 +340,10 @@ async function removeInvalidFileAndReadMissing(filePath, decoded) {
|
|
|
302
340
|
};
|
|
303
341
|
}
|
|
304
342
|
async function removeScopedCacheFiles(rootPath, prefix, isEnvelope, resolveName) {
|
|
343
|
+
async function removeScopedFile(filePath) {
|
|
344
|
+
await removeFileIfPresent(filePath);
|
|
345
|
+
await removeEmptyDirectoryIfPresent(dirname(filePath));
|
|
346
|
+
}
|
|
305
347
|
if (!prefix) {
|
|
306
348
|
await rm(rootPath, { recursive: true, force: true });
|
|
307
349
|
await mkdir(rootPath, { recursive: true });
|
|
@@ -310,11 +352,11 @@ async function removeScopedCacheFiles(rootPath, prefix, isEnvelope, resolveName)
|
|
|
310
352
|
for (const filePath of await listFiles(rootPath)) {
|
|
311
353
|
const decoded = await readJsonFile(filePath);
|
|
312
354
|
if (!decoded || decoded === MALFORMED_FILE || !isEnvelope(decoded)) {
|
|
313
|
-
await
|
|
355
|
+
await removeScopedFile(filePath);
|
|
314
356
|
continue;
|
|
315
357
|
}
|
|
316
358
|
if (resolveName(decoded).startsWith(prefix)) {
|
|
317
|
-
await
|
|
359
|
+
await removeScopedFile(filePath);
|
|
318
360
|
}
|
|
319
361
|
}
|
|
320
362
|
}
|
|
@@ -335,17 +377,77 @@ async function readEntry(rootPath, key, now) {
|
|
|
335
377
|
}
|
|
336
378
|
async function readLock(rootPath, name, now) {
|
|
337
379
|
const filePath = resolveLockFilePath(rootPath, name);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
};
|
|
341
417
|
}
|
|
342
|
-
if (
|
|
343
|
-
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
|
+
};
|
|
344
446
|
}
|
|
447
|
+
await removeEmptyDirectoryIfPresent(filePath);
|
|
345
448
|
return {
|
|
346
|
-
state: "
|
|
347
|
-
filePath
|
|
348
|
-
value: decoded
|
|
449
|
+
state: "missing",
|
|
450
|
+
filePath
|
|
349
451
|
};
|
|
350
452
|
}
|
|
351
453
|
function createEntryEnvelope(input) {
|
|
@@ -356,7 +458,27 @@ function createEntryEnvelope(input) {
|
|
|
356
458
|
};
|
|
357
459
|
}
|
|
358
460
|
function createFileLock(rootPath, name, seconds, now, sleep, ownerFactory) {
|
|
461
|
+
validateLockSeconds(seconds);
|
|
359
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
|
+
}
|
|
360
482
|
async function tryAcquire() {
|
|
361
483
|
const filePath = resolveLockFilePath(rootPath, name);
|
|
362
484
|
const envelope = JSON.stringify({
|
|
@@ -364,12 +486,12 @@ function createFileLock(rootPath, name, seconds, now, sleep, ownerFactory) {
|
|
|
364
486
|
owner,
|
|
365
487
|
expiresAt: now() + seconds * 1e3
|
|
366
488
|
});
|
|
367
|
-
if (await
|
|
489
|
+
if (await writeLockDirectory(filePath, envelope)) {
|
|
368
490
|
return true;
|
|
369
491
|
}
|
|
370
492
|
const currentLock = await readLock(rootPath, name, now());
|
|
371
493
|
if (currentLock.state === "missing") {
|
|
372
|
-
return
|
|
494
|
+
return writeLockDirectory(filePath, envelope);
|
|
373
495
|
}
|
|
374
496
|
return false;
|
|
375
497
|
}
|
|
@@ -396,14 +518,12 @@ function createFileLock(rootPath, name, seconds, now, sleep, ownerFactory) {
|
|
|
396
518
|
if (currentLock.state === "missing" || currentLock.value.owner !== owner) {
|
|
397
519
|
return false;
|
|
398
520
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
|
-
await removeFileIfPresent(currentLock.filePath);
|
|
521
|
+
await removeFileIfPresent(currentLock.markerFilePath);
|
|
522
|
+
await removeEmptyDirectoryIfPresent(currentLock.filePath);
|
|
404
523
|
return true;
|
|
405
524
|
},
|
|
406
525
|
async block(waitSeconds, callback) {
|
|
526
|
+
validateLockWaitSeconds(waitSeconds);
|
|
407
527
|
const deadline = now() + waitSeconds * 1e3;
|
|
408
528
|
while (true) {
|
|
409
529
|
if (await tryAcquire()) {
|
|
@@ -505,13 +625,13 @@ function createFileCacheDriver(options) {
|
|
|
505
625
|
},
|
|
506
626
|
async flush() {
|
|
507
627
|
await removeScopedCacheFiles(
|
|
508
|
-
|
|
628
|
+
join2(rootPath, "entries"),
|
|
509
629
|
prefix,
|
|
510
630
|
isFileCacheEntryEnvelope,
|
|
511
631
|
(entry) => entry.key
|
|
512
632
|
);
|
|
513
633
|
await removeScopedCacheFiles(
|
|
514
|
-
|
|
634
|
+
join2(rootPath, "locks"),
|
|
515
635
|
prefix,
|
|
516
636
|
isFileCacheLockEnvelope,
|
|
517
637
|
(lock) => lock.name
|
|
@@ -549,7 +669,18 @@ function defaultSleep2(milliseconds) {
|
|
|
549
669
|
setTimeout(resolve2, milliseconds);
|
|
550
670
|
});
|
|
551
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
|
+
}
|
|
552
682
|
function createMemoryLock(state, name, seconds, now, sleep) {
|
|
683
|
+
validateLockSeconds2(seconds);
|
|
553
684
|
const owner = Symbol(name);
|
|
554
685
|
function clearExpiredLock() {
|
|
555
686
|
const current = state.locks.get(name);
|
|
@@ -597,6 +728,7 @@ function createMemoryLock(state, name, seconds, now, sleep) {
|
|
|
597
728
|
return true;
|
|
598
729
|
},
|
|
599
730
|
async block(waitSeconds, callback) {
|
|
731
|
+
validateLockWaitSeconds2(waitSeconds);
|
|
600
732
|
const deadline = now() + waitSeconds * 1e3;
|
|
601
733
|
while (true) {
|
|
602
734
|
if (tryAcquire()) {
|
|
@@ -713,9 +845,13 @@ function createMemoryCacheDriver(options) {
|
|
|
713
845
|
}
|
|
714
846
|
|
|
715
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";
|
|
716
851
|
import {
|
|
717
852
|
normalizeRedisConfig
|
|
718
853
|
} from "@holo-js/config";
|
|
854
|
+
var CACHE_DRIVER_DISPOSE_SYMBOL = /* @__PURE__ */ Symbol.for("holo.cache.driver.dispose");
|
|
719
855
|
function isNormalizedRedisConfig(config) {
|
|
720
856
|
return typeof config.default === "string" && typeof config.connections === "object" && config.connections !== null && Object.values(config.connections).every((connection) => {
|
|
721
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";
|
|
@@ -725,6 +861,9 @@ function normalizeRuntimeRedisConfig(config) {
|
|
|
725
861
|
if (!config) return void 0;
|
|
726
862
|
return isNormalizedRedisConfig(config) ? config : normalizeRedisConfig(config);
|
|
727
863
|
}
|
|
864
|
+
function escapeRegExp2(value) {
|
|
865
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
866
|
+
}
|
|
728
867
|
function resolveSharedRedisConnection(redisConfig, connectionName) {
|
|
729
868
|
if (!redisConfig) {
|
|
730
869
|
throw new CacheDriverResolutionError(
|
|
@@ -738,11 +877,28 @@ function resolveSharedRedisConnection(redisConfig, connectionName) {
|
|
|
738
877
|
`[@holo-js/cache] Redis cache connection "${connectionName}" was not found in config/redis.ts. Available connections: ${availableConnections.join(", ") || "(none)"}.`
|
|
739
878
|
);
|
|
740
879
|
}
|
|
741
|
-
function isModuleNotFoundError2(error) {
|
|
742
|
-
|
|
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;
|
|
743
899
|
}
|
|
744
|
-
function normalizeRedisModuleLoadError(error) {
|
|
745
|
-
if (isModuleNotFoundError2(error)) {
|
|
900
|
+
function normalizeRedisModuleLoadError(error, expectedSpecifier = "@holo-js/cache-redis") {
|
|
901
|
+
if (isModuleNotFoundError2(error, expectedSpecifier)) {
|
|
746
902
|
return new CacheOptionalPackageError(
|
|
747
903
|
"[@holo-js/cache] Redis cache support requires @holo-js/cache-redis to be installed.",
|
|
748
904
|
{ cause: error }
|
|
@@ -750,15 +906,26 @@ function normalizeRedisModuleLoadError(error) {
|
|
|
750
906
|
}
|
|
751
907
|
return error;
|
|
752
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
|
+
}
|
|
753
913
|
async function loadRedisDriverModule() {
|
|
914
|
+
const specifier = "@holo-js/cache-redis";
|
|
754
915
|
try {
|
|
755
|
-
const specifier = "@holo-js/cache-redis";
|
|
756
916
|
return await import(
|
|
757
917
|
/* webpackIgnore: true */
|
|
758
918
|
specifier
|
|
759
919
|
);
|
|
760
920
|
} catch (error) {
|
|
761
|
-
|
|
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
|
+
}
|
|
762
929
|
}
|
|
763
930
|
}
|
|
764
931
|
var redisDriverModuleLoader = loadRedisDriverModule;
|
|
@@ -775,14 +942,18 @@ var LazyRedisCacheDriver = class {
|
|
|
775
942
|
driver = "redis";
|
|
776
943
|
driverInstance;
|
|
777
944
|
pending;
|
|
945
|
+
disposalGeneration = 0;
|
|
778
946
|
get name() {
|
|
779
947
|
return this.options.name;
|
|
780
948
|
}
|
|
781
949
|
async resolveDriver() {
|
|
782
950
|
if (this.driverInstance) return this.driverInstance;
|
|
951
|
+
const pendingGeneration = this.disposalGeneration;
|
|
783
952
|
this.pending ??= redisDriverModuleLoader().then((module) => {
|
|
784
953
|
const driver = module.createRedisCacheDriver(this.options);
|
|
785
|
-
this.
|
|
954
|
+
if (this.disposalGeneration === pendingGeneration) {
|
|
955
|
+
this.driverInstance = driver;
|
|
956
|
+
}
|
|
786
957
|
return driver;
|
|
787
958
|
}).finally(() => {
|
|
788
959
|
this.pending = void 0;
|
|
@@ -792,6 +963,23 @@ var LazyRedisCacheDriver = class {
|
|
|
792
963
|
async withDriver(callback) {
|
|
793
964
|
return callback(await this.resolveDriver());
|
|
794
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
|
+
}
|
|
795
983
|
createLockProxy(name, seconds) {
|
|
796
984
|
let lockPromise;
|
|
797
985
|
const resolveLock = async () => {
|
|
@@ -851,6 +1039,23 @@ var cacheRedisInternals = {
|
|
|
851
1039
|
};
|
|
852
1040
|
|
|
853
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
|
+
}
|
|
854
1059
|
function isNormalizedCacheConfig(config) {
|
|
855
1060
|
return typeof config.default === "string" && typeof config.prefix === "string" && typeof config.drivers === "object" && config.drivers !== null && Object.values(config.drivers).every((driver) => {
|
|
856
1061
|
return typeof driver === "object" && driver !== null && "name" in driver && "prefix" in driver && typeof driver.name === "string" && typeof driver.prefix === "string";
|
|
@@ -1161,9 +1366,10 @@ function createCacheQueryBridge(dependencyIndex = getOrCreateDependencyIndex())
|
|
|
1161
1366
|
await dependencyIndex.removeKey(indexedKey);
|
|
1162
1367
|
return context.driver.forget(resolveNormalizedKey(key, options?.driver));
|
|
1163
1368
|
},
|
|
1164
|
-
async invalidateDependencies(dependencies) {
|
|
1369
|
+
async invalidateDependencies(dependencies, options) {
|
|
1165
1370
|
const invalidatedKeys = /* @__PURE__ */ new Set();
|
|
1166
1371
|
const runtime = getCacheRuntime();
|
|
1372
|
+
const driverName = options?.driver?.trim();
|
|
1167
1373
|
for (const dependency of dependencies) {
|
|
1168
1374
|
const indexedKeys = await dependencyIndex.listKeys(dependency);
|
|
1169
1375
|
for (const indexedKey of indexedKeys) {
|
|
@@ -1172,6 +1378,9 @@ function createCacheQueryBridge(dependencyIndex = getOrCreateDependencyIndex())
|
|
|
1172
1378
|
}
|
|
1173
1379
|
invalidatedKeys.add(indexedKey);
|
|
1174
1380
|
const parsed = parseIndexedKey(indexedKey);
|
|
1381
|
+
if (driverName && parsed.driverName !== driverName) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1175
1384
|
const driver = resolveConfiguredDriver(runtime, parsed.driverName);
|
|
1176
1385
|
await driver.forget(parsed.normalizedKey);
|
|
1177
1386
|
await dependencyIndex.removeKey(indexedKey);
|
|
@@ -1192,6 +1401,7 @@ var cacheQueryBridgeInternals = {
|
|
|
1192
1401
|
|
|
1193
1402
|
// src/runtime.ts
|
|
1194
1403
|
function configureCacheRuntime(bindings) {
|
|
1404
|
+
disposeCacheRuntimeBindings(getCacheRuntimeState().bindings);
|
|
1195
1405
|
if (!bindings) {
|
|
1196
1406
|
getCacheRuntimeState().bindings = void 0;
|
|
1197
1407
|
resetDefaultDependencyIndex();
|
|
@@ -1211,6 +1421,7 @@ function configureCacheRuntime(bindings) {
|
|
|
1211
1421
|
setGlobalDatabaseQueryCacheBridge(queryBridge);
|
|
1212
1422
|
}
|
|
1213
1423
|
function resetCacheRuntime() {
|
|
1424
|
+
disposeCacheRuntimeBindings(getCacheRuntimeState().bindings);
|
|
1214
1425
|
getCacheRuntimeState().bindings = void 0;
|
|
1215
1426
|
resetDefaultDependencyIndex();
|
|
1216
1427
|
setGlobalDatabaseQueryCacheBridge(void 0);
|
|
@@ -1291,7 +1502,13 @@ function createCacheRepository(driverName) {
|
|
|
1291
1502
|
}
|
|
1292
1503
|
async function getCachedValue2(key) {
|
|
1293
1504
|
const payload = await getEntryPayload(key);
|
|
1294
|
-
|
|
1505
|
+
if (typeof payload !== "string") {
|
|
1506
|
+
return Object.freeze({ hit: false });
|
|
1507
|
+
}
|
|
1508
|
+
return Object.freeze({
|
|
1509
|
+
hit: true,
|
|
1510
|
+
value: deserializeCacheValue(payload)
|
|
1511
|
+
});
|
|
1295
1512
|
}
|
|
1296
1513
|
async function putFlexibleEnvelope(key, ttl, value) {
|
|
1297
1514
|
const now = Date.now();
|
|
@@ -1383,8 +1600,8 @@ function createCacheRepository(driverName) {
|
|
|
1383
1600
|
},
|
|
1384
1601
|
async remember(key, ttl, callback) {
|
|
1385
1602
|
const cached = await getCachedValue2(key);
|
|
1386
|
-
if (cached
|
|
1387
|
-
return cached;
|
|
1603
|
+
if (cached.hit) {
|
|
1604
|
+
return cached.value;
|
|
1388
1605
|
}
|
|
1389
1606
|
const value = await resolveValue(callback);
|
|
1390
1607
|
await repository.put(key, value, ttl);
|
|
@@ -1392,8 +1609,8 @@ function createCacheRepository(driverName) {
|
|
|
1392
1609
|
},
|
|
1393
1610
|
async rememberForever(key, callback) {
|
|
1394
1611
|
const cached = await getCachedValue2(key);
|
|
1395
|
-
if (cached
|
|
1396
|
-
return cached;
|
|
1612
|
+
if (cached.hit) {
|
|
1613
|
+
return cached.value;
|
|
1397
1614
|
}
|
|
1398
1615
|
const value = await resolveValue(callback);
|
|
1399
1616
|
await repository.forever(key, value);
|
|
@@ -1403,17 +1620,17 @@ function createCacheRepository(driverName) {
|
|
|
1403
1620
|
const normalizedTtl = normalizeFlexibleTtl2(ttl);
|
|
1404
1621
|
const now = Date.now();
|
|
1405
1622
|
const cached = await getCachedValue2(key);
|
|
1406
|
-
if (isFlexibleEnvelope2(cached)) {
|
|
1407
|
-
if (now <= cached.freshUntil) {
|
|
1408
|
-
return cached.value;
|
|
1623
|
+
if (cached.hit && isFlexibleEnvelope2(cached.value)) {
|
|
1624
|
+
if (now <= cached.value.freshUntil) {
|
|
1625
|
+
return cached.value.value;
|
|
1409
1626
|
}
|
|
1410
|
-
if (now <= cached.staleUntil) {
|
|
1627
|
+
if (now <= cached.value.staleUntil) {
|
|
1411
1628
|
const refreshLock2 = createRefreshLock(key, normalizedTtl.staleSeconds);
|
|
1412
1629
|
void refreshLock2.get(async () => {
|
|
1413
1630
|
await refreshFlexibleValue(key, normalizedTtl, callback);
|
|
1414
1631
|
return true;
|
|
1415
1632
|
}).catch(() => void 0);
|
|
1416
|
-
return cached.value;
|
|
1633
|
+
return cached.value.value;
|
|
1417
1634
|
}
|
|
1418
1635
|
}
|
|
1419
1636
|
const refreshLock = createRefreshLock(key, normalizedTtl.staleSeconds);
|
|
@@ -1428,9 +1645,9 @@ function createCacheRepository(driverName) {
|
|
|
1428
1645
|
return refreshed;
|
|
1429
1646
|
}
|
|
1430
1647
|
const retried = await getCachedValue2(key);
|
|
1431
|
-
if (isFlexibleEnvelope2(retried)) {
|
|
1432
|
-
if (Date.now() <= retried.staleUntil) {
|
|
1433
|
-
return retried.value;
|
|
1648
|
+
if (retried.hit && isFlexibleEnvelope2(retried.value)) {
|
|
1649
|
+
if (Date.now() <= retried.value.staleUntil) {
|
|
1650
|
+
return retried.value.value;
|
|
1434
1651
|
}
|
|
1435
1652
|
}
|
|
1436
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
|
}
|