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