@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.d.mts +227 -48
- package/dist/index.d.ts +227 -48
- package/dist/index.js +416 -150
- package/dist/index.mjs +439 -160
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
// src/context.tsx
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState
|
|
11
|
+
} from "react";
|
|
5
12
|
import { Fragment, jsx } from "react/jsx-runtime";
|
|
6
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
|
+
}
|
|
7
34
|
function TalaDBProvider(props) {
|
|
8
35
|
if ("db" in props && props.db) {
|
|
9
|
-
return /* @__PURE__ */ jsx(TalaDBContext.Provider, { value: props.db, children: props.children });
|
|
36
|
+
return /* @__PURE__ */ jsx(TalaDBContext.Provider, { value: props.db, children: /* @__PURE__ */ jsx(CollectionOptionsProvider, { collections: props.collections, children: props.children }) });
|
|
10
37
|
}
|
|
11
38
|
return /* @__PURE__ */ jsx(NamedProvider, { ...props });
|
|
12
39
|
}
|
|
@@ -14,6 +41,7 @@ function NamedProvider({
|
|
|
14
41
|
name,
|
|
15
42
|
options,
|
|
16
43
|
fallback = null,
|
|
44
|
+
collections,
|
|
17
45
|
children
|
|
18
46
|
}) {
|
|
19
47
|
const [db, setDb] = useState(null);
|
|
@@ -41,7 +69,7 @@ function NamedProvider({
|
|
|
41
69
|
}, [name, optionsKey]);
|
|
42
70
|
if (error !== null) throw error;
|
|
43
71
|
if (db === null) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
44
|
-
return /* @__PURE__ */ jsx(TalaDBContext.Provider, { value: db, children });
|
|
72
|
+
return /* @__PURE__ */ jsx(TalaDBContext.Provider, { value: db, children: /* @__PURE__ */ jsx(CollectionOptionsProvider, { collections, children }) });
|
|
45
73
|
}
|
|
46
74
|
function useTalaDB() {
|
|
47
75
|
const db = useContext(TalaDBContext);
|
|
@@ -52,16 +80,22 @@ function useTalaDB() {
|
|
|
52
80
|
}
|
|
53
81
|
|
|
54
82
|
// src/useCollection.ts
|
|
55
|
-
import { useMemo } from "react";
|
|
56
|
-
function useCollection(name) {
|
|
83
|
+
import { useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
84
|
+
function useCollection(name, options) {
|
|
57
85
|
const db = useTalaDB();
|
|
58
|
-
|
|
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
|
+
);
|
|
59
93
|
}
|
|
60
94
|
|
|
61
95
|
// src/useFind.ts
|
|
62
|
-
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
96
|
+
import { useCallback, useRef as useRef3, useSyncExternalStore } from "react";
|
|
63
97
|
function useFind(collection, filter) {
|
|
64
|
-
const snapshotRef =
|
|
98
|
+
const snapshotRef = useRef3({ data: [], loading: true, error: null });
|
|
65
99
|
const filterKey = JSON.stringify(filter ?? null);
|
|
66
100
|
const subscribe = useCallback(
|
|
67
101
|
(notify) => {
|
|
@@ -83,9 +117,9 @@ function useFind(collection, filter) {
|
|
|
83
117
|
}
|
|
84
118
|
|
|
85
119
|
// src/useFindOne.ts
|
|
86
|
-
import { useCallback as useCallback2, useRef as
|
|
120
|
+
import { useCallback as useCallback2, useRef as useRef4, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
87
121
|
function useFindOne(collection, filter) {
|
|
88
|
-
const snapshotRef =
|
|
122
|
+
const snapshotRef = useRef4({ data: null, loading: true, error: null });
|
|
89
123
|
const filterKey = JSON.stringify(filter);
|
|
90
124
|
const subscribe = useCallback2(
|
|
91
125
|
(notify) => {
|
|
@@ -105,13 +139,40 @@ function useFindOne(collection, filter) {
|
|
|
105
139
|
return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
|
|
106
140
|
}
|
|
107
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
|
+
|
|
108
169
|
// src/replication/config.tsx
|
|
109
170
|
import {
|
|
110
|
-
createContext as
|
|
111
|
-
useContext as
|
|
112
|
-
useEffect as
|
|
113
|
-
useMemo as
|
|
114
|
-
useRef as
|
|
171
|
+
createContext as createContext3,
|
|
172
|
+
useContext as useContext3,
|
|
173
|
+
useEffect as useEffect3,
|
|
174
|
+
useMemo as useMemo4,
|
|
175
|
+
useRef as useRef7
|
|
115
176
|
} from "react";
|
|
116
177
|
|
|
117
178
|
// src/replication/engine.ts
|
|
@@ -165,20 +226,150 @@ async function replicateWithRetry(db, config, collection, direction) {
|
|
|
165
226
|
throw lastError;
|
|
166
227
|
}
|
|
167
228
|
|
|
168
|
-
// src/replication/
|
|
169
|
-
import {
|
|
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";
|
|
170
243
|
var ReplicationContext = createContext2(null);
|
|
171
|
-
function
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
(
|
|
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,
|
|
175
365
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
176
366
|
[key]
|
|
177
367
|
);
|
|
178
|
-
|
|
179
|
-
value
|
|
368
|
+
const inner = /* @__PURE__ */ jsxs(ReplicationContext2.Provider, { value, children: [
|
|
369
|
+
value?.prefetch && value.prefetch.length > 0 ? /* @__PURE__ */ jsx3(PrefetchRunner, {}) : null,
|
|
180
370
|
children
|
|
181
371
|
] });
|
|
372
|
+
return replicate2 ? /* @__PURE__ */ jsx3(ReplicationScopes, { replicate: replicate2, children: inner }) : inner;
|
|
182
373
|
}
|
|
183
374
|
function resolveReplicationConfig(base, overrides) {
|
|
184
375
|
const endpoint = overrides?.endpoint ?? base?.endpoint;
|
|
@@ -195,10 +386,10 @@ function resolveReplicationConfig(base, overrides) {
|
|
|
195
386
|
};
|
|
196
387
|
}
|
|
197
388
|
function useReplicationBase() {
|
|
198
|
-
return
|
|
389
|
+
return useContext3(ReplicationContext2);
|
|
199
390
|
}
|
|
200
391
|
function useReplicationConfig(overrides) {
|
|
201
|
-
return resolveReplicationConfig(
|
|
392
|
+
return resolveReplicationConfig(useContext3(ReplicationContext2), overrides);
|
|
202
393
|
}
|
|
203
394
|
var CURSOR_COLLECTION = "__taladb_sync";
|
|
204
395
|
function normalizePrefetch(entries) {
|
|
@@ -228,10 +419,10 @@ function PrefetchRunner() {
|
|
|
228
419
|
const slices = normalizePrefetch(base?.prefetch);
|
|
229
420
|
const mode = base?.prefetchMode ?? "once";
|
|
230
421
|
const concurrency = Math.max(1, base?.prefetchConcurrency ?? 2);
|
|
231
|
-
const baseRef =
|
|
422
|
+
const baseRef = useRef7(base);
|
|
232
423
|
baseRef.current = base;
|
|
233
424
|
const sig = JSON.stringify({ slices, mode, concurrency, endpoint: base?.endpoint ?? null });
|
|
234
|
-
|
|
425
|
+
useEffect3(() => {
|
|
235
426
|
if (slices.length === 0) return void 0;
|
|
236
427
|
let cancelled = false;
|
|
237
428
|
const cancelSchedule = schedule(() => {
|
|
@@ -266,167 +457,251 @@ function PrefetchRunner() {
|
|
|
266
457
|
return null;
|
|
267
458
|
}
|
|
268
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
|
+
|
|
269
476
|
// src/useQuery.ts
|
|
270
|
-
import { useCallback as
|
|
477
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo5, useRef as useRef8, useState as useState3 } from "react";
|
|
271
478
|
function useQuery(options) {
|
|
272
|
-
const { collection, filter,
|
|
273
|
-
const networked = source !== "local-only";
|
|
274
|
-
const db = useTalaDB();
|
|
479
|
+
const { collection, filter, sort, page, limit, skip, enabled = true } = options;
|
|
275
480
|
const col = useCollection(collection);
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
const [
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const refetch = useCallback3(async () => {
|
|
291
|
-
const cfg = configRef.current;
|
|
292
|
-
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;
|
|
293
495
|
setSyncing(true);
|
|
294
496
|
setSyncError(null);
|
|
295
497
|
try {
|
|
296
498
|
await replicate(db, cfg, collection, "pull");
|
|
297
|
-
} catch (
|
|
298
|
-
setSyncError(
|
|
499
|
+
} catch (error) {
|
|
500
|
+
setSyncError(error);
|
|
299
501
|
} finally {
|
|
300
502
|
setSyncing(false);
|
|
301
503
|
setFirstSyncDone(true);
|
|
302
504
|
}
|
|
303
|
-
}, [db, collection,
|
|
304
|
-
|
|
305
|
-
if (!
|
|
306
|
-
void
|
|
505
|
+
}, [db, collection, legacyNetworked, legacyConfig?.endpoint]);
|
|
506
|
+
useEffect4(() => {
|
|
507
|
+
if (!enabled || !legacyNetworked || !legacyConfig) return;
|
|
508
|
+
void legacyRefetch();
|
|
307
509
|
if (pollMs > 0) {
|
|
308
|
-
const
|
|
309
|
-
return () => clearInterval(
|
|
510
|
+
const timer = setInterval(() => void legacyRefetch(), pollMs);
|
|
511
|
+
return () => clearInterval(timer);
|
|
310
512
|
}
|
|
311
513
|
return void 0;
|
|
312
|
-
}, [
|
|
313
|
-
|
|
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) {
|
|
314
564
|
throw new Error(
|
|
315
|
-
`useQuery({ collection: '${collection}' }) needs
|
|
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.`
|
|
316
566
|
);
|
|
317
567
|
}
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
};
|
|
320
580
|
}
|
|
321
581
|
|
|
322
582
|
// src/useQueries.ts
|
|
323
|
-
import { useEffect as
|
|
324
|
-
var NOOP_REFETCH = async () => {
|
|
325
|
-
};
|
|
326
|
-
function emptyResult() {
|
|
327
|
-
return { data: [], loading: true, error: null, syncing: false, syncError: null, refetch: NOOP_REFETCH };
|
|
328
|
-
}
|
|
583
|
+
import { useEffect as useEffect5, useMemo as useMemo6, useRef as useRef9, useState as useState4 } from "react";
|
|
329
584
|
function useQueries(queries) {
|
|
330
585
|
const db = useTalaDB();
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
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(
|
|
341
592
|
queries.map((q) => ({
|
|
342
593
|
collection: q.collection,
|
|
343
594
|
filter: q.filter ?? null,
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
347
600
|
}))
|
|
348
601
|
);
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
}))
|
|
355
610
|
);
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
setResults((prev) => {
|
|
362
|
-
if (i >= prev.length) return prev;
|
|
363
|
-
const copy = prev.slice();
|
|
364
|
-
copy[i] = fn(copy[i]);
|
|
365
|
-
return copy;
|
|
366
|
-
});
|
|
367
|
-
};
|
|
368
|
-
const resolved = qs.map((q, i) => {
|
|
369
|
-
const { config } = resolveReplicationConfig(b, {
|
|
370
|
-
endpoint: q.endpoint,
|
|
371
|
-
getAuth: q.getAuth,
|
|
372
|
-
fetch: q.fetch,
|
|
373
|
-
paths: q.paths,
|
|
374
|
-
pollMs: q.pollMs
|
|
375
|
-
});
|
|
376
|
-
const networked = (q.source ?? "local-first") !== "local-only";
|
|
377
|
-
const pollMs = q.pollMs ?? b?.pollMs ?? 0;
|
|
378
|
-
const refetch = async () => {
|
|
379
|
-
if (!networked || !config) return;
|
|
380
|
-
setAt(i, (r) => ({ ...r, syncing: true, syncError: null }));
|
|
381
|
-
try {
|
|
382
|
-
await replicate(db, config, q.collection, "pull");
|
|
383
|
-
} catch (e) {
|
|
384
|
-
if (!cancelled) setAt(i, (r) => ({ ...r, syncError: e }));
|
|
385
|
-
} finally {
|
|
386
|
-
if (!cancelled) setAt(i, (r) => ({ ...r, syncing: false }));
|
|
387
|
-
}
|
|
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 () => {
|
|
388
616
|
};
|
|
389
|
-
|
|
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
|
+
);
|
|
390
645
|
});
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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,
|
|
396
691
|
syncing: false,
|
|
397
692
|
syncError: null,
|
|
398
|
-
refetch:
|
|
399
|
-
|
|
400
|
-
);
|
|
401
|
-
const unsubs = qs.map((q, i) => {
|
|
402
|
-
const col = db.collection(q.collection);
|
|
403
|
-
return col.subscribe(
|
|
404
|
-
q.filter ?? {},
|
|
405
|
-
(docs) => {
|
|
406
|
-
if (!cancelled) setAt(i, (r) => ({ ...r, data: docs, loading: false, error: null }));
|
|
407
|
-
},
|
|
408
|
-
(error) => {
|
|
409
|
-
if (!cancelled) setAt(i, (r) => ({ ...r, loading: false, error }));
|
|
693
|
+
refetch: async () => {
|
|
694
|
+
await replication?.coordinators.get(q.collection)?.refresh();
|
|
410
695
|
}
|
|
411
|
-
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
void res.refetch();
|
|
417
|
-
if (res.pollMs > 0) intervals.push(setInterval(() => void res.refetch(), res.pollMs));
|
|
418
|
-
});
|
|
419
|
-
return () => {
|
|
420
|
-
cancelled = true;
|
|
421
|
-
unsubs.forEach((u) => u());
|
|
422
|
-
intervals.forEach((id) => clearInterval(id));
|
|
423
|
-
};
|
|
424
|
-
}, [db, sig]);
|
|
425
|
-
return queries.map((_q, i) => results[i] ?? emptyResult());
|
|
696
|
+
};
|
|
697
|
+
}),
|
|
698
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
699
|
+
[results, signature, replication]
|
|
700
|
+
);
|
|
426
701
|
}
|
|
427
702
|
|
|
428
703
|
// src/useMutation.ts
|
|
429
|
-
import { useCallback as
|
|
704
|
+
import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef10, useState as useState5 } from "react";
|
|
430
705
|
function useMutation(options) {
|
|
431
706
|
const { collection, direction = "push", drainOnMount = true } = options;
|
|
432
707
|
const db = useTalaDB();
|
|
@@ -437,12 +712,12 @@ function useMutation(options) {
|
|
|
437
712
|
fetch: options.fetch,
|
|
438
713
|
paths: options.paths
|
|
439
714
|
});
|
|
440
|
-
const configRef =
|
|
715
|
+
const configRef = useRef10(config);
|
|
441
716
|
configRef.current = config;
|
|
442
|
-
const [pending, setPending] =
|
|
443
|
-
const [error, setError] =
|
|
717
|
+
const [pending, setPending] = useState5(false);
|
|
718
|
+
const [error, setError] = useState5(null);
|
|
444
719
|
const endpoint = config?.endpoint;
|
|
445
|
-
const applyLocal =
|
|
720
|
+
const applyLocal = useCallback5(
|
|
446
721
|
async (op) => {
|
|
447
722
|
switch (op.type) {
|
|
448
723
|
case "insert":
|
|
@@ -458,12 +733,12 @@ function useMutation(options) {
|
|
|
458
733
|
},
|
|
459
734
|
[col]
|
|
460
735
|
);
|
|
461
|
-
const drain =
|
|
736
|
+
const drain = useCallback5(async () => {
|
|
462
737
|
const cfg = configRef.current;
|
|
463
738
|
if (!cfg) return;
|
|
464
739
|
await replicateWithRetry(db, cfg, collection, direction);
|
|
465
740
|
}, [db, collection, direction, endpoint]);
|
|
466
|
-
const mutateAsync =
|
|
741
|
+
const mutateAsync = useCallback5(
|
|
467
742
|
async (op) => {
|
|
468
743
|
setPending(true);
|
|
469
744
|
setError(null);
|
|
@@ -479,14 +754,14 @@ function useMutation(options) {
|
|
|
479
754
|
},
|
|
480
755
|
[applyLocal, drain]
|
|
481
756
|
);
|
|
482
|
-
const mutate =
|
|
757
|
+
const mutate = useCallback5(
|
|
483
758
|
(op) => {
|
|
484
759
|
void mutateAsync(op).catch(() => {
|
|
485
760
|
});
|
|
486
761
|
},
|
|
487
762
|
[mutateAsync]
|
|
488
763
|
);
|
|
489
|
-
|
|
764
|
+
useEffect6(() => {
|
|
490
765
|
if (!drainOnMount || !configRef.current) return;
|
|
491
766
|
void drain().catch(() => {
|
|
492
767
|
});
|
|
@@ -501,9 +776,13 @@ function useMutation(options) {
|
|
|
501
776
|
export {
|
|
502
777
|
ReplicationProvider,
|
|
503
778
|
TalaDBProvider,
|
|
779
|
+
useAggregate,
|
|
504
780
|
useCollection,
|
|
781
|
+
useCollectionOptions,
|
|
782
|
+
useCoverage,
|
|
505
783
|
useFind,
|
|
506
784
|
useFindOne,
|
|
785
|
+
useHydrationProgress,
|
|
507
786
|
useMutation,
|
|
508
787
|
useQueries,
|
|
509
788
|
useQuery,
|