@syncular/core 0.0.6-126 → 0.0.6-136
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/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/realtime-connection-registry.d.ts +34 -0
- package/dist/utils/realtime-connection-registry.d.ts.map +1 -0
- package/dist/utils/realtime-connection-registry.js +218 -0
- package/dist/utils/realtime-connection-registry.js.map +1 -0
- package/package.json +1 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/realtime-connection-registry.ts +253 -0
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC;AACzB,cAAc,gCAAgC,CAAC"}
|
package/dist/utils/index.js
CHANGED
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC;AACzB,cAAc,gCAAgC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface RealtimeConnection {
|
|
2
|
+
readonly clientId: string;
|
|
3
|
+
readonly isOpen: boolean;
|
|
4
|
+
sendHeartbeat(): void;
|
|
5
|
+
close(code?: number, reason?: string): void;
|
|
6
|
+
}
|
|
7
|
+
export declare class RealtimeConnectionRegistry<TConnection extends RealtimeConnection> {
|
|
8
|
+
private connectionsByClientId;
|
|
9
|
+
private scopeKeysByClientId;
|
|
10
|
+
private connectionsByScopeKey;
|
|
11
|
+
private readonly heartbeatIntervalMs;
|
|
12
|
+
private heartbeatTimer;
|
|
13
|
+
private readonly onClientDisconnected?;
|
|
14
|
+
constructor(options?: {
|
|
15
|
+
heartbeatIntervalMs?: number;
|
|
16
|
+
onClientDisconnected?: (clientId: string) => void;
|
|
17
|
+
});
|
|
18
|
+
register(connection: TConnection, initialScopeKeys?: string[]): () => void;
|
|
19
|
+
updateClientScopeKeys(clientId: string, scopeKeys: string[]): void;
|
|
20
|
+
isClientSubscribedToScopeKey(clientId: string, scopeKey: string): boolean;
|
|
21
|
+
getConnectionsForClient(clientId: string): ReadonlySet<TConnection> | undefined;
|
|
22
|
+
getConnectionCount(clientId: string): number;
|
|
23
|
+
getTotalConnections(): number;
|
|
24
|
+
forEachConnectionInScopeKeys(scopeKeys: Iterable<string>, visitor: (connection: TConnection) => void, options?: {
|
|
25
|
+
excludeClientIds?: readonly string[];
|
|
26
|
+
}): void;
|
|
27
|
+
forEachConnection(visitor: (connection: TConnection) => void): void;
|
|
28
|
+
closeClientConnections(clientId: string, code?: number, reason?: string): void;
|
|
29
|
+
closeAll(code?: number, reason?: string): void;
|
|
30
|
+
private ensureHeartbeat;
|
|
31
|
+
private sendHeartbeats;
|
|
32
|
+
private unregister;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=realtime-connection-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-connection-registry.d.ts","sourceRoot":"","sources":["../../src/utils/realtime-connection-registry.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,aAAa,IAAI,IAAI,CAAC;IACtB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7C;AAED,qBAAa,0BAA0B,CACrC,WAAW,SAAS,kBAAkB;IAEtC,OAAO,CAAC,qBAAqB,CAAuC;IACpE,OAAO,CAAC,mBAAmB,CAAkC;IAC7D,OAAO,CAAC,qBAAqB,CAAuC;IAEpE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAA6B;IAEnE,YAAY,OAAO,CAAC,EAAE;QACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,oBAAoB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;KACnD,EAGA;IAED,QAAQ,CACN,UAAU,EAAE,WAAW,EACvB,gBAAgB,GAAE,MAAM,EAAO,GAC9B,MAAM,IAAI,CA8BZ;IAED,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAqCjE;IAED,4BAA4B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIxE;IAED,uBAAuB,CACrB,QAAQ,EAAE,MAAM,GACf,WAAW,CAAC,WAAW,CAAC,GAAG,SAAS,CAEtC;IAED,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE3C;IAED,mBAAmB,IAAI,MAAM,CAM5B;IAED,4BAA4B,CAC1B,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC3B,OAAO,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,IAAI,EAC1C,OAAO,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GACjD,IAAI,CAcN;IAED,iBAAiB,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAOlE;IAED,sBAAsB,CACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CAqBN;IAED,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAW7C;IAED,OAAO,CAAC,eAAe;IAkBvB,OAAO,CAAC,cAAc;IAmBtB,OAAO,CAAC,UAAU;CAsBnB"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
export class RealtimeConnectionRegistry {
|
|
2
|
+
connectionsByClientId = new Map();
|
|
3
|
+
scopeKeysByClientId = new Map();
|
|
4
|
+
connectionsByScopeKey = new Map();
|
|
5
|
+
heartbeatIntervalMs;
|
|
6
|
+
heartbeatTimer = null;
|
|
7
|
+
onClientDisconnected;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 30_000;
|
|
10
|
+
this.onClientDisconnected = options?.onClientDisconnected;
|
|
11
|
+
}
|
|
12
|
+
register(connection, initialScopeKeys = []) {
|
|
13
|
+
const clientId = connection.clientId;
|
|
14
|
+
let clientConns = this.connectionsByClientId.get(clientId);
|
|
15
|
+
if (!clientConns) {
|
|
16
|
+
clientConns = new Set();
|
|
17
|
+
this.connectionsByClientId.set(clientId, clientConns);
|
|
18
|
+
}
|
|
19
|
+
clientConns.add(connection);
|
|
20
|
+
if (!this.scopeKeysByClientId.has(clientId)) {
|
|
21
|
+
this.scopeKeysByClientId.set(clientId, new Set(initialScopeKeys));
|
|
22
|
+
}
|
|
23
|
+
const scopeKeys = this.scopeKeysByClientId.get(clientId);
|
|
24
|
+
if (scopeKeys) {
|
|
25
|
+
for (const key of scopeKeys) {
|
|
26
|
+
let scopedConns = this.connectionsByScopeKey.get(key);
|
|
27
|
+
if (!scopedConns) {
|
|
28
|
+
scopedConns = new Set();
|
|
29
|
+
this.connectionsByScopeKey.set(key, scopedConns);
|
|
30
|
+
}
|
|
31
|
+
scopedConns.add(connection);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
this.ensureHeartbeat();
|
|
35
|
+
return () => {
|
|
36
|
+
this.unregister(connection);
|
|
37
|
+
this.ensureHeartbeat();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
updateClientScopeKeys(clientId, scopeKeys) {
|
|
41
|
+
const conns = this.connectionsByClientId.get(clientId);
|
|
42
|
+
if (!conns || conns.size === 0)
|
|
43
|
+
return;
|
|
44
|
+
const next = new Set(scopeKeys);
|
|
45
|
+
const prev = this.scopeKeysByClientId.get(clientId) ?? new Set();
|
|
46
|
+
if (prev.size === next.size) {
|
|
47
|
+
let unchanged = true;
|
|
48
|
+
for (const key of prev) {
|
|
49
|
+
if (!next.has(key)) {
|
|
50
|
+
unchanged = false;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (unchanged)
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.scopeKeysByClientId.set(clientId, next);
|
|
58
|
+
for (const key of prev) {
|
|
59
|
+
if (next.has(key))
|
|
60
|
+
continue;
|
|
61
|
+
const scopedConns = this.connectionsByScopeKey.get(key);
|
|
62
|
+
if (!scopedConns)
|
|
63
|
+
continue;
|
|
64
|
+
for (const conn of conns)
|
|
65
|
+
scopedConns.delete(conn);
|
|
66
|
+
if (scopedConns.size === 0)
|
|
67
|
+
this.connectionsByScopeKey.delete(key);
|
|
68
|
+
}
|
|
69
|
+
for (const key of next) {
|
|
70
|
+
if (prev.has(key))
|
|
71
|
+
continue;
|
|
72
|
+
let scopedConns = this.connectionsByScopeKey.get(key);
|
|
73
|
+
if (!scopedConns) {
|
|
74
|
+
scopedConns = new Set();
|
|
75
|
+
this.connectionsByScopeKey.set(key, scopedConns);
|
|
76
|
+
}
|
|
77
|
+
for (const conn of conns)
|
|
78
|
+
scopedConns.add(conn);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
isClientSubscribedToScopeKey(clientId, scopeKey) {
|
|
82
|
+
const scopeKeys = this.scopeKeysByClientId.get(clientId);
|
|
83
|
+
if (!scopeKeys || scopeKeys.size === 0)
|
|
84
|
+
return false;
|
|
85
|
+
return scopeKeys.has(scopeKey);
|
|
86
|
+
}
|
|
87
|
+
getConnectionsForClient(clientId) {
|
|
88
|
+
return this.connectionsByClientId.get(clientId);
|
|
89
|
+
}
|
|
90
|
+
getConnectionCount(clientId) {
|
|
91
|
+
return this.connectionsByClientId.get(clientId)?.size ?? 0;
|
|
92
|
+
}
|
|
93
|
+
getTotalConnections() {
|
|
94
|
+
let total = 0;
|
|
95
|
+
for (const conns of this.connectionsByClientId.values()) {
|
|
96
|
+
total += conns.size;
|
|
97
|
+
}
|
|
98
|
+
return total;
|
|
99
|
+
}
|
|
100
|
+
forEachConnectionInScopeKeys(scopeKeys, visitor, options) {
|
|
101
|
+
const targets = new Set();
|
|
102
|
+
for (const key of scopeKeys) {
|
|
103
|
+
const conns = this.connectionsByScopeKey.get(key);
|
|
104
|
+
if (!conns)
|
|
105
|
+
continue;
|
|
106
|
+
for (const conn of conns)
|
|
107
|
+
targets.add(conn);
|
|
108
|
+
}
|
|
109
|
+
const excludedClientIds = new Set(options?.excludeClientIds ?? []);
|
|
110
|
+
for (const conn of targets) {
|
|
111
|
+
if (!conn.isOpen)
|
|
112
|
+
continue;
|
|
113
|
+
if (excludedClientIds.has(conn.clientId))
|
|
114
|
+
continue;
|
|
115
|
+
visitor(conn);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
forEachConnection(visitor) {
|
|
119
|
+
for (const conns of this.connectionsByClientId.values()) {
|
|
120
|
+
for (const conn of conns) {
|
|
121
|
+
if (!conn.isOpen)
|
|
122
|
+
continue;
|
|
123
|
+
visitor(conn);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
closeClientConnections(clientId, code, reason) {
|
|
128
|
+
const conns = this.connectionsByClientId.get(clientId);
|
|
129
|
+
if (!conns)
|
|
130
|
+
return;
|
|
131
|
+
const scopeKeys = this.scopeKeysByClientId.get(clientId);
|
|
132
|
+
if (scopeKeys) {
|
|
133
|
+
for (const key of scopeKeys) {
|
|
134
|
+
const scopedConns = this.connectionsByScopeKey.get(key);
|
|
135
|
+
if (!scopedConns)
|
|
136
|
+
continue;
|
|
137
|
+
for (const conn of conns)
|
|
138
|
+
scopedConns.delete(conn);
|
|
139
|
+
if (scopedConns.size === 0)
|
|
140
|
+
this.connectionsByScopeKey.delete(key);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const conn of conns) {
|
|
144
|
+
conn.close(code, reason);
|
|
145
|
+
}
|
|
146
|
+
this.connectionsByClientId.delete(clientId);
|
|
147
|
+
this.scopeKeysByClientId.delete(clientId);
|
|
148
|
+
this.ensureHeartbeat();
|
|
149
|
+
}
|
|
150
|
+
closeAll(code, reason) {
|
|
151
|
+
for (const conns of this.connectionsByClientId.values()) {
|
|
152
|
+
for (const conn of conns) {
|
|
153
|
+
conn.close(code, reason);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
this.connectionsByClientId.clear();
|
|
157
|
+
this.scopeKeysByClientId.clear();
|
|
158
|
+
this.connectionsByScopeKey.clear();
|
|
159
|
+
this.ensureHeartbeat();
|
|
160
|
+
}
|
|
161
|
+
ensureHeartbeat() {
|
|
162
|
+
if (this.heartbeatIntervalMs <= 0)
|
|
163
|
+
return;
|
|
164
|
+
const total = this.getTotalConnections();
|
|
165
|
+
if (total === 0) {
|
|
166
|
+
if (this.heartbeatTimer) {
|
|
167
|
+
clearInterval(this.heartbeatTimer);
|
|
168
|
+
this.heartbeatTimer = null;
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (this.heartbeatTimer)
|
|
173
|
+
return;
|
|
174
|
+
this.heartbeatTimer = setInterval(() => {
|
|
175
|
+
this.sendHeartbeats();
|
|
176
|
+
}, this.heartbeatIntervalMs);
|
|
177
|
+
}
|
|
178
|
+
sendHeartbeats() {
|
|
179
|
+
const closedConnections = [];
|
|
180
|
+
for (const conns of this.connectionsByClientId.values()) {
|
|
181
|
+
for (const conn of conns) {
|
|
182
|
+
if (!conn.isOpen) {
|
|
183
|
+
closedConnections.push(conn);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
conn.sendHeartbeat();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const conn of closedConnections) {
|
|
190
|
+
this.unregister(conn);
|
|
191
|
+
}
|
|
192
|
+
this.ensureHeartbeat();
|
|
193
|
+
}
|
|
194
|
+
unregister(connection) {
|
|
195
|
+
const clientId = connection.clientId;
|
|
196
|
+
const scopeKeys = this.scopeKeysByClientId.get(clientId);
|
|
197
|
+
if (scopeKeys) {
|
|
198
|
+
for (const key of scopeKeys) {
|
|
199
|
+
const scopedConns = this.connectionsByScopeKey.get(key);
|
|
200
|
+
if (!scopedConns)
|
|
201
|
+
continue;
|
|
202
|
+
scopedConns.delete(connection);
|
|
203
|
+
if (scopedConns.size === 0)
|
|
204
|
+
this.connectionsByScopeKey.delete(key);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const conns = this.connectionsByClientId.get(clientId);
|
|
208
|
+
if (!conns)
|
|
209
|
+
return;
|
|
210
|
+
conns.delete(connection);
|
|
211
|
+
if (conns.size > 0)
|
|
212
|
+
return;
|
|
213
|
+
this.connectionsByClientId.delete(clientId);
|
|
214
|
+
this.scopeKeysByClientId.delete(clientId);
|
|
215
|
+
this.onClientDisconnected?.(clientId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=realtime-connection-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-connection-registry.js","sourceRoot":"","sources":["../../src/utils/realtime-connection-registry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
export interface RealtimeConnection {
|
|
2
|
+
readonly clientId: string;
|
|
3
|
+
readonly isOpen: boolean;
|
|
4
|
+
sendHeartbeat(): void;
|
|
5
|
+
close(code?: number, reason?: string): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class RealtimeConnectionRegistry<
|
|
9
|
+
TConnection extends RealtimeConnection,
|
|
10
|
+
> {
|
|
11
|
+
private connectionsByClientId = new Map<string, Set<TConnection>>();
|
|
12
|
+
private scopeKeysByClientId = new Map<string, Set<string>>();
|
|
13
|
+
private connectionsByScopeKey = new Map<string, Set<TConnection>>();
|
|
14
|
+
|
|
15
|
+
private readonly heartbeatIntervalMs: number;
|
|
16
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
17
|
+
private readonly onClientDisconnected?: (clientId: string) => void;
|
|
18
|
+
|
|
19
|
+
constructor(options?: {
|
|
20
|
+
heartbeatIntervalMs?: number;
|
|
21
|
+
onClientDisconnected?: (clientId: string) => void;
|
|
22
|
+
}) {
|
|
23
|
+
this.heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 30_000;
|
|
24
|
+
this.onClientDisconnected = options?.onClientDisconnected;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
register(
|
|
28
|
+
connection: TConnection,
|
|
29
|
+
initialScopeKeys: string[] = []
|
|
30
|
+
): () => void {
|
|
31
|
+
const clientId = connection.clientId;
|
|
32
|
+
let clientConns = this.connectionsByClientId.get(clientId);
|
|
33
|
+
if (!clientConns) {
|
|
34
|
+
clientConns = new Set();
|
|
35
|
+
this.connectionsByClientId.set(clientId, clientConns);
|
|
36
|
+
}
|
|
37
|
+
clientConns.add(connection);
|
|
38
|
+
|
|
39
|
+
if (!this.scopeKeysByClientId.has(clientId)) {
|
|
40
|
+
this.scopeKeysByClientId.set(clientId, new Set(initialScopeKeys));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const scopeKeys = this.scopeKeysByClientId.get(clientId);
|
|
44
|
+
if (scopeKeys) {
|
|
45
|
+
for (const key of scopeKeys) {
|
|
46
|
+
let scopedConns = this.connectionsByScopeKey.get(key);
|
|
47
|
+
if (!scopedConns) {
|
|
48
|
+
scopedConns = new Set();
|
|
49
|
+
this.connectionsByScopeKey.set(key, scopedConns);
|
|
50
|
+
}
|
|
51
|
+
scopedConns.add(connection);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.ensureHeartbeat();
|
|
56
|
+
return () => {
|
|
57
|
+
this.unregister(connection);
|
|
58
|
+
this.ensureHeartbeat();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
updateClientScopeKeys(clientId: string, scopeKeys: string[]): void {
|
|
63
|
+
const conns = this.connectionsByClientId.get(clientId);
|
|
64
|
+
if (!conns || conns.size === 0) return;
|
|
65
|
+
|
|
66
|
+
const next = new Set(scopeKeys);
|
|
67
|
+
const prev = this.scopeKeysByClientId.get(clientId) ?? new Set<string>();
|
|
68
|
+
|
|
69
|
+
if (prev.size === next.size) {
|
|
70
|
+
let unchanged = true;
|
|
71
|
+
for (const key of prev) {
|
|
72
|
+
if (!next.has(key)) {
|
|
73
|
+
unchanged = false;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (unchanged) return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.scopeKeysByClientId.set(clientId, next);
|
|
81
|
+
|
|
82
|
+
for (const key of prev) {
|
|
83
|
+
if (next.has(key)) continue;
|
|
84
|
+
const scopedConns = this.connectionsByScopeKey.get(key);
|
|
85
|
+
if (!scopedConns) continue;
|
|
86
|
+
for (const conn of conns) scopedConns.delete(conn);
|
|
87
|
+
if (scopedConns.size === 0) this.connectionsByScopeKey.delete(key);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const key of next) {
|
|
91
|
+
if (prev.has(key)) continue;
|
|
92
|
+
let scopedConns = this.connectionsByScopeKey.get(key);
|
|
93
|
+
if (!scopedConns) {
|
|
94
|
+
scopedConns = new Set();
|
|
95
|
+
this.connectionsByScopeKey.set(key, scopedConns);
|
|
96
|
+
}
|
|
97
|
+
for (const conn of conns) scopedConns.add(conn);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
isClientSubscribedToScopeKey(clientId: string, scopeKey: string): boolean {
|
|
102
|
+
const scopeKeys = this.scopeKeysByClientId.get(clientId);
|
|
103
|
+
if (!scopeKeys || scopeKeys.size === 0) return false;
|
|
104
|
+
return scopeKeys.has(scopeKey);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getConnectionsForClient(
|
|
108
|
+
clientId: string
|
|
109
|
+
): ReadonlySet<TConnection> | undefined {
|
|
110
|
+
return this.connectionsByClientId.get(clientId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getConnectionCount(clientId: string): number {
|
|
114
|
+
return this.connectionsByClientId.get(clientId)?.size ?? 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getTotalConnections(): number {
|
|
118
|
+
let total = 0;
|
|
119
|
+
for (const conns of this.connectionsByClientId.values()) {
|
|
120
|
+
total += conns.size;
|
|
121
|
+
}
|
|
122
|
+
return total;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
forEachConnectionInScopeKeys(
|
|
126
|
+
scopeKeys: Iterable<string>,
|
|
127
|
+
visitor: (connection: TConnection) => void,
|
|
128
|
+
options?: { excludeClientIds?: readonly string[] }
|
|
129
|
+
): void {
|
|
130
|
+
const targets = new Set<TConnection>();
|
|
131
|
+
for (const key of scopeKeys) {
|
|
132
|
+
const conns = this.connectionsByScopeKey.get(key);
|
|
133
|
+
if (!conns) continue;
|
|
134
|
+
for (const conn of conns) targets.add(conn);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const excludedClientIds = new Set(options?.excludeClientIds ?? []);
|
|
138
|
+
for (const conn of targets) {
|
|
139
|
+
if (!conn.isOpen) continue;
|
|
140
|
+
if (excludedClientIds.has(conn.clientId)) continue;
|
|
141
|
+
visitor(conn);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
forEachConnection(visitor: (connection: TConnection) => void): void {
|
|
146
|
+
for (const conns of this.connectionsByClientId.values()) {
|
|
147
|
+
for (const conn of conns) {
|
|
148
|
+
if (!conn.isOpen) continue;
|
|
149
|
+
visitor(conn);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
closeClientConnections(
|
|
155
|
+
clientId: string,
|
|
156
|
+
code?: number,
|
|
157
|
+
reason?: string
|
|
158
|
+
): void {
|
|
159
|
+
const conns = this.connectionsByClientId.get(clientId);
|
|
160
|
+
if (!conns) return;
|
|
161
|
+
|
|
162
|
+
const scopeKeys = this.scopeKeysByClientId.get(clientId);
|
|
163
|
+
if (scopeKeys) {
|
|
164
|
+
for (const key of scopeKeys) {
|
|
165
|
+
const scopedConns = this.connectionsByScopeKey.get(key);
|
|
166
|
+
if (!scopedConns) continue;
|
|
167
|
+
for (const conn of conns) scopedConns.delete(conn);
|
|
168
|
+
if (scopedConns.size === 0) this.connectionsByScopeKey.delete(key);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const conn of conns) {
|
|
173
|
+
conn.close(code, reason);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.connectionsByClientId.delete(clientId);
|
|
177
|
+
this.scopeKeysByClientId.delete(clientId);
|
|
178
|
+
this.ensureHeartbeat();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
closeAll(code?: number, reason?: string): void {
|
|
182
|
+
for (const conns of this.connectionsByClientId.values()) {
|
|
183
|
+
for (const conn of conns) {
|
|
184
|
+
conn.close(code, reason);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.connectionsByClientId.clear();
|
|
189
|
+
this.scopeKeysByClientId.clear();
|
|
190
|
+
this.connectionsByScopeKey.clear();
|
|
191
|
+
this.ensureHeartbeat();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private ensureHeartbeat(): void {
|
|
195
|
+
if (this.heartbeatIntervalMs <= 0) return;
|
|
196
|
+
|
|
197
|
+
const total = this.getTotalConnections();
|
|
198
|
+
if (total === 0) {
|
|
199
|
+
if (this.heartbeatTimer) {
|
|
200
|
+
clearInterval(this.heartbeatTimer);
|
|
201
|
+
this.heartbeatTimer = null;
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (this.heartbeatTimer) return;
|
|
207
|
+
this.heartbeatTimer = setInterval(() => {
|
|
208
|
+
this.sendHeartbeats();
|
|
209
|
+
}, this.heartbeatIntervalMs);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private sendHeartbeats(): void {
|
|
213
|
+
const closedConnections: TConnection[] = [];
|
|
214
|
+
for (const conns of this.connectionsByClientId.values()) {
|
|
215
|
+
for (const conn of conns) {
|
|
216
|
+
if (!conn.isOpen) {
|
|
217
|
+
closedConnections.push(conn);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
conn.sendHeartbeat();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const conn of closedConnections) {
|
|
225
|
+
this.unregister(conn);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.ensureHeartbeat();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private unregister(connection: TConnection): void {
|
|
232
|
+
const clientId = connection.clientId;
|
|
233
|
+
|
|
234
|
+
const scopeKeys = this.scopeKeysByClientId.get(clientId);
|
|
235
|
+
if (scopeKeys) {
|
|
236
|
+
for (const key of scopeKeys) {
|
|
237
|
+
const scopedConns = this.connectionsByScopeKey.get(key);
|
|
238
|
+
if (!scopedConns) continue;
|
|
239
|
+
scopedConns.delete(connection);
|
|
240
|
+
if (scopedConns.size === 0) this.connectionsByScopeKey.delete(key);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const conns = this.connectionsByClientId.get(clientId);
|
|
245
|
+
if (!conns) return;
|
|
246
|
+
conns.delete(connection);
|
|
247
|
+
if (conns.size > 0) return;
|
|
248
|
+
|
|
249
|
+
this.connectionsByClientId.delete(clientId);
|
|
250
|
+
this.scopeKeysByClientId.delete(clientId);
|
|
251
|
+
this.onClientDisconnected?.(clientId);
|
|
252
|
+
}
|
|
253
|
+
}
|