@syncular/client-react 0.0.2-2 → 0.0.3-14
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/createSyncularReact.d.ts +89 -2
- package/dist/createSyncularReact.d.ts.map +1 -1
- package/dist/createSyncularReact.js +346 -25
- package/dist/createSyncularReact.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/useSyncGroup.d.ts +42 -0
- package/dist/useSyncGroup.d.ts.map +1 -0
- package/dist/useSyncGroup.js +74 -0
- package/dist/useSyncGroup.js.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/SyncEngine.test.ts +64 -0
- package/src/__tests__/hooks.test.tsx +152 -0
- package/src/createSyncularReact.tsx +558 -20
- package/src/index.ts +16 -0
- package/src/useSyncGroup.ts +169 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* const syncular = createSyncularReact<MyDb>();
|
|
8
8
|
* const { SyncProvider, useSyncQuery } = syncular;
|
|
9
9
|
*/
|
|
10
|
-
import type { ClientTableRegistry, MutationReceipt, MutationsApi, SyncClientDb, SyncClientPlugin, SyncSubscriptionRequest, SyncTransport } from '@syncular/client';
|
|
10
|
+
import type { ClientTableRegistry, MutationReceipt, MutationsApi, SubscriptionState, SyncAwaitBootstrapOptions, SyncAwaitPhaseOptions, SyncClientDb, SyncClientPlugin, SyncDiagnostics, SyncInspectorOptions, SyncInspectorSnapshot, SyncProgress, SyncRepairOptions, SyncResetOptions, SyncResetResult, SyncSubscriptionRequest, SyncTransport, TransportHealth } from '@syncular/client';
|
|
11
11
|
import { type ConflictInfo, type OutboxStats, type PresenceEntry, type QueryContext, type SyncConnectionState, SyncEngine, type SyncEngineState, type SyncError, type SyncResult, type SyncTransportMode } from '@syncular/client';
|
|
12
12
|
import { type Kysely } from 'kysely';
|
|
13
13
|
import { type ReactNode } from 'react';
|
|
@@ -54,17 +54,42 @@ export interface UseSyncEngineResult {
|
|
|
54
54
|
disconnect: () => void;
|
|
55
55
|
start: () => Promise<void>;
|
|
56
56
|
resetLocalState: () => void;
|
|
57
|
+
getTransportHealth: () => Readonly<TransportHealth>;
|
|
58
|
+
getProgress: () => Promise<SyncProgress>;
|
|
59
|
+
getDiagnostics: () => Promise<SyncDiagnostics>;
|
|
60
|
+
getInspectorSnapshot: (options?: SyncInspectorOptions) => Promise<SyncInspectorSnapshot>;
|
|
61
|
+
listSubscriptionStates: (args?: {
|
|
62
|
+
stateId?: string;
|
|
63
|
+
table?: string;
|
|
64
|
+
status?: 'active' | 'revoked';
|
|
65
|
+
}) => Promise<SubscriptionState[]>;
|
|
66
|
+
getSubscriptionState: (subscriptionId: string, options?: {
|
|
67
|
+
stateId?: string;
|
|
68
|
+
}) => Promise<SubscriptionState | null>;
|
|
69
|
+
reset: (options: SyncResetOptions) => Promise<SyncResetResult>;
|
|
70
|
+
repair: (options: SyncRepairOptions) => Promise<SyncResetResult>;
|
|
71
|
+
awaitPhase: (phase: SyncProgress['channelPhase'], options?: SyncAwaitPhaseOptions) => Promise<SyncProgress>;
|
|
72
|
+
awaitBootstrapComplete: (options?: SyncAwaitBootstrapOptions) => Promise<SyncProgress>;
|
|
57
73
|
}
|
|
58
74
|
export interface SyncStatus {
|
|
59
75
|
enabled: boolean;
|
|
60
76
|
isOnline: boolean;
|
|
61
77
|
isSyncing: boolean;
|
|
62
78
|
lastSyncAt: number | null;
|
|
79
|
+
lastSyncAgeMs: number | null;
|
|
80
|
+
isStale: boolean;
|
|
63
81
|
pendingCount: number;
|
|
64
82
|
error: SyncError | null;
|
|
65
83
|
isRetrying: boolean;
|
|
66
84
|
retryCount: number;
|
|
67
85
|
}
|
|
86
|
+
export interface UseSyncStatusOptions {
|
|
87
|
+
/**
|
|
88
|
+
* Mark status as stale when `Date.now() - lastSyncAt` exceeds this value.
|
|
89
|
+
* If omitted, `isStale` is always false.
|
|
90
|
+
*/
|
|
91
|
+
staleAfterMs?: number;
|
|
92
|
+
}
|
|
68
93
|
export interface UseSyncConnectionResult {
|
|
69
94
|
state: SyncConnectionState;
|
|
70
95
|
mode: SyncTransportMode;
|
|
@@ -73,6 +98,56 @@ export interface UseSyncConnectionResult {
|
|
|
73
98
|
reconnect: () => void;
|
|
74
99
|
disconnect: () => void;
|
|
75
100
|
}
|
|
101
|
+
export interface UseTransportHealthResult {
|
|
102
|
+
health: TransportHealth;
|
|
103
|
+
}
|
|
104
|
+
export interface UseSyncProgressOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Polling interval while bootstrapping.
|
|
107
|
+
* Set to 0 to disable interval refresh.
|
|
108
|
+
*/
|
|
109
|
+
pollIntervalMs?: number;
|
|
110
|
+
}
|
|
111
|
+
export interface UseSyncProgressResult {
|
|
112
|
+
progress: SyncProgress | null;
|
|
113
|
+
isLoading: boolean;
|
|
114
|
+
error: Error | null;
|
|
115
|
+
refresh: () => Promise<void>;
|
|
116
|
+
}
|
|
117
|
+
export interface UseSyncInspectorOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Polling interval for refreshing inspector snapshots.
|
|
120
|
+
* Set to 0 to disable interval refresh.
|
|
121
|
+
*/
|
|
122
|
+
pollIntervalMs?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Max number of recent events in the snapshot.
|
|
125
|
+
*/
|
|
126
|
+
eventLimit?: number;
|
|
127
|
+
}
|
|
128
|
+
export interface UseSyncInspectorResult {
|
|
129
|
+
snapshot: SyncInspectorSnapshot | null;
|
|
130
|
+
isLoading: boolean;
|
|
131
|
+
error: Error | null;
|
|
132
|
+
refresh: () => Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
export interface UseSyncSubscriptionsOptions {
|
|
135
|
+
stateId?: string;
|
|
136
|
+
table?: string;
|
|
137
|
+
status?: 'active' | 'revoked';
|
|
138
|
+
}
|
|
139
|
+
export interface UseSyncSubscriptionsResult {
|
|
140
|
+
subscriptions: SubscriptionState[];
|
|
141
|
+
isLoading: boolean;
|
|
142
|
+
error: Error | null;
|
|
143
|
+
refresh: () => Promise<void>;
|
|
144
|
+
}
|
|
145
|
+
export interface UseSyncSubscriptionResult {
|
|
146
|
+
subscription: SubscriptionState | null;
|
|
147
|
+
isLoading: boolean;
|
|
148
|
+
error: Error | null;
|
|
149
|
+
refresh: () => Promise<void>;
|
|
150
|
+
}
|
|
76
151
|
export interface UseConflictsResult {
|
|
77
152
|
conflicts: ConflictInfo[];
|
|
78
153
|
count: number;
|
|
@@ -96,12 +171,17 @@ export interface UseSyncQueryResult<T> {
|
|
|
96
171
|
data: T | undefined;
|
|
97
172
|
isLoading: boolean;
|
|
98
173
|
error: Error | null;
|
|
174
|
+
isStale: boolean;
|
|
175
|
+
lastSyncAt: number | null;
|
|
99
176
|
refetch: () => Promise<void>;
|
|
100
177
|
}
|
|
101
178
|
export interface UseSyncQueryOptions {
|
|
102
179
|
enabled?: boolean;
|
|
103
180
|
deps?: unknown[];
|
|
104
181
|
keyField?: string;
|
|
182
|
+
watchTables?: string[];
|
|
183
|
+
pollIntervalMs?: number;
|
|
184
|
+
staleAfterMs?: number;
|
|
105
185
|
}
|
|
106
186
|
export interface UseQueryResult<T> {
|
|
107
187
|
data: T | undefined;
|
|
@@ -208,8 +288,15 @@ export declare function createSyncularReact<DB extends SyncClientDb>(): {
|
|
|
208
288
|
readonly useSyncContext: () => SyncContextValue<DB>;
|
|
209
289
|
readonly useEngine: () => SyncEngine<DB>;
|
|
210
290
|
readonly useSyncEngine: () => UseSyncEngineResult;
|
|
211
|
-
readonly useSyncStatus: () => SyncStatus;
|
|
291
|
+
readonly useSyncStatus: (options?: UseSyncStatusOptions) => SyncStatus;
|
|
212
292
|
readonly useSyncConnection: () => UseSyncConnectionResult;
|
|
293
|
+
readonly useTransportHealth: () => UseTransportHealthResult;
|
|
294
|
+
readonly useSyncProgress: (options?: UseSyncProgressOptions) => UseSyncProgressResult;
|
|
295
|
+
readonly useSyncInspector: (options?: UseSyncInspectorOptions) => UseSyncInspectorResult;
|
|
296
|
+
readonly useSyncSubscriptions: (options?: UseSyncSubscriptionsOptions) => UseSyncSubscriptionsResult;
|
|
297
|
+
readonly useSyncSubscription: (subscriptionId: string, options?: {
|
|
298
|
+
stateId?: string | undefined;
|
|
299
|
+
}) => UseSyncSubscriptionResult;
|
|
213
300
|
readonly useSyncQuery: <TResult>(queryFn: (ctx: QueryContext<DB>) => ExecutableQuery<TResult> | Promise<TResult>, options?: UseSyncQueryOptions) => UseSyncQueryResult<TResult>;
|
|
214
301
|
readonly useQuery: <TResult>(queryFn: (ctx: QueryContext<DB>) => ExecutableQuery<TResult> | Promise<TResult>, options?: UseQueryOptions) => UseQueryResult<TResult>;
|
|
215
302
|
readonly useMutation: <TTable extends keyof DB & string>(options: UseMutationOptions<TTable>) => UseMutationResult<TTable>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createSyncularReact.d.ts","sourceRoot":"","sources":["../src/createSyncularReact.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EACf,YAAY,EAIZ,YAAY,EACZ,gBAAgB,
|
|
1
|
+
{"version":3,"file":"createSyncularReact.d.ts","sourceRoot":"","sources":["../src/createSyncularReact.tsx"],"names":[],"mappings":"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"}
|
|
@@ -167,6 +167,16 @@ export function createSyncularReact() {
|
|
|
167
167
|
const disconnect = useCallback(() => engine.disconnect(), [engine]);
|
|
168
168
|
const start = useCallback(() => engine.start(), [engine]);
|
|
169
169
|
const resetLocalState = useCallback(() => engine.resetLocalState(), [engine]);
|
|
170
|
+
const getTransportHealth = useCallback(() => engine.getTransportHealth(), [engine]);
|
|
171
|
+
const getProgress = useCallback(() => engine.getProgress(), [engine]);
|
|
172
|
+
const getDiagnostics = useCallback(() => engine.getDiagnostics(), [engine]);
|
|
173
|
+
const getInspectorSnapshot = useCallback((options) => engine.getInspectorSnapshot(options), [engine]);
|
|
174
|
+
const listSubscriptionStates = useCallback((args) => engine.listSubscriptionStates(args), [engine]);
|
|
175
|
+
const getSubscriptionState = useCallback((subscriptionId, options) => engine.getSubscriptionState(subscriptionId, options), [engine]);
|
|
176
|
+
const reset = useCallback((options) => engine.reset(options), [engine]);
|
|
177
|
+
const repair = useCallback((options) => engine.repair(options), [engine]);
|
|
178
|
+
const awaitPhase = useCallback((phase, options) => engine.awaitPhase(phase, options), [engine]);
|
|
179
|
+
const awaitBootstrapComplete = useCallback((options) => engine.awaitBootstrapComplete(options), [engine]);
|
|
170
180
|
return {
|
|
171
181
|
state,
|
|
172
182
|
sync,
|
|
@@ -174,21 +184,51 @@ export function createSyncularReact() {
|
|
|
174
184
|
disconnect,
|
|
175
185
|
start,
|
|
176
186
|
resetLocalState,
|
|
187
|
+
getTransportHealth,
|
|
188
|
+
getProgress,
|
|
189
|
+
getDiagnostics,
|
|
190
|
+
getInspectorSnapshot,
|
|
191
|
+
listSubscriptionStates,
|
|
192
|
+
getSubscriptionState,
|
|
193
|
+
reset,
|
|
194
|
+
repair,
|
|
195
|
+
awaitPhase,
|
|
196
|
+
awaitBootstrapComplete,
|
|
177
197
|
};
|
|
178
198
|
}
|
|
179
|
-
function useSyncStatus() {
|
|
199
|
+
function useSyncStatus(options = {}) {
|
|
200
|
+
const { staleAfterMs } = options;
|
|
180
201
|
const engine = useEngine();
|
|
181
202
|
const state = useSyncExternalStore(useCallback((callback) => engine.subscribe(callback), [engine]), useCallback(() => engine.getState(), [engine]), useCallback(() => engine.getState(), [engine]));
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
203
|
+
const [staleClock, setStaleClock] = useState(Date.now());
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (staleAfterMs === undefined || staleAfterMs <= 0)
|
|
206
|
+
return;
|
|
207
|
+
const intervalMs = Math.min(1000, Math.max(100, Math.floor(staleAfterMs / 2)));
|
|
208
|
+
const timer = setInterval(() => {
|
|
209
|
+
setStaleClock(Date.now());
|
|
210
|
+
}, intervalMs);
|
|
211
|
+
return () => clearInterval(timer);
|
|
212
|
+
}, [staleAfterMs]);
|
|
213
|
+
return useMemo(() => {
|
|
214
|
+
const now = staleAfterMs !== undefined ? staleClock : Date.now();
|
|
215
|
+
const lastSyncAgeMs = state.lastSyncAt === null ? null : Math.max(0, now - state.lastSyncAt);
|
|
216
|
+
const isStale = staleAfterMs !== undefined && staleAfterMs > 0
|
|
217
|
+
? state.lastSyncAt === null || (lastSyncAgeMs ?? 0) > staleAfterMs
|
|
218
|
+
: false;
|
|
219
|
+
return {
|
|
220
|
+
enabled: state.enabled,
|
|
221
|
+
isOnline: state.connectionState === 'connected',
|
|
222
|
+
isSyncing: state.isSyncing,
|
|
223
|
+
lastSyncAt: state.lastSyncAt,
|
|
224
|
+
lastSyncAgeMs,
|
|
225
|
+
isStale,
|
|
226
|
+
pendingCount: state.pendingCount,
|
|
227
|
+
error: state.error,
|
|
228
|
+
isRetrying: state.isRetrying,
|
|
229
|
+
retryCount: state.retryCount,
|
|
230
|
+
};
|
|
231
|
+
}, [state, staleAfterMs, staleClock]);
|
|
192
232
|
}
|
|
193
233
|
function useSyncConnection() {
|
|
194
234
|
const engine = useEngine();
|
|
@@ -209,6 +249,239 @@ export function createSyncularReact() {
|
|
|
209
249
|
disconnect,
|
|
210
250
|
]);
|
|
211
251
|
}
|
|
252
|
+
function useTransportHealth() {
|
|
253
|
+
const engine = useEngine();
|
|
254
|
+
const health = useSyncExternalStore(useCallback((callback) => {
|
|
255
|
+
const unsubscribers = [
|
|
256
|
+
engine.subscribe(callback),
|
|
257
|
+
engine.on('connection:change', callback),
|
|
258
|
+
engine.on('sync:complete', callback),
|
|
259
|
+
engine.on('sync:error', callback),
|
|
260
|
+
];
|
|
261
|
+
return () => {
|
|
262
|
+
for (const unsubscribe of unsubscribers)
|
|
263
|
+
unsubscribe();
|
|
264
|
+
};
|
|
265
|
+
}, [engine]), useCallback(() => engine.getTransportHealth(), [engine]), useCallback(() => engine.getTransportHealth(), [engine]));
|
|
266
|
+
return useMemo(() => ({ health }), [health]);
|
|
267
|
+
}
|
|
268
|
+
function useSyncProgress(options = {}) {
|
|
269
|
+
const engine = useEngine();
|
|
270
|
+
const { pollIntervalMs = 500 } = options;
|
|
271
|
+
const [progress, setProgress] = useState(null);
|
|
272
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
273
|
+
const [error, setError] = useState(null);
|
|
274
|
+
const loadedRef = useRef(false);
|
|
275
|
+
const refresh = useCallback(async () => {
|
|
276
|
+
if (!loadedRef.current) {
|
|
277
|
+
setIsLoading(true);
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const next = await engine.getProgress();
|
|
281
|
+
setProgress(next);
|
|
282
|
+
setError(null);
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
loadedRef.current = true;
|
|
289
|
+
setIsLoading(false);
|
|
290
|
+
}
|
|
291
|
+
}, [engine]);
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
void refresh();
|
|
294
|
+
}, [refresh]);
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
const unsubscribers = [
|
|
297
|
+
engine.on('sync:start', refresh),
|
|
298
|
+
engine.on('sync:complete', refresh),
|
|
299
|
+
engine.on('sync:error', refresh),
|
|
300
|
+
engine.on('bootstrap:start', refresh),
|
|
301
|
+
engine.on('bootstrap:progress', refresh),
|
|
302
|
+
engine.on('bootstrap:complete', refresh),
|
|
303
|
+
];
|
|
304
|
+
return () => {
|
|
305
|
+
for (const unsubscribe of unsubscribers)
|
|
306
|
+
unsubscribe();
|
|
307
|
+
};
|
|
308
|
+
}, [engine, refresh]);
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
if (pollIntervalMs <= 0)
|
|
311
|
+
return;
|
|
312
|
+
if (progress?.channelPhase !== 'bootstrapping')
|
|
313
|
+
return;
|
|
314
|
+
const timer = setInterval(() => {
|
|
315
|
+
void refresh();
|
|
316
|
+
}, pollIntervalMs);
|
|
317
|
+
return () => clearInterval(timer);
|
|
318
|
+
}, [pollIntervalMs, progress?.channelPhase, refresh]);
|
|
319
|
+
return useMemo(() => ({
|
|
320
|
+
progress,
|
|
321
|
+
isLoading,
|
|
322
|
+
error,
|
|
323
|
+
refresh,
|
|
324
|
+
}), [progress, isLoading, error, refresh]);
|
|
325
|
+
}
|
|
326
|
+
function useSyncInspector(options = {}) {
|
|
327
|
+
const engine = useEngine();
|
|
328
|
+
const { pollIntervalMs = 2_000, eventLimit } = options;
|
|
329
|
+
const [snapshot, setSnapshot] = useState(null);
|
|
330
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
331
|
+
const [error, setError] = useState(null);
|
|
332
|
+
const loadedRef = useRef(false);
|
|
333
|
+
const refresh = useCallback(async () => {
|
|
334
|
+
if (!loadedRef.current) {
|
|
335
|
+
setIsLoading(true);
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const next = await engine.getInspectorSnapshot({ eventLimit });
|
|
339
|
+
setSnapshot(next);
|
|
340
|
+
setError(null);
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
loadedRef.current = true;
|
|
347
|
+
setIsLoading(false);
|
|
348
|
+
}
|
|
349
|
+
}, [engine, eventLimit]);
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
void refresh();
|
|
352
|
+
}, [refresh]);
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
const unsubscribers = [
|
|
355
|
+
engine.on('sync:start', refresh),
|
|
356
|
+
engine.on('sync:complete', refresh),
|
|
357
|
+
engine.on('sync:error', refresh),
|
|
358
|
+
engine.on('bootstrap:start', refresh),
|
|
359
|
+
engine.on('bootstrap:progress', refresh),
|
|
360
|
+
engine.on('bootstrap:complete', refresh),
|
|
361
|
+
engine.on('connection:change', refresh),
|
|
362
|
+
engine.on('outbox:change', refresh),
|
|
363
|
+
engine.on('data:change', refresh),
|
|
364
|
+
];
|
|
365
|
+
return () => {
|
|
366
|
+
for (const unsubscribe of unsubscribers)
|
|
367
|
+
unsubscribe();
|
|
368
|
+
};
|
|
369
|
+
}, [engine, refresh]);
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (pollIntervalMs <= 0)
|
|
372
|
+
return;
|
|
373
|
+
const timer = setInterval(() => {
|
|
374
|
+
void refresh();
|
|
375
|
+
}, pollIntervalMs);
|
|
376
|
+
return () => clearInterval(timer);
|
|
377
|
+
}, [pollIntervalMs, refresh]);
|
|
378
|
+
return useMemo(() => ({
|
|
379
|
+
snapshot,
|
|
380
|
+
isLoading,
|
|
381
|
+
error,
|
|
382
|
+
refresh,
|
|
383
|
+
}), [snapshot, isLoading, error, refresh]);
|
|
384
|
+
}
|
|
385
|
+
function useSyncSubscriptions(options = {}) {
|
|
386
|
+
const engine = useEngine();
|
|
387
|
+
const { stateId, table, status } = options;
|
|
388
|
+
const [subscriptions, setSubscriptions] = useState([]);
|
|
389
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
390
|
+
const [error, setError] = useState(null);
|
|
391
|
+
const loadedRef = useRef(false);
|
|
392
|
+
const refresh = useCallback(async () => {
|
|
393
|
+
if (!loadedRef.current) {
|
|
394
|
+
setIsLoading(true);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const next = await engine.listSubscriptionStates({
|
|
398
|
+
stateId,
|
|
399
|
+
table,
|
|
400
|
+
status,
|
|
401
|
+
});
|
|
402
|
+
setSubscriptions(next);
|
|
403
|
+
setError(null);
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
loadedRef.current = true;
|
|
410
|
+
setIsLoading(false);
|
|
411
|
+
}
|
|
412
|
+
}, [engine, stateId, table, status]);
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
void refresh();
|
|
415
|
+
}, [refresh]);
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
const unsubscribers = [
|
|
418
|
+
engine.on('sync:complete', refresh),
|
|
419
|
+
engine.on('sync:error', refresh),
|
|
420
|
+
engine.on('bootstrap:start', refresh),
|
|
421
|
+
engine.on('bootstrap:progress', refresh),
|
|
422
|
+
engine.on('bootstrap:complete', refresh),
|
|
423
|
+
];
|
|
424
|
+
return () => {
|
|
425
|
+
for (const unsubscribe of unsubscribers)
|
|
426
|
+
unsubscribe();
|
|
427
|
+
};
|
|
428
|
+
}, [engine, refresh]);
|
|
429
|
+
return useMemo(() => ({
|
|
430
|
+
subscriptions,
|
|
431
|
+
isLoading,
|
|
432
|
+
error,
|
|
433
|
+
refresh,
|
|
434
|
+
}), [subscriptions, isLoading, error, refresh]);
|
|
435
|
+
}
|
|
436
|
+
function useSyncSubscription(subscriptionId, options = {}) {
|
|
437
|
+
const engine = useEngine();
|
|
438
|
+
const { stateId } = options;
|
|
439
|
+
const [subscription, setSubscription] = useState(null);
|
|
440
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
441
|
+
const [error, setError] = useState(null);
|
|
442
|
+
const loadedRef = useRef(false);
|
|
443
|
+
const refresh = useCallback(async () => {
|
|
444
|
+
if (!loadedRef.current) {
|
|
445
|
+
setIsLoading(true);
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const next = await engine.getSubscriptionState(subscriptionId, {
|
|
449
|
+
stateId,
|
|
450
|
+
});
|
|
451
|
+
setSubscription(next);
|
|
452
|
+
setError(null);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
loadedRef.current = true;
|
|
459
|
+
setIsLoading(false);
|
|
460
|
+
}
|
|
461
|
+
}, [engine, stateId, subscriptionId]);
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
void refresh();
|
|
464
|
+
}, [refresh]);
|
|
465
|
+
useEffect(() => {
|
|
466
|
+
const unsubscribers = [
|
|
467
|
+
engine.on('sync:complete', refresh),
|
|
468
|
+
engine.on('sync:error', refresh),
|
|
469
|
+
engine.on('bootstrap:start', refresh),
|
|
470
|
+
engine.on('bootstrap:progress', refresh),
|
|
471
|
+
engine.on('bootstrap:complete', refresh),
|
|
472
|
+
];
|
|
473
|
+
return () => {
|
|
474
|
+
for (const unsubscribe of unsubscribers)
|
|
475
|
+
unsubscribe();
|
|
476
|
+
};
|
|
477
|
+
}, [engine, refresh]);
|
|
478
|
+
return useMemo(() => ({
|
|
479
|
+
subscription,
|
|
480
|
+
isLoading,
|
|
481
|
+
error,
|
|
482
|
+
refresh,
|
|
483
|
+
}), [subscription, isLoading, error, refresh]);
|
|
484
|
+
}
|
|
212
485
|
function useConflicts() {
|
|
213
486
|
const engine = useEngine();
|
|
214
487
|
const [conflicts, setConflicts] = useState([]);
|
|
@@ -298,14 +571,17 @@ export function createSyncularReact() {
|
|
|
298
571
|
}), [resolve, isPending, error, reset]);
|
|
299
572
|
}
|
|
300
573
|
function useSyncQuery(queryFn, options = {}) {
|
|
301
|
-
const { enabled = true, deps = [], keyField = 'id' } = options;
|
|
574
|
+
const { enabled = true, deps = [], keyField = 'id', watchTables = [], pollIntervalMs, staleAfterMs, } = options;
|
|
302
575
|
const { db } = useSyncContext();
|
|
303
576
|
const engine = useEngine();
|
|
577
|
+
const watchTablesSet = useMemo(() => new Set(watchTables), [watchTables]);
|
|
304
578
|
const queryFnRef = useRef(queryFn);
|
|
305
579
|
queryFnRef.current = queryFn;
|
|
306
580
|
const [data, setData] = useState(undefined);
|
|
307
581
|
const [isLoading, setIsLoading] = useState(true);
|
|
308
582
|
const [error, setError] = useState(null);
|
|
583
|
+
const [lastSyncAt, setLastSyncAt] = useState(() => engine.getState().lastSyncAt);
|
|
584
|
+
const [staleClock, setStaleClock] = useState(Date.now());
|
|
309
585
|
const versionRef = useRef(0);
|
|
310
586
|
const watchedScopesRef = useRef(new Set());
|
|
311
587
|
const fingerprintCollectorRef = useRef(new FingerprintCollector());
|
|
@@ -317,6 +593,7 @@ export function createSyncularReact() {
|
|
|
317
593
|
previousFingerprintRef.current = 'disabled';
|
|
318
594
|
setData(undefined);
|
|
319
595
|
}
|
|
596
|
+
setLastSyncAt(engine.getState().lastSyncAt);
|
|
320
597
|
setIsLoading(false);
|
|
321
598
|
hasLoadedRef.current = true;
|
|
322
599
|
return;
|
|
@@ -335,6 +612,7 @@ export function createSyncularReact() {
|
|
|
335
612
|
: await fnResult;
|
|
336
613
|
if (version === versionRef.current) {
|
|
337
614
|
watchedScopesRef.current = scopeCollector;
|
|
615
|
+
setLastSyncAt(engine.getState().lastSyncAt);
|
|
338
616
|
const fingerprint = fingerprintCollectorRef.current.getCombined();
|
|
339
617
|
if (fingerprint !== previousFingerprintRef.current ||
|
|
340
618
|
fingerprint === '') {
|
|
@@ -360,11 +638,18 @@ export function createSyncularReact() {
|
|
|
360
638
|
executeQuery();
|
|
361
639
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
362
640
|
}, [executeQuery, ...deps]);
|
|
641
|
+
useEffect(() => {
|
|
642
|
+
const unsubscribe = engine.subscribe(() => {
|
|
643
|
+
const nextLastSyncAt = engine.getState().lastSyncAt;
|
|
644
|
+
setLastSyncAt((previous) => previous === nextLastSyncAt ? previous : nextLastSyncAt);
|
|
645
|
+
});
|
|
646
|
+
return unsubscribe;
|
|
647
|
+
}, [engine]);
|
|
363
648
|
useEffect(() => {
|
|
364
649
|
if (!enabled)
|
|
365
650
|
return;
|
|
366
651
|
const unsubscribe = engine.on('sync:complete', () => {
|
|
367
|
-
executeQuery();
|
|
652
|
+
void executeQuery();
|
|
368
653
|
});
|
|
369
654
|
return unsubscribe;
|
|
370
655
|
}, [engine, enabled, executeQuery]);
|
|
@@ -372,26 +657,57 @@ export function createSyncularReact() {
|
|
|
372
657
|
if (!enabled)
|
|
373
658
|
return;
|
|
374
659
|
const unsubscribe = engine.on('data:change', (event) => {
|
|
375
|
-
const changedScopes = event.scopes
|
|
660
|
+
const changedScopes = event.scopes ?? [];
|
|
376
661
|
const watchedScopes = watchedScopesRef.current;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
662
|
+
const hasDynamicFilter = watchedScopes.size > 0;
|
|
663
|
+
const hasTableFilter = watchTablesSet.size > 0;
|
|
664
|
+
if (hasDynamicFilter || hasTableFilter) {
|
|
665
|
+
const matchesDynamic = changedScopes.some((scope) => watchedScopes.has(scope));
|
|
666
|
+
const matchesConfigured = changedScopes.some((scope) => watchTablesSet.has(scope));
|
|
667
|
+
if (!matchesDynamic && !matchesConfigured) {
|
|
380
668
|
return;
|
|
669
|
+
}
|
|
381
670
|
}
|
|
382
|
-
executeQuery();
|
|
671
|
+
void executeQuery();
|
|
383
672
|
});
|
|
384
673
|
return unsubscribe;
|
|
385
|
-
}, [engine, enabled, executeQuery]);
|
|
674
|
+
}, [engine, enabled, executeQuery, watchTablesSet]);
|
|
675
|
+
useEffect(() => {
|
|
676
|
+
if (!enabled)
|
|
677
|
+
return;
|
|
678
|
+
if (pollIntervalMs === undefined || pollIntervalMs <= 0)
|
|
679
|
+
return;
|
|
680
|
+
const timer = setInterval(() => {
|
|
681
|
+
void executeQuery();
|
|
682
|
+
}, pollIntervalMs);
|
|
683
|
+
return () => clearInterval(timer);
|
|
684
|
+
}, [enabled, pollIntervalMs, executeQuery]);
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
if (staleAfterMs === undefined || staleAfterMs <= 0)
|
|
687
|
+
return;
|
|
688
|
+
const intervalMs = Math.min(1000, Math.max(100, Math.floor(staleAfterMs / 2)));
|
|
689
|
+
const timer = setInterval(() => {
|
|
690
|
+
setStaleClock(Date.now());
|
|
691
|
+
}, intervalMs);
|
|
692
|
+
return () => clearInterval(timer);
|
|
693
|
+
}, [staleAfterMs]);
|
|
386
694
|
const refetch = useCallback(async () => {
|
|
387
695
|
await executeQuery();
|
|
388
696
|
}, [executeQuery]);
|
|
389
|
-
return useMemo(() =>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
697
|
+
return useMemo(() => {
|
|
698
|
+
const now = staleAfterMs !== undefined ? staleClock : Date.now();
|
|
699
|
+
const isStale = staleAfterMs !== undefined && staleAfterMs > 0
|
|
700
|
+
? lastSyncAt === null || now - lastSyncAt > staleAfterMs
|
|
701
|
+
: false;
|
|
702
|
+
return {
|
|
703
|
+
data,
|
|
704
|
+
isLoading,
|
|
705
|
+
error,
|
|
706
|
+
isStale,
|
|
707
|
+
lastSyncAt,
|
|
708
|
+
refetch,
|
|
709
|
+
};
|
|
710
|
+
}, [data, isLoading, error, staleAfterMs, staleClock, lastSyncAt, refetch]);
|
|
395
711
|
}
|
|
396
712
|
function useQuery(queryFn, options = {}) {
|
|
397
713
|
const { enabled = true, deps = [], keyField = 'id' } = options;
|
|
@@ -801,6 +1117,11 @@ export function createSyncularReact() {
|
|
|
801
1117
|
useSyncEngine,
|
|
802
1118
|
useSyncStatus,
|
|
803
1119
|
useSyncConnection,
|
|
1120
|
+
useTransportHealth,
|
|
1121
|
+
useSyncProgress,
|
|
1122
|
+
useSyncInspector,
|
|
1123
|
+
useSyncSubscriptions,
|
|
1124
|
+
useSyncSubscription,
|
|
804
1125
|
useSyncQuery,
|
|
805
1126
|
useQuery,
|
|
806
1127
|
useMutation,
|