@chromahq/react 1.0.23 → 1.0.25
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 +153 -2
- package/dist/index.js +64 -14
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -76,7 +76,158 @@ interface ConnectionStatusResult {
|
|
|
76
76
|
reconnect: (() => void) | undefined;
|
|
77
77
|
/** Any connection error */
|
|
78
78
|
error: Error | null | undefined;
|
|
79
|
+
/** Whether the system is fully ready (bridge connected + optional store ready) */
|
|
80
|
+
isReady: boolean;
|
|
81
|
+
/** Whether the system is loading (bridge connecting/reconnecting OR store not ready) */
|
|
82
|
+
isLoading: boolean;
|
|
79
83
|
}
|
|
80
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Store interface for readiness checking.
|
|
86
|
+
* Pass the store instance directly (e.g., `appStore`), NOT via useStore() hook.
|
|
87
|
+
*/
|
|
88
|
+
interface StoreReadyMethods {
|
|
89
|
+
onReady: (callback: () => void) => () => void;
|
|
90
|
+
isReady: () => boolean;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Hook to get unified connection status.
|
|
94
|
+
*
|
|
95
|
+
* @param store Optional store INSTANCE to include in readiness check.
|
|
96
|
+
* Pass the store directly (e.g., `appStore`), NOT via useStore() hook.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```tsx
|
|
100
|
+
* // Correct - pass store instance directly:
|
|
101
|
+
* import { appStore } from './stores/app';
|
|
102
|
+
* const { isLoading } = useConnectionStatus(appStore);
|
|
103
|
+
*
|
|
104
|
+
* // WRONG - don't use useStore() to get the store:
|
|
105
|
+
* const store = useStore(s => s); // This won't work!
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
declare const useConnectionStatus: (store?: StoreReadyMethods) => ConnectionStatusResult;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* ServiceWorkerHealth - Single source of truth for service worker health status.
|
|
112
|
+
*
|
|
113
|
+
* This module provides a centralized health monitoring system that:
|
|
114
|
+
* 1. Monitors the service worker connection via the bridge
|
|
115
|
+
* 2. Broadcasts status changes to all subscribers (bridge, store, UI)
|
|
116
|
+
* 3. Provides a simple `isHealthy` boolean for UI components
|
|
117
|
+
*
|
|
118
|
+
* Usage:
|
|
119
|
+
* ```tsx
|
|
120
|
+
* // In your app root, wrap with ServiceWorkerHealthProvider:
|
|
121
|
+
* <BridgeProvider>
|
|
122
|
+
* <ServiceWorkerHealthProvider>
|
|
123
|
+
* <App />
|
|
124
|
+
* </ServiceWorkerHealthProvider>
|
|
125
|
+
* </BridgeProvider>
|
|
126
|
+
*
|
|
127
|
+
* // In any component, use the hook:
|
|
128
|
+
* const { isHealthy, isRecovering } = useServiceWorkerHealth();
|
|
129
|
+
*
|
|
130
|
+
* if (!isHealthy) {
|
|
131
|
+
* return <Spinner message="Reconnecting..." />;
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
|
|
136
|
+
type HealthStatus = 'healthy' | 'unhealthy' | 'recovering' | 'unknown';
|
|
137
|
+
interface ServiceWorkerHealthContextValue {
|
|
138
|
+
/** Current health status */
|
|
139
|
+
status: HealthStatus;
|
|
140
|
+
/** Simple boolean: true when SW is connected and responsive */
|
|
141
|
+
isHealthy: boolean;
|
|
142
|
+
/** True when actively trying to reconnect */
|
|
143
|
+
isRecovering: boolean;
|
|
144
|
+
/** True during initial connection or recovery */
|
|
145
|
+
isLoading: boolean;
|
|
146
|
+
/** Timestamp of last successful ping (ms) */
|
|
147
|
+
lastHealthyAt: number | null;
|
|
148
|
+
/** Force a reconnection attempt */
|
|
149
|
+
forceReconnect: () => void;
|
|
150
|
+
}
|
|
151
|
+
interface ServiceWorkerHealthProviderProps {
|
|
152
|
+
children: ReactNode;
|
|
153
|
+
/**
|
|
154
|
+
* Optional callback when health status changes.
|
|
155
|
+
* Useful for stores to listen and react (e.g., pause operations).
|
|
156
|
+
*/
|
|
157
|
+
onHealthChange?: (status: HealthStatus, isHealthy: boolean) => void;
|
|
158
|
+
}
|
|
159
|
+
type HealthSubscriber = (status: HealthStatus, isHealthy: boolean) => void;
|
|
160
|
+
/**
|
|
161
|
+
* Subscribe to health status changes from outside React.
|
|
162
|
+
* Returns an unsubscribe function.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* // In BridgeStore
|
|
167
|
+
* const unsubscribe = subscribeToHealth((status, isHealthy) => {
|
|
168
|
+
* if (!isHealthy) {
|
|
169
|
+
* // Pause operations, show loading state
|
|
170
|
+
* } else {
|
|
171
|
+
* // Resume operations
|
|
172
|
+
* }
|
|
173
|
+
* });
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare function subscribeToHealth(callback: HealthSubscriber): () => void;
|
|
177
|
+
/**
|
|
178
|
+
* Get current health status synchronously.
|
|
179
|
+
* Useful for non-React code that needs immediate access.
|
|
180
|
+
*/
|
|
181
|
+
declare function getHealthStatus(): {
|
|
182
|
+
status: HealthStatus;
|
|
183
|
+
isHealthy: boolean;
|
|
184
|
+
};
|
|
185
|
+
declare const ServiceWorkerHealthProvider: FC<ServiceWorkerHealthProviderProps>;
|
|
186
|
+
/**
|
|
187
|
+
* Hook to access service worker health status.
|
|
188
|
+
*
|
|
189
|
+
* @returns Health status object with `isHealthy` boolean
|
|
190
|
+
* @throws Error if used outside ServiceWorkerHealthProvider
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```tsx
|
|
194
|
+
* function App() {
|
|
195
|
+
* const { isHealthy, isRecovering } = useServiceWorkerHealth();
|
|
196
|
+
*
|
|
197
|
+
* if (!isHealthy) {
|
|
198
|
+
* return (
|
|
199
|
+
* <div className="loading-overlay">
|
|
200
|
+
* <Spinner />
|
|
201
|
+
* <p>{isRecovering ? 'Reconnecting...' : 'Connection lost'}</p>
|
|
202
|
+
* </div>
|
|
203
|
+
* );
|
|
204
|
+
* }
|
|
205
|
+
*
|
|
206
|
+
* return <MainApp />;
|
|
207
|
+
* }
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
declare function useServiceWorkerHealth(): ServiceWorkerHealthContextValue;
|
|
211
|
+
/**
|
|
212
|
+
* Lightweight hook that directly consumes BridgeContext without needing
|
|
213
|
+
* ServiceWorkerHealthProvider. Use this for simple cases where you don't
|
|
214
|
+
* need the global subscription API.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```tsx
|
|
218
|
+
* function App() {
|
|
219
|
+
* const { isHealthy } = useServiceWorkerHealthSimple();
|
|
220
|
+
* if (!isHealthy) return <Spinner />;
|
|
221
|
+
* return <MainApp />;
|
|
222
|
+
* }
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
declare function useServiceWorkerHealthSimple(): {
|
|
226
|
+
isHealthy: boolean;
|
|
227
|
+
isRecovering: boolean;
|
|
228
|
+
isLoading: boolean;
|
|
229
|
+
reconnect: () => void;
|
|
230
|
+
};
|
|
81
231
|
|
|
82
|
-
export { BridgeProvider, useBridge, useBridgeQuery, useConnectionStatus };
|
|
232
|
+
export { BridgeProvider, ServiceWorkerHealthProvider, getHealthStatus, subscribeToHealth, useBridge, useBridgeQuery, useConnectionStatus, useServiceWorkerHealth, useServiceWorkerHealthSimple };
|
|
233
|
+
export type { HealthStatus, ServiceWorkerHealthContextValue };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
|
-
import { createContext, useState, useRef, useEffect, useCallback, useMemo, useContext } from 'react';
|
|
2
|
+
import { createContext, useState, useRef, useEffect, useCallback, useMemo, useContext, useSyncExternalStore } from 'react';
|
|
3
3
|
|
|
4
4
|
const BRIDGE_ENABLE_LOGS = true;
|
|
5
5
|
const CONFIG = {
|
|
6
6
|
RETRY_AFTER: 1e3,
|
|
7
7
|
MAX_RETRIES: 10,
|
|
8
|
-
PING_INTERVAL:
|
|
8
|
+
PING_INTERVAL: 2e3,
|
|
9
|
+
// Check every 2s for faster SW death detection
|
|
9
10
|
MAX_RETRY_COOLDOWN: 3e4,
|
|
10
11
|
DEFAULT_TIMEOUT: 1e4,
|
|
11
12
|
MAX_RETRY_DELAY: 3e4,
|
|
@@ -46,6 +47,7 @@ function createBridgeInstance(deps) {
|
|
|
46
47
|
messageIdRef,
|
|
47
48
|
isConnectedRef,
|
|
48
49
|
consecutiveTimeoutsRef,
|
|
50
|
+
reconnectionGracePeriodRef,
|
|
49
51
|
defaultTimeout,
|
|
50
52
|
onReconnectNeeded
|
|
51
53
|
} = deps;
|
|
@@ -66,11 +68,15 @@ function createBridgeInstance(deps) {
|
|
|
66
68
|
const timeout = setTimeout(() => {
|
|
67
69
|
if (!pendingRequestsRef.current.has(id)) return;
|
|
68
70
|
pendingRequestsRef.current.delete(id);
|
|
69
|
-
|
|
71
|
+
if (!reconnectionGracePeriodRef.current) {
|
|
72
|
+
consecutiveTimeoutsRef.current++;
|
|
73
|
+
}
|
|
70
74
|
{
|
|
71
|
-
console.warn(
|
|
75
|
+
console.warn(
|
|
76
|
+
`[Bridge] Request timed out: ${key} (${timeoutDuration}ms)${reconnectionGracePeriodRef.current ? " [grace period]" : ""}`
|
|
77
|
+
);
|
|
72
78
|
}
|
|
73
|
-
if (consecutiveTimeoutsRef.current >= CONFIG.CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
79
|
+
if (!reconnectionGracePeriodRef.current && consecutiveTimeoutsRef.current >= CONFIG.CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
74
80
|
{
|
|
75
81
|
console.warn(
|
|
76
82
|
`[Bridge] ${consecutiveTimeoutsRef.current} consecutive timeouts, reconnecting...`
|
|
@@ -170,7 +176,8 @@ function startHealthMonitor(deps) {
|
|
|
170
176
|
consecutivePingFailuresRef,
|
|
171
177
|
pingInterval,
|
|
172
178
|
setIsServiceWorkerAlive,
|
|
173
|
-
onReconnectNeeded
|
|
179
|
+
onReconnectNeeded,
|
|
180
|
+
rejectAllPendingRequests
|
|
174
181
|
} = deps;
|
|
175
182
|
clearIntervalSafe(pingIntervalRef);
|
|
176
183
|
consecutivePingFailuresRef.current = 0;
|
|
@@ -197,9 +204,12 @@ function startHealthMonitor(deps) {
|
|
|
197
204
|
}
|
|
198
205
|
if (consecutivePingFailuresRef.current >= CONFIG.CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
199
206
|
{
|
|
200
|
-
console.warn(
|
|
207
|
+
console.warn(
|
|
208
|
+
"[Bridge] Service worker unresponsive, rejecting pending requests and reconnecting..."
|
|
209
|
+
);
|
|
201
210
|
}
|
|
202
211
|
consecutivePingFailuresRef.current = 0;
|
|
212
|
+
rejectAllPendingRequests("Service worker unresponsive");
|
|
203
213
|
onReconnectNeeded();
|
|
204
214
|
}
|
|
205
215
|
}, pingInterval);
|
|
@@ -230,6 +240,7 @@ const BridgeProvider = ({
|
|
|
230
240
|
const errorCheckIntervalRef = useRef(null);
|
|
231
241
|
const consecutivePingFailuresRef = useRef(0);
|
|
232
242
|
const consecutiveTimeoutsRef = useRef(0);
|
|
243
|
+
const reconnectionGracePeriodRef = useRef(false);
|
|
233
244
|
const pendingRequestsRef = useRef(/* @__PURE__ */ new Map());
|
|
234
245
|
const eventListenersRef = useRef(/* @__PURE__ */ new Map());
|
|
235
246
|
const messageIdRef = useRef(0);
|
|
@@ -263,7 +274,18 @@ const BridgeProvider = ({
|
|
|
263
274
|
},
|
|
264
275
|
[onError, updateStatus]
|
|
265
276
|
);
|
|
266
|
-
const cleanup = useCallback(() => {
|
|
277
|
+
const cleanup = useCallback((emitDisconnect = true) => {
|
|
278
|
+
if (emitDisconnect) {
|
|
279
|
+
eventListenersRef.current.get("bridge:disconnected")?.forEach((handler) => {
|
|
280
|
+
try {
|
|
281
|
+
handler(void 0);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
{
|
|
284
|
+
console.warn("[Bridge] bridge:disconnected handler error:", err);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
267
289
|
clearTimeoutSafe(reconnectTimeoutRef);
|
|
268
290
|
clearTimeoutSafe(triggerReconnectTimeoutRef);
|
|
269
291
|
clearIntervalSafe(errorCheckIntervalRef);
|
|
@@ -280,7 +302,6 @@ const BridgeProvider = ({
|
|
|
280
302
|
reject(new Error("Bridge disconnected"));
|
|
281
303
|
});
|
|
282
304
|
pendingRequestsRef.current.clear();
|
|
283
|
-
eventListenersRef.current.clear();
|
|
284
305
|
setIsServiceWorkerAlive(false);
|
|
285
306
|
setBridge(null);
|
|
286
307
|
isConnectingRef.current = false;
|
|
@@ -471,6 +492,7 @@ const BridgeProvider = ({
|
|
|
471
492
|
messageIdRef,
|
|
472
493
|
isConnectedRef,
|
|
473
494
|
consecutiveTimeoutsRef,
|
|
495
|
+
reconnectionGracePeriodRef,
|
|
474
496
|
defaultTimeout,
|
|
475
497
|
onReconnectNeeded: triggerReconnect
|
|
476
498
|
});
|
|
@@ -483,6 +505,13 @@ const BridgeProvider = ({
|
|
|
483
505
|
swRestartRetryCountRef.current = 0;
|
|
484
506
|
consecutiveTimeoutsRef.current = 0;
|
|
485
507
|
isConnectingRef.current = false;
|
|
508
|
+
reconnectionGracePeriodRef.current = true;
|
|
509
|
+
setTimeout(() => {
|
|
510
|
+
reconnectionGracePeriodRef.current = false;
|
|
511
|
+
if (BRIDGE_ENABLE_LOGS) {
|
|
512
|
+
console.log("[Bridge] Grace period ended, timeout monitoring active");
|
|
513
|
+
}
|
|
514
|
+
}, 3e3);
|
|
486
515
|
eventListenersRef.current.get("bridge:connected")?.forEach((handler) => {
|
|
487
516
|
try {
|
|
488
517
|
handler({ timestamp: Date.now() });
|
|
@@ -492,13 +521,22 @@ const BridgeProvider = ({
|
|
|
492
521
|
}
|
|
493
522
|
}
|
|
494
523
|
});
|
|
524
|
+
const rejectAllPendingRequests = (message) => {
|
|
525
|
+
pendingRequestsRef.current.forEach(({ reject, timeout }) => {
|
|
526
|
+
clearTimeout(timeout);
|
|
527
|
+
reject(new Error(message));
|
|
528
|
+
});
|
|
529
|
+
pendingRequestsRef.current.clear();
|
|
530
|
+
consecutiveTimeoutsRef.current = 0;
|
|
531
|
+
};
|
|
495
532
|
startHealthMonitor({
|
|
496
533
|
bridge: bridgeInstance,
|
|
497
534
|
pingIntervalRef,
|
|
498
535
|
consecutivePingFailuresRef,
|
|
499
536
|
pingInterval,
|
|
500
537
|
setIsServiceWorkerAlive,
|
|
501
|
-
onReconnectNeeded: triggerReconnect
|
|
538
|
+
onReconnectNeeded: triggerReconnect,
|
|
539
|
+
rejectAllPendingRequests
|
|
502
540
|
});
|
|
503
541
|
} catch (e) {
|
|
504
542
|
isConnectingRef.current = false;
|
|
@@ -557,7 +595,7 @@ const BridgeProvider = ({
|
|
|
557
595
|
return () => {
|
|
558
596
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
559
597
|
clearTimeoutSafe(maxRetryCooldownRef);
|
|
560
|
-
cleanup();
|
|
598
|
+
cleanup(false);
|
|
561
599
|
};
|
|
562
600
|
}, [connect, cleanup]);
|
|
563
601
|
const contextValue = useMemo(
|
|
@@ -701,14 +739,26 @@ function useBridgeMutation(key, options = {}) {
|
|
|
701
739
|
return { mutate, data, loading, error, reset };
|
|
702
740
|
}
|
|
703
741
|
|
|
704
|
-
const useConnectionStatus = () => {
|
|
742
|
+
const useConnectionStatus = (store) => {
|
|
705
743
|
const context = useContext(BridgeContext);
|
|
744
|
+
const storeReady = useSyncExternalStore(
|
|
745
|
+
store?.onReady ?? (() => () => {
|
|
746
|
+
}),
|
|
747
|
+
store?.isReady ?? (() => true),
|
|
748
|
+
() => false
|
|
749
|
+
// Server-side fallback
|
|
750
|
+
);
|
|
751
|
+
const bridgeConnected = context?.status === "connected";
|
|
752
|
+
const isReady = bridgeConnected && storeReady;
|
|
753
|
+
const isLoading = !isReady && (context?.status === "connecting" || context?.status === "reconnecting" || bridgeConnected && !storeReady);
|
|
706
754
|
return {
|
|
707
755
|
status: context?.status,
|
|
708
|
-
isConnected:
|
|
756
|
+
isConnected: bridgeConnected,
|
|
709
757
|
isServiceWorkerAlive: context?.isServiceWorkerAlive ?? false,
|
|
710
758
|
reconnect: context?.reconnect,
|
|
711
|
-
error: context?.error
|
|
759
|
+
error: context?.error,
|
|
760
|
+
isReady,
|
|
761
|
+
isLoading
|
|
712
762
|
};
|
|
713
763
|
};
|
|
714
764
|
|