@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.mjs CHANGED
@@ -1448,6 +1448,12 @@ function createRouteClient(opts) {
1448
1448
  configurable: false,
1449
1449
  writable: false
1450
1450
  });
1451
+ Object.defineProperty(built, "metadata", {
1452
+ value: { leaf },
1453
+ enumerable: true,
1454
+ configurable: false,
1455
+ writable: false
1456
+ });
1451
1457
  return built;
1452
1458
  }
1453
1459
  const fetchRaw = async (input) => {
@@ -1558,25 +1564,76 @@ function useSocketClient() {
1558
1564
 
1559
1565
  // src/sockets/socket.client.context.connection.ts
1560
1566
  import * as React2 from "react";
1561
- function useSocketConnection(args) {
1567
+ function normalizeRooms(rooms) {
1568
+ if (rooms == null) return [];
1569
+ return Array.isArray(rooms) ? rooms : [rooms];
1570
+ }
1571
+ function safeSerialize(value) {
1572
+ try {
1573
+ return JSON.stringify(value) ?? String(value);
1574
+ } catch {
1575
+ return "[unserializable]";
1576
+ }
1577
+ }
1578
+ function derivePayloadIdentity(payload) {
1579
+ if (!payload || typeof payload !== "object") return void 0;
1580
+ const record = payload;
1581
+ const idCandidate = record.id ?? record._id ?? record.notificationId ?? record.uuid ?? record.key;
1582
+ if (typeof idCandidate === "string" || typeof idCandidate === "number" || typeof idCandidate === "bigint") {
1583
+ return String(idCandidate);
1584
+ }
1585
+ return void 0;
1586
+ }
1587
+ function useLatestRef(value) {
1588
+ const ref = React2.useRef(value);
1589
+ React2.useEffect(() => {
1590
+ ref.current = value;
1591
+ }, [value]);
1592
+ return ref;
1593
+ }
1594
+ function useDebugEmitter(debug) {
1595
+ const debugRef = useLatestRef(debug);
1596
+ return React2.useCallback((event) => {
1597
+ const dbg3 = debugRef.current;
1598
+ if (!dbg3?.enabled) return;
1599
+ if (dbg3[event.type] === false) return;
1600
+ const logger = dbg3.logger;
1601
+ if (logger) {
1602
+ try {
1603
+ logger(event);
1604
+ } catch (error) {
1605
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
1606
+ console.warn("[socket] debug logger threw", error);
1607
+ }
1608
+ }
1609
+ return;
1610
+ }
1611
+ if (typeof console !== "undefined" && typeof console.log === "function") {
1612
+ console.log("[socket]", event);
1613
+ }
1614
+ }, []);
1615
+ }
1616
+ function useSocketRooms(args) {
1562
1617
  const {
1563
- event,
1564
1618
  rooms,
1565
- onMessage,
1566
- onCleanup,
1567
1619
  autoJoin = true,
1568
1620
  autoLeave = true,
1621
+ enabled = true,
1622
+ onCleanup,
1569
1623
  debug
1570
1624
  } = args;
1571
1625
  const client = useSocketClient();
1572
- const normalizedRooms = React2.useMemo(
1573
- () => rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms],
1574
- [rooms]
1575
- );
1626
+ const normalizedRooms = React2.useMemo(() => normalizeRooms(rooms), [rooms]);
1576
1627
  const normalizedRoomsKey = React2.useMemo(
1577
1628
  () => normalizedRooms.join(""),
1578
1629
  [normalizedRooms]
1579
1630
  );
1631
+ const joinMetaRef = useLatestRef(args.joinMeta);
1632
+ const leaveMetaRef = useLatestRef(args.leaveMeta);
1633
+ const onCleanupRef = useLatestRef(onCleanup);
1634
+ const debugRef = useLatestRef(debug);
1635
+ const emitDebug = useDebugEmitter(debug);
1636
+ const missingClientWarnedRef = React2.useRef(false);
1580
1637
  const joinMetaKey = React2.useMemo(
1581
1638
  () => JSON.stringify(args.joinMeta ?? null),
1582
1639
  [args.joinMeta]
@@ -1585,107 +1642,48 @@ function useSocketConnection(args) {
1585
1642
  () => JSON.stringify(args.leaveMeta ?? null),
1586
1643
  [args.leaveMeta]
1587
1644
  );
1588
- const missingClientWarnedRef = React2.useRef(false);
1589
- const onMessageRef = React2.useRef(onMessage);
1590
- const onCleanupRef = React2.useRef(onCleanup);
1591
- const joinMetaRef = React2.useRef(args.joinMeta);
1592
- const leaveMetaRef = React2.useRef(args.leaveMeta);
1593
- const debugRef = React2.useRef(debug);
1594
- React2.useEffect(() => {
1595
- onMessageRef.current = onMessage;
1596
- }, [onMessage]);
1597
- React2.useEffect(() => {
1598
- onCleanupRef.current = onCleanup;
1599
- }, [onCleanup]);
1600
- React2.useEffect(() => {
1601
- joinMetaRef.current = args.joinMeta;
1602
- }, [args.joinMeta]);
1603
- React2.useEffect(() => {
1604
- leaveMetaRef.current = args.leaveMeta;
1605
- }, [args.leaveMeta]);
1606
- React2.useEffect(() => {
1607
- debugRef.current = debug;
1608
- }, [debug]);
1609
- const emitDebug = React2.useCallback(
1610
- (event2) => {
1611
- const dbg3 = debugRef.current;
1612
- if (!dbg3?.enabled) return;
1613
- if (dbg3[event2.type] === false) return;
1614
- const logger = dbg3.logger;
1615
- if (logger) {
1616
- try {
1617
- logger(event2);
1618
- } catch (error) {
1619
- if (typeof console !== "undefined" && typeof console.warn === "function") {
1620
- console.warn("[socket] useSocketConnection debug logger threw", error);
1621
- }
1622
- }
1623
- return;
1624
- }
1625
- if (typeof console !== "undefined" && typeof console.log === "function") {
1626
- console.log("[socket] useSocketConnection", event2);
1627
- }
1628
- },
1629
- []
1630
- );
1631
- const reportAsyncError = React2.useCallback(
1632
- (phase, error) => {
1633
- if (typeof console !== "undefined" && typeof console.warn === "function") {
1634
- console.warn(`[socket] useSocketConnection ${phase} failed`, error);
1635
- }
1636
- },
1637
- []
1638
- );
1639
1645
  React2.useEffect(() => {
1640
1646
  emitDebug({
1641
1647
  type: "lifecycle",
1642
1648
  phase: "effect_start",
1643
- event,
1644
1649
  rooms: normalizedRooms,
1645
1650
  autoJoin,
1646
1651
  autoLeave
1647
1652
  });
1653
+ if (!enabled) {
1654
+ emitDebug({
1655
+ type: "room",
1656
+ phase: "join_skip",
1657
+ rooms: normalizedRooms,
1658
+ reason: "disabled"
1659
+ });
1660
+ return;
1661
+ }
1648
1662
  if (!client) {
1649
1663
  emitDebug({
1650
1664
  type: "lifecycle",
1651
1665
  phase: "client_missing",
1652
- event,
1653
1666
  rooms: normalizedRooms,
1654
1667
  autoJoin,
1655
1668
  autoLeave
1656
1669
  });
1657
- if (debugRef.current?.throwIfClientMissing) {
1658
- throw new Error(
1659
- `useSocketConnection("${event}") missing SocketClient. Wrap with <SocketProvider>.`
1660
- );
1661
- }
1662
1670
  if (debugRef.current?.warnIfClientMissing && !missingClientWarnedRef.current && typeof console !== "undefined" && typeof console.warn === "function") {
1663
1671
  missingClientWarnedRef.current = true;
1664
- console.warn(
1665
- `[socket] useSocketConnection("${event}") skipped because SocketClient is null.`
1666
- );
1672
+ console.warn("[socket] useSocketRooms skipped because SocketClient is null.");
1667
1673
  }
1668
1674
  return;
1669
1675
  }
1670
1676
  missingClientWarnedRef.current = false;
1671
- emitDebug({ type: "subscription", phase: "register", event });
1672
1677
  if (autoJoin && normalizedRooms.length > 0) {
1673
- emitDebug({
1674
- type: "room",
1675
- phase: "join_attempt",
1676
- rooms: normalizedRooms
1677
- });
1678
- void client.joinRooms(normalizedRooms, joinMetaRef.current).then(() => {
1679
- emitDebug({ type: "room", phase: "join_ok", rooms: normalizedRooms });
1680
- }).catch((error) => {
1681
- emitDebug({
1678
+ emitDebug({ type: "room", phase: "join_attempt", rooms: normalizedRooms });
1679
+ void client.joinRooms(normalizedRooms, joinMetaRef.current).then(() => emitDebug({ type: "room", phase: "join_ok", rooms: normalizedRooms })).catch(
1680
+ (error) => emitDebug({
1682
1681
  type: "room",
1683
1682
  phase: "join_error",
1684
1683
  rooms: normalizedRooms,
1685
1684
  err: String(error)
1686
- });
1687
- reportAsyncError("joinRooms", error);
1688
- });
1685
+ })
1686
+ );
1689
1687
  } else {
1690
1688
  emitDebug({
1691
1689
  type: "room",
@@ -1694,30 +1692,23 @@ function useSocketConnection(args) {
1694
1692
  reason: autoJoin ? "no_rooms" : "auto_disabled"
1695
1693
  });
1696
1694
  }
1697
- const unsubscribe = client.on(event, (payload, meta) => {
1698
- emitDebug({ type: "subscription", phase: "message", event });
1699
- onMessageRef.current(payload, meta);
1700
- });
1701
1695
  return () => {
1702
- emitDebug({ type: "subscription", phase: "unregister", event });
1703
- unsubscribe();
1704
1696
  if (autoLeave && normalizedRooms.length > 0) {
1705
1697
  emitDebug({
1706
1698
  type: "room",
1707
1699
  phase: "leave_attempt",
1708
1700
  rooms: normalizedRooms
1709
1701
  });
1710
- void client.leaveRooms(normalizedRooms, leaveMetaRef.current).then(() => {
1711
- emitDebug({ type: "room", phase: "leave_ok", rooms: normalizedRooms });
1712
- }).catch((error) => {
1713
- emitDebug({
1702
+ void client.leaveRooms(normalizedRooms, leaveMetaRef.current).then(
1703
+ () => emitDebug({ type: "room", phase: "leave_ok", rooms: normalizedRooms })
1704
+ ).catch(
1705
+ (error) => emitDebug({
1714
1706
  type: "room",
1715
1707
  phase: "leave_error",
1716
1708
  rooms: normalizedRooms,
1717
1709
  err: String(error)
1718
- });
1719
- reportAsyncError("leaveRooms", error);
1720
- });
1710
+ })
1711
+ );
1721
1712
  } else {
1722
1713
  emitDebug({
1723
1714
  type: "room",
@@ -1726,11 +1717,10 @@ function useSocketConnection(args) {
1726
1717
  reason: autoLeave ? "no_rooms" : "auto_disabled"
1727
1718
  });
1728
1719
  }
1729
- if (onCleanupRef.current) onCleanupRef.current();
1720
+ onCleanupRef.current?.();
1730
1721
  emitDebug({
1731
1722
  type: "lifecycle",
1732
1723
  phase: "effect_cleanup",
1733
- event,
1734
1724
  rooms: normalizedRooms,
1735
1725
  autoJoin,
1736
1726
  autoLeave
@@ -1738,16 +1728,79 @@ function useSocketConnection(args) {
1738
1728
  };
1739
1729
  }, [
1740
1730
  client,
1741
- event,
1731
+ enabled,
1742
1732
  autoJoin,
1743
1733
  autoLeave,
1744
- emitDebug,
1745
- reportAsyncError,
1746
1734
  normalizedRoomsKey,
1747
1735
  joinMetaKey,
1748
- leaveMetaKey
1736
+ leaveMetaKey,
1737
+ emitDebug
1749
1738
  ]);
1750
1739
  }
1740
+ function useSocketEvent(args) {
1741
+ const {
1742
+ event,
1743
+ onMessage,
1744
+ onCleanup,
1745
+ enabled = true,
1746
+ messageDeduplication,
1747
+ debug
1748
+ } = args;
1749
+ const client = useSocketClient();
1750
+ const onMessageRef = useLatestRef(onMessage);
1751
+ const onCleanupRef = useLatestRef(onCleanup);
1752
+ const messageDeduplicationRef = useLatestRef(messageDeduplication);
1753
+ const debugRef = useLatestRef(debug);
1754
+ const emitDebug = useDebugEmitter(debug);
1755
+ const missingClientWarnedRef = React2.useRef(false);
1756
+ const seenMessageKeysRef = React2.useRef(/* @__PURE__ */ new Map());
1757
+ React2.useEffect(() => {
1758
+ emitDebug({ type: "lifecycle", phase: "effect_start", event });
1759
+ if (!enabled) return;
1760
+ if (!client) {
1761
+ emitDebug({ type: "lifecycle", phase: "client_missing", event });
1762
+ if (debugRef.current?.warnIfClientMissing && !missingClientWarnedRef.current && typeof console !== "undefined" && typeof console.warn === "function") {
1763
+ missingClientWarnedRef.current = true;
1764
+ console.warn(`[socket] useSocketEvent("${event}") skipped because SocketClient is null.`);
1765
+ }
1766
+ return;
1767
+ }
1768
+ missingClientWarnedRef.current = false;
1769
+ emitDebug({ type: "subscription", phase: "register", event });
1770
+ const unsubscribe = client.on(event, (payload, meta) => {
1771
+ const dedupeCfg = messageDeduplicationRef.current;
1772
+ if (dedupeCfg !== false) {
1773
+ const ttlMs = Math.max(0, dedupeCfg?.ttlMs ?? 2500);
1774
+ const now = Date.now();
1775
+ const payloadId = derivePayloadIdentity(payload);
1776
+ 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)}`);
1777
+ const prev = seenMessageKeysRef.current.get(dedupeKey);
1778
+ if (typeof prev === "number" && now - prev <= ttlMs) {
1779
+ emitDebug({
1780
+ type: "subscription",
1781
+ phase: "message_dropped_duplicate",
1782
+ event,
1783
+ dedupeKey
1784
+ });
1785
+ return;
1786
+ }
1787
+ seenMessageKeysRef.current.set(dedupeKey, now);
1788
+ }
1789
+ emitDebug({ type: "subscription", phase: "message", event });
1790
+ onMessageRef.current(payload, meta);
1791
+ });
1792
+ return () => {
1793
+ emitDebug({ type: "subscription", phase: "unregister", event });
1794
+ unsubscribe();
1795
+ onCleanupRef.current?.();
1796
+ emitDebug({ type: "lifecycle", phase: "effect_cleanup", event });
1797
+ };
1798
+ }, [client, enabled, event, emitDebug]);
1799
+ }
1800
+ function useSocketConnection(roomsArgs, eventArgs) {
1801
+ useSocketRooms(roomsArgs);
1802
+ useSocketEvent(eventArgs);
1803
+ }
1751
1804
 
1752
1805
  // src/sockets/socket.client.context.debug.ts
1753
1806
  function dbg(dbgOpts, e) {
@@ -2669,7 +2722,9 @@ function buildSocketProvider(args) {
2669
2722
  }
2670
2723
  ),
2671
2724
  useSocketClient: () => useSocketClient(),
2672
- useSocketConnection: (p) => useSocketConnection(p)
2725
+ useSocketRooms: (p) => useSocketRooms(p),
2726
+ useSocketEvent: (p) => useSocketEvent(p),
2727
+ useSocketConnection: (...args2) => useSocketConnection(...args2)
2673
2728
  };
2674
2729
  }
2675
2730
  function SocketProvider(props) {
@@ -2884,7 +2939,7 @@ function trackHookTrigger2({
2884
2939
  }
2885
2940
 
2886
2941
  // src/sockets/socketedRoute/socket.client.helper.rooms.ts
2887
- function normalizeRooms(rooms) {
2942
+ function normalizeRooms2(rooms) {
2888
2943
  if (rooms == null) return [];
2889
2944
  const list = Array.isArray(rooms) ? rooms : [rooms];
2890
2945
  const seen = /* @__PURE__ */ new Set();
@@ -2909,7 +2964,7 @@ function roomStateEqual(prev, next) {
2909
2964
  }
2910
2965
  function mergeRoomState(prev, toRoomsResult) {
2911
2966
  const merged = new Set(prev.rooms);
2912
- for (const r of normalizeRooms(toRoomsResult.rooms)) merged.add(r);
2967
+ for (const r of normalizeRooms2(toRoomsResult.rooms)) merged.add(r);
2913
2968
  return {
2914
2969
  rooms: Array.from(merged),
2915
2970
  joinMeta: toRoomsResult.joinMeta ?? prev.joinMeta,
@@ -3269,7 +3324,6 @@ export {
3269
3324
  buildSocketedRoute,
3270
3325
  createRouteClient,
3271
3326
  defaultFetcher,
3272
- useSocketClient,
3273
- useSocketConnection
3327
+ useSocketClient
3274
3328
  };
3275
3329
  //# sourceMappingURL=index.mjs.map