@alxia/redis 0.2.0 → 0.4.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,24 +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
158
  import {
93
159
  defineMiddleware,
160
+ markFactory as markFactory2,
94
161
  settle
95
162
  } from "@alxia/core";
96
163
  import {
97
164
  bindIdempotency,
98
165
  defineIdempotency,
99
166
  GuardError
100
- } from "@nxgt/redis-guard";
167
+ } from "@nxgt/redis";
168
+
169
+ // src/idempotency-response.ts
101
170
  import { z as z2 } from "zod";
102
- var Stored2 = z2.object({
171
+ var idempotencyResult = z2.object({
103
172
  status: z2.number().int(),
104
173
  headers: z2.array(z2.tuple([z2.string(), z2.string()])),
105
174
  body: z2.string()
106
175
  });
107
176
  var UNSTORED_HEADERS = new Set(["set-cookie", "date", "content-length"]);
108
- var KEY = /^[\x21-\x7e]{1,255}$/;
109
177
 
110
178
  class Unstored extends Error {
111
179
  response;
@@ -114,29 +182,54 @@ class Unstored extends Error {
114
182
  this.response = response;
115
183
  }
116
184
  }
117
- 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);
118
222
  const methods = new Set(options.methods ?? ["POST", "PATCH"]);
119
223
  const header = options.header ?? "idempotency-key";
120
224
  const scope = options.scope ?? ((ctx) => ctx.ip);
121
- const bound = bindIdempotency(client, defineIdempotency({
122
- name: options.name,
123
- key: (key) => key,
124
- ttl: options.ttl ?? 86400,
125
- ...options.lease === undefined ? {} : { lease: options.lease },
126
- schema: Stored2
127
- }));
128
225
  let warned = false;
129
226
  const warnUnscoped = () => {
130
227
  if (warned)
131
228
  return;
132
229
  warned = true;
133
- console.warn(unscopedWarning(options.name));
230
+ console.warn(unscopedWarning(label()));
134
231
  };
135
- const refuse = (error, retryAfter) => {
136
- const body = retryAfter === undefined ? { error } : { error, retryAfter };
137
- return body;
138
- };
139
- return defineMiddleware(async (ctx, next) => {
232
+ return defineMiddleware(async function idempotency(ctx, next) {
140
233
  const { request, reply, route } = ctx;
141
234
  if (route === undefined || !methods.has(request.method))
142
235
  return next();
@@ -175,51 +268,56 @@ function idempotency(client, options) {
175
268
  }
176
269
  });
177
270
  }
178
- function unscopedWarning(name) {
179
- 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().`;
271
+ function refuse(error, retryAfter) {
272
+ return retryAfter === undefined ? { error } : { error, retryAfter };
180
273
  }
181
- async function fingerprintOf(request, url) {
182
- const body = new Uint8Array(await request.clone().arrayBuffer());
183
- const head = new TextEncoder().encode(`${request.method} ${url.pathname}${url.search}
184
- `);
185
- const fingerprint = new Uint8Array(head.length + body.length);
186
- fingerprint.set(head);
187
- fingerprint.set(body, head.length);
188
- return fingerprint;
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 };
189
284
  }
190
- async function store(response) {
191
- if (response.status >= 500 || response.headers.get("content-type")?.startsWith("text/event-stream")) {
192
- throw new Unstored(response);
193
- }
194
- const headers = [];
195
- for (const [name, value] of response.headers) {
196
- if (!UNSTORED_HEADERS.has(name))
197
- headers.push([name, value]);
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
+ }
198
290
  }
199
- const bytes = new Uint8Array(await response.arrayBuffer());
200
- return {
201
- status: response.status,
202
- headers,
203
- body: Buffer.from(bytes).toString("base64")
291
+ const label = () => {
292
+ try {
293
+ return wired.keyFor("").replace(/:$/, "");
294
+ } catch {
295
+ return "wired";
296
+ }
204
297
  };
298
+ return { bound: wired, label };
205
299
  }
206
- function restore(stored, replayed) {
207
- const headers = new Headers(stored.headers);
208
- if (replayed)
209
- headers.set("idempotent-replayed", "true");
210
- const body = Buffer.from(stored.body, "base64");
211
- 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().`;
212
302
  }
303
+ markFactory2(idempotency);
213
304
  // src/store.ts
214
305
  import {
215
306
  bindRateLimit,
216
307
  defineRateLimit
217
- } from "@nxgt/redis-guard";
218
- function redisStore(client, options) {
308
+ } from "@nxgt/redis";
309
+ function redisStore(target, options) {
310
+ if (isWiredLimit(target)) {
311
+ return options === undefined ? wiredStore(target) : policyStore(target, options);
312
+ }
313
+ if (options === undefined || !("name" in options)) {
314
+ throw new TypeError("redisStore: a client or a handle needs { name }; a wired rate limit, as handle.limits.api, is the only form without");
315
+ }
316
+ const { client, name } = nameUnder(target, options.name);
219
317
  const limits = new Map;
220
- const policies = `${options.name}:policies`;
318
+ const policies = `${name}:policies`;
221
319
  const bind = (limit, windowMs) => bindRateLimit(client, defineRateLimit({
222
- name: `${options.name}:${limit}/${windowMs}`,
320
+ name: `${name}:${limit}/${windowMs}`,
223
321
  key: (key) => key,
224
322
  limit,
225
323
  per: windowMs
@@ -264,12 +362,38 @@ function redisStore(client, options) {
264
362
  }
265
363
  };
266
364
  }
365
+ function wiredStore(limit) {
366
+ return {
367
+ async consume(key) {
368
+ const result = await limit.consume(key);
369
+ return {
370
+ allowed: result.allowed,
371
+ remaining: result.remaining,
372
+ resetAfter: result.resetAfter,
373
+ retryAfter: result.retryAfter
374
+ };
375
+ },
376
+ async reset(key) {
377
+ await limit.reset(key);
378
+ }
379
+ };
380
+ }
381
+ function policyStore(limit, definition) {
382
+ const probe = definition.key("probe");
383
+ if (!limit.keyFor(probe).endsWith(`${definition.name}:${probe}`)) {
384
+ throw new TypeError(`redisStore: the definition "${definition.name}" is not the one that wired this limit (it counts under "${limit.keyFor(probe)}")`);
385
+ }
386
+ const policy = { limit: definition.limit, windowMs: definition.per };
387
+ return { ...wiredStore(limit), policy };
388
+ }
267
389
  export {
268
390
  idempotency,
391
+ idempotencyResult,
269
392
  redis,
270
393
  redisCacheStore,
394
+ redisCheck,
271
395
  redisStore
272
396
  };
273
397
 
274
- //# debugId=FF8A722FABD88B9264756E2164756E21
398
+ //# debugId=48E780DBDBA328FC64756E2164756E21
275
399
  //# 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().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\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 */\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 {\n\ttype BaseContext,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\ttype MiddlewareMark,\n\ttype Reply,\n\tsettle,\n} 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. 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\tMiddlewareMark;\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 middleware, 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; 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 */\nexport function idempotency(\n\tclient: RedisClient,\n\toptions: IdempotencyOptions,\n): IdempotencyMiddleware {\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\tlet warned = false;\n\tconst warnUnscoped = () => {\n\t\tif (warned) return;\n\t\twarned = true;\n\t\tconsole.warn(unscopedWarning(options.name));\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 defineMiddleware(async (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\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\n/** What a key is bound to: the method, the path and query, the body. */\nasync function fingerprintOf(request: Request, url: URL): 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\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 {\n\tDecision,\n\tPolicy,\n\tPolicyStore,\n\tRateLimitStore,\n} from '@alxia/rate-limit';\nimport {\n\ttype BoundRateLimit,\n\tbindRateLimit,\n\tdefineRateLimit,\n\ttype RateLimitDefinition,\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, or give the definition as the second argument, which\n * `rateLimit` then reads them from (below). The limit's key must take a\n * string, the one `rateLimit` counts by.\n */\nexport function redisStore(limit: BoundRateLimit<string>): RateLimitStore;\n/**\n * A wired rate limit with its definition: the store declares the definition's\n * `limit` and `per` as its `policy`, so `rateLimit({ store })` takes `limit`\n * and `windowMs` from it and the numbers are written once. (`@nxgt/redis`\n * 0.6 does not expose a bound limit's rate, so the definition that wired it\n * is given beside it; a definition of another name than the bound limit's is refused.)\n *\n * ```ts\n * app.use(rateLimit({ store: redisStore(handle.limits.api, api) }));\n * ```\n *\n * A `limit` or `windowMs` given to `rateLimit` too must equal the\n * definition's, or `rateLimit()` throws at declaration.\n */\nexport function redisStore(\n\tlimit: BoundRateLimit<string>,\n\tdefinition: RateLimitDefinition<string>,\n): PolicyStore;\nexport function redisStore(\n\ttarget: RedisTarget | BoundRateLimit<string>,\n\toptions?: RedisStoreOptions | RateLimitDefinition<string>,\n): RateLimitStore {\n\tif (isWiredLimit(target)) {\n\t\treturn options === undefined\n\t\t\t? wiredStore(target)\n\t\t\t: policyStore(target, options as RateLimitDefinition<string>);\n\t}\n\tif (options === undefined || !('name' in options)) {\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(\n\t\ttarget,\n\t\t(options as RedisStoreOptions).name,\n\t);\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\n/** A wired limit with its definition: the same store, declaring the definition's rate as its policy. */\nfunction policyStore(\n\tlimit: BoundRateLimit<string>,\n\tdefinition: RateLimitDefinition<string>,\n): PolicyStore {\n\t// A bound limit's key is `<prefix>:<name>:<key>`: a definition of another\n\t// name is not the one that wired it.\n\tconst probe = definition.key('probe');\n\tif (!limit.keyFor(probe).endsWith(`${definition.name}:${probe}`)) {\n\t\tthrow new TypeError(\n\t\t\t`redisStore: the definition \"${definition.name}\" is not the one that wired this limit (it counts under \"${limit.keyFor(probe)}\")`,\n\t\t);\n\t}\n\tconst policy: Policy = { limit: definition.limit, windowMs: definition.per };\n\treturn { ...wiredStore(limit), policy };\n}\n"
9
12
  ],
10
- "mappings": "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",
11
- "debugId": "FF8A722FABD88B9264756E2164756E21",
13
+ "mappings": "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",
14
+ "debugId": "48E780DBDBA328FC64756E2164756E21",
12
15
  "names": []
13
16
  }
package/dist/store.d.ts CHANGED
@@ -1,17 +1,59 @@
1
- import type { RateLimitStore } from '@alxia/rate-limit';
2
- import type { RedisClient } from 'bun';
1
+ import type { PolicyStore, RateLimitStore } from '@alxia/rate-limit';
2
+ import { type BoundRateLimit, type RateLimitDefinition } 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, or give the definition as the second argument, which
40
+ * `rateLimit` then reads them from (below). The limit's key must take a
41
+ * string, the one `rateLimit` counts by.
42
+ */
43
+ export declare function redisStore(limit: BoundRateLimit<string>): RateLimitStore;
44
+ /**
45
+ * A wired rate limit with its definition: the store declares the definition's
46
+ * `limit` and `per` as its `policy`, so `rateLimit({ store })` takes `limit`
47
+ * and `windowMs` from it and the numbers are written once. (`@nxgt/redis`
48
+ * 0.6 does not expose a bound limit's rate, so the definition that wired it
49
+ * is given beside it; a definition of another name than the bound limit's is refused.)
50
+ *
51
+ * ```ts
52
+ * app.use(rateLimit({ store: redisStore(handle.limits.api, api) }));
53
+ * ```
54
+ *
55
+ * A `limit` or `windowMs` given to `rateLimit` too must equal the
56
+ * definition's, or `rateLimit()` throws at declaration.
15
57
  */
16
- export declare function redisStore(client: RedisClient, options: RedisStoreOptions): RateLimitStore;
58
+ export declare function redisStore(limit: BoundRateLimit<string>, definition: RateLimitDefinition<string>): PolicyStore;
17
59
  //# 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,EAGX,WAAW,EACX,cAAc,EACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,KAAK,cAAc,EAGnB,KAAK,mBAAmB,EACxB,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;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC;AAC1E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CACzB,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,EAC7B,UAAU,EAAE,mBAAmB,CAAC,MAAM,CAAC,GACrC,WAAW,CAAC"}