@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.
Files changed (43) hide show
  1. package/README.md +556 -556
  2. package/build/{base_channel-DPOnqned.d.ts → base_channel-CasBQV5I.d.ts} +7 -25
  3. package/build/{chunk-6PJ4ALJD.js → chunk-DM3UQMWW.js} +5 -3
  4. package/build/chunk-DM3UQMWW.js.map +1 -0
  5. package/build/{chunk-QNOOQXYY.js → chunk-DR6GWQUW.js} +242 -199
  6. package/build/chunk-DR6GWQUW.js.map +1 -0
  7. package/build/chunk-NUPBNOP3.js +61 -0
  8. package/build/chunk-NUPBNOP3.js.map +1 -0
  9. package/build/{chunk-YVC7IMDN.js → chunk-UE75J3JE.js} +2 -2
  10. package/build/chunk-YSW4MNFR.js +7 -0
  11. package/build/chunk-YSW4MNFR.js.map +1 -0
  12. package/build/{framework-B8pJ4cCA.d.ts → framework-Cb71JA5f.d.ts} +1 -1
  13. package/build/{index-DjBYUhFR.d.ts → index-DCPtfkJL.d.ts} +1 -1
  14. package/build/index.d.ts +3 -2
  15. package/build/providers/socket_provider.d.ts +3 -2
  16. package/build/providers/socket_provider.js +312 -130
  17. package/build/providers/socket_provider.js.map +1 -1
  18. package/build/services/socket.d.ts +3 -2
  19. package/build/shared_types-B11CaOr5.d.ts +26 -0
  20. package/build/{socket_service-BcC9ASCE.d.ts → socket_service-C2YRbhrO.d.ts} +1 -1
  21. package/build/src/assembler_hook.js +54 -30
  22. package/build/src/assembler_hook.js.map +1 -1
  23. package/build/src/client/index.d.ts +2 -7
  24. package/build/src/client/index.js +3 -2
  25. package/build/src/client/react.d.ts +3 -8
  26. package/build/src/client/react.js +4 -3
  27. package/build/src/client/react.js.map +1 -1
  28. package/build/src/client/types.d.ts +9 -13
  29. package/build/src/client/vue.d.ts +3 -8
  30. package/build/src/client/vue.js +4 -3
  31. package/build/src/client/vue.js.map +1 -1
  32. package/build/src/decorators.d.ts +2 -1
  33. package/build/src/health_check.d.ts +3 -2
  34. package/build/src/otel.d.ts +1 -0
  35. package/build/src/otel.js +211 -27
  36. package/build/src/otel.js.map +1 -1
  37. package/build/src/types.d.ts +2 -1
  38. package/package.json +31 -7
  39. package/build/chunk-6PJ4ALJD.js.map +0 -1
  40. package/build/chunk-D3HUBCBW.js +0 -18
  41. package/build/chunk-D3HUBCBW.js.map +0 -1
  42. package/build/chunk-QNOOQXYY.js.map +0 -1
  43. /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-D3HUBCBW.js";
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, async () => {
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 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
- );
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(channelName, operation) {
332
- const previous = this.#channelOperations.get(channelName) ?? Promise.resolve();
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(channelName, queued);
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(channelName) === queued) {
342
- this.#channelOperations.delete(channelName);
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
- constructor(id, ok, data, error) {
397
- this.id = id;
398
- this.ok = ok;
399
- this.data = data;
400
- this.error = error;
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, void 0);
402
+ return new _Reply(id, { ok: true, data });
404
403
  }
405
404
  static error(id, error) {
406
- return new _Reply(id, false, void 0, error);
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 ?? "Subscribe failed");
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 ?? "Handler error");
426
+ return ack.ok ? _Reply.ok(id, ack.data) : _Reply.error(id, ack.error);
428
427
  }
429
428
  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;
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 frame;
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
- #status = "stopped";
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
- if (!this.#server) {
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 this.#server;
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
- async boot(httpServer, config, channelRouter, logger, runWithHttpContext) {
1274
- this.#closePromise = null;
1275
- this.#status = "starting";
1276
- this.#lastError = null;
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 websocketConfig = config.websocket;
1279
- const heartbeatConfig = resolveHeartbeatConfig(websocketConfig);
1280
- const inboundMessageConfig = resolveInboundMessageConfig(websocketConfig);
1281
- this.#outboundMessageConfig = resolveOutboundMessageConfig(websocketConfig);
1282
- this.#channelSubscriptions = new ChannelSubscriptions(
1283
- this,
1284
- channelRouter,
1285
- logger,
1286
- this.#presenceManager ?? void 0,
1287
- inboundMessageConfig.subscriptionLimits
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
- this.#bus = new SocketBus(config.transport, {
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 this.#bus.start();
1320
- this.#configurePresenceFetcher();
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
- this.#upgradeHandler = (request, socket, head) => {
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
- httpServer.on("upgrade", this.#upgradeHandler);
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.#status = "ready";
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.#lastError = error instanceof Error ? error : new Error("Socket service boot failed");
1347
- this.#status = "failed";
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 this.#channelSubscriptions?.leaveAll(socket);
1428
- this.#channelSubscriptions?.deleteSocket(socket.id);
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: this.#channelSubscriptions?.subscriptionCountFor(socket.id) ?? 0
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
- this.#emitLifecycleEvent("unsubscribe", { socket, channel: message.channel });
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 ?? "Handler 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 || !this.#bus) {
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 this.#bus.fetchPresenceSockets(channel)
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" && Boolean(this.#server);
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
- this.#lastError = error instanceof Error ? error : new Error("Socket service failed");
1698
- this.#status = "failed";
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
- ...this.#lastError ? { lastError: this.#lastError.message } : {}
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
- this.#channelSubscriptions?.clear();
1910
+ channelSubscriptions?.clear();
1793
1911
  }
1794
1912
  close() {
1795
- if (!this.#closePromise) {
1796
- this.#closePromise = this.#close();
1797
- }
1798
- return this.#closePromise;
1799
- }
1800
- async #close() {
1801
- const server = this.#server;
1802
- const bus = this.#bus;
1803
- const httpServer = this.#httpServer;
1804
- const upgradeHandler = this.#upgradeHandler;
1805
- if (this.#status !== "failed") {
1806
- this.#status = "stopping";
1807
- }
1808
- this.#server = null;
1809
- this.#httpServer = null;
1810
- this.#upgradeHandler = null;
1811
- if (httpServer && upgradeHandler) {
1812
- httpServer.off("upgrade", upgradeHandler);
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
- const serverClose = server ? new Promise((resolve) => {
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
- }) : Promise.resolve(null);
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.connection.terminate();
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
- for (const result of results) {
1828
- if (result.status === "rejected") {
1829
- errors.push(result.reason);
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
- this.#cleanup();
1833
- this.#presenceManager?.setSocketFetcher(null);
1834
- this.#bus = null;
1835
- try {
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
- const serverCloseError = await serverClose;
1841
- if (serverCloseError) {
1842
- errors.push(serverCloseError);
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