@chromahq/react 1.0.15 → 1.0.16
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 +344 -299
- 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,27 @@ 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);
|
|
31
203
|
const pingIntervalRef = useRef(null);
|
|
204
|
+
const errorCheckIntervalRef = useRef(null);
|
|
205
|
+
const consecutivePingFailuresRef = useRef(0);
|
|
32
206
|
const consecutiveTimeoutsRef = useRef(0);
|
|
207
|
+
const pendingRequestsRef = useRef(/* @__PURE__ */ new Map());
|
|
208
|
+
const eventListenersRef = useRef(/* @__PURE__ */ new Map());
|
|
209
|
+
const messageIdRef = useRef(0);
|
|
210
|
+
const statusRef = useRef(status);
|
|
211
|
+
const bridgeRef = useRef(bridge);
|
|
212
|
+
const isMountedRef = useRef(true);
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
statusRef.current = status;
|
|
215
|
+
}, [status]);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
bridgeRef.current = bridge;
|
|
218
|
+
}, [bridge]);
|
|
33
219
|
const updateStatus = useCallback(
|
|
34
220
|
(newStatus) => {
|
|
35
221
|
setStatus(newStatus);
|
|
@@ -46,356 +232,215 @@ const BridgeProvider = ({
|
|
|
46
232
|
[onError, updateStatus]
|
|
47
233
|
);
|
|
48
234
|
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
|
-
}
|
|
235
|
+
clearTimeoutSafe(reconnectTimeoutRef);
|
|
236
|
+
clearIntervalSafe(errorCheckIntervalRef);
|
|
237
|
+
clearIntervalSafe(pingIntervalRef);
|
|
61
238
|
if (portRef.current) {
|
|
62
239
|
try {
|
|
63
240
|
portRef.current.disconnect();
|
|
64
|
-
} catch
|
|
241
|
+
} catch {
|
|
65
242
|
}
|
|
66
243
|
portRef.current = null;
|
|
67
244
|
}
|
|
68
|
-
|
|
245
|
+
pendingRequestsRef.current.forEach(({ reject, timeout }) => {
|
|
69
246
|
clearTimeout(timeout);
|
|
70
247
|
reject(new Error("Bridge disconnected"));
|
|
71
248
|
});
|
|
72
|
-
|
|
249
|
+
pendingRequestsRef.current.clear();
|
|
73
250
|
eventListenersRef.current.clear();
|
|
74
251
|
setIsServiceWorkerAlive(false);
|
|
75
252
|
setBridge(null);
|
|
76
253
|
isConnectingRef.current = false;
|
|
254
|
+
isConnectedRef.current = false;
|
|
255
|
+
}, []);
|
|
256
|
+
const handleMessage = useCallback((message) => {
|
|
257
|
+
if (message.id && pendingRequestsRef.current.has(message.id)) {
|
|
258
|
+
const pending = pendingRequestsRef.current.get(message.id);
|
|
259
|
+
clearTimeout(pending.timeout);
|
|
260
|
+
consecutiveTimeoutsRef.current = 0;
|
|
261
|
+
if (message.error) {
|
|
262
|
+
pending.reject(new Error(message.error));
|
|
263
|
+
} else {
|
|
264
|
+
pending.resolve(message.data);
|
|
265
|
+
}
|
|
266
|
+
pendingRequestsRef.current.delete(message.id);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (message.type === "broadcast" && message.key) {
|
|
270
|
+
eventListenersRef.current.get(message.key)?.forEach((handler) => {
|
|
271
|
+
try {
|
|
272
|
+
handler(message.payload);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.warn("[Bridge] Event handler error:", err);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
77
278
|
}, []);
|
|
279
|
+
const scheduleReconnect = useCallback(
|
|
280
|
+
(connectFn) => {
|
|
281
|
+
if (retryCountRef.current < maxRetries) {
|
|
282
|
+
retryCountRef.current++;
|
|
283
|
+
const delay = calculateBackoffDelay(
|
|
284
|
+
retryCountRef.current,
|
|
285
|
+
retryAfter,
|
|
286
|
+
CONFIG.MAX_RETRY_DELAY
|
|
287
|
+
);
|
|
288
|
+
console.log(`[Bridge] Reconnecting in ${delay}ms (${retryCountRef.current}/${maxRetries})`);
|
|
289
|
+
updateStatus("reconnecting");
|
|
290
|
+
reconnectTimeoutRef.current = setTimeout(connectFn, delay);
|
|
291
|
+
} else {
|
|
292
|
+
console.warn(`[Bridge] Max retries reached. Cooldown: ${maxRetryCooldown}ms`);
|
|
293
|
+
clearTimeoutSafe(maxRetryCooldownRef);
|
|
294
|
+
maxRetryCooldownRef.current = setTimeout(() => {
|
|
295
|
+
console.log("[Bridge] Cooldown complete, reconnecting...");
|
|
296
|
+
retryCountRef.current = 0;
|
|
297
|
+
connectFn();
|
|
298
|
+
}, maxRetryCooldown);
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
[maxRetries, retryAfter, maxRetryCooldown, updateStatus]
|
|
302
|
+
);
|
|
78
303
|
const connect = useCallback(() => {
|
|
79
|
-
if (isConnectingRef.current
|
|
80
|
-
|
|
304
|
+
if (isConnectingRef.current) return;
|
|
305
|
+
if (retryCountRef.current >= maxRetries) {
|
|
306
|
+
console.warn("[Bridge] Waiting for cooldown...");
|
|
81
307
|
return;
|
|
82
308
|
}
|
|
83
309
|
isConnectingRef.current = true;
|
|
84
310
|
cleanup();
|
|
85
311
|
if (!chrome?.runtime?.connect) {
|
|
86
312
|
handleError(new Error("Chrome runtime not available"));
|
|
313
|
+
isConnectingRef.current = false;
|
|
87
314
|
return;
|
|
88
315
|
}
|
|
89
316
|
try {
|
|
90
|
-
const port = chrome.runtime.connect({ name:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
317
|
+
const port = chrome.runtime.connect({ name: CONFIG.PORT_NAME });
|
|
318
|
+
const immediateError = consumeRuntimeError();
|
|
319
|
+
if (immediateError) throw new Error(immediateError);
|
|
94
320
|
portRef.current = port;
|
|
95
|
-
let
|
|
96
|
-
const maxErrorChecks = 10;
|
|
321
|
+
let errorChecks = 0;
|
|
97
322
|
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");
|
|
323
|
+
errorChecks++;
|
|
324
|
+
const err = consumeRuntimeError();
|
|
325
|
+
if (err) {
|
|
326
|
+
clearIntervalSafe(errorCheckIntervalRef);
|
|
327
|
+
if (err.includes("Receiving end does not exist")) {
|
|
328
|
+
console.warn("[Bridge] Background not ready, retrying...");
|
|
109
329
|
cleanup();
|
|
110
330
|
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;
|
|
331
|
+
scheduleReconnect(connect);
|
|
122
332
|
}
|
|
333
|
+
return;
|
|
123
334
|
}
|
|
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
|
-
}
|
|
335
|
+
if (errorChecks >= CONFIG.MAX_ERROR_CHECKS) {
|
|
336
|
+
clearIntervalSafe(errorCheckIntervalRef);
|
|
147
337
|
}
|
|
148
|
-
});
|
|
338
|
+
}, CONFIG.ERROR_CHECK_INTERVAL);
|
|
339
|
+
port.onMessage.addListener(handleMessage);
|
|
149
340
|
port.onDisconnect.addListener(() => {
|
|
150
|
-
console.warn("[Bridge]
|
|
341
|
+
console.warn("[Bridge] Disconnected");
|
|
151
342
|
isConnectingRef.current = false;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
156
|
-
chrome.runtime.lastError;
|
|
343
|
+
const disconnectError = consumeRuntimeError();
|
|
344
|
+
if (disconnectError) {
|
|
345
|
+
handleError(new Error(disconnectError));
|
|
157
346
|
} else {
|
|
158
347
|
updateStatus("disconnected");
|
|
159
348
|
}
|
|
160
349
|
cleanup();
|
|
161
|
-
|
|
162
|
-
retryCountRef.current++;
|
|
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);
|
|
183
|
-
}
|
|
350
|
+
scheduleReconnect(connect);
|
|
184
351
|
});
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (pendingRef.current.has(id)) {
|
|
195
|
-
pendingRef.current.delete(id);
|
|
196
|
-
consecutiveTimeoutsRef.current++;
|
|
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
|
-
}
|
|
352
|
+
const triggerReconnect = () => {
|
|
353
|
+
setIsServiceWorkerAlive(false);
|
|
354
|
+
updateStatus("reconnecting");
|
|
355
|
+
retryCountRef.current = 0;
|
|
356
|
+
isConnectingRef.current = false;
|
|
357
|
+
setTimeout(() => {
|
|
358
|
+
cleanup();
|
|
359
|
+
connect();
|
|
360
|
+
}, CONFIG.RECONNECT_DELAY);
|
|
298
361
|
};
|
|
362
|
+
const bridgeInstance = createBridgeInstance({
|
|
363
|
+
portRef,
|
|
364
|
+
pendingRequestsRef,
|
|
365
|
+
eventListenersRef,
|
|
366
|
+
messageIdRef,
|
|
367
|
+
isConnectedRef,
|
|
368
|
+
consecutiveTimeoutsRef,
|
|
369
|
+
defaultTimeout,
|
|
370
|
+
onReconnectNeeded: triggerReconnect
|
|
371
|
+
});
|
|
299
372
|
setBridge(bridgeInstance);
|
|
373
|
+
isConnectedRef.current = true;
|
|
300
374
|
updateStatus("connected");
|
|
301
375
|
setIsServiceWorkerAlive(true);
|
|
302
376
|
setError(null);
|
|
303
377
|
retryCountRef.current = 0;
|
|
304
378
|
consecutiveTimeoutsRef.current = 0;
|
|
305
379
|
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);
|
|
380
|
+
startHealthMonitor({
|
|
381
|
+
bridge: bridgeInstance,
|
|
382
|
+
pingIntervalRef,
|
|
383
|
+
consecutivePingFailuresRef,
|
|
384
|
+
pingInterval,
|
|
385
|
+
setIsServiceWorkerAlive,
|
|
386
|
+
onReconnectNeeded: triggerReconnect
|
|
387
|
+
});
|
|
333
388
|
} catch (e) {
|
|
334
389
|
isConnectingRef.current = false;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (retryCountRef.current < maxRetries) {
|
|
338
|
-
retryCountRef.current++;
|
|
339
|
-
reconnectTimeoutRef.current = setTimeout(connect, retryAfter);
|
|
340
|
-
}
|
|
390
|
+
handleError(e instanceof Error ? e : new Error("Connection failed"));
|
|
391
|
+
scheduleReconnect(connect);
|
|
341
392
|
}
|
|
342
|
-
}, [
|
|
393
|
+
}, [
|
|
394
|
+
maxRetries,
|
|
395
|
+
cleanup,
|
|
396
|
+
handleError,
|
|
397
|
+
handleMessage,
|
|
398
|
+
scheduleReconnect,
|
|
399
|
+
defaultTimeout,
|
|
400
|
+
updateStatus,
|
|
401
|
+
pingInterval
|
|
402
|
+
]);
|
|
343
403
|
const reconnect = useCallback(() => {
|
|
344
404
|
retryCountRef.current = 0;
|
|
345
405
|
consecutiveTimeoutsRef.current = 0;
|
|
346
406
|
updateStatus("connecting");
|
|
347
407
|
connect();
|
|
348
408
|
}, [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
409
|
useEffect(() => {
|
|
358
410
|
connect();
|
|
359
411
|
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
|
-
}
|
|
412
|
+
if (document.visibilityState !== "visible") return;
|
|
413
|
+
if (!isMountedRef.current) return;
|
|
414
|
+
if (isConnectingRef.current) return;
|
|
415
|
+
const currentStatus = statusRef.current;
|
|
416
|
+
const currentBridge = bridgeRef.current;
|
|
417
|
+
if (currentStatus === "disconnected" || currentStatus === "error") {
|
|
418
|
+
console.log("[Bridge] Tab visible, reconnecting...");
|
|
419
|
+
retryCountRef.current = 0;
|
|
420
|
+
connect();
|
|
421
|
+
} else if (currentStatus === "connected" && currentBridge) {
|
|
422
|
+
currentBridge.ping().then((alive) => {
|
|
423
|
+
if (!isMountedRef.current) return;
|
|
424
|
+
if (!alive) {
|
|
425
|
+
console.warn("[Bridge] Tab visible but unresponsive, reconnecting...");
|
|
426
|
+
retryCountRef.current = 0;
|
|
427
|
+
isConnectingRef.current = false;
|
|
428
|
+
cleanup();
|
|
429
|
+
connect();
|
|
430
|
+
}
|
|
431
|
+
});
|
|
380
432
|
}
|
|
381
433
|
};
|
|
382
434
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
383
435
|
return () => {
|
|
436
|
+
isMountedRef.current = false;
|
|
384
437
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
385
|
-
|
|
386
|
-
clearTimeout(maxRetryCooldownRef.current);
|
|
387
|
-
}
|
|
438
|
+
clearTimeoutSafe(maxRetryCooldownRef);
|
|
388
439
|
cleanup();
|
|
389
440
|
};
|
|
390
441
|
}, [connect, cleanup]);
|
|
391
442
|
const contextValue = useMemo(
|
|
392
|
-
() => ({
|
|
393
|
-
bridge,
|
|
394
|
-
status,
|
|
395
|
-
error,
|
|
396
|
-
reconnect,
|
|
397
|
-
isServiceWorkerAlive
|
|
398
|
-
}),
|
|
443
|
+
() => ({ bridge, status, error, reconnect, isServiceWorkerAlive }),
|
|
399
444
|
[bridge, status, error, reconnect, isServiceWorkerAlive]
|
|
400
445
|
);
|
|
401
446
|
return /* @__PURE__ */ jsx(BridgeContext.Provider, { value: contextValue, children });
|