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