@rlanz/socket 0.0.1-9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +556 -556
- package/build/{base_channel-DPOnqned.d.ts → base_channel-CasBQV5I.d.ts} +7 -25
- package/build/{chunk-6PJ4ALJD.js → chunk-DM3UQMWW.js} +5 -3
- package/build/chunk-DM3UQMWW.js.map +1 -0
- package/build/{chunk-QNOOQXYY.js → chunk-DR6GWQUW.js} +242 -199
- package/build/chunk-DR6GWQUW.js.map +1 -0
- package/build/chunk-NUPBNOP3.js +61 -0
- package/build/chunk-NUPBNOP3.js.map +1 -0
- package/build/{chunk-YVC7IMDN.js → chunk-UE75J3JE.js} +2 -2
- package/build/chunk-YSW4MNFR.js +7 -0
- package/build/chunk-YSW4MNFR.js.map +1 -0
- package/build/{framework-B8pJ4cCA.d.ts → framework-Cb71JA5f.d.ts} +1 -1
- package/build/{index-DjBYUhFR.d.ts → index-DCPtfkJL.d.ts} +1 -1
- package/build/index.d.ts +3 -2
- package/build/providers/socket_provider.d.ts +3 -2
- package/build/providers/socket_provider.js +312 -130
- package/build/providers/socket_provider.js.map +1 -1
- package/build/services/socket.d.ts +3 -2
- package/build/shared_types-B11CaOr5.d.ts +26 -0
- package/build/{socket_service-BcC9ASCE.d.ts → socket_service-C2YRbhrO.d.ts} +1 -1
- package/build/src/assembler_hook.js +54 -30
- package/build/src/assembler_hook.js.map +1 -1
- package/build/src/client/index.d.ts +2 -7
- package/build/src/client/index.js +3 -2
- package/build/src/client/react.d.ts +3 -8
- package/build/src/client/react.js +4 -3
- package/build/src/client/react.js.map +1 -1
- package/build/src/client/types.d.ts +9 -13
- package/build/src/client/vue.d.ts +3 -8
- package/build/src/client/vue.js +4 -3
- package/build/src/client/vue.js.map +1 -1
- package/build/src/decorators.d.ts +2 -1
- package/build/src/health_check.d.ts +3 -2
- package/build/src/otel.d.ts +1 -0
- package/build/src/otel.js +211 -27
- package/build/src/otel.js.map +1 -1
- package/build/src/types.d.ts +2 -1
- package/package.json +31 -7
- package/build/chunk-6PJ4ALJD.js.map +0 -1
- package/build/chunk-D3HUBCBW.js +0 -18
- package/build/chunk-D3HUBCBW.js.map +0 -1
- package/build/chunk-QNOOQXYY.js.map +0 -1
- /package/build/{chunk-YVC7IMDN.js.map → chunk-UE75J3JE.js.map} +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SERVER_DISCONNECT_CODE
|
|
3
|
+
} from "../chunk-YSW4MNFR.js";
|
|
1
4
|
import {
|
|
2
5
|
BaseChannel,
|
|
3
6
|
SocketResponseError
|
|
@@ -13,7 +16,7 @@ import {
|
|
|
13
16
|
disconnectChannel,
|
|
14
17
|
subscribeChannel,
|
|
15
18
|
unsubscribeChannel
|
|
16
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-NUPBNOP3.js";
|
|
17
20
|
import {
|
|
18
21
|
SocketFake
|
|
19
22
|
} from "../chunk-IUQ3GYZZ.js";
|
|
@@ -138,7 +141,7 @@ var ChannelSubscriptions = class _ChannelSubscriptions {
|
|
|
138
141
|
};
|
|
139
142
|
}
|
|
140
143
|
if (this.#usesPresence(result.instance)) {
|
|
141
|
-
return this.#withChannelOperation(channelName
|
|
144
|
+
return this.#withChannelOperation(`${socket.id}\0${channelName}`, async () => {
|
|
142
145
|
const concurrentSubscription = await this.#refreshExistingSubscription(socket, channelName);
|
|
143
146
|
if (concurrentSubscription) {
|
|
144
147
|
return concurrentSubscription;
|
|
@@ -162,17 +165,13 @@ var ChannelSubscriptions = class _ChannelSubscriptions {
|
|
|
162
165
|
try {
|
|
163
166
|
if (this.#usesPresence(result.instance)) {
|
|
164
167
|
const presenceInfo = result.instance.$getPresenceInfo(socket);
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
);
|
|
168
|
-
const { joinedAt: _joinedAt, ...member } = this.presenceManager.join(
|
|
169
|
-
channelName,
|
|
170
|
-
socket.raw,
|
|
171
|
-
presenceInfo
|
|
172
|
-
);
|
|
168
|
+
const joinedUser = this.presenceManager.join(channelName, socket.raw, presenceInfo);
|
|
169
|
+
const { joinedAt: _joinedAt, ...member } = joinedUser;
|
|
173
170
|
presenceJoined = true;
|
|
174
171
|
presenceSnapshot = await this.presenceManager.snapshot(channelName);
|
|
175
172
|
presenceData = presenceSnapshot;
|
|
173
|
+
const visibleUser = presenceSnapshot.users.find((user) => user.id === presenceInfo.id);
|
|
174
|
+
const wasPresent = Boolean(visibleUser && visibleUser !== joinedUser);
|
|
176
175
|
try {
|
|
177
176
|
JSON.stringify(presenceData);
|
|
178
177
|
} catch (error) {
|
|
@@ -238,7 +237,7 @@ var ChannelSubscriptions = class _ChannelSubscriptions {
|
|
|
238
237
|
this.#storage.delete(socket.id, channelName);
|
|
239
238
|
if (this.#usesPresence(subscription.instance)) {
|
|
240
239
|
await this.#withChannelOperation(
|
|
241
|
-
channelName
|
|
240
|
+
`${socket.id}\0${channelName}`,
|
|
242
241
|
() => this.#cleanupAfterLeave(socket, channelName, subscription)
|
|
243
242
|
);
|
|
244
243
|
} else {
|
|
@@ -328,18 +327,18 @@ var ChannelSubscriptions = class _ChannelSubscriptions {
|
|
|
328
327
|
} catch {
|
|
329
328
|
}
|
|
330
329
|
}
|
|
331
|
-
async #withChannelOperation(
|
|
332
|
-
const previous = this.#channelOperations.get(
|
|
330
|
+
async #withChannelOperation(operationKey, operation) {
|
|
331
|
+
const previous = this.#channelOperations.get(operationKey) ?? Promise.resolve();
|
|
333
332
|
const release = Promise.withResolvers();
|
|
334
333
|
const queued = previous.then(() => release.promise);
|
|
335
|
-
this.#channelOperations.set(
|
|
334
|
+
this.#channelOperations.set(operationKey, queued);
|
|
336
335
|
await previous;
|
|
337
336
|
try {
|
|
338
337
|
return await operation();
|
|
339
338
|
} finally {
|
|
340
339
|
release.resolve();
|
|
341
|
-
if (this.#channelOperations.get(
|
|
342
|
-
this.#channelOperations.delete(
|
|
340
|
+
if (this.#channelOperations.get(operationKey) === queued) {
|
|
341
|
+
this.#channelOperations.delete(operationKey);
|
|
343
342
|
}
|
|
344
343
|
}
|
|
345
344
|
}
|
|
@@ -393,17 +392,17 @@ var ChannelSubscriptions = class _ChannelSubscriptions {
|
|
|
393
392
|
// src/protocol/reply.ts
|
|
394
393
|
var INVALID_SOCKET_MESSAGE = "Invalid socket message";
|
|
395
394
|
var Reply = class _Reply {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
this
|
|
400
|
-
this
|
|
395
|
+
#id;
|
|
396
|
+
#ack;
|
|
397
|
+
constructor(id, ack) {
|
|
398
|
+
this.#id = id;
|
|
399
|
+
this.#ack = ack;
|
|
401
400
|
}
|
|
402
401
|
static ok(id, data) {
|
|
403
|
-
return new _Reply(id, true, data
|
|
402
|
+
return new _Reply(id, { ok: true, data });
|
|
404
403
|
}
|
|
405
404
|
static error(id, error) {
|
|
406
|
-
return new _Reply(id,
|
|
405
|
+
return new _Reply(id, { ok: false, error });
|
|
407
406
|
}
|
|
408
407
|
static invalidMessage(id) {
|
|
409
408
|
if (id === void 0) {
|
|
@@ -416,7 +415,7 @@ var Reply = class _Reply {
|
|
|
416
415
|
return _Reply.error(id, "Socket service is not initialized");
|
|
417
416
|
}
|
|
418
417
|
if (!result.ack.ok) {
|
|
419
|
-
return _Reply.error(id, result.ack.error
|
|
418
|
+
return _Reply.error(id, result.ack.error);
|
|
420
419
|
}
|
|
421
420
|
const data = result.ack.presenceData ? {
|
|
422
421
|
presenceData: result.ack.presenceData
|
|
@@ -424,21 +423,18 @@ var Reply = class _Reply {
|
|
|
424
423
|
return _Reply.ok(id, data);
|
|
425
424
|
}
|
|
426
425
|
static fromChannelAck(id, ack) {
|
|
427
|
-
return ack.ok ? _Reply.ok(id, ack.data) : _Reply.error(id, ack.error
|
|
426
|
+
return ack.ok ? _Reply.ok(id, ack.data) : _Reply.error(id, ack.error);
|
|
428
427
|
}
|
|
429
428
|
toFrame() {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
if (this.error !== void 0) {
|
|
439
|
-
frame.error = this.error;
|
|
429
|
+
if (!this.#ack.ok) {
|
|
430
|
+
return {
|
|
431
|
+
id: this.#id,
|
|
432
|
+
type: "ack",
|
|
433
|
+
ok: false,
|
|
434
|
+
error: this.#ack.error
|
|
435
|
+
};
|
|
440
436
|
}
|
|
441
|
-
return
|
|
437
|
+
return this.#ack.data === void 0 ? { id: this.#id, type: "ack", ok: true } : { id: this.#id, type: "ack", ok: true, data: this.#ack.data };
|
|
442
438
|
}
|
|
443
439
|
};
|
|
444
440
|
|
|
@@ -1126,10 +1122,34 @@ var DEFAULT_MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024;
|
|
|
1126
1122
|
var DEFAULT_MAX_OUTBOUND_PAYLOAD = 1024 * 1024;
|
|
1127
1123
|
var DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET = 100;
|
|
1128
1124
|
var DEFAULT_MAX_CHANNEL_NAME_LENGTH = 255;
|
|
1125
|
+
var DEFAULT_SHUTDOWN_TIMEOUT = 5e3;
|
|
1129
1126
|
var CLOSE_POLICY_VIOLATION = 1008;
|
|
1130
1127
|
var CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
1131
1128
|
var CLOSE_OUTBOUND_BUFFER_LIMIT = "Socket outbound buffer limit exceeded";
|
|
1132
1129
|
var CLOSE_OUTBOUND_MESSAGE_TOO_BIG = "Socket outbound message too big";
|
|
1130
|
+
var SERVER_DISCONNECT_REASON = "Socket server disconnected";
|
|
1131
|
+
async function settleBeforeDeadline(promise, deadline) {
|
|
1132
|
+
const remaining = deadline - Date.now();
|
|
1133
|
+
if (remaining <= 0) {
|
|
1134
|
+
void promise.catch(() => {
|
|
1135
|
+
});
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
let timer;
|
|
1139
|
+
try {
|
|
1140
|
+
return await Promise.race([
|
|
1141
|
+
promise.then(
|
|
1142
|
+
(value) => ({ status: "fulfilled", value }),
|
|
1143
|
+
(reason) => ({ status: "rejected", reason })
|
|
1144
|
+
),
|
|
1145
|
+
new Promise((resolve) => {
|
|
1146
|
+
timer = setTimeout(() => resolve(null), remaining);
|
|
1147
|
+
})
|
|
1148
|
+
]);
|
|
1149
|
+
} finally {
|
|
1150
|
+
if (timer) clearTimeout(timer);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1133
1153
|
function serializeFrame(frame) {
|
|
1134
1154
|
return JSON.stringify(frame);
|
|
1135
1155
|
}
|
|
@@ -1232,13 +1252,8 @@ function closeConnection(connection, code, reason) {
|
|
|
1232
1252
|
}
|
|
1233
1253
|
}
|
|
1234
1254
|
var SocketService = class extends Emittery {
|
|
1235
|
-
#server = null;
|
|
1236
|
-
#httpServer = null;
|
|
1237
|
-
#upgradeHandler = null;
|
|
1238
1255
|
#sockets = /* @__PURE__ */ new Map();
|
|
1239
|
-
#channelSubscriptions = null;
|
|
1240
1256
|
#presenceManager = null;
|
|
1241
|
-
#bus = null;
|
|
1242
1257
|
#fake = null;
|
|
1243
1258
|
#deliverySink = {
|
|
1244
1259
|
dispatch: (emission) => {
|
|
@@ -1246,49 +1261,113 @@ var SocketService = class extends Emittery {
|
|
|
1246
1261
|
}
|
|
1247
1262
|
};
|
|
1248
1263
|
#broadcastSink = this.#deliverySink;
|
|
1249
|
-
#
|
|
1250
|
-
#lastError = null;
|
|
1264
|
+
#lifecycle = { status: "stopped" };
|
|
1251
1265
|
#heartbeatInterval = null;
|
|
1252
1266
|
#heartbeatTimeouts = /* @__PURE__ */ new Map();
|
|
1253
1267
|
#messageQueue = new MessageQueue();
|
|
1254
1268
|
#messageRates = /* @__PURE__ */ new Map();
|
|
1255
1269
|
#socketFinalizations = /* @__PURE__ */ new Map();
|
|
1256
|
-
#closePromise = null;
|
|
1257
1270
|
#outboundMessageConfig = resolveOutboundMessageConfig(void 0);
|
|
1271
|
+
#shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT;
|
|
1258
1272
|
#logger = null;
|
|
1259
1273
|
/**
|
|
1260
1274
|
* Accesses the underlying ws server.
|
|
1261
1275
|
*/
|
|
1262
1276
|
get server() {
|
|
1263
|
-
|
|
1277
|
+
const runtime = this.#runtime;
|
|
1278
|
+
if (!runtime) {
|
|
1264
1279
|
throw new Error("WebSocket server is not initialized. Call boot() first.");
|
|
1265
1280
|
}
|
|
1266
|
-
return
|
|
1281
|
+
return runtime.server;
|
|
1282
|
+
}
|
|
1283
|
+
get #resources() {
|
|
1284
|
+
switch (this.#lifecycle.status) {
|
|
1285
|
+
case "starting":
|
|
1286
|
+
return this.#lifecycle.operation;
|
|
1287
|
+
case "ready":
|
|
1288
|
+
return this.#lifecycle.runtime;
|
|
1289
|
+
case "stopping":
|
|
1290
|
+
case "failed":
|
|
1291
|
+
return this.#lifecycle.resources;
|
|
1292
|
+
case "stopped":
|
|
1293
|
+
return void 0;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
get #runtime() {
|
|
1297
|
+
if (this.#lifecycle.status === "ready") return this.#lifecycle.runtime;
|
|
1298
|
+
if (this.#lifecycle.status !== "failed") return void 0;
|
|
1299
|
+
const resources = this.#lifecycle.resources;
|
|
1300
|
+
if (!resources) return void 0;
|
|
1301
|
+
if (resources.kind === "runtime") return resources;
|
|
1302
|
+
return resources.runtime;
|
|
1303
|
+
}
|
|
1304
|
+
get #channelSubscriptions() {
|
|
1305
|
+
const resources = this.#resources;
|
|
1306
|
+
if (!resources) return void 0;
|
|
1307
|
+
if (resources.kind === "runtime") return resources.channelSubscriptions;
|
|
1308
|
+
return resources.runtime?.channelSubscriptions ?? resources.channelSubscriptions;
|
|
1309
|
+
}
|
|
1310
|
+
get #bus() {
|
|
1311
|
+
const resources = this.#resources;
|
|
1312
|
+
if (!resources) return void 0;
|
|
1313
|
+
if (resources.kind === "runtime") return resources.bus;
|
|
1314
|
+
return resources.runtime?.bus ?? resources.bus;
|
|
1315
|
+
}
|
|
1316
|
+
#isStarting(operation) {
|
|
1317
|
+
return this.#lifecycle.status === "starting" && this.#lifecycle.operation === operation;
|
|
1318
|
+
}
|
|
1319
|
+
#closeBootBusAfterStart(operation) {
|
|
1320
|
+
const bus = operation.bus;
|
|
1321
|
+
if (!bus) return Promise.resolve();
|
|
1322
|
+
if (operation.busCloseAfterStart) return operation.busCloseAfterStart;
|
|
1323
|
+
const close = bus.close();
|
|
1324
|
+
operation.busCloseAfterStart = close;
|
|
1325
|
+
return close;
|
|
1267
1326
|
}
|
|
1268
1327
|
setPresenceManager(presenceManager) {
|
|
1269
1328
|
this.#presenceManager = presenceManager;
|
|
1270
1329
|
this.#channelSubscriptions?.setPresenceManager(presenceManager);
|
|
1271
|
-
this.#configurePresenceFetcher();
|
|
1330
|
+
this.#configurePresenceFetcher(this.#bus);
|
|
1272
1331
|
}
|
|
1273
|
-
|
|
1274
|
-
this.#
|
|
1275
|
-
this.#status
|
|
1276
|
-
this.#
|
|
1332
|
+
boot(httpServer, config, channelRouter, logger, runWithHttpContext) {
|
|
1333
|
+
if (this.#lifecycle.status === "ready") return Promise.resolve();
|
|
1334
|
+
if (this.#lifecycle.status === "starting") return this.#lifecycle.operation.promise;
|
|
1335
|
+
if (this.#lifecycle.status === "stopping") {
|
|
1336
|
+
return Promise.reject(new Error("Socket service is stopping"));
|
|
1337
|
+
}
|
|
1338
|
+
if (this.#lifecycle.status === "failed" && this.#lifecycle.resources) {
|
|
1339
|
+
return Promise.reject(new Error("Socket service must be closed before rebooting"));
|
|
1340
|
+
}
|
|
1277
1341
|
this.#logger = logger;
|
|
1278
|
-
const
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1342
|
+
const deferred = Promise.withResolvers();
|
|
1343
|
+
const operation = {
|
|
1344
|
+
kind: "boot",
|
|
1345
|
+
promise: deferred.promise,
|
|
1346
|
+
closeRequested: false
|
|
1347
|
+
};
|
|
1348
|
+
this.#lifecycle = { status: "starting", operation };
|
|
1349
|
+
void this.#boot(httpServer, config, channelRouter, logger, runWithHttpContext, operation).then(
|
|
1350
|
+
deferred.resolve,
|
|
1351
|
+
deferred.reject
|
|
1288
1352
|
);
|
|
1353
|
+
return operation.promise;
|
|
1354
|
+
}
|
|
1355
|
+
async #boot(httpServer, config, channelRouter, logger, runWithHttpContext, operation) {
|
|
1289
1356
|
try {
|
|
1357
|
+
const websocketConfig = config.websocket;
|
|
1358
|
+
const heartbeatConfig = resolveHeartbeatConfig(websocketConfig);
|
|
1359
|
+
const inboundMessageConfig = resolveInboundMessageConfig(websocketConfig);
|
|
1360
|
+
this.#outboundMessageConfig = resolveOutboundMessageConfig(websocketConfig);
|
|
1361
|
+
this.#shutdownTimeout = parseDuration("websocket.shutdownTimeout", websocketConfig?.shutdownTimeout) ?? DEFAULT_SHUTDOWN_TIMEOUT;
|
|
1362
|
+
operation.channelSubscriptions = new ChannelSubscriptions(
|
|
1363
|
+
this,
|
|
1364
|
+
channelRouter,
|
|
1365
|
+
logger,
|
|
1366
|
+
this.#presenceManager ?? void 0,
|
|
1367
|
+
inboundMessageConfig.subscriptionLimits
|
|
1368
|
+
);
|
|
1290
1369
|
if (config.transport) {
|
|
1291
|
-
|
|
1370
|
+
operation.bus = new SocketBus(config.transport, {
|
|
1292
1371
|
channel: (message) => {
|
|
1293
1372
|
this.#traceBroadcast(
|
|
1294
1373
|
{
|
|
@@ -1316,22 +1395,22 @@ var SocketService = class extends Emittery {
|
|
|
1316
1395
|
return this.#getLocalPresenceSockets(channel);
|
|
1317
1396
|
}
|
|
1318
1397
|
});
|
|
1319
|
-
await
|
|
1320
|
-
this.#
|
|
1398
|
+
await operation.bus.start();
|
|
1399
|
+
if (!this.#isStarting(operation)) {
|
|
1400
|
+
throw new Error("Socket service boot interrupted by shutdown");
|
|
1401
|
+
}
|
|
1321
1402
|
}
|
|
1322
1403
|
const websocketServer = new WebSocketServer({
|
|
1323
1404
|
noServer: true,
|
|
1324
1405
|
maxPayload: inboundMessageConfig.maxPayload
|
|
1325
1406
|
});
|
|
1326
|
-
this.#server = websocketServer;
|
|
1327
|
-
this.#httpServer = httpServer;
|
|
1328
1407
|
const upgrader = new SocketUpgrader(
|
|
1329
1408
|
websocketServer,
|
|
1330
1409
|
websocketConfig,
|
|
1331
1410
|
runWithHttpContext,
|
|
1332
1411
|
(message, error) => this.#warn(message, error)
|
|
1333
1412
|
);
|
|
1334
|
-
|
|
1413
|
+
const upgradeHandler = (request, socket, head) => {
|
|
1335
1414
|
upgrader.handle(request, socket, head, (connection, upgradeRequest, accepted) => {
|
|
1336
1415
|
this.#handleConnection(connection, upgradeRequest, accepted, inboundMessageConfig);
|
|
1337
1416
|
}).catch((error) => {
|
|
@@ -1339,18 +1418,40 @@ var SocketService = class extends Emittery {
|
|
|
1339
1418
|
SocketUpgrader.reject(socket, 500, "Internal Server Error");
|
|
1340
1419
|
});
|
|
1341
1420
|
};
|
|
1342
|
-
|
|
1421
|
+
const runtime = {
|
|
1422
|
+
kind: "runtime",
|
|
1423
|
+
server: websocketServer,
|
|
1424
|
+
httpServer,
|
|
1425
|
+
upgradeHandler,
|
|
1426
|
+
channelSubscriptions: operation.channelSubscriptions,
|
|
1427
|
+
bus: operation.bus ?? null
|
|
1428
|
+
};
|
|
1429
|
+
operation.runtime = runtime;
|
|
1430
|
+
httpServer.on("upgrade", upgradeHandler);
|
|
1343
1431
|
this.#startHeartbeat(heartbeatConfig);
|
|
1344
|
-
this.#
|
|
1432
|
+
if (!this.#isStarting(operation)) {
|
|
1433
|
+
throw new Error("Socket service boot interrupted by shutdown");
|
|
1434
|
+
}
|
|
1435
|
+
this.#lifecycle = { status: "ready", runtime };
|
|
1436
|
+
this.#configurePresenceFetcher(runtime.bus);
|
|
1345
1437
|
} catch (error) {
|
|
1346
|
-
this.#
|
|
1347
|
-
|
|
1438
|
+
if (!this.#isStarting(operation)) {
|
|
1439
|
+
if (operation.closeRequested) {
|
|
1440
|
+
await this.#closeBootBusAfterStart(operation);
|
|
1441
|
+
}
|
|
1442
|
+
if (this.#lifecycle.status === "failed" && this.#lifecycle.resources === operation && !this.#lifecycle.close) {
|
|
1443
|
+
await this.close();
|
|
1444
|
+
}
|
|
1445
|
+
throw error;
|
|
1446
|
+
}
|
|
1447
|
+
const failure = error instanceof Error ? error : new Error("Socket service boot failed");
|
|
1448
|
+
this.#lifecycle = { status: "failed", error: failure, resources: operation };
|
|
1348
1449
|
await this.close();
|
|
1349
1450
|
throw error;
|
|
1350
1451
|
}
|
|
1351
1452
|
}
|
|
1352
1453
|
#handleConnection(connection, request, upgrade, inboundMessageConfig) {
|
|
1353
|
-
if (this.#status !== "ready") {
|
|
1454
|
+
if (this.#lifecycle.status !== "ready") {
|
|
1354
1455
|
connection.terminate();
|
|
1355
1456
|
return;
|
|
1356
1457
|
}
|
|
@@ -1418,21 +1519,22 @@ var SocketService = class extends Emittery {
|
|
|
1418
1519
|
if (!this.#sockets.has(socket.id)) {
|
|
1419
1520
|
return Promise.resolve();
|
|
1420
1521
|
}
|
|
1522
|
+
const channelSubscriptions = this.#channelSubscriptions;
|
|
1421
1523
|
const finalization = disconnectChannel.tracePromise(
|
|
1422
1524
|
async () => {
|
|
1423
1525
|
this.#clearHeartbeatTimeout(socket.id);
|
|
1424
1526
|
this.#messageRates.delete(socket.id);
|
|
1425
1527
|
await this.#messageQueue.drain(socket.id);
|
|
1426
1528
|
this.#messageQueue.delete(socket.id);
|
|
1427
|
-
await
|
|
1428
|
-
|
|
1529
|
+
await channelSubscriptions?.leaveAll(socket);
|
|
1530
|
+
channelSubscriptions?.deleteSocket(socket.id);
|
|
1429
1531
|
this.#sockets.delete(socket.id);
|
|
1430
1532
|
this.#emitLifecycleEvent("disconnect", { socket, reason: "close" });
|
|
1431
1533
|
},
|
|
1432
1534
|
{
|
|
1433
1535
|
socketId: socket.id,
|
|
1434
1536
|
reason: "close",
|
|
1435
|
-
subscriptions:
|
|
1537
|
+
subscriptions: channelSubscriptions?.subscriptionCountFor(socket.id) ?? 0
|
|
1436
1538
|
}
|
|
1437
1539
|
).finally(() => {
|
|
1438
1540
|
this.#socketFinalizations.delete(socket.id);
|
|
@@ -1479,7 +1581,9 @@ var SocketService = class extends Emittery {
|
|
|
1479
1581
|
await unsubscribeChannel.tracePromise(async () => {
|
|
1480
1582
|
traceMessage.removed = await this.#channelSubscriptions?.leave(socket, message.channel) ?? false;
|
|
1481
1583
|
traceMessage.ok = true;
|
|
1482
|
-
|
|
1584
|
+
if (traceMessage.removed) {
|
|
1585
|
+
this.#emitLifecycleEvent("unsubscribe", { socket, channel: message.channel });
|
|
1586
|
+
}
|
|
1483
1587
|
sendFrame(
|
|
1484
1588
|
socket.raw.connection,
|
|
1485
1589
|
Reply.ok(message.id).toFrame(),
|
|
@@ -1506,7 +1610,7 @@ var SocketService = class extends Emittery {
|
|
|
1506
1610
|
const ack = await this.#handleChannelPayload(socket, message.type, payload);
|
|
1507
1611
|
traceMessage.ok = ack.ok;
|
|
1508
1612
|
if (!ack.ok) {
|
|
1509
|
-
traceMessage.error = ack.error
|
|
1613
|
+
traceMessage.error = ack.error;
|
|
1510
1614
|
}
|
|
1511
1615
|
sendFrame(
|
|
1512
1616
|
socket.raw.connection,
|
|
@@ -1545,7 +1649,7 @@ var SocketService = class extends Emittery {
|
|
|
1545
1649
|
this.#sendSerializedEventFrame(connection, serializeFrame({ type: "event", event, data }));
|
|
1546
1650
|
},
|
|
1547
1651
|
disconnect() {
|
|
1548
|
-
connection.close();
|
|
1652
|
+
connection.close(SERVER_DISCONNECT_CODE, SERVER_DISCONNECT_REASON);
|
|
1549
1653
|
},
|
|
1550
1654
|
raw
|
|
1551
1655
|
};
|
|
@@ -1651,14 +1755,14 @@ var SocketService = class extends Emittery {
|
|
|
1651
1755
|
}
|
|
1652
1756
|
return delivered;
|
|
1653
1757
|
}
|
|
1654
|
-
#configurePresenceFetcher() {
|
|
1655
|
-
if (!this.#presenceManager || !
|
|
1758
|
+
#configurePresenceFetcher(bus) {
|
|
1759
|
+
if (!this.#presenceManager || !bus) {
|
|
1656
1760
|
return;
|
|
1657
1761
|
}
|
|
1658
1762
|
this.#presenceManager.setSocketFetcher(async (channel) => {
|
|
1659
1763
|
return [
|
|
1660
1764
|
...this.#presenceManager.getLocalSockets(channel),
|
|
1661
|
-
...await
|
|
1765
|
+
...await bus.fetchPresenceSockets(channel)
|
|
1662
1766
|
];
|
|
1663
1767
|
});
|
|
1664
1768
|
}
|
|
@@ -1674,10 +1778,10 @@ var SocketService = class extends Emittery {
|
|
|
1674
1778
|
return this.#sockets.size;
|
|
1675
1779
|
}
|
|
1676
1780
|
get status() {
|
|
1677
|
-
return this.#status;
|
|
1781
|
+
return this.#lifecycle.status;
|
|
1678
1782
|
}
|
|
1679
1783
|
get ready() {
|
|
1680
|
-
return this.#status === "ready"
|
|
1784
|
+
return this.#lifecycle.status === "ready";
|
|
1681
1785
|
}
|
|
1682
1786
|
fake() {
|
|
1683
1787
|
const fake = new SocketFake(() => {
|
|
@@ -1694,8 +1798,21 @@ var SocketService = class extends Emittery {
|
|
|
1694
1798
|
this.#broadcastSink = this.#deliverySink;
|
|
1695
1799
|
}
|
|
1696
1800
|
markFailed(error) {
|
|
1697
|
-
|
|
1698
|
-
|
|
1801
|
+
const failure = error instanceof Error ? error : new Error("Socket service failed");
|
|
1802
|
+
const lifecycle = this.#lifecycle;
|
|
1803
|
+
if (lifecycle.status === "stopped") {
|
|
1804
|
+
this.#lifecycle = { status: "failed", error: failure };
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (lifecycle.status === "starting") {
|
|
1808
|
+
this.#lifecycle = { status: "failed", error: failure, resources: lifecycle.operation };
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (lifecycle.status === "ready") {
|
|
1812
|
+
this.#lifecycle = { status: "failed", error: failure, resources: lifecycle.runtime };
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
this.#lifecycle = { ...lifecycle, status: "failed", error: failure };
|
|
1699
1816
|
}
|
|
1700
1817
|
#warn(message, error) {
|
|
1701
1818
|
try {
|
|
@@ -1712,12 +1829,13 @@ var SocketService = class extends Emittery {
|
|
|
1712
1829
|
});
|
|
1713
1830
|
}
|
|
1714
1831
|
health() {
|
|
1832
|
+
const error = this.#lifecycle.status === "failed" ? this.#lifecycle.error : void 0;
|
|
1715
1833
|
return {
|
|
1716
|
-
status: this.#status,
|
|
1834
|
+
status: this.#lifecycle.status,
|
|
1717
1835
|
ready: this.ready,
|
|
1718
1836
|
connections: this.#sockets.size,
|
|
1719
1837
|
channels: this.#channelSubscriptions?.channelsCount ?? 0,
|
|
1720
|
-
...
|
|
1838
|
+
...error ? { lastError: error.message } : {}
|
|
1721
1839
|
};
|
|
1722
1840
|
}
|
|
1723
1841
|
#startHeartbeat(config) {
|
|
@@ -1783,72 +1901,136 @@ var SocketService = class extends Emittery {
|
|
|
1783
1901
|
rate.count += 1;
|
|
1784
1902
|
return true;
|
|
1785
1903
|
}
|
|
1786
|
-
#cleanup() {
|
|
1904
|
+
#cleanup(channelSubscriptions) {
|
|
1787
1905
|
this.#stopHeartbeat();
|
|
1788
1906
|
this.#sockets.clear();
|
|
1789
1907
|
this.#messageQueue.clear();
|
|
1790
1908
|
this.#messageRates.clear();
|
|
1791
1909
|
this.#socketFinalizations.clear();
|
|
1792
|
-
|
|
1910
|
+
channelSubscriptions?.clear();
|
|
1793
1911
|
}
|
|
1794
1912
|
close() {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
return
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
this.#
|
|
1811
|
-
|
|
1812
|
-
|
|
1913
|
+
const lifecycle = this.#lifecycle;
|
|
1914
|
+
if (lifecycle.status === "stopped") return Promise.resolve();
|
|
1915
|
+
if (lifecycle.status === "stopping") return lifecycle.close;
|
|
1916
|
+
if (lifecycle.status === "failed" && lifecycle.close) return lifecycle.close;
|
|
1917
|
+
if (lifecycle.status === "failed" && !lifecycle.resources) return Promise.resolve();
|
|
1918
|
+
if (lifecycle.status === "starting") {
|
|
1919
|
+
return this.#startClose(lifecycle.operation, lifecycle.operation.promise);
|
|
1920
|
+
}
|
|
1921
|
+
if (lifecycle.status === "ready") return this.#startClose(lifecycle.runtime);
|
|
1922
|
+
return this.#startClose(lifecycle.resources, void 0, lifecycle.error);
|
|
1923
|
+
}
|
|
1924
|
+
#startClose(resources, pendingBoot, preservedError) {
|
|
1925
|
+
const deferred = Promise.withResolvers();
|
|
1926
|
+
const close = deferred.promise;
|
|
1927
|
+
if (resources.kind === "boot") resources.closeRequested = true;
|
|
1928
|
+
this.#lifecycle = preservedError ? { status: "failed", error: preservedError, resources, close } : { status: "stopping", resources, close };
|
|
1929
|
+
void this.#close(resources, pendingBoot).then(
|
|
1930
|
+
() => {
|
|
1931
|
+
if (!("close" in this.#lifecycle) || this.#lifecycle.close !== close) return;
|
|
1932
|
+
this.#lifecycle = this.#lifecycle.status === "failed" ? { status: "failed", error: this.#lifecycle.error, close } : { status: "stopped" };
|
|
1933
|
+
deferred.resolve();
|
|
1934
|
+
},
|
|
1935
|
+
(error) => {
|
|
1936
|
+
const failure = error instanceof Error ? error : new Error("Socket service shutdown failed");
|
|
1937
|
+
if ("close" in this.#lifecycle && this.#lifecycle.close === close) {
|
|
1938
|
+
this.#lifecycle = { status: "failed", error: failure, close };
|
|
1939
|
+
}
|
|
1940
|
+
deferred.reject(error);
|
|
1941
|
+
}
|
|
1942
|
+
);
|
|
1943
|
+
return close;
|
|
1944
|
+
}
|
|
1945
|
+
async #waitForBootClose(pendingBoot, deadline) {
|
|
1946
|
+
const result = await settleBeforeDeadline(pendingBoot, deadline);
|
|
1947
|
+
if (result === null) {
|
|
1948
|
+
this.#warn(
|
|
1949
|
+
"socket boot exceeded the %dms shutdown timeout; forcing cleanup",
|
|
1950
|
+
this.#shutdownTimeout
|
|
1951
|
+
);
|
|
1813
1952
|
}
|
|
1814
|
-
|
|
1953
|
+
}
|
|
1954
|
+
#startServerClose(server) {
|
|
1955
|
+
if (!server) return Promise.resolve(null);
|
|
1956
|
+
return new Promise((resolve) => {
|
|
1815
1957
|
server.close((error) => resolve(error ?? null));
|
|
1816
|
-
})
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
async #closeSockets(deadline) {
|
|
1817
1961
|
this.#stopHeartbeat();
|
|
1818
1962
|
const sockets = [...this.#sockets.values()];
|
|
1819
1963
|
for (const socket of sockets) {
|
|
1820
|
-
socket.raw
|
|
1964
|
+
const { connection } = socket.raw;
|
|
1965
|
+
connection.close(SERVER_DISCONNECT_CODE, SERVER_DISCONNECT_REASON);
|
|
1966
|
+
const terminateTimer = setTimeout(() => connection.terminate(), 50);
|
|
1967
|
+
connection.once("close", () => clearTimeout(terminateTimer));
|
|
1821
1968
|
}
|
|
1822
|
-
const errors = [];
|
|
1823
1969
|
const finalizations = sockets.map((socket) => {
|
|
1824
1970
|
return Promise.resolve().then(() => this.#finalizeSocket(socket));
|
|
1825
1971
|
});
|
|
1826
|
-
const results = await Promise.allSettled(finalizations);
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1972
|
+
const results = await settleBeforeDeadline(Promise.allSettled(finalizations), deadline);
|
|
1973
|
+
if (results === null) {
|
|
1974
|
+
this.#warn(
|
|
1975
|
+
"socket finalization exceeded the %dms shutdown timeout; forcing cleanup",
|
|
1976
|
+
this.#shutdownTimeout
|
|
1977
|
+
);
|
|
1978
|
+
return [];
|
|
1831
1979
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
await bus?.close();
|
|
1837
|
-
} catch (error) {
|
|
1838
|
-
errors.push(error);
|
|
1980
|
+
if (results.status === "rejected") return [];
|
|
1981
|
+
const errors = [];
|
|
1982
|
+
for (const result of results.value) {
|
|
1983
|
+
if (result.status === "rejected") errors.push(result.reason);
|
|
1839
1984
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1985
|
+
return errors;
|
|
1986
|
+
}
|
|
1987
|
+
async #closeBus(resources, bus, deadline) {
|
|
1988
|
+
if (!bus) return [];
|
|
1989
|
+
const close = resources.kind === "boot" ? resources.busCloseAfterStart ?? (resources.busCloseBeforeStart ??= bus.close()) : bus.close();
|
|
1990
|
+
const result = await settleBeforeDeadline(close, deadline);
|
|
1991
|
+
if (result === null) {
|
|
1992
|
+
this.#warn(
|
|
1993
|
+
"socket transport exceeded the %dms shutdown timeout; forcing cleanup",
|
|
1994
|
+
this.#shutdownTimeout
|
|
1995
|
+
);
|
|
1996
|
+
return [];
|
|
1843
1997
|
}
|
|
1998
|
+
return result.status === "rejected" ? [result.reason] : [];
|
|
1999
|
+
}
|
|
2000
|
+
async #finishServerClose(close, deadline) {
|
|
2001
|
+
const result = await settleBeforeDeadline(close, deadline);
|
|
2002
|
+
if (result === null) {
|
|
2003
|
+
this.#warn(
|
|
2004
|
+
"socket server exceeded the %dms shutdown timeout; forcing cleanup",
|
|
2005
|
+
this.#shutdownTimeout
|
|
2006
|
+
);
|
|
2007
|
+
return [];
|
|
2008
|
+
}
|
|
2009
|
+
if (result.status === "rejected" || !result.value) return [];
|
|
2010
|
+
return [result.value];
|
|
2011
|
+
}
|
|
2012
|
+
async #close(resources, pendingBoot) {
|
|
2013
|
+
const deadline = Date.now() + this.#shutdownTimeout;
|
|
2014
|
+
if (pendingBoot) await this.#waitForBootClose(pendingBoot, deadline);
|
|
2015
|
+
const runtime = resources.kind === "runtime" ? resources : resources.runtime;
|
|
2016
|
+
const channelSubscriptions = runtime?.channelSubscriptions ?? (resources.kind === "boot" ? resources.channelSubscriptions : void 0);
|
|
2017
|
+
const server = runtime?.server;
|
|
2018
|
+
const bus = runtime?.bus ?? (resources.kind === "boot" ? resources.bus : void 0);
|
|
2019
|
+
const httpServer = runtime?.httpServer;
|
|
2020
|
+
const upgradeHandler = runtime?.upgradeHandler;
|
|
2021
|
+
if (httpServer && upgradeHandler) {
|
|
2022
|
+
httpServer.off("upgrade", upgradeHandler);
|
|
2023
|
+
}
|
|
2024
|
+
const serverClose = this.#startServerClose(server);
|
|
2025
|
+
const errors = await this.#closeSockets(deadline);
|
|
2026
|
+
this.#cleanup(channelSubscriptions);
|
|
2027
|
+
this.#presenceManager?.setSocketFetcher(null);
|
|
2028
|
+
errors.push(...await this.#closeBus(resources, bus, deadline));
|
|
2029
|
+
errors.push(...await this.#finishServerClose(serverClose, deadline));
|
|
1844
2030
|
if (errors.length > 0) {
|
|
1845
2031
|
const error = errors.length === 1 && errors[0] instanceof Error ? errors[0] : new AggregateError(errors, "Socket service shutdown failed");
|
|
1846
|
-
this.markFailed(error);
|
|
1847
2032
|
throw error;
|
|
1848
2033
|
}
|
|
1849
|
-
if (this.#status !== "failed") {
|
|
1850
|
-
this.#status = "stopped";
|
|
1851
|
-
}
|
|
1852
2034
|
}
|
|
1853
2035
|
};
|
|
1854
2036
|
|