@taladb/react 0.10.2 → 0.11.1

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
@@ -1,127 +1,18 @@
1
1
  'use client';
2
-
3
- // src/context.tsx
4
2
  import {
5
- createContext,
6
- useContext,
7
- useEffect,
8
- useMemo,
9
- useRef,
10
- useState
11
- } from "react";
12
- import { Fragment, jsx } from "react/jsx-runtime";
13
- var TalaDBContext = createContext(null);
14
- var CollectionOptionsContext = createContext({
15
- get: () => void 0
16
- });
17
- function useCollectionOptions() {
18
- return useContext(CollectionOptionsContext);
19
- }
20
- function CollectionOptionsProvider({
21
- collections,
22
- children
23
- }) {
24
- const latest = useRef(collections);
25
- latest.current = collections;
26
- const resolver = useMemo(
27
- () => ({
28
- get: (name) => latest.current?.[name]
29
- }),
30
- []
31
- );
32
- return /* @__PURE__ */ jsx(CollectionOptionsContext.Provider, { value: resolver, children });
33
- }
34
- function TalaDBProvider(props) {
35
- if ("db" in props && props.db) {
36
- return /* @__PURE__ */ jsx(TalaDBContext.Provider, { value: props.db, children: /* @__PURE__ */ jsx(CollectionOptionsProvider, { collections: props.collections, children: props.children }) });
37
- }
38
- return /* @__PURE__ */ jsx(NamedProvider, { ...props });
39
- }
40
- function NamedProvider({
41
- name,
42
- options,
43
- fallback = null,
44
- collections,
45
- children
46
- }) {
47
- const [db, setDb] = useState(null);
48
- const [error, setError] = useState(null);
49
- const optionsKey = JSON.stringify(options ?? null);
50
- useEffect(() => {
51
- setError(null);
52
- let cancelled = false;
53
- let opened = null;
54
- import("taladb").then(({ openDB }) => openDB(name, options)).then((instance) => {
55
- if (cancelled) {
56
- void instance.close();
57
- return;
58
- }
59
- opened = instance;
60
- setDb(instance);
61
- }).catch((e) => {
62
- if (!cancelled) setError(e);
63
- });
64
- return () => {
65
- cancelled = true;
66
- if (opened) void opened.close();
67
- setDb(null);
68
- };
69
- }, [name, optionsKey]);
70
- if (error !== null) throw error;
71
- if (db === null) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
72
- return /* @__PURE__ */ jsx(TalaDBContext.Provider, { value: db, children: /* @__PURE__ */ jsx(CollectionOptionsProvider, { collections, children }) });
73
- }
74
- function useTalaDB() {
75
- const db = useContext(TalaDBContext);
76
- if (db === null) {
77
- throw new Error('useTalaDB must be used inside <TalaDBProvider db={...}> or <TalaDBProvider name="...">');
78
- }
79
- return db;
80
- }
81
-
82
- // src/useCollection.ts
83
- import { useMemo as useMemo2, useRef as useRef2 } from "react";
84
- function useCollection(name, options) {
85
- const db = useTalaDB();
86
- const registry = useCollectionOptions();
87
- const explicit = useRef2(options);
88
- explicit.current = options;
89
- return useMemo2(
90
- () => db.collection(name, explicit.current ?? registry.get(name)),
91
- [db, name, registry]
92
- );
93
- }
94
-
95
- // src/useFind.ts
96
- import { useCallback, useRef as useRef3, useSyncExternalStore } from "react";
97
- function useFind(collection, filter) {
98
- const snapshotRef = useRef3({ data: [], loading: true, error: null });
99
- const filterKey = JSON.stringify(filter ?? null);
100
- const subscribe = useCallback(
101
- (notify) => {
102
- snapshotRef.current = { data: snapshotRef.current.data, loading: true, error: null };
103
- return collection.subscribe(filter ?? {}, (docs) => {
104
- snapshotRef.current = { data: docs, loading: false, error: null };
105
- notify();
106
- }, (error) => {
107
- snapshotRef.current = { ...snapshotRef.current, loading: false, error };
108
- notify();
109
- });
110
- },
111
- // filterKey captures the serialised filter; collection is the identity dep.
112
- // eslint-disable-next-line react-hooks/exhaustive-deps
113
- [collection, filterKey]
114
- );
115
- const getSnapshot = useCallback(() => snapshotRef.current, []);
116
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
117
- }
3
+ TalaDBProvider,
4
+ useCollection,
5
+ useCollectionOptions,
6
+ useFind,
7
+ useTalaDB
8
+ } from "./chunk-RAGFVMZN.mjs";
118
9
 
119
10
  // src/useFindOne.ts
120
- import { useCallback as useCallback2, useRef as useRef4, useSyncExternalStore as useSyncExternalStore2 } from "react";
11
+ import { useCallback, useRef, useSyncExternalStore } from "react";
121
12
  function useFindOne(collection, filter) {
122
- const snapshotRef = useRef4({ data: null, loading: true, error: null });
13
+ const snapshotRef = useRef({ data: null, loading: true, error: null });
123
14
  const filterKey = JSON.stringify(filter);
124
- const subscribe = useCallback2(
15
+ const subscribe = useCallback(
125
16
  (notify) => {
126
17
  snapshotRef.current = { data: snapshotRef.current.data, loading: true, error: null };
127
18
  return collection.subscribe(filter, (docs) => {
@@ -135,16 +26,16 @@ function useFindOne(collection, filter) {
135
26
  // eslint-disable-next-line react-hooks/exhaustive-deps
136
27
  [collection, filterKey]
137
28
  );
138
- const getSnapshot = useCallback2(() => snapshotRef.current, []);
139
- return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
29
+ const getSnapshot = useCallback(() => snapshotRef.current, []);
30
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
140
31
  }
141
32
 
142
33
  // src/useAggregate.ts
143
- import { useCallback as useCallback3, useRef as useRef5, useSyncExternalStore as useSyncExternalStore3 } from "react";
34
+ import { useCallback as useCallback2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
144
35
  function useAggregate(collection, pipeline) {
145
- const snapshotRef = useRef5({ data: [], loading: true, error: null });
36
+ const snapshotRef = useRef2({ data: [], loading: true, error: null });
146
37
  const pipelineKey = JSON.stringify(pipeline);
147
- const subscribe = useCallback3(
38
+ const subscribe = useCallback2(
148
39
  (notify) => {
149
40
  snapshotRef.current = { data: snapshotRef.current.data, loading: true, error: null };
150
41
  return collection.subscribeAggregate(
@@ -162,562 +53,18 @@ function useAggregate(collection, pipeline) {
162
53
  // eslint-disable-next-line react-hooks/exhaustive-deps
163
54
  [collection, pipelineKey]
164
55
  );
165
- const getSnapshot = useCallback3(() => snapshotRef.current, []);
166
- return useSyncExternalStore3(subscribe, getSnapshot, getSnapshot);
167
- }
168
-
169
- // src/replication/config.tsx
170
- import {
171
- createContext as createContext3,
172
- useContext as useContext3,
173
- useEffect as useEffect3,
174
- useMemo as useMemo4,
175
- useRef as useRef7
176
- } from "react";
177
-
178
- // src/replication/engine.ts
179
- import { HttpSyncAdapter } from "taladb";
180
- function replicationTarget(endpoint, collection) {
181
- return `${endpoint}::${collection}`;
182
- }
183
- async function buildAdapter(config) {
184
- const headers = config.getAuth ? await config.getAuth() : void 0;
185
- return new HttpSyncAdapter({
186
- endpoint: config.endpoint,
187
- headers,
188
- fetch: config.fetch,
189
- paths: config.paths
190
- });
191
- }
192
- var inflight = /* @__PURE__ */ new Map();
193
- function inflightKey(endpoint, collection, direction) {
194
- return `${endpoint}::${collection}::${direction}`;
195
- }
196
- function replicate(db, config, collection, direction) {
197
- const key = inflightKey(config.endpoint, collection, direction);
198
- const existing = inflight.get(key);
199
- if (existing) return existing;
200
- const pass = (async () => {
201
- const adapter = await buildAdapter(config);
202
- await db.sync(adapter, {
203
- collections: [collection],
204
- direction,
205
- target: replicationTarget(config.endpoint, collection)
206
- });
207
- })().finally(() => {
208
- inflight.delete(key);
209
- });
210
- inflight.set(key, pass);
211
- return pass;
212
- }
213
- var BACKOFFS_MS = [200, 400, 800];
214
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
215
- async function replicateWithRetry(db, config, collection, direction) {
216
- let lastError;
217
- for (let attempt = 0; attempt <= BACKOFFS_MS.length; attempt++) {
218
- try {
219
- await replicate(db, config, collection, direction);
220
- return;
221
- } catch (error) {
222
- lastError = error;
223
- if (attempt < BACKOFFS_MS.length) await sleep(BACKOFFS_MS[attempt]);
224
- }
225
- }
226
- throw lastError;
227
- }
228
-
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";
243
- var ReplicationContext = createContext2(null);
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]);
344
- const value = useMemo3(
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,
365
- // eslint-disable-next-line react-hooks/exhaustive-deps
366
- [key]
367
- );
368
- const inner = /* @__PURE__ */ jsxs(ReplicationContext2.Provider, { value, children: [
369
- value?.prefetch && value.prefetch.length > 0 ? /* @__PURE__ */ jsx3(PrefetchRunner, {}) : null,
370
- children
371
- ] });
372
- return replicate2 ? /* @__PURE__ */ jsx3(ReplicationScopes, { replicate: replicate2, children: inner }) : inner;
373
- }
374
- function resolveReplicationConfig(base, overrides) {
375
- const endpoint = overrides?.endpoint ?? base?.endpoint;
376
- const pollMs = overrides?.pollMs ?? base?.pollMs ?? 0;
377
- if (!endpoint) return { config: null, pollMs };
378
- return {
379
- config: {
380
- endpoint,
381
- getAuth: overrides?.getAuth ?? base?.getAuth,
382
- fetch: overrides?.fetch ?? base?.fetch,
383
- paths: overrides?.paths ?? base?.paths
384
- },
385
- pollMs
386
- };
387
- }
388
- function useReplicationBase() {
389
- return useContext3(ReplicationContext2);
390
- }
391
- function useReplicationConfig(overrides) {
392
- return resolveReplicationConfig(useContext3(ReplicationContext2), overrides);
393
- }
394
- var CURSOR_COLLECTION = "__taladb_sync";
395
- function normalizePrefetch(entries) {
396
- return (entries ?? []).map((e) => typeof e === "string" ? { collection: e } : e);
397
- }
398
- var idleScheduler = (fn) => {
399
- const g = globalThis;
400
- if (typeof g.requestIdleCallback === "function") {
401
- const id2 = g.requestIdleCallback(fn, { timeout: 2e3 });
402
- return () => g.cancelIdleCallback?.(id2);
403
- }
404
- const id = setTimeout(fn, 0);
405
- return () => clearTimeout(id);
406
- };
407
- var schedule = idleScheduler;
408
- async function hasSynced(db, target) {
409
- try {
410
- const doc = await db.collection(CURSOR_COLLECTION).findOne({ target });
411
- return doc != null;
412
- } catch {
413
- return false;
414
- }
415
- }
416
- function PrefetchRunner() {
417
- const db = useTalaDB();
418
- const base = useReplicationBase();
419
- const slices = normalizePrefetch(base?.prefetch);
420
- const mode = base?.prefetchMode ?? "once";
421
- const concurrency = Math.max(1, base?.prefetchConcurrency ?? 2);
422
- const baseRef = useRef7(base);
423
- baseRef.current = base;
424
- const sig = JSON.stringify({ slices, mode, concurrency, endpoint: base?.endpoint ?? null });
425
- useEffect3(() => {
426
- if (slices.length === 0) return void 0;
427
- let cancelled = false;
428
- const cancelSchedule = schedule(() => {
429
- void run();
430
- });
431
- async function run() {
432
- const b = baseRef.current;
433
- const queue = normalizePrefetch(b?.prefetch);
434
- const worker = async () => {
435
- while (!cancelled) {
436
- const slice = queue.shift();
437
- if (!slice) return;
438
- const { config } = resolveReplicationConfig(b, { endpoint: slice.endpoint });
439
- if (!config) continue;
440
- const target = replicationTarget(config.endpoint, slice.collection);
441
- if (mode === "once" && await hasSynced(db, target)) continue;
442
- if (cancelled) return;
443
- try {
444
- await replicate(db, config, slice.collection, "pull");
445
- } catch {
446
- }
447
- }
448
- };
449
- const lanes = Math.min(concurrency, queue.length);
450
- await Promise.all(Array.from({ length: lanes }, () => worker()));
451
- }
452
- return () => {
453
- cancelled = true;
454
- cancelSchedule();
455
- };
456
- }, [db, sig]);
457
- return null;
458
- }
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
-
476
- // src/useQuery.ts
477
- import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo5, useRef as useRef8, useState as useState3 } from "react";
478
- function useQuery(options) {
479
- const { collection, filter, sort, page, limit, skip, enabled = true } = options;
480
- const col = useCollection(collection);
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;
495
- setSyncing(true);
496
- setSyncError(null);
497
- try {
498
- await replicate(db, cfg, collection, "pull");
499
- } catch (error) {
500
- setSyncError(error);
501
- } finally {
502
- setSyncing(false);
503
- setFirstSyncDone(true);
504
- }
505
- }, [db, collection, legacyNetworked, legacyConfig?.endpoint]);
506
- useEffect4(() => {
507
- if (!enabled || !legacyNetworked || !legacyConfig) return;
508
- void legacyRefetch();
509
- if (pollMs > 0) {
510
- const timer = setInterval(() => void legacyRefetch(), pollMs);
511
- return () => clearInterval(timer);
512
- }
513
- return void 0;
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) {
564
- throw new Error(
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.`
566
- );
567
- }
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
- };
580
- }
581
-
582
- // src/useQueries.ts
583
- import { useEffect as useEffect5, useMemo as useMemo6, useRef as useRef9, useState as useState4 } from "react";
584
- function useQueries(queries) {
585
- const db = useTalaDB();
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(
592
- queries.map((q) => ({
593
- collection: q.collection,
594
- filter: q.filter ?? 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
600
- }))
601
- );
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
- }))
610
- );
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 () => {
616
- };
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
- );
645
- });
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,
691
- syncing: false,
692
- syncError: null,
693
- refetch: async () => {
694
- await replication?.coordinators.get(q.collection)?.refresh();
695
- }
696
- };
697
- }),
698
- // eslint-disable-next-line react-hooks/exhaustive-deps
699
- [results, signature, replication]
700
- );
56
+ const getSnapshot = useCallback2(() => snapshotRef.current, []);
57
+ return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
701
58
  }
702
59
 
703
- // src/useMutation.ts
704
- import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef10, useState as useState5 } from "react";
705
- function useMutation(options) {
706
- const { collection, direction = "push", drainOnMount = true } = options;
707
- const db = useTalaDB();
60
+ // src/useWrite.ts
61
+ import { useCallback as useCallback3, useState } from "react";
62
+ function useWrite(options) {
63
+ const { collection } = options;
708
64
  const col = useCollection(collection);
709
- const { config } = useReplicationConfig({
710
- endpoint: options.endpoint,
711
- getAuth: options.getAuth,
712
- fetch: options.fetch,
713
- paths: options.paths
714
- });
715
- const configRef = useRef10(config);
716
- configRef.current = config;
717
- const [pending, setPending] = useState5(false);
718
- const [error, setError] = useState5(null);
719
- const endpoint = config?.endpoint;
720
- const applyLocal = useCallback5(
65
+ const [pending, setPending] = useState(false);
66
+ const [error, setError] = useState(null);
67
+ const apply = useCallback3(
721
68
  async (op) => {
722
69
  switch (op.type) {
723
70
  case "insert":
@@ -733,18 +80,12 @@ function useMutation(options) {
733
80
  },
734
81
  [col]
735
82
  );
736
- const drain = useCallback5(async () => {
737
- const cfg = configRef.current;
738
- if (!cfg) return;
739
- await replicateWithRetry(db, cfg, collection, direction);
740
- }, [db, collection, direction, endpoint]);
741
- const mutateAsync = useCallback5(
83
+ const writeAsync = useCallback3(
742
84
  async (op) => {
743
85
  setPending(true);
744
86
  setError(null);
745
87
  try {
746
- await applyLocal(op);
747
- await drain();
88
+ await apply(op);
748
89
  } catch (e) {
749
90
  setError(e);
750
91
  throw e;
@@ -752,40 +93,24 @@ function useMutation(options) {
752
93
  setPending(false);
753
94
  }
754
95
  },
755
- [applyLocal, drain]
96
+ [apply]
756
97
  );
757
- const mutate = useCallback5(
98
+ const write = useCallback3(
758
99
  (op) => {
759
- void mutateAsync(op).catch(() => {
100
+ void writeAsync(op).catch(() => {
760
101
  });
761
102
  },
762
- [mutateAsync]
103
+ [writeAsync]
763
104
  );
764
- useEffect6(() => {
765
- if (!drainOnMount || !configRef.current) return;
766
- void drain().catch(() => {
767
- });
768
- }, [drain, drainOnMount]);
769
- if (!config) {
770
- throw new Error(
771
- `useMutation({ collection: '${collection}' }) needs an endpoint. Wrap the tree in <ReplicationProvider endpoint="\u2026"> or pass { endpoint }.`
772
- );
773
- }
774
- return { mutate, mutateAsync, pending, error };
105
+ return { write, writeAsync, pending, error };
775
106
  }
776
107
  export {
777
- ReplicationProvider,
778
108
  TalaDBProvider,
779
109
  useAggregate,
780
110
  useCollection,
781
111
  useCollectionOptions,
782
- useCoverage,
783
112
  useFind,
784
113
  useFindOne,
785
- useHydrationProgress,
786
- useMutation,
787
- useQueries,
788
- useQuery,
789
- useReplicationConfig,
790
- useTalaDB
114
+ useTalaDB,
115
+ useWrite
791
116
  };