@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 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: any) => void;
5
- on: (key: string, handler: (payload: any) => void) => void;
6
- off: (key: string, handler: (payload: any) => void) => void;
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 Props {
18
- children: React.ReactNode;
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: React.FC<Props>;
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, useCallback, useEffect, useMemo, useContext } from 'react';
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 = 1e3,
8
- maxRetries = 10,
9
- pingInterval = 5e3,
10
- maxRetryCooldown = 3e4,
11
- // Reset retry count after 30s of max retries
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 maxRetryCooldownRef = useRef(null);
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 errorCheckIntervalRef = useRef(null);
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
- if (reconnectTimeoutRef.current) {
50
- clearTimeout(reconnectTimeoutRef.current);
51
- reconnectTimeoutRef.current = null;
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 (e) {
243
+ } catch {
65
244
  }
66
245
  portRef.current = null;
67
246
  }
68
- pendingRef.current.forEach(({ reject, timeout }) => {
247
+ pendingRequestsRef.current.forEach(({ reject, timeout }) => {
69
248
  clearTimeout(timeout);
70
249
  reject(new Error("Bridge disconnected"));
71
250
  });
72
- pendingRef.current.clear();
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 || retryCountRef.current >= maxRetries) {
80
- console.warn("[Bridge] Already connecting or max retries reached");
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: "chroma-bridge" });
91
- if (chrome.runtime.lastError) {
92
- throw new Error(chrome.runtime.lastError.message || "Failed to connect to extension");
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 errorCheckCount = 0;
96
- const maxErrorChecks = 10;
327
+ let errorChecks = 0;
97
328
  errorCheckIntervalRef.current = setInterval(() => {
98
- errorCheckCount++;
99
- if (chrome.runtime.lastError) {
100
- const errorMessage = chrome.runtime.lastError.message;
101
- console.warn("[Bridge] Runtime error detected:", errorMessage);
102
- chrome.runtime.lastError;
103
- if (errorCheckIntervalRef.current) {
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
- if (retryCountRef.current < maxRetries) {
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
- }, 100);
125
- port.onMessage.addListener((msg) => {
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] disconnected");
347
+ console.warn("[Bridge] Disconnected");
151
348
  isConnectingRef.current = false;
152
- if (chrome.runtime.lastError) {
153
- handleError(
154
- new Error(chrome.runtime.lastError.message || "Port disconnected with error")
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 (retryCountRef.current < maxRetries) {
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);
356
+ if (isMountedRef.current) {
357
+ scheduleReconnect(connect);
183
358
  }
184
359
  });
185
- const bridgeInstance = {
186
- send: (key, payload, timeoutDuration = 1e4) => {
187
- return new Promise((resolve, reject) => {
188
- if (!portRef.current) {
189
- reject(new Error("Bridge disconnected"));
190
- return;
191
- }
192
- const id = `msg${uidRef.current++}`;
193
- const timeout = setTimeout(() => {
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
- }
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
- if (pingIntervalRef.current) {
307
- clearInterval(pingIntervalRef.current);
308
- }
309
- consecutivePingFailuresRef.current = 0;
310
- pingIntervalRef.current = setInterval(async () => {
311
- if (bridgeInstance && portRef.current) {
312
- const alive = await bridgeInstance.ping();
313
- setIsServiceWorkerAlive(alive);
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
- const error2 = e instanceof Error ? e : new Error("Connection failed");
336
- handleError(error2);
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
- }, [retryAfter, maxRetries, pingInterval, handleError, updateStatus, cleanup]);
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 === "visible") {
361
- const currentStatus = statusRef.current;
362
- const currentBridge = bridgeRef.current;
363
- if (currentStatus === "disconnected" || currentStatus === "error") {
364
- console.log("[Bridge] Tab became visible, attempting reconnection...");
365
- retryCountRef.current = 0;
366
- connect();
367
- } else if (currentStatus === "connected" && currentBridge) {
368
- currentBridge.ping().then((alive) => {
369
- if (!alive) {
370
- console.warn(
371
- "[Bridge] Tab became visible but service worker unresponsive, reconnecting..."
372
- );
373
- retryCountRef.current = 0;
374
- isConnectingRef.current = false;
375
- cleanup();
376
- connect();
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
- if (maxRetryCooldownRef.current) {
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chromahq/react",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "React bindings for the Chroma Chrome extension framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",