sonamu 0.11.16 → 0.12.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/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +12 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/stream/__tests__/ws-test-helpers.d.ts +108 -0
- package/dist/stream/__tests__/ws-test-helpers.d.ts.map +1 -0
- package/dist/stream/__tests__/ws-test-helpers.js +260 -0
- package/dist/stream/index.d.ts +1 -0
- package/dist/stream/index.d.ts.map +1 -1
- package/dist/stream/index.js +4 -2
- package/dist/stream/ws-audience-resolver.d.ts +8 -5
- package/dist/stream/ws-audience-resolver.d.ts.map +1 -1
- package/dist/stream/ws-audience-resolver.js +16 -13
- package/dist/stream/ws-cluster-bus.d.ts +9 -4
- package/dist/stream/ws-cluster-bus.d.ts.map +1 -1
- package/dist/stream/ws-cluster-bus.js +1 -1
- package/dist/stream/ws-core.d.ts +10 -0
- package/dist/stream/ws-core.d.ts.map +1 -1
- package/dist/stream/ws-delivery.d.ts +30 -4
- package/dist/stream/ws-delivery.d.ts.map +1 -1
- package/dist/stream/ws-delivery.js +199 -52
- package/dist/stream/ws-frame.d.ts +15 -0
- package/dist/stream/ws-frame.d.ts.map +1 -0
- package/dist/stream/ws-frame.js +29 -0
- package/dist/stream/ws-local-connection-store.d.ts +4 -4
- package/dist/stream/ws-local-connection-store.d.ts.map +1 -1
- package/dist/stream/ws-local-connection-store.js +2 -2
- package/dist/stream/ws-presence-index.d.ts +36 -0
- package/dist/stream/ws-presence-index.d.ts.map +1 -0
- package/dist/stream/ws-presence-index.js +177 -0
- package/dist/stream/ws-presence-store.d.ts +39 -39
- package/dist/stream/ws-presence-store.d.ts.map +1 -1
- package/dist/stream/ws-presence-store.js +22 -152
- package/dist/stream/ws-registry.d.ts +45 -17
- package/dist/stream/ws-registry.d.ts.map +1 -1
- package/dist/stream/ws-registry.js +169 -60
- package/dist/stream/ws-session-id.d.ts +14 -0
- package/dist/stream/ws-session-id.d.ts.map +1 -0
- package/dist/stream/ws-session-id.js +37 -0
- package/dist/stream/ws.d.ts +20 -10
- package/dist/stream/ws.d.ts.map +1 -1
- package/dist/stream/ws.js +59 -36
- package/package.json +1 -1
- package/src/api/__tests__/sonamu.websocket-routes.test.ts +234 -0
- package/src/api/__tests__/sonamu.websocket.test.ts +241 -20
- package/src/api/sonamu.ts +20 -7
- package/src/index.ts +1 -0
- package/src/stream/__tests__/ws-contracts.test.ts +483 -265
- package/src/stream/__tests__/ws-multinode.test.ts +326 -0
- package/src/stream/__tests__/ws-registry-async.test.ts +855 -0
- package/src/stream/__tests__/ws-test-helpers.ts +360 -0
- package/src/stream/__tests__/ws.test.ts +342 -145
- package/src/stream/index.ts +1 -0
- package/src/stream/ws-audience-resolver.ts +21 -19
- package/src/stream/ws-cluster-bus.ts +12 -4
- package/src/stream/ws-core.ts +14 -0
- package/src/stream/ws-delivery.ts +321 -97
- package/src/stream/ws-frame.ts +39 -0
- package/src/stream/ws-local-connection-store.ts +10 -9
- package/src/stream/ws-presence-index.ts +273 -0
- package/src/stream/ws-presence-store.ts +64 -284
- package/src/stream/ws-registry.ts +272 -85
- package/src/stream/ws-session-id.ts +39 -0
- package/src/stream/ws.ts +91 -50
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type WebSocketAudience } from "./ws-audience";
|
|
2
|
-
import { type
|
|
2
|
+
import { type MaybePromise, type WebSocketRoomId, type WebSocketUserId } from "./ws-core";
|
|
3
|
+
import { WebSocketPresenceIndex } from "./ws-presence-index";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* 세션 presence snapshot. live 객체가 아니므로 값을 바꿔도 registry나 store 상태에 영향을 주지 않는다.
|
|
7
|
+
*/
|
|
4
8
|
export type WebSocketSessionPresence = {
|
|
5
9
|
sessionId: string;
|
|
6
10
|
nodeId: string;
|
|
@@ -12,320 +16,96 @@ export type WebSocketSessionPresence = {
|
|
|
12
16
|
userId?: WebSocketUserId;
|
|
13
17
|
};
|
|
14
18
|
|
|
19
|
+
export type WebSocketPresenceRegistration = {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
nodeId: string;
|
|
22
|
+
namespace: string;
|
|
23
|
+
active: boolean;
|
|
24
|
+
connectedAt: Date;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type WebSocketRoutingPlan = {
|
|
28
|
+
localSessionIds: string[];
|
|
29
|
+
remoteNodeIds: string[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 클러스터 presence replica 계약. 각 노드의 registry가 자기 세션의 mutation을 세션별 호출 순서대로 전달한다.
|
|
34
|
+
* 모든 메서드는 값이나 promise를 반환할 수 있고, registry는 동기 throw도 rejection으로 바꿔 처리한다.
|
|
35
|
+
* userId는 타입까지 일치해야 매칭된다(1과 "1"은 다르다).
|
|
36
|
+
*/
|
|
15
37
|
export interface WebSocketPresenceStore {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
getConnectionCount(namespace?: string): number;
|
|
31
|
-
getRoomMembers(roomId: WebSocketRoomId, namespace?: string): WebSocketSessionPresence[];
|
|
32
|
-
getStats(): WebSocketRegistryStats;
|
|
33
|
-
queryAudience(audience: WebSocketAudience): WebSocketSessionPresence[];
|
|
38
|
+
// 설정하면 runtime nodeId와 같아야 한다. 다르면 runtime 생성이 실패한다
|
|
39
|
+
readonly nodeId?: string;
|
|
40
|
+
register(input: WebSocketPresenceRegistration): MaybePromise<void>;
|
|
41
|
+
activate(sessionId: string): MaybePromise<void>;
|
|
42
|
+
unregister(sessionId: string): MaybePromise<void>;
|
|
43
|
+
setUserId(sessionId: string, userId: WebSocketUserId): MaybePromise<void>;
|
|
44
|
+
clearUserId(sessionId: string): MaybePromise<void>;
|
|
45
|
+
join(sessionId: string, roomId: WebSocketRoomId): MaybePromise<void>;
|
|
46
|
+
leave(sessionId: string, roomId: WebSocketRoomId): MaybePromise<void>;
|
|
47
|
+
// nodeId 관점에서 audience를 로컬 세션 ID와 중복 없는 원격 노드 ID로 나눈다. 비활성 세션은 제외한다
|
|
48
|
+
resolveRoute(audience: WebSocketAudience, nodeId: string): MaybePromise<WebSocketRoutingPlan>;
|
|
49
|
+
// 클러스터 전체의 활성 방 인원수
|
|
50
|
+
getRoomMemberCount(roomId: WebSocketRoomId, namespace?: string): MaybePromise<number>;
|
|
51
|
+
shutdown?(): MaybePromise<void>;
|
|
34
52
|
}
|
|
35
53
|
|
|
54
|
+
/**
|
|
55
|
+
* 프로세스 메모리에 presence를 두는 store. 단일 노드의 기본값이며, 같은 프로세스의 여러 runtime이 공유하면 클러스터 replica로도 동작한다.
|
|
56
|
+
*/
|
|
36
57
|
export class InMemoryWebSocketPresenceStore implements WebSocketPresenceStore {
|
|
37
|
-
private readonly
|
|
38
|
-
private readonly rooms = new Map<string, Set<string>>();
|
|
39
|
-
private readonly users = new Map<string, Set<string>>();
|
|
58
|
+
private readonly index = new WebSocketPresenceIndex();
|
|
40
59
|
|
|
41
|
-
register(input: {
|
|
42
|
-
|
|
43
|
-
nodeId: string;
|
|
44
|
-
namespace: string;
|
|
45
|
-
active?: boolean;
|
|
46
|
-
}): WebSocketSessionPresence {
|
|
47
|
-
const meta: WebSocketSessionPresence = {
|
|
48
|
-
sessionId: input.sessionId,
|
|
49
|
-
nodeId: input.nodeId,
|
|
50
|
-
namespace: input.namespace,
|
|
51
|
-
active: input.active ?? true,
|
|
52
|
-
rooms: new Set(),
|
|
53
|
-
connectedAt: new Date(),
|
|
54
|
-
lastSeenAt: new Date(),
|
|
55
|
-
};
|
|
56
|
-
this.connections.set(input.sessionId, meta);
|
|
57
|
-
return meta;
|
|
60
|
+
register(input: WebSocketPresenceRegistration): void {
|
|
61
|
+
this.index.register(input);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
activate(sessionId: string): void {
|
|
61
|
-
|
|
62
|
-
if (!meta) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
meta.active = true;
|
|
65
|
+
this.index.activate(sessionId);
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
unregister(sessionId: string):
|
|
70
|
-
|
|
71
|
-
if (!meta) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (meta.userId !== undefined) {
|
|
76
|
-
this.removeUserBinding(meta.namespace, meta.userId, sessionId);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
for (const roomId of meta.rooms) {
|
|
80
|
-
this.removeRoomBinding(meta.namespace, roomId, sessionId);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.connections.delete(sessionId);
|
|
84
|
-
return meta;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
touch(sessionId: string): void {
|
|
88
|
-
const meta = this.connections.get(sessionId);
|
|
89
|
-
if (!meta) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
meta.lastSeenAt = new Date();
|
|
68
|
+
unregister(sessionId: string): void {
|
|
69
|
+
this.index.unregister(sessionId);
|
|
94
70
|
}
|
|
95
71
|
|
|
96
72
|
setUserId(sessionId: string, userId: WebSocketUserId): void {
|
|
97
|
-
|
|
98
|
-
if (!meta) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (meta.userId !== undefined) {
|
|
103
|
-
this.removeUserBinding(meta.namespace, meta.userId, sessionId);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
meta.userId = userId;
|
|
107
|
-
const key = getBindingKey(meta.namespace, String(userId));
|
|
108
|
-
const bound = this.users.get(key) ?? new Set<string>();
|
|
109
|
-
bound.add(sessionId);
|
|
110
|
-
this.users.set(key, bound);
|
|
73
|
+
this.index.setUserId(sessionId, userId);
|
|
111
74
|
}
|
|
112
75
|
|
|
113
76
|
clearUserId(sessionId: string): void {
|
|
114
|
-
|
|
115
|
-
if (!meta || meta.userId === undefined) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
this.removeUserBinding(meta.namespace, meta.userId, sessionId);
|
|
120
|
-
delete meta.userId;
|
|
77
|
+
this.index.clearUserId(sessionId);
|
|
121
78
|
}
|
|
122
79
|
|
|
123
80
|
join(sessionId: string, roomId: WebSocketRoomId): void {
|
|
124
|
-
|
|
125
|
-
if (!meta) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
meta.rooms.add(roomId);
|
|
130
|
-
const key = getBindingKey(meta.namespace, roomId);
|
|
131
|
-
const members = this.rooms.get(key) ?? new Set<string>();
|
|
132
|
-
members.add(sessionId);
|
|
133
|
-
this.rooms.set(key, members);
|
|
81
|
+
this.index.join(sessionId, roomId);
|
|
134
82
|
}
|
|
135
83
|
|
|
136
84
|
leave(sessionId: string, roomId: WebSocketRoomId): void {
|
|
137
|
-
|
|
138
|
-
if (!meta) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
meta.rooms.delete(roomId);
|
|
143
|
-
this.removeRoomBinding(meta.namespace, roomId, sessionId);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
getConnection(sessionId: string): WebSocketSessionPresence | undefined {
|
|
147
|
-
return this.connections.get(sessionId);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
getConnectionCount(namespace?: string): number {
|
|
151
|
-
let count = 0;
|
|
152
|
-
|
|
153
|
-
for (const meta of this.connections.values()) {
|
|
154
|
-
if (!meta.active || (namespace && meta.namespace !== namespace)) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
count += 1;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return count;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
getRoomMembers(roomId: WebSocketRoomId, namespace?: string): WebSocketSessionPresence[] {
|
|
165
|
-
return this.getBoundConnections(this.rooms, roomId, namespace);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
getStats(): WebSocketRegistryStats {
|
|
169
|
-
const byNamespace: Record<string, number> = {};
|
|
170
|
-
|
|
171
|
-
for (const meta of this.connections.values()) {
|
|
172
|
-
if (!meta.active) {
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
byNamespace[meta.namespace] = (byNamespace[meta.namespace] ?? 0) + 1;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
totalConnections: Object.values(byNamespace).reduce((sum, count) => sum + count, 0),
|
|
180
|
-
totalRooms: this.rooms.size,
|
|
181
|
-
byNamespace,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
queryAudience(audience: WebSocketAudience): WebSocketSessionPresence[] {
|
|
186
|
-
switch (audience.type) {
|
|
187
|
-
case "all":
|
|
188
|
-
return this.getActiveConnections(audience.namespace);
|
|
189
|
-
case "room":
|
|
190
|
-
return this.getBoundConnections(this.rooms, audience.roomId, audience.namespace);
|
|
191
|
-
case "user":
|
|
192
|
-
return this.getBoundConnections(this.users, String(audience.userId), audience.namespace);
|
|
193
|
-
case "connections": {
|
|
194
|
-
const metas: WebSocketSessionPresence[] = [];
|
|
195
|
-
|
|
196
|
-
for (const connectionId of audience.connectionIds) {
|
|
197
|
-
const meta = this.connections.get(connectionId);
|
|
198
|
-
if (!meta?.active) {
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (audience.namespace && meta.namespace !== audience.namespace) {
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
metas.push(meta);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return metas;
|
|
208
|
-
}
|
|
209
|
-
case "union": {
|
|
210
|
-
const seen = new Set<string>();
|
|
211
|
-
const metas: WebSocketSessionPresence[] = [];
|
|
212
|
-
|
|
213
|
-
for (const item of audience.audiences) {
|
|
214
|
-
for (const meta of this.queryAudience(item)) {
|
|
215
|
-
if (seen.has(meta.sessionId)) {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
seen.add(meta.sessionId);
|
|
220
|
-
metas.push(meta);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return metas;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private getActiveConnections(namespace?: string): WebSocketSessionPresence[] {
|
|
230
|
-
const metas: WebSocketSessionPresence[] = [];
|
|
231
|
-
|
|
232
|
-
for (const meta of this.connections.values()) {
|
|
233
|
-
if (!meta.active || (namespace && meta.namespace !== namespace)) {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
metas.push(meta);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return metas;
|
|
85
|
+
this.index.leave(sessionId, roomId);
|
|
241
86
|
}
|
|
242
87
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
namespace?: string,
|
|
247
|
-
): WebSocketSessionPresence[] {
|
|
248
|
-
const metas: WebSocketSessionPresence[] = [];
|
|
249
|
-
const seen = new Set<string>();
|
|
88
|
+
resolveRoute(audience: WebSocketAudience, nodeId: string): WebSocketRoutingPlan {
|
|
89
|
+
const localSessionIds: string[] = [];
|
|
90
|
+
const remoteNodeIds = new Set<string>();
|
|
250
91
|
|
|
251
|
-
for (const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
seen.add(connectionId);
|
|
257
|
-
|
|
258
|
-
const meta = this.connections.get(connectionId);
|
|
259
|
-
if (meta?.active) {
|
|
260
|
-
metas.push(meta);
|
|
261
|
-
}
|
|
92
|
+
for (const session of this.index.queryAudience(audience)) {
|
|
93
|
+
if (session.nodeId === nodeId) {
|
|
94
|
+
localSessionIds.push(session.sessionId);
|
|
95
|
+
} else {
|
|
96
|
+
remoteNodeIds.add(session.nodeId);
|
|
262
97
|
}
|
|
263
98
|
}
|
|
264
99
|
|
|
265
|
-
return
|
|
100
|
+
return { localSessionIds, remoteNodeIds: [...remoteNodeIds] };
|
|
266
101
|
}
|
|
267
102
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
id: string,
|
|
271
|
-
namespace?: string,
|
|
272
|
-
): Set<string>[] {
|
|
273
|
-
if (namespace) {
|
|
274
|
-
const exact = bindings.get(getBindingKey(namespace, id));
|
|
275
|
-
return exact ? [exact] : [];
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const matches: Set<string>[] = [];
|
|
279
|
-
for (const [key, members] of bindings) {
|
|
280
|
-
const parsed = parseBindingKey(key);
|
|
281
|
-
if (parsed.id === id) {
|
|
282
|
-
matches.push(members);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return matches;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private removeRoomBinding(namespace: string, roomId: WebSocketRoomId, sessionId: string): void {
|
|
290
|
-
const key = getBindingKey(namespace, roomId);
|
|
291
|
-
const members = this.rooms.get(key);
|
|
292
|
-
if (!members) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
members.delete(sessionId);
|
|
297
|
-
if (members.size === 0) {
|
|
298
|
-
this.rooms.delete(key);
|
|
299
|
-
}
|
|
103
|
+
getRoomMemberCount(roomId: WebSocketRoomId, namespace?: string): number {
|
|
104
|
+
return this.index.getRoomMemberCount(roomId, namespace);
|
|
300
105
|
}
|
|
301
106
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (!members) {
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
members.delete(sessionId);
|
|
310
|
-
if (members.size === 0) {
|
|
311
|
-
this.users.delete(key);
|
|
312
|
-
}
|
|
107
|
+
// 복사본을 반환하므로 호출자가 값을 바꿔도 store 상태는 그대로다
|
|
108
|
+
getConnection(sessionId: string): WebSocketSessionPresence | undefined {
|
|
109
|
+
return this.index.getConnection(sessionId);
|
|
313
110
|
}
|
|
314
111
|
}
|
|
315
|
-
|
|
316
|
-
function getBindingKey(namespace: string, id: string): string {
|
|
317
|
-
return `${namespace}::${id}`;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
interface PresenceBinding {
|
|
321
|
-
namespace: string;
|
|
322
|
-
id: string;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function parseBindingKey(key: string): PresenceBinding {
|
|
326
|
-
const [namespace, ...rest] = key.split("::");
|
|
327
|
-
return {
|
|
328
|
-
namespace,
|
|
329
|
-
id: rest.join("::"),
|
|
330
|
-
};
|
|
331
|
-
}
|