@socket.io/redis-streams-adapter 0.1.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,12 +42,44 @@ 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";
44
50
  import { createAdapter } from "@socket.io/redis-streams-adapter";
45
51
 
46
- const redisClient = createClient({ host: "localhost", port: 6379 });
52
+ const redisClient = createClient({ url: "redis://localhost:6379" });
53
+
54
+ await redisClient.connect();
55
+
56
+ const io = new Server({
57
+ adapter: createAdapter(redisClient)
58
+ });
59
+
60
+ io.listen(3000);
61
+ ```
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
+ });
47
83
 
48
84
  await redisClient.connect();
49
85
 
@@ -54,15 +90,61 @@ const io = new Server({
54
90
  io.listen(3000);
55
91
  ```
56
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
- | Name | Description | Default value |
60
- |---------------------|--------------------------------------------------------------------|---------------|
61
- | `streamName` | The name of the Redis stream. | `socket.io` |
62
- | `maxLen` | The maximum size of the stream. Almost exact trimming (~) is used. | `10_000` |
63
- | `readCount` | The number of elements to fetch per XREAD call. | `100` |
64
- | `heartbeatInterval` | The number of ms between two heartbeats. | `5_000` |
65
- | `heartbeatTimeout` | The number of ms without heartbeat before we consider a node down. | `10_000` |
140
+ | Name | Description | Default value |
141
+ |---------------------|-------------------------------------------------------------------------------------------------------------------|----------------|
142
+ | `streamName` | The name of the Redis stream. | `socket.io` |
143
+ | `maxLen` | The maximum size of the stream. Almost exact trimming (~) is used. | `10_000` |
144
+ | `readCount` | The number of elements to fetch per XREAD call. | `100` |
145
+ | `sessionKeyPrefix` | The prefix of the key used to store the Socket.IO session, when the connection state recovery feature is enabled. | `sio:session:` |
146
+ | `heartbeatInterval` | The number of ms between two heartbeats. | `5_000` |
147
+ | `heartbeatTimeout` | The number of ms without heartbeat before we consider a node down. | `10_000` |
66
148
 
67
149
  ## How it works
68
150
 
package/dist/adapter.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { PrivateSessionId, Session } from "socket.io-adapter";
2
- import { ClusterAdapter, ClusterAdapterOptions, ClusterMessage } from "./cluster-adapter";
3
- export interface RedisStreamsAdapterOptions extends ClusterAdapterOptions {
1
+ import { ClusterAdapterWithHeartbeat } from "socket.io-adapter";
2
+ import type { ClusterAdapterOptions, ClusterMessage, PrivateSessionId, Session, ServerId, ClusterResponse } from "socket.io-adapter";
3
+ export interface RedisStreamsAdapterOptions {
4
4
  /**
5
5
  * The name of the Redis stream.
6
+ * @default "socket.io"
6
7
  */
7
8
  streamName?: string;
8
9
  /**
@@ -15,6 +16,11 @@ export interface RedisStreamsAdapterOptions extends ClusterAdapterOptions {
15
16
  * @default 100
16
17
  */
17
18
  readCount?: number;
19
+ /**
20
+ * The prefix of the key used to store the Socket.IO session, when the connection state recovery feature is enabled.
21
+ * @default "sio:session:"
22
+ */
23
+ sessionKeyPrefix?: string;
18
24
  }
19
25
  interface RawClusterMessage {
20
26
  uid: string;
@@ -28,11 +34,12 @@ interface RawClusterMessage {
28
34
  * @param redisClient - a Redis client that will be used to publish messages
29
35
  * @param opts - additional options
30
36
  */
31
- export declare function createAdapter(redisClient: any, opts?: RedisStreamsAdapterOptions): (nsp: any) => RedisStreamsAdapter;
32
- declare class RedisStreamsAdapter extends ClusterAdapter {
37
+ export declare function createAdapter(redisClient: any, opts?: RedisStreamsAdapterOptions & ClusterAdapterOptions): (nsp: any) => RedisStreamsAdapter;
38
+ declare class RedisStreamsAdapter extends ClusterAdapterWithHeartbeat {
33
39
  #private;
34
- constructor(nsp: any, redisClient: any, opts: Required<RedisStreamsAdapterOptions>);
40
+ constructor(nsp: any, redisClient: any, opts: Required<RedisStreamsAdapterOptions> & ClusterAdapterOptions);
35
41
  doPublish(message: ClusterMessage): any;
42
+ protected doPublishResponse(requesterUid: ServerId, response: ClusterResponse): Promise<void>;
36
43
  static encode(message: ClusterMessage): RawClusterMessage;
37
44
  onRawMessage(rawMessage: RawClusterMessage, offset: string): any;
38
45
  static decode(rawMessage: RawClusterMessage): ClusterMessage;
package/dist/adapter.js CHANGED
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createAdapter = void 0;
4
+ const socket_io_adapter_1 = require("socket.io-adapter");
4
5
  const msgpack_1 = require("@msgpack/msgpack");
5
- const redis_1 = require("redis");
6
- const cluster_adapter_1 = require("./cluster-adapter");
7
6
  const debug_1 = require("debug");
8
7
  const util_1 = require("./util");
9
8
  const debug = (0, debug_1.default)("socket.io-redis-streams-adapter");
@@ -20,24 +19,16 @@ function createAdapter(redisClient, opts) {
20
19
  streamName: "socket.io",
21
20
  maxLen: 10000,
22
21
  readCount: 100,
22
+ sessionKeyPrefix: "sio:session:",
23
23
  heartbeatInterval: 5000,
24
24
  heartbeatTimeout: 10000,
25
25
  }, opts);
26
26
  let offset = "$";
27
27
  let polling = false;
28
+ let shouldClose = false;
28
29
  async function poll() {
29
30
  try {
30
- let response = await redisClient.xRead((0, redis_1.commandOptions)({
31
- isolated: true,
32
- }), [
33
- {
34
- key: options.streamName,
35
- id: offset,
36
- },
37
- ], {
38
- COUNT: options.readCount,
39
- BLOCK: 5000,
40
- });
31
+ let response = await (0, util_1.XREAD)(redisClient, options.streamName, offset, options.readCount);
41
32
  if (response) {
42
33
  for (const entry of response[0].messages) {
43
34
  debug("reading entry %s", entry.id);
@@ -54,7 +45,7 @@ function createAdapter(redisClient, opts) {
54
45
  catch (e) {
55
46
  debug("something went wrong while consuming the stream: %s", e.message);
56
47
  }
57
- if (namespaceToAdapters.size > 0 && redisClient.isOpen) {
48
+ if (namespaceToAdapters.size > 0 && !shouldClose) {
58
49
  poll();
59
50
  }
60
51
  else {
@@ -66,35 +57,37 @@ function createAdapter(redisClient, opts) {
66
57
  namespaceToAdapters.set(nsp.name, adapter);
67
58
  if (!polling) {
68
59
  polling = true;
60
+ shouldClose = false;
69
61
  poll();
70
62
  }
71
63
  const defaultClose = adapter.close;
72
64
  adapter.close = () => {
73
65
  namespaceToAdapters.delete(nsp.name);
66
+ if (namespaceToAdapters.size === 0) {
67
+ shouldClose = true;
68
+ }
74
69
  defaultClose.call(adapter);
75
70
  };
76
71
  return adapter;
77
72
  };
78
73
  }
79
74
  exports.createAdapter = createAdapter;
80
- class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
75
+ class RedisStreamsAdapter extends socket_io_adapter_1.ClusterAdapterWithHeartbeat {
81
76
  #redisClient;
82
77
  #opts;
83
78
  constructor(nsp, redisClient, opts) {
84
79
  super(nsp, opts);
85
80
  this.#redisClient = redisClient;
86
81
  this.#opts = opts;
87
- this.initHeartbeat();
82
+ this.init();
88
83
  }
89
84
  doPublish(message) {
90
85
  debug("publishing %o", message);
91
- return this.#redisClient.xAdd(this.#opts.streamName, "*", RedisStreamsAdapter.encode(message), {
92
- TRIM: {
93
- strategy: "MAXLEN",
94
- strategyModifier: "~",
95
- threshold: this.#opts.maxLen,
96
- },
97
- });
86
+ return (0, util_1.XADD)(this.#redisClient, this.#opts.streamName, RedisStreamsAdapter.encode(message), this.#opts.maxLen);
87
+ }
88
+ doPublishResponse(requesterUid, response) {
89
+ // @ts-ignore
90
+ return this.doPublish(response);
98
91
  }
99
92
  static encode(message) {
100
93
  const rawMessage = {
@@ -102,18 +95,22 @@ class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
102
95
  nsp: message.nsp,
103
96
  type: message.type.toString(),
104
97
  };
98
+ // @ts-ignore
105
99
  if (message.data) {
106
100
  const mayContainBinary = [
107
- cluster_adapter_1.MessageType.BROADCAST,
108
- cluster_adapter_1.MessageType.BROADCAST_ACK,
109
- cluster_adapter_1.MessageType.FETCH_SOCKETS_RESPONSE,
110
- cluster_adapter_1.MessageType.SERVER_SIDE_EMIT,
111
- cluster_adapter_1.MessageType.SERVER_SIDE_EMIT_RESPONSE,
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,
112
106
  ].includes(message.type);
107
+ // @ts-ignore
113
108
  if (mayContainBinary && (0, util_1.hasBinary)(message.data)) {
109
+ // @ts-ignore
114
110
  rawMessage.data = Buffer.from((0, msgpack_1.encode)(message.data)).toString("base64");
115
111
  }
116
112
  else {
113
+ // @ts-ignore
117
114
  rawMessage.data = JSON.stringify(message.data);
118
115
  }
119
116
  }
@@ -137,9 +134,11 @@ class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
137
134
  };
138
135
  if (rawMessage.data) {
139
136
  if (rawMessage.data.startsWith("{")) {
137
+ // @ts-ignore
140
138
  message.data = JSON.parse(rawMessage.data);
141
139
  }
142
140
  else {
141
+ // @ts-ignore
143
142
  message.data = (0, msgpack_1.decode)(Buffer.from(rawMessage.data, "base64"));
144
143
  }
145
144
  }
@@ -147,23 +146,22 @@ class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
147
146
  }
148
147
  persistSession(session) {
149
148
  debug("persisting session %o", session);
149
+ const sessionKey = this.#opts.sessionKeyPrefix + session.pid;
150
150
  const encodedSession = Buffer.from((0, msgpack_1.encode)(session)).toString("base64");
151
- this.#redisClient.set(`sio:session:${session.pid}`, encodedSession, {
152
- PX: this.nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration,
153
- });
151
+ (0, util_1.SET)(this.#redisClient, sessionKey, encodedSession, this.nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration);
154
152
  }
155
153
  async restoreSession(pid, offset) {
156
154
  debug("restoring session %s from offset %s", pid, offset);
157
155
  if (!/^[0-9]+-[0-9]+$/.test(offset)) {
158
156
  return Promise.reject("invalid offset");
159
157
  }
160
- const sessionKey = `sio:session:${pid}`;
161
- const [rawSession, offsetExists] = await this.#redisClient
162
- .multi()
163
- .get(sessionKey)
164
- .del(sessionKey) // GETDEL was added in Redis version 6.2
165
- .xRange(this.#opts.streamName, offset, offset)
166
- .exec();
158
+ const sessionKey = this.#opts.sessionKeyPrefix + pid;
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];
167
165
  if (!rawSession || !offsetExists) {
168
166
  return Promise.reject("session or offset not found");
169
167
  }
@@ -173,15 +171,14 @@ class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
173
171
  // FIXME we need to add an arbitrary limit here, because if entries are added faster than what we can consume, then
174
172
  // we will loop endlessly. But if we stop before reaching the end of the stream, we might lose messages.
175
173
  for (let i = 0; i < RESTORE_SESSION_MAX_XRANGE_CALLS; i++) {
176
- const entries = await this.#redisClient.xRange(this.#opts.streamName, RedisStreamsAdapter.nextOffset(offset), "+", {
177
- COUNT: this.#opts.readCount,
178
- });
174
+ const entries = await (0, util_1.XRANGE)(this.#redisClient, this.#opts.streamName, RedisStreamsAdapter.nextOffset(offset), "+");
179
175
  if (entries.length === 0) {
180
176
  break;
181
177
  }
182
178
  for (const entry of entries) {
183
179
  if (entry.message.nsp === this.nsp.name && entry.message.type === "3") {
184
180
  const message = RedisStreamsAdapter.decode(entry.message);
181
+ // @ts-ignore
185
182
  if (shouldIncludePacket(session.rooms, message.data.opts)) {
186
183
  // @ts-ignore
187
184
  session.missedPackets.push(message.data.packet.data);
package/dist/util.d.ts CHANGED
@@ -1,2 +1,22 @@
1
1
  export declare function hasBinary(obj: any, toJSON?: boolean): boolean;
2
2
  export declare function randomId(): string;
3
+ /**
4
+ * @see https://redis.io/commands/xread/
5
+ */
6
+ export declare function XREAD(redisClient: any, streamName: string, offset: string, readCount: number): any;
7
+ /**
8
+ * @see https://redis.io/commands/xadd/
9
+ */
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,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- 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
+ const redis_1 = require("redis");
5
6
  function hasBinary(obj, toJSON) {
6
7
  if (!obj || typeof obj !== "object") {
7
8
  return false;
@@ -32,3 +33,132 @@ function randomId() {
32
33
  return (0, crypto_1.randomBytes)(8).toString("hex");
33
34
  }
34
35
  exports.randomId = randomId;
36
+ /**
37
+ * Whether the client comes from the `redis` package
38
+ *
39
+ * @param redisClient
40
+ *
41
+ * @see https://github.com/redis/node-redis
42
+ */
43
+ function isRedisV4Client(redisClient) {
44
+ return typeof redisClient.sSubscribe === "function";
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
+ }
62
+ /**
63
+ * @see https://redis.io/commands/xread/
64
+ */
65
+ function XREAD(redisClient, streamName, offset, readCount) {
66
+ if (isRedisV4Client(redisClient)) {
67
+ return redisClient.xRead((0, redis_1.commandOptions)({
68
+ isolated: true,
69
+ }), [
70
+ {
71
+ key: streamName,
72
+ id: offset,
73
+ },
74
+ ], {
75
+ COUNT: readCount,
76
+ BLOCK: 5000,
77
+ });
78
+ }
79
+ else {
80
+ return redisClient
81
+ .xread("BLOCK", 100, "COUNT", readCount, "STREAMS", streamName, offset)
82
+ .then((results) => {
83
+ if (results === null) {
84
+ return null;
85
+ }
86
+ return [
87
+ {
88
+ messages: results[0][1].map(mapResult),
89
+ },
90
+ ];
91
+ });
92
+ }
93
+ }
94
+ exports.XREAD = XREAD;
95
+ /**
96
+ * @see https://redis.io/commands/xadd/
97
+ */
98
+ function XADD(redisClient, streamName, payload, maxLenThreshold) {
99
+ if (isRedisV4Client(redisClient)) {
100
+ return redisClient.xAdd(streamName, "*", payload, {
101
+ TRIM: {
102
+ strategy: "MAXLEN",
103
+ strategyModifier: "~",
104
+ threshold: maxLenThreshold,
105
+ },
106
+ });
107
+ }
108
+ else {
109
+ const args = [streamName, "MAXLEN", "~", maxLenThreshold, "*"];
110
+ Object.keys(payload).forEach((k) => {
111
+ args.push(k, payload[k]);
112
+ });
113
+ return redisClient.xadd.call(redisClient, args);
114
+ }
115
+ }
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.1.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,28 +13,38 @@
13
13
  "main": "./dist/index.js",
14
14
  "types": "./dist/index.d.ts",
15
15
  "scripts": {
16
- "compile": "tsc",
17
- "format:check": "prettier --parser typescript --check lib/**/*.ts test/**/*.ts",
18
- "format:fix": "prettier --parser typescript --write lib/**/*.ts test/**/*.ts",
16
+ "compile": "rimraf ./dist && tsc",
17
+ "format:check": "prettier --parser typescript --check \"lib/**/*.ts\" \"test/**/*.ts\"",
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 && nyc mocha --bail --require ts-node/register test/**/*.ts"
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",
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
+ },
26
+ "prettier": {
27
+ "endOfLine": "auto"
21
28
  },
22
29
  "dependencies": {
23
30
  "@msgpack/msgpack": "~2.8.0",
24
31
  "debug": "~4.3.1"
25
32
  },
26
33
  "peerDependencies": {
27
- "socket.io-adapter": "^2.5.2"
34
+ "socket.io-adapter": "^2.5.4"
28
35
  },
29
36
  "devDependencies": {
30
37
  "@types/expect.js": "^0.3.29",
31
38
  "@types/mocha": "^8.2.1",
32
39
  "@types/node": "^18.15.11",
40
+ "cross-env": "7.0.3",
33
41
  "expect.js": "0.3.1",
42
+ "ioredis": "^5.3.2",
34
43
  "mocha": "^10.1.0",
35
44
  "nyc": "^15.1.0",
36
45
  "prettier": "^2.8.7",
37
46
  "redis": "^4.6.5",
47
+ "rimraf": "^5.0.5",
38
48
  "socket.io": "^4.6.1",
39
49
  "socket.io-client": "^4.6.1",
40
50
  "ts-node": "^10.9.1",
@@ -1,54 +0,0 @@
1
- import { Adapter, 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
- export declare enum MessageType {
15
- INITIAL_HEARTBEAT = 1,
16
- HEARTBEAT = 2,
17
- BROADCAST = 3,
18
- SOCKETS_JOIN = 4,
19
- SOCKETS_LEAVE = 5,
20
- DISCONNECT_SOCKETS = 6,
21
- FETCH_SOCKETS = 7,
22
- FETCH_SOCKETS_RESPONSE = 8,
23
- SERVER_SIDE_EMIT = 9,
24
- SERVER_SIDE_EMIT_RESPONSE = 10,
25
- BROADCAST_CLIENT_COUNT = 11,
26
- BROADCAST_ACK = 12
27
- }
28
- export interface ClusterMessage {
29
- uid: string;
30
- nsp: string;
31
- type: MessageType;
32
- data?: Record<string, unknown>;
33
- }
34
- export declare abstract class ClusterAdapter extends Adapter {
35
- #private;
36
- protected constructor(nsp: any, opts: Required<ClusterAdapterOptions>);
37
- protected initHeartbeat(): void;
38
- close(): Promise<void> | void;
39
- onMessage(message: ClusterMessage, offset: string): Promise<any>;
40
- broadcast(packet: any, opts: BroadcastOptions): Promise<any>;
41
- broadcastWithAck(packet: any, opts: BroadcastOptions, clientCountCallback: (clientCount: number) => void, ack: (...args: any[]) => void): void;
42
- serverCount(): Promise<number>;
43
- /**
44
- *
45
- * @param opts
46
- * @param rooms
47
- */
48
- addSockets(opts: BroadcastOptions, rooms: Room[]): void;
49
- delSockets(opts: BroadcastOptions, rooms: Room[]): void;
50
- disconnectSockets(opts: BroadcastOptions, close: boolean): void;
51
- fetchSockets(opts: BroadcastOptions): Promise<any[]>;
52
- serverSideEmit(packet: any[]): any;
53
- abstract doPublish(message: ClusterMessage): Promise<string>;
54
- }
@@ -1,420 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ClusterAdapter = exports.MessageType = void 0;
4
- const socket_io_adapter_1 = require("socket.io-adapter");
5
- const debug_1 = require("debug");
6
- const util_1 = require("./util");
7
- const debug = (0, debug_1.default)("socket.io-adapter");
8
- const EMITTER_UID = "emitter";
9
- const DEFAULT_TIMEOUT = 5000;
10
- var MessageType;
11
- (function (MessageType) {
12
- MessageType[MessageType["INITIAL_HEARTBEAT"] = 1] = "INITIAL_HEARTBEAT";
13
- MessageType[MessageType["HEARTBEAT"] = 2] = "HEARTBEAT";
14
- MessageType[MessageType["BROADCAST"] = 3] = "BROADCAST";
15
- MessageType[MessageType["SOCKETS_JOIN"] = 4] = "SOCKETS_JOIN";
16
- MessageType[MessageType["SOCKETS_LEAVE"] = 5] = "SOCKETS_LEAVE";
17
- MessageType[MessageType["DISCONNECT_SOCKETS"] = 6] = "DISCONNECT_SOCKETS";
18
- MessageType[MessageType["FETCH_SOCKETS"] = 7] = "FETCH_SOCKETS";
19
- MessageType[MessageType["FETCH_SOCKETS_RESPONSE"] = 8] = "FETCH_SOCKETS_RESPONSE";
20
- MessageType[MessageType["SERVER_SIDE_EMIT"] = 9] = "SERVER_SIDE_EMIT";
21
- MessageType[MessageType["SERVER_SIDE_EMIT_RESPONSE"] = 10] = "SERVER_SIDE_EMIT_RESPONSE";
22
- MessageType[MessageType["BROADCAST_CLIENT_COUNT"] = 11] = "BROADCAST_CLIENT_COUNT";
23
- MessageType[MessageType["BROADCAST_ACK"] = 12] = "BROADCAST_ACK";
24
- })(MessageType = exports.MessageType || (exports.MessageType = {}));
25
- function encodeOptions(opts) {
26
- return {
27
- rooms: [...opts.rooms],
28
- except: [...opts.except],
29
- flags: opts.flags,
30
- };
31
- }
32
- function decodeOptions(opts) {
33
- return {
34
- rooms: new Set(opts.rooms),
35
- except: new Set(opts.except),
36
- flags: opts.flags,
37
- };
38
- }
39
- class ClusterAdapter extends socket_io_adapter_1.Adapter {
40
- #opts;
41
- #uid;
42
- #heartbeatTimer;
43
- #nodesMap = new Map(); // uid => timestamp of last message
44
- #requests = new Map();
45
- #ackRequests = new Map();
46
- constructor(nsp, opts) {
47
- super(nsp);
48
- this.#opts = opts;
49
- this.#uid = (0, util_1.randomId)();
50
- }
51
- initHeartbeat() {
52
- this.#publish({
53
- type: MessageType.INITIAL_HEARTBEAT,
54
- });
55
- }
56
- #scheduleHeartbeat() {
57
- if (this.#heartbeatTimer) {
58
- clearTimeout(this.#heartbeatTimer);
59
- }
60
- this.#heartbeatTimer = setTimeout(() => {
61
- this.#publish({
62
- type: MessageType.HEARTBEAT,
63
- });
64
- }, this.#opts.heartbeatInterval);
65
- }
66
- close() {
67
- clearTimeout(this.#heartbeatTimer);
68
- }
69
- async onMessage(message, offset) {
70
- if (message.uid === this.#uid) {
71
- return debug("ignore message from self");
72
- }
73
- if (message.uid && message.uid !== EMITTER_UID) {
74
- this.#nodesMap.set(message.uid, Date.now());
75
- }
76
- debug("new event of type %d from %s", message.type, message.uid);
77
- switch (message.type) {
78
- case MessageType.INITIAL_HEARTBEAT:
79
- this.#publish({
80
- type: MessageType.HEARTBEAT,
81
- });
82
- break;
83
- case MessageType.BROADCAST: {
84
- const withAck = message.data.requestId !== undefined;
85
- if (withAck) {
86
- super.broadcastWithAck(message.data.packet, decodeOptions(message.data.opts), (clientCount) => {
87
- debug("waiting for %d client acknowledgements", clientCount);
88
- this.#publish({
89
- type: MessageType.BROADCAST_CLIENT_COUNT,
90
- data: {
91
- requestId: message.data.requestId,
92
- clientCount,
93
- },
94
- });
95
- }, (arg) => {
96
- debug("received acknowledgement with value %j", arg);
97
- this.#publish({
98
- type: MessageType.BROADCAST_ACK,
99
- data: {
100
- requestId: message.data.requestId,
101
- packet: arg,
102
- },
103
- });
104
- });
105
- }
106
- else {
107
- const packet = message.data.packet;
108
- const opts = decodeOptions(message.data.opts);
109
- this.#addOffsetIfNecessary(packet, opts, offset);
110
- super.broadcast(packet, opts);
111
- }
112
- break;
113
- }
114
- case MessageType.BROADCAST_CLIENT_COUNT: {
115
- const request = this.#ackRequests.get(message.data.requestId);
116
- request?.clientCountCallback(message.data.clientCount);
117
- break;
118
- }
119
- case MessageType.BROADCAST_ACK: {
120
- const request = this.#ackRequests.get(message.data.requestId);
121
- request?.ack(message.data.packet);
122
- break;
123
- }
124
- case MessageType.SOCKETS_JOIN:
125
- super.addSockets(decodeOptions(message.data.opts), message.data.rooms);
126
- break;
127
- case MessageType.SOCKETS_LEAVE:
128
- super.delSockets(decodeOptions(message.data.opts), message.data.rooms);
129
- break;
130
- case MessageType.DISCONNECT_SOCKETS:
131
- super.disconnectSockets(decodeOptions(message.data.opts), message.data.close);
132
- break;
133
- case MessageType.FETCH_SOCKETS: {
134
- debug("calling fetchSockets with opts %j", message.data.opts);
135
- const localSockets = await super.fetchSockets(decodeOptions(message.data.opts));
136
- this.#publish({
137
- type: MessageType.FETCH_SOCKETS_RESPONSE,
138
- data: {
139
- requestId: message.data.requestId,
140
- sockets: localSockets.map((socket) => {
141
- // remove sessionStore from handshake, as it may contain circular references
142
- const { sessionStore, ...handshake } = socket.handshake;
143
- return {
144
- id: socket.id,
145
- handshake,
146
- rooms: [...socket.rooms],
147
- data: socket.data,
148
- };
149
- }),
150
- },
151
- });
152
- break;
153
- }
154
- case MessageType.FETCH_SOCKETS_RESPONSE: {
155
- const requestId = message.data.requestId;
156
- const request = this.#requests.get(requestId);
157
- if (!request) {
158
- return;
159
- }
160
- request.current++;
161
- message.data.sockets.forEach((socket) => request.responses.push(socket));
162
- if (request.current === request.expected) {
163
- clearTimeout(request.timeout);
164
- request.resolve(request.responses);
165
- this.#requests.delete(requestId);
166
- }
167
- break;
168
- }
169
- case MessageType.SERVER_SIDE_EMIT: {
170
- const packet = message.data.packet;
171
- const withAck = message.data.requestId !== undefined;
172
- if (!withAck) {
173
- this.nsp._onServerSideEmit(packet);
174
- return;
175
- }
176
- let called = false;
177
- const callback = (arg) => {
178
- // only one argument is expected
179
- if (called) {
180
- return;
181
- }
182
- called = true;
183
- debug("calling acknowledgement with %j", arg);
184
- this.#publish({
185
- type: MessageType.SERVER_SIDE_EMIT_RESPONSE,
186
- data: {
187
- requestId: message.data.requestId,
188
- packet: arg,
189
- },
190
- });
191
- };
192
- packet.push(callback);
193
- this.nsp._onServerSideEmit(packet);
194
- break;
195
- }
196
- case MessageType.SERVER_SIDE_EMIT_RESPONSE: {
197
- const requestId = message.data.requestId;
198
- const request = this.#requests.get(requestId);
199
- if (!request) {
200
- return;
201
- }
202
- request.current++;
203
- request.responses.push(message.data.packet);
204
- if (request.current === request.expected) {
205
- clearTimeout(request.timeout);
206
- request.resolve(null, request.responses);
207
- this.#requests.delete(requestId);
208
- }
209
- }
210
- }
211
- }
212
- async broadcast(packet, opts) {
213
- const onlyLocal = opts.flags?.local;
214
- if (!onlyLocal) {
215
- try {
216
- const offset = await this.#publish({
217
- type: MessageType.BROADCAST,
218
- data: {
219
- packet,
220
- opts: encodeOptions(opts),
221
- },
222
- });
223
- this.#addOffsetIfNecessary(packet, opts, offset);
224
- }
225
- catch (e) {
226
- return debug("error while broadcasting message: %s", e.message);
227
- }
228
- }
229
- super.broadcast(packet, opts);
230
- }
231
- /**
232
- * Adds an offset at the end of the data array in order to allow the client to receive any missed packets when it
233
- * reconnects after a temporary disconnection.
234
- *
235
- * @param packet
236
- * @param opts
237
- * @param offset
238
- * @private
239
- */
240
- #addOffsetIfNecessary(packet, opts, offset) {
241
- if (!this.nsp.server.opts.connectionStateRecovery) {
242
- return;
243
- }
244
- const isEventPacket = packet.type === 2;
245
- // packets with acknowledgement are not stored because the acknowledgement function cannot be serialized and
246
- // restored on another server upon reconnection
247
- const withoutAcknowledgement = packet.id === undefined;
248
- const notVolatile = opts.flags?.volatile === undefined;
249
- if (isEventPacket && withoutAcknowledgement && notVolatile) {
250
- packet.data.push(offset);
251
- }
252
- }
253
- broadcastWithAck(packet, opts, clientCountCallback, ack) {
254
- const onlyLocal = opts?.flags?.local;
255
- if (!onlyLocal) {
256
- const requestId = (0, util_1.randomId)();
257
- this.#publish({
258
- type: MessageType.BROADCAST,
259
- data: {
260
- packet,
261
- requestId,
262
- opts: encodeOptions(opts),
263
- },
264
- });
265
- this.#ackRequests.set(requestId, {
266
- clientCountCallback,
267
- ack,
268
- });
269
- // we have no way to know at this level whether the server has received an acknowledgement from each client, so we
270
- // will simply clean up the ackRequests map after the given delay
271
- setTimeout(() => {
272
- this.#ackRequests.delete(requestId);
273
- }, opts.flags.timeout);
274
- }
275
- super.broadcastWithAck(packet, opts, clientCountCallback, ack);
276
- }
277
- serverCount() {
278
- return Promise.resolve(1 + this.#nodesMap.size);
279
- }
280
- /**
281
- *
282
- * @param opts
283
- * @param rooms
284
- */
285
- addSockets(opts, rooms) {
286
- super.addSockets(opts, rooms);
287
- const onlyLocal = opts.flags?.local;
288
- if (onlyLocal) {
289
- return;
290
- }
291
- this.#publish({
292
- type: MessageType.SOCKETS_JOIN,
293
- data: {
294
- opts: encodeOptions(opts),
295
- rooms,
296
- },
297
- });
298
- }
299
- delSockets(opts, rooms) {
300
- super.delSockets(opts, rooms);
301
- const onlyLocal = opts.flags?.local;
302
- if (onlyLocal) {
303
- return;
304
- }
305
- this.#publish({
306
- type: MessageType.SOCKETS_LEAVE,
307
- data: {
308
- opts: encodeOptions(opts),
309
- rooms,
310
- },
311
- });
312
- }
313
- disconnectSockets(opts, close) {
314
- super.disconnectSockets(opts, close);
315
- const onlyLocal = opts.flags?.local;
316
- if (onlyLocal) {
317
- return;
318
- }
319
- this.#publish({
320
- type: MessageType.DISCONNECT_SOCKETS,
321
- data: {
322
- opts: encodeOptions(opts),
323
- close,
324
- },
325
- });
326
- }
327
- #getExpectedResponseCount() {
328
- this.#nodesMap.forEach((lastSeen, uid) => {
329
- const nodeSeemsDown = Date.now() - lastSeen > this.#opts.heartbeatTimeout;
330
- if (nodeSeemsDown) {
331
- debug("node %s seems down", uid);
332
- this.#nodesMap.delete(uid);
333
- }
334
- });
335
- return this.#nodesMap.size;
336
- }
337
- async fetchSockets(opts) {
338
- const localSockets = await super.fetchSockets(opts);
339
- const expectedResponseCount = this.#getExpectedResponseCount();
340
- if (opts.flags?.local || expectedResponseCount === 0) {
341
- return localSockets;
342
- }
343
- const requestId = (0, util_1.randomId)();
344
- return new Promise((resolve, reject) => {
345
- const timeout = setTimeout(() => {
346
- const storedRequest = this.#requests.get(requestId);
347
- if (storedRequest) {
348
- reject(new Error(`timeout reached: only ${storedRequest.current} responses received out of ${storedRequest.expected}`));
349
- this.#requests.delete(requestId);
350
- }
351
- }, opts.flags.timeout || DEFAULT_TIMEOUT);
352
- const storedRequest = {
353
- type: MessageType.FETCH_SOCKETS,
354
- resolve,
355
- timeout,
356
- current: 0,
357
- expected: expectedResponseCount,
358
- responses: localSockets,
359
- };
360
- this.#requests.set(requestId, storedRequest);
361
- this.#publish({
362
- type: MessageType.FETCH_SOCKETS,
363
- data: {
364
- opts: encodeOptions(opts),
365
- requestId,
366
- },
367
- });
368
- });
369
- }
370
- serverSideEmit(packet) {
371
- const withAck = typeof packet[packet.length - 1] === "function";
372
- if (!withAck) {
373
- return this.#publish({
374
- type: MessageType.SERVER_SIDE_EMIT,
375
- data: {
376
- packet,
377
- },
378
- });
379
- }
380
- const ack = packet.pop();
381
- const expectedResponseCount = this.#getExpectedResponseCount();
382
- debug('waiting for %d responses to "serverSideEmit" request', expectedResponseCount);
383
- if (expectedResponseCount <= 0) {
384
- return ack(null, []);
385
- }
386
- const requestId = (0, util_1.randomId)();
387
- const timeout = setTimeout(() => {
388
- const storedRequest = this.#requests.get(requestId);
389
- if (storedRequest) {
390
- ack(new Error(`timeout reached: only ${storedRequest.current} responses received out of ${storedRequest.expected}`), storedRequest.responses);
391
- this.#requests.delete(requestId);
392
- }
393
- }, DEFAULT_TIMEOUT);
394
- const storedRequest = {
395
- type: MessageType.SERVER_SIDE_EMIT,
396
- resolve: ack,
397
- timeout,
398
- current: 0,
399
- expected: expectedResponseCount,
400
- responses: [],
401
- };
402
- this.#requests.set(requestId, storedRequest);
403
- this.#publish({
404
- type: MessageType.SERVER_SIDE_EMIT,
405
- data: {
406
- requestId,
407
- packet,
408
- },
409
- });
410
- }
411
- #publish(message) {
412
- this.#scheduleHeartbeat();
413
- return this.doPublish({
414
- uid: this.#uid,
415
- nsp: this.nsp.name,
416
- ...message,
417
- });
418
- }
419
- }
420
- exports.ClusterAdapter = ClusterAdapter;
@@ -1,47 +0,0 @@
1
- import type { PrivateSessionId, Session } from "socket.io-adapter";
2
- import { ClusterAdapter, ClusterAdapterOptions, ClusterMessage } from "./cluster-adapter";
3
- export interface RedisAdapterOptions extends ClusterAdapterOptions {
4
- /**
5
- * The name of the stream.
6
- */
7
- streamName?: string;
8
- /**
9
- * The maximum size of the stream. Almost exact trimming (~) is used.
10
- * @default 10_000
11
- */
12
- maxLen?: number;
13
- /**
14
- * The number of elements to return per XREAD call
15
- * @default 100
16
- */
17
- readCount?: number;
18
- }
19
- interface RawClusterMessage {
20
- uid: string;
21
- nsp: string;
22
- type: string;
23
- data?: string;
24
- }
25
- /**
26
- * Returns a function that will create a RedisAdapter instance.
27
- *
28
- * @param redisClient - a Redis client that will be used to publish messages
29
- * @param opts - additional options
30
- */
31
- export declare function createAdapter(redisClient: any, opts?: RedisAdapterOptions): (nsp: any) => RedisStreamsAdapter;
32
- declare class RedisStreamsAdapter extends ClusterAdapter {
33
- #private;
34
- constructor(nsp: any, redisClient: any, opts: Required<RedisAdapterOptions>);
35
- doPublish(message: ClusterMessage): any;
36
- static encode(message: ClusterMessage): RawClusterMessage;
37
- onRawMessage(rawMessage: RawClusterMessage, offset: string): any;
38
- static decode(rawMessage: RawClusterMessage): ClusterMessage;
39
- persistSession(session: any): void;
40
- restoreSession(pid: PrivateSessionId, offset: string): Promise<Session>;
41
- /**
42
- * Exclusive ranges were added in Redis 6.2, so this is necessary for previous versions.
43
- * @param offset
44
- */
45
- static nextOffset(offset: any): string;
46
- }
47
- export {};
@@ -1,212 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createAdapter = void 0;
4
- const msgpack_1 = require("@msgpack/msgpack");
5
- const redis_1 = require("redis");
6
- const cluster_adapter_1 = require("./cluster-adapter");
7
- const debug_1 = require("debug");
8
- const util_1 = require("./util");
9
- // TODO
10
- // BROADCAST / other channels
11
- // MAXLEN
12
- const debug = (0, debug_1.default)("socket.io-redis-streams-adapter");
13
- /**
14
- * Returns a function that will create a RedisAdapter instance.
15
- *
16
- * @param redisClient - a Redis client that will be used to publish messages
17
- * @param opts - additional options
18
- */
19
- function createAdapter(redisClient, opts) {
20
- const namespaceToAdapters = new Map();
21
- const options = Object.assign({
22
- streamName: "socket.io",
23
- maxLen: 1000,
24
- readCount: 100,
25
- heartbeatInterval: 5000,
26
- heartbeatTimeout: 10000,
27
- }, opts);
28
- let offset = "$";
29
- let polling = false;
30
- async function poll() {
31
- try {
32
- let response = await redisClient.xRead((0, redis_1.commandOptions)({
33
- isolated: true,
34
- }), [
35
- {
36
- key: options.streamName,
37
- id: offset,
38
- },
39
- ], {
40
- COUNT: options.readCount,
41
- BLOCK: 5000,
42
- });
43
- if (response) {
44
- for (const entry of response[0].messages) {
45
- debug("reading entry %s", entry.id);
46
- const message = entry.message;
47
- if (message.nsp) {
48
- namespaceToAdapters
49
- .get(message.nsp)
50
- ?.onRawMessage(message, entry.id);
51
- }
52
- offset = entry.id;
53
- }
54
- }
55
- }
56
- catch (e) {
57
- debug("something went wrong while consuming the stream: %s", e.message);
58
- }
59
- if (namespaceToAdapters.size > 0 && redisClient.isOpen) {
60
- poll();
61
- }
62
- else {
63
- polling = false;
64
- }
65
- }
66
- return function (nsp) {
67
- const adapter = new RedisStreamsAdapter(nsp, redisClient, options);
68
- namespaceToAdapters.set(nsp.name, adapter);
69
- if (!polling) {
70
- polling = true;
71
- poll();
72
- }
73
- const defaultClose = adapter.close;
74
- adapter.close = () => {
75
- namespaceToAdapters.delete(nsp.name);
76
- defaultClose.call(adapter);
77
- };
78
- return adapter;
79
- };
80
- }
81
- exports.createAdapter = createAdapter;
82
- class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
83
- #redisClient;
84
- #opts;
85
- constructor(nsp, redisClient, opts) {
86
- super(nsp, opts);
87
- this.#redisClient = redisClient;
88
- this.#opts = opts;
89
- this.initHeartbeat();
90
- }
91
- doPublish(message) {
92
- debug("publishing %o", message);
93
- return this.#redisClient.xAdd(this.#opts.streamName, "*", RedisStreamsAdapter.encode(message), {
94
- TRIM: {
95
- strategy: "MAXLEN",
96
- strategyModifier: "~",
97
- threshold: this.#opts.maxLen,
98
- },
99
- });
100
- }
101
- static encode(message) {
102
- const rawMessage = {
103
- uid: message.uid,
104
- nsp: message.nsp,
105
- type: message.type.toString(),
106
- };
107
- if (message.data) {
108
- const mayContainBinary = [
109
- cluster_adapter_1.MessageType.BROADCAST,
110
- cluster_adapter_1.MessageType.BROADCAST_ACK,
111
- cluster_adapter_1.MessageType.FETCH_SOCKETS_RESPONSE,
112
- cluster_adapter_1.MessageType.SERVER_SIDE_EMIT,
113
- cluster_adapter_1.MessageType.SERVER_SIDE_EMIT_RESPONSE,
114
- ].includes(message.type);
115
- if (mayContainBinary && (0, util_1.hasBinary)(message.data)) {
116
- rawMessage.data = Buffer.from((0, msgpack_1.encode)(message.data)).toString("base64");
117
- }
118
- else {
119
- rawMessage.data = JSON.stringify(message.data);
120
- }
121
- }
122
- return rawMessage;
123
- }
124
- onRawMessage(rawMessage, offset) {
125
- let message;
126
- try {
127
- message = RedisStreamsAdapter.decode(rawMessage);
128
- }
129
- catch (e) {
130
- return debug("invalid format: %s", e.message);
131
- }
132
- this.onMessage(message, offset);
133
- }
134
- static decode(rawMessage) {
135
- const message = {
136
- uid: rawMessage.uid,
137
- nsp: rawMessage.nsp,
138
- type: parseInt(rawMessage.type, 10),
139
- };
140
- if (rawMessage.data) {
141
- if (rawMessage.data.startsWith("{")) {
142
- message.data = JSON.parse(rawMessage.data);
143
- }
144
- else {
145
- message.data = (0, msgpack_1.decode)(Buffer.from(rawMessage.data, "base64"));
146
- }
147
- }
148
- return message;
149
- }
150
- persistSession(session) {
151
- debug("persist session %o", session);
152
- this.#redisClient.set(session.pid, JSON.stringify(session), {
153
- PX: this.nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration,
154
- });
155
- }
156
- async restoreSession(pid, offset) {
157
- console.log("restoreSession", pid);
158
- console.log("offset", offset);
159
- if (!/^[0-9]+-[0-9]+$/.test(offset)) {
160
- console.log("invalid offset");
161
- return Promise.reject("invalid offset");
162
- }
163
- const value = await this.#redisClient.get(pid);
164
- if (!value) {
165
- console.log("session or offset not found");
166
- return Promise.reject("session or offset not found");
167
- }
168
- const session = JSON.parse(value);
169
- console.log("session", session);
170
- session.missedPackets = [];
171
- // FIXME si les messages sont ajoutés plus vite que consommés, problème !
172
- for (let i = 0; i < 10; i++) {
173
- const entries = await this.#redisClient.xRange(this.#opts.streamName, RedisStreamsAdapter.nextOffset(offset), "+", {
174
- COUNT: 100,
175
- });
176
- console.log("entries", entries.length);
177
- console.log("offset", offset);
178
- if (entries.length === 0) {
179
- break;
180
- }
181
- for (const entry of entries) {
182
- if (entry.message.nsp === this.nsp.name && entry.message.type === "3") {
183
- const message = RedisStreamsAdapter.decode(entry.message);
184
- console.log("message from the log", message);
185
- if (message.type === cluster_adapter_1.MessageType.BROADCAST &&
186
- shouldIncludePacket(session.rooms, message.data.opts)) {
187
- // TODO match rooms
188
- // @ts-ignore
189
- session.missedPackets.push(message.data.packet.data);
190
- }
191
- }
192
- offset = entry.id;
193
- }
194
- }
195
- console.log("restored", session);
196
- return session;
197
- }
198
- /**
199
- * Exclusive ranges were added in Redis 6.2, so this is necessary for previous versions.
200
- * @param offset
201
- */
202
- static nextOffset(offset) {
203
- const [timestamp, sequence] = offset.split("-");
204
- return timestamp + "-" + (parseInt(sequence) + 1);
205
- }
206
- }
207
- function shouldIncludePacket(sessionRooms, opts) {
208
- const included = opts.rooms.length === 0 ||
209
- sessionRooms.some((room) => opts.rooms.indexOf(room) !== -1);
210
- const notExcluded = sessionRooms.every((room) => opts.except.indexOf(room) === -1);
211
- return included && notExcluded;
212
- }
package/dist/test.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/dist/test.js DELETED
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const socket_io_1 = require("socket.io");
4
- const redis_1 = require("redis");
5
- const index_1 = require("./index");
6
- const io = new socket_io_1.Server();
7
- const pubClient = (0, redis_1.createClient)({ host: 'localhost', port: 6379 });
8
- const subClient = pubClient.duplicate();
9
- Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
10
- io.adapter((0, index_1.createAdapter)(pubClient, subClient));
11
- io.listen(3000);
12
- });