@dotpress/cache 0.0.0-stage → 2.0.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +305 -2
- package/dist/als.d.ts +36 -0
- package/dist/als.js +35 -0
- package/dist/als.js.map +1 -0
- package/dist/chunk-2IEBMYSF.js +27 -0
- package/dist/chunk-2IEBMYSF.js.map +1 -0
- package/dist/chunk-6ZNJJARH.js +281 -0
- package/dist/chunk-6ZNJJARH.js.map +1 -0
- package/dist/cloudflare-kv.d.ts +52 -0
- package/dist/cloudflare-kv.js +75 -0
- package/dist/cloudflare-kv.js.map +1 -0
- package/dist/duration-2mZnG0_i.d.ts +8 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.js +696 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +51 -0
- package/dist/plugin.js +49 -0
- package/dist/plugin.js.map +1 -0
- package/dist/redis.d.ts +39 -0
- package/dist/redis.js +73 -0
- package/dist/redis.js.map +1 -0
- package/dist/store-D0BfLqyM.d.ts +56 -0
- package/dist/upstash.d.ts +30 -0
- package/dist/upstash.js +88 -0
- package/dist/upstash.js.map +1 -0
- package/package.json +89 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CacheError,
|
|
3
|
+
CacheStoreError,
|
|
4
|
+
CacheTimeoutError,
|
|
5
|
+
MemoBatchError,
|
|
6
|
+
createMemo,
|
|
7
|
+
defineMemo,
|
|
8
|
+
keyOf
|
|
9
|
+
} from "./chunk-6ZNJJARH.js";
|
|
10
|
+
import {
|
|
11
|
+
toMilliseconds
|
|
12
|
+
} from "./chunk-2IEBMYSF.js";
|
|
13
|
+
|
|
14
|
+
// src/logger.ts
|
|
15
|
+
var noop = () => void 0;
|
|
16
|
+
var silentLogger = {
|
|
17
|
+
debug: noop,
|
|
18
|
+
info: noop,
|
|
19
|
+
warn: noop,
|
|
20
|
+
error: noop
|
|
21
|
+
};
|
|
22
|
+
var checkLogger = (label, logger) => {
|
|
23
|
+
for (const level of ["info", "warn", "error"]) {
|
|
24
|
+
if (typeof logger?.[level] !== "function") {
|
|
25
|
+
throw new TypeError(
|
|
26
|
+
`${label}: logger must have info(), warn() and error() methods.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return logger;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/store.ts
|
|
34
|
+
var isStore = (value) => {
|
|
35
|
+
const store = value;
|
|
36
|
+
return typeof store?.name === "string" && typeof store.get === "function" && typeof store.set === "function" && typeof store.delete === "function";
|
|
37
|
+
};
|
|
38
|
+
var createMemoryStore = (options = {}) => {
|
|
39
|
+
const label = "createMemoryStore()";
|
|
40
|
+
if (typeof options !== "object" || options === null) {
|
|
41
|
+
throw new TypeError(`${label}: expected an object of options.`);
|
|
42
|
+
}
|
|
43
|
+
for (const key of Object.keys(options)) {
|
|
44
|
+
if (key !== "maxEntries") {
|
|
45
|
+
throw new TypeError(`${label}: unknown option "${key}".`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const { maxEntries = 1e4 } = options;
|
|
49
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
|
|
50
|
+
throw new TypeError(`${label}: maxEntries must be a positive integer.`);
|
|
51
|
+
}
|
|
52
|
+
const entries2 = /* @__PURE__ */ new Map();
|
|
53
|
+
const locks = /* @__PURE__ */ new Map();
|
|
54
|
+
const read = (key) => {
|
|
55
|
+
const entry = entries2.get(key);
|
|
56
|
+
if (!entry) return void 0;
|
|
57
|
+
entries2.delete(key);
|
|
58
|
+
if (entry.expiresAt <= Date.now()) return void 0;
|
|
59
|
+
entries2.set(key, entry);
|
|
60
|
+
return entry.value;
|
|
61
|
+
};
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
name: "memory",
|
|
64
|
+
get size() {
|
|
65
|
+
return entries2.size;
|
|
66
|
+
},
|
|
67
|
+
get: async (key) => read(key),
|
|
68
|
+
getMany: async (keys) => keys.map(read),
|
|
69
|
+
set: async (key, value, ttl) => {
|
|
70
|
+
entries2.delete(key);
|
|
71
|
+
entries2.set(key, { value, expiresAt: Date.now() + ttl });
|
|
72
|
+
for (const oldest of entries2.keys()) {
|
|
73
|
+
if (entries2.size <= maxEntries) break;
|
|
74
|
+
entries2.delete(oldest);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
delete: async (key) => {
|
|
78
|
+
entries2.delete(key);
|
|
79
|
+
},
|
|
80
|
+
lock: async (key, ttl) => {
|
|
81
|
+
const held = locks.get(key);
|
|
82
|
+
if (held && held.expiresAt > Date.now()) return void 0;
|
|
83
|
+
const token = crypto.randomUUID();
|
|
84
|
+
locks.set(key, { token, expiresAt: Date.now() + ttl });
|
|
85
|
+
return token;
|
|
86
|
+
},
|
|
87
|
+
unlock: async (key, token) => {
|
|
88
|
+
if (locks.get(key)?.token === token) locks.delete(key);
|
|
89
|
+
},
|
|
90
|
+
clear: () => {
|
|
91
|
+
entries2.clear();
|
|
92
|
+
locks.clear();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/cache.ts
|
|
98
|
+
var entries = /* @__PURE__ */ new WeakMap();
|
|
99
|
+
var NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
100
|
+
var POLICY = [
|
|
101
|
+
"ttl",
|
|
102
|
+
"grace",
|
|
103
|
+
"graceBackoff",
|
|
104
|
+
"staleWhileRevalidate",
|
|
105
|
+
"timeout",
|
|
106
|
+
"hardTimeout",
|
|
107
|
+
"lock"
|
|
108
|
+
];
|
|
109
|
+
var ENTRY_OPTIONS = /* @__PURE__ */ new Set([
|
|
110
|
+
"name",
|
|
111
|
+
"key",
|
|
112
|
+
"schema",
|
|
113
|
+
"version",
|
|
114
|
+
"tags",
|
|
115
|
+
...POLICY
|
|
116
|
+
]);
|
|
117
|
+
var policyOf = (label, values) => {
|
|
118
|
+
const policy = {};
|
|
119
|
+
for (const option of POLICY) {
|
|
120
|
+
const value = values[option];
|
|
121
|
+
if (value === void 0) continue;
|
|
122
|
+
policy[option] = option === "ttl" || option === "timeout" || option === "hardTimeout" || option === "lock" ? positive(`${label}: ${option}`, value) : toMilliseconds(`${label}: ${option}`, value);
|
|
123
|
+
}
|
|
124
|
+
return policy;
|
|
125
|
+
};
|
|
126
|
+
function defineCacheEntry(options) {
|
|
127
|
+
const label = "defineCacheEntry()";
|
|
128
|
+
if (typeof options !== "object" || options === null) {
|
|
129
|
+
throw new TypeError(`${label}: expected an object of options.`);
|
|
130
|
+
}
|
|
131
|
+
const values = options;
|
|
132
|
+
const { name, key, schema, tags, version = 1 } = values;
|
|
133
|
+
if (typeof name !== "string" || !NAME.test(name)) {
|
|
134
|
+
throw new TypeError(
|
|
135
|
+
`${label}: name must have 1 to 128 letters, digits, ".", "_" or "-", starting with a letter or a digit.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const named = `defineCacheEntry("${name}")`;
|
|
139
|
+
for (const option of Object.keys(values)) {
|
|
140
|
+
if (!ENTRY_OPTIONS.has(option)) {
|
|
141
|
+
throw new TypeError(`${named}: unknown option "${option}".`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (key !== void 0 && typeof key !== "function") {
|
|
145
|
+
throw new TypeError(`${named}: key must be a function.`);
|
|
146
|
+
}
|
|
147
|
+
if (schema !== void 0 && typeof schema?.safeParseAsync !== "function") {
|
|
148
|
+
throw new TypeError(
|
|
149
|
+
`${named}: schema must have a safeParseAsync() method, like a Zod schema.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (!Number.isInteger(version) || version < 0) {
|
|
153
|
+
throw new TypeError(`${named}: version must be a non-negative integer.`);
|
|
154
|
+
}
|
|
155
|
+
if (tags !== void 0 && typeof tags !== "function") {
|
|
156
|
+
throw new TypeError(`${named}: tags must be a function.`);
|
|
157
|
+
}
|
|
158
|
+
const entry = Object.freeze({ name, version });
|
|
159
|
+
entries.set(entry, {
|
|
160
|
+
name,
|
|
161
|
+
version,
|
|
162
|
+
key,
|
|
163
|
+
schema,
|
|
164
|
+
tags,
|
|
165
|
+
policy: policyOf(named, values)
|
|
166
|
+
});
|
|
167
|
+
return entry;
|
|
168
|
+
}
|
|
169
|
+
var positive = (label, value) => {
|
|
170
|
+
const ms = toMilliseconds(label, value);
|
|
171
|
+
if (ms < 1) throw new TypeError(`${label} must be at least 1 millisecond.`);
|
|
172
|
+
return ms;
|
|
173
|
+
};
|
|
174
|
+
var CACHE_OPTIONS = /* @__PURE__ */ new Set([
|
|
175
|
+
"store",
|
|
176
|
+
"prefix",
|
|
177
|
+
"ttl",
|
|
178
|
+
"serializer",
|
|
179
|
+
"logger",
|
|
180
|
+
"onError",
|
|
181
|
+
"failOpen",
|
|
182
|
+
"storeTimeout",
|
|
183
|
+
"onEvent",
|
|
184
|
+
...POLICY
|
|
185
|
+
]);
|
|
186
|
+
var jsonSerializer = {
|
|
187
|
+
serialize: (value) => JSON.stringify(value),
|
|
188
|
+
deserialize: (text) => JSON.parse(text)
|
|
189
|
+
};
|
|
190
|
+
var parseStored = (text) => {
|
|
191
|
+
try {
|
|
192
|
+
const stored = JSON.parse(text);
|
|
193
|
+
if (stored?.v === 1 && typeof stored.c === "number" && typeof stored.e === "number" && typeof stored.d === "string" && (stored.t === void 0 || typeof stored.t === "object" && stored.t !== null)) {
|
|
194
|
+
return stored;
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
return void 0;
|
|
199
|
+
};
|
|
200
|
+
var encoder = new TextEncoder();
|
|
201
|
+
var TAG_LIFETIME = 30 * 864e5;
|
|
202
|
+
var LOCK_POLL = 50;
|
|
203
|
+
var sha256 = async (text) => {
|
|
204
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(text));
|
|
205
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
206
|
+
};
|
|
207
|
+
var createCache = (options) => {
|
|
208
|
+
const label = "createCache()";
|
|
209
|
+
if (typeof options !== "object" || options === null) {
|
|
210
|
+
throw new TypeError(`${label}: expected an object of options.`);
|
|
211
|
+
}
|
|
212
|
+
for (const key of Object.keys(options)) {
|
|
213
|
+
if (!CACHE_OPTIONS.has(key)) {
|
|
214
|
+
throw new TypeError(`${label}: unknown option "${key}".`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const {
|
|
218
|
+
store,
|
|
219
|
+
prefix,
|
|
220
|
+
serializer = jsonSerializer,
|
|
221
|
+
onError,
|
|
222
|
+
onEvent,
|
|
223
|
+
failOpen = true
|
|
224
|
+
} = options;
|
|
225
|
+
if (!isStore(store)) {
|
|
226
|
+
throw new TypeError(
|
|
227
|
+
`${label}: store must be a cache store (createMemoryStore(), createRedisStore()\u2026).`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (prefix !== void 0 && (typeof prefix !== "string" || prefix === "")) {
|
|
231
|
+
throw new TypeError(`${label}: prefix must be a non-empty string.`);
|
|
232
|
+
}
|
|
233
|
+
const defaults = policyOf(
|
|
234
|
+
label,
|
|
235
|
+
options
|
|
236
|
+
);
|
|
237
|
+
if (typeof serializer?.serialize !== "function" || typeof serializer.deserialize !== "function") {
|
|
238
|
+
throw new TypeError(
|
|
239
|
+
`${label}: serializer must have serialize() and deserialize() methods.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (onError !== void 0 && typeof onError !== "function") {
|
|
243
|
+
throw new TypeError(`${label}: onError must be a function.`);
|
|
244
|
+
}
|
|
245
|
+
if (onEvent !== void 0 && typeof onEvent !== "function") {
|
|
246
|
+
throw new TypeError(`${label}: onEvent must be a function.`);
|
|
247
|
+
}
|
|
248
|
+
if (typeof failOpen !== "boolean") {
|
|
249
|
+
throw new TypeError(`${label}: failOpen must be a boolean.`);
|
|
250
|
+
}
|
|
251
|
+
const storeTimeout = options.storeTimeout === void 0 ? void 0 : positive(`${label}: storeTimeout`, options.storeTimeout);
|
|
252
|
+
const rootLogger = options.logger === void 0 ? silentLogger : checkLogger(label, options.logger);
|
|
253
|
+
const loading = /* @__PURE__ */ new Map();
|
|
254
|
+
const backoffs = /* @__PURE__ */ new Map();
|
|
255
|
+
const configOf = (method, entry) => {
|
|
256
|
+
const config = typeof entry === "object" && entry !== null ? entries.get(entry) : void 0;
|
|
257
|
+
if (!config) {
|
|
258
|
+
throw new TypeError(
|
|
259
|
+
`cache.${method}(): expected an entry created by defineCacheEntry().`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return config;
|
|
263
|
+
};
|
|
264
|
+
const policyFor = (config) => {
|
|
265
|
+
const value = (option) => config.policy[option] ?? defaults[option];
|
|
266
|
+
return {
|
|
267
|
+
ttl: value("ttl") ?? 36e5,
|
|
268
|
+
grace: value("grace") ?? 0,
|
|
269
|
+
graceBackoff: value("graceBackoff") ?? 1e4,
|
|
270
|
+
staleWhileRevalidate: value("staleWhileRevalidate") ?? 0,
|
|
271
|
+
timeout: value("timeout"),
|
|
272
|
+
hardTimeout: value("hardTimeout"),
|
|
273
|
+
lock: value("lock")
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
const fit = async (base, part) => {
|
|
277
|
+
const key = `${base}:${part}`;
|
|
278
|
+
return encoder.encode(key).length > (store.maxKeyLength ?? Infinity) ? `${base}:#${await sha256(part)}` : key;
|
|
279
|
+
};
|
|
280
|
+
const keyFor = async (config, args) => {
|
|
281
|
+
const raw = config.key ? config.key(...args) : args.length === 1 ? args[0] : args;
|
|
282
|
+
const base = [prefix, config.name, `v${config.version}`].filter((item) => item !== void 0).join(":");
|
|
283
|
+
if (!config.key && args.length === 0) return base;
|
|
284
|
+
return fit(
|
|
285
|
+
base,
|
|
286
|
+
typeof raw === "string" ? raw : keyOf(`Cache entry "${config.name}"`, raw)
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
const checkTags = (label2, tags) => {
|
|
290
|
+
if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === "string" && tag !== "")) {
|
|
291
|
+
throw new TypeError(
|
|
292
|
+
`${label2}: tags must be an array of non-empty strings.`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return [...new Set(tags)];
|
|
296
|
+
};
|
|
297
|
+
const tagKeysOf = (tags) => {
|
|
298
|
+
const base = prefix === void 0 ? "_tag" : `${prefix}:_tag`;
|
|
299
|
+
return Promise.all(
|
|
300
|
+
tags.map(async (tag) => ({ tag, key: await fit(base, tag) }))
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
const create = (logger) => {
|
|
304
|
+
const describe = (error) => error instanceof Error ? error.message : String(error);
|
|
305
|
+
const guard = async (operation, key, entry, run, fallback) => {
|
|
306
|
+
let timer;
|
|
307
|
+
try {
|
|
308
|
+
const running = run();
|
|
309
|
+
return storeTimeout === void 0 ? await running : await Promise.race([
|
|
310
|
+
running,
|
|
311
|
+
new Promise((_, reject) => {
|
|
312
|
+
timer = setTimeout(
|
|
313
|
+
() => reject(
|
|
314
|
+
new Error(`no answer within ${storeTimeout} milliseconds`)
|
|
315
|
+
),
|
|
316
|
+
storeTimeout
|
|
317
|
+
);
|
|
318
|
+
})
|
|
319
|
+
]);
|
|
320
|
+
} catch (cause) {
|
|
321
|
+
const error = new CacheStoreError(store.name, operation, key, {
|
|
322
|
+
cause
|
|
323
|
+
});
|
|
324
|
+
const data = {
|
|
325
|
+
store: store.name,
|
|
326
|
+
operation,
|
|
327
|
+
key,
|
|
328
|
+
entry,
|
|
329
|
+
error: error.message
|
|
330
|
+
};
|
|
331
|
+
if (operation === "delete" || operation === "invalidate") {
|
|
332
|
+
logger.error(data, "Cache store failed");
|
|
333
|
+
} else {
|
|
334
|
+
logger.warn(data, "Cache store failed");
|
|
335
|
+
}
|
|
336
|
+
if (onError) {
|
|
337
|
+
try {
|
|
338
|
+
await onError(error, { operation, key, entry });
|
|
339
|
+
} catch (hookError) {
|
|
340
|
+
logger.error(
|
|
341
|
+
{ error: describe(hookError) },
|
|
342
|
+
"Cache onError hook failed"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (!failOpen) throw error;
|
|
347
|
+
return fallback;
|
|
348
|
+
} finally {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const report = (event) => {
|
|
353
|
+
const { type, reason, entry, key } = event;
|
|
354
|
+
logger.debug?.(
|
|
355
|
+
{ entry, key, ...reason ? { reason } : {} },
|
|
356
|
+
type === "hit" ? "Cache hit" : type === "miss" ? "Cache miss" : "Cache served a stale value"
|
|
357
|
+
);
|
|
358
|
+
if (!onEvent) return;
|
|
359
|
+
try {
|
|
360
|
+
onEvent(event);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
logger.error({ error: describe(error) }, "Cache onEvent hook failed");
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const tokensOf = async (entry, tagKeys) => {
|
|
366
|
+
const keys = tagKeys.map(({ key }) => key);
|
|
367
|
+
const values = await guard(
|
|
368
|
+
"get",
|
|
369
|
+
keys.join(" "),
|
|
370
|
+
entry,
|
|
371
|
+
() => store.getMany ? store.getMany(keys) : Promise.all(keys.map((key) => store.get(key))),
|
|
372
|
+
void 0
|
|
373
|
+
);
|
|
374
|
+
return values?.map((token, index) => ({
|
|
375
|
+
tag: tagKeys[index].tag,
|
|
376
|
+
token
|
|
377
|
+
}));
|
|
378
|
+
};
|
|
379
|
+
const decode = async (config, key, data) => {
|
|
380
|
+
let value;
|
|
381
|
+
try {
|
|
382
|
+
value = serializer.deserialize(data);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
logger.warn(
|
|
385
|
+
{ entry: config.name, key, error: describe(error) },
|
|
386
|
+
"Cache value cannot be deserialized"
|
|
387
|
+
);
|
|
388
|
+
return void 0;
|
|
389
|
+
}
|
|
390
|
+
if (!config.schema) return { value };
|
|
391
|
+
const result = await config.schema.safeParseAsync(value);
|
|
392
|
+
return result.success ? { value: result.data } : void 0;
|
|
393
|
+
};
|
|
394
|
+
const lookup = async (config, policy, key) => {
|
|
395
|
+
const text = await guard(
|
|
396
|
+
"get",
|
|
397
|
+
key,
|
|
398
|
+
config.name,
|
|
399
|
+
() => store.get(key),
|
|
400
|
+
void 0
|
|
401
|
+
);
|
|
402
|
+
if (text === void 0) return void 0;
|
|
403
|
+
const stored = parseStored(text);
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
if (!stored || stored.e + Math.max(policy.grace, policy.staleWhileRevalidate) <= now) {
|
|
406
|
+
return void 0;
|
|
407
|
+
}
|
|
408
|
+
const decoded = await decode(config, key, stored.d);
|
|
409
|
+
if (!decoded) return void 0;
|
|
410
|
+
const tags = Object.entries(stored.t ?? {});
|
|
411
|
+
let tagsValid = true;
|
|
412
|
+
if (tags.length > 0) {
|
|
413
|
+
const current = await tokensOf(
|
|
414
|
+
config.name,
|
|
415
|
+
await tagKeysOf(tags.map(([tag]) => tag))
|
|
416
|
+
);
|
|
417
|
+
tagsValid = current?.every(({ tag, token }) => token === stored.t?.[tag]) ?? false;
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
value: decoded.value,
|
|
421
|
+
expiresAt: stored.e,
|
|
422
|
+
fresh: stored.e > now && tagsValid,
|
|
423
|
+
tagsValid
|
|
424
|
+
};
|
|
425
|
+
};
|
|
426
|
+
const write = async (config, key, args, value) => {
|
|
427
|
+
if (value === void 0) return void 0;
|
|
428
|
+
let data;
|
|
429
|
+
try {
|
|
430
|
+
data = serializer.serialize(value);
|
|
431
|
+
if (typeof data !== "string") throw new TypeError("not a string");
|
|
432
|
+
} catch (error) {
|
|
433
|
+
logger.warn(
|
|
434
|
+
{ entry: config.name, key, error: describe(error) },
|
|
435
|
+
"Cache value cannot be serialized"
|
|
436
|
+
);
|
|
437
|
+
return void 0;
|
|
438
|
+
}
|
|
439
|
+
const checked = config.schema ? await decode(config, key, data) : { value };
|
|
440
|
+
if (!checked) {
|
|
441
|
+
logger.warn(
|
|
442
|
+
{ entry: config.name, key },
|
|
443
|
+
"Cache value does not match the schema of the entry: not cached"
|
|
444
|
+
);
|
|
445
|
+
return void 0;
|
|
446
|
+
}
|
|
447
|
+
const policy = policyFor(config);
|
|
448
|
+
const lifetime = policy.ttl + Math.max(policy.grace, policy.staleWhileRevalidate);
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
const stored = {
|
|
451
|
+
v: 1,
|
|
452
|
+
c: now,
|
|
453
|
+
e: now + policy.ttl,
|
|
454
|
+
d: data
|
|
455
|
+
};
|
|
456
|
+
if (config.tags) {
|
|
457
|
+
const tags = checkTags(
|
|
458
|
+
`defineCacheEntry("${config.name}")`,
|
|
459
|
+
config.tags(...args)
|
|
460
|
+
);
|
|
461
|
+
if (tags.length > 0) {
|
|
462
|
+
const tagKeys = await tagKeysOf(tags);
|
|
463
|
+
const current = await tokensOf(config.name, tagKeys);
|
|
464
|
+
if (!current) return checked;
|
|
465
|
+
stored.t = {};
|
|
466
|
+
for (const [index, { tag, token }] of current.entries()) {
|
|
467
|
+
if (token !== void 0) {
|
|
468
|
+
stored.t[tag] = token;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const created = crypto.randomUUID();
|
|
472
|
+
const tagKey = tagKeys[index].key;
|
|
473
|
+
await guard(
|
|
474
|
+
"set",
|
|
475
|
+
tagKey,
|
|
476
|
+
config.name,
|
|
477
|
+
() => store.set(tagKey, created, Math.max(lifetime, TAG_LIFETIME)),
|
|
478
|
+
void 0
|
|
479
|
+
);
|
|
480
|
+
stored.t[tag] = created;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
await guard(
|
|
485
|
+
"set",
|
|
486
|
+
key,
|
|
487
|
+
config.name,
|
|
488
|
+
() => store.set(key, JSON.stringify(stored), lifetime),
|
|
489
|
+
void 0
|
|
490
|
+
);
|
|
491
|
+
return checked;
|
|
492
|
+
};
|
|
493
|
+
const load = (config, key, args, loader) => {
|
|
494
|
+
const running = loading.get(key);
|
|
495
|
+
if (running) return running;
|
|
496
|
+
const loaded = (async () => {
|
|
497
|
+
const policy = policyFor(config);
|
|
498
|
+
const lockKey = `${key}:lock`;
|
|
499
|
+
let token;
|
|
500
|
+
if (policy.lock !== void 0 && store.lock) {
|
|
501
|
+
const lock = store.lock.bind(store);
|
|
502
|
+
const ttl = policy.lock;
|
|
503
|
+
const taken = await guard(
|
|
504
|
+
"lock",
|
|
505
|
+
lockKey,
|
|
506
|
+
config.name,
|
|
507
|
+
() => lock(lockKey, ttl),
|
|
508
|
+
null
|
|
509
|
+
);
|
|
510
|
+
if (taken === void 0) {
|
|
511
|
+
const until = Date.now() + ttl;
|
|
512
|
+
while (Date.now() < until) {
|
|
513
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_POLL));
|
|
514
|
+
const found = await lookup(config, policy, key);
|
|
515
|
+
if (found?.fresh) return found.value;
|
|
516
|
+
}
|
|
517
|
+
logger.warn(
|
|
518
|
+
{ entry: config.name, key, lock: ttl },
|
|
519
|
+
"Cache lock not released in time: loading anyway"
|
|
520
|
+
);
|
|
521
|
+
} else if (taken !== null) {
|
|
522
|
+
token = taken;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const value = await loader();
|
|
527
|
+
const written = await write(config, key, args, value);
|
|
528
|
+
backoffs.delete(key);
|
|
529
|
+
return written ? written.value : value;
|
|
530
|
+
} finally {
|
|
531
|
+
if (token !== void 0 && store.unlock) {
|
|
532
|
+
const unlock = store.unlock.bind(store);
|
|
533
|
+
const held = token;
|
|
534
|
+
await guard(
|
|
535
|
+
"unlock",
|
|
536
|
+
lockKey,
|
|
537
|
+
config.name,
|
|
538
|
+
() => unlock(lockKey, held),
|
|
539
|
+
void 0
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
})();
|
|
544
|
+
loading.set(key, loaded);
|
|
545
|
+
void loaded.then(
|
|
546
|
+
() => loading.delete(key),
|
|
547
|
+
() => loading.delete(key)
|
|
548
|
+
);
|
|
549
|
+
return loaded;
|
|
550
|
+
};
|
|
551
|
+
const inBackground = (config, key, loaded) => {
|
|
552
|
+
loaded.catch((error) => {
|
|
553
|
+
logger.warn(
|
|
554
|
+
{ entry: config.name, key, error: describe(error) },
|
|
555
|
+
"Cache refresh failed"
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
const deadline = (ms, error) => {
|
|
560
|
+
let timer;
|
|
561
|
+
const promise = new Promise((_, reject) => {
|
|
562
|
+
if (ms !== void 0) timer = setTimeout(() => reject(error()), ms);
|
|
563
|
+
});
|
|
564
|
+
return { promise, clear: () => clearTimeout(timer) };
|
|
565
|
+
};
|
|
566
|
+
const cache = {
|
|
567
|
+
get: (async (entry, ...args) => {
|
|
568
|
+
const config = configOf("get", entry);
|
|
569
|
+
const key = await keyFor(config, args);
|
|
570
|
+
const found = await lookup(config, policyFor(config), key);
|
|
571
|
+
report({ type: found?.fresh ? "hit" : "miss", entry: config.name, key });
|
|
572
|
+
return found?.fresh ? found.value : void 0;
|
|
573
|
+
}),
|
|
574
|
+
set: (async (entry, ...argsAndValue) => {
|
|
575
|
+
const config = configOf("set", entry);
|
|
576
|
+
const args = argsAndValue.slice(0, -1);
|
|
577
|
+
const key = await keyFor(config, args);
|
|
578
|
+
await write(config, key, args, argsAndValue.at(-1));
|
|
579
|
+
}),
|
|
580
|
+
delete: (async (entry, ...args) => {
|
|
581
|
+
const config = configOf("delete", entry);
|
|
582
|
+
const key = await keyFor(config, args);
|
|
583
|
+
await guard(
|
|
584
|
+
"delete",
|
|
585
|
+
key,
|
|
586
|
+
config.name,
|
|
587
|
+
() => store.delete(key),
|
|
588
|
+
void 0
|
|
589
|
+
);
|
|
590
|
+
}),
|
|
591
|
+
getOrSet: (async (entry, ...argsAndLoader) => {
|
|
592
|
+
const config = configOf("getOrSet", entry);
|
|
593
|
+
const loader = argsAndLoader.at(-1);
|
|
594
|
+
if (typeof loader !== "function") {
|
|
595
|
+
throw new TypeError(
|
|
596
|
+
"cache.getOrSet(): the last argument must be the function that loads the value."
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
const args = argsAndLoader.slice(0, -1);
|
|
600
|
+
const key = await keyFor(config, args);
|
|
601
|
+
const policy = policyFor(config);
|
|
602
|
+
const found = await lookup(config, policy, key);
|
|
603
|
+
const event = { entry: config.name, key };
|
|
604
|
+
if (found?.fresh) {
|
|
605
|
+
report({ type: "hit", ...event });
|
|
606
|
+
return found.value;
|
|
607
|
+
}
|
|
608
|
+
const now = Date.now();
|
|
609
|
+
if (found?.tagsValid && found.expiresAt + policy.staleWhileRevalidate > now) {
|
|
610
|
+
report({ type: "stale", reason: "revalidate", ...event });
|
|
611
|
+
inBackground(
|
|
612
|
+
config,
|
|
613
|
+
key,
|
|
614
|
+
load(config, key, args, loader)
|
|
615
|
+
);
|
|
616
|
+
return found.value;
|
|
617
|
+
}
|
|
618
|
+
const stale = found && policy.grace > 0 && found.expiresAt + policy.grace > now ? found : void 0;
|
|
619
|
+
if (stale && (backoffs.get(key) ?? 0) > now) {
|
|
620
|
+
report({ type: "stale", reason: "backoff", ...event });
|
|
621
|
+
return stale.value;
|
|
622
|
+
}
|
|
623
|
+
report({ type: "miss", ...event });
|
|
624
|
+
const loaded = load(config, key, args, loader);
|
|
625
|
+
const slow = /* @__PURE__ */ Symbol("slow");
|
|
626
|
+
const soft = deadline(stale ? policy.timeout : void 0, () => slow);
|
|
627
|
+
const hard = deadline(
|
|
628
|
+
policy.hardTimeout,
|
|
629
|
+
() => new CacheTimeoutError(
|
|
630
|
+
config.name,
|
|
631
|
+
key,
|
|
632
|
+
policy.hardTimeout
|
|
633
|
+
)
|
|
634
|
+
);
|
|
635
|
+
try {
|
|
636
|
+
return await Promise.race([loaded, soft.promise, hard.promise]);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (error === slow || error instanceof CacheTimeoutError) {
|
|
639
|
+
inBackground(config, key, loaded);
|
|
640
|
+
}
|
|
641
|
+
if (!stale) throw error;
|
|
642
|
+
if (error === slow) {
|
|
643
|
+
logger.warn(
|
|
644
|
+
{ ...event, timeout: policy.timeout },
|
|
645
|
+
"Cache served a stale value: the loader is slow"
|
|
646
|
+
);
|
|
647
|
+
report({ type: "stale", reason: "timeout", ...event });
|
|
648
|
+
return stale.value;
|
|
649
|
+
}
|
|
650
|
+
backoffs.set(key, Date.now() + policy.graceBackoff);
|
|
651
|
+
logger.warn(
|
|
652
|
+
{ ...event, error: describe(error) },
|
|
653
|
+
"Cache served a stale value: the loader failed"
|
|
654
|
+
);
|
|
655
|
+
report({ type: "stale", reason: "error", ...event });
|
|
656
|
+
return stale.value;
|
|
657
|
+
} finally {
|
|
658
|
+
soft.clear();
|
|
659
|
+
hard.clear();
|
|
660
|
+
}
|
|
661
|
+
}),
|
|
662
|
+
invalidateTags: async (tags) => {
|
|
663
|
+
const tagKeys = await tagKeysOf(
|
|
664
|
+
checkTags("cache.invalidateTags()", tags)
|
|
665
|
+
);
|
|
666
|
+
await Promise.all(
|
|
667
|
+
tagKeys.map(
|
|
668
|
+
({ tag, key }) => guard(
|
|
669
|
+
"invalidate",
|
|
670
|
+
key,
|
|
671
|
+
`tag:${tag}`,
|
|
672
|
+
() => store.set(key, crypto.randomUUID(), TAG_LIFETIME),
|
|
673
|
+
void 0
|
|
674
|
+
)
|
|
675
|
+
)
|
|
676
|
+
);
|
|
677
|
+
logger.debug?.({ tags }, "Cache tags invalidated");
|
|
678
|
+
},
|
|
679
|
+
withLogger: (other) => create(checkLogger("withLogger()", other))
|
|
680
|
+
};
|
|
681
|
+
return Object.freeze(cache);
|
|
682
|
+
};
|
|
683
|
+
return create(rootLogger);
|
|
684
|
+
};
|
|
685
|
+
export {
|
|
686
|
+
CacheError,
|
|
687
|
+
CacheStoreError,
|
|
688
|
+
CacheTimeoutError,
|
|
689
|
+
MemoBatchError,
|
|
690
|
+
createCache,
|
|
691
|
+
createMemo,
|
|
692
|
+
createMemoryStore,
|
|
693
|
+
defineCacheEntry,
|
|
694
|
+
defineMemo
|
|
695
|
+
};
|
|
696
|
+
//# sourceMappingURL=index.js.map
|