@peerbit/react 0.0.25 → 0.0.27
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/usePeer.d.ts +1 -1
- package/lib/esm/usePeer.js.map +1 -1
- package/lib/esm/useProgram.d.ts +1 -1
- package/lib/esm/useProgram.js +5 -5
- package/lib/esm/useProgram.js.map +1 -1
- package/lib/esm/useQuery.d.ts +34 -16
- package/lib/esm/useQuery.js +142 -189
- package/lib/esm/useQuery.js.map +1 -1
- package/package.json +8 -8
- package/src/usePeer.tsx +1 -4
- package/src/useProgram.tsx +6 -5
- package/src/useQuery.tsx +240 -294
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,79 +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?: (prev: RT[], change: DocumentsChange<T, I>) => RT[];
|
|
54
|
-
};
|
|
55
|
-
local?: boolean;
|
|
56
|
-
remote?: boolean | RemoteQueryOptions;
|
|
57
|
-
} & 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>
|
|
58
82
|
) => {
|
|
59
|
-
/*
|
|
83
|
+
/* ─────── internal type alias for convenience ─────── */
|
|
60
84
|
type Item = RT;
|
|
61
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
|
+
|
|
62
93
|
/* ────────────── state & refs ────────────── */
|
|
63
94
|
const [all, setAll] = useState<Item[]>([]);
|
|
64
95
|
const allRef = useRef<Item[]>([]);
|
|
65
96
|
const [isLoading, setIsLoading] = useState(false);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
const iteratorRefs = useRef<
|
|
98
|
+
{
|
|
99
|
+
id: string;
|
|
100
|
+
db: Documents<T, I>;
|
|
101
|
+
iterator: ResultsIterator<Item>;
|
|
102
|
+
itemsConsumed: number;
|
|
103
|
+
}[]
|
|
104
|
+
>([]);
|
|
72
105
|
const emptyResultsRef = useRef(false);
|
|
73
106
|
const closeControllerRef = useRef<AbortController | null>(null);
|
|
74
107
|
const waitedOnceRef = useRef(false);
|
|
75
108
|
|
|
76
|
-
|
|
109
|
+
/* keep an id mostly for debugging – mirrors original behaviour */
|
|
110
|
+
const [id, setId] = useState<string | undefined>(options.id);
|
|
77
111
|
|
|
78
|
-
const reverseRef = useRef(options
|
|
112
|
+
const reverseRef = useRef(options.reverse);
|
|
79
113
|
useEffect(() => {
|
|
80
|
-
reverseRef.current = options
|
|
81
|
-
}, [options
|
|
114
|
+
reverseRef.current = options.reverse;
|
|
115
|
+
}, [options.reverse]);
|
|
82
116
|
|
|
83
|
-
/* ──────────────
|
|
117
|
+
/* ────────────── utilities ────────────── */
|
|
84
118
|
const log = (...a: any[]) => {
|
|
85
|
-
if (!options
|
|
119
|
+
if (!options.debug) return;
|
|
86
120
|
if (typeof options.debug === "boolean") console.log(...a);
|
|
87
121
|
else console.log(options.debug.id, ...a);
|
|
88
122
|
};
|
|
@@ -92,303 +126,215 @@ export const useQuery = <
|
|
|
92
126
|
setAll(combined);
|
|
93
127
|
};
|
|
94
128
|
|
|
95
|
-
const reset = (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
iterator: ResultsIterator<Item>;
|
|
99
|
-
} | null
|
|
100
|
-
) => {
|
|
101
|
-
const toClose = iteratorRef.current;
|
|
102
|
-
if (toClose && fromRef && toClose !== fromRef) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
iteratorRef.current = null;
|
|
129
|
+
const reset = () => {
|
|
130
|
+
iteratorRefs.current.forEach(({ iterator }) => iterator.close());
|
|
131
|
+
iteratorRefs.current = [];
|
|
107
132
|
|
|
108
133
|
closeControllerRef.current?.abort();
|
|
109
134
|
closeControllerRef.current = new AbortController();
|
|
110
135
|
emptyResultsRef.current = false;
|
|
111
|
-
|
|
112
|
-
toClose?.iterator.close();
|
|
136
|
+
waitedOnceRef.current = false;
|
|
113
137
|
|
|
114
138
|
allRef.current = [];
|
|
115
139
|
setAll([]);
|
|
116
|
-
|
|
117
140
|
setIsLoading(false);
|
|
118
|
-
|
|
119
|
-
log("Iterator reset", toClose?.id, fromRef?.id);
|
|
120
|
-
setId(undefined);
|
|
141
|
+
log("Iterators reset");
|
|
121
142
|
};
|
|
122
143
|
|
|
144
|
+
/* ────────── rebuild iterators when db list / query etc. change ────────── */
|
|
123
145
|
useEffect(() => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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();
|
|
131
154
|
return;
|
|
132
155
|
}
|
|
133
156
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
let remoteQueryOptions =
|
|
137
|
-
options.remote == null || options.remote === false
|
|
138
|
-
? false
|
|
139
|
-
: {
|
|
140
|
-
...(typeof options.remote === "object"
|
|
141
|
-
? options.remote
|
|
142
|
-
: {}),
|
|
143
|
-
joining:
|
|
144
|
-
typeof options.remote === "object" &&
|
|
145
|
-
options.remote.joining?.waitFor !== undefined
|
|
146
|
-
? {
|
|
147
|
-
waitFor:
|
|
148
|
-
options.remote.joining?.waitFor ??
|
|
149
|
-
5e3,
|
|
150
|
-
onMissedResults: ({ amount }) => {
|
|
151
|
-
loadMore(amount, true);
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
: undefined,
|
|
155
|
-
};
|
|
156
|
-
const ref = {
|
|
157
|
-
id,
|
|
158
|
-
iterator: db.index.iterate(options.query ?? {}, {
|
|
159
|
-
local: options?.local ?? true,
|
|
160
|
-
remote: remoteQueryOptions,
|
|
161
|
-
resolve: options?.resolve,
|
|
162
|
-
signal: closeControllerRef.current?.signal,
|
|
163
|
-
}) as ResultsIterator<Item>,
|
|
164
|
-
itemsConsumed: 0,
|
|
165
|
-
};
|
|
166
|
-
iteratorRef.current = ref;
|
|
167
|
-
if (options?.prefetch) {
|
|
168
|
-
loadMore();
|
|
169
|
-
}
|
|
170
|
-
setId(id);
|
|
157
|
+
reset();
|
|
158
|
+
const abortSignal = closeControllerRef.current?.signal;
|
|
171
159
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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>;
|
|
178
167
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (options?.onChange && options.onChange.merge !== false) {
|
|
185
|
-
const mergeFn =
|
|
186
|
-
typeof options.onChange.merge === "function"
|
|
187
|
-
? options.onChange.merge
|
|
188
|
-
: (c: DocumentsChange<T, I>) => c;
|
|
189
|
-
|
|
190
|
-
handleChange = async (e: CustomEvent<DocumentsChange<T, I>>) => {
|
|
191
|
-
log("Merge change", e.detail, "iterator", newIteratorRef.id);
|
|
192
|
-
const filtered = await mergeFn(e.detail);
|
|
193
|
-
if (
|
|
194
|
-
!filtered ||
|
|
195
|
-
(filtered.added.length === 0 &&
|
|
196
|
-
filtered.removed.length === 0)
|
|
197
|
-
)
|
|
198
|
-
return;
|
|
199
|
-
|
|
200
|
-
let merged: Item[];
|
|
201
|
-
if (options.onChange?.update) {
|
|
202
|
-
merged = [
|
|
203
|
-
...options.onChange?.update(allRef.current, filtered),
|
|
204
|
-
];
|
|
205
|
-
} else {
|
|
206
|
-
merged = await db.index.updateResults(
|
|
207
|
-
allRef.current as WithContext<RT>[],
|
|
208
|
-
filtered,
|
|
209
|
-
options.query || {},
|
|
210
|
-
options.resolve ?? true
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
log("After update", allRef.current, merged);
|
|
214
|
-
const expectedDiff =
|
|
215
|
-
filtered.added.length - filtered.removed.length;
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
merged === allRef.current ||
|
|
219
|
-
(expectedDiff !== 0 &&
|
|
220
|
-
merged.length === allRef.current.length)
|
|
221
|
-
) {
|
|
222
|
-
// no change
|
|
223
|
-
log("no change after merge");
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
updateAll(options?.reverse ? merged.reverse() : merged);
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
db.events.addEventListener("change", handleChange);
|
|
232
|
-
}
|
|
168
|
+
const ref = { id: uuid(), db, iterator, itemsConsumed: 0 };
|
|
169
|
+
log("Iterator init", ref.id, "db", db.address);
|
|
170
|
+
return ref;
|
|
171
|
+
});
|
|
233
172
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
239
178
|
}, [
|
|
240
|
-
|
|
241
|
-
options
|
|
242
|
-
options
|
|
243
|
-
options
|
|
179
|
+
dbs.map((d) => d?.address).join("|"),
|
|
180
|
+
options.query,
|
|
181
|
+
options.resolve,
|
|
182
|
+
options.reverse,
|
|
244
183
|
]);
|
|
245
184
|
|
|
246
|
-
/* ────────────── loadMore
|
|
247
|
-
const batchSize = options
|
|
185
|
+
/* ────────────── loadMore implementation ────────────── */
|
|
186
|
+
const batchSize = options.batchSize ?? 10;
|
|
248
187
|
|
|
249
188
|
const shouldWait = (): boolean => {
|
|
250
|
-
if (waitedOnceRef.current)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (options?.remote === false) return false;
|
|
254
|
-
if (options?.remote === true) return true;
|
|
255
|
-
if (options?.remote == null) return true;
|
|
256
|
-
if (typeof options?.remote === "object") {
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
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
|
|
260
192
|
};
|
|
261
193
|
|
|
262
194
|
const markWaited = () => {
|
|
263
195
|
waitedOnceRef.current = true;
|
|
264
196
|
};
|
|
265
197
|
|
|
266
|
-
const loadMore = async (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
) => {
|
|
270
|
-
const iterator = iteratorRef.current;
|
|
271
|
-
if (
|
|
272
|
-
!iterator ||
|
|
273
|
-
(emptyResultsRef.current && !pollEvenIfWasEmpty) ||
|
|
274
|
-
iterator.iterator.done() ||
|
|
275
|
-
loadingMoreRef.current
|
|
276
|
-
) {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
198
|
+
const loadMore = async (n: number = batchSize): Promise<boolean> => {
|
|
199
|
+
const iterators = iteratorRefs.current;
|
|
200
|
+
if (!iterators.length || emptyResultsRef.current) return false;
|
|
279
201
|
|
|
280
202
|
setIsLoading(true);
|
|
281
|
-
loadingMoreRef.current = true;
|
|
282
|
-
|
|
283
203
|
try {
|
|
284
|
-
/*
|
|
204
|
+
/* one-time replicator warm-up across all DBs */
|
|
285
205
|
if (shouldWait()) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return;
|
|
307
|
-
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
|
+
}
|
|
308
226
|
})
|
|
309
|
-
|
|
310
|
-
log(
|
|
311
|
-
"Wait for replicators done",
|
|
312
|
-
iterator.id,
|
|
313
|
-
"time",
|
|
314
|
-
Date.now() - t0
|
|
315
|
-
);
|
|
316
|
-
markWaited();
|
|
317
|
-
});
|
|
227
|
+
);
|
|
318
228
|
}
|
|
319
|
-
|
|
320
|
-
log("Skip wait for replicators", iterator.id);
|
|
229
|
+
markWaited();
|
|
321
230
|
}
|
|
322
231
|
|
|
323
|
-
/*
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
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
|
+
}
|
|
333
241
|
}
|
|
334
242
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return
|
|
243
|
+
if (!newlyFetched.length) {
|
|
244
|
+
emptyResultsRef.current = iterators.every((i) =>
|
|
245
|
+
i.iterator.done()
|
|
246
|
+
);
|
|
247
|
+
return !emptyResultsRef.current;
|
|
340
248
|
}
|
|
341
249
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (newItems.length) {
|
|
347
|
-
log(
|
|
348
|
-
"Loaded more items for iterator",
|
|
349
|
-
iterator.id,
|
|
350
|
-
"current id",
|
|
351
|
-
iteratorRef.current?.id,
|
|
352
|
-
"new items",
|
|
353
|
-
newItems.length,
|
|
354
|
-
"previous results",
|
|
355
|
-
allRef.current.length,
|
|
356
|
-
"batchSize",
|
|
357
|
-
batchSize,
|
|
358
|
-
"items consumed",
|
|
359
|
-
iterator.itemsConsumed
|
|
360
|
-
);
|
|
361
|
-
const prev = allRef.current;
|
|
362
|
-
const dedup = new Set(
|
|
363
|
-
prev.map((x) => (x as any).__context.head)
|
|
364
|
-
);
|
|
365
|
-
const unique = newItems.filter(
|
|
366
|
-
(x) => !dedup.has((x as any).__context.head)
|
|
367
|
-
);
|
|
368
|
-
if (!unique.length) return;
|
|
369
|
-
|
|
370
|
-
const combined = reverseRef.current
|
|
371
|
-
? [...unique.reverse(), ...prev]
|
|
372
|
-
: [...prev, ...unique];
|
|
373
|
-
updateAll(combined);
|
|
374
|
-
} else {
|
|
375
|
-
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));
|
|
376
254
|
}
|
|
377
|
-
|
|
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;
|
|
378
274
|
} catch (e) {
|
|
379
275
|
if (!(e instanceof ClosedError)) throw e;
|
|
276
|
+
return false;
|
|
380
277
|
} finally {
|
|
381
278
|
setIsLoading(false);
|
|
382
|
-
loadingMoreRef.current = false;
|
|
383
279
|
}
|
|
384
280
|
};
|
|
385
281
|
|
|
386
|
-
/* ──────────────
|
|
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 ────────────── */
|
|
387
333
|
return {
|
|
388
334
|
items: all,
|
|
389
335
|
loadMore,
|
|
390
336
|
isLoading,
|
|
391
337
|
empty: () => emptyResultsRef.current,
|
|
392
|
-
id
|
|
338
|
+
id,
|
|
393
339
|
};
|
|
394
340
|
};
|