@emeryld/rrroutes-client 2.6.3 → 2.6.5

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