@peerbit/react 0.0.31 → 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
@@ -1,16 +1,18 @@
1
1
  import { useState, useEffect, useRef, useMemo } from "react";
2
2
  import {
3
+ AbstractSearchRequest,
4
+ AbstractSearchResult,
3
5
  ClosedError,
6
+ Context,
4
7
  Documents,
5
- DocumentsChange,
8
+ RemoteQueryOptions,
6
9
  ResultsIterator,
7
10
  WithContext,
8
11
  } from "@peerbit/document";
9
12
  import * as indexerTypes from "@peerbit/indexer-interface";
10
- import { AbortError } from "@peerbit/time";
11
- import { NoPeersError } from "@peerbit/shared-log";
12
13
  import { v4 as uuid } from "uuid";
13
14
  import { WithIndexedContext } from "@peerbit/document";
15
+ import { UpdateOptions } from "@peerbit/document";
14
16
 
15
17
  type QueryOptions = { query: QueryLike; id?: string };
16
18
 
@@ -26,38 +28,39 @@ export type QueryLike = {
26
28
  * All the non-DB-specific options supported by the original single-DB hook.
27
29
  * They stay fully backward-compatible.
28
30
  */
29
- export type UseQuerySharedOptions<T, I, R extends boolean | undefined, RT> = {
31
+ export type UseQuerySharedOptions<
32
+ T,
33
+ I,
34
+ R extends boolean | undefined,
35
+ RT = R extends false ? WithContext<I> : WithIndexedContext<T, I>,
36
+ > = {
30
37
  /* original behavioural flags */
31
38
  resolve?: R;
32
39
  transform?: (r: RT) => Promise<RT>;
33
40
  debounce?: number;
34
- debug?: boolean | { id: string };
41
+ debug?: boolean | string;
35
42
  reverse?: boolean;
36
43
  batchSize?: number;
37
44
  prefetch?: boolean;
38
- ignoreUpdates?: boolean;
39
- onChange?: {
40
- merge?:
41
- | boolean
42
- | ((
43
- c: DocumentsChange<T, I>
44
- ) =>
45
- | DocumentsChange<T, I>
46
- | Promise<DocumentsChange<T, I>>
47
- | undefined);
48
- update?: (
49
- prev: RT[],
50
- change: DocumentsChange<T, I>
51
- ) => RT[] | Promise<RT[]>;
52
- };
45
+ /* onChange?: {
46
+ merge?:
47
+ | boolean
48
+ | ((
49
+ c: DocumentsChange<T, I>
50
+ ) =>
51
+ | DocumentsChange<T, I>
52
+ | Promise<DocumentsChange<T, I>>
53
+ | undefined);
54
+ update?: (
55
+ prev: RT[],
56
+ change: DocumentsChange<T, I>
57
+ ) => RT[] | Promise<RT[]>;
58
+ }; */
59
+ updates?: UpdateOptions<T, I, R>;
53
60
  local?: boolean;
54
61
  remote?:
55
- | boolean
56
- | {
57
- warmup?: number;
58
- joining?: { waitFor?: number };
59
- eager?: boolean;
60
- };
62
+ | boolean
63
+ | RemoteQueryOptions<AbstractSearchRequest, AbstractSearchResult, any>;
61
64
  } & QueryOptions;
62
65
 
63
66
  /* ────────────────────────── Main Hook ────────────────────────── */
@@ -74,7 +77,7 @@ export const useQuery = <
74
77
  T extends Record<string, any>,
75
78
  I extends Record<string, any>,
76
79
  R extends boolean | undefined = true,
77
- RT = R extends false ? WithContext<I> : WithIndexedContext<T, I>
80
+ RT = R extends false ? WithContext<I> : WithIndexedContext<T, I>,
78
81
  >(
79
82
  /** Single DB or list of DBs. 100 % backward-compatible with the old single param. */
80
83
  dbOrDbs: Documents<T, I> | Documents<T, I>[] | undefined,
@@ -82,6 +85,12 @@ export const useQuery = <
82
85
  ) => {
83
86
  /* ─────── internal type alias for convenience ─────── */
84
87
  type Item = RT;
88
+ type IteratorRef = {
89
+ id: string;
90
+ db: Documents<T, I>;
91
+ iterator: ResultsIterator<Item>;
92
+ itemsConsumed: number;
93
+ };
85
94
 
86
95
  /* ────────────── normalise DBs input ────────────── */
87
96
  const dbs = useMemo<(Documents<T, I> | undefined)[]>(() => {
@@ -94,14 +103,8 @@ export const useQuery = <
94
103
  const [all, setAll] = useState<Item[]>([]);
95
104
  const allRef = useRef<Item[]>([]);
96
105
  const [isLoading, setIsLoading] = useState(false);
97
- const iteratorRefs = useRef<
98
- {
99
- id: string;
100
- db: Documents<T, I>;
101
- iterator: ResultsIterator<Item>;
102
- itemsConsumed: number;
103
- }[]
104
- >([]);
106
+ const iteratorRefs = useRef<IteratorRef[]>([]);
107
+ const itemIdRef = useRef(new WeakMap<object, string>());
105
108
  const emptyResultsRef = useRef(false);
106
109
  const closeControllerRef = useRef<AbortController | null>(null);
107
110
  const waitedOnceRef = useRef(false);
@@ -118,7 +121,7 @@ export const useQuery = <
118
121
  const log = (...a: any[]) => {
119
122
  if (!options.debug) return;
120
123
  if (typeof options.debug === "boolean") console.log(...a);
121
- else console.log(options.debug.id, ...a);
124
+ else console.log(options.debug, ...a);
122
125
  };
123
126
 
124
127
  const updateAll = (combined: Item[]) => {
@@ -127,15 +130,16 @@ export const useQuery = <
127
130
  };
128
131
 
129
132
  const reset = () => {
130
- iteratorRefs.current.forEach(({ iterator }) => iterator.close());
133
+ iteratorRefs.current?.forEach(({ iterator }) => iterator.close());
131
134
  iteratorRefs.current = [];
132
135
 
133
- closeControllerRef.current?.abort();
136
+ closeControllerRef.current?.abort(new Error("Reset"));
134
137
  closeControllerRef.current = new AbortController();
135
138
  emptyResultsRef.current = false;
136
139
  waitedOnceRef.current = false;
137
140
 
138
141
  allRef.current = [];
142
+ itemIdRef.current = new WeakMap();
139
143
  setAll([]);
140
144
  setIsLoading(false);
141
145
  log("Iterators reset");
@@ -156,24 +160,104 @@ export const useQuery = <
156
160
 
157
161
  reset();
158
162
  const abortSignal = closeControllerRef.current?.signal;
163
+ const onMissedResults = (evt: { amount: number }) => {
164
+ console.error("Not effective yet: missed results", evt);
165
+ /* if (allRef.current.length > 0 || typeof options.remote !== "object" || !options.updates) {
166
+ return;
167
+ }
168
+ console.log("Missed results, loading more", evt.amount);
169
+ loadMore(evt.amount); */
170
+ };
171
+ let draining = false;
172
+ const scheduleDrain = (ref: ResultsIterator<RT>, amount: number) => {
173
+ log("Schedule drain", draining, ref, amount);
174
+ if (draining) return;
175
+ draining = true;
176
+ loadMore(amount)
177
+ .catch((e) => {
178
+ if (!(e instanceof ClosedError)) throw e;
179
+ })
180
+ .finally(() => {
181
+ draining = false;
182
+ });
183
+ };
159
184
 
160
185
  iteratorRefs.current = openDbs.map((db) => {
186
+ let currentRef: IteratorRef | undefined;
161
187
  const iterator = db.index.iterate(query ?? {}, {
188
+ closePolicy: "manual",
162
189
  local: options.local ?? true,
163
- remote: options.remote ?? undefined,
190
+ remote: options.remote
191
+ ? {
192
+ ...(typeof options?.remote === "object"
193
+ ? {
194
+ ...options.remote,
195
+ onLateResults: onMissedResults,
196
+ wait: {
197
+ ...options?.remote?.wait,
198
+ timeout:
199
+ options?.remote?.wait?.timeout ??
200
+ 5000,
201
+ },
202
+ }
203
+ : options?.remote
204
+ ? {
205
+ onLateResults: onMissedResults,
206
+ }
207
+ : undefined),
208
+ }
209
+ : undefined,
164
210
  resolve,
165
211
  signal: abortSignal,
212
+ updates: {
213
+ push: true,
214
+ merge:
215
+ typeof options.updates === "boolean" && options.updates
216
+ ? true
217
+ : typeof options.updates === "object" &&
218
+ options.updates.merge
219
+ ? true
220
+ : false,
221
+ onChange: (evt) => {
222
+ log("Live update", evt);
223
+ if (evt.added.length > 0) {
224
+ scheduleDrain(
225
+ iterator as ResultsIterator<RT>,
226
+ evt.added.length
227
+ );
228
+ }
229
+ },
230
+ onResults: (batch, props) => {
231
+ log("onResults", { batch, props, currentRef: !!currentRef });
232
+ if (
233
+ props.reason === "join" ||
234
+ props.reason === "change"
235
+ ) {
236
+ if (!currentRef) return;
237
+ handleBatch(iteratorRefs.current, [
238
+ { ref: currentRef, items: batch as Item[] },
239
+ ]);
240
+ }
241
+ },
242
+ },
166
243
  }) as ResultsIterator<Item>;
167
244
 
168
- 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;
169
252
  log("Iterator init", ref.id, "db", db.address);
170
253
  return ref;
171
254
  });
172
255
 
173
- /* prefetch if requested */
174
- if (options.prefetch) void loadMore();
175
256
  /* store a deterministic id (useful for external keys) */
176
257
  setId(uuid());
258
+
259
+ /* prefetch if requested */
260
+ if (options.prefetch) void loadMore();
177
261
  // eslint-disable-next-line react-hooks/exhaustive-deps
178
262
  }, [
179
263
  dbs.map((d) => d?.address).join("|"),
@@ -195,82 +279,200 @@ export const useQuery = <
195
279
  waitedOnceRef.current = true;
196
280
  };
197
281
 
198
- const loadMore = async (n: number = batchSize): Promise<boolean> => {
199
- const iterators = iteratorRefs.current;
200
- if (!iterators.length || emptyResultsRef.current) return false;
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
+ };
201
293
 
202
- setIsLoading(true);
203
- try {
204
- /* one-time replicator warm-up across all DBs */
205
- if (shouldWait()) {
206
- if (
207
- typeof options.remote === "object" &&
208
- options.remote.warmup
209
- ) {
210
- await Promise.all(
211
- iterators.map(async ({ db }) => {
212
- try {
213
- await db.log.waitForReplicators({
214
- timeout: (options.remote as { warmup })
215
- .warmup,
216
- signal: closeControllerRef.current?.signal,
217
- });
218
- } catch (e) {
219
- if (
220
- e instanceof AbortError ||
221
- e instanceof NoPeersError
222
- )
223
- return;
224
- console.warn("Remote replicators not ready", e);
225
- }
226
- })
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
227
349
  );
350
+ } catch (error) {
351
+ log("useQuery: failed to resolve id", error);
228
352
  }
229
- markWaited();
230
- }
231
353
 
232
- /* pull items round-robin */
233
- const newlyFetched: Item[] = [];
234
- for (const ref of iterators) {
235
- if (ref.iterator.done()) continue;
236
- const batch = await ref.iterator.next(n); // pull up to <n> at once
237
- if (batch.length) {
238
- ref.itemsConsumed += batch.length;
239
- newlyFetched.push(...batch);
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;
240
378
  }
241
- }
242
379
 
243
- if (!newlyFetched.length) {
244
- emptyResultsRef.current = iterators.every((i) =>
245
- i.iterator.done()
246
- );
247
- return !emptyResultsRef.current;
248
- }
380
+ if (head != null && seenHeads.has(head)) continue;
381
+ if (head != null) seenHeads.add(head);
249
382
 
250
- /* optional transform */
251
- let processed = newlyFetched;
252
- if (options.transform) {
253
- processed = await Promise.all(processed.map(options.transform));
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
+ }
254
388
  }
389
+ }
255
390
 
256
- /* deduplicate & merge */
257
- const prev = allRef.current;
258
- const dedupHeads = new Set(
259
- prev.map((x) => (x as any).__context.head)
260
- );
261
- const unique = processed.filter(
262
- (x) => !dedupHeads.has((x as any).__context.head)
263
- );
264
- if (!unique.length)
265
- return !iterators.every((i) => i.iterator.done());
266
-
267
- const combined = reverseRef.current
268
- ? [...unique.reverse(), ...prev]
269
- : [...prev, ...unique];
270
- updateAll(combined);
271
391
 
392
+ if (!freshItems.length && !hasMutations) {
272
393
  emptyResultsRef.current = iterators.every((i) => i.iterator.done());
394
+ log("No new items or mutations");
273
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
+
431
+ /* maybe make the rule that if results are empty and we get results from joining
432
+ set the results to the joining results
433
+ when results are not empty use onMerge option to merge the results ? */
434
+
435
+ const loadMore = async (n: number = batchSize): Promise<boolean> => {
436
+ const iterators = iteratorRefs.current;
437
+ if (!iterators.length || emptyResultsRef.current) {
438
+ log("No iterators or already empty", {
439
+ length: iterators.length,
440
+ emptyResultsRef: emptyResultsRef.current,
441
+ });
442
+ return false;
443
+ }
444
+
445
+ setIsLoading(true);
446
+ try {
447
+ /* one-time replicator warm-up across all DBs */
448
+ if (shouldWait()) {
449
+ /* if (
450
+ typeof options.remote === "object" &&
451
+ options.remote.wait
452
+ ) {
453
+ await Promise.all(
454
+ iterators.map(async ({ db }) => {
455
+ try {
456
+ await db.log.waitForReplicators({
457
+ timeout: (options.remote as { warmup })
458
+ .warmup,
459
+ signal: closeControllerRef.current?.signal,
460
+ });
461
+ } catch (e) {
462
+ if (
463
+ e instanceof AbortError ||
464
+ e instanceof NoPeersError
465
+ )
466
+ return;
467
+ console.warn("Remote replicators not ready", e);
468
+ }
469
+ })
470
+ );
471
+ }*/
472
+ markWaited();
473
+ }
474
+
475
+ return drainRoundRobin(iterators, n);
274
476
  } catch (e) {
275
477
  if (!(e instanceof ClosedError)) throw e;
276
478
  return false;
@@ -281,13 +483,15 @@ export const useQuery = <
281
483
 
282
484
  /* ────────────── live-merge listeners ────────────── */
283
485
  useEffect(() => {
284
- if (!options.onChange || options.onChange.merge === false) return;
486
+ if (!options.updates) {
487
+ return;
488
+ }
285
489
 
286
- const listeners = iteratorRefs.current.map(({ db, id: itId }) => {
287
- const mergeFn =
490
+ /* const listeners = iteratorRefs.current.map(({ db, id: itId }) => {
491
+ const mergeFn =
288
492
  typeof options.onChange?.merge === "function"
289
493
  ? options.onChange.merge
290
- : (c: DocumentsChange<T, I>) => c;
494
+ : (c: DocumentsChange<T, I>) => c;
291
495
 
292
496
  const handler = async (e: CustomEvent<DocumentsChange<T, I>>) => {
293
497
  log("Merge change", e.detail, "it", itId);
@@ -314,7 +518,7 @@ export const useQuery = <
314
518
  }
315
519
  updateAll(options.reverse ? merged.reverse() : merged);
316
520
  };
317
- db.events.addEventListener("change", handler);
521
+ db.events.addEventListener("change", handler);
318
522
  return { db, handler };
319
523
  });
320
524
 
@@ -322,11 +526,15 @@ export const useQuery = <
322
526
  listeners.forEach(({ db, handler }) =>
323
527
  db.events.removeEventListener("change", handler)
324
528
  );
325
- };
529
+ }; */
530
+
326
531
  // eslint-disable-next-line react-hooks/exhaustive-deps
327
532
  }, [
328
533
  iteratorRefs.current.map((r) => r.db.address).join("|"),
329
- options.onChange,
534
+ options.updates,
535
+ options.query,
536
+ options.resolve,
537
+ options.reverse,
330
538
  ]);
331
539
 
332
540
  /* ────────────── public API – unchanged from the caller's perspective ────────────── */
package/src/utils.ts CHANGED
@@ -142,7 +142,7 @@ export const inIframe = () => {
142
142
  };
143
143
 
144
144
  export function debounceLeadingTrailing<
145
- T extends (this: any, ...args: any[]) => void
145
+ T extends (this: any, ...args: any[]) => void,
146
146
  >(
147
147
  func: T,
148
148
  delay: number