@taladb/react 0.9.3 → 0.9.4
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.mts +175 -41
- package/dist/index.d.ts +175 -41
- package/dist/index.js +383 -146
- package/dist/index.mjs +393 -149
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -139,13 +139,40 @@ function useFindOne(collection, filter) {
|
|
|
139
139
|
return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// src/useAggregate.ts
|
|
143
|
+
import { useCallback as useCallback3, useRef as useRef5, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
144
|
+
function useAggregate(collection, pipeline) {
|
|
145
|
+
const snapshotRef = useRef5({ data: [], loading: true, error: null });
|
|
146
|
+
const pipelineKey = JSON.stringify(pipeline);
|
|
147
|
+
const subscribe = useCallback3(
|
|
148
|
+
(notify) => {
|
|
149
|
+
snapshotRef.current = { data: snapshotRef.current.data, loading: true, error: null };
|
|
150
|
+
return collection.subscribeAggregate(
|
|
151
|
+
pipeline,
|
|
152
|
+
(docs) => {
|
|
153
|
+
snapshotRef.current = { data: docs, loading: false, error: null };
|
|
154
|
+
notify();
|
|
155
|
+
},
|
|
156
|
+
(error) => {
|
|
157
|
+
snapshotRef.current = { ...snapshotRef.current, loading: false, error };
|
|
158
|
+
notify();
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
+
[collection, pipelineKey]
|
|
164
|
+
);
|
|
165
|
+
const getSnapshot = useCallback3(() => snapshotRef.current, []);
|
|
166
|
+
return useSyncExternalStore3(subscribe, getSnapshot, getSnapshot);
|
|
167
|
+
}
|
|
168
|
+
|
|
142
169
|
// src/replication/config.tsx
|
|
143
170
|
import {
|
|
144
|
-
createContext as
|
|
145
|
-
useContext as
|
|
146
|
-
useEffect as
|
|
147
|
-
useMemo as
|
|
148
|
-
useRef as
|
|
171
|
+
createContext as createContext3,
|
|
172
|
+
useContext as useContext3,
|
|
173
|
+
useEffect as useEffect3,
|
|
174
|
+
useMemo as useMemo4,
|
|
175
|
+
useRef as useRef7
|
|
149
176
|
} from "react";
|
|
150
177
|
|
|
151
178
|
// src/replication/engine.ts
|
|
@@ -199,20 +226,150 @@ async function replicateWithRetry(db, config, collection, direction) {
|
|
|
199
226
|
throw lastError;
|
|
200
227
|
}
|
|
201
228
|
|
|
202
|
-
// src/replication/
|
|
203
|
-
import {
|
|
229
|
+
// src/replication/provider.tsx
|
|
230
|
+
import {
|
|
231
|
+
createContext as createContext2,
|
|
232
|
+
useContext as useContext2,
|
|
233
|
+
useEffect as useEffect2,
|
|
234
|
+
useMemo as useMemo3,
|
|
235
|
+
useRef as useRef6,
|
|
236
|
+
useState as useState2
|
|
237
|
+
} from "react";
|
|
238
|
+
import {
|
|
239
|
+
ReplicationCoordinator,
|
|
240
|
+
createRestSource
|
|
241
|
+
} from "taladb";
|
|
242
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
204
243
|
var ReplicationContext = createContext2(null);
|
|
205
|
-
function
|
|
206
|
-
const
|
|
244
|
+
function whenIdle(fn) {
|
|
245
|
+
const ric = globalThis.requestIdleCallback;
|
|
246
|
+
if (typeof ric === "function") {
|
|
247
|
+
const handle = ric(fn, { timeout: 2e3 });
|
|
248
|
+
return () => {
|
|
249
|
+
const cic = globalThis.cancelIdleCallback;
|
|
250
|
+
cic?.(handle);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const t = setTimeout(fn, 0);
|
|
254
|
+
return () => clearTimeout(t);
|
|
255
|
+
}
|
|
256
|
+
var yieldToUi = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
257
|
+
function ReplicationScopes({ replicate: replicate2, children }) {
|
|
258
|
+
const db = useTalaDB();
|
|
259
|
+
const collectionOptions = useCollectionOptions();
|
|
260
|
+
const [coverage, setCoverage] = useState2({});
|
|
261
|
+
const registryKey = JSON.stringify(
|
|
262
|
+
Object.fromEntries(
|
|
263
|
+
Object.entries(replicate2).map(([name, s]) => [
|
|
264
|
+
name,
|
|
265
|
+
{
|
|
266
|
+
endpoint: s.endpoint,
|
|
267
|
+
origin: s.origin,
|
|
268
|
+
scope: s.scope,
|
|
269
|
+
projectionVersion: s.projectionVersion,
|
|
270
|
+
schemaVersion: s.schemaVersion,
|
|
271
|
+
key: s.key,
|
|
272
|
+
hydrate: s.hydrate,
|
|
273
|
+
pageSize: s.pageSize,
|
|
274
|
+
refreshMs: s.refreshMs,
|
|
275
|
+
bridge: s.bridge,
|
|
276
|
+
source: s.source ? {
|
|
277
|
+
origin: s.source.origin,
|
|
278
|
+
collection: s.source.collection,
|
|
279
|
+
scope: s.source.scope,
|
|
280
|
+
projectionVersion: s.source.projectionVersion,
|
|
281
|
+
schemaVersion: s.source.schemaVersion,
|
|
282
|
+
configVersion: s.source.configVersion
|
|
283
|
+
} : null
|
|
284
|
+
}
|
|
285
|
+
])
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
const latest = useRef6(replicate2);
|
|
289
|
+
latest.current = replicate2;
|
|
290
|
+
const coordinators = useMemo3(() => {
|
|
291
|
+
const map = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const [collection, scope] of Object.entries(latest.current)) {
|
|
293
|
+
const source = scope.source ?? createRestSource({ ...scope, collection });
|
|
294
|
+
map.set(
|
|
295
|
+
collection,
|
|
296
|
+
new ReplicationCoordinator(db, source, {
|
|
297
|
+
pageSize: scope.pageSize,
|
|
298
|
+
yieldFn: yieldToUi,
|
|
299
|
+
onProgress: (state) => setCoverage((prev) => ({ ...prev, [collection]: state })),
|
|
300
|
+
collectionOptions: collectionOptions.get(collection)
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return map;
|
|
305
|
+
}, [db, registryKey, collectionOptions]);
|
|
306
|
+
useEffect2(() => {
|
|
307
|
+
let cancelled = false;
|
|
308
|
+
void (async () => {
|
|
309
|
+
const seeded = {};
|
|
310
|
+
for (const [collection, coord] of coordinators) {
|
|
311
|
+
seeded[collection] = await coord.getCoverage();
|
|
312
|
+
}
|
|
313
|
+
if (!cancelled) setCoverage(seeded);
|
|
314
|
+
})();
|
|
315
|
+
return () => {
|
|
316
|
+
cancelled = true;
|
|
317
|
+
};
|
|
318
|
+
}, [coordinators]);
|
|
319
|
+
useEffect2(() => {
|
|
320
|
+
const cancels = [];
|
|
321
|
+
for (const [collection, coord] of coordinators) {
|
|
322
|
+
const mode = latest.current[collection]?.hydrate ?? "idle";
|
|
323
|
+
if (mode === "manual") continue;
|
|
324
|
+
const start = () => {
|
|
325
|
+
void coord.hydrate().catch(() => {
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
if (mode === "eager") start();
|
|
329
|
+
else cancels.push(whenIdle(start));
|
|
330
|
+
}
|
|
331
|
+
return () => cancels.forEach((c) => c());
|
|
332
|
+
}, [coordinators]);
|
|
333
|
+
useEffect2(() => {
|
|
334
|
+
const timers = [];
|
|
335
|
+
for (const [collection, coord] of coordinators) {
|
|
336
|
+
const ms = latest.current[collection]?.refreshMs ?? 0;
|
|
337
|
+
if (ms > 0) {
|
|
338
|
+
timers.push(setInterval(() => void coord.refresh().catch(() => {
|
|
339
|
+
}), ms));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return () => timers.forEach(clearInterval);
|
|
343
|
+
}, [coordinators]);
|
|
207
344
|
const value = useMemo3(
|
|
208
|
-
() =>
|
|
345
|
+
() => ({ coordinators, scopes: latest.current, coverage }),
|
|
346
|
+
[coordinators, coverage]
|
|
347
|
+
);
|
|
348
|
+
return /* @__PURE__ */ jsx2(ReplicationContext.Provider, { value, children });
|
|
349
|
+
}
|
|
350
|
+
function useReplication() {
|
|
351
|
+
return useContext2(ReplicationContext);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/replication/config.tsx
|
|
355
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
356
|
+
var ReplicationContext2 = createContext3(null);
|
|
357
|
+
function ReplicationProvider({
|
|
358
|
+
children,
|
|
359
|
+
replicate: replicate2,
|
|
360
|
+
...config
|
|
361
|
+
}) {
|
|
362
|
+
const key = `${config.endpoint ?? ""}|${config.pollMs ?? ""}|${JSON.stringify(config.paths ?? null)}|${JSON.stringify(config.prefetch ?? null)}|${config.prefetchMode ?? ""}|${config.prefetchConcurrency ?? ""}`;
|
|
363
|
+
const value = useMemo4(
|
|
364
|
+
() => config.endpoint ? config : null,
|
|
209
365
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
210
366
|
[key]
|
|
211
367
|
);
|
|
212
|
-
|
|
213
|
-
value
|
|
368
|
+
const inner = /* @__PURE__ */ jsxs(ReplicationContext2.Provider, { value, children: [
|
|
369
|
+
value?.prefetch && value.prefetch.length > 0 ? /* @__PURE__ */ jsx3(PrefetchRunner, {}) : null,
|
|
214
370
|
children
|
|
215
371
|
] });
|
|
372
|
+
return replicate2 ? /* @__PURE__ */ jsx3(ReplicationScopes, { replicate: replicate2, children: inner }) : inner;
|
|
216
373
|
}
|
|
217
374
|
function resolveReplicationConfig(base, overrides) {
|
|
218
375
|
const endpoint = overrides?.endpoint ?? base?.endpoint;
|
|
@@ -229,10 +386,10 @@ function resolveReplicationConfig(base, overrides) {
|
|
|
229
386
|
};
|
|
230
387
|
}
|
|
231
388
|
function useReplicationBase() {
|
|
232
|
-
return
|
|
389
|
+
return useContext3(ReplicationContext2);
|
|
233
390
|
}
|
|
234
391
|
function useReplicationConfig(overrides) {
|
|
235
|
-
return resolveReplicationConfig(
|
|
392
|
+
return resolveReplicationConfig(useContext3(ReplicationContext2), overrides);
|
|
236
393
|
}
|
|
237
394
|
var CURSOR_COLLECTION = "__taladb_sync";
|
|
238
395
|
function normalizePrefetch(entries) {
|
|
@@ -262,10 +419,10 @@ function PrefetchRunner() {
|
|
|
262
419
|
const slices = normalizePrefetch(base?.prefetch);
|
|
263
420
|
const mode = base?.prefetchMode ?? "once";
|
|
264
421
|
const concurrency = Math.max(1, base?.prefetchConcurrency ?? 2);
|
|
265
|
-
const baseRef =
|
|
422
|
+
const baseRef = useRef7(base);
|
|
266
423
|
baseRef.current = base;
|
|
267
424
|
const sig = JSON.stringify({ slices, mode, concurrency, endpoint: base?.endpoint ?? null });
|
|
268
|
-
|
|
425
|
+
useEffect3(() => {
|
|
269
426
|
if (slices.length === 0) return void 0;
|
|
270
427
|
let cancelled = false;
|
|
271
428
|
const cancelSchedule = schedule(() => {
|
|
@@ -300,167 +457,251 @@ function PrefetchRunner() {
|
|
|
300
457
|
return null;
|
|
301
458
|
}
|
|
302
459
|
|
|
460
|
+
// src/useCoverage.ts
|
|
461
|
+
import { isAuthoritative, progress as progressOf, rowsApplied } from "taladb";
|
|
462
|
+
function useCoverage(collection) {
|
|
463
|
+
const replication = useReplication();
|
|
464
|
+
const state = replication?.coverage[collection] ?? { status: "empty" };
|
|
465
|
+
return {
|
|
466
|
+
status: state.status,
|
|
467
|
+
ready: isAuthoritative(state),
|
|
468
|
+
rows: rowsApplied(state),
|
|
469
|
+
total: "total" in state ? state.total : void 0,
|
|
470
|
+
progress: progressOf(state),
|
|
471
|
+
reason: state.status === "error" ? state.error : state.status === "best-effort" || state.status === "stale" ? state.reason : void 0
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
var useHydrationProgress = useCoverage;
|
|
475
|
+
|
|
303
476
|
// src/useQuery.ts
|
|
304
|
-
import { useCallback as
|
|
477
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo5, useRef as useRef8, useState as useState3 } from "react";
|
|
305
478
|
function useQuery(options) {
|
|
306
|
-
const { collection, filter,
|
|
307
|
-
const networked = source !== "local-only";
|
|
308
|
-
const db = useTalaDB();
|
|
479
|
+
const { collection, filter, sort, page, limit, skip, enabled = true } = options;
|
|
309
480
|
const col = useCollection(collection);
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
const [
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const refetch = useCallback3(async () => {
|
|
325
|
-
const cfg = configRef.current;
|
|
326
|
-
if (!networked || !cfg) return;
|
|
481
|
+
const db = useTalaDB();
|
|
482
|
+
const coverage = useCoverage(collection);
|
|
483
|
+
const replication = useReplication();
|
|
484
|
+
const coord = replication?.coordinators.get(collection);
|
|
485
|
+
const legacyNetworked = !coord && options.source !== "local-only";
|
|
486
|
+
const { config: legacyConfig, pollMs } = useReplicationConfig(options);
|
|
487
|
+
const legacyConfigRef = useRef8(legacyConfig);
|
|
488
|
+
legacyConfigRef.current = legacyConfig;
|
|
489
|
+
const [syncing, setSyncing] = useState3(false);
|
|
490
|
+
const [syncError, setSyncError] = useState3(null);
|
|
491
|
+
const [firstSyncDone, setFirstSyncDone] = useState3(false);
|
|
492
|
+
const legacyRefetch = useCallback4(async () => {
|
|
493
|
+
const cfg = legacyConfigRef.current;
|
|
494
|
+
if (!legacyNetworked || !cfg) return;
|
|
327
495
|
setSyncing(true);
|
|
328
496
|
setSyncError(null);
|
|
329
497
|
try {
|
|
330
498
|
await replicate(db, cfg, collection, "pull");
|
|
331
|
-
} catch (
|
|
332
|
-
setSyncError(
|
|
499
|
+
} catch (error) {
|
|
500
|
+
setSyncError(error);
|
|
333
501
|
} finally {
|
|
334
502
|
setSyncing(false);
|
|
335
503
|
setFirstSyncDone(true);
|
|
336
504
|
}
|
|
337
|
-
}, [db, collection,
|
|
338
|
-
|
|
339
|
-
if (!
|
|
340
|
-
void
|
|
505
|
+
}, [db, collection, legacyNetworked, legacyConfig?.endpoint]);
|
|
506
|
+
useEffect4(() => {
|
|
507
|
+
if (!enabled || !legacyNetworked || !legacyConfig) return;
|
|
508
|
+
void legacyRefetch();
|
|
341
509
|
if (pollMs > 0) {
|
|
342
|
-
const
|
|
343
|
-
return () => clearInterval(
|
|
510
|
+
const timer = setInterval(() => void legacyRefetch(), pollMs);
|
|
511
|
+
return () => clearInterval(timer);
|
|
344
512
|
}
|
|
345
513
|
return void 0;
|
|
346
|
-
}, [
|
|
347
|
-
|
|
514
|
+
}, [enabled, legacyNetworked, legacyConfig?.endpoint, pollMs, legacyRefetch]);
|
|
515
|
+
const offset = page !== void 0 && limit !== void 0 ? (page - 1) * limit : skip ?? 0;
|
|
516
|
+
const filterKey = JSON.stringify(filter ?? null);
|
|
517
|
+
const sortKey = JSON.stringify(sort ?? null);
|
|
518
|
+
const [bridgeIds, setBridgeIds] = useState3([]);
|
|
519
|
+
const [fetchError, setFetchError] = useState3(null);
|
|
520
|
+
const scopeValue = coord?.replicaScope;
|
|
521
|
+
const bridgeIdKey = (bridgeIds ?? []).join("|");
|
|
522
|
+
const pipeline = useMemo5(() => {
|
|
523
|
+
const stages = [];
|
|
524
|
+
const scoped = scopeValue ? { _replica_scope: scopeValue } : void 0;
|
|
525
|
+
const bridgeOnly = !coverage.ready ? { _id: { $in: bridgeIds ?? [] } } : void 0;
|
|
526
|
+
const matches = [scoped, bridgeOnly, filter].filter(Boolean);
|
|
527
|
+
if (matches.length === 1) stages.push({ $match: matches[0] });
|
|
528
|
+
else if (matches.length > 1) stages.push({ $match: { $and: matches } });
|
|
529
|
+
if (sort) stages.push({ $sort: sort });
|
|
530
|
+
if (coverage.ready && offset > 0) stages.push({ $skip: offset });
|
|
531
|
+
if (limit !== void 0) stages.push({ $limit: limit });
|
|
532
|
+
return stages;
|
|
533
|
+
}, [filterKey, sortKey, offset, limit, coverage.ready, scopeValue, bridgeIdKey]);
|
|
534
|
+
const read = useAggregate(col, enabled ? pipeline : [{ $limit: 0 }]);
|
|
535
|
+
const [fetching, setFetching] = useState3(false);
|
|
536
|
+
const bridgeKey = `${collection}|${filterKey}|${sortKey}|${offset}|${limit}`;
|
|
537
|
+
const canBridge = replication?.scopes[collection]?.bridge !== false;
|
|
538
|
+
useEffect4(() => {
|
|
539
|
+
if (!enabled || coverage.ready || !canBridge) return;
|
|
540
|
+
if (!coord) return;
|
|
541
|
+
let cancelled = false;
|
|
542
|
+
setFetching(true);
|
|
543
|
+
setFetchError(null);
|
|
544
|
+
setBridgeIds([]);
|
|
545
|
+
void coord.bridge({
|
|
546
|
+
filter,
|
|
547
|
+
sort,
|
|
548
|
+
page,
|
|
549
|
+
limit
|
|
550
|
+
}).then((result) => setBridgeIds(result.ids ?? [])).catch((error) => {
|
|
551
|
+
if (!cancelled) setFetchError(error);
|
|
552
|
+
}).finally(() => {
|
|
553
|
+
if (!cancelled) setFetching(false);
|
|
554
|
+
});
|
|
555
|
+
return () => {
|
|
556
|
+
cancelled = true;
|
|
557
|
+
};
|
|
558
|
+
}, [bridgeKey, coverage.ready, canBridge, enabled, coord]);
|
|
559
|
+
const refetch = async () => {
|
|
560
|
+
if (coord) await coord.refresh();
|
|
561
|
+
else await legacyRefetch();
|
|
562
|
+
};
|
|
563
|
+
if (enabled && legacyNetworked && !legacyConfig) {
|
|
348
564
|
throw new Error(
|
|
349
|
-
`useQuery({ collection: '${collection}' }) needs
|
|
565
|
+
`useQuery({ collection: '${collection}' }) needs either a coverage-first replicate scope or a legacy sync endpoint. Use source: 'local-only' for a purely local query.`
|
|
350
566
|
);
|
|
351
567
|
}
|
|
352
|
-
|
|
353
|
-
|
|
568
|
+
return {
|
|
569
|
+
data: read.data,
|
|
570
|
+
total: coverage.total,
|
|
571
|
+
loading: options.source === "remote-first" && legacyNetworked ? read.loading || !firstSyncDone : read.loading,
|
|
572
|
+
error: read.error ?? fetchError,
|
|
573
|
+
fetchError,
|
|
574
|
+
coverage,
|
|
575
|
+
fetching,
|
|
576
|
+
syncing,
|
|
577
|
+
syncError,
|
|
578
|
+
refetch
|
|
579
|
+
};
|
|
354
580
|
}
|
|
355
581
|
|
|
356
582
|
// src/useQueries.ts
|
|
357
|
-
import { useEffect as
|
|
358
|
-
var NOOP_REFETCH = async () => {
|
|
359
|
-
};
|
|
360
|
-
function emptyResult() {
|
|
361
|
-
return { data: [], loading: true, error: null, syncing: false, syncError: null, refetch: NOOP_REFETCH };
|
|
362
|
-
}
|
|
583
|
+
import { useEffect as useEffect5, useMemo as useMemo6, useRef as useRef9, useState as useState4 } from "react";
|
|
363
584
|
function useQueries(queries) {
|
|
364
585
|
const db = useTalaDB();
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
const sig = JSON.stringify(
|
|
586
|
+
const registry = useCollectionOptions();
|
|
587
|
+
const replication = useReplication();
|
|
588
|
+
const [results, setResults] = useState4(() => queries.map(() => ({ data: [], loading: true, error: null })));
|
|
589
|
+
const [bridgeIds, setBridgeIds] = useState4({});
|
|
590
|
+
const [fetchErrors, setFetchErrors] = useState4({});
|
|
591
|
+
const signature = JSON.stringify(
|
|
375
592
|
queries.map((q) => ({
|
|
376
593
|
collection: q.collection,
|
|
377
594
|
filter: q.filter ?? null,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
595
|
+
sort: q.sort ?? null,
|
|
596
|
+
page: q.page ?? null,
|
|
597
|
+
limit: q.limit ?? null,
|
|
598
|
+
skip: q.skip ?? null,
|
|
599
|
+
enabled: q.enabled ?? true
|
|
381
600
|
}))
|
|
382
601
|
);
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
602
|
+
const latest = useRef9(queries);
|
|
603
|
+
latest.current = queries;
|
|
604
|
+
const bridgeManifestKey = JSON.stringify(bridgeIds);
|
|
605
|
+
const replicationReadKey = JSON.stringify(
|
|
606
|
+
queries.map((q) => ({
|
|
607
|
+
scope: replication?.coordinators.get(q.collection)?.replicaScope ?? null,
|
|
608
|
+
ready: replication?.coverage[q.collection]?.status === "complete"
|
|
609
|
+
}))
|
|
389
610
|
);
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
setResults((prev) => {
|
|
396
|
-
if (i >= prev.length) return prev;
|
|
397
|
-
const copy = prev.slice();
|
|
398
|
-
copy[i] = fn(copy[i]);
|
|
399
|
-
return copy;
|
|
400
|
-
});
|
|
401
|
-
};
|
|
402
|
-
const resolved = qs.map((q, i) => {
|
|
403
|
-
const { config } = resolveReplicationConfig(b, {
|
|
404
|
-
endpoint: q.endpoint,
|
|
405
|
-
getAuth: q.getAuth,
|
|
406
|
-
fetch: q.fetch,
|
|
407
|
-
paths: q.paths,
|
|
408
|
-
pollMs: q.pollMs
|
|
409
|
-
});
|
|
410
|
-
const networked = (q.source ?? "local-first") !== "local-only";
|
|
411
|
-
const pollMs = q.pollMs ?? b?.pollMs ?? 0;
|
|
412
|
-
const refetch = async () => {
|
|
413
|
-
if (!networked || !config) return;
|
|
414
|
-
setAt(i, (r) => ({ ...r, syncing: true, syncError: null }));
|
|
415
|
-
try {
|
|
416
|
-
await replicate(db, config, q.collection, "pull");
|
|
417
|
-
} catch (e) {
|
|
418
|
-
if (!cancelled) setAt(i, (r) => ({ ...r, syncError: e }));
|
|
419
|
-
} finally {
|
|
420
|
-
if (!cancelled) setAt(i, (r) => ({ ...r, syncing: false }));
|
|
421
|
-
}
|
|
611
|
+
useEffect5(() => {
|
|
612
|
+
const current = latest.current;
|
|
613
|
+
setResults(current.map(() => ({ data: [], loading: true, error: null })));
|
|
614
|
+
const unsubs = current.map((q, i) => {
|
|
615
|
+
if (q.enabled === false) return () => {
|
|
422
616
|
};
|
|
423
|
-
|
|
617
|
+
const col = db.collection(q.collection, registry.get(q.collection));
|
|
618
|
+
const offset = q.page !== void 0 && q.limit !== void 0 ? (q.page - 1) * q.limit : q.skip ?? 0;
|
|
619
|
+
const pipeline = [];
|
|
620
|
+
const coord = replication?.coordinators.get(q.collection);
|
|
621
|
+
const covered = replication?.coverage[q.collection]?.status === "complete";
|
|
622
|
+
const matches = [
|
|
623
|
+
coord ? { _replica_scope: coord.replicaScope } : void 0,
|
|
624
|
+
!covered ? { _id: { $in: bridgeIds[i] ?? [] } } : void 0,
|
|
625
|
+
q.filter
|
|
626
|
+
].filter(Boolean);
|
|
627
|
+
if (matches.length === 1) pipeline.push({ $match: matches[0] });
|
|
628
|
+
else if (matches.length > 1) pipeline.push({ $match: { $and: matches } });
|
|
629
|
+
if (q.sort) pipeline.push({ $sort: q.sort });
|
|
630
|
+
if (covered && offset > 0) pipeline.push({ $skip: offset });
|
|
631
|
+
if (q.limit !== void 0) pipeline.push({ $limit: q.limit });
|
|
632
|
+
return col.subscribeAggregate(
|
|
633
|
+
pipeline,
|
|
634
|
+
(docs) => setResults((prev) => {
|
|
635
|
+
const next = [...prev];
|
|
636
|
+
next[i] = { data: docs, loading: false, error: null };
|
|
637
|
+
return next;
|
|
638
|
+
}),
|
|
639
|
+
(error) => setResults((prev) => {
|
|
640
|
+
const next = [...prev];
|
|
641
|
+
next[i] = { ...next[i], loading: false, error };
|
|
642
|
+
return next;
|
|
643
|
+
})
|
|
644
|
+
);
|
|
424
645
|
});
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
646
|
+
return () => unsubs.forEach((u) => u());
|
|
647
|
+
}, [db, registry, signature, replicationReadKey, bridgeManifestKey]);
|
|
648
|
+
useEffect5(() => {
|
|
649
|
+
for (const [i, q] of latest.current.entries()) {
|
|
650
|
+
if (q.enabled === false) continue;
|
|
651
|
+
const coord = replication?.coordinators.get(q.collection);
|
|
652
|
+
if (!coord || replication?.scopes[q.collection]?.bridge === false) continue;
|
|
653
|
+
void coord.getCoverage().then((state) => {
|
|
654
|
+
if (state.status === "complete") return;
|
|
655
|
+
return coord.bridge({
|
|
656
|
+
filter: q.filter,
|
|
657
|
+
sort: q.sort,
|
|
658
|
+
page: q.page,
|
|
659
|
+
limit: q.limit
|
|
660
|
+
}).then((result) => {
|
|
661
|
+
setBridgeIds((prev) => ({ ...prev, [i]: result.ids }));
|
|
662
|
+
setFetchErrors((prev) => {
|
|
663
|
+
const next = { ...prev };
|
|
664
|
+
delete next[i];
|
|
665
|
+
return next;
|
|
666
|
+
});
|
|
667
|
+
}).catch((error) => setFetchErrors((prev) => ({ ...prev, [i]: error })));
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}, [replication, signature]);
|
|
671
|
+
return useMemo6(
|
|
672
|
+
() => latest.current.map((q, i) => {
|
|
673
|
+
const state = replication?.coverage[q.collection] ?? { status: "empty" };
|
|
674
|
+
const coverage = {
|
|
675
|
+
status: state.status,
|
|
676
|
+
// Only `complete` licenses a local-only read — see `useCoverage`.
|
|
677
|
+
ready: state.status === "complete",
|
|
678
|
+
rows: "rowsApplied" in state ? state.rowsApplied ?? 0 : 0,
|
|
679
|
+
total: "total" in state ? state.total : void 0,
|
|
680
|
+
progress: state.status === "complete" ? 1 : void 0,
|
|
681
|
+
reason: state.status === "error" ? state.error : state.status === "best-effort" || state.status === "stale" ? state.reason : void 0
|
|
682
|
+
};
|
|
683
|
+
return {
|
|
684
|
+
data: results[i]?.data ?? [],
|
|
685
|
+
total: coverage.total,
|
|
686
|
+
loading: results[i]?.loading ?? true,
|
|
687
|
+
error: results[i]?.error ?? fetchErrors[i] ?? null,
|
|
688
|
+
fetchError: fetchErrors[i] ?? null,
|
|
689
|
+
coverage,
|
|
690
|
+
fetching: false,
|
|
430
691
|
syncing: false,
|
|
431
692
|
syncError: null,
|
|
432
|
-
refetch:
|
|
433
|
-
|
|
434
|
-
);
|
|
435
|
-
const unsubs = qs.map((q, i) => {
|
|
436
|
-
const col = db.collection(q.collection);
|
|
437
|
-
return col.subscribe(
|
|
438
|
-
q.filter ?? {},
|
|
439
|
-
(docs) => {
|
|
440
|
-
if (!cancelled) setAt(i, (r) => ({ ...r, data: docs, loading: false, error: null }));
|
|
441
|
-
},
|
|
442
|
-
(error) => {
|
|
443
|
-
if (!cancelled) setAt(i, (r) => ({ ...r, loading: false, error }));
|
|
693
|
+
refetch: async () => {
|
|
694
|
+
await replication?.coordinators.get(q.collection)?.refresh();
|
|
444
695
|
}
|
|
445
|
-
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
void res.refetch();
|
|
451
|
-
if (res.pollMs > 0) intervals.push(setInterval(() => void res.refetch(), res.pollMs));
|
|
452
|
-
});
|
|
453
|
-
return () => {
|
|
454
|
-
cancelled = true;
|
|
455
|
-
unsubs.forEach((u) => u());
|
|
456
|
-
intervals.forEach((id) => clearInterval(id));
|
|
457
|
-
};
|
|
458
|
-
}, [db, sig]);
|
|
459
|
-
return queries.map((_q, i) => results[i] ?? emptyResult());
|
|
696
|
+
};
|
|
697
|
+
}),
|
|
698
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
699
|
+
[results, signature, replication]
|
|
700
|
+
);
|
|
460
701
|
}
|
|
461
702
|
|
|
462
703
|
// src/useMutation.ts
|
|
463
|
-
import { useCallback as
|
|
704
|
+
import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef10, useState as useState5 } from "react";
|
|
464
705
|
function useMutation(options) {
|
|
465
706
|
const { collection, direction = "push", drainOnMount = true } = options;
|
|
466
707
|
const db = useTalaDB();
|
|
@@ -471,12 +712,12 @@ function useMutation(options) {
|
|
|
471
712
|
fetch: options.fetch,
|
|
472
713
|
paths: options.paths
|
|
473
714
|
});
|
|
474
|
-
const configRef =
|
|
715
|
+
const configRef = useRef10(config);
|
|
475
716
|
configRef.current = config;
|
|
476
|
-
const [pending, setPending] =
|
|
477
|
-
const [error, setError] =
|
|
717
|
+
const [pending, setPending] = useState5(false);
|
|
718
|
+
const [error, setError] = useState5(null);
|
|
478
719
|
const endpoint = config?.endpoint;
|
|
479
|
-
const applyLocal =
|
|
720
|
+
const applyLocal = useCallback5(
|
|
480
721
|
async (op) => {
|
|
481
722
|
switch (op.type) {
|
|
482
723
|
case "insert":
|
|
@@ -492,12 +733,12 @@ function useMutation(options) {
|
|
|
492
733
|
},
|
|
493
734
|
[col]
|
|
494
735
|
);
|
|
495
|
-
const drain =
|
|
736
|
+
const drain = useCallback5(async () => {
|
|
496
737
|
const cfg = configRef.current;
|
|
497
738
|
if (!cfg) return;
|
|
498
739
|
await replicateWithRetry(db, cfg, collection, direction);
|
|
499
740
|
}, [db, collection, direction, endpoint]);
|
|
500
|
-
const mutateAsync =
|
|
741
|
+
const mutateAsync = useCallback5(
|
|
501
742
|
async (op) => {
|
|
502
743
|
setPending(true);
|
|
503
744
|
setError(null);
|
|
@@ -513,14 +754,14 @@ function useMutation(options) {
|
|
|
513
754
|
},
|
|
514
755
|
[applyLocal, drain]
|
|
515
756
|
);
|
|
516
|
-
const mutate =
|
|
757
|
+
const mutate = useCallback5(
|
|
517
758
|
(op) => {
|
|
518
759
|
void mutateAsync(op).catch(() => {
|
|
519
760
|
});
|
|
520
761
|
},
|
|
521
762
|
[mutateAsync]
|
|
522
763
|
);
|
|
523
|
-
|
|
764
|
+
useEffect6(() => {
|
|
524
765
|
if (!drainOnMount || !configRef.current) return;
|
|
525
766
|
void drain().catch(() => {
|
|
526
767
|
});
|
|
@@ -535,10 +776,13 @@ function useMutation(options) {
|
|
|
535
776
|
export {
|
|
536
777
|
ReplicationProvider,
|
|
537
778
|
TalaDBProvider,
|
|
779
|
+
useAggregate,
|
|
538
780
|
useCollection,
|
|
539
781
|
useCollectionOptions,
|
|
782
|
+
useCoverage,
|
|
540
783
|
useFind,
|
|
541
784
|
useFindOne,
|
|
785
|
+
useHydrationProgress,
|
|
542
786
|
useMutation,
|
|
543
787
|
useQueries,
|
|
544
788
|
useQuery,
|