@chromahq/react 1.0.40 → 1.0.42

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,5 +1,27 @@
1
1
  import { FC, ReactNode } from 'react';
2
2
 
3
+ interface BridgeDiagnosticsSnapshot {
4
+ lastUpdatedAt: number;
5
+ portDisconnects: number;
6
+ lastDisconnectError?: string;
7
+ lastDisconnectAt?: number;
8
+ sendMessageFallbacks: number;
9
+ lastFallbackReason?: string;
10
+ lastFallbackAt?: number;
11
+ sendMessageFallbackErrors: number;
12
+ lastFallbackError?: string;
13
+ pingFailures: number;
14
+ lastPingFailureAt?: number;
15
+ }
16
+ interface BridgeDiagnosticsApi {
17
+ get: () => BridgeDiagnosticsSnapshot;
18
+ reset: () => void;
19
+ }
20
+ declare global {
21
+ interface Window {
22
+ __CHROMA_BRIDGE_DIAGNOSTICS__?: BridgeDiagnosticsApi;
23
+ }
24
+ }
3
25
  type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error' | 'reconnecting';
4
26
  interface Bridge {
5
27
  send: <Req = unknown, Res = unknown>(key: string, payload?: Req, timeoutDuration?: number) => Promise<Res>;
package/dist/index.js CHANGED
@@ -2,29 +2,92 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import { createContext, useState, useRef, useEffect, useCallback, useMemo, useContext, useSyncExternalStore } from 'react';
3
3
 
4
4
  const BRIDGE_ENABLE_LOGS = true;
5
+ const DIRECT_MESSAGE_FLAG = "__CHROMA_BRIDGE_DIRECT_MESSAGE__";
6
+ const createInitialDiagnosticsState = () => ({
7
+ lastUpdatedAt: Date.now(),
8
+ portDisconnects: 0,
9
+ lastDisconnectError: void 0,
10
+ lastDisconnectAt: void 0,
11
+ sendMessageFallbacks: 0,
12
+ lastFallbackReason: void 0,
13
+ lastFallbackAt: void 0,
14
+ sendMessageFallbackErrors: 0,
15
+ lastFallbackError: void 0,
16
+ pingFailures: 0,
17
+ lastPingFailureAt: void 0
18
+ });
19
+ const bridgeDiagnosticsState = createInitialDiagnosticsState();
20
+ const updateDiagnosticsState = (mutator) => {
21
+ mutator(bridgeDiagnosticsState);
22
+ bridgeDiagnosticsState.lastUpdatedAt = Date.now();
23
+ };
24
+ const resetDiagnosticsState = () => {
25
+ Object.assign(bridgeDiagnosticsState, createInitialDiagnosticsState());
26
+ };
27
+ const attachDiagnosticsApi = () => {
28
+ if (typeof window === "undefined") {
29
+ return;
30
+ }
31
+ window.__CHROMA_BRIDGE_DIAGNOSTICS__ = {
32
+ get: () => ({ ...bridgeDiagnosticsState }),
33
+ reset: () => {
34
+ resetDiagnosticsState();
35
+ }
36
+ };
37
+ };
38
+ attachDiagnosticsApi();
39
+ const recordDiagnostics = {
40
+ portDisconnect: (error) => {
41
+ updateDiagnosticsState((state) => {
42
+ state.portDisconnects += 1;
43
+ state.lastDisconnectError = error;
44
+ state.lastDisconnectAt = Date.now();
45
+ });
46
+ },
47
+ fallbackSent: (reason) => {
48
+ updateDiagnosticsState((state) => {
49
+ state.sendMessageFallbacks += 1;
50
+ state.lastFallbackReason = reason;
51
+ state.lastFallbackAt = Date.now();
52
+ });
53
+ },
54
+ fallbackError: (error) => {
55
+ updateDiagnosticsState((state) => {
56
+ state.sendMessageFallbackErrors += 1;
57
+ state.lastFallbackError = error;
58
+ });
59
+ },
60
+ pingFailure: () => {
61
+ updateDiagnosticsState((state) => {
62
+ state.pingFailures += 1;
63
+ state.lastPingFailureAt = Date.now();
64
+ });
65
+ }
66
+ };
5
67
  const CONFIG = {
6
68
  RETRY_AFTER: 1e3,
7
69
  MAX_RETRIES: 10,
8
- PING_INTERVAL: 5e3,
9
- // Check every 5s (reduced frequency to avoid false positives during heavy operations)
70
+ PING_INTERVAL: 15e3,
71
+ // Check every 15s (tolerant of long operations)
10
72
  MAX_RETRY_COOLDOWN: 3e4,
11
- DEFAULT_TIMEOUT: 3e4,
12
- // Increased from 20s to 30s for slow operations
73
+ DEFAULT_TIMEOUT: 6e4,
74
+ // 60s default for slow operations
13
75
  MAX_RETRY_DELAY: 3e4,
14
- PING_TIMEOUT: 1e4,
15
- // Give SW 10s to respond (handles busy periods like large storage reads)
76
+ PING_TIMEOUT: 2e4,
77
+ // Give SW 20s to respond to ping
16
78
  ERROR_CHECK_INTERVAL: 100,
17
79
  MAX_ERROR_CHECKS: 10,
18
80
  CONSECUTIVE_FAILURE_THRESHOLD: 5,
19
- // Require 5 consecutive failures (25s total) before reconnecting
81
+ // Require 5 consecutive failures (75s total) before reconnecting
20
82
  RECONNECT_DELAY: 100,
21
83
  PORT_NAME: "chroma-bridge",
22
84
  // Service worker restart retry settings (indefinite retries)
23
85
  SW_RESTART_RETRY_DELAY: 500,
24
86
  SW_RESTART_MAX_DELAY: 5e3,
25
87
  // 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
88
+ // Requests with timeout > this value are considered intentional long operations
89
+ TIMEOUT_FAILURE_THRESHOLD_MS: 3e4
90
+ // Only count timeouts < 30s as potential SW issues
28
91
  };
29
92
  const calculateBackoffDelay = (attempt, baseDelay, maxDelay) => Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
30
93
  const clearTimeoutSafe = (ref) => {
@@ -70,11 +133,71 @@ function createBridgeInstance(deps) {
70
133
  };
71
134
  const send = (key, payload, timeoutDuration = defaultTimeout) => {
72
135
  return new Promise((resolve, reject) => {
73
- if (!portRef.current) {
74
- reject(new Error("Bridge disconnected"));
75
- return;
76
- }
77
136
  const id = `msg_${++messageIdRef.current}`;
137
+ const finalizePendingWithError = (error) => {
138
+ const pending = pendingRequestsRef.current.get(id);
139
+ if (!pending) return;
140
+ clearTimeout(pending.timeout);
141
+ pendingRequestsRef.current.delete(id);
142
+ pending.reject(error instanceof Error ? error : new Error(error));
143
+ };
144
+ const triggerRuntimeFallback = (reason) => {
145
+ {
146
+ console.warn(`[Bridge] Falling back to runtime.sendMessage (${reason})`);
147
+ }
148
+ recordDiagnostics.fallbackSent(reason);
149
+ onReconnectNeeded();
150
+ if (!chrome?.runtime?.sendMessage) {
151
+ const message = "chrome.runtime.sendMessage not available";
152
+ recordDiagnostics.fallbackError(message);
153
+ finalizePendingWithError(message);
154
+ return;
155
+ }
156
+ try {
157
+ chrome.runtime.sendMessage(
158
+ {
159
+ id,
160
+ key,
161
+ payload,
162
+ metadata: { transport: "direct", fallbackReason: reason },
163
+ [DIRECT_MESSAGE_FLAG]: true
164
+ },
165
+ (response) => {
166
+ const runtimeError = consumeRuntimeError();
167
+ const pending = pendingRequestsRef.current.get(id);
168
+ if (!pending) return;
169
+ if (runtimeError) {
170
+ recordDiagnostics.fallbackError(runtimeError);
171
+ clearTimeout(pending.timeout);
172
+ pendingRequestsRef.current.delete(id);
173
+ pending.reject(new Error(runtimeError));
174
+ return;
175
+ }
176
+ if (!response) {
177
+ const message = "No response from service worker";
178
+ recordDiagnostics.fallbackError(message);
179
+ clearTimeout(pending.timeout);
180
+ pendingRequestsRef.current.delete(id);
181
+ pending.reject(new Error(message));
182
+ return;
183
+ }
184
+ clearTimeout(pending.timeout);
185
+ pendingRequestsRef.current.delete(id);
186
+ consecutiveTimeoutsRef.current = 0;
187
+ if (response.error) {
188
+ recordDiagnostics.fallbackError(response.error);
189
+ pending.reject(new Error(response.error));
190
+ } else {
191
+ pending.resolve(response.data);
192
+ }
193
+ }
194
+ );
195
+ } catch (error) {
196
+ const message = error instanceof Error ? error.message : "chrome.runtime.sendMessage failed";
197
+ recordDiagnostics.fallbackError(message);
198
+ finalizePendingWithError(error instanceof Error ? error : new Error(message));
199
+ }
200
+ };
78
201
  const timeout = setTimeout(() => {
79
202
  if (!pendingRequestsRef.current.has(id)) return;
80
203
  pendingRequestsRef.current.delete(id);
@@ -105,17 +228,16 @@ function createBridgeInstance(deps) {
105
228
  timeout,
106
229
  timeoutDuration
107
230
  });
231
+ if (!portRef.current) {
232
+ triggerRuntimeFallback("port-unavailable");
233
+ return;
234
+ }
108
235
  try {
109
236
  portRef.current.postMessage({ id, key, payload });
110
237
  setTimeout(() => {
111
238
  const errorMessage = consumeRuntimeError();
112
239
  if (errorMessage && pendingRequestsRef.current.has(id)) {
113
- const pending = pendingRequestsRef.current.get(id);
114
- if (pending) {
115
- clearTimeout(pending.timeout);
116
- pendingRequestsRef.current.delete(id);
117
- reject(new Error(errorMessage));
118
- }
240
+ finalizePendingWithError(errorMessage);
119
241
  }
120
242
  }, 0);
121
243
  const immediateError = consumeRuntimeError();
@@ -123,12 +245,10 @@ function createBridgeInstance(deps) {
123
245
  throw new Error(immediateError);
124
246
  }
125
247
  } catch (e) {
126
- const pending = pendingRequestsRef.current.get(id);
127
- if (pending) {
128
- clearTimeout(pending.timeout);
129
- pendingRequestsRef.current.delete(id);
248
+ {
249
+ console.warn("[Bridge] Port send failed, attempting fallback", e);
130
250
  }
131
- reject(e instanceof Error ? e : new Error("Send failed"));
251
+ triggerRuntimeFallback("port-postmessage-error");
132
252
  }
133
253
  });
134
254
  };
@@ -234,6 +354,7 @@ function startHealthMonitor(deps) {
234
354
  consecutivePingFailuresRef.current = 0;
235
355
  return;
236
356
  }
357
+ recordDiagnostics.pingFailure();
237
358
  consecutivePingFailuresRef.current++;
238
359
  {
239
360
  console.warn(`[Bridge] Ping failed (${consecutivePingFailuresRef.current}x)`);
@@ -501,6 +622,7 @@ const BridgeProvider = ({
501
622
  }
502
623
  isConnectingRef.current = false;
503
624
  const disconnectError = consumeRuntimeError();
625
+ recordDiagnostics.portDisconnect(disconnectError);
504
626
  if (BRIDGE_ENABLE_LOGS) {
505
627
  console.warn("[Bridge] Disconnect error:", disconnectError || "(none)");
506
628
  console.warn("[Bridge] isMounted:", isMountedRef.current);
@@ -559,7 +681,7 @@ const BridgeProvider = ({
559
681
  if (BRIDGE_ENABLE_LOGS) {
560
682
  console.log("[Bridge] Grace period ended, timeout monitoring active");
561
683
  }
562
- }, 3e3);
684
+ }, 1e4);
563
685
  eventListenersRef.current.get("bridge:connected")?.forEach((handler) => {
564
686
  try {
565
687
  handler({ timestamp: Date.now() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chromahq/react",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "description": "React bindings for the Chroma Chrome extension framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",