@sesionario/blocks 1.13.0 → 1.14.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.d.cts CHANGED
@@ -1123,8 +1123,12 @@ type UseRealtimeChannelOptions = {
1123
1123
  };
1124
1124
  /**
1125
1125
  * Subscribe to a realtime-svc channel WebSocket.
1126
- * Reconnects with exponential backoff; refreshes the URL (token) on each attempt.
1127
- * Treats events as invalidation signals — callers should re-fetch authoritative data.
1126
+ * Components on the same channel share one socket (ref-counted; closed when
1127
+ * the last subscriber unmounts). Reconnects with exponential backoff and
1128
+ * refreshes the URL (token) on each attempt; if the URL resolves null the
1129
+ * connection goes idle until the tab becomes visible again.
1130
+ * Treats events as invalidation signals — callers should re-fetch
1131
+ * authoritative data.
1128
1132
  */
1129
1133
  declare function useRealtimeChannel({ getWsUrl, onEvent, enabled, reconnectDelayMs, }: UseRealtimeChannelOptions): void;
1130
1134
 
package/dist/index.d.ts CHANGED
@@ -1123,8 +1123,12 @@ type UseRealtimeChannelOptions = {
1123
1123
  };
1124
1124
  /**
1125
1125
  * Subscribe to a realtime-svc channel WebSocket.
1126
- * Reconnects with exponential backoff; refreshes the URL (token) on each attempt.
1127
- * Treats events as invalidation signals — callers should re-fetch authoritative data.
1126
+ * Components on the same channel share one socket (ref-counted; closed when
1127
+ * the last subscriber unmounts). Reconnects with exponential backoff and
1128
+ * refreshes the URL (token) on each attempt; if the URL resolves null the
1129
+ * connection goes idle until the tab becomes visible again.
1130
+ * Treats events as invalidation signals — callers should re-fetch
1131
+ * authoritative data.
1128
1132
  */
1129
1133
  declare function useRealtimeChannel({ getWsUrl, onEvent, enabled, reconnectDelayMs, }: UseRealtimeChannelOptions): void;
1130
1134
 
package/dist/index.js CHANGED
@@ -6154,7 +6154,7 @@ function MessageScrollerButton({
6154
6154
  "data-size": size,
6155
6155
  direction,
6156
6156
  className: cn(
6157
- "absolute inset-s-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180",
6157
+ "absolute left-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full data-[direction=start]:[&_svg]:rotate-180",
6158
6158
  className
6159
6159
  ),
6160
6160
  render: render ?? /* @__PURE__ */ jsx(Button, { variant, size }),
@@ -6954,6 +6954,162 @@ function createSupportChatClient(basePath = "/api/chat-svc") {
6954
6954
  }
6955
6955
  var DEFAULT_RECONNECT_MS = 2e3;
6956
6956
  var MAX_RECONNECT_MS = 3e4;
6957
+ var HEARTBEAT_MS = 3e4;
6958
+ var registry = /* @__PURE__ */ new Map();
6959
+ function channelKeyFromUrl(url) {
6960
+ const q = url.indexOf("?");
6961
+ return q === -1 ? url : url.slice(0, q);
6962
+ }
6963
+ function stopHeartbeat(shared) {
6964
+ if (shared.heartbeatTimer) {
6965
+ clearInterval(shared.heartbeatTimer);
6966
+ shared.heartbeatTimer = null;
6967
+ }
6968
+ }
6969
+ function startHeartbeat(shared, gen) {
6970
+ stopHeartbeat(shared);
6971
+ shared.heartbeatTimer = setInterval(() => {
6972
+ if (gen !== shared.gen) return;
6973
+ if (shared.ws?.readyState === WebSocket.OPEN) {
6974
+ try {
6975
+ shared.ws.send("ping");
6976
+ } catch {
6977
+ }
6978
+ }
6979
+ }, HEARTBEAT_MS);
6980
+ }
6981
+ function clearReconnect(shared) {
6982
+ if (shared.reconnectTimer) {
6983
+ clearTimeout(shared.reconnectTimer);
6984
+ shared.reconnectTimer = null;
6985
+ }
6986
+ }
6987
+ function openSocket(shared, url) {
6988
+ const gen = ++shared.gen;
6989
+ clearReconnect(shared);
6990
+ if (shared.ws) {
6991
+ try {
6992
+ shared.ws.close();
6993
+ } catch {
6994
+ }
6995
+ }
6996
+ const ws = new WebSocket(url);
6997
+ shared.ws = ws;
6998
+ ws.onopen = () => {
6999
+ if (gen !== shared.gen) return;
7000
+ shared.attempt = 0;
7001
+ startHeartbeat(shared, gen);
7002
+ };
7003
+ ws.onmessage = (msg) => {
7004
+ if (gen !== shared.gen) return;
7005
+ let data;
7006
+ try {
7007
+ data = JSON.parse(String(msg.data));
7008
+ } catch {
7009
+ return;
7010
+ }
7011
+ if (typeof data?.type !== "string") return;
7012
+ if (data.type === "pong") return;
7013
+ for (const listener of shared.listeners) {
7014
+ listener(data);
7015
+ }
7016
+ };
7017
+ ws.onclose = () => {
7018
+ if (gen !== shared.gen) return;
7019
+ shared.ws = null;
7020
+ stopHeartbeat(shared);
7021
+ scheduleReconnect(shared);
7022
+ };
7023
+ ws.onerror = () => {
7024
+ };
7025
+ }
7026
+ function scheduleReconnect(shared) {
7027
+ if (shared.listeners.size === 0) return;
7028
+ clearReconnect(shared);
7029
+ const delay = Math.min(
7030
+ shared.reconnectDelayMs * 2 ** shared.attempt,
7031
+ MAX_RECONNECT_MS
7032
+ );
7033
+ shared.attempt += 1;
7034
+ shared.reconnectTimer = setTimeout(() => {
7035
+ shared.reconnectTimer = null;
7036
+ void connectShared(shared);
7037
+ }, delay);
7038
+ }
7039
+ async function connectShared(shared) {
7040
+ if (shared.connecting || shared.listeners.size === 0) return;
7041
+ shared.connecting = true;
7042
+ let url = null;
7043
+ try {
7044
+ url = await shared.getWsUrl();
7045
+ } finally {
7046
+ shared.connecting = false;
7047
+ }
7048
+ if (shared.listeners.size === 0) return;
7049
+ if (!url) return;
7050
+ openSocket(shared, url);
7051
+ }
7052
+ function destroyShared(shared) {
7053
+ shared.gen += 1;
7054
+ clearReconnect(shared);
7055
+ stopHeartbeat(shared);
7056
+ if (shared.ws) {
7057
+ try {
7058
+ shared.ws.close();
7059
+ } catch {
7060
+ }
7061
+ shared.ws = null;
7062
+ }
7063
+ }
7064
+ function joinChannel(key, url, getWsUrl, reconnectDelayMs, listener) {
7065
+ ensureVisibilityHook();
7066
+ let shared = registry.get(key);
7067
+ if (!shared) {
7068
+ shared = {
7069
+ key,
7070
+ listeners: /* @__PURE__ */ new Set(),
7071
+ getWsUrl,
7072
+ reconnectDelayMs,
7073
+ ws: null,
7074
+ attempt: 0,
7075
+ reconnectTimer: null,
7076
+ heartbeatTimer: null,
7077
+ connecting: false,
7078
+ gen: 0
7079
+ };
7080
+ registry.set(key, shared);
7081
+ shared.listeners.add(listener);
7082
+ openSocket(shared, url);
7083
+ return;
7084
+ }
7085
+ shared.getWsUrl = getWsUrl;
7086
+ shared.listeners.add(listener);
7087
+ if (!shared.ws && !shared.connecting && !shared.reconnectTimer) {
7088
+ openSocket(shared, url);
7089
+ }
7090
+ }
7091
+ function leaveChannel(key, listener) {
7092
+ const shared = registry.get(key);
7093
+ if (!shared) return;
7094
+ shared.listeners.delete(listener);
7095
+ if (shared.listeners.size === 0) {
7096
+ destroyShared(shared);
7097
+ registry.delete(key);
7098
+ }
7099
+ }
7100
+ var visibilityHooked = false;
7101
+ function ensureVisibilityHook() {
7102
+ if (visibilityHooked || typeof document === "undefined") return;
7103
+ visibilityHooked = true;
7104
+ document.addEventListener("visibilitychange", () => {
7105
+ if (document.visibilityState !== "visible") return;
7106
+ for (const shared of registry.values()) {
7107
+ if (!shared.ws && !shared.reconnectTimer && !shared.connecting) {
7108
+ void connectShared(shared);
7109
+ }
7110
+ }
7111
+ });
7112
+ }
6957
7113
  function useRealtimeChannel({
6958
7114
  getWsUrl,
6959
7115
  onEvent,
@@ -6967,87 +7123,26 @@ function useRealtimeChannel({
6967
7123
  useEffect(() => {
6968
7124
  if (!enabled) return;
6969
7125
  let cancelled = false;
6970
- let socket = null;
6971
- let reconnectTimer = null;
6972
- let attempt = 0;
6973
- const connectGen = { current: 0 };
6974
- const clearReconnect = () => {
6975
- if (reconnectTimer) {
6976
- clearTimeout(reconnectTimer);
6977
- reconnectTimer = null;
6978
- }
6979
- };
6980
- const scheduleReconnect = () => {
6981
- if (cancelled) return;
6982
- clearReconnect();
6983
- const delay = Math.min(reconnectDelayMs * 2 ** attempt, MAX_RECONNECT_MS);
6984
- attempt += 1;
6985
- reconnectTimer = setTimeout(() => {
6986
- void connect();
6987
- }, delay);
6988
- };
6989
- const connect = async () => {
6990
- const gen = ++connectGen.current;
6991
- clearReconnect();
6992
- const url = await getWsUrlRef.current();
6993
- if (cancelled || gen !== connectGen.current) return;
6994
- if (!url) {
6995
- scheduleReconnect();
6996
- return;
6997
- }
6998
- if (socket) {
6999
- try {
7000
- socket.close();
7001
- } catch {
7002
- }
7003
- socket = null;
7004
- }
7005
- const ws = new WebSocket(url);
7006
- socket = ws;
7007
- ws.onopen = () => {
7008
- if (cancelled || gen !== connectGen.current) return;
7009
- attempt = 0;
7010
- };
7011
- ws.onmessage = (msg) => {
7012
- if (cancelled || gen !== connectGen.current) return;
7013
- let data;
7014
- try {
7015
- data = JSON.parse(String(msg.data));
7016
- } catch {
7017
- return;
7018
- }
7019
- if (typeof data?.type !== "string") return;
7020
- if (data.type === "pong") return;
7021
- onEventRef.current(data);
7022
- };
7023
- ws.onclose = () => {
7024
- if (cancelled || gen !== connectGen.current) return;
7025
- socket = null;
7026
- scheduleReconnect();
7027
- };
7028
- ws.onerror = () => {
7029
- };
7126
+ let joinedKey = null;
7127
+ const listener = (event) => onEventRef.current(event);
7128
+ const resolver = () => getWsUrlRef.current();
7129
+ const join = async () => {
7130
+ if (cancelled || joinedKey) return;
7131
+ const url = await resolver();
7132
+ if (cancelled || !url) return;
7133
+ joinedKey = channelKeyFromUrl(url);
7134
+ joinChannel(joinedKey, url, resolver, reconnectDelayMs, listener);
7030
7135
  };
7031
7136
  const onVisibility = () => {
7032
7137
  if (document.visibilityState !== "visible" || cancelled) return;
7033
- if (!socket || socket.readyState === WebSocket.CLOSED) {
7034
- void connect();
7035
- }
7138
+ if (!joinedKey) void join();
7036
7139
  };
7037
- void connect();
7140
+ void join();
7038
7141
  document.addEventListener("visibilitychange", onVisibility);
7039
7142
  return () => {
7040
7143
  cancelled = true;
7041
- connectGen.current += 1;
7042
- clearReconnect();
7043
7144
  document.removeEventListener("visibilitychange", onVisibility);
7044
- if (socket) {
7045
- try {
7046
- socket.close();
7047
- } catch {
7048
- }
7049
- socket = null;
7050
- }
7145
+ if (joinedKey) leaveChannel(joinedKey, listener);
7051
7146
  };
7052
7147
  }, [enabled, reconnectDelayMs]);
7053
7148
  }