@emeryld/rrroutes-client 2.7.11 → 2.7.13

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
@@ -40,7 +40,9 @@ __export(index_exports, {
40
40
  createRouteClient: () => createRouteClient,
41
41
  defaultFetcher: () => defaultFetcher,
42
42
  useSocketClient: () => useSocketClient,
43
- useSocketConnection: () => useSocketConnection
43
+ useSocketConnection: () => useSocketConnection,
44
+ useSocketEvent: () => useSocketEvent,
45
+ useSocketRooms: () => useSocketRooms
44
46
  });
45
47
  module.exports = __toCommonJS(index_exports);
46
48
 
@@ -1581,10 +1583,7 @@ var React = __toESM(require("react"), 1);
1581
1583
  var SocketCtx = React.createContext(void 0);
1582
1584
  function useSocketClient() {
1583
1585
  const ctx = React.useContext(SocketCtx);
1584
- if (typeof ctx === "undefined") {
1585
- throw new Error("SocketClient not found. Wrap with <SocketProvider>.");
1586
- }
1587
- if (!ctx) {
1586
+ if (typeof ctx === "undefined" || !ctx) {
1588
1587
  return null;
1589
1588
  }
1590
1589
  return ctx;
@@ -1592,25 +1591,67 @@ function useSocketClient() {
1592
1591
 
1593
1592
  // src/sockets/socket.client.context.connection.ts
1594
1593
  var React2 = __toESM(require("react"), 1);
1595
- function useSocketConnection(args) {
1594
+ function normalizeRooms(rooms) {
1595
+ if (rooms == null) return [];
1596
+ return Array.isArray(rooms) ? rooms : [rooms];
1597
+ }
1598
+ function safeSerialize(value) {
1599
+ try {
1600
+ return JSON.stringify(value) ?? String(value);
1601
+ } catch {
1602
+ return "[unserializable]";
1603
+ }
1604
+ }
1605
+ function useLatestRef(value) {
1606
+ const ref = React2.useRef(value);
1607
+ React2.useEffect(() => {
1608
+ ref.current = value;
1609
+ }, [value]);
1610
+ return ref;
1611
+ }
1612
+ function useDebugEmitter(debug) {
1613
+ const debugRef = useLatestRef(debug);
1614
+ return React2.useCallback((event) => {
1615
+ const dbg3 = debugRef.current;
1616
+ if (!dbg3?.enabled) return;
1617
+ if (dbg3[event.type] === false) return;
1618
+ const logger = dbg3.logger;
1619
+ if (logger) {
1620
+ try {
1621
+ logger(event);
1622
+ } catch (error) {
1623
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
1624
+ console.warn("[socket] debug logger threw", error);
1625
+ }
1626
+ }
1627
+ return;
1628
+ }
1629
+ if (typeof console !== "undefined" && typeof console.log === "function") {
1630
+ console.log("[socket]", event);
1631
+ }
1632
+ }, []);
1633
+ }
1634
+ function useSocketRooms(args) {
1596
1635
  const {
1597
- event,
1598
1636
  rooms,
1599
- onMessage,
1600
- onCleanup,
1601
1637
  autoJoin = true,
1602
1638
  autoLeave = true,
1639
+ enabled = true,
1640
+ onCleanup,
1603
1641
  debug
1604
1642
  } = args;
1605
1643
  const client = useSocketClient();
1606
- const normalizedRooms = React2.useMemo(
1607
- () => rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms],
1608
- [rooms]
1609
- );
1644
+ const normalizedRooms = React2.useMemo(() => normalizeRooms(rooms), [rooms]);
1610
1645
  const normalizedRoomsKey = React2.useMemo(
1611
1646
  () => normalizedRooms.join(""),
1612
1647
  [normalizedRooms]
1613
1648
  );
1649
+ const joinMetaRef = useLatestRef(args.joinMeta);
1650
+ const leaveMetaRef = useLatestRef(args.leaveMeta);
1651
+ const onCleanupRef = useLatestRef(onCleanup);
1652
+ const debugRef = useLatestRef(debug);
1653
+ const emitDebug = useDebugEmitter(debug);
1654
+ const missingClientWarnedRef = React2.useRef(false);
1614
1655
  const joinMetaKey = React2.useMemo(
1615
1656
  () => JSON.stringify(args.joinMeta ?? null),
1616
1657
  [args.joinMeta]
@@ -1619,86 +1660,48 @@ function useSocketConnection(args) {
1619
1660
  () => JSON.stringify(args.leaveMeta ?? null),
1620
1661
  [args.leaveMeta]
1621
1662
  );
1622
- const missingClientWarnedRef = React2.useRef(false);
1623
- const emitDebug = React2.useCallback(
1624
- (event2) => {
1625
- if (!debug?.enabled) return;
1626
- if (debug[event2.type] === false) return;
1627
- const logger = debug.logger;
1628
- if (logger) {
1629
- try {
1630
- logger(event2);
1631
- } catch (error) {
1632
- if (typeof console !== "undefined" && typeof console.warn === "function") {
1633
- console.warn("[socket] useSocketConnection debug logger threw", error);
1634
- }
1635
- }
1636
- return;
1637
- }
1638
- if (typeof console !== "undefined" && typeof console.log === "function") {
1639
- console.log("[socket] useSocketConnection", event2);
1640
- }
1641
- },
1642
- [debug]
1643
- );
1644
- const reportAsyncError = React2.useCallback(
1645
- (phase, error) => {
1646
- if (typeof console !== "undefined" && typeof console.warn === "function") {
1647
- console.warn(`[socket] useSocketConnection ${phase} failed`, error);
1648
- }
1649
- },
1650
- []
1651
- );
1652
1663
  React2.useEffect(() => {
1653
1664
  emitDebug({
1654
1665
  type: "lifecycle",
1655
1666
  phase: "effect_start",
1656
- event,
1657
1667
  rooms: normalizedRooms,
1658
1668
  autoJoin,
1659
1669
  autoLeave
1660
1670
  });
1671
+ if (!enabled) {
1672
+ emitDebug({
1673
+ type: "room",
1674
+ phase: "join_skip",
1675
+ rooms: normalizedRooms,
1676
+ reason: "disabled"
1677
+ });
1678
+ return;
1679
+ }
1661
1680
  if (!client) {
1662
1681
  emitDebug({
1663
1682
  type: "lifecycle",
1664
1683
  phase: "client_missing",
1665
- event,
1666
1684
  rooms: normalizedRooms,
1667
1685
  autoJoin,
1668
1686
  autoLeave
1669
1687
  });
1670
- if (debug?.throwIfClientMissing) {
1671
- throw new Error(
1672
- `useSocketConnection("${event}") missing SocketClient. Wrap with <SocketProvider>.`
1673
- );
1674
- }
1675
- if (debug?.warnIfClientMissing && !missingClientWarnedRef.current && typeof console !== "undefined" && typeof console.warn === "function") {
1688
+ if (debugRef.current?.warnIfClientMissing && !missingClientWarnedRef.current && typeof console !== "undefined" && typeof console.warn === "function") {
1676
1689
  missingClientWarnedRef.current = true;
1677
- console.warn(
1678
- `[socket] useSocketConnection("${event}") skipped because SocketClient is null.`
1679
- );
1690
+ console.warn("[socket] useSocketRooms skipped because SocketClient is null.");
1680
1691
  }
1681
1692
  return;
1682
1693
  }
1683
1694
  missingClientWarnedRef.current = false;
1684
- emitDebug({ type: "subscription", phase: "register", event });
1685
1695
  if (autoJoin && normalizedRooms.length > 0) {
1686
- emitDebug({
1687
- type: "room",
1688
- phase: "join_attempt",
1689
- rooms: normalizedRooms
1690
- });
1691
- void client.joinRooms(normalizedRooms, args.joinMeta).then(() => {
1692
- emitDebug({ type: "room", phase: "join_ok", rooms: normalizedRooms });
1693
- }).catch((error) => {
1694
- emitDebug({
1696
+ emitDebug({ type: "room", phase: "join_attempt", rooms: normalizedRooms });
1697
+ void client.joinRooms(normalizedRooms, joinMetaRef.current).then(() => emitDebug({ type: "room", phase: "join_ok", rooms: normalizedRooms })).catch(
1698
+ (error) => emitDebug({
1695
1699
  type: "room",
1696
1700
  phase: "join_error",
1697
1701
  rooms: normalizedRooms,
1698
1702
  err: String(error)
1699
- });
1700
- reportAsyncError("joinRooms", error);
1701
- });
1703
+ })
1704
+ );
1702
1705
  } else {
1703
1706
  emitDebug({
1704
1707
  type: "room",
@@ -1707,30 +1710,23 @@ function useSocketConnection(args) {
1707
1710
  reason: autoJoin ? "no_rooms" : "auto_disabled"
1708
1711
  });
1709
1712
  }
1710
- const unsubscribe = client.on(event, (payload, meta) => {
1711
- emitDebug({ type: "subscription", phase: "message", event });
1712
- onMessage(payload, meta);
1713
- });
1714
1713
  return () => {
1715
- emitDebug({ type: "subscription", phase: "unregister", event });
1716
- unsubscribe();
1717
1714
  if (autoLeave && normalizedRooms.length > 0) {
1718
1715
  emitDebug({
1719
1716
  type: "room",
1720
1717
  phase: "leave_attempt",
1721
1718
  rooms: normalizedRooms
1722
1719
  });
1723
- void client.leaveRooms(normalizedRooms, args.leaveMeta).then(() => {
1724
- emitDebug({ type: "room", phase: "leave_ok", rooms: normalizedRooms });
1725
- }).catch((error) => {
1726
- emitDebug({
1720
+ void client.leaveRooms(normalizedRooms, leaveMetaRef.current).then(
1721
+ () => emitDebug({ type: "room", phase: "leave_ok", rooms: normalizedRooms })
1722
+ ).catch(
1723
+ (error) => emitDebug({
1727
1724
  type: "room",
1728
1725
  phase: "leave_error",
1729
1726
  rooms: normalizedRooms,
1730
1727
  err: String(error)
1731
- });
1732
- reportAsyncError("leaveRooms", error);
1733
- });
1728
+ })
1729
+ );
1734
1730
  } else {
1735
1731
  emitDebug({
1736
1732
  type: "room",
@@ -1739,11 +1735,10 @@ function useSocketConnection(args) {
1739
1735
  reason: autoLeave ? "no_rooms" : "auto_disabled"
1740
1736
  });
1741
1737
  }
1742
- if (onCleanup) onCleanup();
1738
+ onCleanupRef.current?.();
1743
1739
  emitDebug({
1744
1740
  type: "lifecycle",
1745
1741
  phase: "effect_cleanup",
1746
- event,
1747
1742
  rooms: normalizedRooms,
1748
1743
  autoJoin,
1749
1744
  autoLeave
@@ -1751,19 +1746,78 @@ function useSocketConnection(args) {
1751
1746
  };
1752
1747
  }, [
1753
1748
  client,
1754
- event,
1755
- onMessage,
1756
- onCleanup,
1749
+ enabled,
1757
1750
  autoJoin,
1758
1751
  autoLeave,
1759
- debug,
1760
- emitDebug,
1761
- reportAsyncError,
1762
1752
  normalizedRoomsKey,
1763
1753
  joinMetaKey,
1764
- leaveMetaKey
1754
+ leaveMetaKey,
1755
+ emitDebug
1765
1756
  ]);
1766
1757
  }
1758
+ function useSocketEvent(args) {
1759
+ const {
1760
+ event,
1761
+ onMessage,
1762
+ onCleanup,
1763
+ enabled = true,
1764
+ messageDeduplication,
1765
+ debug
1766
+ } = args;
1767
+ const client = useSocketClient();
1768
+ const onMessageRef = useLatestRef(onMessage);
1769
+ const onCleanupRef = useLatestRef(onCleanup);
1770
+ const messageDeduplicationRef = useLatestRef(messageDeduplication);
1771
+ const debugRef = useLatestRef(debug);
1772
+ const emitDebug = useDebugEmitter(debug);
1773
+ const missingClientWarnedRef = React2.useRef(false);
1774
+ const seenMessageKeysRef = React2.useRef(/* @__PURE__ */ new Map());
1775
+ React2.useEffect(() => {
1776
+ emitDebug({ type: "lifecycle", phase: "effect_start", event });
1777
+ if (!enabled) return;
1778
+ if (!client) {
1779
+ emitDebug({ type: "lifecycle", phase: "client_missing", event });
1780
+ if (debugRef.current?.warnIfClientMissing && !missingClientWarnedRef.current && typeof console !== "undefined" && typeof console.warn === "function") {
1781
+ missingClientWarnedRef.current = true;
1782
+ console.warn(`[socket] useSocketEvent("${event}") skipped because SocketClient is null.`);
1783
+ }
1784
+ return;
1785
+ }
1786
+ missingClientWarnedRef.current = false;
1787
+ emitDebug({ type: "subscription", phase: "register", event });
1788
+ const unsubscribe = client.on(event, (payload, meta) => {
1789
+ const dedupeCfg = messageDeduplicationRef.current;
1790
+ if (dedupeCfg) {
1791
+ const ttlMs = Math.max(0, dedupeCfg.ttlMs ?? 1500);
1792
+ const now = Date.now();
1793
+ const dedupeKey = dedupeCfg.key?.(payload, meta) ?? (meta?.envelope ? `${event}|${String(meta.envelope.eventName)}|${String(meta.envelope.sentAt)}|${safeSerialize(payload)}` : `${event}|${safeSerialize(payload)}`);
1794
+ const prev = seenMessageKeysRef.current.get(dedupeKey);
1795
+ if (typeof prev === "number" && now - prev <= ttlMs) {
1796
+ emitDebug({
1797
+ type: "subscription",
1798
+ phase: "message_dropped_duplicate",
1799
+ event,
1800
+ dedupeKey
1801
+ });
1802
+ return;
1803
+ }
1804
+ seenMessageKeysRef.current.set(dedupeKey, now);
1805
+ }
1806
+ emitDebug({ type: "subscription", phase: "message", event });
1807
+ onMessageRef.current(payload, meta);
1808
+ });
1809
+ return () => {
1810
+ emitDebug({ type: "subscription", phase: "unregister", event });
1811
+ unsubscribe();
1812
+ onCleanupRef.current?.();
1813
+ emitDebug({ type: "lifecycle", phase: "effect_cleanup", event });
1814
+ };
1815
+ }, [client, enabled, event, emitDebug]);
1816
+ }
1817
+ function useSocketConnection(roomsArgs, eventArgs) {
1818
+ useSocketRooms(roomsArgs);
1819
+ useSocketEvent(eventArgs);
1820
+ }
1767
1821
 
1768
1822
  // src/sockets/socket.client.context.debug.ts
1769
1823
  function dbg(dbgOpts, e) {
@@ -2685,7 +2739,9 @@ function buildSocketProvider(args) {
2685
2739
  }
2686
2740
  ),
2687
2741
  useSocketClient: () => useSocketClient(),
2688
- useSocketConnection: (p) => useSocketConnection(p)
2742
+ useSocketRooms: (p) => useSocketRooms(p),
2743
+ useSocketEvent: (p) => useSocketEvent(p),
2744
+ useSocketConnection: (...args2) => useSocketConnection(...args2)
2689
2745
  };
2690
2746
  }
2691
2747
  function SocketProvider(props) {
@@ -2900,7 +2956,7 @@ function trackHookTrigger2({
2900
2956
  }
2901
2957
 
2902
2958
  // src/sockets/socketedRoute/socket.client.helper.rooms.ts
2903
- function normalizeRooms(rooms) {
2959
+ function normalizeRooms2(rooms) {
2904
2960
  if (rooms == null) return [];
2905
2961
  const list = Array.isArray(rooms) ? rooms : [rooms];
2906
2962
  const seen = /* @__PURE__ */ new Set();
@@ -2925,7 +2981,7 @@ function roomStateEqual(prev, next) {
2925
2981
  }
2926
2982
  function mergeRoomState(prev, toRoomsResult) {
2927
2983
  const merged = new Set(prev.rooms);
2928
- for (const r of normalizeRooms(toRoomsResult.rooms)) merged.add(r);
2984
+ for (const r of normalizeRooms2(toRoomsResult.rooms)) merged.add(r);
2929
2985
  return {
2930
2986
  rooms: Array.from(merged),
2931
2987
  joinMeta: toRoomsResult.joinMeta ?? prev.joinMeta,
@@ -3287,6 +3343,8 @@ function buildSocketedRoute(options) {
3287
3343
  createRouteClient,
3288
3344
  defaultFetcher,
3289
3345
  useSocketClient,
3290
- useSocketConnection
3346
+ useSocketConnection,
3347
+ useSocketEvent,
3348
+ useSocketRooms
3291
3349
  });
3292
3350
  //# sourceMappingURL=index.cjs.map