@socket.io/redis-streams-adapter 0.2.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 +21 -13
- package/dist/adapter.d.ts +41 -5
- package/dist/adapter.js +126 -44
- package/dist/util.d.ts +9 -1
- package/dist/util.js +124 -15
- package/package.json +4 -10
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Socket.IO Redis Streams adapter
|
|
2
2
|
|
|
3
|
-
The `@socket.io/redis-streams-adapter` package allows broadcasting packets between multiple Socket.IO servers
|
|
3
|
+
The `@socket.io/redis-streams-adapter` package allows broadcasting packets between multiple Socket.IO servers using:
|
|
4
|
+
|
|
5
|
+
- Redis Streams: https://redis.io/docs/latest/develop/data-types/streams/
|
|
6
|
+
- Redis PUB/SUB for ephemeral communications: https://redis.io/docs/latest/develop/pubsub/
|
|
4
7
|
|
|
5
8
|
**Table of contents**
|
|
6
9
|
|
|
@@ -127,24 +130,29 @@ io.listen(3000);
|
|
|
127
130
|
|
|
128
131
|
## Options
|
|
129
132
|
|
|
130
|
-
| Name | Description
|
|
131
|
-
|
|
132
|
-
| `streamName` | The name of the Redis stream.
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
133
|
+
| Name | Description | Default value |
|
|
134
|
+
|---------------------|-----------------------------------------------------------------------------------------------------------------------|----------------|
|
|
135
|
+
| `streamName` | The name of the Redis stream. | `socket.io` |
|
|
136
|
+
| `streamCount` | The number of streams to use to scale horizontally. | `1` |
|
|
137
|
+
| `channelPrefix` | The prefix of the Redis PUB/SUB channels used to communicate between the nodes. | `socket.io` |
|
|
138
|
+
| `useShardedPubSub` | Whether to use sharded PUB/SUB (added in Redis 7.0) to communicate between the nodes. | `false` |
|
|
139
|
+
| `maxLen` | The maximum size of the stream. Almost exact trimming (~) is used. | `10_000` |
|
|
140
|
+
| `readCount` | The number of elements to fetch per XREAD call. | `100` |
|
|
141
|
+
| `blockTimeInMs` | The number of ms before the XREAD call times out. | `5_000` |
|
|
142
|
+
| `sessionKeyPrefix` | The prefix of the key used to store the Socket.IO session, when the connection state recovery feature is enabled. | `sio:session:` |
|
|
143
|
+
| `heartbeatInterval` | The number of ms between two heartbeats. | `5_000` |
|
|
144
|
+
| `heartbeatTimeout` | The number of ms without heartbeat before we consider a node down. | `10_000` |
|
|
145
|
+
| `onlyPlaintext` | Whether the transmitted data contains only JSON-serializable objects without binary data (Buffer, ArrayBuffer, etc.). | `false` |
|
|
138
146
|
|
|
139
147
|
## How it works
|
|
140
148
|
|
|
141
|
-
The adapter will use a [Redis stream](https://redis.io/docs/data-types/streams/) to forward events between the Socket.IO servers.
|
|
149
|
+
The adapter will use a [Redis stream](https://redis.io/docs/latest/develop/data-types/streams/) to forward events between the Socket.IO servers.
|
|
142
150
|
|
|
143
151
|
Notes:
|
|
144
152
|
|
|
145
|
-
- a single stream is used for all namespaces
|
|
146
|
-
- the `maxLen` option allows
|
|
147
|
-
- unlike the adapter based on Redis PUB/SUB mechanism, this adapter will properly handle any temporary disconnection to the Redis server and resume the stream
|
|
153
|
+
- by default, a single stream is used for all namespaces (see the `streamCount` option)
|
|
154
|
+
- the `maxLen` option allows limiting the size of the stream
|
|
155
|
+
- unlike the adapter based on the Redis PUB/SUB mechanism, this adapter will properly handle any temporary disconnection to the Redis server and resume the stream
|
|
148
156
|
- if [connection state recovery](https://socket.io/docs/v4/connection-state-recovery) is enabled, the sessions will be stored in Redis as a classic key/value pair
|
|
149
157
|
|
|
150
158
|
## License
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ClusterAdapter } from "socket.io-adapter";
|
|
2
2
|
import type { ClusterAdapterOptions, ClusterMessage, PrivateSessionId, Session, ServerId, ClusterResponse } from "socket.io-adapter";
|
|
3
3
|
export interface RedisStreamsAdapterOptions {
|
|
4
4
|
/**
|
|
5
|
-
* The name of the Redis stream.
|
|
5
|
+
* The name of the Redis stream (or the prefix used when using multiple streams).
|
|
6
|
+
*
|
|
7
|
+
* @see streamCount
|
|
6
8
|
* @default "socket.io"
|
|
7
9
|
*/
|
|
8
10
|
streamName?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The number of streams to use to scale horizontally.
|
|
13
|
+
*
|
|
14
|
+
* Each namespace is routed to a specific stream to ensure the ordering of messages.
|
|
15
|
+
*
|
|
16
|
+
* Note: using multiple streams is useless when using a single namespace.
|
|
17
|
+
*
|
|
18
|
+
* @default 1
|
|
19
|
+
*/
|
|
20
|
+
streamCount?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The prefix of the Redis PUB/SUB channels used to communicate between the nodes.
|
|
23
|
+
* @default "socket.io"
|
|
24
|
+
*/
|
|
25
|
+
channelPrefix?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to use sharded PUB/SUB (added in Redis 7.0) to communicate between the nodes.
|
|
28
|
+
* @default false
|
|
29
|
+
* @see https://redis.io/docs/latest/develop/pubsub/#sharded-pubsub
|
|
30
|
+
*/
|
|
31
|
+
useShardedPubSub?: boolean;
|
|
9
32
|
/**
|
|
10
33
|
* The maximum size of the stream. Almost exact trimming (~) is used.
|
|
11
34
|
* @default 10_000
|
|
@@ -16,11 +39,23 @@ export interface RedisStreamsAdapterOptions {
|
|
|
16
39
|
* @default 100
|
|
17
40
|
*/
|
|
18
41
|
readCount?: number;
|
|
42
|
+
/**
|
|
43
|
+
* The number of ms before the XREAD call times out.
|
|
44
|
+
* @default 5_000
|
|
45
|
+
* @see https://redis.io/docs/latest/commands/xread/#blocking-for-data
|
|
46
|
+
*/
|
|
47
|
+
blockTimeInMs?: number;
|
|
19
48
|
/**
|
|
20
49
|
* The prefix of the key used to store the Socket.IO session, when the connection state recovery feature is enabled.
|
|
21
50
|
* @default "sio:session:"
|
|
22
51
|
*/
|
|
23
52
|
sessionKeyPrefix?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the transmitted data contains only JSON-serializable objects without binary data (Buffer, ArrayBuffer, etc.).
|
|
55
|
+
* When enabled, binary data checks are skipped for better performance.
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
onlyPlaintext?: boolean;
|
|
24
59
|
}
|
|
25
60
|
interface RawClusterMessage {
|
|
26
61
|
uid: string;
|
|
@@ -35,14 +70,15 @@ interface RawClusterMessage {
|
|
|
35
70
|
* @param opts - additional options
|
|
36
71
|
*/
|
|
37
72
|
export declare function createAdapter(redisClient: any, opts?: RedisStreamsAdapterOptions & ClusterAdapterOptions): (nsp: any) => RedisStreamsAdapter;
|
|
38
|
-
declare class RedisStreamsAdapter extends
|
|
73
|
+
declare class RedisStreamsAdapter extends ClusterAdapter {
|
|
39
74
|
#private;
|
|
40
|
-
constructor(nsp: any, redisClient: any, opts: Required<RedisStreamsAdapterOptions> & ClusterAdapterOptions);
|
|
75
|
+
constructor(nsp: any, redisClient: any, subClientPromise: Promise<any>, opts: Required<RedisStreamsAdapterOptions> & ClusterAdapterOptions);
|
|
41
76
|
doPublish(message: ClusterMessage): any;
|
|
42
77
|
protected doPublishResponse(requesterUid: ServerId, response: ClusterResponse): Promise<void>;
|
|
43
|
-
|
|
78
|
+
private encode;
|
|
44
79
|
onRawMessage(rawMessage: RawClusterMessage, offset: string): any;
|
|
45
80
|
static decode(rawMessage: RawClusterMessage): ClusterMessage;
|
|
81
|
+
serverCount(): Promise<number>;
|
|
46
82
|
persistSession(session: any): void;
|
|
47
83
|
restoreSession(pid: PrivateSessionId, offset: string): Promise<Session>;
|
|
48
84
|
/**
|
package/dist/adapter.js
CHANGED
|
@@ -7,36 +7,39 @@ const debug_1 = require("debug");
|
|
|
7
7
|
const util_1 = require("./util");
|
|
8
8
|
const debug = (0, debug_1.default)("socket.io-redis-streams-adapter");
|
|
9
9
|
const RESTORE_SESSION_MAX_XRANGE_CALLS = 100;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
async function createReadOnlyClients(redisClient, opts) {
|
|
11
|
+
if (opts.streamCount === 1) {
|
|
12
|
+
const newClient = await (0, util_1.duplicateClient)(redisClient);
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
client: newClient,
|
|
16
|
+
streamName: opts.streamName,
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const streamNames = [];
|
|
22
|
+
for (let i = 0; i < opts.streamCount; i++) {
|
|
23
|
+
const newClient = await (0, util_1.duplicateClient)(redisClient);
|
|
24
|
+
streamNames.push({
|
|
25
|
+
client: newClient,
|
|
26
|
+
streamName: opts.streamName + "-" + i,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return streamNames;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function startPolling(redisClient, streamName, options, onMessage, signal) {
|
|
26
33
|
let offset = "$";
|
|
27
|
-
let polling = false;
|
|
28
|
-
let shouldClose = false;
|
|
29
34
|
async function poll() {
|
|
30
35
|
try {
|
|
31
|
-
let response = await (0, util_1.XREAD)(redisClient,
|
|
36
|
+
let response = await (0, util_1.XREAD)(redisClient, streamName, offset, options.readCount, options.blockTimeInMs);
|
|
32
37
|
if (response) {
|
|
33
38
|
for (const entry of response[0].messages) {
|
|
34
39
|
debug("reading entry %s", entry.id);
|
|
35
40
|
const message = entry.message;
|
|
36
41
|
if (message.nsp) {
|
|
37
|
-
|
|
38
|
-
.get(message.nsp)
|
|
39
|
-
?.onRawMessage(message, entry.id);
|
|
42
|
+
onMessage(message, entry.id);
|
|
40
43
|
}
|
|
41
44
|
offset = entry.id;
|
|
42
45
|
}
|
|
@@ -45,26 +48,62 @@ function createAdapter(redisClient, opts) {
|
|
|
45
48
|
catch (e) {
|
|
46
49
|
debug("something went wrong while consuming the stream: %s", e.message);
|
|
47
50
|
}
|
|
48
|
-
if (
|
|
49
|
-
|
|
51
|
+
if (signal.aborted) {
|
|
52
|
+
redisClient.disconnect();
|
|
50
53
|
}
|
|
51
54
|
else {
|
|
52
|
-
|
|
55
|
+
poll();
|
|
53
56
|
}
|
|
54
57
|
}
|
|
58
|
+
poll();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Returns a function that will create a new adapter instance.
|
|
62
|
+
*
|
|
63
|
+
* @param redisClient - a Redis client that will be used to publish messages
|
|
64
|
+
* @param opts - additional options
|
|
65
|
+
*/
|
|
66
|
+
function createAdapter(redisClient, opts) {
|
|
67
|
+
const namespaceToAdapters = new Map();
|
|
68
|
+
const options = Object.assign({
|
|
69
|
+
streamName: "socket.io",
|
|
70
|
+
streamCount: 1,
|
|
71
|
+
channelPrefix: "socket.io",
|
|
72
|
+
useShardedPubSub: false,
|
|
73
|
+
maxLen: 10000,
|
|
74
|
+
readCount: 100,
|
|
75
|
+
blockTimeInMs: 5000,
|
|
76
|
+
sessionKeyPrefix: "sio:session:",
|
|
77
|
+
heartbeatInterval: 5000,
|
|
78
|
+
heartbeatTimeout: 10000,
|
|
79
|
+
onlyPlaintext: false,
|
|
80
|
+
}, opts);
|
|
81
|
+
function onMessage(message, offset) {
|
|
82
|
+
namespaceToAdapters.get(message.nsp)?.onRawMessage(message, offset);
|
|
83
|
+
}
|
|
84
|
+
let readOnlyClients;
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
// note: we create one Redis client per stream so they don't block each other. We could also have used one Redis
|
|
87
|
+
// client per master in the cluster (reading from the streams assigned to the given node), but that would have been
|
|
88
|
+
// trickier to implement.
|
|
89
|
+
createReadOnlyClients(redisClient, options).then((clients) => {
|
|
90
|
+
readOnlyClients = clients;
|
|
91
|
+
for (const { client, streamName } of readOnlyClients) {
|
|
92
|
+
startPolling(client, streamName, options, onMessage, controller.signal);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const subClientPromise = (0, util_1.duplicateClient)(redisClient);
|
|
96
|
+
controller.signal.addEventListener("abort", () => {
|
|
97
|
+
subClientPromise.then((subClient) => subClient.disconnect());
|
|
98
|
+
});
|
|
55
99
|
return function (nsp) {
|
|
56
|
-
const adapter = new RedisStreamsAdapter(nsp, redisClient, options);
|
|
100
|
+
const adapter = new RedisStreamsAdapter(nsp, redisClient, subClientPromise, options);
|
|
57
101
|
namespaceToAdapters.set(nsp.name, adapter);
|
|
58
|
-
if (!polling) {
|
|
59
|
-
polling = true;
|
|
60
|
-
shouldClose = false;
|
|
61
|
-
poll();
|
|
62
|
-
}
|
|
63
102
|
const defaultClose = adapter.close;
|
|
64
103
|
adapter.close = () => {
|
|
65
104
|
namespaceToAdapters.delete(nsp.name);
|
|
66
105
|
if (namespaceToAdapters.size === 0) {
|
|
67
|
-
|
|
106
|
+
controller.abort();
|
|
68
107
|
}
|
|
69
108
|
defaultClose.call(adapter);
|
|
70
109
|
};
|
|
@@ -72,24 +111,62 @@ function createAdapter(redisClient, opts) {
|
|
|
72
111
|
};
|
|
73
112
|
}
|
|
74
113
|
exports.createAdapter = createAdapter;
|
|
75
|
-
|
|
114
|
+
function computeStreamName(namespaceName, opts) {
|
|
115
|
+
if (opts.streamCount === 1) {
|
|
116
|
+
return opts.streamName;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const i = (0, util_1.hashCode)(namespaceName) % opts.streamCount;
|
|
120
|
+
return opts.streamName + "-" + i;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isEphemeral(message) {
|
|
124
|
+
const isBroadcastWithAck = message.type === socket_io_adapter_1.MessageType.BROADCAST &&
|
|
125
|
+
message.data.requestId !== undefined;
|
|
126
|
+
return (isBroadcastWithAck ||
|
|
127
|
+
[socket_io_adapter_1.MessageType.SERVER_SIDE_EMIT, socket_io_adapter_1.MessageType.FETCH_SOCKETS].includes(message.type));
|
|
128
|
+
}
|
|
129
|
+
class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapter {
|
|
76
130
|
#redisClient;
|
|
77
131
|
#opts;
|
|
78
|
-
|
|
79
|
-
|
|
132
|
+
#streamName;
|
|
133
|
+
#publicChannel;
|
|
134
|
+
constructor(nsp, redisClient, subClientPromise, opts) {
|
|
135
|
+
super(nsp);
|
|
80
136
|
this.#redisClient = redisClient;
|
|
81
137
|
this.#opts = opts;
|
|
82
|
-
|
|
138
|
+
// each namespace is routed to a specific stream to ensure the ordering of messages
|
|
139
|
+
this.#streamName = computeStreamName(nsp.name, opts);
|
|
140
|
+
this.#publicChannel = `${opts.channelPrefix}#${nsp.name}#`;
|
|
141
|
+
const privateChannel = `${opts.channelPrefix}#${nsp.name}#${this.uid}#`;
|
|
142
|
+
subClientPromise.then((subClient) => {
|
|
143
|
+
(this.#opts.useShardedPubSub ? util_1.SSUBSCRIBE : util_1.SUBSCRIBE)(subClient, [this.#publicChannel, privateChannel], (payload) => {
|
|
144
|
+
try {
|
|
145
|
+
const message = (0, msgpack_1.decode)(payload);
|
|
146
|
+
this.onMessage(message);
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
return debug("invalid format: %s", e.message);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
83
153
|
}
|
|
84
154
|
doPublish(message) {
|
|
85
155
|
debug("publishing %o", message);
|
|
86
|
-
|
|
156
|
+
if (isEphemeral(message)) {
|
|
157
|
+
// ephemeral messages are sent with Redis PUB/SUB
|
|
158
|
+
const payload = Buffer.from((0, msgpack_1.encode)(message));
|
|
159
|
+
(this.#opts.useShardedPubSub ? util_1.SPUBLISH : util_1.PUBLISH)(this.#redisClient, this.#publicChannel, payload);
|
|
160
|
+
return Promise.resolve("");
|
|
161
|
+
}
|
|
162
|
+
return (0, util_1.XADD)(this.#redisClient, this.#streamName, this.encode(message), this.#opts.maxLen);
|
|
87
163
|
}
|
|
88
164
|
doPublishResponse(requesterUid, response) {
|
|
89
|
-
|
|
90
|
-
|
|
165
|
+
const responseChannel = `${this.#opts.channelPrefix}#${this.nsp.name}#${requesterUid}#`;
|
|
166
|
+
const payload = Buffer.from((0, msgpack_1.encode)(response));
|
|
167
|
+
return (this.#opts.useShardedPubSub ? util_1.SPUBLISH : util_1.PUBLISH)(this.#redisClient, responseChannel, payload).then();
|
|
91
168
|
}
|
|
92
|
-
|
|
169
|
+
encode(message) {
|
|
93
170
|
const rawMessage = {
|
|
94
171
|
uid: message.uid,
|
|
95
172
|
nsp: message.nsp,
|
|
@@ -104,8 +181,10 @@ class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbea
|
|
|
104
181
|
socket_io_adapter_1.MessageType.SERVER_SIDE_EMIT_RESPONSE,
|
|
105
182
|
socket_io_adapter_1.MessageType.BROADCAST_ACK,
|
|
106
183
|
].includes(message.type);
|
|
107
|
-
|
|
108
|
-
|
|
184
|
+
if (!this.#opts.onlyPlaintext &&
|
|
185
|
+
mayContainBinary &&
|
|
186
|
+
// @ts-ignore
|
|
187
|
+
(0, util_1.hasBinary)(message.data)) {
|
|
109
188
|
// @ts-ignore
|
|
110
189
|
rawMessage.data = Buffer.from((0, msgpack_1.encode)(message.data)).toString("base64");
|
|
111
190
|
}
|
|
@@ -144,6 +223,9 @@ class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbea
|
|
|
144
223
|
}
|
|
145
224
|
return message;
|
|
146
225
|
}
|
|
226
|
+
serverCount() {
|
|
227
|
+
return (0, util_1.PUBSUB)(this.#redisClient, this.#opts.useShardedPubSub ? "SHARDNUMSUB" : "NUMSUB", this.#publicChannel);
|
|
228
|
+
}
|
|
147
229
|
persistSession(session) {
|
|
148
230
|
debug("persisting session %o", session);
|
|
149
231
|
const sessionKey = this.#opts.sessionKeyPrefix + session.pid;
|
|
@@ -158,7 +240,7 @@ class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbea
|
|
|
158
240
|
const sessionKey = this.#opts.sessionKeyPrefix + pid;
|
|
159
241
|
const results = await Promise.all([
|
|
160
242
|
(0, util_1.GETDEL)(this.#redisClient, sessionKey),
|
|
161
|
-
(0, util_1.XRANGE)(this.#redisClient, this.#
|
|
243
|
+
(0, util_1.XRANGE)(this.#redisClient, this.#streamName, offset, offset),
|
|
162
244
|
]);
|
|
163
245
|
const rawSession = results[0][0];
|
|
164
246
|
const offsetExists = results[1][0];
|
|
@@ -171,7 +253,7 @@ class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbea
|
|
|
171
253
|
// FIXME we need to add an arbitrary limit here, because if entries are added faster than what we can consume, then
|
|
172
254
|
// we will loop endlessly. But if we stop before reaching the end of the stream, we might lose messages.
|
|
173
255
|
for (let i = 0; i < RESTORE_SESSION_MAX_XRANGE_CALLS; i++) {
|
|
174
|
-
const entries = await (0, util_1.XRANGE)(this.#redisClient, this.#
|
|
256
|
+
const entries = await (0, util_1.XRANGE)(this.#redisClient, this.#streamName, RedisStreamsAdapter.nextOffset(offset), "+");
|
|
175
257
|
if (entries.length === 0) {
|
|
176
258
|
break;
|
|
177
259
|
}
|
package/dist/util.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
export declare function hasBinary(obj: any, toJSON?: boolean): boolean;
|
|
3
|
+
export declare function duplicateClient(redisClient: any): Promise<any>;
|
|
2
4
|
/**
|
|
3
5
|
* @see https://redis.io/commands/xread/
|
|
4
6
|
*/
|
|
5
|
-
export declare function XREAD(redisClient: any, streamName: string, offset: string, readCount: number): any;
|
|
7
|
+
export declare function XREAD(redisClient: any, streamName: string, offset: string, readCount: number, blockTimeInMs: number): any;
|
|
6
8
|
/**
|
|
7
9
|
* @see https://redis.io/commands/xadd/
|
|
8
10
|
*/
|
|
@@ -19,3 +21,9 @@ export declare function SET(redisClient: any, key: string, value: string, expiry
|
|
|
19
21
|
* @see https://redis.io/commands/getdel/
|
|
20
22
|
*/
|
|
21
23
|
export declare function GETDEL(redisClient: any, key: string): any;
|
|
24
|
+
export declare function hashCode(str: string): number;
|
|
25
|
+
export declare function PUBLISH(redisClient: any, channel: string, payload: Buffer): any;
|
|
26
|
+
export declare function SPUBLISH(redisClient: any, channel: string, payload: Buffer): any;
|
|
27
|
+
export declare function SUBSCRIBE(subClient: any, channels: string[], listener: (payload: Buffer) => void): void;
|
|
28
|
+
export declare function SSUBSCRIBE(subClient: any, channels: string[], listener: (payload: Buffer) => void): void;
|
|
29
|
+
export declare function PUBSUB(redisClient: any, arg: "NUMSUB" | "SHARDNUMSUB", channel: string): any;
|
package/dist/util.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GETDEL = exports.SET = exports.XRANGE = exports.XADD = exports.XREAD = exports.hasBinary = void 0;
|
|
3
|
+
exports.PUBSUB = exports.SSUBSCRIBE = exports.SUBSCRIBE = exports.SPUBLISH = exports.PUBLISH = exports.hashCode = exports.GETDEL = exports.SET = exports.XRANGE = exports.XADD = exports.XREAD = exports.duplicateClient = exports.hasBinary = void 0;
|
|
4
4
|
function hasBinary(obj, toJSON) {
|
|
5
5
|
if (!obj || typeof obj !== "object") {
|
|
6
6
|
return false;
|
|
@@ -37,6 +37,17 @@ exports.hasBinary = hasBinary;
|
|
|
37
37
|
function isRedisV4Client(redisClient) {
|
|
38
38
|
return typeof redisClient.sSubscribe === "function";
|
|
39
39
|
}
|
|
40
|
+
async function duplicateClient(redisClient) {
|
|
41
|
+
const newClient = redisClient.duplicate();
|
|
42
|
+
newClient.on("error", (err) => {
|
|
43
|
+
// ignore errors
|
|
44
|
+
});
|
|
45
|
+
if (isRedisV4Client(redisClient)) {
|
|
46
|
+
await newClient.connect();
|
|
47
|
+
}
|
|
48
|
+
return newClient;
|
|
49
|
+
}
|
|
50
|
+
exports.duplicateClient = duplicateClient;
|
|
40
51
|
/**
|
|
41
52
|
* Map the output of the XREAD/XRANGE command with the ioredis package to the format of the redis package
|
|
42
53
|
* @param result
|
|
@@ -56,25 +67,21 @@ function mapResult(result) {
|
|
|
56
67
|
/**
|
|
57
68
|
* @see https://redis.io/commands/xread/
|
|
58
69
|
*/
|
|
59
|
-
function XREAD(redisClient, streamName, offset, readCount) {
|
|
70
|
+
function XREAD(redisClient, streamName, offset, readCount, blockTimeInMs) {
|
|
60
71
|
if (isRedisV4Client(redisClient)) {
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
], {
|
|
70
|
-
COUNT: readCount,
|
|
71
|
-
BLOCK: 5000,
|
|
72
|
-
});
|
|
72
|
+
return redisClient.xRead([
|
|
73
|
+
{
|
|
74
|
+
key: streamName,
|
|
75
|
+
id: offset,
|
|
76
|
+
},
|
|
77
|
+
], {
|
|
78
|
+
COUNT: readCount,
|
|
79
|
+
BLOCK: blockTimeInMs,
|
|
73
80
|
});
|
|
74
81
|
}
|
|
75
82
|
else {
|
|
76
83
|
return redisClient
|
|
77
|
-
.xread("BLOCK",
|
|
84
|
+
.xread("BLOCK", blockTimeInMs, "COUNT", readCount, "STREAMS", streamName, offset)
|
|
78
85
|
.then((results) => {
|
|
79
86
|
if (results === null) {
|
|
80
87
|
return null;
|
|
@@ -158,3 +165,105 @@ function GETDEL(redisClient, key) {
|
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
exports.GETDEL = GETDEL;
|
|
168
|
+
function hashCode(str) {
|
|
169
|
+
let hash = 0;
|
|
170
|
+
for (let i = 0; i < str.length; i++) {
|
|
171
|
+
let chr = str.charCodeAt(i);
|
|
172
|
+
hash = hash * 31 + chr;
|
|
173
|
+
hash |= 0;
|
|
174
|
+
}
|
|
175
|
+
return hash;
|
|
176
|
+
}
|
|
177
|
+
exports.hashCode = hashCode;
|
|
178
|
+
function PUBLISH(redisClient, channel, payload) {
|
|
179
|
+
return redisClient.publish(channel, payload);
|
|
180
|
+
}
|
|
181
|
+
exports.PUBLISH = PUBLISH;
|
|
182
|
+
function SPUBLISH(redisClient, channel, payload) {
|
|
183
|
+
if (isRedisV4Client(redisClient)) {
|
|
184
|
+
return redisClient.sPublish(channel, payload);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
return redisClient.spublish(channel, payload);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.SPUBLISH = SPUBLISH;
|
|
191
|
+
const RETURN_BUFFERS = true;
|
|
192
|
+
function SUBSCRIBE(subClient, channels, listener) {
|
|
193
|
+
if (isRedisV4Client(subClient)) {
|
|
194
|
+
subClient.subscribe(channels, listener, RETURN_BUFFERS);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
subClient.subscribe(channels);
|
|
198
|
+
subClient.on("messageBuffer", (channel, payload) => {
|
|
199
|
+
if (channels.includes(channel.toString())) {
|
|
200
|
+
listener(payload);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
exports.SUBSCRIBE = SUBSCRIBE;
|
|
206
|
+
function SSUBSCRIBE(subClient, channels, listener) {
|
|
207
|
+
if (isRedisV4Client(subClient)) {
|
|
208
|
+
// note: we could also have used a hash tag ({...}) to ensure the channels are mapped to the same slot
|
|
209
|
+
for (const channel of channels) {
|
|
210
|
+
subClient.sSubscribe(channel, listener, RETURN_BUFFERS);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
for (const channel of channels) {
|
|
215
|
+
subClient.ssubscribe(channel);
|
|
216
|
+
}
|
|
217
|
+
subClient.on("smessageBuffer", (channel, payload) => {
|
|
218
|
+
if (channels.includes(channel.toString())) {
|
|
219
|
+
listener(payload);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
exports.SSUBSCRIBE = SSUBSCRIBE;
|
|
225
|
+
function parseNumSubResponse(res) {
|
|
226
|
+
return parseInt(res[1], 10);
|
|
227
|
+
}
|
|
228
|
+
function sumValues(values) {
|
|
229
|
+
return values.reduce((acc, val) => acc + val, 0);
|
|
230
|
+
}
|
|
231
|
+
function PUBSUB(redisClient, arg, channel) {
|
|
232
|
+
if (redisClient.constructor.name === "Cluster" || redisClient.isCluster) {
|
|
233
|
+
// ioredis cluster
|
|
234
|
+
return Promise.all(redisClient.nodes().map((node) => {
|
|
235
|
+
return node
|
|
236
|
+
.send_command("PUBSUB", [arg, channel])
|
|
237
|
+
.then(parseNumSubResponse);
|
|
238
|
+
})).then(sumValues);
|
|
239
|
+
}
|
|
240
|
+
else if (isRedisV4Client(redisClient)) {
|
|
241
|
+
const isCluster = Array.isArray(redisClient.masters);
|
|
242
|
+
if (isCluster) {
|
|
243
|
+
// redis@4 cluster
|
|
244
|
+
const nodes = redisClient.masters;
|
|
245
|
+
return Promise.all(nodes.map((node) => {
|
|
246
|
+
return node.client
|
|
247
|
+
.sendCommand(["PUBSUB", arg, channel])
|
|
248
|
+
.then(parseNumSubResponse);
|
|
249
|
+
})).then(sumValues);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// redis@4 standalone
|
|
253
|
+
return redisClient
|
|
254
|
+
.sendCommand(["PUBSUB", arg, channel])
|
|
255
|
+
.then(parseNumSubResponse);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// ioredis / redis@3 standalone
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
redisClient.send_command("PUBSUB", [arg, channel], (err, numSub) => {
|
|
262
|
+
if (err)
|
|
263
|
+
return reject(err);
|
|
264
|
+
resolve(parseNumSubResponse(numSub));
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
exports.PUBSUB = PUBSUB;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socket.io/redis-streams-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The Socket.IO adapter based on Redis Streams, allowing to broadcast events between several Socket.IO servers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,12 +17,7 @@
|
|
|
17
17
|
"format:check": "prettier --parser typescript --check \"lib/**/*.ts\" \"test/**/*.ts\"",
|
|
18
18
|
"format:fix": "prettier --parser typescript --write \"lib/**/*.ts\" \"test/**/*.ts\"",
|
|
19
19
|
"prepack": "npm run compile",
|
|
20
|
-
"test": "npm run format:check && npm run compile &&
|
|
21
|
-
"test:redis-standalone": "nyc mocha --require ts-node/register test/**/*.ts",
|
|
22
|
-
"test:redis-cluster": "cross-env REDIS_CLUSTER=1 mocha --require ts-node/register test/**/*.ts",
|
|
23
|
-
"test:ioredis-standalone": "cross-env REDIS_LIB=ioredis mocha --require ts-node/register test/**/*.ts",
|
|
24
|
-
"test:ioredis-cluster": "cross-env REDIS_LIB=ioredis REDIS_CLUSTER=1 mocha --require ts-node/register test/**/*.ts",
|
|
25
|
-
"test:valkey-standalone": "cross-env VALKEY=1 mocha --require ts-node/register test/**/*.ts"
|
|
20
|
+
"test": "npm run format:check && npm run compile && nyc mocha --import=tsx test/test-runner.ts"
|
|
26
21
|
},
|
|
27
22
|
"prettier": {
|
|
28
23
|
"endOfLine": "auto"
|
|
@@ -38,17 +33,16 @@
|
|
|
38
33
|
"@types/expect.js": "^0.3.29",
|
|
39
34
|
"@types/mocha": "^8.2.1",
|
|
40
35
|
"@types/node": "^18.15.11",
|
|
41
|
-
"cross-env": "7.0.3",
|
|
42
36
|
"expect.js": "0.3.1",
|
|
43
37
|
"ioredis": "^5.3.2",
|
|
44
38
|
"mocha": "^10.1.0",
|
|
45
39
|
"nyc": "^15.1.0",
|
|
46
40
|
"prettier": "^2.8.7",
|
|
47
|
-
"redis": "
|
|
41
|
+
"redis": "~5.10.0",
|
|
48
42
|
"rimraf": "^5.0.5",
|
|
49
43
|
"socket.io": "^4.6.1",
|
|
50
44
|
"socket.io-client": "^4.6.1",
|
|
51
|
-
"
|
|
45
|
+
"tsx": "~4.21.0",
|
|
52
46
|
"typescript": "^4.9.5"
|
|
53
47
|
},
|
|
54
48
|
"engines": {
|