@holo-js/cache 0.1.4 → 0.1.6

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,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
- 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
+ }
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 join(rootPath, "entries", hash.slice(0, 2), `${hash}.json`);
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 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`);
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 = join(rootPath, entry.name);
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 removeFileIfPresent(filePath);
355
+ await removeScopedFile(filePath);
314
356
  continue;
315
357
  }
316
358
  if (resolveName(decoded).startsWith(prefix)) {
317
- await removeFileIfPresent(filePath);
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
- const decoded = await readJsonFile(filePath);
339
- if (decoded === MALFORMED_FILE || !isFileCacheLockEnvelope(decoded) || decoded.name !== name) {
340
- 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
+ };
341
417
  }
342
- if (decoded.expiresAt <= now) {
343
- 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
+ };
344
446
  }
447
+ await removeEmptyDirectoryIfPresent(filePath);
345
448
  return {
346
- state: "hit",
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 writeFileExclusively(filePath, envelope)) {
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 writeFileExclusively(filePath, envelope);
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
- const latestLock = await readLock(rootPath, name, now());
400
- if (latestLock.state === "missing" || latestLock.value.owner !== owner) {
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
- join(rootPath, "entries"),
628
+ join2(rootPath, "entries"),
509
629
  prefix,
510
630
  isFileCacheEntryEnvelope,
511
631
  (entry) => entry.key
512
632
  );
513
633
  await removeScopedCacheFiles(
514
- join(rootPath, "locks"),
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
- 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;
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
- 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
+ }
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.driverInstance = driver;
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
- 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
+ });
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 !== null) {
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 !== null) {
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.4",
3
+ "version": "0.1.6",
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.4"
31
+ "@holo-js/config": "^0.1.6"
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
  }