react-realtime-hooks 1.0.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/LICENSE +21 -0
- package/README.md +474 -0
- package/dist/index.cjs +1245 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +216 -0
- package/dist/index.d.ts +216 -0
- package/dist/index.js +1239 -0
- package/dist/index.js.map +1 -0
- package/package.json +86 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1245 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
// src/hooks/useOnlineStatus.ts
|
|
6
|
+
|
|
7
|
+
// src/core/env.ts
|
|
8
|
+
var isWebSocketSupported = () => typeof WebSocket !== "undefined";
|
|
9
|
+
var isEventSourceSupported = () => typeof EventSource !== "undefined";
|
|
10
|
+
var hasNavigatorOnLineSupport = () => typeof navigator !== "undefined" && typeof navigator.onLine === "boolean";
|
|
11
|
+
var readOnlineStatus = (initialOnline = true) => {
|
|
12
|
+
if (!hasNavigatorOnLineSupport()) {
|
|
13
|
+
return {
|
|
14
|
+
isOnline: initialOnline,
|
|
15
|
+
isSupported: false
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
isOnline: navigator.onLine,
|
|
20
|
+
isSupported: true
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/hooks/useOnlineStatus.ts
|
|
25
|
+
var subscribeToOnlineStatus = (onStoreChange) => {
|
|
26
|
+
if (typeof window === "undefined") {
|
|
27
|
+
return () => {
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
window.addEventListener("online", onStoreChange);
|
|
31
|
+
window.addEventListener("offline", onStoreChange);
|
|
32
|
+
return () => {
|
|
33
|
+
window.removeEventListener("online", onStoreChange);
|
|
34
|
+
window.removeEventListener("offline", onStoreChange);
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
var createEmptyTransitionState = () => ({
|
|
38
|
+
lastChangedAt: null,
|
|
39
|
+
wentOfflineAt: null,
|
|
40
|
+
wentOnlineAt: null
|
|
41
|
+
});
|
|
42
|
+
var useOnlineStatus = (options = {}) => {
|
|
43
|
+
const initialOnline = options.initialOnline ?? true;
|
|
44
|
+
const trackTransitions = options.trackTransitions ?? true;
|
|
45
|
+
const isOnline = react.useSyncExternalStore(
|
|
46
|
+
subscribeToOnlineStatus,
|
|
47
|
+
() => readOnlineStatus(initialOnline).isOnline,
|
|
48
|
+
() => initialOnline
|
|
49
|
+
);
|
|
50
|
+
const previousOnlineRef = react.useRef(isOnline);
|
|
51
|
+
const [transitions, setTransitions] = react.useState(createEmptyTransitionState);
|
|
52
|
+
react.useEffect(() => {
|
|
53
|
+
if (!trackTransitions) {
|
|
54
|
+
previousOnlineRef.current = isOnline;
|
|
55
|
+
setTransitions(createEmptyTransitionState);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (previousOnlineRef.current === isOnline) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const changedAt = Date.now();
|
|
62
|
+
previousOnlineRef.current = isOnline;
|
|
63
|
+
setTransitions((current) => ({
|
|
64
|
+
lastChangedAt: changedAt,
|
|
65
|
+
wentOfflineAt: isOnline ? current.wentOfflineAt : changedAt,
|
|
66
|
+
wentOnlineAt: isOnline ? changedAt : current.wentOnlineAt
|
|
67
|
+
}));
|
|
68
|
+
}, [isOnline, trackTransitions]);
|
|
69
|
+
return {
|
|
70
|
+
isOnline,
|
|
71
|
+
isSupported: hasNavigatorOnLineSupport(),
|
|
72
|
+
...transitions
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/core/reconnect.ts
|
|
77
|
+
var DEFAULT_RECONNECT_OPTIONS = {
|
|
78
|
+
backoffFactor: 2,
|
|
79
|
+
enabled: true,
|
|
80
|
+
initialDelayMs: 1e3,
|
|
81
|
+
jitterRatio: 0.2,
|
|
82
|
+
maxDelayMs: 3e4,
|
|
83
|
+
resetOnSuccess: true
|
|
84
|
+
};
|
|
85
|
+
var clampNumber = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
86
|
+
var sanitizeDelay = (value) => {
|
|
87
|
+
if (!Number.isFinite(value)) {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
return Math.max(0, Math.round(value));
|
|
91
|
+
};
|
|
92
|
+
var sanitizeBackoffFactor = (value) => {
|
|
93
|
+
if (!Number.isFinite(value)) {
|
|
94
|
+
return DEFAULT_RECONNECT_OPTIONS.backoffFactor;
|
|
95
|
+
}
|
|
96
|
+
return Math.max(1, value);
|
|
97
|
+
};
|
|
98
|
+
var sanitizeMaxAttempts = (value) => {
|
|
99
|
+
if (value === null || value === void 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (!Number.isFinite(value)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return Math.max(0, Math.floor(value));
|
|
106
|
+
};
|
|
107
|
+
var sanitizeJitterRatio = (value) => {
|
|
108
|
+
if (!Number.isFinite(value)) {
|
|
109
|
+
return DEFAULT_RECONNECT_OPTIONS.jitterRatio;
|
|
110
|
+
}
|
|
111
|
+
return clampNumber(value, 0, 1);
|
|
112
|
+
};
|
|
113
|
+
var normalizeReconnectOptions = (options) => {
|
|
114
|
+
if (options === false) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const initialDelayMs = sanitizeDelay(
|
|
118
|
+
options?.initialDelayMs ?? DEFAULT_RECONNECT_OPTIONS.initialDelayMs
|
|
119
|
+
);
|
|
120
|
+
const maxDelayMs = Math.max(
|
|
121
|
+
initialDelayMs,
|
|
122
|
+
sanitizeDelay(options?.maxDelayMs ?? DEFAULT_RECONNECT_OPTIONS.maxDelayMs)
|
|
123
|
+
);
|
|
124
|
+
const normalized = {
|
|
125
|
+
backoffFactor: sanitizeBackoffFactor(
|
|
126
|
+
options?.backoffFactor ?? DEFAULT_RECONNECT_OPTIONS.backoffFactor
|
|
127
|
+
),
|
|
128
|
+
enabled: options?.enabled ?? DEFAULT_RECONNECT_OPTIONS.enabled,
|
|
129
|
+
initialDelayMs,
|
|
130
|
+
jitterRatio: sanitizeJitterRatio(
|
|
131
|
+
options?.jitterRatio ?? DEFAULT_RECONNECT_OPTIONS.jitterRatio
|
|
132
|
+
),
|
|
133
|
+
maxAttempts: sanitizeMaxAttempts(options?.maxAttempts),
|
|
134
|
+
maxDelayMs,
|
|
135
|
+
resetOnSuccess: options?.resetOnSuccess ?? DEFAULT_RECONNECT_OPTIONS.resetOnSuccess
|
|
136
|
+
};
|
|
137
|
+
if (options?.getDelayMs !== void 0) {
|
|
138
|
+
normalized.getDelayMs = options.getDelayMs;
|
|
139
|
+
}
|
|
140
|
+
if (options?.onCancel !== void 0) {
|
|
141
|
+
normalized.onCancel = options.onCancel;
|
|
142
|
+
}
|
|
143
|
+
if (options?.onReset !== void 0) {
|
|
144
|
+
normalized.onReset = options.onReset;
|
|
145
|
+
}
|
|
146
|
+
if (options?.onSchedule !== void 0) {
|
|
147
|
+
normalized.onSchedule = options.onSchedule;
|
|
148
|
+
}
|
|
149
|
+
return normalized;
|
|
150
|
+
};
|
|
151
|
+
var createReconnectDelayContext = (attempt, options, lastDelayMs) => ({
|
|
152
|
+
attempt: Math.max(1, Math.floor(attempt)),
|
|
153
|
+
backoffFactor: options.backoffFactor,
|
|
154
|
+
initialDelayMs: options.initialDelayMs,
|
|
155
|
+
jitterRatio: options.jitterRatio,
|
|
156
|
+
lastDelayMs,
|
|
157
|
+
maxDelayMs: options.maxDelayMs
|
|
158
|
+
});
|
|
159
|
+
var defaultReconnectDelayStrategy = (context) => {
|
|
160
|
+
const exponent = Math.max(0, context.attempt - 1);
|
|
161
|
+
const baseDelay = context.initialDelayMs * context.backoffFactor ** exponent;
|
|
162
|
+
return Math.min(context.maxDelayMs, sanitizeDelay(baseDelay));
|
|
163
|
+
};
|
|
164
|
+
var applyJitterToDelay = (delayMs, jitterRatio, random = Math.random) => {
|
|
165
|
+
if (delayMs === 0 || jitterRatio === 0) {
|
|
166
|
+
return delayMs;
|
|
167
|
+
}
|
|
168
|
+
const safeRandom = clampNumber(random(), 0, 1);
|
|
169
|
+
const variance = delayMs * jitterRatio;
|
|
170
|
+
const offset = (safeRandom * 2 - 1) * variance;
|
|
171
|
+
return sanitizeDelay(delayMs + offset);
|
|
172
|
+
};
|
|
173
|
+
var calculateReconnectDelay = (context, options = {}) => {
|
|
174
|
+
const baseDelay = sanitizeDelay(
|
|
175
|
+
(options.strategy ?? defaultReconnectDelayStrategy)(context)
|
|
176
|
+
);
|
|
177
|
+
const cappedDelay = Math.min(context.maxDelayMs, baseDelay);
|
|
178
|
+
return Math.min(
|
|
179
|
+
context.maxDelayMs,
|
|
180
|
+
applyJitterToDelay(cappedDelay, context.jitterRatio, options.random)
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
var canScheduleReconnectAttempt = (attempt, options) => {
|
|
184
|
+
if (!options.enabled) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (attempt < 1) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return options.maxAttempts === null || attempt <= options.maxAttempts;
|
|
191
|
+
};
|
|
192
|
+
var createReconnectAttempt = (attempt, trigger, options, lastDelayMs, config = {}) => {
|
|
193
|
+
if (!canScheduleReconnectAttempt(attempt, options)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const context = createReconnectDelayContext(attempt, options, lastDelayMs);
|
|
197
|
+
const calculationOptions = {};
|
|
198
|
+
if (config.random !== void 0) {
|
|
199
|
+
calculationOptions.random = config.random;
|
|
200
|
+
}
|
|
201
|
+
if (options.getDelayMs !== void 0) {
|
|
202
|
+
calculationOptions.strategy = options.getDelayMs;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
attempt,
|
|
206
|
+
delayMs: calculateReconnectDelay(context, calculationOptions),
|
|
207
|
+
scheduledAt: config.now ?? Date.now(),
|
|
208
|
+
trigger
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// src/core/timers.ts
|
|
213
|
+
var sanitizeTimerDelay = (delayMs) => {
|
|
214
|
+
if (!Number.isFinite(delayMs)) {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
return Math.max(0, Math.round(delayMs));
|
|
218
|
+
};
|
|
219
|
+
var createManagedTimeout = () => {
|
|
220
|
+
let timeoutId = null;
|
|
221
|
+
return {
|
|
222
|
+
cancel() {
|
|
223
|
+
if (timeoutId !== null) {
|
|
224
|
+
clearTimeout(timeoutId);
|
|
225
|
+
timeoutId = null;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
isActive() {
|
|
229
|
+
return timeoutId !== null;
|
|
230
|
+
},
|
|
231
|
+
schedule(callback, delayMs) {
|
|
232
|
+
if (timeoutId !== null) {
|
|
233
|
+
clearTimeout(timeoutId);
|
|
234
|
+
}
|
|
235
|
+
timeoutId = setTimeout(() => {
|
|
236
|
+
timeoutId = null;
|
|
237
|
+
callback();
|
|
238
|
+
}, sanitizeTimerDelay(delayMs));
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
var createManagedInterval = () => {
|
|
243
|
+
let intervalId = null;
|
|
244
|
+
return {
|
|
245
|
+
cancel() {
|
|
246
|
+
if (intervalId !== null) {
|
|
247
|
+
clearInterval(intervalId);
|
|
248
|
+
intervalId = null;
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
isActive() {
|
|
252
|
+
return intervalId !== null;
|
|
253
|
+
},
|
|
254
|
+
start(callback, intervalMs) {
|
|
255
|
+
if (intervalId !== null) {
|
|
256
|
+
clearInterval(intervalId);
|
|
257
|
+
}
|
|
258
|
+
intervalId = setInterval(callback, sanitizeTimerDelay(intervalMs));
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/hooks/useReconnect.ts
|
|
264
|
+
var createInitialState = (enabled) => ({
|
|
265
|
+
attempt: 0,
|
|
266
|
+
nextDelayMs: null,
|
|
267
|
+
status: enabled ? "idle" : "stopped"
|
|
268
|
+
});
|
|
269
|
+
var useReconnect = (options = {}) => {
|
|
270
|
+
const normalizedOptions = normalizeReconnectOptions(options) ?? normalizeReconnectOptions();
|
|
271
|
+
const timeoutRef = react.useRef(createManagedTimeout());
|
|
272
|
+
const lastDelayRef = react.useRef(null);
|
|
273
|
+
const [state, setState] = react.useState(
|
|
274
|
+
() => createInitialState(normalizedOptions.enabled)
|
|
275
|
+
);
|
|
276
|
+
const stateRef = react.useRef(state);
|
|
277
|
+
stateRef.current = state;
|
|
278
|
+
const commitState = (next) => {
|
|
279
|
+
const resolved = typeof next === "function" ? next(stateRef.current) : next;
|
|
280
|
+
stateRef.current = resolved;
|
|
281
|
+
react.startTransition(() => {
|
|
282
|
+
setState(resolved);
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
const runAttempt = react.useEffectEvent((attempt) => {
|
|
286
|
+
commitState({
|
|
287
|
+
attempt,
|
|
288
|
+
nextDelayMs: null,
|
|
289
|
+
status: "running"
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
const emitSchedule = react.useEffectEvent((attempt) => {
|
|
293
|
+
normalizedOptions.onSchedule?.(attempt);
|
|
294
|
+
});
|
|
295
|
+
const emitCancel = react.useEffectEvent(() => {
|
|
296
|
+
normalizedOptions.onCancel?.();
|
|
297
|
+
});
|
|
298
|
+
const emitReset = react.useEffectEvent(() => {
|
|
299
|
+
normalizedOptions.onReset?.();
|
|
300
|
+
});
|
|
301
|
+
react.useEffect(() => {
|
|
302
|
+
if (!normalizedOptions.enabled) {
|
|
303
|
+
timeoutRef.current.cancel();
|
|
304
|
+
commitState((current) => ({
|
|
305
|
+
...current,
|
|
306
|
+
nextDelayMs: null,
|
|
307
|
+
status: "stopped"
|
|
308
|
+
}));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
commitState(
|
|
312
|
+
(current) => current.status === "stopped" ? {
|
|
313
|
+
...current,
|
|
314
|
+
status: "idle"
|
|
315
|
+
} : current
|
|
316
|
+
);
|
|
317
|
+
}, [normalizedOptions.enabled]);
|
|
318
|
+
react.useEffect(() => () => {
|
|
319
|
+
timeoutRef.current.cancel();
|
|
320
|
+
}, []);
|
|
321
|
+
const schedule = (trigger = "manual") => {
|
|
322
|
+
const current = stateRef.current;
|
|
323
|
+
const nextAttempt = current.attempt + 1;
|
|
324
|
+
const attempt = createReconnectAttempt(
|
|
325
|
+
nextAttempt,
|
|
326
|
+
trigger,
|
|
327
|
+
normalizedOptions,
|
|
328
|
+
lastDelayRef.current
|
|
329
|
+
);
|
|
330
|
+
timeoutRef.current.cancel();
|
|
331
|
+
if (attempt === null) {
|
|
332
|
+
commitState((snapshot) => ({
|
|
333
|
+
...snapshot,
|
|
334
|
+
nextDelayMs: null,
|
|
335
|
+
status: "stopped"
|
|
336
|
+
}));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
lastDelayRef.current = attempt.delayMs;
|
|
340
|
+
timeoutRef.current.schedule(() => {
|
|
341
|
+
runAttempt(attempt.attempt);
|
|
342
|
+
}, attempt.delayMs);
|
|
343
|
+
commitState({
|
|
344
|
+
attempt: attempt.attempt,
|
|
345
|
+
nextDelayMs: attempt.delayMs,
|
|
346
|
+
status: "scheduled"
|
|
347
|
+
});
|
|
348
|
+
emitSchedule(attempt);
|
|
349
|
+
};
|
|
350
|
+
const cancel = () => {
|
|
351
|
+
const current = stateRef.current;
|
|
352
|
+
const shouldEmitCancel = timeoutRef.current.isActive() || current.status === "scheduled" || current.status === "running";
|
|
353
|
+
timeoutRef.current.cancel();
|
|
354
|
+
commitState((snapshot) => ({
|
|
355
|
+
...snapshot,
|
|
356
|
+
nextDelayMs: null,
|
|
357
|
+
status: "stopped"
|
|
358
|
+
}));
|
|
359
|
+
if (shouldEmitCancel) {
|
|
360
|
+
emitCancel();
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const reset = () => {
|
|
364
|
+
timeoutRef.current.cancel();
|
|
365
|
+
lastDelayRef.current = null;
|
|
366
|
+
commitState(createInitialState(normalizedOptions.enabled));
|
|
367
|
+
emitReset();
|
|
368
|
+
};
|
|
369
|
+
const markConnected = () => {
|
|
370
|
+
timeoutRef.current.cancel();
|
|
371
|
+
if (normalizedOptions.resetOnSuccess) {
|
|
372
|
+
lastDelayRef.current = null;
|
|
373
|
+
commitState(createInitialState(normalizedOptions.enabled));
|
|
374
|
+
emitReset();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
commitState((current) => ({
|
|
378
|
+
...current,
|
|
379
|
+
nextDelayMs: null,
|
|
380
|
+
status: normalizedOptions.enabled ? "idle" : "stopped"
|
|
381
|
+
}));
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
attempt: state.attempt,
|
|
385
|
+
cancel,
|
|
386
|
+
isActive: state.status === "scheduled" || state.status === "running",
|
|
387
|
+
isScheduled: state.status === "scheduled",
|
|
388
|
+
markConnected,
|
|
389
|
+
nextDelayMs: state.nextDelayMs,
|
|
390
|
+
reset,
|
|
391
|
+
schedule,
|
|
392
|
+
status: state.status
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
var createInitialState2 = (isRunning) => ({
|
|
396
|
+
hasTimedOut: false,
|
|
397
|
+
isRunning,
|
|
398
|
+
lastAckAt: null,
|
|
399
|
+
lastBeatAt: null,
|
|
400
|
+
latencyMs: null
|
|
401
|
+
});
|
|
402
|
+
var useHeartbeat = (options) => {
|
|
403
|
+
const enabled = options.enabled ?? true;
|
|
404
|
+
const startOnMount = options.startOnMount ?? true;
|
|
405
|
+
const intervalRef = react.useRef(createManagedInterval());
|
|
406
|
+
const timeoutRef = react.useRef(createManagedTimeout());
|
|
407
|
+
const [state, setState] = react.useState(
|
|
408
|
+
() => createInitialState2(enabled && startOnMount)
|
|
409
|
+
);
|
|
410
|
+
const stateRef = react.useRef(state);
|
|
411
|
+
stateRef.current = state;
|
|
412
|
+
const commitState = (next) => {
|
|
413
|
+
const resolved = typeof next === "function" ? next(stateRef.current) : next;
|
|
414
|
+
stateRef.current = resolved;
|
|
415
|
+
setState(resolved);
|
|
416
|
+
};
|
|
417
|
+
const handleTimeout = react.useEffectEvent(() => {
|
|
418
|
+
commitState((current) => ({
|
|
419
|
+
...current,
|
|
420
|
+
hasTimedOut: true
|
|
421
|
+
}));
|
|
422
|
+
options.onTimeout?.();
|
|
423
|
+
});
|
|
424
|
+
const scheduleTimeout = react.useEffectEvent(() => {
|
|
425
|
+
if (options.timeoutMs === void 0) {
|
|
426
|
+
timeoutRef.current.cancel();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
timeoutRef.current.schedule(() => {
|
|
430
|
+
handleTimeout();
|
|
431
|
+
}, options.timeoutMs);
|
|
432
|
+
});
|
|
433
|
+
const runBeat = react.useEffectEvent(() => {
|
|
434
|
+
const performedAt = Date.now();
|
|
435
|
+
commitState((current) => ({
|
|
436
|
+
...current,
|
|
437
|
+
hasTimedOut: false,
|
|
438
|
+
lastBeatAt: performedAt
|
|
439
|
+
}));
|
|
440
|
+
scheduleTimeout();
|
|
441
|
+
options.onBeat?.();
|
|
442
|
+
void options.beat?.();
|
|
443
|
+
});
|
|
444
|
+
const start = () => {
|
|
445
|
+
if (!enabled) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!intervalRef.current.isActive()) {
|
|
449
|
+
intervalRef.current.start(() => {
|
|
450
|
+
runBeat();
|
|
451
|
+
}, options.intervalMs);
|
|
452
|
+
}
|
|
453
|
+
commitState((current) => ({
|
|
454
|
+
...current,
|
|
455
|
+
hasTimedOut: false,
|
|
456
|
+
isRunning: true
|
|
457
|
+
}));
|
|
458
|
+
};
|
|
459
|
+
const stop = () => {
|
|
460
|
+
intervalRef.current.cancel();
|
|
461
|
+
timeoutRef.current.cancel();
|
|
462
|
+
commitState((current) => ({
|
|
463
|
+
...current,
|
|
464
|
+
hasTimedOut: false,
|
|
465
|
+
isRunning: false
|
|
466
|
+
}));
|
|
467
|
+
};
|
|
468
|
+
const beat = () => {
|
|
469
|
+
if (!enabled) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
runBeat();
|
|
473
|
+
};
|
|
474
|
+
const notifyAck = (message) => {
|
|
475
|
+
if (stateRef.current.lastBeatAt === null) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const matchesAck = options.matchesAck === void 0 || options.matchesAck(message);
|
|
479
|
+
if (!matchesAck) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
const acknowledgedAt = Date.now();
|
|
483
|
+
timeoutRef.current.cancel();
|
|
484
|
+
commitState((current) => ({
|
|
485
|
+
...current,
|
|
486
|
+
hasTimedOut: false,
|
|
487
|
+
lastAckAt: acknowledgedAt,
|
|
488
|
+
latencyMs: current.lastBeatAt === null ? null : acknowledgedAt - current.lastBeatAt
|
|
489
|
+
}));
|
|
490
|
+
return true;
|
|
491
|
+
};
|
|
492
|
+
react.useEffect(() => {
|
|
493
|
+
if (!enabled) {
|
|
494
|
+
stop();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (startOnMount) {
|
|
498
|
+
start();
|
|
499
|
+
return stop;
|
|
500
|
+
}
|
|
501
|
+
stop();
|
|
502
|
+
return void 0;
|
|
503
|
+
}, [enabled, options.intervalMs, startOnMount]);
|
|
504
|
+
react.useEffect(() => () => {
|
|
505
|
+
intervalRef.current.cancel();
|
|
506
|
+
timeoutRef.current.cancel();
|
|
507
|
+
}, []);
|
|
508
|
+
return {
|
|
509
|
+
beat,
|
|
510
|
+
hasTimedOut: state.hasTimedOut,
|
|
511
|
+
isRunning: state.isRunning,
|
|
512
|
+
lastAckAt: state.lastAckAt,
|
|
513
|
+
lastBeatAt: state.lastBeatAt,
|
|
514
|
+
latencyMs: state.latencyMs,
|
|
515
|
+
notifyAck,
|
|
516
|
+
start,
|
|
517
|
+
stop
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// src/core/connection-state.ts
|
|
522
|
+
var createConnectionStateSnapshot = (status, config = {}) => {
|
|
523
|
+
const base = {
|
|
524
|
+
isSupported: config.isSupported ?? true,
|
|
525
|
+
lastChangedAt: config.lastChangedAt ?? null
|
|
526
|
+
};
|
|
527
|
+
switch (status) {
|
|
528
|
+
case "open":
|
|
529
|
+
return {
|
|
530
|
+
...base,
|
|
531
|
+
isClosed: false,
|
|
532
|
+
isConnected: true,
|
|
533
|
+
isConnecting: false,
|
|
534
|
+
status
|
|
535
|
+
};
|
|
536
|
+
case "connecting":
|
|
537
|
+
case "reconnecting":
|
|
538
|
+
return {
|
|
539
|
+
...base,
|
|
540
|
+
isClosed: false,
|
|
541
|
+
isConnected: false,
|
|
542
|
+
isConnecting: true,
|
|
543
|
+
status
|
|
544
|
+
};
|
|
545
|
+
case "closing":
|
|
546
|
+
return {
|
|
547
|
+
...base,
|
|
548
|
+
isClosed: false,
|
|
549
|
+
isConnected: false,
|
|
550
|
+
isConnecting: false,
|
|
551
|
+
status
|
|
552
|
+
};
|
|
553
|
+
case "idle":
|
|
554
|
+
case "closed":
|
|
555
|
+
case "error":
|
|
556
|
+
return {
|
|
557
|
+
...base,
|
|
558
|
+
isClosed: true,
|
|
559
|
+
isConnected: false,
|
|
560
|
+
isConnecting: false,
|
|
561
|
+
status
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// src/core/url.ts
|
|
567
|
+
var normalizeResolvedUrl = (value) => {
|
|
568
|
+
if (value === null) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
if (value instanceof URL) {
|
|
572
|
+
return value.toString();
|
|
573
|
+
}
|
|
574
|
+
const trimmed = value.trim();
|
|
575
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
576
|
+
};
|
|
577
|
+
var resolveUrlProvider = (url) => {
|
|
578
|
+
const resolved = typeof url === "function" ? url() : url;
|
|
579
|
+
return normalizeResolvedUrl(resolved ?? null);
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/hooks/useWebSocket.ts
|
|
583
|
+
var createInitialState3 = (status = "idle") => ({
|
|
584
|
+
bufferedAmount: 0,
|
|
585
|
+
lastChangedAt: null,
|
|
586
|
+
lastCloseEvent: null,
|
|
587
|
+
lastError: null,
|
|
588
|
+
lastMessage: null,
|
|
589
|
+
lastMessageEvent: null,
|
|
590
|
+
status
|
|
591
|
+
});
|
|
592
|
+
var defaultParseMessage = (event) => event.data;
|
|
593
|
+
var defaultSerializeMessage = (message) => {
|
|
594
|
+
if (typeof message === "string" || message instanceof Blob || message instanceof ArrayBuffer) {
|
|
595
|
+
return message;
|
|
596
|
+
}
|
|
597
|
+
if (ArrayBuffer.isView(message)) {
|
|
598
|
+
return message;
|
|
599
|
+
}
|
|
600
|
+
return JSON.stringify(message);
|
|
601
|
+
};
|
|
602
|
+
var resolveFactoryValue = (value) => typeof value === "function" ? value() : value;
|
|
603
|
+
var toProtocolsDependency = (protocols) => {
|
|
604
|
+
if (protocols === void 0) {
|
|
605
|
+
return "";
|
|
606
|
+
}
|
|
607
|
+
return Array.isArray(protocols) ? protocols.join("|") : protocols;
|
|
608
|
+
};
|
|
609
|
+
var toHeartbeatConfig = (heartbeat) => heartbeat === void 0 || heartbeat === false ? null : heartbeat;
|
|
610
|
+
var useWebSocket = (options) => {
|
|
611
|
+
const connect = options.connect ?? true;
|
|
612
|
+
const supported = isWebSocketSupported();
|
|
613
|
+
const resolvedUrl = react.useMemo(() => resolveUrlProvider(options.url), [options.url]);
|
|
614
|
+
const protocolsDependency = toProtocolsDependency(options.protocols);
|
|
615
|
+
const socketRef = react.useRef(null);
|
|
616
|
+
const socketKeyRef = react.useRef(null);
|
|
617
|
+
const manualCloseRef = react.useRef(false);
|
|
618
|
+
const manualOpenRef = react.useRef(false);
|
|
619
|
+
const skipCloseReconnectRef = react.useRef(false);
|
|
620
|
+
const suppressReconnectRef = react.useRef(false);
|
|
621
|
+
const [openNonce, setOpenNonce] = react.useState(0);
|
|
622
|
+
const [state, setState] = react.useState(
|
|
623
|
+
() => createInitialState3(connect ? "connecting" : "idle")
|
|
624
|
+
);
|
|
625
|
+
const stateRef = react.useRef(state);
|
|
626
|
+
stateRef.current = state;
|
|
627
|
+
const reconnectEnabled = options.reconnect !== false && supported && resolvedUrl !== null;
|
|
628
|
+
const reconnect = useReconnect(
|
|
629
|
+
options.reconnect === false ? { enabled: false } : {
|
|
630
|
+
...options.reconnect,
|
|
631
|
+
enabled: reconnectEnabled && (options.reconnect?.enabled ?? true)
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
const heartbeatEnabled = options.heartbeat !== false && supported && resolvedUrl !== null;
|
|
635
|
+
const heartbeatConfig = toHeartbeatConfig(
|
|
636
|
+
options.heartbeat
|
|
637
|
+
);
|
|
638
|
+
const heartbeatHookOptions = heartbeatConfig === null ? {
|
|
639
|
+
enabled: false,
|
|
640
|
+
intervalMs: 1e3,
|
|
641
|
+
startOnMount: false
|
|
642
|
+
} : {
|
|
643
|
+
beat: () => {
|
|
644
|
+
const socket2 = socketRef.current;
|
|
645
|
+
if (socket2 === null || socket2.readyState !== WebSocket.OPEN) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
const heartbeatMessage = heartbeatConfig.message;
|
|
649
|
+
if (heartbeatMessage !== void 0) {
|
|
650
|
+
const serialized = (options.serializeMessage ?? defaultSerializeMessage)(
|
|
651
|
+
resolveFactoryValue(heartbeatMessage)
|
|
652
|
+
);
|
|
653
|
+
socket2.send(serialized);
|
|
654
|
+
}
|
|
655
|
+
return heartbeatConfig.beat?.() ?? true;
|
|
656
|
+
},
|
|
657
|
+
enabled: heartbeatEnabled && (heartbeatConfig.enabled ?? true),
|
|
658
|
+
intervalMs: heartbeatConfig.intervalMs,
|
|
659
|
+
startOnMount: false
|
|
660
|
+
};
|
|
661
|
+
if (heartbeatConfig !== null && heartbeatConfig.timeoutMs !== void 0) {
|
|
662
|
+
heartbeatHookOptions.timeoutMs = heartbeatConfig.timeoutMs;
|
|
663
|
+
}
|
|
664
|
+
if (heartbeatConfig !== null && heartbeatConfig.matchesAck !== void 0) {
|
|
665
|
+
heartbeatHookOptions.matchesAck = heartbeatConfig.matchesAck;
|
|
666
|
+
}
|
|
667
|
+
if (heartbeatConfig !== null && heartbeatConfig.onBeat !== void 0) {
|
|
668
|
+
heartbeatHookOptions.onBeat = heartbeatConfig.onBeat;
|
|
669
|
+
}
|
|
670
|
+
if (heartbeatConfig !== null && heartbeatConfig.onTimeout !== void 0) {
|
|
671
|
+
heartbeatHookOptions.onTimeout = heartbeatConfig.onTimeout;
|
|
672
|
+
}
|
|
673
|
+
const heartbeat = useHeartbeat(
|
|
674
|
+
heartbeatHookOptions
|
|
675
|
+
);
|
|
676
|
+
const commitState = (next) => {
|
|
677
|
+
const resolved = typeof next === "function" ? next(stateRef.current) : next;
|
|
678
|
+
stateRef.current = resolved;
|
|
679
|
+
setState(resolved);
|
|
680
|
+
};
|
|
681
|
+
const closeSocket = react.useEffectEvent((code, reason) => {
|
|
682
|
+
const socket2 = socketRef.current;
|
|
683
|
+
if (socket2 === null) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
socketRef.current = null;
|
|
687
|
+
socketKeyRef.current = null;
|
|
688
|
+
if (socket2.readyState === WebSocket.OPEN || socket2.readyState === WebSocket.CONNECTING) {
|
|
689
|
+
socket2.close(code, reason);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
const parseMessage = react.useEffectEvent((event) => {
|
|
693
|
+
const parser = options.parseMessage ?? defaultParseMessage;
|
|
694
|
+
return parser(event);
|
|
695
|
+
});
|
|
696
|
+
const updateBufferedAmount = react.useEffectEvent(() => {
|
|
697
|
+
commitState((current) => ({
|
|
698
|
+
...current,
|
|
699
|
+
bufferedAmount: socketRef.current?.bufferedAmount ?? 0
|
|
700
|
+
}));
|
|
701
|
+
});
|
|
702
|
+
const handleOpen = react.useEffectEvent((event, socket2) => {
|
|
703
|
+
manualCloseRef.current = false;
|
|
704
|
+
suppressReconnectRef.current = false;
|
|
705
|
+
reconnect.markConnected();
|
|
706
|
+
heartbeat.start();
|
|
707
|
+
commitState((current) => ({
|
|
708
|
+
...current,
|
|
709
|
+
bufferedAmount: socket2.bufferedAmount,
|
|
710
|
+
lastChangedAt: Date.now(),
|
|
711
|
+
status: "open"
|
|
712
|
+
}));
|
|
713
|
+
options.onOpen?.(event, socket2);
|
|
714
|
+
});
|
|
715
|
+
const handleMessage = react.useEffectEvent((event) => {
|
|
716
|
+
try {
|
|
717
|
+
const message = parseMessage(event);
|
|
718
|
+
heartbeat.notifyAck(message);
|
|
719
|
+
commitState((current) => ({
|
|
720
|
+
...current,
|
|
721
|
+
bufferedAmount: socketRef.current?.bufferedAmount ?? current.bufferedAmount,
|
|
722
|
+
lastMessage: message,
|
|
723
|
+
lastMessageEvent: event
|
|
724
|
+
}));
|
|
725
|
+
options.onMessage?.(message, event);
|
|
726
|
+
} catch {
|
|
727
|
+
const parseError = new Event("error");
|
|
728
|
+
commitState((current) => ({
|
|
729
|
+
...current,
|
|
730
|
+
lastError: parseError,
|
|
731
|
+
status: "error"
|
|
732
|
+
}));
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
const handleError = react.useEffectEvent((event) => {
|
|
736
|
+
heartbeat.stop();
|
|
737
|
+
commitState((current) => ({
|
|
738
|
+
...current,
|
|
739
|
+
lastError: event,
|
|
740
|
+
status: "error"
|
|
741
|
+
}));
|
|
742
|
+
options.onError?.(event);
|
|
743
|
+
});
|
|
744
|
+
const handleClose = react.useEffectEvent((event) => {
|
|
745
|
+
socketRef.current = null;
|
|
746
|
+
socketKeyRef.current = null;
|
|
747
|
+
heartbeat.stop();
|
|
748
|
+
updateBufferedAmount();
|
|
749
|
+
const skipCloseReconnect = skipCloseReconnectRef.current;
|
|
750
|
+
skipCloseReconnectRef.current = false;
|
|
751
|
+
const shouldReconnect = !suppressReconnectRef.current && !skipCloseReconnect && reconnectEnabled && (options.shouldReconnect?.(event) ?? true);
|
|
752
|
+
commitState((current) => ({
|
|
753
|
+
...current,
|
|
754
|
+
lastChangedAt: Date.now(),
|
|
755
|
+
lastCloseEvent: event,
|
|
756
|
+
status: shouldReconnect ? "reconnecting" : "closed"
|
|
757
|
+
}));
|
|
758
|
+
options.onClose?.(event);
|
|
759
|
+
if (shouldReconnect) {
|
|
760
|
+
reconnect.schedule("close");
|
|
761
|
+
} else {
|
|
762
|
+
suppressReconnectRef.current = false;
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
const open = () => {
|
|
766
|
+
manualCloseRef.current = false;
|
|
767
|
+
manualOpenRef.current = true;
|
|
768
|
+
suppressReconnectRef.current = false;
|
|
769
|
+
reconnect.cancel();
|
|
770
|
+
setOpenNonce((current) => current + 1);
|
|
771
|
+
};
|
|
772
|
+
const reconnectNow = () => {
|
|
773
|
+
manualCloseRef.current = false;
|
|
774
|
+
manualOpenRef.current = true;
|
|
775
|
+
skipCloseReconnectRef.current = true;
|
|
776
|
+
suppressReconnectRef.current = true;
|
|
777
|
+
heartbeat.stop();
|
|
778
|
+
closeSocket();
|
|
779
|
+
suppressReconnectRef.current = false;
|
|
780
|
+
reconnect.schedule("manual");
|
|
781
|
+
};
|
|
782
|
+
const close = (code, reason) => {
|
|
783
|
+
manualCloseRef.current = true;
|
|
784
|
+
manualOpenRef.current = false;
|
|
785
|
+
suppressReconnectRef.current = true;
|
|
786
|
+
reconnect.cancel();
|
|
787
|
+
heartbeat.stop();
|
|
788
|
+
commitState((current) => ({
|
|
789
|
+
...current,
|
|
790
|
+
lastChangedAt: Date.now(),
|
|
791
|
+
status: "closing"
|
|
792
|
+
}));
|
|
793
|
+
closeSocket(code, reason);
|
|
794
|
+
};
|
|
795
|
+
const send = (message) => {
|
|
796
|
+
const socket2 = socketRef.current;
|
|
797
|
+
if (socket2 === null || socket2.readyState !== WebSocket.OPEN) {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
const serializer = options.serializeMessage ?? defaultSerializeMessage;
|
|
801
|
+
socket2.send(serializer(message));
|
|
802
|
+
updateBufferedAmount();
|
|
803
|
+
return true;
|
|
804
|
+
};
|
|
805
|
+
react.useEffect(() => {
|
|
806
|
+
if (!supported) {
|
|
807
|
+
socketKeyRef.current = null;
|
|
808
|
+
commitState((current) => ({
|
|
809
|
+
...current,
|
|
810
|
+
status: "closed"
|
|
811
|
+
}));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (resolvedUrl === null) {
|
|
815
|
+
socketKeyRef.current = null;
|
|
816
|
+
closeSocket();
|
|
817
|
+
commitState((current) => ({
|
|
818
|
+
...current,
|
|
819
|
+
status: "closed"
|
|
820
|
+
}));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const shouldConnect = connect && !manualCloseRef.current || manualOpenRef.current || reconnect.status === "running";
|
|
824
|
+
const nextSocketKey = `${resolvedUrl}::${protocolsDependency}::${options.binaryType ?? "blob"}`;
|
|
825
|
+
if (!shouldConnect) {
|
|
826
|
+
if (socketRef.current !== null) {
|
|
827
|
+
suppressReconnectRef.current = true;
|
|
828
|
+
closeSocket();
|
|
829
|
+
}
|
|
830
|
+
socketKeyRef.current = null;
|
|
831
|
+
commitState((current) => ({
|
|
832
|
+
...current,
|
|
833
|
+
status: manualCloseRef.current ? "closed" : "idle"
|
|
834
|
+
}));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (socketRef.current !== null && socketKeyRef.current !== nextSocketKey) {
|
|
838
|
+
suppressReconnectRef.current = true;
|
|
839
|
+
closeSocket();
|
|
840
|
+
}
|
|
841
|
+
if (socketRef.current !== null) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const socket2 = new WebSocket(resolvedUrl, options.protocols);
|
|
845
|
+
socketRef.current = socket2;
|
|
846
|
+
socketKeyRef.current = nextSocketKey;
|
|
847
|
+
socket2.binaryType = options.binaryType ?? "blob";
|
|
848
|
+
commitState((current) => ({
|
|
849
|
+
...current,
|
|
850
|
+
bufferedAmount: socket2.bufferedAmount,
|
|
851
|
+
lastChangedAt: Date.now(),
|
|
852
|
+
status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
|
|
853
|
+
}));
|
|
854
|
+
const handleSocketOpen = (event) => {
|
|
855
|
+
handleOpen(event, socket2);
|
|
856
|
+
};
|
|
857
|
+
const handleSocketMessage = (event) => {
|
|
858
|
+
handleMessage(event);
|
|
859
|
+
};
|
|
860
|
+
const handleSocketError = (event) => {
|
|
861
|
+
handleError(event);
|
|
862
|
+
};
|
|
863
|
+
const handleSocketClose = (event) => {
|
|
864
|
+
handleClose(event);
|
|
865
|
+
};
|
|
866
|
+
socket2.addEventListener("open", handleSocketOpen);
|
|
867
|
+
socket2.addEventListener("message", handleSocketMessage);
|
|
868
|
+
socket2.addEventListener("error", handleSocketError);
|
|
869
|
+
socket2.addEventListener("close", handleSocketClose);
|
|
870
|
+
return () => {
|
|
871
|
+
socket2.removeEventListener("open", handleSocketOpen);
|
|
872
|
+
socket2.removeEventListener("message", handleSocketMessage);
|
|
873
|
+
socket2.removeEventListener("error", handleSocketError);
|
|
874
|
+
socket2.removeEventListener("close", handleSocketClose);
|
|
875
|
+
};
|
|
876
|
+
}, [
|
|
877
|
+
connect,
|
|
878
|
+
openNonce,
|
|
879
|
+
options.binaryType,
|
|
880
|
+
protocolsDependency,
|
|
881
|
+
reconnect.status,
|
|
882
|
+
resolvedUrl,
|
|
883
|
+
supported
|
|
884
|
+
]);
|
|
885
|
+
react.useEffect(() => () => {
|
|
886
|
+
suppressReconnectRef.current = true;
|
|
887
|
+
socketKeyRef.current = null;
|
|
888
|
+
const socket2 = socketRef.current;
|
|
889
|
+
socketRef.current = null;
|
|
890
|
+
if (socket2 === null) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (socket2.readyState === WebSocket.OPEN || socket2.readyState === WebSocket.CONNECTING) {
|
|
894
|
+
socket2.close();
|
|
895
|
+
}
|
|
896
|
+
}, []);
|
|
897
|
+
react.useEffect(() => {
|
|
898
|
+
if (state.status !== "open") {
|
|
899
|
+
heartbeat.stop();
|
|
900
|
+
}
|
|
901
|
+
}, [state.status]);
|
|
902
|
+
const status = (reconnect.status === "scheduled" || reconnect.status === "running") && state.status !== "open" ? "reconnecting" : state.status;
|
|
903
|
+
const snapshot = createConnectionStateSnapshot(status, {
|
|
904
|
+
isSupported: supported,
|
|
905
|
+
lastChangedAt: state.lastChangedAt
|
|
906
|
+
});
|
|
907
|
+
const heartbeatState = options.heartbeat === false ? null : {
|
|
908
|
+
hasTimedOut: heartbeat.hasTimedOut,
|
|
909
|
+
isRunning: heartbeat.isRunning,
|
|
910
|
+
lastAckAt: heartbeat.lastAckAt,
|
|
911
|
+
lastBeatAt: heartbeat.lastBeatAt,
|
|
912
|
+
latencyMs: heartbeat.latencyMs
|
|
913
|
+
};
|
|
914
|
+
const reconnectState = options.reconnect === false ? null : {
|
|
915
|
+
attempt: reconnect.attempt,
|
|
916
|
+
isScheduled: reconnect.isScheduled,
|
|
917
|
+
nextDelayMs: reconnect.nextDelayMs,
|
|
918
|
+
status: reconnect.status
|
|
919
|
+
};
|
|
920
|
+
const commonResult = {
|
|
921
|
+
bufferedAmount: state.bufferedAmount,
|
|
922
|
+
close,
|
|
923
|
+
heartbeatState,
|
|
924
|
+
lastCloseEvent: state.lastCloseEvent,
|
|
925
|
+
lastError: state.lastError,
|
|
926
|
+
lastMessage: state.lastMessage,
|
|
927
|
+
lastMessageEvent: state.lastMessageEvent,
|
|
928
|
+
open,
|
|
929
|
+
reconnect: reconnectNow,
|
|
930
|
+
reconnectState,
|
|
931
|
+
send,
|
|
932
|
+
transport: "websocket"
|
|
933
|
+
};
|
|
934
|
+
const socket = socketRef.current;
|
|
935
|
+
if (snapshot.status === "open") {
|
|
936
|
+
return {
|
|
937
|
+
...snapshot,
|
|
938
|
+
...commonResult,
|
|
939
|
+
socket
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
...snapshot,
|
|
944
|
+
...commonResult,
|
|
945
|
+
socket
|
|
946
|
+
};
|
|
947
|
+
};
|
|
948
|
+
var createInitialState4 = (status = "idle") => ({
|
|
949
|
+
lastChangedAt: null,
|
|
950
|
+
lastError: null,
|
|
951
|
+
lastEventName: null,
|
|
952
|
+
lastMessage: null,
|
|
953
|
+
lastMessageEvent: null,
|
|
954
|
+
status
|
|
955
|
+
});
|
|
956
|
+
var defaultParseMessage2 = (event) => event.data;
|
|
957
|
+
var toEventsDependency = (events) => {
|
|
958
|
+
if (events === void 0 || events.length === 0) {
|
|
959
|
+
return "";
|
|
960
|
+
}
|
|
961
|
+
return [...new Set(events)].sort().join("|");
|
|
962
|
+
};
|
|
963
|
+
var normalizeNamedEvents = (events) => {
|
|
964
|
+
if (events === void 0 || events.length === 0) {
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
967
|
+
return [...new Set(events)].filter((eventName) => eventName !== "message");
|
|
968
|
+
};
|
|
969
|
+
var useEventSource = (options) => {
|
|
970
|
+
const connect = options.connect ?? true;
|
|
971
|
+
const supported = isEventSourceSupported();
|
|
972
|
+
const resolvedUrl = react.useMemo(() => resolveUrlProvider(options.url), [options.url]);
|
|
973
|
+
const eventsDependency = toEventsDependency(options.events);
|
|
974
|
+
const namedEvents = react.useMemo(
|
|
975
|
+
() => normalizeNamedEvents(options.events),
|
|
976
|
+
[eventsDependency]
|
|
977
|
+
);
|
|
978
|
+
const eventSourceRef = react.useRef(null);
|
|
979
|
+
const eventSourceKeyRef = react.useRef(null);
|
|
980
|
+
const manualCloseRef = react.useRef(false);
|
|
981
|
+
const manualOpenRef = react.useRef(false);
|
|
982
|
+
const skipErrorReconnectRef = react.useRef(false);
|
|
983
|
+
const suppressReconnectRef = react.useRef(false);
|
|
984
|
+
const [openNonce, setOpenNonce] = react.useState(0);
|
|
985
|
+
const [state, setState] = react.useState(
|
|
986
|
+
() => createInitialState4(connect ? "connecting" : "idle")
|
|
987
|
+
);
|
|
988
|
+
const stateRef = react.useRef(state);
|
|
989
|
+
stateRef.current = state;
|
|
990
|
+
const reconnectEnabled = options.reconnect !== false && supported && resolvedUrl !== null;
|
|
991
|
+
const reconnect = useReconnect(
|
|
992
|
+
options.reconnect === false ? { enabled: false } : {
|
|
993
|
+
...options.reconnect,
|
|
994
|
+
enabled: reconnectEnabled && (options.reconnect?.enabled ?? true)
|
|
995
|
+
}
|
|
996
|
+
);
|
|
997
|
+
const commitState = (next) => {
|
|
998
|
+
const resolved = typeof next === "function" ? next(stateRef.current) : next;
|
|
999
|
+
stateRef.current = resolved;
|
|
1000
|
+
setState(resolved);
|
|
1001
|
+
};
|
|
1002
|
+
const closeEventSource = react.useEffectEvent(() => {
|
|
1003
|
+
const source = eventSourceRef.current;
|
|
1004
|
+
if (source === null) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
eventSourceRef.current = null;
|
|
1008
|
+
eventSourceKeyRef.current = null;
|
|
1009
|
+
source.close();
|
|
1010
|
+
});
|
|
1011
|
+
const parseMessage = react.useEffectEvent((event) => {
|
|
1012
|
+
const parser = options.parseMessage ?? defaultParseMessage2;
|
|
1013
|
+
return parser(event);
|
|
1014
|
+
});
|
|
1015
|
+
const handleOpen = react.useEffectEvent((event, source) => {
|
|
1016
|
+
manualCloseRef.current = false;
|
|
1017
|
+
suppressReconnectRef.current = false;
|
|
1018
|
+
reconnect.markConnected();
|
|
1019
|
+
commitState((current) => ({
|
|
1020
|
+
...current,
|
|
1021
|
+
lastChangedAt: Date.now(),
|
|
1022
|
+
status: "open"
|
|
1023
|
+
}));
|
|
1024
|
+
options.onOpen?.(event, source);
|
|
1025
|
+
});
|
|
1026
|
+
const commitParsedMessage = react.useEffectEvent(
|
|
1027
|
+
(eventName, event, isNamedEvent) => {
|
|
1028
|
+
try {
|
|
1029
|
+
const message = parseMessage(event);
|
|
1030
|
+
commitState((current) => ({
|
|
1031
|
+
...current,
|
|
1032
|
+
lastEventName: eventName,
|
|
1033
|
+
lastMessage: message,
|
|
1034
|
+
lastMessageEvent: event
|
|
1035
|
+
}));
|
|
1036
|
+
if (isNamedEvent) {
|
|
1037
|
+
options.onEvent?.(eventName, message, event);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
options.onMessage?.(message, event);
|
|
1041
|
+
} catch {
|
|
1042
|
+
const parseError = new Event("error");
|
|
1043
|
+
commitState((current) => ({
|
|
1044
|
+
...current,
|
|
1045
|
+
lastError: parseError,
|
|
1046
|
+
status: "error"
|
|
1047
|
+
}));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
);
|
|
1051
|
+
const handleError = react.useEffectEvent((event, source) => {
|
|
1052
|
+
const skipErrorReconnect = skipErrorReconnectRef.current;
|
|
1053
|
+
skipErrorReconnectRef.current = false;
|
|
1054
|
+
const shouldReconnect = !suppressReconnectRef.current && !skipErrorReconnect && reconnectEnabled && (options.shouldReconnect?.(event) ?? true);
|
|
1055
|
+
const readyState = source.readyState;
|
|
1056
|
+
commitState((current) => ({
|
|
1057
|
+
...current,
|
|
1058
|
+
lastChangedAt: Date.now(),
|
|
1059
|
+
lastError: event,
|
|
1060
|
+
status: readyState === EventSource.OPEN ? "open" : shouldReconnect ? "reconnecting" : "closed"
|
|
1061
|
+
}));
|
|
1062
|
+
options.onError?.(event);
|
|
1063
|
+
if (!shouldReconnect) {
|
|
1064
|
+
suppressReconnectRef.current = false;
|
|
1065
|
+
closeEventSource();
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (readyState === EventSource.CLOSED) {
|
|
1069
|
+
eventSourceRef.current = null;
|
|
1070
|
+
eventSourceKeyRef.current = null;
|
|
1071
|
+
reconnect.schedule("error");
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
const open = () => {
|
|
1075
|
+
manualCloseRef.current = false;
|
|
1076
|
+
manualOpenRef.current = true;
|
|
1077
|
+
suppressReconnectRef.current = false;
|
|
1078
|
+
reconnect.cancel();
|
|
1079
|
+
setOpenNonce((current) => current + 1);
|
|
1080
|
+
};
|
|
1081
|
+
const reconnectNow = () => {
|
|
1082
|
+
manualCloseRef.current = false;
|
|
1083
|
+
manualOpenRef.current = true;
|
|
1084
|
+
skipErrorReconnectRef.current = true;
|
|
1085
|
+
suppressReconnectRef.current = true;
|
|
1086
|
+
closeEventSource();
|
|
1087
|
+
suppressReconnectRef.current = false;
|
|
1088
|
+
reconnect.schedule("manual");
|
|
1089
|
+
};
|
|
1090
|
+
const close = () => {
|
|
1091
|
+
manualCloseRef.current = true;
|
|
1092
|
+
manualOpenRef.current = false;
|
|
1093
|
+
suppressReconnectRef.current = true;
|
|
1094
|
+
reconnect.cancel();
|
|
1095
|
+
closeEventSource();
|
|
1096
|
+
commitState((current) => ({
|
|
1097
|
+
...current,
|
|
1098
|
+
lastChangedAt: Date.now(),
|
|
1099
|
+
status: "closed"
|
|
1100
|
+
}));
|
|
1101
|
+
};
|
|
1102
|
+
react.useEffect(() => {
|
|
1103
|
+
if (!supported) {
|
|
1104
|
+
eventSourceKeyRef.current = null;
|
|
1105
|
+
commitState((current) => ({
|
|
1106
|
+
...current,
|
|
1107
|
+
status: "closed"
|
|
1108
|
+
}));
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (resolvedUrl === null) {
|
|
1112
|
+
eventSourceKeyRef.current = null;
|
|
1113
|
+
closeEventSource();
|
|
1114
|
+
commitState((current) => ({
|
|
1115
|
+
...current,
|
|
1116
|
+
status: "closed"
|
|
1117
|
+
}));
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const shouldConnect = connect && !manualCloseRef.current || manualOpenRef.current || reconnect.status === "running";
|
|
1121
|
+
const nextEventSourceKey = [
|
|
1122
|
+
resolvedUrl,
|
|
1123
|
+
options.withCredentials ? "credentials" : "anonymous",
|
|
1124
|
+
eventsDependency
|
|
1125
|
+
].join("::");
|
|
1126
|
+
if (!shouldConnect) {
|
|
1127
|
+
if (eventSourceRef.current !== null) {
|
|
1128
|
+
suppressReconnectRef.current = true;
|
|
1129
|
+
closeEventSource();
|
|
1130
|
+
}
|
|
1131
|
+
eventSourceKeyRef.current = null;
|
|
1132
|
+
commitState((current) => ({
|
|
1133
|
+
...current,
|
|
1134
|
+
status: manualCloseRef.current ? "closed" : "idle"
|
|
1135
|
+
}));
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (eventSourceRef.current !== null && eventSourceKeyRef.current !== nextEventSourceKey) {
|
|
1139
|
+
suppressReconnectRef.current = true;
|
|
1140
|
+
closeEventSource();
|
|
1141
|
+
}
|
|
1142
|
+
if (eventSourceRef.current !== null) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const source = new EventSource(resolvedUrl, {
|
|
1146
|
+
withCredentials: options.withCredentials ?? false
|
|
1147
|
+
});
|
|
1148
|
+
eventSourceRef.current = source;
|
|
1149
|
+
eventSourceKeyRef.current = nextEventSourceKey;
|
|
1150
|
+
commitState((current) => ({
|
|
1151
|
+
...current,
|
|
1152
|
+
lastChangedAt: Date.now(),
|
|
1153
|
+
status: reconnect.status === "running" || reconnect.status === "scheduled" ? "reconnecting" : "connecting"
|
|
1154
|
+
}));
|
|
1155
|
+
const handleSourceOpen = (event) => {
|
|
1156
|
+
handleOpen(event, source);
|
|
1157
|
+
};
|
|
1158
|
+
const handleSourceMessage = (event) => {
|
|
1159
|
+
commitParsedMessage("message", event, false);
|
|
1160
|
+
};
|
|
1161
|
+
const namedEventHandlers = /* @__PURE__ */ new Map();
|
|
1162
|
+
const handleSourceError = (event) => {
|
|
1163
|
+
handleError(event, source);
|
|
1164
|
+
};
|
|
1165
|
+
source.addEventListener("open", handleSourceOpen);
|
|
1166
|
+
source.addEventListener("message", handleSourceMessage);
|
|
1167
|
+
for (const eventName of namedEvents) {
|
|
1168
|
+
const handler = (event) => {
|
|
1169
|
+
commitParsedMessage(eventName, event, true);
|
|
1170
|
+
};
|
|
1171
|
+
namedEventHandlers.set(eventName, handler);
|
|
1172
|
+
source.addEventListener(eventName, handler);
|
|
1173
|
+
}
|
|
1174
|
+
source.addEventListener("error", handleSourceError);
|
|
1175
|
+
return () => {
|
|
1176
|
+
source.removeEventListener("open", handleSourceOpen);
|
|
1177
|
+
source.removeEventListener("message", handleSourceMessage);
|
|
1178
|
+
for (const [eventName, handler] of namedEventHandlers) {
|
|
1179
|
+
source.removeEventListener(eventName, handler);
|
|
1180
|
+
}
|
|
1181
|
+
source.removeEventListener("error", handleSourceError);
|
|
1182
|
+
};
|
|
1183
|
+
}, [
|
|
1184
|
+
connect,
|
|
1185
|
+
eventsDependency,
|
|
1186
|
+
namedEvents,
|
|
1187
|
+
openNonce,
|
|
1188
|
+
options.withCredentials,
|
|
1189
|
+
reconnect.status,
|
|
1190
|
+
resolvedUrl,
|
|
1191
|
+
supported
|
|
1192
|
+
]);
|
|
1193
|
+
react.useEffect(() => () => {
|
|
1194
|
+
suppressReconnectRef.current = true;
|
|
1195
|
+
eventSourceKeyRef.current = null;
|
|
1196
|
+
const source = eventSourceRef.current;
|
|
1197
|
+
eventSourceRef.current = null;
|
|
1198
|
+
if (source !== null) {
|
|
1199
|
+
source.close();
|
|
1200
|
+
}
|
|
1201
|
+
}, []);
|
|
1202
|
+
const status = (reconnect.status === "scheduled" || reconnect.status === "running") && state.status !== "open" ? "reconnecting" : state.status;
|
|
1203
|
+
const snapshot = createConnectionStateSnapshot(status, {
|
|
1204
|
+
isSupported: supported,
|
|
1205
|
+
lastChangedAt: state.lastChangedAt
|
|
1206
|
+
});
|
|
1207
|
+
const reconnectState = options.reconnect === false ? null : {
|
|
1208
|
+
attempt: reconnect.attempt,
|
|
1209
|
+
isScheduled: reconnect.isScheduled,
|
|
1210
|
+
nextDelayMs: reconnect.nextDelayMs,
|
|
1211
|
+
status: reconnect.status
|
|
1212
|
+
};
|
|
1213
|
+
const commonResult = {
|
|
1214
|
+
close,
|
|
1215
|
+
lastError: state.lastError,
|
|
1216
|
+
lastEventName: state.lastEventName,
|
|
1217
|
+
lastMessage: state.lastMessage,
|
|
1218
|
+
lastMessageEvent: state.lastMessageEvent,
|
|
1219
|
+
open,
|
|
1220
|
+
reconnect: reconnectNow,
|
|
1221
|
+
reconnectState,
|
|
1222
|
+
transport: "eventsource"
|
|
1223
|
+
};
|
|
1224
|
+
const eventSource = eventSourceRef.current;
|
|
1225
|
+
if (snapshot.status === "open") {
|
|
1226
|
+
return {
|
|
1227
|
+
...snapshot,
|
|
1228
|
+
...commonResult,
|
|
1229
|
+
eventSource
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
...snapshot,
|
|
1234
|
+
...commonResult,
|
|
1235
|
+
eventSource
|
|
1236
|
+
};
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
exports.useEventSource = useEventSource;
|
|
1240
|
+
exports.useHeartbeat = useHeartbeat;
|
|
1241
|
+
exports.useOnlineStatus = useOnlineStatus;
|
|
1242
|
+
exports.useReconnect = useReconnect;
|
|
1243
|
+
exports.useWebSocket = useWebSocket;
|
|
1244
|
+
//# sourceMappingURL=index.cjs.map
|
|
1245
|
+
//# sourceMappingURL=index.cjs.map
|