@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 +9 -8
- package/dist/adapter.d.ts +24 -6
- package/dist/adapter.js +32 -30
- package/dist/cluster-adapter-v2.d.ts +184 -0
- package/dist/cluster-adapter-v2.js +488 -0
- package/dist/util.d.ts +8 -0
- package/dist/util.js +78 -1
- package/package.json +14 -5
- package/dist/redis-streams-adapter.d.ts +0 -47
- package/dist/redis-streams-adapter.js +0 -212
- package/dist/test.d.ts +0 -1
- package/dist/test.js +0 -12
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({
|
|
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
|
|
60
|
-
|
|
61
|
-
| `streamName` | The name of the Redis stream.
|
|
62
|
-
| `maxLen` | The maximum size of the stream. Almost exact trimming (~) is used.
|
|
63
|
-
| `readCount` | The number of elements to fetch per XREAD call.
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
|
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.
|
|
82
|
+
this.init();
|
|
88
83
|
}
|
|
89
84
|
doPublish(message) {
|
|
90
85
|
debug("publishing %o", message);
|
|
91
|
-
return this.#redisClient
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
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 =
|
|
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.
|
|
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 &&
|
|
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.
|
|
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
|
-
});
|