@alxia/redis 0.3.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,16 +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({ limit: 100, windowMs: 60_000, store: redisStore(wired.limits.api) })) // shop:api:<address>
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: `rateLimit`'s `limit` and
207
- `windowMs` only write its headers, so repeat the definition's numbers, and the
208
- 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
209
212
  string, and the idempotency's `schema` is `idempotencyResult`: another is a
210
213
  compile error. A rate limit moved from `redisStore(handle, { name })` starts
211
214
  its counts again, the layouts differing; an idempotency keeps its keys. Both
@@ -221,7 +224,8 @@ The package's specs run against `$REDIS_URL`, or a `redis-server` on
221
224
  | export | |
222
225
  | --- | --- |
223
226
  | `redisStore(client \| handle, { name })`, `RedisStoreOptions` | an `@alxia/rate-limit` store |
224
- | `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>` |
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 |
225
229
  | `redisCacheStore(client \| handle, { name })`, `RedisCacheStoreOptions` | an `@alxia/cache` store |
226
230
  | `idempotency(client \| handle, options)` | the middleware, given to `app.use` |
227
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().`;
@@ -307,9 +303,10 @@ import {
307
303
  defineRateLimit
308
304
  } from "@nxgt/redis";
309
305
  function redisStore(target, options) {
310
- if (isWiredLimit(target))
311
- return wiredStore(target);
312
- if (options === undefined) {
306
+ if (isWiredLimit(target)) {
307
+ return policyStore(target, options);
308
+ }
309
+ if (options === undefined || !("name" in options)) {
313
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");
314
311
  }
315
312
  const { client, name } = nameUnder(target, options.name);
@@ -377,6 +374,13 @@ function wiredStore(limit) {
377
374
  }
378
375
  };
379
376
  }
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)`);
381
+ }
382
+ return { ...wiredStore(limit), policy: { limit: count, windowMs: per } };
383
+ }
380
384
  export {
381
385
  idempotency,
382
386
  idempotencyResult,
@@ -386,5 +390,5 @@ export {
386
390
  redisStore
387
391
  };
388
392
 
389
- //# debugId=1962E3DF430E773F64756E2164756E21
393
+ //# debugId=8102DF4343F3D91E64756E2164756E21
390
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 { Decision, Policy, RateLimitStore } from '@alxia/rate-limit';\nimport {\n\ttype BoundRateLimit,\n\tbindRateLimit,\n\tdefineRateLimit,\n} from '@nxgt/redis';\nimport { isWiredLimit, nameUnder, type RedisTarget } from './handle';\n\nexport interface RedisStoreOptions {\n\t/**\n\t * Prepended to every key it counts: one name per limit, so two never share\n\t * a count. Under a handle's prefix, when it is given one.\n\t */\n\treadonly name: string;\n}\n\n/**\n * An `@alxia/rate-limit` store in Redis, with `@nxgt/redis`'s GCRA:\n * every process sharing the Redis counts together, timed by the Redis\n * server's clock, and a refused request counts nothing.\n *\n * ```ts\n * app.use(rateLimit({ limit: 100, windowMs: 60_000, store: redisStore(redis.client, { name: 'api' }) }));\n * ```\n *\n * Given an `@nxgt/redis` handle instead of a client, every key is under the\n * handle's `prefix`: `redisStore(handle, { name: 'api' })` counts under\n * `<prefix>:api:…`.\n */\nexport function redisStore(\n\ttarget: RedisTarget,\n\toptions: RedisStoreOptions,\n): RateLimitStore;\n/**\n * The store of a rate limit wired by `defineRedis`: the definition — its\n * name, `limit`, `per` and `burst` — is the one place, and the keys are\n * those `@nxgt/redis` writes, `<prefix>:<name>:<key>`, so every consumer of\n * the handle counts together, alxia's routes included.\n *\n * ```ts\n * const api = defineRateLimit({ name: 'api', key: (ip: string) => ip, limit: 100, per: 60_000 });\n * const handle = await openRedis(defineRedis({ uri, prefix: 'shop', limits: { api } }));\n * app.use(rateLimit({ limit: 100, windowMs: 60_000, store: redisStore(handle.limits.api) }));\n * ```\n *\n * It counts by the wired limit's own rate, not by the `limit` and\n * `windowMs` the middleware is given, which only write its headers: give it\n * the same numbers. The limit's key must take a string, the one `rateLimit`\n * counts by.\n */\nexport function redisStore(limit: BoundRateLimit<string>): RateLimitStore;\nexport function redisStore(\n\ttarget: RedisTarget | BoundRateLimit<string>,\n\toptions?: RedisStoreOptions,\n): RateLimitStore {\n\tif (isWiredLimit(target)) return wiredStore(target);\n\tif (options === undefined) {\n\t\tthrow new TypeError(\n\t\t\t'redisStore: a client or a handle needs { name }; a wired rate limit, as handle.limits.api, is the only form without',\n\t\t);\n\t}\n\tconst { client, name } = nameUnder(target, options.name);\n\tconst limits = new Map<string, Promise<BoundRateLimit<string>>>();\n\t// Every policy counted under this name, by any process: what `reset` forgets.\n\tconst policies = `${name}:policies`;\n\tconst bind = (limit: number, windowMs: number) =>\n\t\tbindRateLimit(\n\t\t\tclient,\n\t\t\tdefineRateLimit({\n\t\t\t\tname: `${name}:${limit}/${windowMs}`,\n\t\t\t\tkey: (key: string) => key,\n\t\t\t\tlimit,\n\t\t\t\tper: windowMs,\n\t\t\t}),\n\t\t);\n\t/** The bound limit of a policy, recorded in `policies` the first time this process counts under it. */\n\tconst limitFor = (policy: Policy) => {\n\t\tconst id = `${policy.limit}/${policy.windowMs}`;\n\t\tlet bound = limits.get(id);\n\t\tif (bound === undefined) {\n\t\t\tconst limit = bind(policy.limit, policy.windowMs);\n\t\t\tconst recorded = client.send('SADD', [policies, id]).then(() => limit);\n\t\t\trecorded.catch(() => {\n\t\t\t\tif (limits.get(id) === recorded) limits.delete(id);\n\t\t\t});\n\t\t\tlimits.set(id, recorded);\n\t\t\tbound = recorded;\n\t\t}\n\t\treturn bound;\n\t};\n\treturn {\n\t\tasync consume(key, policy): Promise<Decision> {\n\t\t\tconst result = await (await limitFor(policy)).consume(key);\n\t\t\treturn {\n\t\t\t\tallowed: result.allowed,\n\t\t\t\tremaining: result.remaining,\n\t\t\t\tresetAfter: result.resetAfter,\n\t\t\t\tretryAfter: result.retryAfter,\n\t\t\t};\n\t\t},\n\t\tasync reset(key) {\n\t\t\tconst ids = (await client.send('SMEMBERS', [policies])) as string[];\n\t\t\tawait Promise.all(\n\t\t\t\tids.map(async (id) => {\n\t\t\t\t\t// Only what `limitFor` writes: anything else in the set is not ours.\n\t\t\t\t\tconst policy = /^(\\d+)\\/(\\d+)$/.exec(id);\n\t\t\t\t\tif (policy === null) return;\n\t\t\t\t\tconst limit = Number(policy[1]);\n\t\t\t\t\tconst windowMs = Number(policy[2]);\n\t\t\t\t\tif (limit < 1 || windowMs < 1) return;\n\t\t\t\t\tawait bind(limit, windowMs).reset(key);\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t};\n}\n\n/** A wired limit as a store: it counts by its own rate, the policy the middleware holds is left to its headers. */\nfunction wiredStore(limit: BoundRateLimit<string>): RateLimitStore {\n\treturn {\n\t\tasync consume(key): Promise<Decision> {\n\t\t\tconst result = await limit.consume(key);\n\t\t\treturn {\n\t\t\t\tallowed: result.allowed,\n\t\t\t\tremaining: result.remaining,\n\t\t\t\tresetAfter: result.resetAfter,\n\t\t\t\tretryAfter: result.retryAfter,\n\t\t\t};\n\t\t},\n\t\tasync reset(key) {\n\t\t\tawait limit.reset(key);\n\t\t},\n\t};\n}\n"
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": "1962E3DF430E773F64756E2164756E21",
13
+ "mappings": "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",
14
+ "debugId": "8102DF4343F3D91E64756E2164756E21",
15
15
  "names": []
16
16
  }
package/dist/store.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { RateLimitStore } from '@alxia/rate-limit';
2
- import { type BoundRateLimit } from '@nxgt/redis';
1
+ import type { PolicyStore, RateLimitStore } from '@alxia/rate-limit';
2
+ import { type BoundRateLimit, type RateLimitDefinition } from '@nxgt/redis';
3
3
  import { type RedisTarget } from './handle';
4
4
  export interface RedisStoreOptions {
5
5
  /**
@@ -23,21 +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. The limit's key must take a string, the one `rateLimit`
40
- * 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.
41
43
  */
42
- export declare function redisStore(limit: BoundRateLimit<string>): RateLimitStore;
44
+ export declare function redisStore(limit: BoundRateLimit<string>): PolicyStore;
45
+ /**
46
+ * A bound rate limit with its definition beside it.
47
+ *
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.
52
+ */
53
+ export declare function redisStore(limit: BoundRateLimit<string>, definition: RateLimitDefinition<string>): PolicyStore;
43
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,EAAoB,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EACN,KAAK,cAAc,EAGnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAErE,MAAM,WAAW,iBAAiB;IACjC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CACzB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,iBAAiB,GACxB,cAAc,CAAC;AAClB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,cAAc,CAAC"}
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({ limit: 100, windowMs: 60_000, store: redisStore(handle.limits.api) })) // shop:api:<address>
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
 
@@ -30,6 +30,7 @@ bun add @alxia/rate-limit
30
30
  ```ts
31
31
  function redisStore(target: RedisClient | Redis<any>, options: RedisStoreOptions): RateLimitStore;
32
32
  function redisStore(limit: BoundRateLimit<string>): RateLimitStore; // wired by defineRedis
33
+ function redisStore(limit: BoundRateLimit<string>, definition: RateLimitDefinition<string>): PolicyStore; // and its policy
33
34
 
34
35
  interface RedisStoreOptions {
35
36
  /** Prepended to every key it counts: one name per limit, so two never share a count. */
@@ -52,6 +53,34 @@ A limit wired by `defineRedis`, `redisStore(handle.limits.api)`, takes no
52
53
  `<prefix>:<name>:<key>`, shared with every other `@nxgt/redis` consumer. See
53
54
  [Defined once, in `defineRedis`](connecting.md#defined-once-in-defineredis).
54
55
 
56
+ ### The rate, written once
57
+
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`:
60
+
61
+ ```ts
62
+ const api = defineRateLimit({ name: 'api', key: (ip: string) => ip, limit: 100, per: 60_000 });
63
+ const handle = await openRedis(defineRedis({ uri, prefix: 'shop', limits: { api } }));
64
+
65
+ app.use(rateLimit({ store: redisStore(handle.limits.api) })); // 100 per 60 s
66
+ ```
67
+
68
+ `rateLimit` reads `limit` (the definition's `limit`) and `windowMs` (its
69
+ `per`) from the policy: the 429 and the `RateLimit-Limit`, `-Remaining`,
70
+ `-Reset` and `-Policy` headers come from the definition, and one place holds
71
+ the numbers. A `limit` or `windowMs` given too must equal them, or
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.)
75
+ The definition's `burst`, when it sets one, still governs how many requests
76
+ pass at once; the policy states `limit` per `per`.
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
+
55
84
  ## What changes with Redis
56
85
 
57
86
  - **Every process counts together.** Behind a load balancer, a client gets
package/docs/roadmap.md CHANGED
@@ -12,11 +12,7 @@ shipped in 0.2.0 and finished in 0.3.0.
12
12
 
13
13
  ## Next
14
14
 
15
- - **A rate limit that reads its policy from the store.** With
16
- `redisStore(handle.limits.api)` the rate lives in the definition, and
17
- `rateLimit({ limit, windowMs })` still repeats it for its headers. The store
18
- telling `rateLimit` its own policy, so the numbers are written once, is
19
- planned for `@alxia/rate-limit` and this package together.
15
+ Nothing scheduled yet.
20
16
 
21
17
  ## Later
22
18
 
@@ -32,9 +28,35 @@ Nothing scheduled yet.
32
28
 
33
29
  ## Shipped
34
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
+
45
+ ### 0.4.0: the rate, written once
46
+
47
+ - **A rate limit that reads its policy from the store.**
48
+ `redisStore(handle.limits.api, api)`, given the definition beside the wired
49
+ limit, declares the definition's `limit` and `per` as the store's `policy`;
50
+ `rateLimit({ store })` takes `limit` and `windowMs` from it (optional in the
51
+ type, with `@alxia/rate-limit` 0.4) and writes its `RateLimit-*` headers
52
+ from it, and numbers that differ throw at declaration. `redisStore` returns
53
+ a `PolicyStore`. The one-argument form stays, with its numbers repeated.
54
+ (`@nxgt/redis` 0.6 does not expose a bound limit's rate, hence the
55
+ definition as an argument.)
56
+
35
57
  ### 0.3.0, continued: wired guards
36
58
 
37
- - **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
38
60
  need no more than 0.5's types, and 0.6 only adds `handle.limits` and
39
61
  `handle.idempotency` to wire them.
40
62
  - **A rate limit and an idempotency defined once.** `redisStore(handle.limits.api)`
@@ -21,6 +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 rate this limit counts by`](#typeerror-redisstore-the-definition--is-not-the-rate-this-limit-counts-by)
24
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)
25
26
 
26
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)
@@ -262,6 +263,26 @@ defineRedis({ uri, prefix: 'shop', caches: { users } });
262
263
  defineRedis({ uri, prefix: 'shop', limits: { api } });
263
264
  ```
264
265
 
266
+ ### `TypeError: redisStore: the definition "…" is not the rate this limit counts by`
267
+
268
+ **When:** the deprecated `redisStore(handle.limits.api, other)` is given a
269
+ definition whose `limit` or `per` is not the bound limit's:
270
+
271
+ ```text
272
+ TypeError: redisStore: the definition "api" (3 per 60000ms) is not the rate this limit counts by (3 per 30000ms)
273
+ ```
274
+
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.
279
+
280
+ **Fix:** drop the second argument, the bound limit carries its definition:
281
+
282
+ ```ts
283
+ app.use(rateLimit({ store: redisStore(handle.limits.api) }));
284
+ ```
285
+
265
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.`
266
287
 
267
288
  **When:** the `defineRedis(...)` of an app that gives `caches`, `limits` and
@@ -740,9 +761,11 @@ themselves.
740
761
 
741
762
  **Fix:** none is needed but the wait, within `burst × per ÷ limit` of the
742
763
  limit; switch at a quiet moment. `redisStore(handle.limits.api)` counts by the
743
- definition's rate, not by the `limit` and `windowMs` the middleware is given,
744
- which only write the headers: if the `RateLimit-Policy` header says another
745
- rate than the limit enforces, make the two numbers equal.
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
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-)
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`.
746
769
 
747
770
  ### The handle is closed while something still uses it
748
771
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/redis",
3
- "version": "0.3.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.3.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.3.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
  },