@chromahq/react 1.0.45 → 1.0.47
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 +68 -1
- package/dist/index.js +572 -40
- package/package.json +7 -3
package/dist/index.d.ts
CHANGED
|
@@ -23,8 +23,58 @@ declare global {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error' | 'reconnecting';
|
|
26
|
+
/**
|
|
27
|
+
* Options for critical operations that need acknowledgment and deduplication.
|
|
28
|
+
*/
|
|
29
|
+
interface CriticalOperationOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Client-generated nonce for idempotency. If not provided, one will be generated.
|
|
32
|
+
* The SW should store processed nonces and reject duplicates.
|
|
33
|
+
*/
|
|
34
|
+
nonce?: string;
|
|
35
|
+
/**
|
|
36
|
+
* If true, the request will NOT be queued during disconnection - it will fail immediately.
|
|
37
|
+
* Use this for operations where you want explicit user retry rather than automatic retry.
|
|
38
|
+
* Default: false (requests are queued)
|
|
39
|
+
*/
|
|
40
|
+
noQueue?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Callback fired when SW acknowledges receipt of the request (before processing).
|
|
43
|
+
* Use this to update UI to show "processing" state.
|
|
44
|
+
*/
|
|
45
|
+
onAcknowledged?: () => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Result of a critical operation, including metadata about the request.
|
|
49
|
+
*/
|
|
50
|
+
interface CriticalOperationResult<T> {
|
|
51
|
+
data: T;
|
|
52
|
+
nonce: string;
|
|
53
|
+
acknowledged: boolean;
|
|
54
|
+
}
|
|
26
55
|
interface Bridge {
|
|
27
56
|
send: <Req = unknown, Res = unknown>(key: string, payload?: Req, timeoutDuration?: number) => Promise<Res>;
|
|
57
|
+
/**
|
|
58
|
+
* Send a critical operation that requires acknowledgment and idempotency.
|
|
59
|
+
* Use this for transfers, signing, and other non-idempotent operations.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const result = await bridge.sendCritical('transfer', {
|
|
64
|
+
* to: '0x...',
|
|
65
|
+
* amount: '1000000',
|
|
66
|
+
* }, {
|
|
67
|
+
* onAcknowledged: () => setStatus('processing'),
|
|
68
|
+
* noQueue: true, // Don't auto-retry transfers
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
/** Alias: clearer naming for nonce/idempotency semantics */
|
|
73
|
+
sendWithNonce: <Req = unknown, Res = unknown>(key: string, payload?: Req, options?: CriticalOperationOptions, timeoutDuration?: number) => Promise<CriticalOperationResult<Res>>;
|
|
74
|
+
/** Alias for callers that think in idempotency terms */
|
|
75
|
+
sendIdempotent: <Req = unknown, Res = unknown>(key: string, payload?: Req, options?: CriticalOperationOptions, timeoutDuration?: number) => Promise<CriticalOperationResult<Res>>;
|
|
76
|
+
/** Back-compat name (kept) */
|
|
77
|
+
sendCritical: <Req = unknown, Res = unknown>(key: string, payload?: Req, options?: CriticalOperationOptions, timeoutDuration?: number) => Promise<CriticalOperationResult<Res>>;
|
|
28
78
|
broadcast: (key: string, payload: unknown) => void;
|
|
29
79
|
on: (key: string, handler: (payload: unknown) => void) => void;
|
|
30
80
|
off: (key: string, handler: (payload: unknown) => void) => void;
|
|
@@ -36,6 +86,23 @@ interface Bridge {
|
|
|
36
86
|
* @param durationMs - How long to pause health checks in milliseconds
|
|
37
87
|
*/
|
|
38
88
|
pauseHealthChecks: (durationMs: number) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Ensure the service worker is connected and responsive before performing a heavy operation.
|
|
91
|
+
* This performs a quick ping and returns true if successful.
|
|
92
|
+
* On Windows/Brave, use this before crypto operations to verify the SW hasn't silently restarted.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* const ready = await bridge.ensureConnected();
|
|
97
|
+
* if (!ready) {
|
|
98
|
+
* showToast('Connection lost. Please try again.');
|
|
99
|
+
* return;
|
|
100
|
+
* }
|
|
101
|
+
* bridge.pauseHealthChecks(30000);
|
|
102
|
+
* await bridge.send('unlock', { password });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
ensureConnected: () => Promise<boolean>;
|
|
39
106
|
}
|
|
40
107
|
interface BridgeContextValue {
|
|
41
108
|
bridge: Bridge | null;
|
|
@@ -274,4 +341,4 @@ declare function useServiceWorkerHealthSimple(options?: ServiceWorkerHealthOptio
|
|
|
274
341
|
};
|
|
275
342
|
|
|
276
343
|
export { BridgeProvider, ServiceWorkerHealthProvider, getHealthStatus, subscribeToHealth, useBridge, useBridgeQuery, useConnectionStatus, useServiceWorkerHealth, useServiceWorkerHealthSimple };
|
|
277
|
-
export type { HealthStatus, ServiceWorkerHealthContextValue };
|
|
344
|
+
export type { CriticalOperationOptions, CriticalOperationResult, HealthStatus, ServiceWorkerHealthContextValue };
|
package/dist/index.js
CHANGED
|
@@ -67,18 +67,18 @@ const recordDiagnostics = {
|
|
|
67
67
|
const CONFIG = {
|
|
68
68
|
RETRY_AFTER: 1e3,
|
|
69
69
|
MAX_RETRIES: 10,
|
|
70
|
-
PING_INTERVAL:
|
|
71
|
-
// Check every
|
|
70
|
+
PING_INTERVAL: 5e3,
|
|
71
|
+
// Check every 5s for faster SW down detection
|
|
72
72
|
MAX_RETRY_COOLDOWN: 3e4,
|
|
73
73
|
DEFAULT_TIMEOUT: 6e4,
|
|
74
74
|
// 60s default for slow operations
|
|
75
75
|
MAX_RETRY_DELAY: 3e4,
|
|
76
|
-
PING_TIMEOUT:
|
|
77
|
-
// Give SW
|
|
76
|
+
PING_TIMEOUT: 8e3,
|
|
77
|
+
// Give SW 8s to respond to ping (reduced from 20s)
|
|
78
78
|
ERROR_CHECK_INTERVAL: 100,
|
|
79
79
|
MAX_ERROR_CHECKS: 10,
|
|
80
|
-
CONSECUTIVE_FAILURE_THRESHOLD:
|
|
81
|
-
// Require
|
|
80
|
+
CONSECUTIVE_FAILURE_THRESHOLD: 2,
|
|
81
|
+
// Require 2 consecutive failures (~10s total) before reconnecting
|
|
82
82
|
RECONNECT_DELAY: 100,
|
|
83
83
|
PORT_NAME: "chroma-bridge",
|
|
84
84
|
// Service worker restart retry settings (indefinite retries)
|
|
@@ -86,8 +86,31 @@ 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: 1e3,
|
|
104
|
+
// Wait this long before showing unhealthy (reduced from 3s)
|
|
105
|
+
// Critical operation settings
|
|
106
|
+
CRITICAL_OP_TIMEOUT: 12e4
|
|
107
|
+
// 2 minutes for critical operations (transfers, signing)
|
|
108
|
+
};
|
|
109
|
+
const generateNonce = () => {
|
|
110
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
111
|
+
return crypto.randomUUID();
|
|
112
|
+
}
|
|
113
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
91
114
|
};
|
|
92
115
|
const calculateBackoffDelay = (attempt, baseDelay, maxDelay) => Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
93
116
|
const clearTimeoutSafe = (ref) => {
|
|
@@ -111,17 +134,74 @@ const BridgeContext = createContext(null);
|
|
|
111
134
|
function createBridgeInstance(deps) {
|
|
112
135
|
const {
|
|
113
136
|
portRef,
|
|
137
|
+
portDisconnectedRef,
|
|
114
138
|
pendingRequestsRef,
|
|
139
|
+
requestQueueRef,
|
|
140
|
+
activeIdempotencyKeysRef,
|
|
115
141
|
eventListenersRef,
|
|
116
142
|
messageIdRef,
|
|
117
143
|
isConnectedRef,
|
|
144
|
+
isReconnectingRef,
|
|
118
145
|
consecutiveTimeoutsRef,
|
|
119
146
|
reconnectionGracePeriodRef,
|
|
120
147
|
healthPausedUntilRef,
|
|
121
148
|
defaultTimeout,
|
|
122
149
|
timeoutFailureThreshold,
|
|
123
|
-
onReconnectNeeded
|
|
124
|
-
|
|
150
|
+
onReconnectNeeded} = deps;
|
|
151
|
+
const isPortValid = () => {
|
|
152
|
+
if (!portRef.current) return false;
|
|
153
|
+
if (portDisconnectedRef.current) return false;
|
|
154
|
+
if (!isConnectedRef.current) return false;
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
const generateIdempotencyKey = (key, payload) => {
|
|
158
|
+
const isWriteOperation = !key.startsWith("get") && !key.startsWith("fetch") && key !== "__ping__";
|
|
159
|
+
if (!isWriteOperation) return "";
|
|
160
|
+
try {
|
|
161
|
+
return `${key}:${JSON.stringify(payload)}`;
|
|
162
|
+
} catch {
|
|
163
|
+
return `${key}:${Date.now()}`;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const queueRequest = (id, key, payload, timeoutDuration, resolve, reject, idempotencyKey) => {
|
|
167
|
+
if (key === "__ping__" || key === "__bridge_diagnostics__") {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (requestQueueRef.current.length >= CONFIG.REQUEST_QUEUE_MAX_SIZE) {
|
|
171
|
+
{
|
|
172
|
+
console.warn("[Bridge] Request queue full, rejecting request");
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
if (idempotencyKey && activeIdempotencyKeysRef.current.has(idempotencyKey)) {
|
|
177
|
+
{
|
|
178
|
+
console.log(`[Bridge] Duplicate request detected, skipping: ${key}`);
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (idempotencyKey) {
|
|
183
|
+
activeIdempotencyKeysRef.current.add(idempotencyKey);
|
|
184
|
+
}
|
|
185
|
+
const queuedRequest = {
|
|
186
|
+
id,
|
|
187
|
+
key,
|
|
188
|
+
payload,
|
|
189
|
+
timeoutDuration,
|
|
190
|
+
resolve,
|
|
191
|
+
reject,
|
|
192
|
+
retryCount: 0,
|
|
193
|
+
maxRetries: CONFIG.REQUEST_MAX_RETRIES,
|
|
194
|
+
queuedAt: Date.now(),
|
|
195
|
+
idempotencyKey
|
|
196
|
+
};
|
|
197
|
+
requestQueueRef.current.push(queuedRequest);
|
|
198
|
+
{
|
|
199
|
+
console.log(
|
|
200
|
+
`[Bridge] Request queued: ${key} (queue size: ${requestQueueRef.current.length})`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
};
|
|
125
205
|
const rejectAllPending = (message) => {
|
|
126
206
|
pendingRequestsRef.current.forEach(({ reject, timeout, timeoutDuration }, id) => {
|
|
127
207
|
if (timeoutDuration <= timeoutFailureThreshold) {
|
|
@@ -134,19 +214,29 @@ function createBridgeInstance(deps) {
|
|
|
134
214
|
const send = (key, payload, timeoutDuration = defaultTimeout) => {
|
|
135
215
|
return new Promise((resolve, reject) => {
|
|
136
216
|
const id = `msg_${++messageIdRef.current}`;
|
|
217
|
+
const idempotencyKey = generateIdempotencyKey(key, payload);
|
|
137
218
|
const finalizePendingWithError = (error) => {
|
|
138
219
|
const pending = pendingRequestsRef.current.get(id);
|
|
139
220
|
if (!pending) return;
|
|
140
221
|
clearTimeout(pending.timeout);
|
|
141
222
|
pendingRequestsRef.current.delete(id);
|
|
223
|
+
if (idempotencyKey) {
|
|
224
|
+
activeIdempotencyKeysRef.current.delete(idempotencyKey);
|
|
225
|
+
}
|
|
142
226
|
pending.reject(error instanceof Error ? error : new Error(error));
|
|
143
227
|
};
|
|
144
|
-
const triggerRuntimeFallback = (reason) => {
|
|
228
|
+
const triggerRuntimeFallback = (reason, retryCount = 0) => {
|
|
229
|
+
const MAX_FALLBACK_RETRIES = 2;
|
|
230
|
+
const FALLBACK_RETRY_DELAY = 300;
|
|
145
231
|
{
|
|
146
|
-
console.warn(
|
|
232
|
+
console.warn(
|
|
233
|
+
`[Bridge] Falling back to runtime.sendMessage (${reason})${retryCount > 0 ? ` [retry ${retryCount}]` : ""}`
|
|
234
|
+
);
|
|
147
235
|
}
|
|
148
236
|
recordDiagnostics.fallbackSent(reason);
|
|
149
|
-
|
|
237
|
+
if (retryCount === 0) {
|
|
238
|
+
onReconnectNeeded();
|
|
239
|
+
}
|
|
150
240
|
if (!chrome?.runtime?.sendMessage) {
|
|
151
241
|
const message = "chrome.runtime.sendMessage not available";
|
|
152
242
|
recordDiagnostics.fallbackError(message);
|
|
@@ -159,13 +249,24 @@ function createBridgeInstance(deps) {
|
|
|
159
249
|
id,
|
|
160
250
|
key,
|
|
161
251
|
payload,
|
|
162
|
-
metadata: { transport: "direct", fallbackReason: reason },
|
|
252
|
+
metadata: { transport: "direct", fallbackReason: reason, retryCount },
|
|
163
253
|
[DIRECT_MESSAGE_FLAG]: true
|
|
164
254
|
},
|
|
165
255
|
(response) => {
|
|
166
256
|
const runtimeError = consumeRuntimeError();
|
|
167
257
|
const pending = pendingRequestsRef.current.get(id);
|
|
168
258
|
if (!pending) return;
|
|
259
|
+
if (runtimeError?.includes("Receiving end does not exist") && retryCount < MAX_FALLBACK_RETRIES) {
|
|
260
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
261
|
+
console.warn(
|
|
262
|
+
`[Bridge] SW not ready, retrying fallback in ${FALLBACK_RETRY_DELAY}ms...`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
triggerRuntimeFallback(reason, retryCount + 1);
|
|
267
|
+
}, FALLBACK_RETRY_DELAY);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
169
270
|
if (runtimeError) {
|
|
170
271
|
recordDiagnostics.fallbackError(runtimeError);
|
|
171
272
|
clearTimeout(pending.timeout);
|
|
@@ -226,10 +327,32 @@ function createBridgeInstance(deps) {
|
|
|
226
327
|
resolve,
|
|
227
328
|
reject,
|
|
228
329
|
timeout,
|
|
229
|
-
timeoutDuration
|
|
330
|
+
timeoutDuration,
|
|
331
|
+
key,
|
|
332
|
+
payload
|
|
230
333
|
});
|
|
231
|
-
if (!
|
|
232
|
-
|
|
334
|
+
if (!isPortValid()) {
|
|
335
|
+
if (isReconnectingRef.current) {
|
|
336
|
+
clearTimeout(timeout);
|
|
337
|
+
pendingRequestsRef.current.delete(id);
|
|
338
|
+
const queued = queueRequest(
|
|
339
|
+
id,
|
|
340
|
+
key,
|
|
341
|
+
payload,
|
|
342
|
+
timeoutDuration,
|
|
343
|
+
resolve,
|
|
344
|
+
reject,
|
|
345
|
+
idempotencyKey
|
|
346
|
+
);
|
|
347
|
+
if (queued) {
|
|
348
|
+
{
|
|
349
|
+
console.log(`[Bridge] Request queued during reconnection: ${key}`);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const reason = !portRef.current ? "port-unavailable" : portDisconnectedRef.current ? "port-disconnected" : "port-not-connected";
|
|
355
|
+
triggerRuntimeFallback(reason);
|
|
233
356
|
return;
|
|
234
357
|
}
|
|
235
358
|
try {
|
|
@@ -237,6 +360,23 @@ function createBridgeInstance(deps) {
|
|
|
237
360
|
setTimeout(() => {
|
|
238
361
|
const errorMessage = consumeRuntimeError();
|
|
239
362
|
if (errorMessage && pendingRequestsRef.current.has(id)) {
|
|
363
|
+
if (isReconnectingRef.current) {
|
|
364
|
+
const pending = pendingRequestsRef.current.get(id);
|
|
365
|
+
if (pending) {
|
|
366
|
+
clearTimeout(pending.timeout);
|
|
367
|
+
pendingRequestsRef.current.delete(id);
|
|
368
|
+
const queued = queueRequest(
|
|
369
|
+
id,
|
|
370
|
+
key,
|
|
371
|
+
payload,
|
|
372
|
+
timeoutDuration,
|
|
373
|
+
pending.resolve,
|
|
374
|
+
pending.reject,
|
|
375
|
+
idempotencyKey
|
|
376
|
+
);
|
|
377
|
+
if (queued) return;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
240
380
|
finalizePendingWithError(errorMessage);
|
|
241
381
|
}
|
|
242
382
|
}, 0);
|
|
@@ -304,7 +444,86 @@ function createBridgeInstance(deps) {
|
|
|
304
444
|
{
|
|
305
445
|
console.log(`[Bridge] Health checks paused for ${Math.round(durationMs / 1e3)}s`);
|
|
306
446
|
}
|
|
307
|
-
}
|
|
447
|
+
},
|
|
448
|
+
ensureConnected: async () => {
|
|
449
|
+
if (!isPortValid()) {
|
|
450
|
+
{
|
|
451
|
+
console.warn("[Bridge] ensureConnected: Port invalid, triggering reconnect");
|
|
452
|
+
}
|
|
453
|
+
onReconnectNeeded();
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
const QUICK_PING_TIMEOUT = 5e3;
|
|
457
|
+
try {
|
|
458
|
+
await send("__ping__", void 0, QUICK_PING_TIMEOUT);
|
|
459
|
+
return true;
|
|
460
|
+
} catch {
|
|
461
|
+
{
|
|
462
|
+
console.warn("[Bridge] ensureConnected: Ping failed, SW may be unresponsive");
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
sendCritical: async (key, payload, options, timeoutDuration = CONFIG.CRITICAL_OP_TIMEOUT) => {
|
|
468
|
+
const nonce = options?.nonce || generateNonce();
|
|
469
|
+
const noQueue = options?.noQueue ?? false;
|
|
470
|
+
if (noQueue && !isPortValid()) {
|
|
471
|
+
throw new Error("Not connected. Please try again.");
|
|
472
|
+
}
|
|
473
|
+
const criticalPayload = {
|
|
474
|
+
__critical__: true,
|
|
475
|
+
__nonce__: nonce,
|
|
476
|
+
__timestamp__: Date.now(),
|
|
477
|
+
data: payload
|
|
478
|
+
};
|
|
479
|
+
let acknowledged = false;
|
|
480
|
+
const ackKey = `__ack__:${nonce}`;
|
|
481
|
+
const ackPromise = new Promise((resolveAck) => {
|
|
482
|
+
const ackHandler = () => {
|
|
483
|
+
acknowledged = true;
|
|
484
|
+
options?.onAcknowledged?.();
|
|
485
|
+
resolveAck();
|
|
486
|
+
off(ackKey, ackHandler);
|
|
487
|
+
};
|
|
488
|
+
on(ackKey, ackHandler);
|
|
489
|
+
setTimeout(() => {
|
|
490
|
+
off(ackKey, ackHandler);
|
|
491
|
+
resolveAck();
|
|
492
|
+
}, 5e3);
|
|
493
|
+
});
|
|
494
|
+
{
|
|
495
|
+
console.log(`[Bridge] Sending critical operation: ${key} (nonce: ${nonce})`);
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
const [data] = await Promise.all([
|
|
499
|
+
send(key, criticalPayload, timeoutDuration),
|
|
500
|
+
ackPromise
|
|
501
|
+
]);
|
|
502
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
503
|
+
console.log(
|
|
504
|
+
`[Bridge] Critical operation completed: ${key} (nonce: ${nonce}, acked: ${acknowledged})`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
data,
|
|
509
|
+
nonce,
|
|
510
|
+
acknowledged
|
|
511
|
+
};
|
|
512
|
+
} catch (error) {
|
|
513
|
+
{
|
|
514
|
+
console.error(
|
|
515
|
+
`[Bridge] Critical operation failed: ${key} (nonce: ${nonce}, acked: ${acknowledged})`,
|
|
516
|
+
error
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
520
|
+
err.nonce = nonce;
|
|
521
|
+
err.acknowledged = acknowledged;
|
|
522
|
+
throw err;
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
sendWithNonce: async (key, payload, options, timeoutDuration) => bridge.sendCritical(key, payload, options, timeoutDuration),
|
|
526
|
+
sendIdempotent: async (key, payload, options, timeoutDuration) => bridge.sendCritical(key, payload, options, timeoutDuration)
|
|
308
527
|
};
|
|
309
528
|
return bridge;
|
|
310
529
|
}
|
|
@@ -387,8 +606,13 @@ const BridgeProvider = ({
|
|
|
387
606
|
const [error, setError] = useState(null);
|
|
388
607
|
const [isServiceWorkerAlive, setIsServiceWorkerAlive] = useState(false);
|
|
389
608
|
const portRef = useRef(null);
|
|
609
|
+
const portDisconnectedRef = useRef(false);
|
|
390
610
|
const isConnectedRef = useRef(false);
|
|
391
611
|
const isConnectingRef = useRef(false);
|
|
612
|
+
const isReconnectingRef = useRef(false);
|
|
613
|
+
const requestQueueRef = useRef([]);
|
|
614
|
+
const activeIdempotencyKeysRef = useRef(/* @__PURE__ */ new Set());
|
|
615
|
+
const queueDrainTimeoutRef = useRef(null);
|
|
392
616
|
const retryCountRef = useRef(0);
|
|
393
617
|
const reconnectTimeoutRef = useRef(null);
|
|
394
618
|
const maxRetryCooldownRef = useRef(null);
|
|
@@ -433,7 +657,7 @@ const BridgeProvider = ({
|
|
|
433
657
|
},
|
|
434
658
|
[onError, updateStatus]
|
|
435
659
|
);
|
|
436
|
-
const cleanup = useCallback((emitDisconnect = true) => {
|
|
660
|
+
const cleanup = useCallback((emitDisconnect = true, preserveQueue = false) => {
|
|
437
661
|
const wasConnected = isConnectedRef.current || portRef.current !== null;
|
|
438
662
|
if (emitDisconnect && wasConnected) {
|
|
439
663
|
eventListenersRef.current.get("bridge:disconnected")?.forEach((handler) => {
|
|
@@ -448,6 +672,7 @@ const BridgeProvider = ({
|
|
|
448
672
|
}
|
|
449
673
|
clearTimeoutSafe(reconnectTimeoutRef);
|
|
450
674
|
clearTimeoutSafe(triggerReconnectTimeoutRef);
|
|
675
|
+
clearTimeoutSafe(queueDrainTimeoutRef);
|
|
451
676
|
clearIntervalSafe(errorCheckIntervalRef);
|
|
452
677
|
clearIntervalSafe(pingIntervalRef);
|
|
453
678
|
if (portRef.current) {
|
|
@@ -457,11 +682,50 @@ const BridgeProvider = ({
|
|
|
457
682
|
}
|
|
458
683
|
portRef.current = null;
|
|
459
684
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
685
|
+
portDisconnectedRef.current = false;
|
|
686
|
+
if (preserveQueue) {
|
|
687
|
+
const pendingCount = pendingRequestsRef.current.size;
|
|
688
|
+
if (pendingCount > 0 && BRIDGE_ENABLE_LOGS) {
|
|
689
|
+
console.log(
|
|
690
|
+
`[Bridge] Preserving ${pendingCount} pending requests for retry after reconnect`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
pendingRequestsRef.current.forEach(
|
|
694
|
+
({ resolve, reject, timeout, timeoutDuration, key, payload }, id) => {
|
|
695
|
+
clearTimeout(timeout);
|
|
696
|
+
requestQueueRef.current.push({
|
|
697
|
+
id,
|
|
698
|
+
key,
|
|
699
|
+
payload,
|
|
700
|
+
// Preserve the original payload for retry
|
|
701
|
+
timeoutDuration,
|
|
702
|
+
resolve,
|
|
703
|
+
reject,
|
|
704
|
+
retryCount: 0,
|
|
705
|
+
maxRetries: CONFIG.REQUEST_MAX_RETRIES,
|
|
706
|
+
queuedAt: Date.now()
|
|
707
|
+
});
|
|
708
|
+
{
|
|
709
|
+
console.log(`[Bridge] Queued pending request for retry: ${key}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
);
|
|
713
|
+
pendingRequestsRef.current.clear();
|
|
714
|
+
} else {
|
|
715
|
+
pendingRequestsRef.current.forEach(({ reject, timeout }) => {
|
|
716
|
+
clearTimeout(timeout);
|
|
717
|
+
reject(new Error("Bridge disconnected"));
|
|
718
|
+
});
|
|
719
|
+
pendingRequestsRef.current.clear();
|
|
720
|
+
requestQueueRef.current.forEach(({ reject, idempotencyKey }) => {
|
|
721
|
+
if (idempotencyKey) {
|
|
722
|
+
activeIdempotencyKeysRef.current.delete(idempotencyKey);
|
|
723
|
+
}
|
|
724
|
+
reject(new Error("Bridge disconnected"));
|
|
725
|
+
});
|
|
726
|
+
requestQueueRef.current = [];
|
|
727
|
+
activeIdempotencyKeysRef.current.clear();
|
|
728
|
+
}
|
|
465
729
|
if (!emitDisconnect) {
|
|
466
730
|
eventListenersRef.current.clear();
|
|
467
731
|
}
|
|
@@ -484,7 +748,14 @@ const BridgeProvider = ({
|
|
|
484
748
|
return;
|
|
485
749
|
}
|
|
486
750
|
if (message.type === "broadcast" && message.key) {
|
|
487
|
-
eventListenersRef.current.get(message.key)
|
|
751
|
+
const listeners = eventListenersRef.current.get(message.key);
|
|
752
|
+
const listenerCount = listeners?.size ?? 0;
|
|
753
|
+
{
|
|
754
|
+
console.log(
|
|
755
|
+
`[Bridge] \u{1F4E1} Received broadcast: ${message.key}, dispatching to ${listenerCount} listeners`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
listeners?.forEach((handler) => {
|
|
488
759
|
try {
|
|
489
760
|
handler(message.payload);
|
|
490
761
|
} catch (err) {
|
|
@@ -499,6 +770,95 @@ const BridgeProvider = ({
|
|
|
499
770
|
console.warn("[Bridge] Received response for unknown/expired request:", message.id);
|
|
500
771
|
}
|
|
501
772
|
}, []);
|
|
773
|
+
const drainRequestQueue = useCallback(() => {
|
|
774
|
+
if (requestQueueRef.current.length === 0) {
|
|
775
|
+
{
|
|
776
|
+
console.log("[Bridge] Request queue empty, nothing to drain");
|
|
777
|
+
}
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (!bridgeRef.current || !isConnectedRef.current) {
|
|
781
|
+
{
|
|
782
|
+
console.log("[Bridge] Cannot drain queue - not connected");
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
{
|
|
787
|
+
console.log(`[Bridge] Draining request queue (${requestQueueRef.current.length} requests)`);
|
|
788
|
+
}
|
|
789
|
+
const processNextRequest = () => {
|
|
790
|
+
if (!isMountedRef.current || !isConnectedRef.current) return;
|
|
791
|
+
const request = requestQueueRef.current.shift();
|
|
792
|
+
if (!request) {
|
|
793
|
+
{
|
|
794
|
+
console.log("[Bridge] Request queue drained successfully");
|
|
795
|
+
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const queuedDuration = Date.now() - request.queuedAt;
|
|
799
|
+
if (queuedDuration > request.timeoutDuration) {
|
|
800
|
+
{
|
|
801
|
+
console.warn(`[Bridge] Queued request expired: ${request.key}`);
|
|
802
|
+
}
|
|
803
|
+
if (request.idempotencyKey) {
|
|
804
|
+
activeIdempotencyKeysRef.current.delete(request.idempotencyKey);
|
|
805
|
+
}
|
|
806
|
+
request.reject(new Error(`Request expired while queued: ${request.key}`));
|
|
807
|
+
queueDrainTimeoutRef.current = setTimeout(processNextRequest, CONFIG.QUEUE_DRAIN_DELAY);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
{
|
|
811
|
+
console.log(
|
|
812
|
+
`[Bridge] Re-sending queued request: ${request.key} (retry ${request.retryCount + 1}/${request.maxRetries})`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
bridgeRef.current.send(request.key, request.payload, request.timeoutDuration - queuedDuration).then((data) => {
|
|
816
|
+
{
|
|
817
|
+
console.log(`[Bridge] \u2705 Queued request succeeded: ${request.key}`);
|
|
818
|
+
}
|
|
819
|
+
if (request.idempotencyKey) {
|
|
820
|
+
activeIdempotencyKeysRef.current.delete(request.idempotencyKey);
|
|
821
|
+
}
|
|
822
|
+
request.resolve(data);
|
|
823
|
+
}).catch((error2) => {
|
|
824
|
+
request.retryCount++;
|
|
825
|
+
if (request.retryCount < request.maxRetries && isConnectedRef.current) {
|
|
826
|
+
const retryDelay = calculateBackoffDelay(
|
|
827
|
+
request.retryCount,
|
|
828
|
+
CONFIG.REQUEST_RETRY_BASE_DELAY,
|
|
829
|
+
CONFIG.REQUEST_RETRY_MAX_DELAY
|
|
830
|
+
);
|
|
831
|
+
{
|
|
832
|
+
console.log(
|
|
833
|
+
`[Bridge] Request failed, re-queuing: ${request.key} (retry in ${retryDelay}ms)`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
setTimeout(() => {
|
|
837
|
+
if (isMountedRef.current) {
|
|
838
|
+
requestQueueRef.current.unshift(request);
|
|
839
|
+
processNextRequest();
|
|
840
|
+
}
|
|
841
|
+
}, retryDelay);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
{
|
|
845
|
+
console.error(
|
|
846
|
+
`[Bridge] \u274C Queued request failed after ${request.maxRetries} retries: ${request.key}`,
|
|
847
|
+
error2
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (request.idempotencyKey) {
|
|
851
|
+
activeIdempotencyKeysRef.current.delete(request.idempotencyKey);
|
|
852
|
+
}
|
|
853
|
+
request.reject(error2);
|
|
854
|
+
}).finally(() => {
|
|
855
|
+
if (isMountedRef.current) {
|
|
856
|
+
queueDrainTimeoutRef.current = setTimeout(processNextRequest, CONFIG.QUEUE_DRAIN_DELAY);
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
};
|
|
860
|
+
processNextRequest();
|
|
861
|
+
}, []);
|
|
502
862
|
const scheduleReconnect = useCallback(
|
|
503
863
|
(connectFn) => {
|
|
504
864
|
if (!isMountedRef.current) return;
|
|
@@ -546,6 +906,7 @@ const BridgeProvider = ({
|
|
|
546
906
|
}
|
|
547
907
|
return;
|
|
548
908
|
}
|
|
909
|
+
isReconnectingRef.current = true;
|
|
549
910
|
swRestartRetryCountRef.current++;
|
|
550
911
|
const delay = calculateBackoffDelay(
|
|
551
912
|
swRestartRetryCountRef.current,
|
|
@@ -578,7 +939,7 @@ const BridgeProvider = ({
|
|
|
578
939
|
return;
|
|
579
940
|
}
|
|
580
941
|
isConnectingRef.current = true;
|
|
581
|
-
cleanup(false);
|
|
942
|
+
cleanup(false, true);
|
|
582
943
|
if (!chrome?.runtime?.connect) {
|
|
583
944
|
handleError(new Error("Chrome runtime not available"));
|
|
584
945
|
isConnectingRef.current = false;
|
|
@@ -620,6 +981,7 @@ const BridgeProvider = ({
|
|
|
620
981
|
if (BRIDGE_ENABLE_LOGS) {
|
|
621
982
|
console.warn("[Bridge] *** onDisconnect FIRED ***");
|
|
622
983
|
}
|
|
984
|
+
portDisconnectedRef.current = true;
|
|
623
985
|
isConnectingRef.current = false;
|
|
624
986
|
const disconnectError = consumeRuntimeError();
|
|
625
987
|
recordDiagnostics.portDisconnect(disconnectError);
|
|
@@ -628,7 +990,7 @@ const BridgeProvider = ({
|
|
|
628
990
|
console.warn("[Bridge] isMounted:", isMountedRef.current);
|
|
629
991
|
}
|
|
630
992
|
updateStatus("disconnected");
|
|
631
|
-
cleanup();
|
|
993
|
+
cleanup(true, true);
|
|
632
994
|
if (isMountedRef.current) {
|
|
633
995
|
if (BRIDGE_ENABLE_LOGS) {
|
|
634
996
|
console.log("[Bridge] Scheduling SW restart reconnect...");
|
|
@@ -644,28 +1006,36 @@ const BridgeProvider = ({
|
|
|
644
1006
|
if (!isMountedRef.current) return;
|
|
645
1007
|
setIsServiceWorkerAlive(false);
|
|
646
1008
|
updateStatus("reconnecting");
|
|
1009
|
+
isReconnectingRef.current = true;
|
|
647
1010
|
retryCountRef.current = 0;
|
|
648
1011
|
isConnectingRef.current = false;
|
|
649
1012
|
clearTimeoutSafe(triggerReconnectTimeoutRef);
|
|
650
1013
|
triggerReconnectTimeoutRef.current = setTimeout(() => {
|
|
651
1014
|
if (!isMountedRef.current) return;
|
|
652
|
-
cleanup(false);
|
|
1015
|
+
cleanup(false, true);
|
|
653
1016
|
connect();
|
|
654
1017
|
}, CONFIG.RECONNECT_DELAY);
|
|
655
1018
|
};
|
|
656
1019
|
const bridgeInstance = createBridgeInstance({
|
|
657
1020
|
portRef,
|
|
1021
|
+
portDisconnectedRef,
|
|
658
1022
|
pendingRequestsRef,
|
|
1023
|
+
requestQueueRef,
|
|
1024
|
+
activeIdempotencyKeysRef,
|
|
659
1025
|
eventListenersRef,
|
|
660
1026
|
messageIdRef,
|
|
661
1027
|
isConnectedRef,
|
|
1028
|
+
isReconnectingRef,
|
|
662
1029
|
consecutiveTimeoutsRef,
|
|
663
1030
|
reconnectionGracePeriodRef,
|
|
664
1031
|
healthPausedUntilRef,
|
|
665
1032
|
defaultTimeout,
|
|
666
1033
|
timeoutFailureThreshold,
|
|
667
|
-
onReconnectNeeded: triggerReconnect
|
|
1034
|
+
onReconnectNeeded: triggerReconnect,
|
|
1035
|
+
drainRequestQueue
|
|
668
1036
|
});
|
|
1037
|
+
portDisconnectedRef.current = false;
|
|
1038
|
+
isReconnectingRef.current = false;
|
|
669
1039
|
setBridge(bridgeInstance);
|
|
670
1040
|
isConnectedRef.current = true;
|
|
671
1041
|
updateStatus("connected");
|
|
@@ -675,6 +1045,145 @@ const BridgeProvider = ({
|
|
|
675
1045
|
swRestartRetryCountRef.current = 0;
|
|
676
1046
|
consecutiveTimeoutsRef.current = 0;
|
|
677
1047
|
isConnectingRef.current = false;
|
|
1048
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1049
|
+
const queueSize = requestQueueRef.current.length;
|
|
1050
|
+
const pendingSize = pendingRequestsRef.current.size;
|
|
1051
|
+
console.log(`[Bridge] \u2705 PORT CONNECTED | Queued: ${queueSize} | Pending: ${pendingSize}`);
|
|
1052
|
+
}
|
|
1053
|
+
const verifyConnection = (targetPort) => {
|
|
1054
|
+
const VERIFY_PING_TIMEOUT = 8e3;
|
|
1055
|
+
const MAX_VERIFY_RETRIES = 3;
|
|
1056
|
+
const VERIFY_RETRY_DELAY = 2e3;
|
|
1057
|
+
const attemptVerify = (attempt) => {
|
|
1058
|
+
if (portRef.current !== targetPort) {
|
|
1059
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1060
|
+
console.log(`[Bridge] Verification aborted - port changed (attempt ${attempt})`);
|
|
1061
|
+
}
|
|
1062
|
+
return Promise.resolve(false);
|
|
1063
|
+
}
|
|
1064
|
+
if (attempt > MAX_VERIFY_RETRIES) {
|
|
1065
|
+
return Promise.resolve(false);
|
|
1066
|
+
}
|
|
1067
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1068
|
+
console.log(
|
|
1069
|
+
`[Bridge] Verifying connection (attempt ${attempt}/${MAX_VERIFY_RETRIES})...`
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
const pingId = `verify_${Date.now()}_${attempt}`;
|
|
1073
|
+
return new Promise((resolve) => {
|
|
1074
|
+
if (portRef.current !== targetPort) {
|
|
1075
|
+
resolve(false);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const timeout = setTimeout(() => {
|
|
1079
|
+
pendingRequestsRef.current.delete(pingId);
|
|
1080
|
+
if (portRef.current !== targetPort) {
|
|
1081
|
+
resolve(false);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1085
|
+
console.warn(`[Bridge] Verification ping timeout (attempt ${attempt})`);
|
|
1086
|
+
}
|
|
1087
|
+
setTimeout(() => {
|
|
1088
|
+
attemptVerify(attempt + 1).then(resolve);
|
|
1089
|
+
}, VERIFY_RETRY_DELAY);
|
|
1090
|
+
}, VERIFY_PING_TIMEOUT);
|
|
1091
|
+
pendingRequestsRef.current.set(pingId, {
|
|
1092
|
+
resolve: () => {
|
|
1093
|
+
clearTimeout(timeout);
|
|
1094
|
+
if (portRef.current !== targetPort) {
|
|
1095
|
+
resolve(false);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1099
|
+
console.log("[Bridge] \u2705 VERIFIED - SW is responding");
|
|
1100
|
+
}
|
|
1101
|
+
resolve(true);
|
|
1102
|
+
},
|
|
1103
|
+
reject: () => {
|
|
1104
|
+
clearTimeout(timeout);
|
|
1105
|
+
if (portRef.current !== targetPort) {
|
|
1106
|
+
resolve(false);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1110
|
+
console.warn(`[Bridge] Verification ping error (attempt ${attempt})`);
|
|
1111
|
+
}
|
|
1112
|
+
setTimeout(() => {
|
|
1113
|
+
attemptVerify(attempt + 1).then(resolve);
|
|
1114
|
+
}, VERIFY_RETRY_DELAY);
|
|
1115
|
+
},
|
|
1116
|
+
timeout,
|
|
1117
|
+
timeoutDuration: VERIFY_PING_TIMEOUT,
|
|
1118
|
+
key: "__ping__",
|
|
1119
|
+
payload: void 0
|
|
1120
|
+
});
|
|
1121
|
+
try {
|
|
1122
|
+
targetPort.postMessage({ id: pingId, key: "__ping__", payload: void 0 });
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
clearTimeout(timeout);
|
|
1125
|
+
pendingRequestsRef.current.delete(pingId);
|
|
1126
|
+
if (portRef.current !== targetPort) {
|
|
1127
|
+
resolve(false);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1131
|
+
console.warn(`[Bridge] Verification postMessage error (attempt ${attempt}):`, e);
|
|
1132
|
+
}
|
|
1133
|
+
setTimeout(() => {
|
|
1134
|
+
attemptVerify(attempt + 1).then(resolve);
|
|
1135
|
+
}, VERIFY_RETRY_DELAY);
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
};
|
|
1139
|
+
return attemptVerify(1);
|
|
1140
|
+
};
|
|
1141
|
+
const startVerification = () => {
|
|
1142
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1143
|
+
console.log("[Bridge] Starting verification after initial delay...");
|
|
1144
|
+
}
|
|
1145
|
+
verifyConnection(port).then((verified) => {
|
|
1146
|
+
if (!isMountedRef.current || portRef.current !== port) {
|
|
1147
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1148
|
+
console.log("[Bridge] Verification callback aborted - context changed");
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (!verified) {
|
|
1153
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1154
|
+
console.error("[Bridge] \u274C Connection verification failed - SW not responding");
|
|
1155
|
+
}
|
|
1156
|
+
isConnectingRef.current = false;
|
|
1157
|
+
isReconnectingRef.current = true;
|
|
1158
|
+
scheduleSwRestartReconnect(connect);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1162
|
+
const queueSize = requestQueueRef.current.length;
|
|
1163
|
+
console.log(
|
|
1164
|
+
`[Bridge] \u2705 RECONNECTED SUCCESSFULLY | Queue: ${queueSize} requests to drain`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
setTimeout(() => {
|
|
1168
|
+
if (isMountedRef.current && isConnectedRef.current) {
|
|
1169
|
+
drainRequestQueue();
|
|
1170
|
+
}
|
|
1171
|
+
}, 200);
|
|
1172
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1173
|
+
console.log("[Bridge] Emitting bridge:connected event to stores");
|
|
1174
|
+
}
|
|
1175
|
+
eventListenersRef.current.get("bridge:connected")?.forEach((handler) => {
|
|
1176
|
+
try {
|
|
1177
|
+
handler({ timestamp: Date.now() });
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
1180
|
+
console.warn("[Bridge] bridge:connected handler error:", err);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
};
|
|
1186
|
+
setTimeout(startVerification, 2e3);
|
|
678
1187
|
reconnectionGracePeriodRef.current = true;
|
|
679
1188
|
setTimeout(() => {
|
|
680
1189
|
reconnectionGracePeriodRef.current = false;
|
|
@@ -682,15 +1191,6 @@ const BridgeProvider = ({
|
|
|
682
1191
|
console.log("[Bridge] Grace period ended, timeout monitoring active");
|
|
683
1192
|
}
|
|
684
1193
|
}, 1e4);
|
|
685
|
-
eventListenersRef.current.get("bridge:connected")?.forEach((handler) => {
|
|
686
|
-
try {
|
|
687
|
-
handler({ timestamp: Date.now() });
|
|
688
|
-
} catch (err) {
|
|
689
|
-
if (BRIDGE_ENABLE_LOGS) {
|
|
690
|
-
console.warn("[Bridge] bridge:connected handler error:", err);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
1194
|
const rejectAllPendingRequests = (message) => {
|
|
695
1195
|
pendingRequestsRef.current.forEach(({ reject, timeout, timeoutDuration }, id) => {
|
|
696
1196
|
if (timeoutDuration <= timeoutFailureThreshold) {
|
|
@@ -723,6 +1223,7 @@ const BridgeProvider = ({
|
|
|
723
1223
|
handleMessage,
|
|
724
1224
|
scheduleReconnect,
|
|
725
1225
|
scheduleSwRestartReconnect,
|
|
1226
|
+
drainRequestQueue,
|
|
726
1227
|
defaultTimeout,
|
|
727
1228
|
updateStatus,
|
|
728
1229
|
pingInterval
|
|
@@ -980,15 +1481,19 @@ function broadcastHealthChange(status, isHealthy) {
|
|
|
980
1481
|
});
|
|
981
1482
|
}
|
|
982
1483
|
const ServiceWorkerHealthContext = createContext(null);
|
|
1484
|
+
const HEALTH_GRACE_PERIOD_MS = 1e3;
|
|
983
1485
|
const ServiceWorkerHealthProvider = ({
|
|
984
1486
|
children,
|
|
985
1487
|
onHealthChange
|
|
986
1488
|
}) => {
|
|
987
1489
|
const bridgeContext = useContext(BridgeContext);
|
|
988
1490
|
const [lastHealthyAt, setLastHealthyAt] = useState(null);
|
|
1491
|
+
const [unhealthyStartedAt, setUnhealthyStartedAt] = useState(null);
|
|
1492
|
+
const [graceExpired, setGraceExpired] = useState(false);
|
|
1493
|
+
const graceTimeoutRef = useRef(null);
|
|
989
1494
|
const bridgeStatus = bridgeContext?.status;
|
|
990
1495
|
const isServiceWorkerAlive = bridgeContext?.isServiceWorkerAlive ?? false;
|
|
991
|
-
const
|
|
1496
|
+
const rawStatus = useMemo(() => {
|
|
992
1497
|
if (!bridgeStatus) return "unknown";
|
|
993
1498
|
if (bridgeStatus === "connected" && isServiceWorkerAlive) {
|
|
994
1499
|
return "healthy";
|
|
@@ -998,14 +1503,41 @@ const ServiceWorkerHealthProvider = ({
|
|
|
998
1503
|
}
|
|
999
1504
|
return "unhealthy";
|
|
1000
1505
|
}, [bridgeStatus, isServiceWorkerAlive]);
|
|
1506
|
+
useEffect(() => {
|
|
1507
|
+
if (rawStatus === "healthy") {
|
|
1508
|
+
setUnhealthyStartedAt(null);
|
|
1509
|
+
setGraceExpired(false);
|
|
1510
|
+
if (graceTimeoutRef.current) {
|
|
1511
|
+
clearTimeout(graceTimeoutRef.current);
|
|
1512
|
+
graceTimeoutRef.current = null;
|
|
1513
|
+
}
|
|
1514
|
+
} else if (!unhealthyStartedAt) {
|
|
1515
|
+
setUnhealthyStartedAt(Date.now());
|
|
1516
|
+
graceTimeoutRef.current = setTimeout(() => {
|
|
1517
|
+
setGraceExpired(true);
|
|
1518
|
+
}, HEALTH_GRACE_PERIOD_MS);
|
|
1519
|
+
}
|
|
1520
|
+
return () => {
|
|
1521
|
+
if (graceTimeoutRef.current) {
|
|
1522
|
+
clearTimeout(graceTimeoutRef.current);
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
}, [rawStatus, unhealthyStartedAt]);
|
|
1526
|
+
const derivedStatus = useMemo(() => {
|
|
1527
|
+
if (rawStatus === "healthy") return "healthy";
|
|
1528
|
+
if (!graceExpired && lastHealthyAt) {
|
|
1529
|
+
return "healthy";
|
|
1530
|
+
}
|
|
1531
|
+
return rawStatus;
|
|
1532
|
+
}, [rawStatus, graceExpired, lastHealthyAt]);
|
|
1001
1533
|
const isHealthy = derivedStatus === "healthy";
|
|
1002
1534
|
const isRecovering = derivedStatus === "recovering";
|
|
1003
1535
|
const isLoading = derivedStatus === "recovering" || derivedStatus === "unknown";
|
|
1004
1536
|
useEffect(() => {
|
|
1005
|
-
if (
|
|
1537
|
+
if (rawStatus === "healthy") {
|
|
1006
1538
|
setLastHealthyAt(Date.now());
|
|
1007
1539
|
}
|
|
1008
|
-
}, [
|
|
1540
|
+
}, [rawStatus]);
|
|
1009
1541
|
useEffect(() => {
|
|
1010
1542
|
broadcastHealthChange(derivedStatus, isHealthy);
|
|
1011
1543
|
onHealthChange?.(derivedStatus, isHealthy);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chromahq/react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.47",
|
|
4
4
|
"description": "React bindings for the Chroma Chrome extension framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -41,16 +41,20 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
43
43
|
"@rollup/plugin-typescript": "^12.1.3",
|
|
44
|
+
"@testing-library/react": "^16.0.0",
|
|
44
45
|
"@types/react": "^18.2.7",
|
|
46
|
+
"jsdom": "^25.0.0",
|
|
45
47
|
"rollup": "^4.8.0",
|
|
46
48
|
"rollup-plugin-dts": "^6.1.0",
|
|
47
49
|
"rollup-plugin-esbuild": "^6.1.0",
|
|
48
50
|
"typescript": "^5.6.0",
|
|
49
51
|
"react": "^19.1.0",
|
|
50
|
-
"react-dom": "^19.1.0"
|
|
52
|
+
"react-dom": "^19.1.0",
|
|
53
|
+
"vitest": "^3.2.4"
|
|
51
54
|
},
|
|
52
55
|
"scripts": {
|
|
53
56
|
"build": "rollup -c",
|
|
54
|
-
"dev": "rollup -c --watch"
|
|
57
|
+
"dev": "rollup -c --watch",
|
|
58
|
+
"test": "vitest run"
|
|
55
59
|
}
|
|
56
60
|
}
|