@chromahq/react 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +32 -11
- package/dist/index.js +221 -28
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface Props {
|
|
|
19
19
|
retryAfter?: number;
|
|
20
20
|
maxRetries?: number;
|
|
21
21
|
pingInterval?: number;
|
|
22
|
+
maxRetryCooldown?: number;
|
|
22
23
|
onConnectionChange?: (status: ConnectionStatus) => void;
|
|
23
24
|
onError?: (error: Error) => void;
|
|
24
25
|
}
|
|
@@ -30,22 +31,42 @@ declare const BridgeProvider: React.FC<Props>;
|
|
|
30
31
|
*/
|
|
31
32
|
declare const useBridge: () => BridgeContextValue;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*/
|
|
39
|
-
|
|
34
|
+
interface UseBridgeQueryOptions {
|
|
35
|
+
/** Whether to automatically refetch when bridge reconnects. Default: true */
|
|
36
|
+
refetchOnReconnect?: boolean;
|
|
37
|
+
/** Whether to skip the query entirely. Default: false */
|
|
38
|
+
skip?: boolean;
|
|
39
|
+
/** Timeout duration in ms. Default: 10000 */
|
|
40
|
+
timeout?: number;
|
|
41
|
+
}
|
|
42
|
+
interface UseBridgeQueryResult<Res> {
|
|
40
43
|
data: Res | undefined;
|
|
41
44
|
loading: boolean;
|
|
42
45
|
error: unknown;
|
|
43
|
-
|
|
46
|
+
refetch: () => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Custom hook to send a query to the bridge and return the response.
|
|
50
|
+
* Automatically waits for bridge connection and retries on reconnect.
|
|
51
|
+
* @param key The message key to send
|
|
52
|
+
* @param payload Optional payload to send with the message
|
|
53
|
+
* @param options Query options
|
|
54
|
+
* @returns { data, loading, error, refetch }
|
|
55
|
+
*/
|
|
56
|
+
declare function useBridgeQuery<Res = unknown>(key: string, payload?: any, options?: UseBridgeQueryOptions): UseBridgeQueryResult<Res>;
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
status:
|
|
58
|
+
interface ConnectionStatusResult {
|
|
59
|
+
/** Current connection status: 'connecting' | 'connected' | 'disconnected' | 'error' | 'reconnecting' */
|
|
60
|
+
status: 'connecting' | 'connected' | 'disconnected' | 'error' | 'reconnecting' | undefined;
|
|
61
|
+
/** Whether the bridge port is connected */
|
|
62
|
+
isConnected: boolean;
|
|
63
|
+
/** Whether the service worker is responding to pings */
|
|
47
64
|
isServiceWorkerAlive: boolean;
|
|
65
|
+
/** Manually trigger a reconnection */
|
|
48
66
|
reconnect: (() => void) | undefined;
|
|
49
|
-
|
|
67
|
+
/** Any connection error */
|
|
68
|
+
error: Error | null | undefined;
|
|
69
|
+
}
|
|
70
|
+
declare const useConnectionStatus: () => ConnectionStatusResult;
|
|
50
71
|
|
|
51
72
|
export { BridgeProvider, useBridge, useBridgeQuery, useConnectionStatus };
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,8 @@ const BridgeProvider = ({
|
|
|
7
7
|
retryAfter = 1e3,
|
|
8
8
|
maxRetries = 10,
|
|
9
9
|
pingInterval = 5e3,
|
|
10
|
+
maxRetryCooldown = 3e4,
|
|
11
|
+
// Reset retry count after 30s of max retries
|
|
10
12
|
onConnectionChange,
|
|
11
13
|
onError
|
|
12
14
|
}) => {
|
|
@@ -15,6 +17,8 @@ const BridgeProvider = ({
|
|
|
15
17
|
const [error, setError] = useState(null);
|
|
16
18
|
const [isServiceWorkerAlive, setIsServiceWorkerAlive] = useState(false);
|
|
17
19
|
const portRef = useRef(null);
|
|
20
|
+
const maxRetryCooldownRef = useRef(null);
|
|
21
|
+
const consecutivePingFailuresRef = useRef(0);
|
|
18
22
|
const pendingRef = useRef(
|
|
19
23
|
/* @__PURE__ */ new Map()
|
|
20
24
|
);
|
|
@@ -156,8 +160,26 @@ const BridgeProvider = ({
|
|
|
156
160
|
cleanup();
|
|
157
161
|
if (retryCountRef.current < maxRetries) {
|
|
158
162
|
retryCountRef.current++;
|
|
159
|
-
const delay = retryAfter * Math.pow(2, retryCountRef.current - 1);
|
|
163
|
+
const delay = Math.min(retryAfter * Math.pow(2, retryCountRef.current - 1), 3e4);
|
|
164
|
+
console.log(
|
|
165
|
+
`[Bridge] Reconnecting in ${delay}ms (attempt ${retryCountRef.current}/${maxRetries})`
|
|
166
|
+
);
|
|
167
|
+
updateStatus("reconnecting");
|
|
160
168
|
reconnectTimeoutRef.current = setTimeout(connect, delay);
|
|
169
|
+
} else {
|
|
170
|
+
console.warn(
|
|
171
|
+
`[Bridge] Max retries (${maxRetries}) reached. Will retry after ${maxRetryCooldown}ms cooldown.`
|
|
172
|
+
);
|
|
173
|
+
if (maxRetryCooldownRef.current) {
|
|
174
|
+
clearTimeout(maxRetryCooldownRef.current);
|
|
175
|
+
}
|
|
176
|
+
maxRetryCooldownRef.current = setTimeout(() => {
|
|
177
|
+
console.log(
|
|
178
|
+
"[Bridge] Cooldown complete, resetting retry count and attempting reconnection..."
|
|
179
|
+
);
|
|
180
|
+
retryCountRef.current = 0;
|
|
181
|
+
connect();
|
|
182
|
+
}, maxRetryCooldown);
|
|
161
183
|
}
|
|
162
184
|
});
|
|
163
185
|
const bridgeInstance = {
|
|
@@ -172,22 +194,32 @@ const BridgeProvider = ({
|
|
|
172
194
|
if (pendingRef.current.has(id)) {
|
|
173
195
|
pendingRef.current.delete(id);
|
|
174
196
|
consecutiveTimeoutsRef.current++;
|
|
175
|
-
|
|
197
|
+
const errorMessage = `Request timed out after ${timeoutDuration}ms for key: ${key} (id: ${id})`;
|
|
198
|
+
console.warn(`[Bridge] ${errorMessage}`);
|
|
199
|
+
if (consecutiveTimeoutsRef.current >= 2 && status !== "reconnecting") {
|
|
176
200
|
console.warn(
|
|
177
|
-
|
|
201
|
+
`[Bridge] ${consecutiveTimeoutsRef.current} consecutive timeouts detected, service worker may be unresponsive. Reconnecting...`
|
|
178
202
|
);
|
|
179
203
|
setIsServiceWorkerAlive(false);
|
|
180
204
|
updateStatus("reconnecting");
|
|
181
|
-
|
|
205
|
+
pendingRef.current.forEach(
|
|
206
|
+
({ reject: pendingReject, timeout: pendingTimeout }, pendingId) => {
|
|
207
|
+
if (pendingId !== id) {
|
|
208
|
+
clearTimeout(pendingTimeout);
|
|
209
|
+
pendingReject(new Error("Bridge reconnecting due to consecutive timeouts"));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
pendingRef.current.clear();
|
|
182
214
|
retryCountRef.current = 0;
|
|
183
215
|
isConnectingRef.current = false;
|
|
184
|
-
|
|
216
|
+
consecutiveTimeoutsRef.current = 0;
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
cleanup();
|
|
219
|
+
connect();
|
|
220
|
+
}, 100);
|
|
185
221
|
}
|
|
186
|
-
reject(
|
|
187
|
-
new Error(
|
|
188
|
-
`Request timed out after ${timeoutDuration} ms for key: ${key} with id: ${id}`
|
|
189
|
-
)
|
|
190
|
-
);
|
|
222
|
+
reject(new Error(errorMessage));
|
|
191
223
|
}
|
|
192
224
|
}, timeoutDuration);
|
|
193
225
|
pendingRef.current.set(id, { resolve, reject, timeout });
|
|
@@ -260,7 +292,9 @@ const BridgeProvider = ({
|
|
|
260
292
|
return false;
|
|
261
293
|
}
|
|
262
294
|
},
|
|
263
|
-
isConnected
|
|
295
|
+
get isConnected() {
|
|
296
|
+
return portRef.current !== null && status === "connected";
|
|
297
|
+
}
|
|
264
298
|
};
|
|
265
299
|
setBridge(bridgeInstance);
|
|
266
300
|
updateStatus("connected");
|
|
@@ -272,12 +306,27 @@ const BridgeProvider = ({
|
|
|
272
306
|
if (pingIntervalRef.current) {
|
|
273
307
|
clearInterval(pingIntervalRef.current);
|
|
274
308
|
}
|
|
309
|
+
consecutivePingFailuresRef.current = 0;
|
|
275
310
|
pingIntervalRef.current = setInterval(async () => {
|
|
276
311
|
if (bridgeInstance && portRef.current) {
|
|
277
312
|
const alive = await bridgeInstance.ping();
|
|
278
313
|
setIsServiceWorkerAlive(alive);
|
|
279
314
|
if (!alive) {
|
|
280
|
-
|
|
315
|
+
consecutivePingFailuresRef.current++;
|
|
316
|
+
console.warn(
|
|
317
|
+
`[Bridge] Service worker ping failed (${consecutivePingFailuresRef.current} consecutive failures)`
|
|
318
|
+
);
|
|
319
|
+
if (consecutivePingFailuresRef.current >= 2) {
|
|
320
|
+
console.warn("[Bridge] Auto-reconnecting due to unresponsive service worker...");
|
|
321
|
+
consecutivePingFailuresRef.current = 0;
|
|
322
|
+
retryCountRef.current = 0;
|
|
323
|
+
isConnectingRef.current = false;
|
|
324
|
+
updateStatus("reconnecting");
|
|
325
|
+
cleanup();
|
|
326
|
+
connect();
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
consecutivePingFailuresRef.current = 0;
|
|
281
330
|
}
|
|
282
331
|
}
|
|
283
332
|
}, pingInterval);
|
|
@@ -299,8 +348,36 @@ const BridgeProvider = ({
|
|
|
299
348
|
}, [connect, updateStatus]);
|
|
300
349
|
useEffect(() => {
|
|
301
350
|
connect();
|
|
302
|
-
|
|
303
|
-
|
|
351
|
+
const handleVisibilityChange = () => {
|
|
352
|
+
if (document.visibilityState === "visible") {
|
|
353
|
+
if (status === "disconnected" || status === "error") {
|
|
354
|
+
console.log("[Bridge] Tab became visible, attempting reconnection...");
|
|
355
|
+
retryCountRef.current = 0;
|
|
356
|
+
connect();
|
|
357
|
+
} else if (status === "connected" && bridge) {
|
|
358
|
+
bridge.ping().then((alive) => {
|
|
359
|
+
if (!alive) {
|
|
360
|
+
console.warn(
|
|
361
|
+
"[Bridge] Tab became visible but service worker unresponsive, reconnecting..."
|
|
362
|
+
);
|
|
363
|
+
retryCountRef.current = 0;
|
|
364
|
+
isConnectingRef.current = false;
|
|
365
|
+
cleanup();
|
|
366
|
+
connect();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
373
|
+
return () => {
|
|
374
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
375
|
+
if (maxRetryCooldownRef.current) {
|
|
376
|
+
clearTimeout(maxRetryCooldownRef.current);
|
|
377
|
+
}
|
|
378
|
+
cleanup();
|
|
379
|
+
};
|
|
380
|
+
}, [connect, cleanup, status, bridge]);
|
|
304
381
|
const contextValue = useMemo(
|
|
305
382
|
() => ({
|
|
306
383
|
bridge,
|
|
@@ -322,25 +399,141 @@ const useBridge = () => {
|
|
|
322
399
|
return context;
|
|
323
400
|
};
|
|
324
401
|
|
|
325
|
-
function useBridgeQuery(key, payload) {
|
|
326
|
-
const {
|
|
402
|
+
function useBridgeQuery(key, payload, options = {}) {
|
|
403
|
+
const { refetchOnReconnect = true, skip = false, timeout } = options;
|
|
404
|
+
const { bridge, status } = useBridge();
|
|
327
405
|
const [data, setData] = useState();
|
|
328
|
-
const [loading, setLoading] = useState(
|
|
406
|
+
const [loading, setLoading] = useState(!skip);
|
|
329
407
|
const [error, setError] = useState();
|
|
330
|
-
|
|
331
|
-
|
|
408
|
+
const prevStatusRef = useRef(status);
|
|
409
|
+
const fetchIdRef = useRef(0);
|
|
410
|
+
const executeQuery = useCallback(async () => {
|
|
411
|
+
if (skip) {
|
|
412
|
+
setLoading(false);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!bridge || !bridge.isConnected) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const currentFetchId = ++fetchIdRef.current;
|
|
332
419
|
setLoading(true);
|
|
333
|
-
|
|
334
|
-
|
|
420
|
+
setError(void 0);
|
|
421
|
+
try {
|
|
422
|
+
const res = await bridge.send(key, payload, timeout);
|
|
423
|
+
if (currentFetchId === fetchIdRef.current) {
|
|
424
|
+
setData(res);
|
|
425
|
+
setError(void 0);
|
|
426
|
+
}
|
|
427
|
+
} catch (e) {
|
|
428
|
+
if (currentFetchId === fetchIdRef.current) {
|
|
429
|
+
setError(e);
|
|
430
|
+
}
|
|
431
|
+
} finally {
|
|
432
|
+
if (currentFetchId === fetchIdRef.current) {
|
|
433
|
+
setLoading(false);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}, [bridge, key, payload, skip, timeout]);
|
|
437
|
+
useEffect(() => {
|
|
438
|
+
if (skip) {
|
|
335
439
|
setLoading(false);
|
|
336
440
|
return;
|
|
337
441
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
442
|
+
if (status === "connected" && bridge?.isConnected) {
|
|
443
|
+
executeQuery();
|
|
444
|
+
} else if (status === "connecting") {
|
|
445
|
+
setLoading(true);
|
|
446
|
+
} else if (status === "disconnected" || status === "error") {
|
|
447
|
+
setLoading(false);
|
|
448
|
+
}
|
|
449
|
+
}, [status, bridge?.isConnected, key, JSON.stringify(payload), skip, executeQuery]);
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
const wasDisconnected = prevStatusRef.current === "disconnected" || prevStatusRef.current === "error" || prevStatusRef.current === "reconnecting";
|
|
452
|
+
const isNowConnected = status === "connected";
|
|
453
|
+
if (refetchOnReconnect && wasDisconnected && isNowConnected && !skip) {
|
|
454
|
+
executeQuery();
|
|
455
|
+
}
|
|
456
|
+
prevStatusRef.current = status;
|
|
457
|
+
}, [status, refetchOnReconnect, skip, executeQuery]);
|
|
458
|
+
const refetch = useCallback(async () => {
|
|
459
|
+
await executeQuery();
|
|
460
|
+
}, [executeQuery]);
|
|
461
|
+
return { data, loading, error, refetch };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function useBridgeMutation(key, options = {}) {
|
|
465
|
+
const { timeout, onSuccess, onError, retries = 0, retryDelay = 1e3 } = options;
|
|
466
|
+
const { bridge, status } = useBridge();
|
|
467
|
+
const [data, setData] = useState();
|
|
468
|
+
const [loading, setLoading] = useState(false);
|
|
469
|
+
const [error, setError] = useState();
|
|
470
|
+
const mutationIdRef = useRef(0);
|
|
471
|
+
const mutate = useCallback(
|
|
472
|
+
async (payload) => {
|
|
473
|
+
if (!bridge) {
|
|
474
|
+
const err = new Error("Bridge is not initialized");
|
|
475
|
+
setError(err);
|
|
476
|
+
onError?.(err);
|
|
477
|
+
throw err;
|
|
478
|
+
}
|
|
479
|
+
if (!bridge.isConnected) {
|
|
480
|
+
const err = new Error("Bridge is not connected");
|
|
481
|
+
setError(err);
|
|
482
|
+
onError?.(err);
|
|
483
|
+
throw err;
|
|
484
|
+
}
|
|
485
|
+
const currentMutationId = ++mutationIdRef.current;
|
|
486
|
+
setLoading(true);
|
|
487
|
+
setError(void 0);
|
|
488
|
+
let lastError;
|
|
489
|
+
let attempts = 0;
|
|
490
|
+
while (attempts <= retries) {
|
|
491
|
+
try {
|
|
492
|
+
const result = await bridge.send(key, payload, timeout);
|
|
493
|
+
if (currentMutationId === mutationIdRef.current) {
|
|
494
|
+
setData(result);
|
|
495
|
+
setError(void 0);
|
|
496
|
+
setLoading(false);
|
|
497
|
+
onSuccess?.(result);
|
|
498
|
+
}
|
|
499
|
+
return result;
|
|
500
|
+
} catch (e) {
|
|
501
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
502
|
+
attempts++;
|
|
503
|
+
if (attempts <= retries && bridge.isConnected) {
|
|
504
|
+
console.warn(
|
|
505
|
+
`[Bridge] Mutation "${key}" failed (attempt ${attempts}/${retries + 1}), retrying in ${retryDelay}ms...`
|
|
506
|
+
);
|
|
507
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (currentMutationId === mutationIdRef.current) {
|
|
512
|
+
setError(lastError);
|
|
513
|
+
setLoading(false);
|
|
514
|
+
onError?.(lastError);
|
|
515
|
+
}
|
|
516
|
+
throw lastError;
|
|
517
|
+
},
|
|
518
|
+
[bridge, key, timeout, onSuccess, onError, retries, retryDelay]
|
|
519
|
+
);
|
|
520
|
+
const reset = useCallback(() => {
|
|
521
|
+
setData(void 0);
|
|
522
|
+
setError(void 0);
|
|
523
|
+
setLoading(false);
|
|
524
|
+
}, []);
|
|
525
|
+
return { mutate, data, loading, error, reset };
|
|
344
526
|
}
|
|
345
527
|
|
|
346
|
-
|
|
528
|
+
const useConnectionStatus = () => {
|
|
529
|
+
const context = useContext(BridgeContext);
|
|
530
|
+
return {
|
|
531
|
+
status: context?.status,
|
|
532
|
+
isConnected: context?.status === "connected",
|
|
533
|
+
isServiceWorkerAlive: context?.isServiceWorkerAlive ?? false,
|
|
534
|
+
reconnect: context?.reconnect,
|
|
535
|
+
error: context?.error
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
export { BridgeProvider, useBridge, useBridgeMutation, useBridgeQuery, useConnectionStatus };
|