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.
- package/dist/client/index.cjs +307 -12
- package/dist/client/index.js +304 -2
- package/dist/server/index.cjs +559 -13
- package/dist/server/index.js +537 -2
- package/dist/shared/index.cjs +23 -6
- package/dist/shared/index.js +22 -1
- package/package.json +30 -11
package/dist/client/index.cjs
CHANGED
|
@@ -1,19 +1,314 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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;
|
package/dist/client/index.js
CHANGED
|
@@ -1,2 +1,304 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 };
|