@alxia/redis 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -16
- package/dist/cache-store.d.ts +6 -3
- package/dist/cache-store.d.ts.map +1 -1
- package/dist/check.d.ts +19 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/context.d.ts +35 -4
- package/dist/context.d.ts.map +1 -1
- package/dist/handle.d.ts +31 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/idempotency-response.d.ts +26 -0
- package/dist/idempotency-response.d.ts.map +1 -0
- package/dist/idempotency.d.ts +40 -8
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -61
- package/dist/index.js.map +10 -7
- package/dist/store.d.ts +30 -4
- package/dist/store.d.ts.map +1 -1
- package/docs/README.md +10 -2
- package/docs/guide/caches-and-locks.md +22 -8
- package/docs/guide/connecting.md +136 -6
- package/docs/guide/idempotency.md +49 -26
- package/docs/guide/rate-limits.md +10 -4
- package/docs/guide/response-cache.md +3 -3
- package/docs/guide/testing.md +11 -14
- package/docs/roadmap.md +48 -5
- package/docs/troubleshooting.md +157 -10
- package/package.json +10 -13
package/docs/roadmap.md
CHANGED
|
@@ -7,11 +7,16 @@ number on it. Every release, with each change it made, is in
|
|
|
7
7
|
|
|
8
8
|
## Now
|
|
9
9
|
|
|
10
|
-
Nothing
|
|
10
|
+
Nothing in progress: `idempotency` as a middleware, the last change planned,
|
|
11
|
+
shipped in 0.2.0 and finished in 0.3.0.
|
|
11
12
|
|
|
12
13
|
## Next
|
|
13
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
20
|
|
|
16
21
|
## Later
|
|
17
22
|
|
|
@@ -20,14 +25,52 @@ Nothing scheduled yet.
|
|
|
20
25
|
## Not planned
|
|
21
26
|
|
|
22
27
|
- **A second Redis implementation.** `@alxia/redis` is an adapter over
|
|
23
|
-
[`@nxgt/redis`](https://www.npmjs.com/package/@nxgt/redis)
|
|
24
|
-
|
|
25
|
-
never a rewrite of them: their scripts, keys and errors are what it runs.
|
|
28
|
+
[`@nxgt/redis`](https://www.npmjs.com/package/@nxgt/redis),
|
|
29
|
+
never a rewrite of it: its scripts, keys and errors are what it runs.
|
|
26
30
|
They run on Bun's built-in `RedisClient`, so there is no driver to
|
|
27
31
|
install, and it does not run on Node.
|
|
28
32
|
|
|
29
33
|
## Shipped
|
|
30
34
|
|
|
35
|
+
### 0.3.0, continued: wired guards
|
|
36
|
+
|
|
37
|
+
- **On `@nxgt/redis` 0.6.** The peer range is `^0.5.0 || ^0.6.0`: the new forms
|
|
38
|
+
need no more than 0.5's types, and 0.6 only adds `handle.limits` and
|
|
39
|
+
`handle.idempotency` to wire them.
|
|
40
|
+
- **A rate limit and an idempotency defined once.** `redisStore(handle.limits.api)`
|
|
41
|
+
and `idempotency(handle.idempotency.orders)` take what `defineRedis`
|
|
42
|
+
wired, so the definition lives in one place and writes the keys
|
|
43
|
+
`@nxgt/redis` writes, `<prefix>:<name>:<key>`: every consumer of the handle
|
|
44
|
+
shares the count. `idempotencyResult` is the schema of the wired idempotency.
|
|
45
|
+
Both older forms stay.
|
|
46
|
+
|
|
47
|
+
### 0.3.0
|
|
48
|
+
|
|
49
|
+
- **On `@nxgt/redis` 0.5 alone.** The rate limits and the idempotency that
|
|
50
|
+
`@nxgt/redis-guard` held live in `@nxgt/redis` now, and `@nxgt/redis-guard`
|
|
51
|
+
is no longer a peer.
|
|
52
|
+
- **One form for `idempotency`.** The middleware's type is a plain
|
|
53
|
+
`(ctx, next)` function, and `app.plugin(idempotency(…))`, deprecated in
|
|
54
|
+
0.2.0, is gone: give it to `use(…)`.
|
|
55
|
+
- **An `@nxgt/redis` handle everywhere.** `redis(handle)` takes the handle
|
|
56
|
+
`openRedis(defineRedis({ … }))` gives: typed `caches` from its scopes, a
|
|
57
|
+
`lock` and every key under its `prefix`, and the handle closed once in
|
|
58
|
+
`onStop`, after the drain (`{ close: false }` to opt out). `redisStore`,
|
|
59
|
+
`redisCacheStore` and `idempotency` take it where they take a client and
|
|
60
|
+
put its prefix in front of their keys, and `redisCheck` is a readiness
|
|
61
|
+
check for `health()`. The bare `RedisClient` forms are unchanged.
|
|
62
|
+
|
|
63
|
+
### 0.2.0
|
|
64
|
+
|
|
65
|
+
- **Middlewares, under the same names.** `app.use(idempotency(client, …))`
|
|
66
|
+
replaces `app.plugin(idempotency(…))`, which stayed, deprecated, until 0.3.0;
|
|
67
|
+
`IdempotencyMiddleware` is the type it returns. A request no route matches
|
|
68
|
+
passes through it, never kept.
|
|
69
|
+
- **A client no one can tell apart is not shared.** A request with no
|
|
70
|
+
`ctx.ip` and no `scope` runs unguarded, nothing stored or replayed, and
|
|
71
|
+
the middleware warns once, instead of keying every such client to
|
|
72
|
+
`anyone`, where one could be replayed another's response.
|
|
73
|
+
|
|
31
74
|
### 0.1.0
|
|
32
75
|
|
|
33
76
|
- **A rate limit every process shares.** `redisStore` is an
|
package/docs/troubleshooting.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Each entry is headed by the text you see: a TypeScript error, an exception
|
|
4
4
|
at startup, an exception in the log beside a `500 {"error":"internal"}`, or
|
|
5
|
-
the response a client got. `@alxia/redis` throws nothing of its own: the
|
|
6
|
-
messages are `@nxgt/redis`'s
|
|
7
|
-
each through
|
|
5
|
+
the response a client got. `@alxia/redis` throws almost nothing of its own: the
|
|
6
|
+
messages are `@nxgt/redis`'s and Bun's, and it lets
|
|
7
|
+
each through; its own refusals, a handle of several instances, a wired guard given a
|
|
8
|
+
`name`, `ttl` or `lease`, or a client given without a `name`, say what to pass instead. It prints one warning of its own, under
|
|
9
|
+
[Runtime: a warning in the log](#runtime-a-warning-in-the-log). What prints nothing is under [Traps](#traps), by symptom.
|
|
8
10
|
|
|
9
11
|
**Install and types**
|
|
10
12
|
|
|
@@ -21,6 +23,10 @@ each through. What prints nothing is under [Traps](#traps), by symptom.
|
|
|
21
23
|
- [`RedisError: Connection closed`](#rediserror-connection-closed)
|
|
22
24
|
- [`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)
|
|
23
25
|
|
|
26
|
+
- [`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)
|
|
27
|
+
- [`TypeError: defineRedis: instance "default" wires no cache, no channel, no rate limit and no idempotency. Pass the module that exports them, or drop the instance.`](#typeerror-defineredis-instance-default-wires-no-cache-no-channel-no-rate-limit-and-no-idempotency-pass-the-module-that-exports-them-or-drop-the-instance)
|
|
28
|
+
- [`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.`](#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)
|
|
29
|
+
|
|
24
30
|
**Runtime: a 500, with this in the log**
|
|
25
31
|
|
|
26
32
|
- [`TypeError: defineRateLimit: "…" has a burst of … and a per of …ms; burst × per must be at most 9007199254740 for the script to count exactly`](#typeerror-defineratelimit--has-a-burst-of--and-a-per-of-ms-burst--per-must-be-at-most-9007199254740-for-the-script-to-count-exactly)
|
|
@@ -32,6 +38,10 @@ each through. What prints nothing is under [Traps](#traps), by symptom.
|
|
|
32
38
|
- [`RedisError: The lock "…" expired before its work finished: it ran longer than the …ms ttl, so it may have run beside another holder`](#rediserror-the-lock--expired-before-its-work-finished-it-ran-longer-than-the-ms-ttl-so-it-may-have-run-beside-another-holder)
|
|
33
39
|
- [`TypeError: undefined is not an object (evaluating 'cache.…')`](#typeerror-undefined-is-not-an-object-evaluating-cache)
|
|
34
40
|
|
|
41
|
+
**Runtime: a warning in the log**
|
|
42
|
+
|
|
43
|
+
- [`idempotency "…": 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().`](#idempotency--no-client-scope-could-be-derived-ctxip-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)
|
|
44
|
+
|
|
35
45
|
**Calling the store yourself**
|
|
36
46
|
|
|
37
47
|
- [`TypeError: defineRateLimit: "…" has a per of …; it is a whole number of milliseconds, and must be at least 1`](#typeerror-defineratelimit--has-a-per-of--it-is-a-whole-number-of-milliseconds-and-must-be-at-least-1)
|
|
@@ -50,6 +60,9 @@ each through. What prints nothing is under [Traps](#traps), by symptom.
|
|
|
50
60
|
- [Two limits count each other's requests](#two-limits-count-each-others-requests)
|
|
51
61
|
- [A `401` or a `429` is replayed, with `Idempotent-Replayed: true`, after the client fixed it](#a-401-or-a-429-is-replayed-with-idempotent-replayed-true-after-the-client-fixed-it)
|
|
52
62
|
- [One client gets another client's response](#one-client-gets-another-clients-response)
|
|
63
|
+
- [Counts and kept responses vanish after moving to a handle with a `prefix`](#counts-and-kept-responses-vanish-after-moving-to-a-handle-with-a-prefix)
|
|
64
|
+
- [Counts restart after moving a rate limit to the wired form](#counts-restart-after-moving-a-rate-limit-to-the-wired-form)
|
|
65
|
+
- [The handle is closed while something still uses it](#the-handle-is-closed-while-something-still-uses-it)
|
|
53
66
|
|
|
54
67
|
## Install and types
|
|
55
68
|
|
|
@@ -106,7 +119,7 @@ never meet `@alxia/cache`'s `cache`.
|
|
|
106
119
|
|
|
107
120
|
```ts
|
|
108
121
|
alxia()
|
|
109
|
-
.
|
|
122
|
+
.plugin(redis(connection.client, { caches: { users } }))
|
|
110
123
|
.get('/users/:id', async ({ caches, params, reply }) =>
|
|
111
124
|
reply.ok(await caches.users.remember(params.id, () => loadUser(params.id))),
|
|
112
125
|
);
|
|
@@ -217,6 +230,59 @@ await check.close();
|
|
|
217
230
|
const connection = await connectRedis(Bun.env['REDIS_URL']!);
|
|
218
231
|
```
|
|
219
232
|
|
|
233
|
+
### `TypeError: @alxia/redis: this @nxgt/redis handle wires N Redis instances (…), and one is needed.`
|
|
234
|
+
|
|
235
|
+
**When:** a handle wiring several instances is given to `redis()`,
|
|
236
|
+
`redisStore`, `redisCacheStore`, `idempotency` or `redisCheck`, at startup.
|
|
237
|
+
|
|
238
|
+
**Why:** the keys of one deployment live on one Redis; which of several is
|
|
239
|
+
meant is not guessed.
|
|
240
|
+
|
|
241
|
+
**Fix:** give the bare client of the one, with the prefix in the `name`:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
redisStore(handle.clients.cache, { name: 'shop:api' });
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
or wire one instance per handle.
|
|
248
|
+
|
|
249
|
+
### `TypeError: defineRedis: instance "default" wires no cache, no channel, no rate limit and no idempotency. Pass the module that exports them, or drop the instance.`
|
|
250
|
+
|
|
251
|
+
**When:** `defineRedis({ uri, prefix })` is written only to give the stores
|
|
252
|
+
and `idempotency` a prefix. `@nxgt/redis` 0.5 said `wires no cache and no
|
|
253
|
+
channel`; 0.6 counts the rate limits and the idempotency it can now wire too.
|
|
254
|
+
|
|
255
|
+
**Why:** `@nxgt/redis` refuses a handle that wires nothing.
|
|
256
|
+
|
|
257
|
+
**Fix:** wire at least one of the four on it: a cache, which `redis(handle)`
|
|
258
|
+
then puts in the context as `caches`, a channel, a rate limit or an idempotency:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
defineRedis({ uri, prefix: 'shop', caches: { users } });
|
|
262
|
+
defineRedis({ uri, prefix: 'shop', limits: { api } });
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### `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
|
+
|
|
267
|
+
**When:** the `defineRedis(...)` of an app that gives `caches`, `limits` and
|
|
268
|
+
`idempotency` one `name` on one instance: here the cache exported as `users`
|
|
269
|
+
and the rate limit exported as `login`, both named `user`. The sentence says
|
|
270
|
+
which two, and the export keys they are wired under. It is thrown at wiring
|
|
271
|
+
time, before the app serves; with the same kind twice it reads `wires the rate
|
|
272
|
+
limit named "user" twice, under "a" and "b"`.
|
|
273
|
+
|
|
274
|
+
**Why:** a cache, a rate limit and an idempotency all write
|
|
275
|
+
`<prefix>:<name>:<key>`, so one name is one keyspace, and they would
|
|
276
|
+
overwrite each other's values or meet as `WRONGTYPE`. The same wiring that lets
|
|
277
|
+
`redisStore(handle.limits.login)` and `idempotency(handle.idempotency.orders)`
|
|
278
|
+
write the layout `@nxgt/redis` writes refuses the clash.
|
|
279
|
+
|
|
280
|
+
**Fix:** rename one: the **definition's** `name`, not its export:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
export const login = defineRateLimit({ name: 'login.limit', key: (ip: string) => ip, limit: 5, per: 60_000 });
|
|
284
|
+
```
|
|
285
|
+
|
|
220
286
|
## Runtime: a 500, with this in the log
|
|
221
287
|
|
|
222
288
|
Each of these makes the request answer `500 {"error":"internal"}`; the
|
|
@@ -234,7 +300,7 @@ TypeError: defineRateLimit: "api:1000000/31536000000" has a burst of 1000000 and
|
|
|
234
300
|
|
|
235
301
|
**Why:** the script counts in exact integers; `limit × windowMs` past that
|
|
236
302
|
bound would lose precision. `redisStore` hands each policy to
|
|
237
|
-
`@nxgt/redis
|
|
303
|
+
`@nxgt/redis` when it first counts under it, and a refused policy is
|
|
238
304
|
not kept, so it is checked again, and refused again, on each request.
|
|
239
305
|
|
|
240
306
|
**Fix:** state the same rate over a shorter window:
|
|
@@ -277,7 +343,7 @@ with a fraction or below 0.
|
|
|
277
343
|
TypeError: run on "payments": wait is a whole number of milliseconds, 0 or more
|
|
278
344
|
```
|
|
279
345
|
|
|
280
|
-
**Why:** `wait` is checked when it is used, not when the
|
|
346
|
+
**Why:** `wait` is checked when it is used, not when the middleware is made.
|
|
281
347
|
|
|
282
348
|
**Fix:**
|
|
283
349
|
|
|
@@ -398,6 +464,38 @@ with `undefined`.
|
|
|
398
464
|
**Fix:** read `caches.<name>`, drop the `derive`, and typecheck:
|
|
399
465
|
`tsc --noEmit` finds every place.
|
|
400
466
|
|
|
467
|
+
## Runtime: a warning in the log
|
|
468
|
+
|
|
469
|
+
### `idempotency "…": 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().`
|
|
470
|
+
|
|
471
|
+
**When:** a guarded request carries an `Idempotency-Key`, and the
|
|
472
|
+
middleware has no client to scope it by: `ctx.ip` is `undefined` — under
|
|
473
|
+
`app.request()` in a test, or a server that cannot see the address — and
|
|
474
|
+
there is no `scope` option, or it returned `undefined`. Printed once per
|
|
475
|
+
`idempotency(…)`, with its `name`.
|
|
476
|
+
|
|
477
|
+
**Why:** keys are scoped by the client, so two clients choosing the same key
|
|
478
|
+
never see each other's response. With no client, the request runs
|
|
479
|
+
unguarded: the route runs, nothing is stored, a repeat runs it again.
|
|
480
|
+
Sharing one key space between every client would replay one client's
|
|
481
|
+
response to another.
|
|
482
|
+
|
|
483
|
+
**Fix:** scope by the user where there is one, or give `alxia()` an `ip`
|
|
484
|
+
option that reads the address:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
idempotency(connection.client, {
|
|
488
|
+
name: 'payments',
|
|
489
|
+
scope: ({ request }) => request.headers.get('x-user-id') ?? undefined,
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
or, behind a proxy you trust:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
alxia({ ip: (request) => request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() });
|
|
497
|
+
```
|
|
498
|
+
|
|
401
499
|
## Calling the store yourself
|
|
402
500
|
|
|
403
501
|
A policy `rateLimit` would refuse at startup still reaches the store when
|
|
@@ -414,7 +512,7 @@ number, or below 1.
|
|
|
414
512
|
TypeError: defineRateLimit: "api:5/1.5" has a per of 1.5; it is a whole number of milliseconds, and must be at least 1
|
|
415
513
|
```
|
|
416
514
|
|
|
417
|
-
**Why:** `redisStore` hands each `limit`/`windowMs` to `@nxgt/redis
|
|
515
|
+
**Why:** `redisStore` hands each `limit`/`windowMs` to `@nxgt/redis`
|
|
418
516
|
the first time it counts under it, and the guard counts in whole
|
|
419
517
|
milliseconds. `rateLimit` never passes such a value: it refuses it at
|
|
420
518
|
startup, with [`TypeError: rateLimit: windowMs must be a whole number of 1 or more, not …`](https://github.com/softistx/alxia/blob/develop/packages/rate-limit/docs/troubleshooting.md#typeerror-ratelimit--must-be-a-whole-number-of-1-or-more-not-).
|
|
@@ -567,7 +665,8 @@ rateLimit({ limit: 5, windowMs: 60_000, store: redisStore(connection.client, { n
|
|
|
567
665
|
### A `401` or a `429` is replayed, with `Idempotent-Replayed: true`, after the client fixed it
|
|
568
666
|
|
|
569
667
|
**When:** a request under a key was refused with a `4xx` — by the route, or
|
|
570
|
-
by a rate limit or an authentication check declared after `idempotency
|
|
668
|
+
by a rate limit or an authentication check declared after `idempotency`,
|
|
669
|
+
or by an error handler —
|
|
571
670
|
and the client, once allowed, retries with the same key and body.
|
|
572
671
|
|
|
573
672
|
**Why:** every response below `500` is kept and replayed for `ttl`, not
|
|
@@ -596,8 +695,9 @@ behind a proxy, or with keys that are not random — `1`, `order-1`.
|
|
|
596
695
|
|
|
597
696
|
**Why:** keys are scoped by `scope(ctx)`, the client's address by default.
|
|
598
697
|
Behind a proxy that the app's `ip` option does not see through, every
|
|
599
|
-
client has the proxy's address
|
|
600
|
-
`
|
|
698
|
+
client has the proxy's address, and they then share one key space. (With
|
|
699
|
+
no address at all and no `scope`, the request runs unguarded instead, with
|
|
700
|
+
a [warning](#runtime-a-warning-in-the-log).)
|
|
601
701
|
|
|
602
702
|
**Fix:** scope by the user where there is one, and have clients send
|
|
603
703
|
random keys:
|
|
@@ -611,3 +711,50 @@ idempotency(connection.client, {
|
|
|
611
711
|
|
|
612
712
|
[Scope](guide/idempotency.md#scope-whose-key-it-is) shows the `ip` option
|
|
613
713
|
behind a proxy.
|
|
714
|
+
|
|
715
|
+
### Counts and kept responses vanish after moving to a handle with a `prefix`
|
|
716
|
+
|
|
717
|
+
**Symptom:** after `redisStore(client, …)` becomes `redisStore(handle, …)`,
|
|
718
|
+
rate-limit counts start from zero, kept responses miss and an idempotent
|
|
719
|
+
repeat runs again.
|
|
720
|
+
|
|
721
|
+
**Why:** the handle's `prefix` is in front of every key, so the keys are
|
|
722
|
+
new: `api:…` is now `shop:api:…`. The old ones expire on their own.
|
|
723
|
+
|
|
724
|
+
**Fix:** none is needed beyond waiting for the longest `ttl`; switch during a
|
|
725
|
+
quiet period, or keep the bare client until then.
|
|
726
|
+
|
|
727
|
+
### Counts restart after moving a rate limit to the wired form
|
|
728
|
+
|
|
729
|
+
**Symptom:** after `redisStore(handle, { name: 'api' })` becomes
|
|
730
|
+
`redisStore(handle.limits.api)`, every client's allowance starts full again.
|
|
731
|
+
An idempotent route does not do this: `idempotency(handle, { name: 'orders' })`
|
|
732
|
+
and `idempotency(handle.idempotency.orders)` write the same keys, so a kept
|
|
733
|
+
response is still replayed.
|
|
734
|
+
|
|
735
|
+
**Why:** the two layouts differ. The by-name store keeps one bucket per policy,
|
|
736
|
+
`shop:api:<limit>/<windowMs>:<key>` and `shop:api:policies`; the wired limit
|
|
737
|
+
is `@nxgt/redis`'s own, `shop:api:<key>`, the layout every other consumer of
|
|
738
|
+
the handle shares. The old buckets are not read any more and expire by
|
|
739
|
+
themselves.
|
|
740
|
+
|
|
741
|
+
**Fix:** none is needed but the wait, within `burst × per ÷ limit` of the
|
|
742
|
+
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.
|
|
746
|
+
|
|
747
|
+
### The handle is closed while something still uses it
|
|
748
|
+
|
|
749
|
+
**Symptom:** after the app stopped, or in a second app sharing the handle,
|
|
750
|
+
commands fail with `RedisError: Connection closed`.
|
|
751
|
+
|
|
752
|
+
**Why:** `redis(handle)` closes the handle when its app stops. Two apps
|
|
753
|
+
given one handle close it with the first.
|
|
754
|
+
|
|
755
|
+
**Fix:** pass `{ close: false }` to every `redis(handle, …)` but the one that
|
|
756
|
+
owns the handle's life, or close it yourself:
|
|
757
|
+
|
|
758
|
+
```ts
|
|
759
|
+
app.plugin(redis(handle, { close: false }));
|
|
760
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/redis",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Redis for alxia on @nxgt/redis
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -41,21 +41,18 @@
|
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@alxia/cache": "^0.
|
|
45
|
-
"@alxia/
|
|
46
|
-
"@alxia/
|
|
47
|
-
"@
|
|
48
|
-
"@nxgt/redis": "^0.3.1",
|
|
49
|
-
"@nxgt/redis-guard": "^0.3.1",
|
|
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",
|
|
50
48
|
"@types/bun": "^1.4.2",
|
|
51
49
|
"zod": "^4.6.5"
|
|
52
50
|
},
|
|
53
51
|
"peerDependencies": {
|
|
54
|
-
"@alxia/cache": "^0.
|
|
55
|
-
"@alxia/core": "^0.
|
|
56
|
-
"@alxia/rate-limit": "^0.
|
|
57
|
-
"@nxgt/redis": "^0.
|
|
58
|
-
"@nxgt/redis-guard": "^0.3.1",
|
|
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",
|
|
59
56
|
"typescript": "^6.0.3 || ^7.0.0",
|
|
60
57
|
"zod": "^4.6.5"
|
|
61
58
|
},
|