@chromahq/react 1.0.45 → 1.0.46
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 +17 -0
- package/dist/index.js +332 -22
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,23 @@ interface Bridge {
|
|
|
36
36
|
* @param durationMs - How long to pause health checks in milliseconds
|
|
37
37
|
*/
|
|
38
38
|
pauseHealthChecks: (durationMs: number) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Ensure the service worker is connected and responsive before performing a heavy operation.
|
|
41
|
+
* This performs a quick ping and returns true if successful.
|
|
42
|
+
* On Windows/Brave, use this before crypto operations to verify the SW hasn't silently restarted.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const ready = await bridge.ensureConnected();
|
|
47
|
+
* if (!ready) {
|
|
48
|
+
* showToast('Connection lost. Please try again.');
|
|
49
|
+
* return;
|
|
50
|
+
* }
|
|
51
|
+
* bridge.pauseHealthChecks(30000);
|
|
52
|
+
* await bridge.send('unlock', { password });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
ensureConnected: () => Promise<boolean>;
|
|
39
56
|
}
|
|
40
57
|
interface BridgeContextValue {
|
|
41
58
|
bridge: Bridge | null;
|
package/dist/index.js
CHANGED
|
@@ -86,8 +86,22 @@ const CONFIG = {
|
|
|
86
86
|
SW_RESTART_MAX_DELAY: 5e3,
|
|
87
87
|
// Threshold for counting timeouts toward reconnection (only count fast timeouts as failures)
|
|
88
88
|
// Requests with timeout > this value are considered intentional long operations
|
|
89
|
-
TIMEOUT_FAILURE_THRESHOLD_MS: 3e4
|
|
89
|
+
TIMEOUT_FAILURE_THRESHOLD_MS: 3e4,
|
|
90
90
|
// Only count timeouts < 30s as potential SW issues
|
|
91
|
+
// Request queue settings for transparent retry
|
|
92
|
+
REQUEST_QUEUE_MAX_SIZE: 50,
|
|
93
|
+
// Maximum queued requests during disconnection
|
|
94
|
+
REQUEST_MAX_RETRIES: 3,
|
|
95
|
+
// Max retries per request before giving up
|
|
96
|
+
REQUEST_RETRY_BASE_DELAY: 200,
|
|
97
|
+
// Base delay for request retry backoff
|
|
98
|
+
REQUEST_RETRY_MAX_DELAY: 2e3,
|
|
99
|
+
// Max delay for request retry backoff
|
|
100
|
+
QUEUE_DRAIN_DELAY: 50,
|
|
101
|
+
// Delay between processing queued requests
|
|
102
|
+
// Optimistic health - don't surface unhealthy state immediately
|
|
103
|
+
HEALTH_GRACE_PERIOD_MS: 3e3
|
|
104
|
+
// Wait this long before showing unhealthy
|
|
91
105
|
};
|
|
92
106
|
const calculateBackoffDelay = (attempt, baseDelay, maxDelay) => Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
93
107
|
const clearTimeoutSafe = (ref) => {
|
|
@@ -111,17 +125,74 @@ const BridgeContext = createContext(null);
|
|
|
111
125
|
function createBridgeInstance(deps) {
|
|
112
126
|
const {
|
|
113
127
|
portRef,
|
|
128
|
+
portDisconnectedRef,
|
|
114
129
|
pendingRequestsRef,
|
|
130
|
+
requestQueueRef,
|
|
131
|
+
activeIdempotencyKeysRef,
|
|
115
132
|
eventListenersRef,
|
|
116
133
|
messageIdRef,
|
|
117
134
|
isConnectedRef,
|
|
135
|
+
isReconnectingRef,
|
|
118
136
|
consecutiveTimeoutsRef,
|
|
119
137
|
reconnectionGracePeriodRef,
|
|
120
138
|
healthPausedUntilRef,
|
|
121
139
|
defaultTimeout,
|
|
122
140
|
timeoutFailureThreshold,
|
|
123
|
-
onReconnectNeeded
|
|
124
|
-
|
|
141
|
+
onReconnectNeeded} = deps;
|
|
142
|
+
const isPortValid = () => {
|
|
143
|
+
if (!portRef.current) return false;
|
|
144
|
+
if (portDisconnectedRef.current) return false;
|
|
145
|
+
if (!isConnectedRef.current) return false;
|
|
146
|
+
return true;
|
|
147
|
+
};
|
|
148
|
+
const generateIdempotencyKey = (key, payload) => {
|
|
149
|
+
const isWriteOperation = !key.startsWith("get") && !key.startsWith("fetch") && key !== "__ping__";
|
|
150
|
+
if (!isWriteOperation) return "";
|
|
151
|
+
try {
|
|
152
|
+
return `${key}:${JSON.stringify(payload)}`;
|
|
153
|
+
} catch {
|
|
154
|
+
return `${key}:${Date.now()}`;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const queueRequest = (id, key, payload, timeoutDuration, resolve, reject, idempotencyKey) => {
|
|
158
|
+
if (key === "__ping__" || key === "__bridge_diagnostics__") {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (requestQueueRef.current.length >= CONFIG.REQUEST_QUEUE_MAX_SIZE) {
|
|
162
|
+
{
|
|
163
|
+
console.warn("[Bridge] Request queue full, rejecting request");
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (idempotencyKey && activeIdempotencyKeysRef.current.has(idempotencyKey)) {
|
|
168
|
+
{
|
|
169
|
+
console.log(`[Bridge] Duplicate request detected, skipping: ${key}`);
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
if (idempotencyKey) {
|
|
174
|
+
activeIdempotencyKeysRef.current.add(idempotencyKey);
|
|
175
|
+
}
|
|
176
|
+
const queuedRequest = {
|
|
177
|
+
id,
|
|
178
|
+
key,
|
|
179
|
+
payload,
|
|
180
|
+
timeoutDuration,
|
|
181
|
+
resolve,
|
|
182
|
+
reject,
|
|
183
|
+
retryCount: 0,
|
|
184
|
+
maxRetries: CONFIG.REQUEST_MAX_RETRIES,
|
|
185
|
+
queuedAt: Date.now(),
|
|
186
|
+
idempotencyKey
|
|
187
|
+
};
|
|
188
|
+
requestQueueRef.current.push(queuedRequest);
|
|
189
|
+
{
|
|
190
|
+
console.log(
|
|
191
|
+
`[Bridge] Request queued: ${key} (queue size: ${requestQueueRef.current.length})`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
};
|
|
125
196
|
const rejectAllPending = (message) => {
|
|
126
197
|
pendingRequestsRef.current.forEach(({ reject, timeout, timeoutDuration }, id) => {
|
|
127
198
|
if (timeoutDuration <= timeoutFailureThreshold) {
|
|
@@ -134,19 +205,29 @@ function createBridgeInstance(deps) {
|
|
|
134
205
|
const send = (key, payload, timeoutDuration = defaultTimeout) => {
|
|
135
206
|
return new Promise((resolve, reject) => {
|
|
136
207
|
const id = `msg_${++messageIdRef.current}`;
|
|
208
|
+
const idempotencyKey = generateIdempotencyKey(key, payload);
|
|
137
209
|
const finalizePendingWithError = (error) => {
|
|
138
210
|
const pending = pendingRequestsRef.current.get(id);
|
|
139
211
|
if (!pending) return;
|
|
140
212
|
clearTimeout(pending.timeout);
|
|
141
213
|
pendingRequestsRef.current.delete(id);
|
|
214
|
+
if (idempotencyKey) {
|
|
215
|
+
activeIdempotencyKeysRef.current.delete(idempotencyKey);
|
|
216
|
+
}
|
|
142
217
|
pending.reject(error instanceof Error ? error : new Error(error));
|
|
143
218
|
};
|
|
144
|
-
const triggerRuntimeFallback = (reason) => {
|
|
219
|
+
const triggerRuntimeFallback = (reason, retryCount = 0) => {
|
|
220
|
+
const MAX_FALLBACK_RETRIES = 2;
|
|
221
|
+
const FALLBACK_RETRY_DELAY = 300;
|
|
145
222
|
{
|
|
146
|
-
console.warn(
|
|
223
|
+
console.warn(
|
|
224
|
+
`[Bridge] Falling back to runtime.sendMessage (${reason})${retryCount > 0 ? ` [retry ${retryCount}]` : ""}`
|
|
225
|
+
);
|
|
147
226
|
}
|
|
148
227
|
recordDiagnostics.fallbackSent(reason);
|
|
149
|
-
|
|
228
|
+
if (retryCount === 0) {
|
|
229
|
+
onReconnectNeeded();
|
|
230
|
+
}
|
|
150
231
|
if (!chrome?.runtime?.sendMessage) {
|
|
151
232
|
const message = "chrome.runtime.sendMessage not available";
|
|
152
233
|
recordDiagnostics.fallbackError(message);
|
|
@@ -159,13 +240,24 @@ function createBridgeInstance(deps) {
|
|
|
159
240
|
id,
|
|
160
241
|
key,
|
|
161
242
|
payload,
|
|
162
|
-
metadata: { transport: "direct", fallbackReason: reason },
|
|
243
|
+
metadata: { transport: "direct", fallbackReason: reason, retryCount },
|
|
163
244
|
[DIRECT_MESSAGE_FLAG]: true
|
|
164
245
|
},
|
|
165
246
|
(response) => {
|
|
166
247
|
const runtimeError = consumeRuntimeError();
|
|
167
248
|
const pending = pendingRequestsRef.current.get(id);
|
|
168
249
|
if (!pending) return;
|
|
250
|
+
if (runtimeError?.includes("Receiving end does not exist") && retryCount < MAX_FALLBACK_RETRIES) {
|
|
251
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
252
|
+
console.warn(
|
|
253
|
+
`[Bridge] SW not ready, retrying fallback in ${FALLBACK_RETRY_DELAY}ms...`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
triggerRuntimeFallback(reason, retryCount + 1);
|
|
258
|
+
}, FALLBACK_RETRY_DELAY);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
169
261
|
if (runtimeError) {
|
|
170
262
|
recordDiagnostics.fallbackError(runtimeError);
|
|
171
263
|
clearTimeout(pending.timeout);
|
|
@@ -228,8 +320,28 @@ function createBridgeInstance(deps) {
|
|
|
228
320
|
timeout,
|
|
229
321
|
timeoutDuration
|
|
230
322
|
});
|
|
231
|
-
if (!
|
|
232
|
-
|
|
323
|
+
if (!isPortValid()) {
|
|
324
|
+
if (isReconnectingRef.current) {
|
|
325
|
+
clearTimeout(timeout);
|
|
326
|
+
pendingRequestsRef.current.delete(id);
|
|
327
|
+
const queued = queueRequest(
|
|
328
|
+
id,
|
|
329
|
+
key,
|
|
330
|
+
payload,
|
|
331
|
+
timeoutDuration,
|
|
332
|
+
resolve,
|
|
333
|
+
reject,
|
|
334
|
+
idempotencyKey
|
|
335
|
+
);
|
|
336
|
+
if (queued) {
|
|
337
|
+
{
|
|
338
|
+
console.log(`[Bridge] Request queued during reconnection: ${key}`);
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const reason = !portRef.current ? "port-unavailable" : portDisconnectedRef.current ? "port-disconnected" : "port-not-connected";
|
|
344
|
+
triggerRuntimeFallback(reason);
|
|
233
345
|
return;
|
|
234
346
|
}
|
|
235
347
|
try {
|
|
@@ -237,6 +349,23 @@ function createBridgeInstance(deps) {
|
|
|
237
349
|
setTimeout(() => {
|
|
238
350
|
const errorMessage = consumeRuntimeError();
|
|
239
351
|
if (errorMessage && pendingRequestsRef.current.has(id)) {
|
|
352
|
+
if (isReconnectingRef.current) {
|
|
353
|
+
const pending = pendingRequestsRef.current.get(id);
|
|
354
|
+
if (pending) {
|
|
355
|
+
clearTimeout(pending.timeout);
|
|
356
|
+
pendingRequestsRef.current.delete(id);
|
|
357
|
+
const queued = queueRequest(
|
|
358
|
+
id,
|
|
359
|
+
key,
|
|
360
|
+
payload,
|
|
361
|
+
timeoutDuration,
|
|
362
|
+
pending.resolve,
|
|
363
|
+
pending.reject,
|
|
364
|
+
idempotencyKey
|
|
365
|
+
);
|
|
366
|
+
if (queued) return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
240
369
|
finalizePendingWithError(errorMessage);
|
|
241
370
|
}
|
|
242
371
|
}, 0);
|
|
@@ -304,6 +433,25 @@ function createBridgeInstance(deps) {
|
|
|
304
433
|
{
|
|
305
434
|
console.log(`[Bridge] Health checks paused for ${Math.round(durationMs / 1e3)}s`);
|
|
306
435
|
}
|
|
436
|
+
},
|
|
437
|
+
ensureConnected: async () => {
|
|
438
|
+
if (!isPortValid()) {
|
|
439
|
+
{
|
|
440
|
+
console.warn("[Bridge] ensureConnected: Port invalid, triggering reconnect");
|
|
441
|
+
}
|
|
442
|
+
onReconnectNeeded();
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const QUICK_PING_TIMEOUT = 5e3;
|
|
446
|
+
try {
|
|
447
|
+
await send("__ping__", void 0, QUICK_PING_TIMEOUT);
|
|
448
|
+
return true;
|
|
449
|
+
} catch {
|
|
450
|
+
{
|
|
451
|
+
console.warn("[Bridge] ensureConnected: Ping failed, SW may be unresponsive");
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
307
455
|
}
|
|
308
456
|
};
|
|
309
457
|
return bridge;
|
|
@@ -387,8 +535,13 @@ const BridgeProvider = ({
|
|
|
387
535
|
const [error, setError] = useState(null);
|
|
388
536
|
const [isServiceWorkerAlive, setIsServiceWorkerAlive] = useState(false);
|
|
389
537
|
const portRef = useRef(null);
|
|
538
|
+
const portDisconnectedRef = useRef(false);
|
|
390
539
|
const isConnectedRef = useRef(false);
|
|
391
540
|
const isConnectingRef = useRef(false);
|
|
541
|
+
const isReconnectingRef = useRef(false);
|
|
542
|
+
const requestQueueRef = useRef([]);
|
|
543
|
+
const activeIdempotencyKeysRef = useRef(/* @__PURE__ */ new Set());
|
|
544
|
+
const queueDrainTimeoutRef = useRef(null);
|
|
392
545
|
const retryCountRef = useRef(0);
|
|
393
546
|
const reconnectTimeoutRef = useRef(null);
|
|
394
547
|
const maxRetryCooldownRef = useRef(null);
|
|
@@ -433,7 +586,7 @@ const BridgeProvider = ({
|
|
|
433
586
|
},
|
|
434
587
|
[onError, updateStatus]
|
|
435
588
|
);
|
|
436
|
-
const cleanup = useCallback((emitDisconnect = true) => {
|
|
589
|
+
const cleanup = useCallback((emitDisconnect = true, preserveQueue = false) => {
|
|
437
590
|
const wasConnected = isConnectedRef.current || portRef.current !== null;
|
|
438
591
|
if (emitDisconnect && wasConnected) {
|
|
439
592
|
eventListenersRef.current.get("bridge:disconnected")?.forEach((handler) => {
|
|
@@ -448,6 +601,7 @@ const BridgeProvider = ({
|
|
|
448
601
|
}
|
|
449
602
|
clearTimeoutSafe(reconnectTimeoutRef);
|
|
450
603
|
clearTimeoutSafe(triggerReconnectTimeoutRef);
|
|
604
|
+
clearTimeoutSafe(queueDrainTimeoutRef);
|
|
451
605
|
clearIntervalSafe(errorCheckIntervalRef);
|
|
452
606
|
clearIntervalSafe(pingIntervalRef);
|
|
453
607
|
if (portRef.current) {
|
|
@@ -457,11 +611,40 @@ const BridgeProvider = ({
|
|
|
457
611
|
}
|
|
458
612
|
portRef.current = null;
|
|
459
613
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
614
|
+
portDisconnectedRef.current = false;
|
|
615
|
+
if (preserveQueue) {
|
|
616
|
+
pendingRequestsRef.current.forEach(({ resolve, reject, timeout, timeoutDuration }, id) => {
|
|
617
|
+
clearTimeout(timeout);
|
|
618
|
+
const key = id.split("_")[0] || "unknown";
|
|
619
|
+
requestQueueRef.current.push({
|
|
620
|
+
id,
|
|
621
|
+
key,
|
|
622
|
+
payload: void 0,
|
|
623
|
+
// We don't have the payload anymore, but the request is already in flight
|
|
624
|
+
timeoutDuration,
|
|
625
|
+
resolve,
|
|
626
|
+
reject,
|
|
627
|
+
retryCount: 0,
|
|
628
|
+
maxRetries: CONFIG.REQUEST_MAX_RETRIES,
|
|
629
|
+
queuedAt: Date.now()
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
pendingRequestsRef.current.clear();
|
|
633
|
+
} else {
|
|
634
|
+
pendingRequestsRef.current.forEach(({ reject, timeout }) => {
|
|
635
|
+
clearTimeout(timeout);
|
|
636
|
+
reject(new Error("Bridge disconnected"));
|
|
637
|
+
});
|
|
638
|
+
pendingRequestsRef.current.clear();
|
|
639
|
+
requestQueueRef.current.forEach(({ reject, idempotencyKey }) => {
|
|
640
|
+
if (idempotencyKey) {
|
|
641
|
+
activeIdempotencyKeysRef.current.delete(idempotencyKey);
|
|
642
|
+
}
|
|
643
|
+
reject(new Error("Bridge disconnected"));
|
|
644
|
+
});
|
|
645
|
+
requestQueueRef.current = [];
|
|
646
|
+
activeIdempotencyKeysRef.current.clear();
|
|
647
|
+
}
|
|
465
648
|
if (!emitDisconnect) {
|
|
466
649
|
eventListenersRef.current.clear();
|
|
467
650
|
}
|
|
@@ -499,6 +682,86 @@ const BridgeProvider = ({
|
|
|
499
682
|
console.warn("[Bridge] Received response for unknown/expired request:", message.id);
|
|
500
683
|
}
|
|
501
684
|
}, []);
|
|
685
|
+
const drainRequestQueue = useCallback(() => {
|
|
686
|
+
if (requestQueueRef.current.length === 0) {
|
|
687
|
+
{
|
|
688
|
+
console.log("[Bridge] Request queue empty, nothing to drain");
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (!bridgeRef.current || !isConnectedRef.current) {
|
|
693
|
+
{
|
|
694
|
+
console.log("[Bridge] Cannot drain queue - not connected");
|
|
695
|
+
}
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
{
|
|
699
|
+
console.log(`[Bridge] Draining request queue (${requestQueueRef.current.length} requests)`);
|
|
700
|
+
}
|
|
701
|
+
const processNextRequest = () => {
|
|
702
|
+
if (!isMountedRef.current || !isConnectedRef.current) return;
|
|
703
|
+
const request = requestQueueRef.current.shift();
|
|
704
|
+
if (!request) {
|
|
705
|
+
{
|
|
706
|
+
console.log("[Bridge] Request queue drained successfully");
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const queuedDuration = Date.now() - request.queuedAt;
|
|
711
|
+
if (queuedDuration > request.timeoutDuration) {
|
|
712
|
+
{
|
|
713
|
+
console.warn(`[Bridge] Queued request expired: ${request.key}`);
|
|
714
|
+
}
|
|
715
|
+
if (request.idempotencyKey) {
|
|
716
|
+
activeIdempotencyKeysRef.current.delete(request.idempotencyKey);
|
|
717
|
+
}
|
|
718
|
+
request.reject(new Error(`Request expired while queued: ${request.key}`));
|
|
719
|
+
queueDrainTimeoutRef.current = setTimeout(processNextRequest, CONFIG.QUEUE_DRAIN_DELAY);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
{
|
|
723
|
+
console.log(
|
|
724
|
+
`[Bridge] Re-sending queued request: ${request.key} (retry ${request.retryCount + 1}/${request.maxRetries})`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
bridgeRef.current.send(request.key, request.payload, request.timeoutDuration - queuedDuration).then((data) => {
|
|
728
|
+
if (request.idempotencyKey) {
|
|
729
|
+
activeIdempotencyKeysRef.current.delete(request.idempotencyKey);
|
|
730
|
+
}
|
|
731
|
+
request.resolve(data);
|
|
732
|
+
}).catch((error2) => {
|
|
733
|
+
request.retryCount++;
|
|
734
|
+
if (request.retryCount < request.maxRetries && isConnectedRef.current) {
|
|
735
|
+
const retryDelay = calculateBackoffDelay(
|
|
736
|
+
request.retryCount,
|
|
737
|
+
CONFIG.REQUEST_RETRY_BASE_DELAY,
|
|
738
|
+
CONFIG.REQUEST_RETRY_MAX_DELAY
|
|
739
|
+
);
|
|
740
|
+
{
|
|
741
|
+
console.log(
|
|
742
|
+
`[Bridge] Request failed, re-queuing: ${request.key} (retry in ${retryDelay}ms)`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
setTimeout(() => {
|
|
746
|
+
if (isMountedRef.current) {
|
|
747
|
+
requestQueueRef.current.unshift(request);
|
|
748
|
+
processNextRequest();
|
|
749
|
+
}
|
|
750
|
+
}, retryDelay);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (request.idempotencyKey) {
|
|
754
|
+
activeIdempotencyKeysRef.current.delete(request.idempotencyKey);
|
|
755
|
+
}
|
|
756
|
+
request.reject(error2);
|
|
757
|
+
}).finally(() => {
|
|
758
|
+
if (isMountedRef.current) {
|
|
759
|
+
queueDrainTimeoutRef.current = setTimeout(processNextRequest, CONFIG.QUEUE_DRAIN_DELAY);
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
};
|
|
763
|
+
processNextRequest();
|
|
764
|
+
}, []);
|
|
502
765
|
const scheduleReconnect = useCallback(
|
|
503
766
|
(connectFn) => {
|
|
504
767
|
if (!isMountedRef.current) return;
|
|
@@ -546,6 +809,7 @@ const BridgeProvider = ({
|
|
|
546
809
|
}
|
|
547
810
|
return;
|
|
548
811
|
}
|
|
812
|
+
isReconnectingRef.current = true;
|
|
549
813
|
swRestartRetryCountRef.current++;
|
|
550
814
|
const delay = calculateBackoffDelay(
|
|
551
815
|
swRestartRetryCountRef.current,
|
|
@@ -578,7 +842,7 @@ const BridgeProvider = ({
|
|
|
578
842
|
return;
|
|
579
843
|
}
|
|
580
844
|
isConnectingRef.current = true;
|
|
581
|
-
cleanup(false);
|
|
845
|
+
cleanup(false, true);
|
|
582
846
|
if (!chrome?.runtime?.connect) {
|
|
583
847
|
handleError(new Error("Chrome runtime not available"));
|
|
584
848
|
isConnectingRef.current = false;
|
|
@@ -620,6 +884,7 @@ const BridgeProvider = ({
|
|
|
620
884
|
if (BRIDGE_ENABLE_LOGS) {
|
|
621
885
|
console.warn("[Bridge] *** onDisconnect FIRED ***");
|
|
622
886
|
}
|
|
887
|
+
portDisconnectedRef.current = true;
|
|
623
888
|
isConnectingRef.current = false;
|
|
624
889
|
const disconnectError = consumeRuntimeError();
|
|
625
890
|
recordDiagnostics.portDisconnect(disconnectError);
|
|
@@ -628,7 +893,7 @@ const BridgeProvider = ({
|
|
|
628
893
|
console.warn("[Bridge] isMounted:", isMountedRef.current);
|
|
629
894
|
}
|
|
630
895
|
updateStatus("disconnected");
|
|
631
|
-
cleanup();
|
|
896
|
+
cleanup(true, true);
|
|
632
897
|
if (isMountedRef.current) {
|
|
633
898
|
if (BRIDGE_ENABLE_LOGS) {
|
|
634
899
|
console.log("[Bridge] Scheduling SW restart reconnect...");
|
|
@@ -644,28 +909,36 @@ const BridgeProvider = ({
|
|
|
644
909
|
if (!isMountedRef.current) return;
|
|
645
910
|
setIsServiceWorkerAlive(false);
|
|
646
911
|
updateStatus("reconnecting");
|
|
912
|
+
isReconnectingRef.current = true;
|
|
647
913
|
retryCountRef.current = 0;
|
|
648
914
|
isConnectingRef.current = false;
|
|
649
915
|
clearTimeoutSafe(triggerReconnectTimeoutRef);
|
|
650
916
|
triggerReconnectTimeoutRef.current = setTimeout(() => {
|
|
651
917
|
if (!isMountedRef.current) return;
|
|
652
|
-
cleanup(false);
|
|
918
|
+
cleanup(false, true);
|
|
653
919
|
connect();
|
|
654
920
|
}, CONFIG.RECONNECT_DELAY);
|
|
655
921
|
};
|
|
656
922
|
const bridgeInstance = createBridgeInstance({
|
|
657
923
|
portRef,
|
|
924
|
+
portDisconnectedRef,
|
|
658
925
|
pendingRequestsRef,
|
|
926
|
+
requestQueueRef,
|
|
927
|
+
activeIdempotencyKeysRef,
|
|
659
928
|
eventListenersRef,
|
|
660
929
|
messageIdRef,
|
|
661
930
|
isConnectedRef,
|
|
931
|
+
isReconnectingRef,
|
|
662
932
|
consecutiveTimeoutsRef,
|
|
663
933
|
reconnectionGracePeriodRef,
|
|
664
934
|
healthPausedUntilRef,
|
|
665
935
|
defaultTimeout,
|
|
666
936
|
timeoutFailureThreshold,
|
|
667
|
-
onReconnectNeeded: triggerReconnect
|
|
937
|
+
onReconnectNeeded: triggerReconnect,
|
|
938
|
+
drainRequestQueue
|
|
668
939
|
});
|
|
940
|
+
portDisconnectedRef.current = false;
|
|
941
|
+
isReconnectingRef.current = false;
|
|
669
942
|
setBridge(bridgeInstance);
|
|
670
943
|
isConnectedRef.current = true;
|
|
671
944
|
updateStatus("connected");
|
|
@@ -675,6 +948,11 @@ const BridgeProvider = ({
|
|
|
675
948
|
swRestartRetryCountRef.current = 0;
|
|
676
949
|
consecutiveTimeoutsRef.current = 0;
|
|
677
950
|
isConnectingRef.current = false;
|
|
951
|
+
setTimeout(() => {
|
|
952
|
+
if (isMountedRef.current && isConnectedRef.current) {
|
|
953
|
+
drainRequestQueue();
|
|
954
|
+
}
|
|
955
|
+
}, 200);
|
|
678
956
|
reconnectionGracePeriodRef.current = true;
|
|
679
957
|
setTimeout(() => {
|
|
680
958
|
reconnectionGracePeriodRef.current = false;
|
|
@@ -723,6 +1001,7 @@ const BridgeProvider = ({
|
|
|
723
1001
|
handleMessage,
|
|
724
1002
|
scheduleReconnect,
|
|
725
1003
|
scheduleSwRestartReconnect,
|
|
1004
|
+
drainRequestQueue,
|
|
726
1005
|
defaultTimeout,
|
|
727
1006
|
updateStatus,
|
|
728
1007
|
pingInterval
|
|
@@ -980,15 +1259,19 @@ function broadcastHealthChange(status, isHealthy) {
|
|
|
980
1259
|
});
|
|
981
1260
|
}
|
|
982
1261
|
const ServiceWorkerHealthContext = createContext(null);
|
|
1262
|
+
const HEALTH_GRACE_PERIOD_MS = 3e3;
|
|
983
1263
|
const ServiceWorkerHealthProvider = ({
|
|
984
1264
|
children,
|
|
985
1265
|
onHealthChange
|
|
986
1266
|
}) => {
|
|
987
1267
|
const bridgeContext = useContext(BridgeContext);
|
|
988
1268
|
const [lastHealthyAt, setLastHealthyAt] = useState(null);
|
|
1269
|
+
const [unhealthyStartedAt, setUnhealthyStartedAt] = useState(null);
|
|
1270
|
+
const [graceExpired, setGraceExpired] = useState(false);
|
|
1271
|
+
const graceTimeoutRef = useRef(null);
|
|
989
1272
|
const bridgeStatus = bridgeContext?.status;
|
|
990
1273
|
const isServiceWorkerAlive = bridgeContext?.isServiceWorkerAlive ?? false;
|
|
991
|
-
const
|
|
1274
|
+
const rawStatus = useMemo(() => {
|
|
992
1275
|
if (!bridgeStatus) return "unknown";
|
|
993
1276
|
if (bridgeStatus === "connected" && isServiceWorkerAlive) {
|
|
994
1277
|
return "healthy";
|
|
@@ -998,14 +1281,41 @@ const ServiceWorkerHealthProvider = ({
|
|
|
998
1281
|
}
|
|
999
1282
|
return "unhealthy";
|
|
1000
1283
|
}, [bridgeStatus, isServiceWorkerAlive]);
|
|
1284
|
+
useEffect(() => {
|
|
1285
|
+
if (rawStatus === "healthy") {
|
|
1286
|
+
setUnhealthyStartedAt(null);
|
|
1287
|
+
setGraceExpired(false);
|
|
1288
|
+
if (graceTimeoutRef.current) {
|
|
1289
|
+
clearTimeout(graceTimeoutRef.current);
|
|
1290
|
+
graceTimeoutRef.current = null;
|
|
1291
|
+
}
|
|
1292
|
+
} else if (rawStatus !== "healthy" && !unhealthyStartedAt) {
|
|
1293
|
+
setUnhealthyStartedAt(Date.now());
|
|
1294
|
+
graceTimeoutRef.current = setTimeout(() => {
|
|
1295
|
+
setGraceExpired(true);
|
|
1296
|
+
}, HEALTH_GRACE_PERIOD_MS);
|
|
1297
|
+
}
|
|
1298
|
+
return () => {
|
|
1299
|
+
if (graceTimeoutRef.current) {
|
|
1300
|
+
clearTimeout(graceTimeoutRef.current);
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}, [rawStatus, unhealthyStartedAt]);
|
|
1304
|
+
const derivedStatus = useMemo(() => {
|
|
1305
|
+
if (rawStatus === "healthy") return "healthy";
|
|
1306
|
+
if (!graceExpired && lastHealthyAt) {
|
|
1307
|
+
return "healthy";
|
|
1308
|
+
}
|
|
1309
|
+
return rawStatus;
|
|
1310
|
+
}, [rawStatus, graceExpired, lastHealthyAt]);
|
|
1001
1311
|
const isHealthy = derivedStatus === "healthy";
|
|
1002
1312
|
const isRecovering = derivedStatus === "recovering";
|
|
1003
1313
|
const isLoading = derivedStatus === "recovering" || derivedStatus === "unknown";
|
|
1004
1314
|
useEffect(() => {
|
|
1005
|
-
if (
|
|
1315
|
+
if (rawStatus === "healthy") {
|
|
1006
1316
|
setLastHealthyAt(Date.now());
|
|
1007
1317
|
}
|
|
1008
|
-
}, [
|
|
1318
|
+
}, [rawStatus]);
|
|
1009
1319
|
useEffect(() => {
|
|
1010
1320
|
broadcastHealthChange(derivedStatus, isHealthy);
|
|
1011
1321
|
onHealthChange?.(derivedStatus, isHealthy);
|