@taladb/react 0.10.1 → 0.11.0
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/LICENSE-MIT +21 -0
- package/README.md +187 -0
- package/dist/chunk-SEH233OC.mjs +129 -0
- package/dist/index.d.mts +40 -309
- package/dist/index.d.ts +40 -309
- package/dist/index.js +23 -575
- package/dist/index.mjs +32 -707
- package/dist/query/index.d.mts +1664 -0
- package/dist/query/index.d.ts +1664 -0
- package/dist/query/index.js +2226 -0
- package/dist/query/index.mjs +2091 -0
- package/package.json +14 -5
- /package/{LICENSE → LICENSE-APACHE} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,127 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
|
|
3
|
-
// src/context.tsx
|
|
4
2
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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-SEH233OC.mjs";
|
|
118
9
|
|
|
119
10
|
// src/useFindOne.ts
|
|
120
|
-
import { useCallback
|
|
11
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
121
12
|
function useFindOne(collection, filter) {
|
|
122
|
-
const snapshotRef =
|
|
13
|
+
const snapshotRef = useRef({ data: null, loading: true, error: null });
|
|
123
14
|
const filterKey = JSON.stringify(filter);
|
|
124
|
-
const subscribe =
|
|
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 =
|
|
139
|
-
return
|
|
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
|
|
34
|
+
import { useCallback as useCallback2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
144
35
|
function useAggregate(collection, pipeline) {
|
|
145
|
-
const snapshotRef =
|
|
36
|
+
const snapshotRef = useRef2({ data: [], loading: true, error: null });
|
|
146
37
|
const pipelineKey = JSON.stringify(pipeline);
|
|
147
|
-
const subscribe =
|
|
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 =
|
|
166
|
-
return
|
|
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/
|
|
704
|
-
import { useCallback as
|
|
705
|
-
function
|
|
706
|
-
const { collection
|
|
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
|
|
710
|
-
|
|
711
|
-
|
|
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
|
|
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
|
|
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
|
-
[
|
|
96
|
+
[apply]
|
|
756
97
|
);
|
|
757
|
-
const
|
|
98
|
+
const write = useCallback3(
|
|
758
99
|
(op) => {
|
|
759
|
-
void
|
|
100
|
+
void writeAsync(op).catch(() => {
|
|
760
101
|
});
|
|
761
102
|
},
|
|
762
|
-
[
|
|
103
|
+
[writeAsync]
|
|
763
104
|
);
|
|
764
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
useQueries,
|
|
788
|
-
useQuery,
|
|
789
|
-
useReplicationConfig,
|
|
790
|
-
useTalaDB
|
|
114
|
+
useTalaDB,
|
|
115
|
+
useWrite
|
|
791
116
|
};
|