@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 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,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 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);
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
- 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
- }
235
+ clearTimeoutSafe(reconnectTimeoutRef);
236
+ clearIntervalSafe(errorCheckIntervalRef);
237
+ clearIntervalSafe(pingIntervalRef);
61
238
  if (portRef.current) {
62
239
  try {
63
240
  portRef.current.disconnect();
64
- } catch (e) {
241
+ } catch {
65
242
  }
66
243
  portRef.current = null;
67
244
  }
68
- pendingRef.current.forEach(({ reject, timeout }) => {
245
+ pendingRequestsRef.current.forEach(({ reject, timeout }) => {
69
246
  clearTimeout(timeout);
70
247
  reject(new Error("Bridge disconnected"));
71
248
  });
72
- pendingRef.current.clear();
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 || retryCountRef.current >= maxRetries) {
80
- console.warn("[Bridge] Already connecting or max retries reached");
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: "chroma-bridge" });
91
- if (chrome.runtime.lastError) {
92
- throw new Error(chrome.runtime.lastError.message || "Failed to connect to extension");
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 errorCheckCount = 0;
96
- const maxErrorChecks = 10;
321
+ let errorChecks = 0;
97
322
  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");
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
- 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;
331
+ scheduleReconnect(connect);
122
332
  }
333
+ return;
123
334
  }
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
- }
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] disconnected");
341
+ console.warn("[Bridge] Disconnected");
151
342
  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;
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
- 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);
183
- }
350
+ scheduleReconnect(connect);
184
351
  });
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
- }
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
- 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);
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
- 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
- }
390
+ handleError(e instanceof Error ? e : new Error("Connection failed"));
391
+ scheduleReconnect(connect);
341
392
  }
342
- }, [retryAfter, maxRetries, pingInterval, handleError, updateStatus, cleanup]);
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 === "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
- }
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
- if (maxRetryCooldownRef.current) {
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chromahq/react",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "React bindings for the Chroma Chrome extension framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",