openart-realtime-sdk 1.0.1 → 1.0.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.
@@ -1,19 +1,314 @@
1
1
  'use strict';
2
2
 
3
- var useRealtime_js = require('./use-realtime.js');
4
- var provider_js = require('./provider.js');
3
+ var react = require('react');
4
+ var z = require('zod/v4');
5
+ var jsxRuntime = require('react/jsx-runtime');
5
6
 
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
8
 
9
+ var z__default = /*#__PURE__*/_interopDefault(z);
7
10
 
8
- Object.keys(useRealtime_js).forEach(function (k) {
9
- if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
10
- enumerable: true,
11
- get: function () { return useRealtime_js[k]; }
12
- });
11
+ // src/client/use-realtime.ts
12
+ var systemEvent = z__default.default.discriminatedUnion("type", [
13
+ z__default.default.object({
14
+ type: z__default.default.literal("connected"),
15
+ channel: z__default.default.string(),
16
+ cursor: z__default.default.string().optional()
17
+ }),
18
+ z__default.default.object({ type: z__default.default.literal("reconnect"), timestamp: z__default.default.number() }),
19
+ z__default.default.object({ type: z__default.default.literal("error"), error: z__default.default.string() }),
20
+ z__default.default.object({ type: z__default.default.literal("disconnected"), channels: z__default.default.array(z__default.default.string()) }),
21
+ z__default.default.object({ type: z__default.default.literal("ping"), timestamp: z__default.default.number() })
22
+ ]);
23
+ var userEvent = z__default.default.object({
24
+ id: z__default.default.string(),
25
+ data: z__default.default.unknown(),
26
+ event: z__default.default.string(),
27
+ channel: z__default.default.string()
13
28
  });
14
- Object.keys(provider_js).forEach(function (k) {
15
- if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
16
- enumerable: true,
17
- get: function () { return provider_js[k]; }
18
- });
29
+ var RealtimeContext = react.createContext(null);
30
+ var PING_TIMEOUT_MS = 75e3;
31
+ function RealtimeProvider({
32
+ children,
33
+ api = { url: "/api/realtime", withCredentials: false },
34
+ maxReconnectAttempts = 3,
35
+ disconnectOnWindowHidden = true,
36
+ disconnectDelay = 5e3
37
+ }) {
38
+ const [status, setStatus] = react.useState("disconnected");
39
+ const localSubsRef = react.useRef(/* @__PURE__ */ new Map());
40
+ const channelSubsRef = react.useRef(/* @__PURE__ */ new Map());
41
+ const eventSourceRef = react.useRef(null);
42
+ const reconnectTimeoutRef = react.useRef(null);
43
+ const pingTimeoutRef = react.useRef(null);
44
+ const reconnectAttemptsRef = react.useRef(0);
45
+ const lastAckRef = react.useRef(/* @__PURE__ */ new Map());
46
+ const connectTimeoutRef = react.useRef(null);
47
+ const debounceTimeoutRef = react.useRef(null);
48
+ const visibilityTimeoutRef = react.useRef(null);
49
+ const getAllNeededChannels = react.useCallback(() => {
50
+ const channels = /* @__PURE__ */ new Set();
51
+ localSubsRef.current.forEach((sub) => {
52
+ sub.channels.forEach((ch) => channels.add(ch));
53
+ });
54
+ return channels;
55
+ }, []);
56
+ const cleanup = () => {
57
+ if (eventSourceRef.current) {
58
+ eventSourceRef.current.close();
59
+ eventSourceRef.current = null;
60
+ }
61
+ if (reconnectTimeoutRef.current) {
62
+ clearTimeout(reconnectTimeoutRef.current);
63
+ reconnectTimeoutRef.current = null;
64
+ }
65
+ if (pingTimeoutRef.current) {
66
+ clearTimeout(pingTimeoutRef.current);
67
+ pingTimeoutRef.current = null;
68
+ }
69
+ if (connectTimeoutRef.current) {
70
+ clearTimeout(connectTimeoutRef.current);
71
+ connectTimeoutRef.current = null;
72
+ }
73
+ if (visibilityTimeoutRef.current) {
74
+ clearTimeout(visibilityTimeoutRef.current);
75
+ visibilityTimeoutRef.current = null;
76
+ }
77
+ reconnectAttemptsRef.current = 0;
78
+ setStatus("disconnected");
79
+ };
80
+ const resetPingTimeout = react.useCallback(() => {
81
+ if (pingTimeoutRef.current) {
82
+ clearTimeout(pingTimeoutRef.current);
83
+ }
84
+ pingTimeoutRef.current = setTimeout(() => {
85
+ console.warn("Connection timed out, reconnecting...");
86
+ connect();
87
+ }, PING_TIMEOUT_MS);
88
+ }, []);
89
+ const connect = react.useCallback((opts) => {
90
+ const { replayEventsSince } = opts ?? { replayEventsSince: Date.now() };
91
+ const channels = Array.from(getAllNeededChannels());
92
+ if (channels.length === 0) return;
93
+ if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
94
+ console.log("Max reconnection attempts reached.");
95
+ setStatus("error");
96
+ return;
97
+ }
98
+ if (visibilityTimeoutRef.current) {
99
+ clearTimeout(visibilityTimeoutRef.current);
100
+ visibilityTimeoutRef.current = null;
101
+ }
102
+ if (eventSourceRef.current) {
103
+ eventSourceRef.current.close();
104
+ }
105
+ setStatus("connecting");
106
+ try {
107
+ const channelsParam = channels.map((ch) => `channel=${encodeURIComponent(ch)}`).join("&");
108
+ const lastAckParam = channels.map((c) => {
109
+ const lastAck = lastAckRef.current.get(c) ?? String(replayEventsSince);
110
+ return `last_ack_${encodeURIComponent(c)}=${encodeURIComponent(lastAck)}`;
111
+ }).join("&");
112
+ const url = api.url + "?" + channelsParam + "&" + lastAckParam;
113
+ const eventSource = new EventSource(url, {
114
+ withCredentials: api.withCredentials ?? false
115
+ });
116
+ eventSourceRef.current = eventSource;
117
+ eventSource.onopen = () => {
118
+ reconnectAttemptsRef.current = 0;
119
+ setStatus("connected");
120
+ resetPingTimeout();
121
+ };
122
+ eventSource.onmessage = (evt) => {
123
+ try {
124
+ const payload = JSON.parse(evt.data);
125
+ resetPingTimeout();
126
+ handleMessage(payload);
127
+ const systemResult = systemEvent.safeParse(payload);
128
+ if (systemResult.success) {
129
+ if (systemResult.data.type === "reconnect") {
130
+ connect({ replayEventsSince: systemResult.data.timestamp });
131
+ }
132
+ }
133
+ } catch (error) {
134
+ console.warn("Error parsing message:", error);
135
+ }
136
+ };
137
+ eventSource.onerror = () => {
138
+ if (eventSource !== eventSourceRef.current) return;
139
+ const readyState = eventSourceRef.current?.readyState;
140
+ if (readyState === EventSource.CONNECTING) return;
141
+ if (readyState === EventSource.CLOSED) {
142
+ console.log("Connection closed, reconnecting...");
143
+ }
144
+ setStatus("disconnected");
145
+ if (reconnectAttemptsRef.current < maxReconnectAttempts) {
146
+ reconnectAttemptsRef.current++;
147
+ const timeoutMs = Math.min(1e3 * Math.pow(2, reconnectAttemptsRef.current), 3e4);
148
+ console.log(`Reconnecting in ${timeoutMs}ms... (Attempt ${reconnectAttemptsRef.current})`);
149
+ reconnectTimeoutRef.current = setTimeout(() => {
150
+ connect();
151
+ }, timeoutMs);
152
+ } else {
153
+ setStatus("error");
154
+ }
155
+ };
156
+ } catch (error) {
157
+ setStatus("error");
158
+ }
159
+ }, [getAllNeededChannels, maxReconnectAttempts, api.url, api.withCredentials]);
160
+ const debouncedConnect = react.useCallback(() => {
161
+ if (debounceTimeoutRef.current) {
162
+ clearTimeout(debounceTimeoutRef.current);
163
+ }
164
+ if (disconnectOnWindowHidden && typeof document !== "undefined" && document.hidden) {
165
+ return;
166
+ }
167
+ debounceTimeoutRef.current = setTimeout(() => {
168
+ connect();
169
+ debounceTimeoutRef.current = null;
170
+ }, 25);
171
+ }, [connect, disconnectOnWindowHidden]);
172
+ react.useEffect(() => {
173
+ if (!disconnectOnWindowHidden || typeof document === "undefined") return;
174
+ const handleVisibilityChange = () => {
175
+ if (document.hidden) {
176
+ if (visibilityTimeoutRef.current) clearTimeout(visibilityTimeoutRef.current);
177
+ visibilityTimeoutRef.current = setTimeout(() => {
178
+ console.log("Window hidden for too long, disconnecting...");
179
+ cleanup();
180
+ }, disconnectDelay);
181
+ } else {
182
+ if (visibilityTimeoutRef.current) {
183
+ clearTimeout(visibilityTimeoutRef.current);
184
+ visibilityTimeoutRef.current = null;
185
+ }
186
+ if (status === "disconnected" && localSubsRef.current.size > 0) {
187
+ console.log("Window visible, reconnecting...");
188
+ connect();
189
+ }
190
+ }
191
+ };
192
+ document.addEventListener("visibilitychange", handleVisibilityChange);
193
+ return () => {
194
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
195
+ if (visibilityTimeoutRef.current) clearTimeout(visibilityTimeoutRef.current);
196
+ };
197
+ }, [disconnectOnWindowHidden, disconnectDelay, connect, status]);
198
+ const handleMessage = (payload) => {
199
+ const systemResult = systemEvent.safeParse(payload);
200
+ if (systemResult.success) {
201
+ const event2 = systemResult.data;
202
+ if (event2.type === "connected") {
203
+ if (event2.cursor) {
204
+ lastAckRef.current.set(event2.channel, event2.cursor);
205
+ }
206
+ }
207
+ return;
208
+ }
209
+ const event = userEvent.safeParse(payload);
210
+ if (event.success) {
211
+ lastAckRef.current.set(event.data.channel, event.data.id);
212
+ const channel = event.data.channel;
213
+ const subscriberIds = channelSubsRef.current.get(channel);
214
+ if (subscriberIds) {
215
+ subscriberIds.forEach((id) => {
216
+ const sub = localSubsRef.current.get(id);
217
+ if (sub) {
218
+ sub.cb(payload);
219
+ }
220
+ });
221
+ }
222
+ }
223
+ };
224
+ react.useEffect(() => {
225
+ return () => cleanup();
226
+ }, []);
227
+ const register = (id, channels, cb) => {
228
+ localSubsRef.current.set(id, { channels: new Set(channels), cb });
229
+ channels.forEach((channel) => {
230
+ if (!channelSubsRef.current.has(channel)) {
231
+ channelSubsRef.current.set(channel, /* @__PURE__ */ new Set());
232
+ }
233
+ channelSubsRef.current.get(channel).add(id);
234
+ });
235
+ debouncedConnect();
236
+ };
237
+ const unregister = (id) => {
238
+ const channels = Array.from(localSubsRef.current.get(id)?.channels ?? []);
239
+ channels.forEach((channel) => {
240
+ lastAckRef.current.delete(channel);
241
+ const channelSet = channelSubsRef.current.get(channel);
242
+ if (channelSet) {
243
+ channelSet.delete(id);
244
+ if (channelSet.size === 0) {
245
+ channelSubsRef.current.delete(channel);
246
+ }
247
+ }
248
+ });
249
+ localSubsRef.current.delete(id);
250
+ if (localSubsRef.current.size === 0) {
251
+ cleanup();
252
+ if (debounceTimeoutRef.current) {
253
+ clearTimeout(debounceTimeoutRef.current);
254
+ debounceTimeoutRef.current = null;
255
+ }
256
+ return;
257
+ }
258
+ debouncedConnect();
259
+ };
260
+ return /* @__PURE__ */ jsxRuntime.jsx(RealtimeContext.Provider, { value: { status, register, unregister }, children });
261
+ }
262
+ function useRealtimeContext() {
263
+ const context = react.useContext(RealtimeContext);
264
+ if (!context) {
265
+ throw new Error("useRealtimeContext must be used within a RealtimeProvider");
266
+ }
267
+ return context;
268
+ }
269
+ var createRealtime = () => ({
270
+ useRealtime: (opts) => useRealtime(opts)
19
271
  });
272
+
273
+ // src/client/use-realtime.ts
274
+ function useRealtime(opts) {
275
+ const { channels = ["default"], events, onData, enabled } = opts;
276
+ const context = react.useContext(RealtimeContext);
277
+ if (!context) {
278
+ throw new Error(
279
+ "useRealtime: No RealtimeProvider found. Wrap your app in <RealtimeProvider> to use Realtime."
280
+ );
281
+ }
282
+ const registrationId = react.useRef(Math.random().toString(36).substring(2)).current;
283
+ const onDataRef = react.useRef(onData);
284
+ onDataRef.current = onData;
285
+ react.useEffect(() => {
286
+ if (enabled === false) {
287
+ context.unregister(registrationId);
288
+ return;
289
+ }
290
+ const validChannels = channels.filter(Boolean);
291
+ if (validChannels.length === 0) return;
292
+ context.register(registrationId, validChannels, (msg) => {
293
+ const result = userEvent.safeParse(msg);
294
+ if (result.success) {
295
+ const { event, channel, data } = result.data;
296
+ if (events && events.length > 0 && !events.includes(event)) {
297
+ return;
298
+ }
299
+ const payload = { event, data, channel };
300
+ onDataRef.current?.(payload);
301
+ }
302
+ });
303
+ return () => {
304
+ context.unregister(registrationId);
305
+ };
306
+ }, [JSON.stringify(channels), enabled, JSON.stringify(events)]);
307
+ return { status: context.status };
308
+ }
309
+
310
+ exports.RealtimeContext = RealtimeContext;
311
+ exports.RealtimeProvider = RealtimeProvider;
312
+ exports.createRealtime = createRealtime;
313
+ exports.useRealtime = useRealtime;
314
+ exports.useRealtimeContext = useRealtimeContext;
@@ -1,2 +1,304 @@
1
- export * from './use-realtime.js';
2
- export * from './provider.js';
1
+ import { createContext, useState, useRef, useCallback, useEffect, useContext } from 'react';
2
+ import z from 'zod/v4';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ // src/client/use-realtime.ts
6
+ var systemEvent = z.discriminatedUnion("type", [
7
+ z.object({
8
+ type: z.literal("connected"),
9
+ channel: z.string(),
10
+ cursor: z.string().optional()
11
+ }),
12
+ z.object({ type: z.literal("reconnect"), timestamp: z.number() }),
13
+ z.object({ type: z.literal("error"), error: z.string() }),
14
+ z.object({ type: z.literal("disconnected"), channels: z.array(z.string()) }),
15
+ z.object({ type: z.literal("ping"), timestamp: z.number() })
16
+ ]);
17
+ var userEvent = z.object({
18
+ id: z.string(),
19
+ data: z.unknown(),
20
+ event: z.string(),
21
+ channel: z.string()
22
+ });
23
+ var RealtimeContext = createContext(null);
24
+ var PING_TIMEOUT_MS = 75e3;
25
+ function RealtimeProvider({
26
+ children,
27
+ api = { url: "/api/realtime", withCredentials: false },
28
+ maxReconnectAttempts = 3,
29
+ disconnectOnWindowHidden = true,
30
+ disconnectDelay = 5e3
31
+ }) {
32
+ const [status, setStatus] = useState("disconnected");
33
+ const localSubsRef = useRef(/* @__PURE__ */ new Map());
34
+ const channelSubsRef = useRef(/* @__PURE__ */ new Map());
35
+ const eventSourceRef = useRef(null);
36
+ const reconnectTimeoutRef = useRef(null);
37
+ const pingTimeoutRef = useRef(null);
38
+ const reconnectAttemptsRef = useRef(0);
39
+ const lastAckRef = useRef(/* @__PURE__ */ new Map());
40
+ const connectTimeoutRef = useRef(null);
41
+ const debounceTimeoutRef = useRef(null);
42
+ const visibilityTimeoutRef = useRef(null);
43
+ const getAllNeededChannels = useCallback(() => {
44
+ const channels = /* @__PURE__ */ new Set();
45
+ localSubsRef.current.forEach((sub) => {
46
+ sub.channels.forEach((ch) => channels.add(ch));
47
+ });
48
+ return channels;
49
+ }, []);
50
+ const cleanup = () => {
51
+ if (eventSourceRef.current) {
52
+ eventSourceRef.current.close();
53
+ eventSourceRef.current = null;
54
+ }
55
+ if (reconnectTimeoutRef.current) {
56
+ clearTimeout(reconnectTimeoutRef.current);
57
+ reconnectTimeoutRef.current = null;
58
+ }
59
+ if (pingTimeoutRef.current) {
60
+ clearTimeout(pingTimeoutRef.current);
61
+ pingTimeoutRef.current = null;
62
+ }
63
+ if (connectTimeoutRef.current) {
64
+ clearTimeout(connectTimeoutRef.current);
65
+ connectTimeoutRef.current = null;
66
+ }
67
+ if (visibilityTimeoutRef.current) {
68
+ clearTimeout(visibilityTimeoutRef.current);
69
+ visibilityTimeoutRef.current = null;
70
+ }
71
+ reconnectAttemptsRef.current = 0;
72
+ setStatus("disconnected");
73
+ };
74
+ const resetPingTimeout = useCallback(() => {
75
+ if (pingTimeoutRef.current) {
76
+ clearTimeout(pingTimeoutRef.current);
77
+ }
78
+ pingTimeoutRef.current = setTimeout(() => {
79
+ console.warn("Connection timed out, reconnecting...");
80
+ connect();
81
+ }, PING_TIMEOUT_MS);
82
+ }, []);
83
+ const connect = useCallback((opts) => {
84
+ const { replayEventsSince } = opts ?? { replayEventsSince: Date.now() };
85
+ const channels = Array.from(getAllNeededChannels());
86
+ if (channels.length === 0) return;
87
+ if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
88
+ console.log("Max reconnection attempts reached.");
89
+ setStatus("error");
90
+ return;
91
+ }
92
+ if (visibilityTimeoutRef.current) {
93
+ clearTimeout(visibilityTimeoutRef.current);
94
+ visibilityTimeoutRef.current = null;
95
+ }
96
+ if (eventSourceRef.current) {
97
+ eventSourceRef.current.close();
98
+ }
99
+ setStatus("connecting");
100
+ try {
101
+ const channelsParam = channels.map((ch) => `channel=${encodeURIComponent(ch)}`).join("&");
102
+ const lastAckParam = channels.map((c) => {
103
+ const lastAck = lastAckRef.current.get(c) ?? String(replayEventsSince);
104
+ return `last_ack_${encodeURIComponent(c)}=${encodeURIComponent(lastAck)}`;
105
+ }).join("&");
106
+ const url = api.url + "?" + channelsParam + "&" + lastAckParam;
107
+ const eventSource = new EventSource(url, {
108
+ withCredentials: api.withCredentials ?? false
109
+ });
110
+ eventSourceRef.current = eventSource;
111
+ eventSource.onopen = () => {
112
+ reconnectAttemptsRef.current = 0;
113
+ setStatus("connected");
114
+ resetPingTimeout();
115
+ };
116
+ eventSource.onmessage = (evt) => {
117
+ try {
118
+ const payload = JSON.parse(evt.data);
119
+ resetPingTimeout();
120
+ handleMessage(payload);
121
+ const systemResult = systemEvent.safeParse(payload);
122
+ if (systemResult.success) {
123
+ if (systemResult.data.type === "reconnect") {
124
+ connect({ replayEventsSince: systemResult.data.timestamp });
125
+ }
126
+ }
127
+ } catch (error) {
128
+ console.warn("Error parsing message:", error);
129
+ }
130
+ };
131
+ eventSource.onerror = () => {
132
+ if (eventSource !== eventSourceRef.current) return;
133
+ const readyState = eventSourceRef.current?.readyState;
134
+ if (readyState === EventSource.CONNECTING) return;
135
+ if (readyState === EventSource.CLOSED) {
136
+ console.log("Connection closed, reconnecting...");
137
+ }
138
+ setStatus("disconnected");
139
+ if (reconnectAttemptsRef.current < maxReconnectAttempts) {
140
+ reconnectAttemptsRef.current++;
141
+ const timeoutMs = Math.min(1e3 * Math.pow(2, reconnectAttemptsRef.current), 3e4);
142
+ console.log(`Reconnecting in ${timeoutMs}ms... (Attempt ${reconnectAttemptsRef.current})`);
143
+ reconnectTimeoutRef.current = setTimeout(() => {
144
+ connect();
145
+ }, timeoutMs);
146
+ } else {
147
+ setStatus("error");
148
+ }
149
+ };
150
+ } catch (error) {
151
+ setStatus("error");
152
+ }
153
+ }, [getAllNeededChannels, maxReconnectAttempts, api.url, api.withCredentials]);
154
+ const debouncedConnect = useCallback(() => {
155
+ if (debounceTimeoutRef.current) {
156
+ clearTimeout(debounceTimeoutRef.current);
157
+ }
158
+ if (disconnectOnWindowHidden && typeof document !== "undefined" && document.hidden) {
159
+ return;
160
+ }
161
+ debounceTimeoutRef.current = setTimeout(() => {
162
+ connect();
163
+ debounceTimeoutRef.current = null;
164
+ }, 25);
165
+ }, [connect, disconnectOnWindowHidden]);
166
+ useEffect(() => {
167
+ if (!disconnectOnWindowHidden || typeof document === "undefined") return;
168
+ const handleVisibilityChange = () => {
169
+ if (document.hidden) {
170
+ if (visibilityTimeoutRef.current) clearTimeout(visibilityTimeoutRef.current);
171
+ visibilityTimeoutRef.current = setTimeout(() => {
172
+ console.log("Window hidden for too long, disconnecting...");
173
+ cleanup();
174
+ }, disconnectDelay);
175
+ } else {
176
+ if (visibilityTimeoutRef.current) {
177
+ clearTimeout(visibilityTimeoutRef.current);
178
+ visibilityTimeoutRef.current = null;
179
+ }
180
+ if (status === "disconnected" && localSubsRef.current.size > 0) {
181
+ console.log("Window visible, reconnecting...");
182
+ connect();
183
+ }
184
+ }
185
+ };
186
+ document.addEventListener("visibilitychange", handleVisibilityChange);
187
+ return () => {
188
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
189
+ if (visibilityTimeoutRef.current) clearTimeout(visibilityTimeoutRef.current);
190
+ };
191
+ }, [disconnectOnWindowHidden, disconnectDelay, connect, status]);
192
+ const handleMessage = (payload) => {
193
+ const systemResult = systemEvent.safeParse(payload);
194
+ if (systemResult.success) {
195
+ const event2 = systemResult.data;
196
+ if (event2.type === "connected") {
197
+ if (event2.cursor) {
198
+ lastAckRef.current.set(event2.channel, event2.cursor);
199
+ }
200
+ }
201
+ return;
202
+ }
203
+ const event = userEvent.safeParse(payload);
204
+ if (event.success) {
205
+ lastAckRef.current.set(event.data.channel, event.data.id);
206
+ const channel = event.data.channel;
207
+ const subscriberIds = channelSubsRef.current.get(channel);
208
+ if (subscriberIds) {
209
+ subscriberIds.forEach((id) => {
210
+ const sub = localSubsRef.current.get(id);
211
+ if (sub) {
212
+ sub.cb(payload);
213
+ }
214
+ });
215
+ }
216
+ }
217
+ };
218
+ useEffect(() => {
219
+ return () => cleanup();
220
+ }, []);
221
+ const register = (id, channels, cb) => {
222
+ localSubsRef.current.set(id, { channels: new Set(channels), cb });
223
+ channels.forEach((channel) => {
224
+ if (!channelSubsRef.current.has(channel)) {
225
+ channelSubsRef.current.set(channel, /* @__PURE__ */ new Set());
226
+ }
227
+ channelSubsRef.current.get(channel).add(id);
228
+ });
229
+ debouncedConnect();
230
+ };
231
+ const unregister = (id) => {
232
+ const channels = Array.from(localSubsRef.current.get(id)?.channels ?? []);
233
+ channels.forEach((channel) => {
234
+ lastAckRef.current.delete(channel);
235
+ const channelSet = channelSubsRef.current.get(channel);
236
+ if (channelSet) {
237
+ channelSet.delete(id);
238
+ if (channelSet.size === 0) {
239
+ channelSubsRef.current.delete(channel);
240
+ }
241
+ }
242
+ });
243
+ localSubsRef.current.delete(id);
244
+ if (localSubsRef.current.size === 0) {
245
+ cleanup();
246
+ if (debounceTimeoutRef.current) {
247
+ clearTimeout(debounceTimeoutRef.current);
248
+ debounceTimeoutRef.current = null;
249
+ }
250
+ return;
251
+ }
252
+ debouncedConnect();
253
+ };
254
+ return /* @__PURE__ */ jsx(RealtimeContext.Provider, { value: { status, register, unregister }, children });
255
+ }
256
+ function useRealtimeContext() {
257
+ const context = useContext(RealtimeContext);
258
+ if (!context) {
259
+ throw new Error("useRealtimeContext must be used within a RealtimeProvider");
260
+ }
261
+ return context;
262
+ }
263
+ var createRealtime = () => ({
264
+ useRealtime: (opts) => useRealtime(opts)
265
+ });
266
+
267
+ // src/client/use-realtime.ts
268
+ function useRealtime(opts) {
269
+ const { channels = ["default"], events, onData, enabled } = opts;
270
+ const context = useContext(RealtimeContext);
271
+ if (!context) {
272
+ throw new Error(
273
+ "useRealtime: No RealtimeProvider found. Wrap your app in <RealtimeProvider> to use Realtime."
274
+ );
275
+ }
276
+ const registrationId = useRef(Math.random().toString(36).substring(2)).current;
277
+ const onDataRef = useRef(onData);
278
+ onDataRef.current = onData;
279
+ useEffect(() => {
280
+ if (enabled === false) {
281
+ context.unregister(registrationId);
282
+ return;
283
+ }
284
+ const validChannels = channels.filter(Boolean);
285
+ if (validChannels.length === 0) return;
286
+ context.register(registrationId, validChannels, (msg) => {
287
+ const result = userEvent.safeParse(msg);
288
+ if (result.success) {
289
+ const { event, channel, data } = result.data;
290
+ if (events && events.length > 0 && !events.includes(event)) {
291
+ return;
292
+ }
293
+ const payload = { event, data, channel };
294
+ onDataRef.current?.(payload);
295
+ }
296
+ });
297
+ return () => {
298
+ context.unregister(registrationId);
299
+ };
300
+ }, [JSON.stringify(channels), enabled, JSON.stringify(events)]);
301
+ return { status: context.status };
302
+ }
303
+
304
+ export { RealtimeContext, RealtimeProvider, createRealtime, useRealtime, useRealtimeContext };