@alxia/redis 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,19 @@
1
1
  # @alxia/redis
2
2
 
3
3
  Redis for [alxia](https://www.npmjs.com/package/@alxia/core), on
4
- [`@nxgt/redis`](https://www.npmjs.com/package/@nxgt/redis) and
5
- [`@nxgt/redis-guard`](https://www.npmjs.com/package/@nxgt/redis-guard) —
4
+ [`@nxgt/redis`](https://www.npmjs.com/package/@nxgt/redis) —
6
5
  Bun's own Redis client, no driver, no dependency:
7
6
 
8
7
  - `redisStore`: a rate-limit store every process shares;
9
8
  - `idempotency`: a middleware, so routes run once per `Idempotency-Key`;
10
9
  - `redisCacheStore`: an `@alxia/cache` store every process shares;
11
- - `redis`: the client, typed caches and a lock in the context.
10
+ - `redis`: the client, typed caches and a lock in the context;
11
+ - `redisCheck`: a readiness check for core's `health()`.
12
+
13
+ Each takes Bun's `RedisClient`, or an [`@nxgt/redis`](https://www.npmjs.com/package/@nxgt/redis) handle: see [With @nxgt/redis](#with-nxgtredis).
12
14
 
13
15
  ```sh
14
- bun add @alxia/redis @nxgt/redis @nxgt/redis-guard zod @alxia/core
16
+ bun add @alxia/redis @nxgt/redis zod @alxia/core
15
17
  bun add -d typescript
16
18
  ```
17
19
 
@@ -144,6 +146,74 @@ const app = alxia()
144
146
  for everything else. It sits beside `@alxia/cache`'s `ctx.cache` — the
145
147
  response cache's `{ tag, skip }` — without touching it, in either order.
146
148
 
149
+ ## With @nxgt/redis
150
+
151
+ Open the Redis once with `@nxgt/redis`'s `openRedis(defineRedis({ … }))` and
152
+ hand the handle to every export that takes a client: one `prefix` is in front
153
+ of every key of the deployment, and the handle closes with the app.
154
+
155
+ ```ts
156
+ import { alxia, health } from '@alxia/core';
157
+ import { redis, redisCheck, redisStore } from '@alxia/redis';
158
+ import { rateLimit } from '@alxia/rate-limit';
159
+ import { defineCache, defineRedis, openRedis } from '@nxgt/redis';
160
+ import { z } from 'zod';
161
+
162
+ const users = defineCache({ name: 'user', key: (id: string) => id, ttl: 300, schema: z.object({ id: z.string(), name: z.string() }) });
163
+ const handle = await openRedis(defineRedis({ uri: Bun.env['REDIS_URL']!, prefix: 'shop', caches: { users } }));
164
+
165
+ const app = alxia()
166
+ .use(rateLimit({ limit: 100, windowMs: 60_000, store: redisStore(handle, { name: 'api' }) })) // shop:api:…
167
+ .plugin(redis(handle)) // caches typed from handle.cache; handle.close() in onStop
168
+ .plugin(health({ checks: { redis: redisCheck(handle) } }))
169
+ .get('/users/:id', async ({ caches, lock, params, reply }) =>
170
+ reply.ok(await lock(params.id, () => caches.users.remember(params.id, () => ({ id: params.id, name: 'Ada' })))), // shop:user:…, lock:shop:…
171
+ );
172
+ ```
173
+
174
+ `redisStore`, `redisCacheStore` and `idempotency` take the handle where they
175
+ take a client, and put its `prefix` in front of their `name`: the keys are
176
+ `shop:api:…`, `shop:pages:response:…`, `shop:orders:…`. `redis(handle)` puts
177
+ `caches` (the handle's own bound caches), `lock` (under the prefix), `redis`
178
+ (the client) and `prefix` in the context, and closes the handle once when the
179
+ app stops, after the requests in flight finished: `redis(handle, { close: false })`
180
+ when something else closes it. The handle must wire one Redis instance.
181
+ The bare `RedisClient` forms are unchanged and add no prefix.
182
+
183
+ ### Defined once, in `defineRedis`
184
+
185
+ With `@nxgt/redis` 0.6 the rate limit and the idempotency are wired by
186
+ `defineRedis` too (`limits`, `idempotency`), and `redisStore` and `idempotency`
187
+ take the wired entry, so the definition lives in one place and its types flow
188
+ through:
189
+
190
+ ```ts
191
+ import { idempotency, idempotencyResult, redisStore } from '@alxia/redis';
192
+ import { defineIdempotency, defineRateLimit, defineRedis, openRedis } from '@nxgt/redis';
193
+
194
+ const api = defineRateLimit({ name: 'api', key: (ip: string) => ip, limit: 100, per: 60_000 });
195
+ const orders = defineIdempotency({ name: 'orders', key: (id: string) => id, ttl: 86_400, schema: idempotencyResult });
196
+ const wired = await openRedis(defineRedis({ uri: Bun.env['REDIS_URL']!, prefix: 'shop', limits: { api }, idempotency: { orders } }));
197
+
198
+ alxia()
199
+ .use(rateLimit({ store: redisStore(wired.limits.api, api) })) // shop:api:<address>, 100 per 60 s from `api`
200
+ .use(idempotency(wired.idempotency.orders, { required: true })) // shop:orders:<route>:<scope>:<key>
201
+ .post('/orders', ({ reply }) => reply(201, { id: crypto.randomUUID() }));
202
+ ```
203
+
204
+ The keys are those `@nxgt/redis` writes, `<prefix>:<name>:<key>`, so another
205
+ 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
212
+ string, and the idempotency's `schema` is `idempotencyResult`: another is a
213
+ compile error. A rate limit moved from `redisStore(handle, { name })` starts
214
+ its counts again, the layouts differing; an idempotency keeps its keys. Both
215
+ older forms stay. [More](docs/guide/connecting.md#defined-once-in-defineredis).
216
+
147
217
  ## Testing
148
218
 
149
219
  The package's specs run against `$REDIS_URL`, or a `redis-server` on
@@ -153,10 +223,17 @@ The package's specs run against `$REDIS_URL`, or a `redis-server` on
153
223
 
154
224
  | export | |
155
225
  | --- | --- |
156
- | `redisStore(client, { name })`, `RedisStoreOptions` | an `@alxia/rate-limit` store |
157
- | `redisCacheStore(client, { name })`, `RedisCacheStoreOptions` | an `@alxia/cache` store |
158
- | `idempotency(client, options)` | the middleware, given to `app.use` |
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` |
229
+ | `redisCacheStore(client \| handle, { name })`, `RedisCacheStoreOptions` | an `@alxia/cache` store |
230
+ | `idempotency(client \| handle, options)` | the middleware, given to `app.use` |
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 |
232
+ | `idempotencyResult`, `IdempotencyResult` | the `schema` a wired idempotency given to `idempotency()` must have: the response it keeps |
159
233
  | `redis(client, { caches? })`, `RedisContextOptions` | a plugin, given to `app.plugin`: `redis`, `caches`, `lock` in the context |
234
+ | `redis(handle, { close? })`, `RedisHandleOptions`, `RedisHandleContext` | the same from an `@nxgt/redis` handle: its typed caches, a prefixed `lock`, `prefix`; closes the handle in `onStop` unless `close: false` |
235
+ | `redisCheck(client \| handle, { timeout? })`, `RedisCheckOptions` | a check for core's `health({ checks })`: down when a Redis instance does not answer a `PING`; `timeout` bounds a handle's ping, a client's is bounded by `health({ timeout })` |
236
+ | `RedisTarget` | a client or a handle: what the factories above take |
160
237
  | `IdempotencyOptions`, `IdempotencyErrorBody`, `RedisContext`, `BoundCaches` | its types |
161
238
  | `IdempotencyMiddleware` | what `idempotency()` returns: a middleware that adds nothing, and may answer a 400, a 409 or a 422 |
162
239
  | `AnyCache` | any cache definition: the constraint of a function generic over the caches it hands to `redis()` |
@@ -166,3 +243,4 @@ The package's specs run against `$REDIS_URL`, or a `redis-server` on
166
243
  - [Guide](https://github.com/softistx/alxia/tree/develop/packages/redis/docs): a page per area — connecting, rate limits, the response cache, idempotency, caches and locks, and testing against a real Redis.
167
244
  - [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/redis/docs/troubleshooting.md): an error message, a refusal a client got, or a limit, cache or replay that does not behave as expected, and what to do about it.
168
245
  - [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/redis/docs/roadmap.md): what is coming, and what is not planned.
246
+ - [Recipes](https://github.com/softistx/alxia/blob/develop/docs/recipes/README.md): [Caching and rate limiting with Redis](https://github.com/softistx/alxia/blob/develop/docs/recipes/caching-and-rate-limiting.md), [Test an alxia app](https://github.com/softistx/alxia/blob/develop/docs/recipes/testing.md), [Health checks and graceful shutdown](https://github.com/softistx/alxia/blob/develop/docs/recipes/health-and-shutdown.md).
@@ -1,7 +1,7 @@
1
1
  import type { CacheStore } from '@alxia/cache';
2
- import type { RedisClient } from 'bun';
2
+ import { type RedisTarget } from './handle';
3
3
  export interface RedisCacheStoreOptions {
4
- /** Prepended to every key it writes: one name per app or deployment. */
4
+ /** Prepended to every key it writes: one name per app or deployment. Under a handle's prefix, when it is given one. */
5
5
  readonly name: string;
6
6
  }
7
7
  /**
@@ -13,6 +13,9 @@ export interface RedisCacheStoreOptions {
13
13
  * ```ts
14
14
  * app.use(cache({ ttl: 60, store: redisCacheStore(connection.client, { name: 'shop' }) }));
15
15
  * ```
16
+ *
17
+ * Given an `@nxgt/redis` handle instead of a client, every key, the tag sets
18
+ * included, is under the handle's `prefix`.
16
19
  */
17
- export declare function redisCacheStore(client: RedisClient, options: RedisCacheStoreOptions): CacheStore;
20
+ export declare function redisCacheStore(target: RedisTarget, options: RedisCacheStoreOptions): CacheStore;
18
21
  //# sourceMappingURL=cache-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache-store.d.ts","sourceRoot":"","sources":["../src/cache-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAGvC,MAAM,WAAW,sBAAsB;IACtC,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAeD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,sBAAsB,GAC7B,UAAU,CA4EZ"}
1
+ {"version":3,"file":"cache-store.d.ts","sourceRoot":"","sources":["../src/cache-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/D,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvD,MAAM,WAAW,sBAAsB;IACtC,uHAAuH;IACvH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AA4CD;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,sBAAsB,GAC7B,UAAU,CAwDZ"}
@@ -0,0 +1,19 @@
1
+ import type { RedisTarget } from './handle';
2
+ export interface RedisCheckOptions {
3
+ /**
4
+ * Milliseconds a handle's ping waits for each instance, as
5
+ * `health({ timeout })` names its own: `@nxgt/redis`'s 2 s by default.
6
+ */
7
+ readonly timeout?: number;
8
+ }
9
+ /**
10
+ * A readiness check for core's `health({ checks })`: it passes when every
11
+ * Redis instance of the handle answers a `PING`, and is down when one does
12
+ * not, or when the call fails. Given a client, it sends the `PING` itself.
13
+ *
14
+ * ```ts
15
+ * app.plugin(health({ checks: { redis: redisCheck(handle) } }));
16
+ * ```
17
+ */
18
+ export declare function redisCheck(target: RedisTarget, options?: RedisCheckOptions): () => Promise<boolean>;
19
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,MAAM,WAAW,iBAAiB;IACjC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACzB,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE,iBAAsB,GAC7B,MAAM,OAAO,CAAC,OAAO,CAAC,CAaxB"}
package/dist/context.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type BoundCache, type CacheDefinition, type LockOptions } from '@nxgt/redis';
1
+ import { type BoundCache, type CacheDefinition, type LockOptions, type Redis, type SoleInstance } from '@nxgt/redis';
2
2
  import type { RedisClient } from 'bun';
3
3
  import type { z } from 'zod';
4
4
  /**
@@ -30,6 +30,26 @@ export interface RedisContext<Caches extends Record<string, AnyCache>> {
30
30
  /** `work` under a lock every process sharing the Redis respects: `@nxgt/redis`'s `withLock`. */
31
31
  lock<T>(key: string, work: () => Promise<T> | T, options?: LockOptions): Promise<T>;
32
32
  }
33
+ export interface RedisHandleOptions {
34
+ /**
35
+ * Whether the handle is closed when the app stops: once, after the drain,
36
+ * in `onStop`. On by default; `false` when something else closes it.
37
+ */
38
+ readonly close?: boolean;
39
+ }
40
+ /** What routes after `redis(handle)` read. */
41
+ export interface RedisHandleContext<C> {
42
+ /** Bun's own client, untouched: the handle's one instance. */
43
+ readonly redis: RedisClient;
44
+ /** The handle's caches, typed by their schemas, the prefix already in front of every key. */
45
+ readonly caches: SoleInstance<C>['cache'];
46
+ /** `work` under a lock every process sharing the Redis respects, under the handle's prefix. */
47
+ readonly lock: SoleInstance<C>['lock'];
48
+ /** What goes in front of every key, or `undefined`. */
49
+ readonly prefix: string | undefined;
50
+ }
51
+ declare function fromClient<const Caches extends Record<string, AnyCache> = Record<never, never>>(client: RedisClient, options?: RedisContextOptions<Caches>): import("@alxia/core").Alxia<import("@alxia/core").Empty & RedisContext<Caches>, "">;
52
+ declare function fromHandle<C>(handle: Redis<C>, options?: RedisHandleOptions): import("@alxia/core").Alxia<import("@alxia/core").Empty & RedisHandleContext<C>, "">;
33
53
  /**
34
54
  * Redis in the context, as a plugin: the client, the caches bound once, and
35
55
  * a lock — typed, for every route declared after it.
@@ -39,6 +59,17 @@ export interface RedisContext<Caches extends Record<string, AnyCache>> {
39
59
  * app.plugin(redis(connection.client, { caches: { users } }))
40
60
  * .get('/users/:id', async ({ caches, params, reply }) => reply.ok(await caches.users.remember(params.id, load)));
41
61
  * ```
62
+ *
63
+ * Given an `@nxgt/redis` handle, the caches are the handle's own, the
64
+ * prefix is in front of every key and lock, and the handle is closed when
65
+ * the app stops, after the drain (`{ close: false }` to close it yourself):
66
+ *
67
+ * ```ts
68
+ * const handle = await openRedis(defineRedis({ uri, prefix: 'shop', caches }));
69
+ * app.plugin(redis(handle)).get('/u/:id', ({ caches, params }) => caches.users.get(params.id));
70
+ * ```
42
71
  */
43
- export declare function redis<const Caches extends Record<string, AnyCache> = Record<never, never>>(client: RedisClient, options?: RedisContextOptions<Caches>): import("@alxia/core").Alxia<import("@alxia/core").Empty & RedisContext<Caches>, "", never>;
72
+ export declare function redis<const Caches extends Record<string, AnyCache> = Record<never, never>>(client: RedisClient, options?: RedisContextOptions<Caches>): ReturnType<typeof fromClient<Caches>>;
73
+ export declare function redis<C>(handle: Redis<C>, options?: RedisHandleOptions): ReturnType<typeof fromHandle<C>>;
74
+ export {};
44
75
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,UAAU,EAEf,KAAK,eAAe,EACpB,KAAK,WAAW,EAEhB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B;;;;;;;;;;GAUG;AACH,MAAM,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;AAEvD,wDAAwD;AACxD,MAAM,MAAM,WAAW,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI;IAClE,QAAQ,EAAE,IAAI,IAAI,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,eAAe,CACpE,MAAM,MAAM,EACZ,MAAM,MAAM,CACZ,GACE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GACrD,KAAK;CACR,CAAC;AAEF,MAAM,WAAW,mBAAmB,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC3E,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wCAAwC;AACxC,MAAM,WAAW,YAAY,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;IACpE,mCAAmC;IACnC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,gGAAgG;IAChG,IAAI,CAAC,CAAC,EACL,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAC1B,OAAO,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,CAAC,CAAC,CAAC;CACd;AAED;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CACpB,KAAK,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EACnE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,mBAAmB,CAAC,MAAM,CAAM,8FAa/D"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { BoundIdempotency, BoundRateLimit, Redis } from '@nxgt/redis';
2
+ import type { RedisClient } from 'bun';
3
+ /**
4
+ * What the factories take: Bun's own client, or the handle `@nxgt/redis`'s
5
+ * `openRedis(defineRedis({ … }))` gives back.
6
+ */
7
+ export type RedisTarget = RedisClient | Redis<any>;
8
+ /** The one instance of a handle, resolved: its client and the prefix in front of its keys. */
9
+ export interface ResolvedTarget {
10
+ readonly client: RedisClient;
11
+ /** `undefined` when nothing goes in front of the keys, and always for a bare client. */
12
+ readonly prefix: string | undefined;
13
+ }
14
+ /** A handle has `instances` and `close`; Bun's `RedisClient` has neither. */
15
+ export declare function isHandle(target: RedisTarget): target is Redis<any>;
16
+ /**
17
+ * The client and prefix of a target. A handle must wire exactly one Redis
18
+ * instance: with several, which one an app's keys live on is for the app to
19
+ * say, with the bare client of `handle.clients.<name>`.
20
+ */
21
+ export declare function resolve(target: RedisTarget): ResolvedTarget;
22
+ /** `name`, with the handle's prefix in front when there is one: the one name a deployment's keys share. */
23
+ export declare function nameUnder(target: RedisTarget, name: string): {
24
+ readonly client: RedisClient;
25
+ readonly name: string;
26
+ };
27
+ /** A rate limit bound by `@nxgt/redis`, wired or by hand: `consume`, `peek`, `reset` and `keyFor`. */
28
+ export declare function isWiredLimit(value: object): value is BoundRateLimit<string>;
29
+ /** An idempotency bound by `@nxgt/redis`, wired or by hand: `run`, `forget` and `keyFor`. */
30
+ export declare function isWiredIdempotency(value: object): value is BoundIdempotency<string, any, any>;
31
+ //# sourceMappingURL=handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAEvC;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;AAEnD,8FAA8F;AAC9F,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,CAMlE;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,cAAc,CAc3D;AAED,2GAA2G;AAC3G,wBAAgB,SAAS,CACxB,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,GACV;IACF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB,CAGA;AAED,sGAAsG;AACtG,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,cAAc,CAAC,MAAM,CAAC,CAQ3E;AAED,6FAA6F;AAC7F,wBAAgB,kBAAkB,CACjC,KAAK,EAAE,MAAM,GACX,KAAK,IAAI,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAO7C"}
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * What `idempotency()` keeps under a key: the response, as the route
4
+ * answered it. The `schema` of a wired `defineIdempotency` that
5
+ * `idempotency(handle.idempotency.orders)` is given:
6
+ *
7
+ * ```ts
8
+ * export const orders = defineIdempotency({ name: 'orders', key: (id: string) => id, ttl: 86_400, schema: idempotencyResult });
9
+ * ```
10
+ */
11
+ export declare const idempotencyResult: z.ZodObject<{
12
+ status: z.ZodNumber;
13
+ headers: z.ZodArray<z.ZodTuple<[z.ZodString, z.ZodString], null>>;
14
+ body: z.ZodString;
15
+ }, z.core.$strip>;
16
+ export type IdempotencyResult = z.infer<typeof idempotencyResult>;
17
+ /** A response that is answered and not kept: a 5xx, a stream. */
18
+ export declare class Unstored extends Error {
19
+ readonly response: Response;
20
+ constructor(response: Response);
21
+ }
22
+ /** What a key is bound to: the method, the path and query, the body. */
23
+ export declare function fingerprintOf(request: Request, url: URL): Promise<Uint8Array>;
24
+ export declare function store(response: Response): Promise<IdempotencyResult>;
25
+ export declare function restore(stored: IdempotencyResult, replayed: boolean): Response;
26
+ //# sourceMappingURL=idempotency-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency-response.d.ts","sourceRoot":"","sources":["../src/idempotency-response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAKlE,iEAAiE;AACjE,qBAAa,QAAS,SAAQ,KAAK;IAClC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;gBAChB,QAAQ,EAAE,QAAQ;CAI9B;AAED,wEAAwE;AACxE,wBAAsB,aAAa,CAClC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,GAAG,GACN,OAAO,CAAC,UAAU,CAAC,CASrB;AAED,wBAAsB,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiB1E;AAED,wBAAgB,OAAO,CACtB,MAAM,EAAE,iBAAiB,EACzB,QAAQ,EAAE,OAAO,GACf,QAAQ,CAQV"}
@@ -1,11 +1,13 @@
1
- import { type BaseContext, type Empty, type Middleware, type MiddlewareMark, type Reply } from '@alxia/core';
2
- import type { RedisClient } from 'bun';
1
+ import { type BaseContext, type Empty, type Middleware, type Reply } from '@alxia/core';
2
+ import { type BoundIdempotency } from '@nxgt/redis';
3
+ import { type RedisTarget } from './handle';
4
+ import { type IdempotencyResult } from './idempotency-response';
3
5
  export interface IdempotencyOptions {
4
- /** Names the keys it stores. */
6
+ /** Names the keys it stores, under a handle's prefix when it is given one. */
5
7
  readonly name: string;
6
8
  /** Seconds a finished response is kept and replayed. A day by default. */
7
9
  readonly ttl?: number;
8
- /** Milliseconds a running request holds its key unless renewed. `@nxgt/redis-guard`'s 10 s by default. */
10
+ /** Milliseconds a running request holds its key unless renewed. `@nxgt/redis`'s 10 s by default. */
9
11
  readonly lease?: number;
10
12
  /** Milliseconds a repeat waits for the first to finish before a 409. None by default. */
11
13
  readonly wait?: number;
@@ -34,9 +36,17 @@ export interface IdempotencyErrorBody {
34
36
  * What `idempotency()` makes: a middleware that adds nothing to the
35
37
  * context, and may answer a 400, a 409 or a 422.
36
38
  */
37
- export type IdempotencyMiddleware = Middleware<Empty, Promise<Response | Reply<400, IdempotencyErrorBody> | Reply<409, IdempotencyErrorBody> | Reply<422, IdempotencyErrorBody>>> & MiddlewareMark;
39
+ export type IdempotencyMiddleware = Middleware<Empty, Promise<Response | Reply<400, IdempotencyErrorBody> | Reply<409, IdempotencyErrorBody> | Reply<422, IdempotencyErrorBody>>>;
38
40
  /**
39
- * Idempotent routes, as a middleware, with `@nxgt/redis-guard`: a `POST` or
41
+ * An idempotency wired by `defineRedis` — `handle.idempotency.orders` — whose
42
+ * definition holds `idempotencyResult` as its schema and a key taken as a
43
+ * string.
44
+ */
45
+ export type WiredIdempotency = BoundIdempotency<string, IdempotencyResult, IdempotencyResult>;
46
+ /** What a wired idempotency leaves to the middleware: its name, `ttl` and `lease` are the definition's. */
47
+ export type WiredIdempotencyOptions = Omit<IdempotencyOptions, 'name' | 'ttl' | 'lease'>;
48
+ /**
49
+ * Idempotent routes, as a middleware, with `@nxgt/redis`: a `POST` or
40
50
  * `PATCH` carrying an `Idempotency-Key` runs once per key, and every repeat
41
51
  * gets the first response back, marked `Idempotent-Replayed: true` — across
42
52
  * every process sharing the Redis. Routes declared after it are guarded; a
@@ -52,6 +62,18 @@ export type IdempotencyMiddleware = Middleware<Empty, Promise<Response | Reply<4
52
62
  * ```ts
53
63
  * app.use(idempotency(redis.client, { name: 'payments' })).post('/payments', ...);
54
64
  * ```
65
+ *
66
+ * Given an `@nxgt/redis` handle instead of a client, the keys are under the
67
+ * handle's `prefix`: `idempotency(handle, { name: 'payments' })`.
68
+ *
69
+ * Given an idempotency wired by `defineRedis`, the definition — name, `ttl`,
70
+ * `lease` — is the one place, and the keys are those `@nxgt/redis` writes,
71
+ * `<prefix>:<name>:<route>:<scope>:<key>`:
72
+ *
73
+ * ```ts
74
+ * app.use(idempotency(handle.idempotency.orders, { required: true })).post('/orders', ...);
75
+ * ```
55
76
  */
56
- export declare function idempotency(client: RedisClient, options: IdempotencyOptions): IdempotencyMiddleware;
77
+ export declare function idempotency(target: RedisTarget, options: IdempotencyOptions): IdempotencyMiddleware;
78
+ export declare function idempotency(wired: WiredIdempotency, options?: WiredIdempotencyOptions): IdempotencyMiddleware;
57
79
  //# sourceMappingURL=idempotency.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,KAAK,EACV,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,KAAK,EAEV,MAAM,aAAa,CAAC;AAMrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAGvC,MAAM,WAAW,kBAAkB;IAClC,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,0GAA0G;IAC1G,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,yFAAyF;IACzF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,+FAA+F;IAC/F,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,MAAM,GAAG,SAAS,CAAC;CAC1D;AAED,6BAA6B;AAC7B,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,KAAK,EACX,yBAAyB,GACzB,yBAAyB,GACzB,yBAAyB,GACzB,wBAAwB,CAAC;IAC5B,sFAAsF;IACtF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,UAAU,CAC7C,KAAK,EACL,OAAO,CACJ,QAAQ,GACR,KAAK,CAAC,GAAG,EAAE,oBAAoB,CAAC,GAChC,KAAK,CAAC,GAAG,EAAE,oBAAoB,CAAC,GAChC,KAAK,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAClC,CACD,GACA,cAAc,CAAC;AAuBhB;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAC1B,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,kBAAkB,GACzB,qBAAqB,CA2EvB"}
1
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,WAAW,EAEhB,KAAK,KAAK,EACV,KAAK,UAAU,EAEf,KAAK,KAAK,EAEV,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,KAAK,gBAAgB,EAIrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAiC,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAC3E,OAAO,EAEN,KAAK,iBAAiB,EAKtB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,kBAAkB;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,oGAAoG;IACpG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,yFAAyF;IACzF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,+FAA+F;IAC/F,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,MAAM,GAAG,SAAS,CAAC;CAC1D;AAED,6BAA6B;AAC7B,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,KAAK,EACX,yBAAyB,GACzB,yBAAyB,GACzB,yBAAyB,GACzB,wBAAwB,CAAC;IAC5B,sFAAsF;IACtF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,UAAU,CAC7C,KAAK,EACL,OAAO,CACJ,QAAQ,GACR,KAAK,CAAC,GAAG,EAAE,oBAAoB,CAAC,GAChC,KAAK,CAAC,GAAG,EAAE,oBAAoB,CAAC,GAChC,KAAK,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAClC,CACD,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,CAC9C,MAAM,EACN,iBAAiB,EACjB,iBAAiB,CACjB,CAAC;AAEF,2GAA2G;AAC3G,MAAM,MAAM,uBAAuB,GAAG,IAAI,CACzC,kBAAkB,EAClB,MAAM,GAAG,KAAK,GAAG,OAAO,CACxB,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CAC1B,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,kBAAkB,GACzB,qBAAqB,CAAC;AACzB,wBAAgB,WAAW,CAC1B,KAAK,EAAE,gBAAgB,EACvB,OAAO,CAAC,EAAE,uBAAuB,GAC/B,qBAAqB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { type RedisCacheStoreOptions, redisCacheStore } from './cache-store';
2
- export { type AnyCache, type BoundCaches, type RedisContext, type RedisContextOptions, redis, } from './context';
3
- export { type IdempotencyErrorBody, type IdempotencyMiddleware, type IdempotencyOptions, idempotency, } from './idempotency';
2
+ export { type RedisCheckOptions, redisCheck } from './check';
3
+ export { type AnyCache, type BoundCaches, type RedisContext, type RedisContextOptions, type RedisHandleContext, type RedisHandleOptions, redis, } from './context';
4
+ export type { RedisTarget } from './handle';
5
+ export { type IdempotencyErrorBody, type IdempotencyMiddleware, type IdempotencyOptions, idempotency, type WiredIdempotency, type WiredIdempotencyOptions, } from './idempotency';
6
+ export { type IdempotencyResult, idempotencyResult, } from './idempotency-response';
4
7
  export { type RedisStoreOptions, redisStore } from './store';
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,sBAAsB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EACN,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,GACL,MAAM,WAAW,CAAC;AACnB,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,WAAW,GACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,sBAAsB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EACN,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,GACL,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC5B,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,KAAK,iBAAiB,EACtB,iBAAiB,GACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}