@rlanz/socket 0.0.1-4 → 0.0.1-5
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 +427 -22
- package/build/chunk-ANT2E3LT.js +136 -0
- package/build/chunk-ANT2E3LT.js.map +1 -0
- package/build/chunk-B4Y3TNDI.js +159 -0
- package/build/chunk-B4Y3TNDI.js.map +1 -0
- package/build/{chunk-GKAD2UOA.js → chunk-D3HUBCBW.js} +1 -1
- package/build/chunk-D3HUBCBW.js.map +1 -0
- package/build/chunk-SFAY2ZA4.js +28 -0
- package/build/chunk-SFAY2ZA4.js.map +1 -0
- package/build/{chunk-XUDFDJME.js → chunk-SHH6U4CI.js} +12 -9
- package/build/chunk-SHH6U4CI.js.map +1 -0
- package/build/chunk-YAX5EHHB.js +453 -0
- package/build/chunk-YAX5EHHB.js.map +1 -0
- package/build/framework-CvxgHwbv.d.ts +12 -0
- package/build/index.d.ts +7 -13
- package/build/index.js +18 -10
- package/build/index.js.map +1 -1
- package/build/providers/socket_provider.d.ts +4 -3
- package/build/providers/socket_provider.js +1774 -57
- package/build/providers/socket_provider.js.map +1 -1
- package/build/services/socket.d.ts +4 -3
- package/build/socket_service-mybqv1hj.d.ts +105 -0
- package/build/src/assembler_hook.d.ts +13 -0
- package/build/src/assembler_hook.js +249 -0
- package/build/src/assembler_hook.js.map +1 -0
- package/build/src/client/index.d.ts +46 -12
- package/build/src/client/index.js +223 -76
- package/build/src/client/index.js.map +1 -1
- package/build/src/client/react.d.ts +27 -0
- package/build/src/client/react.js +69 -0
- package/build/src/client/react.js.map +1 -0
- package/build/src/client/svelte.d.ts +22 -0
- package/build/src/client/svelte.js +82 -0
- package/build/src/client/svelte.js.map +1 -0
- package/build/src/client/types.d.ts +68 -5
- package/build/src/client/vue.d.ts +25 -0
- package/build/src/client/vue.js +81 -0
- package/build/src/client/vue.js.map +1 -0
- package/build/src/decorators.d.ts +4 -4
- package/build/src/decorators.js +1 -1
- package/build/src/health_check.d.ts +4 -3
- package/build/src/otel.js +4 -5
- package/build/src/otel.js.map +1 -1
- package/build/src/testing.d.ts +54 -0
- package/build/src/testing.js +7 -0
- package/build/src/testing.js.map +1 -0
- package/build/src/types.d.ts +2 -2
- package/build/{types-BBLNfcWk.d.ts → types-C7Q36ryo.d.ts} +110 -65
- package/package.json +41 -6
- package/build/chunk-GKAD2UOA.js.map +0 -1
- package/build/chunk-ILDK672E.js +0 -1921
- package/build/chunk-ILDK672E.js.map +0 -1
- package/build/chunk-XUDFDJME.js.map +0 -1
- package/build/shared_types-Dw9AphfO.d.ts +0 -21
- package/build/socket_service-CRmPa74K.d.ts +0 -144
package/build/chunk-ILDK672E.js
DELETED
|
@@ -1,1921 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getDecoratedChannelHandlers
|
|
3
|
-
} from "./chunk-XUDFDJME.js";
|
|
4
|
-
import {
|
|
5
|
-
broadcastChannel,
|
|
6
|
-
channelMessageChannel,
|
|
7
|
-
connectChannel,
|
|
8
|
-
disconnectChannel,
|
|
9
|
-
subscribeChannel,
|
|
10
|
-
unsubscribeChannel
|
|
11
|
-
} from "./chunk-GKAD2UOA.js";
|
|
12
|
-
|
|
13
|
-
// src/presence_manager.ts
|
|
14
|
-
var PRESENCE_DATA_KEY = "__rlanzSocketPresence";
|
|
15
|
-
var PresenceManager = class {
|
|
16
|
-
#presence = /* @__PURE__ */ new Map();
|
|
17
|
-
#fetchSockets = null;
|
|
18
|
-
/**
|
|
19
|
-
* Sets the socket fetcher used to build distributed snapshots.
|
|
20
|
-
*/
|
|
21
|
-
setSocketFetcher(fetchSockets) {
|
|
22
|
-
this.#fetchSockets = fetchSockets;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Adds a user to a channel.
|
|
26
|
-
*/
|
|
27
|
-
join(channel, socket, member) {
|
|
28
|
-
if (!this.#presence.has(channel)) {
|
|
29
|
-
this.#presence.set(channel, /* @__PURE__ */ new Map());
|
|
30
|
-
}
|
|
31
|
-
const presenceUser = typeof member === "string" ? { id: socket.id, name: member } : {
|
|
32
|
-
...member,
|
|
33
|
-
id: member.id || socket.id
|
|
34
|
-
};
|
|
35
|
-
const user = {
|
|
36
|
-
...presenceUser,
|
|
37
|
-
joinedAt: /* @__PURE__ */ new Date()
|
|
38
|
-
};
|
|
39
|
-
this.#presence.get(channel).set(socket.id, user);
|
|
40
|
-
this.#setSocketPresence(socket, channel, user);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Removes a user from a channel.
|
|
44
|
-
*/
|
|
45
|
-
leave(channel, socket) {
|
|
46
|
-
const channelPresence = this.#presence.get(channel);
|
|
47
|
-
if (channelPresence) {
|
|
48
|
-
channelPresence.delete(socket.id);
|
|
49
|
-
if (channelPresence.size === 0) {
|
|
50
|
-
this.#presence.delete(channel);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
this.#deleteSocketPresence(socket, channel);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Removes a user from every channel.
|
|
57
|
-
* Returns the list of channels the user left.
|
|
58
|
-
*/
|
|
59
|
-
leaveAll(socket) {
|
|
60
|
-
const leftChannels = [];
|
|
61
|
-
for (const [channel, users] of this.#presence) {
|
|
62
|
-
if (users.has(socket.id)) {
|
|
63
|
-
users.delete(socket.id);
|
|
64
|
-
leftChannels.push(channel);
|
|
65
|
-
if (users.size === 0) {
|
|
66
|
-
this.#presence.delete(channel);
|
|
67
|
-
}
|
|
68
|
-
this.#deleteSocketPresence(socket, channel);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return leftChannels;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Gets presence data for a channel.
|
|
75
|
-
*/
|
|
76
|
-
async snapshot(channel) {
|
|
77
|
-
if (this.#fetchSockets) {
|
|
78
|
-
return this.#getFromSockets(channel);
|
|
79
|
-
}
|
|
80
|
-
return this.#localSnapshot(channel);
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Gets the number of users in a channel.
|
|
84
|
-
*/
|
|
85
|
-
async count(channel) {
|
|
86
|
-
return (await this.snapshot(channel)).count;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Checks whether a socket is locally present in a channel.
|
|
90
|
-
*/
|
|
91
|
-
hasLocal(channel, socketId) {
|
|
92
|
-
return this.#presence.get(channel)?.has(socketId) || false;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Lists every local channel where a socket is present.
|
|
96
|
-
*/
|
|
97
|
-
getLocalChannelsForSocket(socketId) {
|
|
98
|
-
const channels = [];
|
|
99
|
-
for (const [channel, users] of this.#presence) {
|
|
100
|
-
if (users.has(socketId)) {
|
|
101
|
-
channels.push(channel);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return channels;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Lists sockets that are locally present in a channel.
|
|
108
|
-
*/
|
|
109
|
-
getLocalSockets(channel) {
|
|
110
|
-
const users = this.#presence.get(channel);
|
|
111
|
-
if (!users) {
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
return Array.from(users.entries()).map(([socketId, user]) => ({
|
|
115
|
-
id: socketId,
|
|
116
|
-
data: {
|
|
117
|
-
[PRESENCE_DATA_KEY]: {
|
|
118
|
-
[channel]: user
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
123
|
-
#localSnapshot(channel) {
|
|
124
|
-
const users = this.#presence.get(channel);
|
|
125
|
-
return {
|
|
126
|
-
channel,
|
|
127
|
-
users: users ? Array.from(users.values()) : [],
|
|
128
|
-
count: users?.size || 0
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
async #getFromSockets(channel) {
|
|
132
|
-
const sockets = await this.#fetchSockets(channel);
|
|
133
|
-
const users = sockets.map((socket) => socket.data?.[PRESENCE_DATA_KEY]?.[channel]).filter((user) => Boolean(user));
|
|
134
|
-
return {
|
|
135
|
-
channel,
|
|
136
|
-
users,
|
|
137
|
-
count: users.length
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
#setSocketPresence(socket, channel, user) {
|
|
141
|
-
socket.data[PRESENCE_DATA_KEY] ??= {};
|
|
142
|
-
socket.data[PRESENCE_DATA_KEY][channel] = user;
|
|
143
|
-
}
|
|
144
|
-
#deleteSocketPresence(socket, channel) {
|
|
145
|
-
if (!socket.data[PRESENCE_DATA_KEY]) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
delete socket.data[PRESENCE_DATA_KEY][channel];
|
|
149
|
-
if (Object.keys(socket.data[PRESENCE_DATA_KEY]).length === 0) {
|
|
150
|
-
delete socket.data[PRESENCE_DATA_KEY];
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// src/base_channel.ts
|
|
156
|
-
var MissingPresenceInfoError = class extends Error {
|
|
157
|
-
constructor() {
|
|
158
|
-
super("Presence channels must implement getPresenceInfo(socket)");
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
var BaseChannel = class {
|
|
162
|
-
/**
|
|
163
|
-
* URL-like channel pattern, for example: 'chat/:roomId'.
|
|
164
|
-
*/
|
|
165
|
-
static pattern;
|
|
166
|
-
/**
|
|
167
|
-
* Channel options.
|
|
168
|
-
*/
|
|
169
|
-
static options;
|
|
170
|
-
/**
|
|
171
|
-
* Middlewares to run before subscription.
|
|
172
|
-
*/
|
|
173
|
-
static middlewares;
|
|
174
|
-
/**
|
|
175
|
-
* Event handler map.
|
|
176
|
-
* Ex: { 'chat:send': 'handleSend' }
|
|
177
|
-
*/
|
|
178
|
-
handlers = {};
|
|
179
|
-
/**
|
|
180
|
-
* Injected socket service.
|
|
181
|
-
*/
|
|
182
|
-
socket;
|
|
183
|
-
/**
|
|
184
|
-
* Full channel name, for example: 'chat/general'.
|
|
185
|
-
*/
|
|
186
|
-
channelName;
|
|
187
|
-
/**
|
|
188
|
-
* Parameters extracted from the pattern, for example: { roomId: 'general' }.
|
|
189
|
-
*/
|
|
190
|
-
params;
|
|
191
|
-
/**
|
|
192
|
-
* Injected presence manager when presence is enabled.
|
|
193
|
-
*/
|
|
194
|
-
presence;
|
|
195
|
-
/**
|
|
196
|
-
* Injects dependencies.
|
|
197
|
-
*/
|
|
198
|
-
$setContext(socket, channelName, params, presence) {
|
|
199
|
-
this.socket = socket;
|
|
200
|
-
this.channelName = channelName;
|
|
201
|
-
this.params = params;
|
|
202
|
-
if (presence) {
|
|
203
|
-
this.presence = presence;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Emits to every channel member.
|
|
208
|
-
*/
|
|
209
|
-
broadcast(event, data) {
|
|
210
|
-
this.socket.to(this.channelName).emit(event, data);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Emits to every channel member except one socket.
|
|
214
|
-
*/
|
|
215
|
-
broadcastExcept(socketId, event, data) {
|
|
216
|
-
this.socket.to(this.channelName).except(socketId).emit(event, data);
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Runs the middleware chain.
|
|
220
|
-
*
|
|
221
|
-
* middleware[0] -> middleware[1] -> ... -> subscribe
|
|
222
|
-
*/
|
|
223
|
-
async $runMiddlewares(ctx) {
|
|
224
|
-
const middlewares = this.constructor.middlewares || [];
|
|
225
|
-
let index = 0;
|
|
226
|
-
const next = async () => {
|
|
227
|
-
if (index >= middlewares.length) return;
|
|
228
|
-
const middleware = middlewares[index++];
|
|
229
|
-
if (typeof middleware === "function") {
|
|
230
|
-
await middleware(ctx, next);
|
|
231
|
-
} else {
|
|
232
|
-
await middleware.handle(ctx, next);
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
await next();
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Dispatches a message to the matching handler.
|
|
239
|
-
* Called by SocketService. Do not override.
|
|
240
|
-
*/
|
|
241
|
-
async $handleMessage(socket, event, data) {
|
|
242
|
-
const handlers = {
|
|
243
|
-
...this.handlers,
|
|
244
|
-
...getDecoratedChannelHandlers(this)
|
|
245
|
-
};
|
|
246
|
-
const handler = handlers[event];
|
|
247
|
-
if (typeof handler === "function") {
|
|
248
|
-
return handler.call(this, socket, data);
|
|
249
|
-
}
|
|
250
|
-
return this.onMessage?.(socket, event, data);
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Runs the generic join hook. ChannelSubscriptions owns subscription lifecycle.
|
|
254
|
-
*/
|
|
255
|
-
async $handleJoin(socket, ...params) {
|
|
256
|
-
await this.onJoin?.(socket, ...params);
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Runs the generic leave hook. ChannelSubscriptions owns subscription lifecycle.
|
|
260
|
-
*/
|
|
261
|
-
async $handleLeave(socket, ...params) {
|
|
262
|
-
await this.onLeave?.(socket, ...params);
|
|
263
|
-
}
|
|
264
|
-
$getPresenceInfo(socket) {
|
|
265
|
-
if (!this.getPresenceInfo) {
|
|
266
|
-
throw new MissingPresenceInfoError();
|
|
267
|
-
}
|
|
268
|
-
return this.getPresenceInfo(socket);
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
// src/fake_socket.ts
|
|
273
|
-
import { AssertionError } from "assert";
|
|
274
|
-
import { isDeepStrictEqual } from "util";
|
|
275
|
-
var SocketFake = class {
|
|
276
|
-
#emissions = [];
|
|
277
|
-
#restore;
|
|
278
|
-
constructor(restore) {
|
|
279
|
-
this.#restore = restore;
|
|
280
|
-
}
|
|
281
|
-
get emissions() {
|
|
282
|
-
return [...this.#emissions];
|
|
283
|
-
}
|
|
284
|
-
record(emission) {
|
|
285
|
-
this.#emissions.push({
|
|
286
|
-
...emission,
|
|
287
|
-
..."except" in emission && emission.except ? { except: [...emission.except] } : {}
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
dispatch(emission) {
|
|
291
|
-
this.record(emission);
|
|
292
|
-
}
|
|
293
|
-
clear() {
|
|
294
|
-
this.#emissions = [];
|
|
295
|
-
}
|
|
296
|
-
restore() {
|
|
297
|
-
this.#restore();
|
|
298
|
-
}
|
|
299
|
-
[Symbol.dispose]() {
|
|
300
|
-
this.restore();
|
|
301
|
-
}
|
|
302
|
-
assertBroadcasted(event, query) {
|
|
303
|
-
this.#assertEmitted(
|
|
304
|
-
(emission) => emission.target === "global" && emission.event === event,
|
|
305
|
-
query,
|
|
306
|
-
`Expected socket event "${event}" to have been broadcasted`
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
assertNotBroadcasted(event, query) {
|
|
310
|
-
this.#assertNotEmitted(
|
|
311
|
-
(emission) => emission.target === "global" && emission.event === event,
|
|
312
|
-
query,
|
|
313
|
-
`Expected socket event "${event}" not to have been broadcasted`
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
assertEmittedTo(channel, event, query) {
|
|
317
|
-
this.#assertEmitted(
|
|
318
|
-
(emission) => emission.target === "channel" && emission.channel === channel && emission.event === event,
|
|
319
|
-
query,
|
|
320
|
-
`Expected socket event "${event}" to have been emitted to channel "${channel}"`
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
assertNotEmittedTo(channel, event, query) {
|
|
324
|
-
this.#assertNotEmitted(
|
|
325
|
-
(emission) => emission.target === "channel" && emission.channel === channel && emission.event === event,
|
|
326
|
-
query,
|
|
327
|
-
`Expected socket event "${event}" not to have been emitted to channel "${channel}"`
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
assertEmittedToUser(userId, event, query) {
|
|
331
|
-
this.#assertEmitted(
|
|
332
|
-
(emission) => emission.target === "user" && emission.userId === userId && emission.event === event,
|
|
333
|
-
query,
|
|
334
|
-
`Expected socket event "${event}" to have been emitted to user "${String(userId)}"`
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
assertNotEmittedToUser(userId, event, query) {
|
|
338
|
-
this.#assertNotEmitted(
|
|
339
|
-
(emission) => emission.target === "user" && emission.userId === userId && emission.event === event,
|
|
340
|
-
query,
|
|
341
|
-
`Expected socket event "${event}" not to have been emitted to user "${String(userId)}"`
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
assertEmittedCount(count, query) {
|
|
345
|
-
const actual = this.#matching(query).length;
|
|
346
|
-
if (actual !== count) {
|
|
347
|
-
throw new AssertionError({
|
|
348
|
-
message: `Expected ${count} socket event(s) to have been emitted, but found ${actual}`,
|
|
349
|
-
actual,
|
|
350
|
-
expected: count,
|
|
351
|
-
operator: "strictEqual"
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
assertNothingEmitted() {
|
|
356
|
-
this.assertEmittedCount(0);
|
|
357
|
-
}
|
|
358
|
-
#assertEmitted(predicate, query, message) {
|
|
359
|
-
const matching = this.#emissions.filter((emission) => {
|
|
360
|
-
return predicate(emission) && this.#matchesQuery(emission, query);
|
|
361
|
-
});
|
|
362
|
-
if (matching.length === 0) {
|
|
363
|
-
throw new AssertionError({
|
|
364
|
-
message,
|
|
365
|
-
actual: this.#emissions,
|
|
366
|
-
expected: query,
|
|
367
|
-
operator: "includes"
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
#assertNotEmitted(predicate, query, message) {
|
|
372
|
-
const matching = this.#emissions.filter((emission) => {
|
|
373
|
-
return predicate(emission) && this.#matchesQuery(emission, query);
|
|
374
|
-
});
|
|
375
|
-
if (matching.length > 0) {
|
|
376
|
-
throw new AssertionError({
|
|
377
|
-
message,
|
|
378
|
-
actual: matching,
|
|
379
|
-
expected: [],
|
|
380
|
-
operator: "doesNotInclude"
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
#matching(query) {
|
|
385
|
-
return this.#emissions.filter((emission) => {
|
|
386
|
-
if (query?.target && emission.target !== query.target) {
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
if (query?.event && emission.event !== query.event) {
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
if (query?.channel && (!("channel" in emission) || emission.channel !== query.channel)) {
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
if (query?.userId !== void 0 && (!("userId" in emission) || emission.userId !== query.userId)) {
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
return this.#matchesQuery(emission, query);
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
#matchesQuery(emission, query) {
|
|
402
|
-
if (!query) {
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
if ("data" in query) {
|
|
406
|
-
if (typeof query.data === "function") {
|
|
407
|
-
if (!query.data(emission.data)) {
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
} else if (!isDeepStrictEqual(emission.data, query.data)) {
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
if ("except" in query) {
|
|
415
|
-
if (typeof query.except === "function") {
|
|
416
|
-
if (!query.except("except" in emission ? emission.except : void 0)) {
|
|
417
|
-
return false;
|
|
418
|
-
}
|
|
419
|
-
} else if (!isDeepStrictEqual(("except" in emission ? emission.except : void 0) ?? [], query.except)) {
|
|
420
|
-
return false;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return true;
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
// src/socket_service.ts
|
|
428
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
429
|
-
import Emittery from "emittery";
|
|
430
|
-
import { WebSocket, WebSocketServer } from "ws";
|
|
431
|
-
|
|
432
|
-
// src/channel_subscription_storage.ts
|
|
433
|
-
var ChannelSubscriptionStorage = class {
|
|
434
|
-
#subscriptions = /* @__PURE__ */ new Map();
|
|
435
|
-
#subscribers = /* @__PURE__ */ new Map();
|
|
436
|
-
has(socketId, channelName) {
|
|
437
|
-
return this.#subscriptions.get(socketId)?.has(channelName) ?? false;
|
|
438
|
-
}
|
|
439
|
-
get(socketId, channelName) {
|
|
440
|
-
return this.#subscriptions.get(socketId)?.get(channelName);
|
|
441
|
-
}
|
|
442
|
-
set(socketId, channelName, subscription) {
|
|
443
|
-
if (!this.#subscriptions.has(socketId)) {
|
|
444
|
-
this.#subscriptions.set(socketId, /* @__PURE__ */ new Map());
|
|
445
|
-
}
|
|
446
|
-
this.#subscriptions.get(socketId).set(channelName, subscription);
|
|
447
|
-
if (!this.#subscribers.has(channelName)) {
|
|
448
|
-
this.#subscribers.set(channelName, /* @__PURE__ */ new Set());
|
|
449
|
-
}
|
|
450
|
-
this.#subscribers.get(channelName).add(socketId);
|
|
451
|
-
}
|
|
452
|
-
delete(socketId, channelName) {
|
|
453
|
-
const subscriptions = this.#subscriptions.get(socketId);
|
|
454
|
-
if (!subscriptions) {
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
subscriptions.delete(channelName);
|
|
458
|
-
if (subscriptions.size === 0) {
|
|
459
|
-
this.#subscriptions.delete(socketId);
|
|
460
|
-
}
|
|
461
|
-
const subscribers = this.#subscribers.get(channelName);
|
|
462
|
-
subscribers?.delete(socketId);
|
|
463
|
-
if (subscribers?.size === 0) {
|
|
464
|
-
this.#subscribers.delete(channelName);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
deleteSocket(socketId) {
|
|
468
|
-
for (const channelName of this.channelNamesFor(socketId)) {
|
|
469
|
-
this.delete(socketId, channelName);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
channelNamesFor(socketId) {
|
|
473
|
-
return [...this.#subscriptions.get(socketId)?.keys() ?? []];
|
|
474
|
-
}
|
|
475
|
-
socketIdsFor(channelName) {
|
|
476
|
-
return [...this.#subscribers.get(channelName) ?? []];
|
|
477
|
-
}
|
|
478
|
-
get channelsCount() {
|
|
479
|
-
return this.#subscribers.size;
|
|
480
|
-
}
|
|
481
|
-
clear() {
|
|
482
|
-
this.#subscriptions.clear();
|
|
483
|
-
this.#subscribers.clear();
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
// src/channel_subscriptions.ts
|
|
488
|
-
var ChannelSubscriptions = class {
|
|
489
|
-
constructor(socketService, channelRouter, logger, presenceManager) {
|
|
490
|
-
this.socketService = socketService;
|
|
491
|
-
this.channelRouter = channelRouter;
|
|
492
|
-
this.logger = logger;
|
|
493
|
-
this.presenceManager = presenceManager;
|
|
494
|
-
}
|
|
495
|
-
#storage = new ChannelSubscriptionStorage();
|
|
496
|
-
setPresenceManager(presenceManager) {
|
|
497
|
-
this.presenceManager = presenceManager;
|
|
498
|
-
}
|
|
499
|
-
async subscribe(socket, channelName) {
|
|
500
|
-
if (this.#storage.has(socket.id, channelName)) {
|
|
501
|
-
return {
|
|
502
|
-
ack: {
|
|
503
|
-
ok: true,
|
|
504
|
-
presenceData: await this.#getPresenceData(socket, channelName)
|
|
505
|
-
},
|
|
506
|
-
created: false
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
const result = await this.channelRouter.authorize(socket, channelName);
|
|
510
|
-
if (!result.success) {
|
|
511
|
-
return {
|
|
512
|
-
ack: { ok: false, error: result.error },
|
|
513
|
-
created: false
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
result.instance.$setContext(
|
|
517
|
-
this.socketService,
|
|
518
|
-
channelName,
|
|
519
|
-
result.params,
|
|
520
|
-
this.presenceManager
|
|
521
|
-
);
|
|
522
|
-
let presenceData = result.presenceData;
|
|
523
|
-
let presenceJoined = false;
|
|
524
|
-
try {
|
|
525
|
-
if (this.#usesPresence(result.instance)) {
|
|
526
|
-
const member = this.#getPresenceMember(result.instance, socket);
|
|
527
|
-
this.presenceManager.join(channelName, socket.raw, member);
|
|
528
|
-
presenceJoined = true;
|
|
529
|
-
await result.instance.onMemberJoin?.(socket, member, ...result.paramValues);
|
|
530
|
-
await result.instance.onJoin?.(socket, ...result.paramValues);
|
|
531
|
-
presenceData = await this.presenceManager.snapshot(channelName);
|
|
532
|
-
} else {
|
|
533
|
-
await result.instance.onJoin?.(socket, ...result.paramValues);
|
|
534
|
-
}
|
|
535
|
-
this.#storage.set(socket.id, channelName, {
|
|
536
|
-
channelName,
|
|
537
|
-
instance: result.instance,
|
|
538
|
-
paramValues: result.paramValues
|
|
539
|
-
});
|
|
540
|
-
socket.channels.add(channelName);
|
|
541
|
-
if (this.#usesPresence(result.instance) && presenceData) {
|
|
542
|
-
this.socketService.to(channelName).except(socket.id).emit("presence:update", presenceData);
|
|
543
|
-
}
|
|
544
|
-
} catch (error) {
|
|
545
|
-
if (presenceJoined) {
|
|
546
|
-
this.presenceManager.leave(channelName, socket.raw);
|
|
547
|
-
const rollbackData = await this.presenceManager.snapshot(channelName).catch(() => null);
|
|
548
|
-
if (rollbackData) {
|
|
549
|
-
this.socketService.to(channelName).emit("presence:update", rollbackData);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
this.#storage.delete(socket.id, channelName);
|
|
553
|
-
socket.channels.delete(channelName);
|
|
554
|
-
return {
|
|
555
|
-
ack: {
|
|
556
|
-
ok: false,
|
|
557
|
-
error: error instanceof MissingPresenceInfoError ? error.message : "Join failed"
|
|
558
|
-
},
|
|
559
|
-
created: false
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
return {
|
|
563
|
-
ack: { ok: true, presenceData },
|
|
564
|
-
created: true
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
async leave(socket, channelName) {
|
|
568
|
-
const subscription = this.#storage.get(socket.id, channelName);
|
|
569
|
-
if (subscription) {
|
|
570
|
-
this.#storage.delete(socket.id, channelName);
|
|
571
|
-
socket.channels.delete(channelName);
|
|
572
|
-
try {
|
|
573
|
-
if (this.#usesPresence(subscription.instance)) {
|
|
574
|
-
const member = this.#getPresenceMember(subscription.instance, socket);
|
|
575
|
-
this.presenceManager.leave(channelName, socket.raw);
|
|
576
|
-
const presenceData = await this.presenceManager.snapshot(channelName);
|
|
577
|
-
this.socketService.to(channelName).emit("presence:update", presenceData);
|
|
578
|
-
await subscription.instance.onMemberLeave?.(socket, member, ...subscription.paramValues);
|
|
579
|
-
await subscription.instance.onLeave?.(socket, ...subscription.paramValues);
|
|
580
|
-
} else {
|
|
581
|
-
await subscription.instance.onLeave?.(socket, ...subscription.paramValues);
|
|
582
|
-
}
|
|
583
|
-
} catch (error) {
|
|
584
|
-
this.logger.warn(`failed to leave channel "${channelName}": %s`, error);
|
|
585
|
-
}
|
|
586
|
-
} else {
|
|
587
|
-
socket.channels.delete(channelName);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
async leaveAll(socket) {
|
|
591
|
-
for (const channelName of this.getChannelNames(socket.id)) {
|
|
592
|
-
await this.leave(socket, channelName);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
async handleMessage(socket, payload) {
|
|
596
|
-
const subscription = this.#storage.get(socket.id, payload.channel);
|
|
597
|
-
if (!subscription) {
|
|
598
|
-
return { ok: false, error: "Not subscribed" };
|
|
599
|
-
}
|
|
600
|
-
try {
|
|
601
|
-
const result = await subscription.instance.$handleMessage(socket, payload.event, payload.data);
|
|
602
|
-
return { ok: true, data: result };
|
|
603
|
-
} catch (error) {
|
|
604
|
-
return {
|
|
605
|
-
ok: false,
|
|
606
|
-
error: error instanceof Error ? error.message : "Handler error"
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
relayWhisper(socket, payload) {
|
|
611
|
-
if (!this.#storage.has(socket.id, payload.channel)) {
|
|
612
|
-
return { ok: false, error: "Not subscribed" };
|
|
613
|
-
}
|
|
614
|
-
this.socketService.to(payload.channel).except(socket.id).emit(`client:${payload.event}`, payload.data);
|
|
615
|
-
return { ok: true };
|
|
616
|
-
}
|
|
617
|
-
deleteSocket(socketId) {
|
|
618
|
-
this.#storage.deleteSocket(socketId);
|
|
619
|
-
}
|
|
620
|
-
clear() {
|
|
621
|
-
this.#storage.clear();
|
|
622
|
-
}
|
|
623
|
-
getSocketIds(channelName) {
|
|
624
|
-
return this.#storage.socketIdsFor(channelName);
|
|
625
|
-
}
|
|
626
|
-
getChannelNames(socketId) {
|
|
627
|
-
return this.#storage.channelNamesFor(socketId);
|
|
628
|
-
}
|
|
629
|
-
get channelsCount() {
|
|
630
|
-
return this.#storage.channelsCount;
|
|
631
|
-
}
|
|
632
|
-
#usesPresence(instance) {
|
|
633
|
-
return Boolean(
|
|
634
|
-
instance.constructor.options?.presence && this.presenceManager
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
#getPresenceMember(instance, socket) {
|
|
638
|
-
return instance.$getPresenceInfo(socket);
|
|
639
|
-
}
|
|
640
|
-
async #getPresenceData(socket, channelName) {
|
|
641
|
-
if (!this.presenceManager?.hasLocal(channelName, socket.id)) {
|
|
642
|
-
return void 0;
|
|
643
|
-
}
|
|
644
|
-
return this.presenceManager.snapshot(channelName);
|
|
645
|
-
}
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
// src/protocol/reply.ts
|
|
649
|
-
var INVALID_SOCKET_MESSAGE = "Invalid socket message";
|
|
650
|
-
var Reply = class _Reply {
|
|
651
|
-
constructor(id, ok, data, error) {
|
|
652
|
-
this.id = id;
|
|
653
|
-
this.ok = ok;
|
|
654
|
-
this.data = data;
|
|
655
|
-
this.error = error;
|
|
656
|
-
}
|
|
657
|
-
static ok(id, data) {
|
|
658
|
-
return new _Reply(id, true, data, void 0);
|
|
659
|
-
}
|
|
660
|
-
static error(id, error) {
|
|
661
|
-
return new _Reply(id, false, void 0, error);
|
|
662
|
-
}
|
|
663
|
-
static invalidMessage(id) {
|
|
664
|
-
if (id === void 0) {
|
|
665
|
-
return { type: "error", error: INVALID_SOCKET_MESSAGE };
|
|
666
|
-
}
|
|
667
|
-
return _Reply.error(id, INVALID_SOCKET_MESSAGE).toFrame();
|
|
668
|
-
}
|
|
669
|
-
static fromSubscribeResult(id, result) {
|
|
670
|
-
if (!result) {
|
|
671
|
-
return _Reply.error(id, "Socket service is not initialized");
|
|
672
|
-
}
|
|
673
|
-
if (!result.ack.ok) {
|
|
674
|
-
return _Reply.error(id, result.ack.error ?? "Subscribe failed");
|
|
675
|
-
}
|
|
676
|
-
const data = result.ack.presenceData ? {
|
|
677
|
-
presenceData: result.ack.presenceData
|
|
678
|
-
} : void 0;
|
|
679
|
-
return _Reply.ok(id, data);
|
|
680
|
-
}
|
|
681
|
-
static fromChannelAck(id, ack) {
|
|
682
|
-
return ack.ok ? _Reply.ok(id, ack.data) : _Reply.error(id, ack.error ?? "Handler error");
|
|
683
|
-
}
|
|
684
|
-
toFrame() {
|
|
685
|
-
const frame = {
|
|
686
|
-
id: this.id,
|
|
687
|
-
type: "ack",
|
|
688
|
-
ok: this.ok
|
|
689
|
-
};
|
|
690
|
-
if (this.data !== void 0) {
|
|
691
|
-
frame.data = this.data;
|
|
692
|
-
}
|
|
693
|
-
if (this.error !== void 0) {
|
|
694
|
-
frame.error = this.error;
|
|
695
|
-
}
|
|
696
|
-
return frame;
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
// src/protocol/message.ts
|
|
701
|
-
function isRecord(value) {
|
|
702
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
703
|
-
}
|
|
704
|
-
var Message = class _Message {
|
|
705
|
-
static fromTransport(raw) {
|
|
706
|
-
try {
|
|
707
|
-
return _Message.fromParsedTransport(JSON.parse(raw));
|
|
708
|
-
} catch {
|
|
709
|
-
return new InvalidMessage(void 0);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
static fromParsedTransport(value) {
|
|
713
|
-
if (!isRecord(value)) {
|
|
714
|
-
return new InvalidMessage(void 0);
|
|
715
|
-
}
|
|
716
|
-
const id = typeof value.id === "string" ? value.id : void 0;
|
|
717
|
-
if ("id" in value && typeof value.id !== "string") {
|
|
718
|
-
return new InvalidMessage(void 0);
|
|
719
|
-
}
|
|
720
|
-
if (!isClientMessageType(value.type)) {
|
|
721
|
-
return new InvalidMessage(id);
|
|
722
|
-
}
|
|
723
|
-
if (value.type === "ping") {
|
|
724
|
-
return new PingMessage(id);
|
|
725
|
-
}
|
|
726
|
-
if (value.type === "subscribe" && typeof value.channel === "string") {
|
|
727
|
-
return new SubscribeMessage(id, value.channel);
|
|
728
|
-
}
|
|
729
|
-
if (value.type === "unsubscribe" && typeof value.channel === "string") {
|
|
730
|
-
return new UnsubscribeMessage(id, value.channel);
|
|
731
|
-
}
|
|
732
|
-
if ((value.type === "message" || value.type === "whisper") && typeof value.channel === "string" && typeof value.event === "string") {
|
|
733
|
-
return new ChannelProtocolMessage(id, value.type, value.channel, value.event, value.data);
|
|
734
|
-
}
|
|
735
|
-
return new InvalidMessage(id);
|
|
736
|
-
}
|
|
737
|
-
};
|
|
738
|
-
var InvalidMessage = class extends Message {
|
|
739
|
-
constructor(id) {
|
|
740
|
-
super();
|
|
741
|
-
this.id = id;
|
|
742
|
-
}
|
|
743
|
-
valid = false;
|
|
744
|
-
type = "invalid";
|
|
745
|
-
toRejectionFrame() {
|
|
746
|
-
return Reply.invalidMessage(this.id);
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
|
-
var PingMessage = class extends Message {
|
|
750
|
-
constructor(id) {
|
|
751
|
-
super();
|
|
752
|
-
this.id = id;
|
|
753
|
-
}
|
|
754
|
-
valid = true;
|
|
755
|
-
type = "ping";
|
|
756
|
-
};
|
|
757
|
-
var SubscribeMessage = class extends Message {
|
|
758
|
-
constructor(id, channel) {
|
|
759
|
-
super();
|
|
760
|
-
this.id = id;
|
|
761
|
-
this.channel = channel;
|
|
762
|
-
}
|
|
763
|
-
valid = true;
|
|
764
|
-
type = "subscribe";
|
|
765
|
-
};
|
|
766
|
-
var UnsubscribeMessage = class extends Message {
|
|
767
|
-
constructor(id, channel) {
|
|
768
|
-
super();
|
|
769
|
-
this.id = id;
|
|
770
|
-
this.channel = channel;
|
|
771
|
-
}
|
|
772
|
-
valid = true;
|
|
773
|
-
type = "unsubscribe";
|
|
774
|
-
};
|
|
775
|
-
var ChannelProtocolMessage = class extends Message {
|
|
776
|
-
constructor(id, type, channel, event, data) {
|
|
777
|
-
super();
|
|
778
|
-
this.id = id;
|
|
779
|
-
this.type = type;
|
|
780
|
-
this.channel = channel;
|
|
781
|
-
this.event = event;
|
|
782
|
-
this.data = data;
|
|
783
|
-
}
|
|
784
|
-
valid = true;
|
|
785
|
-
toChannelMessage() {
|
|
786
|
-
return {
|
|
787
|
-
channel: this.channel,
|
|
788
|
-
event: this.event,
|
|
789
|
-
data: this.data
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
function isClientMessageType(value) {
|
|
794
|
-
return value === "ping" || value === "subscribe" || value === "unsubscribe" || value === "message" || value === "whisper";
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// src/message_queue.ts
|
|
798
|
-
var MessageQueue = class {
|
|
799
|
-
#queues = /* @__PURE__ */ new Map();
|
|
800
|
-
#depths = /* @__PURE__ */ new Map();
|
|
801
|
-
enqueue(key, task, options = {}) {
|
|
802
|
-
const depth = this.#depths.get(key) ?? 0;
|
|
803
|
-
if (options.maxDepth !== void 0 && depth >= options.maxDepth) {
|
|
804
|
-
return false;
|
|
805
|
-
}
|
|
806
|
-
this.#depths.set(key, depth + 1);
|
|
807
|
-
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
808
|
-
const next = previous.catch(() => {
|
|
809
|
-
}).then(task).catch(() => {
|
|
810
|
-
}).finally(() => {
|
|
811
|
-
const nextDepth = (this.#depths.get(key) ?? 1) - 1;
|
|
812
|
-
if (nextDepth <= 0) {
|
|
813
|
-
this.#depths.delete(key);
|
|
814
|
-
} else {
|
|
815
|
-
this.#depths.set(key, nextDepth);
|
|
816
|
-
}
|
|
817
|
-
if (this.#queues.get(key) === next) {
|
|
818
|
-
this.#queues.delete(key);
|
|
819
|
-
}
|
|
820
|
-
});
|
|
821
|
-
this.#queues.set(key, next);
|
|
822
|
-
return true;
|
|
823
|
-
}
|
|
824
|
-
delete(key) {
|
|
825
|
-
this.#queues.delete(key);
|
|
826
|
-
this.#depths.delete(key);
|
|
827
|
-
}
|
|
828
|
-
clear() {
|
|
829
|
-
this.#queues.clear();
|
|
830
|
-
this.#depths.clear();
|
|
831
|
-
}
|
|
832
|
-
};
|
|
833
|
-
|
|
834
|
-
// src/broadcasting.ts
|
|
835
|
-
function userRoom(userId) {
|
|
836
|
-
return `socket:user:${String(userId)}`;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// src/socket_bus.ts
|
|
840
|
-
import { randomUUID } from "crypto";
|
|
841
|
-
import { Bus } from "@boringnode/bus";
|
|
842
|
-
|
|
843
|
-
// src/duration.ts
|
|
844
|
-
import { parse as parseDurationExpression } from "@lukeed/ms";
|
|
845
|
-
function parseDuration(name, duration) {
|
|
846
|
-
if (duration === void 0) {
|
|
847
|
-
return void 0;
|
|
848
|
-
}
|
|
849
|
-
const milliseconds = typeof duration === "number" ? duration : parseDurationExpression(duration);
|
|
850
|
-
if (typeof milliseconds === "undefined" || !Number.isFinite(milliseconds) || milliseconds <= 0) {
|
|
851
|
-
throw new Error(`${name} must be a positive duration`);
|
|
852
|
-
}
|
|
853
|
-
return milliseconds;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// src/socket_bus.ts
|
|
857
|
-
var DEFAULT_CHANNEL = "socket::broadcast";
|
|
858
|
-
var DEFAULT_PRESENCE_TIMEOUT = 100;
|
|
859
|
-
var SocketBus = class {
|
|
860
|
-
constructor(transport, handlers) {
|
|
861
|
-
this.handlers = handlers;
|
|
862
|
-
this.#channel = transport.channel ?? DEFAULT_CHANNEL;
|
|
863
|
-
this.#presenceTimeout = parseDuration("transport.presenceTimeout", transport.presenceTimeout) ?? DEFAULT_PRESENCE_TIMEOUT;
|
|
864
|
-
this.#bus = new Bus(transport.driver(), { retryQueue: { enabled: true } });
|
|
865
|
-
}
|
|
866
|
-
#origin = randomUUID();
|
|
867
|
-
#channel;
|
|
868
|
-
#bus;
|
|
869
|
-
#presenceTimeout;
|
|
870
|
-
#pendingPresenceRequests = /* @__PURE__ */ new Map();
|
|
871
|
-
async start() {
|
|
872
|
-
await this.#bus.subscribe(this.#channel, async (payload) => {
|
|
873
|
-
const message = payload;
|
|
874
|
-
if (message.origin === this.#origin) {
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
switch (message.type) {
|
|
878
|
-
case "channel:event":
|
|
879
|
-
this.handlers.channel(message);
|
|
880
|
-
return;
|
|
881
|
-
case "user:event":
|
|
882
|
-
this.handlers.user(message);
|
|
883
|
-
return;
|
|
884
|
-
case "broadcast:event":
|
|
885
|
-
this.handlers.broadcast(message);
|
|
886
|
-
return;
|
|
887
|
-
case "presence:sockets:request":
|
|
888
|
-
await this.#respondToPresenceRequest(message);
|
|
889
|
-
return;
|
|
890
|
-
case "presence:sockets:response":
|
|
891
|
-
this.#handlePresenceResponse(message);
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
publishChannel(channel, event, data, except) {
|
|
897
|
-
const message = {
|
|
898
|
-
type: "channel:event",
|
|
899
|
-
origin: this.#origin,
|
|
900
|
-
channel,
|
|
901
|
-
event,
|
|
902
|
-
data
|
|
903
|
-
};
|
|
904
|
-
if (except) {
|
|
905
|
-
message.except = except;
|
|
906
|
-
}
|
|
907
|
-
void this.#bus.publish(this.#channel, message);
|
|
908
|
-
}
|
|
909
|
-
publishUser(room, event, data) {
|
|
910
|
-
void this.#bus.publish(this.#channel, {
|
|
911
|
-
type: "user:event",
|
|
912
|
-
origin: this.#origin,
|
|
913
|
-
room,
|
|
914
|
-
event,
|
|
915
|
-
data
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
publishBroadcast(event, data) {
|
|
919
|
-
void this.#bus.publish(this.#channel, {
|
|
920
|
-
type: "broadcast:event",
|
|
921
|
-
origin: this.#origin,
|
|
922
|
-
event,
|
|
923
|
-
data
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
fetchPresenceSockets(channel) {
|
|
927
|
-
const requestId = randomUUID();
|
|
928
|
-
return new Promise((resolve) => {
|
|
929
|
-
const timeout = setTimeout(() => {
|
|
930
|
-
const request = this.#pendingPresenceRequests.get(requestId);
|
|
931
|
-
if (!request) {
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
this.#pendingPresenceRequests.delete(requestId);
|
|
935
|
-
resolve(request.sockets.map((socket) => this.#deserializePresenceSocket(socket)));
|
|
936
|
-
}, this.#presenceTimeout);
|
|
937
|
-
this.#pendingPresenceRequests.set(requestId, {
|
|
938
|
-
sockets: [],
|
|
939
|
-
resolve,
|
|
940
|
-
timeout
|
|
941
|
-
});
|
|
942
|
-
void this.#bus.publish(this.#channel, {
|
|
943
|
-
type: "presence:sockets:request",
|
|
944
|
-
origin: this.#origin,
|
|
945
|
-
requestId,
|
|
946
|
-
channel
|
|
947
|
-
});
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
async close() {
|
|
951
|
-
for (const [requestId, request] of this.#pendingPresenceRequests) {
|
|
952
|
-
clearTimeout(request.timeout);
|
|
953
|
-
request.resolve(request.sockets.map((socket) => this.#deserializePresenceSocket(socket)));
|
|
954
|
-
this.#pendingPresenceRequests.delete(requestId);
|
|
955
|
-
}
|
|
956
|
-
await this.#bus.disconnect();
|
|
957
|
-
}
|
|
958
|
-
async #respondToPresenceRequest(message) {
|
|
959
|
-
await this.#bus.publish(this.#channel, {
|
|
960
|
-
type: "presence:sockets:response",
|
|
961
|
-
origin: this.#origin,
|
|
962
|
-
target: message.origin,
|
|
963
|
-
requestId: message.requestId,
|
|
964
|
-
sockets: this.handlers.presenceSockets(message.channel)
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
#handlePresenceResponse(message) {
|
|
968
|
-
if (message.target !== this.#origin) {
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
const request = this.#pendingPresenceRequests.get(message.requestId);
|
|
972
|
-
if (!request) {
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
request.sockets.push(...message.sockets);
|
|
976
|
-
}
|
|
977
|
-
#deserializePresenceSocket(socket) {
|
|
978
|
-
const presence = socket.data[PRESENCE_DATA_KEY];
|
|
979
|
-
return {
|
|
980
|
-
id: socket.id,
|
|
981
|
-
data: {
|
|
982
|
-
[PRESENCE_DATA_KEY]: presence ? Object.fromEntries(
|
|
983
|
-
Object.entries(presence).map(([channel, user]) => [
|
|
984
|
-
channel,
|
|
985
|
-
{
|
|
986
|
-
...user,
|
|
987
|
-
joinedAt: new Date(user.joinedAt)
|
|
988
|
-
}
|
|
989
|
-
])
|
|
990
|
-
) : void 0
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
};
|
|
995
|
-
|
|
996
|
-
// src/socket_upgrader.ts
|
|
997
|
-
var DEFAULT_WEBSOCKET_PATH = "/socket";
|
|
998
|
-
var SocketUpgrader = class _SocketUpgrader {
|
|
999
|
-
constructor(server, config) {
|
|
1000
|
-
this.server = server;
|
|
1001
|
-
this.config = config;
|
|
1002
|
-
this.#path = config?.path ?? DEFAULT_WEBSOCKET_PATH;
|
|
1003
|
-
}
|
|
1004
|
-
#path;
|
|
1005
|
-
async handle(request, socket, head, accept) {
|
|
1006
|
-
if (!this.#matchesPath(request)) {
|
|
1007
|
-
return false;
|
|
1008
|
-
}
|
|
1009
|
-
const accepted = await this.#authenticate(request);
|
|
1010
|
-
if (!accepted) {
|
|
1011
|
-
_SocketUpgrader.reject(socket, 401, "Unauthorized");
|
|
1012
|
-
return true;
|
|
1013
|
-
}
|
|
1014
|
-
this.server.handleUpgrade(request, socket, head, (connection) => {
|
|
1015
|
-
accept(connection, request, accepted);
|
|
1016
|
-
});
|
|
1017
|
-
return true;
|
|
1018
|
-
}
|
|
1019
|
-
static reject(socket, statusCode, reason) {
|
|
1020
|
-
if (!socket.writableEnded) {
|
|
1021
|
-
socket.write(`HTTP/1.1 ${statusCode} ${reason}\r
|
|
1022
|
-
\r
|
|
1023
|
-
`);
|
|
1024
|
-
}
|
|
1025
|
-
socket.destroy();
|
|
1026
|
-
}
|
|
1027
|
-
#matchesPath(request) {
|
|
1028
|
-
const url = new URL(request.url ?? "/", "ws://localhost");
|
|
1029
|
-
return url.pathname === this.#path;
|
|
1030
|
-
}
|
|
1031
|
-
async #authenticate(request) {
|
|
1032
|
-
const config = this.config;
|
|
1033
|
-
if (!config) {
|
|
1034
|
-
return {};
|
|
1035
|
-
}
|
|
1036
|
-
if (!config.authenticate && !config.createContextOnUpgrade) {
|
|
1037
|
-
return {};
|
|
1038
|
-
}
|
|
1039
|
-
let httpContext;
|
|
1040
|
-
try {
|
|
1041
|
-
httpContext = await config.createContext?.(request);
|
|
1042
|
-
if (!config.authenticate) {
|
|
1043
|
-
return { httpContext };
|
|
1044
|
-
}
|
|
1045
|
-
const ctx = {
|
|
1046
|
-
httpContext
|
|
1047
|
-
};
|
|
1048
|
-
const result = await config.authenticate(ctx);
|
|
1049
|
-
if (result === false || result === null) {
|
|
1050
|
-
return null;
|
|
1051
|
-
}
|
|
1052
|
-
return {
|
|
1053
|
-
httpContext,
|
|
1054
|
-
user: result
|
|
1055
|
-
};
|
|
1056
|
-
} catch {
|
|
1057
|
-
return null;
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
};
|
|
1061
|
-
|
|
1062
|
-
// src/socket_service.ts
|
|
1063
|
-
var DEFAULT_PING_INTERVAL = 25e3;
|
|
1064
|
-
var DEFAULT_PING_TIMEOUT = 5e3;
|
|
1065
|
-
var DEFAULT_MAX_PAYLOAD = 1024 * 1024;
|
|
1066
|
-
var DEFAULT_MAX_QUEUED_MESSAGES = 100;
|
|
1067
|
-
var DEFAULT_MAX_MESSAGES_PER_INTERVAL = 1e3;
|
|
1068
|
-
var DEFAULT_MESSAGE_RATE_INTERVAL = 1e3;
|
|
1069
|
-
var DEFAULT_MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024;
|
|
1070
|
-
var CLOSE_POLICY_VIOLATION = 1008;
|
|
1071
|
-
var CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
1072
|
-
var CLOSE_OUTBOUND_BUFFER_LIMIT = "Socket outbound buffer limit exceeded";
|
|
1073
|
-
function serializeFrame(frame) {
|
|
1074
|
-
return JSON.stringify(frame);
|
|
1075
|
-
}
|
|
1076
|
-
function sendSerializedFrame(connection, serializedFrame) {
|
|
1077
|
-
if (connection.readyState !== WebSocket.OPEN) {
|
|
1078
|
-
return false;
|
|
1079
|
-
}
|
|
1080
|
-
connection.send(serializedFrame);
|
|
1081
|
-
return true;
|
|
1082
|
-
}
|
|
1083
|
-
function sendFrame(connection, frame) {
|
|
1084
|
-
return sendSerializedFrame(connection, serializeFrame(frame));
|
|
1085
|
-
}
|
|
1086
|
-
function sendSerializedFrameWithBackpressure(connection, serializedFrame, config) {
|
|
1087
|
-
if (connection.readyState !== WebSocket.OPEN) {
|
|
1088
|
-
return false;
|
|
1089
|
-
}
|
|
1090
|
-
if (connection.bufferedAmount > config.maxBufferedAmount) {
|
|
1091
|
-
closeConnection(connection, CLOSE_POLICY_VIOLATION, CLOSE_OUTBOUND_BUFFER_LIMIT);
|
|
1092
|
-
return false;
|
|
1093
|
-
}
|
|
1094
|
-
connection.send(serializedFrame);
|
|
1095
|
-
return true;
|
|
1096
|
-
}
|
|
1097
|
-
function parsePositiveInteger(name, value, fallback) {
|
|
1098
|
-
if (value === void 0) {
|
|
1099
|
-
return fallback;
|
|
1100
|
-
}
|
|
1101
|
-
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
1102
|
-
throw new Error(`${name} must be a positive integer`);
|
|
1103
|
-
}
|
|
1104
|
-
return value;
|
|
1105
|
-
}
|
|
1106
|
-
function resolveHeartbeatConfig(config) {
|
|
1107
|
-
const pingInterval = parseDuration("websocket.pingInterval", config?.pingInterval);
|
|
1108
|
-
const pingTimeout = parseDuration("websocket.pingTimeout", config?.pingTimeout);
|
|
1109
|
-
if (pingInterval === void 0 && pingTimeout === void 0) {
|
|
1110
|
-
return null;
|
|
1111
|
-
}
|
|
1112
|
-
return {
|
|
1113
|
-
interval: pingInterval ?? DEFAULT_PING_INTERVAL,
|
|
1114
|
-
timeout: pingTimeout ?? DEFAULT_PING_TIMEOUT
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
function resolveInboundMessageConfig(config) {
|
|
1118
|
-
const messageRateInterval = parseDuration("websocket.messageRateInterval", config?.messageRateInterval) ?? DEFAULT_MESSAGE_RATE_INTERVAL;
|
|
1119
|
-
return {
|
|
1120
|
-
maxPayload: parsePositiveInteger(
|
|
1121
|
-
"websocket.maxPayload",
|
|
1122
|
-
config?.maxPayload,
|
|
1123
|
-
DEFAULT_MAX_PAYLOAD
|
|
1124
|
-
),
|
|
1125
|
-
maxQueuedMessages: parsePositiveInteger(
|
|
1126
|
-
"websocket.maxQueuedMessages",
|
|
1127
|
-
config?.maxQueuedMessages,
|
|
1128
|
-
DEFAULT_MAX_QUEUED_MESSAGES
|
|
1129
|
-
),
|
|
1130
|
-
maxMessagesPerInterval: parsePositiveInteger(
|
|
1131
|
-
"websocket.maxMessagesPerInterval",
|
|
1132
|
-
config?.maxMessagesPerInterval,
|
|
1133
|
-
DEFAULT_MAX_MESSAGES_PER_INTERVAL
|
|
1134
|
-
),
|
|
1135
|
-
messageRateInterval
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
function resolveOutboundMessageConfig(config) {
|
|
1139
|
-
return {
|
|
1140
|
-
maxBufferedAmount: parsePositiveInteger(
|
|
1141
|
-
"websocket.maxBufferedAmount",
|
|
1142
|
-
config?.maxBufferedAmount,
|
|
1143
|
-
DEFAULT_MAX_BUFFERED_AMOUNT
|
|
1144
|
-
)
|
|
1145
|
-
};
|
|
1146
|
-
}
|
|
1147
|
-
function getRawDataByteLength(data) {
|
|
1148
|
-
if (Array.isArray(data)) {
|
|
1149
|
-
return data.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
1150
|
-
}
|
|
1151
|
-
return data.byteLength;
|
|
1152
|
-
}
|
|
1153
|
-
function closeConnection(connection, code, reason) {
|
|
1154
|
-
if (connection.readyState === WebSocket.OPEN) {
|
|
1155
|
-
connection.close(code, reason);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
var SocketService = class extends Emittery {
|
|
1159
|
-
#server = null;
|
|
1160
|
-
#httpServer = null;
|
|
1161
|
-
#upgradeHandler = null;
|
|
1162
|
-
#sockets = /* @__PURE__ */ new Map();
|
|
1163
|
-
#userRooms = /* @__PURE__ */ new Map();
|
|
1164
|
-
#socketUserRoom = /* @__PURE__ */ new Map();
|
|
1165
|
-
#channelSubscriptions = null;
|
|
1166
|
-
#presenceManager = null;
|
|
1167
|
-
#bus = null;
|
|
1168
|
-
#fake = null;
|
|
1169
|
-
#deliverySink = {
|
|
1170
|
-
dispatch: (emission) => {
|
|
1171
|
-
this.#deliverBroadcast(emission);
|
|
1172
|
-
}
|
|
1173
|
-
};
|
|
1174
|
-
#broadcastSink = this.#deliverySink;
|
|
1175
|
-
#status = "stopped";
|
|
1176
|
-
#lastError = null;
|
|
1177
|
-
#heartbeatInterval = null;
|
|
1178
|
-
#heartbeatTimeouts = /* @__PURE__ */ new Map();
|
|
1179
|
-
#messageQueue = new MessageQueue();
|
|
1180
|
-
#messageRates = /* @__PURE__ */ new Map();
|
|
1181
|
-
#outboundMessageConfig = resolveOutboundMessageConfig(void 0);
|
|
1182
|
-
/**
|
|
1183
|
-
* Accesses the underlying ws server.
|
|
1184
|
-
*/
|
|
1185
|
-
get server() {
|
|
1186
|
-
if (!this.#server) {
|
|
1187
|
-
throw new Error("WebSocket server is not initialized. Call boot() first.");
|
|
1188
|
-
}
|
|
1189
|
-
return this.#server;
|
|
1190
|
-
}
|
|
1191
|
-
setPresenceManager(presenceManager) {
|
|
1192
|
-
this.#presenceManager = presenceManager;
|
|
1193
|
-
this.#channelSubscriptions?.setPresenceManager(presenceManager);
|
|
1194
|
-
this.#configurePresenceFetcher();
|
|
1195
|
-
}
|
|
1196
|
-
async boot(httpServer, config, channelRouter, logger) {
|
|
1197
|
-
this.#status = "starting";
|
|
1198
|
-
this.#lastError = null;
|
|
1199
|
-
this.#channelSubscriptions = new ChannelSubscriptions(
|
|
1200
|
-
this,
|
|
1201
|
-
channelRouter,
|
|
1202
|
-
logger,
|
|
1203
|
-
this.#presenceManager ?? void 0
|
|
1204
|
-
);
|
|
1205
|
-
const websocketConfig = config.websocket;
|
|
1206
|
-
const heartbeatConfig = resolveHeartbeatConfig(websocketConfig);
|
|
1207
|
-
const inboundMessageConfig = resolveInboundMessageConfig(websocketConfig);
|
|
1208
|
-
this.#outboundMessageConfig = resolveOutboundMessageConfig(websocketConfig);
|
|
1209
|
-
try {
|
|
1210
|
-
if (config.transport) {
|
|
1211
|
-
this.#bus = new SocketBus(config.transport, {
|
|
1212
|
-
channel: (message) => {
|
|
1213
|
-
this.#traceBroadcast(
|
|
1214
|
-
{
|
|
1215
|
-
target: "channel",
|
|
1216
|
-
channel: message.channel,
|
|
1217
|
-
event: message.event,
|
|
1218
|
-
via: "bus",
|
|
1219
|
-
except: message.except
|
|
1220
|
-
},
|
|
1221
|
-
() => this.#emitToChannelLocally(
|
|
1222
|
-
message.channel,
|
|
1223
|
-
message.event,
|
|
1224
|
-
message.data,
|
|
1225
|
-
message.except
|
|
1226
|
-
)
|
|
1227
|
-
);
|
|
1228
|
-
},
|
|
1229
|
-
user: (message) => {
|
|
1230
|
-
this.#traceBroadcast(
|
|
1231
|
-
{ target: "user", room: message.room, event: message.event, via: "bus" },
|
|
1232
|
-
() => this.#emitToUserRoomLocally(message.room, message.event, message.data)
|
|
1233
|
-
);
|
|
1234
|
-
},
|
|
1235
|
-
broadcast: (message) => {
|
|
1236
|
-
this.#traceBroadcast(
|
|
1237
|
-
{ target: "global", event: message.event, via: "bus" },
|
|
1238
|
-
() => this.#broadcastLocally(message.event, message.data)
|
|
1239
|
-
);
|
|
1240
|
-
},
|
|
1241
|
-
presenceSockets: (channel) => {
|
|
1242
|
-
return this.#getLocalPresenceSockets(channel);
|
|
1243
|
-
}
|
|
1244
|
-
});
|
|
1245
|
-
await this.#bus.start();
|
|
1246
|
-
this.#configurePresenceFetcher();
|
|
1247
|
-
}
|
|
1248
|
-
const websocketServer = new WebSocketServer({
|
|
1249
|
-
noServer: true,
|
|
1250
|
-
maxPayload: inboundMessageConfig.maxPayload
|
|
1251
|
-
});
|
|
1252
|
-
this.#server = websocketServer;
|
|
1253
|
-
this.#httpServer = httpServer;
|
|
1254
|
-
const upgrader = new SocketUpgrader(websocketServer, websocketConfig);
|
|
1255
|
-
this.#upgradeHandler = (request, socket, head) => {
|
|
1256
|
-
upgrader.handle(request, socket, head, (connection, upgradeRequest, accepted) => {
|
|
1257
|
-
this.#handleConnection(connection, upgradeRequest, accepted, inboundMessageConfig);
|
|
1258
|
-
}).catch((error) => {
|
|
1259
|
-
logger.warn("failed to upgrade socket connection: %s", error);
|
|
1260
|
-
SocketUpgrader.reject(socket, 500, "Internal Server Error");
|
|
1261
|
-
});
|
|
1262
|
-
};
|
|
1263
|
-
httpServer.on("upgrade", this.#upgradeHandler);
|
|
1264
|
-
this.#startHeartbeat(heartbeatConfig);
|
|
1265
|
-
this.#status = "ready";
|
|
1266
|
-
} catch (error) {
|
|
1267
|
-
this.#lastError = error instanceof Error ? error : new Error("Socket service boot failed");
|
|
1268
|
-
this.#status = "failed";
|
|
1269
|
-
await this.close();
|
|
1270
|
-
throw error;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
#handleConnection(connection, request, upgrade, inboundMessageConfig) {
|
|
1274
|
-
const socket = this.#wrapSocket(connection, request, upgrade);
|
|
1275
|
-
connectChannel.traceSync(
|
|
1276
|
-
() => {
|
|
1277
|
-
this.#sockets.set(socket.id, socket);
|
|
1278
|
-
this.emit("connect", { socket });
|
|
1279
|
-
},
|
|
1280
|
-
{ socketId: socket.id }
|
|
1281
|
-
);
|
|
1282
|
-
connection.on("error", () => {
|
|
1283
|
-
});
|
|
1284
|
-
connection.on("message", (data) => {
|
|
1285
|
-
if (getRawDataByteLength(data) > inboundMessageConfig.maxPayload) {
|
|
1286
|
-
closeConnection(connection, CLOSE_MESSAGE_TOO_BIG, "Message too big");
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
if (!this.#acceptMessageRate(socket.id, inboundMessageConfig)) {
|
|
1290
|
-
closeConnection(connection, CLOSE_POLICY_VIOLATION, "Socket message rate limit exceeded");
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
const message = Message.fromTransport(data.toString());
|
|
1294
|
-
if (message.type === "ping") {
|
|
1295
|
-
sendFrame(socket.raw.connection, { id: message.id, type: "pong" });
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1298
|
-
const enqueued = this.#messageQueue.enqueue(
|
|
1299
|
-
socket.id,
|
|
1300
|
-
async () => {
|
|
1301
|
-
await this.#handleMessage(socket, message).catch((error) => {
|
|
1302
|
-
sendFrame(connection, {
|
|
1303
|
-
type: "error",
|
|
1304
|
-
error: error instanceof Error ? error.message : "Unexpected socket error"
|
|
1305
|
-
});
|
|
1306
|
-
});
|
|
1307
|
-
},
|
|
1308
|
-
{ maxDepth: inboundMessageConfig.maxQueuedMessages }
|
|
1309
|
-
);
|
|
1310
|
-
if (!enqueued) {
|
|
1311
|
-
closeConnection(connection, CLOSE_POLICY_VIOLATION, "Socket message queue limit exceeded");
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1314
|
-
connection.on("pong", () => {
|
|
1315
|
-
this.#clearHeartbeatTimeout(socket.id);
|
|
1316
|
-
});
|
|
1317
|
-
connection.on("close", async () => {
|
|
1318
|
-
const traceMessage = {
|
|
1319
|
-
socketId: socket.id,
|
|
1320
|
-
reason: "close",
|
|
1321
|
-
subscriptions: socket.channels.size
|
|
1322
|
-
};
|
|
1323
|
-
await disconnectChannel.tracePromise(async () => {
|
|
1324
|
-
this.#clearHeartbeatTimeout(socket.id);
|
|
1325
|
-
this.#messageQueue.delete(socket.id);
|
|
1326
|
-
this.#messageRates.delete(socket.id);
|
|
1327
|
-
await this.#channelSubscriptions?.leaveAll(socket);
|
|
1328
|
-
this.#channelSubscriptions?.deleteSocket(socket.id);
|
|
1329
|
-
this.#sockets.delete(socket.id);
|
|
1330
|
-
this.#leaveUserRoom(socket.id);
|
|
1331
|
-
this.emit("disconnect", { socket, reason: "close" });
|
|
1332
|
-
}, traceMessage);
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
async #handleMessage(socket, message) {
|
|
1336
|
-
if (!message.valid) {
|
|
1337
|
-
sendFrame(socket.raw.connection, message.toRejectionFrame());
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
switch (message.type) {
|
|
1341
|
-
case "ping":
|
|
1342
|
-
return;
|
|
1343
|
-
case "subscribe": {
|
|
1344
|
-
const traceMessage = {
|
|
1345
|
-
socketId: socket.id,
|
|
1346
|
-
channel: message.channel
|
|
1347
|
-
};
|
|
1348
|
-
await subscribeChannel.tracePromise(async () => {
|
|
1349
|
-
const result = await this.#channelSubscriptions?.subscribe(socket, message.channel);
|
|
1350
|
-
traceMessage.created = result?.created ?? false;
|
|
1351
|
-
traceMessage.ok = result?.ack.ok ?? false;
|
|
1352
|
-
if (!result?.ack.ok) {
|
|
1353
|
-
traceMessage.error = result?.ack.error ?? "Socket service is not initialized";
|
|
1354
|
-
}
|
|
1355
|
-
if (result?.created) {
|
|
1356
|
-
this.emit("subscribe", { socket, channel: message.channel });
|
|
1357
|
-
}
|
|
1358
|
-
sendFrame(socket.raw.connection, Reply.fromSubscribeResult(message.id, result).toFrame());
|
|
1359
|
-
}, traceMessage);
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
case "unsubscribe": {
|
|
1363
|
-
const traceMessage = {
|
|
1364
|
-
socketId: socket.id,
|
|
1365
|
-
channel: message.channel
|
|
1366
|
-
};
|
|
1367
|
-
await unsubscribeChannel.tracePromise(async () => {
|
|
1368
|
-
traceMessage.removed = socket.channels.has(message.channel);
|
|
1369
|
-
await this.#channelSubscriptions?.leave(socket, message.channel);
|
|
1370
|
-
traceMessage.ok = true;
|
|
1371
|
-
this.emit("unsubscribe", { socket, channel: message.channel });
|
|
1372
|
-
sendFrame(socket.raw.connection, Reply.ok(message.id).toFrame());
|
|
1373
|
-
}, traceMessage);
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
case "message":
|
|
1377
|
-
case "whisper": {
|
|
1378
|
-
await this.#handleChannelFrame(socket, message);
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
async #handleChannelFrame(socket, message) {
|
|
1384
|
-
const payload = message.toChannelMessage();
|
|
1385
|
-
const traceMessage = {
|
|
1386
|
-
socketId: socket.id,
|
|
1387
|
-
channel: payload.channel,
|
|
1388
|
-
event: payload.event
|
|
1389
|
-
};
|
|
1390
|
-
await channelMessageChannel.tracePromise(async () => {
|
|
1391
|
-
const ack = await this.#handleChannelPayload(socket, message.type, payload);
|
|
1392
|
-
traceMessage.ok = ack.ok;
|
|
1393
|
-
if (!ack.ok) {
|
|
1394
|
-
traceMessage.error = ack.error ?? "Handler error";
|
|
1395
|
-
}
|
|
1396
|
-
sendFrame(socket.raw.connection, Reply.fromChannelAck(message.id, ack).toFrame());
|
|
1397
|
-
}, traceMessage);
|
|
1398
|
-
}
|
|
1399
|
-
async #handleChannelPayload(socket, type, payload) {
|
|
1400
|
-
if (typeof payload.channel !== "string" || typeof payload.event !== "string") {
|
|
1401
|
-
return { ok: false, error: `Invalid channel ${type}` };
|
|
1402
|
-
}
|
|
1403
|
-
if (type === "message") {
|
|
1404
|
-
return this.#channelSubscriptions.handleMessage(socket, payload);
|
|
1405
|
-
}
|
|
1406
|
-
return this.#channelSubscriptions.relayWhisper(socket, payload);
|
|
1407
|
-
}
|
|
1408
|
-
#wrapSocket(connection, request, upgrade) {
|
|
1409
|
-
const raw = {
|
|
1410
|
-
id: randomUUID2(),
|
|
1411
|
-
data: {},
|
|
1412
|
-
connection,
|
|
1413
|
-
request,
|
|
1414
|
-
httpContext: upgrade.httpContext
|
|
1415
|
-
};
|
|
1416
|
-
return {
|
|
1417
|
-
id: raw.id,
|
|
1418
|
-
user: upgrade.user,
|
|
1419
|
-
channels: /* @__PURE__ */ new Set(),
|
|
1420
|
-
emit: (event, data) => {
|
|
1421
|
-
this.#sendEventFrame(connection, { type: "event", event, data });
|
|
1422
|
-
},
|
|
1423
|
-
joinUserRoom: (userId) => {
|
|
1424
|
-
this.#joinUserRoom(raw.id, userRoom(userId));
|
|
1425
|
-
return Promise.resolve();
|
|
1426
|
-
},
|
|
1427
|
-
leaveUserRoom: () => {
|
|
1428
|
-
this.#leaveUserRoom(raw.id);
|
|
1429
|
-
return Promise.resolve();
|
|
1430
|
-
},
|
|
1431
|
-
disconnect() {
|
|
1432
|
-
connection.close();
|
|
1433
|
-
},
|
|
1434
|
-
raw
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
#joinUserRoom(socketId, room) {
|
|
1438
|
-
this.#leaveUserRoom(socketId);
|
|
1439
|
-
if (!this.#userRooms.has(room)) {
|
|
1440
|
-
this.#userRooms.set(room, /* @__PURE__ */ new Set());
|
|
1441
|
-
}
|
|
1442
|
-
this.#userRooms.get(room).add(socketId);
|
|
1443
|
-
this.#socketUserRoom.set(socketId, room);
|
|
1444
|
-
}
|
|
1445
|
-
#leaveUserRoom(socketId) {
|
|
1446
|
-
const room = this.#socketUserRoom.get(socketId);
|
|
1447
|
-
if (!room) {
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
const sockets = this.#userRooms.get(room);
|
|
1451
|
-
sockets?.delete(socketId);
|
|
1452
|
-
if (sockets?.size === 0) {
|
|
1453
|
-
this.#userRooms.delete(room);
|
|
1454
|
-
}
|
|
1455
|
-
this.#socketUserRoom.delete(socketId);
|
|
1456
|
-
}
|
|
1457
|
-
to(channel) {
|
|
1458
|
-
return {
|
|
1459
|
-
emit: (event, data) => {
|
|
1460
|
-
this.#broadcastSink.dispatch({ target: "channel", channel, event, data });
|
|
1461
|
-
},
|
|
1462
|
-
except: (socketId) => ({
|
|
1463
|
-
emit: (event, data) => {
|
|
1464
|
-
this.#broadcastSink.dispatch({
|
|
1465
|
-
target: "channel",
|
|
1466
|
-
channel,
|
|
1467
|
-
event,
|
|
1468
|
-
data,
|
|
1469
|
-
except: [socketId]
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
})
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
toUser(userId) {
|
|
1476
|
-
const room = userRoom(userId);
|
|
1477
|
-
return {
|
|
1478
|
-
emit: (event, data) => {
|
|
1479
|
-
this.#broadcastSink.dispatch({ target: "user", userId, room, event, data });
|
|
1480
|
-
}
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
broadcast(event, data) {
|
|
1484
|
-
this.#broadcastSink.dispatch({ target: "global", event, data });
|
|
1485
|
-
}
|
|
1486
|
-
#deliverBroadcast(emission) {
|
|
1487
|
-
switch (emission.target) {
|
|
1488
|
-
case "channel": {
|
|
1489
|
-
const channel = emission.channel;
|
|
1490
|
-
this.#bus?.publishChannel(channel, emission.event, emission.data, emission.except);
|
|
1491
|
-
const traceMessage = {
|
|
1492
|
-
target: "channel",
|
|
1493
|
-
channel,
|
|
1494
|
-
event: emission.event,
|
|
1495
|
-
via: "local"
|
|
1496
|
-
};
|
|
1497
|
-
if (emission.except) {
|
|
1498
|
-
traceMessage.except = emission.except;
|
|
1499
|
-
}
|
|
1500
|
-
this.#traceBroadcast(
|
|
1501
|
-
traceMessage,
|
|
1502
|
-
() => this.#emitToChannelLocally(channel, emission.event, emission.data, emission.except)
|
|
1503
|
-
);
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
case "user": {
|
|
1507
|
-
const room = emission.room;
|
|
1508
|
-
this.#bus?.publishUser(room, emission.event, emission.data);
|
|
1509
|
-
this.#traceBroadcast(
|
|
1510
|
-
{ target: "user", room, event: emission.event, via: "local" },
|
|
1511
|
-
() => this.#emitToUserRoomLocally(room, emission.event, emission.data)
|
|
1512
|
-
);
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
case "global":
|
|
1516
|
-
this.#bus?.publishBroadcast(emission.event, emission.data);
|
|
1517
|
-
this.#traceBroadcast(
|
|
1518
|
-
{ target: "global", event: emission.event, via: "local" },
|
|
1519
|
-
() => this.#broadcastLocally(emission.event, emission.data)
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
#traceBroadcast(message, deliver) {
|
|
1524
|
-
broadcastChannel.traceSync(() => {
|
|
1525
|
-
message.delivered = deliver();
|
|
1526
|
-
}, message);
|
|
1527
|
-
}
|
|
1528
|
-
#sendEventFrame(connection, frame) {
|
|
1529
|
-
return this.#sendSerializedEventFrame(connection, serializeFrame(frame));
|
|
1530
|
-
}
|
|
1531
|
-
#sendSerializedEventFrame(connection, serializedFrame) {
|
|
1532
|
-
return sendSerializedFrameWithBackpressure(
|
|
1533
|
-
connection,
|
|
1534
|
-
serializedFrame,
|
|
1535
|
-
this.#outboundMessageConfig
|
|
1536
|
-
);
|
|
1537
|
-
}
|
|
1538
|
-
#emitToChannelLocally(channel, event, data, except = []) {
|
|
1539
|
-
let delivered = 0;
|
|
1540
|
-
const serializedFrame = serializeFrame({ type: "event", channel, event, data });
|
|
1541
|
-
for (const socketId of this.#channelSubscriptions?.getSocketIds(channel) ?? []) {
|
|
1542
|
-
if (except.includes(socketId)) {
|
|
1543
|
-
continue;
|
|
1544
|
-
}
|
|
1545
|
-
const socket = this.#sockets.get(socketId);
|
|
1546
|
-
if (socket && this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
|
|
1547
|
-
delivered += 1;
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
return delivered;
|
|
1551
|
-
}
|
|
1552
|
-
#emitToUserRoomLocally(room, event, data) {
|
|
1553
|
-
let delivered = 0;
|
|
1554
|
-
const serializedFrame = serializeFrame({ type: "event", event, data });
|
|
1555
|
-
for (const socketId of this.#userRooms.get(room) ?? []) {
|
|
1556
|
-
const socket = this.#sockets.get(socketId);
|
|
1557
|
-
if (socket && this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
|
|
1558
|
-
delivered += 1;
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
return delivered;
|
|
1562
|
-
}
|
|
1563
|
-
#broadcastLocally(event, data) {
|
|
1564
|
-
let delivered = 0;
|
|
1565
|
-
const serializedFrame = serializeFrame({ type: "event", event, data });
|
|
1566
|
-
for (const socket of this.#sockets.values()) {
|
|
1567
|
-
if (this.#sendSerializedEventFrame(socket.raw.connection, serializedFrame)) {
|
|
1568
|
-
delivered += 1;
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
return delivered;
|
|
1572
|
-
}
|
|
1573
|
-
#configurePresenceFetcher() {
|
|
1574
|
-
if (!this.#presenceManager || !this.#bus) {
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
this.#presenceManager.setSocketFetcher(async (channel) => {
|
|
1578
|
-
return [
|
|
1579
|
-
...this.#presenceManager.getLocalSockets(channel),
|
|
1580
|
-
...await this.#bus.fetchPresenceSockets(channel)
|
|
1581
|
-
];
|
|
1582
|
-
});
|
|
1583
|
-
}
|
|
1584
|
-
#getLocalPresenceSockets(channel) {
|
|
1585
|
-
return (this.#presenceManager?.getLocalSockets(channel) ?? []).map((socket) => {
|
|
1586
|
-
return this.#serializePresenceSocket(channel, socket);
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
#serializePresenceSocket(channel, socket) {
|
|
1590
|
-
const user = socket.data[PRESENCE_DATA_KEY][channel];
|
|
1591
|
-
return {
|
|
1592
|
-
id: socket.id,
|
|
1593
|
-
data: {
|
|
1594
|
-
[PRESENCE_DATA_KEY]: {
|
|
1595
|
-
[channel]: {
|
|
1596
|
-
...user,
|
|
1597
|
-
joinedAt: user.joinedAt.toISOString()
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
getSocket(socketId) {
|
|
1604
|
-
return this.#sockets.get(socketId);
|
|
1605
|
-
}
|
|
1606
|
-
get connectionsCount() {
|
|
1607
|
-
return this.#sockets.size;
|
|
1608
|
-
}
|
|
1609
|
-
get status() {
|
|
1610
|
-
return this.#status;
|
|
1611
|
-
}
|
|
1612
|
-
get ready() {
|
|
1613
|
-
return this.#status === "ready" && Boolean(this.#server);
|
|
1614
|
-
}
|
|
1615
|
-
fake() {
|
|
1616
|
-
const fake = new SocketFake(() => {
|
|
1617
|
-
if (this.#fake === fake) {
|
|
1618
|
-
this.restore();
|
|
1619
|
-
}
|
|
1620
|
-
});
|
|
1621
|
-
this.#fake = fake;
|
|
1622
|
-
this.#broadcastSink = fake;
|
|
1623
|
-
return fake;
|
|
1624
|
-
}
|
|
1625
|
-
restore() {
|
|
1626
|
-
this.#fake = null;
|
|
1627
|
-
this.#broadcastSink = this.#deliverySink;
|
|
1628
|
-
}
|
|
1629
|
-
markFailed(error) {
|
|
1630
|
-
this.#lastError = error instanceof Error ? error : new Error("Socket service failed");
|
|
1631
|
-
this.#status = "failed";
|
|
1632
|
-
}
|
|
1633
|
-
health() {
|
|
1634
|
-
return {
|
|
1635
|
-
status: this.#status,
|
|
1636
|
-
ready: this.ready,
|
|
1637
|
-
connections: this.#sockets.size,
|
|
1638
|
-
channels: this.#channelSubscriptions?.channelsCount ?? 0,
|
|
1639
|
-
...this.#lastError ? { lastError: this.#lastError.message } : {}
|
|
1640
|
-
};
|
|
1641
|
-
}
|
|
1642
|
-
#startHeartbeat(config) {
|
|
1643
|
-
this.#stopHeartbeat();
|
|
1644
|
-
if (!config) {
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
this.#heartbeatInterval = setInterval(() => {
|
|
1648
|
-
this.#pingSockets(config.timeout);
|
|
1649
|
-
}, config.interval);
|
|
1650
|
-
}
|
|
1651
|
-
#pingSockets(timeout) {
|
|
1652
|
-
for (const socket of this.#sockets.values()) {
|
|
1653
|
-
const { connection } = socket.raw;
|
|
1654
|
-
if (connection.readyState !== WebSocket.OPEN) {
|
|
1655
|
-
this.#clearHeartbeatTimeout(socket.id);
|
|
1656
|
-
continue;
|
|
1657
|
-
}
|
|
1658
|
-
if (this.#heartbeatTimeouts.has(socket.id)) {
|
|
1659
|
-
continue;
|
|
1660
|
-
}
|
|
1661
|
-
const timeoutId = setTimeout(() => {
|
|
1662
|
-
this.#heartbeatTimeouts.delete(socket.id);
|
|
1663
|
-
connection.terminate();
|
|
1664
|
-
}, timeout);
|
|
1665
|
-
this.#heartbeatTimeouts.set(socket.id, timeoutId);
|
|
1666
|
-
try {
|
|
1667
|
-
connection.ping();
|
|
1668
|
-
} catch {
|
|
1669
|
-
this.#clearHeartbeatTimeout(socket.id);
|
|
1670
|
-
connection.terminate();
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
#clearHeartbeatTimeout(socketId) {
|
|
1675
|
-
const timeout = this.#heartbeatTimeouts.get(socketId);
|
|
1676
|
-
if (!timeout) {
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
clearTimeout(timeout);
|
|
1680
|
-
this.#heartbeatTimeouts.delete(socketId);
|
|
1681
|
-
}
|
|
1682
|
-
#stopHeartbeat() {
|
|
1683
|
-
if (this.#heartbeatInterval) {
|
|
1684
|
-
clearInterval(this.#heartbeatInterval);
|
|
1685
|
-
this.#heartbeatInterval = null;
|
|
1686
|
-
}
|
|
1687
|
-
for (const timeout of this.#heartbeatTimeouts.values()) {
|
|
1688
|
-
clearTimeout(timeout);
|
|
1689
|
-
}
|
|
1690
|
-
this.#heartbeatTimeouts.clear();
|
|
1691
|
-
}
|
|
1692
|
-
#acceptMessageRate(socketId, config) {
|
|
1693
|
-
const now = Date.now();
|
|
1694
|
-
const rate = this.#messageRates.get(socketId);
|
|
1695
|
-
if (!rate || now - rate.intervalStartedAt >= config.messageRateInterval) {
|
|
1696
|
-
this.#messageRates.set(socketId, { count: 1, intervalStartedAt: now });
|
|
1697
|
-
return true;
|
|
1698
|
-
}
|
|
1699
|
-
if (rate.count >= config.maxMessagesPerInterval) {
|
|
1700
|
-
return false;
|
|
1701
|
-
}
|
|
1702
|
-
rate.count += 1;
|
|
1703
|
-
return true;
|
|
1704
|
-
}
|
|
1705
|
-
#cleanup() {
|
|
1706
|
-
this.#stopHeartbeat();
|
|
1707
|
-
for (const socket of this.#sockets.values()) {
|
|
1708
|
-
socket.raw.connection.terminate();
|
|
1709
|
-
}
|
|
1710
|
-
this.#sockets.clear();
|
|
1711
|
-
this.#userRooms.clear();
|
|
1712
|
-
this.#socketUserRoom.clear();
|
|
1713
|
-
this.#messageQueue.clear();
|
|
1714
|
-
this.#messageRates.clear();
|
|
1715
|
-
this.#channelSubscriptions?.clear();
|
|
1716
|
-
}
|
|
1717
|
-
async close() {
|
|
1718
|
-
const server = this.#server;
|
|
1719
|
-
const bus = this.#bus;
|
|
1720
|
-
const httpServer = this.#httpServer;
|
|
1721
|
-
const upgradeHandler = this.#upgradeHandler;
|
|
1722
|
-
if (this.#status !== "failed") {
|
|
1723
|
-
this.#status = "stopping";
|
|
1724
|
-
}
|
|
1725
|
-
this.#server = null;
|
|
1726
|
-
this.#bus = null;
|
|
1727
|
-
this.#httpServer = null;
|
|
1728
|
-
this.#upgradeHandler = null;
|
|
1729
|
-
if (httpServer && upgradeHandler) {
|
|
1730
|
-
httpServer.off("upgrade", upgradeHandler);
|
|
1731
|
-
}
|
|
1732
|
-
this.#cleanup();
|
|
1733
|
-
try {
|
|
1734
|
-
await bus?.close();
|
|
1735
|
-
if (!server) {
|
|
1736
|
-
if (this.#status !== "failed") {
|
|
1737
|
-
this.#status = "stopped";
|
|
1738
|
-
}
|
|
1739
|
-
return;
|
|
1740
|
-
}
|
|
1741
|
-
await new Promise((resolve) => {
|
|
1742
|
-
server.close(() => {
|
|
1743
|
-
resolve();
|
|
1744
|
-
});
|
|
1745
|
-
});
|
|
1746
|
-
if (this.#status !== "failed") {
|
|
1747
|
-
this.#status = "stopped";
|
|
1748
|
-
}
|
|
1749
|
-
} catch (error) {
|
|
1750
|
-
this.markFailed(error);
|
|
1751
|
-
throw error;
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
};
|
|
1755
|
-
|
|
1756
|
-
// src/channel_pattern.ts
|
|
1757
|
-
import { exec, match, parse } from "@poppinss/matchit";
|
|
1758
|
-
var ChannelPattern = class _ChannelPattern {
|
|
1759
|
-
constructor(value) {
|
|
1760
|
-
this.value = value;
|
|
1761
|
-
this.route = parse(value);
|
|
1762
|
-
this.score = this.route.map((segment) => {
|
|
1763
|
-
switch (segment.type) {
|
|
1764
|
-
case 0:
|
|
1765
|
-
return 40;
|
|
1766
|
-
case 1:
|
|
1767
|
-
return 30;
|
|
1768
|
-
case 3:
|
|
1769
|
-
return 20;
|
|
1770
|
-
case 2:
|
|
1771
|
-
return 0;
|
|
1772
|
-
}
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
route;
|
|
1776
|
-
score;
|
|
1777
|
-
static from(value) {
|
|
1778
|
-
return new _ChannelPattern(value);
|
|
1779
|
-
}
|
|
1780
|
-
match(channelName) {
|
|
1781
|
-
const matched = match(channelName, [this.route]);
|
|
1782
|
-
if (matched.length === 0) {
|
|
1783
|
-
return null;
|
|
1784
|
-
}
|
|
1785
|
-
return exec(channelName, matched, true);
|
|
1786
|
-
}
|
|
1787
|
-
/**
|
|
1788
|
-
* Returns the most specific matching candidate. Static segments outrank params,
|
|
1789
|
-
* params outrank optional params, and optional params outrank wildcards.
|
|
1790
|
-
* Insertion order is preserved for ties.
|
|
1791
|
-
*/
|
|
1792
|
-
static firstMatch(channelName, candidates) {
|
|
1793
|
-
const sorted = [...candidates].sort((left, right) => {
|
|
1794
|
-
return _ChannelPattern.#compareSpecificity(left.pattern, right.pattern);
|
|
1795
|
-
});
|
|
1796
|
-
const routes = sorted.map((candidate) => candidate.pattern.route);
|
|
1797
|
-
const matched = match(channelName, routes);
|
|
1798
|
-
if (matched.length === 0) {
|
|
1799
|
-
return null;
|
|
1800
|
-
}
|
|
1801
|
-
for (const { pattern, channel } of sorted) {
|
|
1802
|
-
if (pattern.route !== matched) {
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1805
|
-
const params = pattern.match(channelName);
|
|
1806
|
-
if (params) {
|
|
1807
|
-
return { pattern, channel, params };
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
return null;
|
|
1811
|
-
}
|
|
1812
|
-
static #compareSpecificity(left, right) {
|
|
1813
|
-
const length = Math.max(left.score.length, right.score.length);
|
|
1814
|
-
for (let index = 0; index < length; index++) {
|
|
1815
|
-
const scoreDifference = (right.score[index] ?? -1) - (left.score[index] ?? -1);
|
|
1816
|
-
if (scoreDifference !== 0) {
|
|
1817
|
-
return scoreDifference;
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
return right.score.length - left.score.length;
|
|
1821
|
-
}
|
|
1822
|
-
};
|
|
1823
|
-
|
|
1824
|
-
// src/channel_router.ts
|
|
1825
|
-
var ChannelRouter = class {
|
|
1826
|
-
/**
|
|
1827
|
-
* Registered channels keyed by their pattern.
|
|
1828
|
-
*/
|
|
1829
|
-
#channels = /* @__PURE__ */ new Map();
|
|
1830
|
-
/**
|
|
1831
|
-
* Registers a channel.
|
|
1832
|
-
*/
|
|
1833
|
-
register(channel) {
|
|
1834
|
-
const patternValue = channel.pattern;
|
|
1835
|
-
if (!patternValue) {
|
|
1836
|
-
throw new Error(`Channel ${channel.name} must define a static pattern`);
|
|
1837
|
-
}
|
|
1838
|
-
if (this.#channels.has(patternValue)) {
|
|
1839
|
-
throw new Error(`Channel pattern "${patternValue}" is already registered`);
|
|
1840
|
-
}
|
|
1841
|
-
const pattern = ChannelPattern.from(patternValue);
|
|
1842
|
-
this.#channels.set(pattern.value, { pattern, channel });
|
|
1843
|
-
}
|
|
1844
|
-
/**
|
|
1845
|
-
* Matches a channel name against registered patterns.
|
|
1846
|
-
*/
|
|
1847
|
-
match(channelName) {
|
|
1848
|
-
const matched = ChannelPattern.firstMatch(channelName, this.#channels.values());
|
|
1849
|
-
if (!matched) {
|
|
1850
|
-
return null;
|
|
1851
|
-
}
|
|
1852
|
-
return {
|
|
1853
|
-
channel: matched.channel,
|
|
1854
|
-
pattern: matched.pattern.value,
|
|
1855
|
-
params: matched.params
|
|
1856
|
-
};
|
|
1857
|
-
}
|
|
1858
|
-
/**
|
|
1859
|
-
* Authorizes a socket before it joins a channel.
|
|
1860
|
-
*/
|
|
1861
|
-
async authorize(socket, channelName) {
|
|
1862
|
-
const matched = this.match(channelName);
|
|
1863
|
-
if (!matched) {
|
|
1864
|
-
return {
|
|
1865
|
-
success: false,
|
|
1866
|
-
error: "Channel not found"
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
const instance = new matched.channel();
|
|
1870
|
-
const paramValues = Object.values(matched.params);
|
|
1871
|
-
const ctx = {
|
|
1872
|
-
socket,
|
|
1873
|
-
channel: channelName,
|
|
1874
|
-
params: matched.params,
|
|
1875
|
-
presenceData: void 0,
|
|
1876
|
-
setPresenceData(data) {
|
|
1877
|
-
ctx.presenceData = data;
|
|
1878
|
-
}
|
|
1879
|
-
};
|
|
1880
|
-
try {
|
|
1881
|
-
await instance.$runMiddlewares(ctx);
|
|
1882
|
-
} catch (error) {
|
|
1883
|
-
return {
|
|
1884
|
-
success: false,
|
|
1885
|
-
error: error instanceof Error ? error.message : "Middleware rejected"
|
|
1886
|
-
};
|
|
1887
|
-
}
|
|
1888
|
-
return {
|
|
1889
|
-
success: true,
|
|
1890
|
-
instance,
|
|
1891
|
-
params: matched.params,
|
|
1892
|
-
paramValues,
|
|
1893
|
-
presenceData: ctx.presenceData ?? null
|
|
1894
|
-
};
|
|
1895
|
-
}
|
|
1896
|
-
/**
|
|
1897
|
-
* Lists all registered channels.
|
|
1898
|
-
*/
|
|
1899
|
-
list() {
|
|
1900
|
-
return [...this.#channels.values()].map(({ pattern, channel }) => ({
|
|
1901
|
-
pattern: pattern.value,
|
|
1902
|
-
name: channel.name,
|
|
1903
|
-
options: channel.options
|
|
1904
|
-
}));
|
|
1905
|
-
}
|
|
1906
|
-
/**
|
|
1907
|
-
* Number of registered channels.
|
|
1908
|
-
*/
|
|
1909
|
-
get size() {
|
|
1910
|
-
return this.#channels.size;
|
|
1911
|
-
}
|
|
1912
|
-
};
|
|
1913
|
-
|
|
1914
|
-
export {
|
|
1915
|
-
PresenceManager,
|
|
1916
|
-
BaseChannel,
|
|
1917
|
-
SocketFake,
|
|
1918
|
-
SocketService,
|
|
1919
|
-
ChannelRouter
|
|
1920
|
-
};
|
|
1921
|
-
//# sourceMappingURL=chunk-ILDK672E.js.map
|