@emeryld/rrroutes-client 2.7.12 → 2.8.0

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
@@ -39,8 +39,7 @@ __export(index_exports, {
39
39
  buildSocketedRoute: () => buildSocketedRoute,
40
40
  createRouteClient: () => createRouteClient,
41
41
  defaultFetcher: () => defaultFetcher,
42
- useSocketClient: () => useSocketClient,
43
- useSocketConnection: () => useSocketConnection
42
+ useSocketClient: () => useSocketClient
44
43
  });
45
44
  module.exports = __toCommonJS(index_exports);
46
45
 
@@ -1479,6 +1478,12 @@ function createRouteClient(opts) {
1479
1478
  configurable: false,
1480
1479
  writable: false
1481
1480
  });
1481
+ Object.defineProperty(built, "metadata", {
1482
+ value: { leaf },
1483
+ enumerable: true,
1484
+ configurable: false,
1485
+ writable: false
1486
+ });
1482
1487
  return built;
1483
1488
  }
1484
1489
  const fetchRaw = async (input) => {
@@ -1589,25 +1594,76 @@ function useSocketClient() {
1589
1594
 
1590
1595
  // src/sockets/socket.client.context.connection.ts
1591
1596
  var React2 = __toESM(require("react"), 1);
1592
- function useSocketConnection(args) {
1597
+ function normalizeRooms(rooms) {
1598
+ if (rooms == null) return [];
1599
+ return Array.isArray(rooms) ? rooms : [rooms];
1600
+ }
1601
+ function safeSerialize(value) {
1602
+ try {
1603
+ return JSON.stringify(value) ?? String(value);
1604
+ } catch {
1605
+ return "[unserializable]";
1606
+ }
1607
+ }
1608
+ function derivePayloadIdentity(payload) {
1609
+ if (!payload || typeof payload !== "object") return void 0;
1610
+ const record = payload;
1611
+ const idCandidate = record.id ?? record._id ?? record.notificationId ?? record.uuid ?? record.key;
1612
+ if (typeof idCandidate === "string" || typeof idCandidate === "number" || typeof idCandidate === "bigint") {
1613
+ return String(idCandidate);
1614
+ }
1615
+ return void 0;
1616
+ }
1617
+ function useLatestRef(value) {
1618
+ const ref = React2.useRef(value);
1619
+ React2.useEffect(() => {
1620
+ ref.current = value;
1621
+ }, [value]);
1622
+ return ref;
1623
+ }
1624
+ function useDebugEmitter(debug) {
1625
+ const debugRef = useLatestRef(debug);
1626
+ return React2.useCallback((event) => {
1627
+ const dbg3 = debugRef.current;
1628
+ if (!dbg3?.enabled) return;
1629
+ if (dbg3[event.type] === false) return;
1630
+ const logger = dbg3.logger;
1631
+ if (logger) {
1632
+ try {
1633
+ logger(event);
1634
+ } catch (error) {
1635
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
1636
+ console.warn("[socket] debug logger threw", error);
1637
+ }
1638
+ }
1639
+ return;
1640
+ }
1641
+ if (typeof console !== "undefined" && typeof console.log === "function") {
1642
+ console.log("[socket]", event);
1643
+ }
1644
+ }, []);
1645
+ }
1646
+ function useSocketRooms(args) {
1593
1647
  const {
1594
- event,
1595
1648
  rooms,
1596
- onMessage,
1597
- onCleanup,
1598
1649
  autoJoin = true,
1599
1650
  autoLeave = true,
1651
+ enabled = true,
1652
+ onCleanup,
1600
1653
  debug
1601
1654
  } = args;
1602
1655
  const client = useSocketClient();
1603
- const normalizedRooms = React2.useMemo(
1604
- () => rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms],
1605
- [rooms]
1606
- );
1656
+ const normalizedRooms = React2.useMemo(() => normalizeRooms(rooms), [rooms]);
1607
1657
  const normalizedRoomsKey = React2.useMemo(
1608
1658
  () => normalizedRooms.join(""),
1609
1659
  [normalizedRooms]
1610
1660
  );
1661
+ const joinMetaRef = useLatestRef(args.joinMeta);
1662
+ const leaveMetaRef = useLatestRef(args.leaveMeta);
1663
+ const onCleanupRef = useLatestRef(onCleanup);
1664
+ const debugRef = useLatestRef(debug);
1665
+ const emitDebug = useDebugEmitter(debug);
1666
+ const missingClientWarnedRef = React2.useRef(false);
1611
1667
  const joinMetaKey = React2.useMemo(
1612
1668
  () => JSON.stringify(args.joinMeta ?? null),
1613
1669
  [args.joinMeta]
@@ -1616,107 +1672,48 @@ function useSocketConnection(args) {
1616
1672
  () => JSON.stringify(args.leaveMeta ?? null),
1617
1673
  [args.leaveMeta]
1618
1674
  );
1619
- const missingClientWarnedRef = React2.useRef(false);
1620
- const onMessageRef = React2.useRef(onMessage);
1621
- const onCleanupRef = React2.useRef(onCleanup);
1622
- const joinMetaRef = React2.useRef(args.joinMeta);
1623
- const leaveMetaRef = React2.useRef(args.leaveMeta);
1624
- const debugRef = React2.useRef(debug);
1625
- React2.useEffect(() => {
1626
- onMessageRef.current = onMessage;
1627
- }, [onMessage]);
1628
- React2.useEffect(() => {
1629
- onCleanupRef.current = onCleanup;
1630
- }, [onCleanup]);
1631
- React2.useEffect(() => {
1632
- joinMetaRef.current = args.joinMeta;
1633
- }, [args.joinMeta]);
1634
- React2.useEffect(() => {
1635
- leaveMetaRef.current = args.leaveMeta;
1636
- }, [args.leaveMeta]);
1637
- React2.useEffect(() => {
1638
- debugRef.current = debug;
1639
- }, [debug]);
1640
- const emitDebug = React2.useCallback(
1641
- (event2) => {
1642
- const dbg3 = debugRef.current;
1643
- if (!dbg3?.enabled) return;
1644
- if (dbg3[event2.type] === false) return;
1645
- const logger = dbg3.logger;
1646
- if (logger) {
1647
- try {
1648
- logger(event2);
1649
- } catch (error) {
1650
- if (typeof console !== "undefined" && typeof console.warn === "function") {
1651
- console.warn("[socket] useSocketConnection debug logger threw", error);
1652
- }
1653
- }
1654
- return;
1655
- }
1656
- if (typeof console !== "undefined" && typeof console.log === "function") {
1657
- console.log("[socket] useSocketConnection", event2);
1658
- }
1659
- },
1660
- []
1661
- );
1662
- const reportAsyncError = React2.useCallback(
1663
- (phase, error) => {
1664
- if (typeof console !== "undefined" && typeof console.warn === "function") {
1665
- console.warn(`[socket] useSocketConnection ${phase} failed`, error);
1666
- }
1667
- },
1668
- []
1669
- );
1670
1675
  React2.useEffect(() => {
1671
1676
  emitDebug({
1672
1677
  type: "lifecycle",
1673
1678
  phase: "effect_start",
1674
- event,
1675
1679
  rooms: normalizedRooms,
1676
1680
  autoJoin,
1677
1681
  autoLeave
1678
1682
  });
1683
+ if (!enabled) {
1684
+ emitDebug({
1685
+ type: "room",
1686
+ phase: "join_skip",
1687
+ rooms: normalizedRooms,
1688
+ reason: "disabled"
1689
+ });
1690
+ return;
1691
+ }
1679
1692
  if (!client) {
1680
1693
  emitDebug({
1681
1694
  type: "lifecycle",
1682
1695
  phase: "client_missing",
1683
- event,
1684
1696
  rooms: normalizedRooms,
1685
1697
  autoJoin,
1686
1698
  autoLeave
1687
1699
  });
1688
- if (debugRef.current?.throwIfClientMissing) {
1689
- throw new Error(
1690
- `useSocketConnection("${event}") missing SocketClient. Wrap with <SocketProvider>.`
1691
- );
1692
- }
1693
1700
  if (debugRef.current?.warnIfClientMissing && !missingClientWarnedRef.current && typeof console !== "undefined" && typeof console.warn === "function") {
1694
1701
  missingClientWarnedRef.current = true;
1695
- console.warn(
1696
- `[socket] useSocketConnection("${event}") skipped because SocketClient is null.`
1697
- );
1702
+ console.warn("[socket] useSocketRooms skipped because SocketClient is null.");
1698
1703
  }
1699
1704
  return;
1700
1705
  }
1701
1706
  missingClientWarnedRef.current = false;
1702
- emitDebug({ type: "subscription", phase: "register", event });
1703
1707
  if (autoJoin && normalizedRooms.length > 0) {
1704
- emitDebug({
1705
- type: "room",
1706
- phase: "join_attempt",
1707
- rooms: normalizedRooms
1708
- });
1709
- void client.joinRooms(normalizedRooms, joinMetaRef.current).then(() => {
1710
- emitDebug({ type: "room", phase: "join_ok", rooms: normalizedRooms });
1711
- }).catch((error) => {
1712
- emitDebug({
1708
+ emitDebug({ type: "room", phase: "join_attempt", rooms: normalizedRooms });
1709
+ void client.joinRooms(normalizedRooms, joinMetaRef.current).then(() => emitDebug({ type: "room", phase: "join_ok", rooms: normalizedRooms })).catch(
1710
+ (error) => emitDebug({
1713
1711
  type: "room",
1714
1712
  phase: "join_error",
1715
1713
  rooms: normalizedRooms,
1716
1714
  err: String(error)
1717
- });
1718
- reportAsyncError("joinRooms", error);
1719
- });
1715
+ })
1716
+ );
1720
1717
  } else {
1721
1718
  emitDebug({
1722
1719
  type: "room",
@@ -1725,30 +1722,23 @@ function useSocketConnection(args) {
1725
1722
  reason: autoJoin ? "no_rooms" : "auto_disabled"
1726
1723
  });
1727
1724
  }
1728
- const unsubscribe = client.on(event, (payload, meta) => {
1729
- emitDebug({ type: "subscription", phase: "message", event });
1730
- onMessageRef.current(payload, meta);
1731
- });
1732
1725
  return () => {
1733
- emitDebug({ type: "subscription", phase: "unregister", event });
1734
- unsubscribe();
1735
1726
  if (autoLeave && normalizedRooms.length > 0) {
1736
1727
  emitDebug({
1737
1728
  type: "room",
1738
1729
  phase: "leave_attempt",
1739
1730
  rooms: normalizedRooms
1740
1731
  });
1741
- void client.leaveRooms(normalizedRooms, leaveMetaRef.current).then(() => {
1742
- emitDebug({ type: "room", phase: "leave_ok", rooms: normalizedRooms });
1743
- }).catch((error) => {
1744
- emitDebug({
1732
+ void client.leaveRooms(normalizedRooms, leaveMetaRef.current).then(
1733
+ () => emitDebug({ type: "room", phase: "leave_ok", rooms: normalizedRooms })
1734
+ ).catch(
1735
+ (error) => emitDebug({
1745
1736
  type: "room",
1746
1737
  phase: "leave_error",
1747
1738
  rooms: normalizedRooms,
1748
1739
  err: String(error)
1749
- });
1750
- reportAsyncError("leaveRooms", error);
1751
- });
1740
+ })
1741
+ );
1752
1742
  } else {
1753
1743
  emitDebug({
1754
1744
  type: "room",
@@ -1757,11 +1747,10 @@ function useSocketConnection(args) {
1757
1747
  reason: autoLeave ? "no_rooms" : "auto_disabled"
1758
1748
  });
1759
1749
  }
1760
- if (onCleanupRef.current) onCleanupRef.current();
1750
+ onCleanupRef.current?.();
1761
1751
  emitDebug({
1762
1752
  type: "lifecycle",
1763
1753
  phase: "effect_cleanup",
1764
- event,
1765
1754
  rooms: normalizedRooms,
1766
1755
  autoJoin,
1767
1756
  autoLeave
@@ -1769,16 +1758,79 @@ function useSocketConnection(args) {
1769
1758
  };
1770
1759
  }, [
1771
1760
  client,
1772
- event,
1761
+ enabled,
1773
1762
  autoJoin,
1774
1763
  autoLeave,
1775
- emitDebug,
1776
- reportAsyncError,
1777
1764
  normalizedRoomsKey,
1778
1765
  joinMetaKey,
1779
- leaveMetaKey
1766
+ leaveMetaKey,
1767
+ emitDebug
1780
1768
  ]);
1781
1769
  }
1770
+ function useSocketEvent(args) {
1771
+ const {
1772
+ event,
1773
+ onMessage,
1774
+ onCleanup,
1775
+ enabled = true,
1776
+ messageDeduplication,
1777
+ debug
1778
+ } = args;
1779
+ const client = useSocketClient();
1780
+ const onMessageRef = useLatestRef(onMessage);
1781
+ const onCleanupRef = useLatestRef(onCleanup);
1782
+ const messageDeduplicationRef = useLatestRef(messageDeduplication);
1783
+ const debugRef = useLatestRef(debug);
1784
+ const emitDebug = useDebugEmitter(debug);
1785
+ const missingClientWarnedRef = React2.useRef(false);
1786
+ const seenMessageKeysRef = React2.useRef(/* @__PURE__ */ new Map());
1787
+ React2.useEffect(() => {
1788
+ emitDebug({ type: "lifecycle", phase: "effect_start", event });
1789
+ if (!enabled) return;
1790
+ if (!client) {
1791
+ emitDebug({ type: "lifecycle", phase: "client_missing", event });
1792
+ if (debugRef.current?.warnIfClientMissing && !missingClientWarnedRef.current && typeof console !== "undefined" && typeof console.warn === "function") {
1793
+ missingClientWarnedRef.current = true;
1794
+ console.warn(`[socket] useSocketEvent("${event}") skipped because SocketClient is null.`);
1795
+ }
1796
+ return;
1797
+ }
1798
+ missingClientWarnedRef.current = false;
1799
+ emitDebug({ type: "subscription", phase: "register", event });
1800
+ const unsubscribe = client.on(event, (payload, meta) => {
1801
+ const dedupeCfg = messageDeduplicationRef.current;
1802
+ if (dedupeCfg !== false) {
1803
+ const ttlMs = Math.max(0, dedupeCfg?.ttlMs ?? 2500);
1804
+ const now = Date.now();
1805
+ const payloadId = derivePayloadIdentity(payload);
1806
+ const dedupeKey = dedupeCfg?.key?.(payload, meta) ?? (payloadId ? `${event}|payload:${payloadId}` : void 0) ?? (meta?.envelope ? `${event}|${String(meta.envelope.eventName)}|${String(meta.envelope.sentAt)}|${safeSerialize(payload)}` : `${event}|${safeSerialize(payload)}`);
1807
+ const prev = seenMessageKeysRef.current.get(dedupeKey);
1808
+ if (typeof prev === "number" && now - prev <= ttlMs) {
1809
+ emitDebug({
1810
+ type: "subscription",
1811
+ phase: "message_dropped_duplicate",
1812
+ event,
1813
+ dedupeKey
1814
+ });
1815
+ return;
1816
+ }
1817
+ seenMessageKeysRef.current.set(dedupeKey, now);
1818
+ }
1819
+ emitDebug({ type: "subscription", phase: "message", event });
1820
+ onMessageRef.current(payload, meta);
1821
+ });
1822
+ return () => {
1823
+ emitDebug({ type: "subscription", phase: "unregister", event });
1824
+ unsubscribe();
1825
+ onCleanupRef.current?.();
1826
+ emitDebug({ type: "lifecycle", phase: "effect_cleanup", event });
1827
+ };
1828
+ }, [client, enabled, event, emitDebug]);
1829
+ }
1830
+ function useSocketConnection(roomsArgs, eventArgs) {
1831
+ useSocketRooms(roomsArgs);
1832
+ useSocketEvent(eventArgs);
1833
+ }
1782
1834
 
1783
1835
  // src/sockets/socket.client.context.debug.ts
1784
1836
  function dbg(dbgOpts, e) {
@@ -2700,7 +2752,9 @@ function buildSocketProvider(args) {
2700
2752
  }
2701
2753
  ),
2702
2754
  useSocketClient: () => useSocketClient(),
2703
- useSocketConnection: (p) => useSocketConnection(p)
2755
+ useSocketRooms: (p) => useSocketRooms(p),
2756
+ useSocketEvent: (p) => useSocketEvent(p),
2757
+ useSocketConnection: (...args2) => useSocketConnection(...args2)
2704
2758
  };
2705
2759
  }
2706
2760
  function SocketProvider(props) {
@@ -2915,7 +2969,7 @@ function trackHookTrigger2({
2915
2969
  }
2916
2970
 
2917
2971
  // src/sockets/socketedRoute/socket.client.helper.rooms.ts
2918
- function normalizeRooms(rooms) {
2972
+ function normalizeRooms2(rooms) {
2919
2973
  if (rooms == null) return [];
2920
2974
  const list = Array.isArray(rooms) ? rooms : [rooms];
2921
2975
  const seen = /* @__PURE__ */ new Set();
@@ -2940,7 +2994,7 @@ function roomStateEqual(prev, next) {
2940
2994
  }
2941
2995
  function mergeRoomState(prev, toRoomsResult) {
2942
2996
  const merged = new Set(prev.rooms);
2943
- for (const r of normalizeRooms(toRoomsResult.rooms)) merged.add(r);
2997
+ for (const r of normalizeRooms2(toRoomsResult.rooms)) merged.add(r);
2944
2998
  return {
2945
2999
  rooms: Array.from(merged),
2946
3000
  joinMeta: toRoomsResult.joinMeta ?? prev.joinMeta,
@@ -3301,7 +3355,6 @@ function buildSocketedRoute(options) {
3301
3355
  buildSocketedRoute,
3302
3356
  createRouteClient,
3303
3357
  defaultFetcher,
3304
- useSocketClient,
3305
- useSocketConnection
3358
+ useSocketClient
3306
3359
  });
3307
3360
  //# sourceMappingURL=index.cjs.map