@socket.io/redis-streams-adapter 0.1.0 → 0.2.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 CHANGED
@@ -43,7 +43,7 @@ import { createClient } from "redis";
43
43
  import { Server } from "socket.io";
44
44
  import { createAdapter } from "@socket.io/redis-streams-adapter";
45
45
 
46
- const redisClient = createClient({ host: "localhost", port: 6379 });
46
+ const redisClient = createClient({ url: "redis://localhost:6379" });
47
47
 
48
48
  await redisClient.connect();
49
49
 
@@ -56,13 +56,14 @@ io.listen(3000);
56
56
 
57
57
  ## Options
58
58
 
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` |
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
+ | `sessionKeyPrefix` | The prefix of the key used to store the Socket.IO session, when the connection state recovery feature is enabled. | `sio:session:` |
65
+ | `heartbeatInterval` | The number of ms between two heartbeats. | `5_000` |
66
+ | `heartbeatTimeout` | The number of ms without heartbeat before we consider a node down. | `10_000` |
66
67
 
67
68
  ## How it works
68
69
 
package/dist/adapter.d.ts CHANGED
@@ -1,8 +1,20 @@
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, 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
+ }
14
+ export interface RedisStreamsAdapterOptions {
4
15
  /**
5
16
  * The name of the Redis stream.
17
+ * @default "socket.io"
6
18
  */
7
19
  streamName?: string;
8
20
  /**
@@ -15,6 +27,11 @@ export interface RedisStreamsAdapterOptions extends ClusterAdapterOptions {
15
27
  * @default 100
16
28
  */
17
29
  readCount?: number;
30
+ /**
31
+ * The prefix of the key used to store the Socket.IO session, when the connection state recovery feature is enabled.
32
+ * @default "sio:session:"
33
+ */
34
+ sessionKeyPrefix?: string;
18
35
  }
19
36
  interface RawClusterMessage {
20
37
  uid: string;
@@ -28,11 +45,12 @@ interface RawClusterMessage {
28
45
  * @param redisClient - a Redis client that will be used to publish messages
29
46
  * @param opts - additional options
30
47
  */
31
- export declare function createAdapter(redisClient: any, opts?: RedisStreamsAdapterOptions): (nsp: any) => RedisStreamsAdapter;
32
- declare class RedisStreamsAdapter extends ClusterAdapter {
48
+ export declare function createAdapter(redisClient: any, opts?: RedisStreamsAdapterOptions & ClusterAdapterOptions): (nsp: any) => RedisStreamsAdapter;
49
+ declare class RedisStreamsAdapter extends ClusterAdapterWithHeartbeat {
33
50
  #private;
34
- constructor(nsp: any, redisClient: any, opts: Required<RedisStreamsAdapterOptions>);
51
+ constructor(nsp: any, redisClient: any, opts: Required<RedisStreamsAdapterOptions> & ClusterAdapterOptions);
35
52
  doPublish(message: ClusterMessage): any;
53
+ protected doPublishResponse(requesterUid: ServerId, response: ClusterResponse): Promise<void>;
36
54
  static encode(message: ClusterMessage): RawClusterMessage;
37
55
  onRawMessage(rawMessage: RawClusterMessage, offset: string): any;
38
56
  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,23 @@ 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) {
100
+ // TODO MessageType should be exported by the socket.io-adapter package
106
101
  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,
102
+ 3,
103
+ 8,
104
+ 9,
105
+ 10,
106
+ 12, // MessageType.BROADCAST_ACK,
112
107
  ].includes(message.type);
108
+ // @ts-ignore
113
109
  if (mayContainBinary && (0, util_1.hasBinary)(message.data)) {
110
+ // @ts-ignore
114
111
  rawMessage.data = Buffer.from((0, msgpack_1.encode)(message.data)).toString("base64");
115
112
  }
116
113
  else {
114
+ // @ts-ignore
117
115
  rawMessage.data = JSON.stringify(message.data);
118
116
  }
119
117
  }
@@ -137,9 +135,11 @@ class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
137
135
  };
138
136
  if (rawMessage.data) {
139
137
  if (rawMessage.data.startsWith("{")) {
138
+ // @ts-ignore
140
139
  message.data = JSON.parse(rawMessage.data);
141
140
  }
142
141
  else {
142
+ // @ts-ignore
143
143
  message.data = (0, msgpack_1.decode)(Buffer.from(rawMessage.data, "base64"));
144
144
  }
145
145
  }
@@ -147,8 +147,9 @@ class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
147
147
  }
148
148
  persistSession(session) {
149
149
  debug("persisting session %o", session);
150
+ const sessionKey = this.#opts.sessionKeyPrefix + session.pid;
150
151
  const encodedSession = Buffer.from((0, msgpack_1.encode)(session)).toString("base64");
151
- this.#redisClient.set(`sio:session:${session.pid}`, encodedSession, {
152
+ this.#redisClient.set(sessionKey, encodedSession, {
152
153
  PX: this.nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration,
153
154
  });
154
155
  }
@@ -157,7 +158,7 @@ class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
157
158
  if (!/^[0-9]+-[0-9]+$/.test(offset)) {
158
159
  return Promise.reject("invalid offset");
159
160
  }
160
- const sessionKey = `sio:session:${pid}`;
161
+ const sessionKey = this.#opts.sessionKeyPrefix + pid;
161
162
  const [rawSession, offsetExists] = await this.#redisClient
162
163
  .multi()
163
164
  .get(sessionKey)
@@ -182,6 +183,7 @@ class RedisStreamsAdapter extends cluster_adapter_1.ClusterAdapter {
182
183
  for (const entry of entries) {
183
184
  if (entry.message.nsp === this.nsp.name && entry.message.type === "3") {
184
185
  const message = RedisStreamsAdapter.decode(entry.message);
186
+ // @ts-ignore
185
187
  if (shouldIncludePacket(session.rooms, message.data.opts)) {
186
188
  // @ts-ignore
187
189
  session.missedPackets.push(message.data.packet.data);
@@ -0,0 +1,184 @@
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 {};
@@ -0,0 +1,488 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClusterAdapterWithHeartbeat = exports.ClusterAdapter = exports.MessageType = void 0;
4
+ const socket_io_adapter_1 = require("socket.io-adapter");
5
+ const debug_1 = require("debug");
6
+ const crypto_1 = require("crypto");
7
+ const debug = (0, debug_1.default)("socket.io-adapter");
8
+ const EMITTER_UID = "emitter";
9
+ const DEFAULT_TIMEOUT = 5000;
10
+ const randomId = () => (0, crypto_1.randomBytes)(8).toString("hex");
11
+ var MessageType;
12
+ (function (MessageType) {
13
+ MessageType[MessageType["INITIAL_HEARTBEAT"] = 1] = "INITIAL_HEARTBEAT";
14
+ MessageType[MessageType["HEARTBEAT"] = 2] = "HEARTBEAT";
15
+ MessageType[MessageType["BROADCAST"] = 3] = "BROADCAST";
16
+ MessageType[MessageType["SOCKETS_JOIN"] = 4] = "SOCKETS_JOIN";
17
+ MessageType[MessageType["SOCKETS_LEAVE"] = 5] = "SOCKETS_LEAVE";
18
+ MessageType[MessageType["DISCONNECT_SOCKETS"] = 6] = "DISCONNECT_SOCKETS";
19
+ MessageType[MessageType["FETCH_SOCKETS"] = 7] = "FETCH_SOCKETS";
20
+ MessageType[MessageType["FETCH_SOCKETS_RESPONSE"] = 8] = "FETCH_SOCKETS_RESPONSE";
21
+ MessageType[MessageType["SERVER_SIDE_EMIT"] = 9] = "SERVER_SIDE_EMIT";
22
+ MessageType[MessageType["SERVER_SIDE_EMIT_RESPONSE"] = 10] = "SERVER_SIDE_EMIT_RESPONSE";
23
+ MessageType[MessageType["BROADCAST_CLIENT_COUNT"] = 11] = "BROADCAST_CLIENT_COUNT";
24
+ MessageType[MessageType["BROADCAST_ACK"] = 12] = "BROADCAST_ACK";
25
+ })(MessageType = exports.MessageType || (exports.MessageType = {}));
26
+ function encodeOptions(opts) {
27
+ return {
28
+ rooms: [...opts.rooms],
29
+ except: [...opts.except],
30
+ flags: opts.flags,
31
+ };
32
+ }
33
+ function decodeOptions(opts) {
34
+ return {
35
+ rooms: new Set(opts.rooms),
36
+ except: new Set(opts.except),
37
+ flags: opts.flags,
38
+ };
39
+ }
40
+ /**
41
+ * A cluster-ready adapter. Any extending class must:
42
+ *
43
+ * - implement {@link ClusterAdapter#publishMessage} and {@link ClusterAdapter#publishResponse}
44
+ * - call {@link ClusterAdapter#onMessage} and {@link ClusterAdapter#onResponse}
45
+ */
46
+ class ClusterAdapter extends socket_io_adapter_1.Adapter {
47
+ uid;
48
+ requests = new Map();
49
+ ackRequests = new Map();
50
+ constructor(nsp) {
51
+ super(nsp);
52
+ this.uid = randomId();
53
+ }
54
+ /**
55
+ * Called when receiving a message from another member of the cluster.
56
+ *
57
+ * @param message
58
+ * @param offset
59
+ * @protected
60
+ */
61
+ async onMessage(message, offset) {
62
+ if (message.uid === this.uid) {
63
+ debug("ignore message from self");
64
+ }
65
+ debug("new event of type %d from %s", message.type, message.uid);
66
+ switch (message.type) {
67
+ case MessageType.BROADCAST: {
68
+ const withAck = message.data.requestId !== undefined;
69
+ if (withAck) {
70
+ super.broadcastWithAck(message.data.packet, decodeOptions(message.data.opts), (clientCount) => {
71
+ debug("waiting for %d client acknowledgements", clientCount);
72
+ this.publishResponse(message.uid, {
73
+ type: MessageType.BROADCAST_CLIENT_COUNT,
74
+ data: {
75
+ requestId: message.data.requestId,
76
+ clientCount,
77
+ },
78
+ });
79
+ }, (arg) => {
80
+ debug("received acknowledgement with value %j", arg);
81
+ this.publishResponse(message.uid, {
82
+ type: MessageType.BROADCAST_ACK,
83
+ data: {
84
+ requestId: message.data.requestId,
85
+ packet: arg,
86
+ },
87
+ });
88
+ });
89
+ }
90
+ else {
91
+ const packet = message.data.packet;
92
+ const opts = decodeOptions(message.data.opts);
93
+ this.addOffsetIfNecessary(packet, opts, offset);
94
+ super.broadcast(packet, opts);
95
+ }
96
+ break;
97
+ }
98
+ case MessageType.SOCKETS_JOIN:
99
+ super.addSockets(decodeOptions(message.data.opts), message.data.rooms);
100
+ break;
101
+ case MessageType.SOCKETS_LEAVE:
102
+ super.delSockets(decodeOptions(message.data.opts), message.data.rooms);
103
+ break;
104
+ case MessageType.DISCONNECT_SOCKETS:
105
+ super.disconnectSockets(decodeOptions(message.data.opts), message.data.close);
106
+ break;
107
+ case MessageType.FETCH_SOCKETS: {
108
+ debug("calling fetchSockets with opts %j", message.data.opts);
109
+ const localSockets = await super.fetchSockets(decodeOptions(message.data.opts));
110
+ this.publishResponse(message.uid, {
111
+ type: MessageType.FETCH_SOCKETS_RESPONSE,
112
+ data: {
113
+ requestId: message.data.requestId,
114
+ sockets: localSockets.map((socket) => {
115
+ // remove sessionStore from handshake, as it may contain circular references
116
+ const { sessionStore, ...handshake } = socket.handshake;
117
+ return {
118
+ id: socket.id,
119
+ handshake,
120
+ rooms: [...socket.rooms],
121
+ data: socket.data,
122
+ };
123
+ }),
124
+ },
125
+ });
126
+ break;
127
+ }
128
+ case MessageType.SERVER_SIDE_EMIT: {
129
+ const packet = message.data.packet;
130
+ const withAck = message.data.requestId !== undefined;
131
+ if (!withAck) {
132
+ this.nsp._onServerSideEmit(packet);
133
+ return;
134
+ }
135
+ let called = false;
136
+ const callback = (arg) => {
137
+ // only one argument is expected
138
+ if (called) {
139
+ return;
140
+ }
141
+ called = true;
142
+ debug("calling acknowledgement with %j", arg);
143
+ this.publishResponse(message.uid, {
144
+ type: MessageType.SERVER_SIDE_EMIT_RESPONSE,
145
+ data: {
146
+ requestId: message.data.requestId,
147
+ packet: arg,
148
+ },
149
+ });
150
+ };
151
+ packet.push(callback);
152
+ this.nsp._onServerSideEmit(packet);
153
+ break;
154
+ }
155
+ // @ts-ignore
156
+ case MessageType.BROADCAST_CLIENT_COUNT:
157
+ // @ts-ignore
158
+ case MessageType.BROADCAST_ACK:
159
+ // @ts-ignore
160
+ case MessageType.FETCH_SOCKETS_RESPONSE:
161
+ // @ts-ignore
162
+ case MessageType.SERVER_SIDE_EMIT_RESPONSE:
163
+ this.onResponse(message);
164
+ break;
165
+ default:
166
+ debug("unknown message type: %s", message.type);
167
+ }
168
+ }
169
+ /**
170
+ * Called when receiving a response from another member of the cluster.
171
+ *
172
+ * @param response
173
+ * @protected
174
+ */
175
+ onResponse(response) {
176
+ const requestId = response.data.requestId;
177
+ debug("received response %s to request %s", response.type, requestId);
178
+ switch (response.type) {
179
+ case MessageType.BROADCAST_CLIENT_COUNT: {
180
+ this.ackRequests
181
+ .get(requestId)
182
+ ?.clientCountCallback(response.data.clientCount);
183
+ break;
184
+ }
185
+ case MessageType.BROADCAST_ACK: {
186
+ this.ackRequests.get(requestId)?.ack(response.data.packet);
187
+ break;
188
+ }
189
+ case MessageType.FETCH_SOCKETS_RESPONSE: {
190
+ const request = this.requests.get(requestId);
191
+ if (!request) {
192
+ return;
193
+ }
194
+ request.current++;
195
+ response.data.sockets.forEach((socket) => request.responses.push(socket));
196
+ if (request.current === request.expected) {
197
+ clearTimeout(request.timeout);
198
+ request.resolve(request.responses);
199
+ this.requests.delete(requestId);
200
+ }
201
+ break;
202
+ }
203
+ case MessageType.SERVER_SIDE_EMIT_RESPONSE: {
204
+ const request = this.requests.get(requestId);
205
+ if (!request) {
206
+ return;
207
+ }
208
+ request.current++;
209
+ request.responses.push(response.data.packet);
210
+ if (request.current === request.expected) {
211
+ clearTimeout(request.timeout);
212
+ request.resolve(null, request.responses);
213
+ this.requests.delete(requestId);
214
+ }
215
+ break;
216
+ }
217
+ default:
218
+ // @ts-ignore
219
+ debug("unknown response type: %s", response.type);
220
+ }
221
+ }
222
+ async broadcast(packet, opts) {
223
+ const onlyLocal = opts.flags?.local;
224
+ if (!onlyLocal) {
225
+ try {
226
+ const offset = await this.publish({
227
+ type: MessageType.BROADCAST,
228
+ data: {
229
+ packet,
230
+ opts: encodeOptions(opts),
231
+ },
232
+ });
233
+ this.addOffsetIfNecessary(packet, opts, offset);
234
+ }
235
+ catch (e) {
236
+ return debug("error while broadcasting message: %s", e.message);
237
+ }
238
+ }
239
+ super.broadcast(packet, opts);
240
+ }
241
+ /**
242
+ * Adds an offset at the end of the data array in order to allow the client to receive any missed packets when it
243
+ * reconnects after a temporary disconnection.
244
+ *
245
+ * @param packet
246
+ * @param opts
247
+ * @param offset
248
+ * @private
249
+ */
250
+ addOffsetIfNecessary(packet, opts, offset) {
251
+ if (!this.nsp.server.opts.connectionStateRecovery) {
252
+ return;
253
+ }
254
+ const isEventPacket = packet.type === 2;
255
+ // packets with acknowledgement are not stored because the acknowledgement function cannot be serialized and
256
+ // restored on another server upon reconnection
257
+ const withoutAcknowledgement = packet.id === undefined;
258
+ const notVolatile = opts.flags?.volatile === undefined;
259
+ if (isEventPacket && withoutAcknowledgement && notVolatile) {
260
+ packet.data.push(offset);
261
+ }
262
+ }
263
+ broadcastWithAck(packet, opts, clientCountCallback, ack) {
264
+ const onlyLocal = opts?.flags?.local;
265
+ if (!onlyLocal) {
266
+ const requestId = randomId();
267
+ this.publish({
268
+ type: MessageType.BROADCAST,
269
+ data: {
270
+ packet,
271
+ requestId,
272
+ opts: encodeOptions(opts),
273
+ },
274
+ });
275
+ this.ackRequests.set(requestId, {
276
+ clientCountCallback,
277
+ ack,
278
+ });
279
+ // we have no way to know at this level whether the server has received an acknowledgement from each client, so we
280
+ // will simply clean up the ackRequests map after the given delay
281
+ setTimeout(() => {
282
+ this.ackRequests.delete(requestId);
283
+ }, opts.flags.timeout);
284
+ }
285
+ super.broadcastWithAck(packet, opts, clientCountCallback, ack);
286
+ }
287
+ addSockets(opts, rooms) {
288
+ super.addSockets(opts, rooms);
289
+ const onlyLocal = opts.flags?.local;
290
+ if (onlyLocal) {
291
+ return;
292
+ }
293
+ this.publish({
294
+ type: MessageType.SOCKETS_JOIN,
295
+ data: {
296
+ opts: encodeOptions(opts),
297
+ rooms,
298
+ },
299
+ });
300
+ }
301
+ delSockets(opts, rooms) {
302
+ super.delSockets(opts, rooms);
303
+ const onlyLocal = opts.flags?.local;
304
+ if (onlyLocal) {
305
+ return;
306
+ }
307
+ this.publish({
308
+ type: MessageType.SOCKETS_LEAVE,
309
+ data: {
310
+ opts: encodeOptions(opts),
311
+ rooms,
312
+ },
313
+ });
314
+ }
315
+ disconnectSockets(opts, close) {
316
+ super.disconnectSockets(opts, close);
317
+ const onlyLocal = opts.flags?.local;
318
+ if (onlyLocal) {
319
+ return;
320
+ }
321
+ this.publish({
322
+ type: MessageType.DISCONNECT_SOCKETS,
323
+ data: {
324
+ opts: encodeOptions(opts),
325
+ close,
326
+ },
327
+ });
328
+ }
329
+ async fetchSockets(opts) {
330
+ const [localSockets, serverCount] = await Promise.all([
331
+ super.fetchSockets(opts),
332
+ this.serverCount(),
333
+ ]);
334
+ const expectedResponseCount = serverCount - 1;
335
+ if (opts.flags?.local || expectedResponseCount === 0) {
336
+ return localSockets;
337
+ }
338
+ const requestId = randomId();
339
+ return new Promise((resolve, reject) => {
340
+ const timeout = setTimeout(() => {
341
+ const storedRequest = this.requests.get(requestId);
342
+ if (storedRequest) {
343
+ reject(new Error(`timeout reached: only ${storedRequest.current} responses received out of ${storedRequest.expected}`));
344
+ this.requests.delete(requestId);
345
+ }
346
+ }, opts.flags.timeout || DEFAULT_TIMEOUT);
347
+ const storedRequest = {
348
+ type: MessageType.FETCH_SOCKETS,
349
+ resolve,
350
+ timeout,
351
+ current: 0,
352
+ expected: expectedResponseCount,
353
+ responses: localSockets,
354
+ };
355
+ this.requests.set(requestId, storedRequest);
356
+ this.publish({
357
+ type: MessageType.FETCH_SOCKETS,
358
+ data: {
359
+ opts: encodeOptions(opts),
360
+ requestId,
361
+ },
362
+ });
363
+ });
364
+ }
365
+ async serverSideEmit(packet) {
366
+ const withAck = typeof packet[packet.length - 1] === "function";
367
+ if (!withAck) {
368
+ return this.publish({
369
+ type: MessageType.SERVER_SIDE_EMIT,
370
+ data: {
371
+ packet,
372
+ },
373
+ });
374
+ }
375
+ const ack = packet.pop();
376
+ const expectedResponseCount = (await this.serverCount()) - 1;
377
+ debug('waiting for %d responses to "serverSideEmit" request', expectedResponseCount);
378
+ if (expectedResponseCount <= 0) {
379
+ return ack(null, []);
380
+ }
381
+ const requestId = randomId();
382
+ const timeout = setTimeout(() => {
383
+ const storedRequest = this.requests.get(requestId);
384
+ if (storedRequest) {
385
+ ack(new Error(`timeout reached: only ${storedRequest.current} responses received out of ${storedRequest.expected}`), storedRequest.responses);
386
+ this.requests.delete(requestId);
387
+ }
388
+ }, DEFAULT_TIMEOUT);
389
+ const storedRequest = {
390
+ type: MessageType.SERVER_SIDE_EMIT,
391
+ resolve: ack,
392
+ timeout,
393
+ current: 0,
394
+ expected: expectedResponseCount,
395
+ responses: [],
396
+ };
397
+ this.requests.set(requestId, storedRequest);
398
+ this.publish({
399
+ type: MessageType.SERVER_SIDE_EMIT,
400
+ data: {
401
+ requestId,
402
+ packet,
403
+ },
404
+ });
405
+ }
406
+ publish(message) {
407
+ return this.publishMessage({
408
+ uid: this.uid,
409
+ nsp: this.nsp.name,
410
+ ...message,
411
+ });
412
+ }
413
+ publishResponse(requesterUid, message) {
414
+ const response = message;
415
+ response.uid = this.uid;
416
+ response.nsp = this.nsp.name;
417
+ return this.doPublishResponse(requesterUid, response);
418
+ }
419
+ }
420
+ exports.ClusterAdapter = ClusterAdapter;
421
+ class ClusterAdapterWithHeartbeat extends ClusterAdapter {
422
+ _opts;
423
+ heartbeatTimer;
424
+ nodesMap = new Map(); // uid => timestamp of last message
425
+ constructor(nsp, opts) {
426
+ super(nsp);
427
+ this._opts = Object.assign({
428
+ heartbeatInterval: 5000,
429
+ heartbeatTimeout: 10000,
430
+ }, opts);
431
+ }
432
+ init() {
433
+ this.publish({
434
+ type: MessageType.INITIAL_HEARTBEAT,
435
+ });
436
+ }
437
+ scheduleHeartbeat() {
438
+ if (this.heartbeatTimer) {
439
+ clearTimeout(this.heartbeatTimer);
440
+ }
441
+ this.heartbeatTimer = setTimeout(() => {
442
+ this.publish({
443
+ type: MessageType.HEARTBEAT,
444
+ });
445
+ }, this._opts.heartbeatInterval);
446
+ }
447
+ close() {
448
+ if (this.heartbeatTimer)
449
+ clearTimeout(this.heartbeatTimer);
450
+ }
451
+ async onMessage(message, offset) {
452
+ if (message.uid === this.uid) {
453
+ return debug("ignore message from self");
454
+ }
455
+ if (message.uid && message.uid !== EMITTER_UID) {
456
+ // we track the UID of each sender, in order to know how many servers there are in the cluster
457
+ this.nodesMap.set(message.uid, Date.now());
458
+ }
459
+ switch (message.type) {
460
+ case MessageType.INITIAL_HEARTBEAT:
461
+ this.publish({
462
+ type: MessageType.HEARTBEAT,
463
+ });
464
+ break;
465
+ case MessageType.HEARTBEAT:
466
+ // nothing to do
467
+ break;
468
+ default:
469
+ return super.onMessage(message, offset);
470
+ }
471
+ }
472
+ serverCount() {
473
+ const now = Date.now();
474
+ this.nodesMap.forEach((lastSeen, uid) => {
475
+ const nodeSeemsDown = now - lastSeen > this._opts.heartbeatTimeout;
476
+ if (nodeSeemsDown) {
477
+ debug("node %s seems down", uid);
478
+ this.nodesMap.delete(uid);
479
+ }
480
+ });
481
+ return Promise.resolve(1 + this.nodesMap.size);
482
+ }
483
+ publish(message) {
484
+ this.scheduleHeartbeat();
485
+ return super.publish(message);
486
+ }
487
+ }
488
+ exports.ClusterAdapterWithHeartbeat = ClusterAdapterWithHeartbeat;
package/dist/util.d.ts CHANGED
@@ -1,2 +1,10 @@
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;
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.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,79 @@ 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
+ * @see https://redis.io/commands/xread/
48
+ */
49
+ function XREAD(redisClient, streamName, offset, readCount) {
50
+ if (isRedisV4Client(redisClient)) {
51
+ return redisClient.xRead((0, redis_1.commandOptions)({
52
+ isolated: true,
53
+ }), [
54
+ {
55
+ key: streamName,
56
+ id: offset,
57
+ },
58
+ ], {
59
+ COUNT: readCount,
60
+ BLOCK: 5000,
61
+ });
62
+ }
63
+ else {
64
+ return redisClient
65
+ .xread("BLOCK", 100, "COUNT", readCount, "STREAMS", streamName, offset)
66
+ .then((results) => {
67
+ if (results === null) {
68
+ return null;
69
+ }
70
+ return [
71
+ {
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
+ }),
84
+ },
85
+ ];
86
+ });
87
+ }
88
+ }
89
+ exports.XREAD = XREAD;
90
+ /**
91
+ * @see https://redis.io/commands/xadd/
92
+ */
93
+ function XADD(redisClient, streamName, payload, maxLenThreshold) {
94
+ if (isRedisV4Client(redisClient)) {
95
+ return redisClient.xAdd(streamName, "*", payload, {
96
+ TRIM: {
97
+ strategy: "MAXLEN",
98
+ strategyModifier: "~",
99
+ threshold: maxLenThreshold,
100
+ },
101
+ });
102
+ }
103
+ else {
104
+ const args = [streamName, "MAXLEN", "~", maxLenThreshold, "*"];
105
+ Object.keys(payload).forEach((k) => {
106
+ args.push(k, payload[k]);
107
+ });
108
+ return redisClient.xadd.call(redisClient, args);
109
+ }
110
+ }
111
+ exports.XADD = XADD;
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.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": {
@@ -14,23 +14,32 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "scripts": {
16
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",
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.3"
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",
@@ -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
- });