@peerbit/react 0.0.20 → 0.0.22

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 } from "react";
1
+ import { useState, useEffect, useRef, useReducer } from "react";
2
2
  import {
3
3
  ClosedError,
4
4
  Documents,
@@ -8,26 +8,19 @@ import {
8
8
  } from "@peerbit/document";
9
9
  import * as indexerTypes from "@peerbit/indexer-interface";
10
10
  import { AbortError } from "@peerbit/time";
11
+ import { NoPeersError } from "@peerbit/shared-log";
12
+ import { v4 as uuid } from "uuid";
11
13
 
14
+ /* ────────────── helper types ────────────── */
12
15
  type QueryLike = {
13
16
  query?: indexerTypes.Query[] | indexerTypes.QueryLike;
14
17
  sort?: indexerTypes.Sort[] | indexerTypes.Sort | indexerTypes.SortLike;
15
18
  };
16
- type QueryOptions = { query: QueryLike; id: string };
19
+ type QueryOptions = { query: QueryLike; id?: string };
17
20
 
18
- const logWithId = (
19
- options: { debug?: boolean | { id: string } } | undefined,
20
- ...args: any[]
21
- ) => {
22
- if (!options?.debug) return;
23
-
24
- if (typeof options.debug === "boolean") {
25
- console.log(...args);
26
- } else if (typeof options.debug.id === "string") {
27
- console.log(options.debug.id, ...args);
28
- }
29
- };
21
+ type WaitForReplicatorsOption = { warmup?: number; eager?: boolean };
30
22
 
23
+ /* ────────────── main hook ────────────── */
31
24
  export const useQuery = <
32
25
  T extends Record<string, any>,
33
26
  I extends Record<string, any>,
@@ -37,167 +30,179 @@ export const useQuery = <
37
30
  db?: Documents<T, I>,
38
31
  options?: {
39
32
  resolve?: R;
40
- waitForReplicators?: boolean | { timeout?: number };
41
- transform?: (result: WithContext<RT>) => Promise<WithContext<RT>>;
33
+ transform?: (r: RT) => Promise<RT>;
42
34
  debounce?: number;
43
35
  debug?: boolean | { id: string };
44
36
  reverse?: boolean;
45
- batchSize?: number; // You can set a default batch size here
37
+ batchSize?: number;
38
+ prefetch?: boolean;
46
39
  onChange?: {
47
40
  merge?:
48
41
  | boolean
49
42
  | ((
50
- change: DocumentsChange<T>
43
+ c: DocumentsChange<T>
51
44
  ) =>
52
45
  | DocumentsChange<T>
53
46
  | Promise<DocumentsChange<T>>
54
- | undefined); // if true, the iterator will be updated with new documents
55
- update?: (
56
- prev: WithContext<RT>[],
57
- change: DocumentsChange<T>
58
- ) => WithContext<RT>[];
47
+ | undefined);
48
+ update?: (prev: RT[], change: DocumentsChange<T>) => RT[];
59
49
  };
60
- local?: boolean; // if true, (default is true) the iterator will only return local documents
61
- remote?:
62
- | boolean
63
- | {
64
- eager?: boolean;
65
- };
50
+ local?: boolean;
51
+ remote?: boolean | WaitForReplicatorsOption;
66
52
  } & QueryOptions
67
53
  ) => {
68
- const [all, setAll] = useState<WithContext<RT>[]>([]);
69
- const allRef = useRef<WithContext<RT>[]>([]);
54
+ /* ── «Item» is the concrete element type flowing through the hook ── */
55
+ type Item = RT;
56
+
57
+ /* ────────────── state & refs ────────────── */
58
+ const [all, setAll] = useState<Item[]>([]);
59
+ const allRef = useRef<Item[]>([]);
70
60
  const [isLoading, setIsLoading] = useState(false);
71
- const loadingMoreRef = useRef(false);
61
+ const loadingMoreRef = useRef<boolean>(false);
72
62
  const iteratorRef = useRef<{
73
63
  id?: string;
74
- iterator: ResultsIterator<WithContext<RT>>;
64
+ iterator: ResultsIterator<Item>;
65
+ itemsConsumed: number;
75
66
  } | null>(null);
76
67
  const emptyResultsRef = useRef(false);
77
68
  const closeControllerRef = useRef<AbortController | null>(null);
69
+ const waitedOnceRef = useRef(false);
70
+ const resetResultsOnReset = useRef(true);
78
71
 
79
- const updateAll = (
80
- combined: WithContext<RT>[],
81
- fromChange?: DocumentsChange<any> | null
82
- ) => {
83
- logWithId(
84
- options,
85
- "Loading more items, new combined length",
86
- combined.length,
87
- "from change",
88
- fromChange
89
- );
72
+ const [id, setId] = useState<string | undefined>(undefined);
73
+ const [resetCounter, invokeReset] = useReducer((n) => n + 1, 0);
90
74
 
91
- allRef.current = combined;
75
+ const reverseRef = useRef(options?.reverse);
76
+ useEffect(() => {
77
+ reverseRef.current = options?.reverse;
78
+ }, [options?.reverse]);
79
+
80
+ /* ────────────── util ────────────── */
81
+ const log = (...a: any[]) => {
82
+ if (!options?.debug) return;
83
+ if (typeof options.debug === "boolean") console.log(...a);
84
+ else console.log(options.debug.id, ...a);
85
+ };
92
86
 
87
+ const updateAll = (combined: Item[]) => {
88
+ allRef.current = combined;
93
89
  setAll(combined);
94
90
  };
95
91
 
96
92
  const reset = (
97
93
  fromRef: {
98
94
  id?: string;
99
- iterator: ResultsIterator<WithContext<RT>>;
95
+ iterator: ResultsIterator<Item>;
100
96
  } | null
101
97
  ) => {
102
- if (iteratorRef.current != null && iteratorRef.current !== fromRef) {
98
+ const toClose = iteratorRef.current;
99
+ if (toClose && fromRef && toClose !== fromRef) {
103
100
  return;
104
101
  }
102
+
103
+ iteratorRef.current = null;
104
+
105
105
  closeControllerRef.current?.abort();
106
106
  closeControllerRef.current = new AbortController();
107
-
108
107
  emptyResultsRef.current = false;
109
- logWithId(options, "reset", {
110
- id: iteratorRef.current?.id,
111
- size: allRef.current.length,
112
- });
113
108
 
114
- !iteratorRef.current?.iterator.done() &&
115
- iteratorRef.current?.iterator?.close();
116
- iteratorRef.current = null;
117
- setAll([]);
109
+ toClose?.iterator.close();
110
+
111
+ if (resetResultsOnReset.current) {
112
+ allRef.current = [];
113
+ setAll([]);
114
+ }
115
+
118
116
  setIsLoading(false);
119
117
  loadingMoreRef.current = false;
120
- allRef.current = [];
118
+ log(options, "Iterator reset", toClose?.id, fromRef?.id);
119
+ setId(undefined);
121
120
  };
122
121
 
123
- // Initialize the iterator only once or when query changes
124
122
  useEffect(() => {
125
- if (!db || db.closed || options?.query === null) {
123
+ resetResultsOnReset.current = true;
124
+ waitedOnceRef.current = false;
125
+ }, [db, options?.id ?? options?.query, options?.resolve, options?.reverse]);
126
+
127
+ /* ────────────── effect: (re)create iterator ────────────── */
128
+ useEffect(() => {
129
+ if (!db || db.closed || options?.query == null) {
126
130
  reset(null);
127
131
  return;
128
132
  }
129
133
 
130
134
  const initIterator = () => {
131
- // Don't make this async, it will cause issues with the iterator refs
132
- try {
133
- // Initialize the iterator and load initial batch.
134
-
135
- const ref = {
136
- id: options?.id,
137
- iterator: db.index.iterate(options?.query ?? {}, {
138
- local: options?.local ?? true,
139
- remote: options?.remote ?? true,
140
- resolve: options?.resolve as any,
141
- }) as any as ResultsIterator<WithContext<RT>>,
142
- };
143
- iteratorRef.current = ref;
144
-
145
- logWithId(options, "Initializing iterator", {
146
- id: options?.id,
147
- options: options,
148
- });
149
-
150
- loadMore(); // initial load
151
- return ref;
152
- } catch (error) {
153
- console.error("Error initializing iterator", error);
154
- return null;
135
+ let id = options?.id ?? uuid();
136
+ const ref = {
137
+ id,
138
+ iterator: db.index.iterate(options.query ?? {}, {
139
+ local: options?.local ?? true,
140
+ remote:
141
+ options.remote == null || options.remote === false
142
+ ? false
143
+ : typeof options.remote === "object"
144
+ ? options.remote
145
+ : true,
146
+ resolve: options?.resolve,
147
+ }) as ResultsIterator<Item>,
148
+ itemsConsumed: 0,
149
+ };
150
+ iteratorRef.current = ref;
151
+ if (options?.prefetch) {
152
+ loadMore();
155
153
  }
154
+ setId(id);
155
+
156
+ log("Iterator initialised", ref.id);
157
+ return ref;
156
158
  };
157
159
 
158
- // Reset state when the db or query changes.
159
160
  reset(iteratorRef.current);
160
-
161
161
  const newIteratorRef = initIterator();
162
162
 
163
+ /* live-merge listener (optional) */
163
164
  let handleChange:
164
- | undefined
165
- | ((e: CustomEvent<DocumentsChange<T>>) => void | Promise<void>) =
166
- undefined;
167
- if (options?.onChange && options?.onChange?.merge !== false) {
168
- let mergeFunction =
165
+ | ((e: CustomEvent<DocumentsChange<T>>) => void | Promise<void>)
166
+ | undefined;
167
+
168
+ if (options?.onChange && options.onChange.merge !== false) {
169
+ const mergeFn =
169
170
  typeof options.onChange.merge === "function"
170
171
  ? options.onChange.merge
171
- : (change: DocumentsChange<T>) => change;
172
+ : (c: DocumentsChange<T>) => c;
173
+
172
174
  handleChange = async (e: CustomEvent<DocumentsChange<T>>) => {
173
- // while we are iterating, we might get new documents.. so this method inserts them where they should be
174
- let filteredChange = await mergeFunction(e.detail);
175
+ log(
176
+ options,
177
+ "Merge change",
178
+ e.detail,
179
+ "iterator",
180
+ newIteratorRef.id
181
+ );
182
+ const filtered = await mergeFn(e.detail);
175
183
  if (
176
- !filteredChange ||
177
- (filteredChange.added.length === 0 &&
178
- filteredChange.removed.length === 0)
179
- ) {
184
+ !filtered ||
185
+ (filtered.added.length === 0 &&
186
+ filtered.removed.length === 0)
187
+ )
180
188
  return;
181
- }
182
- let merged: WithContext<RT>[] = [];
189
+
190
+ let merged: Item[];
183
191
  if (options.onChange?.update) {
184
192
  merged = [
185
- ...options.onChange.update(
186
- allRef.current,
187
- filteredChange
188
- ),
193
+ ...options.onChange?.update(allRef.current, filtered),
189
194
  ];
190
195
  } else {
191
196
  merged = await db.index.updateResults(
192
- allRef.current,
193
- filteredChange,
194
- options?.query || {},
195
- options?.resolve ?? true
197
+ allRef.current as WithContext<RT>[],
198
+ filtered,
199
+ options.query || {},
200
+ options.resolve ?? true
196
201
  );
197
- logWithId(options, "After update", allRef.current, merged);
202
+
203
+ log(options, "After update", allRef.current, merged);
198
204
  const expectedDiff =
199
- filteredChange.added.length -
200
- filteredChange.removed.length;
205
+ filtered.added.length - filtered.removed.length;
201
206
 
202
207
  if (
203
208
  merged === allRef.current ||
@@ -205,23 +210,14 @@ export const useQuery = <
205
210
  merged.length === allRef.current.length)
206
211
  ) {
207
212
  // no change
208
- logWithId(options, "no change after merge");
213
+ log(options, "no change after merge");
209
214
  return;
210
215
  }
211
216
  }
212
217
 
213
- logWithId(options, "handleChange", {
214
- added: e.detail.added.length,
215
- removed: e.detail.removed.length,
216
- merged: merged.length,
217
- allRef: allRef.current.length,
218
- });
219
-
220
- updateAll(
221
- options?.reverse ? merged.reverse() : merged,
222
- e.detail
223
- );
218
+ updateAll(options?.reverse ? merged.reverse() : merged);
224
219
  };
220
+
225
221
  db.events.addEventListener("change", handleChange);
226
222
  }
227
223
 
@@ -232,137 +228,162 @@ export const useQuery = <
232
228
  };
233
229
  }, [
234
230
  db?.closed ? undefined : db?.address,
235
- options?.id != null ? options?.id : options?.query,
231
+ options?.id ?? options?.query,
236
232
  options?.resolve,
233
+ options?.reverse,
234
+ resetCounter,
237
235
  ]);
238
236
 
239
- // Define the loadMore function
237
+ /* ────────────── loadMore (once-wait aware) ────────────── */
240
238
  const batchSize = options?.batchSize ?? 10;
239
+
240
+ const shouldWait = (): boolean => {
241
+ if (waitedOnceRef.current) {
242
+ return false;
243
+ }
244
+ if (options?.remote === false) return false;
245
+ if (options?.remote === true) return true;
246
+ if (options?.remote == null) return true;
247
+ if (typeof options?.remote === "object") {
248
+ return true;
249
+ }
250
+ return true;
251
+ };
252
+
253
+ const reloadAfterTime = (): number | undefined => {
254
+ if (typeof options?.remote === "object" && options.remote.eager) {
255
+ return options.remote.warmup ?? db?.log.timeUntilRoleMaturity;
256
+ }
257
+ return undefined;
258
+ };
259
+
260
+ const markWaited = () => {
261
+ waitedOnceRef.current = true;
262
+ };
263
+
241
264
  const loadMore = async () => {
265
+ const iterator = iteratorRef.current;
242
266
  if (
243
- !iteratorRef.current ||
267
+ !iterator ||
244
268
  emptyResultsRef.current ||
245
- iteratorRef.current.iterator.done() ||
269
+ iterator.iterator.done() ||
246
270
  loadingMoreRef.current
247
271
  ) {
248
- logWithId(options, "loadMore: already loading or no more items", {
249
- isLoading,
250
- emptyResultsRef: emptyResultsRef.current,
251
- iteratorRef: !iteratorRef.current,
252
- });
253
- return;
272
+ return false;
254
273
  }
255
- const iterator = iteratorRef.current;
256
274
 
257
275
  setIsLoading(true);
258
276
  loadingMoreRef.current = true;
277
+
259
278
  try {
260
- // Fetch next batchSize number of items:
261
- logWithId(
262
- options,
263
- "wait for replicators for iterator " + iterator.id
264
- );
265
- if (options?.waitForReplicators !== false) {
266
- let timeout = 5e3;
267
- if (typeof options?.waitForReplicators === "object") {
268
- timeout = options.waitForReplicators.timeout ?? 1e4;
269
- }
270
- await db?.log
279
+ /* ── optional replicate-wait ── */
280
+ if (shouldWait()) {
281
+ log(options, "Wait for replicators", iterator.id);
282
+
283
+ let isEager =
284
+ typeof options?.remote === "object" && options.remote.eager;
285
+ const waitTimeout =
286
+ typeof options?.remote === "object" &&
287
+ typeof options?.remote.warmup === "number"
288
+ ? options?.remote.warmup
289
+ : 5_000;
290
+
291
+ let shouldResetAfterMaturity =
292
+ isEager && !waitedOnceRef.current;
293
+ let promise = db?.log
271
294
  .waitForReplicators({
272
- timeout,
295
+ timeout: waitTimeout,
273
296
  signal: closeControllerRef.current?.signal,
274
297
  })
275
298
  .catch((e) => {
276
- if (e instanceof AbortError) {
277
- // Ignore abort error
299
+ if (
300
+ e instanceof AbortError ||
301
+ e instanceof NoPeersError
302
+ )
278
303
  return;
279
- }
280
304
  console.warn("Remote replicators not ready", e);
305
+ })
306
+ .finally(() => {
307
+ markWaited();
308
+ if (shouldResetAfterMaturity) {
309
+ resetResultsOnReset.current = false; // don't reset results, because we expect to get same or more results
310
+ invokeReset();
311
+ }
281
312
  });
313
+ if (!shouldResetAfterMaturity) {
314
+ await promise;
315
+ }
316
+ } else {
317
+ log(options, "Skip wait for replicators", iterator.id);
282
318
  }
283
319
 
284
- logWithId(
285
- options,
286
- "loadMore: loading more items for iterator " +
287
- iteratorRef.current?.id,
288
- "should resolve?: " + options?.resolve,
289
- "query local?: " + options?.local,
290
- "query remote?: " + options?.remote,
291
- "isReplicating: " + (await db?.log.isReplicating())
292
- );
293
-
294
- let newItems: WithContext<RT>[] = await iterator.iterator.next(
295
- batchSize
296
- );
297
-
320
+ /* ── fetch next batch ── */
321
+ log(options, "Retrieve next batch", iterator.id);
322
+ let newItems = await iterator.iterator.next(batchSize);
298
323
  if (options?.transform) {
299
- newItems = await Promise.all(
300
- newItems.map((item) => options.transform!(item))
301
- );
324
+ log(options, "Transform start", iterator.id);
325
+
326
+ newItems = await Promise.all(newItems.map(options.transform));
327
+ log(options, "Transform end", iterator.id);
302
328
  }
303
329
 
330
+ /* iterator might have been reset while we were async… */
331
+
304
332
  if (iteratorRef.current !== iterator) {
305
- // If the iterator has changed, we should not update the state
306
- // This can happen if the iterator was closed and a new one was created
307
- logWithId(options, "Iterator ref changed, not updating state", {
308
- refBefore: iterator.id,
309
- currentRef: iteratorRef.current?.id,
310
- ignoredItems: newItems.length,
311
- });
312
- return;
333
+ log(options, "Iterator reset while loading more");
334
+ return false;
313
335
  }
314
336
 
315
- logWithId(
316
- options,
317
- "loadMore: loaded more items for iterator " +
318
- iteratorRef.current?.id,
319
- "new items length",
320
- newItems.length,
321
- "all items length",
322
- allRef.current.length
323
- );
337
+ iterator.itemsConsumed += newItems.length;
324
338
 
325
339
  emptyResultsRef.current = newItems.length === 0;
326
340
 
327
- if (newItems.length > 0) {
328
- let prev = allRef.current;
329
- let prevHash = new Set(prev.map((x) => x.__context.head));
330
- let newItemsNoHash = newItems.filter(
331
- (x) => !prevHash.has(x.__context.head)
341
+ if (newItems.length) {
342
+ log(
343
+ "Loaded more items for iterator",
344
+ iterator.id,
345
+ "current id",
346
+ iteratorRef.current?.id,
347
+ "new items",
348
+ newItems.length,
349
+ "previous results",
350
+ allRef.current.length,
351
+ "batchSize",
352
+ batchSize,
353
+ "items consumed",
354
+ iterator.itemsConsumed
332
355
  );
333
- if (newItemsNoHash.length === 0) {
334
- logWithId(
335
- options,
336
- "no new items after dedup, not updating state. Prev length",
337
- prev.length
338
- );
339
- return;
340
- }
341
- const combined = options?.reverse
342
- ? [...newItemsNoHash.reverse(), ...prev]
343
- : [...prev, ...newItemsNoHash];
344
- updateAll(combined, null);
345
- } else {
346
- logWithId(
347
- options,
348
- "no new items, not updating state for iterator " +
349
- iteratorRef.current?.id +
350
- " existing results length",
351
- allRef.current.length
356
+ const prev = allRef.current;
357
+ const dedup = new Set(
358
+ prev.map((x) => (x as any).__context.head)
352
359
  );
353
- }
354
- } catch (error) {
355
- if (error instanceof ClosedError) {
356
- // Handle closed database gracefully
357
- logWithId(options, "Database closed error");
360
+ const unique = newItems.filter(
361
+ (x) => !dedup.has((x as any).__context.head)
362
+ );
363
+ if (!unique.length) return;
364
+
365
+ const combined = reverseRef.current
366
+ ? [...unique.reverse(), ...prev]
367
+ : [...prev, ...unique];
368
+ updateAll(combined);
358
369
  } else {
359
- throw error;
370
+ log(options, "No new items", iterator.id);
360
371
  }
372
+ return !iterator.iterator.done();
373
+ } catch (e) {
374
+ if (!(e instanceof ClosedError)) throw e;
361
375
  } finally {
362
376
  setIsLoading(false);
363
377
  loadingMoreRef.current = false;
364
378
  }
365
379
  };
366
380
 
367
- return { items: all, loadMore, isLoading, empty: emptyResultsRef.current };
381
+ /* ────────────── public API ────────────── */
382
+ return {
383
+ items: all,
384
+ loadMore,
385
+ isLoading,
386
+ empty: () => emptyResultsRef.current,
387
+ id: id,
388
+ };
368
389
  };