@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.js CHANGED
@@ -31,20 +31,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
- ReplicationProvider: () => ReplicationProvider,
35
34
  TalaDBProvider: () => TalaDBProvider,
36
35
  useAggregate: () => useAggregate,
37
36
  useCollection: () => useCollection,
38
37
  useCollectionOptions: () => useCollectionOptions,
39
- useCoverage: () => useCoverage,
40
38
  useFind: () => useFind,
41
39
  useFindOne: () => useFindOne,
42
- useHydrationProgress: () => useHydrationProgress,
43
- useMutation: () => useMutation,
44
- useQueries: () => useQueries,
45
- useQuery: () => useQuery,
46
- useReplicationConfig: () => useReplicationConfig,
47
- useTalaDB: () => useTalaDB
40
+ useTalaDB: () => useTalaDB,
41
+ useWrite: () => useWrite
48
42
  });
49
43
  module.exports = __toCommonJS(index_exports);
50
44
 
@@ -53,7 +47,8 @@ var import_react = require("react");
53
47
  var import_jsx_runtime = require("react/jsx-runtime");
54
48
  var TalaDBContext = (0, import_react.createContext)(null);
55
49
  var CollectionOptionsContext = (0, import_react.createContext)({
56
- get: () => void 0
50
+ get: () => void 0,
51
+ names: () => []
57
52
  });
58
53
  function useCollectionOptions() {
59
54
  return (0, import_react.useContext)(CollectionOptionsContext);
@@ -66,7 +61,10 @@ function CollectionOptionsProvider({
66
61
  latest.current = collections;
67
62
  const resolver = (0, import_react.useMemo)(
68
63
  () => ({
69
- get: (name) => latest.current?.[name]
64
+ get: (name) => latest.current?.[name],
65
+ // Read through the same ref as `get`, so a registry passed as an inline
66
+ // object stays current without giving the resolver a new identity.
67
+ names: () => Object.keys(latest.current ?? {})
70
68
  }),
71
69
  []
72
70
  );
@@ -78,6 +76,47 @@ function TalaDBProvider(props) {
78
76
  }
79
77
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NamedProvider, { ...props });
80
78
  }
79
+ var openDatabases = /* @__PURE__ */ new Map();
80
+ function databaseKey(name, optionsKey) {
81
+ return `${name}\0${optionsKey}`;
82
+ }
83
+ function acquireDatabase(name, optionsKey, options) {
84
+ const key = databaseKey(name, optionsKey);
85
+ let entry = openDatabases.get(key);
86
+ if (entry) {
87
+ if (entry.closing !== void 0) {
88
+ clearTimeout(entry.closing);
89
+ entry.closing = void 0;
90
+ }
91
+ } else {
92
+ entry = {
93
+ // Dynamic import so `taladb`'s runtime never loads during SSR module
94
+ // evaluation (its Node entry pulls in the native binding, which a web
95
+ // app's server bundle does not ship).
96
+ promise: import("taladb").then(({ openDB }) => openDB(name, options)),
97
+ refs: 0
98
+ };
99
+ openDatabases.set(key, entry);
100
+ }
101
+ entry.refs++;
102
+ return entry.promise;
103
+ }
104
+ function releaseDatabase(name, optionsKey) {
105
+ const key = databaseKey(name, optionsKey);
106
+ const entry = openDatabases.get(key);
107
+ if (!entry) return;
108
+ entry.refs--;
109
+ if (entry.refs > 0 || entry.closing !== void 0) return;
110
+ entry.closing = setTimeout(() => {
111
+ if (entry.refs > 0) {
112
+ entry.closing = void 0;
113
+ return;
114
+ }
115
+ openDatabases.delete(key);
116
+ void entry.promise.then((db) => db.close()).catch(() => {
117
+ });
118
+ }, 0);
119
+ }
81
120
  function NamedProvider({
82
121
  name,
83
122
  options,
@@ -91,20 +130,14 @@ function NamedProvider({
91
130
  (0, import_react.useEffect)(() => {
92
131
  setError(null);
93
132
  let cancelled = false;
94
- let opened = null;
95
- import("taladb").then(({ openDB }) => openDB(name, options)).then((instance) => {
96
- if (cancelled) {
97
- void instance.close();
98
- return;
99
- }
100
- opened = instance;
101
- setDb(instance);
133
+ acquireDatabase(name, optionsKey, options).then((instance) => {
134
+ if (!cancelled) setDb(instance);
102
135
  }).catch((e) => {
103
136
  if (!cancelled) setError(e);
104
137
  });
105
138
  return () => {
106
139
  cancelled = true;
107
- if (opened) void opened.close();
140
+ releaseDatabase(name, optionsKey);
108
141
  setDb(null);
109
142
  };
110
143
  }, [name, optionsKey]);
@@ -207,542 +240,14 @@ function useAggregate(collection, pipeline) {
207
240
  return (0, import_react5.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
208
241
  }
209
242
 
210
- // src/replication/config.tsx
211
- var import_react7 = require("react");
212
-
213
- // src/replication/engine.ts
214
- var import_taladb = require("taladb");
215
- function replicationTarget(endpoint, collection) {
216
- return `${endpoint}::${collection}`;
217
- }
218
- async function buildAdapter(config) {
219
- const headers = config.getAuth ? await config.getAuth() : void 0;
220
- return new import_taladb.HttpSyncAdapter({
221
- endpoint: config.endpoint,
222
- headers,
223
- fetch: config.fetch,
224
- paths: config.paths
225
- });
226
- }
227
- var inflight = /* @__PURE__ */ new Map();
228
- function inflightKey(endpoint, collection, direction) {
229
- return `${endpoint}::${collection}::${direction}`;
230
- }
231
- function replicate(db, config, collection, direction) {
232
- const key = inflightKey(config.endpoint, collection, direction);
233
- const existing = inflight.get(key);
234
- if (existing) return existing;
235
- const pass = (async () => {
236
- const adapter = await buildAdapter(config);
237
- await db.sync(adapter, {
238
- collections: [collection],
239
- direction,
240
- target: replicationTarget(config.endpoint, collection)
241
- });
242
- })().finally(() => {
243
- inflight.delete(key);
244
- });
245
- inflight.set(key, pass);
246
- return pass;
247
- }
248
- var BACKOFFS_MS = [200, 400, 800];
249
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
250
- async function replicateWithRetry(db, config, collection, direction) {
251
- let lastError;
252
- for (let attempt = 0; attempt <= BACKOFFS_MS.length; attempt++) {
253
- try {
254
- await replicate(db, config, collection, direction);
255
- return;
256
- } catch (error) {
257
- lastError = error;
258
- if (attempt < BACKOFFS_MS.length) await sleep(BACKOFFS_MS[attempt]);
259
- }
260
- }
261
- throw lastError;
262
- }
263
-
264
- // src/replication/provider.tsx
243
+ // src/useWrite.ts
265
244
  var import_react6 = require("react");
266
- var import_taladb2 = require("taladb");
267
- var import_jsx_runtime2 = require("react/jsx-runtime");
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,
390
- // eslint-disable-next-line react-hooks/exhaustive-deps
391
- [key]
392
- );
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,
395
- children
396
- ] });
397
- return replicate2 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReplicationScopes, { replicate: replicate2, children: inner }) : inner;
398
- }
399
- function resolveReplicationConfig(base, overrides) {
400
- const endpoint = overrides?.endpoint ?? base?.endpoint;
401
- const pollMs = overrides?.pollMs ?? base?.pollMs ?? 0;
402
- if (!endpoint) return { config: null, pollMs };
403
- return {
404
- config: {
405
- endpoint,
406
- getAuth: overrides?.getAuth ?? base?.getAuth,
407
- fetch: overrides?.fetch ?? base?.fetch,
408
- paths: overrides?.paths ?? base?.paths
409
- },
410
- pollMs
411
- };
412
- }
413
- function useReplicationBase() {
414
- return (0, import_react7.useContext)(ReplicationContext2);
415
- }
416
- function useReplicationConfig(overrides) {
417
- return resolveReplicationConfig((0, import_react7.useContext)(ReplicationContext2), overrides);
418
- }
419
- var CURSOR_COLLECTION = "__taladb_sync";
420
- function normalizePrefetch(entries) {
421
- return (entries ?? []).map((e) => typeof e === "string" ? { collection: e } : e);
422
- }
423
- var idleScheduler = (fn) => {
424
- const g = globalThis;
425
- if (typeof g.requestIdleCallback === "function") {
426
- const id2 = g.requestIdleCallback(fn, { timeout: 2e3 });
427
- return () => g.cancelIdleCallback?.(id2);
428
- }
429
- const id = setTimeout(fn, 0);
430
- return () => clearTimeout(id);
431
- };
432
- var schedule = idleScheduler;
433
- async function hasSynced(db, target) {
434
- try {
435
- const doc = await db.collection(CURSOR_COLLECTION).findOne({ target });
436
- return doc != null;
437
- } catch {
438
- return false;
439
- }
440
- }
441
- function PrefetchRunner() {
442
- const db = useTalaDB();
443
- const base = useReplicationBase();
444
- const slices = normalizePrefetch(base?.prefetch);
445
- const mode = base?.prefetchMode ?? "once";
446
- const concurrency = Math.max(1, base?.prefetchConcurrency ?? 2);
447
- const baseRef = (0, import_react7.useRef)(base);
448
- baseRef.current = base;
449
- const sig = JSON.stringify({ slices, mode, concurrency, endpoint: base?.endpoint ?? null });
450
- (0, import_react7.useEffect)(() => {
451
- if (slices.length === 0) return void 0;
452
- let cancelled = false;
453
- const cancelSchedule = schedule(() => {
454
- void run();
455
- });
456
- async function run() {
457
- const b = baseRef.current;
458
- const queue = normalizePrefetch(b?.prefetch);
459
- const worker = async () => {
460
- while (!cancelled) {
461
- const slice = queue.shift();
462
- if (!slice) return;
463
- const { config } = resolveReplicationConfig(b, { endpoint: slice.endpoint });
464
- if (!config) continue;
465
- const target = replicationTarget(config.endpoint, slice.collection);
466
- if (mode === "once" && await hasSynced(db, target)) continue;
467
- if (cancelled) return;
468
- try {
469
- await replicate(db, config, slice.collection, "pull");
470
- } catch {
471
- }
472
- }
473
- };
474
- const lanes = Math.min(concurrency, queue.length);
475
- await Promise.all(Array.from({ length: lanes }, () => worker()));
476
- }
477
- return () => {
478
- cancelled = true;
479
- cancelSchedule();
480
- };
481
- }, [db, sig]);
482
- return null;
483
- }
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
-
501
- // src/useQuery.ts
502
- var import_react8 = require("react");
503
- function useQuery(options) {
504
- const { collection, filter, sort, page, limit, skip, enabled = true } = options;
245
+ function useWrite(options) {
246
+ const { collection } = options;
505
247
  const col = useCollection(collection);
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;
520
- setSyncing(true);
521
- setSyncError(null);
522
- try {
523
- await replicate(db, cfg, collection, "pull");
524
- } catch (error) {
525
- setSyncError(error);
526
- } finally {
527
- setSyncing(false);
528
- setFirstSyncDone(true);
529
- }
530
- }, [db, collection, legacyNetworked, legacyConfig?.endpoint]);
531
- (0, import_react8.useEffect)(() => {
532
- if (!enabled || !legacyNetworked || !legacyConfig) return;
533
- void legacyRefetch();
534
- if (pollMs > 0) {
535
- const timer = setInterval(() => void legacyRefetch(), pollMs);
536
- return () => clearInterval(timer);
537
- }
538
- return void 0;
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) {
589
- throw new Error(
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.`
591
- );
592
- }
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
- };
605
- }
606
-
607
- // src/useQueries.ts
608
- var import_react9 = require("react");
609
- function useQueries(queries) {
610
- const db = useTalaDB();
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(
617
- queries.map((q) => ({
618
- collection: q.collection,
619
- filter: q.filter ?? 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
625
- }))
626
- );
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
- }))
635
- );
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 () => {
641
- };
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
- );
670
- });
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,
716
- syncing: false,
717
- syncError: null,
718
- refetch: async () => {
719
- await replication?.coordinators.get(q.collection)?.refresh();
720
- }
721
- };
722
- }),
723
- // eslint-disable-next-line react-hooks/exhaustive-deps
724
- [results, signature, replication]
725
- );
726
- }
727
-
728
- // src/useMutation.ts
729
- var import_react10 = require("react");
730
- function useMutation(options) {
731
- const { collection, direction = "push", drainOnMount = true } = options;
732
- const db = useTalaDB();
733
- const col = useCollection(collection);
734
- const { config } = useReplicationConfig({
735
- endpoint: options.endpoint,
736
- getAuth: options.getAuth,
737
- fetch: options.fetch,
738
- paths: options.paths
739
- });
740
- const configRef = (0, import_react10.useRef)(config);
741
- configRef.current = config;
742
- const [pending, setPending] = (0, import_react10.useState)(false);
743
- const [error, setError] = (0, import_react10.useState)(null);
744
- const endpoint = config?.endpoint;
745
- const applyLocal = (0, import_react10.useCallback)(
248
+ const [pending, setPending] = (0, import_react6.useState)(false);
249
+ const [error, setError] = (0, import_react6.useState)(null);
250
+ const apply = (0, import_react6.useCallback)(
746
251
  async (op) => {
747
252
  switch (op.type) {
748
253
  case "insert":
@@ -758,18 +263,12 @@ function useMutation(options) {
758
263
  },
759
264
  [col]
760
265
  );
761
- const drain = (0, import_react10.useCallback)(async () => {
762
- const cfg = configRef.current;
763
- if (!cfg) return;
764
- await replicateWithRetry(db, cfg, collection, direction);
765
- }, [db, collection, direction, endpoint]);
766
- const mutateAsync = (0, import_react10.useCallback)(
266
+ const writeAsync = (0, import_react6.useCallback)(
767
267
  async (op) => {
768
268
  setPending(true);
769
269
  setError(null);
770
270
  try {
771
- await applyLocal(op);
772
- await drain();
271
+ await apply(op);
773
272
  } catch (e) {
774
273
  setError(e);
775
274
  throw e;
@@ -777,41 +276,25 @@ function useMutation(options) {
777
276
  setPending(false);
778
277
  }
779
278
  },
780
- [applyLocal, drain]
279
+ [apply]
781
280
  );
782
- const mutate = (0, import_react10.useCallback)(
281
+ const write = (0, import_react6.useCallback)(
783
282
  (op) => {
784
- void mutateAsync(op).catch(() => {
283
+ void writeAsync(op).catch(() => {
785
284
  });
786
285
  },
787
- [mutateAsync]
286
+ [writeAsync]
788
287
  );
789
- (0, import_react10.useEffect)(() => {
790
- if (!drainOnMount || !configRef.current) return;
791
- void drain().catch(() => {
792
- });
793
- }, [drain, drainOnMount]);
794
- if (!config) {
795
- throw new Error(
796
- `useMutation({ collection: '${collection}' }) needs an endpoint. Wrap the tree in <ReplicationProvider endpoint="\u2026"> or pass { endpoint }.`
797
- );
798
- }
799
- return { mutate, mutateAsync, pending, error };
288
+ return { write, writeAsync, pending, error };
800
289
  }
801
290
  // Annotate the CommonJS export names for ESM import in node:
802
291
  0 && (module.exports = {
803
- ReplicationProvider,
804
292
  TalaDBProvider,
805
293
  useAggregate,
806
294
  useCollection,
807
295
  useCollectionOptions,
808
- useCoverage,
809
296
  useFind,
810
297
  useFindOne,
811
- useHydrationProgress,
812
- useMutation,
813
- useQueries,
814
- useQuery,
815
- useReplicationConfig,
816
- useTalaDB
298
+ useTalaDB,
299
+ useWrite
817
300
  });