@socket.io/redis-streams-adapter 0.2.0 → 0.2.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
@@ -26,6 +26,10 @@ Related packages:
26
26
 
27
27
  - [Installation](#installation)
28
28
  - [Usage](#usage)
29
+ - [With the `redis` package](#with-the-redis-package)
30
+ - [With the `redis` package and a Redis cluster](#with-the-redis-package-and-a-redis-cluster)
31
+ - [With the `ioredis` package](#with-the-ioredis-package)
32
+ - [With the `ioredis` package and a Redis cluster](#with-the-ioredis-package-and-a-redis-cluster)
29
33
  - [Options](#options)
30
34
  - [How it works](#how-it-works)
31
35
  - [License](#license)
@@ -38,6 +42,8 @@ npm install @socket.io/redis-streams-adapter redis
38
42
 
39
43
  ## Usage
40
44
 
45
+ ### With the `redis` package
46
+
41
47
  ```js
42
48
  import { createClient } from "redis";
43
49
  import { Server } from "socket.io";
@@ -54,6 +60,81 @@ const io = new Server({
54
60
  io.listen(3000);
55
61
  ```
56
62
 
63
+ ### With the `redis` package and a Redis cluster
64
+
65
+ ```js
66
+ import { createCluster } from "redis";
67
+ import { Server } from "socket.io";
68
+ import { createAdapter } from "@socket.io/redis-streams-adapter";
69
+
70
+ const redisClient = createCluster({
71
+ rootNodes: [
72
+ {
73
+ url: "redis://localhost:7000",
74
+ },
75
+ {
76
+ url: "redis://localhost:7001",
77
+ },
78
+ {
79
+ url: "redis://localhost:7002",
80
+ },
81
+ ],
82
+ });
83
+
84
+ await redisClient.connect();
85
+
86
+ const io = new Server({
87
+ adapter: createAdapter(redisClient)
88
+ });
89
+
90
+ io.listen(3000);
91
+ ```
92
+
93
+ ### With the `ioredis` package
94
+
95
+ ```js
96
+ import { Redis } from "ioredis";
97
+ import { Server } from "socket.io";
98
+ import { createAdapter } from "@socket.io/redis-streams-adapter";
99
+
100
+ const redisClient = new Redis();
101
+
102
+ const io = new Server({
103
+ adapter: createAdapter(redisClient)
104
+ });
105
+
106
+ io.listen(3000);
107
+ ```
108
+
109
+ ### With the `ioredis` package and a Redis cluster
110
+
111
+ ```js
112
+ import { Cluster } from "ioredis";
113
+ import { Server } from "socket.io";
114
+ import { createAdapter } from "@socket.io/redis-streams-adapter";
115
+
116
+ const redisClient = new Cluster([
117
+ {
118
+ host: "localhost",
119
+ port: 7000,
120
+ },
121
+ {
122
+ host: "localhost",
123
+ port: 7001,
124
+ },
125
+ {
126
+ host: "localhost",
127
+ port: 7002,
128
+ },
129
+ ]);
130
+
131
+ const io = new Server({
132
+ adapter: createAdapter(redisClient)
133
+ });
134
+
135
+ io.listen(3000);
136
+ ```
137
+
57
138
  ## Options
58
139
 
59
140
  | Name | Description | Default value |
package/dist/adapter.d.ts CHANGED
@@ -1,16 +1,5 @@
1
- import { ClusterAdapterWithHeartbeat, type ClusterMessage, type PrivateSessionId, type Session, type ServerId, type ClusterResponse } from "socket.io-adapter";
2
- interface ClusterAdapterOptions {
3
- /**
4
- * The number of ms between two heartbeats.
5
- * @default 5_000
6
- */
7
- heartbeatInterval?: number;
8
- /**
9
- * The number of ms without heartbeat before we consider a node down.
10
- * @default 10_000
11
- */
12
- heartbeatTimeout?: number;
13
- }
1
+ import { ClusterAdapterWithHeartbeat } from "socket.io-adapter";
2
+ import type { ClusterAdapterOptions, ClusterMessage, PrivateSessionId, Session, ServerId, ClusterResponse } from "socket.io-adapter";
14
3
  export interface RedisStreamsAdapterOptions {
15
4
  /**
16
5
  * The name of the Redis stream.
package/dist/adapter.js CHANGED
@@ -97,13 +97,12 @@ class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbea
97
97
  };
98
98
  // @ts-ignore
99
99
  if (message.data) {
100
- // TODO MessageType should be exported by the socket.io-adapter package
101
100
  const mayContainBinary = [
102
- 3,
103
- 8,
104
- 9,
105
- 10,
106
- 12, // MessageType.BROADCAST_ACK,
101
+ socket_io_adapter_1.MessageType.BROADCAST,
102
+ socket_io_adapter_1.MessageType.FETCH_SOCKETS_RESPONSE,
103
+ socket_io_adapter_1.MessageType.SERVER_SIDE_EMIT,
104
+ socket_io_adapter_1.MessageType.SERVER_SIDE_EMIT_RESPONSE,
105
+ socket_io_adapter_1.MessageType.BROADCAST_ACK,
107
106
  ].includes(message.type);
108
107
  // @ts-ignore
109
108
  if (mayContainBinary && (0, util_1.hasBinary)(message.data)) {
@@ -149,9 +148,7 @@ class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbea
149
148
  debug("persisting session %o", session);
150
149
  const sessionKey = this.#opts.sessionKeyPrefix + session.pid;
151
150
  const encodedSession = Buffer.from((0, msgpack_1.encode)(session)).toString("base64");
152
- this.#redisClient.set(sessionKey, encodedSession, {
153
- PX: this.nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration,
154
- });
151
+ (0, util_1.SET)(this.#redisClient, sessionKey, encodedSession, this.nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration);
155
152
  }
156
153
  async restoreSession(pid, offset) {
157
154
  debug("restoring session %s from offset %s", pid, offset);
@@ -159,12 +156,12 @@ class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbea
159
156
  return Promise.reject("invalid offset");
160
157
  }
161
158
  const sessionKey = this.#opts.sessionKeyPrefix + pid;
162
- const [rawSession, offsetExists] = await this.#redisClient
163
- .multi()
164
- .get(sessionKey)
165
- .del(sessionKey) // GETDEL was added in Redis version 6.2
166
- .xRange(this.#opts.streamName, offset, offset)
167
- .exec();
159
+ const results = await Promise.all([
160
+ (0, util_1.GETDEL)(this.#redisClient, sessionKey),
161
+ (0, util_1.XRANGE)(this.#redisClient, this.#opts.streamName, offset, offset),
162
+ ]);
163
+ const rawSession = results[0][0];
164
+ const offsetExists = results[1][0];
168
165
  if (!rawSession || !offsetExists) {
169
166
  return Promise.reject("session or offset not found");
170
167
  }
@@ -174,9 +171,7 @@ class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbea
174
171
  // FIXME we need to add an arbitrary limit here, because if entries are added faster than what we can consume, then
175
172
  // we will loop endlessly. But if we stop before reaching the end of the stream, we might lose messages.
176
173
  for (let i = 0; i < RESTORE_SESSION_MAX_XRANGE_CALLS; i++) {
177
- const entries = await this.#redisClient.xRange(this.#opts.streamName, RedisStreamsAdapter.nextOffset(offset), "+", {
178
- COUNT: this.#opts.readCount,
179
- });
174
+ const entries = await (0, util_1.XRANGE)(this.#redisClient, this.#opts.streamName, RedisStreamsAdapter.nextOffset(offset), "+");
180
175
  if (entries.length === 0) {
181
176
  break;
182
177
  }
package/dist/util.d.ts CHANGED
@@ -8,3 +8,15 @@ export declare function XREAD(redisClient: any, streamName: string, offset: stri
8
8
  * @see https://redis.io/commands/xadd/
9
9
  */
10
10
  export declare function XADD(redisClient: any, streamName: string, payload: any, maxLenThreshold: number): any;
11
+ /**
12
+ * @see https://redis.io/commands/xrange/
13
+ */
14
+ export declare function XRANGE(redisClient: any, streamName: string, start: string, end: string): any;
15
+ /**
16
+ * @see https://redis.io/commands/set/
17
+ */
18
+ export declare function SET(redisClient: any, key: string, value: string, expiryInSeconds: number): any;
19
+ /**
20
+ * @see https://redis.io/commands/getdel/
21
+ */
22
+ export declare function GETDEL(redisClient: any, key: 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.XADD = exports.XREAD = exports.randomId = exports.hasBinary = void 0;
3
+ exports.GETDEL = exports.SET = exports.XRANGE = exports.XADD = exports.XREAD = exports.randomId = exports.hasBinary = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const redis_1 = require("redis");
6
6
  function hasBinary(obj, toJSON) {
@@ -43,6 +43,22 @@ exports.randomId = randomId;
43
43
  function isRedisV4Client(redisClient) {
44
44
  return typeof redisClient.sSubscribe === "function";
45
45
  }
46
+ /**
47
+ * Map the output of the XREAD/XRANGE command with the ioredis package to the format of the redis package
48
+ * @param result
49
+ */
50
+ function mapResult(result) {
51
+ const id = result[0];
52
+ const inlineValues = result[1];
53
+ const message = {};
54
+ for (let i = 0; i < inlineValues.length; i += 2) {
55
+ message[inlineValues[i]] = inlineValues[i + 1];
56
+ }
57
+ return {
58
+ id,
59
+ message,
60
+ };
61
+ }
46
62
  /**
47
63
  * @see https://redis.io/commands/xread/
48
64
  */
@@ -69,18 +85,7 @@ function XREAD(redisClient, streamName, offset, readCount) {
69
85
  }
70
86
  return [
71
87
  {
72
- messages: results[0][1].map((result) => {
73
- const id = result[0];
74
- const inlineValues = result[1];
75
- const message = {};
76
- for (let i = 0; i < inlineValues.length; i += 2) {
77
- message[inlineValues[i]] = inlineValues[i + 1];
78
- }
79
- return {
80
- id,
81
- message,
82
- };
83
- }),
88
+ messages: results[0][1].map(mapResult),
84
89
  },
85
90
  ];
86
91
  });
@@ -109,3 +114,51 @@ function XADD(redisClient, streamName, payload, maxLenThreshold) {
109
114
  }
110
115
  }
111
116
  exports.XADD = XADD;
117
+ /**
118
+ * @see https://redis.io/commands/xrange/
119
+ */
120
+ function XRANGE(redisClient, streamName, start, end) {
121
+ if (isRedisV4Client(redisClient)) {
122
+ return redisClient.xRange(streamName, start, end);
123
+ }
124
+ else {
125
+ return redisClient.xrange(streamName, start, end).then((res) => {
126
+ return res.map(mapResult);
127
+ });
128
+ }
129
+ }
130
+ exports.XRANGE = XRANGE;
131
+ /**
132
+ * @see https://redis.io/commands/set/
133
+ */
134
+ function SET(redisClient, key, value, expiryInSeconds) {
135
+ if (isRedisV4Client(redisClient)) {
136
+ return redisClient.set(key, value, {
137
+ PX: expiryInSeconds,
138
+ });
139
+ }
140
+ else {
141
+ return redisClient.set(key, value, "PX", expiryInSeconds);
142
+ }
143
+ }
144
+ exports.SET = SET;
145
+ /**
146
+ * @see https://redis.io/commands/getdel/
147
+ */
148
+ function GETDEL(redisClient, key) {
149
+ if (isRedisV4Client(redisClient)) {
150
+ // note: GETDEL was added in Redis version 6.2
151
+ return redisClient.multi().get(key).del(key).exec();
152
+ }
153
+ else {
154
+ return redisClient
155
+ .multi()
156
+ .get(key)
157
+ .del(key)
158
+ .exec()
159
+ .then((res) => {
160
+ return [res[0][1]];
161
+ });
162
+ }
163
+ }
164
+ exports.GETDEL = GETDEL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socket.io/redis-streams-adapter",
3
- "version": "0.2.0",
3
+ "version": "0.2.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": {
@@ -13,7 +13,7 @@
13
13
  "main": "./dist/index.js",
14
14
  "types": "./dist/index.d.ts",
15
15
  "scripts": {
16
- "compile": "tsc",
16
+ "compile": "rimraf ./dist && tsc",
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",
@@ -31,7 +31,7 @@
31
31
  "debug": "~4.3.1"
32
32
  },
33
33
  "peerDependencies": {
34
- "socket.io-adapter": "^2.5.3"
34
+ "socket.io-adapter": "^2.5.4"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/expect.js": "^0.3.29",
@@ -44,6 +44,7 @@
44
44
  "nyc": "^15.1.0",
45
45
  "prettier": "^2.8.7",
46
46
  "redis": "^4.6.5",
47
+ "rimraf": "^5.0.5",
47
48
  "socket.io": "^4.6.1",
48
49
  "socket.io-client": "^4.6.1",
49
50
  "ts-node": "^10.9.1",
@@ -1,184 +0,0 @@
1
- import { Adapter, BroadcastFlags, BroadcastOptions, Room } from "socket.io-adapter";
2
- export interface ClusterAdapterOptions {
3
- /**
4
- * The number of ms between two heartbeats.
5
- * @default 5_000
6
- */
7
- heartbeatInterval?: number;
8
- /**
9
- * The number of ms without heartbeat before we consider a node down.
10
- * @default 10_000
11
- */
12
- heartbeatTimeout?: number;
13
- }
14
- type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
15
- export declare enum MessageType {
16
- INITIAL_HEARTBEAT = 1,
17
- HEARTBEAT = 2,
18
- BROADCAST = 3,
19
- SOCKETS_JOIN = 4,
20
- SOCKETS_LEAVE = 5,
21
- DISCONNECT_SOCKETS = 6,
22
- FETCH_SOCKETS = 7,
23
- FETCH_SOCKETS_RESPONSE = 8,
24
- SERVER_SIDE_EMIT = 9,
25
- SERVER_SIDE_EMIT_RESPONSE = 10,
26
- BROADCAST_CLIENT_COUNT = 11,
27
- BROADCAST_ACK = 12
28
- }
29
- export type ClusterMessage = {
30
- uid: string;
31
- nsp: string;
32
- } & ({
33
- type: MessageType.INITIAL_HEARTBEAT | MessageType.HEARTBEAT;
34
- } | {
35
- type: MessageType.BROADCAST;
36
- data: {
37
- opts: {
38
- rooms: string[];
39
- except: string[];
40
- flags: BroadcastFlags;
41
- };
42
- packet: unknown;
43
- requestId?: string;
44
- };
45
- } | {
46
- type: MessageType.SOCKETS_JOIN | MessageType.SOCKETS_LEAVE;
47
- data: {
48
- opts: {
49
- rooms: string[];
50
- except: string[];
51
- flags: BroadcastFlags;
52
- };
53
- rooms: string[];
54
- };
55
- } | {
56
- type: MessageType.DISCONNECT_SOCKETS;
57
- data: {
58
- opts: {
59
- rooms: string[];
60
- except: string[];
61
- flags: BroadcastFlags;
62
- };
63
- close?: boolean;
64
- };
65
- } | {
66
- type: MessageType.FETCH_SOCKETS;
67
- data: {
68
- opts: {
69
- rooms: string[];
70
- except: string[];
71
- flags: BroadcastFlags;
72
- };
73
- requestId: string;
74
- };
75
- } | {
76
- type: MessageType.SERVER_SIDE_EMIT;
77
- data: {
78
- requestId?: string;
79
- packet: unknown;
80
- };
81
- });
82
- export type ClusterResponse = {
83
- uid: string;
84
- nsp: string;
85
- } & ({
86
- type: MessageType.FETCH_SOCKETS_RESPONSE;
87
- data: {
88
- requestId: string;
89
- sockets: unknown[];
90
- };
91
- } | {
92
- type: MessageType.SERVER_SIDE_EMIT_RESPONSE;
93
- data: {
94
- requestId: string;
95
- packet: unknown;
96
- };
97
- } | {
98
- type: MessageType.BROADCAST_CLIENT_COUNT;
99
- data: {
100
- requestId: string;
101
- clientCount: number;
102
- };
103
- } | {
104
- type: MessageType.BROADCAST_ACK;
105
- data: {
106
- requestId: string;
107
- packet: unknown;
108
- };
109
- });
110
- /**
111
- * A cluster-ready adapter. Any extending class must:
112
- *
113
- * - implement {@link ClusterAdapter#publishMessage} and {@link ClusterAdapter#publishResponse}
114
- * - call {@link ClusterAdapter#onMessage} and {@link ClusterAdapter#onResponse}
115
- */
116
- export declare abstract class ClusterAdapter extends Adapter {
117
- protected readonly uid: string;
118
- private requests;
119
- private ackRequests;
120
- protected constructor(nsp: unknown);
121
- /**
122
- * Called when receiving a message from another member of the cluster.
123
- *
124
- * @param message
125
- * @param offset
126
- * @protected
127
- */
128
- protected onMessage(message: ClusterMessage, offset?: string): Promise<void>;
129
- /**
130
- * Called when receiving a response from another member of the cluster.
131
- *
132
- * @param response
133
- * @protected
134
- */
135
- protected onResponse(response: ClusterResponse): void;
136
- broadcast(packet: any, opts: BroadcastOptions): Promise<any>;
137
- /**
138
- * Adds an offset at the end of the data array in order to allow the client to receive any missed packets when it
139
- * reconnects after a temporary disconnection.
140
- *
141
- * @param packet
142
- * @param opts
143
- * @param offset
144
- * @private
145
- */
146
- private addOffsetIfNecessary;
147
- broadcastWithAck(packet: any, opts: BroadcastOptions, clientCountCallback: (clientCount: number) => void, ack: (...args: any[]) => void): void;
148
- addSockets(opts: BroadcastOptions, rooms: Room[]): void;
149
- delSockets(opts: BroadcastOptions, rooms: Room[]): void;
150
- disconnectSockets(opts: BroadcastOptions, close: boolean): void;
151
- fetchSockets(opts: BroadcastOptions): Promise<any[]>;
152
- serverSideEmit(packet: any[]): Promise<any>;
153
- protected publish(message: DistributiveOmit<ClusterMessage, "uid" | "nsp">): Promise<string>;
154
- /**
155
- * Send a message to the other members of the cluster.
156
- *
157
- * @param message
158
- * @protected
159
- * @return an offset, if applicable
160
- */
161
- protected abstract publishMessage(message: ClusterMessage): Promise<string>;
162
- private publishResponse;
163
- /**
164
- * Send a response to the given member of the cluster.
165
- *
166
- * @param requesterUid
167
- * @param response
168
- * @protected
169
- */
170
- protected abstract doPublishResponse(requesterUid: string, response: ClusterResponse): void;
171
- }
172
- export declare abstract class ClusterAdapterWithHeartbeat extends ClusterAdapter {
173
- private readonly _opts;
174
- private heartbeatTimer;
175
- private nodesMap;
176
- protected constructor(nsp: unknown, opts: ClusterAdapterOptions);
177
- init(): Promise<void> | void;
178
- private scheduleHeartbeat;
179
- close(): Promise<void> | void;
180
- onMessage(message: ClusterMessage, offset?: string): Promise<any>;
181
- serverCount(): Promise<number>;
182
- publish(message: DistributiveOmit<ClusterMessage, "nsp" | "uid">): Promise<string>;
183
- }
184
- export {};