@peerbit/react 0.0.32 → 0.0.33
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/lib/esm/__tests__/useQuery.dom.test.js +81 -7
- package/lib/esm/__tests__/useQuery.dom.test.js.map +1 -1
- package/lib/esm/usePeer.d.ts +1 -1
- package/lib/esm/usePeer.js +35 -11
- package/lib/esm/usePeer.js.map +1 -1
- package/lib/esm/useQuery.js +138 -68
- package/lib/esm/useQuery.js.map +1 -1
- package/package.json +74 -75
- package/src/__tests__/useQuery.dom.test.ts +134 -7
- package/src/usePeer.tsx +42 -19
- package/src/useQuery.tsx +177 -98
package/src/useQuery.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
AbstractSearchRequest,
|
|
4
4
|
AbstractSearchResult,
|
|
5
5
|
ClosedError,
|
|
6
|
+
Context,
|
|
6
7
|
Documents,
|
|
7
8
|
RemoteQueryOptions,
|
|
8
9
|
ResultsIterator,
|
|
@@ -84,6 +85,12 @@ export const useQuery = <
|
|
|
84
85
|
) => {
|
|
85
86
|
/* ─────── internal type alias for convenience ─────── */
|
|
86
87
|
type Item = RT;
|
|
88
|
+
type IteratorRef = {
|
|
89
|
+
id: string;
|
|
90
|
+
db: Documents<T, I>;
|
|
91
|
+
iterator: ResultsIterator<Item>;
|
|
92
|
+
itemsConsumed: number;
|
|
93
|
+
};
|
|
87
94
|
|
|
88
95
|
/* ────────────── normalise DBs input ────────────── */
|
|
89
96
|
const dbs = useMemo<(Documents<T, I> | undefined)[]>(() => {
|
|
@@ -96,14 +103,8 @@ export const useQuery = <
|
|
|
96
103
|
const [all, setAll] = useState<Item[]>([]);
|
|
97
104
|
const allRef = useRef<Item[]>([]);
|
|
98
105
|
const [isLoading, setIsLoading] = useState(false);
|
|
99
|
-
const iteratorRefs = useRef<
|
|
100
|
-
|
|
101
|
-
id: string;
|
|
102
|
-
db: Documents<T, I>;
|
|
103
|
-
iterator: ResultsIterator<Item>;
|
|
104
|
-
itemsConsumed: number;
|
|
105
|
-
}[]
|
|
106
|
-
>([]);
|
|
106
|
+
const iteratorRefs = useRef<IteratorRef[]>([]);
|
|
107
|
+
const itemIdRef = useRef(new WeakMap<object, string>());
|
|
107
108
|
const emptyResultsRef = useRef(false);
|
|
108
109
|
const closeControllerRef = useRef<AbortController | null>(null);
|
|
109
110
|
const waitedOnceRef = useRef(false);
|
|
@@ -132,12 +133,13 @@ export const useQuery = <
|
|
|
132
133
|
iteratorRefs.current?.forEach(({ iterator }) => iterator.close());
|
|
133
134
|
iteratorRefs.current = [];
|
|
134
135
|
|
|
135
|
-
closeControllerRef.current?.abort();
|
|
136
|
+
closeControllerRef.current?.abort(new Error("Reset"));
|
|
136
137
|
closeControllerRef.current = new AbortController();
|
|
137
138
|
emptyResultsRef.current = false;
|
|
138
139
|
waitedOnceRef.current = false;
|
|
139
140
|
|
|
140
141
|
allRef.current = [];
|
|
142
|
+
itemIdRef.current = new WeakMap();
|
|
141
143
|
setAll([]);
|
|
142
144
|
setIsLoading(false);
|
|
143
145
|
log("Iterators reset");
|
|
@@ -156,14 +158,6 @@ export const useQuery = <
|
|
|
156
158
|
return;
|
|
157
159
|
}
|
|
158
160
|
|
|
159
|
-
let isLogOpenInterval = options.debug
|
|
160
|
-
? setInterval(() => {
|
|
161
|
-
log(
|
|
162
|
-
"is open?",
|
|
163
|
-
iteratorRefs.current.map((x) => !x.iterator.done())
|
|
164
|
-
);
|
|
165
|
-
}, 5e3)
|
|
166
|
-
: undefined;
|
|
167
161
|
reset();
|
|
168
162
|
const abortSignal = closeControllerRef.current?.signal;
|
|
169
163
|
const onMissedResults = (evt: { amount: number }) => {
|
|
@@ -176,6 +170,7 @@ export const useQuery = <
|
|
|
176
170
|
};
|
|
177
171
|
let draining = false;
|
|
178
172
|
const scheduleDrain = (ref: ResultsIterator<RT>, amount: number) => {
|
|
173
|
+
log("Schedule drain", draining, ref, amount);
|
|
179
174
|
if (draining) return;
|
|
180
175
|
draining = true;
|
|
181
176
|
loadMore(amount)
|
|
@@ -188,6 +183,7 @@ export const useQuery = <
|
|
|
188
183
|
};
|
|
189
184
|
|
|
190
185
|
iteratorRefs.current = openDbs.map((db) => {
|
|
186
|
+
let currentRef: IteratorRef | undefined;
|
|
191
187
|
const iterator = db.index.iterate(query ?? {}, {
|
|
192
188
|
closePolicy: "manual",
|
|
193
189
|
local: options.local ?? true,
|
|
@@ -214,6 +210,7 @@ export const useQuery = <
|
|
|
214
210
|
resolve,
|
|
215
211
|
signal: abortSignal,
|
|
216
212
|
updates: {
|
|
213
|
+
push: true,
|
|
217
214
|
merge:
|
|
218
215
|
typeof options.updates === "boolean" && options.updates
|
|
219
216
|
? true
|
|
@@ -222,6 +219,7 @@ export const useQuery = <
|
|
|
222
219
|
? true
|
|
223
220
|
: false,
|
|
224
221
|
onChange: (evt) => {
|
|
222
|
+
log("Live update", evt);
|
|
225
223
|
if (evt.added.length > 0) {
|
|
226
224
|
scheduleDrain(
|
|
227
225
|
iterator as ResultsIterator<RT>,
|
|
@@ -230,49 +228,27 @@ export const useQuery = <
|
|
|
230
228
|
}
|
|
231
229
|
},
|
|
232
230
|
onResults: (batch, props) => {
|
|
231
|
+
log("onResults", { batch, props, currentRef: !!currentRef });
|
|
233
232
|
if (
|
|
234
233
|
props.reason === "join" ||
|
|
235
234
|
props.reason === "change"
|
|
236
235
|
) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
let ix = (
|
|
242
|
-
options?.resolve
|
|
243
|
-
? (x as WithIndexedContext<T, I>)
|
|
244
|
-
?.__indexed
|
|
245
|
-
: (x as WithContext<I>)
|
|
246
|
-
) as I;
|
|
247
|
-
const existingId = db.index.resolveId(ix);
|
|
248
|
-
return existingId === id;
|
|
249
|
-
});
|
|
250
|
-
if (existingIndex !== -1) {
|
|
251
|
-
newArr[existingIndex] = item as Item;
|
|
252
|
-
} else {
|
|
253
|
-
if (!options.reverse) {
|
|
254
|
-
newArr.unshift(item as Item);
|
|
255
|
-
} else {
|
|
256
|
-
newArr.push(item as Item);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
log(
|
|
261
|
-
"merging ",
|
|
262
|
-
batch,
|
|
263
|
-
"into ",
|
|
264
|
-
newArr,
|
|
265
|
-
[...allRef.current],
|
|
266
|
-
options?.resolve
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
updateAll(newArr);
|
|
236
|
+
if (!currentRef) return;
|
|
237
|
+
handleBatch(iteratorRefs.current, [
|
|
238
|
+
{ ref: currentRef, items: batch as Item[] },
|
|
239
|
+
]);
|
|
270
240
|
}
|
|
271
241
|
},
|
|
272
242
|
},
|
|
273
243
|
}) as ResultsIterator<Item>;
|
|
274
244
|
|
|
275
|
-
const ref = {
|
|
245
|
+
const ref: IteratorRef = {
|
|
246
|
+
id: uuid(),
|
|
247
|
+
db,
|
|
248
|
+
iterator,
|
|
249
|
+
itemsConsumed: 0,
|
|
250
|
+
};
|
|
251
|
+
currentRef = ref;
|
|
276
252
|
log("Iterator init", ref.id, "db", db.address);
|
|
277
253
|
return ref;
|
|
278
254
|
});
|
|
@@ -282,10 +258,6 @@ export const useQuery = <
|
|
|
282
258
|
|
|
283
259
|
/* prefetch if requested */
|
|
284
260
|
if (options.prefetch) void loadMore();
|
|
285
|
-
|
|
286
|
-
return () => {
|
|
287
|
-
clearInterval(isLogOpenInterval);
|
|
288
|
-
};
|
|
289
261
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
290
262
|
}, [
|
|
291
263
|
dbs.map((d) => d?.address).join("|"),
|
|
@@ -307,6 +279,155 @@ export const useQuery = <
|
|
|
307
279
|
waitedOnceRef.current = true;
|
|
308
280
|
};
|
|
309
281
|
|
|
282
|
+
/* helper to turn primitive ids into stable map keys */
|
|
283
|
+
const idToKey = (value: indexerTypes.IdPrimitive): string => {
|
|
284
|
+
switch (typeof value) {
|
|
285
|
+
case "string":
|
|
286
|
+
return `s:${value}`;
|
|
287
|
+
case "number":
|
|
288
|
+
return `n:${value}`;
|
|
289
|
+
default:
|
|
290
|
+
return `b:${value.toString()}`;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const handleBatch = async (
|
|
295
|
+
iterators: IteratorRef[],
|
|
296
|
+
batches: { ref: IteratorRef; items: Item[] }[]
|
|
297
|
+
): Promise<boolean> => {
|
|
298
|
+
if (!iterators.length) {
|
|
299
|
+
log("No iterators in handleBatch");
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const totalFetched = batches.reduce(
|
|
304
|
+
(sum, batch) => sum + batch.items.length,
|
|
305
|
+
0
|
|
306
|
+
);
|
|
307
|
+
if (totalFetched === 0) {
|
|
308
|
+
log("No items fetched");
|
|
309
|
+
emptyResultsRef.current = iterators.every((i) => i.iterator.done());
|
|
310
|
+
return !emptyResultsRef.current;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let processed = batches;
|
|
314
|
+
if (options.transform) {
|
|
315
|
+
const transform = options.transform;
|
|
316
|
+
processed = await Promise.all(
|
|
317
|
+
batches.map(async ({ ref, items }) => ({
|
|
318
|
+
ref,
|
|
319
|
+
items: await Promise.all(items.map(transform)),
|
|
320
|
+
}))
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const prev = allRef.current;
|
|
325
|
+
const next = [...prev];
|
|
326
|
+
const keyIndex = new Map<string, number>();
|
|
327
|
+
prev.forEach((item, idx) => {
|
|
328
|
+
const key = itemIdRef.current.get(item as object);
|
|
329
|
+
if (key) keyIndex.set(key, idx);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const seenHeads = new Set(prev.map((x) => (x as any).__context?.head));
|
|
333
|
+
const freshItems: Item[] = [];
|
|
334
|
+
let hasMutations = false;
|
|
335
|
+
|
|
336
|
+
log("Processing batches", { processed, keyIndex });
|
|
337
|
+
for (const { ref, items } of processed) {
|
|
338
|
+
const db = ref.db;
|
|
339
|
+
for (const item of items) {
|
|
340
|
+
const ctx = (item as WithContext<any>).__context;
|
|
341
|
+
const head = ctx?.head;
|
|
342
|
+
|
|
343
|
+
let key: string | null = null;
|
|
344
|
+
try {
|
|
345
|
+
key = idToKey(
|
|
346
|
+
db.index.resolveId(
|
|
347
|
+
item as WithContext<I> | WithIndexedContext<T, I>
|
|
348
|
+
).primitive
|
|
349
|
+
);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
log("useQuery: failed to resolve id", error);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (key && keyIndex.has(key)) {
|
|
355
|
+
const existingIndex = keyIndex.get(key)!;
|
|
356
|
+
const current = next[existingIndex];
|
|
357
|
+
const currentContext: Context | undefined = (
|
|
358
|
+
current as WithContext<any>
|
|
359
|
+
)?.__context;
|
|
360
|
+
const incomingContext: Context | undefined = ctx;
|
|
361
|
+
const shouldReplace =
|
|
362
|
+
!currentContext ||
|
|
363
|
+
!incomingContext ||
|
|
364
|
+
currentContext.modified <= incomingContext.modified;
|
|
365
|
+
|
|
366
|
+
if (shouldReplace && current !== item) {
|
|
367
|
+
itemIdRef.current.delete(current as object);
|
|
368
|
+
next[existingIndex] = item;
|
|
369
|
+
hasMutations = true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (key) {
|
|
373
|
+
itemIdRef.current.set(item as object, key);
|
|
374
|
+
keyIndex.set(key, existingIndex);
|
|
375
|
+
}
|
|
376
|
+
if (head != null) seenHeads.add(head);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (head != null && seenHeads.has(head)) continue;
|
|
381
|
+
if (head != null) seenHeads.add(head);
|
|
382
|
+
|
|
383
|
+
freshItems.push(item);
|
|
384
|
+
if (key) {
|
|
385
|
+
itemIdRef.current.set(item as object, key);
|
|
386
|
+
keyIndex.set(key, prev.length + freshItems.length - 1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
if (!freshItems.length && !hasMutations) {
|
|
393
|
+
emptyResultsRef.current = iterators.every((i) => i.iterator.done());
|
|
394
|
+
log("No new items or mutations");
|
|
395
|
+
return !emptyResultsRef.current;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
const combined = reverseRef.current
|
|
400
|
+
? [...freshItems.reverse(), ...next]
|
|
401
|
+
: [...next, ...freshItems];
|
|
402
|
+
|
|
403
|
+
log("Updating all with", {
|
|
404
|
+
prevLength: prev.length,
|
|
405
|
+
freshLength: freshItems.length,
|
|
406
|
+
combinedLength: combined.length,
|
|
407
|
+
});
|
|
408
|
+
updateAll(combined);
|
|
409
|
+
|
|
410
|
+
emptyResultsRef.current = iterators.every((i) => i.iterator.done());
|
|
411
|
+
return !emptyResultsRef.current;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const drainRoundRobin = async (
|
|
415
|
+
iterators: IteratorRef[],
|
|
416
|
+
n: number
|
|
417
|
+
): Promise<boolean> => {
|
|
418
|
+
const batches: { ref: IteratorRef; items: Item[] }[] = [];
|
|
419
|
+
for (const ref of iterators) {
|
|
420
|
+
if (ref.iterator.done()) continue;
|
|
421
|
+
const batch = await ref.iterator.next(n);
|
|
422
|
+
log("Iterator", ref.id, "fetched", batch.length, "items");
|
|
423
|
+
if (batch.length) {
|
|
424
|
+
ref.itemsConsumed += batch.length;
|
|
425
|
+
batches.push({ ref, items: batch });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return handleBatch(iterators, batches);
|
|
429
|
+
};
|
|
430
|
+
|
|
310
431
|
/* maybe make the rule that if results are empty and we get results from joining
|
|
311
432
|
set the results to the joining results
|
|
312
433
|
when results are not empty use onMerge option to merge the results ? */
|
|
@@ -351,49 +472,7 @@ export const useQuery = <
|
|
|
351
472
|
markWaited();
|
|
352
473
|
}
|
|
353
474
|
|
|
354
|
-
|
|
355
|
-
const newlyFetched: Item[] = [];
|
|
356
|
-
for (const ref of iterators) {
|
|
357
|
-
if (ref.iterator.done()) continue;
|
|
358
|
-
const batch = await ref.iterator.next(n); // pull up to <n> at once
|
|
359
|
-
log("Iterator", ref.id, "fetched", batch.length, "items");
|
|
360
|
-
if (batch.length) {
|
|
361
|
-
ref.itemsConsumed += batch.length;
|
|
362
|
-
newlyFetched.push(...batch);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (!newlyFetched.length) {
|
|
367
|
-
emptyResultsRef.current = iterators.every((i) =>
|
|
368
|
-
i.iterator.done()
|
|
369
|
-
);
|
|
370
|
-
return !emptyResultsRef.current;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/* optional transform */
|
|
374
|
-
let processed = newlyFetched;
|
|
375
|
-
if (options.transform) {
|
|
376
|
-
processed = await Promise.all(processed.map(options.transform));
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/* deduplicate & merge */
|
|
380
|
-
const prev = allRef.current;
|
|
381
|
-
const dedupHeads = new Set(
|
|
382
|
-
prev.map((x) => (x as any).__context.head)
|
|
383
|
-
);
|
|
384
|
-
const unique = processed.filter(
|
|
385
|
-
(x) => !dedupHeads.has((x as any).__context.head)
|
|
386
|
-
);
|
|
387
|
-
if (!unique.length)
|
|
388
|
-
return !iterators.every((i) => i.iterator.done());
|
|
389
|
-
|
|
390
|
-
const combined = reverseRef.current
|
|
391
|
-
? [...unique.reverse(), ...prev]
|
|
392
|
-
: [...prev, ...unique];
|
|
393
|
-
updateAll(combined);
|
|
394
|
-
|
|
395
|
-
emptyResultsRef.current = iterators.every((i) => i.iterator.done());
|
|
396
|
-
return !emptyResultsRef.current;
|
|
475
|
+
return drainRoundRobin(iterators, n);
|
|
397
476
|
} catch (e) {
|
|
398
477
|
if (!(e instanceof ClosedError)) throw e;
|
|
399
478
|
return false;
|