@holo-js/cache 0.1.3

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.mjs ADDED
@@ -0,0 +1,1523 @@
1
+ import {
2
+ CacheConfigError,
3
+ CacheDriverResolutionError,
4
+ CacheError,
5
+ CacheInvalidNumericMutationError,
6
+ CacheInvalidTtlError,
7
+ CacheLockAcquisitionError,
8
+ CacheOptionalPackageError,
9
+ CacheQueryIntegrationError,
10
+ CacheRuntimeNotConfiguredError,
11
+ CacheSerializationError,
12
+ cacheContractsInternals,
13
+ defineCacheKey,
14
+ deserializeCacheValue,
15
+ isCacheKey,
16
+ normalizeCacheTtl,
17
+ resolveCacheKey,
18
+ serializeCacheValue
19
+ } from "./chunk-KIYULZES.mjs";
20
+
21
+ // src/index.ts
22
+ import { defineCacheConfig } from "@holo-js/config";
23
+
24
+ // src/runtime-shared.ts
25
+ import {
26
+ holoCacheDefaults,
27
+ normalizeCacheConfig
28
+ } from "@holo-js/config";
29
+
30
+ // src/db.ts
31
+ import {
32
+ normalizeDatabaseConfig
33
+ } from "@holo-js/config";
34
+ function escapeRegExp(value) {
35
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36
+ }
37
+ function isNormalizedDatabaseConfig(config) {
38
+ return typeof config === "object" && config !== null && typeof config.connections === "object" && config.connections !== null;
39
+ }
40
+ function normalizeRuntimeDatabaseConfig(config) {
41
+ if (!config) return void 0;
42
+ return normalizeDatabaseConfig(config);
43
+ }
44
+ function resolveSharedDatabaseConnection(databaseConfig, connectionName) {
45
+ if (!databaseConfig) {
46
+ throw new CacheDriverResolutionError(
47
+ `[@holo-js/cache] Database cache driver "${connectionName}" requires a top-level database config from config/database.ts.`
48
+ );
49
+ }
50
+ const connection = databaseConfig.connections[connectionName];
51
+ if (connection) return connection;
52
+ const availableConnections = Object.keys(databaseConfig.connections);
53
+ throw new CacheDriverResolutionError(
54
+ `[@holo-js/cache] Database cache connection "${connectionName}" was not found in config/database.ts. Available connections: ${availableConnections.join(", ") || "(none)"}.`
55
+ );
56
+ }
57
+ function isModuleNotFoundError(error, expectedSpecifier = "@holo-js/cache-db") {
58
+ if (!error || typeof error !== "object") {
59
+ return false;
60
+ }
61
+ const message = "message" in error && typeof error.message === "string" ? error.message : "";
62
+ const escapedSpecifier = escapeRegExp(expectedSpecifier);
63
+ if ("code" in error && error.code === "ERR_MODULE_NOT_FOUND" && [
64
+ new RegExp(`Cannot find package ['"]${escapedSpecifier}['"]`),
65
+ new RegExp(`Cannot find module ['"]${escapedSpecifier}['"]`),
66
+ new RegExp(`Could not resolve ['"]${escapedSpecifier}['"]`),
67
+ new RegExp(`Failed to load url\\s+(?:['"\`]${escapedSpecifier}['"\`]|${escapedSpecifier}(?=[\\s(]|$))`)
68
+ ].some((pattern) => pattern.test(message))) {
69
+ return true;
70
+ }
71
+ if ("cause" in error) {
72
+ return isModuleNotFoundError(error.cause, expectedSpecifier);
73
+ }
74
+ return false;
75
+ }
76
+ function normalizeDatabaseModuleLoadError(error, expectedSpecifier = "@holo-js/cache-db") {
77
+ if (isModuleNotFoundError(error, expectedSpecifier)) {
78
+ return new CacheOptionalPackageError(
79
+ "[@holo-js/cache] Database cache support requires @holo-js/cache-db to be installed.",
80
+ { cause: error }
81
+ );
82
+ }
83
+ return error;
84
+ }
85
+ async function loadDatabaseDriverModule() {
86
+ try {
87
+ const specifier = "@holo-js/cache-db";
88
+ return await import(specifier);
89
+ } catch (error) {
90
+ throw normalizeDatabaseModuleLoadError(error, "@holo-js/cache-db");
91
+ }
92
+ }
93
+ var databaseDriverModuleLoader = loadDatabaseDriverModule;
94
+ function setDatabaseDriverModuleLoader(loader) {
95
+ databaseDriverModuleLoader = loader;
96
+ }
97
+ function resetDatabaseDriverModuleLoader() {
98
+ databaseDriverModuleLoader = loadDatabaseDriverModule;
99
+ }
100
+ var LazyDatabaseCacheDriver = class {
101
+ constructor(options) {
102
+ this.options = options;
103
+ }
104
+ driver = "database";
105
+ driverInstance;
106
+ pending;
107
+ get name() {
108
+ return this.options.name;
109
+ }
110
+ async resolveDriver() {
111
+ if (this.driverInstance) return this.driverInstance;
112
+ this.pending ??= databaseDriverModuleLoader().then((module) => {
113
+ const driver = module.createDatabaseCacheDriver(this.options);
114
+ this.driverInstance = driver;
115
+ return driver;
116
+ }).finally(() => {
117
+ this.pending = void 0;
118
+ });
119
+ return this.pending;
120
+ }
121
+ async withDriver(callback) {
122
+ return callback(await this.resolveDriver());
123
+ }
124
+ createLockProxy(name, seconds) {
125
+ let lockPromise;
126
+ const resolveLock = async () => {
127
+ lockPromise ??= this.withDriver((driver) => driver.lock(name, seconds));
128
+ return lockPromise;
129
+ };
130
+ return {
131
+ name,
132
+ async get(callback) {
133
+ return (await resolveLock()).get(callback);
134
+ },
135
+ async release() {
136
+ return (await resolveLock()).release();
137
+ },
138
+ async block(waitSeconds, callback) {
139
+ return (await resolveLock()).block(waitSeconds, callback);
140
+ }
141
+ };
142
+ }
143
+ async get(key) {
144
+ return this.withDriver((driver) => driver.get(key));
145
+ }
146
+ async put(input) {
147
+ return this.withDriver((driver) => driver.put(input));
148
+ }
149
+ async add(input) {
150
+ return this.withDriver((driver) => driver.add(input));
151
+ }
152
+ async forget(key) {
153
+ return this.withDriver((driver) => driver.forget(key));
154
+ }
155
+ async flush() {
156
+ await this.withDriver((driver) => driver.flush());
157
+ }
158
+ async increment(key, amount) {
159
+ return this.withDriver((driver) => driver.increment(key, amount));
160
+ }
161
+ async decrement(key, amount) {
162
+ return this.withDriver((driver) => driver.decrement(key, amount));
163
+ }
164
+ lock(name, seconds) {
165
+ return this.createLockProxy(name, seconds);
166
+ }
167
+ };
168
+ function createDatabaseCacheDriver(options) {
169
+ return new LazyDatabaseCacheDriver(options);
170
+ }
171
+ var cacheDbInternals = {
172
+ isModuleNotFoundError,
173
+ isNormalizedDatabaseConfig,
174
+ loadDatabaseDriverModule,
175
+ normalizeDatabaseModuleLoadError,
176
+ normalizeRuntimeDatabaseConfig,
177
+ resolveSharedDatabaseConnection,
178
+ resetDatabaseDriverModuleLoader,
179
+ setDatabaseDriverModuleLoader
180
+ };
181
+
182
+ // src/file.ts
183
+ import { createHash, randomUUID } from "crypto";
184
+ import { mkdir, open, readFile, readdir, rename, rm, writeFile } from "fs/promises";
185
+ import { dirname, join, resolve } from "path";
186
+ var MALFORMED_FILE = /* @__PURE__ */ Symbol("MALFORMED_FILE");
187
+ function defaultSleep(milliseconds) {
188
+ return new Promise((resolveDelay) => {
189
+ setTimeout(resolveDelay, milliseconds);
190
+ });
191
+ }
192
+ function hashCacheKey(key) {
193
+ return createHash("sha256").update(key).digest("hex");
194
+ }
195
+ function resolveDriverRoot(path) {
196
+ return resolve(path);
197
+ }
198
+ function resolveEntryFilePath(rootPath, key) {
199
+ const hash = hashCacheKey(key);
200
+ return join(rootPath, "entries", hash.slice(0, 2), `${hash}.json`);
201
+ }
202
+ function resolveLockFilePath(rootPath, name) {
203
+ const hash = hashCacheKey(name);
204
+ return join(rootPath, "locks", hash.slice(0, 2), `${hash}.lock`);
205
+ }
206
+ function isPositiveTimestamp(value) {
207
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
208
+ }
209
+ function isFileCacheEntryEnvelope(value) {
210
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
211
+ return false;
212
+ }
213
+ const entry = value;
214
+ return typeof entry.key === "string" && typeof entry.payload === "string" && (typeof entry.expiresAt === "undefined" || isPositiveTimestamp(entry.expiresAt));
215
+ }
216
+ function isFileCacheLockEnvelope(value) {
217
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
218
+ return false;
219
+ }
220
+ const lock = value;
221
+ return typeof lock.name === "string" && typeof lock.owner === "string" && isPositiveTimestamp(lock.expiresAt);
222
+ }
223
+ async function ensureParentDirectory(filePath) {
224
+ await mkdir(dirname(filePath), { recursive: true });
225
+ }
226
+ async function removeFileIfPresent(filePath) {
227
+ await rm(filePath, { force: true });
228
+ }
229
+ async function readFileIfPresent(filePath) {
230
+ try {
231
+ return await readFile(filePath, "utf8");
232
+ } catch (error) {
233
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
234
+ return void 0;
235
+ }
236
+ throw error;
237
+ }
238
+ }
239
+ async function writeFileAtomically(filePath, contents) {
240
+ await ensureParentDirectory(filePath);
241
+ const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
242
+ await writeFile(temporaryPath, contents, "utf8");
243
+ try {
244
+ await rename(temporaryPath, filePath);
245
+ } catch (error) {
246
+ await removeFileIfPresent(temporaryPath);
247
+ throw error;
248
+ }
249
+ }
250
+ async function writeFileExclusively(filePath, contents) {
251
+ await ensureParentDirectory(filePath);
252
+ try {
253
+ const handle = await open(filePath, "wx");
254
+ try {
255
+ await handle.writeFile(contents, "utf8");
256
+ } finally {
257
+ await handle.close();
258
+ }
259
+ return true;
260
+ } catch (error) {
261
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
262
+ return false;
263
+ }
264
+ throw error;
265
+ }
266
+ }
267
+ async function readJsonFile(filePath) {
268
+ const contents = await readFileIfPresent(filePath);
269
+ if (typeof contents === "undefined") return void 0;
270
+ try {
271
+ return JSON.parse(contents);
272
+ } catch {
273
+ return MALFORMED_FILE;
274
+ }
275
+ }
276
+ async function listFiles(rootPath) {
277
+ try {
278
+ const entries = await readdir(rootPath, { withFileTypes: true });
279
+ const nested = await Promise.all(entries.map(async (entry) => {
280
+ const entryPath = join(rootPath, entry.name);
281
+ if (entry.isDirectory()) return listFiles(entryPath);
282
+ return [entryPath];
283
+ }));
284
+ return nested.flat();
285
+ } catch (error) {
286
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
287
+ return [];
288
+ }
289
+ throw error;
290
+ }
291
+ }
292
+ async function removeInvalidFileAndReadMissing(filePath, decoded) {
293
+ if (typeof decoded !== "undefined") {
294
+ await removeFileIfPresent(filePath);
295
+ }
296
+ return {
297
+ state: "missing",
298
+ filePath
299
+ };
300
+ }
301
+ async function removeScopedCacheFiles(rootPath, prefix, isEnvelope, resolveName) {
302
+ if (!prefix) {
303
+ await rm(rootPath, { recursive: true, force: true });
304
+ await mkdir(rootPath, { recursive: true });
305
+ return;
306
+ }
307
+ for (const filePath of await listFiles(rootPath)) {
308
+ const decoded = await readJsonFile(filePath);
309
+ if (!decoded || decoded === MALFORMED_FILE || !isEnvelope(decoded)) {
310
+ await removeFileIfPresent(filePath);
311
+ continue;
312
+ }
313
+ if (resolveName(decoded).startsWith(prefix)) {
314
+ await removeFileIfPresent(filePath);
315
+ }
316
+ }
317
+ }
318
+ async function readEntry(rootPath, key, now) {
319
+ const filePath = resolveEntryFilePath(rootPath, key);
320
+ const decoded = await readJsonFile(filePath);
321
+ if (decoded === MALFORMED_FILE || !isFileCacheEntryEnvelope(decoded) || decoded.key !== key) {
322
+ return removeInvalidFileAndReadMissing(filePath, decoded);
323
+ }
324
+ if (typeof decoded.expiresAt === "number" && decoded.expiresAt <= now) {
325
+ return removeInvalidFileAndReadMissing(filePath, decoded);
326
+ }
327
+ return {
328
+ state: "hit",
329
+ filePath,
330
+ value: decoded
331
+ };
332
+ }
333
+ async function readLock(rootPath, name, now) {
334
+ 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);
338
+ }
339
+ if (decoded.expiresAt <= now) {
340
+ return removeInvalidFileAndReadMissing(filePath, decoded);
341
+ }
342
+ return {
343
+ state: "hit",
344
+ filePath,
345
+ value: decoded
346
+ };
347
+ }
348
+ function createEntryEnvelope(input) {
349
+ return {
350
+ key: input.key,
351
+ payload: input.payload,
352
+ expiresAt: input.expiresAt
353
+ };
354
+ }
355
+ function createFileLock(rootPath, name, seconds, now, sleep, ownerFactory) {
356
+ const owner = ownerFactory();
357
+ async function tryAcquire() {
358
+ const filePath = resolveLockFilePath(rootPath, name);
359
+ const envelope = JSON.stringify({
360
+ name,
361
+ owner,
362
+ expiresAt: now() + seconds * 1e3
363
+ });
364
+ if (await writeFileExclusively(filePath, envelope)) {
365
+ return true;
366
+ }
367
+ const currentLock = await readLock(rootPath, name, now());
368
+ if (currentLock.state === "missing") {
369
+ return writeFileExclusively(filePath, envelope);
370
+ }
371
+ return false;
372
+ }
373
+ async function withCallback(callback) {
374
+ if (!callback) {
375
+ return true;
376
+ }
377
+ try {
378
+ return await callback();
379
+ } finally {
380
+ await lock.release();
381
+ }
382
+ }
383
+ const lock = {
384
+ name,
385
+ async get(callback) {
386
+ if (!await tryAcquire()) {
387
+ return false;
388
+ }
389
+ return withCallback(callback);
390
+ },
391
+ async release() {
392
+ const currentLock = await readLock(rootPath, name, now());
393
+ if (currentLock.state === "missing" || currentLock.value.owner !== owner) {
394
+ return false;
395
+ }
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);
401
+ return true;
402
+ },
403
+ async block(waitSeconds, callback) {
404
+ const deadline = now() + waitSeconds * 1e3;
405
+ while (true) {
406
+ if (await tryAcquire()) {
407
+ return withCallback(callback);
408
+ }
409
+ if (now() >= deadline) {
410
+ return false;
411
+ }
412
+ await sleep(10);
413
+ }
414
+ }
415
+ };
416
+ return lock;
417
+ }
418
+ function createFileCacheDriver(options) {
419
+ const rootPath = resolveDriverRoot(options.path);
420
+ const prefix = options.prefix ?? "";
421
+ const now = options.now ?? Date.now;
422
+ const sleep = options.sleep ?? defaultSleep;
423
+ const ownerFactory = options.ownerFactory ?? randomUUID;
424
+ const numericMutationLockSeconds = options.numericMutationLockSeconds ?? 5;
425
+ const numericMutationWaitSeconds = options.numericMutationWaitSeconds ?? 2;
426
+ async function writeEntry(input) {
427
+ const filePath = resolveEntryFilePath(rootPath, input.key);
428
+ await writeFileAtomically(filePath, JSON.stringify(createEntryEnvelope(input)));
429
+ return true;
430
+ }
431
+ async function mutateNumericValue(key, amount) {
432
+ const result = await createFileLock(
433
+ rootPath,
434
+ `__numeric__:${key}`,
435
+ numericMutationLockSeconds,
436
+ now,
437
+ sleep,
438
+ ownerFactory
439
+ ).block(numericMutationWaitSeconds, async () => {
440
+ const entry = await readEntry(rootPath, key, now());
441
+ const currentValue = entry.state === "hit" ? deserializeCacheValue(entry.value.payload) : 0;
442
+ if (typeof currentValue !== "number" || !Number.isFinite(currentValue)) {
443
+ throw new CacheInvalidNumericMutationError(`[@holo-js/cache] Cache key "${key}" does not contain a numeric value.`);
444
+ }
445
+ const nextValue = currentValue + amount;
446
+ await writeEntry({
447
+ key,
448
+ payload: serializeCacheValue(nextValue),
449
+ expiresAt: entry.state === "hit" ? entry.value.expiresAt : void 0
450
+ });
451
+ return nextValue;
452
+ });
453
+ if (result === false) {
454
+ throw new CacheLockAcquisitionError(`[@holo-js/cache] Could not acquire file cache mutation lock for "${key}".`);
455
+ }
456
+ if (result === true) {
457
+ throw new CacheLockAcquisitionError(`[@holo-js/cache] File cache mutation lock for "${key}" returned no numeric result.`);
458
+ }
459
+ return result;
460
+ }
461
+ return {
462
+ name: options.name,
463
+ driver: "file",
464
+ async get(key) {
465
+ const entry = await readEntry(rootPath, key, now());
466
+ if (entry.state === "missing") {
467
+ return Object.freeze({ hit: false });
468
+ }
469
+ return Object.freeze({
470
+ hit: true,
471
+ payload: entry.value.payload,
472
+ expiresAt: entry.value.expiresAt
473
+ });
474
+ },
475
+ async put(input) {
476
+ return writeEntry(input);
477
+ },
478
+ async add(input) {
479
+ const nowValue = now();
480
+ const filePath = resolveEntryFilePath(rootPath, input.key);
481
+ const existing = await readEntry(rootPath, input.key, nowValue);
482
+ if (existing.state === "hit") {
483
+ return false;
484
+ }
485
+ if (await writeFileExclusively(filePath, JSON.stringify(createEntryEnvelope(input)))) {
486
+ return true;
487
+ }
488
+ const afterCollision = await readEntry(rootPath, input.key, nowValue);
489
+ if (afterCollision.state === "hit") {
490
+ return false;
491
+ }
492
+ return writeFileExclusively(filePath, JSON.stringify(createEntryEnvelope(input)));
493
+ },
494
+ async forget(key) {
495
+ const filePath = resolveEntryFilePath(rootPath, key);
496
+ const contents = await readFileIfPresent(filePath);
497
+ if (typeof contents === "undefined") {
498
+ return false;
499
+ }
500
+ await removeFileIfPresent(filePath);
501
+ return true;
502
+ },
503
+ async flush() {
504
+ await removeScopedCacheFiles(
505
+ join(rootPath, "entries"),
506
+ prefix,
507
+ isFileCacheEntryEnvelope,
508
+ (entry) => entry.key
509
+ );
510
+ await removeScopedCacheFiles(
511
+ join(rootPath, "locks"),
512
+ prefix,
513
+ isFileCacheLockEnvelope,
514
+ (lock) => lock.name
515
+ );
516
+ },
517
+ async increment(key, amount) {
518
+ return mutateNumericValue(key, amount);
519
+ },
520
+ async decrement(key, amount) {
521
+ return mutateNumericValue(key, -amount);
522
+ },
523
+ lock(name, seconds) {
524
+ return createFileLock(rootPath, name, seconds, now, sleep, ownerFactory);
525
+ }
526
+ };
527
+ }
528
+ var fileDriverInternals = {
529
+ hashCacheKey,
530
+ isFileCacheEntryEnvelope,
531
+ isFileCacheLockEnvelope,
532
+ resolveDriverRoot,
533
+ resolveEntryFilePath,
534
+ resolveLockFilePath
535
+ };
536
+
537
+ // src/memory.ts
538
+ function createMemoryCacheDriverState() {
539
+ return {
540
+ entries: /* @__PURE__ */ new Map(),
541
+ locks: /* @__PURE__ */ new Map()
542
+ };
543
+ }
544
+ function defaultSleep2(milliseconds) {
545
+ return new Promise((resolve2) => {
546
+ setTimeout(resolve2, milliseconds);
547
+ });
548
+ }
549
+ function createMemoryLock(state, name, seconds, now, sleep) {
550
+ const owner = Symbol(name);
551
+ function clearExpiredLock() {
552
+ const current = state.locks.get(name);
553
+ if (current && current.expiresAt <= now()) {
554
+ state.locks.delete(name);
555
+ }
556
+ }
557
+ function tryAcquire() {
558
+ clearExpiredLock();
559
+ const current = state.locks.get(name);
560
+ if (current) {
561
+ return false;
562
+ }
563
+ state.locks.set(name, {
564
+ owner,
565
+ expiresAt: now() + seconds * 1e3
566
+ });
567
+ return true;
568
+ }
569
+ async function withCallback(callback) {
570
+ if (!callback) {
571
+ return true;
572
+ }
573
+ try {
574
+ return await callback();
575
+ } finally {
576
+ await lock.release();
577
+ }
578
+ }
579
+ const lock = {
580
+ name,
581
+ async get(callback) {
582
+ if (!tryAcquire()) {
583
+ return false;
584
+ }
585
+ return withCallback(callback);
586
+ },
587
+ async release() {
588
+ clearExpiredLock();
589
+ const current = state.locks.get(name);
590
+ if (!current || current.owner !== owner) {
591
+ return false;
592
+ }
593
+ state.locks.delete(name);
594
+ return true;
595
+ },
596
+ async block(waitSeconds, callback) {
597
+ const deadline = now() + waitSeconds * 1e3;
598
+ while (true) {
599
+ if (tryAcquire()) {
600
+ return withCallback(callback);
601
+ }
602
+ if (now() >= deadline) {
603
+ return false;
604
+ }
605
+ await sleep(10);
606
+ }
607
+ }
608
+ };
609
+ return lock;
610
+ }
611
+ function isExpired(entry, timestamp) {
612
+ return typeof entry.expiresAt === "number" && entry.expiresAt <= timestamp;
613
+ }
614
+ function pruneExpiredEntry(state, key, timestamp) {
615
+ const entry = state.entries.get(key);
616
+ if (!entry) {
617
+ return void 0;
618
+ }
619
+ if (isExpired(entry, timestamp)) {
620
+ state.entries.delete(key);
621
+ return void 0;
622
+ }
623
+ return entry;
624
+ }
625
+ function pruneExpiredEntries(state, timestamp) {
626
+ for (const [key, entry] of state.entries.entries()) {
627
+ if (isExpired(entry, timestamp)) {
628
+ state.entries.delete(key);
629
+ }
630
+ }
631
+ }
632
+ function setEntry(state, input) {
633
+ state.entries.set(input.key, {
634
+ payload: input.payload,
635
+ expiresAt: input.expiresAt
636
+ });
637
+ }
638
+ function enforceMaxEntries(state, maxEntries, timestamp) {
639
+ if (typeof maxEntries === "undefined") {
640
+ return;
641
+ }
642
+ pruneExpiredEntries(state, timestamp);
643
+ while (state.entries.size > maxEntries) {
644
+ const oldestKey = state.entries.keys().next().value;
645
+ if (typeof oldestKey === "undefined") {
646
+ break;
647
+ }
648
+ state.entries.delete(oldestKey);
649
+ }
650
+ }
651
+ function createMemoryCacheDriver(options) {
652
+ const state = createMemoryCacheDriverState();
653
+ const now = options.now ?? Date.now;
654
+ const sleep = options.sleep ?? defaultSleep2;
655
+ return {
656
+ name: options.name,
657
+ driver: "memory",
658
+ async get(key) {
659
+ const entry = pruneExpiredEntry(state, key, now());
660
+ if (!entry) return Object.freeze({ hit: false });
661
+ return Object.freeze({
662
+ hit: true,
663
+ payload: entry.payload,
664
+ expiresAt: entry.expiresAt
665
+ });
666
+ },
667
+ async put(input) {
668
+ setEntry(state, input);
669
+ enforceMaxEntries(state, options.maxEntries, now());
670
+ return true;
671
+ },
672
+ async add(input) {
673
+ const timestamp = now();
674
+ if (pruneExpiredEntry(state, input.key, timestamp)) return false;
675
+ setEntry(state, input);
676
+ enforceMaxEntries(state, options.maxEntries, timestamp);
677
+ return true;
678
+ },
679
+ async forget(key) {
680
+ const existed = typeof pruneExpiredEntry(state, key, now()) !== "undefined";
681
+ state.entries.delete(key);
682
+ return existed;
683
+ },
684
+ async flush() {
685
+ state.entries.clear();
686
+ state.locks.clear();
687
+ },
688
+ async increment(key, amount) {
689
+ const entry = pruneExpiredEntry(state, key, now());
690
+ const currentValue = entry ? deserializeCacheValue(entry.payload) : 0;
691
+ if (typeof currentValue !== "number" || !Number.isFinite(currentValue)) {
692
+ throw new CacheInvalidNumericMutationError(`[@holo-js/cache] Cache key "${key}" does not contain a numeric value.`);
693
+ }
694
+ const nextValue = currentValue + amount;
695
+ setEntry(state, {
696
+ key,
697
+ payload: serializeCacheValue(nextValue),
698
+ expiresAt: entry?.expiresAt
699
+ });
700
+ enforceMaxEntries(state, options.maxEntries, now());
701
+ return nextValue;
702
+ },
703
+ async decrement(key, amount) {
704
+ return this.increment(key, -amount);
705
+ },
706
+ lock(name, seconds) {
707
+ return createMemoryLock(state, name, seconds, now, sleep);
708
+ }
709
+ };
710
+ }
711
+
712
+ // src/redis.ts
713
+ import {
714
+ normalizeRedisConfig
715
+ } from "@holo-js/config";
716
+ function isNormalizedRedisConfig(config) {
717
+ return typeof config.default === "string" && typeof config.connections === "object" && config.connections !== null && Object.values(config.connections).every((connection) => {
718
+ 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";
719
+ });
720
+ }
721
+ function normalizeRuntimeRedisConfig(config) {
722
+ if (!config) return void 0;
723
+ return isNormalizedRedisConfig(config) ? config : normalizeRedisConfig(config);
724
+ }
725
+ function resolveSharedRedisConnection(redisConfig, connectionName) {
726
+ if (!redisConfig) {
727
+ throw new CacheDriverResolutionError(
728
+ `[@holo-js/cache] Redis cache driver "${connectionName}" requires a top-level redis config from config/redis.ts.`
729
+ );
730
+ }
731
+ const connection = redisConfig.connections[connectionName];
732
+ if (connection) return connection;
733
+ const availableConnections = Object.keys(redisConfig.connections);
734
+ throw new CacheDriverResolutionError(
735
+ `[@holo-js/cache] Redis cache connection "${connectionName}" was not found in config/redis.ts. Available connections: ${availableConnections.join(", ") || "(none)"}.`
736
+ );
737
+ }
738
+ function isModuleNotFoundError2(error) {
739
+ return !!error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND";
740
+ }
741
+ function normalizeRedisModuleLoadError(error) {
742
+ if (isModuleNotFoundError2(error)) {
743
+ return new CacheOptionalPackageError(
744
+ "[@holo-js/cache] Redis cache support requires @holo-js/cache-redis to be installed.",
745
+ { cause: error }
746
+ );
747
+ }
748
+ return error;
749
+ }
750
+ async function loadRedisDriverModule() {
751
+ try {
752
+ const specifier = "@holo-js/cache-redis";
753
+ return await import(specifier);
754
+ } catch (error) {
755
+ throw normalizeRedisModuleLoadError(error);
756
+ }
757
+ }
758
+ var redisDriverModuleLoader = loadRedisDriverModule;
759
+ function setRedisDriverModuleLoader(loader) {
760
+ redisDriverModuleLoader = loader;
761
+ }
762
+ function resetRedisDriverModuleLoader() {
763
+ redisDriverModuleLoader = loadRedisDriverModule;
764
+ }
765
+ var LazyRedisCacheDriver = class {
766
+ constructor(options) {
767
+ this.options = options;
768
+ }
769
+ driver = "redis";
770
+ driverInstance;
771
+ pending;
772
+ get name() {
773
+ return this.options.name;
774
+ }
775
+ async resolveDriver() {
776
+ if (this.driverInstance) return this.driverInstance;
777
+ this.pending ??= redisDriverModuleLoader().then((module) => {
778
+ const driver = module.createRedisCacheDriver(this.options);
779
+ this.driverInstance = driver;
780
+ return driver;
781
+ }).finally(() => {
782
+ this.pending = void 0;
783
+ });
784
+ return this.pending;
785
+ }
786
+ async withDriver(callback) {
787
+ return callback(await this.resolveDriver());
788
+ }
789
+ createLockProxy(name, seconds) {
790
+ let lockPromise;
791
+ const resolveLock = async () => {
792
+ lockPromise ??= this.withDriver((driver) => driver.lock(name, seconds));
793
+ return lockPromise;
794
+ };
795
+ return {
796
+ name,
797
+ async get(callback) {
798
+ return (await resolveLock()).get(callback);
799
+ },
800
+ async release() {
801
+ return (await resolveLock()).release();
802
+ },
803
+ async block(waitSeconds, callback) {
804
+ return (await resolveLock()).block(waitSeconds, callback);
805
+ }
806
+ };
807
+ }
808
+ async get(key) {
809
+ return this.withDriver((driver) => driver.get(key));
810
+ }
811
+ async put(input) {
812
+ return this.withDriver((driver) => driver.put(input));
813
+ }
814
+ async add(input) {
815
+ return this.withDriver((driver) => driver.add(input));
816
+ }
817
+ async forget(key) {
818
+ return this.withDriver((driver) => driver.forget(key));
819
+ }
820
+ async flush() {
821
+ await this.withDriver((driver) => driver.flush());
822
+ }
823
+ async increment(key, amount) {
824
+ return this.withDriver((driver) => driver.increment(key, amount));
825
+ }
826
+ async decrement(key, amount) {
827
+ return this.withDriver((driver) => driver.decrement(key, amount));
828
+ }
829
+ lock(name, seconds) {
830
+ return this.createLockProxy(name, seconds);
831
+ }
832
+ };
833
+ function createRedisCacheDriver(options) {
834
+ return new LazyRedisCacheDriver(options);
835
+ }
836
+ var cacheRedisInternals = {
837
+ isModuleNotFoundError: isModuleNotFoundError2,
838
+ isNormalizedRedisConfig,
839
+ loadRedisDriverModule,
840
+ normalizeRedisModuleLoadError,
841
+ normalizeRuntimeRedisConfig,
842
+ resolveSharedRedisConnection,
843
+ resetRedisDriverModuleLoader,
844
+ setRedisDriverModuleLoader
845
+ };
846
+
847
+ // src/runtime-shared.ts
848
+ function isNormalizedCacheConfig(config) {
849
+ return typeof config.default === "string" && typeof config.prefix === "string" && typeof config.drivers === "object" && config.drivers !== null && Object.values(config.drivers).every((driver) => {
850
+ return typeof driver === "object" && driver !== null && "name" in driver && "prefix" in driver && typeof driver.name === "string" && typeof driver.prefix === "string";
851
+ });
852
+ }
853
+ function normalizeRuntimeConfig(config) {
854
+ if (!config) return holoCacheDefaults;
855
+ return isNormalizedCacheConfig(config) ? config : normalizeCacheConfig(config);
856
+ }
857
+ function getCacheRuntimeState() {
858
+ const runtime = globalThis;
859
+ runtime.__holoCacheRuntime__ ??= {};
860
+ return runtime.__holoCacheRuntime__;
861
+ }
862
+ function getCacheRuntimeBindings() {
863
+ return getCacheRuntimeState().bindings;
864
+ }
865
+ function getCacheRuntime() {
866
+ const bindings = getCacheRuntimeBindings();
867
+ if (!bindings) {
868
+ throw new CacheRuntimeNotConfiguredError();
869
+ }
870
+ return bindings;
871
+ }
872
+ function createDriverMap(drivers) {
873
+ return drivers ? new Map(drivers.entries()) : /* @__PURE__ */ new Map();
874
+ }
875
+ function cacheResolvedDriver(facade, driverName, driver) {
876
+ facade.drivers.set(driverName, driver);
877
+ return driver;
878
+ }
879
+ function resolveConfiguredDriver(facade, requestedName) {
880
+ const driverName = requestedName?.trim() || facade.config.default;
881
+ const cachedDriver = facade.drivers.get(driverName);
882
+ if (cachedDriver) {
883
+ return cachedDriver;
884
+ }
885
+ const driverConfig = facade.config.drivers[driverName];
886
+ if (!driverConfig) {
887
+ throw new CacheDriverResolutionError(`[@holo-js/cache] Cache driver "${driverName}" is not configured.`);
888
+ }
889
+ switch (driverConfig.driver) {
890
+ case "file": {
891
+ return cacheResolvedDriver(facade, driverName, createFileCacheDriver({
892
+ name: driverConfig.name,
893
+ path: driverConfig.path,
894
+ prefix: driverConfig.prefix
895
+ }));
896
+ }
897
+ case "memory": {
898
+ return cacheResolvedDriver(facade, driverName, createMemoryCacheDriver({
899
+ name: driverConfig.name,
900
+ maxEntries: driverConfig.maxEntries
901
+ }));
902
+ }
903
+ case "redis": {
904
+ const connection = cacheRedisInternals.resolveSharedRedisConnection(
905
+ facade.redisConfig,
906
+ driverConfig.connection
907
+ );
908
+ return cacheResolvedDriver(facade, driverName, createRedisCacheDriver({
909
+ name: driverConfig.name,
910
+ connectionName: connection.name,
911
+ prefix: driverConfig.prefix,
912
+ redis: connection
913
+ }));
914
+ }
915
+ case "database": {
916
+ const connection = cacheDbInternals.resolveSharedDatabaseConnection(
917
+ facade.databaseConfig,
918
+ driverConfig.connection
919
+ );
920
+ return cacheResolvedDriver(facade, driverName, createDatabaseCacheDriver({
921
+ name: driverConfig.name,
922
+ connectionName: driverConfig.connection,
923
+ table: driverConfig.table,
924
+ lockTable: driverConfig.lockTable,
925
+ prefix: driverConfig.prefix,
926
+ connection
927
+ }));
928
+ }
929
+ default:
930
+ throw new CacheDriverResolutionError(
931
+ `[@holo-js/cache] Cache driver "${driverName}" uses unsupported driver "${String(driverConfig.driver)}" in this phase.`
932
+ );
933
+ }
934
+ }
935
+
936
+ // src/query-bridge.ts
937
+ function createDependencyIndexState() {
938
+ return {
939
+ keyToDependencies: /* @__PURE__ */ new Map(),
940
+ dependencyToKeys: /* @__PURE__ */ new Map()
941
+ };
942
+ }
943
+ function createMemoryDependencyIndex(state = createDependencyIndexState()) {
944
+ return Object.freeze({
945
+ async register(key, dependencies) {
946
+ await this.removeKey(key);
947
+ if (dependencies.length === 0) {
948
+ return;
949
+ }
950
+ const uniqueDependencies = new Set(dependencies);
951
+ state.keyToDependencies.set(key, uniqueDependencies);
952
+ for (const dependency of uniqueDependencies) {
953
+ const keys = state.dependencyToKeys.get(dependency) ?? /* @__PURE__ */ new Set();
954
+ keys.add(key);
955
+ state.dependencyToKeys.set(dependency, keys);
956
+ }
957
+ },
958
+ async listKeys(dependency) {
959
+ return Object.freeze([...state.dependencyToKeys.get(dependency) ?? /* @__PURE__ */ new Set()]);
960
+ },
961
+ async listRegisteredKeys() {
962
+ return Object.freeze([...state.keyToDependencies.keys()]);
963
+ },
964
+ async removeKey(key) {
965
+ const dependencies = state.keyToDependencies.get(key);
966
+ if (!dependencies) {
967
+ return;
968
+ }
969
+ state.keyToDependencies.delete(key);
970
+ for (const dependency of dependencies) {
971
+ const keys = state.dependencyToKeys.get(dependency);
972
+ if (!keys) {
973
+ continue;
974
+ }
975
+ keys.delete(key);
976
+ if (keys.size === 0) {
977
+ state.dependencyToKeys.delete(dependency);
978
+ }
979
+ }
980
+ },
981
+ async clear() {
982
+ state.keyToDependencies.clear();
983
+ state.dependencyToKeys.clear();
984
+ }
985
+ });
986
+ }
987
+ function getQueryBridgeState() {
988
+ const runtime = globalThis;
989
+ runtime.__holoCacheQueryBridge__ ??= {};
990
+ return runtime.__holoCacheQueryBridge__;
991
+ }
992
+ function getOrCreateDependencyIndex() {
993
+ const state = getQueryBridgeState();
994
+ state.dependencyIndex ??= createMemoryDependencyIndex();
995
+ return state.dependencyIndex;
996
+ }
997
+ function resetDefaultDependencyIndex() {
998
+ getQueryBridgeState().dependencyIndex = void 0;
999
+ }
1000
+ function resolveDriverContext(driverName) {
1001
+ const runtime = getCacheRuntime();
1002
+ const configuredDriverName = driverName?.trim() || runtime.config.default;
1003
+ const driverConfig = runtime.config.drivers[configuredDriverName];
1004
+ let normalizedKeyPrefix = runtime.config.prefix;
1005
+ if (typeof driverConfig?.prefix === "string") {
1006
+ normalizedKeyPrefix = driverConfig.prefix;
1007
+ }
1008
+ return Object.freeze({
1009
+ driverName: configuredDriverName,
1010
+ driver: resolveConfiguredDriver(runtime, configuredDriverName),
1011
+ normalizedKeyPrefix
1012
+ });
1013
+ }
1014
+ function resolveNormalizedKey(key, driverName) {
1015
+ const context = resolveDriverContext(driverName);
1016
+ return `${context.normalizedKeyPrefix}${resolveCacheKey(key)}`;
1017
+ }
1018
+ function normalizeFlexibleTtl(ttl) {
1019
+ const freshSeconds = "fresh" in ttl ? ttl.fresh : ttl[0];
1020
+ const staleSeconds = "stale" in ttl ? ttl.stale : ttl[1];
1021
+ if (!Number.isInteger(freshSeconds) || freshSeconds < 0) {
1022
+ throw new CacheInvalidTtlError("[@holo-js/cache] Flexible fresh TTL must be an integer greater than or equal to 0.");
1023
+ }
1024
+ if (!Number.isInteger(staleSeconds) || staleSeconds < freshSeconds) {
1025
+ throw new CacheInvalidTtlError("[@holo-js/cache] Flexible stale TTL must be an integer greater than or equal to the fresh TTL.");
1026
+ }
1027
+ return Object.freeze({
1028
+ freshSeconds,
1029
+ staleSeconds
1030
+ });
1031
+ }
1032
+ function isFlexibleEnvelope(value) {
1033
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1034
+ return false;
1035
+ }
1036
+ const envelope = value;
1037
+ return envelope.__holo_cache_flexible === true && typeof envelope.freshUntil === "number" && Number.isFinite(envelope.freshUntil) && typeof envelope.staleUntil === "number" && Number.isFinite(envelope.staleUntil) && "value" in envelope;
1038
+ }
1039
+ async function getCachedValue(key, driverName) {
1040
+ const context = resolveDriverContext(driverName);
1041
+ const entry = await context.driver.get(resolveNormalizedKey(key, driverName));
1042
+ if (!entry.hit || typeof entry.payload !== "string") {
1043
+ return null;
1044
+ }
1045
+ return deserializeCacheValue(entry.payload);
1046
+ }
1047
+ async function putCachedValue(key, value, ttl, driverName) {
1048
+ const context = resolveDriverContext(driverName);
1049
+ const expiresAt = typeof ttl === "undefined" ? void 0 : normalizeCacheTtl(ttl).expiresAt;
1050
+ await context.driver.put({
1051
+ key: resolveNormalizedKey(key, driverName),
1052
+ payload: serializeCacheValue(value),
1053
+ expiresAt
1054
+ });
1055
+ }
1056
+ function createFlexibleLock(key, ttl, driverName) {
1057
+ const context = resolveDriverContext(driverName);
1058
+ return context.driver.lock(
1059
+ `${context.normalizedKeyPrefix}__flexible__:${resolveCacheKey(key)}`,
1060
+ Math.max(1, ttl.staleSeconds)
1061
+ );
1062
+ }
1063
+ function createIndexedKey(key, driverName) {
1064
+ const context = resolveDriverContext(driverName);
1065
+ return `${context.driverName}\0${resolveNormalizedKey(key, driverName)}`;
1066
+ }
1067
+ function parseIndexedKey(indexedKey) {
1068
+ const delimiterIndex = indexedKey.indexOf("\0");
1069
+ if (delimiterIndex === -1) {
1070
+ return Object.freeze({
1071
+ driverName: getCacheRuntime().config.default,
1072
+ normalizedKey: indexedKey
1073
+ });
1074
+ }
1075
+ return Object.freeze({
1076
+ driverName: indexedKey.slice(0, delimiterIndex),
1077
+ normalizedKey: indexedKey.slice(delimiterIndex + 1)
1078
+ });
1079
+ }
1080
+ function setGlobalDatabaseQueryCacheBridge(bridge) {
1081
+ const runtime = globalThis;
1082
+ runtime.__holoDbQueryCacheBridge__ ??= {};
1083
+ runtime.__holoDbQueryCacheBridge__.bridge = bridge;
1084
+ }
1085
+ function createCacheQueryBridge(dependencyIndex = getOrCreateDependencyIndex()) {
1086
+ async function syncDependencies(indexedKey, dependencies) {
1087
+ if (dependencies && dependencies.length > 0) {
1088
+ await dependencyIndex.register(indexedKey, dependencies);
1089
+ return;
1090
+ }
1091
+ await dependencyIndex.removeKey(indexedKey);
1092
+ }
1093
+ return Object.freeze({
1094
+ async get(key, options) {
1095
+ return getCachedValue(key, options?.driver);
1096
+ },
1097
+ async put(key, value, options) {
1098
+ const indexedKey = createIndexedKey(key, options.driver);
1099
+ const resolvedTtl = typeof options.flexible === "undefined" ? options.ttl : normalizeFlexibleTtl(options.flexible).staleSeconds;
1100
+ await putCachedValue(key, value, resolvedTtl, options.driver);
1101
+ await syncDependencies(indexedKey, options.dependencies);
1102
+ },
1103
+ async flexible(key, ttl, callback, options = {}) {
1104
+ const indexedKey = createIndexedKey(key, options.driver);
1105
+ const normalizedTtl = normalizeFlexibleTtl(ttl);
1106
+ const now = Date.now();
1107
+ const cached = await getCachedValue(key, options.driver);
1108
+ const refreshValue = async () => {
1109
+ const value = await callback();
1110
+ const refreshedAt = Date.now();
1111
+ const envelope = {
1112
+ __holo_cache_flexible: true,
1113
+ value,
1114
+ freshUntil: refreshedAt + normalizedTtl.freshSeconds * 1e3,
1115
+ staleUntil: refreshedAt + normalizedTtl.staleSeconds * 1e3
1116
+ };
1117
+ await putCachedValue(
1118
+ key,
1119
+ envelope,
1120
+ normalizedTtl.staleSeconds,
1121
+ options.driver
1122
+ );
1123
+ await syncDependencies(indexedKey, options.dependencies);
1124
+ return value;
1125
+ };
1126
+ if (isFlexibleEnvelope(cached)) {
1127
+ if (now <= cached.freshUntil) {
1128
+ return cached.value;
1129
+ }
1130
+ if (now <= cached.staleUntil) {
1131
+ const refreshLock2 = createFlexibleLock(key, normalizedTtl, options.driver);
1132
+ void refreshLock2.get(async () => {
1133
+ await refreshValue();
1134
+ return true;
1135
+ }).catch(() => void 0);
1136
+ return cached.value;
1137
+ }
1138
+ }
1139
+ const refreshLock = createFlexibleLock(key, normalizedTtl, options.driver);
1140
+ const refreshed = await refreshLock.block(1, async () => refreshValue());
1141
+ if (refreshed !== false) {
1142
+ return refreshed;
1143
+ }
1144
+ const retried = await getCachedValue(key, options.driver);
1145
+ if (isFlexibleEnvelope(retried)) {
1146
+ if (Date.now() <= retried.staleUntil) {
1147
+ return retried.value;
1148
+ }
1149
+ }
1150
+ return refreshValue();
1151
+ },
1152
+ async forget(key, options) {
1153
+ const indexedKey = createIndexedKey(key, options?.driver);
1154
+ const context = resolveDriverContext(options?.driver);
1155
+ await dependencyIndex.removeKey(indexedKey);
1156
+ return context.driver.forget(resolveNormalizedKey(key, options?.driver));
1157
+ },
1158
+ async invalidateDependencies(dependencies) {
1159
+ const invalidatedKeys = /* @__PURE__ */ new Set();
1160
+ const runtime = getCacheRuntime();
1161
+ for (const dependency of dependencies) {
1162
+ const indexedKeys = await dependencyIndex.listKeys(dependency);
1163
+ for (const indexedKey of indexedKeys) {
1164
+ if (invalidatedKeys.has(indexedKey)) {
1165
+ continue;
1166
+ }
1167
+ invalidatedKeys.add(indexedKey);
1168
+ const parsed = parseIndexedKey(indexedKey);
1169
+ const driver = resolveConfiguredDriver(runtime, parsed.driverName);
1170
+ await driver.forget(parsed.normalizedKey);
1171
+ await dependencyIndex.removeKey(indexedKey);
1172
+ }
1173
+ }
1174
+ }
1175
+ });
1176
+ }
1177
+ var cacheQueryBridgeInternals = {
1178
+ createCacheQueryBridge,
1179
+ createIndexedKey,
1180
+ createMemoryDependencyIndex,
1181
+ getOrCreateDependencyIndex,
1182
+ parseIndexedKey,
1183
+ resetDefaultDependencyIndex,
1184
+ setGlobalDatabaseQueryCacheBridge
1185
+ };
1186
+
1187
+ // src/runtime.ts
1188
+ function configureCacheRuntime(bindings) {
1189
+ if (!bindings) {
1190
+ getCacheRuntimeState().bindings = void 0;
1191
+ resetDefaultDependencyIndex();
1192
+ setGlobalDatabaseQueryCacheBridge(void 0);
1193
+ return;
1194
+ }
1195
+ const dependencyIndex = bindings.dependencyIndex ?? getOrCreateDependencyIndex();
1196
+ const queryBridge = bindings.queryBridge ?? createCacheQueryBridge(dependencyIndex);
1197
+ getCacheRuntimeState().bindings = Object.freeze({
1198
+ config: normalizeRuntimeConfig(bindings.config),
1199
+ databaseConfig: cacheDbInternals.normalizeRuntimeDatabaseConfig(bindings.databaseConfig),
1200
+ redisConfig: cacheRedisInternals.normalizeRuntimeRedisConfig(bindings.redisConfig),
1201
+ drivers: createDriverMap(bindings.drivers),
1202
+ dependencyIndex,
1203
+ queryBridge
1204
+ });
1205
+ setGlobalDatabaseQueryCacheBridge(queryBridge);
1206
+ }
1207
+ function resetCacheRuntime() {
1208
+ getCacheRuntimeState().bindings = void 0;
1209
+ resetDefaultDependencyIndex();
1210
+ setGlobalDatabaseQueryCacheBridge(void 0);
1211
+ }
1212
+ var cacheRuntimeInternals = {
1213
+ getCacheRuntimeState,
1214
+ isNormalizedCacheConfig,
1215
+ normalizeRuntimeConfig,
1216
+ resolveConfiguredDriver
1217
+ };
1218
+
1219
+ // src/facade.ts
1220
+ var MAX_REFRESH_BLOCK_SECONDS = 30;
1221
+ function resolveFallback(fallback) {
1222
+ return typeof fallback === "function" ? fallback() : fallback;
1223
+ }
1224
+ function resolveValue(callback) {
1225
+ return Promise.resolve(callback());
1226
+ }
1227
+ function resolveDriverKey(driverName) {
1228
+ const normalized = driverName?.trim();
1229
+ return normalized || "__default__";
1230
+ }
1231
+ function normalizeFlexibleTtl2(ttl) {
1232
+ const freshSeconds = "fresh" in ttl ? ttl.fresh : ttl[0];
1233
+ const staleSeconds = "stale" in ttl ? ttl.stale : ttl[1];
1234
+ if (!Number.isInteger(freshSeconds) || freshSeconds < 0) {
1235
+ throw new CacheInvalidTtlError("[@holo-js/cache] Flexible fresh TTL must be an integer greater than or equal to 0.");
1236
+ }
1237
+ if (!Number.isInteger(staleSeconds) || staleSeconds < freshSeconds) {
1238
+ throw new CacheInvalidTtlError("[@holo-js/cache] Flexible stale TTL must be an integer greater than or equal to the fresh TTL.");
1239
+ }
1240
+ return Object.freeze({
1241
+ freshSeconds,
1242
+ staleSeconds
1243
+ });
1244
+ }
1245
+ function isFlexibleEnvelope2(value) {
1246
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1247
+ return false;
1248
+ }
1249
+ const envelope = value;
1250
+ return envelope.__holo_cache_flexible === true && typeof envelope.freshUntil === "number" && Number.isFinite(envelope.freshUntil) && typeof envelope.staleUntil === "number" && Number.isFinite(envelope.staleUntil) && "value" in envelope;
1251
+ }
1252
+ function createCacheRepository(driverName) {
1253
+ function resolveDriverContext2() {
1254
+ const runtime = getCacheRuntime();
1255
+ const configuredDriverName = driverName?.trim() || runtime.config.default;
1256
+ const driver = cacheRuntimeInternals.resolveConfiguredDriver(runtime, configuredDriverName);
1257
+ const config = runtime.config.drivers[configuredDriverName];
1258
+ return {
1259
+ configuredDriverName,
1260
+ driver,
1261
+ prefix: config?.prefix ?? runtime.config.prefix
1262
+ };
1263
+ }
1264
+ function resolveNormalizedKey2(key) {
1265
+ const { prefix } = resolveDriverContext2();
1266
+ return `${prefix}${resolveCacheKey(key)}`;
1267
+ }
1268
+ function resolveNormalizedLockName(name) {
1269
+ const { prefix } = resolveDriverContext2();
1270
+ return `${prefix}${resolveCacheKey(name)}`;
1271
+ }
1272
+ async function getEntryPayload(key) {
1273
+ const { driver } = resolveDriverContext2();
1274
+ const entry = await driver.get(resolveNormalizedKey2(key));
1275
+ return entry.hit ? entry.payload : void 0;
1276
+ }
1277
+ async function putSerializedValue(key, payload, ttl) {
1278
+ const { driver } = resolveDriverContext2();
1279
+ const normalizedTtl = normalizeCacheTtl(ttl);
1280
+ return driver.put({
1281
+ key: resolveNormalizedKey2(key),
1282
+ payload,
1283
+ expiresAt: normalizedTtl.expiresAt
1284
+ });
1285
+ }
1286
+ async function getCachedValue2(key) {
1287
+ const payload = await getEntryPayload(key);
1288
+ return typeof payload === "string" ? deserializeCacheValue(payload) : null;
1289
+ }
1290
+ async function putFlexibleEnvelope(key, ttl, value) {
1291
+ const now = Date.now();
1292
+ const envelope = {
1293
+ __holo_cache_flexible: true,
1294
+ value,
1295
+ freshUntil: now + ttl.freshSeconds * 1e3,
1296
+ staleUntil: now + ttl.staleSeconds * 1e3
1297
+ };
1298
+ await putSerializedValue(key, serializeCacheValue(envelope), ttl.staleSeconds);
1299
+ return value;
1300
+ }
1301
+ async function refreshFlexibleValue(key, ttl, callback) {
1302
+ const value = await resolveValue(callback);
1303
+ return putFlexibleEnvelope(key, ttl, value);
1304
+ }
1305
+ function createRefreshLock(key, staleSeconds) {
1306
+ return repository.lock(`__flexible__:${resolveCacheKey(key)}`, Math.max(1, staleSeconds));
1307
+ }
1308
+ const repository = Object.freeze({
1309
+ async get(key, fallback) {
1310
+ const payload = await getEntryPayload(key);
1311
+ if (typeof payload === "string") {
1312
+ return deserializeCacheValue(payload);
1313
+ }
1314
+ if (typeof fallback === "undefined") {
1315
+ return null;
1316
+ }
1317
+ return await resolveFallback(fallback);
1318
+ },
1319
+ async put(key, value, ttl) {
1320
+ return putSerializedValue(key, serializeCacheValue(value), ttl);
1321
+ },
1322
+ async add(key, value, ttl) {
1323
+ const { driver } = resolveDriverContext2();
1324
+ const normalizedTtl = normalizeCacheTtl(ttl);
1325
+ return driver.add({
1326
+ key: resolveNormalizedKey2(key),
1327
+ payload: serializeCacheValue(value),
1328
+ expiresAt: normalizedTtl.expiresAt
1329
+ });
1330
+ },
1331
+ async forever(key, value) {
1332
+ const { driver } = resolveDriverContext2();
1333
+ return driver.put({
1334
+ key: resolveNormalizedKey2(key),
1335
+ payload: serializeCacheValue(value)
1336
+ });
1337
+ },
1338
+ async has(key) {
1339
+ return typeof await getEntryPayload(key) === "string";
1340
+ },
1341
+ async missing(key) {
1342
+ return !await this.has(key);
1343
+ },
1344
+ async forget(key) {
1345
+ const runtime = getCacheRuntime();
1346
+ const { configuredDriverName, driver } = resolveDriverContext2();
1347
+ const forgotten = await driver.forget(resolveNormalizedKey2(key));
1348
+ const dependencyIndex = runtime.dependencyIndex;
1349
+ if (!dependencyIndex) {
1350
+ return forgotten;
1351
+ }
1352
+ await dependencyIndex.removeKey(cacheQueryBridgeInternals.createIndexedKey(key, configuredDriverName));
1353
+ return forgotten;
1354
+ },
1355
+ async flush() {
1356
+ const runtime = getCacheRuntime();
1357
+ const { configuredDriverName, driver } = resolveDriverContext2();
1358
+ await driver.flush();
1359
+ const dependencyIndex = runtime.dependencyIndex;
1360
+ if (!dependencyIndex) {
1361
+ return;
1362
+ }
1363
+ const registeredKeys = await dependencyIndex.listRegisteredKeys();
1364
+ for (const indexedKey of registeredKeys) {
1365
+ if (cacheQueryBridgeInternals.parseIndexedKey(indexedKey).driverName === configuredDriverName) {
1366
+ await dependencyIndex.removeKey(indexedKey);
1367
+ }
1368
+ }
1369
+ },
1370
+ async increment(key, amount = 1) {
1371
+ const { driver } = resolveDriverContext2();
1372
+ return driver.increment(resolveNormalizedKey2(key), amount);
1373
+ },
1374
+ async decrement(key, amount = 1) {
1375
+ const { driver } = resolveDriverContext2();
1376
+ return driver.decrement(resolveNormalizedKey2(key), amount);
1377
+ },
1378
+ async remember(key, ttl, callback) {
1379
+ const cached = await getCachedValue2(key);
1380
+ if (cached !== null) {
1381
+ return cached;
1382
+ }
1383
+ const value = await resolveValue(callback);
1384
+ await repository.put(key, value, ttl);
1385
+ return value;
1386
+ },
1387
+ async rememberForever(key, callback) {
1388
+ const cached = await getCachedValue2(key);
1389
+ if (cached !== null) {
1390
+ return cached;
1391
+ }
1392
+ const value = await resolveValue(callback);
1393
+ await repository.forever(key, value);
1394
+ return value;
1395
+ },
1396
+ async flexible(key, ttl, callback) {
1397
+ const normalizedTtl = normalizeFlexibleTtl2(ttl);
1398
+ const now = Date.now();
1399
+ const cached = await getCachedValue2(key);
1400
+ if (isFlexibleEnvelope2(cached)) {
1401
+ if (now <= cached.freshUntil) {
1402
+ return cached.value;
1403
+ }
1404
+ if (now <= cached.staleUntil) {
1405
+ const refreshLock2 = createRefreshLock(key, normalizedTtl.staleSeconds);
1406
+ void refreshLock2.get(async () => {
1407
+ await refreshFlexibleValue(key, normalizedTtl, callback);
1408
+ return true;
1409
+ }).catch(() => void 0);
1410
+ return cached.value;
1411
+ }
1412
+ }
1413
+ const refreshLock = createRefreshLock(key, normalizedTtl.staleSeconds);
1414
+ const refreshed = await refreshLock.block(
1415
+ Math.min(
1416
+ MAX_REFRESH_BLOCK_SECONDS,
1417
+ Math.max(1, Math.ceil(normalizedTtl.staleSeconds / 300))
1418
+ ),
1419
+ async () => refreshFlexibleValue(key, normalizedTtl, callback)
1420
+ );
1421
+ if (refreshed !== false) {
1422
+ return refreshed;
1423
+ }
1424
+ const retried = await getCachedValue2(key);
1425
+ if (isFlexibleEnvelope2(retried)) {
1426
+ if (Date.now() <= retried.staleUntil) {
1427
+ return retried.value;
1428
+ }
1429
+ }
1430
+ return refreshFlexibleValue(key, normalizedTtl, callback);
1431
+ },
1432
+ lock(name, seconds) {
1433
+ const { driver } = resolveDriverContext2();
1434
+ return driver.lock(resolveNormalizedLockName(name), seconds);
1435
+ }
1436
+ });
1437
+ return repository;
1438
+ }
1439
+ var repositories = /* @__PURE__ */ new Map();
1440
+ function getOrCreateRepository(driverName) {
1441
+ const key = resolveDriverKey(driverName);
1442
+ const existing = repositories.get(key);
1443
+ if (existing) {
1444
+ return existing;
1445
+ }
1446
+ const repository = createCacheRepository(driverName);
1447
+ repositories.set(key, repository);
1448
+ return repository;
1449
+ }
1450
+ function resetCacheFacadeRepositories() {
1451
+ repositories.clear();
1452
+ }
1453
+ var defaultRepository = getOrCreateRepository();
1454
+ var cacheFacade = Object.freeze({
1455
+ ...defaultRepository,
1456
+ driver(name) {
1457
+ return getOrCreateRepository(name);
1458
+ }
1459
+ });
1460
+ var cacheFacadeInternals = {
1461
+ createRefreshLockName(key) {
1462
+ return `__flexible__:${resolveCacheKey(key)}`;
1463
+ },
1464
+ getOrCreateRepository,
1465
+ isFlexibleEnvelope: isFlexibleEnvelope2,
1466
+ normalizeFlexibleTtl: normalizeFlexibleTtl2,
1467
+ resolveDriverKey,
1468
+ resolveFallback,
1469
+ resolveValue
1470
+ };
1471
+
1472
+ // src/index.ts
1473
+ function configureCacheRuntime2(...parameters) {
1474
+ configureCacheRuntime(...parameters);
1475
+ }
1476
+ function resetCacheRuntime2() {
1477
+ resetCacheRuntime();
1478
+ resetCacheFacadeRepositories();
1479
+ }
1480
+ var cache = Object.freeze({
1481
+ defineCacheKey,
1482
+ normalizeCacheTtl,
1483
+ serializeCacheValue,
1484
+ deserializeCacheValue,
1485
+ configureCacheRuntime: configureCacheRuntime2,
1486
+ getCacheRuntime,
1487
+ getCacheRuntimeBindings,
1488
+ resetCacheRuntime: resetCacheRuntime2,
1489
+ ...cacheFacade
1490
+ });
1491
+ var src_default = cache;
1492
+ export {
1493
+ CacheConfigError,
1494
+ CacheDriverResolutionError,
1495
+ CacheError,
1496
+ CacheInvalidNumericMutationError,
1497
+ CacheInvalidTtlError,
1498
+ CacheLockAcquisitionError,
1499
+ CacheOptionalPackageError,
1500
+ CacheQueryIntegrationError,
1501
+ CacheRuntimeNotConfiguredError,
1502
+ CacheSerializationError,
1503
+ cacheContractsInternals,
1504
+ cacheDbInternals,
1505
+ cacheFacade,
1506
+ cacheFacadeInternals,
1507
+ cacheQueryBridgeInternals,
1508
+ cacheRedisInternals,
1509
+ cacheRuntimeInternals,
1510
+ configureCacheRuntime2 as configureCacheRuntime,
1511
+ src_default as default,
1512
+ defineCacheConfig,
1513
+ defineCacheKey,
1514
+ deserializeCacheValue,
1515
+ fileDriverInternals,
1516
+ getCacheRuntime,
1517
+ getCacheRuntimeBindings,
1518
+ isCacheKey,
1519
+ normalizeCacheTtl,
1520
+ resetCacheRuntime2 as resetCacheRuntime,
1521
+ resolveCacheKey,
1522
+ serializeCacheValue
1523
+ };