@warlock.js/cache 4.0.174 → 4.1.1

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.
Files changed (213) hide show
  1. package/README.md +85 -0
  2. package/cjs/index.cjs +4088 -0
  3. package/cjs/index.cjs.map +1 -0
  4. package/esm/cache-manager.d.mts +314 -0
  5. package/esm/cache-manager.d.mts.map +1 -0
  6. package/esm/cache-manager.mjs +486 -0
  7. package/esm/cache-manager.mjs.map +1 -0
  8. package/esm/cached/auto-key.d.mts +25 -0
  9. package/esm/cached/auto-key.d.mts.map +1 -0
  10. package/esm/cached/auto-key.mjs +55 -0
  11. package/esm/cached/auto-key.mjs.map +1 -0
  12. package/esm/cached/cached.d.mts +54 -0
  13. package/esm/cached/cached.d.mts.map +1 -0
  14. package/esm/cached/cached.mjs +25 -0
  15. package/esm/cached/cached.mjs.map +1 -0
  16. package/esm/cached/index.d.mts +3 -0
  17. package/esm/cached/index.mjs +5 -0
  18. package/esm/cached/normalize-args.d.mts +51 -0
  19. package/esm/cached/normalize-args.d.mts.map +1 -0
  20. package/esm/cached/normalize-args.mjs +26 -0
  21. package/esm/cached/normalize-args.mjs.map +1 -0
  22. package/esm/drivers/base-cache-driver.d.mts +322 -0
  23. package/esm/drivers/base-cache-driver.d.mts.map +1 -0
  24. package/esm/drivers/base-cache-driver.mjs +522 -0
  25. package/esm/drivers/base-cache-driver.mjs.map +1 -0
  26. package/esm/drivers/file-cache-driver.d.mts +68 -0
  27. package/esm/drivers/file-cache-driver.d.mts.map +1 -0
  28. package/esm/drivers/file-cache-driver.mjs +174 -0
  29. package/esm/drivers/file-cache-driver.mjs.map +1 -0
  30. package/esm/drivers/index.d.mts +9 -0
  31. package/esm/drivers/index.mjs +11 -0
  32. package/esm/drivers/lru-memory-cache-driver.d.mts +136 -0
  33. package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -0
  34. package/esm/drivers/lru-memory-cache-driver.mjs +317 -0
  35. package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -0
  36. package/esm/drivers/memory-cache-driver.d.mts +112 -0
  37. package/esm/drivers/memory-cache-driver.d.mts.map +1 -0
  38. package/esm/drivers/memory-cache-driver.mjs +241 -0
  39. package/esm/drivers/memory-cache-driver.mjs.map +1 -0
  40. package/esm/drivers/memory-extended-cache-driver.d.mts +17 -0
  41. package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -0
  42. package/esm/drivers/memory-extended-cache-driver.mjs +34 -0
  43. package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -0
  44. package/esm/drivers/mock-cache-driver.d.mts +137 -0
  45. package/esm/drivers/mock-cache-driver.d.mts.map +1 -0
  46. package/esm/drivers/mock-cache-driver.mjs +226 -0
  47. package/esm/drivers/mock-cache-driver.mjs.map +1 -0
  48. package/esm/drivers/null-cache-driver.d.mts +69 -0
  49. package/esm/drivers/null-cache-driver.d.mts.map +1 -0
  50. package/esm/drivers/null-cache-driver.mjs +92 -0
  51. package/esm/drivers/null-cache-driver.mjs.map +1 -0
  52. package/esm/drivers/pg-cache-driver.d.mts +148 -0
  53. package/esm/drivers/pg-cache-driver.d.mts.map +1 -0
  54. package/esm/drivers/pg-cache-driver.mjs +437 -0
  55. package/esm/drivers/pg-cache-driver.mjs.map +1 -0
  56. package/esm/drivers/redis-cache-driver.d.mts +86 -0
  57. package/esm/drivers/redis-cache-driver.d.mts.map +1 -0
  58. package/esm/drivers/redis-cache-driver.mjs +312 -0
  59. package/esm/drivers/redis-cache-driver.mjs.map +1 -0
  60. package/esm/index.d.mts +21 -0
  61. package/esm/index.mjs +24 -0
  62. package/esm/list/index.d.mts +1 -0
  63. package/esm/list/memory-cache-list.d.mts +77 -0
  64. package/esm/list/memory-cache-list.d.mts.map +1 -0
  65. package/esm/list/memory-cache-list.mjs +119 -0
  66. package/esm/list/memory-cache-list.mjs.map +1 -0
  67. package/esm/metrics.d.mts +118 -0
  68. package/esm/metrics.d.mts.map +1 -0
  69. package/esm/metrics.mjs +197 -0
  70. package/esm/metrics.mjs.map +1 -0
  71. package/esm/scoped-cache.d.mts +205 -0
  72. package/esm/scoped-cache.d.mts.map +1 -0
  73. package/esm/scoped-cache.mjs +274 -0
  74. package/esm/scoped-cache.mjs.map +1 -0
  75. package/esm/tagged-cache.d.mts +89 -0
  76. package/esm/tagged-cache.d.mts.map +1 -0
  77. package/esm/tagged-cache.mjs +147 -0
  78. package/esm/tagged-cache.mjs.map +1 -0
  79. package/esm/tagged-scoped-cache.d.mts +111 -0
  80. package/esm/tagged-scoped-cache.d.mts.map +1 -0
  81. package/esm/tagged-scoped-cache.mjs +142 -0
  82. package/esm/tagged-scoped-cache.mjs.map +1 -0
  83. package/esm/types.d.mts +1067 -0
  84. package/esm/types.d.mts.map +1 -0
  85. package/esm/types.mjs +62 -0
  86. package/esm/types.mjs.map +1 -0
  87. package/esm/utils.d.mts +161 -0
  88. package/esm/utils.d.mts.map +1 -0
  89. package/esm/utils.mjs +222 -0
  90. package/esm/utils.mjs.map +1 -0
  91. package/llms-full.txt +2071 -0
  92. package/llms.txt +28 -0
  93. package/package.json +53 -39
  94. package/skills/apply-cache-patterns/SKILL.md +97 -0
  95. package/skills/cache-basics/SKILL.md +121 -0
  96. package/skills/configure-pg-cache/SKILL.md +115 -0
  97. package/skills/configure-set-options/SKILL.md +96 -0
  98. package/skills/handle-cache-errors/SKILL.md +91 -0
  99. package/skills/observe-cache/SKILL.md +103 -0
  100. package/skills/overview/SKILL.md +69 -0
  101. package/skills/pick-cache-driver/SKILL.md +115 -0
  102. package/skills/test-cache-code/SKILL.md +219 -0
  103. package/skills/use-cache-atomic/SKILL.md +67 -0
  104. package/skills/use-cache-bulk/SKILL.md +57 -0
  105. package/skills/use-cache-list/SKILL.md +85 -0
  106. package/skills/use-cache-lock/SKILL.md +104 -0
  107. package/skills/use-cache-namespace/SKILL.md +88 -0
  108. package/skills/use-cache-similarity/SKILL.md +94 -0
  109. package/skills/use-cache-tags/SKILL.md +85 -0
  110. package/skills/use-cache-update-merge/SKILL.md +84 -0
  111. package/skills/use-cache-utils/SKILL.md +89 -0
  112. package/skills/use-cached-hof/SKILL.md +102 -0
  113. package/skills/use-swr/SKILL.md +104 -0
  114. package/cjs/cache-manager.d.ts +0 -163
  115. package/cjs/cache-manager.d.ts.map +0 -1
  116. package/cjs/cache-manager.js +0 -322
  117. package/cjs/cache-manager.js.map +0 -1
  118. package/cjs/drivers/base-cache-driver.d.ts +0 -152
  119. package/cjs/drivers/base-cache-driver.d.ts.map +0 -1
  120. package/cjs/drivers/base-cache-driver.js +0 -321
  121. package/cjs/drivers/base-cache-driver.js.map +0 -1
  122. package/cjs/drivers/file-cache-driver.d.ts +0 -45
  123. package/cjs/drivers/file-cache-driver.d.ts.map +0 -1
  124. package/cjs/drivers/file-cache-driver.js +0 -133
  125. package/cjs/drivers/file-cache-driver.js.map +0 -1
  126. package/cjs/drivers/index.d.ts +0 -8
  127. package/cjs/drivers/index.d.ts.map +0 -1
  128. package/cjs/drivers/lru-memory-cache-driver.d.ts +0 -98
  129. package/cjs/drivers/lru-memory-cache-driver.d.ts.map +0 -1
  130. package/cjs/drivers/lru-memory-cache-driver.js +0 -252
  131. package/cjs/drivers/lru-memory-cache-driver.js.map +0 -1
  132. package/cjs/drivers/memory-cache-driver.d.ts +0 -82
  133. package/cjs/drivers/memory-cache-driver.d.ts.map +0 -1
  134. package/cjs/drivers/memory-cache-driver.js +0 -218
  135. package/cjs/drivers/memory-cache-driver.js.map +0 -1
  136. package/cjs/drivers/memory-extended-cache-driver.d.ts +0 -13
  137. package/cjs/drivers/memory-extended-cache-driver.d.ts.map +0 -1
  138. package/cjs/drivers/memory-extended-cache-driver.js +0 -25
  139. package/cjs/drivers/memory-extended-cache-driver.js.map +0 -1
  140. package/cjs/drivers/null-cache-driver.d.ts +0 -58
  141. package/cjs/drivers/null-cache-driver.d.ts.map +0 -1
  142. package/cjs/drivers/null-cache-driver.js +0 -84
  143. package/cjs/drivers/null-cache-driver.js.map +0 -1
  144. package/cjs/drivers/redis-cache-driver.d.ts +0 -57
  145. package/cjs/drivers/redis-cache-driver.d.ts.map +0 -1
  146. package/cjs/drivers/redis-cache-driver.js +0 -263
  147. package/cjs/drivers/redis-cache-driver.js.map +0 -1
  148. package/cjs/index.d.ts +0 -6
  149. package/cjs/index.d.ts.map +0 -1
  150. package/cjs/index.js +0 -1
  151. package/cjs/index.js.map +0 -1
  152. package/cjs/tagged-cache.d.ts +0 -77
  153. package/cjs/tagged-cache.d.ts.map +0 -1
  154. package/cjs/tagged-cache.js +0 -160
  155. package/cjs/tagged-cache.js.map +0 -1
  156. package/cjs/types.d.ts +0 -391
  157. package/cjs/types.d.ts.map +0 -1
  158. package/cjs/types.js +0 -36
  159. package/cjs/types.js.map +0 -1
  160. package/cjs/utils.d.ts +0 -50
  161. package/cjs/utils.d.ts.map +0 -1
  162. package/cjs/utils.js +0 -55
  163. package/cjs/utils.js.map +0 -1
  164. package/esm/cache-manager.d.ts +0 -163
  165. package/esm/cache-manager.d.ts.map +0 -1
  166. package/esm/cache-manager.js +0 -322
  167. package/esm/cache-manager.js.map +0 -1
  168. package/esm/drivers/base-cache-driver.d.ts +0 -152
  169. package/esm/drivers/base-cache-driver.d.ts.map +0 -1
  170. package/esm/drivers/base-cache-driver.js +0 -321
  171. package/esm/drivers/base-cache-driver.js.map +0 -1
  172. package/esm/drivers/file-cache-driver.d.ts +0 -45
  173. package/esm/drivers/file-cache-driver.d.ts.map +0 -1
  174. package/esm/drivers/file-cache-driver.js +0 -133
  175. package/esm/drivers/file-cache-driver.js.map +0 -1
  176. package/esm/drivers/index.d.ts +0 -8
  177. package/esm/drivers/index.d.ts.map +0 -1
  178. package/esm/drivers/lru-memory-cache-driver.d.ts +0 -98
  179. package/esm/drivers/lru-memory-cache-driver.d.ts.map +0 -1
  180. package/esm/drivers/lru-memory-cache-driver.js +0 -252
  181. package/esm/drivers/lru-memory-cache-driver.js.map +0 -1
  182. package/esm/drivers/memory-cache-driver.d.ts +0 -82
  183. package/esm/drivers/memory-cache-driver.d.ts.map +0 -1
  184. package/esm/drivers/memory-cache-driver.js +0 -218
  185. package/esm/drivers/memory-cache-driver.js.map +0 -1
  186. package/esm/drivers/memory-extended-cache-driver.d.ts +0 -13
  187. package/esm/drivers/memory-extended-cache-driver.d.ts.map +0 -1
  188. package/esm/drivers/memory-extended-cache-driver.js +0 -25
  189. package/esm/drivers/memory-extended-cache-driver.js.map +0 -1
  190. package/esm/drivers/null-cache-driver.d.ts +0 -58
  191. package/esm/drivers/null-cache-driver.d.ts.map +0 -1
  192. package/esm/drivers/null-cache-driver.js +0 -84
  193. package/esm/drivers/null-cache-driver.js.map +0 -1
  194. package/esm/drivers/redis-cache-driver.d.ts +0 -57
  195. package/esm/drivers/redis-cache-driver.d.ts.map +0 -1
  196. package/esm/drivers/redis-cache-driver.js +0 -263
  197. package/esm/drivers/redis-cache-driver.js.map +0 -1
  198. package/esm/index.d.ts +0 -6
  199. package/esm/index.d.ts.map +0 -1
  200. package/esm/index.js +0 -1
  201. package/esm/index.js.map +0 -1
  202. package/esm/tagged-cache.d.ts +0 -77
  203. package/esm/tagged-cache.d.ts.map +0 -1
  204. package/esm/tagged-cache.js +0 -160
  205. package/esm/tagged-cache.js.map +0 -1
  206. package/esm/types.d.ts +0 -391
  207. package/esm/types.d.ts.map +0 -1
  208. package/esm/types.js +0 -36
  209. package/esm/types.js.map +0 -1
  210. package/esm/utils.d.ts +0 -50
  211. package/esm/utils.d.ts.map +0 -1
  212. package/esm/utils.js +0 -55
  213. package/esm/utils.js.map +0 -1
package/cjs/index.cjs ADDED
@@ -0,0 +1,4088 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let _mongez_reinforcements = require("@mongez/reinforcements");
30
+ let ms = require("ms");
31
+ ms = __toESM(ms, 1);
32
+ let _warlock_js_logger = require("@warlock.js/logger");
33
+ let _warlock_js_fs = require("@warlock.js/fs");
34
+ let path = require("path");
35
+ path = __toESM(path, 1);
36
+
37
+ //#region ../../@warlock.js/cache/src/metrics.ts
38
+ /**
39
+ * Default size of the circular latency-sample buffer. 1000 samples covers
40
+ * "tell me the current p95" for every realistic workload while keeping the
41
+ * memory cost negligible (8KB at 8 bytes per number).
42
+ */
43
+ const DEFAULT_LATENCY_BUFFER_SIZE = 1e3;
44
+ /**
45
+ * Listens to `CacheManager` events and accumulates running counters + a
46
+ * circular latency buffer per driver. Returned to consumers via
47
+ * `cache.metrics()` as a {@link CacheMetricsSnapshot}.
48
+ *
49
+ * **Role.** Single-instance observability layer attached to the manager.
50
+ * Subscribes once at construction; survives `cache.use()` driver switches
51
+ * because the global event registry on the manager re-attaches handlers to
52
+ * every loaded driver.
53
+ *
54
+ * **Responsibility.**
55
+ * - Owns: per-driver and aggregate counters (`hits`, `misses`, `sets`,
56
+ * `removed`, `errors`), the latency circular buffer, and snapshot
57
+ * computation including hit-rate + percentile calculation.
58
+ * - Does NOT own: event emission (driven by drivers via `BaseCacheDriver.emit`),
59
+ * timing instrumentation (done at the manager level via `recordLatency`),
60
+ * or persistence — every metric resets on `resetMetrics()` and on process
61
+ * restart.
62
+ *
63
+ * @example
64
+ * cache.metrics();
65
+ * // {
66
+ * // hits: 9821, misses: 173, hitRate: 0.983,
67
+ * // latencyMs: { p50: 0.4, p95: 2.1, p99: 8.2, samples: 1000 },
68
+ * // byDriver: { memory: { ... }, redis: { ... } },
69
+ * // startedAt: 1714185600000,
70
+ * // }
71
+ */
72
+ var CacheMetricsCollector = class {
73
+ constructor(bufferSize = DEFAULT_LATENCY_BUFFER_SIZE) {
74
+ this.byDriver = /* @__PURE__ */ new Map();
75
+ this.startedAt = Date.now();
76
+ this.bufferSize = bufferSize;
77
+ this.aggregate = this.createCounters();
78
+ }
79
+ /**
80
+ * Increment the appropriate counters for a cache event. Called from the
81
+ * manager's global listeners (one per event type).
82
+ */
83
+ recordEvent(event, data) {
84
+ const driverBucket = this.bucketFor(data.driver);
85
+ const aggregate = this.aggregate;
86
+ switch (event) {
87
+ case "hit":
88
+ aggregate.hits += 1;
89
+ driverBucket.hits += 1;
90
+ break;
91
+ case "miss":
92
+ aggregate.misses += 1;
93
+ driverBucket.misses += 1;
94
+ break;
95
+ case "set":
96
+ aggregate.sets += 1;
97
+ driverBucket.sets += 1;
98
+ break;
99
+ case "removed":
100
+ aggregate.removed += 1;
101
+ driverBucket.removed += 1;
102
+ break;
103
+ case "error":
104
+ aggregate.errors += 1;
105
+ driverBucket.errors += 1;
106
+ break;
107
+ }
108
+ }
109
+ /**
110
+ * Append a latency sample for `driver`. Called by the manager from its
111
+ * timed wrappers around `get` / `set` / `remove`. Uses circular-buffer
112
+ * semantics: oldest samples are overwritten once the buffer is full.
113
+ */
114
+ recordLatency(driver, durationMs) {
115
+ this.appendLatency(this.aggregate, durationMs);
116
+ this.appendLatency(this.bucketFor(driver), durationMs);
117
+ }
118
+ /**
119
+ * Compute and return the current snapshot. Latency percentiles are
120
+ * derived from a sorted copy of the buffer at call time — O(N log N)
121
+ * on N=1000 is cheap enough that we don't bother caching.
122
+ */
123
+ snapshot() {
124
+ const byDriver = {};
125
+ for (const [driverName, bucket] of this.byDriver) byDriver[driverName] = this.toRow(bucket);
126
+ return {
127
+ ...this.toRow(this.aggregate),
128
+ byDriver,
129
+ startedAt: this.startedAt
130
+ };
131
+ }
132
+ /**
133
+ * Wipe every counter, drop every latency sample, and reset `startedAt`.
134
+ * The collector itself stays subscribed to events.
135
+ */
136
+ reset() {
137
+ this.resetCounters(this.aggregate);
138
+ this.byDriver.clear();
139
+ this.startedAt = Date.now();
140
+ }
141
+ /**
142
+ * Locate the per-driver bucket, creating it on first reference. Driver
143
+ * names are taken verbatim from `CacheEventData.driver`.
144
+ */
145
+ bucketFor(driverName) {
146
+ let bucket = this.byDriver.get(driverName);
147
+ if (!bucket) {
148
+ bucket = this.createCounters();
149
+ this.byDriver.set(driverName, bucket);
150
+ }
151
+ return bucket;
152
+ }
153
+ /**
154
+ * Convert raw counters into the public snapshot row shape. Computes
155
+ * `hitRate` and the latency percentiles on the fly.
156
+ */
157
+ toRow(bucket) {
158
+ const totalReads = bucket.hits + bucket.misses;
159
+ const hitRate = totalReads === 0 ? 0 : bucket.hits / totalReads;
160
+ const latency = this.computeLatency(bucket.latencySamples);
161
+ return {
162
+ hits: bucket.hits,
163
+ misses: bucket.misses,
164
+ sets: bucket.sets,
165
+ removed: bucket.removed,
166
+ errors: bucket.errors,
167
+ hitRate,
168
+ latencyMs: latency,
169
+ startedAt: this.startedAt
170
+ };
171
+ }
172
+ /**
173
+ * Sort a copy of the buffer and pick the percentile entries directly.
174
+ * Empty buffers return zeroed percentiles so consumers can render
175
+ * dashboards without null-checking.
176
+ */
177
+ computeLatency(samples) {
178
+ if (samples.length === 0) return {
179
+ p50: 0,
180
+ p95: 0,
181
+ p99: 0,
182
+ samples: 0
183
+ };
184
+ const sorted = [...samples].sort((a, b) => a - b);
185
+ const pick = (quantile) => {
186
+ return sorted[Math.min(sorted.length - 1, Math.floor(quantile * sorted.length))];
187
+ };
188
+ return {
189
+ p50: pick(.5),
190
+ p95: pick(.95),
191
+ p99: pick(.99),
192
+ samples: sorted.length
193
+ };
194
+ }
195
+ /**
196
+ * Append a latency sample using circular-buffer semantics — overwrite the
197
+ * oldest entry once the buffer is full instead of growing unbounded.
198
+ */
199
+ appendLatency(bucket, durationMs) {
200
+ if (bucket.latencySamples.length < this.bufferSize) {
201
+ bucket.latencySamples.push(durationMs);
202
+ return;
203
+ }
204
+ bucket.latencySamples[bucket.latencyCursor] = durationMs;
205
+ bucket.latencyCursor = (bucket.latencyCursor + 1) % this.bufferSize;
206
+ }
207
+ /** Build a fresh counter row with zeroed totals and an empty buffer. */
208
+ createCounters() {
209
+ return {
210
+ hits: 0,
211
+ misses: 0,
212
+ sets: 0,
213
+ removed: 0,
214
+ errors: 0,
215
+ latencySamples: [],
216
+ latencyCursor: 0
217
+ };
218
+ }
219
+ /** Reset an existing counter row in place. Used by `reset()` for the aggregate. */
220
+ resetCounters(bucket) {
221
+ bucket.hits = 0;
222
+ bucket.misses = 0;
223
+ bucket.sets = 0;
224
+ bucket.removed = 0;
225
+ bucket.errors = 0;
226
+ bucket.latencySamples.length = 0;
227
+ bucket.latencyCursor = 0;
228
+ }
229
+ };
230
+
231
+ //#endregion
232
+ //#region ../../@warlock.js/cache/src/types.ts
233
+ /**
234
+ * Base error class for cache-related errors
235
+ */
236
+ var CacheError = class extends Error {
237
+ constructor(message) {
238
+ super(message);
239
+ this.name = "CacheError";
240
+ }
241
+ };
242
+ /**
243
+ * Error thrown when cache connection fails
244
+ */
245
+ var CacheConnectionError = class extends CacheError {
246
+ constructor(message) {
247
+ super(message);
248
+ this.name = "CacheConnectionError";
249
+ }
250
+ };
251
+ /**
252
+ * Error thrown when cache driver configuration is invalid
253
+ */
254
+ var CacheConfigurationError = class extends CacheError {
255
+ constructor(message) {
256
+ super(message);
257
+ this.name = "CacheConfigurationError";
258
+ }
259
+ };
260
+ /**
261
+ * Error thrown when cache driver is not initialized
262
+ */
263
+ var CacheDriverNotInitializedError = class extends CacheError {
264
+ constructor(message = "No cache driver initialized. Call cache.init() or cache.use() first.") {
265
+ super(message);
266
+ this.name = "CacheDriverNotInitializedError";
267
+ }
268
+ };
269
+ /**
270
+ * Error thrown when a driver does not implement a requested operation.
271
+ *
272
+ * Raised when a caller invokes a method the driver cannot fulfill —
273
+ * e.g. `update()` on the file driver before the file-lock primitive lands.
274
+ */
275
+ var CacheUnsupportedError = class extends CacheError {
276
+ constructor(message) {
277
+ super(message);
278
+ this.name = "CacheUnsupportedError";
279
+ }
280
+ };
281
+ /**
282
+ * Error thrown when an optimistic-concurrency update exhausts its retry budget.
283
+ */
284
+ var CacheConcurrencyError = class extends CacheError {
285
+ constructor(message) {
286
+ super(message);
287
+ this.name = "CacheConcurrencyError";
288
+ }
289
+ };
290
+
291
+ //#endregion
292
+ //#region ../../@warlock.js/cache/src/utils.ts
293
+ /**
294
+ * Make a proper key for the cache
295
+ */
296
+ function parseCacheKey(key, options = {}) {
297
+ if (typeof key === "object") key = JSON.stringify(key);
298
+ key = key.replace(/[{}"[\]]/g, "").replaceAll(/[:,]/g, ".");
299
+ const cachePrefix = typeof options.globalPrefix === "function" ? options.globalPrefix() : options.globalPrefix;
300
+ return (0, _mongez_reinforcements.rtrim)(String(cachePrefix ? (0, _mongez_reinforcements.rtrim)(cachePrefix, ".") + "." + key : key), ".");
301
+ }
302
+ /**
303
+ * Parse a TTL value into seconds.
304
+ *
305
+ * Accepts:
306
+ * - a number (already in seconds) — returned unchanged
307
+ * - `Infinity` — no expiration, returned unchanged
308
+ * - a human-readable duration string (e.g. `"1h"`, `"30m"`, `"7d"`) — parsed via `ms` then converted to seconds
309
+ *
310
+ * Throws `CacheConfigurationError` on unparseable strings or negative numbers.
311
+ *
312
+ * @example
313
+ * parseTtl(3600); // 3600
314
+ * parseTtl("1h"); // 3600
315
+ * parseTtl("7d"); // 604800
316
+ * parseTtl(Infinity); // Infinity
317
+ */
318
+ function parseTtl(input) {
319
+ if (typeof input === "number") {
320
+ if (input < 0) throw new CacheConfigurationError(`Invalid TTL: negative number (${input}).`);
321
+ return input;
322
+ }
323
+ if (typeof input !== "string" || input.trim() === "") throw new CacheConfigurationError(`Invalid TTL: expected number or duration string, got ${typeof input}.`);
324
+ const milliseconds = (0, ms.default)(input);
325
+ if (milliseconds === void 0 || Number.isNaN(milliseconds)) throw new CacheConfigurationError(`Invalid TTL duration string: "${input}". Expected forms like "1h", "30m", "7d".`);
326
+ return Math.floor(milliseconds / 1e3);
327
+ }
328
+ /**
329
+ * Convert an absolute `expiresAt` (Date or epoch milliseconds) into a
330
+ * relative TTL in seconds.
331
+ *
332
+ * Throws {@link CacheConfigurationError} when the deadline is in the past —
333
+ * the caller almost certainly has a bug (stale timestamp, wrong unit, etc.)
334
+ * and silently storing an already-expired entry would hide it.
335
+ *
336
+ * @example
337
+ * expiresAtToTtl(new Date(Date.now() + 60_000)); // ~60
338
+ * expiresAtToTtl(Date.now() + 30 * 60 * 1000); // ~1800
339
+ */
340
+ function expiresAtToTtl(expiresAt) {
341
+ const deadline = expiresAt instanceof Date ? expiresAt.getTime() : expiresAt;
342
+ const relativeMs = deadline - Date.now();
343
+ if (relativeMs <= 0) throw new CacheConfigurationError(`\`expiresAt\` must be in the future; got ${new Date(deadline).toISOString()}.`);
344
+ return Math.ceil(relativeMs / 1e3);
345
+ }
346
+ /**
347
+ * Coerce the polymorphic 3rd `set` argument into a uniform `CacheSetOptions`
348
+ * shape. Lets callers (and `BaseCacheDriver.resolveSetOptions`) skip per-shape
349
+ * branching.
350
+ *
351
+ * - `undefined` / `null` → `{}` (resolves to driver-level defaults later)
352
+ * - `number` / `string` (positional TTL) → `{ ttl }`
353
+ * - already an options object → returned as-is
354
+ */
355
+ function normalizeToOptions(input) {
356
+ if (input === void 0 || input === null) return {};
357
+ if (typeof input === "number" || typeof input === "string") return { ttl: input };
358
+ return input;
359
+ }
360
+ /**
361
+ * Sibling of {@link normalizeToOptions} for the `remember()` call site, where
362
+ * the polymorphic 2nd argument is `CacheTtl | RememberOptions` (no `expiresAt`,
363
+ * no `onConflict`). Returns the same shape so callers can `{ ...opts, ... }`
364
+ * without branching.
365
+ *
366
+ * @example
367
+ * normalizeToRememberOptions(60); // { ttl: 60 }
368
+ * normalizeToRememberOptions("1h"); // { ttl: "1h" }
369
+ * normalizeToRememberOptions({ ttl: "1h", tags: ["x"] }); // returned as-is
370
+ */
371
+ function normalizeToRememberOptions(input) {
372
+ if (input === void 0 || input === null) return {};
373
+ if (typeof input === "number" || typeof input === "string") return { ttl: input };
374
+ return input;
375
+ }
376
+ /**
377
+ * Resolve the final TTL in seconds for a `set` call. Precedence:
378
+ *
379
+ * 1. Caller's `ttl` (number or duration string) wins.
380
+ * 2. Otherwise, caller's `expiresAt` is converted to relative seconds.
381
+ * 3. Otherwise, `fallback` is used (driver-level default — typically
382
+ * `Infinity` when no default is configured, meaning "never expires").
383
+ *
384
+ * Throws {@link CacheConfigurationError} when `ttl` and `expiresAt` are
385
+ * supplied together (mutually exclusive).
386
+ */
387
+ function resolveTtl(ttl, expiresAt, fallback) {
388
+ if (ttl !== void 0 && expiresAt !== void 0) throw new CacheConfigurationError("Cache set options cannot specify both `ttl` and `expiresAt` — choose one.");
389
+ if (ttl !== void 0) return parseTtl(ttl);
390
+ if (expiresAt !== void 0) return expiresAtToTtl(expiresAt);
391
+ return fallback;
392
+ }
393
+ /**
394
+ * Combine any number of tag lists into a single deduped array, dropping
395
+ * `undefined`/empty entries. Returns `undefined` when no tags survive — lets
396
+ * callers skip emitting empty `tags: []` into option payloads.
397
+ *
398
+ * Used by scoped-cache merging where scope tags + handle tags + per-call tags
399
+ * must union additively without duplicates.
400
+ *
401
+ * @example
402
+ * mergeTagSets(["a", "b"], ["b", "c"]); // ["a", "b", "c"]
403
+ * mergeTagSets(undefined, ["x"]); // ["x"]
404
+ * mergeTagSets(undefined, undefined); // undefined
405
+ * mergeTagSets([], []); // undefined
406
+ */
407
+ function mergeTagSets(...lists) {
408
+ const flat = [];
409
+ for (const list of lists) {
410
+ if (!list || list.length === 0) continue;
411
+ flat.push(...list);
412
+ }
413
+ if (flat.length === 0) return;
414
+ return Array.from(new Set(flat));
415
+ }
416
+ /**
417
+ * Add extra tags to any option-bag that already shapes `tags?: string[]`.
418
+ * Pure — clones the input shape, never mutates. Tags are appended (caller
419
+ * is responsible for de-duplication if needed; pair with {@link mergeTagSets}).
420
+ *
421
+ * @example
422
+ * injectTags({ ttl: "1h" }, ["unread"]); // { ttl: "1h", tags: ["unread"] }
423
+ * injectTags({ tags: ["a"] }, ["b"]); // { tags: ["a", "b"] }
424
+ */
425
+ function injectTags(options, extraTags) {
426
+ if (extraTags.length === 0) return options;
427
+ return {
428
+ ...options,
429
+ tags: [...options.tags ?? [], ...extraTags]
430
+ };
431
+ }
432
+ /**
433
+ * Cosine similarity between two equal-length numeric vectors.
434
+ *
435
+ * Returns a value in `[-1, 1]` where `1` means perfectly aligned, `0` means
436
+ * orthogonal, and `-1` means opposing. For typical embedding spaces (where
437
+ * vectors live in the positive cone) the practical range is `[0, 1]`.
438
+ *
439
+ * Throws {@link CacheConfigurationError} on dimension mismatch — fail loud at
440
+ * the call site rather than silently returning a misleading score. A zero-norm
441
+ * vector on either side returns `0` (no defined direction to compare).
442
+ *
443
+ * @example
444
+ * cosineSimilarity([1, 0, 0], [1, 0, 0]); // 1
445
+ * cosineSimilarity([1, 0, 0], [0, 1, 0]); // 0
446
+ */
447
+ function cosineSimilarity(a, b) {
448
+ if (a.length !== b.length) throw new CacheConfigurationError(`Vector dimension mismatch: got ${a.length} and ${b.length}.`);
449
+ if (a.length === 0) throw new CacheConfigurationError("Vector dimension mismatch: empty vector cannot be compared.");
450
+ let dot = 0;
451
+ let normA = 0;
452
+ let normB = 0;
453
+ for (let i = 0; i < a.length; i++) {
454
+ const x = a[i];
455
+ const y = b[i];
456
+ dot += x * y;
457
+ normA += x * x;
458
+ normB += y * y;
459
+ }
460
+ if (normA === 0 || normB === 0) return 0;
461
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
462
+ }
463
+ let CACHE_FOR = /* @__PURE__ */ function(CACHE_FOR) {
464
+ /**
465
+ * Cache for 30 Minutes (in seconds)
466
+ */
467
+ CACHE_FOR[CACHE_FOR["HALF_HOUR"] = 1800] = "HALF_HOUR";
468
+ /**
469
+ * Cache for 1 Hour (in seconds)
470
+ */
471
+ CACHE_FOR[CACHE_FOR["ONE_HOUR"] = 3600] = "ONE_HOUR";
472
+ /**
473
+ * Cache for 12 Hours (in seconds)
474
+ */
475
+ CACHE_FOR[CACHE_FOR["HALF_DAY"] = 43200] = "HALF_DAY";
476
+ /**
477
+ * Cache for 24 Hours (in seconds)
478
+ */
479
+ CACHE_FOR[CACHE_FOR["ONE_DAY"] = 86400] = "ONE_DAY";
480
+ /**
481
+ * Cache for 7 Days (in seconds)
482
+ */
483
+ CACHE_FOR[CACHE_FOR["ONE_WEEK"] = 604800] = "ONE_WEEK";
484
+ /**
485
+ * Cache for 15 Days (in seconds)
486
+ */
487
+ CACHE_FOR[CACHE_FOR["HALF_MONTH"] = 1296e3] = "HALF_MONTH";
488
+ /**
489
+ * Cache for 30 Days (in seconds)
490
+ */
491
+ CACHE_FOR[CACHE_FOR["ONE_MONTH"] = 2592e3] = "ONE_MONTH";
492
+ /**
493
+ * Cache for 60 Days (in seconds)
494
+ */
495
+ CACHE_FOR[CACHE_FOR["TWO_MONTHS"] = 5184e3] = "TWO_MONTHS";
496
+ /**
497
+ * Cache for 180 Days (in seconds)
498
+ */
499
+ CACHE_FOR[CACHE_FOR["SIX_MONTHS"] = 15768e3] = "SIX_MONTHS";
500
+ /**
501
+ * Cache for 365 Days (in seconds)
502
+ */
503
+ CACHE_FOR[CACHE_FOR["ONE_YEAR"] = 31536e3] = "ONE_YEAR";
504
+ return CACHE_FOR;
505
+ }({});
506
+
507
+ //#endregion
508
+ //#region ../../@warlock.js/cache/src/tagged-scoped-cache.ts
509
+ /**
510
+ * One-shot tagged write handle on top of a {@link ScopedCache}.
511
+ *
512
+ * **Role.** Returned by `scope.tags([...])`. Adds a fixed list of tags to
513
+ * every write produced through this handle, on top of any tags the parent
514
+ * scope already contributes. Stateless except for the captured tag list.
515
+ *
516
+ * **Responsibility.**
517
+ * - Owns: appending the handle's tags to writes, delegating tag-index
518
+ * bookkeeping for `setNX` (which lacks an inline `tags` knob on the driver
519
+ * contract), and computing the union for `invalidate()` calls.
520
+ * - Does NOT own: storage, prefix-prepending (delegated to the scope),
521
+ * default `ttl` (delegated to the scope), or any kind of long-lived state.
522
+ *
523
+ * Tags compose additively: scope tags + handle tags + per-call tags, all
524
+ * unioned and deduped. The handle never replaces scope tags — `invalidate()`
525
+ * always sees the full union.
526
+ *
527
+ * @example
528
+ * // Inside application code — scope provides the per-user tag automatically:
529
+ * const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });
530
+ *
531
+ * await feed.tags(["unread"]).set("messages.1", message);
532
+ * // → tagged with [user.<id>, unread]
533
+ *
534
+ * await feed.tags(["unread"]).invalidate();
535
+ * // → wipes everything tagged with user.<id> OR unread.
536
+ */
537
+ var TaggedScopedCache = class {
538
+ /**
539
+ * Build a tagged handle. Constructed via `scope.tags([...])` — users never
540
+ * call this directly.
541
+ */
542
+ constructor(scope, handleTags) {
543
+ this.scope = scope;
544
+ this.handleTags = [...handleTags];
545
+ }
546
+ /**
547
+ * Write the scoped key with the handle's tags appended to whatever the
548
+ * caller passed. Scope-level tags are added on top by the scope itself.
549
+ */
550
+ set(key, value, ttlOrOptions) {
551
+ const options = injectTags(normalizeToOptions(ttlOrOptions), this.handleTags);
552
+ return this.scope.set(key, value, options);
553
+ }
554
+ /**
555
+ * Read the scoped key. Tags don't affect reads — pass-through.
556
+ */
557
+ get(key) {
558
+ return this.scope.get(key);
559
+ }
560
+ /**
561
+ * Check presence of the scoped key.
562
+ */
563
+ has(key) {
564
+ return this.scope.has(key);
565
+ }
566
+ /**
567
+ * Remove the scoped key. The tag-index entry will eventually be cleaned up
568
+ * by `invalidate()`; we don't proactively rewrite it here for cost reasons.
569
+ */
570
+ remove(key) {
571
+ return this.scope.remove(key);
572
+ }
573
+ /**
574
+ * Read-and-remove the scoped key.
575
+ */
576
+ pull(key) {
577
+ return this.scope.pull(key);
578
+ }
579
+ /**
580
+ * Permanent write with handle tags applied. Bypasses both the scope's and
581
+ * the caller's TTL (forever means forever) — only tags get injected.
582
+ */
583
+ forever(key, value) {
584
+ return this.scope.set(key, value, {
585
+ ttl: Infinity,
586
+ tags: this.handleTags
587
+ });
588
+ }
589
+ /**
590
+ * Atomic create-or-skip with the handle's tags applied on success. The
591
+ * driver contract has no inline `tags` knob on `setNX`, so we register the
592
+ * tag relationship manually after a successful write.
593
+ */
594
+ async setNX(key, value, ttl) {
595
+ if (!await this.scope.setNX(key, value, ttl)) return false;
596
+ const allTags = mergeTagSets(this.scope.defaults.tags, this.handleTags);
597
+ if (!allTags || allTags.length === 0) return true;
598
+ const scopedKey = this.buildScopedKey(key);
599
+ const parsedKey = this.scope.source.parseKey(scopedKey);
600
+ await this.scope.source.tags(allTags).storeTagRelationship(parsedKey);
601
+ return true;
602
+ }
603
+ /**
604
+ * Read-or-compute with handle tags appended on the cache-miss write.
605
+ */
606
+ remember(key, ttlOrOptions, callback) {
607
+ const options = injectTags(normalizeToRememberOptions(ttlOrOptions), this.handleTags);
608
+ return this.scope.remember(key, options, callback);
609
+ }
610
+ /**
611
+ * Atomic counter increment on the scoped key. Tags aren't applied to
612
+ * subsequent increments — they're attached at first-write time.
613
+ */
614
+ increment(key, value) {
615
+ return this.scope.increment(key, value);
616
+ }
617
+ /**
618
+ * Atomic counter decrement on the scoped key. See {@link increment}.
619
+ */
620
+ decrement(key, value) {
621
+ return this.scope.decrement(key, value);
622
+ }
623
+ /**
624
+ * Wipe every entry tagged with the union of (scope tags + handle tags).
625
+ * Tags are global across the package, so this reaches outside the scope's
626
+ * prefix when scope tags are also used elsewhere.
627
+ */
628
+ async invalidate() {
629
+ const allTags = mergeTagSets(this.scope.defaults.tags, this.handleTags);
630
+ if (!allTags || allTags.length === 0) return;
631
+ await this.scope.source.tags(allTags).invalidate();
632
+ }
633
+ /**
634
+ * Compute the source-side key the same way `ScopedCache.scopedKey` does —
635
+ * needed for `setNX`, where we have to register the tag relationship by
636
+ * hand because the driver contract doesn't accept inline tags there.
637
+ */
638
+ buildScopedKey(key) {
639
+ const keyString = typeof key === "string" ? key : parseCacheKey(key);
640
+ if (!keyString) return this.scope.prefix;
641
+ return `${this.scope.prefix}.${keyString}`;
642
+ }
643
+ };
644
+
645
+ //#endregion
646
+ //#region ../../@warlock.js/cache/src/scoped-cache.ts
647
+ /**
648
+ * Scoped view over a cache source. Returned by `cache.namespace(prefix, options?)`.
649
+ *
650
+ * **Role.** A `ScopedCache` is a stateless wrapper that prepends a fixed
651
+ * prefix to every key and applies optional default `ttl` / `tags` to every
652
+ * write. Stores nothing itself — every call forwards to the underlying
653
+ * `source` (typically the `CacheManager`, but any `CacheDriver` works).
654
+ *
655
+ * **Responsibility.**
656
+ * - Owns: prefix-prepending of keys, normalization of nested-scope prefixes,
657
+ * merging scope defaults into write options (`ttl`, `tags`), filtering
658
+ * `similar()` hits to its own scope, and exposing `.clear()` as a sugar
659
+ * for `removeNamespace(prefix)`.
660
+ * - Does NOT own: actual storage, connection lifecycle, event listeners,
661
+ * driver selection, or tag-index bookkeeping (delegated to the source's
662
+ * tagged-cache machinery).
663
+ *
664
+ * Per-call options always win over scope defaults; tags merge additively
665
+ * across (scope defaults + per-call) layers. Nested scopes inherit and may
666
+ * override the parent's defaults — see {@link ScopedCache.namespace}.
667
+ *
668
+ * @example
669
+ * const chat = cache.namespace(`chats.${id}`, { ttl: "30d" });
670
+ *
671
+ * await chat.set("messages.10", msg); // 30d default
672
+ * await chat.set("draft", d, { ttl: "1h" }); // per-call override
673
+ * await chat.namespace("typing", { ttl: "5s" }).set("user.42", true);
674
+ * await chat.clear();
675
+ */
676
+ var ScopedCache = class ScopedCache {
677
+ /**
678
+ * Build a scope. Constructed via `cache.namespace(prefix, options)` —
679
+ * users never call this directly.
680
+ */
681
+ constructor(source, prefix, defaults = {}) {
682
+ this.source = source;
683
+ this.prefix = parseCacheKey(prefix);
684
+ this.defaults = {
685
+ ttl: defaults.ttl,
686
+ tags: defaults.tags && defaults.tags.length > 0 ? [...defaults.tags] : void 0
687
+ };
688
+ }
689
+ /**
690
+ * Build a nested scope. The child's prefix is `parent.child`; child options
691
+ * override the parent's `ttl` and union into `tags`.
692
+ *
693
+ * @example
694
+ * const chat = cache.namespace("chats.10", { ttl: "30d" });
695
+ * const typing = chat.namespace("typing", { ttl: "5s" });
696
+ * // typing.prefix === "chats.10.typing"
697
+ */
698
+ namespace(prefix, options = {}) {
699
+ const childPrefix = `${this.prefix}.${parseCacheKey(prefix)}`;
700
+ return new ScopedCache(this.source, childPrefix, {
701
+ ttl: options.ttl ?? this.defaults.ttl,
702
+ tags: mergeTagSets(this.defaults.tags, options.tags)
703
+ });
704
+ }
705
+ /**
706
+ * Return a one-shot tagged write handle. The handle's tags merge additively
707
+ * with scope-level defaults — final tag list per write is the union of
708
+ * (scope tags + handle tags + per-call tags), deduped.
709
+ */
710
+ tags(tags) {
711
+ return new TaggedScopedCache(this, tags);
712
+ }
713
+ /**
714
+ * Wipe every entry under this scope's prefix. Sugar over
715
+ * `source.removeNamespace(prefix)` — siblings outside the scope are
716
+ * untouched.
717
+ */
718
+ clear() {
719
+ return this.source.removeNamespace(this.prefix);
720
+ }
721
+ /**
722
+ * Read the value at the scoped key. Forwards to the source after prefixing.
723
+ */
724
+ get(key) {
725
+ return this.source.get(this.scopedKey(key));
726
+ }
727
+ /**
728
+ * Check presence of the scoped key without fetching the value.
729
+ */
730
+ has(key) {
731
+ return this.source.has(this.scopedKey(key));
732
+ }
733
+ /**
734
+ * Batch-read scoped keys. Each input key is prefixed before forwarding.
735
+ */
736
+ many(keys) {
737
+ return this.source.many(keys.map((key) => this.scopedKey(key)));
738
+ }
739
+ /**
740
+ * Read-and-remove. Returns the value or `null`; the entry is gone after.
741
+ */
742
+ pull(key) {
743
+ return this.source.pull(this.scopedKey(key));
744
+ }
745
+ /**
746
+ * Write the scoped key. Per-call `ttl`/`tags` win over scope defaults;
747
+ * `expiresAt` is preserved as-is (absolute deadlines are never overridden
748
+ * by the scope's relative-ttl default).
749
+ */
750
+ set(key, value, ttlOrOptions) {
751
+ return this.source.set(this.scopedKey(key), value, this.mergeSetOptions(ttlOrOptions));
752
+ }
753
+ /**
754
+ * Batch-write under the scope. Caller's positional `ttl` wins; otherwise
755
+ * the scope default is parsed to seconds (since `setMany` accepts only a
756
+ * numeric ttl).
757
+ */
758
+ setMany(items, ttl) {
759
+ const scoped = {};
760
+ for (const [key, value] of Object.entries(items)) scoped[this.scopedKey(key)] = value;
761
+ return this.source.setMany(scoped, ttl ?? this.scopeTtlSeconds());
762
+ }
763
+ /**
764
+ * Atomic create-or-skip on the scoped key. Throws when the underlying
765
+ * source has no `setNX` (driver-specific — Redis-only today).
766
+ */
767
+ setNX(key, value, ttl) {
768
+ if (!this.source.setNX) throw new Error(`setNX is not supported by the underlying cache source: ${this.source.name}`);
769
+ return this.source.setNX(this.scopedKey(key), value, ttl ?? this.scopeTtlSeconds());
770
+ }
771
+ /**
772
+ * Permanent write (no expiration). Bypasses the scope's `ttl` default —
773
+ * `forever` always means forever, regardless of scope policy.
774
+ */
775
+ forever(key, value) {
776
+ return this.source.forever(this.scopedKey(key), value);
777
+ }
778
+ /**
779
+ * Remove a single scoped key.
780
+ */
781
+ remove(key) {
782
+ return this.source.remove(this.scopedKey(key));
783
+ }
784
+ /**
785
+ * Read-or-compute. Cache-miss writes pick up the scope's default `ttl`
786
+ * and `tags` unless the caller passed an options object that overrides.
787
+ */
788
+ remember(key, ttlOrOptions, callback) {
789
+ return this.source.remember(this.scopedKey(key), this.mergeRememberOptions(ttlOrOptions), callback);
790
+ }
791
+ /**
792
+ * Stale-while-revalidate on the scoped key. Scope-level `tags` merge
793
+ * additively with `options.tags`; `freshTtl`/`staleTtl` always come from
794
+ * the caller (no scope-default precedence — the SWR shape is too
795
+ * specific to the call site to inherit).
796
+ */
797
+ swr(key, options, callback) {
798
+ const merged = {
799
+ ...options,
800
+ tags: mergeTagSets(this.defaults.tags, options.tags)
801
+ };
802
+ return this.source.swr(this.scopedKey(key), merged, callback);
803
+ }
804
+ /**
805
+ * Atomic counter increment on the scoped key. TTL is preserved by the
806
+ * underlying driver — scope ttl is only applied on first write via `set`.
807
+ */
808
+ increment(key, value) {
809
+ return this.source.increment(this.scopedKey(key), value);
810
+ }
811
+ /**
812
+ * Atomic counter decrement on the scoped key. See {@link increment} for
813
+ * TTL semantics.
814
+ */
815
+ decrement(key, value) {
816
+ return this.source.decrement(this.scopedKey(key), value);
817
+ }
818
+ /**
819
+ * Atomic read-modify-write. Falls back to the scope's `ttl` when the caller
820
+ * doesn't provide one; the source still keeps the existing entry's TTL on
821
+ * an update unless `options.ttl` is explicitly set.
822
+ */
823
+ update(key, fn, options) {
824
+ return this.source.update(this.scopedKey(key), fn, { ttl: options?.ttl ?? this.defaults.ttl });
825
+ }
826
+ /**
827
+ * Shallow-merge a partial object into the scoped entry. Same TTL semantics
828
+ * as {@link update}.
829
+ */
830
+ merge(key, partial, options) {
831
+ return this.source.merge(this.scopedKey(key), partial, { ttl: options?.ttl ?? this.defaults.ttl });
832
+ }
833
+ /**
834
+ * Return a list accessor bound to the scoped key. The accessor itself
835
+ * does its own read-mutate-write under the prefixed entry.
836
+ */
837
+ list(key) {
838
+ return this.source.list(this.scopedKey(key));
839
+ }
840
+ /**
841
+ * Acquire a distributed lock on the scoped key. Caller's TTL wins; when
842
+ * the options form omits `ttl`, the scope default fills in.
843
+ */
844
+ lock(key, ttlOrOptions, fn) {
845
+ if (typeof ttlOrOptions === "object" && ttlOrOptions !== null) {
846
+ const merged = {
847
+ ...ttlOrOptions,
848
+ ttl: ttlOrOptions.ttl ?? this.defaults.ttl ?? ttlOrOptions.ttl
849
+ };
850
+ return this.source.lock(this.scopedKey(key), merged, fn);
851
+ }
852
+ return this.source.lock(this.scopedKey(key), ttlOrOptions, fn);
853
+ }
854
+ /**
855
+ * Similarity retrieval, scope-isolated. Hits whose keys fall outside this
856
+ * scope are filtered out before the result is returned. `topK` applies to
857
+ * the underlying retrieval — when the scope contains fewer than `topK`
858
+ * matches but other scopes do, the caller will see fewer hits than `topK`.
859
+ */
860
+ async similar(vector, options) {
861
+ const hits = await this.source.similar(vector, options);
862
+ const parsedPrefix = this.source.parseKey(this.prefix);
863
+ return hits.filter((hit) => hit.key === parsedPrefix || hit.key.startsWith(parsedPrefix + "."));
864
+ }
865
+ /**
866
+ * Build the source-side key by prepending the scope prefix. Object keys
867
+ * are normalized via {@link parseCacheKey} first so they compose with the
868
+ * prefix as plain dot-strings.
869
+ */
870
+ scopedKey(key) {
871
+ const keyString = typeof key === "string" ? key : parseCacheKey(key);
872
+ if (!keyString) return this.prefix;
873
+ return `${this.prefix}.${keyString}`;
874
+ }
875
+ /**
876
+ * Coerce the polymorphic 3rd `set` argument into a {@link CacheSetOptions}
877
+ * with scope defaults filled in. Per-call values always win; tags merge
878
+ * additively. `expiresAt` is preserved without injecting the scope's `ttl`
879
+ * default (absolute deadlines override relative ones).
880
+ */
881
+ mergeSetOptions(input) {
882
+ const options = normalizeToOptions(input);
883
+ const ttl = options.ttl ?? (options.expiresAt === void 0 ? this.defaults.ttl : void 0);
884
+ const tags = mergeTagSets(this.defaults.tags, options.tags);
885
+ const merged = { ...options };
886
+ if (ttl !== void 0) merged.ttl = ttl;
887
+ if (tags !== void 0) merged.tags = tags;
888
+ return merged;
889
+ }
890
+ /**
891
+ * Same merge as {@link mergeSetOptions} but for the `remember()` shape
892
+ * ({@link RememberOptions} — no `expiresAt`).
893
+ */
894
+ mergeRememberOptions(input) {
895
+ const options = normalizeToRememberOptions(input);
896
+ const ttl = options.ttl ?? this.defaults.ttl;
897
+ const tags = mergeTagSets(this.defaults.tags, options.tags);
898
+ const merged = { ...options };
899
+ if (ttl !== void 0) merged.ttl = ttl;
900
+ if (tags !== void 0) merged.tags = tags;
901
+ return merged;
902
+ }
903
+ /**
904
+ * Convert the scope's default `ttl` (which may be a duration string) into
905
+ * seconds, for the few methods (`setMany`, `setNX`) that accept only a
906
+ * numeric ttl.
907
+ */
908
+ scopeTtlSeconds() {
909
+ if (this.defaults.ttl === void 0) return;
910
+ return parseTtl(this.defaults.ttl);
911
+ }
912
+ };
913
+
914
+ //#endregion
915
+ //#region ../../@warlock.js/cache/src/cache-manager.ts
916
+ var CacheManager = class {
917
+ constructor() {
918
+ this.loadedDrivers = {};
919
+ this.configurations = {
920
+ drivers: {},
921
+ options: {}
922
+ };
923
+ this.globalEventListeners = /* @__PURE__ */ new Map();
924
+ this.name = "cacheManager";
925
+ }
926
+ /**
927
+ * {@inheritdoc}
928
+ */
929
+ get client() {
930
+ return this.currentDriver?.client;
931
+ }
932
+ /**
933
+ * Set the cache configurations
934
+ */
935
+ setCacheConfigurations(configurations) {
936
+ this.configurations.default = configurations.default;
937
+ this.configurations.drivers = configurations.drivers;
938
+ this.configurations.options = configurations.options;
939
+ this.configurations.logging = configurations.logging;
940
+ }
941
+ /**
942
+ * Set logging state
943
+ */
944
+ setLoggingState(loggingState) {
945
+ this.ensureDriverInitialized();
946
+ this.currentDriver.setLoggingState(loggingState);
947
+ }
948
+ /**
949
+ * Switch the manager to a registered driver, optionally injecting runtime
950
+ * options that merge over the static config.
951
+ *
952
+ * The string form looks the driver up in `setCacheConfigurations({ drivers })`,
953
+ * loads it (or returns the cached instance), and sets it as `currentDriver`.
954
+ * The instance form takes a pre-built driver and bypasses the registry; the
955
+ * `runtimeOptions` argument is silently ignored in that case because the
956
+ * instance was constructed externally.
957
+ *
958
+ * Runtime options merge over `config.options[name]` per-key — runtime wins.
959
+ * Use this for constructor-only knobs that can't live in static config
960
+ * (e.g. `pg`'s `client: pg.Pool`).
961
+ *
962
+ * @example
963
+ * const pool = new Pool({ connectionString });
964
+ * await cache.use("pg", { client: pool });
965
+ */
966
+ async use(driver, runtimeOptions) {
967
+ if (typeof driver === "string") {
968
+ const driverInstance = await this.load(driver, runtimeOptions);
969
+ if (!driverInstance) throw new CacheConfigurationError(`Cache driver ${driver} is not found, please declare it in the cache drivers in the configurations list.`);
970
+ driver = driverInstance;
971
+ }
972
+ this.attachGlobalListeners(driver);
973
+ if (this.configurations.logging !== void 0) driver.setLoggingState(this.configurations.logging);
974
+ this.currentDriver = driver;
975
+ return this;
976
+ }
977
+ /**
978
+ * Ensure driver is initialized before operations
979
+ */
980
+ ensureDriverInitialized() {
981
+ if (!this.currentDriver) throw new CacheDriverNotInitializedError();
982
+ }
983
+ /**
984
+ * Return the running metrics snapshot — counters, hit-rate, latency
985
+ * percentiles, per-driver breakdowns. Lazy-attaches the collector on
986
+ * first call so apps that never read metrics pay zero cost.
987
+ *
988
+ * @example
989
+ * const m = cache.metrics();
990
+ * console.log(`hit rate: ${(m.hitRate * 100).toFixed(1)}%`);
991
+ * console.log(`p95: ${m.latencyMs.p95.toFixed(2)}ms`);
992
+ */
993
+ metrics() {
994
+ return this.ensureMetricsCollector().snapshot();
995
+ }
996
+ /**
997
+ * Wipe every counter + latency sample and reset `startedAt` to now.
998
+ * The collector itself stays subscribed to events.
999
+ */
1000
+ resetMetrics() {
1001
+ this.ensureMetricsCollector().reset();
1002
+ }
1003
+ /**
1004
+ * Lazy-construct the metrics collector and wire it to the global event
1005
+ * bus. Subsequent calls return the same instance — survives `cache.use()`
1006
+ * driver switches because handlers attach via `on()` and re-bind to every
1007
+ * loaded driver.
1008
+ */
1009
+ ensureMetricsCollector() {
1010
+ if (this.metricsCollector) return this.metricsCollector;
1011
+ const collector = new CacheMetricsCollector();
1012
+ this.on("hit", (data) => collector.recordEvent("hit", data));
1013
+ this.on("miss", (data) => collector.recordEvent("miss", data));
1014
+ this.on("set", (data) => collector.recordEvent("set", data));
1015
+ this.on("removed", (data) => collector.recordEvent("removed", data));
1016
+ this.on("error", (data) => collector.recordEvent("error", data));
1017
+ this.metricsCollector = collector;
1018
+ return collector;
1019
+ }
1020
+ /**
1021
+ * Time the body, record the elapsed milliseconds against the metrics
1022
+ * collector for the given driver (defaults to the current driver's name).
1023
+ * Pass-through if the collector hasn't been instantiated yet — apps that
1024
+ * don't read metrics never pay for sample collection.
1025
+ */
1026
+ async timed(body, driverName) {
1027
+ if (!this.metricsCollector) return body();
1028
+ const start = performance.now();
1029
+ try {
1030
+ return await body();
1031
+ } finally {
1032
+ const elapsed = performance.now() - start;
1033
+ const name = driverName ?? this.currentDriver?.name ?? "unknown";
1034
+ this.metricsCollector.recordLatency(name, elapsed);
1035
+ }
1036
+ }
1037
+ /**
1038
+ * {@inheritdoc}
1039
+ */
1040
+ async get(key) {
1041
+ this.ensureDriverInitialized();
1042
+ return this.timed(() => this.currentDriver.get(key));
1043
+ }
1044
+ /**
1045
+ * Set a value in the cache.
1046
+ *
1047
+ * Accepts a positional TTL (number of seconds or duration string like `"1h"`)
1048
+ * or a rich {@link CacheSetOptions} object supporting `ttl`, `expiresAt`,
1049
+ * `tags`, `onConflict`, `namespace`, and per-call `driver` overrides.
1050
+ */
1051
+ async set(key, value, ttlOrOptions) {
1052
+ this.ensureDriverInitialized();
1053
+ const driverOverride = ttlOrOptions && typeof ttlOrOptions === "object" && "driver" in ttlOrOptions ? ttlOrOptions.driver : void 0;
1054
+ if (driverOverride) {
1055
+ const driver = await this.load(driverOverride);
1056
+ return this.timed(() => driver.set(key, value, ttlOrOptions), driver.name);
1057
+ }
1058
+ return this.timed(() => this.currentDriver.set(key, value, ttlOrOptions));
1059
+ }
1060
+ /**
1061
+ * {@inheritdoc}
1062
+ */
1063
+ async remove(key) {
1064
+ this.ensureDriverInitialized();
1065
+ return this.timed(() => this.currentDriver.remove(key));
1066
+ }
1067
+ /**
1068
+ * {@inheritdoc}
1069
+ */
1070
+ async removeNamespace(namespace) {
1071
+ this.ensureDriverInitialized();
1072
+ return this.currentDriver.removeNamespace(namespace);
1073
+ }
1074
+ /**
1075
+ * {@inheritdoc}
1076
+ */
1077
+ async flush() {
1078
+ this.ensureDriverInitialized();
1079
+ return this.currentDriver.flush();
1080
+ }
1081
+ /**
1082
+ * {@inheritdoc}
1083
+ */
1084
+ async connect() {
1085
+ this.ensureDriverInitialized();
1086
+ return this.currentDriver.connect();
1087
+ }
1088
+ /**
1089
+ * {@inheritdoc}
1090
+ */
1091
+ parseKey(key) {
1092
+ this.ensureDriverInitialized();
1093
+ return this.currentDriver.parseKey(key);
1094
+ }
1095
+ /**
1096
+ * {@inheritdoc}
1097
+ */
1098
+ get options() {
1099
+ this.ensureDriverInitialized();
1100
+ return this.currentDriver.options;
1101
+ }
1102
+ /**
1103
+ * {@inheritdoc}
1104
+ */
1105
+ setOptions(options) {
1106
+ this.ensureDriverInitialized();
1107
+ return this.currentDriver.setOptions(options || {});
1108
+ }
1109
+ /**
1110
+ * Return the loaded driver instance for `driverName`, loading it on first
1111
+ * call. Optional `runtimeOptions` follow the same merge-over-config rules
1112
+ * as {@link load}; passing options after the driver has already been
1113
+ * loaded throws to avoid silent swallowing.
1114
+ */
1115
+ async driver(driverName, runtimeOptions) {
1116
+ if (this.loadedDrivers[driverName]) {
1117
+ this.assertNoConflictingReload(driverName, runtimeOptions);
1118
+ return this.loadedDrivers[driverName];
1119
+ }
1120
+ return this.load(driverName, runtimeOptions);
1121
+ }
1122
+ /**
1123
+ * Initialize the cache manager and pick the default driver
1124
+ */
1125
+ async init() {
1126
+ const defaultCacheDriverName = this.configurations.default;
1127
+ if (!defaultCacheDriverName) return;
1128
+ const driver = await this.driver(defaultCacheDriverName);
1129
+ await this.use(driver);
1130
+ }
1131
+ /**
1132
+ * Load and connect the registered driver named `driver`. First-call wins —
1133
+ * subsequent calls without `runtimeOptions` return the cached instance, and
1134
+ * subsequent calls *with* `runtimeOptions` throw {@link CacheConfigurationError}
1135
+ * to avoid silently dropping the new options.
1136
+ *
1137
+ * `runtimeOptions` merge over `config.options[driver]` per-key (runtime wins),
1138
+ * letting consumers split static knobs (table, ttl, globalPrefix) from
1139
+ * constructor-only ones (pg's `client`, custom adapters, etc.).
1140
+ *
1141
+ * @example
1142
+ * const pool = new Pool({ connectionString });
1143
+ * const pg = await cache.load("pg", { client: pool });
1144
+ */
1145
+ async load(driver, runtimeOptions) {
1146
+ if (this.loadedDrivers[driver]) {
1147
+ this.assertNoConflictingReload(driver, runtimeOptions);
1148
+ return this.loadedDrivers[driver];
1149
+ }
1150
+ const Driver = this.configurations.drivers[driver];
1151
+ if (!Driver) throw new CacheConfigurationError(`Cache driver ${driver} is not found, please declare it in the cache drivers in the configurations list.`);
1152
+ const driverInstance = new Driver();
1153
+ const configOptions = this.configurations.options[driver] || {};
1154
+ driverInstance.setOptions({
1155
+ ...configOptions,
1156
+ ...runtimeOptions ?? {}
1157
+ });
1158
+ await driverInstance.connect();
1159
+ this.attachGlobalListeners(driverInstance);
1160
+ this.loadedDrivers[driver] = driverInstance;
1161
+ return driverInstance;
1162
+ }
1163
+ /**
1164
+ * Guard against silently dropping runtime options on a re-load. Once a
1165
+ * driver has been instantiated, its options are frozen — calling `load` /
1166
+ * `driver` / `use` again with a non-empty `runtimeOptions` would otherwise
1167
+ * appear to work but actually use the original options. We throw instead
1168
+ * so the misuse surfaces at the call site.
1169
+ */
1170
+ assertNoConflictingReload(driverName, runtimeOptions) {
1171
+ if (runtimeOptions === void 0) return;
1172
+ if (Object.keys(runtimeOptions).length === 0) return;
1173
+ throw new CacheConfigurationError(`Cache driver '${driverName}' is already loaded; runtime options on subsequent calls are ignored — register a second driver name if you need a different configuration.`);
1174
+ }
1175
+ /**
1176
+ * Register and bind a driver
1177
+ */
1178
+ registerDriver(driverName, driverClass) {
1179
+ this.configurations.drivers[driverName] = driverClass;
1180
+ }
1181
+ /**
1182
+ * Disconnect the cache manager
1183
+ */
1184
+ async disconnect() {
1185
+ if (this.currentDriver) await this.currentDriver.disconnect();
1186
+ }
1187
+ /**
1188
+ * {@inheritdoc}
1189
+ */
1190
+ async has(key) {
1191
+ this.ensureDriverInitialized();
1192
+ return this.currentDriver.has(key);
1193
+ }
1194
+ /**
1195
+ * {@inheritdoc}
1196
+ */
1197
+ async remember(key, ttlOrOptions, callback) {
1198
+ this.ensureDriverInitialized();
1199
+ const driverOverride = ttlOrOptions && typeof ttlOrOptions === "object" && "driver" in ttlOrOptions ? ttlOrOptions.driver : void 0;
1200
+ if (driverOverride) return (await this.load(driverOverride)).remember(key, ttlOrOptions, callback);
1201
+ return this.currentDriver.remember(key, ttlOrOptions, callback);
1202
+ }
1203
+ /**
1204
+ * Stale-while-revalidate. Returns cached when fresh, returns the stale
1205
+ * value plus a background refresh when within `freshTtl..staleTtl`,
1206
+ * blocks like a normal miss past `staleTtl`. Honors per-call `driver`
1207
+ * override the same way `remember()` does.
1208
+ *
1209
+ * @example
1210
+ * const product = await cache.swr(
1211
+ * "product.42",
1212
+ * { freshTtl: "1m", staleTtl: "1h" },
1213
+ * () => db.products.find(42),
1214
+ * );
1215
+ */
1216
+ async swr(key, options, callback) {
1217
+ this.ensureDriverInitialized();
1218
+ const driverOverride = options.driver;
1219
+ if (driverOverride) return (await this.load(driverOverride)).swr(key, options, callback);
1220
+ return this.currentDriver.swr(key, options, callback);
1221
+ }
1222
+ /**
1223
+ * {@inheritdoc}
1224
+ */
1225
+ async pull(key) {
1226
+ this.ensureDriverInitialized();
1227
+ return this.currentDriver.pull(key);
1228
+ }
1229
+ /**
1230
+ * {@inheritdoc}
1231
+ */
1232
+ async forever(key, value) {
1233
+ this.ensureDriverInitialized();
1234
+ return this.currentDriver.forever(key, value);
1235
+ }
1236
+ /**
1237
+ * {@inheritdoc}
1238
+ */
1239
+ async increment(key, value) {
1240
+ this.ensureDriverInitialized();
1241
+ return this.currentDriver.increment(key, value);
1242
+ }
1243
+ /**
1244
+ * {@inheritdoc}
1245
+ */
1246
+ async decrement(key, value) {
1247
+ this.ensureDriverInitialized();
1248
+ return this.currentDriver.decrement(key, value);
1249
+ }
1250
+ /**
1251
+ * {@inheritdoc}
1252
+ */
1253
+ async many(keys) {
1254
+ this.ensureDriverInitialized();
1255
+ return this.currentDriver.many(keys);
1256
+ }
1257
+ /**
1258
+ * {@inheritdoc}
1259
+ */
1260
+ async setMany(items, ttl) {
1261
+ this.ensureDriverInitialized();
1262
+ return this.currentDriver.setMany(items, ttl);
1263
+ }
1264
+ /**
1265
+ * Register a global event listener (applies to all drivers)
1266
+ */
1267
+ on(event, handler) {
1268
+ if (!this.globalEventListeners.has(event)) this.globalEventListeners.set(event, /* @__PURE__ */ new Set());
1269
+ this.globalEventListeners.get(event).add(handler);
1270
+ if (this.currentDriver) this.currentDriver.on(event, handler);
1271
+ for (const driver of Object.values(this.loadedDrivers)) driver.on(event, handler);
1272
+ return this;
1273
+ }
1274
+ /**
1275
+ * Remove a global event listener
1276
+ */
1277
+ off(event, handler) {
1278
+ const handlers = this.globalEventListeners.get(event);
1279
+ if (handlers) handlers.delete(handler);
1280
+ if (this.currentDriver) this.currentDriver.off(event, handler);
1281
+ for (const driver of Object.values(this.loadedDrivers)) driver.off(event, handler);
1282
+ return this;
1283
+ }
1284
+ /**
1285
+ * Register a one-time global event listener
1286
+ */
1287
+ once(event, handler) {
1288
+ const onceHandler = async (data) => {
1289
+ await handler(data);
1290
+ this.off(event, onceHandler);
1291
+ };
1292
+ return this.on(event, onceHandler);
1293
+ }
1294
+ /**
1295
+ * Attach global listeners to a driver
1296
+ */
1297
+ attachGlobalListeners(driver) {
1298
+ for (const [event, handlers] of this.globalEventListeners) for (const handler of handlers) driver.on(event, handler);
1299
+ }
1300
+ /**
1301
+ * Set if not exists (atomic operation)
1302
+ * Returns true if key was set, false if key already existed
1303
+ * Note: Only supported by drivers that implement setNX (e.g., Redis)
1304
+ */
1305
+ async setNX(key, value, ttl) {
1306
+ this.ensureDriverInitialized();
1307
+ if (!this.currentDriver.setNX) throw new Error(`setNX is not supported by the current cache driver: ${this.currentDriver.name}`);
1308
+ return this.currentDriver.setNX(key, value, ttl);
1309
+ }
1310
+ /**
1311
+ * Create a tagged cache instance for the given tags
1312
+ */
1313
+ tags(tags) {
1314
+ this.ensureDriverInitialized();
1315
+ return this.currentDriver.tags(tags);
1316
+ }
1317
+ /**
1318
+ * Atomically read, transform, and write a cached value. Delegates to the current driver.
1319
+ */
1320
+ async update(key, fn, options) {
1321
+ this.ensureDriverInitialized();
1322
+ return this.currentDriver.update(key, fn, options);
1323
+ }
1324
+ /**
1325
+ * Shallow-merge a partial object into a cached value.
1326
+ */
1327
+ async merge(key, partial, options) {
1328
+ this.ensureDriverInitialized();
1329
+ return this.currentDriver.merge(key, partial, options);
1330
+ }
1331
+ /**
1332
+ * Obtain a list accessor bound to the current driver.
1333
+ */
1334
+ list(key) {
1335
+ this.ensureDriverInitialized();
1336
+ return this.currentDriver.list(key);
1337
+ }
1338
+ /**
1339
+ * Acquire a distributed lock, run `fn`, and auto-release. Returns a
1340
+ * {@link LockOutcome} discriminated union so callers can distinguish
1341
+ * "ran and got this value" from "skipped because someone else holds it".
1342
+ *
1343
+ * Honors the `driver` option for per-call driver override, same as `set`
1344
+ * and `remember`.
1345
+ *
1346
+ * @example
1347
+ * const outcome = await cache.lock("lock.import", "5m", async () => {
1348
+ * await runImport();
1349
+ * return "done";
1350
+ * });
1351
+ * if (!outcome.acquired) {
1352
+ * console.log("another worker is already importing");
1353
+ * }
1354
+ */
1355
+ async lock(key, ttlOrOptions, fn) {
1356
+ this.ensureDriverInitialized();
1357
+ const driverOverride = ttlOrOptions && typeof ttlOrOptions === "object" && "driver" in ttlOrOptions ? ttlOrOptions.driver : void 0;
1358
+ return (driverOverride ? await this.load(driverOverride) : this.currentDriver).lock(key, ttlOrOptions, fn);
1359
+ }
1360
+ /**
1361
+ * Similarity retrieval. Delegates to the current driver's `similar()` impl.
1362
+ *
1363
+ * Drivers that lack a similarity index throw {@link CacheUnsupportedError}.
1364
+ *
1365
+ * @example
1366
+ * const hits = await cache.similar(await embed(query), { topK: 5, threshold: 0.7 });
1367
+ */
1368
+ /**
1369
+ * Create a scoped view over the cache. Every key written through the
1370
+ * returned scope is automatically prefixed with `prefix`; optional defaults
1371
+ * (`ttl`, `tags`) flow through every write inside the scope.
1372
+ *
1373
+ * Per-call options always win over scope defaults. Scope tags merge
1374
+ * additively with per-call tags. Nested scopes inherit from the parent.
1375
+ *
1376
+ * @example
1377
+ * const chat = cache.namespace("chats.10", { ttl: "30d" });
1378
+ * await chat.set("messages.1", msg); // → "chats.10.messages.1", 30d
1379
+ * await chat.set("draft", d, { ttl: "1h" }); // per-call ttl wins
1380
+ * await chat.namespace("typing", { ttl: "5s" }).set("user.42", true);
1381
+ * await chat.clear(); // wipe the whole scope
1382
+ */
1383
+ namespace(prefix, options) {
1384
+ this.ensureDriverInitialized();
1385
+ return new ScopedCache(this, prefix, options);
1386
+ }
1387
+ async similar(vector, options) {
1388
+ this.ensureDriverInitialized();
1389
+ return this.currentDriver.similar(vector, options);
1390
+ }
1391
+ };
1392
+ const cache = new CacheManager();
1393
+
1394
+ //#endregion
1395
+ //#region ../../@warlock.js/cache/src/cached/auto-key.ts
1396
+ /**
1397
+ * Derive a cache key from a prefix and a set of function arguments.
1398
+ *
1399
+ * Rules (in order of precedence):
1400
+ * 1. No args → prefix alone.
1401
+ * 2. All primitives (`string`, `number`, `boolean`) or `null` / `undefined` /
1402
+ * `bigint` → joined onto the prefix with dots.
1403
+ * 3. Any non-primitive arg present → the full args array is `JSON.stringify`-ed
1404
+ * and appended to the prefix.
1405
+ * 4. Serialization throws (circular refs, `BigInt` nested in an object) → we
1406
+ * re-throw as `CacheConfigurationError` so the caller sees a cache-scoped
1407
+ * error rather than a cryptic `TypeError`.
1408
+ *
1409
+ * @example
1410
+ * deriveAutoKey("user", [42]); // "user.42"
1411
+ * deriveAutoKey("orders", [42, "abc"]); // "orders.42.abc"
1412
+ * deriveAutoKey("featured", []); // "featured"
1413
+ * deriveAutoKey("search", [{ q: "hello" }]); // "search.[{\"q\":\"hello\"}]"
1414
+ * deriveAutoKey("user", [null, undefined]); // "user.null.undefined"
1415
+ */
1416
+ function deriveAutoKey(prefix, args) {
1417
+ if (args.length === 0) return prefix;
1418
+ if (args.every(isPrimitiveOrNullish)) return prefix + "." + args.map(serializePrimitive).join(".");
1419
+ try {
1420
+ return prefix + "." + JSON.stringify(args);
1421
+ } catch (error) {
1422
+ throw new CacheConfigurationError(`cached(): could not derive an auto-key from args for prefix "${prefix}". The args include a value that is not JSON-serializable (circular reference, BigInt nested inside an object, or similar). Use the options form with a custom key function. Original error: ${error.message}`);
1423
+ }
1424
+ }
1425
+ /**
1426
+ * Primitives and nullish values can be concatenated directly onto a key without
1427
+ * JSON serialization. Adding `bigint` here avoids the `JSON.stringify` throw on
1428
+ * top-level bigint args.
1429
+ */
1430
+ function isPrimitiveOrNullish(value) {
1431
+ if (value === null || value === void 0) return true;
1432
+ const type = typeof value;
1433
+ return type === "string" || type === "number" || type === "boolean" || type === "bigint";
1434
+ }
1435
+ /**
1436
+ * Serialize a single primitive or nullish value to its string key-segment form.
1437
+ */
1438
+ function serializePrimitive(value) {
1439
+ if (value === null) return "null";
1440
+ if (value === void 0) return "undefined";
1441
+ if (typeof value === "bigint") return value.toString();
1442
+ return String(value);
1443
+ }
1444
+
1445
+ //#endregion
1446
+ //#region ../../@warlock.js/cache/src/cached/normalize-args.ts
1447
+ /**
1448
+ * Resolve the positional-or-options arguments of `cached()` into a single
1449
+ * normalized config. Keeps the wrapper body free of shape-checks.
1450
+ */
1451
+ function normalizeCachedArgs(prefixOrOptions, maybeTtl) {
1452
+ if (typeof prefixOrOptions === "string") {
1453
+ const prefix = prefixOrOptions;
1454
+ return {
1455
+ key: (...args) => deriveAutoKey(prefix, args),
1456
+ ttl: maybeTtl
1457
+ };
1458
+ }
1459
+ return {
1460
+ key: prefixOrOptions.key,
1461
+ ttl: prefixOrOptions.ttl,
1462
+ tags: prefixOrOptions.tags,
1463
+ driver: prefixOrOptions.driver
1464
+ };
1465
+ }
1466
+
1467
+ //#endregion
1468
+ //#region ../../@warlock.js/cache/src/cached/cached.ts
1469
+ function cached(fn, prefixOrOptions, maybeTtl) {
1470
+ const config = normalizeCachedArgs(prefixOrOptions, maybeTtl);
1471
+ const buildRememberOptions = () => ({
1472
+ ttl: config.ttl,
1473
+ tags: config.tags,
1474
+ driver: config.driver
1475
+ });
1476
+ const wrapper = (async (...args) => {
1477
+ const key = config.key(...args);
1478
+ return cache.remember(key, buildRememberOptions(), () => fn(...args));
1479
+ });
1480
+ wrapper.invalidate = async (...args) => {
1481
+ const key = config.key(...args);
1482
+ await cache.remove(key);
1483
+ };
1484
+ return wrapper;
1485
+ }
1486
+
1487
+ //#endregion
1488
+ //#region ../../@warlock.js/cache/src/list/memory-cache-list.ts
1489
+ /**
1490
+ * Generic array-backed {@link CacheListAccessor}.
1491
+ *
1492
+ * Stores the full list as a single cache entry and performs read-mutate-write
1493
+ * for every operation. Correct for any driver, but O(n) per op. The Redis
1494
+ * driver overrides `list()` to return a native-command accessor instead.
1495
+ *
1496
+ * **Role.** Fallback list accessor bound to a driver + key. Every mutation
1497
+ * fetches the array, transforms it in memory, and writes it back.
1498
+ *
1499
+ * **Responsibility.**
1500
+ * - Owns: translating list operations into array mutations + driver writes.
1501
+ * - Does NOT own: concurrency control (callers should wrap in a distributed
1502
+ * lock when multi-process writers are possible), TTL preservation across
1503
+ * ops (writes use driver defaults), or tagging of list entries.
1504
+ *
1505
+ * @example
1506
+ * // Never constructed directly — obtained via driver.list():
1507
+ * const list = cache.list<Event>("recent-events");
1508
+ * await list.push(event);
1509
+ */
1510
+ var MemoryCacheList = class {
1511
+ constructor(driver, key) {
1512
+ this.driver = driver;
1513
+ this.key = key;
1514
+ }
1515
+ /**
1516
+ * Read the backing array from the driver. Returns an empty array on miss.
1517
+ */
1518
+ async read() {
1519
+ const current = await this.driver.get(this.key);
1520
+ return Array.isArray(current) ? [...current] : [];
1521
+ }
1522
+ /**
1523
+ * Persist the backing array. Removes the entry when empty to keep the
1524
+ * store clean.
1525
+ */
1526
+ async write(items) {
1527
+ if (items.length === 0) {
1528
+ await this.driver.remove(this.key);
1529
+ return;
1530
+ }
1531
+ await this.driver.set(this.key, items);
1532
+ }
1533
+ /**
1534
+ * {@inheritdoc}
1535
+ */
1536
+ async push(...items) {
1537
+ const current = await this.read();
1538
+ current.push(...items);
1539
+ await this.write(current);
1540
+ return current.length;
1541
+ }
1542
+ /**
1543
+ * {@inheritdoc}
1544
+ */
1545
+ async unshift(...items) {
1546
+ const current = await this.read();
1547
+ current.unshift(...items);
1548
+ await this.write(current);
1549
+ return current.length;
1550
+ }
1551
+ /**
1552
+ * {@inheritdoc}
1553
+ */
1554
+ async pop() {
1555
+ const current = await this.read();
1556
+ if (current.length === 0) return null;
1557
+ const value = current.pop();
1558
+ await this.write(current);
1559
+ return value;
1560
+ }
1561
+ /**
1562
+ * {@inheritdoc}
1563
+ */
1564
+ async shift() {
1565
+ const current = await this.read();
1566
+ if (current.length === 0) return null;
1567
+ const value = current.shift();
1568
+ await this.write(current);
1569
+ return value;
1570
+ }
1571
+ /**
1572
+ * {@inheritdoc}
1573
+ */
1574
+ async slice(start, end) {
1575
+ return (await this.read()).slice(start, end);
1576
+ }
1577
+ /**
1578
+ * {@inheritdoc}
1579
+ */
1580
+ async all() {
1581
+ return this.read();
1582
+ }
1583
+ /**
1584
+ * {@inheritdoc}
1585
+ */
1586
+ async length() {
1587
+ return (await this.read()).length;
1588
+ }
1589
+ /**
1590
+ * {@inheritdoc}
1591
+ */
1592
+ async trim(start, end) {
1593
+ const trimmed = (await this.read()).slice(start, end + 1);
1594
+ await this.write(trimmed);
1595
+ }
1596
+ /**
1597
+ * {@inheritdoc}
1598
+ */
1599
+ async clear() {
1600
+ await this.driver.remove(this.key);
1601
+ }
1602
+ };
1603
+
1604
+ //#endregion
1605
+ //#region ../../@warlock.js/cache/src/tagged-cache.ts
1606
+ /**
1607
+ * Tagged Cache Wrapper
1608
+ * Wraps a cache driver to automatically manage tag relationships
1609
+ */
1610
+ var TaggedCache = class {
1611
+ /**
1612
+ * Constructor
1613
+ */
1614
+ constructor(tags, driver) {
1615
+ this.cacheTags = tags;
1616
+ this.driver = driver;
1617
+ }
1618
+ /**
1619
+ * Get the tag key prefix for storing tag-key relationships
1620
+ */
1621
+ tagKey(tag) {
1622
+ return `cache:tags:${tag}`;
1623
+ }
1624
+ /**
1625
+ * Store tag-key relationship
1626
+ */
1627
+ async storeTaggedKey(key) {
1628
+ await this.storeTagRelationship(key);
1629
+ }
1630
+ /**
1631
+ * Public alias of the tag-index writer. Called by `BaseCacheDriver.applyTags`
1632
+ * when tags are passed inline through `CacheSetOptions.tags`.
1633
+ *
1634
+ * @internal — public for cross-class use within this package; not part of the
1635
+ * stable consumer API.
1636
+ */
1637
+ async storeTagRelationship(parsedKey) {
1638
+ for (const tag of this.cacheTags) {
1639
+ const tagKey = this.tagKey(tag);
1640
+ const keys = await this.driver.get(tagKey) || [];
1641
+ if (!keys.includes(parsedKey)) {
1642
+ keys.push(parsedKey);
1643
+ await this.driver.set(tagKey, keys, Infinity);
1644
+ }
1645
+ }
1646
+ }
1647
+ /**
1648
+ * Get all keys associated with tags
1649
+ */
1650
+ async getTaggedKeys() {
1651
+ const allKeys = /* @__PURE__ */ new Set();
1652
+ for (const tag of this.cacheTags) {
1653
+ const tagKey = this.tagKey(tag);
1654
+ const keys = await this.driver.get(tagKey) || [];
1655
+ for (const key of keys) allKeys.add(key);
1656
+ }
1657
+ return allKeys;
1658
+ }
1659
+ /**
1660
+ * {@inheritdoc}
1661
+ */
1662
+ async set(key, value, ttlOrOptions) {
1663
+ const parsedKey = this.driver.parseKey(key);
1664
+ await this.driver.set(key, value, ttlOrOptions);
1665
+ await this.storeTaggedKey(parsedKey);
1666
+ return value;
1667
+ }
1668
+ /**
1669
+ * {@inheritdoc}
1670
+ */
1671
+ async get(key) {
1672
+ return this.driver.get(key);
1673
+ }
1674
+ /**
1675
+ * {@inheritdoc}
1676
+ */
1677
+ async remove(key) {
1678
+ const parsedKey = this.driver.parseKey(key);
1679
+ await this.driver.remove(key);
1680
+ for (const tag of this.cacheTags) {
1681
+ const tagKey = this.tagKey(tag);
1682
+ const updatedKeys = (await this.driver.get(tagKey) || []).filter((k) => k !== parsedKey);
1683
+ await this.driver.set(tagKey, updatedKeys, Infinity);
1684
+ }
1685
+ }
1686
+ /**
1687
+ * Invalidate (clear) all keys associated with the current tags
1688
+ */
1689
+ async invalidate() {
1690
+ const keysToRemove = await this.getTaggedKeys();
1691
+ for (const key of keysToRemove) await this.driver.remove(key);
1692
+ for (const tag of this.cacheTags) await this.driver.remove(this.tagKey(tag));
1693
+ }
1694
+ /**
1695
+ * Flush all keys associated with the current tags
1696
+ * @deprecated Use invalidate() instead for better semantics
1697
+ */
1698
+ async flush() {
1699
+ return this.invalidate();
1700
+ }
1701
+ /**
1702
+ * {@inheritdoc}
1703
+ */
1704
+ async has(key) {
1705
+ return this.driver.has(key);
1706
+ }
1707
+ /**
1708
+ * {@inheritdoc}
1709
+ */
1710
+ async remember(key, ttl, callback) {
1711
+ const value = await this.get(key);
1712
+ if (value !== null) return value;
1713
+ const result = await callback();
1714
+ await this.set(key, result, ttl);
1715
+ return result;
1716
+ }
1717
+ /**
1718
+ * {@inheritdoc}
1719
+ */
1720
+ async pull(key) {
1721
+ const value = await this.get(key);
1722
+ if (value !== null) await this.remove(key);
1723
+ return value;
1724
+ }
1725
+ /**
1726
+ * {@inheritdoc}
1727
+ */
1728
+ async forever(key, value) {
1729
+ return this.set(key, value, Infinity);
1730
+ }
1731
+ /**
1732
+ * {@inheritdoc}
1733
+ */
1734
+ async increment(key, value = 1) {
1735
+ const current = await this.get(key) || 0;
1736
+ if (typeof current !== "number") throw new Error(`Cannot increment non-numeric value for key: ${this.driver.parseKey(key)}`);
1737
+ const newValue = current + value;
1738
+ await this.set(key, newValue);
1739
+ return newValue;
1740
+ }
1741
+ /**
1742
+ * {@inheritdoc}
1743
+ */
1744
+ async decrement(key, value = 1) {
1745
+ return this.increment(key, -value);
1746
+ }
1747
+ };
1748
+
1749
+ //#endregion
1750
+ //#region ../../@warlock.js/cache/src/drivers/base-cache-driver.ts
1751
+ const messages = {
1752
+ clearing: "Clearing namespace",
1753
+ cleared: "Namespace cleared",
1754
+ fetching: "Fetching key",
1755
+ fetched: "Key fetched",
1756
+ caching: "Caching key",
1757
+ cached: "Key cached",
1758
+ flushing: "Flushing cache",
1759
+ flushed: "Cache flushed",
1760
+ removing: "Removing key",
1761
+ removed: "Key removed",
1762
+ expired: "Key expired",
1763
+ notFound: "Key not found",
1764
+ connecting: "Connecting to the cache engine.",
1765
+ connected: "Connected to the cache engine.",
1766
+ disconnecting: "Disconnecting from the cache engine.",
1767
+ disconnected: "Disconnected from the cache engine.",
1768
+ error: "Error occurred"
1769
+ };
1770
+ var BaseCacheDriver = class {
1771
+ constructor() {
1772
+ this.shouldLog = true;
1773
+ this.eventListeners = /* @__PURE__ */ new Map();
1774
+ this.locks = /* @__PURE__ */ new Map();
1775
+ }
1776
+ /**
1777
+ * {@inheritdoc}
1778
+ */
1779
+ get client() {
1780
+ return this.clientDriver || this;
1781
+ }
1782
+ /**
1783
+ * Set logging state
1784
+ */
1785
+ setLoggingState(shouldLog) {
1786
+ this.shouldLog = shouldLog;
1787
+ return this;
1788
+ }
1789
+ /**
1790
+ * Set client driver
1791
+ */
1792
+ set client(client) {
1793
+ this.clientDriver = client;
1794
+ }
1795
+ /**
1796
+ * {@inheritdoc}
1797
+ */
1798
+ parseKey(key) {
1799
+ return parseCacheKey(key, this.options);
1800
+ }
1801
+ /**
1802
+ * {@inheritdoc}
1803
+ */
1804
+ setOptions(options) {
1805
+ this.options = options || {};
1806
+ return this;
1807
+ }
1808
+ /**
1809
+ * Register an event listener
1810
+ */
1811
+ on(event, handler) {
1812
+ if (!this.eventListeners.has(event)) this.eventListeners.set(event, /* @__PURE__ */ new Set());
1813
+ this.eventListeners.get(event).add(handler);
1814
+ return this;
1815
+ }
1816
+ /**
1817
+ * Remove an event listener
1818
+ */
1819
+ off(event, handler) {
1820
+ const handlers = this.eventListeners.get(event);
1821
+ if (handlers) handlers.delete(handler);
1822
+ return this;
1823
+ }
1824
+ /**
1825
+ * Register a one-time event listener
1826
+ */
1827
+ once(event, handler) {
1828
+ const onceHandler = async (data) => {
1829
+ await handler(data);
1830
+ this.off(event, onceHandler);
1831
+ };
1832
+ return this.on(event, onceHandler);
1833
+ }
1834
+ /**
1835
+ * Emit an event to all registered listeners
1836
+ */
1837
+ async emit(event, data = {}) {
1838
+ const handlers = this.eventListeners.get(event);
1839
+ if (!handlers || handlers.size === 0) return;
1840
+ const eventData = {
1841
+ driver: this.name,
1842
+ ...data
1843
+ };
1844
+ const promises = [];
1845
+ for (const handler of handlers) try {
1846
+ const result = handler(eventData);
1847
+ if (result instanceof Promise) promises.push(result);
1848
+ } catch (error) {
1849
+ this.logError(`Error in event handler for '${event}'`, error);
1850
+ }
1851
+ if (promises.length > 0) await Promise.allSettled(promises);
1852
+ }
1853
+ /**
1854
+ * Normalize the 3rd argument of a `set` call into a single shape every driver
1855
+ * can act on. Handles TTL parsing (number | string | Infinity), `expiresAt` →
1856
+ * relative TTL conversion, and mutual-exclusion validation.
1857
+ *
1858
+ * @throws {CacheConfigurationError} when `ttl` and `expiresAt` are passed together
1859
+ * or an unparseable duration string is supplied.
1860
+ */
1861
+ resolveSetOptions(ttlOrOptions) {
1862
+ const options = normalizeToOptions(ttlOrOptions);
1863
+ return {
1864
+ ttl: resolveTtl(options.ttl, options.expiresAt, this.ttl),
1865
+ tags: options.tags,
1866
+ onConflict: options.onConflict ?? "upsert",
1867
+ vector: options.vector,
1868
+ staleAt: options.staleAt
1869
+ };
1870
+ }
1871
+ /**
1872
+ * Resolve the union of cache keys associated with any of the given tags.
1873
+ * Used by `similar()` to narrow the candidate pool before similarity ranking.
1874
+ *
1875
+ * Returns `null` when no tags are passed (callers should treat that as "no filter").
1876
+ */
1877
+ async getKeysForTags(tags) {
1878
+ if (!tags || tags.length === 0) return null;
1879
+ const allKeys = /* @__PURE__ */ new Set();
1880
+ for (const tag of tags) {
1881
+ const tagKey = `cache:tags:${tag}`;
1882
+ const keys = await this.get(tagKey) || [];
1883
+ for (const k of keys) allKeys.add(k);
1884
+ }
1885
+ return allKeys;
1886
+ }
1887
+ /**
1888
+ * Apply tag relationships after a successful write. Called by drivers once
1889
+ * the value is in storage.
1890
+ */
1891
+ async applyTags(parsedKey, tags) {
1892
+ if (tags.length === 0) return;
1893
+ await this.tags(tags).storeTagRelationship(parsedKey);
1894
+ }
1895
+ /**
1896
+ * {@inheritdoc}
1897
+ */
1898
+ async has(key) {
1899
+ return await this.get(key) !== null;
1900
+ }
1901
+ /**
1902
+ * {@inheritdoc}
1903
+ */
1904
+ async remember(key, ttlOrOptions, callback) {
1905
+ const parsedKey = this.parseKey(key);
1906
+ const setOptions = this.normalizeRememberOptions(ttlOrOptions);
1907
+ const cachedValue = await this.get(key);
1908
+ if (cachedValue) return cachedValue;
1909
+ const existingLock = this.locks.get(parsedKey);
1910
+ if (existingLock) return existingLock;
1911
+ const promise = callback().then(async (result) => {
1912
+ await this.set(key, result, setOptions);
1913
+ this.locks.delete(parsedKey);
1914
+ return result;
1915
+ }).catch((err) => {
1916
+ this.locks.delete(parsedKey);
1917
+ throw err;
1918
+ });
1919
+ this.locks.set(parsedKey, promise);
1920
+ return promise;
1921
+ }
1922
+ /**
1923
+ * Resolve the TTL-or-options arg of `remember` into a `CacheSetOptions` object
1924
+ * that can be passed straight to `set()`. Keeps the implementation unbranched.
1925
+ */
1926
+ normalizeRememberOptions(ttlOrOptions) {
1927
+ if (typeof ttlOrOptions === "number" || typeof ttlOrOptions === "string") return { ttl: ttlOrOptions };
1928
+ return {
1929
+ ttl: ttlOrOptions.ttl,
1930
+ tags: ttlOrOptions.tags
1931
+ };
1932
+ }
1933
+ /**
1934
+ * {@inheritdoc}
1935
+ *
1936
+ * Default implementation: read raw entry, branch on freshness/staleness,
1937
+ * trigger background refresh in the stale window, fall through to
1938
+ * `callback` on miss/expiry. Concurrent stale-window callers share a
1939
+ * single in-flight refresh via {@link locks}.
1940
+ *
1941
+ * Drivers without a real {@link getEntry} override degrade gracefully —
1942
+ * the synthetic entry has no `staleAt`, which the freshness check treats
1943
+ * as "always fresh," so SWR behaves like a TTL-only cached read on those
1944
+ * drivers (no background refresh, but no double-fetch either).
1945
+ */
1946
+ async swr(key, options, callback) {
1947
+ const parsedKey = this.parseKey(key);
1948
+ const freshSeconds = parseTtl(options.freshTtl);
1949
+ const staleSeconds = parseTtl(options.staleTtl);
1950
+ if (staleSeconds <= freshSeconds) throw new Error(`cache.swr: 'staleTtl' (${staleSeconds}s) must be greater than 'freshTtl' (${freshSeconds}s).`);
1951
+ const entry = await this.getEntry(key);
1952
+ const now = Date.now();
1953
+ const isExpired = entry?.expiresAt !== void 0 && entry.expiresAt <= now;
1954
+ if (!entry || isExpired) return this.swrFetchAndStore(key, options, callback, freshSeconds, staleSeconds);
1955
+ if (entry.staleAt === void 0 || entry.staleAt > now) return entry.data;
1956
+ this.scheduleSwrRefresh(parsedKey, key, options, callback, freshSeconds, staleSeconds);
1957
+ return entry.data;
1958
+ }
1959
+ /**
1960
+ * Read the raw {@link CacheData} wrapper for a key, including any
1961
+ * `expiresAt` / `staleAt` metadata. Default implementation falls back to
1962
+ * `get()` and synthesizes a metadata-less wrapper — drivers that store
1963
+ * the wrapper directly (memory, lru, file, redis, pg, mock) override
1964
+ * this to return real metadata so SWR can branch on freshness.
1965
+ */
1966
+ async getEntry(key) {
1967
+ const value = await this.get(key);
1968
+ if (value === null) return null;
1969
+ return { data: value };
1970
+ }
1971
+ /**
1972
+ * Remaining lifetime of an existing entry, in seconds — used by TTL-preserving
1973
+ * writes such as `update()` / `merge()` when the caller passes no explicit
1974
+ * `ttl`.
1975
+ *
1976
+ * - `Infinity` — the entry exists with no expiry (preserve "never expires").
1977
+ * - positive number — seconds left before the entry expires.
1978
+ * - `undefined` — the key is missing or already past its deadline; the caller
1979
+ * should fall back to the driver default TTL.
1980
+ *
1981
+ * Default reads `expiresAt` from {@link getEntry}, which the metadata-aware
1982
+ * drivers (memory, lru, mock, pg) populate. Drivers that track TTL natively
1983
+ * and don't carry `expiresAt` in their payload (Redis) override this.
1984
+ */
1985
+ async getRemainingTtl(key) {
1986
+ const entry = await this.getEntry(key);
1987
+ if (!entry) return;
1988
+ if (!entry.expiresAt || entry.expiresAt === Infinity) return Infinity;
1989
+ const remainingSeconds = Math.ceil((entry.expiresAt - Date.now()) / 1e3);
1990
+ return remainingSeconds > 0 ? remainingSeconds : void 0;
1991
+ }
1992
+ /**
1993
+ * Block-and-fetch path of `swr()`: invoked on miss or past-`staleTtl`
1994
+ * expiry. Writes through `set()` with the SWR options translated into
1995
+ * standard `CacheSetOptions` (ttl = staleTtl, staleAt = now + freshTtl).
1996
+ */
1997
+ async swrFetchAndStore(key, options, callback, freshSeconds, staleSeconds) {
1998
+ const result = await callback();
1999
+ await this.set(key, result, {
2000
+ ttl: staleSeconds,
2001
+ staleAt: Date.now() + freshSeconds * 1e3,
2002
+ tags: options.tags
2003
+ });
2004
+ return result;
2005
+ }
2006
+ /**
2007
+ * Stale-window background refresh. Registers a single in-flight promise
2008
+ * per parsed key so concurrent SWR callers share one refresh. Failed
2009
+ * refreshes preserve the stale entry, log via `logError`, and emit on
2010
+ * `error` — the stale-returning caller never sees the failure.
2011
+ */
2012
+ scheduleSwrRefresh(parsedKey, key, options, callback, freshSeconds, staleSeconds) {
2013
+ if (this.locks.has(parsedKey)) return;
2014
+ let refresh;
2015
+ refresh = (async () => {
2016
+ try {
2017
+ const result = await callback();
2018
+ await this.set(key, result, {
2019
+ ttl: staleSeconds,
2020
+ staleAt: Date.now() + freshSeconds * 1e3,
2021
+ tags: options.tags
2022
+ });
2023
+ } catch (error) {
2024
+ this.logError(`SWR background refresh failed for ${parsedKey}`, error);
2025
+ await this.emit("error", {
2026
+ key: parsedKey,
2027
+ error
2028
+ });
2029
+ } finally {
2030
+ if (this.locks.get(parsedKey) === refresh) this.locks.delete(parsedKey);
2031
+ }
2032
+ })();
2033
+ this.locks.set(parsedKey, refresh);
2034
+ }
2035
+ /**
2036
+ * {@inheritdoc}
2037
+ */
2038
+ async pull(key) {
2039
+ const value = await this.get(key);
2040
+ if (value !== null) await this.remove(key);
2041
+ return value;
2042
+ }
2043
+ /**
2044
+ * {@inheritdoc}
2045
+ */
2046
+ async forever(key, value) {
2047
+ return this.set(key, value, Infinity);
2048
+ }
2049
+ /**
2050
+ * {@inheritdoc}
2051
+ */
2052
+ async increment(key, value = 1) {
2053
+ const current = await this.get(key) || 0;
2054
+ if (typeof current !== "number") throw new Error(`Cannot increment non-numeric value for key: ${this.parseKey(key)}`);
2055
+ const newValue = current + value;
2056
+ await this.set(key, newValue);
2057
+ return newValue;
2058
+ }
2059
+ /**
2060
+ * {@inheritdoc}
2061
+ */
2062
+ async decrement(key, value = 1) {
2063
+ return this.increment(key, -value);
2064
+ }
2065
+ /**
2066
+ * {@inheritdoc}
2067
+ */
2068
+ async many(keys) {
2069
+ return Promise.all(keys.map((key) => this.get(key)));
2070
+ }
2071
+ /**
2072
+ * {@inheritdoc}
2073
+ */
2074
+ async setMany(items, ttl) {
2075
+ await Promise.all(Object.entries(items).map(([key, value]) => this.set(key, value, ttl)));
2076
+ }
2077
+ /**
2078
+ * Log the operation
2079
+ */
2080
+ log(operation, key) {
2081
+ if (!this.shouldLog) return;
2082
+ if (key) key = key.replaceAll("/", ".");
2083
+ if (operation == "notFound" || operation == "expired") return _warlock_js_logger.log.warn("cache." + this.name, operation, (key ? key + " " : "") + messages[operation]);
2084
+ if (operation.endsWith("ed")) return _warlock_js_logger.log.success("cache." + this.name, operation, (key ? key + " " : "") + messages[operation]);
2085
+ _warlock_js_logger.log.info("cache." + this.name, operation, (key ? key + " " : "") + messages[operation]);
2086
+ }
2087
+ /**
2088
+ * Log error message
2089
+ */
2090
+ logError(message, error) {
2091
+ _warlock_js_logger.log.error("cache." + this.name, "error", message);
2092
+ if (error) console.log(error);
2093
+ }
2094
+ /**
2095
+ * Get the default TTL in seconds. Parses human-readable strings (`"1h"`, `"30m"`)
2096
+ * from driver options if present; falls back to `Infinity` when no default is set.
2097
+ */
2098
+ get ttl() {
2099
+ if (this.options.ttl === void 0) return Infinity;
2100
+ return parseTtl(this.options.ttl);
2101
+ }
2102
+ /**
2103
+ * Get time to live value in milliseconds
2104
+ */
2105
+ getExpiresAt(ttl = this.ttl) {
2106
+ if (ttl) return (/* @__PURE__ */ new Date()).getTime() + ttl * 1e3;
2107
+ }
2108
+ /**
2109
+ * Wrap a value with TTL and optional freshness metadata for backend
2110
+ * storage. `staleAt` persists alongside `expiresAt` when supplied — used
2111
+ * by the SWR flow to mark when the entry stops being fresh.
2112
+ */
2113
+ prepareDataForStorage(data, ttl, staleAt) {
2114
+ const preparedData = { data };
2115
+ if (ttl) {
2116
+ preparedData.ttl = ttl;
2117
+ preparedData.expiresAt = this.getExpiresAt(ttl);
2118
+ }
2119
+ if (staleAt !== void 0) preparedData.staleAt = staleAt;
2120
+ return preparedData;
2121
+ }
2122
+ /**
2123
+ * Parse fetched data from cache
2124
+ */
2125
+ async parseCachedData(key, data) {
2126
+ this.log("fetched", key);
2127
+ if (data.expiresAt && data.expiresAt < Date.now()) {
2128
+ this.remove(key);
2129
+ return null;
2130
+ }
2131
+ const value = data.data;
2132
+ if (value === null || value === void 0) return value;
2133
+ const type = typeof value;
2134
+ if (type === "string" || type === "number" || type === "boolean") return value;
2135
+ try {
2136
+ return structuredClone(value);
2137
+ } catch (error) {
2138
+ console.log(value);
2139
+ this.logError(`Failed to clone cached value for ${key}, typeof value: ${typeof value}`, error);
2140
+ throw error;
2141
+ }
2142
+ }
2143
+ /**
2144
+ * {@inheritdoc}
2145
+ */
2146
+ async connect() {
2147
+ this.log("connecting");
2148
+ this.log("connected");
2149
+ await this.emit("connected");
2150
+ }
2151
+ /**
2152
+ * {@inheritdoc}
2153
+ */
2154
+ async disconnect() {
2155
+ this.log("disconnected");
2156
+ await this.emit("disconnected");
2157
+ }
2158
+ /**
2159
+ * Create a tagged cache instance for the given tags
2160
+ */
2161
+ tags(tags) {
2162
+ return new TaggedCache(tags, this);
2163
+ }
2164
+ /**
2165
+ * {@inheritdoc}
2166
+ *
2167
+ * Default implementation: read → transform → write under a per-key in-process
2168
+ * lock. Drivers that can offer stronger semantics (Redis via `WATCH`/`MULTI`)
2169
+ * should override.
2170
+ */
2171
+ async update(key, fn, options = {}) {
2172
+ const parsedKey = this.parseKey(key);
2173
+ const next = (this.locks.get(parsedKey) ?? Promise.resolve()).catch(() => void 0).then(async () => {
2174
+ const result = await fn(await this.get(key));
2175
+ if (result === null) {
2176
+ await this.remove(key);
2177
+ return null;
2178
+ }
2179
+ if (options.ttl !== void 0) {
2180
+ await this.set(key, result, { ttl: options.ttl });
2181
+ return result;
2182
+ }
2183
+ const remainingTtl = await this.getRemainingTtl(key);
2184
+ if (remainingTtl !== void 0) await this.set(key, result, { ttl: remainingTtl });
2185
+ else await this.set(key, result);
2186
+ return result;
2187
+ });
2188
+ this.locks.set(parsedKey, next);
2189
+ next.finally(() => {
2190
+ if (this.locks.get(parsedKey) === next) this.locks.delete(parsedKey);
2191
+ });
2192
+ return next;
2193
+ }
2194
+ /**
2195
+ * {@inheritdoc}
2196
+ */
2197
+ async merge(key, partial, options = {}) {
2198
+ return await this.update(key, (current) => {
2199
+ return {
2200
+ ...current ?? {},
2201
+ ...partial
2202
+ };
2203
+ }, options);
2204
+ }
2205
+ /**
2206
+ * {@inheritdoc}
2207
+ *
2208
+ * Default implementation: read-mutate-write array backed by the underlying
2209
+ * cache entry. Concrete drivers (e.g. Redis) override with native commands.
2210
+ */
2211
+ list(key) {
2212
+ return new MemoryCacheList(this, key);
2213
+ }
2214
+ /**
2215
+ * {@inheritdoc}
2216
+ *
2217
+ * Built on top of `set({ onConflict: "create" })` — Redis-native `SET … NX EX`
2218
+ * under the hood on Redis, emulated via key-existence check on other drivers.
2219
+ * The lock value is the resolved `owner` (defaults to `pid.<process.pid>`).
2220
+ *
2221
+ * Always releases in `finally`, even if `fn` throws — the thrown error
2222
+ * propagates to the caller unchanged.
2223
+ */
2224
+ async lock(key, ttlOrOptions, fn) {
2225
+ const { ttl, owner } = this.normalizeLockOptions(ttlOrOptions);
2226
+ const lockOwner = owner ?? `pid.${process.pid}`;
2227
+ const setResult = await this.set(key, lockOwner, {
2228
+ onConflict: "create",
2229
+ ttl
2230
+ });
2231
+ if (!(typeof setResult === "object" && setResult !== null && "wasSet" in setResult ? setResult.wasSet : true)) return { acquired: false };
2232
+ try {
2233
+ return {
2234
+ acquired: true,
2235
+ value: await fn()
2236
+ };
2237
+ } finally {
2238
+ await this.remove(key);
2239
+ }
2240
+ }
2241
+ /**
2242
+ * {@inheritdoc}
2243
+ *
2244
+ * Default implementation throws {@link CacheUnsupportedError}. Drivers that
2245
+ * support similarity retrieval (memory family, `pg`, `redis` w/ RediSearch)
2246
+ * override this with a real impl.
2247
+ */
2248
+ async similar(_vector, _options) {
2249
+ throw new CacheUnsupportedError(`'${this.name}' driver does not support similarity retrieval. Use a memory driver, 'pg' (with pgvector), or 'redis' (with RediSearch).`);
2250
+ }
2251
+ /**
2252
+ * Resolve the TTL-or-options arg of `lock` into a uniform shape.
2253
+ */
2254
+ normalizeLockOptions(ttlOrOptions) {
2255
+ if (typeof ttlOrOptions === "number" || typeof ttlOrOptions === "string") return { ttl: ttlOrOptions };
2256
+ return {
2257
+ ttl: ttlOrOptions.ttl,
2258
+ owner: ttlOrOptions.owner
2259
+ };
2260
+ }
2261
+ };
2262
+
2263
+ //#endregion
2264
+ //#region ../../@warlock.js/cache/src/drivers/file-cache-driver.ts
2265
+ var FileCacheDriver = class extends BaseCacheDriver {
2266
+ constructor(..._args) {
2267
+ super(..._args);
2268
+ this.name = "file";
2269
+ }
2270
+ /**
2271
+ * {@inheritdoc}
2272
+ */
2273
+ setOptions(options) {
2274
+ if (!options.directory) throw new CacheConfigurationError("File driver requires 'directory' option to be configured.");
2275
+ return super.setOptions(options);
2276
+ }
2277
+ /**
2278
+ * Get the cache directory
2279
+ */
2280
+ get directory() {
2281
+ const directory = this.options.directory;
2282
+ if (typeof directory === "function") return directory();
2283
+ throw new CacheConfigurationError("Cache directory is not defined, please define it in the file driver options");
2284
+ }
2285
+ /**
2286
+ * Get file name
2287
+ */
2288
+ get fileName() {
2289
+ const fileName = this.options.fileName;
2290
+ if (typeof fileName === "function") return fileName();
2291
+ return "cache.json";
2292
+ }
2293
+ /**
2294
+ * {@inheritdoc}
2295
+ */
2296
+ async removeNamespace(namespace) {
2297
+ this.log("clearing", namespace);
2298
+ try {
2299
+ await (0, _warlock_js_fs.removeDirectoryAsync)(path.default.resolve(this.directory, namespace));
2300
+ this.log("cleared", namespace);
2301
+ } catch (error) {}
2302
+ return this;
2303
+ }
2304
+ /**
2305
+ * {@inheritdoc}
2306
+ */
2307
+ async set(key, value, ttlOrOptions) {
2308
+ const parsedKey = this.parseKey(key);
2309
+ const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);
2310
+ if (vector) throw new CacheUnsupportedError("'file' driver does not support similarity retrieval — use a memory driver, 'pg' (with pgvector), or 'redis' (with RediSearch).");
2311
+ this.log("caching", parsedKey);
2312
+ const existing = onConflict === "upsert" ? null : await this.get(key);
2313
+ const exists = existing !== null;
2314
+ if (onConflict === "create" && exists) return {
2315
+ wasSet: false,
2316
+ existing
2317
+ };
2318
+ if (onConflict === "update" && !exists) return {
2319
+ wasSet: false,
2320
+ existing: null
2321
+ };
2322
+ const data = this.prepareDataForStorage(value, ttl, staleAt);
2323
+ const fileDirectory = path.default.resolve(this.directory, parsedKey);
2324
+ await (0, _warlock_js_fs.ensureDirectoryAsync)(fileDirectory);
2325
+ await (0, _warlock_js_fs.putJsonFileAsync)(path.default.resolve(fileDirectory, this.fileName), data);
2326
+ if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
2327
+ this.log("cached", parsedKey);
2328
+ await this.emit("set", {
2329
+ key: parsedKey,
2330
+ value,
2331
+ ttl
2332
+ });
2333
+ if (onConflict === "create" || onConflict === "update") return {
2334
+ wasSet: true,
2335
+ existing: null
2336
+ };
2337
+ return this;
2338
+ }
2339
+ /**
2340
+ * {@inheritdoc}
2341
+ *
2342
+ * File driver does not yet ship with a file-lock primitive, so concurrent
2343
+ * writers could clobber each other. Rather than ship an unsafe default, we
2344
+ * throw — consumers can fall back to memory/redis for `update` until a
2345
+ * proper file lock lands (tracked in `domains/cache/backlog.md`).
2346
+ */
2347
+ async update() {
2348
+ throw new CacheUnsupportedError("`update()` is not supported on the file driver. Use the memory or redis driver, or wait for the file-lock primitive (see domains/cache/backlog.md).");
2349
+ }
2350
+ /**
2351
+ * {@inheritdoc}
2352
+ */
2353
+ async merge() {
2354
+ throw new CacheUnsupportedError("`merge()` is not supported on the file driver. Use the memory or redis driver.");
2355
+ }
2356
+ /**
2357
+ * Read the raw {@link CacheData} wrapper from disk, including `staleAt`
2358
+ * metadata. Returns `null` for missing or expired files — `swr()`
2359
+ * consumes this to branch on freshness.
2360
+ */
2361
+ async getEntry(key) {
2362
+ const parsedKey = this.parseKey(key);
2363
+ const fileDirectory = path.default.resolve(this.directory, parsedKey);
2364
+ try {
2365
+ const entry = await (0, _warlock_js_fs.getJsonFileAsync)(path.default.resolve(fileDirectory, this.fileName));
2366
+ if (!entry) return null;
2367
+ if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) return null;
2368
+ return entry;
2369
+ } catch {
2370
+ return null;
2371
+ }
2372
+ }
2373
+ /**
2374
+ * {@inheritdoc}
2375
+ */
2376
+ async get(key) {
2377
+ const parsedKey = this.parseKey(key);
2378
+ this.log("fetching", parsedKey);
2379
+ const fileDirectory = path.default.resolve(this.directory, parsedKey);
2380
+ try {
2381
+ const value = await (0, _warlock_js_fs.getJsonFileAsync)(path.default.resolve(fileDirectory, this.fileName));
2382
+ const result = await this.parseCachedData(parsedKey, value);
2383
+ if (result === null) await this.emit("miss", { key: parsedKey });
2384
+ else await this.emit("hit", {
2385
+ key: parsedKey,
2386
+ value: result
2387
+ });
2388
+ return result;
2389
+ } catch (error) {
2390
+ this.log("notFound", parsedKey);
2391
+ await this.emit("miss", { key: parsedKey });
2392
+ await this.remove(key);
2393
+ return null;
2394
+ }
2395
+ }
2396
+ /**
2397
+ * {@inheritdoc}
2398
+ */
2399
+ async remove(key) {
2400
+ const parsedKey = this.parseKey(key);
2401
+ this.log("removing", parsedKey);
2402
+ const fileDirectory = path.default.resolve(this.directory, parsedKey);
2403
+ try {
2404
+ await (0, _warlock_js_fs.removeDirectoryAsync)(fileDirectory);
2405
+ this.log("removed", parsedKey);
2406
+ await this.emit("removed", { key: parsedKey });
2407
+ } catch (error) {}
2408
+ }
2409
+ /**
2410
+ * {@inheritdoc}
2411
+ */
2412
+ async flush() {
2413
+ this.log("flushing");
2414
+ if (this.options.globalPrefix) await this.removeNamespace("");
2415
+ else await (0, _warlock_js_fs.removeDirectoryAsync)(this.directory);
2416
+ this.log("flushed");
2417
+ await this.emit("flushed");
2418
+ }
2419
+ /**
2420
+ * {@inheritdoc}
2421
+ */
2422
+ async connect() {
2423
+ this.log("connecting");
2424
+ await (0, _warlock_js_fs.ensureDirectoryAsync)(this.directory);
2425
+ this.log("connected");
2426
+ await this.emit("connected");
2427
+ }
2428
+ };
2429
+
2430
+ //#endregion
2431
+ //#region ../../@warlock.js/cache/src/drivers/lru-memory-cache-driver.ts
2432
+ var CacheNode = class {
2433
+ constructor(key, value, ttl) {
2434
+ this.key = key;
2435
+ this.value = value;
2436
+ this.next = null;
2437
+ this.prev = null;
2438
+ if (ttl && ttl !== Infinity) this.expiresAt = Date.now() + ttl * 1e3;
2439
+ }
2440
+ get isExpired() {
2441
+ return this.expiresAt !== void 0 && this.expiresAt < Date.now();
2442
+ }
2443
+ };
2444
+ /**
2445
+ * LRU Memory Cache Driver
2446
+ * The concept of LRU is to remove the least recently used data
2447
+ * whenever the cache is full
2448
+ * The question that resides here is how to tell the cache is full?
2449
+ */
2450
+ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
2451
+ /**
2452
+ * {@inheritdoc}
2453
+ */
2454
+ constructor() {
2455
+ super();
2456
+ this.name = "lru";
2457
+ this.cache = /* @__PURE__ */ new Map();
2458
+ this.head = new CacheNode("", null);
2459
+ this.tail = new CacheNode("", null);
2460
+ this.init();
2461
+ this.startCleanup();
2462
+ }
2463
+ /**
2464
+ * Initialize the cache
2465
+ */
2466
+ init() {
2467
+ this.head.next = this.tail;
2468
+ this.tail.prev = this.head;
2469
+ }
2470
+ /**
2471
+ * Start the cleanup process for expired items
2472
+ */
2473
+ startCleanup() {
2474
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
2475
+ this.cleanupInterval = setInterval(async () => {
2476
+ const now = Date.now();
2477
+ const expiredKeys = [];
2478
+ for (const [key, node] of this.cache) if (node.expiresAt && node.expiresAt <= now) expiredKeys.push(key);
2479
+ for (const key of expiredKeys) {
2480
+ const node = this.cache.get(key);
2481
+ if (node) {
2482
+ this.removeNode(node);
2483
+ this.cache.delete(key);
2484
+ this.log("expired", key);
2485
+ await this.emit("expired", { key });
2486
+ }
2487
+ }
2488
+ }, 1e3);
2489
+ this.cleanupInterval.unref();
2490
+ }
2491
+ /**
2492
+ * {@inheritdoc}
2493
+ *
2494
+ * Clears every entry whose key starts with the parsed namespace (followed
2495
+ * by a dot) or equals it exactly. Called with an empty namespace while a
2496
+ * `globalPrefix` is configured, clears everything under the prefix — which
2497
+ * is how `flush()` scopes cleanup per tenant.
2498
+ */
2499
+ async removeNamespace(namespace) {
2500
+ const parsedNamespace = this.parseKey(namespace);
2501
+ this.log("clearing", parsedNamespace || "(all)");
2502
+ const removed = [];
2503
+ if (parsedNamespace === "") for (const key of this.cache.keys()) removed.push(key);
2504
+ else {
2505
+ const prefix = parsedNamespace + ".";
2506
+ for (const key of this.cache.keys()) if (key === parsedNamespace || key.startsWith(prefix)) removed.push(key);
2507
+ }
2508
+ for (const key of removed) {
2509
+ const node = this.cache.get(key);
2510
+ if (node) {
2511
+ this.removeNode(node);
2512
+ this.cache.delete(key);
2513
+ }
2514
+ await this.emit("removed", { key });
2515
+ }
2516
+ this.log("cleared", parsedNamespace || "(all)");
2517
+ return removed;
2518
+ }
2519
+ /**
2520
+ * {@inheritdoc}
2521
+ */
2522
+ async set(key, value, ttlOrOptions) {
2523
+ const parsedKey = this.parseKey(key);
2524
+ const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);
2525
+ this.log("caching", parsedKey);
2526
+ let existingNode = this.cache.get(parsedKey);
2527
+ if (existingNode && existingNode.isExpired) {
2528
+ this.removeNode(existingNode);
2529
+ this.cache.delete(parsedKey);
2530
+ existingNode = void 0;
2531
+ }
2532
+ const exists = Boolean(existingNode);
2533
+ if (onConflict === "create" && exists) return {
2534
+ wasSet: false,
2535
+ existing: existingNode.value
2536
+ };
2537
+ if (onConflict === "update" && !exists) return {
2538
+ wasSet: false,
2539
+ existing: null
2540
+ };
2541
+ if (existingNode) {
2542
+ existingNode.value = value;
2543
+ if (ttl && ttl !== Infinity) existingNode.expiresAt = Date.now() + ttl * 1e3;
2544
+ else existingNode.expiresAt = void 0;
2545
+ existingNode.staleAt = staleAt;
2546
+ if (vector) existingNode.vector = vector.slice();
2547
+ this.moveHead(existingNode);
2548
+ } else {
2549
+ const newNode = new CacheNode(parsedKey, value, ttl);
2550
+ newNode.staleAt = staleAt;
2551
+ if (vector) newNode.vector = vector.slice();
2552
+ this.cache.set(parsedKey, newNode);
2553
+ this.addNode(newNode);
2554
+ if (this.cache.size > this.capacity) this.removeTail();
2555
+ }
2556
+ if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
2557
+ this.log("cached", parsedKey);
2558
+ await this.emit("set", {
2559
+ key: parsedKey,
2560
+ value,
2561
+ ttl
2562
+ });
2563
+ if (onConflict === "create" || onConflict === "update") return {
2564
+ wasSet: true,
2565
+ existing: null
2566
+ };
2567
+ return this;
2568
+ }
2569
+ /**
2570
+ * Move the node to the head
2571
+ */
2572
+ moveHead(node) {
2573
+ this.removeNode(node);
2574
+ this.addNode(node);
2575
+ }
2576
+ /**
2577
+ * Remove the node from the cache
2578
+ */
2579
+ removeNode(node) {
2580
+ node.prev.next = node.next;
2581
+ node.next.prev = node.prev;
2582
+ }
2583
+ /**
2584
+ * Add the node to the head
2585
+ */
2586
+ addNode(node) {
2587
+ node.next = this.head.next;
2588
+ node.prev = this.head;
2589
+ this.head.next.prev = node;
2590
+ this.head.next = node;
2591
+ }
2592
+ /**
2593
+ * Remove the tail node
2594
+ */
2595
+ removeTail() {
2596
+ const node = this.tail.prev;
2597
+ this.removeNode(node);
2598
+ this.cache.delete(node.key);
2599
+ }
2600
+ /**
2601
+ * Read the raw {@link CacheData} wrapper, including `staleAt` metadata.
2602
+ * Returns `null` for missing or expired nodes — `swr()` consumes this
2603
+ * to branch on freshness without going through `get()`'s clone-and-emit
2604
+ * path.
2605
+ */
2606
+ async getEntry(key) {
2607
+ const parsedKey = this.parseKey(key);
2608
+ const node = this.cache.get(parsedKey);
2609
+ if (!node || node.isExpired) return null;
2610
+ return {
2611
+ data: node.value,
2612
+ expiresAt: node.expiresAt,
2613
+ staleAt: node.staleAt
2614
+ };
2615
+ }
2616
+ /**
2617
+ * {@inheritdoc}
2618
+ */
2619
+ async get(key) {
2620
+ const parsedKey = this.parseKey(key);
2621
+ this.log("fetching", parsedKey);
2622
+ const node = this.cache.get(parsedKey);
2623
+ if (!node) {
2624
+ this.log("notFound", parsedKey);
2625
+ await this.emit("miss", { key: parsedKey });
2626
+ return null;
2627
+ }
2628
+ if (node.isExpired) {
2629
+ this.removeNode(node);
2630
+ this.cache.delete(parsedKey);
2631
+ this.log("expired", parsedKey);
2632
+ await this.emit("expired", { key: parsedKey });
2633
+ await this.emit("miss", { key: parsedKey });
2634
+ return null;
2635
+ }
2636
+ this.moveHead(node);
2637
+ this.log("fetched", parsedKey);
2638
+ const value = node.value;
2639
+ if (value === null || value === void 0) return value;
2640
+ const type = typeof value;
2641
+ if (type === "string" || type === "number" || type === "boolean") {
2642
+ await this.emit("hit", {
2643
+ key: parsedKey,
2644
+ value
2645
+ });
2646
+ return value;
2647
+ }
2648
+ try {
2649
+ const clonedValue = structuredClone(value);
2650
+ await this.emit("hit", {
2651
+ key: parsedKey,
2652
+ value: clonedValue
2653
+ });
2654
+ return clonedValue;
2655
+ } catch (error) {
2656
+ this.logError(`Failed to clone cached value for ${parsedKey}`, error);
2657
+ throw error;
2658
+ }
2659
+ }
2660
+ /**
2661
+ * {@inheritdoc}
2662
+ */
2663
+ async remove(key) {
2664
+ const parsedKey = this.parseKey(key);
2665
+ this.log("removing", parsedKey);
2666
+ const node = this.cache.get(parsedKey);
2667
+ if (node) {
2668
+ this.removeNode(node);
2669
+ this.cache.delete(parsedKey);
2670
+ }
2671
+ this.log("removed", parsedKey);
2672
+ await this.emit("removed", { key: parsedKey });
2673
+ }
2674
+ /**
2675
+ * {@inheritdoc}
2676
+ *
2677
+ * When a `globalPrefix` is configured, `flush` scopes itself to that prefix
2678
+ * so multi-tenant caches don't accidentally wipe sibling tenants. Without
2679
+ * a prefix, clears everything.
2680
+ */
2681
+ async flush() {
2682
+ this.log("flushing");
2683
+ if (this.options.globalPrefix) await this.removeNamespace("");
2684
+ else {
2685
+ this.cache.clear();
2686
+ this.init();
2687
+ }
2688
+ this.log("flushed");
2689
+ await this.emit("flushed");
2690
+ }
2691
+ /**
2692
+ * {@inheritdoc}
2693
+ *
2694
+ * Brute-force O(N) cosine similarity over every cached node that carries a
2695
+ * vector. Suitable for development and small in-memory knowledge bases —
2696
+ * not for production beyond ~10k entries.
2697
+ *
2698
+ * @warning Dev-only — O(N) per query.
2699
+ */
2700
+ async similar(vector, options) {
2701
+ const tagFilter = await this.getKeysForTags(options.tags);
2702
+ const hits = [];
2703
+ for (const [parsedKey, node] of this.cache) {
2704
+ if (!node.vector) continue;
2705
+ if (node.isExpired) continue;
2706
+ if (tagFilter && !tagFilter.has(parsedKey)) continue;
2707
+ const score = cosineSimilarity(vector, node.vector);
2708
+ if (options.threshold !== void 0 && score < options.threshold) continue;
2709
+ let value = node.value;
2710
+ if (value !== null && value !== void 0) {
2711
+ const t = typeof value;
2712
+ if (t !== "string" && t !== "number" && t !== "boolean") value = structuredClone(value);
2713
+ }
2714
+ hits.push({
2715
+ key: parsedKey,
2716
+ value,
2717
+ score
2718
+ });
2719
+ }
2720
+ hits.sort((a, b) => b.score - a.score);
2721
+ if (options.topK >= 0 && hits.length > options.topK) hits.length = options.topK;
2722
+ return hits;
2723
+ }
2724
+ /**
2725
+ * Get lru capacity
2726
+ */
2727
+ get capacity() {
2728
+ return this.options.capacity || 1e3;
2729
+ }
2730
+ /**
2731
+ * {@inheritdoc}
2732
+ */
2733
+ async disconnect() {
2734
+ if (this.cleanupInterval) {
2735
+ clearInterval(this.cleanupInterval);
2736
+ this.cleanupInterval = void 0;
2737
+ }
2738
+ await super.disconnect();
2739
+ }
2740
+ };
2741
+
2742
+ //#endregion
2743
+ //#region ../../@warlock.js/cache/src/drivers/memory-cache-driver.ts
2744
+ var MemoryCacheDriver = class extends BaseCacheDriver {
2745
+ /**
2746
+ * {@inheritdoc}
2747
+ */
2748
+ constructor() {
2749
+ super();
2750
+ this.name = "memory";
2751
+ this.data = {};
2752
+ this.temporaryData = {};
2753
+ this.accessOrder = [];
2754
+ this.vectorIndex = /* @__PURE__ */ new Map();
2755
+ this.startCleanup();
2756
+ }
2757
+ /**
2758
+ * Start the cleanup process whenever a data that has a cache key is set
2759
+ */
2760
+ startCleanup() {
2761
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
2762
+ this.cleanupInterval = setInterval(async () => {
2763
+ const now = Date.now();
2764
+ for (const key in this.temporaryData) if (this.temporaryData[key].expiresAt <= now) {
2765
+ await this.remove(this.temporaryData[key].key);
2766
+ delete this.temporaryData[key];
2767
+ this.log("expired", key);
2768
+ await this.emit("expired", { key });
2769
+ }
2770
+ }, 1e3);
2771
+ this.cleanupInterval.unref();
2772
+ }
2773
+ /**
2774
+ * {@inheritdoc}
2775
+ */
2776
+ async removeNamespace(namespace) {
2777
+ this.log("clearing", namespace);
2778
+ namespace = this.parseKey(namespace);
2779
+ (0, _mongez_reinforcements.unset)(this.data, [namespace]);
2780
+ if (namespace === "") this.vectorIndex.clear();
2781
+ else {
2782
+ const prefix = namespace + ".";
2783
+ for (const k of [...this.vectorIndex.keys()]) if (k === namespace || k.startsWith(prefix)) this.vectorIndex.delete(k);
2784
+ }
2785
+ this.log("cleared", namespace);
2786
+ return this;
2787
+ }
2788
+ /**
2789
+ * {@inheritdoc}
2790
+ */
2791
+ async set(key, value, ttlOrOptions) {
2792
+ const parsedKey = this.parseKey(key);
2793
+ const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);
2794
+ this.log("caching", parsedKey);
2795
+ const existingValue = onConflict === "upsert" ? null : await this.get(key);
2796
+ const exists = existingValue !== null;
2797
+ if (onConflict === "create" && exists) return {
2798
+ wasSet: false,
2799
+ existing: existingValue
2800
+ };
2801
+ if (onConflict === "update" && !exists) return {
2802
+ wasSet: false,
2803
+ existing: null
2804
+ };
2805
+ const data = this.prepareDataForStorage(value, ttl, staleAt);
2806
+ if (ttl) this.setTemporaryData(key, parsedKey, ttl);
2807
+ (0, _mongez_reinforcements.set)(this.data, parsedKey, data);
2808
+ this.trackAccess(parsedKey);
2809
+ if (!exists && this.options.maxSize) await this.enforceMaxSize();
2810
+ if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
2811
+ if (vector) this.vectorIndex.set(parsedKey, vector.slice());
2812
+ this.log("cached", parsedKey);
2813
+ await this.emit("set", {
2814
+ key: parsedKey,
2815
+ value,
2816
+ ttl
2817
+ });
2818
+ if (onConflict === "create" || onConflict === "update") return {
2819
+ wasSet: true,
2820
+ existing: null
2821
+ };
2822
+ return this;
2823
+ }
2824
+ /**
2825
+ * {@inheritdoc}
2826
+ */
2827
+ async get(key) {
2828
+ const parsedKey = this.parseKey(key);
2829
+ this.log("fetching", parsedKey);
2830
+ const value = (0, _mongez_reinforcements.get)(this.data, parsedKey);
2831
+ if (!value) {
2832
+ this.log("notFound", parsedKey);
2833
+ await this.emit("miss", { key: parsedKey });
2834
+ return null;
2835
+ }
2836
+ const result = await this.parseCachedData(parsedKey, value);
2837
+ if (result === null) await this.emit("miss", { key: parsedKey });
2838
+ else {
2839
+ this.trackAccess(parsedKey);
2840
+ await this.emit("hit", {
2841
+ key: parsedKey,
2842
+ value: result
2843
+ });
2844
+ }
2845
+ return result;
2846
+ }
2847
+ /**
2848
+ * Read the raw {@link CacheData} wrapper, including `staleAt` metadata.
2849
+ * Returns `null` for missing or expired entries so the SWR flow can branch
2850
+ * cleanly. Does not emit `hit`/`miss` events — that's `get()`'s job.
2851
+ */
2852
+ async getEntry(key) {
2853
+ const parsedKey = this.parseKey(key);
2854
+ const entry = (0, _mongez_reinforcements.get)(this.data, parsedKey);
2855
+ if (!entry) return null;
2856
+ if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) return null;
2857
+ return entry;
2858
+ }
2859
+ /**
2860
+ * {@inheritdoc}
2861
+ */
2862
+ async remove(key) {
2863
+ const parsedKey = this.parseKey(key);
2864
+ this.log("removing", parsedKey);
2865
+ (0, _mongez_reinforcements.unset)(this.data, [parsedKey]);
2866
+ delete this.temporaryData[parsedKey];
2867
+ this.removeFromAccessOrder(parsedKey);
2868
+ this.vectorIndex.delete(parsedKey);
2869
+ this.log("removed", parsedKey);
2870
+ await this.emit("removed", { key: parsedKey });
2871
+ }
2872
+ /**
2873
+ * {@inheritdoc}
2874
+ */
2875
+ async flush() {
2876
+ this.log("flushing");
2877
+ if (this.options.globalPrefix) this.removeNamespace("");
2878
+ else {
2879
+ this.data = {};
2880
+ this.accessOrder = [];
2881
+ this.vectorIndex.clear();
2882
+ }
2883
+ this.log("flushed");
2884
+ await this.emit("flushed");
2885
+ }
2886
+ /**
2887
+ * Set the temporary data
2888
+ */
2889
+ setTemporaryData(key, parsedKey, ttl) {
2890
+ this.temporaryData[parsedKey] = {
2891
+ key: JSON.stringify(key),
2892
+ expiresAt: Date.now() + ttl * 1e3
2893
+ };
2894
+ }
2895
+ /**
2896
+ * Track access for LRU eviction
2897
+ */
2898
+ trackAccess(key) {
2899
+ if (!this.options.maxSize) return;
2900
+ const index = this.accessOrder.indexOf(key);
2901
+ if (index > -1) this.accessOrder.splice(index, 1);
2902
+ this.accessOrder.push(key);
2903
+ }
2904
+ /**
2905
+ * Remove key from access order tracking
2906
+ */
2907
+ removeFromAccessOrder(key) {
2908
+ const index = this.accessOrder.indexOf(key);
2909
+ if (index > -1) this.accessOrder.splice(index, 1);
2910
+ }
2911
+ /**
2912
+ * Enforce max size by evicting least recently used items.
2913
+ *
2914
+ * Recomputes the live cache size on every iteration — a single snapshot at
2915
+ * the top of the loop would go stale and cause this routine to evict every
2916
+ * entry in `accessOrder` (including the just-inserted key).
2917
+ */
2918
+ async enforceMaxSize() {
2919
+ if (!this.options.maxSize) return;
2920
+ while (this.getCacheSize() > this.options.maxSize && this.accessOrder.length > 0) {
2921
+ const lruKey = this.accessOrder.shift();
2922
+ if (!lruKey) break;
2923
+ this.log("removing", lruKey);
2924
+ (0, _mongez_reinforcements.unset)(this.data, [lruKey]);
2925
+ delete this.temporaryData[lruKey];
2926
+ this.vectorIndex.delete(lruKey);
2927
+ this.log("removed", lruKey);
2928
+ }
2929
+ }
2930
+ /**
2931
+ * Get current cache size (number of cached items)
2932
+ */
2933
+ getCacheSize() {
2934
+ return Object.keys(this.data).length;
2935
+ }
2936
+ /**
2937
+ * {@inheritdoc}
2938
+ *
2939
+ * Brute-force O(N) cosine similarity over every entry that was written with
2940
+ * `set({ vector })`. Suitable for development and small in-memory knowledge
2941
+ * bases — not for production beyond ~10k entries. Use the `pg` driver
2942
+ * (with pgvector) or `redis` (with RediSearch) at scale.
2943
+ *
2944
+ * @warning Dev-only — O(N) per query.
2945
+ */
2946
+ async similar(vector, options) {
2947
+ const tagFilter = await this.getKeysForTags(options.tags);
2948
+ const hits = [];
2949
+ for (const [parsedKey, stored] of this.vectorIndex) {
2950
+ if (tagFilter && !tagFilter.has(parsedKey)) continue;
2951
+ const value = await this.get(parsedKey);
2952
+ if (value === null) continue;
2953
+ const score = cosineSimilarity(vector, stored);
2954
+ if (options.threshold !== void 0 && score < options.threshold) continue;
2955
+ hits.push({
2956
+ key: parsedKey,
2957
+ value,
2958
+ score
2959
+ });
2960
+ }
2961
+ hits.sort((a, b) => b.score - a.score);
2962
+ if (options.topK >= 0 && hits.length > options.topK) hits.length = options.topK;
2963
+ return hits;
2964
+ }
2965
+ /**
2966
+ * {@inheritdoc}
2967
+ */
2968
+ async disconnect() {
2969
+ if (this.cleanupInterval) {
2970
+ clearInterval(this.cleanupInterval);
2971
+ this.cleanupInterval = void 0;
2972
+ }
2973
+ await super.disconnect();
2974
+ }
2975
+ };
2976
+
2977
+ //#endregion
2978
+ //#region ../../@warlock.js/cache/src/drivers/memory-extended-cache-driver.ts
2979
+ var MemoryExtendedCacheDriver = class extends MemoryCacheDriver {
2980
+ constructor(..._args) {
2981
+ super(..._args);
2982
+ this.name = "memoryExtended";
2983
+ }
2984
+ /**
2985
+ * {@inheritdoc}
2986
+ */
2987
+ async get(key) {
2988
+ const parsedKey = this.parseKey(key);
2989
+ this.log("fetching", parsedKey);
2990
+ const value = (0, _mongez_reinforcements.get)(this.data, parsedKey);
2991
+ if (!value) {
2992
+ this.log("notFound", parsedKey);
2993
+ return null;
2994
+ }
2995
+ const rawTtl = value.ttl ?? this.options.ttl;
2996
+ const ttl = rawTtl !== void 0 ? parseTtl(rawTtl) : void 0;
2997
+ if (ttl) {
2998
+ this.setTemporaryData(key, parsedKey, ttl);
2999
+ value.expiresAt = this.getExpiresAt(ttl);
3000
+ }
3001
+ return this.parseCachedData(parsedKey, value);
3002
+ }
3003
+ };
3004
+
3005
+ //#endregion
3006
+ //#region ../../@warlock.js/cache/src/drivers/mock-cache-driver.ts
3007
+ /**
3008
+ * In-memory cache driver with introspection helpers, intended for use as a
3009
+ * test double in downstream packages.
3010
+ *
3011
+ * **Role.** Drop-in replacement for any real driver in test setups, with
3012
+ * extra surface that makes behavioral assertions easy: every public op gets
3013
+ * recorded into {@link MockCacheDriver.callLog}, and {@link wasCalled} /
3014
+ * {@link getStored} / {@link reset} let tests verify side effects without
3015
+ * pulling in a real Redis / Postgres / file system.
3016
+ *
3017
+ * **Responsibility.**
3018
+ * - Owns: in-memory storage backed by a `Map`, the `callLog`, TTL handling
3019
+ * via `parseCachedData`, `onConflict` policies, and tag-index storage.
3020
+ * - Does NOT own: similarity retrieval (vectors are recorded into the
3021
+ * `callLog` but `similar()` throws — use `MemoryCacheDriver` for tests
3022
+ * that need real nearest-neighbor scoring), connection lifecycle
3023
+ * (no-op `connect`/`disconnect`), or eviction (no `maxSize`).
3024
+ *
3025
+ * Register it like any other driver — sub-paths are not part of the
3026
+ * package's export convention; the same single barrel ships it next to
3027
+ * the production drivers.
3028
+ *
3029
+ * @example
3030
+ * import { cache, MockCacheDriver } from "@warlock.js/cache";
3031
+ *
3032
+ * beforeEach(async () => {
3033
+ * cache.setCacheConfigurations({
3034
+ * default: "mock",
3035
+ * drivers: { mock: MockCacheDriver },
3036
+ * options: { mock: {} },
3037
+ * });
3038
+ * await cache.init();
3039
+ * });
3040
+ *
3041
+ * it("invalidates the user cache after update", async () => {
3042
+ * await userService.update(42, { name: "Jane" });
3043
+ *
3044
+ * const driver = cache.currentDriver as MockCacheDriver;
3045
+ * expect(driver.wasCalled("remove", "users.42")).toBe(true);
3046
+ * });
3047
+ */
3048
+ var MockCacheDriver = class extends BaseCacheDriver {
3049
+ constructor(..._args) {
3050
+ super(..._args);
3051
+ this.name = "mock";
3052
+ this.options = {};
3053
+ this.storage = /* @__PURE__ */ new Map();
3054
+ this.callLog = [];
3055
+ }
3056
+ /**
3057
+ * Standard driver setup. Mirrors the null driver — no connection, no
3058
+ * resources to release.
3059
+ */
3060
+ async connect() {
3061
+ this.recordCall("connect", void 0);
3062
+ await super.connect();
3063
+ }
3064
+ /**
3065
+ * Standard driver teardown.
3066
+ */
3067
+ async disconnect() {
3068
+ this.recordCall("disconnect", void 0);
3069
+ await super.disconnect();
3070
+ }
3071
+ /**
3072
+ * Wipe everything under `namespace`. Matches `MemoryCacheDriver` semantics
3073
+ * — the namespace itself and any key with the namespace prefix is removed.
3074
+ */
3075
+ async removeNamespace(namespace) {
3076
+ const parsed = this.parseKey(namespace);
3077
+ this.recordCall("removeNamespace", parsed, [namespace]);
3078
+ this.log("clearing", parsed);
3079
+ const prefix = parsed + ".";
3080
+ for (const key of [...this.storage.keys()]) if (key === parsed || key.startsWith(prefix)) this.storage.delete(key);
3081
+ this.log("cleared", parsed);
3082
+ }
3083
+ /**
3084
+ * Standard `set` with full `onConflict` support. Honors scope-default TTL
3085
+ * via {@link BaseCacheDriver.resolveSetOptions}.
3086
+ */
3087
+ async set(key, value, ttlOrOptions) {
3088
+ const parsedKey = this.parseKey(key);
3089
+ const { ttl, tags, onConflict, staleAt } = this.resolveSetOptions(ttlOrOptions);
3090
+ this.recordCall("set", parsedKey, [value, ttlOrOptions]);
3091
+ this.log("caching", parsedKey);
3092
+ const existing = onConflict === "upsert" ? null : await this.get(key);
3093
+ const exists = existing !== null;
3094
+ if (onConflict === "create" && exists) return {
3095
+ wasSet: false,
3096
+ existing
3097
+ };
3098
+ if (onConflict === "update" && !exists) return {
3099
+ wasSet: false,
3100
+ existing: null
3101
+ };
3102
+ const data = this.prepareDataForStorage(value, ttl, staleAt);
3103
+ this.storage.set(parsedKey, data);
3104
+ if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3105
+ this.log("cached", parsedKey);
3106
+ await this.emit("set", {
3107
+ key: parsedKey,
3108
+ value,
3109
+ ttl
3110
+ });
3111
+ if (onConflict === "create" || onConflict === "update") return {
3112
+ wasSet: true,
3113
+ existing: null
3114
+ };
3115
+ return value;
3116
+ }
3117
+ /**
3118
+ * Standard `get` with TTL handling. Emits `hit` / `miss` events to keep
3119
+ * downstream metrics tests realistic.
3120
+ */
3121
+ async get(key) {
3122
+ const parsedKey = this.parseKey(key);
3123
+ this.recordCall("get", parsedKey);
3124
+ this.log("fetching", parsedKey);
3125
+ const data = this.storage.get(parsedKey);
3126
+ if (!data) {
3127
+ this.log("notFound", parsedKey);
3128
+ await this.emit("miss", { key: parsedKey });
3129
+ return null;
3130
+ }
3131
+ const value = await this.parseCachedData(parsedKey, data);
3132
+ if (value === null) {
3133
+ this.storage.delete(parsedKey);
3134
+ await this.emit("miss", { key: parsedKey });
3135
+ return null;
3136
+ }
3137
+ await this.emit("hit", {
3138
+ key: parsedKey,
3139
+ value
3140
+ });
3141
+ return value;
3142
+ }
3143
+ /**
3144
+ * Read the raw {@link CacheData} wrapper from the in-memory `Map`,
3145
+ * including `staleAt` metadata. Returns `null` for missing or expired
3146
+ * entries — `swr()` consumes this to branch on freshness.
3147
+ */
3148
+ async getEntry(key) {
3149
+ const parsedKey = this.parseKey(key);
3150
+ const entry = this.storage.get(parsedKey);
3151
+ if (!entry) return null;
3152
+ if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) return null;
3153
+ return entry;
3154
+ }
3155
+ /**
3156
+ * Standard `remove` — drops the entry and emits the `removed` event.
3157
+ */
3158
+ async remove(key) {
3159
+ const parsedKey = this.parseKey(key);
3160
+ this.recordCall("remove", parsedKey);
3161
+ this.log("removing", parsedKey);
3162
+ this.storage.delete(parsedKey);
3163
+ this.log("removed", parsedKey);
3164
+ await this.emit("removed", { key: parsedKey });
3165
+ }
3166
+ /**
3167
+ * Standard `flush` — wipes the entire mock store + tag index. Does NOT
3168
+ * touch the call log; use {@link reset} to clear that as well.
3169
+ */
3170
+ async flush() {
3171
+ this.recordCall("flush", void 0);
3172
+ this.log("flushing");
3173
+ this.storage.clear();
3174
+ this.log("flushed");
3175
+ await this.emit("flushed");
3176
+ }
3177
+ /**
3178
+ * Was a given operation invoked? When `key` is provided, the match is
3179
+ * post-`parseKey` so callers pass the same key shape they used at the
3180
+ * call site — strings or objects, both resolve to the same parsed key.
3181
+ *
3182
+ * @example
3183
+ * driver.wasCalled("set"); // any set
3184
+ * driver.wasCalled("set", "users.42"); // set on this specific key
3185
+ * driver.wasCalled("set", { id: 42 }); // same — object key normalized
3186
+ */
3187
+ wasCalled(operation, key) {
3188
+ if (key === void 0) return this.callLog.some((call) => call.operation === operation);
3189
+ const parsedKey = this.parseKey(key);
3190
+ return this.callLog.some((call) => call.operation === operation && call.key === parsedKey);
3191
+ }
3192
+ /**
3193
+ * Return the raw stored value for `key`, bypassing TTL handling and clone
3194
+ * protection. Useful when a test wants to assert on the persisted shape
3195
+ * (or assert that an entry expired without going through `get`).
3196
+ *
3197
+ * Returns `undefined` when the key isn't present.
3198
+ */
3199
+ getStored(key) {
3200
+ const parsedKey = this.parseKey(key);
3201
+ const entry = this.storage.get(parsedKey);
3202
+ if (!entry) return;
3203
+ return entry.data;
3204
+ }
3205
+ /**
3206
+ * Wipe everything — storage, tag index, and the call log. Pair with
3207
+ * Vitest's `beforeEach` to get clean isolation between tests.
3208
+ */
3209
+ reset() {
3210
+ this.storage.clear();
3211
+ this.callLog.length = 0;
3212
+ }
3213
+ /**
3214
+ * Append a row to {@link callLog}. Internal helper called by every
3215
+ * recorded op before the actual work runs.
3216
+ */
3217
+ recordCall(operation, key, args = []) {
3218
+ this.callLog.push({
3219
+ operation,
3220
+ key,
3221
+ args,
3222
+ timestamp: Date.now()
3223
+ });
3224
+ }
3225
+ };
3226
+
3227
+ //#endregion
3228
+ //#region ../../@warlock.js/cache/src/drivers/null-cache-driver.ts
3229
+ var NullCacheDriver = class extends BaseCacheDriver {
3230
+ /**
3231
+ * {@inheritdoc}
3232
+ */
3233
+ get client() {
3234
+ return this;
3235
+ }
3236
+ /**
3237
+ * Constructor
3238
+ */
3239
+ constructor(options = {}) {
3240
+ super();
3241
+ this.options = {};
3242
+ this.name = "null";
3243
+ this.data = {};
3244
+ this.setOptions(options);
3245
+ }
3246
+ /**
3247
+ * {@inheritdoc}
3248
+ */
3249
+ setOptions(options) {
3250
+ this.options = options;
3251
+ return this;
3252
+ }
3253
+ /**
3254
+ * {@inheritdoc}
3255
+ */
3256
+ parseKey(_key) {
3257
+ return "";
3258
+ }
3259
+ /**
3260
+ * {@inheritdoc}
3261
+ */
3262
+ async removeNamespace(namespace) {
3263
+ _warlock_js_logger.log.info("cache", "clearing namespace", namespace);
3264
+ _warlock_js_logger.log.success("cache", "namespace cleared", namespace);
3265
+ return this;
3266
+ }
3267
+ /**
3268
+ * {@inheritdoc}
3269
+ */
3270
+ async set(key, _value, _ttlOrOptions) {
3271
+ _warlock_js_logger.log.info("cache", "setting key", key);
3272
+ _warlock_js_logger.log.success("cache", "key set", key);
3273
+ return this;
3274
+ }
3275
+ /**
3276
+ * {@inheritdoc}
3277
+ */
3278
+ async get(key) {
3279
+ _warlock_js_logger.log.info("cache", "fetching", key);
3280
+ _warlock_js_logger.log.success("cache", "fetched", key);
3281
+ return null;
3282
+ }
3283
+ /**
3284
+ * {@inheritdoc}
3285
+ */
3286
+ async remove(key) {
3287
+ _warlock_js_logger.log.info("cache", "removing", key);
3288
+ _warlock_js_logger.log.success("cache", "removed", key);
3289
+ }
3290
+ /**
3291
+ * {@inheritdoc}
3292
+ */
3293
+ async flush() {
3294
+ _warlock_js_logger.log.info("cache", "flushing", "all");
3295
+ _warlock_js_logger.log.success("cache", "flushed", "all");
3296
+ }
3297
+ /**
3298
+ * {@inheritdoc}
3299
+ *
3300
+ * The null driver is a black-hole — `similar()` mirrors `get()` and returns
3301
+ * an empty result set rather than throwing.
3302
+ */
3303
+ async similar() {
3304
+ return [];
3305
+ }
3306
+ /**
3307
+ * {@inheritdoc}
3308
+ */
3309
+ async connect() {
3310
+ _warlock_js_logger.log.success("cache", "connected", "Connected to null cache driver");
3311
+ }
3312
+ };
3313
+
3314
+ //#endregion
3315
+ //#region ../../@warlock.js/cache/src/drivers/pg-cache-driver.ts
3316
+ /**
3317
+ * Allowed characters in a Postgres identifier (table name). We accept the
3318
+ * conservative ASCII subset and reject anything else — interpolating an
3319
+ * arbitrary string into DDL would be a SQL-injection footgun.
3320
+ */
3321
+ const SAFE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
3322
+ /**
3323
+ * Postgres cache driver with optional pgvector similarity support.
3324
+ *
3325
+ * Connection lifecycle is the caller's responsibility — pass an already-built
3326
+ * `pg.Pool` or `pg.Client` via the `client` option. The driver never closes it
3327
+ * on `cache.disconnect()`, so the same pool can serve queries elsewhere in
3328
+ * the app.
3329
+ *
3330
+ * Schema is not auto-migrated. Call `driver.schema()` to get the DDL string
3331
+ * and run it through whichever migration tool you use.
3332
+ *
3333
+ * @example
3334
+ * import { Pool } from "pg";
3335
+ * import { cache, PgCacheDriver } from "@warlock.js/cache";
3336
+ *
3337
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
3338
+ *
3339
+ * cache.setCacheConfigurations({
3340
+ * default: "pg",
3341
+ * drivers: { pg: PgCacheDriver },
3342
+ * options: { pg: { client: pool, table: "warlock_cache", ttl: "1h" } },
3343
+ * });
3344
+ *
3345
+ * await cache.init();
3346
+ *
3347
+ * // Run once, via your own migration tooling:
3348
+ * // await pool.query(driver.schema());
3349
+ */
3350
+ var PgCacheDriver = class extends BaseCacheDriver {
3351
+ constructor(..._args) {
3352
+ super(..._args);
3353
+ this.name = "pg";
3354
+ }
3355
+ /**
3356
+ * {@inheritdoc}
3357
+ *
3358
+ * Validates `client` presence and the table name before storing options.
3359
+ */
3360
+ setOptions(options) {
3361
+ if (!options || !options.client || typeof options.client.query !== "function") throw new CacheConfigurationError("Pg cache driver requires a 'client' option implementing { query(text, values) } — pass a pg.Pool or pg.Client.");
3362
+ const table = options.table ?? "warlock_cache";
3363
+ if (!SAFE_IDENT.test(table)) throw new CacheConfigurationError(`Pg cache driver: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`);
3364
+ if (options.vector) {
3365
+ const dim = options.vector.dimensions;
3366
+ if (!Number.isInteger(dim) || dim <= 0) throw new CacheConfigurationError(`Pg cache driver: vector.dimensions must be a positive integer; got ${dim}.`);
3367
+ const idx = options.vector.index ?? "hnsw";
3368
+ if (idx !== "hnsw" && idx !== "ivfflat") throw new CacheConfigurationError(`Pg cache driver: vector.index must be 'hnsw' or 'ivfflat'; got '${idx}'.`);
3369
+ }
3370
+ return super.setOptions({
3371
+ ...options,
3372
+ table
3373
+ });
3374
+ }
3375
+ /**
3376
+ * Lazy pgvector availability check. Runs once on the first vector op and
3377
+ * caches the result — subsequent ops are zero-overhead. Throws
3378
+ * {@link CacheConfigurationError} if the extension isn't installed; throws
3379
+ * {@link CacheUnsupportedError} if `vector` config wasn't provided at all.
3380
+ */
3381
+ async ensureVectorReady() {
3382
+ if (!this.options.vector) throw new CacheUnsupportedError("'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.");
3383
+ if (this.vectorReady === true) return;
3384
+ const { rows } = await this.pgClient.query(`SELECT 1 FROM pg_extension WHERE extname = 'vector'`);
3385
+ if (rows.length === 0) throw new CacheConfigurationError("'pg' driver: pgvector extension not installed. Run 'CREATE EXTENSION vector;' or remove the 'vector' config option.");
3386
+ this.vectorReady = true;
3387
+ }
3388
+ /**
3389
+ * Format a numeric vector for pgvector ingestion. The `vector` type accepts
3390
+ * a string literal `'[1,2,3]'` cast via `::vector` — this avoids depending
3391
+ * on the binary protocol and works against any pg client.
3392
+ */
3393
+ formatVector(vector) {
3394
+ return `[${vector.join(",")}]`;
3395
+ }
3396
+ /**
3397
+ * Resolved table name. Always defined post-`setOptions` — the validator
3398
+ * fills in the default.
3399
+ */
3400
+ get table() {
3401
+ return this.options.table ?? "warlock_cache";
3402
+ }
3403
+ /**
3404
+ * The user-supplied `pg.Pool` / `pg.Client`. Use this rather than `this.client`
3405
+ * (which has a generic fallback to `this`) for actual queries.
3406
+ */
3407
+ get pgClient() {
3408
+ return this.options.client;
3409
+ }
3410
+ /**
3411
+ * Compute an absolute `expires_at` Date for the given relative TTL in seconds,
3412
+ * or `null` when the entry should not expire (`Infinity` / 0 / undefined).
3413
+ */
3414
+ ttlToExpiresAt(ttl) {
3415
+ if (!ttl || ttl === Infinity) return null;
3416
+ return new Date(Date.now() + ttl * 1e3);
3417
+ }
3418
+ /**
3419
+ * Return the SQL needed to provision the cache table + index. Run once via
3420
+ * the caller's migration tooling — the driver never auto-migrates.
3421
+ *
3422
+ * @example
3423
+ * await pool.query(driver.schema());
3424
+ */
3425
+ schema() {
3426
+ const t = this.table;
3427
+ const vec = this.options.vector;
3428
+ const columns = [
3429
+ ` key TEXT PRIMARY KEY,`,
3430
+ ` value JSONB NOT NULL,`,
3431
+ ` expires_at TIMESTAMPTZ,`,
3432
+ ` stale_at TIMESTAMPTZ,`,
3433
+ ` tags TEXT[] NOT NULL DEFAULT '{}'::TEXT[]`
3434
+ ];
3435
+ if (vec) {
3436
+ columns[columns.length - 1] = columns[columns.length - 1] + ",";
3437
+ columns.push(` embedding VECTOR(${vec.dimensions})`);
3438
+ }
3439
+ const lines = [
3440
+ `CREATE TABLE IF NOT EXISTS ${t} (`,
3441
+ ...columns,
3442
+ `);`,
3443
+ `CREATE INDEX IF NOT EXISTS idx_${t}_expires_at ON ${t} (expires_at);`,
3444
+ `CREATE INDEX IF NOT EXISTS idx_${t}_tags ON ${t} USING GIN (tags);`
3445
+ ];
3446
+ if (vec) {
3447
+ const idx = vec.index ?? "hnsw";
3448
+ lines.push(`CREATE INDEX IF NOT EXISTS idx_${t}_embedding ON ${t} USING ${idx} (embedding vector_cosine_ops);`);
3449
+ }
3450
+ return lines.join("\n");
3451
+ }
3452
+ /**
3453
+ * {@inheritdoc}
3454
+ */
3455
+ async connect() {
3456
+ this.log("connecting");
3457
+ this.log("connected");
3458
+ await this.emit("connected");
3459
+ }
3460
+ /**
3461
+ * {@inheritdoc}
3462
+ *
3463
+ * Does NOT close the user-supplied client — lifecycle stays with the caller.
3464
+ */
3465
+ async disconnect() {
3466
+ this.log("disconnected");
3467
+ await this.emit("disconnected");
3468
+ }
3469
+ /**
3470
+ * {@inheritdoc}
3471
+ */
3472
+ async set(key, value, ttlOrOptions) {
3473
+ const parsedKey = this.parseKey(key);
3474
+ const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);
3475
+ if (vector) {
3476
+ if (!this.options.vector) throw new CacheUnsupportedError("'pg' driver: cannot index a vector without options.vector configuration — set { dimensions } and recreate the table via driver.schema().");
3477
+ const expected = this.options.vector.dimensions;
3478
+ if (vector.length !== expected) throw new CacheConfigurationError(`Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`);
3479
+ await this.ensureVectorReady();
3480
+ }
3481
+ this.log("caching", parsedKey);
3482
+ const expiresAt = this.ttlToExpiresAt(ttl);
3483
+ const staleAtDate = staleAt !== void 0 ? new Date(staleAt) : null;
3484
+ const tagsArr = tags ?? [];
3485
+ const serialized = JSON.stringify(value);
3486
+ const vecLiteral = vector ? this.formatVector(vector) : null;
3487
+ const t = this.table;
3488
+ const cols = [
3489
+ "key",
3490
+ "value",
3491
+ "expires_at",
3492
+ "stale_at",
3493
+ "tags"
3494
+ ];
3495
+ const placeholders = [
3496
+ "$1",
3497
+ "$2::jsonb",
3498
+ "$3",
3499
+ "$4",
3500
+ "$5"
3501
+ ];
3502
+ const params = [
3503
+ parsedKey,
3504
+ serialized,
3505
+ expiresAt,
3506
+ staleAtDate,
3507
+ tagsArr
3508
+ ];
3509
+ if (vecLiteral !== null) {
3510
+ cols.push("embedding");
3511
+ placeholders.push(`$${params.length + 1}::vector`);
3512
+ params.push(vecLiteral);
3513
+ }
3514
+ const colList = cols.join(", ");
3515
+ const valList = placeholders.join(", ");
3516
+ const setClause = cols.slice(1).map((c) => `${c} = EXCLUDED.${c}`).join(", ");
3517
+ const updateSetClause = cols.slice(1).map((c, i) => `${c} = ${placeholders[i + 1]}`).join(", ");
3518
+ if (onConflict === "create") {
3519
+ const { rows } = await this.pgClient.query(`INSERT INTO ${t}(${colList})
3520
+ VALUES (${valList})
3521
+ ON CONFLICT (key) DO UPDATE
3522
+ SET ${setClause}
3523
+ WHERE ${t}.expires_at IS NOT NULL AND ${t}.expires_at < now()
3524
+ RETURNING value`, params);
3525
+ if (rows.length === 0) return {
3526
+ wasSet: false,
3527
+ existing: await this.get(key)
3528
+ };
3529
+ if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3530
+ this.log("cached", parsedKey);
3531
+ await this.emit("set", {
3532
+ key: parsedKey,
3533
+ value,
3534
+ ttl
3535
+ });
3536
+ return {
3537
+ wasSet: true,
3538
+ existing: null
3539
+ };
3540
+ }
3541
+ if (onConflict === "update") {
3542
+ const { rows } = await this.pgClient.query(`UPDATE ${t}
3543
+ SET ${updateSetClause}
3544
+ WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())
3545
+ RETURNING value`, params);
3546
+ if (rows.length === 0) return {
3547
+ wasSet: false,
3548
+ existing: null
3549
+ };
3550
+ if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3551
+ this.log("cached", parsedKey);
3552
+ await this.emit("set", {
3553
+ key: parsedKey,
3554
+ value,
3555
+ ttl
3556
+ });
3557
+ return {
3558
+ wasSet: true,
3559
+ existing: null
3560
+ };
3561
+ }
3562
+ await this.pgClient.query(`INSERT INTO ${t}(${colList})
3563
+ VALUES (${valList})
3564
+ ON CONFLICT (key) DO UPDATE
3565
+ SET ${setClause}`, params);
3566
+ if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3567
+ this.log("cached", parsedKey);
3568
+ await this.emit("set", {
3569
+ key: parsedKey,
3570
+ value,
3571
+ ttl
3572
+ });
3573
+ return value;
3574
+ }
3575
+ /**
3576
+ * {@inheritdoc}
3577
+ */
3578
+ async get(key) {
3579
+ const parsedKey = this.parseKey(key);
3580
+ this.log("fetching", parsedKey);
3581
+ const t = this.table;
3582
+ const { rows } = await this.pgClient.query(`SELECT value FROM ${t}
3583
+ WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, [parsedKey]);
3584
+ if (rows.length === 0) {
3585
+ this.log("notFound", parsedKey);
3586
+ await this.emit("miss", { key: parsedKey });
3587
+ return null;
3588
+ }
3589
+ this.log("fetched", parsedKey);
3590
+ let value = rows[0].value;
3591
+ if (typeof value === "string") try {
3592
+ value = JSON.parse(value);
3593
+ } catch {}
3594
+ if (value === null || value === void 0) {
3595
+ await this.emit("hit", {
3596
+ key: parsedKey,
3597
+ value
3598
+ });
3599
+ return value;
3600
+ }
3601
+ const type = typeof value;
3602
+ if (type === "string" || type === "number" || type === "boolean") {
3603
+ await this.emit("hit", {
3604
+ key: parsedKey,
3605
+ value
3606
+ });
3607
+ return value;
3608
+ }
3609
+ try {
3610
+ const cloned = structuredClone(value);
3611
+ await this.emit("hit", {
3612
+ key: parsedKey,
3613
+ value: cloned
3614
+ });
3615
+ return cloned;
3616
+ } catch (error) {
3617
+ this.logError(`Failed to clone cached value for ${parsedKey}`, error);
3618
+ throw error;
3619
+ }
3620
+ }
3621
+ /**
3622
+ * Read the raw {@link CacheData} wrapper, including `staleAt` metadata.
3623
+ * Returns `null` for missing or expired rows — `swr()` consumes this to
3624
+ * branch on freshness without going through `get()`'s clone-and-emit path.
3625
+ */
3626
+ async getEntry(key) {
3627
+ const parsedKey = this.parseKey(key);
3628
+ const t = this.table;
3629
+ const { rows } = await this.pgClient.query(`SELECT value, expires_at, stale_at FROM ${t}
3630
+ WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, [parsedKey]);
3631
+ if (rows.length === 0) return null;
3632
+ let data = rows[0].value;
3633
+ if (typeof data === "string") try {
3634
+ data = JSON.parse(data);
3635
+ } catch {}
3636
+ const entry = { data };
3637
+ const expiresAtRaw = rows[0].expires_at;
3638
+ if (expiresAtRaw) entry.expiresAt = (expiresAtRaw instanceof Date ? expiresAtRaw : new Date(expiresAtRaw)).getTime();
3639
+ const staleAtRaw = rows[0].stale_at;
3640
+ if (staleAtRaw) entry.staleAt = (staleAtRaw instanceof Date ? staleAtRaw : new Date(staleAtRaw)).getTime();
3641
+ return entry;
3642
+ }
3643
+ /**
3644
+ * {@inheritdoc}
3645
+ */
3646
+ async remove(key) {
3647
+ const parsedKey = this.parseKey(key);
3648
+ this.log("removing", parsedKey);
3649
+ const t = this.table;
3650
+ await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1`, [parsedKey]);
3651
+ this.log("removed", parsedKey);
3652
+ await this.emit("removed", { key: parsedKey });
3653
+ }
3654
+ /**
3655
+ * {@inheritdoc}
3656
+ *
3657
+ * Deletes all rows whose key equals the namespace exactly or starts with
3658
+ * `<namespace>.` — same boundary semantics as the other drivers.
3659
+ */
3660
+ async removeNamespace(namespace) {
3661
+ const parsed = this.parseKey(namespace);
3662
+ this.log("clearing", parsed || "(all)");
3663
+ const t = this.table;
3664
+ if (parsed === "") await this.pgClient.query(`DELETE FROM ${t}`);
3665
+ else {
3666
+ const escaped = parsed.replace(/\\/g, "\\\\").replace(/_/g, "\\_").replace(/%/g, "\\%");
3667
+ await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1 OR key LIKE $2 ESCAPE '\\'`, [parsed, `${escaped}.%`]);
3668
+ }
3669
+ this.log("cleared", parsed || "(all)");
3670
+ return this;
3671
+ }
3672
+ /**
3673
+ * {@inheritdoc}
3674
+ *
3675
+ * Honors `globalPrefix` — when configured, scopes the flush to entries
3676
+ * under the prefix rather than truncating the entire table (which could
3677
+ * wipe sibling tenants sharing the same Postgres database).
3678
+ */
3679
+ async flush() {
3680
+ this.log("flushing");
3681
+ if (this.options.globalPrefix) await this.removeNamespace("");
3682
+ else await this.pgClient.query(`DELETE FROM ${this.table}`);
3683
+ this.log("flushed");
3684
+ await this.emit("flushed");
3685
+ }
3686
+ /**
3687
+ * {@inheritdoc}
3688
+ *
3689
+ * pgvector-backed similarity. Uses the `<=>` cosine-distance operator
3690
+ * (lower distance = higher similarity) and converts to cosine similarity
3691
+ * as `1 - distance` so the returned `score` matches the rest of the
3692
+ * package (`[0, 1]`, higher is more similar).
3693
+ *
3694
+ * Honors `topK`, `threshold`, and an optional `tags` filter (native
3695
+ * `tags && $tags` overlap query — much faster than the meta-key path).
3696
+ *
3697
+ * Throws {@link CacheUnsupportedError} when `options.vector` was not
3698
+ * configured at driver setup; throws {@link CacheConfigurationError} when
3699
+ * the pgvector extension is missing or the query vector's dimension count
3700
+ * doesn't match the configured one.
3701
+ */
3702
+ async similar(vector, options) {
3703
+ if (!this.options.vector) throw new CacheUnsupportedError("'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.");
3704
+ const expected = this.options.vector.dimensions;
3705
+ if (vector.length !== expected) throw new CacheConfigurationError(`Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`);
3706
+ if (!Number.isInteger(options.topK) || options.topK <= 0) throw new CacheConfigurationError(`Pg cache driver: similar.topK must be a positive integer; got ${options.topK}.`);
3707
+ await this.ensureVectorReady();
3708
+ const t = this.table;
3709
+ const params = [this.formatVector(vector)];
3710
+ let tagFilter = "";
3711
+ if (options.tags && options.tags.length > 0) {
3712
+ params.push(options.tags);
3713
+ tagFilter = `AND tags && $${params.length}`;
3714
+ }
3715
+ params.push(options.topK);
3716
+ const topKParam = `$${params.length}`;
3717
+ const { rows } = await this.pgClient.query(`SELECT key, value, 1 - (embedding <=> $1::vector) AS score
3718
+ FROM ${t}
3719
+ WHERE embedding IS NOT NULL
3720
+ AND (expires_at IS NULL OR expires_at > now())
3721
+ ${tagFilter}
3722
+ ORDER BY embedding <=> $1::vector
3723
+ LIMIT ${topKParam}`, params);
3724
+ const hits = [];
3725
+ for (const row of rows) {
3726
+ const score = Number(row.score);
3727
+ if (options.threshold !== void 0 && score < options.threshold) continue;
3728
+ let value = row.value;
3729
+ if (typeof value === "string") try {
3730
+ value = JSON.parse(value);
3731
+ } catch {}
3732
+ if (value !== null && value !== void 0) {
3733
+ const ty = typeof value;
3734
+ if (ty !== "string" && ty !== "number" && ty !== "boolean") value = structuredClone(value);
3735
+ }
3736
+ hits.push({
3737
+ key: row.key,
3738
+ value,
3739
+ score
3740
+ });
3741
+ }
3742
+ return hits;
3743
+ }
3744
+ };
3745
+
3746
+ //#endregion
3747
+ //#region ../../@warlock.js/cache/src/drivers/redis-cache-driver.ts
3748
+ /**
3749
+ * Cached Redis module (loaded once, reused)
3750
+ */
3751
+ let RedisClient;
3752
+ let isModuleExists = null;
3753
+ /**
3754
+ * Installation instructions for Redis package
3755
+ */
3756
+ const REDIS_INSTALL_INSTRUCTIONS = `
3757
+ Redis cache driver requires the redis package.
3758
+ Install it with:
3759
+
3760
+ npm install redis
3761
+
3762
+ Or with your preferred package manager:
3763
+
3764
+ pnpm add redis
3765
+ yarn add redis
3766
+ `.trim();
3767
+ /**
3768
+ * Load Redis module
3769
+ */
3770
+ async function loadRedis() {
3771
+ try {
3772
+ RedisClient = await import("redis");
3773
+ isModuleExists = true;
3774
+ } catch {
3775
+ isModuleExists = false;
3776
+ }
3777
+ }
3778
+ loadRedis();
3779
+ var RedisCacheDriver = class extends BaseCacheDriver {
3780
+ constructor(..._args) {
3781
+ super(..._args);
3782
+ this.name = "redis";
3783
+ }
3784
+ /**
3785
+ * {@inheritdoc}
3786
+ */
3787
+ setOptions(options) {
3788
+ if (!options.url && !options.host) throw new CacheConfigurationError("Redis driver requires either 'url' or 'host' option to be configured.");
3789
+ return super.setOptions(options);
3790
+ }
3791
+ /**
3792
+ * {@inheritDoc}
3793
+ */
3794
+ async removeNamespace(namespace) {
3795
+ namespace = this.parseKey(namespace);
3796
+ this.log("clearing", namespace);
3797
+ const keys = await this.client?.keys(`${namespace}*`);
3798
+ if (!keys || keys.length === 0) {
3799
+ this.log("notFound", namespace);
3800
+ return;
3801
+ }
3802
+ await this.client?.del(keys);
3803
+ this.log("cleared", namespace);
3804
+ return keys;
3805
+ }
3806
+ /**
3807
+ * {@inheritDoc}
3808
+ */
3809
+ async set(key, value, ttlOrOptions) {
3810
+ const parsedKey = this.parseKey(key);
3811
+ const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);
3812
+ if (vector) throw new CacheUnsupportedError("'redis' driver does not yet support similarity retrieval. Phase 2 (RediSearch) is on the backlog — use a memory driver or the 'pg' driver (with pgvector) for now.");
3813
+ this.log("caching", parsedKey);
3814
+ const serialized = JSON.stringify(value);
3815
+ const hasExpiry = Boolean(ttl) && ttl !== Infinity;
3816
+ let reply;
3817
+ if (onConflict === "create") {
3818
+ const options = { NX: true };
3819
+ if (hasExpiry) options.EX = ttl;
3820
+ reply = await this.client?.set(parsedKey, serialized, options);
3821
+ } else if (onConflict === "update") {
3822
+ const options = { XX: true };
3823
+ if (hasExpiry) options.EX = ttl;
3824
+ reply = await this.client?.set(parsedKey, serialized, options);
3825
+ } else if (hasExpiry) reply = await this.client?.set(parsedKey, serialized, { EX: ttl });
3826
+ else reply = await this.client?.set(parsedKey, serialized);
3827
+ if ((onConflict === "create" || onConflict === "update") && !(reply === "OK")) return {
3828
+ wasSet: false,
3829
+ existing: onConflict === "create" ? await this.get(key) : null
3830
+ };
3831
+ if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3832
+ if (staleAt !== void 0) {
3833
+ const sidecarOptions = {};
3834
+ if (hasExpiry) sidecarOptions.EX = ttl;
3835
+ await this.client?.set(this.swrMetaKey(parsedKey), String(staleAt), sidecarOptions);
3836
+ }
3837
+ this.log("cached", parsedKey);
3838
+ await this.emit("set", {
3839
+ key: parsedKey,
3840
+ value,
3841
+ ttl
3842
+ });
3843
+ if (onConflict === "create" || onConflict === "update") return {
3844
+ wasSet: true,
3845
+ existing: null
3846
+ };
3847
+ return value;
3848
+ }
3849
+ /**
3850
+ * Build the sidecar key Redis uses to track SWR freshness without
3851
+ * wrapping the main value JSON.
3852
+ */
3853
+ swrMetaKey(parsedKey) {
3854
+ return `__swrmeta:${parsedKey}`;
3855
+ }
3856
+ /**
3857
+ * Read the raw {@link CacheData} wrapper, fetching the value and the
3858
+ * SWR sidecar in parallel. Returns `null` when the main key is missing
3859
+ * or expired (Redis handles expiry natively, so the absence of the
3860
+ * value alone tells us).
3861
+ */
3862
+ async getEntry(key) {
3863
+ const parsedKey = this.parseKey(key);
3864
+ const [valueRaw, staleAtRaw] = await Promise.all([this.client?.get(parsedKey), this.client?.get(this.swrMetaKey(parsedKey))]);
3865
+ if (!valueRaw) return null;
3866
+ const data = JSON.parse(valueRaw);
3867
+ const staleAt = staleAtRaw ? Number(staleAtRaw) : void 0;
3868
+ return staleAt !== void 0 ? {
3869
+ data,
3870
+ staleAt
3871
+ } : { data };
3872
+ }
3873
+ /**
3874
+ * {@inheritdoc}
3875
+ *
3876
+ * Redis tracks expiry natively (the payload carries no `expiresAt`), so read
3877
+ * the remaining lifetime with the `TTL` command. Redis returns `-2` for a
3878
+ * missing key and `-1` for a key with no expiry.
3879
+ */
3880
+ async getRemainingTtl(key) {
3881
+ const parsedKey = this.parseKey(key);
3882
+ const ttl = await this.client?.ttl(parsedKey);
3883
+ if (ttl === void 0 || ttl === -2) return;
3884
+ if (ttl === -1) return Infinity;
3885
+ return ttl;
3886
+ }
3887
+ /**
3888
+ * {@inheritDoc}
3889
+ */
3890
+ async get(key) {
3891
+ key = this.parseKey(key);
3892
+ this.log("fetching", key);
3893
+ const value = await this.client?.get(key);
3894
+ if (!value) {
3895
+ this.log("notFound", key);
3896
+ await this.emit("miss", { key });
3897
+ return null;
3898
+ }
3899
+ this.log("fetched", key);
3900
+ const parsedValue = JSON.parse(value);
3901
+ if (parsedValue === null || parsedValue === void 0) {
3902
+ await this.emit("hit", {
3903
+ key,
3904
+ value: parsedValue
3905
+ });
3906
+ return parsedValue;
3907
+ }
3908
+ const type = typeof parsedValue;
3909
+ if (type === "string" || type === "number" || type === "boolean") {
3910
+ await this.emit("hit", {
3911
+ key,
3912
+ value: parsedValue
3913
+ });
3914
+ return parsedValue;
3915
+ }
3916
+ try {
3917
+ const clonedValue = structuredClone(parsedValue);
3918
+ await this.emit("hit", {
3919
+ key,
3920
+ value: clonedValue
3921
+ });
3922
+ return clonedValue;
3923
+ } catch (error) {
3924
+ this.logError(`Failed to clone cached value for ${key}`, error);
3925
+ throw error;
3926
+ }
3927
+ }
3928
+ /**
3929
+ * {@inheritDoc}
3930
+ */
3931
+ async remove(key) {
3932
+ key = this.parseKey(key);
3933
+ this.log("removing", key);
3934
+ await this.client?.del([key, this.swrMetaKey(key)]);
3935
+ this.log("removed", key);
3936
+ await this.emit("removed", { key });
3937
+ }
3938
+ /**
3939
+ * {@inheritDoc}
3940
+ */
3941
+ async flush() {
3942
+ this.log("flushing");
3943
+ if (this.options.globalPrefix) await this.removeNamespace("");
3944
+ else await this.client?.flushAll();
3945
+ this.log("flushed");
3946
+ await this.emit("flushed");
3947
+ }
3948
+ /**
3949
+ * {@inheritDoc}
3950
+ */
3951
+ async connect() {
3952
+ if (this.clientDriver) return;
3953
+ if (!isModuleExists) throw new Error(REDIS_INSTALL_INSTRUCTIONS);
3954
+ const options = this.options;
3955
+ if (options && !options.url && options.host) {
3956
+ const auth = options.password || options.username ? `${options.username}:${options.password}@` : "";
3957
+ if (!options.url) options.url = `redis://${auth}${options.host || "localhost"}:${options.port || 6379}`;
3958
+ }
3959
+ const clientOptions = {
3960
+ ...options,
3961
+ ...this.options.clientOptions || {}
3962
+ };
3963
+ this.log("connecting");
3964
+ const { createClient } = RedisClient;
3965
+ this.client = createClient(clientOptions);
3966
+ this.client.on("error", (error) => {
3967
+ this.log("error", error.message);
3968
+ });
3969
+ try {
3970
+ await this.client.connect();
3971
+ this.log("connected");
3972
+ await this.emit("connected");
3973
+ } catch (error) {
3974
+ _warlock_js_logger.log.error("cache", "redis", error);
3975
+ await this.emit("error", { error });
3976
+ }
3977
+ }
3978
+ /**
3979
+ * {@inheritDoc}
3980
+ *
3981
+ * Guards against disconnecting when the client was never created. The base
3982
+ * `client` getter falls back to `this` when no client is set, so we check
3983
+ * the backing `clientDriver` directly — using `this.client` for this guard
3984
+ * would always be truthy and crash with "this.quit is not a function".
3985
+ */
3986
+ async disconnect() {
3987
+ if (!this.clientDriver) return;
3988
+ this.log("disconnecting");
3989
+ await this.clientDriver.quit();
3990
+ this.log("disconnected");
3991
+ await this.emit("disconnected");
3992
+ }
3993
+ /**
3994
+ * Atomic increment using Redis native INCRBY command
3995
+ * {@inheritdoc}
3996
+ */
3997
+ async increment(key, value = 1) {
3998
+ const parsedKey = this.parseKey(key);
3999
+ this.log("caching", parsedKey);
4000
+ const result = await this.client?.incrBy(parsedKey, value);
4001
+ this.log("cached", parsedKey);
4002
+ await this.emit("set", {
4003
+ key: parsedKey,
4004
+ value: result,
4005
+ ttl: void 0
4006
+ });
4007
+ return result || 0;
4008
+ }
4009
+ /**
4010
+ * Atomic decrement using Redis native DECRBY command
4011
+ * {@inheritdoc}
4012
+ */
4013
+ async decrement(key, value = 1) {
4014
+ const parsedKey = this.parseKey(key);
4015
+ this.log("caching", parsedKey);
4016
+ const result = await this.client?.decrBy(parsedKey, value);
4017
+ this.log("cached", parsedKey);
4018
+ await this.emit("set", {
4019
+ key: parsedKey,
4020
+ value: result,
4021
+ ttl: void 0
4022
+ });
4023
+ return result || 0;
4024
+ }
4025
+ /**
4026
+ * Set if not exists (atomic operation)
4027
+ * Returns true if key was set, false if key already existed
4028
+ */
4029
+ async setNX(key, value, ttl) {
4030
+ const parsedKey = this.parseKey(key);
4031
+ this.log("caching", parsedKey);
4032
+ if (ttl === void 0) ttl = this.ttl;
4033
+ let result;
4034
+ if (ttl && ttl !== Infinity) result = await this.client?.set(parsedKey, JSON.stringify(value), {
4035
+ NX: true,
4036
+ EX: ttl
4037
+ });
4038
+ else result = await this.client?.set(parsedKey, JSON.stringify(value), { NX: true });
4039
+ const wasSet = result === "OK";
4040
+ if (wasSet) {
4041
+ this.log("cached", parsedKey);
4042
+ await this.emit("set", {
4043
+ key: parsedKey,
4044
+ value,
4045
+ ttl
4046
+ });
4047
+ } else this.log("notFound", parsedKey);
4048
+ return wasSet;
4049
+ }
4050
+ };
4051
+
4052
+ //#endregion
4053
+ exports.BaseCacheDriver = BaseCacheDriver;
4054
+ exports.CACHE_FOR = CACHE_FOR;
4055
+ exports.CacheConcurrencyError = CacheConcurrencyError;
4056
+ exports.CacheConfigurationError = CacheConfigurationError;
4057
+ exports.CacheConnectionError = CacheConnectionError;
4058
+ exports.CacheDriverNotInitializedError = CacheDriverNotInitializedError;
4059
+ exports.CacheError = CacheError;
4060
+ exports.CacheManager = CacheManager;
4061
+ exports.CacheMetricsCollector = CacheMetricsCollector;
4062
+ exports.CacheUnsupportedError = CacheUnsupportedError;
4063
+ exports.FileCacheDriver = FileCacheDriver;
4064
+ exports.LRUMemoryCacheDriver = LRUMemoryCacheDriver;
4065
+ exports.MemoryCacheDriver = MemoryCacheDriver;
4066
+ exports.MemoryCacheList = MemoryCacheList;
4067
+ exports.MemoryExtendedCacheDriver = MemoryExtendedCacheDriver;
4068
+ exports.MockCacheDriver = MockCacheDriver;
4069
+ exports.NullCacheDriver = NullCacheDriver;
4070
+ exports.PgCacheDriver = PgCacheDriver;
4071
+ exports.RedisCacheDriver = RedisCacheDriver;
4072
+ exports.ScopedCache = ScopedCache;
4073
+ exports.TaggedCache = TaggedCache;
4074
+ exports.TaggedScopedCache = TaggedScopedCache;
4075
+ exports.cache = cache;
4076
+ exports.cached = cached;
4077
+ exports.cosineSimilarity = cosineSimilarity;
4078
+ exports.deriveAutoKey = deriveAutoKey;
4079
+ exports.expiresAtToTtl = expiresAtToTtl;
4080
+ exports.injectTags = injectTags;
4081
+ exports.mergeTagSets = mergeTagSets;
4082
+ exports.normalizeCachedArgs = normalizeCachedArgs;
4083
+ exports.normalizeToOptions = normalizeToOptions;
4084
+ exports.normalizeToRememberOptions = normalizeToRememberOptions;
4085
+ exports.parseCacheKey = parseCacheKey;
4086
+ exports.parseTtl = parseTtl;
4087
+ exports.resolveTtl = resolveTtl;
4088
+ //# sourceMappingURL=index.cjs.map