@emeryld/rrroutes-client 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1593,7 +1593,9 @@ function useSocketConnection(args) {
1593
1593
  React.useEffect(() => {
1594
1594
  if (autoJoin && normalizedRooms.length > 0)
1595
1595
  client.joinRooms(normalizedRooms, args.joinMeta);
1596
- const unsubscribe = client.on(event, onMessage);
1596
+ const unsubscribe = client.on(event, (payload, meta) => {
1597
+ onMessage(payload, meta);
1598
+ });
1597
1599
  return () => {
1598
1600
  unsubscribe();
1599
1601
  if (autoLeave && normalizedRooms.length > 0)
@@ -1618,6 +1620,92 @@ function normalizeRooms(rooms) {
1618
1620
  }
1619
1621
  return normalized;
1620
1622
  }
1623
+ var objectReferenceIds = /* @__PURE__ */ new WeakMap();
1624
+ var objectReferenceCounter = 0;
1625
+ function describeObjectReference(value) {
1626
+ if (value == null) return null;
1627
+ const valueType = typeof value;
1628
+ if (valueType !== "object" && valueType !== "function") return null;
1629
+ const obj = value;
1630
+ let id = objectReferenceIds.get(obj);
1631
+ if (!id) {
1632
+ id = ++objectReferenceCounter;
1633
+ objectReferenceIds.set(obj, id);
1634
+ }
1635
+ return `ref#${id}`;
1636
+ }
1637
+ function safeJsonKey(value) {
1638
+ try {
1639
+ const serialized = JSON.stringify(value ?? null);
1640
+ return serialized ?? "null";
1641
+ } catch {
1642
+ return "[unserializable]";
1643
+ }
1644
+ }
1645
+ function arrayShallowEqual(a, b) {
1646
+ if (a.length !== b.length) return false;
1647
+ for (let i = 0; i < a.length; i += 1) {
1648
+ if (a[i] !== b[i]) return false;
1649
+ }
1650
+ return true;
1651
+ }
1652
+ function roomStateEqual(prev, next) {
1653
+ return arrayShallowEqual(prev.rooms, next.rooms) && safeJsonKey(prev.joinMeta) === safeJsonKey(next.joinMeta) && safeJsonKey(prev.leaveMeta) === safeJsonKey(next.leaveMeta);
1654
+ }
1655
+ function safeDescribeHookValue2(value) {
1656
+ if (value == null) return value;
1657
+ const valueType = typeof value;
1658
+ if (valueType === "string" || valueType === "number" || valueType === "boolean") {
1659
+ return value;
1660
+ }
1661
+ if (valueType === "bigint" || valueType === "symbol") return String(value);
1662
+ if (valueType === "function")
1663
+ return `[function ${value.name || "anonymous"}]`;
1664
+ if (Array.isArray(value)) return `[array length=${value.length}]`;
1665
+ const ctorName = value.constructor?.name ?? "object";
1666
+ const keys = Object.keys(value);
1667
+ const keyPreview = keys.slice(0, 4).join(",");
1668
+ const suffix = keys.length > 4 ? ",\u2026" : "";
1669
+ const ref = describeObjectReference(value);
1670
+ return `[${ctorName}${ref ? ` ${ref}` : ""} keys=${keyPreview}${suffix}]`;
1671
+ }
1672
+ function createHookDebugEvent2(prev, next, phase) {
1673
+ const reason = prev ? "change" : "init";
1674
+ const changed = Object.keys(next).reduce((acc, dependency) => {
1675
+ const prevValue = prev ? prev[dependency] : void 0;
1676
+ const nextValue = next[dependency];
1677
+ if (!prev || !Object.is(prevValue, nextValue)) {
1678
+ acc.push({
1679
+ dependency,
1680
+ previous: safeDescribeHookValue2(prevValue),
1681
+ next: safeDescribeHookValue2(nextValue)
1682
+ });
1683
+ }
1684
+ return acc;
1685
+ }, []);
1686
+ if (!changed.length) return null;
1687
+ return { type: "hook", phase, reason, changes: changed };
1688
+ }
1689
+ function dbg2(debug, event) {
1690
+ if (!debug?.logger) return;
1691
+ if (!debug[event.type]) return;
1692
+ debug.logger(event);
1693
+ }
1694
+ function trackHookTrigger2({
1695
+ ref,
1696
+ phase,
1697
+ debug,
1698
+ snapshot
1699
+ }) {
1700
+ const prev = ref.current;
1701
+ ref.current = snapshot;
1702
+ if (!debug?.logger || !debug?.hook) return;
1703
+ const event = createHookDebugEvent2(prev, snapshot, phase);
1704
+ if (event) dbg2(debug, event);
1705
+ }
1706
+ function isSocketClientUnavailableError(err) {
1707
+ return err instanceof Error && err.message.includes("SocketClient not found");
1708
+ }
1621
1709
  function mergeRoomState(prev, toRoomsResult) {
1622
1710
  const merged = new Set(prev.rooms);
1623
1711
  for (const r of normalizeRooms(toRoomsResult.rooms)) merged.add(r);
@@ -1656,84 +1744,247 @@ function roomsFromData(data, toRooms) {
1656
1744
  return state;
1657
1745
  }
1658
1746
  function buildSocketedRoute(options) {
1659
- const { built, toRooms, applySocket, useSocketClient: useSocketClient2 } = options;
1747
+ const { built, toRooms, applySocket, useSocketClient: useSocketClient2, debug } = options;
1660
1748
  const { useEndpoint: useInnerEndpoint, ...rest } = built;
1661
1749
  const useEndpoint = (...useArgs) => {
1662
- const client = useSocketClient2();
1750
+ let client = null;
1751
+ let socketClientError = null;
1752
+ try {
1753
+ client = useSocketClient2();
1754
+ } catch (err) {
1755
+ if (!isSocketClientUnavailableError(err)) throw err;
1756
+ socketClientError = err;
1757
+ }
1663
1758
  const endpointResult = useInnerEndpoint(
1664
1759
  ...useArgs
1665
1760
  );
1666
- const argsKey = (0, import_react4.useMemo)(() => JSON.stringify(useArgs[0] ?? null), [useArgs]);
1761
+ const argsKey = (0, import_react4.useMemo)(() => safeJsonKey(useArgs[0] ?? null), [useArgs]);
1667
1762
  const [roomState, setRoomState] = (0, import_react4.useState)(
1668
1763
  () => roomsFromData(endpointResult.data, toRooms)
1669
1764
  );
1765
+ const renderCountRef = (0, import_react4.useRef)(0);
1766
+ const clientReadyRef = (0, import_react4.useRef)(null);
1767
+ const onReceiveEffectDebugRef = (0, import_react4.useRef)(null);
1768
+ const deriveRoomsEffectDebugRef = (0, import_react4.useRef)(null);
1769
+ const joinRoomsEffectDebugRef = (0, import_react4.useRef)(null);
1770
+ const applySocketEffectDebugRef = (0, import_react4.useRef)(null);
1771
+ renderCountRef.current += 1;
1670
1772
  const roomsKey = (0, import_react4.useMemo)(() => roomState.rooms.join("|"), [roomState.rooms]);
1671
1773
  const joinMetaKey = (0, import_react4.useMemo)(
1672
- () => JSON.stringify(roomState.joinMeta ?? null),
1774
+ () => safeJsonKey(roomState.joinMeta ?? null),
1673
1775
  [roomState.joinMeta]
1674
1776
  );
1675
1777
  const leaveMetaKey = (0, import_react4.useMemo)(
1676
- () => JSON.stringify(roomState.leaveMeta ?? null),
1778
+ () => safeJsonKey(roomState.leaveMeta ?? null),
1677
1779
  [roomState.leaveMeta]
1678
1780
  );
1781
+ const hasClient = !!client;
1782
+ if (clientReadyRef.current !== hasClient) {
1783
+ clientReadyRef.current = hasClient;
1784
+ dbg2(debug, {
1785
+ type: "client",
1786
+ phase: hasClient ? "ready" : "missing",
1787
+ err: hasClient ? void 0 : String(socketClientError)
1788
+ });
1789
+ }
1790
+ dbg2(debug, {
1791
+ type: "render",
1792
+ renderCount: renderCountRef.current,
1793
+ hasClient,
1794
+ argsKey,
1795
+ rooms: roomState.rooms,
1796
+ roomsKey,
1797
+ joinMetaKey,
1798
+ leaveMetaKey
1799
+ });
1679
1800
  (0, import_react4.useEffect)(() => {
1801
+ trackHookTrigger2({
1802
+ ref: onReceiveEffectDebugRef,
1803
+ phase: "endpoint_on_receive_effect",
1804
+ debug,
1805
+ snapshot: {
1806
+ endpointResultRef: describeObjectReference(endpointResult),
1807
+ endpointDataRef: describeObjectReference(endpointResult.data),
1808
+ toRoomsRef: describeObjectReference(toRooms)
1809
+ }
1810
+ });
1680
1811
  const unsubscribe = endpointResult.onReceive((data) => {
1681
- setRoomState(
1682
- (prev) => mergeRoomState(prev, toRooms(data))
1683
- );
1812
+ setRoomState((prev) => {
1813
+ const next = mergeRoomState(prev, toRooms(data));
1814
+ return roomStateEqual(prev, next) ? prev : next;
1815
+ });
1684
1816
  });
1685
1817
  return unsubscribe;
1686
- }, [endpointResult, toRooms]);
1818
+ }, [endpointResult, toRooms, debug]);
1687
1819
  (0, import_react4.useEffect)(() => {
1688
- setRoomState(
1689
- roomsFromData(endpointResult.data, toRooms)
1820
+ trackHookTrigger2({
1821
+ ref: deriveRoomsEffectDebugRef,
1822
+ phase: "derive_rooms_effect",
1823
+ debug,
1824
+ snapshot: {
1825
+ endpointDataRef: describeObjectReference(endpointResult.data),
1826
+ toRoomsRef: describeObjectReference(toRooms)
1827
+ }
1828
+ });
1829
+ const next = roomsFromData(
1830
+ endpointResult.data,
1831
+ toRooms
1690
1832
  );
1691
- }, [endpointResult.data, toRooms]);
1833
+ setRoomState((prev) => roomStateEqual(prev, next) ? prev : next);
1834
+ }, [endpointResult.data, toRooms, debug]);
1692
1835
  (0, import_react4.useEffect)(() => {
1693
- if (roomState.rooms.length === 0) return;
1836
+ trackHookTrigger2({
1837
+ ref: joinRoomsEffectDebugRef,
1838
+ phase: "join_rooms_effect",
1839
+ debug,
1840
+ snapshot: {
1841
+ hasClient: !!client,
1842
+ clientRef: describeObjectReference(client),
1843
+ roomsKey,
1844
+ joinMetaKey,
1845
+ leaveMetaKey
1846
+ }
1847
+ });
1848
+ if (!client) {
1849
+ dbg2(debug, {
1850
+ type: "room",
1851
+ phase: "join_skip",
1852
+ rooms: roomState.rooms,
1853
+ reason: "socket_client_missing"
1854
+ });
1855
+ return;
1856
+ }
1857
+ if (roomState.rooms.length === 0) {
1858
+ dbg2(debug, {
1859
+ type: "room",
1860
+ phase: "join_skip",
1861
+ rooms: [],
1862
+ reason: "no_rooms"
1863
+ });
1864
+ return;
1865
+ }
1694
1866
  const { joinMeta, leaveMeta } = roomState;
1695
- if (!joinMeta || !leaveMeta) return;
1867
+ if (!joinMeta || !leaveMeta) {
1868
+ dbg2(debug, {
1869
+ type: "room",
1870
+ phase: "join_skip",
1871
+ rooms: roomState.rooms,
1872
+ reason: "missing_meta"
1873
+ });
1874
+ return;
1875
+ }
1696
1876
  let active = true;
1877
+ dbg2(debug, {
1878
+ type: "room",
1879
+ phase: "join_attempt",
1880
+ rooms: roomState.rooms
1881
+ });
1697
1882
  (async () => {
1698
1883
  try {
1699
1884
  await client.joinRooms(roomState.rooms, joinMeta);
1700
- } catch {
1885
+ } catch (err) {
1886
+ dbg2(debug, {
1887
+ type: "room",
1888
+ phase: "join_error",
1889
+ rooms: roomState.rooms,
1890
+ err: String(err)
1891
+ });
1701
1892
  }
1702
1893
  })();
1703
1894
  return () => {
1704
- if (!active || roomState.rooms.length === 0) return;
1895
+ if (!active) return;
1705
1896
  active = false;
1706
- void client.leaveRooms(roomState.rooms, leaveMeta).catch(() => {
1897
+ if (roomState.rooms.length === 0) {
1898
+ dbg2(debug, {
1899
+ type: "room",
1900
+ phase: "leave_skip",
1901
+ rooms: [],
1902
+ reason: "no_rooms"
1903
+ });
1904
+ return;
1905
+ }
1906
+ dbg2(debug, {
1907
+ type: "room",
1908
+ phase: "leave_attempt",
1909
+ rooms: roomState.rooms
1910
+ });
1911
+ void client.leaveRooms(roomState.rooms, leaveMeta).catch((err) => {
1912
+ dbg2(debug, {
1913
+ type: "room",
1914
+ phase: "leave_error",
1915
+ rooms: roomState.rooms,
1916
+ err: String(err)
1917
+ });
1707
1918
  });
1708
1919
  };
1709
- }, [
1710
- client,
1711
- roomsKey,
1712
- roomState.joinMeta,
1713
- roomState.leaveMeta,
1714
- joinMetaKey,
1715
- leaveMetaKey
1716
- ]);
1920
+ }, [client, roomsKey, joinMetaKey, leaveMetaKey, debug]);
1717
1921
  (0, import_react4.useEffect)(() => {
1922
+ trackHookTrigger2({
1923
+ ref: applySocketEffectDebugRef,
1924
+ phase: "apply_socket_effect",
1925
+ debug,
1926
+ snapshot: {
1927
+ hasClient: !!client,
1928
+ clientRef: describeObjectReference(client),
1929
+ applySocketKeys: Object.keys(applySocket).sort().join(","),
1930
+ argsKey,
1931
+ toRoomsRef: describeObjectReference(toRooms)
1932
+ }
1933
+ });
1934
+ if (!client) return;
1935
+ const queue = [];
1936
+ let draining = false;
1937
+ let active = true;
1938
+ const drainQueue = () => {
1939
+ if (!active || draining) return;
1940
+ draining = true;
1941
+ try {
1942
+ while (active && queue.length > 0) {
1943
+ const nextUpdate = queue.shift();
1944
+ if (!nextUpdate) continue;
1945
+ built.setData(
1946
+ (prev) => {
1947
+ const next = nextUpdate.fn(
1948
+ prev,
1949
+ nextUpdate.payload,
1950
+ nextUpdate.meta ? { ...nextUpdate.meta, args: useArgs } : { args: useArgs }
1951
+ );
1952
+ const nextRoomState = roomsFromData(
1953
+ next,
1954
+ toRooms
1955
+ );
1956
+ setRoomState(
1957
+ (prevRoomState) => roomStateEqual(prevRoomState, nextRoomState) ? prevRoomState : nextRoomState
1958
+ );
1959
+ return next;
1960
+ },
1961
+ ...useArgs
1962
+ );
1963
+ }
1964
+ } finally {
1965
+ draining = false;
1966
+ if (active && queue.length > 0) drainQueue();
1967
+ }
1968
+ };
1718
1969
  const entries = Object.entries(applySocket).filter(
1719
1970
  ([_event, fn]) => typeof fn === "function"
1720
1971
  );
1721
1972
  const unsubscribes = entries.map(
1722
1973
  ([ev, fn]) => client.on(ev, (payload, meta) => {
1723
- built.setData(
1724
- (prev) => {
1725
- const next = fn(prev, payload, meta);
1726
- setRoomState(
1727
- roomsFromData(next, toRooms)
1728
- );
1729
- return next;
1730
- },
1731
- ...useArgs
1732
- );
1974
+ queue.push({
1975
+ fn,
1976
+ payload,
1977
+ ...meta ? { meta } : {}
1978
+ });
1979
+ drainQueue();
1733
1980
  })
1734
1981
  );
1735
- return () => unsubscribes.forEach((u) => u?.());
1736
- }, [client, applySocket, built, argsKey, toRooms]);
1982
+ return () => {
1983
+ active = false;
1984
+ queue.length = 0;
1985
+ unsubscribes.forEach((u) => u?.());
1986
+ };
1987
+ }, [client, applySocket, built, argsKey, toRooms, debug]);
1737
1988
  return { ...endpointResult, rooms: roomState.rooms };
1738
1989
  };
1739
1990
  return {
@@ -2245,9 +2496,9 @@ var SocketClient = class {
2245
2496
  };
2246
2497
  }
2247
2498
  const socket = this.socket;
2248
- const wrapped = (envelopeOrRaw) => {
2249
- const maybeEnvelope = envelopeOrRaw;
2250
- const rawData = maybeEnvelope?.data ?? maybeEnvelope;
2499
+ const toStringList = (value) => Array.isArray(value) ? value.filter((entry2) => typeof entry2 === "string") : [];
2500
+ const wrappedEnv = (envelope) => {
2501
+ const rawData = envelope.data;
2251
2502
  const parsed = schema.safeParse(rawData);
2252
2503
  if (!parsed.success) {
2253
2504
  this.dbg({
@@ -2258,15 +2509,17 @@ var SocketClient = class {
2258
2509
  });
2259
2510
  return;
2260
2511
  }
2512
+ const data = parsed.data;
2261
2513
  const receivedAt = /* @__PURE__ */ new Date();
2262
- const sentAt = maybeEnvelope?.sentAt ? new Date(maybeEnvelope.sentAt) : void 0;
2514
+ const sentAt = envelope?.sentAt ? new Date(envelope.sentAt) : void 0;
2263
2515
  const meta = {
2264
2516
  envelope: {
2265
- eventName: maybeEnvelope?.eventName ?? event,
2266
- sentAt: maybeEnvelope?.sentAt ?? receivedAt.toISOString(),
2267
- sentTo: maybeEnvelope?.sentTo ?? [],
2268
- data: void 0,
2269
- metadata: maybeEnvelope?.metadata
2517
+ eventName: typeof envelope?.eventName === "string" ? envelope.eventName : event,
2518
+ sentAt: envelope?.sentAt ?? receivedAt.toISOString(),
2519
+ sentTo: toStringList(envelope?.sentTo),
2520
+ data,
2521
+ metadata: envelope?.metadata,
2522
+ rooms: toStringList(envelope?.rooms)
2270
2523
  },
2271
2524
  ctx: {
2272
2525
  receivedAt,
@@ -2286,22 +2539,38 @@ var SocketClient = class {
2286
2539
  metadata: meta.envelope.metadata
2287
2540
  } : void 0
2288
2541
  });
2289
- handler(parsed.data, meta);
2542
+ handler(data, meta);
2543
+ };
2544
+ const wrappedDispatcher = (envelopeOrRaw) => {
2545
+ if (typeof envelopeOrRaw === "object" && envelopeOrRaw !== null && "eventName" in envelopeOrRaw && "sentAt" in envelopeOrRaw && "sentTo" in envelopeOrRaw && "data" in envelopeOrRaw) {
2546
+ wrappedEnv(envelopeOrRaw);
2547
+ } else {
2548
+ this.dbg({
2549
+ type: "receive",
2550
+ event,
2551
+ envelope: void 0
2552
+ });
2553
+ handler(envelopeOrRaw, void 0);
2554
+ }
2290
2555
  };
2291
2556
  const errorWrapped = (e) => {
2292
2557
  this.dbg({ type: "receive", event, err: String(e) });
2293
2558
  };
2294
- socket.on(String(event), wrapped);
2559
+ socket.on(String(event), wrappedDispatcher);
2295
2560
  socket.on(`${String(event)}:error`, errorWrapped);
2296
2561
  let set = this.handlerMap.get(String(event));
2297
2562
  if (!set) {
2298
2563
  set = /* @__PURE__ */ new Set();
2299
2564
  this.handlerMap.set(String(event), set);
2300
2565
  }
2301
- const entry = { orig: handler, wrapped, errorWrapped };
2566
+ const entry = {
2567
+ orig: handler,
2568
+ wrapped: wrappedDispatcher,
2569
+ errorWrapped
2570
+ };
2302
2571
  set.add(entry);
2303
2572
  return () => {
2304
- socket.off(String(event), wrapped);
2573
+ socket.off(String(event), wrappedDispatcher);
2305
2574
  socket.off(`${String(event)}:error`, errorWrapped);
2306
2575
  const s = this.handlerMap.get(String(event));
2307
2576
  if (s) {