@pubber-subber/redis 0.0.1
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/LICENSE +21 -0
- package/README.md +89 -0
- package/dist/index.cjs +165 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/index.ts +216 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sami Mishal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @pubber-subber/redis
|
|
2
|
+
|
|
3
|
+
Redis pub/sub adapter for [`@pubber-subber/core`](https://www.npmjs.com/package/@pubber-subber/core) — built on [`ioredis`](https://github.com/redis/ioredis). Works with Redis, [Valkey](https://valkey.io), [KeyDB](https://docs.keydb.dev), [Dragonfly](https://dragonflydb.io), or any Redis-protocol-compatible server.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @pubber-subber/core @pubber-subber/redis ioredis
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`ioredis` is a **peer dependency** — install it alongside.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { PubSub } from '@pubber-subber/core';
|
|
17
|
+
import { redis } from '@pubber-subber/redis';
|
|
18
|
+
|
|
19
|
+
const pubsub = new PubSub({
|
|
20
|
+
adapter: redis({ url: 'redis://localhost:6379' }),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await pubsub.subscribe('users.*', (msg) => {
|
|
24
|
+
console.log(msg.topic, msg.payload);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await pubsub.publish('users.created', { id: 1, name: 'Alice' });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
redis({
|
|
34
|
+
url?: string;
|
|
35
|
+
options?: RedisOptions; // ioredis-compatible
|
|
36
|
+
clients?: { publisher: Redis; subscriber: Redis };
|
|
37
|
+
codec?: Codec;
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Option | Notes |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `url` | Standard Redis URL: `redis://[:password@]host:port[/db]`, `rediss://...` for TLS. |
|
|
44
|
+
| `options` | Full ioredis `RedisOptions`. Merged with `url`. |
|
|
45
|
+
| `clients` | Bring your own `{ publisher, subscriber }` pair. The adapter will **not** call `quit()` on them. `publisher` and `subscriber` must be distinct clients — Redis rejects non-pub/sub commands on a `SUBSCRIBE`-d connection. |
|
|
46
|
+
| `codec` | Payload encoder/decoder. Default `jsonCodec()`. |
|
|
47
|
+
|
|
48
|
+
## Publish meta
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await pubsub.publish('orders.created', payload, { channel: 'orders' });
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Field | Notes |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `channel` | Override the on-wire Redis channel without changing the logical `AdapterMessage.topic`. |
|
|
57
|
+
|
|
58
|
+
## Subscribe meta
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
await pubsub.subscribe('orders', handler, { pattern: true });
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Field | Default | Notes |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `pattern` | auto-detected | Force `PSUBSCRIBE` even if the topic has no `*` or `?`. Auto-detected from wildcards otherwise. |
|
|
67
|
+
|
|
68
|
+
## Capabilities
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
{ publish: true, subscribe: true, patternSubscribe: true, ack: false }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
- **Two internal connections**: one publisher, one subscriber. Required by Redis (a `SUBSCRIBE`-d connection cannot issue other commands).
|
|
77
|
+
- `subscribe(topic, handler)` issues `SUBSCRIBE` for exact channels and `PSUBSCRIBE` for patterns containing `*` or `?` (override via `meta.pattern: true`).
|
|
78
|
+
- **Reference-counted**: multiple `subscribe()` calls for the same channel only `SUBSCRIBE` once on the wire. `UNSUBSCRIBE` fires when the last handler goes away.
|
|
79
|
+
- **Reconnect**: `ioredis` handles socket-level reconnects automatically. On the `ready` event, the adapter re-subscribes every live channel and pattern — no user-visible message loss for events published *after* the reconnect completes.
|
|
80
|
+
|
|
81
|
+
## Caveats
|
|
82
|
+
|
|
83
|
+
- **Redis pub/sub is fire-and-forget.** Subscribers that fall behind have their messages dropped server-side. Events published while the subscriber is disconnected are not buffered. If you need delivery guarantees, use Redis Streams (different adapter — not in this package) or a different transport.
|
|
84
|
+
- **`capabilities.ack = false`**: there is no `ack`/`nack` to call on `AdapterMessage`.
|
|
85
|
+
- For high message-frequency scenarios, watch ioredis's `lazyConnect` and connection pool tuning.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@pubber-subber/core');
|
|
4
|
+
var ioredis = require('ioredis');
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
function redis(opts = {}) {
|
|
8
|
+
const codec = opts.codec ?? core.jsonCodec();
|
|
9
|
+
let publisher = null;
|
|
10
|
+
let subscriber = null;
|
|
11
|
+
let owned = true;
|
|
12
|
+
const exactHandlers = /* @__PURE__ */ new Map();
|
|
13
|
+
const patternHandlers = /* @__PURE__ */ new Map();
|
|
14
|
+
let idCounter = 0;
|
|
15
|
+
let installedListeners = false;
|
|
16
|
+
const createClient = () => {
|
|
17
|
+
if (opts.url && opts.options) return new ioredis.Redis(opts.url, opts.options);
|
|
18
|
+
if (opts.url) return new ioredis.Redis(opts.url);
|
|
19
|
+
if (opts.options) return new ioredis.Redis(opts.options);
|
|
20
|
+
return new ioredis.Redis();
|
|
21
|
+
};
|
|
22
|
+
const ensure = () => {
|
|
23
|
+
if (publisher && subscriber) return { publisher, subscriber };
|
|
24
|
+
if (opts.clients) {
|
|
25
|
+
publisher = opts.clients.publisher;
|
|
26
|
+
subscriber = opts.clients.subscriber;
|
|
27
|
+
owned = false;
|
|
28
|
+
} else {
|
|
29
|
+
publisher = createClient();
|
|
30
|
+
subscriber = createClient();
|
|
31
|
+
owned = true;
|
|
32
|
+
}
|
|
33
|
+
if (!installedListeners) {
|
|
34
|
+
installedListeners = true;
|
|
35
|
+
subscriber.on("message", (channel, message) => {
|
|
36
|
+
deliver(exactHandlers.get(channel), channel, message, void 0);
|
|
37
|
+
});
|
|
38
|
+
subscriber.on("pmessage", (pattern, channel, message) => {
|
|
39
|
+
deliver(patternHandlers.get(pattern), channel, message, { pattern });
|
|
40
|
+
});
|
|
41
|
+
subscriber.on("ready", () => {
|
|
42
|
+
for (const channel of exactHandlers.keys()) {
|
|
43
|
+
subscriber?.subscribe(channel).catch(noop);
|
|
44
|
+
}
|
|
45
|
+
for (const pattern of patternHandlers.keys()) {
|
|
46
|
+
subscriber?.psubscribe(pattern).catch(noop);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { publisher, subscriber };
|
|
51
|
+
};
|
|
52
|
+
const deliver = (handlers, channel, wireMessage, extraMeta) => {
|
|
53
|
+
if (!handlers || handlers.size === 0) return;
|
|
54
|
+
let payload;
|
|
55
|
+
try {
|
|
56
|
+
payload = codec.decode(wireMessage);
|
|
57
|
+
} catch {
|
|
58
|
+
payload = wireMessage;
|
|
59
|
+
}
|
|
60
|
+
const msg = {
|
|
61
|
+
topic: channel,
|
|
62
|
+
payload,
|
|
63
|
+
raw: wireMessage,
|
|
64
|
+
meta: extraMeta
|
|
65
|
+
};
|
|
66
|
+
for (const handler of [...handlers]) {
|
|
67
|
+
Promise.resolve(handler(msg)).catch(noop);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
name: "redis",
|
|
72
|
+
capabilities: { publish: true, subscribe: true, patternSubscribe: true, ack: false },
|
|
73
|
+
async connect() {
|
|
74
|
+
const { publisher: p, subscriber: s } = ensure();
|
|
75
|
+
await Promise.all([waitReady(p), waitReady(s)]);
|
|
76
|
+
},
|
|
77
|
+
async disconnect() {
|
|
78
|
+
if (!owned) {
|
|
79
|
+
publisher = null;
|
|
80
|
+
subscriber = null;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const closing = [];
|
|
84
|
+
if (subscriber) closing.push(subscriber.quit().catch(noop));
|
|
85
|
+
if (publisher) closing.push(publisher.quit().catch(noop));
|
|
86
|
+
await Promise.all(closing);
|
|
87
|
+
publisher = null;
|
|
88
|
+
subscriber = null;
|
|
89
|
+
installedListeners = false;
|
|
90
|
+
exactHandlers.clear();
|
|
91
|
+
patternHandlers.clear();
|
|
92
|
+
},
|
|
93
|
+
async publish(topic, payload, meta) {
|
|
94
|
+
const { publisher: p } = ensure();
|
|
95
|
+
const channel = meta?.channel ?? topic;
|
|
96
|
+
const encoded = codec.encode(payload);
|
|
97
|
+
const wire = typeof encoded === "string" ? encoded : Buffer.from(encoded);
|
|
98
|
+
await p.publish(channel, wire);
|
|
99
|
+
},
|
|
100
|
+
async subscribe(topic, handler, meta) {
|
|
101
|
+
const { subscriber: s } = ensure();
|
|
102
|
+
const usePattern = meta?.pattern ?? core.isPattern(topic);
|
|
103
|
+
const map = usePattern ? patternHandlers : exactHandlers;
|
|
104
|
+
let set = map.get(topic);
|
|
105
|
+
const firstSubscriber = !set;
|
|
106
|
+
if (!set) {
|
|
107
|
+
set = /* @__PURE__ */ new Set();
|
|
108
|
+
map.set(topic, set);
|
|
109
|
+
}
|
|
110
|
+
if (firstSubscriber) {
|
|
111
|
+
if (usePattern) await s.psubscribe(topic);
|
|
112
|
+
else await s.subscribe(topic);
|
|
113
|
+
}
|
|
114
|
+
set.add(handler);
|
|
115
|
+
idCounter += 1;
|
|
116
|
+
const id = `redis-${idCounter}`;
|
|
117
|
+
const ownSet = set;
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
topic,
|
|
121
|
+
unsubscribe: async () => {
|
|
122
|
+
ownSet.delete(handler);
|
|
123
|
+
if (ownSet.size === 0) {
|
|
124
|
+
map.delete(topic);
|
|
125
|
+
if (subscriber) {
|
|
126
|
+
if (usePattern) await subscriber.punsubscribe(topic).catch(noop);
|
|
127
|
+
else await subscriber.unsubscribe(topic).catch(noop);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function noop() {
|
|
136
|
+
}
|
|
137
|
+
async function waitReady(client) {
|
|
138
|
+
if (client.status === "ready") return;
|
|
139
|
+
if (client.status === "wait" || client.status === "connecting") {
|
|
140
|
+
await new Promise((resolve, reject) => {
|
|
141
|
+
const onReady = () => {
|
|
142
|
+
cleanup();
|
|
143
|
+
resolve();
|
|
144
|
+
};
|
|
145
|
+
const onError = (err) => {
|
|
146
|
+
cleanup();
|
|
147
|
+
reject(err);
|
|
148
|
+
};
|
|
149
|
+
const cleanup = () => {
|
|
150
|
+
client.off("ready", onReady);
|
|
151
|
+
client.off("error", onError);
|
|
152
|
+
};
|
|
153
|
+
client.once("ready", onReady);
|
|
154
|
+
client.once("error", onError);
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await client.connect().catch((err) => {
|
|
159
|
+
if (!String(err?.message ?? "").includes("connect")) throw err;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
exports.redis = redis;
|
|
164
|
+
//# sourceMappingURL=index.cjs.map
|
|
165
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["jsonCodec","Redis","isPattern"],"mappings":"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","file":"index.cjs","sourcesContent":["import {\n type AdapterMessage,\n type Codec,\n type MessageHandler,\n type PubSubAdapter,\n isPattern,\n jsonCodec,\n} from '@pubber-subber/core';\nimport { Redis, type RedisOptions } from 'ioredis';\n\nexport interface RedisAdapterOptions {\n /** Redis connection URL, e.g. `redis://localhost:6379`. */\n url?: string;\n /** ioredis-compatible options. Combined with `url` if both are passed. */\n options?: RedisOptions;\n /**\n * Bring-your-own clients. When provided, the adapter will NOT manage their\n * lifecycle (connect/disconnect become no-ops on `disconnect`).\n * `publisher` and `subscriber` must be distinct clients because Redis\n * disallows non-pub/sub commands on a subscribed connection.\n */\n clients?: { publisher: Redis; subscriber: Redis };\n /** Payload codec. Default: JSON. */\n codec?: Codec;\n}\n\nexport interface RedisPublishMeta {\n /** Override the wire channel for this publish; the AdapterMessage still carries `topic`. */\n channel?: string;\n}\n\nexport interface RedisSubscribeMeta {\n /** Force PSUBSCRIBE even when `topic` has no wildcards. */\n pattern?: boolean;\n}\n\nexport function redis(\n opts: RedisAdapterOptions = {},\n): PubSubAdapter<RedisPublishMeta, RedisSubscribeMeta> {\n const codec = opts.codec ?? jsonCodec();\n\n let publisher: Redis | null = null;\n let subscriber: Redis | null = null;\n let owned = true;\n\n const exactHandlers = new Map<string, Set<MessageHandler>>();\n const patternHandlers = new Map<string, Set<MessageHandler>>();\n let idCounter = 0;\n let installedListeners = false;\n\n const createClient = (): Redis => {\n if (opts.url && opts.options) return new Redis(opts.url, opts.options);\n if (opts.url) return new Redis(opts.url);\n if (opts.options) return new Redis(opts.options);\n return new Redis();\n };\n\n const ensure = (): { publisher: Redis; subscriber: Redis } => {\n if (publisher && subscriber) return { publisher, subscriber };\n\n if (opts.clients) {\n publisher = opts.clients.publisher;\n subscriber = opts.clients.subscriber;\n owned = false;\n } else {\n publisher = createClient();\n subscriber = createClient();\n owned = true;\n }\n\n if (!installedListeners) {\n installedListeners = true;\n subscriber.on('message', (channel: string, message: string) => {\n deliver(exactHandlers.get(channel), channel, message, undefined);\n });\n subscriber.on('pmessage', (pattern: string, channel: string, message: string) => {\n deliver(patternHandlers.get(pattern), channel, message, { pattern });\n });\n // Re-subscribe after a reconnect.\n subscriber.on('ready', () => {\n for (const channel of exactHandlers.keys()) {\n subscriber?.subscribe(channel).catch(noop);\n }\n for (const pattern of patternHandlers.keys()) {\n subscriber?.psubscribe(pattern).catch(noop);\n }\n });\n }\n\n return { publisher, subscriber };\n };\n\n const deliver = (\n handlers: Set<MessageHandler> | undefined,\n channel: string,\n wireMessage: string,\n extraMeta: Record<string, unknown> | undefined,\n ) => {\n if (!handlers || handlers.size === 0) return;\n let payload: unknown;\n try {\n payload = codec.decode(wireMessage);\n } catch {\n payload = wireMessage;\n }\n const msg: AdapterMessage = {\n topic: channel,\n payload,\n raw: wireMessage,\n meta: extraMeta,\n };\n for (const handler of [...handlers]) {\n Promise.resolve(handler(msg)).catch(noop);\n }\n };\n\n return {\n name: 'redis',\n capabilities: { publish: true, subscribe: true, patternSubscribe: true, ack: false },\n\n async connect() {\n const { publisher: p, subscriber: s } = ensure();\n await Promise.all([waitReady(p), waitReady(s)]);\n },\n\n async disconnect() {\n if (!owned) {\n publisher = null;\n subscriber = null;\n return;\n }\n const closing: Promise<unknown>[] = [];\n if (subscriber) closing.push(subscriber.quit().catch(noop));\n if (publisher) closing.push(publisher.quit().catch(noop));\n await Promise.all(closing);\n publisher = null;\n subscriber = null;\n installedListeners = false;\n exactHandlers.clear();\n patternHandlers.clear();\n },\n\n async publish(topic, payload, meta) {\n const { publisher: p } = ensure();\n const channel = meta?.channel ?? topic;\n const encoded = codec.encode(payload);\n const wire = typeof encoded === 'string' ? encoded : Buffer.from(encoded);\n await p.publish(channel, wire as never);\n },\n\n async subscribe(topic, handler, meta) {\n const { subscriber: s } = ensure();\n const usePattern = meta?.pattern ?? isPattern(topic);\n const map = usePattern ? patternHandlers : exactHandlers;\n let set = map.get(topic);\n const firstSubscriber = !set;\n if (!set) {\n set = new Set();\n map.set(topic, set);\n }\n if (firstSubscriber) {\n if (usePattern) await s.psubscribe(topic);\n else await s.subscribe(topic);\n }\n set.add(handler);\n idCounter += 1;\n const id = `redis-${idCounter}`;\n const ownSet = set;\n return {\n id,\n topic,\n unsubscribe: async () => {\n ownSet.delete(handler);\n if (ownSet.size === 0) {\n map.delete(topic);\n if (subscriber) {\n if (usePattern) await subscriber.punsubscribe(topic).catch(noop);\n else await subscriber.unsubscribe(topic).catch(noop);\n }\n }\n },\n };\n },\n };\n}\n\nfunction noop(): void {\n // intentional\n}\n\nasync function waitReady(client: Redis): Promise<void> {\n if (client.status === 'ready') return;\n if (client.status === 'wait' || client.status === 'connecting') {\n await new Promise<void>((resolve, reject) => {\n const onReady = () => {\n cleanup();\n resolve();\n };\n const onError = (err: unknown) => {\n cleanup();\n reject(err);\n };\n const cleanup = () => {\n client.off('ready', onReady);\n client.off('error', onError);\n };\n client.once('ready', onReady);\n client.once('error', onError);\n });\n return;\n }\n await client.connect().catch((err) => {\n // Already connected/connecting is fine.\n if (!String(err?.message ?? '').includes('connect')) throw err;\n });\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Codec, PubSubAdapter } from '@pubber-subber/core';
|
|
2
|
+
import { RedisOptions, Redis } from 'ioredis';
|
|
3
|
+
|
|
4
|
+
interface RedisAdapterOptions {
|
|
5
|
+
/** Redis connection URL, e.g. `redis://localhost:6379`. */
|
|
6
|
+
url?: string;
|
|
7
|
+
/** ioredis-compatible options. Combined with `url` if both are passed. */
|
|
8
|
+
options?: RedisOptions;
|
|
9
|
+
/**
|
|
10
|
+
* Bring-your-own clients. When provided, the adapter will NOT manage their
|
|
11
|
+
* lifecycle (connect/disconnect become no-ops on `disconnect`).
|
|
12
|
+
* `publisher` and `subscriber` must be distinct clients because Redis
|
|
13
|
+
* disallows non-pub/sub commands on a subscribed connection.
|
|
14
|
+
*/
|
|
15
|
+
clients?: {
|
|
16
|
+
publisher: Redis;
|
|
17
|
+
subscriber: Redis;
|
|
18
|
+
};
|
|
19
|
+
/** Payload codec. Default: JSON. */
|
|
20
|
+
codec?: Codec;
|
|
21
|
+
}
|
|
22
|
+
interface RedisPublishMeta {
|
|
23
|
+
/** Override the wire channel for this publish; the AdapterMessage still carries `topic`. */
|
|
24
|
+
channel?: string;
|
|
25
|
+
}
|
|
26
|
+
interface RedisSubscribeMeta {
|
|
27
|
+
/** Force PSUBSCRIBE even when `topic` has no wildcards. */
|
|
28
|
+
pattern?: boolean;
|
|
29
|
+
}
|
|
30
|
+
declare function redis(opts?: RedisAdapterOptions): PubSubAdapter<RedisPublishMeta, RedisSubscribeMeta>;
|
|
31
|
+
|
|
32
|
+
export { type RedisAdapterOptions, type RedisPublishMeta, type RedisSubscribeMeta, redis };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Codec, PubSubAdapter } from '@pubber-subber/core';
|
|
2
|
+
import { RedisOptions, Redis } from 'ioredis';
|
|
3
|
+
|
|
4
|
+
interface RedisAdapterOptions {
|
|
5
|
+
/** Redis connection URL, e.g. `redis://localhost:6379`. */
|
|
6
|
+
url?: string;
|
|
7
|
+
/** ioredis-compatible options. Combined with `url` if both are passed. */
|
|
8
|
+
options?: RedisOptions;
|
|
9
|
+
/**
|
|
10
|
+
* Bring-your-own clients. When provided, the adapter will NOT manage their
|
|
11
|
+
* lifecycle (connect/disconnect become no-ops on `disconnect`).
|
|
12
|
+
* `publisher` and `subscriber` must be distinct clients because Redis
|
|
13
|
+
* disallows non-pub/sub commands on a subscribed connection.
|
|
14
|
+
*/
|
|
15
|
+
clients?: {
|
|
16
|
+
publisher: Redis;
|
|
17
|
+
subscriber: Redis;
|
|
18
|
+
};
|
|
19
|
+
/** Payload codec. Default: JSON. */
|
|
20
|
+
codec?: Codec;
|
|
21
|
+
}
|
|
22
|
+
interface RedisPublishMeta {
|
|
23
|
+
/** Override the wire channel for this publish; the AdapterMessage still carries `topic`. */
|
|
24
|
+
channel?: string;
|
|
25
|
+
}
|
|
26
|
+
interface RedisSubscribeMeta {
|
|
27
|
+
/** Force PSUBSCRIBE even when `topic` has no wildcards. */
|
|
28
|
+
pattern?: boolean;
|
|
29
|
+
}
|
|
30
|
+
declare function redis(opts?: RedisAdapterOptions): PubSubAdapter<RedisPublishMeta, RedisSubscribeMeta>;
|
|
31
|
+
|
|
32
|
+
export { type RedisAdapterOptions, type RedisPublishMeta, type RedisSubscribeMeta, redis };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { jsonCodec, isPattern } from '@pubber-subber/core';
|
|
2
|
+
import { Redis } from 'ioredis';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
function redis(opts = {}) {
|
|
6
|
+
const codec = opts.codec ?? jsonCodec();
|
|
7
|
+
let publisher = null;
|
|
8
|
+
let subscriber = null;
|
|
9
|
+
let owned = true;
|
|
10
|
+
const exactHandlers = /* @__PURE__ */ new Map();
|
|
11
|
+
const patternHandlers = /* @__PURE__ */ new Map();
|
|
12
|
+
let idCounter = 0;
|
|
13
|
+
let installedListeners = false;
|
|
14
|
+
const createClient = () => {
|
|
15
|
+
if (opts.url && opts.options) return new Redis(opts.url, opts.options);
|
|
16
|
+
if (opts.url) return new Redis(opts.url);
|
|
17
|
+
if (opts.options) return new Redis(opts.options);
|
|
18
|
+
return new Redis();
|
|
19
|
+
};
|
|
20
|
+
const ensure = () => {
|
|
21
|
+
if (publisher && subscriber) return { publisher, subscriber };
|
|
22
|
+
if (opts.clients) {
|
|
23
|
+
publisher = opts.clients.publisher;
|
|
24
|
+
subscriber = opts.clients.subscriber;
|
|
25
|
+
owned = false;
|
|
26
|
+
} else {
|
|
27
|
+
publisher = createClient();
|
|
28
|
+
subscriber = createClient();
|
|
29
|
+
owned = true;
|
|
30
|
+
}
|
|
31
|
+
if (!installedListeners) {
|
|
32
|
+
installedListeners = true;
|
|
33
|
+
subscriber.on("message", (channel, message) => {
|
|
34
|
+
deliver(exactHandlers.get(channel), channel, message, void 0);
|
|
35
|
+
});
|
|
36
|
+
subscriber.on("pmessage", (pattern, channel, message) => {
|
|
37
|
+
deliver(patternHandlers.get(pattern), channel, message, { pattern });
|
|
38
|
+
});
|
|
39
|
+
subscriber.on("ready", () => {
|
|
40
|
+
for (const channel of exactHandlers.keys()) {
|
|
41
|
+
subscriber?.subscribe(channel).catch(noop);
|
|
42
|
+
}
|
|
43
|
+
for (const pattern of patternHandlers.keys()) {
|
|
44
|
+
subscriber?.psubscribe(pattern).catch(noop);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return { publisher, subscriber };
|
|
49
|
+
};
|
|
50
|
+
const deliver = (handlers, channel, wireMessage, extraMeta) => {
|
|
51
|
+
if (!handlers || handlers.size === 0) return;
|
|
52
|
+
let payload;
|
|
53
|
+
try {
|
|
54
|
+
payload = codec.decode(wireMessage);
|
|
55
|
+
} catch {
|
|
56
|
+
payload = wireMessage;
|
|
57
|
+
}
|
|
58
|
+
const msg = {
|
|
59
|
+
topic: channel,
|
|
60
|
+
payload,
|
|
61
|
+
raw: wireMessage,
|
|
62
|
+
meta: extraMeta
|
|
63
|
+
};
|
|
64
|
+
for (const handler of [...handlers]) {
|
|
65
|
+
Promise.resolve(handler(msg)).catch(noop);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
name: "redis",
|
|
70
|
+
capabilities: { publish: true, subscribe: true, patternSubscribe: true, ack: false },
|
|
71
|
+
async connect() {
|
|
72
|
+
const { publisher: p, subscriber: s } = ensure();
|
|
73
|
+
await Promise.all([waitReady(p), waitReady(s)]);
|
|
74
|
+
},
|
|
75
|
+
async disconnect() {
|
|
76
|
+
if (!owned) {
|
|
77
|
+
publisher = null;
|
|
78
|
+
subscriber = null;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const closing = [];
|
|
82
|
+
if (subscriber) closing.push(subscriber.quit().catch(noop));
|
|
83
|
+
if (publisher) closing.push(publisher.quit().catch(noop));
|
|
84
|
+
await Promise.all(closing);
|
|
85
|
+
publisher = null;
|
|
86
|
+
subscriber = null;
|
|
87
|
+
installedListeners = false;
|
|
88
|
+
exactHandlers.clear();
|
|
89
|
+
patternHandlers.clear();
|
|
90
|
+
},
|
|
91
|
+
async publish(topic, payload, meta) {
|
|
92
|
+
const { publisher: p } = ensure();
|
|
93
|
+
const channel = meta?.channel ?? topic;
|
|
94
|
+
const encoded = codec.encode(payload);
|
|
95
|
+
const wire = typeof encoded === "string" ? encoded : Buffer.from(encoded);
|
|
96
|
+
await p.publish(channel, wire);
|
|
97
|
+
},
|
|
98
|
+
async subscribe(topic, handler, meta) {
|
|
99
|
+
const { subscriber: s } = ensure();
|
|
100
|
+
const usePattern = meta?.pattern ?? isPattern(topic);
|
|
101
|
+
const map = usePattern ? patternHandlers : exactHandlers;
|
|
102
|
+
let set = map.get(topic);
|
|
103
|
+
const firstSubscriber = !set;
|
|
104
|
+
if (!set) {
|
|
105
|
+
set = /* @__PURE__ */ new Set();
|
|
106
|
+
map.set(topic, set);
|
|
107
|
+
}
|
|
108
|
+
if (firstSubscriber) {
|
|
109
|
+
if (usePattern) await s.psubscribe(topic);
|
|
110
|
+
else await s.subscribe(topic);
|
|
111
|
+
}
|
|
112
|
+
set.add(handler);
|
|
113
|
+
idCounter += 1;
|
|
114
|
+
const id = `redis-${idCounter}`;
|
|
115
|
+
const ownSet = set;
|
|
116
|
+
return {
|
|
117
|
+
id,
|
|
118
|
+
topic,
|
|
119
|
+
unsubscribe: async () => {
|
|
120
|
+
ownSet.delete(handler);
|
|
121
|
+
if (ownSet.size === 0) {
|
|
122
|
+
map.delete(topic);
|
|
123
|
+
if (subscriber) {
|
|
124
|
+
if (usePattern) await subscriber.punsubscribe(topic).catch(noop);
|
|
125
|
+
else await subscriber.unsubscribe(topic).catch(noop);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function noop() {
|
|
134
|
+
}
|
|
135
|
+
async function waitReady(client) {
|
|
136
|
+
if (client.status === "ready") return;
|
|
137
|
+
if (client.status === "wait" || client.status === "connecting") {
|
|
138
|
+
await new Promise((resolve, reject) => {
|
|
139
|
+
const onReady = () => {
|
|
140
|
+
cleanup();
|
|
141
|
+
resolve();
|
|
142
|
+
};
|
|
143
|
+
const onError = (err) => {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(err);
|
|
146
|
+
};
|
|
147
|
+
const cleanup = () => {
|
|
148
|
+
client.off("ready", onReady);
|
|
149
|
+
client.off("error", onError);
|
|
150
|
+
};
|
|
151
|
+
client.once("ready", onReady);
|
|
152
|
+
client.once("error", onError);
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await client.connect().catch((err) => {
|
|
157
|
+
if (!String(err?.message ?? "").includes("connect")) throw err;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { redis };
|
|
162
|
+
//# sourceMappingURL=index.js.map
|
|
163
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import {\n type AdapterMessage,\n type Codec,\n type MessageHandler,\n type PubSubAdapter,\n isPattern,\n jsonCodec,\n} from '@pubber-subber/core';\nimport { Redis, type RedisOptions } from 'ioredis';\n\nexport interface RedisAdapterOptions {\n /** Redis connection URL, e.g. `redis://localhost:6379`. */\n url?: string;\n /** ioredis-compatible options. Combined with `url` if both are passed. */\n options?: RedisOptions;\n /**\n * Bring-your-own clients. When provided, the adapter will NOT manage their\n * lifecycle (connect/disconnect become no-ops on `disconnect`).\n * `publisher` and `subscriber` must be distinct clients because Redis\n * disallows non-pub/sub commands on a subscribed connection.\n */\n clients?: { publisher: Redis; subscriber: Redis };\n /** Payload codec. Default: JSON. */\n codec?: Codec;\n}\n\nexport interface RedisPublishMeta {\n /** Override the wire channel for this publish; the AdapterMessage still carries `topic`. */\n channel?: string;\n}\n\nexport interface RedisSubscribeMeta {\n /** Force PSUBSCRIBE even when `topic` has no wildcards. */\n pattern?: boolean;\n}\n\nexport function redis(\n opts: RedisAdapterOptions = {},\n): PubSubAdapter<RedisPublishMeta, RedisSubscribeMeta> {\n const codec = opts.codec ?? jsonCodec();\n\n let publisher: Redis | null = null;\n let subscriber: Redis | null = null;\n let owned = true;\n\n const exactHandlers = new Map<string, Set<MessageHandler>>();\n const patternHandlers = new Map<string, Set<MessageHandler>>();\n let idCounter = 0;\n let installedListeners = false;\n\n const createClient = (): Redis => {\n if (opts.url && opts.options) return new Redis(opts.url, opts.options);\n if (opts.url) return new Redis(opts.url);\n if (opts.options) return new Redis(opts.options);\n return new Redis();\n };\n\n const ensure = (): { publisher: Redis; subscriber: Redis } => {\n if (publisher && subscriber) return { publisher, subscriber };\n\n if (opts.clients) {\n publisher = opts.clients.publisher;\n subscriber = opts.clients.subscriber;\n owned = false;\n } else {\n publisher = createClient();\n subscriber = createClient();\n owned = true;\n }\n\n if (!installedListeners) {\n installedListeners = true;\n subscriber.on('message', (channel: string, message: string) => {\n deliver(exactHandlers.get(channel), channel, message, undefined);\n });\n subscriber.on('pmessage', (pattern: string, channel: string, message: string) => {\n deliver(patternHandlers.get(pattern), channel, message, { pattern });\n });\n // Re-subscribe after a reconnect.\n subscriber.on('ready', () => {\n for (const channel of exactHandlers.keys()) {\n subscriber?.subscribe(channel).catch(noop);\n }\n for (const pattern of patternHandlers.keys()) {\n subscriber?.psubscribe(pattern).catch(noop);\n }\n });\n }\n\n return { publisher, subscriber };\n };\n\n const deliver = (\n handlers: Set<MessageHandler> | undefined,\n channel: string,\n wireMessage: string,\n extraMeta: Record<string, unknown> | undefined,\n ) => {\n if (!handlers || handlers.size === 0) return;\n let payload: unknown;\n try {\n payload = codec.decode(wireMessage);\n } catch {\n payload = wireMessage;\n }\n const msg: AdapterMessage = {\n topic: channel,\n payload,\n raw: wireMessage,\n meta: extraMeta,\n };\n for (const handler of [...handlers]) {\n Promise.resolve(handler(msg)).catch(noop);\n }\n };\n\n return {\n name: 'redis',\n capabilities: { publish: true, subscribe: true, patternSubscribe: true, ack: false },\n\n async connect() {\n const { publisher: p, subscriber: s } = ensure();\n await Promise.all([waitReady(p), waitReady(s)]);\n },\n\n async disconnect() {\n if (!owned) {\n publisher = null;\n subscriber = null;\n return;\n }\n const closing: Promise<unknown>[] = [];\n if (subscriber) closing.push(subscriber.quit().catch(noop));\n if (publisher) closing.push(publisher.quit().catch(noop));\n await Promise.all(closing);\n publisher = null;\n subscriber = null;\n installedListeners = false;\n exactHandlers.clear();\n patternHandlers.clear();\n },\n\n async publish(topic, payload, meta) {\n const { publisher: p } = ensure();\n const channel = meta?.channel ?? topic;\n const encoded = codec.encode(payload);\n const wire = typeof encoded === 'string' ? encoded : Buffer.from(encoded);\n await p.publish(channel, wire as never);\n },\n\n async subscribe(topic, handler, meta) {\n const { subscriber: s } = ensure();\n const usePattern = meta?.pattern ?? isPattern(topic);\n const map = usePattern ? patternHandlers : exactHandlers;\n let set = map.get(topic);\n const firstSubscriber = !set;\n if (!set) {\n set = new Set();\n map.set(topic, set);\n }\n if (firstSubscriber) {\n if (usePattern) await s.psubscribe(topic);\n else await s.subscribe(topic);\n }\n set.add(handler);\n idCounter += 1;\n const id = `redis-${idCounter}`;\n const ownSet = set;\n return {\n id,\n topic,\n unsubscribe: async () => {\n ownSet.delete(handler);\n if (ownSet.size === 0) {\n map.delete(topic);\n if (subscriber) {\n if (usePattern) await subscriber.punsubscribe(topic).catch(noop);\n else await subscriber.unsubscribe(topic).catch(noop);\n }\n }\n },\n };\n },\n };\n}\n\nfunction noop(): void {\n // intentional\n}\n\nasync function waitReady(client: Redis): Promise<void> {\n if (client.status === 'ready') return;\n if (client.status === 'wait' || client.status === 'connecting') {\n await new Promise<void>((resolve, reject) => {\n const onReady = () => {\n cleanup();\n resolve();\n };\n const onError = (err: unknown) => {\n cleanup();\n reject(err);\n };\n const cleanup = () => {\n client.off('ready', onReady);\n client.off('error', onError);\n };\n client.once('ready', onReady);\n client.once('error', onError);\n });\n return;\n }\n await client.connect().catch((err) => {\n // Already connected/connecting is fine.\n if (!String(err?.message ?? '').includes('connect')) throw err;\n });\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pubber-subber/redis",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Redis (and Redis-compatible: Valkey, KeyDB, Dragonfly) pub/sub adapter for @pubber-subber.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pubsub",
|
|
7
|
+
"pub-sub",
|
|
8
|
+
"messaging",
|
|
9
|
+
"events",
|
|
10
|
+
"adapter",
|
|
11
|
+
"redis",
|
|
12
|
+
"valkey",
|
|
13
|
+
"keydb",
|
|
14
|
+
"dragonfly",
|
|
15
|
+
"ioredis",
|
|
16
|
+
"typescript"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/samishal1998/pubber-subber/tree/main/packages/redis#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/samishal1998/pubber-subber.git",
|
|
22
|
+
"directory": "packages/redis"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/samishal1998/pubber-subber/issues"
|
|
26
|
+
},
|
|
27
|
+
"author": "Sami Mishal",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./dist/index.d.cts",
|
|
39
|
+
"default": "./dist/index.cjs"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"main": "./dist/index.cjs",
|
|
45
|
+
"module": "./dist/index.js",
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"prebuild": "node ../../scripts/swap-package-json.mjs build",
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:integration": "vitest run",
|
|
54
|
+
"test:watch": "vitest",
|
|
55
|
+
"prepublishOnly": "node ../../scripts/swap-package-json.mjs publish",
|
|
56
|
+
"postpublish": "node ../../scripts/swap-package-json.mjs build"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@pubber-subber/core": "^0.0.1",
|
|
60
|
+
"ioredis": "^5.0.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@pubber-subber/core": "workspace:*",
|
|
64
|
+
"ioredis": "^5.4.1"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AdapterMessage,
|
|
3
|
+
type Codec,
|
|
4
|
+
type MessageHandler,
|
|
5
|
+
type PubSubAdapter,
|
|
6
|
+
isPattern,
|
|
7
|
+
jsonCodec,
|
|
8
|
+
} from '@pubber-subber/core';
|
|
9
|
+
import { Redis, type RedisOptions } from 'ioredis';
|
|
10
|
+
|
|
11
|
+
export interface RedisAdapterOptions {
|
|
12
|
+
/** Redis connection URL, e.g. `redis://localhost:6379`. */
|
|
13
|
+
url?: string;
|
|
14
|
+
/** ioredis-compatible options. Combined with `url` if both are passed. */
|
|
15
|
+
options?: RedisOptions;
|
|
16
|
+
/**
|
|
17
|
+
* Bring-your-own clients. When provided, the adapter will NOT manage their
|
|
18
|
+
* lifecycle (connect/disconnect become no-ops on `disconnect`).
|
|
19
|
+
* `publisher` and `subscriber` must be distinct clients because Redis
|
|
20
|
+
* disallows non-pub/sub commands on a subscribed connection.
|
|
21
|
+
*/
|
|
22
|
+
clients?: { publisher: Redis; subscriber: Redis };
|
|
23
|
+
/** Payload codec. Default: JSON. */
|
|
24
|
+
codec?: Codec;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RedisPublishMeta {
|
|
28
|
+
/** Override the wire channel for this publish; the AdapterMessage still carries `topic`. */
|
|
29
|
+
channel?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RedisSubscribeMeta {
|
|
33
|
+
/** Force PSUBSCRIBE even when `topic` has no wildcards. */
|
|
34
|
+
pattern?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function redis(
|
|
38
|
+
opts: RedisAdapterOptions = {},
|
|
39
|
+
): PubSubAdapter<RedisPublishMeta, RedisSubscribeMeta> {
|
|
40
|
+
const codec = opts.codec ?? jsonCodec();
|
|
41
|
+
|
|
42
|
+
let publisher: Redis | null = null;
|
|
43
|
+
let subscriber: Redis | null = null;
|
|
44
|
+
let owned = true;
|
|
45
|
+
|
|
46
|
+
const exactHandlers = new Map<string, Set<MessageHandler>>();
|
|
47
|
+
const patternHandlers = new Map<string, Set<MessageHandler>>();
|
|
48
|
+
let idCounter = 0;
|
|
49
|
+
let installedListeners = false;
|
|
50
|
+
|
|
51
|
+
const createClient = (): Redis => {
|
|
52
|
+
if (opts.url && opts.options) return new Redis(opts.url, opts.options);
|
|
53
|
+
if (opts.url) return new Redis(opts.url);
|
|
54
|
+
if (opts.options) return new Redis(opts.options);
|
|
55
|
+
return new Redis();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ensure = (): { publisher: Redis; subscriber: Redis } => {
|
|
59
|
+
if (publisher && subscriber) return { publisher, subscriber };
|
|
60
|
+
|
|
61
|
+
if (opts.clients) {
|
|
62
|
+
publisher = opts.clients.publisher;
|
|
63
|
+
subscriber = opts.clients.subscriber;
|
|
64
|
+
owned = false;
|
|
65
|
+
} else {
|
|
66
|
+
publisher = createClient();
|
|
67
|
+
subscriber = createClient();
|
|
68
|
+
owned = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!installedListeners) {
|
|
72
|
+
installedListeners = true;
|
|
73
|
+
subscriber.on('message', (channel: string, message: string) => {
|
|
74
|
+
deliver(exactHandlers.get(channel), channel, message, undefined);
|
|
75
|
+
});
|
|
76
|
+
subscriber.on('pmessage', (pattern: string, channel: string, message: string) => {
|
|
77
|
+
deliver(patternHandlers.get(pattern), channel, message, { pattern });
|
|
78
|
+
});
|
|
79
|
+
// Re-subscribe after a reconnect.
|
|
80
|
+
subscriber.on('ready', () => {
|
|
81
|
+
for (const channel of exactHandlers.keys()) {
|
|
82
|
+
subscriber?.subscribe(channel).catch(noop);
|
|
83
|
+
}
|
|
84
|
+
for (const pattern of patternHandlers.keys()) {
|
|
85
|
+
subscriber?.psubscribe(pattern).catch(noop);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { publisher, subscriber };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const deliver = (
|
|
94
|
+
handlers: Set<MessageHandler> | undefined,
|
|
95
|
+
channel: string,
|
|
96
|
+
wireMessage: string,
|
|
97
|
+
extraMeta: Record<string, unknown> | undefined,
|
|
98
|
+
) => {
|
|
99
|
+
if (!handlers || handlers.size === 0) return;
|
|
100
|
+
let payload: unknown;
|
|
101
|
+
try {
|
|
102
|
+
payload = codec.decode(wireMessage);
|
|
103
|
+
} catch {
|
|
104
|
+
payload = wireMessage;
|
|
105
|
+
}
|
|
106
|
+
const msg: AdapterMessage = {
|
|
107
|
+
topic: channel,
|
|
108
|
+
payload,
|
|
109
|
+
raw: wireMessage,
|
|
110
|
+
meta: extraMeta,
|
|
111
|
+
};
|
|
112
|
+
for (const handler of [...handlers]) {
|
|
113
|
+
Promise.resolve(handler(msg)).catch(noop);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
name: 'redis',
|
|
119
|
+
capabilities: { publish: true, subscribe: true, patternSubscribe: true, ack: false },
|
|
120
|
+
|
|
121
|
+
async connect() {
|
|
122
|
+
const { publisher: p, subscriber: s } = ensure();
|
|
123
|
+
await Promise.all([waitReady(p), waitReady(s)]);
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async disconnect() {
|
|
127
|
+
if (!owned) {
|
|
128
|
+
publisher = null;
|
|
129
|
+
subscriber = null;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const closing: Promise<unknown>[] = [];
|
|
133
|
+
if (subscriber) closing.push(subscriber.quit().catch(noop));
|
|
134
|
+
if (publisher) closing.push(publisher.quit().catch(noop));
|
|
135
|
+
await Promise.all(closing);
|
|
136
|
+
publisher = null;
|
|
137
|
+
subscriber = null;
|
|
138
|
+
installedListeners = false;
|
|
139
|
+
exactHandlers.clear();
|
|
140
|
+
patternHandlers.clear();
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async publish(topic, payload, meta) {
|
|
144
|
+
const { publisher: p } = ensure();
|
|
145
|
+
const channel = meta?.channel ?? topic;
|
|
146
|
+
const encoded = codec.encode(payload);
|
|
147
|
+
const wire = typeof encoded === 'string' ? encoded : Buffer.from(encoded);
|
|
148
|
+
await p.publish(channel, wire as never);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async subscribe(topic, handler, meta) {
|
|
152
|
+
const { subscriber: s } = ensure();
|
|
153
|
+
const usePattern = meta?.pattern ?? isPattern(topic);
|
|
154
|
+
const map = usePattern ? patternHandlers : exactHandlers;
|
|
155
|
+
let set = map.get(topic);
|
|
156
|
+
const firstSubscriber = !set;
|
|
157
|
+
if (!set) {
|
|
158
|
+
set = new Set();
|
|
159
|
+
map.set(topic, set);
|
|
160
|
+
}
|
|
161
|
+
if (firstSubscriber) {
|
|
162
|
+
if (usePattern) await s.psubscribe(topic);
|
|
163
|
+
else await s.subscribe(topic);
|
|
164
|
+
}
|
|
165
|
+
set.add(handler);
|
|
166
|
+
idCounter += 1;
|
|
167
|
+
const id = `redis-${idCounter}`;
|
|
168
|
+
const ownSet = set;
|
|
169
|
+
return {
|
|
170
|
+
id,
|
|
171
|
+
topic,
|
|
172
|
+
unsubscribe: async () => {
|
|
173
|
+
ownSet.delete(handler);
|
|
174
|
+
if (ownSet.size === 0) {
|
|
175
|
+
map.delete(topic);
|
|
176
|
+
if (subscriber) {
|
|
177
|
+
if (usePattern) await subscriber.punsubscribe(topic).catch(noop);
|
|
178
|
+
else await subscriber.unsubscribe(topic).catch(noop);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function noop(): void {
|
|
188
|
+
// intentional
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function waitReady(client: Redis): Promise<void> {
|
|
192
|
+
if (client.status === 'ready') return;
|
|
193
|
+
if (client.status === 'wait' || client.status === 'connecting') {
|
|
194
|
+
await new Promise<void>((resolve, reject) => {
|
|
195
|
+
const onReady = () => {
|
|
196
|
+
cleanup();
|
|
197
|
+
resolve();
|
|
198
|
+
};
|
|
199
|
+
const onError = (err: unknown) => {
|
|
200
|
+
cleanup();
|
|
201
|
+
reject(err);
|
|
202
|
+
};
|
|
203
|
+
const cleanup = () => {
|
|
204
|
+
client.off('ready', onReady);
|
|
205
|
+
client.off('error', onError);
|
|
206
|
+
};
|
|
207
|
+
client.once('ready', onReady);
|
|
208
|
+
client.once('error', onError);
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
await client.connect().catch((err) => {
|
|
213
|
+
// Already connected/connecting is fine.
|
|
214
|
+
if (!String(err?.message ?? '').includes('connect')) throw err;
|
|
215
|
+
});
|
|
216
|
+
}
|