@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/lib/esm/__tests__/utils.test.js +2 -2
- package/lib/esm/__tests__/utils.test.js.map +1 -1
- package/lib/esm/index.d.ts +0 -1
- package/lib/esm/index.js +0 -1
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/useCount.js +1 -1
- package/lib/esm/useCount.js.map +1 -1
- package/lib/esm/useLocal.js +1 -1
- package/lib/esm/useLocal.js.map +1 -1
- package/lib/esm/useProgram.d.ts +2 -2
- package/lib/esm/useProgram.js +10 -7
- package/lib/esm/useProgram.js.map +1 -1
- package/lib/esm/useQuery.d.ts +33 -15
- package/lib/esm/useQuery.js +156 -192
- package/lib/esm/useQuery.js.map +1 -1
- package/lib/esm/utils.d.ts +1 -1
- package/package.json +3 -2
- package/src/__tests__/utils.test.ts +2 -2
- package/src/index.ts +0 -1
- package/src/useCount.tsx +1 -1
- package/src/useLocal.tsx +1 -1
- package/src/useProgram.tsx +24 -16
- package/src/useQuery.tsx +240 -305
- package/src/utils.ts +1 -1
package/src/useQuery.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useRef,
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
/*
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
112
|
+
const reverseRef = useRef(options.reverse);
|
|
82
113
|
useEffect(() => {
|
|
83
|
-
reverseRef.current = options
|
|
84
|
-
}, [options
|
|
114
|
+
reverseRef.current = options.reverse;
|
|
115
|
+
}, [options.reverse]);
|
|
85
116
|
|
|
86
|
-
/* ──────────────
|
|
117
|
+
/* ────────────── utilities ────────────── */
|
|
87
118
|
const log = (...a: any[]) => {
|
|
88
|
-
if (!options
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
252
|
-
options
|
|
253
|
-
options
|
|
254
|
-
options
|
|
179
|
+
dbs.map((d) => d?.address).join("|"),
|
|
180
|
+
options.query,
|
|
181
|
+
options.resolve,
|
|
182
|
+
options.reverse,
|
|
255
183
|
]);
|
|
256
184
|
|
|
257
|
-
/* ────────────── loadMore
|
|
258
|
-
const batchSize = options
|
|
185
|
+
/* ────────────── loadMore implementation ────────────── */
|
|
186
|
+
const batchSize = options.batchSize ?? 10;
|
|
259
187
|
|
|
260
188
|
const shouldWait = (): boolean => {
|
|
261
|
-
if (waitedOnceRef.current)
|
|
262
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
/*
|
|
204
|
+
/* one-time replicator warm-up across all DBs */
|
|
296
205
|
if (shouldWait()) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
321
|
-
log(
|
|
322
|
-
"Wait for replicators done",
|
|
323
|
-
iterator.id,
|
|
324
|
-
"time",
|
|
325
|
-
Date.now() - t0
|
|
326
|
-
);
|
|
327
|
-
markWaited();
|
|
328
|
-
});
|
|
227
|
+
);
|
|
329
228
|
}
|
|
330
|
-
|
|
331
|
-
log("Skip wait for replicators", iterator.id);
|
|
229
|
+
markWaited();
|
|
332
230
|
}
|
|
333
231
|
|
|
334
|
-
/*
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return
|
|
243
|
+
if (!newlyFetched.length) {
|
|
244
|
+
emptyResultsRef.current = iterators.every((i) =>
|
|
245
|
+
i.iterator.done()
|
|
246
|
+
);
|
|
247
|
+
return !emptyResultsRef.current;
|
|
351
248
|
}
|
|
352
249
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
/* ──────────────
|
|
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
|
|
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
|
|