@peerbit/react 0.0.26 → 0.0.28

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,4 +1,4 @@
1
- import { useState, useEffect, useRef, useReducer } from "react";
1
+ import { useState, useEffect, useRef, useMemo } from "react";
2
2
  import {
3
3
  ClosedError,
4
4
  Documents,
@@ -10,82 +10,113 @@ import * as indexerTypes from "@peerbit/indexer-interface";
10
10
  import { AbortError } from "@peerbit/time";
11
11
  import { NoPeersError } from "@peerbit/shared-log";
12
12
  import { v4 as uuid } from "uuid";
13
- import { WithIndexedContext } from "@peerbit/document/dist/src/search";
14
- import { on } from "events";
15
- /* ────────────── helper types ────────────── */
16
- type QueryLike = {
17
- query?: indexerTypes.Query[] | indexerTypes.QueryLike;
18
- sort?: indexerTypes.Sort[] | indexerTypes.Sort | indexerTypes.SortLike;
19
- };
13
+ import { WithIndexedContext } from "@peerbit/document";
14
+
20
15
  type QueryOptions = { query: QueryLike; id?: string };
21
16
 
22
- type RemoteQueryOptions = {
23
- warmup?: number;
24
- joining?: { waitFor?: number };
25
- eager?: boolean;
17
+ /* ────────────── helper types ────────────── */
18
+ export type QueryLike = {
19
+ /** Mongo-style selector or array of selectors */
20
+ query?: indexerTypes.QueryLike | indexerTypes.Query[];
21
+ /** Sort definition compatible with `@peerbit/indexer-interface` */
22
+ sort?: indexerTypes.SortLike | indexerTypes.Sort | indexerTypes.Sort[];
26
23
  };
27
24
 
28
- /* ────────────── main hook ────────────── */
25
+ /**
26
+ * All the non-DB-specific options supported by the original single-DB hook.
27
+ * They stay fully backward-compatible.
28
+ */
29
+ export type UseQuerySharedOptions<T, I, R extends boolean | undefined, RT> = {
30
+ /* original behavioural flags */
31
+ resolve?: R;
32
+ transform?: (r: RT) => Promise<RT>;
33
+ debounce?: number;
34
+ debug?: boolean | { id: string };
35
+ reverse?: boolean;
36
+ batchSize?: number;
37
+ 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
+ };
53
+ local?: boolean;
54
+ remote?:
55
+ | boolean
56
+ | {
57
+ warmup?: number;
58
+ joining?: { waitFor?: number };
59
+ eager?: boolean;
60
+ };
61
+ } & QueryOptions;
62
+
63
+ /* ────────────────────────── Main Hook ────────────────────────── */
64
+ /**
65
+ * `useQuery` – unified hook that accepts **either**
66
+ * 1. a single `Documents` instance
67
+ * 2. an array of `Documents` instances
68
+ * 3. *or* omits the first argument and provides `dbs` inside the `options` object.
69
+ *
70
+ * It supersedes the original single-DB version as well as the experimental
71
+ * `useMultiQuery` so callers never have to choose between two APIs.
72
+ */
29
73
  export const useQuery = <
30
74
  T extends Record<string, any>,
31
75
  I extends Record<string, any>,
32
76
  R extends boolean | undefined = true,
33
77
  RT = R extends false ? WithContext<I> : WithIndexedContext<T, I>
34
78
  >(
35
- db?: Documents<T, I>,
36
- options?: {
37
- resolve?: R;
38
- transform?: (r: RT) => Promise<RT>;
39
- debounce?: number;
40
- debug?: boolean | { id: string };
41
- reverse?: boolean;
42
- batchSize?: number;
43
- prefetch?: boolean;
44
- onChange?: {
45
- merge?:
46
- | boolean
47
- | ((
48
- c: DocumentsChange<T, I>
49
- ) =>
50
- | DocumentsChange<T, I>
51
- | Promise<DocumentsChange<T, I>>
52
- | undefined);
53
- update?: (
54
- prev: RT[],
55
- change: DocumentsChange<T, I>
56
- ) => RT[] | Promise<RT[]>;
57
- };
58
- local?: boolean;
59
- remote?: boolean | RemoteQueryOptions;
60
- } & QueryOptions
79
+ /** Single DB or list of DBs. 100 % backward-compatible with the old single param. */
80
+ dbOrDbs: Documents<T, I> | Documents<T, I>[] | undefined,
81
+ options: UseQuerySharedOptions<T, I, R, RT>
61
82
  ) => {
62
- /* ── «Item» is the concrete element type flowing through the hook ── */
83
+ /* ─────── internal type alias for convenience ─────── */
63
84
  type Item = RT;
64
85
 
86
+ /* ────────────── normalise DBs input ────────────── */
87
+ const dbs = useMemo<(Documents<T, I> | undefined)[]>(() => {
88
+ if (Array.isArray(dbOrDbs)) return dbOrDbs;
89
+ if (dbOrDbs) return [dbOrDbs];
90
+ return [];
91
+ }, [dbOrDbs]);
92
+
65
93
  /* ────────────── state & refs ────────────── */
66
94
  const [all, setAll] = useState<Item[]>([]);
67
95
  const allRef = useRef<Item[]>([]);
68
96
  const [isLoading, setIsLoading] = useState(false);
69
- const loadingMoreRef = useRef<boolean>(false);
70
- const iteratorRef = useRef<{
71
- id?: string;
72
- iterator: ResultsIterator<Item>;
73
- itemsConsumed: number;
74
- } | null>(null);
97
+ const iteratorRefs = useRef<
98
+ {
99
+ id: string;
100
+ db: Documents<T, I>;
101
+ iterator: ResultsIterator<Item>;
102
+ itemsConsumed: number;
103
+ }[]
104
+ >([]);
75
105
  const emptyResultsRef = useRef(false);
76
106
  const closeControllerRef = useRef<AbortController | null>(null);
77
107
  const waitedOnceRef = useRef(false);
78
108
 
79
- const [id, setId] = useState<string | undefined>(undefined);
109
+ /* keep an id mostly for debugging mirrors original behaviour */
110
+ const [id, setId] = useState<string | undefined>(options.id);
80
111
 
81
- const reverseRef = useRef(options?.reverse);
112
+ const reverseRef = useRef(options.reverse);
82
113
  useEffect(() => {
83
- reverseRef.current = options?.reverse;
84
- }, [options?.reverse]);
114
+ reverseRef.current = options.reverse;
115
+ }, [options.reverse]);
85
116
 
86
- /* ────────────── util ────────────── */
117
+ /* ────────────── utilities ────────────── */
87
118
  const log = (...a: any[]) => {
88
- if (!options?.debug) return;
119
+ if (!options.debug) return;
89
120
  if (typeof options.debug === "boolean") console.log(...a);
90
121
  else console.log(options.debug.id, ...a);
91
122
  };
@@ -95,311 +126,215 @@ export const useQuery = <
95
126
  setAll(combined);
96
127
  };
97
128
 
98
- const reset = (
99
- fromRef: {
100
- id?: string;
101
- iterator: ResultsIterator<Item>;
102
- } | null
103
- ) => {
104
- const toClose = iteratorRef.current;
105
- if (toClose && fromRef && toClose !== fromRef) {
106
- return;
107
- }
108
-
109
- iteratorRef.current = null;
129
+ const reset = () => {
130
+ iteratorRefs.current.forEach(({ iterator }) => iterator.close());
131
+ iteratorRefs.current = [];
110
132
 
111
133
  closeControllerRef.current?.abort();
112
134
  closeControllerRef.current = new AbortController();
113
135
  emptyResultsRef.current = false;
114
-
115
- toClose?.iterator.close();
136
+ waitedOnceRef.current = false;
116
137
 
117
138
  allRef.current = [];
118
139
  setAll([]);
119
-
120
140
  setIsLoading(false);
121
- loadingMoreRef.current = false;
122
- log("Iterator reset", toClose?.id, fromRef?.id);
123
- setId(undefined);
141
+ log("Iterators reset");
124
142
  };
125
143
 
144
+ /* ────────── rebuild iterators when db list / query etc. change ────────── */
126
145
  useEffect(() => {
127
- waitedOnceRef.current = false;
128
- }, [db, options?.id ?? options?.query, options?.resolve, options?.reverse]);
129
-
130
- /* ────────────── effect: (re)create iterator ────────────── */
131
- useEffect(() => {
132
- if (!db || db.closed || options?.query == null) {
133
- reset(null);
146
+ /* derive canonical list of open DBs */
147
+ const openDbs = dbs.filter((d): d is Documents<T, I> =>
148
+ Boolean(d && !d.closed)
149
+ );
150
+ const { query, resolve } = options;
151
+
152
+ if (!openDbs.length || query == null) {
153
+ reset();
134
154
  return;
135
155
  }
136
156
 
137
- const initIterator = () => {
138
- let id = options?.id ?? uuid();
139
- let remoteQueryOptions =
140
- options.remote == null || options.remote === false
141
- ? false
142
- : {
143
- ...(typeof options.remote === "object"
144
- ? options.remote
145
- : {}),
146
- joining:
147
- typeof options.remote === "object" &&
148
- options.remote.joining?.waitFor !== undefined
149
- ? {
150
- waitFor:
151
- options.remote.joining?.waitFor ??
152
- 5e3,
153
- onMissedResults: ({ amount }) => {
154
- loadMore(amount, true);
155
- },
156
- }
157
- : undefined,
158
- };
159
- const ref = {
160
- id,
161
- iterator: db.index.iterate(options.query ?? {}, {
162
- local: options?.local ?? true,
163
- remote: remoteQueryOptions,
164
- resolve: options?.resolve,
165
- signal: closeControllerRef.current?.signal,
166
- }) as ResultsIterator<Item>,
167
- itemsConsumed: 0,
168
- };
169
- iteratorRef.current = ref;
170
- if (options?.prefetch) {
171
- loadMore();
172
- }
173
- setId(id);
174
-
175
- log("Iterator initialised", ref.id);
176
- return ref;
177
- };
178
-
179
- reset(iteratorRef.current);
180
- const newIteratorRef = initIterator();
181
-
182
- /* live-merge listener (optional) */
183
- let handleChange:
184
- | ((e: CustomEvent<DocumentsChange<T, I>>) => void | Promise<void>)
185
- | undefined;
186
-
187
- if (options?.onChange && options.onChange.merge !== false) {
188
- const mergeFn =
189
- typeof options.onChange.merge === "function"
190
- ? options.onChange.merge
191
- : (c: DocumentsChange<T, I>) => c;
192
-
193
- handleChange = async (e: CustomEvent<DocumentsChange<T, I>>) => {
194
- log("Merge change", e.detail, "iterator", newIteratorRef.id);
195
- const filtered = await mergeFn(e.detail);
196
- if (
197
- !filtered ||
198
- (filtered.added.length === 0 &&
199
- filtered.removed.length === 0)
200
- )
201
- return;
202
-
203
- let merged: Item[];
204
- if (options.onChange?.update) {
205
- merged = [
206
- ...(await options.onChange?.update(
207
- allRef.current,
208
- filtered
209
- )),
210
- ];
211
- } else {
212
- merged = await db.index.updateResults(
213
- allRef.current as WithContext<RT>[],
214
- filtered,
215
- options.query || {},
216
- options.resolve ?? true
217
- );
157
+ reset();
158
+ const abortSignal = closeControllerRef.current?.signal;
218
159
 
219
- log("After update", {
220
- current: allRef.current,
221
- merged,
222
- filtered,
223
- query: options.query,
224
- });
225
- const expectedDiff =
226
- filtered.added.length - filtered.removed.length;
227
-
228
- if (
229
- merged === allRef.current ||
230
- (expectedDiff !== 0 &&
231
- merged.length === allRef.current.length)
232
- ) {
233
- // no change
234
- log("no change after merge");
235
- return;
236
- }
237
- }
238
-
239
- updateAll(options?.reverse ? merged.reverse() : merged);
240
- };
160
+ iteratorRefs.current = openDbs.map((db) => {
161
+ const iterator = db.index.iterate(query ?? {}, {
162
+ local: options.local ?? true,
163
+ remote: options.remote ?? undefined,
164
+ resolve,
165
+ signal: abortSignal,
166
+ }) as ResultsIterator<Item>;
241
167
 
242
- db.events.addEventListener("change", handleChange);
243
- }
168
+ const ref = { id: uuid(), db, iterator, itemsConsumed: 0 };
169
+ log("Iterator init", ref.id, "db", db.address);
170
+ return ref;
171
+ });
244
172
 
245
- return () => {
246
- handleChange &&
247
- db.events.removeEventListener("change", handleChange);
248
- reset(newIteratorRef);
249
- };
173
+ /* prefetch if requested */
174
+ if (options.prefetch) void loadMore();
175
+ /* store a deterministic id (useful for external keys) */
176
+ setId(uuid());
177
+ // eslint-disable-next-line react-hooks/exhaustive-deps
250
178
  }, [
251
- db?.closed ? undefined : db?.address,
252
- options?.id ?? options?.query,
253
- options?.resolve,
254
- options?.reverse,
179
+ dbs.map((d) => d?.address).join("|"),
180
+ options.query,
181
+ options.resolve,
182
+ options.reverse,
255
183
  ]);
256
184
 
257
- /* ────────────── loadMore (once-wait aware) ────────────── */
258
- const batchSize = options?.batchSize ?? 10;
185
+ /* ────────────── loadMore implementation ────────────── */
186
+ const batchSize = options.batchSize ?? 10;
259
187
 
260
188
  const shouldWait = (): boolean => {
261
- if (waitedOnceRef.current) {
262
- return false;
263
- }
264
- if (options?.remote === false) return false;
265
- if (options?.remote === true) return true;
266
- if (options?.remote == null) return true;
267
- if (typeof options?.remote === "object") {
268
- return true;
269
- }
270
- return true;
189
+ if (waitedOnceRef.current) return false;
190
+ if (options.remote === false) return false;
191
+ return true; // mimic original behaviour – wait once if remote allowed
271
192
  };
272
193
 
273
194
  const markWaited = () => {
274
195
  waitedOnceRef.current = true;
275
196
  };
276
197
 
277
- const loadMore = async (
278
- n: number = batchSize,
279
- pollEvenIfWasEmpty = false
280
- ) => {
281
- const iterator = iteratorRef.current;
282
- if (
283
- !iterator ||
284
- (emptyResultsRef.current && !pollEvenIfWasEmpty) ||
285
- iterator.iterator.done() ||
286
- loadingMoreRef.current
287
- ) {
288
- return false;
289
- }
198
+ const loadMore = async (n: number = batchSize): Promise<boolean> => {
199
+ const iterators = iteratorRefs.current;
200
+ if (!iterators.length || emptyResultsRef.current) return false;
290
201
 
291
202
  setIsLoading(true);
292
- loadingMoreRef.current = true;
293
-
294
203
  try {
295
- /* ── optional replicate-wait ── */
204
+ /* one-time replicator warm-up across all DBs */
296
205
  if (shouldWait()) {
297
- log("Wait for replicators", iterator.id);
298
- let t0 = Date.now();
299
-
300
- const warmup =
301
- typeof options?.remote === "object" &&
302
- typeof options?.remote.warmup === "number"
303
- ? options?.remote.warmup
304
- : undefined;
305
-
306
- if (warmup) {
307
- await db?.log
308
- .waitForReplicators({
309
- timeout: warmup,
310
- signal: closeControllerRef.current?.signal,
311
- })
312
- .catch((e) => {
313
- if (
314
- e instanceof AbortError ||
315
- e instanceof NoPeersError
316
- )
317
- return;
318
- console.warn("Remote replicators not ready", e);
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
+ }
319
226
  })
320
- .finally(() => {
321
- log(
322
- "Wait for replicators done",
323
- iterator.id,
324
- "time",
325
- Date.now() - t0
326
- );
327
- markWaited();
328
- });
227
+ );
329
228
  }
330
- } else {
331
- log("Skip wait for replicators", iterator.id);
229
+ markWaited();
332
230
  }
333
231
 
334
- /* ── fetch next batch ── */
335
- log("Retrieve next batch", iterator.id);
336
-
337
- let newItems = await iterator.iterator.next(n);
338
-
339
- if (options?.transform) {
340
- log("Transform start", iterator.id);
341
-
342
- newItems = await Promise.all(newItems.map(options.transform));
343
- log("Transform end", iterator.id);
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);
240
+ }
344
241
  }
345
242
 
346
- /* iterator might have been reset while we were async… */
347
-
348
- if (iteratorRef.current !== iterator) {
349
- log("Iterator reset while loading more");
350
- return false;
243
+ if (!newlyFetched.length) {
244
+ emptyResultsRef.current = iterators.every((i) =>
245
+ i.iterator.done()
246
+ );
247
+ return !emptyResultsRef.current;
351
248
  }
352
249
 
353
- iterator.itemsConsumed += newItems.length;
354
-
355
- emptyResultsRef.current = newItems.length === 0;
356
-
357
- if (newItems.length) {
358
- log(
359
- "Loaded more items for iterator",
360
- iterator.id,
361
- "current id",
362
- iteratorRef.current?.id,
363
- "new items",
364
- newItems.length,
365
- "previous results",
366
- allRef.current.length,
367
- "batchSize",
368
- batchSize,
369
- "items consumed",
370
- iterator.itemsConsumed
371
- );
372
- const prev = allRef.current;
373
- const dedup = new Set(
374
- prev.map((x) => (x as any).__context.head)
375
- );
376
- const unique = newItems.filter(
377
- (x) => !dedup.has((x as any).__context.head)
378
- );
379
- if (!unique.length) return;
380
-
381
- const combined = reverseRef.current
382
- ? [...unique.reverse(), ...prev]
383
- : [...prev, ...unique];
384
- updateAll(combined);
385
- } else {
386
- log("No new items", iterator.id);
250
+ /* optional transform */
251
+ let processed = newlyFetched;
252
+ if (options.transform) {
253
+ processed = await Promise.all(processed.map(options.transform));
387
254
  }
388
- return !iterator.iterator.done();
255
+
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
+
272
+ emptyResultsRef.current = iterators.every((i) => i.iterator.done());
273
+ return !emptyResultsRef.current;
389
274
  } catch (e) {
390
275
  if (!(e instanceof ClosedError)) throw e;
276
+ return false;
391
277
  } finally {
392
278
  setIsLoading(false);
393
- loadingMoreRef.current = false;
394
279
  }
395
280
  };
396
281
 
397
- /* ────────────── public API ────────────── */
282
+ /* ────────────── live-merge listeners ────────────── */
283
+ useEffect(() => {
284
+ if (!options.onChange || options.onChange.merge === false) return;
285
+
286
+ const listeners = iteratorRefs.current.map(({ db, id: itId }) => {
287
+ const mergeFn =
288
+ typeof options.onChange?.merge === "function"
289
+ ? options.onChange.merge
290
+ : (c: DocumentsChange<T, I>) => c;
291
+
292
+ const handler = async (e: CustomEvent<DocumentsChange<T, I>>) => {
293
+ log("Merge change", e.detail, "it", itId);
294
+ const filtered = await mergeFn(e.detail);
295
+ if (
296
+ !filtered ||
297
+ (!filtered.added.length && !filtered.removed.length)
298
+ )
299
+ return;
300
+
301
+ let merged: Item[];
302
+ if (options.onChange?.update) {
303
+ merged = await options.onChange.update(
304
+ allRef.current,
305
+ filtered
306
+ );
307
+ } else {
308
+ merged = await db.index.updateResults(
309
+ allRef.current as WithContext<RT>[],
310
+ filtered,
311
+ options.query || {},
312
+ options.resolve ?? true
313
+ );
314
+ }
315
+ updateAll(options.reverse ? merged.reverse() : merged);
316
+ };
317
+ db.events.addEventListener("change", handler);
318
+ return { db, handler };
319
+ });
320
+
321
+ return () => {
322
+ listeners.forEach(({ db, handler }) =>
323
+ db.events.removeEventListener("change", handler)
324
+ );
325
+ };
326
+ // eslint-disable-next-line react-hooks/exhaustive-deps
327
+ }, [
328
+ iteratorRefs.current.map((r) => r.db.address).join("|"),
329
+ options.onChange,
330
+ ]);
331
+
332
+ /* ────────────── public API – unchanged from the caller's perspective ────────────── */
398
333
  return {
399
334
  items: all,
400
335
  loadMore,
401
336
  isLoading,
402
337
  empty: () => emptyResultsRef.current,
403
- id: id,
338
+ id,
404
339
  };
405
340
  };
package/src/utils.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { serialize, deserialize } from "@dao-xyz/borsh";
2
2
  import { Ed25519Keypair, toBase64, fromBase64 } from "@peerbit/crypto";
3
- import { FastMutex } from "./lockstorage";
3
+ import { FastMutex } from "./lockstorage.js";
4
4
  import { v4 as uuid } from "uuid";
5
5
  import sodium from "libsodium-wrappers";
6
6