@alxia/redis 0.4.0 → 0.5.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/README.md CHANGED
@@ -18,7 +18,8 @@ bun add -d typescript
18
18
  ```
19
19
 
20
20
  They are peers, with `@alxia/rate-limit` for `redisStore` and `@alxia/cache`
21
- for `redisCacheStore`. **Bun 1.4 or
21
+ for `redisCacheStore`. `@nxgt/redis` is `^0.7.0`: `redisStore` reads the
22
+ rate from the bound limit's `definition`, which 0.7 added. **Bun 1.4 or
22
23
  later**: Bun's `RedisClient` is what the nxgt packages run on.
23
24
 
24
25
  ## A shared rate limit
@@ -182,7 +183,7 @@ The bare `RedisClient` forms are unchanged and add no prefix.
182
183
 
183
184
  ### Defined once, in `defineRedis`
184
185
 
185
- With `@nxgt/redis` 0.6 the rate limit and the idempotency are wired by
186
+ With `@nxgt/redis` 0.7 the rate limit and the idempotency are wired by
186
187
  `defineRedis` too (`limits`, `idempotency`), and `redisStore` and `idempotency`
187
188
  take the wired entry, so the definition lives in one place and its types flow
188
189
  through:
@@ -196,19 +197,18 @@ const orders = defineIdempotency({ name: 'orders', key: (id: string) => id, ttl:
196
197
  const wired = await openRedis(defineRedis({ uri: Bun.env['REDIS_URL']!, prefix: 'shop', limits: { api }, idempotency: { orders } }));
197
198
 
198
199
  alxia()
199
- .use(rateLimit({ store: redisStore(wired.limits.api, api) })) // shop:api:<address>, 100 per 60 s from `api`
200
+ .use(rateLimit({ store: redisStore(wired.limits.api) })) // shop:api:<address>, 100 per 60 s from the definition
200
201
  .use(idempotency(wired.idempotency.orders, { required: true })) // shop:orders:<route>:<scope>:<key>
201
202
  .post('/orders', ({ reply }) => reply(201, { id: crypto.randomUUID() }));
202
203
  ```
203
204
 
204
205
  The keys are those `@nxgt/redis` writes, `<prefix>:<name>:<key>`, so another
205
206
  consumer calling `wired.limits.api.consume(address)` shares the count. The
206
- rate, `ttl` and `lease` are the definition's. `redisStore(wired.limits.api, api)`,
207
- given the definition, declares its rate as the store's `policy`, so `rateLimit`
208
- needs no `limit` nor `windowMs` and writes its headers from it (one it is given
209
- that differs throws at declaration); `redisStore(wired.limits.api)` alone has
210
- no policy, and `rateLimit` then takes the numbers, which only write its
211
- headers. The wired `idempotency` takes no `name`, `ttl` or `lease`. The limit's key takes a
207
+ rate, `ttl` and `lease` are the definition's. The bound limit carries its
208
+ `definition`, so `redisStore(wired.limits.api)` declares its `limit` and `per`
209
+ as the store's `policy`: `rateLimit` needs no `limit` nor `windowMs` and writes
210
+ its headers from it (one it is given that differs throws at declaration).
211
+ `redisStore(wired.limits.api, api)` still works and is deprecated. The wired `idempotency` takes no `name`, `ttl` or `lease`. The limit's key takes a
212
212
  string, and the idempotency's `schema` is `idempotencyResult`: another is a
213
213
  compile error. A rate limit moved from `redisStore(handle, { name })` starts
214
214
  its counts again, the layouts differing; an idempotency keeps its keys. Both
@@ -224,8 +224,8 @@ The package's specs run against `$REDIS_URL`, or a `redis-server` on
224
224
  | export | |
225
225
  | --- | --- |
226
226
  | `redisStore(client \| handle, { name })`, `RedisStoreOptions` | an `@alxia/rate-limit` store |
227
- | `redisStore(handle.limits.api)` | the same for a rate limit wired by `defineRedis` (`@nxgt/redis` 0.6): the definition holds the name and the rate, the keys are `<prefix>:<name>:<key>` |
228
- | `redisStore(handle.limits.api, api)` | the same, given the definition, returning a `PolicyStore` (from `@alxia/rate-limit`): the store declares its `limit` and `per` as its `policy`, and `rateLimit({ store })` needs no `limit` nor `windowMs` |
227
+ | `redisStore(handle.limits.api)` | the same for a rate limit wired by `defineRedis` (`@nxgt/redis` 0.7) or bound by hand, returning a `PolicyStore` (from `@alxia/rate-limit`): the definition holds the name and the rate, the store declares its `limit` and `per` (read from `definition`) as its `policy`, and `rateLimit({ store })` needs no `limit` nor `windowMs`; the keys are `<prefix>:<name>:<key>` |
228
+ | `redisStore(handle.limits.api, api)` | deprecated: the same, given the definition beside the limit; its `limit` and `per` must equal the limit's, or it throws |
229
229
  | `redisCacheStore(client \| handle, { name })`, `RedisCacheStoreOptions` | an `@alxia/cache` store |
230
230
  | `idempotency(client \| handle, options)` | the middleware, given to `app.use` |
231
231
  | `idempotency(handle.idempotency.orders, options?)`, `WiredIdempotency`, `WiredIdempotencyOptions` | the same for an idempotency wired by `defineRedis`: its definition holds the `name`, `ttl` and `lease`, so the options take none of them |
package/dist/index.js CHANGED
@@ -253,21 +253,24 @@ function idempotency(target, options = {}) {
253
253
  });
254
254
  return restore(value, replayed);
255
255
  } catch (error) {
256
- if (error instanceof Unstored)
257
- return error.response;
258
- if (error instanceof GuardError && error.code === "IN_PROGRESS") {
259
- const retryAfter = Math.max(1, Math.ceil((error.retryAfter ?? 0) / 1000));
260
- return reply(409, refuse("idempotency_in_progress", retryAfter), {
261
- headers: { "retry-after": String(retryAfter) }
262
- });
263
- }
264
- if (error instanceof GuardError && error.code === "MISMATCH") {
265
- return reply(422, refuse("idempotency_key_reused"));
266
- }
267
- throw error;
256
+ return answer(error, reply);
268
257
  }
269
258
  });
270
259
  }
260
+ function answer(error, reply) {
261
+ if (error instanceof Unstored)
262
+ return error.response;
263
+ if (error instanceof GuardError && error.code === "IN_PROGRESS") {
264
+ const retryAfter = Math.max(1, Math.ceil((error.retryAfter ?? 0) / 1000));
265
+ return reply(409, refuse("idempotency_in_progress", retryAfter), {
266
+ headers: { "retry-after": String(retryAfter) }
267
+ });
268
+ }
269
+ if (error instanceof GuardError && error.code === "MISMATCH") {
270
+ return reply(422, refuse("idempotency_key_reused"));
271
+ }
272
+ throw error;
273
+ }
271
274
  function refuse(error, retryAfter) {
272
275
  return retryAfter === undefined ? { error } : { error, retryAfter };
273
276
  }
@@ -288,14 +291,7 @@ function wiredOf(wired, options) {
288
291
  throw new TypeError(`idempotency: "${own}" is the wired definition's, set in defineIdempotency: the middleware takes none with a wired idempotency`);
289
292
  }
290
293
  }
291
- const label = () => {
292
- try {
293
- return wired.keyFor("").replace(/:$/, "");
294
- } catch {
295
- return "wired";
296
- }
297
- };
298
- return { bound: wired, label };
294
+ return { bound: wired, label: () => wired.definition.name };
299
295
  }
300
296
  function unscopedWarning(name) {
301
297
  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().`;
@@ -308,7 +304,7 @@ import {
308
304
  } from "@nxgt/redis";
309
305
  function redisStore(target, options) {
310
306
  if (isWiredLimit(target)) {
311
- return options === undefined ? wiredStore(target) : policyStore(target, options);
307
+ return policyStore(target, options);
312
308
  }
313
309
  if (options === undefined || !("name" in options)) {
314
310
  throw new TypeError("redisStore: a client or a handle needs { name }; a wired rate limit, as handle.limits.api, is the only form without");
@@ -378,13 +374,12 @@ function wiredStore(limit) {
378
374
  }
379
375
  };
380
376
  }
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)}")`);
377
+ function policyStore(limit, given) {
378
+ const { limit: count, per } = limit.definition;
379
+ if (given !== undefined && (given.limit !== count || given.per !== per)) {
380
+ throw new TypeError(`redisStore: the definition "${given.name}" (${given.limit} per ${given.per}ms) is not the rate this limit counts by (${count} per ${per}ms)`);
385
381
  }
386
- const policy = { limit: definition.limit, windowMs: definition.per };
387
- return { ...wiredStore(limit), policy };
382
+ return { ...wiredStore(limit), policy: { limit: count, windowMs: per } };
388
383
  }
389
384
  export {
390
385
  idempotency,
@@ -395,5 +390,5 @@ export {
395
390
  redisStore
396
391
  };
397
392
 
398
- //# debugId=48E780DBDBA328FC64756E2164756E21
393
+ //# debugId=8102DF4343F3D91E64756E2164756E21
399
394
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -6,11 +6,11 @@
6
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
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
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",
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\treturn answer(error, reply);\n\t\t}\n\t});\n}\n\n/** The answer to what a run threw: the response kept apart, a 409 or a 422, or the error itself. */\nfunction answer(error: unknown, reply: BaseContext['reply']) {\n\tif (error instanceof Unstored) return error.response;\n\tif (error instanceof GuardError && error.code === 'IN_PROGRESS') {\n\t\tconst retryAfter = Math.max(1, Math.ceil((error.retryAfter ?? 0) / 1000));\n\t\treturn reply(409, refuse('idempotency_in_progress', retryAfter), {\n\t\t\theaders: { 'retry-after': String(retryAfter) },\n\t\t});\n\t}\n\tif (error instanceof GuardError && error.code === 'MISMATCH') {\n\t\treturn reply(422, refuse('idempotency_key_reused'));\n\t}\n\tthrow error;\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\treturn { bound: wired, label: () => wired.definition.name };\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
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"
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 bound by `@nxgt/redis` (0.7 and later), wired by\n * `defineRedis` or by hand: the definition — its `limit`, `per` and `burst` —\n * is the one place. The store declares its `limit` and `per` as its `policy`,\n * so `rateLimit({ store })` takes `limit` and `windowMs` from it and the\n * numbers are written once. The keys are those `@nxgt/redis` writes,\n * `<prefix>:<name>:<key>`, so every consumer of the handle counts together,\n * 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({ store: redisStore(handle.limits.api) }));\n * ```\n *\n * A `limit` or `windowMs` given to `rateLimit` too must equal the\n * definition's, or `rateLimit()` throws at declaration. The limit's key must\n * take a string, the one `rateLimit` counts by.\n */\nexport function redisStore(limit: BoundRateLimit<string>): PolicyStore;\n/**\n * A bound rate limit with its definition beside it.\n *\n * @deprecated The bound limit carries its definition since `@nxgt/redis`\n * 0.7: `redisStore(handle.limits.api)` alone is the same store. Given both,\n * the definition's `limit` and `per` must equal the bound limit's, or this\n * throws.\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 policyStore(\n\t\t\ttarget,\n\t\t\toptions as RateLimitDefinition<string> | undefined,\n\t\t);\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/**\n * A bound limit as a store, declaring the rate of its `definition` as its\n * policy. A definition given beside it must hold the same rate.\n */\nfunction policyStore(\n\tlimit: BoundRateLimit<string>,\n\tgiven?: RateLimitDefinition<string>,\n): PolicyStore {\n\tconst { limit: count, per } = limit.definition;\n\tif (given !== undefined && (given.limit !== count || given.per !== per)) {\n\t\tthrow new TypeError(\n\t\t\t`redisStore: the definition \"${given.name}\" (${given.limit} per ${given.per}ms) is not the rate this limit counts by (${count} per ${per}ms)`,\n\t\t);\n\t}\n\treturn { ...wiredStore(limit), policy: { limit: count, windowMs: per } };\n}\n"
12
12
  ],
13
- "mappings": "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",
14
- "debugId": "48E780DBDBA328FC64756E2164756E21",
13
+ "mappings": "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",
14
+ "debugId": "8102DF4343F3D91E64756E2164756E21",
15
15
  "names": []
16
16
  }
package/dist/store.d.ts CHANGED
@@ -23,37 +23,32 @@ export interface RedisStoreOptions {
23
23
  */
24
24
  export declare function redisStore(target: RedisTarget, options: RedisStoreOptions): RateLimitStore;
25
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.
26
+ * The store of a rate limit bound by `@nxgt/redis` (0.7 and later), wired by
27
+ * `defineRedis` or by hand: the definition — its `limit`, `per` and `burst` —
28
+ * is the one place. The store declares its `limit` and `per` as its `policy`,
29
+ * so `rateLimit({ store })` takes `limit` and `windowMs` from it and the
30
+ * numbers are written once. The keys are those `@nxgt/redis` writes,
31
+ * `<prefix>:<name>:<key>`, so every consumer of the handle counts together,
32
+ * alxia's routes included.
30
33
  *
31
34
  * ```ts
32
35
  * const api = defineRateLimit({ name: 'api', key: (ip: string) => ip, limit: 100, per: 60_000 });
33
36
  * const handle = await openRedis(defineRedis({ uri, prefix: 'shop', limits: { api } }));
34
- * app.use(rateLimit({ limit: 100, windowMs: 60_000, store: redisStore(handle.limits.api) }));
37
+ * app.use(rateLimit({ store: redisStore(handle.limits.api) }));
35
38
  * ```
36
39
  *
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.
40
+ * A `limit` or `windowMs` given to `rateLimit` too must equal the
41
+ * definition's, or `rateLimit()` throws at declaration. The limit's key must
42
+ * take a string, the one `rateLimit` counts by.
42
43
  */
43
- export declare function redisStore(limit: BoundRateLimit<string>): RateLimitStore;
44
+ export declare function redisStore(limit: BoundRateLimit<string>): PolicyStore;
44
45
  /**
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.)
46
+ * A bound rate limit with its definition beside it.
50
47
  *
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.
48
+ * @deprecated The bound limit carries its definition since `@nxgt/redis`
49
+ * 0.7: `redisStore(handle.limits.api)` alone is the same store. Given both,
50
+ * the definition's `limit` and `per` must equal the bound limit's, or this
51
+ * throws.
57
52
  */
58
53
  export declare function redisStore(limit: BoundRateLimit<string>, definition: RateLimitDefinition<string>): PolicyStore;
59
54
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
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"}
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;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,WAAW,CAAC;AACvE;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACzB,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,EAC7B,UAAU,EAAE,mBAAmB,CAAC,MAAM,CAAC,GACrC,WAAW,CAAC"}
@@ -199,7 +199,7 @@ several is refused with a `TypeError` naming them. Give a bare client,
199
199
  `handle.clients.<name>`, to the factory that lives on one of them, with the
200
200
  prefix in its `name`.
201
201
 
202
- **Defined once.** With `@nxgt/redis` 0.6 the rate limit and the idempotent
202
+ **Defined once.** With `@nxgt/redis` 0.7 the rate limit and the idempotent
203
203
  operation can be wired by `defineRedis` too, and `redisStore` and `idempotency`
204
204
  take what it wired: [Defined once, in `defineRedis`](#defined-once-in-defineredis).
205
205
 
@@ -229,7 +229,7 @@ const handle = await openRedis(
229
229
  );
230
230
 
231
231
  const app = alxia()
232
- .use(rateLimit({ store: redisStore(handle.limits.api, api) })) // shop:api:<address>, 100 per 60 s from `api`
232
+ .use(rateLimit({ store: redisStore(handle.limits.api) })) // shop:api:<address>, 100 per 60 s from `api`
233
233
  .use(idempotency(handle.idempotency.orders)) // shop:orders:<route>:<scope>:<key>
234
234
  .post('/orders', ({ reply }) => reply(201, { id: crypto.randomUUID() }));
235
235
  ```
@@ -247,10 +247,10 @@ What to know:
247
247
  options (`required`, `scope`, `wait`, `methods`, `header`) but not `name`,
248
248
  `ttl` or `lease`: the types refuse them, and a script that passes them gets a
249
249
  `TypeError`.
250
- - **The rate is the definition's, and `rateLimit` repeats it for its
251
- headers.** The store counts by the wired limit's own rate, so `limit` and
252
- `windowMs` on `rateLimit` only write `RateLimit-*`: give them the definition's
253
- numbers.
250
+ - **The rate is the definition's, and `rateLimit` reads it.** The bound limit
251
+ carries its `definition`, so the store declares `limit` and `per` as its
252
+ `policy`: `rateLimit({ store })` needs no numbers, and one it is given that
253
+ differs throws at declaration. ([The rate, written once](rate-limits.md#the-rate-written-once).)
254
254
  - **The limit counts by a string.** `rateLimit` counts by the string its `key`
255
255
  returns, so the wired limit's key must take one, as `key: (ip: string) => ip`.
256
256
  `redisStore(handle.limits.byIp)` for a limit keyed by `{ ip }` does not
@@ -268,9 +268,9 @@ What to know:
268
268
  does not: the by-name store counts under `shop:api:<limit>/<windowMs>:<key>`,
269
269
  the wired one under `shop:api:<key>`, so counts restart:
270
270
  [troubleshooting](../troubleshooting.md#counts-restart-after-moving-a-rate-limit-to-the-wired-form).
271
- - **Both forms stay**, and need `@nxgt/redis` 0.6 only for `handle.limits` and
272
- `handle.idempotency`: a limit or an idempotency bound by hand with 0.5 is
273
- accepted the same.
271
+ - **Both forms stay**, on `@nxgt/redis` 0.7: the peer is `^0.7.0`, because the
272
+ bound limit's `definition`, which the single-argument `redisStore` reads its
273
+ rate from, is 0.7's.
274
274
 
275
275
  ## Naming keys
276
276
 
@@ -55,27 +55,32 @@ A limit wired by `defineRedis`, `redisStore(handle.limits.api)`, takes no
55
55
 
56
56
  ### The rate, written once
57
57
 
58
- `@nxgt/redis` 0.6 does not expose a bound limit's rate, so a store of the
59
- bound limit alone cannot tell `rateLimit` what it counts by, and `limit` and
60
- `windowMs` repeat the definition for the headers. Give the definition as the
61
- second argument and the store declares it as its `policy`:
58
+ A bound limit carries its `definition` (`@nxgt/redis` 0.7), so the store of
59
+ the bound limit alone declares it as its `policy`:
62
60
 
63
61
  ```ts
64
62
  const api = defineRateLimit({ name: 'api', key: (ip: string) => ip, limit: 100, per: 60_000 });
65
63
  const handle = await openRedis(defineRedis({ uri, prefix: 'shop', limits: { api } }));
66
64
 
67
- app.use(rateLimit({ store: redisStore(handle.limits.api, api) })); // 100 per 60 s
65
+ app.use(rateLimit({ store: redisStore(handle.limits.api) })); // 100 per 60 s
68
66
  ```
69
67
 
70
68
  `rateLimit` reads `limit` (the definition's `limit`) and `windowMs` (its
71
69
  `per`) from the policy: the 429 and the `RateLimit-Limit`, `-Remaining`,
72
70
  `-Reset` and `-Policy` headers come from the definition, and one place holds
73
71
  the numbers. A `limit` or `windowMs` given too must equal them, or
74
- `rateLimit()` throws at declaration. A definition that is not the one that
75
- wired the limit — another `name` — is a `TypeError` from `redisStore`.
72
+ `rateLimit()` throws at declaration. A limit bound by hand,
73
+ `redisStore(bindRateLimit(client, api))`, does the same. (A wired
74
+ limit's `definition.name` is prefixed: only `limit` and `per` are read.)
76
75
  The definition's `burst`, when it sets one, still governs how many requests
77
76
  pass at once; the policy states `limit` per `per`.
78
77
 
78
+ **The two-argument form is deprecated.** `redisStore(handle.limits.api, api)`
79
+ was the way before 0.7 exposed the rate, and still works: given both, it
80
+ compares the definition's `limit` and `per` with the bound limit's, and a
81
+ mismatch is a `TypeError` (the name is not compared: a wired one is prefixed).
82
+ Drop the second argument.
83
+
79
84
  ## What changes with Redis
80
85
 
81
86
  - **Every process counts together.** Behind a load balancer, a client gets
package/docs/roadmap.md CHANGED
@@ -28,6 +28,20 @@ Nothing scheduled yet.
28
28
 
29
29
  ## Shipped
30
30
 
31
+ ### 0.5.0: the definition read from the bound limit
32
+
33
+ - **`redisStore(handle.limits.api)` alone carries the policy.** On
34
+ `@nxgt/redis` 0.7 a bound limit exposes its `definition`, so the store reads
35
+ `limit` and `per` from it: `rateLimit({ store: redisStore(handle.limits.api) })`
36
+ needs no numbers, wired or bound by hand. The two-argument form,
37
+ `redisStore(handle.limits.api, api)`, is deprecated; it compares `limit` and
38
+ `per` (not the prefixed name) and throws on a mismatch.
39
+ - **`idempotency(handle.idempotency.orders)` reads its definition.** It names
40
+ itself from `definition.name` in the unscoped-request warning, where it
41
+ rebuilt the name from a key.
42
+ - **The peer is `@nxgt/redis` `^0.7.0`.** The single-argument form has no rate
43
+ to read from an older bound limit, so it cannot degrade gracefully.
44
+
31
45
  ### 0.4.0: the rate, written once
32
46
 
33
47
  - **A rate limit that reads its policy from the store.**
@@ -42,7 +56,7 @@ Nothing scheduled yet.
42
56
 
43
57
  ### 0.3.0, continued: wired guards
44
58
 
45
- - **On `@nxgt/redis` 0.6.** The peer range is `^0.5.0 || ^0.6.0`: the new forms
59
+ - **On `@nxgt/redis` 0.6 (until 0.5.0).** The peer range is `^0.5.0 || ^0.6.0`: the new forms
46
60
  need no more than 0.5's types, and 0.6 only adds `handle.limits` and
47
61
  `handle.idempotency` to wire them.
48
62
  - **A rate limit and an idempotency defined once.** `redisStore(handle.limits.api)`
@@ -21,7 +21,7 @@ each through; its own refusals, a handle of several instances, a wired guard giv
21
21
  - [`TypeError: defineIdempotency: "…" has a lease of …; it is a whole number of milliseconds, and must be at least 1`](#typeerror-defineidempotency--has-a-lease-of--it-is-a-whole-number-of-milliseconds-and-must-be-at-least-1)
22
22
  - [`TypeError: defineIdempotency: an idempotent operation needs a name, for its keys`](#typeerror-defineidempotency-an-idempotent-operation-needs-a-name-for-its-keys)
23
23
  - [`RedisError: Connection closed`](#rediserror-connection-closed)
24
- - [`TypeError: redisStore: the definition "…" is not the one that wired this limit`](#typeerror-redisstore-the-definition--is-not-the-one-that-wired-this-limit)
24
+ - [`TypeError: redisStore: the definition "…" is not the rate this limit counts by`](#typeerror-redisstore-the-definition--is-not-the-rate-this-limit-counts-by)
25
25
  - [`TypeError: connectRedis: this URI is already connected with other options. Pass the same options everywhere, or close the first connection.`](#typeerror-connectredis-this-uri-is-already-connected-with-other-options-pass-the-same-options-everywhere-or-close-the-first-connection)
26
26
 
27
27
  - [`TypeError: @alxia/redis: this @nxgt/redis handle wires N Redis instances (…), and one is needed.`](#typeerror-alxiaredis-this-nxgtredis-handle-wires-n-redis-instances--and-one-is-needed)
@@ -263,25 +263,24 @@ defineRedis({ uri, prefix: 'shop', caches: { users } });
263
263
  defineRedis({ uri, prefix: 'shop', limits: { api } });
264
264
  ```
265
265
 
266
- ### `TypeError: redisStore: the definition "…" is not the one that wired this limit`
266
+ ### `TypeError: redisStore: the definition "…" is not the rate this limit counts by`
267
267
 
268
- **When:** `redisStore(handle.limits.api, other)` is given a definition whose
269
- `name` is not the one `handle.limits.api` was wired from. The message adds the
270
- key the limit counts under:
268
+ **When:** the deprecated `redisStore(handle.limits.api, other)` is given a
269
+ definition whose `limit` or `per` is not the bound limit's:
271
270
 
272
271
  ```text
273
- TypeError: redisStore: the definition "other" is not the one that wired this limit (it counts under "shop:api:probe")
272
+ TypeError: redisStore: the definition "api" (3 per 60000ms) is not the rate this limit counts by (3 per 30000ms)
274
273
  ```
275
274
 
276
- **Why:** the definition gives `rateLimit` its `limit` and `windowMs`, and a
277
- definition of another limit would write headers for a rate Redis does not
278
- count by. A bound limit's keys are `<prefix>:<name>:<key>`, which is how the
279
- store checks the pair.
275
+ **Why:** the definition would give `rateLimit` its `limit` and `windowMs`, and
276
+ one of another rate would write headers for a rate Redis does not count by.
277
+ The comparison is on the rate, not on the name: a wired limit's
278
+ `definition.name` is prefixed.
280
279
 
281
- **Fix:** give the definition `handle.limits.api` was wired from:
280
+ **Fix:** drop the second argument, the bound limit carries its definition:
282
281
 
283
282
  ```ts
284
- app.use(rateLimit({ store: redisStore(handle.limits.api, api) }));
283
+ app.use(rateLimit({ store: redisStore(handle.limits.api) }));
285
284
  ```
286
285
 
287
286
  ### `TypeError: defineRedis: instance "default" wires the cache "users" and the rate limit "login" under one name, "user". They would share every key in Redis. Give one of them a name of its own.`
@@ -762,15 +761,11 @@ themselves.
762
761
 
763
762
  **Fix:** none is needed but the wait, within `burst × per ÷ limit` of the
764
763
  limit; switch at a quiet moment. `redisStore(handle.limits.api)` counts by the
765
- definition's rate, not by the `limit` and `windowMs` the middleware is given,
766
- which only write the headers: if the `RateLimit-Policy` header says another
767
- rate than the limit enforces, give the definition as the second argument,
768
- `redisStore(handle.limits.api, api)`, and leave `limit` and `windowMs` out of
769
- `rateLimit`: the store declares the definition's rate and the headers come
770
- from it. A `limit` or `windowMs` that differs from it throws
764
+ definition's rate and declares it as its policy: leave `limit` and `windowMs`
765
+ out of `rateLimit` and the headers come from it. A `limit` or `windowMs` that differs from it throws
771
766
  [`rateLimit: limit … differs from the store's policy of …`](https://github.com/softistx/alxia/blob/develop/packages/rate-limit/docs/troubleshooting.md#typeerror-ratelimit-limit--differs-from-the-stores-policy-of-)
772
- at startup; a definition that is not the one that wired the limit throws
773
- `redisStore: the definition "…" is not the one that wired this limit`.
767
+ at startup; the deprecated two-argument form with a definition of another rate
768
+ throws `redisStore: the definition "…" is not the rate this limit counts by`.
774
769
 
775
770
  ### The handle is closed while something still uses it
776
771
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/redis",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Redis for alxia on @nxgt/redis: a rate-limit store every process shares, idempotent routes, caches and locks in the context",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,17 +42,17 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@alxia/cache": "^0.3.0",
45
- "@alxia/core": "^0.5.0",
46
- "@alxia/rate-limit": "^0.4.0",
47
- "@nxgt/redis": "^0.6.0",
45
+ "@alxia/core": "^0.5.1",
46
+ "@alxia/rate-limit": "^0.4.1",
47
+ "@nxgt/redis": "^0.7.0",
48
48
  "@types/bun": "^1.4.2",
49
49
  "zod": "^4.6.5"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@alxia/cache": "^0.3.0",
53
- "@alxia/core": "^0.5.0",
54
- "@alxia/rate-limit": "^0.4.0",
55
- "@nxgt/redis": "^0.5.0 || ^0.6.0",
53
+ "@alxia/core": "^0.5.1",
54
+ "@alxia/rate-limit": "^0.4.1",
55
+ "@nxgt/redis": "^0.7.0",
56
56
  "typescript": "^6.0.3 || ^7.0.0",
57
57
  "zod": "^4.6.5"
58
58
  },