@chromahq/react 1.0.35 → 1.0.39

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
@@ -8,6 +8,12 @@ interface Bridge {
8
8
  off: (key: string, handler: (payload: unknown) => void) => void;
9
9
  isConnected: boolean;
10
10
  ping: () => Promise<boolean>;
11
+ /**
12
+ * Pause health checks for the specified duration.
13
+ * Use this before calling a message that triggers heavy/blocking operations in the SW.
14
+ * @param durationMs - How long to pause health checks in milliseconds
15
+ */
16
+ pauseHealthChecks: (durationMs: number) => void;
11
17
  }
12
18
  interface BridgeContextValue {
13
19
  bridge: Bridge | null;
@@ -26,23 +32,19 @@ interface BridgeProviderProps {
26
32
  pingInterval?: number;
27
33
  /** How long to wait before resetting retry count in ms. Default: 30000 */
28
34
  maxRetryCooldown?: number;
29
- /** Default timeout for messages in ms. Default: 10000 */
35
+ /** Default timeout for messages in ms. Default: 30000 */
30
36
  defaultTimeout?: number;
37
+ /**
38
+ * Timeout threshold for counting failures toward reconnection.
39
+ * Only requests with timeouts <= this value are counted as potential SW issues.
40
+ * Requests with longer timeouts (intentional slow operations) won't trigger reconnection.
41
+ * Default: 15000 (15s)
42
+ */
43
+ timeoutFailureThreshold?: number;
31
44
  /** Callback when connection status changes */
32
45
  onConnectionChange?: (status: ConnectionStatus) => void;
33
46
  /** Callback when an error occurs */
34
47
  onError?: (error: Error) => void;
35
- /**
36
- * Optional function to check if health checks should be paused.
37
- * Return a timestamp (ms) until which health checks are paused, or null/0 if not paused.
38
- * This allows service workers to pause health monitoring during heavy operations.
39
- *
40
- * @example
41
- * ```tsx
42
- * <BridgeProvider isHealthPaused={() => store.getState().healthPausedUntil}>
43
- * ```
44
- */
45
- isHealthPausedUntil?: () => number | null | undefined;
46
48
  }
47
49
  declare const BridgeProvider: FC<BridgeProviderProps>;
48
50
 
package/dist/index.js CHANGED
@@ -5,22 +5,26 @@ const BRIDGE_ENABLE_LOGS = true;
5
5
  const CONFIG = {
6
6
  RETRY_AFTER: 1e3,
7
7
  MAX_RETRIES: 10,
8
- PING_INTERVAL: 3e3,
9
- // Check every 3s (balance between responsiveness and false positives)
8
+ PING_INTERVAL: 5e3,
9
+ // Check every 5s (reduced frequency to avoid false positives during heavy operations)
10
10
  MAX_RETRY_COOLDOWN: 3e4,
11
- DEFAULT_TIMEOUT: 1e4,
11
+ DEFAULT_TIMEOUT: 3e4,
12
+ // Increased from 20s to 30s for slow operations
12
13
  MAX_RETRY_DELAY: 3e4,
13
- PING_TIMEOUT: 5e3,
14
- // Give SW 5s to respond (handles busy periods)
14
+ PING_TIMEOUT: 1e4,
15
+ // Give SW 10s to respond (handles busy periods like large storage reads)
15
16
  ERROR_CHECK_INTERVAL: 100,
16
17
  MAX_ERROR_CHECKS: 10,
17
- CONSECUTIVE_FAILURE_THRESHOLD: 3,
18
- // Require 3 consecutive failures (9s total) before reconnecting
18
+ CONSECUTIVE_FAILURE_THRESHOLD: 5,
19
+ // Require 5 consecutive failures (25s total) before reconnecting
19
20
  RECONNECT_DELAY: 100,
20
21
  PORT_NAME: "chroma-bridge",
21
22
  // Service worker restart retry settings (indefinite retries)
22
23
  SW_RESTART_RETRY_DELAY: 500,
23
- SW_RESTART_MAX_DELAY: 5e3
24
+ SW_RESTART_MAX_DELAY: 5e3,
25
+ // Threshold for counting timeouts toward reconnection (only count fast timeouts as failures)
26
+ TIMEOUT_FAILURE_THRESHOLD_MS: 15e3
27
+ // Only count timeouts < 15s as potential SW issues
24
28
  };
25
29
  const calculateBackoffDelay = (attempt, baseDelay, maxDelay) => Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
26
30
  const clearTimeoutSafe = (ref) => {
@@ -50,15 +54,19 @@ function createBridgeInstance(deps) {
50
54
  isConnectedRef,
51
55
  consecutiveTimeoutsRef,
52
56
  reconnectionGracePeriodRef,
57
+ healthPausedUntilRef,
53
58
  defaultTimeout,
59
+ timeoutFailureThreshold,
54
60
  onReconnectNeeded
55
61
  } = deps;
56
62
  const rejectAllPending = (message) => {
57
- pendingRequestsRef.current.forEach(({ reject, timeout }) => {
58
- clearTimeout(timeout);
59
- reject(new Error(message));
63
+ pendingRequestsRef.current.forEach(({ reject, timeout, timeoutDuration }, id) => {
64
+ if (timeoutDuration <= timeoutFailureThreshold) {
65
+ clearTimeout(timeout);
66
+ reject(new Error(message));
67
+ pendingRequestsRef.current.delete(id);
68
+ }
60
69
  });
61
- pendingRequestsRef.current.clear();
62
70
  };
63
71
  const send = (key, payload, timeoutDuration = defaultTimeout) => {
64
72
  return new Promise((resolve, reject) => {
@@ -70,15 +78,16 @@ function createBridgeInstance(deps) {
70
78
  const timeout = setTimeout(() => {
71
79
  if (!pendingRequestsRef.current.has(id)) return;
72
80
  pendingRequestsRef.current.delete(id);
73
- if (!reconnectionGracePeriodRef.current) {
81
+ const isShortTimeout = timeoutDuration <= timeoutFailureThreshold;
82
+ if (!reconnectionGracePeriodRef.current && isShortTimeout) {
74
83
  consecutiveTimeoutsRef.current++;
75
84
  }
76
85
  {
77
86
  console.warn(
78
- `[Bridge] Request timed out: ${key} (${timeoutDuration}ms)${reconnectionGracePeriodRef.current ? " [grace period]" : ""}`
87
+ `[Bridge] Request timed out: ${key} (${timeoutDuration}ms)${reconnectionGracePeriodRef.current ? " [grace period]" : ""}${!isShortTimeout ? " [long operation, not counted toward reconnect]" : ""}`
79
88
  );
80
89
  }
81
- if (!reconnectionGracePeriodRef.current && consecutiveTimeoutsRef.current >= CONFIG.CONSECUTIVE_FAILURE_THRESHOLD) {
90
+ if (!reconnectionGracePeriodRef.current && isShortTimeout && consecutiveTimeoutsRef.current >= CONFIG.CONSECUTIVE_FAILURE_THRESHOLD) {
82
91
  {
83
92
  console.warn(
84
93
  `[Bridge] ${consecutiveTimeoutsRef.current} consecutive timeouts, reconnecting...`
@@ -93,7 +102,8 @@ function createBridgeInstance(deps) {
93
102
  pendingRequestsRef.current.set(id, {
94
103
  resolve,
95
104
  reject,
96
- timeout
105
+ timeout,
106
+ timeoutDuration
97
107
  });
98
108
  try {
99
109
  portRef.current.postMessage({ id, key, payload });
@@ -167,6 +177,13 @@ function createBridgeInstance(deps) {
167
177
  } catch {
168
178
  return false;
169
179
  }
180
+ },
181
+ pauseHealthChecks: (durationMs) => {
182
+ const pauseUntil = Date.now() + durationMs;
183
+ healthPausedUntilRef.current = pauseUntil;
184
+ {
185
+ console.log(`[Bridge] Health checks paused for ${Math.round(durationMs / 1e3)}s`);
186
+ }
170
187
  }
171
188
  };
172
189
  return bridge;
@@ -176,11 +193,11 @@ function startHealthMonitor(deps) {
176
193
  bridge,
177
194
  pingIntervalRef,
178
195
  consecutivePingFailuresRef,
196
+ healthPausedUntilRef,
179
197
  pingInterval,
180
198
  setIsServiceWorkerAlive,
181
199
  onReconnectNeeded,
182
- rejectAllPendingRequests,
183
- isHealthPausedUntil
200
+ rejectAllPendingRequests
184
201
  } = deps;
185
202
  clearIntervalSafe(pingIntervalRef);
186
203
  consecutivePingFailuresRef.current = 0;
@@ -194,7 +211,7 @@ function startHealthMonitor(deps) {
194
211
  }
195
212
  return;
196
213
  }
197
- const pausedUntil = isHealthPausedUntil?.() ?? 0;
214
+ const pausedUntil = healthPausedUntilRef.current;
198
215
  if (pausedUntil && Date.now() < pausedUntil) {
199
216
  {
200
217
  const remainingMs = pausedUntil - Date.now();
@@ -207,7 +224,7 @@ function startHealthMonitor(deps) {
207
224
  }
208
225
  const alive = await bridge.ping();
209
226
  if (!pingIntervalRef.current) return;
210
- const pausedUntilAfterPing = isHealthPausedUntil?.() ?? 0;
227
+ const pausedUntilAfterPing = healthPausedUntilRef.current;
211
228
  if (pausedUntilAfterPing && Date.now() < pausedUntilAfterPing) {
212
229
  consecutivePingFailuresRef.current = 0;
213
230
  return;
@@ -240,9 +257,9 @@ const BridgeProvider = ({
240
257
  pingInterval = CONFIG.PING_INTERVAL,
241
258
  maxRetryCooldown = CONFIG.MAX_RETRY_COOLDOWN,
242
259
  defaultTimeout = CONFIG.DEFAULT_TIMEOUT,
260
+ timeoutFailureThreshold = CONFIG.TIMEOUT_FAILURE_THRESHOLD_MS,
243
261
  onConnectionChange,
244
- onError,
245
- isHealthPausedUntil
262
+ onError
246
263
  }) => {
247
264
  const [bridge, setBridge] = useState(null);
248
265
  const [status, setStatus] = useState("connecting");
@@ -260,6 +277,7 @@ const BridgeProvider = ({
260
277
  const errorCheckIntervalRef = useRef(null);
261
278
  const consecutivePingFailuresRef = useRef(0);
262
279
  const consecutiveTimeoutsRef = useRef(0);
280
+ const healthPausedUntilRef = useRef(0);
263
281
  const reconnectionGracePeriodRef = useRef(false);
264
282
  const pendingRequestsRef = useRef(/* @__PURE__ */ new Map());
265
283
  const eventListenersRef = useRef(/* @__PURE__ */ new Map());
@@ -323,6 +341,9 @@ const BridgeProvider = ({
323
341
  reject(new Error("Bridge disconnected"));
324
342
  });
325
343
  pendingRequestsRef.current.clear();
344
+ if (!emitDisconnect) {
345
+ eventListenersRef.current.clear();
346
+ }
326
347
  setIsServiceWorkerAlive(false);
327
348
  setBridge(null);
328
349
  isConnectingRef.current = false;
@@ -351,6 +372,10 @@ const BridgeProvider = ({
351
372
  }
352
373
  }
353
374
  });
375
+ return;
376
+ }
377
+ if (message.id && BRIDGE_ENABLE_LOGS) {
378
+ console.warn("[Bridge] Received response for unknown/expired request:", message.id);
354
379
  }
355
380
  }, []);
356
381
  const scheduleReconnect = useCallback(
@@ -514,7 +539,9 @@ const BridgeProvider = ({
514
539
  isConnectedRef,
515
540
  consecutiveTimeoutsRef,
516
541
  reconnectionGracePeriodRef,
542
+ healthPausedUntilRef,
517
543
  defaultTimeout,
544
+ timeoutFailureThreshold,
518
545
  onReconnectNeeded: triggerReconnect
519
546
  });
520
547
  setBridge(bridgeInstance);
@@ -543,22 +570,24 @@ const BridgeProvider = ({
543
570
  }
544
571
  });
545
572
  const rejectAllPendingRequests = (message) => {
546
- pendingRequestsRef.current.forEach(({ reject, timeout }) => {
547
- clearTimeout(timeout);
548
- reject(new Error(message));
573
+ pendingRequestsRef.current.forEach(({ reject, timeout, timeoutDuration }, id) => {
574
+ if (timeoutDuration <= timeoutFailureThreshold) {
575
+ clearTimeout(timeout);
576
+ reject(new Error(message));
577
+ pendingRequestsRef.current.delete(id);
578
+ }
549
579
  });
550
- pendingRequestsRef.current.clear();
551
580
  consecutiveTimeoutsRef.current = 0;
552
581
  };
553
582
  startHealthMonitor({
554
583
  bridge: bridgeInstance,
555
584
  pingIntervalRef,
556
585
  consecutivePingFailuresRef,
586
+ healthPausedUntilRef,
557
587
  pingInterval,
558
588
  setIsServiceWorkerAlive,
559
589
  onReconnectNeeded: triggerReconnect,
560
- rejectAllPendingRequests,
561
- isHealthPausedUntil
590
+ rejectAllPendingRequests
562
591
  });
563
592
  } catch (e) {
564
593
  isConnectingRef.current = false;
@@ -574,8 +603,7 @@ const BridgeProvider = ({
574
603
  scheduleSwRestartReconnect,
575
604
  defaultTimeout,
576
605
  updateStatus,
577
- pingInterval,
578
- isHealthPausedUntil
606
+ pingInterval
579
607
  ]);
580
608
  const reconnect = useCallback(() => {
581
609
  retryCountRef.current = 0;
@@ -606,17 +634,21 @@ const BridgeProvider = ({
606
634
  isConnectingRef.current = false;
607
635
  connect();
608
636
  } else if (currentStatus === "connected" && currentBridge) {
637
+ if (isConnectingRef.current) return;
609
638
  currentBridge.ping().then((alive) => {
610
639
  if (!isMountedRef.current) return;
640
+ if (isConnectingRef.current) return;
611
641
  if (!alive) {
612
642
  {
613
643
  console.warn("[Bridge] Tab visible but unresponsive, reconnecting...");
614
644
  }
615
645
  retryCountRef.current = 0;
646
+ swRestartRetryCountRef.current = 0;
616
647
  isConnectingRef.current = false;
617
648
  cleanup();
618
649
  connect();
619
650
  }
651
+ }).catch(() => {
620
652
  });
621
653
  }
622
654
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chromahq/react",
3
- "version": "1.0.35",
3
+ "version": "1.0.39",
4
4
  "description": "React bindings for the Chroma Chrome extension framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",