@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 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 ("code" in error && error.code === "ERR_MODULE_NOT_FOUND" && [
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
- const specifier = "@holo-js/cache-db";
88
- return await import(specifier);
96
+ return await import(
97
+ /* webpackIgnore: true */
98
+ specifier
99
+ );
89
100
  } catch (error) {
90
- throw normalizeDatabaseModuleLoadError(error, "@holo-js/cache-db");
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 join(rootPath, "entries", hash.slice(0, 2), `${hash}.json`);
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 join(rootPath, "locks", hash.slice(0, 2), `${hash}.lock`);
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 = join(rootPath, entry.name);
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 removeFileIfPresent(filePath);
355
+ await removeScopedFile(filePath);
311
356
  continue;
312
357
  }
313
358
  if (resolveName(decoded).startsWith(prefix)) {
314
- await removeFileIfPresent(filePath);
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
- const decoded = await readJsonFile(filePath);
336
- if (decoded === MALFORMED_FILE || !isFileCacheLockEnvelope(decoded) || decoded.name !== name) {
337
- return removeInvalidFileAndReadMissing(filePath, decoded);
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 (decoded.expiresAt <= now) {
340
- return removeInvalidFileAndReadMissing(filePath, decoded);
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: "hit",
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 writeFileExclusively(filePath, envelope)) {
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 writeFileExclusively(filePath, envelope);
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
- const latestLock = await readLock(rootPath, name, now());
397
- if (latestLock.state === "missing" || latestLock.value.owner !== owner) {
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
- join(rootPath, "entries"),
628
+ join2(rootPath, "entries"),
506
629
  prefix,
507
630
  isFileCacheEntryEnvelope,
508
631
  (entry) => entry.key
509
632
  );
510
633
  await removeScopedCacheFiles(
511
- join(rootPath, "locks"),
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
- return !!error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND";
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
- const specifier = "@holo-js/cache-redis";
753
- return await import(specifier);
916
+ return await import(
917
+ /* webpackIgnore: true */
918
+ specifier
919
+ );
754
920
  } catch (error) {
755
- throw normalizeRedisModuleLoadError(error);
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.driverInstance = driver;
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
- return typeof payload === "string" ? deserializeCacheValue(payload) : null;
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 !== null) {
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 !== null) {
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",
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.3"
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": "^2.1.8"
37
+ "vitest": "^4.1.5"
38
38
  }
39
39
  }