@socket.io/redis-streams-adapter 0.2.3 → 0.3.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/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,27 @@ io.listen(3000);
127
130
 
128
131
  ## Options
129
132
 
130
- | Name | Description | Default value |
131
- |---------------------|-------------------------------------------------------------------------------------------------------------------|----------------|
132
- | `streamName` | The name of the Redis stream. | `socket.io` |
133
- | `maxLen` | The maximum size of the stream. Almost exact trimming (~) is used. | `10_000` |
134
- | `readCount` | The number of elements to fetch per XREAD call. | `100` |
135
- | `sessionKeyPrefix` | The prefix of the key used to store the Socket.IO session, when the connection state recovery feature is enabled. | `sio:session:` |
136
- | `heartbeatInterval` | The number of ms between two heartbeats. | `5_000` |
137
- | `heartbeatTimeout` | The number of ms without heartbeat before we consider a node down. | `10_000` |
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
+ | `onlyPlaintext` | Whether the transmitted data contains only JSON-serializable objects without binary data (Buffer, ArrayBuffer, etc.). | `false` |
138
144
 
139
145
  ## How it works
140
146
 
141
- The adapter will use a [Redis stream](https://redis.io/docs/data-types/streams/) to forward events between the Socket.IO servers.
147
+ 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
148
 
143
149
  Notes:
144
150
 
145
- - a single stream is used for all namespaces
146
- - the `maxLen` option allows to limit the size of the stream
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
151
+ - by default, a single stream is used for all namespaces (see the `streamCount` option)
152
+ - the `maxLen` option allows limiting the size of the stream
153
+ - 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
154
  - 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
155
 
150
156
  ## License
package/dist/adapter.d.ts CHANGED
@@ -1,11 +1,34 @@
1
- import { ClusterAdapterWithHeartbeat } from "socket.io-adapter";
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 ClusterAdapterWithHeartbeat {
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
- static encode(message: ClusterMessage): RawClusterMessage;
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
- * Returns a function that will create a new adapter instance.
12
- *
13
- * @param redisClient - a Redis client that will be used to publish messages
14
- * @param opts - additional options
15
- */
16
- function createAdapter(redisClient, opts) {
17
- const namespaceToAdapters = new Map();
18
- const options = Object.assign({
19
- streamName: "socket.io",
20
- maxLen: 10000,
21
- readCount: 100,
22
- sessionKeyPrefix: "sio:session:",
23
- heartbeatInterval: 5000,
24
- heartbeatTimeout: 10000,
25
- }, opts);
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, options.streamName, offset, options.readCount);
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
- namespaceToAdapters
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 (namespaceToAdapters.size > 0 && !shouldClose) {
49
- poll();
51
+ if (signal.aborted) {
52
+ redisClient.disconnect();
50
53
  }
51
54
  else {
52
- polling = false;
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
- shouldClose = true;
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
- class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbeat {
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
- constructor(nsp, redisClient, opts) {
79
- super(nsp, opts);
132
+ #streamName;
133
+ #publicChannel;
134
+ constructor(nsp, redisClient, subClientPromise, opts) {
135
+ super(nsp);
80
136
  this.#redisClient = redisClient;
81
137
  this.#opts = opts;
82
- this.init();
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
- return (0, util_1.XADD)(this.#redisClient, this.#opts.streamName, RedisStreamsAdapter.encode(message), this.#opts.maxLen);
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
- // @ts-ignore
90
- return this.doPublish(response);
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
- static encode(message) {
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
- // @ts-ignore
108
- if (mayContainBinary && (0, util_1.hasBinary)(message.data)) {
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.#opts.streamName, offset, offset),
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.#opts.streamName, RedisStreamsAdapter.nextOffset(offset), "+");
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) || redisClient.options.lazyConnect) {
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 Promise.resolve().then(() => require("redis")).then((redisPackage) => {
62
- return redisClient.xRead(redisPackage.commandOptions({
63
- isolated: true,
64
- }), [
65
- {
66
- key: streamName,
67
- id: offset,
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", 100, "COUNT", readCount, "STREAMS", streamName, offset)
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.2.3",
3
+ "version": "0.3.1",
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 && npm run test:redis-standalone && npm run test:redis-cluster && npm run test:ioredis-standalone && npm run test:ioredis-cluster && npm run test:valkey-standalone",
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": "^4.6.5",
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
- "ts-node": "^10.9.1",
45
+ "tsx": "~4.21.0",
52
46
  "typescript": "^4.9.5"
53
47
  },
54
48
  "engines": {