@emeryld/rrroutes-client 2.6.2 → 2.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1339,1323 +1339,669 @@ var buildRoomPayloadSchema = (metaSchema) => import_zod.z.object({
1339
1339
  meta: metaSchema
1340
1340
  });
1341
1341
 
1342
- // src/sockets/socket.client.context.tsx
1343
- var React = __toESM(require("react"), 1);
1344
- var import_jsx_runtime = require("react/jsx-runtime");
1345
- var SocketCtx = React.createContext(null);
1346
- function dbg(dbgOpts, e) {
1347
- if (!dbgOpts?.logger) return;
1348
- if (!dbgOpts[e.type]) return;
1349
- dbgOpts.logger(e);
1350
- }
1351
- function isProbablySocket(value) {
1352
- if (!value || typeof value !== "object") return false;
1353
- const anyVal = value;
1354
- const ctorName = anyVal.constructor?.name;
1355
- if (ctorName === "Socket") return true;
1356
- return ("connected" in anyVal || "recovered" in anyVal) && typeof anyVal.on === "function" && typeof anyVal.emit === "function";
1357
- }
1358
- function describeSocketLike(value) {
1359
- if (!value) return null;
1360
- const id = value.id ?? "unknown";
1361
- const connected = value.connected ?? false;
1362
- const recovered = typeof value.recovered === "boolean" ? ` recovered=${value.recovered}` : "";
1363
- return `[Socket id=${id} connected=${connected}${recovered}]`;
1364
- }
1365
- function safeDescribeHookValue(value) {
1366
- if (value == null) return value;
1367
- const valueType = typeof value;
1368
- if (valueType === "string" || valueType === "number" || valueType === "boolean") {
1369
- return value;
1370
- }
1371
- if (valueType === "bigint" || valueType === "symbol") return String(value);
1372
- if (valueType === "function")
1373
- return `[function ${value.name || "anonymous"}]`;
1374
- if (Array.isArray(value)) return `[array length=${value.length}]`;
1375
- if (isProbablySocket(value)) {
1376
- return describeSocketLike(value);
1377
- }
1378
- const ctorName = value.constructor?.name ?? "object";
1379
- const keys = Object.keys(value);
1380
- const keyPreview = keys.slice(0, 4).join(",");
1381
- const suffix = keys.length > 4 ? ",\u2026" : "";
1382
- return `[${ctorName} keys=${keyPreview}${suffix}]`;
1383
- }
1384
- function summarizeEvents(events) {
1385
- if (!events || typeof events !== "object")
1386
- return safeDescribeHookValue(events);
1387
- const keys = Object.keys(events);
1388
- if (!keys.length) return "[events empty]";
1389
- const preview = keys.slice(0, 4).join(",");
1390
- const suffix = keys.length > 4 ? ",\u2026" : "";
1391
- return `[events count=${keys.length} keys=${preview}${suffix}]`;
1392
- }
1393
- function summarizeBaseOptions(options) {
1394
- if (!options || typeof options !== "object")
1395
- return safeDescribeHookValue(options);
1396
- const obj = options;
1397
- const keys = Object.keys(obj);
1398
- if (!keys.length) return "[baseOptions empty]";
1399
- const preview = keys.slice(0, 4).join(",");
1400
- const suffix = keys.length > 4 ? ",\u2026" : "";
1401
- const hasDebug = "debug" in obj;
1402
- return `[baseOptions keys=${preview}${suffix} debug=${hasDebug}]`;
1403
- }
1404
- function summarizeMeta(meta, label) {
1405
- if (meta == null) return null;
1406
- if (typeof meta !== "object") return safeDescribeHookValue(meta);
1407
- const keys = Object.keys(meta);
1408
- if (!keys.length) return `[${label} empty]`;
1409
- const preview = keys.slice(0, 4).join(",");
1410
- const suffix = keys.length > 4 ? ",\u2026" : "";
1411
- return `[${label} keys=${preview}${suffix}]`;
1412
- }
1413
- function createHookDebugEvent(prev, next, hook) {
1414
- const reason = prev ? "change" : "init";
1415
- const changed = Object.keys(next).reduce((acc, dependency) => {
1416
- const prevValue = prev ? prev[dependency] : void 0;
1417
- const nextValue = next[dependency];
1418
- if (!prev || !Object.is(prevValue, nextValue)) {
1419
- acc.push({
1420
- dependency,
1421
- previous: safeDescribeHookValue(prevValue),
1422
- next: safeDescribeHookValue(nextValue)
1342
+ // src/sockets/socket.client.core.ts
1343
+ var SocketClient = class {
1344
+ constructor(events, opts) {
1345
+ this.hbTimer = null;
1346
+ // stats
1347
+ this.roomCounts = /* @__PURE__ */ new Map();
1348
+ this.handlerMap = /* @__PURE__ */ new Map();
1349
+ this.events = events;
1350
+ this.socket = opts.socket ?? null;
1351
+ this.environment = opts.environment ?? "development";
1352
+ this.debug = opts.debug ?? {};
1353
+ this.config = opts.config;
1354
+ this.sysEvents = opts.sys;
1355
+ this.roomJoinSchema = buildRoomPayloadSchema(this.config.joinMetaMessage);
1356
+ this.roomLeaveSchema = buildRoomPayloadSchema(this.config.leaveMetaMessage);
1357
+ const hb = opts.heartbeat ?? {};
1358
+ this.hb = {
1359
+ intervalMs: hb.intervalMs ?? 15e3,
1360
+ timeoutMs: hb.timeoutMs ?? 7500
1361
+ };
1362
+ if (!this.socket) {
1363
+ this.dbg({
1364
+ type: "lifecycle",
1365
+ phase: "init",
1366
+ err: "Socket reference is null during initialization"
1367
+ });
1368
+ } else {
1369
+ this.dbg({
1370
+ type: "lifecycle",
1371
+ phase: "init",
1372
+ heartbeatIntervalMs: this.hb.intervalMs,
1373
+ heartbeatTimeoutMs: this.hb.timeoutMs,
1374
+ environment: this.environment
1423
1375
  });
1376
+ this.logSocketConfigSnapshot("constructor");
1424
1377
  }
1425
- return acc;
1426
- }, []);
1427
- if (!changed.length) return null;
1428
- return { type: "hook", phase: hook, reason, changes: changed };
1429
- }
1430
- function trackHookTrigger({
1431
- ref,
1432
- hook,
1433
- providerDebug,
1434
- snapshot
1435
- }) {
1436
- const prev = ref.current;
1437
- ref.current = snapshot;
1438
- if (!providerDebug?.logger || !providerDebug?.hook) return;
1439
- const event = createHookDebugEvent(prev, snapshot, hook);
1440
- if (event) dbg(providerDebug, event);
1441
- }
1442
- function buildSocketProvider(args) {
1443
- const { events, options: baseOptions } = args;
1444
- return {
1445
- SocketProvider: (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1446
- SocketProvider,
1447
- {
1448
- events,
1449
- baseOptions,
1450
- providerDebug: baseOptions.debug,
1451
- ...props
1452
- }
1453
- ),
1454
- useSocketClient: () => useSocketClient(),
1455
- useSocketConnection: (p) => useSocketConnection(p)
1456
- };
1457
- }
1458
- function SocketProvider(props) {
1459
- const {
1460
- events,
1461
- baseOptions,
1462
- children,
1463
- fallback,
1464
- providerDebug,
1465
- destroyLeaveMeta
1466
- } = props;
1467
- const [resolvedSocket, setResolvedSocket] = React.useState(
1468
- null
1469
- );
1470
- const socket = "socket" in props ? props.socket ?? null : resolvedSocket;
1471
- const providerDebugRef = React.useRef();
1472
- providerDebugRef.current = providerDebug;
1473
- const resolveEffectDebugRef = React.useRef(null);
1474
- const clientMemoDebugRef = React.useRef(null);
1475
- const destroyEffectDebugRef = React.useRef(null);
1476
- React.useEffect(() => {
1477
- trackHookTrigger({
1478
- ref: resolveEffectDebugRef,
1479
- hook: "resolve_effect",
1480
- providerDebug: providerDebugRef.current,
1481
- snapshot: {
1482
- resolvedSocket: describeSocketLike(resolvedSocket)
1378
+ this.onConnect = async () => {
1379
+ if (!this.socket) {
1380
+ this.dbg({
1381
+ type: "connection",
1382
+ phase: "connect_event",
1383
+ err: "Socket is null"
1384
+ });
1385
+ throw new Error("Socket is null in onConnect handler");
1483
1386
  }
1484
- });
1485
- if (!("getSocket" in props)) return;
1486
- let cancelled = false;
1487
- dbg(providerDebugRef.current, { type: "resolve", phase: "start" });
1488
- if (!resolvedSocket) {
1489
- Promise.resolve(props.getSocket()).then((s) => {
1490
- if (cancelled) {
1491
- dbg(providerDebugRef.current, {
1492
- type: "resolve",
1493
- phase: "cancelled"
1494
- });
1495
- return;
1496
- }
1497
- if (!s) {
1498
- dbg(providerDebugRef.current, {
1499
- type: "resolve",
1500
- phase: "socketMissing"
1501
- });
1502
- return;
1387
+ this.dbg({
1388
+ type: "connection",
1389
+ phase: "connect_event",
1390
+ id: this.socket.id,
1391
+ details: {
1392
+ nsp: this.getNamespace(this.socket)
1503
1393
  }
1504
- setResolvedSocket(s);
1505
- dbg(providerDebugRef.current, { type: "resolve", phase: "ok" });
1506
- }).catch((err) => {
1507
- if (cancelled) return;
1508
- dbg(providerDebugRef.current, {
1509
- type: "resolve",
1510
- phase: "error",
1511
- err: String(err)
1512
- });
1513
1394
  });
1514
- }
1515
- return () => {
1516
- cancelled = true;
1395
+ this.logSocketConfigSnapshot("connect_event");
1396
+ await this.getSysEvent("sys:connect")({
1397
+ socket: this.socket,
1398
+ client: this
1399
+ });
1517
1400
  };
1518
- }, [resolvedSocket]);
1519
- trackHookTrigger({
1520
- ref: clientMemoDebugRef,
1521
- hook: "client_memo",
1522
- providerDebug: providerDebugRef.current,
1523
- snapshot: {
1524
- events: summarizeEvents(events),
1525
- baseOptions: summarizeBaseOptions(baseOptions),
1526
- socket: describeSocketLike(socket)
1527
- }
1528
- });
1529
- const client = React.useMemo(() => {
1530
- if (!socket) {
1531
- dbg(providerDebugRef.current, {
1532
- type: "client",
1533
- phase: "init",
1534
- missing: true
1401
+ this.onReconnect = async (attempt) => {
1402
+ if (!this.socket) {
1403
+ this.dbg({
1404
+ type: "connection",
1405
+ phase: "reconnect_event",
1406
+ err: "Socket is null"
1407
+ });
1408
+ throw new Error("Socket is null in onReconnect handler");
1409
+ }
1410
+ this.dbg({
1411
+ type: "connection",
1412
+ phase: "reconnect_event",
1413
+ attempt,
1414
+ id: this.socket.id,
1415
+ details: {
1416
+ nsp: this.getNamespace(this.socket)
1417
+ }
1535
1418
  });
1536
- return null;
1537
- }
1538
- const c = new SocketClient(events, { ...baseOptions, socket });
1539
- dbg(providerDebugRef.current, {
1540
- type: "client",
1541
- phase: "init",
1542
- missing: false
1543
- });
1544
- return c;
1545
- }, [events, baseOptions, socket]);
1546
- const destroyLeaveMetaRef = React.useRef(destroyLeaveMeta);
1547
- React.useEffect(() => {
1548
- destroyLeaveMetaRef.current = destroyLeaveMeta;
1549
- }, [destroyLeaveMeta]);
1550
- React.useEffect(() => {
1551
- trackHookTrigger({
1552
- ref: destroyEffectDebugRef,
1553
- hook: "destroy_effect",
1554
- providerDebug: providerDebugRef.current,
1555
- snapshot: {
1556
- hasClient: !!client,
1557
- destroyLeaveMeta: summarizeMeta(
1558
- destroyLeaveMetaRef.current,
1559
- "destroyLeaveMeta"
1560
- )
1419
+ this.logSocketConfigSnapshot("reconnect_event");
1420
+ await this.getSysEvent("sys:reconnect")({
1421
+ attempt,
1422
+ socket: this.socket,
1423
+ client: this
1424
+ });
1425
+ };
1426
+ this.onDisconnect = async (reason) => {
1427
+ if (!this.socket) {
1428
+ this.dbg({
1429
+ type: "connection",
1430
+ phase: "disconnect_event",
1431
+ err: "Socket is null"
1432
+ });
1433
+ throw new Error("Socket is null in onDisconnect handler");
1561
1434
  }
1562
- });
1563
- return () => {
1564
- if (client) {
1565
- client.destroy(destroyLeaveMetaRef.current);
1566
- dbg(providerDebugRef.current, { type: "client", phase: "destroy" });
1435
+ this.dbg({
1436
+ type: "connection",
1437
+ phase: "disconnect_event",
1438
+ reason: String(reason),
1439
+ details: {
1440
+ roomsTracked: this.roomCounts.size
1441
+ }
1442
+ });
1443
+ this.logSocketConfigSnapshot("disconnect_event");
1444
+ await this.getSysEvent("sys:disconnect")({
1445
+ reason: String(reason),
1446
+ socket: this.socket,
1447
+ client: this
1448
+ });
1449
+ };
1450
+ this.onConnectError = async (err) => {
1451
+ if (!this.socket) {
1452
+ this.dbg({
1453
+ type: "connection",
1454
+ phase: "connect_error_event",
1455
+ err: "Socket is null"
1456
+ });
1457
+ throw new Error("Socket is null in onConnectError handler");
1567
1458
  }
1459
+ this.dbg({
1460
+ type: "connection",
1461
+ phase: "connect_error_event",
1462
+ err: String(err),
1463
+ details: this.getVerboseDetails({ rawError: err })
1464
+ });
1465
+ this.logSocketConfigSnapshot("connect_error_event");
1466
+ await this.getSysEvent("sys:connect_error")({
1467
+ error: String(err),
1468
+ socket: this.socket,
1469
+ client: this
1470
+ });
1568
1471
  };
1569
- }, [client]);
1570
- dbg(providerDebugRef.current, { type: "render", hasClient: !!client });
1571
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SocketCtx.Provider, { value: client, children: client == null ? fallback ?? children : children });
1572
- }
1573
- function useSocketClient() {
1574
- const ctx = React.useContext(SocketCtx);
1575
- if (!ctx)
1576
- throw new Error("SocketClient not found. Wrap with <SocketProvider>.");
1577
- return ctx;
1578
- }
1579
- function useSocketConnection(args) {
1580
- const {
1581
- event,
1582
- rooms,
1583
- onMessage,
1584
- onCleanup,
1585
- autoJoin = true,
1586
- autoLeave = true
1587
- } = args;
1588
- const client = useSocketClient();
1589
- const normalizedRooms = React.useMemo(
1590
- () => rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms],
1591
- [rooms]
1592
- );
1593
- React.useEffect(() => {
1594
- if (autoJoin && normalizedRooms.length > 0)
1595
- client.joinRooms(normalizedRooms, args.joinMeta);
1596
- const unsubscribe = client.on(event, (payload, meta) => {
1597
- onMessage(payload, meta);
1598
- });
1599
- return () => {
1600
- unsubscribe();
1601
- if (autoLeave && normalizedRooms.length > 0)
1602
- client.leaveRooms(normalizedRooms, args.leaveMeta);
1603
- if (onCleanup) onCleanup();
1472
+ this.onPong = async (raw) => {
1473
+ if (!this.socket) {
1474
+ this.dbg({
1475
+ type: "heartbeat",
1476
+ phase: "pong_recv",
1477
+ err: "Socket is null"
1478
+ });
1479
+ throw new Error("Socket is null in onPong handler");
1480
+ }
1481
+ const parsed = this.config.pongPayload.safeParse(raw);
1482
+ if (!parsed.success) {
1483
+ this.dbg({
1484
+ type: "heartbeat",
1485
+ phase: "validation_failed",
1486
+ err: `pong payload validation failed: ${parsed.error.message}`,
1487
+ details: this.getValidationDetails(parsed.error)
1488
+ });
1489
+ return;
1490
+ }
1491
+ const validated = parsed.data;
1492
+ this.dbg({
1493
+ type: "heartbeat",
1494
+ phase: "pong_recv",
1495
+ payload: validated
1496
+ });
1497
+ await this.getSysEvent("sys:pong")({
1498
+ socket: this.socket,
1499
+ payload: validated,
1500
+ client: this
1501
+ });
1604
1502
  };
1605
- }, [client, event, onMessage, autoJoin, autoLeave, ...normalizedRooms]);
1606
- }
1607
-
1608
- // src/sockets/socketedRoute/socket.client.helper.ts
1609
- var import_react4 = require("react");
1610
- function normalizeRooms(rooms) {
1611
- if (rooms == null) return [];
1612
- const list = Array.isArray(rooms) ? rooms : [rooms];
1613
- const seen = /* @__PURE__ */ new Set();
1614
- const normalized = [];
1615
- for (const r of list) {
1616
- if (typeof r !== "string") continue;
1617
- if (seen.has(r)) continue;
1618
- seen.add(r);
1619
- normalized.push(r);
1503
+ if (this.socket) {
1504
+ this.socket.on("connect", this.onConnect);
1505
+ this.socket.on("reconnect", this.onReconnect);
1506
+ this.socket.on("disconnect", this.onDisconnect);
1507
+ this.socket.on("connect_error", this.onConnectError);
1508
+ this.socket.on("sys:pong", this.onPong);
1509
+ }
1620
1510
  }
1621
- return normalized;
1622
- }
1623
- var objectReferenceIds = /* @__PURE__ */ new WeakMap();
1624
- var objectReferenceCounter = 0;
1625
- function describeObjectReference(value) {
1626
- if (value == null) return null;
1627
- const valueType = typeof value;
1628
- if (valueType !== "object" && valueType !== "function") return null;
1629
- const obj = value;
1630
- let id = objectReferenceIds.get(obj);
1631
- if (!id) {
1632
- id = ++objectReferenceCounter;
1633
- objectReferenceIds.set(obj, id);
1511
+ snapshotSocketConfig(socket) {
1512
+ if (!socket) return null;
1513
+ const manager = socket.io ?? null;
1514
+ const base = {
1515
+ nsp: this.getNamespace(socket)
1516
+ };
1517
+ if (!manager) return base;
1518
+ const opts = manager.opts ?? {};
1519
+ const transports = Array.isArray(opts.transports) ? opts.transports.filter((t) => typeof t === "string") : void 0;
1520
+ const transportName = typeof manager.engine?.transport?.name === "string" ? manager.engine.transport?.name : void 0;
1521
+ const readyState = typeof manager._readyState === "string" ? manager._readyState : void 0;
1522
+ const uri = typeof manager.uri === "string" ? manager.uri : void 0;
1523
+ return {
1524
+ ...base,
1525
+ url: uri,
1526
+ path: typeof opts.path === "string" ? opts.path : void 0,
1527
+ transport: transportName ?? transports?.[0],
1528
+ transports,
1529
+ readyState,
1530
+ autoConnect: typeof opts.autoConnect === "boolean" ? opts.autoConnect : void 0,
1531
+ reconnection: typeof opts.reconnection === "boolean" ? opts.reconnection : void 0,
1532
+ reconnectionAttempts: typeof opts.reconnectionAttempts === "number" ? opts.reconnectionAttempts : void 0,
1533
+ reconnectionDelay: typeof opts.reconnectionDelay === "number" ? opts.reconnectionDelay : void 0,
1534
+ reconnectionDelayMax: typeof opts.reconnectionDelayMax === "number" ? opts.reconnectionDelayMax : void 0,
1535
+ timeout: typeof opts.timeout === "number" ? opts.timeout : void 0,
1536
+ hostname: typeof opts.hostname === "string" ? opts.hostname : void 0,
1537
+ port: typeof opts.port === "number" || typeof opts.port === "string" ? opts.port : void 0,
1538
+ secure: typeof opts.secure === "boolean" ? opts.secure : void 0
1539
+ };
1634
1540
  }
1635
- return `ref#${id}`;
1636
- }
1637
- function safeJsonKey(value) {
1638
- try {
1639
- const serialized = JSON.stringify(value ?? null);
1640
- return serialized ?? "null";
1641
- } catch {
1642
- return "[unserializable]";
1541
+ logSocketConfigSnapshot(reason) {
1542
+ if (!this.debug.logger || !this.debug.config) return;
1543
+ const snapshot = this.snapshotSocketConfig(this.socket);
1544
+ this.dbg({
1545
+ type: "config",
1546
+ phase: reason,
1547
+ ...snapshot == null ? { err: "Socket is missing. " } : {
1548
+ socketId: this.socket?.id,
1549
+ snapshot
1550
+ }
1551
+ });
1643
1552
  }
1644
- }
1645
- function arrayShallowEqual(a, b) {
1646
- if (a.length !== b.length) return false;
1647
- for (let i = 0; i < a.length; i += 1) {
1648
- if (a[i] !== b[i]) return false;
1553
+ getValidationDetails(error) {
1554
+ if (!this.debug.verbose) return void 0;
1555
+ return { issues: error.issues };
1649
1556
  }
1650
- return true;
1651
- }
1652
- function roomStateEqual(prev, next) {
1653
- return arrayShallowEqual(prev.rooms, next.rooms) && safeJsonKey(prev.joinMeta) === safeJsonKey(next.joinMeta) && safeJsonKey(prev.leaveMeta) === safeJsonKey(next.leaveMeta);
1654
- }
1655
- function safeDescribeHookValue2(value) {
1656
- if (value == null) return value;
1657
- const valueType = typeof value;
1658
- if (valueType === "string" || valueType === "number" || valueType === "boolean") {
1659
- return value;
1557
+ getVerboseDetails(details) {
1558
+ if (!this.debug.verbose) return void 0;
1559
+ return details;
1660
1560
  }
1661
- if (valueType === "bigint" || valueType === "symbol") return String(value);
1662
- if (valueType === "function")
1663
- return `[function ${value.name || "anonymous"}]`;
1664
- if (Array.isArray(value)) return `[array length=${value.length}]`;
1665
- const ctorName = value.constructor?.name ?? "object";
1666
- const keys = Object.keys(value);
1667
- const keyPreview = keys.slice(0, 4).join(",");
1668
- const suffix = keys.length > 4 ? ",\u2026" : "";
1669
- const ref = describeObjectReference(value);
1670
- return `[${ctorName}${ref ? ` ${ref}` : ""} keys=${keyPreview}${suffix}]`;
1671
- }
1672
- function createHookDebugEvent2(prev, next, phase) {
1673
- const reason = prev ? "change" : "init";
1674
- const changed = Object.keys(next).reduce((acc, dependency) => {
1675
- const prevValue = prev ? prev[dependency] : void 0;
1676
- const nextValue = next[dependency];
1677
- if (!prev || !Object.is(prevValue, nextValue)) {
1678
- acc.push({
1679
- dependency,
1680
- previous: safeDescribeHookValue2(prevValue),
1681
- next: safeDescribeHookValue2(nextValue)
1682
- });
1683
- }
1684
- return acc;
1685
- }, []);
1686
- if (!changed.length) return null;
1687
- return { type: "hook", phase, reason, changes: changed };
1688
- }
1689
- function dbg2(debug, event) {
1690
- if (!debug?.logger) return;
1691
- if (!debug[event.type]) return;
1692
- debug.logger(event);
1693
- }
1694
- function trackHookTrigger2({
1695
- ref,
1696
- phase,
1697
- debug,
1698
- snapshot
1699
- }) {
1700
- const prev = ref.current;
1701
- ref.current = snapshot;
1702
- if (!debug?.logger || !debug?.hook) return;
1703
- const event = createHookDebugEvent2(prev, snapshot, phase);
1704
- if (event) dbg2(debug, event);
1705
- }
1706
- function isSocketClientUnavailableError(err) {
1707
- return err instanceof Error && err.message.includes("SocketClient not found");
1708
- }
1709
- function mergeRoomState(prev, toRoomsResult) {
1710
- const merged = new Set(prev.rooms);
1711
- for (const r of normalizeRooms(toRoomsResult.rooms)) merged.add(r);
1712
- return {
1713
- rooms: Array.from(merged),
1714
- joinMeta: toRoomsResult.joinMeta ?? prev.joinMeta,
1715
- leaveMeta: toRoomsResult.leaveMeta ?? prev.leaveMeta
1716
- };
1717
- }
1718
- function roomsFromData(data, toRooms) {
1719
- if (data == null) return { rooms: [] };
1720
- let state = { rooms: [] };
1721
- const add = (input) => {
1722
- const mergeForValue = (value) => {
1723
- state = mergeRoomState(state, toRooms(value));
1561
+ getNamespace(socket) {
1562
+ if (!socket) return void 0;
1563
+ const nsp = socket.nsp;
1564
+ return typeof nsp === "string" ? nsp : void 0;
1565
+ }
1566
+ getSysEvent(name) {
1567
+ return this.sysEvents[name];
1568
+ }
1569
+ dbg(e) {
1570
+ const d = this.debug;
1571
+ if (!d.logger) return;
1572
+ if (!d[e.type]) return;
1573
+ if (d.only && "event" in e && !d.only.includes(e.event)) return;
1574
+ d.logger(e);
1575
+ }
1576
+ /** internal stats snapshot */
1577
+ stats() {
1578
+ const rooms = Array.from(this.roomCounts.entries()).map(
1579
+ ([room, count]) => ({ room, count })
1580
+ );
1581
+ const handlers = Array.from(this.handlerMap.entries()).map(
1582
+ ([event, set]) => ({
1583
+ event,
1584
+ handlers: set.size
1585
+ })
1586
+ );
1587
+ return {
1588
+ roomsCount: rooms.length,
1589
+ totalHandlers: handlers.reduce((a, b) => a + b.handlers, 0),
1590
+ rooms,
1591
+ handlers
1724
1592
  };
1725
- if (Array.isArray(input)) {
1726
- input.forEach((entry) => mergeForValue(entry));
1727
- return;
1728
- }
1729
- if (input && typeof input === "object") {
1730
- const maybeItems = input.items;
1731
- if (Array.isArray(maybeItems)) {
1732
- maybeItems.forEach((entry) => mergeForValue(entry));
1733
- return;
1734
- }
1593
+ }
1594
+ toArray(rooms) {
1595
+ return rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms];
1596
+ }
1597
+ rollbackJoinIncrement(rooms) {
1598
+ for (const room of rooms) {
1599
+ const curr = this.roomCounts.get(room) ?? 0;
1600
+ const next = Math.max(0, curr - 1);
1601
+ if (next === 0) this.roomCounts.delete(room);
1602
+ else this.roomCounts.set(room, next);
1735
1603
  }
1736
- mergeForValue(input);
1737
- };
1738
- const maybePages = data?.pages;
1739
- if (Array.isArray(maybePages)) {
1740
- for (const page of maybePages) add(page);
1741
- return state;
1742
1604
  }
1743
- add(data);
1744
- return state;
1745
- }
1746
- function buildSocketedRoute(options) {
1747
- const { built, toRooms, applySocket, useSocketClient: useSocketClient2, debug } = options;
1748
- const { useEndpoint: useInnerEndpoint, ...rest } = built;
1749
- const useEndpoint = (...useArgs) => {
1750
- let client = null;
1751
- let socketClientError = null;
1752
- try {
1753
- client = useSocketClient2();
1754
- } catch (err) {
1755
- if (!isSocketClientUnavailableError(err)) throw err;
1756
- socketClientError = err;
1605
+ rollbackLeaveDecrement(rooms) {
1606
+ for (const room of rooms) {
1607
+ const curr = this.roomCounts.get(room) ?? 0;
1608
+ this.roomCounts.set(room, curr + 1);
1757
1609
  }
1758
- const endpointResult = useInnerEndpoint(
1759
- ...useArgs
1760
- );
1761
- const argsKey = (0, import_react4.useMemo)(() => safeJsonKey(useArgs[0] ?? null), [useArgs]);
1762
- const [roomState, setRoomState] = (0, import_react4.useState)(
1763
- () => roomsFromData(endpointResult.data, toRooms)
1764
- );
1765
- const renderCountRef = (0, import_react4.useRef)(0);
1766
- const clientReadyRef = (0, import_react4.useRef)(null);
1767
- const onReceiveEffectDebugRef = (0, import_react4.useRef)(null);
1768
- const deriveRoomsEffectDebugRef = (0, import_react4.useRef)(null);
1769
- const joinRoomsEffectDebugRef = (0, import_react4.useRef)(null);
1770
- const applySocketEffectDebugRef = (0, import_react4.useRef)(null);
1771
- renderCountRef.current += 1;
1772
- const roomsKey = (0, import_react4.useMemo)(() => roomState.rooms.join("|"), [roomState.rooms]);
1773
- const joinMetaKey = (0, import_react4.useMemo)(
1774
- () => safeJsonKey(roomState.joinMeta ?? null),
1775
- [roomState.joinMeta]
1776
- );
1777
- const leaveMetaKey = (0, import_react4.useMemo)(
1778
- () => safeJsonKey(roomState.leaveMeta ?? null),
1779
- [roomState.leaveMeta]
1780
- );
1781
- const hasClient = !!client;
1782
- if (clientReadyRef.current !== hasClient) {
1783
- clientReadyRef.current = hasClient;
1784
- dbg2(debug, {
1785
- type: "client",
1786
- phase: hasClient ? "ready" : "missing",
1787
- err: hasClient ? void 0 : String(socketClientError)
1610
+ }
1611
+ /**
1612
+ * Public: start the heartbeat loop manually.
1613
+ *
1614
+ * This is called by the default 'sys:connect' handler, but you can also
1615
+ * call it yourself from any sysHandler to change when heartbeats start.
1616
+ */
1617
+ startHeartbeat() {
1618
+ this.stopHeartbeat("stop_before_start");
1619
+ if (!this.socket) {
1620
+ this.dbg({
1621
+ type: "heartbeat",
1622
+ phase: "start",
1623
+ err: "Socket is null"
1788
1624
  });
1625
+ return;
1789
1626
  }
1790
- dbg2(debug, {
1791
- type: "render",
1792
- renderCount: renderCountRef.current,
1793
- hasClient,
1794
- argsKey,
1795
- rooms: roomState.rooms,
1796
- roomsKey,
1797
- joinMetaKey,
1798
- leaveMetaKey
1627
+ const socket = this.socket;
1628
+ this.dbg({
1629
+ type: "heartbeat",
1630
+ phase: "start",
1631
+ details: {
1632
+ intervalMs: this.hb.intervalMs,
1633
+ timeoutMs: this.hb.timeoutMs
1634
+ }
1799
1635
  });
1800
- (0, import_react4.useEffect)(() => {
1801
- trackHookTrigger2({
1802
- ref: onReceiveEffectDebugRef,
1803
- phase: "endpoint_on_receive_effect",
1804
- debug,
1805
- snapshot: {
1806
- endpointResultRef: describeObjectReference(endpointResult),
1807
- endpointDataRef: describeObjectReference(endpointResult.data),
1808
- toRoomsRef: describeObjectReference(toRooms)
1809
- }
1810
- });
1811
- const unsubscribe = endpointResult.onReceive((data) => {
1812
- setRoomState((prev) => {
1813
- const next = mergeRoomState(prev, toRooms(data));
1814
- return roomStateEqual(prev, next) ? prev : next;
1636
+ const tick = async () => {
1637
+ if (!socket) {
1638
+ this.dbg({
1639
+ type: "heartbeat",
1640
+ phase: "tick_skip",
1641
+ err: "Socket missing during heartbeat tick"
1815
1642
  });
1643
+ return;
1644
+ }
1645
+ const payload = await this.getSysEvent("sys:ping")({
1646
+ socket,
1647
+ client: this
1816
1648
  });
1817
- return unsubscribe;
1818
- }, [endpointResult, toRooms, debug]);
1819
- (0, import_react4.useEffect)(() => {
1820
- trackHookTrigger2({
1821
- ref: deriveRoomsEffectDebugRef,
1822
- phase: "derive_rooms_effect",
1823
- debug,
1824
- snapshot: {
1825
- endpointDataRef: describeObjectReference(endpointResult.data),
1826
- toRoomsRef: describeObjectReference(toRooms)
1827
- }
1828
- });
1829
- const next = roomsFromData(
1830
- endpointResult.data,
1831
- toRooms
1832
- );
1833
- setRoomState((prev) => roomStateEqual(prev, next) ? prev : next);
1834
- }, [endpointResult.data, toRooms, debug]);
1835
- (0, import_react4.useEffect)(() => {
1836
- trackHookTrigger2({
1837
- ref: joinRoomsEffectDebugRef,
1838
- phase: "join_rooms_effect",
1839
- debug,
1840
- snapshot: {
1841
- hasClient: !!client,
1842
- clientRef: describeObjectReference(client),
1843
- roomsKey,
1844
- joinMetaKey,
1845
- leaveMetaKey
1846
- }
1847
- });
1848
- if (!client) {
1849
- dbg2(debug, {
1850
- type: "room",
1851
- phase: "join_skip",
1852
- rooms: roomState.rooms,
1853
- reason: "socket_client_missing"
1649
+ const check = this.config.pingPayload.safeParse(payload);
1650
+ if (!check.success) {
1651
+ this.dbg({
1652
+ type: "heartbeat",
1653
+ phase: "validation_failed",
1654
+ err: "ping payload validation failed",
1655
+ details: this.getValidationDetails(check.error)
1854
1656
  });
1657
+ if (this.environment === "development") {
1658
+ console.warn(
1659
+ "[socket] ping schema validation failed",
1660
+ check.error.issues
1661
+ );
1662
+ }
1855
1663
  return;
1856
1664
  }
1857
- if (roomState.rooms.length === 0) {
1858
- dbg2(debug, {
1859
- type: "room",
1860
- phase: "join_skip",
1861
- rooms: [],
1862
- reason: "no_rooms"
1863
- });
1864
- return;
1865
- }
1866
- const { joinMeta, leaveMeta } = roomState;
1867
- if (!joinMeta || !leaveMeta) {
1868
- dbg2(debug, {
1869
- type: "room",
1870
- phase: "join_skip",
1871
- rooms: roomState.rooms,
1872
- reason: "missing_meta"
1873
- });
1874
- return;
1875
- }
1876
- let active = true;
1877
- dbg2(debug, {
1878
- type: "room",
1879
- phase: "join_attempt",
1880
- rooms: roomState.rooms
1881
- });
1882
- (async () => {
1883
- try {
1884
- await client.joinRooms(roomState.rooms, joinMeta);
1885
- } catch (err) {
1886
- dbg2(debug, {
1887
- type: "room",
1888
- phase: "join_error",
1889
- rooms: roomState.rooms,
1890
- err: String(err)
1891
- });
1892
- }
1893
- })();
1894
- return () => {
1895
- if (!active) return;
1896
- active = false;
1897
- if (roomState.rooms.length === 0) {
1898
- dbg2(debug, {
1899
- type: "room",
1900
- phase: "leave_skip",
1901
- rooms: [],
1902
- reason: "no_rooms"
1903
- });
1904
- return;
1905
- }
1906
- dbg2(debug, {
1907
- type: "room",
1908
- phase: "leave_attempt",
1909
- rooms: roomState.rooms
1910
- });
1911
- void client.leaveRooms(roomState.rooms, leaveMeta).catch((err) => {
1912
- dbg2(debug, {
1913
- type: "room",
1914
- phase: "leave_error",
1915
- rooms: roomState.rooms,
1916
- err: String(err)
1917
- });
1918
- });
1919
- };
1920
- }, [client, roomsKey, joinMetaKey, leaveMetaKey, debug]);
1921
- (0, import_react4.useEffect)(() => {
1922
- trackHookTrigger2({
1923
- ref: applySocketEffectDebugRef,
1924
- phase: "apply_socket_effect",
1925
- debug,
1926
- snapshot: {
1927
- hasClient: !!client,
1928
- clientRef: describeObjectReference(client),
1929
- applySocketKeys: Object.keys(applySocket).sort().join(","),
1930
- argsKey,
1931
- toRoomsRef: describeObjectReference(toRooms)
1932
- }
1665
+ const dataToSend = check.data;
1666
+ socket.emit("sys:ping", dataToSend);
1667
+ this.dbg({
1668
+ type: "heartbeat",
1669
+ phase: "ping_emit",
1670
+ payload: dataToSend
1933
1671
  });
1934
- if (!client) return;
1935
- const queue = [];
1936
- let draining = false;
1937
- let active = true;
1938
- const drainQueue = () => {
1939
- if (!active || draining) return;
1940
- draining = true;
1941
- try {
1942
- while (active && queue.length > 0) {
1943
- const nextUpdate = queue.shift();
1944
- if (!nextUpdate) continue;
1945
- built.setData(
1946
- (prev) => {
1947
- const next = nextUpdate.fn(
1948
- prev,
1949
- nextUpdate.payload,
1950
- nextUpdate.meta ? { ...nextUpdate.meta, args: useArgs } : { args: useArgs }
1951
- );
1952
- const nextRoomState = roomsFromData(
1953
- next,
1954
- toRooms
1955
- );
1956
- setRoomState(
1957
- (prevRoomState) => roomStateEqual(prevRoomState, nextRoomState) ? prevRoomState : nextRoomState
1958
- );
1959
- return next;
1960
- },
1961
- ...useArgs
1962
- );
1963
- }
1964
- } finally {
1965
- draining = false;
1966
- if (active && queue.length > 0) drainQueue();
1967
- }
1968
- };
1969
- const entries = Object.entries(applySocket).filter(
1970
- ([_event, fn]) => typeof fn === "function"
1971
- );
1972
- const unsubscribes = entries.map(
1973
- ([ev, fn]) => client.on(ev, (payload, meta) => {
1974
- queue.push({
1975
- fn,
1976
- payload,
1977
- ...meta ? { meta } : {}
1978
- });
1979
- drainQueue();
1980
- })
1981
- );
1982
- return () => {
1983
- active = false;
1984
- queue.length = 0;
1985
- unsubscribes.forEach((u) => u?.());
1986
- };
1987
- }, [client, applySocket, built, argsKey, toRooms, debug]);
1988
- return { ...endpointResult, rooms: roomState.rooms };
1989
- };
1990
- return {
1991
- ...rest,
1992
- useEndpoint
1993
- };
1994
- }
1995
-
1996
- // src/sockets/socket.client.index.ts
1997
- var SocketClient = class {
1998
- constructor(events, opts) {
1999
- this.hbTimer = null;
2000
- // stats
2001
- this.roomCounts = /* @__PURE__ */ new Map();
2002
- this.handlerMap = /* @__PURE__ */ new Map();
2003
- this.events = events;
2004
- this.socket = opts.socket ?? null;
2005
- this.environment = opts.environment ?? "development";
2006
- this.debug = opts.debug ?? {};
2007
- this.config = opts.config;
2008
- this.sysEvents = opts.sys;
2009
- this.roomJoinSchema = buildRoomPayloadSchema(this.config.joinMetaMessage);
2010
- this.roomLeaveSchema = buildRoomPayloadSchema(this.config.leaveMetaMessage);
2011
- const hb = opts.heartbeat ?? {};
2012
- this.hb = {
2013
- intervalMs: hb.intervalMs ?? 15e3,
2014
- timeoutMs: hb.timeoutMs ?? 7500
2015
1672
  };
1673
+ this.hbTimer = setInterval(tick, this.hb.intervalMs);
1674
+ tick();
1675
+ }
1676
+ /**
1677
+ * Public: stop the heartbeat loop.
1678
+ *
1679
+ * This is called by the default 'sys:disconnect' handler, but you can also
1680
+ * call it yourself from any sysHandler to fully control heartbeat lifecycle.
1681
+ */
1682
+ stopHeartbeat(reason) {
1683
+ const hadTimer = Boolean(this.hbTimer);
1684
+ if (this.hbTimer) {
1685
+ clearInterval(this.hbTimer);
1686
+ this.hbTimer = null;
1687
+ }
1688
+ this.dbg({
1689
+ type: "heartbeat",
1690
+ phase: "stop",
1691
+ reason,
1692
+ hadTimer
1693
+ });
1694
+ }
1695
+ emit(event, payload, metadata) {
2016
1696
  if (!this.socket) {
1697
+ this.dbg({ type: "emit", event, err: "Socket is null" });
1698
+ return;
1699
+ }
1700
+ const schema = this.events[event].message;
1701
+ const parsed = schema.safeParse(payload);
1702
+ if (!parsed.success) {
2017
1703
  this.dbg({
2018
- type: "lifecycle",
2019
- phase: "init",
2020
- err: "Socket reference is null during initialization"
1704
+ type: "emit",
1705
+ event,
1706
+ err: "payload_validation_failed",
1707
+ details: this.getValidationDetails(parsed.error)
2021
1708
  });
2022
- } else {
1709
+ throw new Error(`Invalid payload for "${event}": ${parsed.error.message}`);
1710
+ }
1711
+ this.socket.emit(String(event), parsed.data);
1712
+ this.dbg({
1713
+ type: "emit",
1714
+ event,
1715
+ metadata
1716
+ });
1717
+ }
1718
+ async joinRooms(rooms, meta) {
1719
+ if (!this.socket) {
2023
1720
  this.dbg({
2024
- type: "lifecycle",
2025
- phase: "init",
2026
- heartbeatIntervalMs: this.hb.intervalMs,
2027
- heartbeatTimeoutMs: this.hb.timeoutMs,
2028
- environment: this.environment
1721
+ type: "room",
1722
+ phase: "join",
1723
+ rooms: this.toArray(rooms),
1724
+ err: "Socket is null"
2029
1725
  });
2030
- this.logSocketConfigSnapshot("constructor");
1726
+ throw new Error("Socket is null in joinRooms method");
2031
1727
  }
2032
- this.onConnect = async () => {
2033
- if (!this.socket) {
2034
- this.dbg({
2035
- type: "connection",
2036
- phase: "connect_event",
2037
- err: "Socket is null"
2038
- });
2039
- throw new Error("Socket is null in onConnect handler");
2040
- }
1728
+ if (!await this.getSysEvent("sys:room_join")({
1729
+ rooms,
1730
+ meta,
1731
+ socket: this.socket,
1732
+ client: this
1733
+ })) {
2041
1734
  this.dbg({
2042
- type: "connection",
2043
- phase: "connect_event",
2044
- id: this.socket.id,
2045
- details: {
2046
- nsp: this.getNamespace(this.socket)
2047
- }
2048
- });
2049
- this.logSocketConfigSnapshot("connect_event");
2050
- await this.getSysEvent("sys:connect")({
2051
- socket: this.socket,
2052
- client: this
1735
+ type: "room",
1736
+ phase: "join",
1737
+ rooms: this.toArray(rooms),
1738
+ err: "sys:room_join handler aborted join"
2053
1739
  });
2054
- };
2055
- this.onReconnect = async (attempt) => {
2056
- if (!this.socket) {
2057
- this.dbg({
2058
- type: "connection",
2059
- phase: "reconnect_event",
2060
- err: "Socket is null"
2061
- });
2062
- throw new Error("Socket is null in onReconnect handler");
2063
- }
2064
- this.dbg({
2065
- type: "connection",
2066
- phase: "reconnect_event",
2067
- attempt,
2068
- id: this.socket.id,
2069
- details: {
2070
- nsp: this.getNamespace(this.socket)
2071
- }
2072
- });
2073
- this.logSocketConfigSnapshot("reconnect_event");
2074
- await this.getSysEvent("sys:reconnect")({
2075
- attempt,
2076
- socket: this.socket,
2077
- client: this
1740
+ return async () => {
1741
+ };
1742
+ }
1743
+ const list = this.toArray(rooms);
1744
+ const toJoin = [];
1745
+ for (const r of list) {
1746
+ const next = (this.roomCounts.get(r) ?? 0) + 1;
1747
+ this.roomCounts.set(r, next);
1748
+ if (next === 1) toJoin.push(r);
1749
+ }
1750
+ if (toJoin.length > 0 && this.socket) {
1751
+ const payloadResult = this.roomJoinSchema.safeParse({
1752
+ rooms: toJoin,
1753
+ meta
2078
1754
  });
2079
- };
2080
- this.onDisconnect = async (reason) => {
2081
- if (!this.socket) {
1755
+ if (!payloadResult.success) {
1756
+ this.rollbackJoinIncrement(toJoin);
2082
1757
  this.dbg({
2083
- type: "connection",
2084
- phase: "disconnect_event",
2085
- err: "Socket is null"
1758
+ type: "room",
1759
+ phase: "join",
1760
+ rooms: toJoin,
1761
+ err: "payload validation failed",
1762
+ details: this.getValidationDetails(payloadResult.error)
2086
1763
  });
2087
- throw new Error("Socket is null in onDisconnect handler");
1764
+ return async () => {
1765
+ };
2088
1766
  }
1767
+ const payload = payloadResult.data;
1768
+ const normalizedRooms = this.toArray(payload.rooms);
1769
+ this.socket.emit("sys:room_join", payload);
1770
+ this.dbg({ type: "room", phase: "join", rooms: normalizedRooms });
1771
+ }
1772
+ return async () => {
1773
+ await this.leaveRooms(rooms, meta);
1774
+ };
1775
+ }
1776
+ async leaveRooms(rooms, meta) {
1777
+ if (!this.socket) {
2089
1778
  this.dbg({
2090
- type: "connection",
2091
- phase: "disconnect_event",
2092
- reason: String(reason),
2093
- details: {
2094
- roomsTracked: this.roomCounts.size
2095
- }
2096
- });
2097
- this.logSocketConfigSnapshot("disconnect_event");
2098
- await this.getSysEvent("sys:disconnect")({
2099
- reason: String(reason),
2100
- socket: this.socket,
2101
- client: this
1779
+ type: "room",
1780
+ phase: "leave",
1781
+ rooms: this.toArray(rooms),
1782
+ err: "Socket is null"
2102
1783
  });
2103
- };
2104
- this.onConnectError = async (err) => {
2105
- if (!this.socket) {
2106
- this.dbg({
2107
- type: "connection",
2108
- phase: "connect_error_event",
2109
- err: "Socket is null"
2110
- });
2111
- throw new Error("Socket is null in onConnectError handler");
2112
- }
1784
+ throw new Error("Socket is null in leaveRooms method");
1785
+ }
1786
+ if (!await this.getSysEvent("sys:room_leave")({
1787
+ rooms,
1788
+ meta,
1789
+ socket: this.socket,
1790
+ client: this
1791
+ })) {
2113
1792
  this.dbg({
2114
- type: "connection",
2115
- phase: "connect_error_event",
2116
- err: String(err),
2117
- details: this.getVerboseDetails({ rawError: err })
1793
+ type: "room",
1794
+ phase: "leave",
1795
+ rooms: this.toArray(rooms),
1796
+ err: "sys:room_leave handler aborted leave"
2118
1797
  });
2119
- this.logSocketConfigSnapshot("connect_error_event");
2120
- await this.getSysEvent("sys:connect_error")({
2121
- error: String(err),
2122
- socket: this.socket,
2123
- client: this
1798
+ return;
1799
+ }
1800
+ const list = this.toArray(rooms);
1801
+ const toLeave = [];
1802
+ for (const r of list) {
1803
+ const curr = this.roomCounts.get(r) ?? 0;
1804
+ const next = Math.max(0, curr - 1);
1805
+ if (next === 0 && curr > 0) toLeave.push(r);
1806
+ if (next === 0) this.roomCounts.delete(r);
1807
+ else this.roomCounts.set(r, next);
1808
+ }
1809
+ if (toLeave.length > 0 && this.socket) {
1810
+ const payloadResult = this.roomLeaveSchema.safeParse({
1811
+ rooms: toLeave,
1812
+ meta
2124
1813
  });
2125
- };
2126
- this.onPong = async (raw) => {
2127
- if (!this.socket) {
1814
+ if (!payloadResult.success) {
1815
+ this.rollbackLeaveDecrement(toLeave);
2128
1816
  this.dbg({
2129
- type: "heartbeat",
2130
- phase: "pong_recv",
2131
- err: "Socket is null"
1817
+ type: "room",
1818
+ phase: "leave",
1819
+ rooms: toLeave,
1820
+ err: "payload validation failed",
1821
+ details: this.getValidationDetails(payloadResult.error)
2132
1822
  });
2133
- throw new Error("Socket is null in onPong handler");
1823
+ return;
2134
1824
  }
2135
- const parsed = this.config.pongPayload.safeParse(raw);
1825
+ const payload = payloadResult.data;
1826
+ const normalizedRooms = this.toArray(payload.rooms);
1827
+ this.socket.emit("sys:room_leave", payload);
1828
+ this.dbg({ type: "room", phase: "leave", rooms: normalizedRooms });
1829
+ }
1830
+ }
1831
+ on(event, handler) {
1832
+ const schema = this.events[event].message;
1833
+ this.dbg({ type: "register", phase: "register", event });
1834
+ if (!this.socket) {
1835
+ this.dbg({
1836
+ type: "register",
1837
+ phase: "register",
1838
+ event,
1839
+ err: "Socket is null"
1840
+ });
1841
+ return () => {
1842
+ };
1843
+ }
1844
+ const socket = this.socket;
1845
+ const toStringList = (value) => Array.isArray(value) ? value.filter((entry2) => typeof entry2 === "string") : [];
1846
+ const wrappedEnv = (envelope) => {
1847
+ const rawData = envelope.data;
1848
+ const parsed = schema.safeParse(rawData);
2136
1849
  if (!parsed.success) {
2137
1850
  this.dbg({
2138
- type: "heartbeat",
2139
- phase: "validation_failed",
2140
- err: `pong payload validation failed: ${parsed.error.message}`,
1851
+ type: "receive",
1852
+ event,
1853
+ err: "payload_validation_failed",
2141
1854
  details: this.getValidationDetails(parsed.error)
2142
1855
  });
2143
1856
  return;
2144
1857
  }
2145
- const validated = parsed.data;
1858
+ const data = parsed.data;
1859
+ const receivedAt = /* @__PURE__ */ new Date();
1860
+ const sentAt = envelope?.sentAt ? new Date(envelope.sentAt) : void 0;
1861
+ const meta = {
1862
+ envelope: {
1863
+ eventName: typeof envelope?.eventName === "string" ? envelope.eventName : event,
1864
+ sentAt: envelope?.sentAt ?? receivedAt.toISOString(),
1865
+ sentTo: toStringList(envelope?.sentTo),
1866
+ data,
1867
+ metadata: envelope?.metadata,
1868
+ rooms: toStringList(envelope?.rooms)
1869
+ },
1870
+ ctx: {
1871
+ receivedAt,
1872
+ latencyMs: sentAt ? Math.max(0, receivedAt.getTime() - sentAt.getTime()) : void 0,
1873
+ nsp: this.getNamespace(socket),
1874
+ socketId: socket.id,
1875
+ socket
1876
+ }
1877
+ };
2146
1878
  this.dbg({
2147
- type: "heartbeat",
2148
- phase: "pong_recv",
2149
- payload: validated
2150
- });
2151
- await this.getSysEvent("sys:pong")({
2152
- socket: this.socket,
2153
- payload: validated,
2154
- client: this
1879
+ type: "receive",
1880
+ event,
1881
+ envelope: this.debug.verbose ? {
1882
+ eventName: meta.envelope.eventName,
1883
+ sentAt: meta.envelope.sentAt,
1884
+ sentTo: meta.envelope.sentTo,
1885
+ metadata: meta.envelope.metadata
1886
+ } : void 0
2155
1887
  });
1888
+ handler(data, meta);
2156
1889
  };
2157
- if (this.socket) {
2158
- this.socket.on("connect", this.onConnect);
2159
- this.socket.on("reconnect", this.onReconnect);
2160
- this.socket.on("disconnect", this.onDisconnect);
2161
- this.socket.on("connect_error", this.onConnectError);
2162
- this.socket.on("sys:pong", this.onPong);
2163
- }
2164
- }
2165
- snapshotSocketConfig(socket) {
2166
- if (!socket) return null;
2167
- const manager = socket.io ?? null;
2168
- const base = {
2169
- nsp: this.getNamespace(socket)
1890
+ const wrappedDispatcher = (envelopeOrRaw) => {
1891
+ if (typeof envelopeOrRaw === "object" && envelopeOrRaw !== null && "eventName" in envelopeOrRaw && "sentAt" in envelopeOrRaw && "sentTo" in envelopeOrRaw && "data" in envelopeOrRaw) {
1892
+ wrappedEnv(envelopeOrRaw);
1893
+ } else {
1894
+ this.dbg({
1895
+ type: "receive",
1896
+ event,
1897
+ envelope: void 0
1898
+ });
1899
+ handler(envelopeOrRaw, void 0);
1900
+ }
2170
1901
  };
2171
- if (!manager) return base;
2172
- const opts = manager.opts ?? {};
2173
- const transports = Array.isArray(opts.transports) ? opts.transports.filter((t) => typeof t === "string") : void 0;
2174
- const transportName = typeof manager.engine?.transport?.name === "string" ? manager.engine.transport?.name : void 0;
2175
- const readyState = typeof manager._readyState === "string" ? manager._readyState : void 0;
2176
- const uri = typeof manager.uri === "string" ? manager.uri : void 0;
2177
- return {
2178
- ...base,
2179
- url: uri,
2180
- path: typeof opts.path === "string" ? opts.path : void 0,
2181
- transport: transportName ?? transports?.[0],
2182
- transports,
2183
- readyState,
2184
- autoConnect: typeof opts.autoConnect === "boolean" ? opts.autoConnect : void 0,
2185
- reconnection: typeof opts.reconnection === "boolean" ? opts.reconnection : void 0,
2186
- reconnectionAttempts: typeof opts.reconnectionAttempts === "number" ? opts.reconnectionAttempts : void 0,
2187
- reconnectionDelay: typeof opts.reconnectionDelay === "number" ? opts.reconnectionDelay : void 0,
2188
- reconnectionDelayMax: typeof opts.reconnectionDelayMax === "number" ? opts.reconnectionDelayMax : void 0,
2189
- timeout: typeof opts.timeout === "number" ? opts.timeout : void 0,
2190
- hostname: typeof opts.hostname === "string" ? opts.hostname : void 0,
2191
- port: typeof opts.port === "number" || typeof opts.port === "string" ? opts.port : void 0,
2192
- secure: typeof opts.secure === "boolean" ? opts.secure : void 0
1902
+ const errorWrapped = (e) => {
1903
+ this.dbg({ type: "receive", event, err: String(e) });
2193
1904
  };
2194
- }
2195
- logSocketConfigSnapshot(reason) {
2196
- if (!this.debug.logger || !this.debug.config) return;
2197
- const snapshot = this.snapshotSocketConfig(this.socket);
2198
- this.dbg({
2199
- type: "config",
2200
- phase: reason,
2201
- ...snapshot == null ? { err: "Socket is missing. " } : {
2202
- socketId: this.socket?.id,
2203
- snapshot
1905
+ socket.on(String(event), wrappedDispatcher);
1906
+ socket.on(`${String(event)}:error`, errorWrapped);
1907
+ let set = this.handlerMap.get(String(event));
1908
+ if (!set) {
1909
+ set = /* @__PURE__ */ new Set();
1910
+ this.handlerMap.set(String(event), set);
1911
+ }
1912
+ const entry = {
1913
+ orig: handler,
1914
+ wrapped: wrappedDispatcher,
1915
+ errorWrapped
1916
+ };
1917
+ set.add(entry);
1918
+ return () => {
1919
+ socket.off(String(event), wrappedDispatcher);
1920
+ socket.off(`${String(event)}:error`, errorWrapped);
1921
+ const s = this.handlerMap.get(String(event));
1922
+ if (s) {
1923
+ s.delete(entry);
1924
+ if (s.size === 0) this.handlerMap.delete(String(event));
2204
1925
  }
2205
- });
2206
- }
2207
- getValidationDetails(error) {
2208
- if (!this.debug.verbose) return void 0;
2209
- return { issues: error.issues };
2210
- }
2211
- getVerboseDetails(details) {
2212
- if (!this.debug.verbose) return void 0;
2213
- return details;
2214
- }
2215
- getNamespace(socket) {
2216
- if (!socket) return void 0;
2217
- const nsp = socket.nsp;
2218
- return typeof nsp === "string" ? nsp : void 0;
2219
- }
2220
- getSysEvent(name) {
2221
- return this.sysEvents[name];
2222
- }
2223
- dbg(e) {
2224
- const d = this.debug;
2225
- if (!d.logger) return;
2226
- if (!d[e.type]) return;
2227
- if (d.only && "event" in e && !d.only.includes(e.event)) return;
2228
- d.logger(e);
2229
- }
2230
- /** internal stats snapshot */
2231
- stats() {
2232
- const rooms = Array.from(this.roomCounts.entries()).map(
2233
- ([room, count]) => ({ room, count })
2234
- );
2235
- const handlers = Array.from(this.handlerMap.entries()).map(
2236
- ([event, set]) => ({
2237
- event,
2238
- handlers: set.size
2239
- })
2240
- );
2241
- return {
2242
- roomsCount: rooms.length,
2243
- totalHandlers: handlers.reduce((a, b) => a + b.handlers, 0),
2244
- rooms,
2245
- handlers
1926
+ this.dbg({ type: "register", phase: "unregister", event });
2246
1927
  };
2247
1928
  }
2248
- toArray(rooms) {
2249
- return rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms];
2250
- }
2251
- rollbackJoinIncrement(rooms) {
2252
- for (const room of rooms) {
2253
- const curr = this.roomCounts.get(room) ?? 0;
2254
- const next = Math.max(0, curr - 1);
2255
- if (next === 0) this.roomCounts.delete(room);
2256
- else this.roomCounts.set(room, next);
2257
- }
2258
- }
2259
- rollbackLeaveDecrement(rooms) {
2260
- for (const room of rooms) {
2261
- const curr = this.roomCounts.get(room) ?? 0;
2262
- this.roomCounts.set(room, curr + 1);
2263
- }
2264
- }
2265
1929
  /**
2266
- * Public: start the heartbeat loop manually.
2267
- *
2268
- * This is called by the default 'sys:connect' handler, but you can also
2269
- * call it yourself from any sysHandler to change when heartbeats start.
1930
+ * Remove all listeners, stop timers, and leave rooms.
1931
+ * Call when disposing the client instance.
2270
1932
  */
2271
- startHeartbeat() {
2272
- this.stopHeartbeat("stop_before_start");
2273
- if (!this.socket) {
2274
- this.dbg({
2275
- type: "heartbeat",
2276
- phase: "start",
2277
- err: "Socket is null"
2278
- });
2279
- return;
2280
- }
1933
+ async destroy(leaveMeta) {
2281
1934
  const socket = this.socket;
2282
1935
  this.dbg({
2283
- type: "heartbeat",
2284
- phase: "start",
1936
+ type: "lifecycle",
1937
+ phase: "destroy_begin",
1938
+ socketId: socket?.id,
2285
1939
  details: {
2286
- intervalMs: this.hb.intervalMs,
2287
- timeoutMs: this.hb.timeoutMs
1940
+ roomsTracked: this.roomCounts.size,
1941
+ handlerEvents: this.handlerMap.size
2288
1942
  }
2289
1943
  });
2290
- const tick = async () => {
2291
- if (!socket) {
2292
- this.dbg({
2293
- type: "heartbeat",
2294
- phase: "tick_skip",
2295
- err: "Socket missing during heartbeat tick"
2296
- });
2297
- return;
2298
- }
2299
- const payload = await this.getSysEvent("sys:ping")({
2300
- socket,
2301
- client: this
2302
- });
2303
- const check = this.config.pingPayload.safeParse(payload);
2304
- if (!check.success) {
2305
- this.dbg({
2306
- type: "heartbeat",
2307
- phase: "validation_failed",
2308
- err: "ping payload validation failed",
2309
- details: this.getValidationDetails(check.error)
2310
- });
2311
- if (this.environment === "development") {
2312
- console.warn(
2313
- "[socket] ping schema validation failed",
2314
- check.error.issues
2315
- );
1944
+ this.stopHeartbeat("destroy");
1945
+ if (socket) {
1946
+ socket.off("connect", this.onConnect);
1947
+ socket.off("reconnect", this.onReconnect);
1948
+ socket.off("disconnect", this.onDisconnect);
1949
+ socket.off("connect_error", this.onConnectError);
1950
+ socket.off("sys:pong", this.onPong);
1951
+ for (const [event, set] of this.handlerMap.entries()) {
1952
+ for (const entry of set) {
1953
+ socket.off(String(event), entry.wrapped);
1954
+ socket.off(`${String(event)}:error`, entry.errorWrapped);
2316
1955
  }
2317
- return;
2318
1956
  }
2319
- const dataToSend = check.data;
2320
- socket.emit("sys:ping", dataToSend);
2321
- this.dbg({
2322
- type: "heartbeat",
2323
- phase: "ping_emit",
2324
- payload: dataToSend
2325
- });
2326
- };
2327
- this.hbTimer = setInterval(tick, this.hb.intervalMs);
2328
- tick();
2329
- }
2330
- /**
2331
- * Public: stop the heartbeat loop.
2332
- *
2333
- * This is called by the default 'sys:disconnect' handler, but you can also
2334
- * call it yourself from any sysHandler to fully control heartbeat lifecycle.
2335
- */
2336
- stopHeartbeat(reason) {
2337
- const hadTimer = Boolean(this.hbTimer);
2338
- if (this.hbTimer) {
2339
- clearInterval(this.hbTimer);
2340
- this.hbTimer = null;
2341
1957
  }
1958
+ this.handlerMap.clear();
1959
+ const toLeave = Array.from(this.roomCounts.entries()).filter(([, count]) => count > 0).map(([room]) => room);
1960
+ if (toLeave.length > 0 && socket) {
1961
+ await this.leaveRooms(toLeave, leaveMeta);
1962
+ }
1963
+ this.roomCounts.clear();
2342
1964
  this.dbg({
2343
- type: "heartbeat",
2344
- phase: "stop",
2345
- reason,
2346
- hadTimer
1965
+ type: "lifecycle",
1966
+ phase: "destroy_complete",
1967
+ socketId: socket?.id,
1968
+ details: {
1969
+ roomsTracked: this.roomCounts.size,
1970
+ handlerEvents: this.handlerMap.size
1971
+ }
2347
1972
  });
2348
1973
  }
2349
- emit(event, payload, metadata) {
1974
+ /** Pass-throughs. Managing connection is the caller’s responsibility. */
1975
+ disconnect() {
2350
1976
  if (!this.socket) {
2351
- this.dbg({ type: "emit", event, err: "Socket is null" });
2352
- return;
2353
- }
2354
- const schema = this.events[event].message;
2355
- const parsed = schema.safeParse(payload);
2356
- if (!parsed.success) {
2357
1977
  this.dbg({
2358
- type: "emit",
2359
- event,
2360
- err: "payload_validation_failed",
2361
- details: this.getValidationDetails(parsed.error)
1978
+ type: "connection",
1979
+ phase: "disconnect",
1980
+ reason: "client_disconnect",
1981
+ err: "Socket is null"
2362
1982
  });
2363
- throw new Error(`Invalid payload for "${event}": ${parsed.error.message}`);
1983
+ return;
2364
1984
  }
2365
- this.socket.emit(String(event), parsed.data);
1985
+ this.stopHeartbeat("disconnect");
1986
+ this.socket.disconnect();
2366
1987
  this.dbg({
2367
- type: "emit",
2368
- event,
2369
- metadata
1988
+ type: "connection",
1989
+ phase: "disconnect",
1990
+ reason: "client_disconnect",
1991
+ details: {
1992
+ nsp: this.getNamespace(this.socket)
1993
+ }
2370
1994
  });
1995
+ this.logSocketConfigSnapshot("disconnect_call");
2371
1996
  }
2372
- async joinRooms(rooms, meta) {
1997
+ connect() {
2373
1998
  if (!this.socket) {
2374
1999
  this.dbg({
2375
- type: "room",
2376
- phase: "join",
2377
- rooms: this.toArray(rooms),
2000
+ type: "connection",
2001
+ phase: "connect",
2378
2002
  err: "Socket is null"
2379
2003
  });
2380
- throw new Error("Socket is null in joinRooms method");
2381
- }
2382
- if (!await this.getSysEvent("sys:room_join")({
2383
- rooms,
2384
- meta,
2385
- socket: this.socket,
2386
- client: this
2387
- })) {
2388
- this.dbg({
2389
- type: "room",
2390
- phase: "join",
2391
- rooms: this.toArray(rooms),
2392
- err: "sys:room_join handler aborted join"
2393
- });
2394
- return async () => {
2395
- };
2396
- }
2397
- const list = this.toArray(rooms);
2398
- const toJoin = [];
2399
- for (const r of list) {
2400
- const next = (this.roomCounts.get(r) ?? 0) + 1;
2401
- this.roomCounts.set(r, next);
2402
- if (next === 1) toJoin.push(r);
2403
- }
2404
- if (toJoin.length > 0 && this.socket) {
2405
- const payloadResult = this.roomJoinSchema.safeParse({
2406
- rooms: toJoin,
2407
- meta
2408
- });
2409
- if (!payloadResult.success) {
2410
- this.rollbackJoinIncrement(toJoin);
2411
- this.dbg({
2412
- type: "room",
2413
- phase: "join",
2414
- rooms: toJoin,
2415
- err: "payload validation failed",
2416
- details: this.getValidationDetails(payloadResult.error)
2417
- });
2418
- return async () => {
2419
- };
2420
- }
2421
- const payload = payloadResult.data;
2422
- const normalizedRooms = this.toArray(payload.rooms);
2423
- this.socket.emit("sys:room_join", payload);
2424
- this.dbg({ type: "room", phase: "join", rooms: normalizedRooms });
2425
- }
2426
- return async () => {
2427
- await this.leaveRooms(rooms, meta);
2428
- };
2429
- }
2430
- async leaveRooms(rooms, meta) {
2431
- if (!this.socket) {
2432
- this.dbg({
2433
- type: "room",
2434
- phase: "leave",
2435
- rooms: this.toArray(rooms),
2436
- err: "Socket is null"
2437
- });
2438
- throw new Error("Socket is null in leaveRooms method");
2439
- }
2440
- if (!await this.getSysEvent("sys:room_leave")({
2441
- rooms,
2442
- meta,
2443
- socket: this.socket,
2444
- client: this
2445
- })) {
2446
- this.dbg({
2447
- type: "room",
2448
- phase: "leave",
2449
- rooms: this.toArray(rooms),
2450
- err: "sys:room_leave handler aborted leave"
2451
- });
2452
- return;
2453
- }
2454
- const list = this.toArray(rooms);
2455
- const toLeave = [];
2456
- for (const r of list) {
2457
- const curr = this.roomCounts.get(r) ?? 0;
2458
- const next = Math.max(0, curr - 1);
2459
- if (next === 0 && curr > 0) toLeave.push(r);
2460
- if (next === 0) this.roomCounts.delete(r);
2461
- else this.roomCounts.set(r, next);
2462
- }
2463
- if (toLeave.length > 0 && this.socket) {
2464
- const payloadResult = this.roomLeaveSchema.safeParse({
2465
- rooms: toLeave,
2466
- meta
2467
- });
2468
- if (!payloadResult.success) {
2469
- this.rollbackLeaveDecrement(toLeave);
2470
- this.dbg({
2471
- type: "room",
2472
- phase: "leave",
2473
- rooms: toLeave,
2474
- err: "payload validation failed",
2475
- details: this.getValidationDetails(payloadResult.error)
2476
- });
2477
- return;
2478
- }
2479
- const payload = payloadResult.data;
2480
- const normalizedRooms = this.toArray(payload.rooms);
2481
- this.socket.emit("sys:room_leave", payload);
2482
- this.dbg({ type: "room", phase: "leave", rooms: normalizedRooms });
2483
- }
2484
- }
2485
- on(event, handler) {
2486
- const schema = this.events[event].message;
2487
- this.dbg({ type: "register", phase: "register", event });
2488
- if (!this.socket) {
2489
- this.dbg({
2490
- type: "register",
2491
- phase: "register",
2492
- event,
2493
- err: "Socket is null"
2494
- });
2495
- return () => {
2496
- };
2497
- }
2498
- const socket = this.socket;
2499
- const toStringList = (value) => Array.isArray(value) ? value.filter((entry2) => typeof entry2 === "string") : [];
2500
- const wrappedEnv = (envelope) => {
2501
- const rawData = envelope.data;
2502
- const parsed = schema.safeParse(rawData);
2503
- if (!parsed.success) {
2504
- this.dbg({
2505
- type: "receive",
2506
- event,
2507
- err: "payload_validation_failed",
2508
- details: this.getValidationDetails(parsed.error)
2509
- });
2510
- return;
2511
- }
2512
- const data = parsed.data;
2513
- const receivedAt = /* @__PURE__ */ new Date();
2514
- const sentAt = envelope?.sentAt ? new Date(envelope.sentAt) : void 0;
2515
- const meta = {
2516
- envelope: {
2517
- eventName: typeof envelope?.eventName === "string" ? envelope.eventName : event,
2518
- sentAt: envelope?.sentAt ?? receivedAt.toISOString(),
2519
- sentTo: toStringList(envelope?.sentTo),
2520
- data,
2521
- metadata: envelope?.metadata,
2522
- rooms: toStringList(envelope?.rooms)
2523
- },
2524
- ctx: {
2525
- receivedAt,
2526
- latencyMs: sentAt ? Math.max(0, receivedAt.getTime() - sentAt.getTime()) : void 0,
2527
- nsp: this.getNamespace(socket),
2528
- socketId: socket.id,
2529
- socket
2530
- }
2531
- };
2532
- this.dbg({
2533
- type: "receive",
2534
- event,
2535
- envelope: this.debug.verbose ? {
2536
- eventName: meta.envelope.eventName,
2537
- sentAt: meta.envelope.sentAt,
2538
- sentTo: meta.envelope.sentTo,
2539
- metadata: meta.envelope.metadata
2540
- } : void 0
2541
- });
2542
- handler(data, meta);
2543
- };
2544
- const wrappedDispatcher = (envelopeOrRaw) => {
2545
- if (typeof envelopeOrRaw === "object" && envelopeOrRaw !== null && "eventName" in envelopeOrRaw && "sentAt" in envelopeOrRaw && "sentTo" in envelopeOrRaw && "data" in envelopeOrRaw) {
2546
- wrappedEnv(envelopeOrRaw);
2547
- } else {
2548
- this.dbg({
2549
- type: "receive",
2550
- event,
2551
- envelope: void 0
2552
- });
2553
- handler(envelopeOrRaw, void 0);
2554
- }
2555
- };
2556
- const errorWrapped = (e) => {
2557
- this.dbg({ type: "receive", event, err: String(e) });
2558
- };
2559
- socket.on(String(event), wrappedDispatcher);
2560
- socket.on(`${String(event)}:error`, errorWrapped);
2561
- let set = this.handlerMap.get(String(event));
2562
- if (!set) {
2563
- set = /* @__PURE__ */ new Set();
2564
- this.handlerMap.set(String(event), set);
2565
- }
2566
- const entry = {
2567
- orig: handler,
2568
- wrapped: wrappedDispatcher,
2569
- errorWrapped
2570
- };
2571
- set.add(entry);
2572
- return () => {
2573
- socket.off(String(event), wrappedDispatcher);
2574
- socket.off(`${String(event)}:error`, errorWrapped);
2575
- const s = this.handlerMap.get(String(event));
2576
- if (s) {
2577
- s.delete(entry);
2578
- if (s.size === 0) this.handlerMap.delete(String(event));
2579
- }
2580
- this.dbg({ type: "register", phase: "unregister", event });
2581
- };
2582
- }
2583
- /**
2584
- * Remove all listeners, stop timers, and leave rooms.
2585
- * Call when disposing the client instance.
2586
- */
2587
- async destroy(leaveMeta) {
2588
- const socket = this.socket;
2589
- this.dbg({
2590
- type: "lifecycle",
2591
- phase: "destroy_begin",
2592
- socketId: socket?.id,
2593
- details: {
2594
- roomsTracked: this.roomCounts.size,
2595
- handlerEvents: this.handlerMap.size
2596
- }
2597
- });
2598
- this.stopHeartbeat("destroy");
2599
- if (socket) {
2600
- socket.off("connect", this.onConnect);
2601
- socket.off("reconnect", this.onReconnect);
2602
- socket.off("disconnect", this.onDisconnect);
2603
- socket.off("connect_error", this.onConnectError);
2604
- socket.off("sys:pong", this.onPong);
2605
- for (const [event, set] of this.handlerMap.entries()) {
2606
- for (const entry of set) {
2607
- socket.off(String(event), entry.wrapped);
2608
- socket.off(`${String(event)}:error`, entry.errorWrapped);
2609
- }
2610
- }
2611
- }
2612
- this.handlerMap.clear();
2613
- const toLeave = Array.from(this.roomCounts.entries()).filter(([, count]) => count > 0).map(([room]) => room);
2614
- if (toLeave.length > 0 && socket) {
2615
- await this.leaveRooms(toLeave, leaveMeta);
2616
- }
2617
- this.roomCounts.clear();
2618
- this.dbg({
2619
- type: "lifecycle",
2620
- phase: "destroy_complete",
2621
- socketId: socket?.id,
2622
- details: {
2623
- roomsTracked: this.roomCounts.size,
2624
- handlerEvents: this.handlerMap.size
2625
- }
2626
- });
2627
- }
2628
- /** Pass-throughs. Managing connection is the caller’s responsibility. */
2629
- disconnect() {
2630
- if (!this.socket) {
2631
- this.dbg({
2632
- type: "connection",
2633
- phase: "disconnect",
2634
- reason: "client_disconnect",
2635
- err: "Socket is null"
2636
- });
2637
- return;
2638
- }
2639
- this.stopHeartbeat("disconnect");
2640
- this.socket.disconnect();
2641
- this.dbg({
2642
- type: "connection",
2643
- phase: "disconnect",
2644
- reason: "client_disconnect",
2645
- details: {
2646
- nsp: this.getNamespace(this.socket)
2647
- }
2648
- });
2649
- this.logSocketConfigSnapshot("disconnect_call");
2650
- }
2651
- connect() {
2652
- if (!this.socket) {
2653
- this.dbg({
2654
- type: "connection",
2655
- phase: "connect",
2656
- err: "Socket is null"
2657
- });
2658
- return;
2004
+ return;
2659
2005
  }
2660
2006
  this.socket.connect();
2661
2007
  this.dbg({
@@ -2669,6 +2015,693 @@ var SocketClient = class {
2669
2015
  this.logSocketConfigSnapshot("connect_call");
2670
2016
  }
2671
2017
  };
2018
+
2019
+ // src/sockets/socket.client.context.provider.tsx
2020
+ var React3 = __toESM(require("react"), 1);
2021
+
2022
+ // src/sockets/socket.client.context.client.ts
2023
+ var React = __toESM(require("react"), 1);
2024
+ var SocketCtx = React.createContext(null);
2025
+ function useSocketClient() {
2026
+ const ctx = React.useContext(SocketCtx);
2027
+ if (!ctx)
2028
+ throw new Error("SocketClient not found. Wrap with <SocketProvider>.");
2029
+ return ctx;
2030
+ }
2031
+
2032
+ // src/sockets/socket.client.context.connection.ts
2033
+ var React2 = __toESM(require("react"), 1);
2034
+ function useSocketConnection(args) {
2035
+ const {
2036
+ event,
2037
+ rooms,
2038
+ onMessage,
2039
+ onCleanup,
2040
+ autoJoin = true,
2041
+ autoLeave = true
2042
+ } = args;
2043
+ const client = useSocketClient();
2044
+ const normalizedRooms = React2.useMemo(
2045
+ () => rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms],
2046
+ [rooms]
2047
+ );
2048
+ React2.useEffect(() => {
2049
+ if (autoJoin && normalizedRooms.length > 0)
2050
+ client.joinRooms(normalizedRooms, args.joinMeta);
2051
+ const unsubscribe = client.on(event, (payload, meta) => {
2052
+ onMessage(payload, meta);
2053
+ });
2054
+ return () => {
2055
+ unsubscribe();
2056
+ if (autoLeave && normalizedRooms.length > 0)
2057
+ client.leaveRooms(normalizedRooms, args.leaveMeta);
2058
+ if (onCleanup) onCleanup();
2059
+ };
2060
+ }, [client, event, onMessage, autoJoin, autoLeave, ...normalizedRooms]);
2061
+ }
2062
+
2063
+ // src/sockets/socket.client.context.debug.ts
2064
+ function dbg(dbgOpts, e) {
2065
+ if (!dbgOpts?.logger) return;
2066
+ if (!dbgOpts[e.type]) return;
2067
+ dbgOpts.logger(e);
2068
+ }
2069
+ function isProbablySocket(value) {
2070
+ if (!value || typeof value !== "object") return false;
2071
+ const anyVal = value;
2072
+ const ctorName = anyVal.constructor?.name;
2073
+ if (ctorName === "Socket") return true;
2074
+ return ("connected" in anyVal || "recovered" in anyVal) && typeof anyVal.on === "function" && typeof anyVal.emit === "function";
2075
+ }
2076
+ function describeSocketLike(value) {
2077
+ if (!value) return null;
2078
+ const id = value.id ?? "unknown";
2079
+ const connected = value.connected ?? false;
2080
+ const recovered = typeof value.recovered === "boolean" ? ` recovered=${value.recovered}` : "";
2081
+ return `[Socket id=${id} connected=${connected}${recovered}]`;
2082
+ }
2083
+ function safeDescribeHookValue(value) {
2084
+ if (value == null) return value;
2085
+ const valueType = typeof value;
2086
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") {
2087
+ return value;
2088
+ }
2089
+ if (valueType === "bigint" || valueType === "symbol") return String(value);
2090
+ if (valueType === "function")
2091
+ return `[function ${value.name || "anonymous"}]`;
2092
+ if (Array.isArray(value)) return `[array length=${value.length}]`;
2093
+ if (isProbablySocket(value)) return describeSocketLike(value);
2094
+ const ctorName = value.constructor?.name ?? "object";
2095
+ const keys = Object.keys(value);
2096
+ const keyPreview = keys.slice(0, 4).join(",");
2097
+ const suffix = keys.length > 4 ? ",\u2026" : "";
2098
+ return `[${ctorName} keys=${keyPreview}${suffix}]`;
2099
+ }
2100
+ function summarizeEvents(events) {
2101
+ if (!events || typeof events !== "object")
2102
+ return safeDescribeHookValue(events);
2103
+ const keys = Object.keys(events);
2104
+ if (!keys.length) return "[events empty]";
2105
+ const preview = keys.slice(0, 4).join(",");
2106
+ const suffix = keys.length > 4 ? ",\u2026" : "";
2107
+ return `[events count=${keys.length} keys=${preview}${suffix}]`;
2108
+ }
2109
+ function summarizeBaseOptions(options) {
2110
+ if (!options || typeof options !== "object")
2111
+ return safeDescribeHookValue(options);
2112
+ const obj = options;
2113
+ const keys = Object.keys(obj);
2114
+ if (!keys.length) return "[baseOptions empty]";
2115
+ const preview = keys.slice(0, 4).join(",");
2116
+ const suffix = keys.length > 4 ? ",\u2026" : "";
2117
+ const hasDebug = "debug" in obj;
2118
+ return `[baseOptions keys=${preview}${suffix} debug=${hasDebug}]`;
2119
+ }
2120
+ function summarizeMeta(meta, label) {
2121
+ if (meta == null) return null;
2122
+ if (typeof meta !== "object") return safeDescribeHookValue(meta);
2123
+ const keys = Object.keys(meta);
2124
+ if (!keys.length) return `[${label} empty]`;
2125
+ const preview = keys.slice(0, 4).join(",");
2126
+ const suffix = keys.length > 4 ? ",\u2026" : "";
2127
+ return `[${label} keys=${preview}${suffix}]`;
2128
+ }
2129
+ function createHookDebugEvent(prev, next, hook) {
2130
+ const reason = prev ? "change" : "init";
2131
+ const changed = Object.keys(next).reduce((acc, dependency) => {
2132
+ const prevValue = prev ? prev[dependency] : void 0;
2133
+ const nextValue = next[dependency];
2134
+ if (!prev || !Object.is(prevValue, nextValue)) {
2135
+ acc.push({
2136
+ dependency,
2137
+ previous: safeDescribeHookValue(prevValue),
2138
+ next: safeDescribeHookValue(nextValue)
2139
+ });
2140
+ }
2141
+ return acc;
2142
+ }, []);
2143
+ if (!changed.length) return null;
2144
+ return { type: "hook", phase: hook, reason, changes: changed };
2145
+ }
2146
+ function trackHookTrigger({
2147
+ ref,
2148
+ hook,
2149
+ providerDebug,
2150
+ snapshot
2151
+ }) {
2152
+ const prev = ref.current;
2153
+ ref.current = snapshot;
2154
+ if (!providerDebug?.logger || !providerDebug?.hook) return;
2155
+ const event = createHookDebugEvent(prev, snapshot, hook);
2156
+ if (event) dbg(providerDebug, event);
2157
+ }
2158
+
2159
+ // src/sockets/socket.client.context.provider.tsx
2160
+ var import_jsx_runtime = require("react/jsx-runtime");
2161
+ function buildSocketProvider(args) {
2162
+ const { events, options: baseOptions } = args;
2163
+ return {
2164
+ SocketProvider: (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2165
+ SocketProvider,
2166
+ {
2167
+ events,
2168
+ baseOptions,
2169
+ providerDebug: baseOptions.debug,
2170
+ ...props
2171
+ }
2172
+ ),
2173
+ useSocketClient: () => useSocketClient(),
2174
+ useSocketConnection: (p) => useSocketConnection(p)
2175
+ };
2176
+ }
2177
+ function SocketProvider(props) {
2178
+ const {
2179
+ events,
2180
+ baseOptions,
2181
+ children,
2182
+ fallback,
2183
+ providerDebug,
2184
+ destroyLeaveMeta
2185
+ } = props;
2186
+ const [resolvedSocket, setResolvedSocket] = React3.useState(
2187
+ null
2188
+ );
2189
+ const socket = "socket" in props ? props.socket ?? null : resolvedSocket;
2190
+ const providerDebugRef = React3.useRef();
2191
+ providerDebugRef.current = providerDebug;
2192
+ const resolveEffectDebugRef = React3.useRef(null);
2193
+ const clientMemoDebugRef = React3.useRef(null);
2194
+ const destroyEffectDebugRef = React3.useRef(null);
2195
+ React3.useEffect(() => {
2196
+ trackHookTrigger({
2197
+ ref: resolveEffectDebugRef,
2198
+ hook: "resolve_effect",
2199
+ providerDebug: providerDebugRef.current,
2200
+ snapshot: {
2201
+ resolvedSocket: describeSocketLike(resolvedSocket)
2202
+ }
2203
+ });
2204
+ if (!("getSocket" in props)) return;
2205
+ let cancelled = false;
2206
+ dbg(providerDebugRef.current, { type: "resolve", phase: "start" });
2207
+ if (!resolvedSocket) {
2208
+ Promise.resolve(props.getSocket()).then((s) => {
2209
+ if (cancelled) {
2210
+ dbg(providerDebugRef.current, {
2211
+ type: "resolve",
2212
+ phase: "cancelled"
2213
+ });
2214
+ return;
2215
+ }
2216
+ if (!s) {
2217
+ dbg(providerDebugRef.current, {
2218
+ type: "resolve",
2219
+ phase: "socketMissing"
2220
+ });
2221
+ return;
2222
+ }
2223
+ setResolvedSocket(s);
2224
+ dbg(providerDebugRef.current, { type: "resolve", phase: "ok" });
2225
+ }).catch((err) => {
2226
+ if (cancelled) return;
2227
+ dbg(providerDebugRef.current, {
2228
+ type: "resolve",
2229
+ phase: "error",
2230
+ err: String(err)
2231
+ });
2232
+ });
2233
+ }
2234
+ return () => {
2235
+ cancelled = true;
2236
+ };
2237
+ }, [resolvedSocket]);
2238
+ trackHookTrigger({
2239
+ ref: clientMemoDebugRef,
2240
+ hook: "client_memo",
2241
+ providerDebug: providerDebugRef.current,
2242
+ snapshot: {
2243
+ events: summarizeEvents(events),
2244
+ baseOptions: summarizeBaseOptions(baseOptions),
2245
+ socket: describeSocketLike(socket)
2246
+ }
2247
+ });
2248
+ const client = React3.useMemo(() => {
2249
+ if (!socket) {
2250
+ dbg(providerDebugRef.current, {
2251
+ type: "client",
2252
+ phase: "init",
2253
+ missing: true
2254
+ });
2255
+ return null;
2256
+ }
2257
+ const c = new SocketClient(events, { ...baseOptions, socket });
2258
+ dbg(providerDebugRef.current, {
2259
+ type: "client",
2260
+ phase: "init",
2261
+ missing: false
2262
+ });
2263
+ return c;
2264
+ }, [events, baseOptions, socket]);
2265
+ const destroyLeaveMetaRef = React3.useRef(destroyLeaveMeta);
2266
+ React3.useEffect(() => {
2267
+ destroyLeaveMetaRef.current = destroyLeaveMeta;
2268
+ }, [destroyLeaveMeta]);
2269
+ React3.useEffect(() => {
2270
+ trackHookTrigger({
2271
+ ref: destroyEffectDebugRef,
2272
+ hook: "destroy_effect",
2273
+ providerDebug: providerDebugRef.current,
2274
+ snapshot: {
2275
+ hasClient: !!client,
2276
+ destroyLeaveMeta: summarizeMeta(
2277
+ destroyLeaveMetaRef.current,
2278
+ "destroyLeaveMeta"
2279
+ )
2280
+ }
2281
+ });
2282
+ return () => {
2283
+ if (client) {
2284
+ client.destroy(destroyLeaveMetaRef.current);
2285
+ dbg(providerDebugRef.current, { type: "client", phase: "destroy" });
2286
+ }
2287
+ };
2288
+ }, [client]);
2289
+ dbg(providerDebugRef.current, { type: "render", hasClient: !!client });
2290
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SocketCtx.Provider, { value: client, children: client == null ? fallback ?? children : children });
2291
+ }
2292
+
2293
+ // src/sockets/socketedRoute/socket.client.helper.route.ts
2294
+ var import_react4 = require("react");
2295
+
2296
+ // src/sockets/socketedRoute/socket.client.helper.debug.ts
2297
+ var objectReferenceIds = /* @__PURE__ */ new WeakMap();
2298
+ var objectReferenceCounter = 0;
2299
+ function describeObjectReference(value) {
2300
+ if (value == null) return null;
2301
+ const valueType = typeof value;
2302
+ if (valueType !== "object" && valueType !== "function") return null;
2303
+ const obj = value;
2304
+ let id = objectReferenceIds.get(obj);
2305
+ if (!id) {
2306
+ id = ++objectReferenceCounter;
2307
+ objectReferenceIds.set(obj, id);
2308
+ }
2309
+ return `ref#${id}`;
2310
+ }
2311
+ function safeJsonKey(value) {
2312
+ try {
2313
+ const serialized = JSON.stringify(value ?? null);
2314
+ return serialized ?? "null";
2315
+ } catch {
2316
+ return "[unserializable]";
2317
+ }
2318
+ }
2319
+ function isSameObjectReference(prev, next) {
2320
+ if (prev !== next) return false;
2321
+ if (next == null) return false;
2322
+ const valueType = typeof next;
2323
+ return valueType === "object" || valueType === "function";
2324
+ }
2325
+ function shouldWarnSocketMutationGuard() {
2326
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
2327
+ return nodeEnv !== "production";
2328
+ }
2329
+ function safeDescribeHookValue2(value) {
2330
+ if (value == null) return value;
2331
+ const valueType = typeof value;
2332
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") {
2333
+ return value;
2334
+ }
2335
+ if (valueType === "bigint" || valueType === "symbol") return String(value);
2336
+ if (valueType === "function")
2337
+ return `[function ${value.name || "anonymous"}]`;
2338
+ if (Array.isArray(value)) return `[array length=${value.length}]`;
2339
+ const ctorName = value.constructor?.name ?? "object";
2340
+ const keys = Object.keys(value);
2341
+ const keyPreview = keys.slice(0, 4).join(",");
2342
+ const suffix = keys.length > 4 ? ",\u2026" : "";
2343
+ const ref = describeObjectReference(value);
2344
+ return `[${ctorName}${ref ? ` ${ref}` : ""} keys=${keyPreview}${suffix}]`;
2345
+ }
2346
+ function createHookDebugEvent2(prev, next, phase) {
2347
+ const reason = prev ? "change" : "init";
2348
+ const changed = Object.keys(next).reduce((acc, dependency) => {
2349
+ const prevValue = prev ? prev[dependency] : void 0;
2350
+ const nextValue = next[dependency];
2351
+ if (!prev || !Object.is(prevValue, nextValue)) {
2352
+ acc.push({
2353
+ dependency,
2354
+ previous: safeDescribeHookValue2(prevValue),
2355
+ next: safeDescribeHookValue2(nextValue)
2356
+ });
2357
+ }
2358
+ return acc;
2359
+ }, []);
2360
+ if (!changed.length) return null;
2361
+ return { type: "hook", phase, reason, changes: changed };
2362
+ }
2363
+ function dbg2(debug, event) {
2364
+ if (!debug?.logger) return;
2365
+ if (!debug[event.type]) return;
2366
+ debug.logger(event);
2367
+ }
2368
+ function trackHookTrigger2({
2369
+ ref,
2370
+ phase,
2371
+ debug,
2372
+ snapshot
2373
+ }) {
2374
+ const prev = ref.current;
2375
+ ref.current = snapshot;
2376
+ if (!debug?.logger || !debug?.hook) return;
2377
+ const event = createHookDebugEvent2(prev, snapshot, phase);
2378
+ if (event) dbg2(debug, event);
2379
+ }
2380
+
2381
+ // src/sockets/socketedRoute/socket.client.helper.rooms.ts
2382
+ function normalizeRooms(rooms) {
2383
+ if (rooms == null) return [];
2384
+ const list = Array.isArray(rooms) ? rooms : [rooms];
2385
+ const seen = /* @__PURE__ */ new Set();
2386
+ const normalized = [];
2387
+ for (const r of list) {
2388
+ if (typeof r !== "string") continue;
2389
+ if (seen.has(r)) continue;
2390
+ seen.add(r);
2391
+ normalized.push(r);
2392
+ }
2393
+ return normalized;
2394
+ }
2395
+ function arrayShallowEqual(a, b) {
2396
+ if (a.length !== b.length) return false;
2397
+ for (let i = 0; i < a.length; i += 1) {
2398
+ if (a[i] !== b[i]) return false;
2399
+ }
2400
+ return true;
2401
+ }
2402
+ function roomStateEqual(prev, next) {
2403
+ return arrayShallowEqual(prev.rooms, next.rooms) && safeJsonKey(prev.joinMeta) === safeJsonKey(next.joinMeta) && safeJsonKey(prev.leaveMeta) === safeJsonKey(next.leaveMeta);
2404
+ }
2405
+ function mergeRoomState(prev, toRoomsResult) {
2406
+ const merged = new Set(prev.rooms);
2407
+ for (const r of normalizeRooms(toRoomsResult.rooms)) merged.add(r);
2408
+ return {
2409
+ rooms: Array.from(merged),
2410
+ joinMeta: toRoomsResult.joinMeta ?? prev.joinMeta,
2411
+ leaveMeta: toRoomsResult.leaveMeta ?? prev.leaveMeta
2412
+ };
2413
+ }
2414
+ function roomsFromData(data, toRooms) {
2415
+ if (data == null) return { rooms: [] };
2416
+ let state = { rooms: [] };
2417
+ const add = (input) => {
2418
+ const mergeForValue = (value) => {
2419
+ state = mergeRoomState(state, toRooms(value));
2420
+ };
2421
+ if (Array.isArray(input)) {
2422
+ input.forEach((entry) => mergeForValue(entry));
2423
+ return;
2424
+ }
2425
+ if (input && typeof input === "object") {
2426
+ const maybeItems = input.items;
2427
+ if (Array.isArray(maybeItems)) {
2428
+ maybeItems.forEach((entry) => mergeForValue(entry));
2429
+ return;
2430
+ }
2431
+ }
2432
+ mergeForValue(input);
2433
+ };
2434
+ const maybePages = data?.pages;
2435
+ if (Array.isArray(maybePages)) {
2436
+ for (const page of maybePages) add(page);
2437
+ return state;
2438
+ }
2439
+ add(data);
2440
+ return state;
2441
+ }
2442
+
2443
+ // src/sockets/socketedRoute/socket.client.helper.route.ts
2444
+ function isSocketClientUnavailableError(err) {
2445
+ return err instanceof Error && err.message.includes("SocketClient not found");
2446
+ }
2447
+ function buildSocketedRoute(options) {
2448
+ const { built, toRooms, applySocket, useSocketClient: useSocketClient2, debug } = options;
2449
+ const { useEndpoint: useInnerEndpoint, ...rest } = built;
2450
+ const useEndpoint = (...useArgs) => {
2451
+ let client = null;
2452
+ let socketClientError = null;
2453
+ try {
2454
+ client = useSocketClient2();
2455
+ } catch (err) {
2456
+ if (!isSocketClientUnavailableError(err)) throw err;
2457
+ socketClientError = err;
2458
+ }
2459
+ const endpointResult = useInnerEndpoint(
2460
+ ...useArgs
2461
+ );
2462
+ const argsKey = (0, import_react4.useMemo)(() => safeJsonKey(useArgs[0] ?? null), [useArgs]);
2463
+ const [roomState, setRoomState] = (0, import_react4.useState)(
2464
+ () => roomsFromData(endpointResult.data, toRooms)
2465
+ );
2466
+ const renderCountRef = (0, import_react4.useRef)(0);
2467
+ const clientReadyRef = (0, import_react4.useRef)(null);
2468
+ const onReceiveEffectDebugRef = (0, import_react4.useRef)(null);
2469
+ const deriveRoomsEffectDebugRef = (0, import_react4.useRef)(null);
2470
+ const joinRoomsEffectDebugRef = (0, import_react4.useRef)(null);
2471
+ const applySocketEffectDebugRef = (0, import_react4.useRef)(null);
2472
+ renderCountRef.current += 1;
2473
+ const roomsKey = (0, import_react4.useMemo)(() => roomState.rooms.join("|"), [roomState.rooms]);
2474
+ const joinMetaKey = (0, import_react4.useMemo)(
2475
+ () => safeJsonKey(roomState.joinMeta ?? null),
2476
+ [roomState.joinMeta]
2477
+ );
2478
+ const leaveMetaKey = (0, import_react4.useMemo)(
2479
+ () => safeJsonKey(roomState.leaveMeta ?? null),
2480
+ [roomState.leaveMeta]
2481
+ );
2482
+ const hasClient = !!client;
2483
+ if (clientReadyRef.current !== hasClient) {
2484
+ clientReadyRef.current = hasClient;
2485
+ dbg2(debug, {
2486
+ type: "client",
2487
+ phase: hasClient ? "ready" : "missing",
2488
+ err: hasClient ? void 0 : String(socketClientError)
2489
+ });
2490
+ }
2491
+ dbg2(debug, {
2492
+ type: "render",
2493
+ renderCount: renderCountRef.current,
2494
+ hasClient,
2495
+ argsKey,
2496
+ rooms: roomState.rooms,
2497
+ roomsKey,
2498
+ joinMetaKey,
2499
+ leaveMetaKey
2500
+ });
2501
+ (0, import_react4.useEffect)(() => {
2502
+ trackHookTrigger2({
2503
+ ref: onReceiveEffectDebugRef,
2504
+ phase: "endpoint_on_receive_effect",
2505
+ debug,
2506
+ snapshot: {
2507
+ endpointResultRef: describeObjectReference(endpointResult),
2508
+ endpointDataRef: describeObjectReference(endpointResult.data),
2509
+ toRoomsRef: describeObjectReference(toRooms)
2510
+ }
2511
+ });
2512
+ const unsubscribe = endpointResult.onReceive((data) => {
2513
+ setRoomState((prev) => {
2514
+ const next = mergeRoomState(prev, toRooms(data));
2515
+ return roomStateEqual(prev, next) ? prev : next;
2516
+ });
2517
+ });
2518
+ return unsubscribe;
2519
+ }, [endpointResult, toRooms, debug]);
2520
+ (0, import_react4.useEffect)(() => {
2521
+ trackHookTrigger2({
2522
+ ref: deriveRoomsEffectDebugRef,
2523
+ phase: "derive_rooms_effect",
2524
+ debug,
2525
+ snapshot: {
2526
+ endpointDataRef: describeObjectReference(endpointResult.data),
2527
+ toRoomsRef: describeObjectReference(toRooms)
2528
+ }
2529
+ });
2530
+ const next = roomsFromData(
2531
+ endpointResult.data,
2532
+ toRooms
2533
+ );
2534
+ setRoomState((prev) => roomStateEqual(prev, next) ? prev : next);
2535
+ }, [endpointResult.data, toRooms, debug]);
2536
+ (0, import_react4.useEffect)(() => {
2537
+ trackHookTrigger2({
2538
+ ref: joinRoomsEffectDebugRef,
2539
+ phase: "join_rooms_effect",
2540
+ debug,
2541
+ snapshot: {
2542
+ hasClient: !!client,
2543
+ clientRef: describeObjectReference(client),
2544
+ roomsKey,
2545
+ joinMetaKey,
2546
+ leaveMetaKey
2547
+ }
2548
+ });
2549
+ if (!client) {
2550
+ dbg2(debug, {
2551
+ type: "room",
2552
+ phase: "join_skip",
2553
+ rooms: roomState.rooms,
2554
+ reason: "socket_client_missing"
2555
+ });
2556
+ return;
2557
+ }
2558
+ if (roomState.rooms.length === 0) {
2559
+ dbg2(debug, {
2560
+ type: "room",
2561
+ phase: "join_skip",
2562
+ rooms: [],
2563
+ reason: "no_rooms"
2564
+ });
2565
+ return;
2566
+ }
2567
+ const { joinMeta, leaveMeta } = roomState;
2568
+ if (!joinMeta || !leaveMeta) {
2569
+ dbg2(debug, {
2570
+ type: "room",
2571
+ phase: "join_skip",
2572
+ rooms: roomState.rooms,
2573
+ reason: "missing_meta"
2574
+ });
2575
+ return;
2576
+ }
2577
+ let active = true;
2578
+ dbg2(debug, {
2579
+ type: "room",
2580
+ phase: "join_attempt",
2581
+ rooms: roomState.rooms
2582
+ });
2583
+ (async () => {
2584
+ try {
2585
+ await client.joinRooms(roomState.rooms, joinMeta);
2586
+ } catch (err) {
2587
+ dbg2(debug, {
2588
+ type: "room",
2589
+ phase: "join_error",
2590
+ rooms: roomState.rooms,
2591
+ err: String(err)
2592
+ });
2593
+ }
2594
+ })();
2595
+ return () => {
2596
+ if (!active) return;
2597
+ active = false;
2598
+ if (roomState.rooms.length === 0) {
2599
+ dbg2(debug, {
2600
+ type: "room",
2601
+ phase: "leave_skip",
2602
+ rooms: [],
2603
+ reason: "no_rooms"
2604
+ });
2605
+ return;
2606
+ }
2607
+ dbg2(debug, {
2608
+ type: "room",
2609
+ phase: "leave_attempt",
2610
+ rooms: roomState.rooms
2611
+ });
2612
+ void client.leaveRooms(roomState.rooms, leaveMeta).catch((err) => {
2613
+ dbg2(debug, {
2614
+ type: "room",
2615
+ phase: "leave_error",
2616
+ rooms: roomState.rooms,
2617
+ err: String(err)
2618
+ });
2619
+ });
2620
+ };
2621
+ }, [client, roomsKey, joinMetaKey, leaveMetaKey, debug]);
2622
+ (0, import_react4.useEffect)(() => {
2623
+ trackHookTrigger2({
2624
+ ref: applySocketEffectDebugRef,
2625
+ phase: "apply_socket_effect",
2626
+ debug,
2627
+ snapshot: {
2628
+ hasClient: !!client,
2629
+ clientRef: describeObjectReference(client),
2630
+ applySocketKeys: Object.keys(applySocket).sort().join(","),
2631
+ argsKey,
2632
+ toRoomsRef: describeObjectReference(toRooms)
2633
+ }
2634
+ });
2635
+ if (!client) return;
2636
+ const queue = [];
2637
+ const sameRefWarnedEvents = /* @__PURE__ */ new Set();
2638
+ let draining = false;
2639
+ let active = true;
2640
+ const drainQueue = () => {
2641
+ if (!active || draining) return;
2642
+ draining = true;
2643
+ try {
2644
+ while (active && queue.length > 0) {
2645
+ const nextUpdate = queue.shift();
2646
+ if (!nextUpdate) continue;
2647
+ built.setData(
2648
+ (prev) => {
2649
+ const next = nextUpdate.fn(
2650
+ prev,
2651
+ nextUpdate.payload,
2652
+ nextUpdate.meta ? { ...nextUpdate.meta, args: useArgs } : { args: useArgs }
2653
+ );
2654
+ if (next === null) return prev;
2655
+ if (shouldWarnSocketMutationGuard() && isSameObjectReference(prev, next) && !sameRefWarnedEvents.has(nextUpdate.event)) {
2656
+ sameRefWarnedEvents.add(nextUpdate.event);
2657
+ console.warn(
2658
+ `[socketedRoute] applySocket("${nextUpdate.event}") returned the previous reference. Return a new object/array for updates, or return null for no change.`
2659
+ );
2660
+ }
2661
+ const nextRoomState = roomsFromData(
2662
+ next,
2663
+ toRooms
2664
+ );
2665
+ setRoomState(
2666
+ (prevRoomState) => roomStateEqual(prevRoomState, nextRoomState) ? prevRoomState : nextRoomState
2667
+ );
2668
+ return next;
2669
+ },
2670
+ ...useArgs
2671
+ );
2672
+ }
2673
+ } finally {
2674
+ draining = false;
2675
+ if (active && queue.length > 0) drainQueue();
2676
+ }
2677
+ };
2678
+ const entries = Object.entries(applySocket).filter(
2679
+ ([_event, fn]) => typeof fn === "function"
2680
+ );
2681
+ const unsubscribes = entries.map(
2682
+ ([ev, fn]) => client.on(ev, (payload, meta) => {
2683
+ queue.push({
2684
+ event: ev,
2685
+ fn,
2686
+ payload,
2687
+ ...meta ? { meta } : {}
2688
+ });
2689
+ drainQueue();
2690
+ })
2691
+ );
2692
+ return () => {
2693
+ active = false;
2694
+ queue.length = 0;
2695
+ unsubscribes.forEach((u) => u?.());
2696
+ };
2697
+ }, [client, applySocket, built, argsKey, toRooms, debug]);
2698
+ return { ...endpointResult, rooms: roomState.rooms };
2699
+ };
2700
+ return {
2701
+ ...rest,
2702
+ useEndpoint
2703
+ };
2704
+ }
2672
2705
  // Annotate the CommonJS export names for ESM import in node:
2673
2706
  0 && (module.exports = {
2674
2707
  HttpError,