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