@chromahq/react 1.0.24 → 1.0.26
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 +142 -3
- package/dist/index.js +122 -8
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -81,14 +81,153 @@ interface ConnectionStatusResult {
|
|
|
81
81
|
/** Whether the system is loading (bridge connecting/reconnecting OR store not ready) */
|
|
82
82
|
isLoading: boolean;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Store interface for readiness checking.
|
|
86
|
+
* Pass the store instance directly (e.g., `appStore`), NOT via useStore() hook.
|
|
87
|
+
*/
|
|
84
88
|
interface StoreReadyMethods {
|
|
85
89
|
onReady: (callback: () => void) => () => void;
|
|
86
90
|
isReady: () => boolean;
|
|
87
91
|
}
|
|
88
92
|
/**
|
|
89
|
-
* Hook to get unified connection status
|
|
90
|
-
*
|
|
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
|
+
* ```
|
|
91
107
|
*/
|
|
92
108
|
declare const useConnectionStatus: (store?: StoreReadyMethods) => ConnectionStatusResult;
|
|
93
109
|
|
|
94
|
-
|
|
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
|
+
};
|
|
231
|
+
|
|
232
|
+
export { BridgeProvider, ServiceWorkerHealthProvider, getHealthStatus, subscribeToHealth, useBridge, useBridgeQuery, useConnectionStatus, useServiceWorkerHealth, useServiceWorkerHealthSimple };
|
|
233
|
+
export type { HealthStatus, ServiceWorkerHealthContextValue };
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ 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,
|
|
@@ -175,7 +176,8 @@ function startHealthMonitor(deps) {
|
|
|
175
176
|
consecutivePingFailuresRef,
|
|
176
177
|
pingInterval,
|
|
177
178
|
setIsServiceWorkerAlive,
|
|
178
|
-
onReconnectNeeded
|
|
179
|
+
onReconnectNeeded,
|
|
180
|
+
rejectAllPendingRequests
|
|
179
181
|
} = deps;
|
|
180
182
|
clearIntervalSafe(pingIntervalRef);
|
|
181
183
|
consecutivePingFailuresRef.current = 0;
|
|
@@ -202,9 +204,12 @@ function startHealthMonitor(deps) {
|
|
|
202
204
|
}
|
|
203
205
|
if (consecutivePingFailuresRef.current >= CONFIG.CONSECUTIVE_FAILURE_THRESHOLD) {
|
|
204
206
|
{
|
|
205
|
-
console.warn(
|
|
207
|
+
console.warn(
|
|
208
|
+
"[Bridge] Service worker unresponsive, rejecting pending requests and reconnecting..."
|
|
209
|
+
);
|
|
206
210
|
}
|
|
207
211
|
consecutivePingFailuresRef.current = 0;
|
|
212
|
+
rejectAllPendingRequests("Service worker unresponsive");
|
|
208
213
|
onReconnectNeeded();
|
|
209
214
|
}
|
|
210
215
|
}, pingInterval);
|
|
@@ -269,7 +274,18 @@ const BridgeProvider = ({
|
|
|
269
274
|
},
|
|
270
275
|
[onError, updateStatus]
|
|
271
276
|
);
|
|
272
|
-
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
|
+
}
|
|
273
289
|
clearTimeoutSafe(reconnectTimeoutRef);
|
|
274
290
|
clearTimeoutSafe(triggerReconnectTimeoutRef);
|
|
275
291
|
clearIntervalSafe(errorCheckIntervalRef);
|
|
@@ -286,7 +302,6 @@ const BridgeProvider = ({
|
|
|
286
302
|
reject(new Error("Bridge disconnected"));
|
|
287
303
|
});
|
|
288
304
|
pendingRequestsRef.current.clear();
|
|
289
|
-
eventListenersRef.current.clear();
|
|
290
305
|
setIsServiceWorkerAlive(false);
|
|
291
306
|
setBridge(null);
|
|
292
307
|
isConnectingRef.current = false;
|
|
@@ -506,13 +521,22 @@ const BridgeProvider = ({
|
|
|
506
521
|
}
|
|
507
522
|
}
|
|
508
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
|
+
};
|
|
509
532
|
startHealthMonitor({
|
|
510
533
|
bridge: bridgeInstance,
|
|
511
534
|
pingIntervalRef,
|
|
512
535
|
consecutivePingFailuresRef,
|
|
513
536
|
pingInterval,
|
|
514
537
|
setIsServiceWorkerAlive,
|
|
515
|
-
onReconnectNeeded: triggerReconnect
|
|
538
|
+
onReconnectNeeded: triggerReconnect,
|
|
539
|
+
rejectAllPendingRequests
|
|
516
540
|
});
|
|
517
541
|
} catch (e) {
|
|
518
542
|
isConnectingRef.current = false;
|
|
@@ -571,7 +595,7 @@ const BridgeProvider = ({
|
|
|
571
595
|
return () => {
|
|
572
596
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
573
597
|
clearTimeoutSafe(maxRetryCooldownRef);
|
|
574
|
-
cleanup();
|
|
598
|
+
cleanup(false);
|
|
575
599
|
};
|
|
576
600
|
}, [connect, cleanup]);
|
|
577
601
|
const contextValue = useMemo(
|
|
@@ -738,4 +762,94 @@ const useConnectionStatus = (store) => {
|
|
|
738
762
|
};
|
|
739
763
|
};
|
|
740
764
|
|
|
741
|
-
|
|
765
|
+
const globalSubscribers = /* @__PURE__ */ new Set();
|
|
766
|
+
function subscribeToHealth(callback) {
|
|
767
|
+
globalSubscribers.add(callback);
|
|
768
|
+
return () => {
|
|
769
|
+
globalSubscribers.delete(callback);
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
let currentHealthStatus = "unknown";
|
|
773
|
+
let currentIsHealthy = false;
|
|
774
|
+
function getHealthStatus() {
|
|
775
|
+
return { status: currentHealthStatus, isHealthy: currentIsHealthy };
|
|
776
|
+
}
|
|
777
|
+
function broadcastHealthChange(status, isHealthy) {
|
|
778
|
+
currentHealthStatus = status;
|
|
779
|
+
currentIsHealthy = isHealthy;
|
|
780
|
+
globalSubscribers.forEach((callback) => {
|
|
781
|
+
try {
|
|
782
|
+
callback(status, isHealthy);
|
|
783
|
+
} catch (e) {
|
|
784
|
+
console.error("[ServiceWorkerHealth] Subscriber error:", e);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
const ServiceWorkerHealthContext = createContext(null);
|
|
789
|
+
const ServiceWorkerHealthProvider = ({
|
|
790
|
+
children,
|
|
791
|
+
onHealthChange
|
|
792
|
+
}) => {
|
|
793
|
+
const bridgeContext = useContext(BridgeContext);
|
|
794
|
+
const [lastHealthyAt, setLastHealthyAt] = useState(null);
|
|
795
|
+
const bridgeStatus = bridgeContext?.status;
|
|
796
|
+
const isServiceWorkerAlive = bridgeContext?.isServiceWorkerAlive ?? false;
|
|
797
|
+
const derivedStatus = useMemo(() => {
|
|
798
|
+
if (!bridgeStatus) return "unknown";
|
|
799
|
+
if (bridgeStatus === "connected" && isServiceWorkerAlive) {
|
|
800
|
+
return "healthy";
|
|
801
|
+
}
|
|
802
|
+
if (bridgeStatus === "reconnecting" || bridgeStatus === "connecting") {
|
|
803
|
+
return "recovering";
|
|
804
|
+
}
|
|
805
|
+
return "unhealthy";
|
|
806
|
+
}, [bridgeStatus, isServiceWorkerAlive]);
|
|
807
|
+
const isHealthy = derivedStatus === "healthy";
|
|
808
|
+
const isRecovering = derivedStatus === "recovering";
|
|
809
|
+
const isLoading = derivedStatus === "recovering" || derivedStatus === "unknown";
|
|
810
|
+
useEffect(() => {
|
|
811
|
+
if (isHealthy) {
|
|
812
|
+
setLastHealthyAt(Date.now());
|
|
813
|
+
}
|
|
814
|
+
}, [isHealthy]);
|
|
815
|
+
useEffect(() => {
|
|
816
|
+
broadcastHealthChange(derivedStatus, isHealthy);
|
|
817
|
+
onHealthChange?.(derivedStatus, isHealthy);
|
|
818
|
+
}, [derivedStatus, isHealthy, onHealthChange]);
|
|
819
|
+
const forceReconnect = useCallback(() => {
|
|
820
|
+
bridgeContext?.reconnect();
|
|
821
|
+
}, [bridgeContext]);
|
|
822
|
+
const value = useMemo(
|
|
823
|
+
() => ({
|
|
824
|
+
status: derivedStatus,
|
|
825
|
+
isHealthy,
|
|
826
|
+
isRecovering,
|
|
827
|
+
isLoading,
|
|
828
|
+
lastHealthyAt,
|
|
829
|
+
forceReconnect
|
|
830
|
+
}),
|
|
831
|
+
[derivedStatus, isHealthy, isRecovering, isLoading, lastHealthyAt, forceReconnect]
|
|
832
|
+
);
|
|
833
|
+
return /* @__PURE__ */ jsx(ServiceWorkerHealthContext.Provider, { value, children });
|
|
834
|
+
};
|
|
835
|
+
function useServiceWorkerHealth() {
|
|
836
|
+
const context = useContext(ServiceWorkerHealthContext);
|
|
837
|
+
if (!context) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
"useServiceWorkerHealth must be used within a ServiceWorkerHealthProvider. Wrap your app with <ServiceWorkerHealthProvider> inside <BridgeProvider>."
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return context;
|
|
843
|
+
}
|
|
844
|
+
function useServiceWorkerHealthSimple() {
|
|
845
|
+
const bridgeContext = useContext(BridgeContext);
|
|
846
|
+
const isHealthy = bridgeContext?.status === "connected" && bridgeContext?.isServiceWorkerAlive === true;
|
|
847
|
+
const isRecovering = bridgeContext?.status === "reconnecting" || bridgeContext?.status === "connecting";
|
|
848
|
+
const isLoading = !isHealthy && (isRecovering || !bridgeContext);
|
|
849
|
+
const reconnect = useCallback(() => {
|
|
850
|
+
bridgeContext?.reconnect();
|
|
851
|
+
}, [bridgeContext]);
|
|
852
|
+
return { isHealthy, isRecovering, isLoading, reconnect };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export { BridgeProvider, ServiceWorkerHealthProvider, getHealthStatus, subscribeToHealth, useBridge, useBridgeMutation, useBridgeQuery, useConnectionStatus, useServiceWorkerHealth, useServiceWorkerHealthSimple };
|