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