@alxia/redis 0.1.3 → 0.3.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.
package/dist/index.js CHANGED
@@ -1,6 +1,36 @@
1
1
  // src/cache-store.ts
2
2
  import { bindCache, defineCache } from "@nxgt/redis";
3
3
  import { z } from "zod";
4
+
5
+ // src/handle.ts
6
+ function isHandle(target) {
7
+ return "instances" in target && "close" in target && typeof target.close === "function";
8
+ }
9
+ function resolve(target) {
10
+ if (!isHandle(target))
11
+ return { client: target, prefix: undefined };
12
+ const names = Object.keys(target.instances);
13
+ const [only] = names;
14
+ if (names.length !== 1 || only === undefined) {
15
+ throw new TypeError(`@alxia/redis: this @nxgt/redis handle wires ${names.length} Redis instances (${names.map((name) => `"${name}"`).join(", ")}), and one is needed. Pass the client of one, as handle.clients.${names[0] ?? "name"}, with its prefix in the name you give.`);
16
+ }
17
+ const instance = target.instances[only];
18
+ return { client: instance.client, prefix: instance.prefix };
19
+ }
20
+ function nameUnder(target, name) {
21
+ const { client, prefix } = resolve(target);
22
+ return { client, name: prefix === undefined ? name : `${prefix}:${name}` };
23
+ }
24
+ function isWiredLimit(value) {
25
+ const limit = value;
26
+ return typeof limit.consume === "function" && typeof limit.peek === "function" && typeof limit.reset === "function" && typeof limit.keyFor === "function";
27
+ }
28
+ function isWiredIdempotency(value) {
29
+ const bound = value;
30
+ return typeof bound.run === "function" && typeof bound.forget === "function" && typeof bound.keyFor === "function";
31
+ }
32
+
33
+ // src/cache-store.ts
4
34
  var Stored = z.object({
5
35
  status: z.number().int(),
6
36
  headers: z.array(z.tuple([z.string(), z.string()])),
@@ -11,16 +41,9 @@ var Stored = z.object({
11
41
  tags: z.array(z.string())
12
42
  });
13
43
  var REFUSED_ARGUMENTS = /wrong number of arguments|syntax error/i;
14
- function redisCacheStore(client, options) {
15
- const records = bindCache(client, defineCache({
16
- name: `${options.name}:response`,
17
- key: (key) => key,
18
- ttl: 60,
19
- schema: Stored
20
- }));
21
- const tagKey = (tag) => `${options.name}:tag:${tag}`;
44
+ function tagKeeper(client) {
22
45
  let flagsKnown = true;
23
- const keepTag = async (key, seconds) => {
46
+ return async (key, seconds) => {
24
47
  const ttl = String(seconds);
25
48
  if (flagsKnown) {
26
49
  try {
@@ -35,6 +58,17 @@ function redisCacheStore(client, options) {
35
58
  }
36
59
  await client.send("EXPIRE", [key, ttl]);
37
60
  };
61
+ }
62
+ function redisCacheStore(target, options) {
63
+ const { client, name } = nameUnder(target, options.name);
64
+ const records = bindCache(client, defineCache({
65
+ name: `${name}:response`,
66
+ key: (key) => key,
67
+ ttl: 60,
68
+ schema: Stored
69
+ }));
70
+ const tagKey = (tag) => `${name}:tag:${tag}`;
71
+ const keepTag = tagKeeper(client);
38
72
  return {
39
73
  async get(key) {
40
74
  const record = await records.get(key);
@@ -70,13 +104,24 @@ function redisCacheStore(client, options) {
70
104
  }
71
105
  };
72
106
  }
107
+ // src/check.ts
108
+ function redisCheck(target, options = {}) {
109
+ return async () => {
110
+ if (!isHandle(target)) {
111
+ await target.send("PING", []);
112
+ return true;
113
+ }
114
+ const results = await target.ping(options.timeout === undefined ? undefined : { timeoutMs: options.timeout });
115
+ return Object.values(results).every((result) => result.ok);
116
+ };
117
+ }
73
118
  // src/context.ts
74
- import { alxia } from "@alxia/core";
119
+ import { alxia, markFactory } from "@alxia/core";
75
120
  import {
76
121
  bindCache as bindCache2,
77
122
  withLock
78
123
  } from "@nxgt/redis";
79
- function redis(client, options = {}) {
124
+ function fromClient(client, options = {}) {
80
125
  const caches = Object.fromEntries(Object.entries(options.caches ?? {}).map(([name, definition]) => [
81
126
  name,
82
127
  bindCache2(client, definition)
@@ -88,21 +133,47 @@ function redis(client, options = {}) {
88
133
  };
89
134
  return alxia().decorate(context);
90
135
  }
136
+ function fromHandle(handle, options = {}) {
137
+ const { client, prefix } = resolve(handle);
138
+ const [name] = Object.keys(handle.instances);
139
+ const instance = handle.instances[name];
140
+ const context = {
141
+ redis: client,
142
+ caches: instance.cache,
143
+ lock: instance.lock,
144
+ prefix
145
+ };
146
+ let closing;
147
+ const app = alxia().decorate(context);
148
+ return options.close === false ? app : app.onStop(() => {
149
+ closing ??= handle.close();
150
+ return closing;
151
+ });
152
+ }
153
+ function redis(target, options) {
154
+ return isHandle(target) ? fromHandle(target, options) : fromClient(target, options);
155
+ }
156
+ markFactory(redis, "plugin");
91
157
  // src/idempotency.ts
92
- import { alxia as alxia2 } from "@alxia/core";
158
+ import {
159
+ defineMiddleware,
160
+ markFactory as markFactory2,
161
+ settle
162
+ } from "@alxia/core";
93
163
  import {
94
164
  bindIdempotency,
95
165
  defineIdempotency,
96
166
  GuardError
97
- } from "@nxgt/redis-guard";
167
+ } from "@nxgt/redis";
168
+
169
+ // src/idempotency-response.ts
98
170
  import { z as z2 } from "zod";
99
- var Stored2 = z2.object({
171
+ var idempotencyResult = z2.object({
100
172
  status: z2.number().int(),
101
173
  headers: z2.array(z2.tuple([z2.string(), z2.string()])),
102
174
  body: z2.string()
103
175
  });
104
176
  var UNSTORED_HEADERS = new Set(["set-cookie", "date", "content-length"]);
105
- var KEY = /^[\x21-\x7e]{1,255}$/;
106
177
 
107
178
  class Unstored extends Error {
108
179
  response;
@@ -111,24 +182,56 @@ class Unstored extends Error {
111
182
  this.response = response;
112
183
  }
113
184
  }
114
- function idempotency(client, options) {
185
+ async function fingerprintOf(request, url) {
186
+ const body = new Uint8Array(await request.clone().arrayBuffer());
187
+ const head = new TextEncoder().encode(`${request.method} ${url.pathname}${url.search}
188
+ `);
189
+ const fingerprint = new Uint8Array(head.length + body.length);
190
+ fingerprint.set(head);
191
+ fingerprint.set(body, head.length);
192
+ return fingerprint;
193
+ }
194
+ async function store(response) {
195
+ if (response.status >= 500 || response.headers.get("content-type")?.startsWith("text/event-stream")) {
196
+ throw new Unstored(response);
197
+ }
198
+ const headers = [];
199
+ for (const [name, value] of response.headers) {
200
+ if (!UNSTORED_HEADERS.has(name))
201
+ headers.push([name, value]);
202
+ }
203
+ const bytes = new Uint8Array(await response.arrayBuffer());
204
+ return {
205
+ status: response.status,
206
+ headers,
207
+ body: Buffer.from(bytes).toString("base64")
208
+ };
209
+ }
210
+ function restore(stored, replayed) {
211
+ const headers = new Headers(stored.headers);
212
+ if (replayed)
213
+ headers.set("idempotent-replayed", "true");
214
+ const body = Buffer.from(stored.body, "base64");
215
+ return new Response(stored.status === 204 || stored.status === 304 ? null : body, { status: stored.status, headers });
216
+ }
217
+
218
+ // src/idempotency.ts
219
+ var KEY = /^[\x21-\x7e]{1,255}$/;
220
+ function idempotency(target, options = {}) {
221
+ const { bound, label } = isWiredIdempotency(target) ? wiredOf(target, options) : boundOf(target, options);
115
222
  const methods = new Set(options.methods ?? ["POST", "PATCH"]);
116
223
  const header = options.header ?? "idempotency-key";
117
224
  const scope = options.scope ?? ((ctx) => ctx.ip);
118
- const bound = bindIdempotency(client, defineIdempotency({
119
- name: options.name,
120
- key: (key) => key,
121
- ttl: options.ttl ?? 86400,
122
- ...options.lease === undefined ? {} : { lease: options.lease },
123
- schema: Stored2
124
- }));
125
- const refuse = (error, retryAfter) => {
126
- const body = retryAfter === undefined ? { error } : { error, retryAfter };
127
- return body;
225
+ let warned = false;
226
+ const warnUnscoped = () => {
227
+ if (warned)
228
+ return;
229
+ warned = true;
230
+ console.warn(unscopedWarning(label()));
128
231
  };
129
- return alxia2().wrap(async (ctx, next) => {
130
- const { request, reply } = ctx;
131
- if (!methods.has(request.method))
232
+ return defineMiddleware(async function idempotency(ctx, next) {
233
+ const { request, reply, route } = ctx;
234
+ if (route === undefined || !methods.has(request.method))
132
235
  return next();
133
236
  const key = request.headers.get(header);
134
237
  if (key === null) {
@@ -136,15 +239,15 @@ function idempotency(client, options) {
136
239
  }
137
240
  if (!KEY.test(key))
138
241
  return reply(400, refuse("idempotency_key_invalid"));
139
- const body = new Uint8Array(await request.clone().arrayBuffer());
140
- const head = new TextEncoder().encode(`${request.method} ${ctx.url.pathname}${ctx.url.search}
141
- `);
142
- const fingerprint = new Uint8Array(head.length + body.length);
143
- fingerprint.set(head);
144
- fingerprint.set(body, head.length);
145
- const id = `${ctx.route}:${scope(ctx) ?? "anyone"}:${key}`;
242
+ const scoped = scope(ctx);
243
+ if (scoped === undefined) {
244
+ warnUnscoped();
245
+ return next();
246
+ }
247
+ const id = `${route}:${scoped}:${key}`;
248
+ const fingerprint = await fingerprintOf(request, ctx.url);
146
249
  try {
147
- const { value, replayed } = await bound.run(id, async () => store(await next()), {
250
+ const { value, replayed } = await bound.run(id, async () => store(await settle(ctx, next())), {
148
251
  fingerprint,
149
252
  ...options.wait === undefined ? {} : { wait: options.wait }
150
253
  });
@@ -165,39 +268,55 @@ function idempotency(client, options) {
165
268
  }
166
269
  });
167
270
  }
168
- async function store(response) {
169
- if (response.status >= 500 || response.headers.get("content-type")?.startsWith("text/event-stream")) {
170
- throw new Unstored(response);
171
- }
172
- const headers = [];
173
- for (const [name, value] of response.headers) {
174
- if (!UNSTORED_HEADERS.has(name))
175
- headers.push([name, value]);
271
+ function refuse(error, retryAfter) {
272
+ return retryAfter === undefined ? { error } : { error, retryAfter };
273
+ }
274
+ function boundOf(target, options) {
275
+ const { client, name } = nameUnder(target, options.name);
276
+ const bound = bindIdempotency(client, defineIdempotency({
277
+ name,
278
+ key: (key) => key,
279
+ ttl: options.ttl ?? 86400,
280
+ ...options.lease === undefined ? {} : { lease: options.lease },
281
+ schema: idempotencyResult
282
+ }));
283
+ return { bound, label: () => options.name };
284
+ }
285
+ function wiredOf(wired, options) {
286
+ for (const own of ["name", "ttl", "lease"]) {
287
+ if (own in options) {
288
+ throw new TypeError(`idempotency: "${own}" is the wired definition's, set in defineIdempotency: the middleware takes none with a wired idempotency`);
289
+ }
176
290
  }
177
- const bytes = new Uint8Array(await response.arrayBuffer());
178
- return {
179
- status: response.status,
180
- headers,
181
- body: Buffer.from(bytes).toString("base64")
291
+ const label = () => {
292
+ try {
293
+ return wired.keyFor("").replace(/:$/, "");
294
+ } catch {
295
+ return "wired";
296
+ }
182
297
  };
298
+ return { bound: wired, label };
183
299
  }
184
- function restore(stored, replayed) {
185
- const headers = new Headers(stored.headers);
186
- if (replayed)
187
- headers.set("idempotent-replayed", "true");
188
- const body = Buffer.from(stored.body, "base64");
189
- return new Response(stored.status === 204 || stored.status === 304 ? null : body, { status: stored.status, headers });
300
+ function unscopedWarning(name) {
301
+ return `idempotency "${name}": no client scope could be derived (ctx.ip is undefined and no scope option returned one), so these requests run unguarded, nothing stored or replayed. Pass a scope option, (ctx) => a user id, or an ip option to alxia().`;
190
302
  }
303
+ markFactory2(idempotency);
191
304
  // src/store.ts
192
305
  import {
193
306
  bindRateLimit,
194
307
  defineRateLimit
195
- } from "@nxgt/redis-guard";
196
- function redisStore(client, options) {
308
+ } from "@nxgt/redis";
309
+ function redisStore(target, options) {
310
+ if (isWiredLimit(target))
311
+ return wiredStore(target);
312
+ if (options === undefined) {
313
+ throw new TypeError("redisStore: a client or a handle needs { name }; a wired rate limit, as handle.limits.api, is the only form without");
314
+ }
315
+ const { client, name } = nameUnder(target, options.name);
197
316
  const limits = new Map;
198
- const policies = `${options.name}:policies`;
317
+ const policies = `${name}:policies`;
199
318
  const bind = (limit, windowMs) => bindRateLimit(client, defineRateLimit({
200
- name: `${options.name}:${limit}/${windowMs}`,
319
+ name: `${name}:${limit}/${windowMs}`,
201
320
  key: (key) => key,
202
321
  limit,
203
322
  per: windowMs
@@ -242,12 +361,30 @@ function redisStore(client, options) {
242
361
  }
243
362
  };
244
363
  }
364
+ function wiredStore(limit) {
365
+ return {
366
+ async consume(key) {
367
+ const result = await limit.consume(key);
368
+ return {
369
+ allowed: result.allowed,
370
+ remaining: result.remaining,
371
+ resetAfter: result.resetAfter,
372
+ retryAfter: result.retryAfter
373
+ };
374
+ },
375
+ async reset(key) {
376
+ await limit.reset(key);
377
+ }
378
+ };
379
+ }
245
380
  export {
246
381
  idempotency,
382
+ idempotencyResult,
247
383
  redis,
248
384
  redisCacheStore,
385
+ redisCheck,
249
386
  redisStore
250
387
  };
251
388
 
252
- //# debugId=04C8078B823357BA64756E2164756E21
389
+ //# debugId=1962E3DF430E773F64756E2164756E21
253
390
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/cache-store.ts", "../src/context.ts", "../src/idempotency.ts", "../src/store.ts"],
3
+ "sources": ["../src/cache-store.ts", "../src/handle.ts", "../src/check.ts", "../src/context.ts", "../src/idempotency.ts", "../src/idempotency-response.ts", "../src/store.ts"],
4
4
  "sourcesContent": [
5
- "import type { CachedResponse, CacheStore } from '@alxia/cache';\nimport { bindCache, defineCache } from '@nxgt/redis';\nimport type { RedisClient } from 'bun';\nimport { z } from 'zod';\n\nexport interface RedisCacheStoreOptions {\n\t/** Prepended to every key it writes: one name per app or deployment. */\n\treadonly name: string;\n}\n\nconst Stored = z.object({\n\tstatus: z.number().int(),\n\theaders: z.array(z.tuple([z.string(), z.string()])),\n\tbody: z.string(),\n\tstoredAt: z.number(),\n\tttl: z.number(),\n\tstale: z.number(),\n\ttags: z.array(z.string()),\n});\n\n/** What a Redis older than 7 answers to `EXPIRE … NX`. */\nconst REFUSED_ARGUMENTS = /wrong number of arguments|syntax error/i;\n\n/**\n * An `@alxia/cache` store in Redis, on `@nxgt/redis`'s typed caches: every\n * process sharing the Redis serves what one of them kept. A record that no\n * longer reads as a response is a miss, and is dropped. Tags are Redis sets\n * of the keys they name.\n *\n * ```ts\n * app.use(cache({ ttl: 60, store: redisCacheStore(connection.client, { name: 'shop' }) }));\n * ```\n */\nexport function redisCacheStore(\n\tclient: RedisClient,\n\toptions: RedisCacheStoreOptions,\n): CacheStore {\n\t// `@nxgt/redis` keeps a record for whole seconds; the store's own `ttl`\n\t// and `stale` decide freshness to the millisecond.\n\tconst records = bindCache(\n\t\tclient,\n\t\tdefineCache({\n\t\t\tname: `${options.name}:response`,\n\t\t\tkey: (key: string) => key,\n\t\t\tttl: 60,\n\t\t\tschema: Stored,\n\t\t}),\n\t);\n\tconst tagKey = (tag: string) => `${options.name}:tag:${tag}`;\n\t/**\n\t * Keeps a tag's set as long as its longest-kept response: `NX` gives a\n\t * new set its first expiry — `GT` alone never would, a key without one\n\t * counting as kept forever — and `GT` then only ever lengthens it. A\n\t * Redis older than 7 refuses both arguments: it gets the plain `EXPIRE`,\n\t * from then on. Any other error is the caller's.\n\t */\n\tlet flagsKnown = true;\n\tconst keepTag = async (key: string, seconds: number) => {\n\t\tconst ttl = String(seconds);\n\t\tif (flagsKnown) {\n\t\t\ttry {\n\t\t\t\tawait client.send('EXPIRE', [key, ttl, 'NX']);\n\t\t\t\tawait client.send('EXPIRE', [key, ttl, 'GT']);\n\t\t\t\treturn;\n\t\t\t} catch (error) {\n\t\t\t\tif (!REFUSED_ARGUMENTS.test(String(error))) throw error;\n\t\t\t\tflagsKnown = false;\n\t\t\t}\n\t\t}\n\t\tawait client.send('EXPIRE', [key, ttl]);\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = await records.get(key);\n\t\t\tif (record === undefined) return undefined;\n\t\t\tconst response: CachedResponse = {\n\t\t\t\t...record,\n\t\t\t\tbody: new Uint8Array(Buffer.from(record.body, 'base64')),\n\t\t\t};\n\t\t\treturn response;\n\t\t},\n\t\tasync set(key, value, keepFor) {\n\t\t\tconst seconds = Math.max(1, Math.ceil(keepFor / 1000));\n\t\t\tawait records.set(\n\t\t\t\tkey,\n\t\t\t\t{\n\t\t\t\t\t...value,\n\t\t\t\t\theaders: value.headers.map(\n\t\t\t\t\t\t([name, header]) => [name, header] as [string, string],\n\t\t\t\t\t),\n\t\t\t\t\ttags: [...value.tags],\n\t\t\t\t\tbody: Buffer.from(value.body).toString('base64'),\n\t\t\t\t},\n\t\t\t\t{ ttl: seconds },\n\t\t\t);\n\t\t\tawait Promise.all(\n\t\t\t\tvalue.tags.map(async (tag) => {\n\t\t\t\t\tawait client.send('SADD', [tagKey(tag), records.keyFor(key)]);\n\t\t\t\t\tawait keepTag(tagKey(tag), seconds);\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t\tasync delete(key) {\n\t\t\tawait records.delete(key);\n\t\t},\n\t\tasync deleteTag(tag) {\n\t\t\tconst keys = (await client.send('SMEMBERS', [tagKey(tag)])) as string[];\n\t\t\tif (keys.length > 0) await client.send('DEL', keys);\n\t\t\tawait client.send('DEL', [tagKey(tag)]);\n\t\t},\n\t};\n}\n",
6
- "import { alxia } from '@alxia/core';\nimport {\n\ttype BoundCache,\n\tbindCache,\n\ttype CacheDefinition,\n\ttype LockOptions,\n\twithLock,\n} from '@nxgt/redis';\nimport type { RedisClient } from 'bun';\nimport type { z } from 'zod';\n\n/**\n * Any `@nxgt/redis` cache definition, whatever its key parameters and\n * schema: what `redis()`'s `caches` hold. The constraint a function generic\n * over the caches it hands to `redis()` takes:\n *\n * ```ts\n * function withCaches<const Caches extends Record<string, AnyCache>>(caches: Caches) {\n * return alxia().use(redis(client, { caches }));\n * }\n * ```\n */\nexport type AnyCache = CacheDefinition<any, z.ZodType>;\n\n/** The caches of `Caches`, each bound to the client. */\nexport type BoundCaches<Caches extends Record<string, AnyCache>> = {\n\treadonly [Name in keyof Caches]: Caches[Name] extends CacheDefinition<\n\t\tinfer Params,\n\t\tinfer Schema\n\t>\n\t\t? BoundCache<Params, z.output<Schema>, z.input<Schema>>\n\t\t: never;\n};\n\nexport interface RedisContextOptions<Caches extends Record<string, AnyCache>> {\n\t/** `@nxgt/redis` cache definitions, by the name routes read them under. */\n\treadonly caches?: Caches;\n}\n\n/** What routes after `redis()` read. */\nexport interface RedisContext<Caches extends Record<string, AnyCache>> {\n\t/** Bun's own client, untouched. */\n\treadonly redis: RedisClient;\n\t/** Each cache, bound and typed by its schema: `caches.users.remember(…)`. */\n\treadonly caches: BoundCaches<Caches>;\n\t/** `work` under a lock every process sharing the Redis respects: `@nxgt/redis`'s `withLock`. */\n\tlock<T>(\n\t\tkey: string,\n\t\twork: () => Promise<T> | T,\n\t\toptions?: LockOptions,\n\t): Promise<T>;\n}\n\n/**\n * Redis in the context, as a plugin: the client, the caches bound once, and\n * a lock — typed, for every route declared after it.\n *\n * ```ts\n * const users = defineCache({ name: 'user', key: (id: string) => id, ttl: 300, schema: User });\n * app.use(redis(connection.client, { caches: { users } }))\n * .get('/users/:id', async ({ caches, params, reply }) => reply.ok(await caches.users.remember(params.id, load)));\n * ```\n */\nexport function redis<\n\tconst Caches extends Record<string, AnyCache> = Record<never, never>,\n>(client: RedisClient, options: RedisContextOptions<Caches> = {}) {\n\tconst caches = Object.fromEntries(\n\t\tObject.entries(options.caches ?? {}).map(([name, definition]) => [\n\t\t\tname,\n\t\t\tbindCache(client, definition),\n\t\t]),\n\t) as BoundCaches<Caches>;\n\tconst context: RedisContext<Caches> = {\n\t\tredis: client,\n\t\tcaches,\n\t\tlock: (key, work, lockOptions) => withLock(client, key, work, lockOptions),\n\t};\n\treturn alxia().decorate(context);\n}\n",
7
- "import { alxia, type BaseContext } from '@alxia/core';\nimport {\n\tbindIdempotency,\n\tdefineIdempotency,\n\tGuardError,\n} from '@nxgt/redis-guard';\nimport type { RedisClient } from 'bun';\nimport { z } from 'zod';\n\nexport interface IdempotencyOptions {\n\t/** Names the keys it stores. */\n\treadonly name: string;\n\t/** Seconds a finished response is kept and replayed. A day by default. */\n\treadonly ttl?: number;\n\t/** Milliseconds a running request holds its key unless renewed. `@nxgt/redis-guard`'s 10 s by default. */\n\treadonly lease?: number;\n\t/** Milliseconds a repeat waits for the first to finish before a 409. None by default. */\n\treadonly wait?: number;\n\t/** The methods it guards. `POST` and `PATCH` by default: the others are idempotent already. */\n\treadonly methods?: readonly string[];\n\t/** The header the key is read from. `Idempotency-Key` by default. */\n\treadonly header?: string;\n\t/** Whether a guarded request without a key is refused, with a 400. Off by default. */\n\treadonly required?: boolean;\n\t/**\n\t * Whose key it is: keys are scoped by the route and by this, so two\n\t * clients choosing the same key never see each other's response. The\n\t * client's address by default; a user id when there is one.\n\t */\n\treadonly scope?: (ctx: BaseContext) => string | undefined;\n}\n\n/** The body of a refusal. */\nexport interface IdempotencyErrorBody {\n\treadonly error:\n\t\t| 'idempotency_key_missing'\n\t\t| 'idempotency_key_invalid'\n\t\t| 'idempotency_in_progress'\n\t\t| 'idempotency_key_reused';\n\t/** Seconds until a running request should be over: with `idempotency_in_progress`. */\n\treadonly retryAfter?: number;\n}\n\nconst Stored = z.object({\n\tstatus: z.number().int(),\n\theaders: z.array(z.tuple([z.string(), z.string()])),\n\tbody: z.string(),\n});\ntype Stored = z.infer<typeof Stored>;\n\n/** Headers a replay never repeats: a session cookie belongs to one response. */\nconst UNSTORED_HEADERS = new Set(['set-cookie', 'date', 'content-length']);\n\nconst KEY = /^[\\x21-\\x7e]{1,255}$/;\n\n/** A response that is answered and not kept: a 5xx, a stream. */\nclass Unstored extends Error {\n\treadonly response: Response;\n\tconstructor(response: Response) {\n\t\tsuper('unstored');\n\t\tthis.response = response;\n\t}\n}\n\n/**\n * Idempotent routes, as a plugin, with `@nxgt/redis-guard`: a `POST` or\n * `PATCH` carrying an `Idempotency-Key` runs once per key, and every repeat\n * gets the first response back, marked `Idempotent-Replayed: true` — across\n * every process sharing the Redis. Routes declared after it are guarded.\n *\n * A repeat while the first still runs is a 409, and the same key with\n * another request — method, path or body — a 422: both are part of every\n * guarded route's type. A 5xx, or a stream, is answered and not kept: the\n * key is free again.\n *\n * ```ts\n * app.use(idempotency(redis.client, { name: 'payments' })).post('/payments', ...);\n * ```\n */\nexport function idempotency(client: RedisClient, options: IdempotencyOptions) {\n\tconst methods = new Set(options.methods ?? ['POST', 'PATCH']);\n\tconst header = options.header ?? 'idempotency-key';\n\tconst scope = options.scope ?? ((ctx: BaseContext) => ctx.ip);\n\tconst bound = bindIdempotency(\n\t\tclient,\n\t\tdefineIdempotency({\n\t\t\tname: options.name,\n\t\t\tkey: (key: string) => key,\n\t\t\tttl: options.ttl ?? 86_400,\n\t\t\t...(options.lease === undefined ? {} : { lease: options.lease }),\n\t\t\tschema: Stored,\n\t\t}),\n\t);\n\tconst refuse = (\n\t\terror: IdempotencyErrorBody['error'],\n\t\tretryAfter?: number,\n\t) => {\n\t\tconst body: IdempotencyErrorBody =\n\t\t\tretryAfter === undefined ? { error } : { error, retryAfter };\n\t\treturn body;\n\t};\n\n\treturn alxia().wrap(async (ctx, next) => {\n\t\tconst { request, reply } = ctx;\n\t\tif (!methods.has(request.method)) return next();\n\t\tconst key = request.headers.get(header);\n\t\tif (key === null) {\n\t\t\treturn options.required\n\t\t\t\t? reply(400, refuse('idempotency_key_missing'))\n\t\t\t\t: next();\n\t\t}\n\t\tif (!KEY.test(key)) return reply(400, refuse('idempotency_key_invalid'));\n\n\t\tconst body = new Uint8Array(await request.clone().arrayBuffer());\n\t\tconst head = new TextEncoder().encode(\n\t\t\t`${request.method} ${ctx.url.pathname}${ctx.url.search}\\n`,\n\t\t);\n\t\tconst fingerprint = new Uint8Array(head.length + body.length);\n\t\tfingerprint.set(head);\n\t\tfingerprint.set(body, head.length);\n\t\tconst id = `${ctx.route}:${scope(ctx) ?? 'anyone'}:${key}`;\n\n\t\ttry {\n\t\t\tconst { value, replayed } = await bound.run(\n\t\t\t\tid,\n\t\t\t\tasync () => store(await next()),\n\t\t\t\t{\n\t\t\t\t\tfingerprint,\n\t\t\t\t\t...(options.wait === undefined ? {} : { wait: options.wait }),\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn restore(value, replayed);\n\t\t} catch (error) {\n\t\t\tif (error instanceof Unstored) return error.response;\n\t\t\tif (error instanceof GuardError && error.code === 'IN_PROGRESS') {\n\t\t\t\tconst retryAfter = Math.max(\n\t\t\t\t\t1,\n\t\t\t\t\tMath.ceil((error.retryAfter ?? 0) / 1000),\n\t\t\t\t);\n\t\t\t\treturn reply(409, refuse('idempotency_in_progress', retryAfter), {\n\t\t\t\t\theaders: { 'retry-after': String(retryAfter) },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (error instanceof GuardError && error.code === 'MISMATCH') {\n\t\t\t\treturn reply(422, refuse('idempotency_key_reused'));\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t});\n}\n\nasync function store(response: Response): Promise<Stored> {\n\tif (\n\t\tresponse.status >= 500 ||\n\t\tresponse.headers.get('content-type')?.startsWith('text/event-stream')\n\t) {\n\t\tthrow new Unstored(response);\n\t}\n\tconst headers: [string, string][] = [];\n\tfor (const [name, value] of response.headers) {\n\t\tif (!UNSTORED_HEADERS.has(name)) headers.push([name, value]);\n\t}\n\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\treturn {\n\t\tstatus: response.status,\n\t\theaders,\n\t\tbody: Buffer.from(bytes).toString('base64'),\n\t};\n}\n\nfunction restore(stored: Stored, replayed: boolean): Response {\n\tconst headers = new Headers(stored.headers);\n\tif (replayed) headers.set('idempotent-replayed', 'true');\n\tconst body = Buffer.from(stored.body, 'base64');\n\treturn new Response(\n\t\tstored.status === 204 || stored.status === 304 ? null : body,\n\t\t{ status: stored.status, headers },\n\t);\n}\n",
8
- "import type { Decision, Policy, RateLimitStore } from '@alxia/rate-limit';\nimport {\n\ttype BoundRateLimit,\n\tbindRateLimit,\n\tdefineRateLimit,\n} from '@nxgt/redis-guard';\nimport type { RedisClient } from 'bun';\n\nexport interface RedisStoreOptions {\n\t/** Prepended to every key it counts: one name per limit, so two never share a count. */\n\treadonly name: string;\n}\n\n/**\n * An `@alxia/rate-limit` store in Redis, with `@nxgt/redis-guard`'s GCRA:\n * every process sharing the Redis counts together, timed by the Redis\n * server's clock, and a refused request counts nothing.\n *\n * ```ts\n * app.use(rateLimit({ limit: 100, windowMs: 60_000, store: redisStore(redis.client, { name: 'api' }) }));\n * ```\n */\nexport function redisStore(\n\tclient: RedisClient,\n\toptions: RedisStoreOptions,\n): RateLimitStore {\n\tconst limits = new Map<string, Promise<BoundRateLimit<string>>>();\n\t// Every policy counted under this name, by any process: what `reset` forgets.\n\tconst policies = `${options.name}:policies`;\n\tconst bind = (limit: number, windowMs: number) =>\n\t\tbindRateLimit(\n\t\t\tclient,\n\t\t\tdefineRateLimit({\n\t\t\t\tname: `${options.name}:${limit}/${windowMs}`,\n\t\t\t\tkey: (key: string) => key,\n\t\t\t\tlimit,\n\t\t\t\tper: windowMs,\n\t\t\t}),\n\t\t);\n\t/** The bound limit of a policy, recorded in `policies` the first time this process counts under it. */\n\tconst limitFor = (policy: Policy) => {\n\t\tconst id = `${policy.limit}/${policy.windowMs}`;\n\t\tlet bound = limits.get(id);\n\t\tif (bound === undefined) {\n\t\t\tconst limit = bind(policy.limit, policy.windowMs);\n\t\t\tconst recorded = client.send('SADD', [policies, id]).then(() => limit);\n\t\t\trecorded.catch(() => {\n\t\t\t\tif (limits.get(id) === recorded) limits.delete(id);\n\t\t\t});\n\t\t\tlimits.set(id, recorded);\n\t\t\tbound = recorded;\n\t\t}\n\t\treturn bound;\n\t};\n\treturn {\n\t\tasync consume(key, policy): Promise<Decision> {\n\t\t\tconst result = await (await limitFor(policy)).consume(key);\n\t\t\treturn {\n\t\t\t\tallowed: result.allowed,\n\t\t\t\tremaining: result.remaining,\n\t\t\t\tresetAfter: result.resetAfter,\n\t\t\t\tretryAfter: result.retryAfter,\n\t\t\t};\n\t\t},\n\t\tasync reset(key) {\n\t\t\tconst ids = (await client.send('SMEMBERS', [policies])) as string[];\n\t\t\tawait Promise.all(\n\t\t\t\tids.map(async (id) => {\n\t\t\t\t\t// Only what `limitFor` writes: anything else in the set is not ours.\n\t\t\t\t\tconst policy = /^(\\d+)\\/(\\d+)$/.exec(id);\n\t\t\t\t\tif (policy === null) return;\n\t\t\t\t\tconst limit = Number(policy[1]);\n\t\t\t\t\tconst windowMs = Number(policy[2]);\n\t\t\t\t\tif (limit < 1 || windowMs < 1) return;\n\t\t\t\t\tawait bind(limit, windowMs).reset(key);\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t};\n}\n"
5
+ "import type { CachedResponse, CacheStore } from '@alxia/cache';\nimport { bindCache, defineCache } from '@nxgt/redis';\nimport { z } from 'zod';\nimport { nameUnder, type RedisTarget } from './handle';\n\nexport interface RedisCacheStoreOptions {\n\t/** Prepended to every key it writes: one name per app or deployment. Under a handle's prefix, when it is given one. */\n\treadonly name: string;\n}\n\nconst Stored = z.object({\n\tstatus: z.number().int(),\n\theaders: z.array(z.tuple([z.string(), z.string()])),\n\tbody: z.string(),\n\tstoredAt: z.number(),\n\tttl: z.number(),\n\tstale: z.number(),\n\ttags: z.array(z.string()),\n});\n\n/** What a Redis older than 7 answers to `EXPIRE … NX`. */\nconst REFUSED_ARGUMENTS = /wrong number of arguments|syntax error/i;\n\ntype Client = ReturnType<typeof nameUnder>['client'];\n\n/**\n * Keeps a tag's set as long as its longest-kept response: `NX` gives a\n * new set its first expiry — `GT` alone never would, a key without one\n * counting as kept forever — and `GT` then only ever lengthens it. A\n * Redis older than 7 refuses both arguments: it gets the plain `EXPIRE`,\n * from then on. Any other error is the caller's.\n */\nfunction tagKeeper(\n\tclient: Client,\n): (key: string, seconds: number) => Promise<void> {\n\tlet flagsKnown = true;\n\treturn async (key, seconds) => {\n\t\tconst ttl = String(seconds);\n\t\tif (flagsKnown) {\n\t\t\ttry {\n\t\t\t\tawait client.send('EXPIRE', [key, ttl, 'NX']);\n\t\t\t\tawait client.send('EXPIRE', [key, ttl, 'GT']);\n\t\t\t\treturn;\n\t\t\t} catch (error) {\n\t\t\t\tif (!REFUSED_ARGUMENTS.test(String(error))) throw error;\n\t\t\t\tflagsKnown = false;\n\t\t\t}\n\t\t}\n\t\tawait client.send('EXPIRE', [key, ttl]);\n\t};\n}\n\n/**\n * An `@alxia/cache` store in Redis, on `@nxgt/redis`'s typed caches: every\n * process sharing the Redis serves what one of them kept. A record that no\n * longer reads as a response is a miss, and is dropped. Tags are Redis sets\n * of the keys they name.\n *\n * ```ts\n * app.use(cache({ ttl: 60, store: redisCacheStore(connection.client, { name: 'shop' }) }));\n * ```\n *\n * Given an `@nxgt/redis` handle instead of a client, every key, the tag sets\n * included, is under the handle's `prefix`.\n */\nexport function redisCacheStore(\n\ttarget: RedisTarget,\n\toptions: RedisCacheStoreOptions,\n): CacheStore {\n\tconst { client, name } = nameUnder(target, options.name);\n\t// `@nxgt/redis` keeps a record for whole seconds; the store's own `ttl`\n\t// and `stale` decide freshness to the millisecond.\n\tconst records = bindCache(\n\t\tclient,\n\t\tdefineCache({\n\t\t\tname: `${name}:response`,\n\t\t\tkey: (key: string) => key,\n\t\t\tttl: 60,\n\t\t\tschema: Stored,\n\t\t}),\n\t);\n\tconst tagKey = (tag: string) => `${name}:tag:${tag}`;\n\tconst keepTag = tagKeeper(client);\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = await records.get(key);\n\t\t\tif (record === undefined) return undefined;\n\t\t\tconst response: CachedResponse = {\n\t\t\t\t...record,\n\t\t\t\tbody: new Uint8Array(Buffer.from(record.body, 'base64')),\n\t\t\t};\n\t\t\treturn response;\n\t\t},\n\t\tasync set(key, value, keepFor) {\n\t\t\tconst seconds = Math.max(1, Math.ceil(keepFor / 1000));\n\t\t\tawait records.set(\n\t\t\t\tkey,\n\t\t\t\t{\n\t\t\t\t\t...value,\n\t\t\t\t\theaders: value.headers.map(\n\t\t\t\t\t\t([name, header]) => [name, header] as [string, string],\n\t\t\t\t\t),\n\t\t\t\t\ttags: [...value.tags],\n\t\t\t\t\tbody: Buffer.from(value.body).toString('base64'),\n\t\t\t\t},\n\t\t\t\t{ ttl: seconds },\n\t\t\t);\n\t\t\tawait Promise.all(\n\t\t\t\tvalue.tags.map(async (tag) => {\n\t\t\t\t\tawait client.send('SADD', [tagKey(tag), records.keyFor(key)]);\n\t\t\t\t\tawait keepTag(tagKey(tag), seconds);\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t\tasync delete(key) {\n\t\t\tawait records.delete(key);\n\t\t},\n\t\tasync deleteTag(tag) {\n\t\t\tconst keys = (await client.send('SMEMBERS', [tagKey(tag)])) as string[];\n\t\t\tif (keys.length > 0) await client.send('DEL', keys);\n\t\t\tawait client.send('DEL', [tagKey(tag)]);\n\t\t},\n\t};\n}\n",
6
+ "import type { BoundIdempotency, BoundRateLimit, Redis } from '@nxgt/redis';\nimport type { RedisClient } from 'bun';\n\n/**\n * What the factories take: Bun's own client, or the handle `@nxgt/redis`'s\n * `openRedis(defineRedis({ … }))` gives back.\n */\nexport type RedisTarget = RedisClient | Redis<any>;\n\n/** The one instance of a handle, resolved: its client and the prefix in front of its keys. */\nexport interface ResolvedTarget {\n\treadonly client: RedisClient;\n\t/** `undefined` when nothing goes in front of the keys, and always for a bare client. */\n\treadonly prefix: string | undefined;\n}\n\n/** A handle has `instances` and `close`; Bun's `RedisClient` has neither. */\nexport function isHandle(target: RedisTarget): target is Redis<any> {\n\treturn (\n\t\t'instances' in target &&\n\t\t'close' in target &&\n\t\ttypeof target.close === 'function'\n\t);\n}\n\n/**\n * The client and prefix of a target. A handle must wire exactly one Redis\n * instance: with several, which one an app's keys live on is for the app to\n * say, with the bare client of `handle.clients.<name>`.\n */\nexport function resolve(target: RedisTarget): ResolvedTarget {\n\tif (!isHandle(target)) return { client: target, prefix: undefined };\n\tconst names = Object.keys(target.instances);\n\tconst [only] = names;\n\tif (names.length !== 1 || only === undefined) {\n\t\tthrow new TypeError(\n\t\t\t`@alxia/redis: this @nxgt/redis handle wires ${names.length} Redis instances (${names.map((name) => `\"${name}\"`).join(', ')}), and one is needed. Pass the client of one, as handle.clients.${names[0] ?? 'name'}, with its prefix in the name you give.`,\n\t\t);\n\t}\n\tconst instance = target.instances[only as never] as {\n\t\tclient: RedisClient;\n\t\tprefix: string | undefined;\n\t};\n\treturn { client: instance.client, prefix: instance.prefix };\n}\n\n/** `name`, with the handle's prefix in front when there is one: the one name a deployment's keys share. */\nexport function nameUnder(\n\ttarget: RedisTarget,\n\tname: string,\n): {\n\treadonly client: RedisClient;\n\treadonly name: string;\n} {\n\tconst { client, prefix } = resolve(target);\n\treturn { client, name: prefix === undefined ? name : `${prefix}:${name}` };\n}\n\n/** A rate limit bound by `@nxgt/redis`, wired or by hand: `consume`, `peek`, `reset` and `keyFor`. */\nexport function isWiredLimit(value: object): value is BoundRateLimit<string> {\n\tconst limit = value as Partial<BoundRateLimit<string>>;\n\treturn (\n\t\ttypeof limit.consume === 'function' &&\n\t\ttypeof limit.peek === 'function' &&\n\t\ttypeof limit.reset === 'function' &&\n\t\ttypeof limit.keyFor === 'function'\n\t);\n}\n\n/** An idempotency bound by `@nxgt/redis`, wired or by hand: `run`, `forget` and `keyFor`. */\nexport function isWiredIdempotency(\n\tvalue: object,\n): value is BoundIdempotency<string, any, any> {\n\tconst bound = value as Partial<BoundIdempotency<string, unknown>>;\n\treturn (\n\t\ttypeof bound.run === 'function' &&\n\t\ttypeof bound.forget === 'function' &&\n\t\ttypeof bound.keyFor === 'function'\n\t);\n}\n",
7
+ "import type { PingResult } from '@nxgt/redis';\nimport type { RedisTarget } from './handle';\nimport { isHandle } from './handle';\n\nexport interface RedisCheckOptions {\n\t/**\n\t * Milliseconds a handle's ping waits for each instance, as\n\t * `health({ timeout })` names its own: `@nxgt/redis`'s 2 s by default.\n\t */\n\treadonly timeout?: number;\n}\n\n/**\n * A readiness check for core's `health({ checks })`: it passes when every\n * Redis instance of the handle answers a `PING`, and is down when one does\n * not, or when the call fails. Given a client, it sends the `PING` itself.\n *\n * ```ts\n * app.plugin(health({ checks: { redis: redisCheck(handle) } }));\n * ```\n */\nexport function redisCheck(\n\ttarget: RedisTarget,\n\toptions: RedisCheckOptions = {},\n): () => Promise<boolean> {\n\treturn async () => {\n\t\tif (!isHandle(target)) {\n\t\t\tawait target.send('PING', []);\n\t\t\treturn true;\n\t\t}\n\t\tconst results: Record<string, PingResult> = await target.ping(\n\t\t\toptions.timeout === undefined\n\t\t\t\t? undefined\n\t\t\t\t: { timeoutMs: options.timeout },\n\t\t);\n\t\treturn Object.values(results).every((result) => result.ok);\n\t};\n}\n",
8
+ "import { alxia, markFactory } from '@alxia/core';\nimport {\n\ttype BoundCache,\n\tbindCache,\n\ttype CacheDefinition,\n\ttype LockOptions,\n\ttype Redis,\n\ttype SoleInstance,\n\twithLock,\n} from '@nxgt/redis';\nimport type { RedisClient } from 'bun';\nimport type { z } from 'zod';\nimport { isHandle, resolve } from './handle';\n\n/**\n * Any `@nxgt/redis` cache definition, whatever its key parameters and\n * schema: what `redis()`'s `caches` hold. The constraint a function generic\n * over the caches it hands to `redis()` takes:\n *\n * ```ts\n * function withCaches<const Caches extends Record<string, AnyCache>>(caches: Caches) {\n * return alxia().plugin(redis(client, { caches }));\n * }\n * ```\n */\nexport type AnyCache = CacheDefinition<any, z.ZodType>;\n\n/** The caches of `Caches`, each bound to the client. */\nexport type BoundCaches<Caches extends Record<string, AnyCache>> = {\n\treadonly [Name in keyof Caches]: Caches[Name] extends CacheDefinition<\n\t\tinfer Params,\n\t\tinfer Schema\n\t>\n\t\t? BoundCache<Params, z.output<Schema>, z.input<Schema>>\n\t\t: never;\n};\n\nexport interface RedisContextOptions<Caches extends Record<string, AnyCache>> {\n\t/** `@nxgt/redis` cache definitions, by the name routes read them under. */\n\treadonly caches?: Caches;\n}\n\n/** What routes after `redis()` read. */\nexport interface RedisContext<Caches extends Record<string, AnyCache>> {\n\t/** Bun's own client, untouched. */\n\treadonly redis: RedisClient;\n\t/** Each cache, bound and typed by its schema: `caches.users.remember(…)`. */\n\treadonly caches: BoundCaches<Caches>;\n\t/** `work` under a lock every process sharing the Redis respects: `@nxgt/redis`'s `withLock`. */\n\tlock<T>(\n\t\tkey: string,\n\t\twork: () => Promise<T> | T,\n\t\toptions?: LockOptions,\n\t): Promise<T>;\n}\n\nexport interface RedisHandleOptions {\n\t/**\n\t * Whether the handle is closed when the app stops: once, after the drain,\n\t * in `onStop`. On by default; `false` when something else closes it.\n\t */\n\treadonly close?: boolean;\n}\n\n/** What routes after `redis(handle)` read. */\nexport interface RedisHandleContext<C> {\n\t/** Bun's own client, untouched: the handle's one instance. */\n\treadonly redis: RedisClient;\n\t/** The handle's caches, typed by their schemas, the prefix already in front of every key. */\n\treadonly caches: SoleInstance<C>['cache'];\n\t/** `work` under a lock every process sharing the Redis respects, under the handle's prefix. */\n\treadonly lock: SoleInstance<C>['lock'];\n\t/** What goes in front of every key, or `undefined`. */\n\treadonly prefix: string | undefined;\n}\n\nfunction fromClient<\n\tconst Caches extends Record<string, AnyCache> = Record<never, never>,\n>(client: RedisClient, options: RedisContextOptions<Caches> = {}) {\n\tconst caches = Object.fromEntries(\n\t\tObject.entries(options.caches ?? {}).map(([name, definition]) => [\n\t\t\tname,\n\t\t\tbindCache(client, definition),\n\t\t]),\n\t) as BoundCaches<Caches>;\n\tconst context: RedisContext<Caches> = {\n\t\tredis: client,\n\t\tcaches,\n\t\tlock: (key, work, lockOptions) => withLock(client, key, work, lockOptions),\n\t};\n\treturn alxia().decorate(context);\n}\n\nfunction fromHandle<C>(handle: Redis<C>, options: RedisHandleOptions = {}) {\n\tconst { client, prefix } = resolve(handle);\n\tconst [name] = Object.keys(handle.instances);\n\tconst instance = handle.instances[\n\t\tname as never\n\t] as unknown as SoleInstance<C>;\n\tconst context: RedisHandleContext<C> = {\n\t\tredis: client,\n\t\tcaches: instance.cache,\n\t\tlock: instance.lock,\n\t\tprefix,\n\t};\n\tlet closing: Promise<void> | undefined;\n\tconst app = alxia().decorate(context);\n\treturn options.close === false\n\t\t? app\n\t\t: app.onStop(() => {\n\t\t\t\tclosing ??= handle.close();\n\t\t\t\treturn closing;\n\t\t\t});\n}\n\n/**\n * Redis in the context, as a plugin: the client, the caches bound once, and\n * a lock — typed, for every route declared after it.\n *\n * ```ts\n * const users = defineCache({ name: 'user', key: (id: string) => id, ttl: 300, schema: User });\n * app.plugin(redis(connection.client, { caches: { users } }))\n * .get('/users/:id', async ({ caches, params, reply }) => reply.ok(await caches.users.remember(params.id, load)));\n * ```\n *\n * Given an `@nxgt/redis` handle, the caches are the handle's own, the\n * prefix is in front of every key and lock, and the handle is closed when\n * the app stops, after the drain (`{ close: false }` to close it yourself):\n *\n * ```ts\n * const handle = await openRedis(defineRedis({ uri, prefix: 'shop', caches }));\n * app.plugin(redis(handle)).get('/u/:id', ({ caches, params }) => caches.users.get(params.id));\n * ```\n */\nexport function redis<\n\tconst Caches extends Record<string, AnyCache> = Record<never, never>,\n>(\n\tclient: RedisClient,\n\toptions?: RedisContextOptions<Caches>,\n): ReturnType<typeof fromClient<Caches>>;\nexport function redis<C>(\n\thandle: Redis<C>,\n\toptions?: RedisHandleOptions,\n): ReturnType<typeof fromHandle<C>>;\nexport function redis(\n\ttarget: RedisClient | Redis<never>,\n\toptions?: RedisContextOptions<Record<string, AnyCache>> & RedisHandleOptions,\n): unknown {\n\treturn isHandle(target)\n\t\t? fromHandle(target, options)\n\t\t: fromClient(target, options);\n}\n\nmarkFactory(redis, 'plugin');\n",
9
+ "import {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\tmarkFactory,\n\ttype Reply,\n\tsettle,\n} from '@alxia/core';\nimport {\n\ttype BoundIdempotency,\n\tbindIdempotency,\n\tdefineIdempotency,\n\tGuardError,\n} from '@nxgt/redis';\nimport { isWiredIdempotency, nameUnder, type RedisTarget } from './handle';\nimport {\n\tfingerprintOf,\n\ttype IdempotencyResult,\n\tidempotencyResult,\n\trestore,\n\tstore,\n\tUnstored,\n} from './idempotency-response';\n\nexport interface IdempotencyOptions {\n\t/** Names the keys it stores, under a handle's prefix when it is given one. */\n\treadonly name: string;\n\t/** Seconds a finished response is kept and replayed. A day by default. */\n\treadonly ttl?: number;\n\t/** Milliseconds a running request holds its key unless renewed. `@nxgt/redis`'s 10 s by default. */\n\treadonly lease?: number;\n\t/** Milliseconds a repeat waits for the first to finish before a 409. None by default. */\n\treadonly wait?: number;\n\t/** The methods it guards. `POST` and `PATCH` by default: the others are idempotent already. */\n\treadonly methods?: readonly string[];\n\t/** The header the key is read from. `Idempotency-Key` by default. */\n\treadonly header?: string;\n\t/** Whether a guarded request without a key is refused, with a 400. Off by default. */\n\treadonly required?: boolean;\n\t/**\n\t * Whose key it is: keys are scoped by the route and by this, so two\n\t * clients choosing the same key never see each other's response. The\n\t * client's address by default; a user id when there is one. A request\n\t * it gives no scope for runs unguarded — nothing stored, nothing\n\t * replayed — with a warning, once.\n\t */\n\treadonly scope?: (ctx: BaseContext) => string | undefined;\n}\n\n/** The body of a refusal. */\nexport interface IdempotencyErrorBody {\n\treadonly error:\n\t\t| 'idempotency_key_missing'\n\t\t| 'idempotency_key_invalid'\n\t\t| 'idempotency_in_progress'\n\t\t| 'idempotency_key_reused';\n\t/** Seconds until a running request should be over: with `idempotency_in_progress`. */\n\treadonly retryAfter?: number;\n}\n\n/**\n * What `idempotency()` makes: a middleware that adds nothing to the\n * context, and may answer a 400, a 409 or a 422.\n */\nexport type IdempotencyMiddleware = Middleware<\n\tEmpty,\n\tPromise<\n\t\t| Response\n\t\t| Reply<400, IdempotencyErrorBody>\n\t\t| Reply<409, IdempotencyErrorBody>\n\t\t| Reply<422, IdempotencyErrorBody>\n\t>\n>;\n\n/**\n * An idempotency wired by `defineRedis` — `handle.idempotency.orders` — whose\n * definition holds `idempotencyResult` as its schema and a key taken as a\n * string.\n */\nexport type WiredIdempotency = BoundIdempotency<\n\tstring,\n\tIdempotencyResult,\n\tIdempotencyResult\n>;\n\n/** What a wired idempotency leaves to the middleware: its name, `ttl` and `lease` are the definition's. */\nexport type WiredIdempotencyOptions = Omit<\n\tIdempotencyOptions,\n\t'name' | 'ttl' | 'lease'\n>;\n\nconst KEY = /^[\\x21-\\x7e]{1,255}$/;\n\n/**\n * Idempotent routes, as a middleware, with `@nxgt/redis`: a `POST` or\n * `PATCH` carrying an `Idempotency-Key` runs once per key, and every repeat\n * gets the first response back, marked `Idempotent-Replayed: true` — across\n * every process sharing the Redis. Routes declared after it are guarded; a\n * request no route matches is not: there is no route to scope its key by.\n * What is kept is the response the route answers, an error's answer\n * included.\n *\n * A repeat while the first still runs is a 409, and the same key with\n * another request — method, path or body — a 422: both are part of every\n * guarded route's type. A 5xx, or a stream, is answered and not kept: the\n * key is free again.\n *\n * ```ts\n * app.use(idempotency(redis.client, { name: 'payments' })).post('/payments', ...);\n * ```\n *\n * Given an `@nxgt/redis` handle instead of a client, the keys are under the\n * handle's `prefix`: `idempotency(handle, { name: 'payments' })`.\n *\n * Given an idempotency wired by `defineRedis`, the definition — name, `ttl`,\n * `lease` — is the one place, and the keys are those `@nxgt/redis` writes,\n * `<prefix>:<name>:<route>:<scope>:<key>`:\n *\n * ```ts\n * app.use(idempotency(handle.idempotency.orders, { required: true })).post('/orders', ...);\n * ```\n */\nexport function idempotency(\n\ttarget: RedisTarget,\n\toptions: IdempotencyOptions,\n): IdempotencyMiddleware;\nexport function idempotency(\n\twired: WiredIdempotency,\n\toptions?: WiredIdempotencyOptions,\n): IdempotencyMiddleware;\nexport function idempotency(\n\ttarget: RedisTarget | WiredIdempotency,\n\toptions: IdempotencyOptions | WiredIdempotencyOptions = {},\n): IdempotencyMiddleware {\n\tconst { bound, label } = isWiredIdempotency(target)\n\t\t? wiredOf(target, options)\n\t\t: boundOf(target, options as IdempotencyOptions);\n\tconst methods = new Set(options.methods ?? ['POST', 'PATCH']);\n\tconst header = options.header ?? 'idempotency-key';\n\tconst scope = options.scope ?? ((ctx: BaseContext) => ctx.ip);\n\tlet warned = false;\n\tconst warnUnscoped = () => {\n\t\tif (warned) return;\n\t\twarned = true;\n\t\tconsole.warn(unscopedWarning(label()));\n\t};\n\n\treturn defineMiddleware(async function idempotency(ctx, next) {\n\t\tconst { request, reply, route } = ctx;\n\t\tif (route === undefined || !methods.has(request.method)) return next();\n\t\tconst key = request.headers.get(header);\n\t\tif (key === null) {\n\t\t\treturn options.required\n\t\t\t\t? reply(400, refuse('idempotency_key_missing'))\n\t\t\t\t: next();\n\t\t}\n\t\tif (!KEY.test(key)) return reply(400, refuse('idempotency_key_invalid'));\n\n\t\tconst scoped = scope(ctx);\n\t\tif (scoped === undefined) {\n\t\t\twarnUnscoped();\n\t\t\treturn next();\n\t\t}\n\t\tconst id = `${route}:${scoped}:${key}`;\n\t\tconst fingerprint = await fingerprintOf(request, ctx.url);\n\n\t\ttry {\n\t\t\tconst { value, replayed } = await bound.run(\n\t\t\t\tid,\n\t\t\t\tasync () => store(await settle(ctx, next())),\n\t\t\t\t{\n\t\t\t\t\tfingerprint,\n\t\t\t\t\t...(options.wait === undefined ? {} : { wait: options.wait }),\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn restore(value, replayed);\n\t\t} catch (error) {\n\t\t\tif (error instanceof Unstored) return error.response;\n\t\t\tif (error instanceof GuardError && error.code === 'IN_PROGRESS') {\n\t\t\t\tconst retryAfter = Math.max(\n\t\t\t\t\t1,\n\t\t\t\t\tMath.ceil((error.retryAfter ?? 0) / 1000),\n\t\t\t\t);\n\t\t\t\treturn reply(409, refuse('idempotency_in_progress', retryAfter), {\n\t\t\t\t\theaders: { 'retry-after': String(retryAfter) },\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (error instanceof GuardError && error.code === 'MISMATCH') {\n\t\t\t\treturn reply(422, refuse('idempotency_key_reused'));\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t});\n}\n\nfunction refuse(\n\terror: IdempotencyErrorBody['error'],\n\tretryAfter?: number,\n): IdempotencyErrorBody {\n\treturn retryAfter === undefined ? { error } : { error, retryAfter };\n}\n\n/** The idempotency bound from a client or a handle: the keys are `<prefix>:<name>:<id>`. */\nfunction boundOf(target: RedisTarget, options: IdempotencyOptions) {\n\tconst { client, name } = nameUnder(target, options.name);\n\tconst bound = bindIdempotency(\n\t\tclient,\n\t\tdefineIdempotency({\n\t\t\tname,\n\t\t\tkey: (key: string) => key,\n\t\t\tttl: options.ttl ?? 86_400,\n\t\t\t...(options.lease === undefined ? {} : { lease: options.lease }),\n\t\t\tschema: idempotencyResult,\n\t\t}),\n\t);\n\treturn { bound: bound as WiredIdempotency, label: () => options.name };\n}\n\n/** The idempotency wired by `defineRedis`, which carries its own name, `ttl` and `lease`. */\nfunction wiredOf(wired: WiredIdempotency, options: object) {\n\tfor (const own of ['name', 'ttl', 'lease']) {\n\t\tif (own in options) {\n\t\t\tthrow new TypeError(\n\t\t\t\t`idempotency: \"${own}\" is the wired definition's, set in defineIdempotency: the middleware takes none with a wired idempotency`,\n\t\t\t);\n\t\t}\n\t}\n\tconst label = () => {\n\t\ttry {\n\t\t\treturn wired.keyFor('').replace(/:$/, '');\n\t\t} catch {\n\t\t\treturn 'wired';\n\t\t}\n\t};\n\treturn { bound: wired, label };\n}\n\n/** The warning a request with no scope prints, once per middleware. */\nfunction unscopedWarning(name: string): string {\n\treturn `idempotency \"${name}\": no client scope could be derived (ctx.ip is undefined and no scope option returned one), so these requests run unguarded, nothing stored or replayed. Pass a scope option, (ctx) => a user id, or an ip option to alxia().`;\n}\n\nmarkFactory(idempotency);\n",
10
+ "import { z } from 'zod';\n\n/**\n * What `idempotency()` keeps under a key: the response, as the route\n * answered it. The `schema` of a wired `defineIdempotency` that\n * `idempotency(handle.idempotency.orders)` is given:\n *\n * ```ts\n * export const orders = defineIdempotency({ name: 'orders', key: (id: string) => id, ttl: 86_400, schema: idempotencyResult });\n * ```\n */\nexport const idempotencyResult = z.object({\n\tstatus: z.number().int(),\n\theaders: z.array(z.tuple([z.string(), z.string()])),\n\tbody: z.string(),\n});\nexport type IdempotencyResult = z.infer<typeof idempotencyResult>;\n\n/** Headers a replay never repeats: a session cookie belongs to one response. */\nconst UNSTORED_HEADERS = new Set(['set-cookie', 'date', 'content-length']);\n\n/** A response that is answered and not kept: a 5xx, a stream. */\nexport class Unstored extends Error {\n\treadonly response: Response;\n\tconstructor(response: Response) {\n\t\tsuper('unstored');\n\t\tthis.response = response;\n\t}\n}\n\n/** What a key is bound to: the method, the path and query, the body. */\nexport async function fingerprintOf(\n\trequest: Request,\n\turl: URL,\n): Promise<Uint8Array> {\n\tconst body = new Uint8Array(await request.clone().arrayBuffer());\n\tconst head = new TextEncoder().encode(\n\t\t`${request.method} ${url.pathname}${url.search}\\n`,\n\t);\n\tconst fingerprint = new Uint8Array(head.length + body.length);\n\tfingerprint.set(head);\n\tfingerprint.set(body, head.length);\n\treturn fingerprint;\n}\n\nexport async function store(response: Response): Promise<IdempotencyResult> {\n\tif (\n\t\tresponse.status >= 500 ||\n\t\tresponse.headers.get('content-type')?.startsWith('text/event-stream')\n\t) {\n\t\tthrow new Unstored(response);\n\t}\n\tconst headers: [string, string][] = [];\n\tfor (const [name, value] of response.headers) {\n\t\tif (!UNSTORED_HEADERS.has(name)) headers.push([name, value]);\n\t}\n\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\treturn {\n\t\tstatus: response.status,\n\t\theaders,\n\t\tbody: Buffer.from(bytes).toString('base64'),\n\t};\n}\n\nexport function restore(\n\tstored: IdempotencyResult,\n\treplayed: boolean,\n): Response {\n\tconst headers = new Headers(stored.headers);\n\tif (replayed) headers.set('idempotent-replayed', 'true');\n\tconst body = Buffer.from(stored.body, 'base64');\n\treturn new Response(\n\t\tstored.status === 204 || stored.status === 304 ? null : body,\n\t\t{ status: stored.status, headers },\n\t);\n}\n",
11
+ "import type { Decision, Policy, RateLimitStore } from '@alxia/rate-limit';\nimport {\n\ttype BoundRateLimit,\n\tbindRateLimit,\n\tdefineRateLimit,\n} from '@nxgt/redis';\nimport { isWiredLimit, nameUnder, type RedisTarget } from './handle';\n\nexport interface RedisStoreOptions {\n\t/**\n\t * Prepended to every key it counts: one name per limit, so two never share\n\t * a count. Under a handle's prefix, when it is given one.\n\t */\n\treadonly name: string;\n}\n\n/**\n * An `@alxia/rate-limit` store in Redis, with `@nxgt/redis`'s GCRA:\n * every process sharing the Redis counts together, timed by the Redis\n * server's clock, and a refused request counts nothing.\n *\n * ```ts\n * app.use(rateLimit({ limit: 100, windowMs: 60_000, store: redisStore(redis.client, { name: 'api' }) }));\n * ```\n *\n * Given an `@nxgt/redis` handle instead of a client, every key is under the\n * handle's `prefix`: `redisStore(handle, { name: 'api' })` counts under\n * `<prefix>:api:…`.\n */\nexport function redisStore(\n\ttarget: RedisTarget,\n\toptions: RedisStoreOptions,\n): RateLimitStore;\n/**\n * The store of a rate limit wired by `defineRedis`: the definition — its\n * name, `limit`, `per` and `burst` — is the one place, and the keys are\n * those `@nxgt/redis` writes, `<prefix>:<name>:<key>`, so every consumer of\n * the handle counts together, alxia's routes included.\n *\n * ```ts\n * const api = defineRateLimit({ name: 'api', key: (ip: string) => ip, limit: 100, per: 60_000 });\n * const handle = await openRedis(defineRedis({ uri, prefix: 'shop', limits: { api } }));\n * app.use(rateLimit({ limit: 100, windowMs: 60_000, store: redisStore(handle.limits.api) }));\n * ```\n *\n * It counts by the wired limit's own rate, not by the `limit` and\n * `windowMs` the middleware is given, which only write its headers: give it\n * the same numbers. The limit's key must take a string, the one `rateLimit`\n * counts by.\n */\nexport function redisStore(limit: BoundRateLimit<string>): RateLimitStore;\nexport function redisStore(\n\ttarget: RedisTarget | BoundRateLimit<string>,\n\toptions?: RedisStoreOptions,\n): RateLimitStore {\n\tif (isWiredLimit(target)) return wiredStore(target);\n\tif (options === undefined) {\n\t\tthrow new TypeError(\n\t\t\t'redisStore: a client or a handle needs { name }; a wired rate limit, as handle.limits.api, is the only form without',\n\t\t);\n\t}\n\tconst { client, name } = nameUnder(target, options.name);\n\tconst limits = new Map<string, Promise<BoundRateLimit<string>>>();\n\t// Every policy counted under this name, by any process: what `reset` forgets.\n\tconst policies = `${name}:policies`;\n\tconst bind = (limit: number, windowMs: number) =>\n\t\tbindRateLimit(\n\t\t\tclient,\n\t\t\tdefineRateLimit({\n\t\t\t\tname: `${name}:${limit}/${windowMs}`,\n\t\t\t\tkey: (key: string) => key,\n\t\t\t\tlimit,\n\t\t\t\tper: windowMs,\n\t\t\t}),\n\t\t);\n\t/** The bound limit of a policy, recorded in `policies` the first time this process counts under it. */\n\tconst limitFor = (policy: Policy) => {\n\t\tconst id = `${policy.limit}/${policy.windowMs}`;\n\t\tlet bound = limits.get(id);\n\t\tif (bound === undefined) {\n\t\t\tconst limit = bind(policy.limit, policy.windowMs);\n\t\t\tconst recorded = client.send('SADD', [policies, id]).then(() => limit);\n\t\t\trecorded.catch(() => {\n\t\t\t\tif (limits.get(id) === recorded) limits.delete(id);\n\t\t\t});\n\t\t\tlimits.set(id, recorded);\n\t\t\tbound = recorded;\n\t\t}\n\t\treturn bound;\n\t};\n\treturn {\n\t\tasync consume(key, policy): Promise<Decision> {\n\t\t\tconst result = await (await limitFor(policy)).consume(key);\n\t\t\treturn {\n\t\t\t\tallowed: result.allowed,\n\t\t\t\tremaining: result.remaining,\n\t\t\t\tresetAfter: result.resetAfter,\n\t\t\t\tretryAfter: result.retryAfter,\n\t\t\t};\n\t\t},\n\t\tasync reset(key) {\n\t\t\tconst ids = (await client.send('SMEMBERS', [policies])) as string[];\n\t\t\tawait Promise.all(\n\t\t\t\tids.map(async (id) => {\n\t\t\t\t\t// Only what `limitFor` writes: anything else in the set is not ours.\n\t\t\t\t\tconst policy = /^(\\d+)\\/(\\d+)$/.exec(id);\n\t\t\t\t\tif (policy === null) return;\n\t\t\t\t\tconst limit = Number(policy[1]);\n\t\t\t\t\tconst windowMs = Number(policy[2]);\n\t\t\t\t\tif (limit < 1 || windowMs < 1) return;\n\t\t\t\t\tawait bind(limit, windowMs).reset(key);\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** A wired limit as a store: it counts by its own rate, the policy the middleware holds is left to its headers. */\nfunction wiredStore(limit: BoundRateLimit<string>): RateLimitStore {\n\treturn {\n\t\tasync consume(key): Promise<Decision> {\n\t\t\tconst result = await limit.consume(key);\n\t\t\treturn {\n\t\t\t\tallowed: result.allowed,\n\t\t\t\tremaining: result.remaining,\n\t\t\t\tresetAfter: result.resetAfter,\n\t\t\t\tretryAfter: result.retryAfter,\n\t\t\t};\n\t\t},\n\t\tasync reset(key) {\n\t\t\tawait limit.reset(key);\n\t\t},\n\t};\n}\n"
9
12
  ],
10
- "mappings": "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",
11
- "debugId": "04C8078B823357BA64756E2164756E21",
13
+ "mappings": "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",
14
+ "debugId": "1962E3DF430E773F64756E2164756E21",
12
15
  "names": []
13
16
  }
package/dist/store.d.ts CHANGED
@@ -1,17 +1,43 @@
1
1
  import type { RateLimitStore } from '@alxia/rate-limit';
2
- import type { RedisClient } from 'bun';
2
+ import { type BoundRateLimit } from '@nxgt/redis';
3
+ import { type RedisTarget } from './handle';
3
4
  export interface RedisStoreOptions {
4
- /** Prepended to every key it counts: one name per limit, so two never share a count. */
5
+ /**
6
+ * Prepended to every key it counts: one name per limit, so two never share
7
+ * a count. Under a handle's prefix, when it is given one.
8
+ */
5
9
  readonly name: string;
6
10
  }
7
11
  /**
8
- * An `@alxia/rate-limit` store in Redis, with `@nxgt/redis-guard`'s GCRA:
12
+ * An `@alxia/rate-limit` store in Redis, with `@nxgt/redis`'s GCRA:
9
13
  * every process sharing the Redis counts together, timed by the Redis
10
14
  * server's clock, and a refused request counts nothing.
11
15
  *
12
16
  * ```ts
13
17
  * app.use(rateLimit({ limit: 100, windowMs: 60_000, store: redisStore(redis.client, { name: 'api' }) }));
14
18
  * ```
19
+ *
20
+ * Given an `@nxgt/redis` handle instead of a client, every key is under the
21
+ * handle's `prefix`: `redisStore(handle, { name: 'api' })` counts under
22
+ * `<prefix>:api:…`.
23
+ */
24
+ export declare function redisStore(target: RedisTarget, options: RedisStoreOptions): RateLimitStore;
25
+ /**
26
+ * The store of a rate limit wired by `defineRedis`: the definition — its
27
+ * name, `limit`, `per` and `burst` — is the one place, and the keys are
28
+ * those `@nxgt/redis` writes, `<prefix>:<name>:<key>`, so every consumer of
29
+ * the handle counts together, alxia's routes included.
30
+ *
31
+ * ```ts
32
+ * const api = defineRateLimit({ name: 'api', key: (ip: string) => ip, limit: 100, per: 60_000 });
33
+ * const handle = await openRedis(defineRedis({ uri, prefix: 'shop', limits: { api } }));
34
+ * app.use(rateLimit({ limit: 100, windowMs: 60_000, store: redisStore(handle.limits.api) }));
35
+ * ```
36
+ *
37
+ * It counts by the wired limit's own rate, not by the `limit` and
38
+ * `windowMs` the middleware is given, which only write its headers: give it
39
+ * the same numbers. The limit's key must take a string, the one `rateLimit`
40
+ * counts by.
15
41
  */
16
- export declare function redisStore(client: RedisClient, options: RedisStoreOptions): RateLimitStore;
42
+ export declare function redisStore(limit: BoundRateLimit<string>): RateLimitStore;
17
43
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAM1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAEvC,MAAM,WAAW,iBAAiB;IACjC,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACzB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,iBAAiB,GACxB,cAAc,CAsDhB"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EACN,KAAK,cAAc,EAGnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAErE,MAAM,WAAW,iBAAiB;IACjC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CACzB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,iBAAiB,GACxB,cAAc,CAAC;AAClB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC"}