@chromahq/react 1.0.15 → 1.0.17
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 +16 -6
- package/dist/index.js +354 -298
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { FC, ReactNode } from 'react';
|
|
2
|
+
|
|
1
3
|
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error' | 'reconnecting';
|
|
2
4
|
interface Bridge {
|
|
3
5
|
send: <Req = unknown, Res = unknown>(key: string, payload?: Req, timeoutDuration?: number) => Promise<Res>;
|
|
4
|
-
broadcast: (key: string, payload:
|
|
5
|
-
on: (key: string, handler: (payload:
|
|
6
|
-
off: (key: string, handler: (payload:
|
|
6
|
+
broadcast: (key: string, payload: unknown) => void;
|
|
7
|
+
on: (key: string, handler: (payload: unknown) => void) => void;
|
|
8
|
+
off: (key: string, handler: (payload: unknown) => void) => void;
|
|
7
9
|
isConnected: boolean;
|
|
8
10
|
ping: () => Promise<boolean>;
|
|
9
11
|
}
|
|
@@ -14,16 +16,24 @@ interface BridgeContextValue {
|
|
|
14
16
|
reconnect: () => void;
|
|
15
17
|
isServiceWorkerAlive: boolean;
|
|
16
18
|
}
|
|
17
|
-
interface
|
|
18
|
-
children:
|
|
19
|
+
interface BridgeProviderProps {
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
/** Base delay for retry attempts in ms. Default: 1000 */
|
|
19
22
|
retryAfter?: number;
|
|
23
|
+
/** Maximum number of retry attempts. Default: 10 */
|
|
20
24
|
maxRetries?: number;
|
|
25
|
+
/** How often to ping the service worker in ms. Default: 5000 */
|
|
21
26
|
pingInterval?: number;
|
|
27
|
+
/** How long to wait before resetting retry count in ms. Default: 30000 */
|
|
22
28
|
maxRetryCooldown?: number;
|
|
29
|
+
/** Default timeout for messages in ms. Default: 10000 */
|
|
30
|
+
defaultTimeout?: number;
|
|
31
|
+
/** Callback when connection status changes */
|
|
23
32
|
onConnectionChange?: (status: ConnectionStatus) => void;
|
|
33
|
+
/** Callback when an error occurs */
|
|
24
34
|
onError?: (error: Error) => void;
|
|
25
35
|
}
|
|
26
|
-
declare const BridgeProvider:
|
|
36
|
+
declare const BridgeProvider: FC<BridgeProviderProps>;
|
|
27
37
|
|
|
28
38
|
/**
|
|
29
39
|
* Custom hook to access the bridge context.
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,192 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
|
-
import { createContext, useState, useRef,
|
|
2
|
+
import { createContext, useState, useRef, useEffect, useCallback, useMemo, useContext } from 'react';
|
|
3
3
|
|
|
4
|
+
const CONFIG = {
|
|
5
|
+
RETRY_AFTER: 1e3,
|
|
6
|
+
MAX_RETRIES: 10,
|
|
7
|
+
PING_INTERVAL: 5e3,
|
|
8
|
+
MAX_RETRY_COOLDOWN: 3e4,
|
|
9
|
+
DEFAULT_TIMEOUT: 1e4,
|
|
10
|
+
MAX_RETRY_DELAY: 3e4,
|
|
11
|
+
PING_TIMEOUT: 2e3,
|
|
12
|
+
ERROR_CHECK_INTERVAL: 100,
|
|
13
|
+
MAX_ERROR_CHECKS: 10,
|
|
14
|
+
CONSECUTIVE_FAILURE_THRESHOLD: 2,
|
|
15
|
+
RECONNECT_DELAY: 100,
|
|
16
|
+
PORT_NAME: "chroma-bridge"
|
|
17
|
+
};
|
|
18
|
+
const calculateBackoffDelay = (attempt, baseDelay, maxDelay) => Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
19
|
+
const clearTimeoutSafe = (ref) => {
|
|
20
|
+
if (ref.current) {
|
|
21
|
+
clearTimeout(ref.current);
|
|
22
|
+
ref.current = null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const clearIntervalSafe = (ref) => {
|
|
26
|
+
if (ref.current) {
|
|
27
|
+
clearInterval(ref.current);
|
|
28
|
+
ref.current = null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const consumeRuntimeError = () => {
|
|
32
|
+
const error = chrome.runtime.lastError?.message;
|
|
33
|
+
void chrome.runtime.lastError;
|
|
34
|
+
return error;
|
|
35
|
+
};
|
|
4
36
|
const BridgeContext = createContext(null);
|
|
37
|
+
function createBridgeInstance(deps) {
|
|
38
|
+
const {
|
|
39
|
+
portRef,
|
|
40
|
+
pendingRequestsRef,
|
|
41
|
+
eventListenersRef,
|
|
42
|
+
messageIdRef,
|
|
43
|
+
isConnectedRef,
|
|
44
|
+
consecutiveTimeoutsRef,
|
|
45
|
+
defaultTimeout,
|
|
46
|
+
onReconnectNeeded
|
|
47
|
+
} = deps;
|
|
48
|
+
const rejectAllPending = (message) => {
|
|
49
|
+
pendingRequestsRef.current.forEach(({ reject, timeout }) => {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
reject(new Error(message));
|
|
52
|
+
});
|
|
53
|
+
pendingRequestsRef.current.clear();
|
|
54
|
+
};
|
|
55
|
+
const send = (key, payload, timeoutDuration = defaultTimeout) => {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
if (!portRef.current) {
|
|
58
|
+
reject(new Error("Bridge disconnected"));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const id = `msg_${++messageIdRef.current}`;
|
|
62
|
+
const timeout = setTimeout(() => {
|
|
63
|
+
if (!pendingRequestsRef.current.has(id)) return;
|
|
64
|
+
pendingRequestsRef.current.delete(id);
|
|
65
|
+
consecutiveTimeoutsRef.current++;
|
|
66
|
+
console.warn(`[Bridge] Request timed out: ${key} (${timeoutDuration}ms)`);
|
|
67
|
+
if (consecutiveTimeoutsRef.current >= CONFIG.CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
68
|
+
console.warn(
|
|
69
|
+
`[Bridge] ${consecutiveTimeoutsRef.current} consecutive timeouts, reconnecting...`
|
|
70
|
+
);
|
|
71
|
+
rejectAllPending("Bridge reconnecting due to timeouts");
|
|
72
|
+
consecutiveTimeoutsRef.current = 0;
|
|
73
|
+
onReconnectNeeded();
|
|
74
|
+
}
|
|
75
|
+
reject(new Error(`Request timed out: ${key}`));
|
|
76
|
+
}, timeoutDuration);
|
|
77
|
+
pendingRequestsRef.current.set(id, {
|
|
78
|
+
resolve,
|
|
79
|
+
reject,
|
|
80
|
+
timeout
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
portRef.current.postMessage({ id, key, payload });
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
const errorMessage = consumeRuntimeError();
|
|
86
|
+
if (errorMessage && pendingRequestsRef.current.has(id)) {
|
|
87
|
+
const pending = pendingRequestsRef.current.get(id);
|
|
88
|
+
if (pending) {
|
|
89
|
+
clearTimeout(pending.timeout);
|
|
90
|
+
pendingRequestsRef.current.delete(id);
|
|
91
|
+
reject(new Error(errorMessage));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, 0);
|
|
95
|
+
const immediateError = consumeRuntimeError();
|
|
96
|
+
if (immediateError) {
|
|
97
|
+
throw new Error(immediateError);
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
const pending = pendingRequestsRef.current.get(id);
|
|
101
|
+
if (pending) {
|
|
102
|
+
clearTimeout(pending.timeout);
|
|
103
|
+
pendingRequestsRef.current.delete(id);
|
|
104
|
+
}
|
|
105
|
+
reject(e instanceof Error ? e : new Error("Send failed"));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const broadcast = (key, payload) => {
|
|
110
|
+
if (!portRef.current) {
|
|
111
|
+
console.warn("[Bridge] Cannot broadcast - disconnected");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
portRef.current.postMessage({ type: "broadcast", key, payload });
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.warn("[Bridge] Broadcast failed:", e);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const on = (key, handler) => {
|
|
121
|
+
if (!eventListenersRef.current.has(key)) {
|
|
122
|
+
eventListenersRef.current.set(key, /* @__PURE__ */ new Set());
|
|
123
|
+
}
|
|
124
|
+
eventListenersRef.current.get(key).add(handler);
|
|
125
|
+
};
|
|
126
|
+
const off = (key, handler) => {
|
|
127
|
+
const listeners = eventListenersRef.current.get(key);
|
|
128
|
+
if (listeners) {
|
|
129
|
+
listeners.delete(handler);
|
|
130
|
+
if (listeners.size === 0) {
|
|
131
|
+
eventListenersRef.current.delete(key);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const bridge = {
|
|
136
|
+
send,
|
|
137
|
+
broadcast,
|
|
138
|
+
on,
|
|
139
|
+
off,
|
|
140
|
+
get isConnected() {
|
|
141
|
+
return portRef.current !== null && isConnectedRef.current;
|
|
142
|
+
},
|
|
143
|
+
ping: async () => {
|
|
144
|
+
try {
|
|
145
|
+
await send("__ping__", void 0, CONFIG.PING_TIMEOUT);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
return bridge;
|
|
153
|
+
}
|
|
154
|
+
function startHealthMonitor(deps) {
|
|
155
|
+
const {
|
|
156
|
+
bridge,
|
|
157
|
+
pingIntervalRef,
|
|
158
|
+
consecutivePingFailuresRef,
|
|
159
|
+
pingInterval,
|
|
160
|
+
setIsServiceWorkerAlive,
|
|
161
|
+
onReconnectNeeded
|
|
162
|
+
} = deps;
|
|
163
|
+
clearIntervalSafe(pingIntervalRef);
|
|
164
|
+
consecutivePingFailuresRef.current = 0;
|
|
165
|
+
pingIntervalRef.current = setInterval(async () => {
|
|
166
|
+
if (!bridge.isConnected) return;
|
|
167
|
+
const alive = await bridge.ping();
|
|
168
|
+
if (!pingIntervalRef.current) return;
|
|
169
|
+
setIsServiceWorkerAlive(alive);
|
|
170
|
+
if (alive) {
|
|
171
|
+
consecutivePingFailuresRef.current = 0;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
consecutivePingFailuresRef.current++;
|
|
175
|
+
console.warn(`[Bridge] Ping failed (${consecutivePingFailuresRef.current}x)`);
|
|
176
|
+
if (consecutivePingFailuresRef.current >= CONFIG.CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
177
|
+
console.warn("[Bridge] Service worker unresponsive, reconnecting...");
|
|
178
|
+
consecutivePingFailuresRef.current = 0;
|
|
179
|
+
onReconnectNeeded();
|
|
180
|
+
}
|
|
181
|
+
}, pingInterval);
|
|
182
|
+
}
|
|
5
183
|
const BridgeProvider = ({
|
|
6
184
|
children,
|
|
7
|
-
retryAfter =
|
|
8
|
-
maxRetries =
|
|
9
|
-
pingInterval =
|
|
10
|
-
maxRetryCooldown =
|
|
11
|
-
|
|
185
|
+
retryAfter = CONFIG.RETRY_AFTER,
|
|
186
|
+
maxRetries = CONFIG.MAX_RETRIES,
|
|
187
|
+
pingInterval = CONFIG.PING_INTERVAL,
|
|
188
|
+
maxRetryCooldown = CONFIG.MAX_RETRY_COOLDOWN,
|
|
189
|
+
defaultTimeout = CONFIG.DEFAULT_TIMEOUT,
|
|
12
190
|
onConnectionChange,
|
|
13
191
|
onError
|
|
14
192
|
}) => {
|
|
@@ -17,19 +195,28 @@ const BridgeProvider = ({
|
|
|
17
195
|
const [error, setError] = useState(null);
|
|
18
196
|
const [isServiceWorkerAlive, setIsServiceWorkerAlive] = useState(false);
|
|
19
197
|
const portRef = useRef(null);
|
|
20
|
-
const
|
|
21
|
-
const consecutivePingFailuresRef = useRef(0);
|
|
22
|
-
const pendingRef = useRef(
|
|
23
|
-
/* @__PURE__ */ new Map()
|
|
24
|
-
);
|
|
25
|
-
const eventListenersRef = useRef(/* @__PURE__ */ new Map());
|
|
26
|
-
const uidRef = useRef(0);
|
|
27
|
-
const reconnectTimeoutRef = useRef(null);
|
|
28
|
-
const retryCountRef = useRef(0);
|
|
198
|
+
const isConnectedRef = useRef(false);
|
|
29
199
|
const isConnectingRef = useRef(false);
|
|
30
|
-
const
|
|
200
|
+
const retryCountRef = useRef(0);
|
|
201
|
+
const reconnectTimeoutRef = useRef(null);
|
|
202
|
+
const maxRetryCooldownRef = useRef(null);
|
|
203
|
+
const triggerReconnectTimeoutRef = useRef(null);
|
|
31
204
|
const pingIntervalRef = useRef(null);
|
|
205
|
+
const errorCheckIntervalRef = useRef(null);
|
|
206
|
+
const consecutivePingFailuresRef = useRef(0);
|
|
32
207
|
const consecutiveTimeoutsRef = useRef(0);
|
|
208
|
+
const pendingRequestsRef = useRef(/* @__PURE__ */ new Map());
|
|
209
|
+
const eventListenersRef = useRef(/* @__PURE__ */ new Map());
|
|
210
|
+
const messageIdRef = useRef(0);
|
|
211
|
+
const statusRef = useRef(status);
|
|
212
|
+
const bridgeRef = useRef(bridge);
|
|
213
|
+
const isMountedRef = useRef(true);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
statusRef.current = status;
|
|
216
|
+
}, [status]);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
bridgeRef.current = bridge;
|
|
219
|
+
}, [bridge]);
|
|
33
220
|
const updateStatus = useCallback(
|
|
34
221
|
(newStatus) => {
|
|
35
222
|
setStatus(newStatus);
|
|
@@ -46,356 +233,225 @@ const BridgeProvider = ({
|
|
|
46
233
|
[onError, updateStatus]
|
|
47
234
|
);
|
|
48
235
|
const cleanup = useCallback(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (errorCheckIntervalRef.current) {
|
|
54
|
-
clearInterval(errorCheckIntervalRef.current);
|
|
55
|
-
errorCheckIntervalRef.current = null;
|
|
56
|
-
}
|
|
57
|
-
if (pingIntervalRef.current) {
|
|
58
|
-
clearInterval(pingIntervalRef.current);
|
|
59
|
-
pingIntervalRef.current = null;
|
|
60
|
-
}
|
|
236
|
+
clearTimeoutSafe(reconnectTimeoutRef);
|
|
237
|
+
clearTimeoutSafe(triggerReconnectTimeoutRef);
|
|
238
|
+
clearIntervalSafe(errorCheckIntervalRef);
|
|
239
|
+
clearIntervalSafe(pingIntervalRef);
|
|
61
240
|
if (portRef.current) {
|
|
62
241
|
try {
|
|
63
242
|
portRef.current.disconnect();
|
|
64
|
-
} catch
|
|
243
|
+
} catch {
|
|
65
244
|
}
|
|
66
245
|
portRef.current = null;
|
|
67
246
|
}
|
|
68
|
-
|
|
247
|
+
pendingRequestsRef.current.forEach(({ reject, timeout }) => {
|
|
69
248
|
clearTimeout(timeout);
|
|
70
249
|
reject(new Error("Bridge disconnected"));
|
|
71
250
|
});
|
|
72
|
-
|
|
251
|
+
pendingRequestsRef.current.clear();
|
|
73
252
|
eventListenersRef.current.clear();
|
|
74
253
|
setIsServiceWorkerAlive(false);
|
|
75
254
|
setBridge(null);
|
|
76
255
|
isConnectingRef.current = false;
|
|
256
|
+
isConnectedRef.current = false;
|
|
77
257
|
}, []);
|
|
258
|
+
const handleMessage = useCallback((message) => {
|
|
259
|
+
if (message.id && pendingRequestsRef.current.has(message.id)) {
|
|
260
|
+
const pending = pendingRequestsRef.current.get(message.id);
|
|
261
|
+
clearTimeout(pending.timeout);
|
|
262
|
+
consecutiveTimeoutsRef.current = 0;
|
|
263
|
+
if (message.error) {
|
|
264
|
+
pending.reject(new Error(message.error));
|
|
265
|
+
} else {
|
|
266
|
+
pending.resolve(message.data);
|
|
267
|
+
}
|
|
268
|
+
pendingRequestsRef.current.delete(message.id);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (message.type === "broadcast" && message.key) {
|
|
272
|
+
eventListenersRef.current.get(message.key)?.forEach((handler) => {
|
|
273
|
+
try {
|
|
274
|
+
handler(message.payload);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.warn("[Bridge] Event handler error:", err);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}, []);
|
|
281
|
+
const scheduleReconnect = useCallback(
|
|
282
|
+
(connectFn) => {
|
|
283
|
+
if (!isMountedRef.current) return;
|
|
284
|
+
if (retryCountRef.current < maxRetries) {
|
|
285
|
+
retryCountRef.current++;
|
|
286
|
+
const delay = calculateBackoffDelay(
|
|
287
|
+
retryCountRef.current,
|
|
288
|
+
retryAfter,
|
|
289
|
+
CONFIG.MAX_RETRY_DELAY
|
|
290
|
+
);
|
|
291
|
+
console.log(`[Bridge] Reconnecting in ${delay}ms (${retryCountRef.current}/${maxRetries})`);
|
|
292
|
+
updateStatus("reconnecting");
|
|
293
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
294
|
+
if (isMountedRef.current) connectFn();
|
|
295
|
+
}, delay);
|
|
296
|
+
} else {
|
|
297
|
+
console.warn(`[Bridge] Max retries reached. Cooldown: ${maxRetryCooldown}ms`);
|
|
298
|
+
clearTimeoutSafe(maxRetryCooldownRef);
|
|
299
|
+
maxRetryCooldownRef.current = setTimeout(() => {
|
|
300
|
+
if (!isMountedRef.current) return;
|
|
301
|
+
console.log("[Bridge] Cooldown complete, reconnecting...");
|
|
302
|
+
retryCountRef.current = 0;
|
|
303
|
+
connectFn();
|
|
304
|
+
}, maxRetryCooldown);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
[maxRetries, retryAfter, maxRetryCooldown, updateStatus]
|
|
308
|
+
);
|
|
78
309
|
const connect = useCallback(() => {
|
|
79
|
-
if (isConnectingRef.current
|
|
80
|
-
|
|
310
|
+
if (isConnectingRef.current) return;
|
|
311
|
+
if (retryCountRef.current >= maxRetries) {
|
|
312
|
+
console.warn("[Bridge] Waiting for cooldown...");
|
|
81
313
|
return;
|
|
82
314
|
}
|
|
83
315
|
isConnectingRef.current = true;
|
|
84
316
|
cleanup();
|
|
85
317
|
if (!chrome?.runtime?.connect) {
|
|
86
318
|
handleError(new Error("Chrome runtime not available"));
|
|
319
|
+
isConnectingRef.current = false;
|
|
87
320
|
return;
|
|
88
321
|
}
|
|
89
322
|
try {
|
|
90
|
-
const port = chrome.runtime.connect({ name:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
323
|
+
const port = chrome.runtime.connect({ name: CONFIG.PORT_NAME });
|
|
324
|
+
const immediateError = consumeRuntimeError();
|
|
325
|
+
if (immediateError) throw new Error(immediateError);
|
|
94
326
|
portRef.current = port;
|
|
95
|
-
let
|
|
96
|
-
const maxErrorChecks = 10;
|
|
327
|
+
let errorChecks = 0;
|
|
97
328
|
errorCheckIntervalRef.current = setInterval(() => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
clearInterval(errorCheckIntervalRef.current);
|
|
105
|
-
errorCheckIntervalRef.current = null;
|
|
106
|
-
}
|
|
107
|
-
if (errorMessage?.includes("Receiving end does not exist")) {
|
|
108
|
-
console.warn("[Bridge] Background script not ready, will retry connection");
|
|
329
|
+
errorChecks++;
|
|
330
|
+
const err = consumeRuntimeError();
|
|
331
|
+
if (err) {
|
|
332
|
+
clearIntervalSafe(errorCheckIntervalRef);
|
|
333
|
+
if (err.includes("Receiving end does not exist")) {
|
|
334
|
+
console.warn("[Bridge] Background not ready, retrying...");
|
|
109
335
|
cleanup();
|
|
110
336
|
isConnectingRef.current = false;
|
|
111
|
-
|
|
112
|
-
retryCountRef.current++;
|
|
113
|
-
const delay = retryAfter * Math.pow(2, retryCountRef.current - 1);
|
|
114
|
-
reconnectTimeoutRef.current = setTimeout(connect, delay);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (errorCheckCount >= maxErrorChecks) {
|
|
119
|
-
if (errorCheckIntervalRef.current) {
|
|
120
|
-
clearInterval(errorCheckIntervalRef.current);
|
|
121
|
-
errorCheckIntervalRef.current = null;
|
|
337
|
+
scheduleReconnect(connect);
|
|
122
338
|
}
|
|
339
|
+
return;
|
|
123
340
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (msg.id && pendingRef.current.has(msg.id)) {
|
|
127
|
-
const pending = pendingRef.current.get(msg.id);
|
|
128
|
-
clearTimeout(pending.timeout);
|
|
129
|
-
consecutiveTimeoutsRef.current = 0;
|
|
130
|
-
if (msg.error) {
|
|
131
|
-
pending.reject(new Error(msg.error));
|
|
132
|
-
} else {
|
|
133
|
-
pending.resolve(msg.data);
|
|
134
|
-
}
|
|
135
|
-
pendingRef.current.delete(msg.id);
|
|
136
|
-
} else if (msg.type === "broadcast" && msg.key) {
|
|
137
|
-
const listeners = eventListenersRef.current.get(msg.key);
|
|
138
|
-
if (listeners) {
|
|
139
|
-
listeners.forEach((handler) => {
|
|
140
|
-
try {
|
|
141
|
-
handler(msg.payload);
|
|
142
|
-
} catch (error2) {
|
|
143
|
-
console.warn("[Bridge] Error in event handler:", error2);
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
}
|
|
341
|
+
if (errorChecks >= CONFIG.MAX_ERROR_CHECKS) {
|
|
342
|
+
clearIntervalSafe(errorCheckIntervalRef);
|
|
147
343
|
}
|
|
148
|
-
});
|
|
344
|
+
}, CONFIG.ERROR_CHECK_INTERVAL);
|
|
345
|
+
port.onMessage.addListener(handleMessage);
|
|
149
346
|
port.onDisconnect.addListener(() => {
|
|
150
|
-
console.warn("[Bridge]
|
|
347
|
+
console.warn("[Bridge] Disconnected");
|
|
151
348
|
isConnectingRef.current = false;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
156
|
-
chrome.runtime.lastError;
|
|
349
|
+
const disconnectError = consumeRuntimeError();
|
|
350
|
+
if (disconnectError) {
|
|
351
|
+
handleError(new Error(disconnectError));
|
|
157
352
|
} else {
|
|
158
353
|
updateStatus("disconnected");
|
|
159
354
|
}
|
|
160
355
|
cleanup();
|
|
161
|
-
if (
|
|
162
|
-
|
|
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");
|
|
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);
|
|
356
|
+
if (isMountedRef.current) {
|
|
357
|
+
scheduleReconnect(connect);
|
|
183
358
|
}
|
|
184
359
|
});
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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") {
|
|
200
|
-
console.warn(
|
|
201
|
-
`[Bridge] ${consecutiveTimeoutsRef.current} consecutive timeouts detected, service worker may be unresponsive. Reconnecting...`
|
|
202
|
-
);
|
|
203
|
-
setIsServiceWorkerAlive(false);
|
|
204
|
-
updateStatus("reconnecting");
|
|
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();
|
|
214
|
-
retryCountRef.current = 0;
|
|
215
|
-
isConnectingRef.current = false;
|
|
216
|
-
consecutiveTimeoutsRef.current = 0;
|
|
217
|
-
setTimeout(() => {
|
|
218
|
-
cleanup();
|
|
219
|
-
connect();
|
|
220
|
-
}, 100);
|
|
221
|
-
}
|
|
222
|
-
reject(new Error(errorMessage));
|
|
223
|
-
}
|
|
224
|
-
}, timeoutDuration);
|
|
225
|
-
pendingRef.current.set(id, { resolve, reject, timeout });
|
|
226
|
-
try {
|
|
227
|
-
portRef.current.postMessage({ id, key, payload });
|
|
228
|
-
setTimeout(() => {
|
|
229
|
-
if (chrome.runtime.lastError) {
|
|
230
|
-
const errorMessage = chrome.runtime.lastError.message;
|
|
231
|
-
console.warn("[Bridge] Async runtime error after postMessage:", errorMessage);
|
|
232
|
-
chrome.runtime.lastError;
|
|
233
|
-
if (pendingRef.current.has(id)) {
|
|
234
|
-
const pending = pendingRef.current.get(id);
|
|
235
|
-
if (pending) clearTimeout(pending.timeout);
|
|
236
|
-
pendingRef.current.delete(id);
|
|
237
|
-
reject(new Error(errorMessage || "Async send failed"));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}, 0);
|
|
241
|
-
if (chrome.runtime.lastError) {
|
|
242
|
-
throw new Error(chrome.runtime.lastError.message || "Failed to send message");
|
|
243
|
-
}
|
|
244
|
-
} catch (e) {
|
|
245
|
-
const pending = pendingRef.current.get(id);
|
|
246
|
-
if (pending) clearTimeout(pending.timeout);
|
|
247
|
-
pendingRef.current.delete(id);
|
|
248
|
-
if (chrome.runtime.lastError) {
|
|
249
|
-
console.warn(
|
|
250
|
-
"[Bridge] Runtime error during postMessage:",
|
|
251
|
-
chrome.runtime.lastError.message
|
|
252
|
-
);
|
|
253
|
-
chrome.runtime.lastError;
|
|
254
|
-
}
|
|
255
|
-
reject(e instanceof Error ? e : new Error("Send failed"));
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
},
|
|
259
|
-
broadcast: (key, payload) => {
|
|
260
|
-
if (!portRef.current) {
|
|
261
|
-
console.warn("[Bridge] Cannot broadcast - disconnected");
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
try {
|
|
265
|
-
portRef.current.postMessage({ type: "broadcast", key, payload });
|
|
266
|
-
} catch (e) {
|
|
267
|
-
console.warn("[Bridge] Broadcast failed:", e);
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
on: (key, handler) => {
|
|
271
|
-
let listeners = eventListenersRef.current.get(key);
|
|
272
|
-
if (!listeners) {
|
|
273
|
-
listeners = /* @__PURE__ */ new Set();
|
|
274
|
-
eventListenersRef.current.set(key, listeners);
|
|
275
|
-
}
|
|
276
|
-
listeners.add(handler);
|
|
277
|
-
},
|
|
278
|
-
off: (key, handler) => {
|
|
279
|
-
const listeners = eventListenersRef.current.get(key);
|
|
280
|
-
if (listeners) {
|
|
281
|
-
listeners.delete(handler);
|
|
282
|
-
if (listeners.size === 0) {
|
|
283
|
-
eventListenersRef.current.delete(key);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
ping: async () => {
|
|
288
|
-
try {
|
|
289
|
-
await bridgeInstance.send("__ping__", void 0, 2e3);
|
|
290
|
-
return true;
|
|
291
|
-
} catch {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
get isConnected() {
|
|
296
|
-
return portRef.current !== null && status === "connected";
|
|
297
|
-
}
|
|
360
|
+
const triggerReconnect = () => {
|
|
361
|
+
if (!isMountedRef.current) return;
|
|
362
|
+
setIsServiceWorkerAlive(false);
|
|
363
|
+
updateStatus("reconnecting");
|
|
364
|
+
retryCountRef.current = 0;
|
|
365
|
+
isConnectingRef.current = false;
|
|
366
|
+
clearTimeoutSafe(triggerReconnectTimeoutRef);
|
|
367
|
+
triggerReconnectTimeoutRef.current = setTimeout(() => {
|
|
368
|
+
if (!isMountedRef.current) return;
|
|
369
|
+
cleanup();
|
|
370
|
+
connect();
|
|
371
|
+
}, CONFIG.RECONNECT_DELAY);
|
|
298
372
|
};
|
|
373
|
+
const bridgeInstance = createBridgeInstance({
|
|
374
|
+
portRef,
|
|
375
|
+
pendingRequestsRef,
|
|
376
|
+
eventListenersRef,
|
|
377
|
+
messageIdRef,
|
|
378
|
+
isConnectedRef,
|
|
379
|
+
consecutiveTimeoutsRef,
|
|
380
|
+
defaultTimeout,
|
|
381
|
+
onReconnectNeeded: triggerReconnect
|
|
382
|
+
});
|
|
299
383
|
setBridge(bridgeInstance);
|
|
384
|
+
isConnectedRef.current = true;
|
|
300
385
|
updateStatus("connected");
|
|
301
386
|
setIsServiceWorkerAlive(true);
|
|
302
387
|
setError(null);
|
|
303
388
|
retryCountRef.current = 0;
|
|
304
389
|
consecutiveTimeoutsRef.current = 0;
|
|
305
390
|
isConnectingRef.current = false;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (!alive) {
|
|
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;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}, pingInterval);
|
|
391
|
+
startHealthMonitor({
|
|
392
|
+
bridge: bridgeInstance,
|
|
393
|
+
pingIntervalRef,
|
|
394
|
+
consecutivePingFailuresRef,
|
|
395
|
+
pingInterval,
|
|
396
|
+
setIsServiceWorkerAlive,
|
|
397
|
+
onReconnectNeeded: triggerReconnect
|
|
398
|
+
});
|
|
333
399
|
} catch (e) {
|
|
334
400
|
isConnectingRef.current = false;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (retryCountRef.current < maxRetries) {
|
|
338
|
-
retryCountRef.current++;
|
|
339
|
-
reconnectTimeoutRef.current = setTimeout(connect, retryAfter);
|
|
340
|
-
}
|
|
401
|
+
handleError(e instanceof Error ? e : new Error("Connection failed"));
|
|
402
|
+
scheduleReconnect(connect);
|
|
341
403
|
}
|
|
342
|
-
}, [
|
|
404
|
+
}, [
|
|
405
|
+
maxRetries,
|
|
406
|
+
cleanup,
|
|
407
|
+
handleError,
|
|
408
|
+
handleMessage,
|
|
409
|
+
scheduleReconnect,
|
|
410
|
+
defaultTimeout,
|
|
411
|
+
updateStatus,
|
|
412
|
+
pingInterval
|
|
413
|
+
]);
|
|
343
414
|
const reconnect = useCallback(() => {
|
|
344
415
|
retryCountRef.current = 0;
|
|
345
416
|
consecutiveTimeoutsRef.current = 0;
|
|
346
417
|
updateStatus("connecting");
|
|
347
418
|
connect();
|
|
348
419
|
}, [connect, updateStatus]);
|
|
349
|
-
const statusRef = useRef(status);
|
|
350
|
-
const bridgeRef = useRef(bridge);
|
|
351
|
-
useEffect(() => {
|
|
352
|
-
statusRef.current = status;
|
|
353
|
-
}, [status]);
|
|
354
|
-
useEffect(() => {
|
|
355
|
-
bridgeRef.current = bridge;
|
|
356
|
-
}, [bridge]);
|
|
357
420
|
useEffect(() => {
|
|
358
421
|
connect();
|
|
359
422
|
const handleVisibilityChange = () => {
|
|
360
|
-
if (document.visibilityState
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
}
|
|
423
|
+
if (document.visibilityState !== "visible") return;
|
|
424
|
+
if (!isMountedRef.current) return;
|
|
425
|
+
if (isConnectingRef.current) return;
|
|
426
|
+
const currentStatus = statusRef.current;
|
|
427
|
+
const currentBridge = bridgeRef.current;
|
|
428
|
+
if (currentStatus === "disconnected" || currentStatus === "error") {
|
|
429
|
+
console.log("[Bridge] Tab visible, reconnecting...");
|
|
430
|
+
retryCountRef.current = 0;
|
|
431
|
+
connect();
|
|
432
|
+
} else if (currentStatus === "connected" && currentBridge) {
|
|
433
|
+
currentBridge.ping().then((alive) => {
|
|
434
|
+
if (!isMountedRef.current) return;
|
|
435
|
+
if (!alive) {
|
|
436
|
+
console.warn("[Bridge] Tab visible but unresponsive, reconnecting...");
|
|
437
|
+
retryCountRef.current = 0;
|
|
438
|
+
isConnectingRef.current = false;
|
|
439
|
+
cleanup();
|
|
440
|
+
connect();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
380
443
|
}
|
|
381
444
|
};
|
|
382
445
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
383
446
|
return () => {
|
|
447
|
+
isMountedRef.current = false;
|
|
384
448
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
385
|
-
|
|
386
|
-
clearTimeout(maxRetryCooldownRef.current);
|
|
387
|
-
}
|
|
449
|
+
clearTimeoutSafe(maxRetryCooldownRef);
|
|
388
450
|
cleanup();
|
|
389
451
|
};
|
|
390
452
|
}, [connect, cleanup]);
|
|
391
453
|
const contextValue = useMemo(
|
|
392
|
-
() => ({
|
|
393
|
-
bridge,
|
|
394
|
-
status,
|
|
395
|
-
error,
|
|
396
|
-
reconnect,
|
|
397
|
-
isServiceWorkerAlive
|
|
398
|
-
}),
|
|
454
|
+
() => ({ bridge, status, error, reconnect, isServiceWorkerAlive }),
|
|
399
455
|
[bridge, status, error, reconnect, isServiceWorkerAlive]
|
|
400
456
|
);
|
|
401
457
|
return /* @__PURE__ */ jsx(BridgeContext.Provider, { value: contextValue, children });
|