@taladb/react 0.9.2 → 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,9 +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,
38
+ useCollectionOptions: () => useCollectionOptions,
39
+ useCoverage: () => useCoverage,
37
40
  useFind: () => useFind,
38
41
  useFindOne: () => useFindOne,
42
+ useHydrationProgress: () => useHydrationProgress,
39
43
  useMutation: () => useMutation,
40
44
  useQueries: () => useQueries,
41
45
  useQuery: () => useQuery,
@@ -48,9 +52,29 @@ module.exports = __toCommonJS(index_exports);
48
52
  var import_react = require("react");
49
53
  var import_jsx_runtime = require("react/jsx-runtime");
50
54
  var TalaDBContext = (0, import_react.createContext)(null);
55
+ var CollectionOptionsContext = (0, import_react.createContext)({
56
+ get: () => void 0
57
+ });
58
+ function useCollectionOptions() {
59
+ return (0, import_react.useContext)(CollectionOptionsContext);
60
+ }
61
+ function CollectionOptionsProvider({
62
+ collections,
63
+ children
64
+ }) {
65
+ const latest = (0, import_react.useRef)(collections);
66
+ latest.current = collections;
67
+ const resolver = (0, import_react.useMemo)(
68
+ () => ({
69
+ get: (name) => latest.current?.[name]
70
+ }),
71
+ []
72
+ );
73
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollectionOptionsContext.Provider, { value: resolver, children });
74
+ }
51
75
  function TalaDBProvider(props) {
52
76
  if ("db" in props && props.db) {
53
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TalaDBContext.Provider, { value: props.db, children: props.children });
77
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TalaDBContext.Provider, { value: props.db, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollectionOptionsProvider, { collections: props.collections, children: props.children }) });
54
78
  }
55
79
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NamedProvider, { ...props });
56
80
  }
@@ -58,6 +82,7 @@ function NamedProvider({
58
82
  name,
59
83
  options,
60
84
  fallback = null,
85
+ collections,
61
86
  children
62
87
  }) {
63
88
  const [db, setDb] = (0, import_react.useState)(null);
@@ -85,7 +110,7 @@ function NamedProvider({
85
110
  }, [name, optionsKey]);
86
111
  if (error !== null) throw error;
87
112
  if (db === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback });
88
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TalaDBContext.Provider, { value: db, children });
113
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TalaDBContext.Provider, { value: db, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollectionOptionsProvider, { collections, children }) });
89
114
  }
90
115
  function useTalaDB() {
91
116
  const db = (0, import_react.useContext)(TalaDBContext);
@@ -97,9 +122,15 @@ function useTalaDB() {
97
122
 
98
123
  // src/useCollection.ts
99
124
  var import_react2 = require("react");
100
- function useCollection(name) {
125
+ function useCollection(name, options) {
101
126
  const db = useTalaDB();
102
- return (0, import_react2.useMemo)(() => db.collection(name), [db, name]);
127
+ const registry = useCollectionOptions();
128
+ const explicit = (0, import_react2.useRef)(options);
129
+ explicit.current = options;
130
+ return (0, import_react2.useMemo)(
131
+ () => db.collection(name, explicit.current ?? registry.get(name)),
132
+ [db, name, registry]
133
+ );
103
134
  }
104
135
 
105
136
  // src/useFind.ts
@@ -149,8 +180,35 @@ function useFindOne(collection, filter) {
149
180
  return (0, import_react4.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
150
181
  }
151
182
 
152
- // src/replication/config.tsx
183
+ // src/useAggregate.ts
153
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");
154
212
 
155
213
  // src/replication/engine.ts
156
214
  var import_taladb = require("taladb");
@@ -203,20 +261,140 @@ async function replicateWithRetry(db, config, collection, direction) {
203
261
  throw lastError;
204
262
  }
205
263
 
206
- // src/replication/config.tsx
264
+ // src/replication/provider.tsx
265
+ var import_react6 = require("react");
266
+ var import_taladb2 = require("taladb");
207
267
  var import_jsx_runtime2 = require("react/jsx-runtime");
208
- var ReplicationContext = (0, import_react5.createContext)(null);
209
- function ReplicationProvider({ children, ...config }) {
210
- const key = `${config.endpoint}|${config.pollMs ?? ""}|${JSON.stringify(config.paths ?? null)}|${JSON.stringify(config.prefetch ?? null)}|${config.prefetchMode ?? ""}|${config.prefetchConcurrency ?? ""}`;
211
- const value = (0, import_react5.useMemo)(
212
- () => 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,
213
390
  // eslint-disable-next-line react-hooks/exhaustive-deps
214
391
  [key]
215
392
  );
216
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(ReplicationContext.Provider, { value, children: [
217
- 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,
218
395
  children
219
396
  ] });
397
+ return replicate2 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReplicationScopes, { replicate: replicate2, children: inner }) : inner;
220
398
  }
221
399
  function resolveReplicationConfig(base, overrides) {
222
400
  const endpoint = overrides?.endpoint ?? base?.endpoint;
@@ -233,10 +411,10 @@ function resolveReplicationConfig(base, overrides) {
233
411
  };
234
412
  }
235
413
  function useReplicationBase() {
236
- return (0, import_react5.useContext)(ReplicationContext);
414
+ return (0, import_react7.useContext)(ReplicationContext2);
237
415
  }
238
416
  function useReplicationConfig(overrides) {
239
- return resolveReplicationConfig((0, import_react5.useContext)(ReplicationContext), overrides);
417
+ return resolveReplicationConfig((0, import_react7.useContext)(ReplicationContext2), overrides);
240
418
  }
241
419
  var CURSOR_COLLECTION = "__taladb_sync";
242
420
  function normalizePrefetch(entries) {
@@ -266,10 +444,10 @@ function PrefetchRunner() {
266
444
  const slices = normalizePrefetch(base?.prefetch);
267
445
  const mode = base?.prefetchMode ?? "once";
268
446
  const concurrency = Math.max(1, base?.prefetchConcurrency ?? 2);
269
- const baseRef = (0, import_react5.useRef)(base);
447
+ const baseRef = (0, import_react7.useRef)(base);
270
448
  baseRef.current = base;
271
449
  const sig = JSON.stringify({ slices, mode, concurrency, endpoint: base?.endpoint ?? null });
272
- (0, import_react5.useEffect)(() => {
450
+ (0, import_react7.useEffect)(() => {
273
451
  if (slices.length === 0) return void 0;
274
452
  let cancelled = false;
275
453
  const cancelSchedule = schedule(() => {
@@ -304,167 +482,251 @@ function PrefetchRunner() {
304
482
  return null;
305
483
  }
306
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
+
307
501
  // src/useQuery.ts
308
- var import_react6 = require("react");
502
+ var import_react8 = require("react");
309
503
  function useQuery(options) {
310
- const { collection, filter, source = "local-first" } = options;
311
- const networked = source !== "local-only";
312
- const db = useTalaDB();
504
+ const { collection, filter, sort, page, limit, skip, enabled = true } = options;
313
505
  const col = useCollection(collection);
314
- const read = useFind(col, filter);
315
- const { config, pollMs } = useReplicationConfig({
316
- endpoint: options.endpoint,
317
- getAuth: options.getAuth,
318
- fetch: options.fetch,
319
- paths: options.paths,
320
- pollMs: options.pollMs
321
- });
322
- const configRef = (0, import_react6.useRef)(config);
323
- configRef.current = config;
324
- const [syncing, setSyncing] = (0, import_react6.useState)(false);
325
- const [syncError, setSyncError] = (0, import_react6.useState)(null);
326
- const [firstSyncDone, setFirstSyncDone] = (0, import_react6.useState)(false);
327
- const endpoint = config?.endpoint;
328
- const refetch = (0, import_react6.useCallback)(async () => {
329
- const cfg = configRef.current;
330
- 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;
331
520
  setSyncing(true);
332
521
  setSyncError(null);
333
522
  try {
334
523
  await replicate(db, cfg, collection, "pull");
335
- } catch (e) {
336
- setSyncError(e);
524
+ } catch (error) {
525
+ setSyncError(error);
337
526
  } finally {
338
527
  setSyncing(false);
339
528
  setFirstSyncDone(true);
340
529
  }
341
- }, [db, collection, networked, endpoint]);
342
- (0, import_react6.useEffect)(() => {
343
- if (!networked) return;
344
- void refetch();
530
+ }, [db, collection, legacyNetworked, legacyConfig?.endpoint]);
531
+ (0, import_react8.useEffect)(() => {
532
+ if (!enabled || !legacyNetworked || !legacyConfig) return;
533
+ void legacyRefetch();
345
534
  if (pollMs > 0) {
346
- const id = setInterval(() => void refetch(), pollMs);
347
- return () => clearInterval(id);
535
+ const timer = setInterval(() => void legacyRefetch(), pollMs);
536
+ return () => clearInterval(timer);
348
537
  }
349
538
  return void 0;
350
- }, [refetch, networked, pollMs]);
351
- 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) {
352
589
  throw new Error(
353
- `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.`
354
591
  );
355
592
  }
356
- const loading = source === "remote-first" ? read.loading || !firstSyncDone : read.loading;
357
- 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
+ };
358
605
  }
359
606
 
360
607
  // src/useQueries.ts
361
- var import_react7 = require("react");
362
- var NOOP_REFETCH = async () => {
363
- };
364
- function emptyResult() {
365
- return { data: [], loading: true, error: null, syncing: false, syncError: null, refetch: NOOP_REFETCH };
366
- }
608
+ var import_react9 = require("react");
367
609
  function useQueries(queries) {
368
610
  const db = useTalaDB();
369
- const base = useReplicationBase();
370
- for (const q of queries) {
371
- const networked = (q.source ?? "local-first") !== "local-only";
372
- if (networked && !(q.endpoint ?? base?.endpoint)) {
373
- throw new Error(
374
- `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".`
375
- );
376
- }
377
- }
378
- 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(
379
617
  queries.map((q) => ({
380
618
  collection: q.collection,
381
619
  filter: q.filter ?? null,
382
- source: q.source ?? "local-first",
383
- endpoint: q.endpoint ?? null,
384
- 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
385
625
  }))
386
626
  );
387
- const queriesRef = (0, import_react7.useRef)(queries);
388
- queriesRef.current = queries;
389
- const baseRef = (0, import_react7.useRef)(base);
390
- baseRef.current = base;
391
- const [results, setResults] = (0, import_react7.useState)(
392
- () => 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
+ }))
393
635
  );
394
- (0, import_react7.useEffect)(() => {
395
- const qs = queriesRef.current;
396
- const b = baseRef.current;
397
- let cancelled = false;
398
- const setAt = (i, fn) => {
399
- setResults((prev) => {
400
- if (i >= prev.length) return prev;
401
- const copy = prev.slice();
402
- copy[i] = fn(copy[i]);
403
- return copy;
404
- });
405
- };
406
- const resolved = qs.map((q, i) => {
407
- const { config } = resolveReplicationConfig(b, {
408
- endpoint: q.endpoint,
409
- getAuth: q.getAuth,
410
- fetch: q.fetch,
411
- paths: q.paths,
412
- pollMs: q.pollMs
413
- });
414
- const networked = (q.source ?? "local-first") !== "local-only";
415
- const pollMs = q.pollMs ?? b?.pollMs ?? 0;
416
- const refetch = async () => {
417
- if (!networked || !config) return;
418
- setAt(i, (r) => ({ ...r, syncing: true, syncError: null }));
419
- try {
420
- await replicate(db, config, q.collection, "pull");
421
- } catch (e) {
422
- if (!cancelled) setAt(i, (r) => ({ ...r, syncError: e }));
423
- } finally {
424
- if (!cancelled) setAt(i, (r) => ({ ...r, syncing: false }));
425
- }
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 () => {
426
641
  };
427
- 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
+ );
428
670
  });
429
- setResults(
430
- qs.map((_q, i) => ({
431
- data: [],
432
- loading: true,
433
- 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,
434
716
  syncing: false,
435
717
  syncError: null,
436
- refetch: resolved[i].refetch
437
- }))
438
- );
439
- const unsubs = qs.map((q, i) => {
440
- const col = db.collection(q.collection);
441
- return col.subscribe(
442
- q.filter ?? {},
443
- (docs) => {
444
- if (!cancelled) setAt(i, (r) => ({ ...r, data: docs, loading: false, error: null }));
445
- },
446
- (error) => {
447
- if (!cancelled) setAt(i, (r) => ({ ...r, loading: false, error }));
718
+ refetch: async () => {
719
+ await replication?.coordinators.get(q.collection)?.refresh();
448
720
  }
449
- );
450
- });
451
- const intervals = [];
452
- resolved.forEach((res) => {
453
- if (!res.networked || !res.config) return;
454
- void res.refetch();
455
- if (res.pollMs > 0) intervals.push(setInterval(() => void res.refetch(), res.pollMs));
456
- });
457
- return () => {
458
- cancelled = true;
459
- unsubs.forEach((u) => u());
460
- intervals.forEach((id) => clearInterval(id));
461
- };
462
- }, [db, sig]);
463
- return queries.map((_q, i) => results[i] ?? emptyResult());
721
+ };
722
+ }),
723
+ // eslint-disable-next-line react-hooks/exhaustive-deps
724
+ [results, signature, replication]
725
+ );
464
726
  }
465
727
 
466
728
  // src/useMutation.ts
467
- var import_react8 = require("react");
729
+ var import_react10 = require("react");
468
730
  function useMutation(options) {
469
731
  const { collection, direction = "push", drainOnMount = true } = options;
470
732
  const db = useTalaDB();
@@ -475,12 +737,12 @@ function useMutation(options) {
475
737
  fetch: options.fetch,
476
738
  paths: options.paths
477
739
  });
478
- const configRef = (0, import_react8.useRef)(config);
740
+ const configRef = (0, import_react10.useRef)(config);
479
741
  configRef.current = config;
480
- const [pending, setPending] = (0, import_react8.useState)(false);
481
- 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);
482
744
  const endpoint = config?.endpoint;
483
- const applyLocal = (0, import_react8.useCallback)(
745
+ const applyLocal = (0, import_react10.useCallback)(
484
746
  async (op) => {
485
747
  switch (op.type) {
486
748
  case "insert":
@@ -496,12 +758,12 @@ function useMutation(options) {
496
758
  },
497
759
  [col]
498
760
  );
499
- const drain = (0, import_react8.useCallback)(async () => {
761
+ const drain = (0, import_react10.useCallback)(async () => {
500
762
  const cfg = configRef.current;
501
763
  if (!cfg) return;
502
764
  await replicateWithRetry(db, cfg, collection, direction);
503
765
  }, [db, collection, direction, endpoint]);
504
- const mutateAsync = (0, import_react8.useCallback)(
766
+ const mutateAsync = (0, import_react10.useCallback)(
505
767
  async (op) => {
506
768
  setPending(true);
507
769
  setError(null);
@@ -517,14 +779,14 @@ function useMutation(options) {
517
779
  },
518
780
  [applyLocal, drain]
519
781
  );
520
- const mutate = (0, import_react8.useCallback)(
782
+ const mutate = (0, import_react10.useCallback)(
521
783
  (op) => {
522
784
  void mutateAsync(op).catch(() => {
523
785
  });
524
786
  },
525
787
  [mutateAsync]
526
788
  );
527
- (0, import_react8.useEffect)(() => {
789
+ (0, import_react10.useEffect)(() => {
528
790
  if (!drainOnMount || !configRef.current) return;
529
791
  void drain().catch(() => {
530
792
  });
@@ -540,9 +802,13 @@ function useMutation(options) {
540
802
  0 && (module.exports = {
541
803
  ReplicationProvider,
542
804
  TalaDBProvider,
805
+ useAggregate,
543
806
  useCollection,
807
+ useCollectionOptions,
808
+ useCoverage,
544
809
  useFind,
545
810
  useFindOne,
811
+ useHydrationProgress,
546
812
  useMutation,
547
813
  useQueries,
548
814
  useQuery,