@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.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 createContext2,
145
- useContext as useContext2,
146
- useEffect as useEffect2,
147
- useMemo as useMemo3,
148
- useRef as useRef5
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/config.tsx
203
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
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 ReplicationProvider({ children, ...config }) {
206
- const key = `${config.endpoint}|${config.pollMs ?? ""}|${JSON.stringify(config.paths ?? null)}|${JSON.stringify(config.prefetch ?? null)}|${config.prefetchMode ?? ""}|${config.prefetchConcurrency ?? ""}`;
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
- () => config,
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
- return /* @__PURE__ */ jsxs(ReplicationContext.Provider, { value, children: [
213
- value.prefetch && value.prefetch.length > 0 ? /* @__PURE__ */ jsx2(PrefetchRunner, {}) : null,
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 useContext2(ReplicationContext);
389
+ return useContext3(ReplicationContext2);
233
390
  }
234
391
  function useReplicationConfig(overrides) {
235
- return resolveReplicationConfig(useContext2(ReplicationContext), overrides);
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 = useRef5(base);
422
+ const baseRef = useRef7(base);
266
423
  baseRef.current = base;
267
424
  const sig = JSON.stringify({ slices, mode, concurrency, endpoint: base?.endpoint ?? null });
268
- useEffect2(() => {
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 useCallback3, useEffect as useEffect3, useRef as useRef6, useState as useState2 } from "react";
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, source = "local-first" } = options;
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 read = useFind(col, filter);
311
- const { config, pollMs } = useReplicationConfig({
312
- endpoint: options.endpoint,
313
- getAuth: options.getAuth,
314
- fetch: options.fetch,
315
- paths: options.paths,
316
- pollMs: options.pollMs
317
- });
318
- const configRef = useRef6(config);
319
- configRef.current = config;
320
- const [syncing, setSyncing] = useState2(false);
321
- const [syncError, setSyncError] = useState2(null);
322
- const [firstSyncDone, setFirstSyncDone] = useState2(false);
323
- const endpoint = config?.endpoint;
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 (e) {
332
- setSyncError(e);
499
+ } catch (error) {
500
+ setSyncError(error);
333
501
  } finally {
334
502
  setSyncing(false);
335
503
  setFirstSyncDone(true);
336
504
  }
337
- }, [db, collection, networked, endpoint]);
338
- useEffect3(() => {
339
- if (!networked) return;
340
- void refetch();
505
+ }, [db, collection, legacyNetworked, legacyConfig?.endpoint]);
506
+ useEffect4(() => {
507
+ if (!enabled || !legacyNetworked || !legacyConfig) return;
508
+ void legacyRefetch();
341
509
  if (pollMs > 0) {
342
- const id = setInterval(() => void refetch(), pollMs);
343
- return () => clearInterval(id);
510
+ const timer = setInterval(() => void legacyRefetch(), pollMs);
511
+ return () => clearInterval(timer);
344
512
  }
345
513
  return void 0;
346
- }, [refetch, networked, pollMs]);
347
- if (networked && !config) {
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 an endpoint for source '${source}'. Wrap the tree in <ReplicationProvider endpoint="\u2026">, pass { endpoint }, or use source: "local-only".`
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
- const loading = source === "remote-first" ? read.loading || !firstSyncDone : read.loading;
353
- return { data: read.data, loading, error: read.error, syncing, syncError, refetch };
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 useEffect4, useRef as useRef7, useState as useState3 } from "react";
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 base = useReplicationBase();
366
- for (const q of queries) {
367
- const networked = (q.source ?? "local-first") !== "local-only";
368
- if (networked && !(q.endpoint ?? base?.endpoint)) {
369
- throw new Error(
370
- `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".`
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
- source: q.source ?? "local-first",
379
- endpoint: q.endpoint ?? null,
380
- pollMs: q.pollMs ?? null
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 queriesRef = useRef7(queries);
384
- queriesRef.current = queries;
385
- const baseRef = useRef7(base);
386
- baseRef.current = base;
387
- const [results, setResults] = useState3(
388
- () => queries.map(() => emptyResult())
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
- useEffect4(() => {
391
- const qs = queriesRef.current;
392
- const b = baseRef.current;
393
- let cancelled = false;
394
- const setAt = (i, fn) => {
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
- return { config, networked, pollMs, refetch };
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
- setResults(
426
- qs.map((_q, i) => ({
427
- data: [],
428
- loading: true,
429
- error: null,
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: resolved[i].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
- const intervals = [];
448
- resolved.forEach((res) => {
449
- if (!res.networked || !res.config) return;
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 useCallback4, useEffect as useEffect5, useRef as useRef8, useState as useState4 } from "react";
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 = useRef8(config);
715
+ const configRef = useRef10(config);
475
716
  configRef.current = config;
476
- const [pending, setPending] = useState4(false);
477
- const [error, setError] = useState4(null);
717
+ const [pending, setPending] = useState5(false);
718
+ const [error, setError] = useState5(null);
478
719
  const endpoint = config?.endpoint;
479
- const applyLocal = useCallback4(
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 = useCallback4(async () => {
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 = useCallback4(
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 = useCallback4(
757
+ const mutate = useCallback5(
517
758
  (op) => {
518
759
  void mutateAsync(op).catch(() => {
519
760
  });
520
761
  },
521
762
  [mutateAsync]
522
763
  );
523
- useEffect5(() => {
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,