@dotpress/cache 0.0.0-stage → 2.0.0-alpha.0

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.
@@ -0,0 +1,281 @@
1
+ // src/errors.ts
2
+ var CacheError = class extends Error {
3
+ name = "CacheError";
4
+ };
5
+ var MemoBatchError = class extends CacheError {
6
+ constructor(memo, message) {
7
+ super(`Memo "${memo}": ${message}`);
8
+ this.memo = memo;
9
+ }
10
+ memo;
11
+ name = "MemoBatchError";
12
+ };
13
+ var CacheStoreError = class extends CacheError {
14
+ constructor(store, operation, key, options) {
15
+ super(
16
+ `Cache store "${store}" failed to ${operation} "${key}": ${options.cause instanceof Error ? options.cause.message : String(options.cause)}`,
17
+ options
18
+ );
19
+ this.store = store;
20
+ this.operation = operation;
21
+ this.key = key;
22
+ }
23
+ store;
24
+ operation;
25
+ key;
26
+ name = "CacheStoreError";
27
+ };
28
+ var CacheTimeoutError = class extends CacheError {
29
+ constructor(entry, key, timeout) {
30
+ super(
31
+ `Cache entry "${entry}": the loader of "${key}" did not finish within ${timeout} milliseconds.`
32
+ );
33
+ this.entry = entry;
34
+ this.key = key;
35
+ this.timeout = timeout;
36
+ }
37
+ entry;
38
+ key;
39
+ timeout;
40
+ name = "CacheTimeoutError";
41
+ };
42
+
43
+ // src/keys.ts
44
+ var keyOf = (name, value) => {
45
+ const seen = /* @__PURE__ */ new Set();
46
+ const serialize = (item) => {
47
+ switch (typeof item) {
48
+ case "string":
49
+ return JSON.stringify(item);
50
+ case "number":
51
+ case "boolean":
52
+ return String(item);
53
+ case "bigint":
54
+ return `${item}n`;
55
+ case "undefined":
56
+ return "undefined";
57
+ }
58
+ if (item === null) return "null";
59
+ if (item instanceof Date) return `Date(${item.getTime()})`;
60
+ if (typeof item === "object") {
61
+ const prototype = Object.getPrototypeOf(item);
62
+ const plain = prototype === Object.prototype || prototype === null;
63
+ if (Array.isArray(item) || plain) {
64
+ if (seen.has(item)) {
65
+ throw new TypeError(
66
+ `${name}: cannot build a key from a circular value.`
67
+ );
68
+ }
69
+ seen.add(item);
70
+ const serialized = Array.isArray(item) ? `[${item.map(serialize).join(",")}]` : `{${Object.keys(item).filter(
71
+ (key) => item[key] !== void 0
72
+ ).sort().map(
73
+ (key) => `${JSON.stringify(key)}:${serialize(item[key])}`
74
+ ).join(",")}}`;
75
+ seen.delete(item);
76
+ return serialized;
77
+ }
78
+ }
79
+ throw new TypeError(
80
+ `${name}: cannot build a key from ${describe(item)}; add a key option that returns a string.`
81
+ );
82
+ };
83
+ return serialize(value);
84
+ };
85
+ var describe = (value) => typeof value === "function" ? "a function" : typeof value === "symbol" ? "a symbol" : `an instance of ${value.constructor?.name ?? "an unknown class"}`;
86
+
87
+ // src/memo.ts
88
+ var configs = /* @__PURE__ */ new WeakMap();
89
+ var NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
90
+ var OPTIONS = {
91
+ load: /* @__PURE__ */ new Set(["name", "load", "key"]),
92
+ batch: /* @__PURE__ */ new Set(["name", "batch", "key", "maxBatchSize", "schedule"])
93
+ };
94
+ function defineMemo(options) {
95
+ const label = "defineMemo()";
96
+ if (typeof options !== "object" || options === null) {
97
+ throw new TypeError(`${label}: expected an object of options.`);
98
+ }
99
+ const values = options;
100
+ const { name } = values;
101
+ if (typeof name !== "string" || !NAME.test(name)) {
102
+ throw new TypeError(
103
+ `${label}: name must have 1 to 128 letters, digits, ".", "_", ":" or "-", starting with a letter or a digit.`
104
+ );
105
+ }
106
+ const named = `defineMemo("${name}")`;
107
+ const kind = values.batch === void 0 ? "load" : "batch";
108
+ if (kind === "batch" && values.load !== void 0) {
109
+ throw new TypeError(`${named}: set load or batch, not both.`);
110
+ }
111
+ for (const key2 of Object.keys(values)) {
112
+ if (!OPTIONS[kind].has(key2)) {
113
+ throw new TypeError(`${named}: unknown option "${key2}".`);
114
+ }
115
+ }
116
+ const { key } = values;
117
+ if (key !== void 0 && typeof key !== "function") {
118
+ throw new TypeError(`${named}: key must be a function.`);
119
+ }
120
+ let config;
121
+ if (kind === "load") {
122
+ if (typeof values.load !== "function") {
123
+ throw new TypeError(`${named}: load or batch must be a function.`);
124
+ }
125
+ config = {
126
+ kind,
127
+ name,
128
+ load: values.load,
129
+ key
130
+ };
131
+ } else {
132
+ const {
133
+ batch,
134
+ maxBatchSize = Infinity,
135
+ schedule = defaultSchedule
136
+ } = values;
137
+ if (typeof batch !== "function") {
138
+ throw new TypeError(`${named}: batch must be a function.`);
139
+ }
140
+ if (maxBatchSize !== Infinity && (!Number.isInteger(maxBatchSize) || maxBatchSize < 1)) {
141
+ throw new TypeError(`${named}: maxBatchSize must be a positive integer.`);
142
+ }
143
+ if (typeof schedule !== "function") {
144
+ throw new TypeError(`${named}: schedule must be a function.`);
145
+ }
146
+ config = {
147
+ kind,
148
+ name,
149
+ batch,
150
+ key,
151
+ maxBatchSize,
152
+ schedule
153
+ };
154
+ }
155
+ const memo = Object.freeze({ name });
156
+ configs.set(memo, config);
157
+ return memo;
158
+ }
159
+ var defaultSchedule = (run) => {
160
+ setTimeout(run, 0);
161
+ };
162
+ var createMemo = () => {
163
+ const values = /* @__PURE__ */ new Map();
164
+ const queues = /* @__PURE__ */ new Map();
165
+ const configOf = (label, memo2) => {
166
+ const config = typeof memo2 === "object" && memo2 !== null ? configs.get(memo2) : void 0;
167
+ if (!config) {
168
+ throw new TypeError(`${label}: expected a memo created by defineMemo().`);
169
+ }
170
+ return config;
171
+ };
172
+ const keyFor = (config, args) => config.kind === "load" ? keyOf(`Memo "${config.name}"`, config.key ? config.key(...args) : args) : keyOf(
173
+ `Memo "${config.name}"`,
174
+ config.key ? config.key(args[0]) : args[0]
175
+ );
176
+ const valuesOf = (config) => {
177
+ let map = values.get(config);
178
+ if (!map) {
179
+ map = /* @__PURE__ */ new Map();
180
+ values.set(config, map);
181
+ }
182
+ return map;
183
+ };
184
+ const runBatch = (config) => {
185
+ const queue = queues.get(config) ?? [];
186
+ queues.delete(config);
187
+ for (let start = 0; start < queue.length; start += config.maxBatchSize) {
188
+ const chunk = queue.slice(start, start + config.maxBatchSize);
189
+ void (async () => {
190
+ let result;
191
+ try {
192
+ result = await config.batch(chunk.map(({ key }) => key));
193
+ } catch (error) {
194
+ for (const { reject } of chunk) reject(error);
195
+ return;
196
+ }
197
+ if (Array.isArray(result)) {
198
+ if (result.length !== chunk.length) {
199
+ const error = new MemoBatchError(
200
+ config.name,
201
+ `batch() returned ${result.length} values for ${chunk.length} keys: return one value per key, in the order of the keys, or a Map.`
202
+ );
203
+ for (const { reject } of chunk) reject(error);
204
+ return;
205
+ }
206
+ chunk.forEach(({ resolve, reject }, index) => {
207
+ const value = result[index];
208
+ if (value instanceof Error) reject(value);
209
+ else resolve(value);
210
+ });
211
+ } else if (result instanceof Map) {
212
+ for (const { key, resolve } of chunk) resolve(result.get(key));
213
+ } else {
214
+ const error = new MemoBatchError(
215
+ config.name,
216
+ "batch() must return an array of values, in the order of the keys, or a Map."
217
+ );
218
+ for (const { reject } of chunk) reject(error);
219
+ }
220
+ })();
221
+ }
222
+ };
223
+ const loadOf = (config, args) => {
224
+ if (config.kind === "load") {
225
+ return (async () => config.load(...args))();
226
+ }
227
+ return new Promise((resolve, reject) => {
228
+ let queue = queues.get(config);
229
+ if (!queue) {
230
+ queue = [];
231
+ queues.set(config, queue);
232
+ config.schedule(() => runBatch(config));
233
+ }
234
+ queue.push({ key: args[0], resolve, reject });
235
+ });
236
+ };
237
+ const memo = ((definition, ...args) => {
238
+ const config = configOf("memo()", definition);
239
+ const map = valuesOf(config);
240
+ const key = keyFor(config, args);
241
+ const known = map.get(key);
242
+ if (known) return known;
243
+ const loading = loadOf(config, args);
244
+ map.set(key, loading);
245
+ loading.catch(() => {
246
+ if (map.get(key) === loading) map.delete(key);
247
+ });
248
+ return loading;
249
+ });
250
+ memo.set = (definition, ...argsAndValue) => {
251
+ const config = configOf("memo.set()", definition);
252
+ const args = argsAndValue.slice(0, -1);
253
+ valuesOf(config).set(
254
+ keyFor(config, args),
255
+ Promise.resolve(argsAndValue.at(-1))
256
+ );
257
+ };
258
+ memo.delete = (definition, ...args) => {
259
+ const config = configOf("memo.delete()", definition);
260
+ valuesOf(config).delete(keyFor(config, args));
261
+ };
262
+ memo.clear = (definition) => {
263
+ if (definition === void 0) {
264
+ values.clear();
265
+ return;
266
+ }
267
+ values.delete(configOf("memo.clear()", definition));
268
+ };
269
+ return Object.freeze(memo);
270
+ };
271
+
272
+ export {
273
+ CacheError,
274
+ MemoBatchError,
275
+ CacheStoreError,
276
+ CacheTimeoutError,
277
+ keyOf,
278
+ defineMemo,
279
+ createMemo
280
+ };
281
+ //# sourceMappingURL=chunk-6ZNJJARH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/keys.ts","../src/memo.ts"],"sourcesContent":["/** Base class of the errors thrown by `@dotpress/cache`. */\nexport class CacheError extends Error {\n override name = 'CacheError'\n}\n\n/**\n * The `batch` function of a memo returned a result that does not match\n * the keys: every call of the batch fails with this error.\n */\nexport class MemoBatchError extends CacheError {\n override name = 'MemoBatchError'\n\n constructor(\n /** Name of the memo. */\n readonly memo: string,\n message: string\n ) {\n super(`Memo \"${memo}\": ${message}`)\n }\n}\n\n/**\n * Operation of a cache store: `invalidate` writes the tokens of tags,\n * `lock` and `unlock` hold the distributed lock of a load.\n */\nexport type StoreOperation =\n 'get' | 'set' | 'delete' | 'invalidate' | 'lock' | 'unlock'\n\n/** A cache store failed or did not answer in time (`failOpen: false`). */\nexport class CacheStoreError extends CacheError {\n override name = 'CacheStoreError'\n\n constructor(\n /** Name of the store. */\n readonly store: string,\n readonly operation: StoreOperation,\n /** Key of the store. */\n readonly key: string,\n options: { cause: unknown }\n ) {\n super(\n `Cache store \"${store}\" failed to ${operation} \"${key}\": ${options.cause instanceof Error ? options.cause.message : String(options.cause)}`,\n options\n )\n }\n}\n\n/**\n * The loader of `getOrSet()` did not finish within `hardTimeout`, and no\n * stale value could be served. The load goes on, and caches its value.\n */\nexport class CacheTimeoutError extends CacheError {\n override name = 'CacheTimeoutError'\n\n constructor(\n /** Name of the entry. */\n readonly entry: string,\n /** Key of the store. */\n readonly key: string,\n /** Timeout, in milliseconds. */\n readonly timeout: number\n ) {\n super(\n `Cache entry \"${entry}\": the loader of \"${key}\" did not finish within ${timeout} milliseconds.`\n )\n }\n}\n","/**\n * Serializes a value to a key: the same for equal values, whatever the\n * order of the properties of objects.\n */\nexport const keyOf = (name: string, value: unknown): string => {\n const seen = new Set<object>()\n const serialize = (item: unknown): string => {\n switch (typeof item) {\n case 'string':\n return JSON.stringify(item)\n case 'number':\n case 'boolean':\n return String(item)\n case 'bigint':\n return `${item}n`\n case 'undefined':\n return 'undefined'\n }\n if (item === null) return 'null'\n if (item instanceof Date) return `Date(${item.getTime()})`\n if (typeof item === 'object') {\n const prototype = Object.getPrototypeOf(item) as unknown\n const plain = prototype === Object.prototype || prototype === null\n if (Array.isArray(item) || plain) {\n if (seen.has(item)) {\n throw new TypeError(\n `${name}: cannot build a key from a circular value.`\n )\n }\n seen.add(item)\n const serialized = Array.isArray(item)\n ? `[${item.map(serialize).join(',')}]`\n : `{${Object.keys(item)\n .filter(\n (key) => (item as Record<string, unknown>)[key] !== undefined\n )\n .sort()\n .map(\n (key) =>\n `${JSON.stringify(key)}:${serialize((item as Record<string, unknown>)[key])}`\n )\n .join(',')}}`\n seen.delete(item)\n return serialized\n }\n }\n throw new TypeError(\n `${name}: cannot build a key from ${describe(item)}; add a key option that returns a string.`\n )\n }\n return serialize(value)\n}\n\nconst describe = (value: unknown) =>\n typeof value === 'function'\n ? 'a function'\n : typeof value === 'symbol'\n ? 'a symbol'\n : `an instance of ${(value as object).constructor?.name ?? 'an unknown class'}`\n","import { MemoBatchError } from './errors.js'\nimport { keyOf } from './keys.js'\n\n/** Options shared by every memo. */\ninterface BaseMemoOptions {\n /**\n * Name of the memo, used in errors: letters, digits, `.`, `_`, `:` and\n * `-`.\n */\n name: string\n}\n\n/** Options of a memo that loads one value at a time. */\nexport interface LoadMemoOptions<\n A extends readonly unknown[],\n T,\n> extends BaseMemoOptions {\n /** Loads the value of the arguments. */\n load: (...args: A) => T | Promise<T>\n /**\n * Key of the arguments. Defaults to the arguments themselves: strings,\n * numbers, booleans, `null`, `undefined`, bigints, dates, arrays and\n * plain objects.\n */\n key?: ((...args: A) => unknown) | undefined\n batch?: never\n}\n\n/** Options of a memo that loads the values of several keys at once. */\nexport interface BatchMemoOptions<K, R> extends BaseMemoOptions {\n /**\n * Loads the values of the keys requested in the same tick, without\n * duplicates. Returns their values in the order of the keys (an `Error`\n * fails its key only), or a `Map` by key (a missing key gives\n * `undefined`).\n */\n batch: (keys: readonly K[]) => R | Promise<R>\n /** Key of a requested key, for keys that are objects. */\n key?: ((key: K) => unknown) | undefined\n /** Keys per call of `batch`. Defaults to no limit. */\n maxBatchSize?: number | undefined\n /**\n * Schedules a batch. Defaults to a timer (`setTimeout(run, 0)`): the\n * batch receives every key requested before it.\n */\n schedule?: ((run: () => void) => void) | undefined\n load?: never\n}\n\ndeclare const memoTypes: unique symbol\n\n/** A memoized loader, created by `defineMemo()`. */\nexport interface MemoDefinition<A extends readonly unknown[], T> {\n readonly name: string\n /** Types only. */\n readonly [memoTypes]?: { args: A; value: T }\n}\n\n/** Memoized values of a unit of work: a request, a message, a job. */\nexport interface Memo {\n /**\n * Loads the value of the arguments once per memo: later calls, and\n * calls made while it loads, share the result. Failures are shared by\n * the calls in progress, not kept.\n */\n <A extends readonly unknown[], T>(\n memo: MemoDefinition<A, T>,\n ...args: A\n ): Promise<T>\n /** Sets the value of the arguments, after an update for instance. */\n set<A extends readonly unknown[], T>(\n memo: MemoDefinition<A, T>,\n ...argsAndValue: [...A, T]\n ): void\n /** Forgets the value of the arguments: the next call loads it again. */\n delete<A extends readonly unknown[], T>(\n memo: MemoDefinition<A, T>,\n ...args: A\n ): void\n /** Forgets the values of a memo, or of every memo. */\n clear<A extends readonly unknown[], T>(memo?: MemoDefinition<A, T>): void\n}\n\ntype Config =\n | {\n kind: 'load'\n name: string\n load: (...args: readonly unknown[]) => unknown\n key: ((...args: readonly unknown[]) => unknown) | undefined\n }\n | {\n kind: 'batch'\n name: string\n batch: (keys: readonly unknown[]) => unknown\n key: ((key: unknown) => unknown) | undefined\n maxBatchSize: number\n schedule: (run: () => void) => void\n }\n\nconst configs = new WeakMap<object, Config>()\n\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/\nconst OPTIONS = {\n load: new Set(['name', 'load', 'key']),\n batch: new Set(['name', 'batch', 'key', 'maxBatchSize', 'schedule']),\n}\n\n/**\n * Defines a memoized loader: `memo(loadUser, id)` loads a user once per\n * memo (request, message, job), and calls made while it loads share the\n * same load (single flight). With `batch`, the keys requested in the same\n * tick are loaded in one call.\n *\n * ```ts\n * const loadUser = defineMemo({\n * name: 'user',\n * load: (id: string) => db.users.findById(id),\n * })\n *\n * const loadOrder = defineMemo({\n * name: 'order',\n * batch: (ids: readonly string[]) => db.orders.findManyById(ids), // a Map by id\n * })\n * ```\n */\nexport function defineMemo<K, T>(\n options: BatchMemoOptions<K, ReadonlyMap<K, T>>\n): MemoDefinition<[key: K], T | undefined>\nexport function defineMemo<K, T>(\n options: BatchMemoOptions<K, readonly (T | Error)[]>\n): MemoDefinition<[key: K], T>\nexport function defineMemo<A extends readonly unknown[], T>(\n options: LoadMemoOptions<A, T>\n): MemoDefinition<A, T>\nexport function defineMemo(options: unknown): { readonly name: string } {\n const label = 'defineMemo()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n const values = options as Record<string, unknown>\n const { name } = values\n if (typeof name !== 'string' || !NAME.test(name)) {\n throw new TypeError(\n `${label}: name must have 1 to 128 letters, digits, \".\", \"_\", \":\" or \"-\", starting with a letter or a digit.`\n )\n }\n const named = `defineMemo(\"${name}\")`\n const kind = values.batch === undefined ? 'load' : 'batch'\n if (kind === 'batch' && values.load !== undefined) {\n throw new TypeError(`${named}: set load or batch, not both.`)\n }\n for (const key of Object.keys(values)) {\n if (!OPTIONS[kind].has(key)) {\n throw new TypeError(`${named}: unknown option \"${key}\".`)\n }\n }\n const { key } = values\n if (key !== undefined && typeof key !== 'function') {\n throw new TypeError(`${named}: key must be a function.`)\n }\n\n let config: Config\n if (kind === 'load') {\n if (typeof values.load !== 'function') {\n throw new TypeError(`${named}: load or batch must be a function.`)\n }\n config = {\n kind,\n name,\n load: values.load as (...args: readonly unknown[]) => unknown,\n key: key as ((...args: readonly unknown[]) => unknown) | undefined,\n }\n } else {\n const {\n batch,\n maxBatchSize = Infinity,\n schedule = defaultSchedule,\n } = values\n if (typeof batch !== 'function') {\n throw new TypeError(`${named}: batch must be a function.`)\n }\n if (\n maxBatchSize !== Infinity &&\n (!Number.isInteger(maxBatchSize) || (maxBatchSize as number) < 1)\n ) {\n throw new TypeError(`${named}: maxBatchSize must be a positive integer.`)\n }\n if (typeof schedule !== 'function') {\n throw new TypeError(`${named}: schedule must be a function.`)\n }\n config = {\n kind,\n name,\n batch: batch as (keys: readonly unknown[]) => unknown,\n key: key as ((key: unknown) => unknown) | undefined,\n maxBatchSize: maxBatchSize as number,\n schedule: schedule as (run: () => void) => void,\n }\n }\n\n const memo = Object.freeze({ name })\n configs.set(memo, config)\n return memo\n}\n\nconst defaultSchedule = (run: () => void) => {\n setTimeout(run, 0)\n}\n\ninterface Deferred {\n key: unknown\n resolve: (value: unknown) => void\n reject: (error: unknown) => void\n}\n\n/**\n * Creates a memo: one per request, message or job. Plugins provide\n * `ctx.memo`; create one yourself elsewhere.\n *\n * ```ts\n * const memo = createMemo()\n * const [ada, grace] = await Promise.all([memo(loadUser, '1'), memo(loadUser, '2')])\n * ```\n */\nexport const createMemo = (): Memo => {\n const values = new Map<Config, Map<string, Promise<unknown>>>()\n const queues = new Map<Config, Deferred[]>()\n\n const configOf = (label: string, memo: unknown) => {\n const config =\n typeof memo === 'object' && memo !== null ? configs.get(memo) : undefined\n if (!config) {\n throw new TypeError(`${label}: expected a memo created by defineMemo().`)\n }\n return config\n }\n\n const keyFor = (config: Config, args: readonly unknown[]) =>\n config.kind === 'load'\n ? keyOf(`Memo \"${config.name}\"`, config.key ? config.key(...args) : args)\n : keyOf(\n `Memo \"${config.name}\"`,\n config.key ? config.key(args[0]) : args[0]\n )\n\n const valuesOf = (config: Config) => {\n let map = values.get(config)\n if (!map) {\n map = new Map()\n values.set(config, map)\n }\n return map\n }\n\n /** Runs the queued keys of a batch memo, in calls of maxBatchSize. */\n const runBatch = (config: Extract<Config, { kind: 'batch' }>) => {\n const queue = queues.get(config) ?? []\n queues.delete(config)\n for (let start = 0; start < queue.length; start += config.maxBatchSize) {\n const chunk = queue.slice(start, start + config.maxBatchSize)\n void (async () => {\n let result: unknown\n try {\n result = await config.batch(chunk.map(({ key }) => key))\n } catch (error) {\n for (const { reject } of chunk) reject(error)\n return\n }\n if (Array.isArray(result)) {\n if (result.length !== chunk.length) {\n const error = new MemoBatchError(\n config.name,\n `batch() returned ${result.length} values for ${chunk.length} keys: return one value per key, in the order of the keys, or a Map.`\n )\n for (const { reject } of chunk) reject(error)\n return\n }\n chunk.forEach(({ resolve, reject }, index) => {\n const value: unknown = result[index]\n if (value instanceof Error) reject(value)\n else resolve(value)\n })\n } else if (result instanceof Map) {\n for (const { key, resolve } of chunk) resolve(result.get(key))\n } else {\n const error = new MemoBatchError(\n config.name,\n 'batch() must return an array of values, in the order of the keys, or a Map.'\n )\n for (const { reject } of chunk) reject(error)\n }\n })()\n }\n }\n\n const loadOf = (config: Config, args: readonly unknown[]) => {\n if (config.kind === 'load') {\n return (async () => config.load(...args))()\n }\n return new Promise<unknown>((resolve, reject) => {\n let queue = queues.get(config)\n if (!queue) {\n queue = []\n queues.set(config, queue)\n config.schedule(() => runBatch(config))\n }\n queue.push({ key: args[0], resolve, reject })\n })\n }\n\n const memo = (<A extends readonly unknown[], T>(\n definition: MemoDefinition<A, T>,\n ...args: A\n ): Promise<T> => {\n const config = configOf('memo()', definition)\n const map = valuesOf(config)\n const key = keyFor(config, args)\n const known = map.get(key)\n if (known) return known as Promise<T>\n\n const loading = loadOf(config, args)\n map.set(key, loading)\n // Failures are not kept: the next call loads again.\n loading.catch(() => {\n if (map.get(key) === loading) map.delete(key)\n })\n return loading as Promise<T>\n }) as Memo\n\n memo.set = (definition, ...argsAndValue) => {\n const config = configOf('memo.set()', definition)\n const args = argsAndValue.slice(0, -1)\n valuesOf(config).set(\n keyFor(config, args),\n Promise.resolve(argsAndValue.at(-1))\n )\n }\n memo.delete = (definition, ...args) => {\n const config = configOf('memo.delete()', definition)\n valuesOf(config).delete(keyFor(config, args))\n }\n memo.clear = (definition) => {\n if (definition === undefined) {\n values.clear()\n return\n }\n values.delete(configOf('memo.clear()', definition))\n }\n return Object.freeze(memo)\n}\n"],"mappings":"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","names":["key","memo"]}
@@ -0,0 +1,52 @@
1
+ import { Duration } from './index.js';
2
+ import { CacheStore } from './index.js';
3
+
4
+ /** A Workers KV binding (the fields used here). */
5
+ interface KvNamespace {
6
+ get(key: string, options?: {
7
+ type?: 'text';
8
+ cacheTtl?: number;
9
+ }): Promise<string | null>;
10
+ get(keys: string[], options?: {
11
+ type?: 'text';
12
+ cacheTtl?: number;
13
+ }): Promise<Map<string, string | null>>;
14
+ put(key: string, value: string, options?: {
15
+ expirationTtl?: number;
16
+ }): Promise<void>;
17
+ delete(key: string): Promise<void>;
18
+ }
19
+ /** Options of `createCloudflareKvStore()`. */
20
+ interface CloudflareKvStoreOptions {
21
+ /**
22
+ * The KV binding, or a function that returns it (with `env` imported
23
+ * from `cloudflare:workers`, resolved on use).
24
+ */
25
+ kv: KvNamespace | (() => KvNamespace);
26
+ /**
27
+ * How long Cloudflare keeps read values in the cache of the location
28
+ * (`cacheTtl` of `get`, at least 30 seconds): faster reads, but writes
29
+ * and invalidations from other locations are seen later. Defaults to
30
+ * the 60 seconds of KV.
31
+ */
32
+ cacheTtl?: Duration | undefined;
33
+ /** Name of the store, in logs and errors. Defaults to `cloudflare-kv`. */
34
+ name?: string | undefined;
35
+ }
36
+ /**
37
+ * Creates a store on [Workers KV](https://developers.cloudflare.com/kv),
38
+ * through the binding of a Worker: no dependency. KV is eventually
39
+ * consistent (a write can take 60 seconds or more to reach other
40
+ * locations) and has no locks: a fit for values read often and changed
41
+ * rarely.
42
+ *
43
+ * ```ts
44
+ * import { env } from 'cloudflare:workers'
45
+ *
46
+ * const store = createCloudflareKvStore({ kv: () => env.CACHE })
47
+ * ```
48
+ */
49
+ declare const createCloudflareKvStore: (options: CloudflareKvStoreOptions) => CacheStore;
50
+
51
+ export { createCloudflareKvStore };
52
+ export type { CloudflareKvStoreOptions, KvNamespace };
@@ -0,0 +1,75 @@
1
+ import {
2
+ toMilliseconds
3
+ } from "./chunk-2IEBMYSF.js";
4
+
5
+ // src/stores/cloudflare-kv.ts
6
+ var MAX_KEY_BYTES = 512;
7
+ var MAX_BULK = 100;
8
+ var MIN_EXPIRATION_TTL = 60;
9
+ var createCloudflareKvStore = (options) => {
10
+ const label = "createCloudflareKvStore()";
11
+ if (typeof options !== "object" || options === null) {
12
+ throw new TypeError(`${label}: expected an object of options.`);
13
+ }
14
+ for (const key of Object.keys(options)) {
15
+ if (!["kv", "cacheTtl", "name"].includes(key)) {
16
+ throw new TypeError(`${label}: unknown option "${key}".`);
17
+ }
18
+ }
19
+ const { kv, name = "cloudflare-kv" } = options;
20
+ const isBinding = (value) => typeof value?.get === "function" && typeof value.put === "function" && typeof value.delete === "function";
21
+ if (typeof kv !== "function" && !isBinding(kv)) {
22
+ throw new TypeError(
23
+ `${label}: kv must be a KV binding, or a function that returns it.`
24
+ );
25
+ }
26
+ let cacheTtl;
27
+ if (options.cacheTtl !== void 0) {
28
+ const ms = toMilliseconds(`${label}: cacheTtl`, options.cacheTtl);
29
+ if (ms < 3e4) {
30
+ throw new TypeError(`${label}: cacheTtl must be at least 30 seconds.`);
31
+ }
32
+ cacheTtl = Math.round(ms / 1e3);
33
+ }
34
+ if (typeof name !== "string" || name === "") {
35
+ throw new TypeError(`${label}: name must be a non-empty string.`);
36
+ }
37
+ const binding = () => {
38
+ const value = typeof kv === "function" ? kv() : kv;
39
+ if (!isBinding(value)) {
40
+ throw new TypeError(`${label}: kv() must return a KV binding.`);
41
+ }
42
+ return value;
43
+ };
44
+ const read = { type: "text", ...cacheTtl ? { cacheTtl } : {} };
45
+ return Object.freeze({
46
+ name,
47
+ maxKeyLength: MAX_KEY_BYTES,
48
+ get: async (key) => await binding().get(key, read) ?? void 0,
49
+ getMany: async (keys) => {
50
+ const namespace = binding();
51
+ const unique = [...new Set(keys)];
52
+ const values = /* @__PURE__ */ new Map();
53
+ for (let start = 0; start < unique.length; start += MAX_BULK) {
54
+ const found = await namespace.get(
55
+ unique.slice(start, start + MAX_BULK),
56
+ read
57
+ );
58
+ for (const [key, value] of found) values.set(key, value);
59
+ }
60
+ return keys.map((key) => values.get(key) ?? void 0);
61
+ },
62
+ set: async (key, value, ttl) => {
63
+ await binding().put(key, value, {
64
+ expirationTtl: Math.max(MIN_EXPIRATION_TTL, Math.ceil(ttl / 1e3))
65
+ });
66
+ },
67
+ delete: async (key) => {
68
+ await binding().delete(key);
69
+ }
70
+ });
71
+ };
72
+ export {
73
+ createCloudflareKvStore
74
+ };
75
+ //# sourceMappingURL=cloudflare-kv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stores/cloudflare-kv.ts"],"sourcesContent":["import { toMilliseconds, type Duration } from '../duration.js'\nimport type { CacheStore } from '../store.js'\n\n/** A Workers KV binding (the fields used here). */\nexport interface KvNamespace {\n get(\n key: string,\n options?: { type?: 'text'; cacheTtl?: number }\n ): Promise<string | null>\n get(\n keys: string[],\n options?: { type?: 'text'; cacheTtl?: number }\n ): Promise<Map<string, string | null>>\n put(\n key: string,\n value: string,\n options?: { expirationTtl?: number }\n ): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/** Options of `createCloudflareKvStore()`. */\nexport interface CloudflareKvStoreOptions {\n /**\n * The KV binding, or a function that returns it (with `env` imported\n * from `cloudflare:workers`, resolved on use).\n */\n kv: KvNamespace | (() => KvNamespace)\n /**\n * How long Cloudflare keeps read values in the cache of the location\n * (`cacheTtl` of `get`, at least 30 seconds): faster reads, but writes\n * and invalidations from other locations are seen later. Defaults to\n * the 60 seconds of KV.\n */\n cacheTtl?: Duration | undefined\n /** Name of the store, in logs and errors. Defaults to `cloudflare-kv`. */\n name?: string | undefined\n}\n\n// Limits of Workers KV: keys of 512 bytes, 100 keys per bulk read, and an\n// expiration of 60 seconds at least.\nconst MAX_KEY_BYTES = 512\nconst MAX_BULK = 100\nconst MIN_EXPIRATION_TTL = 60\n\n/**\n * Creates a store on [Workers KV](https://developers.cloudflare.com/kv),\n * through the binding of a Worker: no dependency. KV is eventually\n * consistent (a write can take 60 seconds or more to reach other\n * locations) and has no locks: a fit for values read often and changed\n * rarely.\n *\n * ```ts\n * import { env } from 'cloudflare:workers'\n *\n * const store = createCloudflareKvStore({ kv: () => env.CACHE })\n * ```\n */\nexport const createCloudflareKvStore = (\n options: CloudflareKvStoreOptions\n): CacheStore => {\n const label = 'createCloudflareKvStore()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (!['kv', 'cacheTtl', 'name'].includes(key)) {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const { kv, name = 'cloudflare-kv' } = options\n const isBinding = (value: unknown): value is KvNamespace =>\n typeof (value as KvNamespace | null)?.get === 'function' &&\n typeof (value as KvNamespace).put === 'function' &&\n typeof (value as KvNamespace).delete === 'function'\n if (typeof kv !== 'function' && !isBinding(kv)) {\n throw new TypeError(\n `${label}: kv must be a KV binding, or a function that returns it.`\n )\n }\n let cacheTtl: number | undefined\n if (options.cacheTtl !== undefined) {\n const ms = toMilliseconds(`${label}: cacheTtl`, options.cacheTtl)\n if (ms < 30_000) {\n throw new TypeError(`${label}: cacheTtl must be at least 30 seconds.`)\n }\n cacheTtl = Math.round(ms / 1000)\n }\n if (typeof name !== 'string' || name === '') {\n throw new TypeError(`${label}: name must be a non-empty string.`)\n }\n\n const binding = () => {\n const value = typeof kv === 'function' ? kv() : kv\n if (!isBinding(value)) {\n throw new TypeError(`${label}: kv() must return a KV binding.`)\n }\n return value\n }\n const read = { type: 'text' as const, ...(cacheTtl ? { cacheTtl } : {}) }\n\n return Object.freeze({\n name,\n maxKeyLength: MAX_KEY_BYTES,\n get: async (key: string) => (await binding().get(key, read)) ?? undefined,\n getMany: async (keys: readonly string[]) => {\n const namespace = binding()\n const unique = [...new Set(keys)]\n const values = new Map<string, string | null>()\n for (let start = 0; start < unique.length; start += MAX_BULK) {\n const found = await namespace.get(\n unique.slice(start, start + MAX_BULK),\n read\n )\n for (const [key, value] of found) values.set(key, value)\n }\n return keys.map((key) => values.get(key) ?? undefined)\n },\n set: async (key: string, value: string, ttl: number) => {\n // The cache keeps the real expiry in the value; KV needs 60 s.\n await binding().put(key, value, {\n expirationTtl: Math.max(MIN_EXPIRATION_TTL, Math.ceil(ttl / 1000)),\n })\n },\n delete: async (key: string) => {\n await binding().delete(key)\n },\n })\n}\n"],"mappings":";;;;;AAyCA,IAAM,gBAAgB;AACtB,IAAM,WAAW;AACjB,IAAM,qBAAqB;AAepB,IAAM,0BAA0B,CACrC,YACe;AACf,QAAM,QAAQ;AACd,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,UAAM,IAAI,UAAU,GAAG,KAAK,kCAAkC;AAAA,EAChE;AACA,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,QAAI,CAAC,CAAC,MAAM,YAAY,MAAM,EAAE,SAAS,GAAG,GAAG;AAC7C,YAAM,IAAI,UAAU,GAAG,KAAK,qBAAqB,GAAG,IAAI;AAAA,IAC1D;AAAA,EACF;AACA,QAAM,EAAE,IAAI,OAAO,gBAAgB,IAAI;AACvC,QAAM,YAAY,CAAC,UACjB,OAAQ,OAA8B,QAAQ,cAC9C,OAAQ,MAAsB,QAAQ,cACtC,OAAQ,MAAsB,WAAW;AAC3C,MAAI,OAAO,OAAO,cAAc,CAAC,UAAU,EAAE,GAAG;AAC9C,UAAM,IAAI;AAAA,MACR,GAAG,KAAK;AAAA,IACV;AAAA,EACF;AACA,MAAI;AACJ,MAAI,QAAQ,aAAa,QAAW;AAClC,UAAM,KAAK,eAAe,GAAG,KAAK,cAAc,QAAQ,QAAQ;AAChE,QAAI,KAAK,KAAQ;AACf,YAAM,IAAI,UAAU,GAAG,KAAK,yCAAyC;AAAA,IACvE;AACA,eAAW,KAAK,MAAM,KAAK,GAAI;AAAA,EACjC;AACA,MAAI,OAAO,SAAS,YAAY,SAAS,IAAI;AAC3C,UAAM,IAAI,UAAU,GAAG,KAAK,oCAAoC;AAAA,EAClE;AAEA,QAAM,UAAU,MAAM;AACpB,UAAM,QAAQ,OAAO,OAAO,aAAa,GAAG,IAAI;AAChD,QAAI,CAAC,UAAU,KAAK,GAAG;AACrB,YAAM,IAAI,UAAU,GAAG,KAAK,kCAAkC;AAAA,IAChE;AACA,WAAO;AAAA,EACT;AACA,QAAM,OAAO,EAAE,MAAM,QAAiB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC,EAAG;AAExE,SAAO,OAAO,OAAO;AAAA,IACnB;AAAA,IACA,cAAc;AAAA,IACd,KAAK,OAAO,QAAiB,MAAM,QAAQ,EAAE,IAAI,KAAK,IAAI,KAAM;AAAA,IAChE,SAAS,OAAO,SAA4B;AAC1C,YAAM,YAAY,QAAQ;AAC1B,YAAM,SAAS,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;AAChC,YAAM,SAAS,oBAAI,IAA2B;AAC9C,eAAS,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS,UAAU;AAC5D,cAAM,QAAQ,MAAM,UAAU;AAAA,UAC5B,OAAO,MAAM,OAAO,QAAQ,QAAQ;AAAA,UACpC;AAAA,QACF;AACA,mBAAW,CAAC,KAAK,KAAK,KAAK,MAAO,QAAO,IAAI,KAAK,KAAK;AAAA,MACzD;AACA,aAAO,KAAK,IAAI,CAAC,QAAQ,OAAO,IAAI,GAAG,KAAK,MAAS;AAAA,IACvD;AAAA,IACA,KAAK,OAAO,KAAa,OAAe,QAAgB;AAEtD,YAAM,QAAQ,EAAE,IAAI,KAAK,OAAO;AAAA,QAC9B,eAAe,KAAK,IAAI,oBAAoB,KAAK,KAAK,MAAM,GAAI,CAAC;AAAA,MACnE,CAAC;AAAA,IACH;AAAA,IACA,QAAQ,OAAO,QAAgB;AAC7B,YAAM,QAAQ,EAAE,OAAO,GAAG;AAAA,IAC5B;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A duration: a number of seconds, or a string with a unit: `'500ms'`,
3
+ * `'30s'`, `'10m'`, `'1h'`, `'7d'`.
4
+ */
5
+ type Duration = number | `${number}${DurationUnit}`;
6
+ type DurationUnit = 'ms' | 's' | 'm' | 'h' | 'd';
7
+
8
+ export type { Duration as D };