@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/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
- let newArr = [...allRef.current];
238
- for (const item of batch) {
239
- const id = db.index.resolveId(item);
240
- const existingIndex = newArr.findIndex((x) => {
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 = { id: uuid(), db, iterator, itemsConsumed: 0 };
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
- /* pull items round-robin */
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;