@owlmeans/redis 0.1.18-rc.4 → 0.1.18-rc.41
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 +26 -8
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/redis/SKILL.md +83 -9
- package/build/service.d.ts +1 -5
- package/build/service.d.ts.map +1 -1
- package/build/service.js +11 -17
- package/build/service.js.map +1 -1
- package/build/types.local.d.ts +19 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/cluster.d.ts.map +1 -1
- package/build/utils/cluster.js +3 -1
- package/build/utils/cluster.js.map +1 -1
- package/build/utils/config.d.ts.map +1 -1
- package/build/utils/config.js +7 -0
- package/build/utils/config.js.map +1 -1
- package/package.json +12 -11
- package/src/service.ts +16 -27
- package/src/types.local.ts +21 -0
- package/src/utils/cluster.ts +6 -8
- package/src/utils/config.ts +7 -0
- package/tests/context.ts +29 -9
- package/tests/query.spec.ts +111 -0
- package/tests/resource.spec.ts +23 -11
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Redis service for OwlMeans server contexts — connection management with cluste
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
bun add @owlmeans/redis
|
|
15
|
+
bun add @owlmeans/redis@^0.1.18-rc.41
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
@@ -24,16 +24,20 @@ import { appendRedis, DEFAULT_ALIAS } from '@owlmeans/redis'
|
|
|
24
24
|
appendRedis<C, T>(context)
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Config (`config.json`)
|
|
27
|
+
Config (`config.json`) — `dbs` is a list, and `schema` becomes the key prefix every resource on
|
|
28
|
+
this connection namespaces itself under:
|
|
28
29
|
|
|
29
30
|
```json
|
|
30
31
|
{
|
|
31
|
-
"dbs":
|
|
32
|
-
|
|
32
|
+
"dbs": [
|
|
33
|
+
{
|
|
34
|
+
"service": "redis",
|
|
35
|
+
"alias": "redis",
|
|
33
36
|
"host": "localhost",
|
|
34
|
-
"port": 6379
|
|
37
|
+
"port": 6379,
|
|
38
|
+
"schema": "app"
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
]
|
|
37
41
|
}
|
|
38
42
|
```
|
|
39
43
|
|
|
@@ -49,7 +53,21 @@ Registers the Redis service in the context.
|
|
|
49
53
|
|
|
50
54
|
### `RedisMeta`
|
|
51
55
|
|
|
52
|
-
Extends `RedisOptions` (ioredis) — all ioredis connection options are supported in config.
|
|
56
|
+
Extends `RedisOptions` (ioredis) — all ioredis connection options are supported in config. Set
|
|
57
|
+
the database index with `dbIndex` (it accepts a string, so it can come from a file-mounted config
|
|
58
|
+
value) rather than ioredis' own `db`.
|
|
59
|
+
|
|
60
|
+
## Server Requirements
|
|
61
|
+
|
|
62
|
+
- Redis **6.2 or newer** — `@owlmeans/redis-resource` deletes with `GETDEL`.
|
|
63
|
+
- `notify-keyspace-events` enabled if any resource uses `watch`; keyspace events are emitted on
|
|
64
|
+
**db 0**, so deployments sharing an instance isolate on the key prefix, not on `dbIndex`.
|
|
65
|
+
|
|
66
|
+
## Tests
|
|
67
|
+
|
|
68
|
+
`tests/` holds the integration specs for the `@owlmeans/redis-resource` contract — this is the
|
|
69
|
+
package that supplies the connection it runs against. Gated on `REDIS_URL` (see `/.env.example`):
|
|
70
|
+
`bun test ./tests`.
|
|
53
71
|
|
|
54
72
|
## Related Packages
|
|
55
73
|
|
|
@@ -64,7 +82,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
64
82
|
your project's skill store (`.agents/skills/`):
|
|
65
83
|
|
|
66
84
|
```sh
|
|
67
|
-
npx @owlmeans/agent-skills
|
|
85
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.48
|
|
68
86
|
```
|
|
69
87
|
|
|
70
88
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/redis",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.41",
|
|
5
|
+
"generatedAt": "2026-10-05T08:32:19.136Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: redis
|
|
3
|
-
description: How to use @owlmeans/redis — Redis
|
|
3
|
+
description: How to use @owlmeans/redis — the Redis connection service (makeRedisService / appendRedis) registered on a server context, its cfg.dbs configuration, the options() seam for consumers that need a connection of their own, and the cluster caveat. Auto-invoked when wiring Redis into a server app.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,26 +8,100 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/redis
|
|
9
9
|
|
|
10
10
|
**Layer:** Infra
|
|
11
|
-
**Install:** `"@owlmeans/redis": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/redis": "^0.1.18-rc.41"` in `dependencies`
|
|
12
|
+
|
|
13
|
+
The connection half. The `Resource` contract over those connections is `@owlmeans/redis-resource`;
|
|
14
|
+
queues on them are `@owlmeans/redis-queue`.
|
|
12
15
|
|
|
13
16
|
## Key Exports
|
|
14
17
|
|
|
15
18
|
| Export | Description |
|
|
16
19
|
|--------|-------------|
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
|
|
|
20
|
+
| `appendRedis(context, alias?)` | Register the service — the usual wiring |
|
|
21
|
+
| `makeRedisService(alias?)` | The bare factory, when registering it yourself |
|
|
22
|
+
| `RedisMeta` | What `cfg.dbs[].meta` accepts here: `dbIndex`, `masterNumber`, `slaveNumber`, plus any ioredis option |
|
|
23
|
+
| Constants | `DEFAULT_ALIAS` (`redis`, shared with `@owlmeans/redis-resource`) |
|
|
24
|
+
|
|
25
|
+
The service interface itself (`RedisDbService`, `RedisClient`, `RedisDb`, `RedisConnection`) is
|
|
26
|
+
declared in `@owlmeans/redis-resource`, so a consumer types against the contract package rather
|
|
27
|
+
than against the driver.
|
|
20
28
|
|
|
21
29
|
## Usage
|
|
22
30
|
|
|
23
31
|
```typescript
|
|
24
|
-
import {
|
|
25
|
-
context.registerService(makeRedisService())
|
|
32
|
+
import { appendRedis } from '@owlmeans/redis'
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
appendRedis(context)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const redis = context.service<RedisDbService>(DEFAULT_ALIAS)
|
|
39
|
+
await redis.ready()
|
|
40
|
+
const client = await redis.client() // the pooled connection
|
|
41
|
+
const { single, cluster, prefix } = redis.options()
|
|
28
42
|
```
|
|
29
43
|
|
|
44
|
+
Connections are configured through `cfg.dbs` — a LIST of `DbConfig` entries, each naming the
|
|
45
|
+
`service` that owns it and the `alias` it answers to. Every entry for this service is resolved when
|
|
46
|
+
the service initializes, and its client is closed on SIGTERM.
|
|
47
|
+
|
|
48
|
+
| Field | Meaning |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `host` | A string for a single server; an ARRAY makes it a cluster (see below) |
|
|
51
|
+
| `port` | Defaults to 6379 |
|
|
52
|
+
| `secret` | The password |
|
|
53
|
+
| `user` | Sent as ioredis' `username` **only when set** — a bare `requirepass` server rejects AUTH with a username, so leave it absent unless the server has ACL users |
|
|
54
|
+
| `schema` | The key prefix, falling back to the entry's `alias` and then the service's. Every resource on the connection namespaces itself `<prefix>-<resource name>:<id>` |
|
|
55
|
+
| `meta.dbIndex` | The database index (`SELECT n`). Accepts a string, so it can come from a file-mounted config value; never use ioredis' own `db` |
|
|
56
|
+
|
|
57
|
+
**A `dbIndex` other than 0 turns `watch` off.** Redis publishes keyspace events per database, as
|
|
58
|
+
`__keyspace@<db>__:<key>`, and `@owlmeans/redis-resource` subscribes to `__keyspace@0__:` — so on
|
|
59
|
+
any other index a resource's `watch` handler silently never fires. Deployments sharing one instance
|
|
60
|
+
isolate on the key prefix (`schema`), and leave `dbIndex` alone wherever anything watches.
|
|
61
|
+
|
|
62
|
+
## `options()` versus `client()`
|
|
63
|
+
|
|
64
|
+
`options(alias)` hands out the settings rather than a client, because a consumer that BLOCKS on a
|
|
65
|
+
read holds its connection for the duration and so cannot share the pooled one. Anything doing that
|
|
66
|
+
— BullMQ's workers and event streams, for instance — builds its own client from these settings,
|
|
67
|
+
which keeps the configuration here instead of being re-derived from `cfg.dbs` and drifting. It
|
|
68
|
+
answers `{ single, prefix }` for one host and `{ cluster, prefix }` for several; exactly one of the
|
|
69
|
+
two is present, which is how a consumer that cannot work on a cluster refuses at connection time.
|
|
70
|
+
|
|
71
|
+
## Server Requirements
|
|
72
|
+
|
|
73
|
+
- Redis **6.2 or newer** — `@owlmeans/redis-resource` deletes with `GETDEL`, which is the whole
|
|
74
|
+
reason `delete`/`take` hand back the record they removed without a preceding read.
|
|
75
|
+
- `notify-keyspace-events` enabled if any resource uses `watch` — redis publishes nothing
|
|
76
|
+
otherwise.
|
|
77
|
+
- **A single server for anything answering criteria.** A resource answers `list`/`count`/`purge`
|
|
78
|
+
and `load(where)` by walking its own prefix with `SCAN`, which reaches one server — so in a
|
|
79
|
+
clustered deployment those calls see a fraction of the keyspace. Keep clustered resources on the
|
|
80
|
+
by-id operations, or put the data in mongo or postgres. Queues refuse a cluster outright.
|
|
81
|
+
|
|
82
|
+
## A multi-host entry OWNS its cluster
|
|
83
|
+
|
|
84
|
+
A `dbs[]` entry whose `host` is an array of more than one does not merely connect: it asserts the
|
|
85
|
+
topology. It MEETs the nodes it was given, FORGETs the ones it was not, and where the layout does
|
|
86
|
+
not match `meta.masterNumber` / `meta.slaveNumber` it resets nodes and reassigns slot ranges —
|
|
87
|
+
which flushes them. Point such an entry only at a cluster this service is meant to own; for a
|
|
88
|
+
cluster managed elsewhere, or for a single server, give `host` one string. (A one-element array
|
|
89
|
+
collapses to a single connection and is safe.)
|
|
90
|
+
|
|
91
|
+
## Tests
|
|
92
|
+
|
|
93
|
+
This package's `tests/` hold the integration specs for the `@owlmeans/redis-resource` contract —
|
|
94
|
+
it supplies the connection they run against. Gated on `REDIS_URL` (see `/.env.example`); run them
|
|
95
|
+
with `bun test ./tests`.
|
|
96
|
+
|
|
30
97
|
## Depends On
|
|
31
98
|
|
|
32
|
-
- `@owlmeans/
|
|
99
|
+
- `@owlmeans/redis-resource` — the service and resource contracts, and `DEFAULT_DB_ALIAS`
|
|
100
|
+
- `@owlmeans/resource` — `createDbService`, `DbConfig`
|
|
101
|
+
- `@owlmeans/server-context` — the config and context this service binds to
|
|
33
102
|
- `ioredis` (runtime)
|
|
103
|
+
|
|
104
|
+
## Related
|
|
105
|
+
|
|
106
|
+
- `redis-resource` — records, TTL, pub/sub, keyspace watching and streams over these connections
|
|
107
|
+
- `redis-queue` — BullMQ queues over them
|
package/build/service.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { RedisDbService } from '@owlmeans/redis-resource';
|
|
2
|
-
import type {
|
|
3
|
-
type Config = ServerConfig;
|
|
4
|
-
interface Context<C extends Config = Config> extends ServerContext<C> {
|
|
5
|
-
}
|
|
2
|
+
import type { Config, Context } from './types.local.js';
|
|
6
3
|
export declare const makeRedisService: (alias?: string) => RedisDbService;
|
|
7
4
|
export declare const appendRedis: <C extends Config, T extends Context<C> = Context<C>>(context: T, alias?: string) => T;
|
|
8
|
-
export {};
|
|
9
5
|
//# sourceMappingURL=service.d.ts.map
|
package/build/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAwB,MAAM,0BAA0B,CAAA;AAKpF,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAwB,MAAM,0BAA0B,CAAA;AAKpF,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAGvD,eAAO,MAAM,gBAAgB,WAAW,MAAM,KAAmB,cAgEhE,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,WACpE,CAAC,UAAS,MAAM,KACxB,CAMF,CAAA"}
|
package/build/service.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_ALIAS } from './consts.js';
|
|
2
2
|
import { createDbService } from '@owlmeans/resource';
|
|
3
|
-
import { assertContext
|
|
4
|
-
import { createClient } from './utils/index.js';
|
|
3
|
+
import { assertContext } from '@owlmeans/context';
|
|
4
|
+
import { createClient, prepareClusterRedisOptions, prepareSingleRedisOptions } from './utils/index.js';
|
|
5
5
|
export const makeRedisService = (alias = DEFAULT_ALIAS) => {
|
|
6
6
|
const location = `redis:${alias}`;
|
|
7
7
|
const service = createDbService(alias, {
|
|
@@ -14,21 +14,21 @@ export const makeRedisService = (alias = DEFAULT_ALIAS) => {
|
|
|
14
14
|
*/
|
|
15
15
|
return { client: client.duplicate(), prefix: name };
|
|
16
16
|
},
|
|
17
|
+
options: configAlias => {
|
|
18
|
+
configAlias = service.ensureConfigAlias(configAlias);
|
|
19
|
+
const config = service.config(configAlias);
|
|
20
|
+
const prefix = service.name(configAlias);
|
|
21
|
+
const hosts = Array.isArray(config.host) ? config.host : [config.host];
|
|
22
|
+
return hosts.length > 1
|
|
23
|
+
? { cluster: prepareClusterRedisOptions(config), prefix }
|
|
24
|
+
: { single: prepareSingleRedisOptions(config, hosts[0]), prefix };
|
|
25
|
+
},
|
|
17
26
|
initialize: async (configAlias) => {
|
|
18
27
|
configAlias = service.ensureConfigAlias(configAlias);
|
|
19
28
|
const config = service.config(configAlias);
|
|
20
29
|
if (service.clients[configAlias] != null) {
|
|
21
30
|
return;
|
|
22
31
|
}
|
|
23
|
-
if (service.layers == null) {
|
|
24
|
-
service.layers = [Layer.Global];
|
|
25
|
-
}
|
|
26
|
-
if (config.serviceSensitive && service.layers.includes(Layer.Service)) {
|
|
27
|
-
service.layers.push(Layer.Service);
|
|
28
|
-
}
|
|
29
|
-
if (config.entitySensitive && service.layers.includes(Layer.Entity)) {
|
|
30
|
-
service.layers.push(Layer.Entity);
|
|
31
|
-
}
|
|
32
32
|
let client = await createClient(config);
|
|
33
33
|
// we need to check all hosts for replication consistancy
|
|
34
34
|
if (service.clients[configAlias] != null) {
|
|
@@ -38,12 +38,6 @@ export const makeRedisService = (alias = DEFAULT_ALIAS) => {
|
|
|
38
38
|
client.quit();
|
|
39
39
|
});
|
|
40
40
|
service.clients[configAlias] = client;
|
|
41
|
-
},
|
|
42
|
-
reinitializeContext: (context) => {
|
|
43
|
-
const _service = makeRedisService(alias);
|
|
44
|
-
_service.ctx = context;
|
|
45
|
-
_service.layers = service.layers;
|
|
46
|
-
return _service;
|
|
47
41
|
}
|
|
48
42
|
}, service => async () => {
|
|
49
43
|
const context = assertContext(service.ctx, location);
|
package/build/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAItG,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAK,GAAW,aAAa,EAAkB,EAAE;IAChF,MAAM,QAAQ,GAAG,SAAS,KAAK,EAAE,CAAA;IAEjC,MAAM,OAAO,GAAmB,eAAe,CAC7C,KAAK,EAAE;QACP,EAAE,EAAE,KAAK,EAAC,WAAW,EAAC,EAAE;YACtB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;YAEhD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAE5C;;;eAGG;YACH,OAAO,EAAE,MAAM,EAAG,MAA2C,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;QAC3F,CAAC;QAED,OAAO,EAAE,WAAW,CAAC,EAAE;YACrB,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAA;YACpD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;YAC1C,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAExC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YAEtE,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC;gBACrB,CAAC,CAAC,EAAE,OAAO,EAAE,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE;gBACzD,CAAC,CAAC,EAAE,MAAM,EAAE,yBAAyB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAA;QACrE,CAAC;QAED,UAAU,EAAE,KAAK,EAAC,WAAW,EAAC,EAAE;YAC9B,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAA;YACpD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;YAE1C,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,IAAI,EAAE,CAAC;gBACzC,OAAM;YACR,CAAC;YAED,IAAI,MAAM,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;YAEvC,yDAAyD;YAEzD,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,IAAI,EAAE,CAAC;gBACzC,MAAM,IAAI,WAAW,CAAC,yCAAyC,WAAW,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,CAAA;YAClG,CAAC;YAED,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;gBACzB,MAAM,CAAC,IAAI,EAAE,CAAA;YACf,CAAC,CAAC,CAAA;YAEF,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,MAAM,CAAA;QACvC,CAAC;KACF,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE;QACvB,MAAM,OAAO,GAAG,aAAa,CAAkB,OAAO,CAAC,GAAc,EAAE,QAAQ,CAAC,CAAA;QAEhF,oCAAoC;QACpC,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;YACpG,MAAM,IAAI,CAAA;YACV,MAAM,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QACtC,CAAC,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QAErB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAA;IAC5B,CAAC,CAAC,CAAA;IAEF,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,OAAU,EAAE,KAAK,GAAW,aAAa,EACtC,EAAE;IACL,MAAM,OAAO,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAA;IAEvC,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAA;IAEhC,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
import type { ServerContext, ServerConfig } from '@owlmeans/server-context';
|
|
3
|
+
export type Config = ServerConfig;
|
|
4
|
+
export interface Context<C extends Config = Config> extends ServerContext<C> {
|
|
5
|
+
}
|
|
6
|
+
/** One line of `CLUSTER NODES`, parsed. */
|
|
7
|
+
export interface NodeInfo {
|
|
8
|
+
nodeId: string;
|
|
9
|
+
addr: string;
|
|
10
|
+
flags: string[];
|
|
11
|
+
master: string;
|
|
12
|
+
state: string;
|
|
13
|
+
slots: number[][] | null;
|
|
14
|
+
}
|
|
15
|
+
export interface ClusterNode {
|
|
16
|
+
node: Redis;
|
|
17
|
+
info: NodeInfo;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=types.local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.local.d.ts","sourceRoot":"","sources":["../src/types.local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE3E,MAAM,MAAM,MAAM,GAAG,YAAY,CAAA;AAEjC,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CAAI;AAEhF,2CAA2C;AAC3C,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,CAAA;IACX,IAAI,EAAE,QAAQ,CAAA;CACf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.local.js","sourceRoot":"","sources":["../src/types.local.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/utils/cluster.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/utils/cluster.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAIjC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAK5C,eAAO,MAAM,aAAa,WAAkB,QAAQ,CAAC,SAAS,CAAC,KAAG,OAAO,CAAC,OAAO,CAuLhF,CAAA"}
|
package/build/utils/cluster.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { logger } from '@owlmeans/log';
|
|
1
2
|
import { Cluster } from 'ioredis';
|
|
2
3
|
import { prepareClusterRedisOptions } from './config.js';
|
|
3
4
|
import { createClient } from './instance.js';
|
|
5
|
+
const log = logger('redis:cluster');
|
|
4
6
|
export const ensuerCluster = async (config) => {
|
|
5
7
|
if (!Array.isArray(config.host)) {
|
|
6
8
|
throw new SyntaxError('We may connect to redis cluster only knowing its nodes');
|
|
@@ -161,7 +163,7 @@ export const ensuerCluster = async (config) => {
|
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
catch (e) {
|
|
164
|
-
|
|
166
|
+
log.error('Redis cluster configuration failed', e);
|
|
165
167
|
throw e;
|
|
166
168
|
}
|
|
167
169
|
// 3. Disconnect single clients
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cluster.js","sourceRoot":"","sources":["../../src/utils/cluster.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cluster.js","sourceRoot":"","sources":["../../src/utils/cluster.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAoB5C,eAAO,MAAM,yBAAyB,WAAY,QAAQ,CAAC,SAAS,CAAC,SAAS,MAAM,KAAG,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAoB5C,eAAO,MAAM,yBAAyB,WAAY,QAAQ,CAAC,SAAS,CAAC,SAAS,MAAM,KAAG,YAiBtF,CAAA;AAED,eAAO,MAAM,0BAA0B,WAAY,QAAQ,CAAC,SAAS,CAAC,KAAG;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAkBvH,CAAA"}
|
package/build/utils/config.js
CHANGED
|
@@ -23,6 +23,12 @@ export const prepareSingleRedisOptions = (config, host) => {
|
|
|
23
23
|
return {
|
|
24
24
|
host: host,
|
|
25
25
|
port: config.port ?? 6379,
|
|
26
|
+
// ioredis 6 speaks RESP3 by default, which changes the shape of replies (a hash comes back as a Map);
|
|
27
|
+
// every reader here is written against RESP2, and `meta.protocol` still moves a deployment to 3.
|
|
28
|
+
protocol: 2,
|
|
29
|
+
// Only sent when configured: a bare `requirepass` server rejects AUTH with a username, so an
|
|
30
|
+
// undefined `user` has to stay absent rather than become an empty string.
|
|
31
|
+
...(config.user != null ? { username: config.user } : {}),
|
|
26
32
|
password: config.secret,
|
|
27
33
|
...normalizeRedisMeta(config.meta)
|
|
28
34
|
};
|
|
@@ -39,6 +45,7 @@ export const prepareClusterRedisOptions = (config) => {
|
|
|
39
45
|
slotsRefreshTimeout: 20000,
|
|
40
46
|
redisOptions: {
|
|
41
47
|
// tls: { rejectUnauthorized: false }, // @TODO check if it's working
|
|
48
|
+
protocol: 2,
|
|
42
49
|
password: config.secret,
|
|
43
50
|
...normalizeRedisMeta(config.meta)
|
|
44
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAIA,yFAAyF;AACzF,yFAAyF;AACzF,2CAA2C;AAC3C,MAAM,kBAAkB,GAAG,CAAC,IAAgB,EAAgB,EAAE;IAC5D,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IACjC,IAAI,OAAO,IAAI,IAAI,IAAI,GAAG,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;IAC1B,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,WAAW,CAAC,2BAA2B,OAAO,GAAG,CAAC,CAAA;IAC9D,CAAC;IACD,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAA;AACxB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,MAA2B,EAAE,IAAa,EAAgB,EAAE;IACpG,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAW,CAAA;IACpD,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,WAAW,CAAC,8EAA8E,CAAC,CAAA;IACvG,CAAC;IACD,OAAO;QACL,IAAI,EAAE,IAAI;QACV,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI;QACzB,QAAQ,EAAE,MAAM,CAAC,MAAM;QACvB,GAAG,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC;KACnC,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,MAA2B,EAAqD,EAAE;IAC3H,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,WAAW,CAAC,kFAAkF,CAAC,CAAA;IAC3G,CAAC;IACD,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE;YACtE,SAAS,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;gBAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;YACzB,CAAC;YACD,mBAAmB,EAAE,KAAK;YAC1B,YAAY,EAAE;gBACZ,qEAAqE;gBACrE,QAAQ,EAAE,MAAM,CAAC,MAAM;gBACvB,GAAG,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC;aACnC;SACF;KACF,CAAA;AACH,CAAC,CAAA"}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAIA,yFAAyF;AACzF,yFAAyF;AACzF,2CAA2C;AAC3C,MAAM,kBAAkB,GAAG,CAAC,IAAgB,EAAgB,EAAE;IAC5D,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IACjC,IAAI,OAAO,IAAI,IAAI,IAAI,GAAG,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;IAC1B,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,WAAW,CAAC,2BAA2B,OAAO,GAAG,CAAC,CAAA;IAC9D,CAAC;IACD,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAA;AACxB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,MAA2B,EAAE,IAAa,EAAgB,EAAE;IACpG,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAW,CAAA;IACpD,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,WAAW,CAAC,8EAA8E,CAAC,CAAA;IACvG,CAAC;IACD,OAAO;QACL,IAAI,EAAE,IAAI;QACV,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI;QACzB,sGAAsG;QACtG,iGAAiG;QACjG,QAAQ,EAAE,CAAC;QACX,6FAA6F;QAC7F,0EAA0E;QAC1E,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,QAAQ,EAAE,MAAM,CAAC,MAAM;QACvB,GAAG,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC;KACnC,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,MAA2B,EAAqD,EAAE;IAC3H,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,WAAW,CAAC,kFAAkF,CAAC,CAAA;IAC3G,CAAC;IACD,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE;YACtE,SAAS,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;gBAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;YACzB,CAAC;YACD,mBAAmB,EAAE,KAAK;YAC1B,YAAY,EAAE;gBACZ,qEAAqE;gBACrE,QAAQ,EAAE,CAAC;gBACX,QAAQ,EAAE,MAAM,CAAC,MAAM;gBACvB,GAAG,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC;aACnC;SACF;KACF,CAAA;AACH,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/redis",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.41",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -23,20 +23,21 @@
|
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@owlmeans/dep-config": "workspace:*",
|
|
26
|
-
"@owlmeans/test-integration": "^0.1.18-rc.
|
|
27
|
-
"@types/bun": "^1.
|
|
28
|
-
"@types/node": "^26.
|
|
26
|
+
"@owlmeans/test-integration": "^0.1.18-rc.32",
|
|
27
|
+
"@types/bun": "^1.4.2",
|
|
28
|
+
"@types/node": "^26.6.4",
|
|
29
29
|
"nodemon": "^3.1.14",
|
|
30
30
|
"typescript": "^7.0.2"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@noble/hashes": "^
|
|
34
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
35
|
-
"@owlmeans/
|
|
36
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
37
|
-
"@owlmeans/
|
|
38
|
-
"@
|
|
39
|
-
"
|
|
33
|
+
"@noble/hashes": "^2.4.0",
|
|
34
|
+
"@owlmeans/context": "^0.1.18-rc.33",
|
|
35
|
+
"@owlmeans/log": "^0.1.18-rc.1",
|
|
36
|
+
"@owlmeans/redis-resource": "^0.1.18-rc.43",
|
|
37
|
+
"@owlmeans/resource": "^0.1.18-rc.39",
|
|
38
|
+
"@owlmeans/server-context": "^0.1.18-rc.42",
|
|
39
|
+
"@scure/base": "^2.4.0",
|
|
40
|
+
"ioredis": "^6.0.0"
|
|
40
41
|
},
|
|
41
42
|
"publishConfig": {
|
|
42
43
|
"access": "public"
|
package/src/service.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { RedisDbService, RedisClient, RedisDb } from '@owlmeans/redis-resource'
|
|
2
2
|
import { DEFAULT_ALIAS } from './consts.js'
|
|
3
3
|
import { createDbService } from '@owlmeans/resource'
|
|
4
|
-
import { assertContext
|
|
5
|
-
import
|
|
6
|
-
import type {
|
|
7
|
-
import { createClient } from './utils/index.js'
|
|
4
|
+
import { assertContext } from '@owlmeans/context'
|
|
5
|
+
import { createClient, prepareClusterRedisOptions, prepareSingleRedisOptions } from './utils/index.js'
|
|
6
|
+
import type { Config, Context } from './types.local.js'
|
|
8
7
|
|
|
9
|
-
type Config = ServerConfig
|
|
10
|
-
interface Context<C extends Config = Config> extends ServerContext<C> { }
|
|
11
8
|
|
|
12
9
|
export const makeRedisService = (alias: string = DEFAULT_ALIAS): RedisDbService => {
|
|
13
10
|
const location = `redis:${alias}`
|
|
@@ -23,7 +20,19 @@ export const makeRedisService = (alias: string = DEFAULT_ALIAS): RedisDbService
|
|
|
23
20
|
* @TODO we need to think how we can reuse the initail
|
|
24
21
|
* one instead of duplication for some cases
|
|
25
22
|
*/
|
|
26
|
-
return { client: client.duplicate(), prefix: name }
|
|
23
|
+
return { client: (client as { duplicate: () => RedisClient }).duplicate(), prefix: name }
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
options: configAlias => {
|
|
27
|
+
configAlias = service.ensureConfigAlias(configAlias)
|
|
28
|
+
const config = service.config(configAlias)
|
|
29
|
+
const prefix = service.name(configAlias)
|
|
30
|
+
|
|
31
|
+
const hosts = Array.isArray(config.host) ? config.host : [config.host]
|
|
32
|
+
|
|
33
|
+
return hosts.length > 1
|
|
34
|
+
? { cluster: prepareClusterRedisOptions(config), prefix }
|
|
35
|
+
: { single: prepareSingleRedisOptions(config, hosts[0]), prefix }
|
|
27
36
|
},
|
|
28
37
|
|
|
29
38
|
initialize: async configAlias => {
|
|
@@ -34,16 +43,6 @@ export const makeRedisService = (alias: string = DEFAULT_ALIAS): RedisDbService
|
|
|
34
43
|
return
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
if (service.layers == null) {
|
|
38
|
-
service.layers = [Layer.Global]
|
|
39
|
-
}
|
|
40
|
-
if (config.serviceSensitive && service.layers.includes(Layer.Service)) {
|
|
41
|
-
service.layers.push(Layer.Service)
|
|
42
|
-
}
|
|
43
|
-
if (config.entitySensitive && service.layers.includes(Layer.Entity)) {
|
|
44
|
-
service.layers.push(Layer.Entity)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
46
|
let client = await createClient(config)
|
|
48
47
|
|
|
49
48
|
// we need to check all hosts for replication consistancy
|
|
@@ -57,16 +56,6 @@ export const makeRedisService = (alias: string = DEFAULT_ALIAS): RedisDbService
|
|
|
57
56
|
})
|
|
58
57
|
|
|
59
58
|
service.clients[configAlias] = client
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
reinitializeContext: <T>(context: BasicContext<ServerConfig>) => {
|
|
63
|
-
const _service = makeRedisService(alias)
|
|
64
|
-
|
|
65
|
-
_service.ctx = context
|
|
66
|
-
|
|
67
|
-
_service.layers = service.layers
|
|
68
|
-
|
|
69
|
-
return _service as T
|
|
70
59
|
}
|
|
71
60
|
}, service => async () => {
|
|
72
61
|
const context = assertContext<Config, Context>(service.ctx as Context, location)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis'
|
|
2
|
+
import type { ServerContext, ServerConfig } from '@owlmeans/server-context'
|
|
3
|
+
|
|
4
|
+
export type Config = ServerConfig
|
|
5
|
+
|
|
6
|
+
export interface Context<C extends Config = Config> extends ServerContext<C> { }
|
|
7
|
+
|
|
8
|
+
/** One line of `CLUSTER NODES`, parsed. */
|
|
9
|
+
export interface NodeInfo {
|
|
10
|
+
nodeId: string
|
|
11
|
+
addr: string
|
|
12
|
+
flags: string[]
|
|
13
|
+
master: string
|
|
14
|
+
state: string
|
|
15
|
+
slots: number[][] | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ClusterNode {
|
|
19
|
+
node: Redis
|
|
20
|
+
info: NodeInfo
|
|
21
|
+
}
|
package/src/utils/cluster.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { logger } from '@owlmeans/log'
|
|
1
2
|
import type { DbConfig } from '@owlmeans/resource'
|
|
2
3
|
import { Cluster } from 'ioredis'
|
|
3
4
|
import type { Redis } from 'ioredis'
|
|
4
5
|
import { prepareClusterRedisOptions } from './config.js'
|
|
5
6
|
import { createClient } from './instance.js'
|
|
6
7
|
import type { RedisMeta } from '../types.js'
|
|
8
|
+
import type { ClusterNode, NodeInfo } from '../types.local.js'
|
|
9
|
+
|
|
10
|
+
const log = logger('redis:cluster')
|
|
7
11
|
|
|
8
12
|
export const ensuerCluster = async (config: DbConfig<RedisMeta>): Promise<Cluster> => {
|
|
9
13
|
if (!Array.isArray(config.host)) {
|
|
@@ -177,7 +181,7 @@ export const ensuerCluster = async (config: DbConfig<RedisMeta>): Promise<Cluste
|
|
|
177
181
|
throw new SyntaxError('Not enough slave nodes in redis cluster after configuration')
|
|
178
182
|
}
|
|
179
183
|
} catch (e) {
|
|
180
|
-
|
|
184
|
+
log.error('Redis cluster configuration failed', e)
|
|
181
185
|
throw e
|
|
182
186
|
}
|
|
183
187
|
|
|
@@ -190,7 +194,7 @@ export const ensuerCluster = async (config: DbConfig<RedisMeta>): Promise<Cluste
|
|
|
190
194
|
return new Cluster(setup.nodes, setup.options)
|
|
191
195
|
}
|
|
192
196
|
|
|
193
|
-
const parseClusterNodeInfo = (clusterNodes: string) => {
|
|
197
|
+
const parseClusterNodeInfo = (clusterNodes: string): NodeInfo[] => {
|
|
194
198
|
const nodesInfo = clusterNodes.split('\n').filter(line => line.trim() !== '')
|
|
195
199
|
return nodesInfo.map(line => {
|
|
196
200
|
const [nodeId, addr, flags, master, , , , state, ...slots] = line.split(' ')
|
|
@@ -207,9 +211,3 @@ const parseClusterNodeInfo = (clusterNodes: string) => {
|
|
|
207
211
|
})
|
|
208
212
|
}
|
|
209
213
|
|
|
210
|
-
type NodeInfo = ReturnType<typeof parseClusterNodeInfo>[0]
|
|
211
|
-
|
|
212
|
-
interface ClusterNode {
|
|
213
|
-
node: Redis
|
|
214
|
-
info: NodeInfo
|
|
215
|
-
}
|
package/src/utils/config.ts
CHANGED
|
@@ -28,6 +28,12 @@ export const prepareSingleRedisOptions = (config: DbConfig<RedisMeta>, host?: st
|
|
|
28
28
|
return {
|
|
29
29
|
host: host,
|
|
30
30
|
port: config.port ?? 6379,
|
|
31
|
+
// ioredis 6 speaks RESP3 by default, which changes the shape of replies (a hash comes back as a Map);
|
|
32
|
+
// every reader here is written against RESP2, and `meta.protocol` still moves a deployment to 3.
|
|
33
|
+
protocol: 2,
|
|
34
|
+
// Only sent when configured: a bare `requirepass` server rejects AUTH with a username, so an
|
|
35
|
+
// undefined `user` has to stay absent rather than become an empty string.
|
|
36
|
+
...(config.user != null ? { username: config.user } : {}),
|
|
31
37
|
password: config.secret,
|
|
32
38
|
...normalizeRedisMeta(config.meta)
|
|
33
39
|
}
|
|
@@ -45,6 +51,7 @@ export const prepareClusterRedisOptions = (config: DbConfig<RedisMeta>): { nodes
|
|
|
45
51
|
slotsRefreshTimeout: 20000,
|
|
46
52
|
redisOptions: {
|
|
47
53
|
// tls: { rejectUnauthorized: false }, // @TODO check if it's working
|
|
54
|
+
protocol: 2,
|
|
48
55
|
password: config.secret,
|
|
49
56
|
...normalizeRedisMeta(config.meta)
|
|
50
57
|
}
|
package/tests/context.ts
CHANGED
|
@@ -28,11 +28,15 @@ export interface RedisSuite {
|
|
|
28
28
|
* One key prefix per suite, dropped by that suite's own `afterAll` — Bun runs every spec
|
|
29
29
|
* file of a package in one process, so a process-global cleanup queue would let the first
|
|
30
30
|
* file to finish flush the keys of the files still to come.
|
|
31
|
+
*
|
|
32
|
+
* A spec that walks the namespace — `list`, `count`, `purge` — boots under its own resource
|
|
33
|
+
* alias, because a resource's keys are namespaced by `<schema>-<alias>` and the walk sees every
|
|
34
|
+
* key of the alias it belongs to, including the ones an earlier test in the same file left.
|
|
31
35
|
*/
|
|
32
36
|
export const makeSuite = (label: string): RedisSuite => {
|
|
33
37
|
const base = process.env.REDIS_TEST_KEY_PREFIX ?? 'omt'
|
|
34
38
|
const prefix = randomNamespace(`${base}_${label}`)
|
|
35
|
-
const
|
|
39
|
+
const resources: Array<RedisResource<TestRecord>> = []
|
|
36
40
|
|
|
37
41
|
const boot = async (alias = 'test-records'): Promise<{
|
|
38
42
|
context: ServerContext<ServerConfig>
|
|
@@ -58,26 +62,42 @@ export const makeSuite = (label: string): RedisSuite => {
|
|
|
58
62
|
|
|
59
63
|
context.configure()
|
|
60
64
|
await context.init()
|
|
61
|
-
|
|
65
|
+
const booted = context.resource<RedisResource<TestRecord>>(alias)
|
|
66
|
+
resources.push(booted)
|
|
62
67
|
|
|
63
|
-
return { context, resource:
|
|
68
|
+
return { context, resource: booted }
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Every key the suite wrote, matched as `<prefix>*` rather than `<prefix>:*`: a resource
|
|
73
|
+
* namespaces its keys as `<schema>-<alias>:<id>`, so the schema is followed by a dash, not
|
|
74
|
+
* by the separator. SCAN for the same reason the resource uses it — KEYS blocks the server.
|
|
75
|
+
*/
|
|
66
76
|
const teardown = async (): Promise<void> => {
|
|
67
77
|
if (gate.skip) {
|
|
68
78
|
return
|
|
69
79
|
}
|
|
70
|
-
for (const
|
|
71
|
-
const resource = context.resource<RedisResource<TestRecord>>('test-records')
|
|
80
|
+
for (const resource of resources) {
|
|
72
81
|
const client = resource.db?.client
|
|
73
82
|
if (client == null) continue
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
try {
|
|
84
|
+
const keys: string[] = []
|
|
85
|
+
let cursor = '0'
|
|
86
|
+
do {
|
|
87
|
+
const [next, batch] = await client.scan(cursor, 'MATCH', `${prefix}*`, 'COUNT', 500)
|
|
88
|
+
cursor = next
|
|
89
|
+
keys.push(...batch)
|
|
90
|
+
} while (cursor !== '0')
|
|
91
|
+
if (keys.length > 0) {
|
|
92
|
+
await client.del(keys)
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// A connection already gone takes its keys' cleanup with it — the prefix is unique
|
|
96
|
+
// per run, so anything left behind never collides with another suite.
|
|
77
97
|
}
|
|
78
98
|
await client.quit().catch(() => undefined)
|
|
79
99
|
}
|
|
80
|
-
|
|
100
|
+
resources.length = 0
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
return { prefix, boot, teardown }
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { gate, makeSuite } from './context.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The criteria surface.
|
|
6
|
+
*
|
|
7
|
+
* Redis has no index, so anything that is not a bare id is a SCAN of the resource's own namespace
|
|
8
|
+
* evaluated in memory. These assert what that walk owes the caller: criteria reads, an unpaged
|
|
9
|
+
* default, the window that was actually asked for, a total that always counts every match, and a
|
|
10
|
+
* purge that refuses to empty the namespace on an unset filter.
|
|
11
|
+
*/
|
|
12
|
+
describe('@owlmeans/redis — resource queries', () => {
|
|
13
|
+
if (gate.skip) {
|
|
14
|
+
test.skip(gate.reason ?? 'redis gate closed', () => { })
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const suite = makeSuite('query')
|
|
19
|
+
|
|
20
|
+
afterAll(async () => {
|
|
21
|
+
await suite.teardown()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/** `rec-0` … `rec-<count-1>`, alternating `even` / `odd`, in a namespace of their own. */
|
|
25
|
+
const seed = async (alias: string, count: number) => {
|
|
26
|
+
const { resource } = await suite.boot(alias)
|
|
27
|
+
for (let index = 0; index < count; index++) {
|
|
28
|
+
await resource.create({ id: `rec-${index}`, value: index % 2 === 0 ? 'even' : 'odd' })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return resource
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('load takes criteria, not only an id', async () => {
|
|
35
|
+
const resource = await seed('q-load', 0)
|
|
36
|
+
await resource.create({ id: 'one', value: 'needle' })
|
|
37
|
+
await resource.create({ id: 'two', value: 'hay' })
|
|
38
|
+
|
|
39
|
+
expect(await resource.load({ value: 'needle' })).toMatchObject({ id: 'one' })
|
|
40
|
+
expect(await resource.load({ value: 'absent' })).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('get throws when nothing matches the criteria', async () => {
|
|
44
|
+
const resource = await seed('q-get', 0)
|
|
45
|
+
|
|
46
|
+
await expect(resource.get({ value: 'absent' })).rejects.toThrow()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('list returns every match when no size is asked for', async () => {
|
|
50
|
+
const resource = await seed('q-unpaged', 5)
|
|
51
|
+
|
|
52
|
+
const result = await resource.list()
|
|
53
|
+
|
|
54
|
+
expect(result.items).toHaveLength(5)
|
|
55
|
+
expect(result.total).toBe(5)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('list returns the page it was asked for, not the list minus that page', async () => {
|
|
59
|
+
const resource = await seed('q-paged', 5)
|
|
60
|
+
|
|
61
|
+
const result = await resource.list({}, { page: 1, size: 2, sort: ['id'] })
|
|
62
|
+
|
|
63
|
+
expect(result.items.map(item => item.id)).toEqual(['rec-2', 'rec-3'])
|
|
64
|
+
expect(result.total).toBe(5)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('a page without a size is a caller error', async () => {
|
|
68
|
+
const resource = await seed('q-page-only', 1)
|
|
69
|
+
|
|
70
|
+
await expect(resource.list({}, { page: 1 })).rejects.toThrow()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('count answers the match count', async () => {
|
|
74
|
+
const resource = await seed('q-count', 5)
|
|
75
|
+
|
|
76
|
+
expect(await resource.count()).toBe(5)
|
|
77
|
+
expect(await resource.count({ value: 'even' })).toBe(3)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('purge deletes every match and refuses empty criteria', async () => {
|
|
81
|
+
const resource = await seed('q-purge', 5)
|
|
82
|
+
|
|
83
|
+
expect(await resource.purge({ value: 'odd' })).toBe(2)
|
|
84
|
+
expect(await resource.count()).toBe(3)
|
|
85
|
+
await expect(resource.purge({})).rejects.toThrow()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('update replaces the record rather than merging into it', async () => {
|
|
89
|
+
const resource = await seed('q-update', 0)
|
|
90
|
+
await resource.create({ id: 'replaced', value: 'before' })
|
|
91
|
+
|
|
92
|
+
await resource.update({ id: 'replaced' })
|
|
93
|
+
|
|
94
|
+
expect(await resource.load('replaced')).toEqual({ id: 'replaced' })
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('update throws for an id that was never created', async () => {
|
|
98
|
+
const resource = await seed('q-missing', 0)
|
|
99
|
+
|
|
100
|
+
await expect(resource.update({ id: 'never-created' })).rejects.toThrow()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('save creates a record that carries no id', async () => {
|
|
104
|
+
const resource = await seed('q-save', 0)
|
|
105
|
+
|
|
106
|
+
const saved = await resource.save({ value: 'fresh' })
|
|
107
|
+
|
|
108
|
+
expect(typeof saved.id).toBe('string')
|
|
109
|
+
expect(await resource.load(saved.id)).toMatchObject({ value: 'fresh' })
|
|
110
|
+
})
|
|
111
|
+
})
|
package/tests/resource.spec.ts
CHANGED
|
@@ -2,13 +2,12 @@ import { afterAll, describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import { gate, makeSuite } from './context.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* `delete`
|
|
5
|
+
* `delete` / `take` coverage.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `issueChallenge` upserts by deleting first — and left consumed codes alive in the store.
|
|
7
|
+
* Both drop the key with GETDEL and hand back what was under it; they differ only in what an
|
|
8
|
+
* absent id means — `delete` answers `null`, `take` throws. The by-id form carries the whole
|
|
9
|
+
* contract: a `delete(id)` that returns without touching redis leaves consumed OTP codes alive
|
|
10
|
+
* in the store and turns the next `create` under that id into `RecordExists`.
|
|
12
11
|
*/
|
|
13
12
|
describe('@owlmeans/redis — resource delete', () => {
|
|
14
13
|
if (gate.skip) {
|
|
@@ -22,7 +21,7 @@ describe('@owlmeans/redis — resource delete', () => {
|
|
|
22
21
|
await suite.teardown()
|
|
23
22
|
})
|
|
24
23
|
|
|
25
|
-
test('removes the record when called by id
|
|
24
|
+
test('removes the record when called by id', async () => {
|
|
26
25
|
const { resource } = await suite.boot()
|
|
27
26
|
await resource.create({ id: 'by-id', value: 'first' })
|
|
28
27
|
|
|
@@ -46,11 +45,24 @@ describe('@owlmeans/redis — resource delete', () => {
|
|
|
46
45
|
expect((await resource.load('otp'))?.value).toBe('second-code')
|
|
47
46
|
})
|
|
48
47
|
|
|
49
|
-
test('
|
|
48
|
+
test('take hands back the record it removed', async () => {
|
|
50
49
|
const { resource } = await suite.boot()
|
|
51
|
-
|
|
50
|
+
await resource.create({ id: 'taken', value: 'once' })
|
|
52
51
|
|
|
53
|
-
expect(await resource.
|
|
54
|
-
expect(await resource.load('
|
|
52
|
+
expect(await resource.take('taken')).toMatchObject({ value: 'once' })
|
|
53
|
+
expect(await resource.load('taken')).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('take throws for an id that is not there', async () => {
|
|
57
|
+
const { resource } = await suite.boot()
|
|
58
|
+
|
|
59
|
+
await expect(resource.take('never-taken')).rejects.toThrow()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('create refuses an id that is already taken', async () => {
|
|
63
|
+
const { resource } = await suite.boot()
|
|
64
|
+
await resource.create({ id: 'occupied', value: 'first' })
|
|
65
|
+
|
|
66
|
+
await expect(resource.create({ id: 'occupied', value: 'second' })).rejects.toThrow()
|
|
55
67
|
})
|
|
56
68
|
})
|
package/build/.gitkeep
DELETED
|
File without changes
|