@emeryld/rrroutes-client 2.6.3 → 2.6.4

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