@peerbit/document 6.0.6 → 6.0.7-218a5bb
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/README.md +2 -2
- package/dist/benchmark/index.d.ts +2 -0
- package/dist/benchmark/index.d.ts.map +1 -0
- package/dist/benchmark/index.js +126 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/replication.d.ts +2 -0
- package/dist/benchmark/replication.d.ts.map +1 -0
- package/dist/benchmark/replication.js +174 -0
- package/dist/benchmark/replication.js.map +1 -0
- package/dist/src/constants.d.ts +2 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +2 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/program.d.ts +90 -0
- package/dist/src/program.d.ts.map +1 -0
- package/{lib/esm/document-store.js → dist/src/program.js} +141 -109
- package/dist/src/program.js.map +1 -0
- package/dist/src/search.d.ts +118 -0
- package/dist/src/search.d.ts.map +1 -0
- package/{lib/esm/document-index.js → dist/src/search.js} +246 -446
- package/dist/src/search.js.map +1 -0
- package/package.json +69 -43
- package/src/constants.ts +1 -0
- package/src/index.ts +4 -3
- package/src/{document-store.ts → program.ts} +216 -183
- package/src/search.ts +997 -0
- package/LICENSE +0 -202
- package/lib/esm/document-index.d.ts +0 -147
- package/lib/esm/document-index.js.map +0 -1
- package/lib/esm/document-store.d.ts +0 -72
- package/lib/esm/document-store.js.map +0 -1
- package/lib/esm/index.d.ts +0 -3
- package/lib/esm/index.js +0 -4
- package/lib/esm/index.js.map +0 -1
- package/lib/esm/query.d.ts +0 -191
- package/lib/esm/query.js +0 -615
- package/lib/esm/query.js.map +0 -1
- package/lib/esm/utils.d.ts +0 -3
- package/lib/esm/utils.js +0 -12
- package/lib/esm/utils.js.map +0 -1
- package/src/document-index.ts +0 -1268
- package/src/query.ts +0 -525
- package/src/utils.ts +0 -17
package/src/document-index.ts
DELETED
|
@@ -1,1268 +0,0 @@
|
|
|
1
|
-
import { AbstractType, field, serialize, variant } from "@dao-xyz/borsh";
|
|
2
|
-
import { asString, Keyable } from "./utils.js";
|
|
3
|
-
import { BORSH_ENCODING, Encoding, Entry } from "@peerbit/log";
|
|
4
|
-
import { equals } from "@peerbit/uint8arrays";
|
|
5
|
-
import { Program } from "@peerbit/program";
|
|
6
|
-
import {
|
|
7
|
-
IntegerCompare,
|
|
8
|
-
ByteMatchQuery,
|
|
9
|
-
StringMatch,
|
|
10
|
-
Query,
|
|
11
|
-
ResultWithSource,
|
|
12
|
-
StateFieldQuery,
|
|
13
|
-
compare,
|
|
14
|
-
Context,
|
|
15
|
-
MissingField,
|
|
16
|
-
StringMatchMethod,
|
|
17
|
-
LogicalQuery,
|
|
18
|
-
And,
|
|
19
|
-
Or,
|
|
20
|
-
BoolQuery,
|
|
21
|
-
Sort,
|
|
22
|
-
CollectNextRequest,
|
|
23
|
-
AbstractSearchRequest,
|
|
24
|
-
SearchRequest,
|
|
25
|
-
SortDirection,
|
|
26
|
-
CloseIteratorRequest,
|
|
27
|
-
NoAccess,
|
|
28
|
-
AbstractSearchResult
|
|
29
|
-
} from "./query.js";
|
|
30
|
-
import {
|
|
31
|
-
RPC,
|
|
32
|
-
RPCResponse,
|
|
33
|
-
queryAll,
|
|
34
|
-
MissingResponsesError,
|
|
35
|
-
RPCRequestAllOptions
|
|
36
|
-
} from "@peerbit/rpc";
|
|
37
|
-
import { Results } from "./query.js";
|
|
38
|
-
import { logger as loggerFn } from "@peerbit/logger";
|
|
39
|
-
import { Cache } from "@peerbit/cache";
|
|
40
|
-
import { PublicSignKey, sha256Base64Sync } from "@peerbit/crypto";
|
|
41
|
-
import { SharedLog } from "@peerbit/shared-log";
|
|
42
|
-
import { concat, fromString } from "uint8arrays";
|
|
43
|
-
import { SilentDelivery } from "@peerbit/stream-interface";
|
|
44
|
-
import { AbortError } from "@peerbit/time";
|
|
45
|
-
|
|
46
|
-
const logger = loggerFn({ module: "document-index" });
|
|
47
|
-
|
|
48
|
-
const stringArraysEquals = (a: string[] | string, b: string[] | string) => {
|
|
49
|
-
if (a.length !== b.length) {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
for (let i = 0; i < a.length; i++) {
|
|
53
|
-
if (a[i] !== b[i]) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return true;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
@variant(0)
|
|
61
|
-
export class Operation<T> {}
|
|
62
|
-
|
|
63
|
-
export const BORSH_ENCODING_OPERATION = BORSH_ENCODING(Operation);
|
|
64
|
-
|
|
65
|
-
@variant(0)
|
|
66
|
-
export class PutOperation<T> extends Operation<T> {
|
|
67
|
-
@field({ type: "string" })
|
|
68
|
-
key: string;
|
|
69
|
-
|
|
70
|
-
@field({ type: Uint8Array })
|
|
71
|
-
data: Uint8Array;
|
|
72
|
-
|
|
73
|
-
_value?: T;
|
|
74
|
-
|
|
75
|
-
constructor(props?: { key: string; data: Uint8Array; value?: T }) {
|
|
76
|
-
super();
|
|
77
|
-
if (props) {
|
|
78
|
-
this.key = props.key;
|
|
79
|
-
this.data = props.data;
|
|
80
|
-
this._value = props.value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
get value(): T | undefined {
|
|
85
|
-
if (!this._value) {
|
|
86
|
-
throw new Error("Value not decoded, invoke getValue(...) once");
|
|
87
|
-
}
|
|
88
|
-
return this._value;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
getValue(encoding: Encoding<T>): T {
|
|
92
|
-
if (this._value) {
|
|
93
|
-
return this._value;
|
|
94
|
-
}
|
|
95
|
-
this._value = encoding.decoder(this.data);
|
|
96
|
-
return this._value;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/* @variant(1)
|
|
101
|
-
export class PutAllOperation<T> extends Operation<T> {
|
|
102
|
-
@field({ type: vec(PutOperation) })
|
|
103
|
-
docs: PutOperation<T>[];
|
|
104
|
-
|
|
105
|
-
constructor(props?: { docs: PutOperation<T>[] }) {
|
|
106
|
-
super();
|
|
107
|
-
if (props) {
|
|
108
|
-
this.docs = props.docs;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
*/
|
|
113
|
-
@variant(2)
|
|
114
|
-
export class DeleteOperation extends Operation<any> {
|
|
115
|
-
@field({ type: "string" })
|
|
116
|
-
key: string;
|
|
117
|
-
|
|
118
|
-
constructor(props?: { key: string }) {
|
|
119
|
-
super();
|
|
120
|
-
if (props) {
|
|
121
|
-
this.key = props.key;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export interface IndexedValue<T> {
|
|
127
|
-
key: string;
|
|
128
|
-
value: Record<string, any> | T; // decrypted, decoded
|
|
129
|
-
context: Context;
|
|
130
|
-
reference?: ValueWithLastOperation<T>;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export type RemoteQueryOptions<R> = RPCRequestAllOptions<R> & {
|
|
134
|
-
sync?: boolean;
|
|
135
|
-
minAge?: number;
|
|
136
|
-
throwOnMissing?: boolean;
|
|
137
|
-
};
|
|
138
|
-
export type QueryOptions<R> = {
|
|
139
|
-
remote?: boolean | RemoteQueryOptions<AbstractSearchResult<R>>;
|
|
140
|
-
local?: boolean;
|
|
141
|
-
};
|
|
142
|
-
export type SearchOptions<R> = { size?: number } & QueryOptions<R>;
|
|
143
|
-
export type IndexableFields<T> = (
|
|
144
|
-
obj: T,
|
|
145
|
-
context: Context
|
|
146
|
-
) => Record<string, any> | Promise<Record<string, any>>;
|
|
147
|
-
|
|
148
|
-
export type ResultsIterator<T> = {
|
|
149
|
-
close: () => Promise<void>;
|
|
150
|
-
next: (number: number) => Promise<T[]>;
|
|
151
|
-
done: () => boolean;
|
|
152
|
-
};
|
|
153
|
-
type ValueWithLastOperation<T> = {
|
|
154
|
-
value: T;
|
|
155
|
-
last: PutOperation<Operation<T>>;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const sortCompare = (av: any, bv: any) => {
|
|
159
|
-
if (typeof av === "string" && typeof bv === "string") {
|
|
160
|
-
return av.localeCompare(bv);
|
|
161
|
-
}
|
|
162
|
-
if (av < bv) {
|
|
163
|
-
return -1;
|
|
164
|
-
} else if (av > bv) {
|
|
165
|
-
return 1;
|
|
166
|
-
}
|
|
167
|
-
return 0;
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const extractFieldValue = <T>(doc: any, path: string[]): T => {
|
|
171
|
-
for (let i = 0; i < path.length; i++) {
|
|
172
|
-
doc = doc[path[i]];
|
|
173
|
-
}
|
|
174
|
-
return doc;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const extractSortCompare = (
|
|
178
|
-
a: Record<string, any>,
|
|
179
|
-
b: Record<string, any>,
|
|
180
|
-
sorts: Sort[]
|
|
181
|
-
) => {
|
|
182
|
-
for (const sort of sorts) {
|
|
183
|
-
const av = extractFieldValue(a, sort.key);
|
|
184
|
-
const bv = extractFieldValue(b, sort.key);
|
|
185
|
-
const cmp = sortCompare(av, bv);
|
|
186
|
-
if (cmp != 0) {
|
|
187
|
-
if (sort.direction === SortDirection.ASC) {
|
|
188
|
-
return cmp;
|
|
189
|
-
} else {
|
|
190
|
-
return -cmp;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return 0;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const resolvedSort = async <
|
|
198
|
-
T,
|
|
199
|
-
Q extends { value: { value: T }; context: Context }
|
|
200
|
-
>(
|
|
201
|
-
arr: Q[],
|
|
202
|
-
index: IndexableFields<T>,
|
|
203
|
-
sorts: Sort[]
|
|
204
|
-
) => {
|
|
205
|
-
await Promise.all(
|
|
206
|
-
arr.map(
|
|
207
|
-
async (result) =>
|
|
208
|
-
(result[SORT_TMP_KEY] = await index(result.value.value, result.context))
|
|
209
|
-
)
|
|
210
|
-
);
|
|
211
|
-
arr.sort((a, b) =>
|
|
212
|
-
extractSortCompare(a[SORT_TMP_KEY], b[SORT_TMP_KEY], sorts)
|
|
213
|
-
);
|
|
214
|
-
return arr;
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const SORT_TMP_KEY = "__sort_ref";
|
|
218
|
-
|
|
219
|
-
type QueryDetailedOptions<T> = QueryOptions<T> & {
|
|
220
|
-
onResponse?: (response: AbstractSearchResult<T>, from: PublicSignKey) => void;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const introduceEntries = async <T>(
|
|
224
|
-
responses: RPCResponse<AbstractSearchResult<T>>[],
|
|
225
|
-
type: AbstractType<T>,
|
|
226
|
-
sync: (result: Results<T>) => Promise<void>,
|
|
227
|
-
options?: QueryDetailedOptions<T>
|
|
228
|
-
): Promise<RPCResponse<Results<T>>[]> => {
|
|
229
|
-
const results: RPCResponse<Results<T>>[] = [];
|
|
230
|
-
for (const response of responses) {
|
|
231
|
-
if (!response.from) {
|
|
232
|
-
logger.error("Missing from for response");
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (response.response instanceof Results) {
|
|
236
|
-
response.response.results.forEach((r) => r.init(type));
|
|
237
|
-
if (typeof options?.remote !== "boolean" && options?.remote?.sync) {
|
|
238
|
-
await sync(response.response);
|
|
239
|
-
}
|
|
240
|
-
options?.onResponse &&
|
|
241
|
-
options.onResponse(response.response, response.from!); // TODO fix types
|
|
242
|
-
results.push(response as RPCResponse<Results<T>>);
|
|
243
|
-
} else if (response.response instanceof NoAccess) {
|
|
244
|
-
logger.error("Search resulted in access error");
|
|
245
|
-
} else {
|
|
246
|
-
throw new Error("Unsupported");
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return results;
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const dedup = <T>(
|
|
253
|
-
allResult: T[],
|
|
254
|
-
dedupBy: (obj: any) => string | Uint8Array
|
|
255
|
-
) => {
|
|
256
|
-
const unique: Set<Keyable> = new Set();
|
|
257
|
-
const dedup: T[] = [];
|
|
258
|
-
for (const result of allResult) {
|
|
259
|
-
const key = asString(dedupBy(result));
|
|
260
|
-
if (unique.has(key)) {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
unique.add(key);
|
|
264
|
-
dedup.push(result);
|
|
265
|
-
}
|
|
266
|
-
return dedup;
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const DEFAULT_INDEX_BY = "id";
|
|
270
|
-
|
|
271
|
-
/*
|
|
272
|
-
if (!(await this.canRead(message.sender))) {
|
|
273
|
-
throw new AccessError();
|
|
274
|
-
} */
|
|
275
|
-
export const MAX_DOCUMENT_SIZE = 5e6;
|
|
276
|
-
|
|
277
|
-
const getBatchFromResults = <T>(
|
|
278
|
-
results: { value: ValueWithLastOperation<T>; context: Context }[],
|
|
279
|
-
wantedSize: number,
|
|
280
|
-
maxSize: number = MAX_DOCUMENT_SIZE
|
|
281
|
-
) => {
|
|
282
|
-
const batch: { value: ValueWithLastOperation<T>; context: Context }[] = [];
|
|
283
|
-
let size = 0;
|
|
284
|
-
for (const result of results) {
|
|
285
|
-
batch.push(result);
|
|
286
|
-
size += result.value.last.data.length;
|
|
287
|
-
if (size > maxSize) {
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
if (wantedSize <= batch.length) {
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
results.splice(0, batch.length);
|
|
295
|
-
return batch;
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
export type CanSearch = (
|
|
299
|
-
request: SearchRequest | CollectNextRequest,
|
|
300
|
-
from: PublicSignKey
|
|
301
|
-
) => Promise<boolean> | boolean;
|
|
302
|
-
|
|
303
|
-
export type CanRead<T> = (
|
|
304
|
-
result: T,
|
|
305
|
-
from: PublicSignKey
|
|
306
|
-
) => Promise<boolean> | boolean;
|
|
307
|
-
|
|
308
|
-
export type InMemoryIndex<T> = { index: DocumentIndex<T> };
|
|
309
|
-
|
|
310
|
-
export type OpenOptions<T> = {
|
|
311
|
-
type: AbstractType<T>;
|
|
312
|
-
dbType: AbstractType<InMemoryIndex<T>>;
|
|
313
|
-
log: SharedLog<Operation<T>>;
|
|
314
|
-
canRead?: CanRead<T>;
|
|
315
|
-
canSearch?: CanSearch;
|
|
316
|
-
fields: IndexableFields<T>;
|
|
317
|
-
sync: (result: Results<T>) => Promise<void>;
|
|
318
|
-
indexBy?: string | string[];
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
@variant("documents_index")
|
|
322
|
-
export class DocumentIndex<T> extends Program<OpenOptions<T>> {
|
|
323
|
-
@field({ type: RPC })
|
|
324
|
-
_query: RPC<AbstractSearchRequest, AbstractSearchResult<T>>;
|
|
325
|
-
|
|
326
|
-
type: AbstractType<T>;
|
|
327
|
-
dbType: AbstractType<InMemoryIndex<T>>;
|
|
328
|
-
|
|
329
|
-
// Index key
|
|
330
|
-
private _indexBy: string | string[];
|
|
331
|
-
private _indexByArr: string[];
|
|
332
|
-
|
|
333
|
-
// Resolve doc value by index key
|
|
334
|
-
indexByResolver: (obj: any) => string | Uint8Array;
|
|
335
|
-
|
|
336
|
-
// Indexed (transforms an docuemnt into an obj with fields that ought to be indexed)
|
|
337
|
-
private _toIndex: IndexableFields<T>;
|
|
338
|
-
|
|
339
|
-
private _valueEncoding: Encoding<T>;
|
|
340
|
-
|
|
341
|
-
private _sync: (result: Results<T>) => Promise<void>;
|
|
342
|
-
private _index: Map<string, IndexedValue<T>>;
|
|
343
|
-
private _resultsCollectQueue: Cache<{
|
|
344
|
-
from: PublicSignKey;
|
|
345
|
-
arr: { value: ValueWithLastOperation<T>; context: Context }[];
|
|
346
|
-
}>;
|
|
347
|
-
|
|
348
|
-
private _log: SharedLog<Operation<T>>;
|
|
349
|
-
|
|
350
|
-
constructor(properties?: {
|
|
351
|
-
query?: RPC<AbstractSearchRequest, AbstractSearchResult<T>>;
|
|
352
|
-
}) {
|
|
353
|
-
super();
|
|
354
|
-
this._query = properties?.query || new RPC();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
get index(): Map<string, IndexedValue<T>> {
|
|
358
|
-
return this._index;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
get valueEncoding() {
|
|
362
|
-
return this._valueEncoding;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
get toIndex(): IndexableFields<T> {
|
|
366
|
-
return this._toIndex;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async open(properties: OpenOptions<T>) {
|
|
370
|
-
this._index = new Map();
|
|
371
|
-
this._log = properties.log;
|
|
372
|
-
this.type = properties.type;
|
|
373
|
-
this.dbType = properties.dbType;
|
|
374
|
-
this._sync = properties.sync;
|
|
375
|
-
this._toIndex = properties.fields;
|
|
376
|
-
this._indexBy = properties.indexBy || DEFAULT_INDEX_BY;
|
|
377
|
-
this._indexByArr = Array.isArray(this._indexBy)
|
|
378
|
-
? this._indexBy
|
|
379
|
-
: [this._indexBy];
|
|
380
|
-
|
|
381
|
-
this.indexByResolver =
|
|
382
|
-
typeof this._indexBy === "string"
|
|
383
|
-
? (obj) => obj[this._indexBy as string]
|
|
384
|
-
: (obj: any) => extractFieldValue(obj, this._indexBy as string[]);
|
|
385
|
-
this._valueEncoding = BORSH_ENCODING(this.type);
|
|
386
|
-
this._resultsCollectQueue = new Cache({ max: 10000 }); // TODO choose limit better
|
|
387
|
-
|
|
388
|
-
await this._query.open({
|
|
389
|
-
topic: sha256Base64Sync(
|
|
390
|
-
concat([this._log.log.id, fromString("/document")])
|
|
391
|
-
),
|
|
392
|
-
responseHandler: async (query, ctx) => {
|
|
393
|
-
if (!ctx.from) {
|
|
394
|
-
logger.info("Receieved query without from");
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
properties.canSearch &&
|
|
400
|
-
(query instanceof SearchRequest ||
|
|
401
|
-
query instanceof CollectNextRequest) &&
|
|
402
|
-
!(await properties.canSearch(
|
|
403
|
-
query as SearchRequest | CollectNextRequest,
|
|
404
|
-
ctx.from
|
|
405
|
-
))
|
|
406
|
-
) {
|
|
407
|
-
return new NoAccess();
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (query instanceof CloseIteratorRequest) {
|
|
411
|
-
this.processCloseIteratorRequest(query, ctx.from);
|
|
412
|
-
} else {
|
|
413
|
-
const results = await this.processFetchRequest(
|
|
414
|
-
query as SearchRequest | SearchRequest | CollectNextRequest,
|
|
415
|
-
ctx.from,
|
|
416
|
-
{
|
|
417
|
-
canRead: properties.canRead
|
|
418
|
-
}
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
return new Results({
|
|
422
|
-
// Even if results might have length 0, respond, because then we now at least there are no matching results
|
|
423
|
-
results: results.results.map((r) => {
|
|
424
|
-
if (r.value.last instanceof PutOperation === false) {
|
|
425
|
-
throw new Error(
|
|
426
|
-
"Unexpected value type on local results: " +
|
|
427
|
-
(r.value.last as any)?.constructor.name ||
|
|
428
|
-
typeof r.value.last
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
return new ResultWithSource({
|
|
432
|
-
source: r.value.last.data,
|
|
433
|
-
context: r.context
|
|
434
|
-
});
|
|
435
|
-
}),
|
|
436
|
-
kept: BigInt(results.kept)
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
},
|
|
440
|
-
responseType: AbstractSearchResult,
|
|
441
|
-
queryType: AbstractSearchRequest
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
public async get(
|
|
446
|
-
key: Keyable,
|
|
447
|
-
options?: QueryOptions<T>
|
|
448
|
-
): Promise<T | undefined> {
|
|
449
|
-
return (await this.getDetailed(key, options))?.[0]?.results[0]?.value;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
public async getDetailed(
|
|
453
|
-
key: Keyable,
|
|
454
|
-
options?: QueryOptions<T>
|
|
455
|
-
): Promise<Results<T>[] | undefined> {
|
|
456
|
-
let results: Results<T>[] | undefined;
|
|
457
|
-
if (key instanceof Uint8Array) {
|
|
458
|
-
results = await this.queryDetailed(
|
|
459
|
-
new SearchRequest({
|
|
460
|
-
query: [new ByteMatchQuery({ key: this._indexByArr, value: key })]
|
|
461
|
-
}),
|
|
462
|
-
options
|
|
463
|
-
);
|
|
464
|
-
} else {
|
|
465
|
-
const stringValue = asString(key);
|
|
466
|
-
results = await this.queryDetailed(
|
|
467
|
-
new SearchRequest({
|
|
468
|
-
query: [
|
|
469
|
-
new StringMatch({
|
|
470
|
-
key: this._indexByArr,
|
|
471
|
-
value: stringValue
|
|
472
|
-
})
|
|
473
|
-
]
|
|
474
|
-
}),
|
|
475
|
-
options
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return results;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
get size(): number {
|
|
483
|
-
return this._index.size;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
private async getDocumentWithLastOperation(value: {
|
|
487
|
-
reference?: ValueWithLastOperation<T>;
|
|
488
|
-
context: { head: string };
|
|
489
|
-
}): Promise<ValueWithLastOperation<T> | undefined> {
|
|
490
|
-
if (value.reference) {
|
|
491
|
-
return value.reference;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const head = await await this._log.log.get(value.context.head);
|
|
495
|
-
if (!head) {
|
|
496
|
-
return undefined; // we could end up here if we recently pruned the document and other peers never persisted the entry
|
|
497
|
-
// TODO update changes in index before removing entries from log entry storage
|
|
498
|
-
}
|
|
499
|
-
const payloadValue = await head.getPayloadValue();
|
|
500
|
-
if (payloadValue instanceof PutOperation) {
|
|
501
|
-
return {
|
|
502
|
-
value: payloadValue.getValue(this.valueEncoding),
|
|
503
|
-
last: payloadValue
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
throw new Error(
|
|
508
|
-
"Unexpected value type when getting document: " +
|
|
509
|
-
payloadValue?.constructor?.name || typeof payloadValue
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
getDocument(value: {
|
|
514
|
-
reference?: ValueWithLastOperation<T>;
|
|
515
|
-
context: { head: string };
|
|
516
|
-
}) {
|
|
517
|
-
return this.getDocumentWithLastOperation(value).then((r) => r?.value);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
async _queryDocuments(
|
|
521
|
-
filter: (doc: IndexedValue<T>) => Promise<boolean>
|
|
522
|
-
): Promise<{ context: Context; value: ValueWithLastOperation<T> }[]> {
|
|
523
|
-
// Whether we return the full operation data or just the db value
|
|
524
|
-
const results: { context: Context; value: ValueWithLastOperation<T> }[] =
|
|
525
|
-
[];
|
|
526
|
-
for (const value of this._index.values()) {
|
|
527
|
-
if (await filter(value)) {
|
|
528
|
-
const topDoc = await this.getDocumentWithLastOperation(value);
|
|
529
|
-
topDoc &&
|
|
530
|
-
results.push({
|
|
531
|
-
context: value.context,
|
|
532
|
-
value: topDoc
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return results;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
async processFetchRequest(
|
|
540
|
-
query: SearchRequest | CollectNextRequest,
|
|
541
|
-
from: PublicSignKey,
|
|
542
|
-
options?: {
|
|
543
|
-
canRead?: CanRead<T>;
|
|
544
|
-
}
|
|
545
|
-
): Promise<{
|
|
546
|
-
results: { context: Context; value: ValueWithLastOperation<T> }[];
|
|
547
|
-
kept: number;
|
|
548
|
-
}> {
|
|
549
|
-
// We do special case for querying the id as we can do it faster than iterating
|
|
550
|
-
|
|
551
|
-
if (query instanceof SearchRequest) {
|
|
552
|
-
// Special case querying ids
|
|
553
|
-
if (
|
|
554
|
-
query.query.length === 1 &&
|
|
555
|
-
(query.query[0] instanceof ByteMatchQuery ||
|
|
556
|
-
query.query[0] instanceof StringMatch) &&
|
|
557
|
-
stringArraysEquals(query.query[0].key, this._indexByArr)
|
|
558
|
-
) {
|
|
559
|
-
const firstQuery = query.query[0];
|
|
560
|
-
if (firstQuery instanceof ByteMatchQuery) {
|
|
561
|
-
const doc = this._index.get(asString(firstQuery.value));
|
|
562
|
-
const topDoc = doc && (await this.getDocumentWithLastOperation(doc));
|
|
563
|
-
return topDoc
|
|
564
|
-
? {
|
|
565
|
-
results: [
|
|
566
|
-
{
|
|
567
|
-
value: topDoc,
|
|
568
|
-
context: doc.context
|
|
569
|
-
}
|
|
570
|
-
],
|
|
571
|
-
kept: 0
|
|
572
|
-
}
|
|
573
|
-
: { results: [], kept: 0 };
|
|
574
|
-
} else if (
|
|
575
|
-
firstQuery instanceof StringMatch &&
|
|
576
|
-
firstQuery.method === StringMatchMethod.exact &&
|
|
577
|
-
firstQuery.caseInsensitive === false
|
|
578
|
-
) {
|
|
579
|
-
const doc = this._index.get(firstQuery.value);
|
|
580
|
-
const topDoc = doc && (await this.getDocumentWithLastOperation(doc));
|
|
581
|
-
return topDoc
|
|
582
|
-
? {
|
|
583
|
-
results: [
|
|
584
|
-
{
|
|
585
|
-
value: topDoc,
|
|
586
|
-
context: doc.context
|
|
587
|
-
}
|
|
588
|
-
],
|
|
589
|
-
kept: 0
|
|
590
|
-
}
|
|
591
|
-
: { results: [], kept: 0 };
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Handle query normally
|
|
596
|
-
let results = await this._queryDocuments(async (doc) => {
|
|
597
|
-
for (const f of query.query) {
|
|
598
|
-
if (!(await this.handleQueryObject(f, doc))) {
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
return true;
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
if (options?.canRead) {
|
|
606
|
-
const keepFilter = await Promise.all(
|
|
607
|
-
results.map((x) => options?.canRead!(x.value.value, from))
|
|
608
|
-
);
|
|
609
|
-
results = results.filter((x, i) => keepFilter[i]);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Sort
|
|
613
|
-
await resolvedSort(results, this._toIndex, query.sort);
|
|
614
|
-
|
|
615
|
-
const batch = getBatchFromResults(results, query.fetch);
|
|
616
|
-
|
|
617
|
-
if (results.length > 0) {
|
|
618
|
-
this._resultsCollectQueue.add(query.idString, {
|
|
619
|
-
arr: results,
|
|
620
|
-
from
|
|
621
|
-
}); // cache resulst not returned
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
return { results: batch, kept: results.length }; // Only return 1 result since we are doing distributed sort, TODO buffer more initially
|
|
625
|
-
} else if (query instanceof CollectNextRequest) {
|
|
626
|
-
const results = this._resultsCollectQueue.get(query.idString);
|
|
627
|
-
if (!results) {
|
|
628
|
-
return {
|
|
629
|
-
results: [],
|
|
630
|
-
kept: 0
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const batch = getBatchFromResults(results.arr, query.amount);
|
|
635
|
-
|
|
636
|
-
if (results.arr.length === 0) {
|
|
637
|
-
this._resultsCollectQueue.del(query.idString); // TODO add tests for proper cleanup/timeouts
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return { results: batch, kept: results.arr.length };
|
|
641
|
-
}
|
|
642
|
-
throw new Error("Unsupported");
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async processCloseIteratorRequest(
|
|
646
|
-
query: CloseIteratorRequest,
|
|
647
|
-
publicKey: PublicSignKey
|
|
648
|
-
): Promise<void> {
|
|
649
|
-
const entry = this._resultsCollectQueue.get(query.idString);
|
|
650
|
-
if (entry?.from.equals(publicKey)) {
|
|
651
|
-
this._resultsCollectQueue.del(query.idString);
|
|
652
|
-
} else if (entry) {
|
|
653
|
-
logger.warn(
|
|
654
|
-
"Received a close iterator request for a iterator that does not belong to the requesting peer"
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
private async handleFieldQuery(
|
|
660
|
-
f: StateFieldQuery,
|
|
661
|
-
obj: T,
|
|
662
|
-
startIndex: number
|
|
663
|
-
) {
|
|
664
|
-
// this clause is needed if we have a field that is of type [][] (we will recursively go through each subarray)
|
|
665
|
-
if (Array.isArray(obj)) {
|
|
666
|
-
for (const element of obj) {
|
|
667
|
-
if (await this.handleFieldQuery(f, element, startIndex)) {
|
|
668
|
-
return true;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
return false;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Resolve the field from the key path. If we reach an array or nested Document store,
|
|
675
|
-
// then do a recursive call or a search to look into them
|
|
676
|
-
for (let i = startIndex; i < f.key.length; i++) {
|
|
677
|
-
obj = obj[f.key[i]];
|
|
678
|
-
if (Array.isArray(obj)) {
|
|
679
|
-
for (const element of obj) {
|
|
680
|
-
if (await this.handleFieldQuery(f, element, i + 1)) {
|
|
681
|
-
return true;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
return false;
|
|
685
|
-
}
|
|
686
|
-
if (obj instanceof this.dbType) {
|
|
687
|
-
const queryCloned = f.clone();
|
|
688
|
-
queryCloned.key.splice(0, i + 1); // remove key path until the document store
|
|
689
|
-
const results = await (obj as any as InMemoryIndex<any>).index.search(
|
|
690
|
-
new SearchRequest({ query: [queryCloned] })
|
|
691
|
-
);
|
|
692
|
-
return results.length > 0 ? true : false; // TODO return INNER HITS?
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// When we reach here, the field value (obj) is comparable
|
|
697
|
-
if (f instanceof StringMatch) {
|
|
698
|
-
let compare = f.value;
|
|
699
|
-
if (f.caseInsensitive) {
|
|
700
|
-
compare = compare.toLowerCase();
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (this.handleStringMatch(f, compare, obj as string)) {
|
|
704
|
-
return true;
|
|
705
|
-
}
|
|
706
|
-
return false;
|
|
707
|
-
} else if (f instanceof ByteMatchQuery) {
|
|
708
|
-
if (obj instanceof Uint8Array === false) {
|
|
709
|
-
if (stringArraysEquals(f.key, this._indexByArr)) {
|
|
710
|
-
return f.valueString === obj;
|
|
711
|
-
}
|
|
712
|
-
return false;
|
|
713
|
-
}
|
|
714
|
-
return equals(obj as Uint8Array, f.value);
|
|
715
|
-
} else if (f instanceof IntegerCompare) {
|
|
716
|
-
const value: bigint | number = obj as bigint | number;
|
|
717
|
-
|
|
718
|
-
if (typeof value !== "bigint" && typeof value !== "number") {
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
return compare(value, f.compare, f.value.value);
|
|
722
|
-
} else if (f instanceof MissingField) {
|
|
723
|
-
return obj == null; // null or undefined
|
|
724
|
-
} else if (f instanceof BoolQuery) {
|
|
725
|
-
return obj === f.value; // true/false
|
|
726
|
-
}
|
|
727
|
-
logger.warn("Unsupported query type: " + f.constructor.name);
|
|
728
|
-
return false;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
private async handleQueryObject(f: Query, doc: IndexedValue<T>) {
|
|
732
|
-
if (f instanceof StateFieldQuery) {
|
|
733
|
-
return this.handleFieldQuery(f, doc.value as T, 0);
|
|
734
|
-
} else if (f instanceof LogicalQuery) {
|
|
735
|
-
if (f instanceof And) {
|
|
736
|
-
for (const and of f.and) {
|
|
737
|
-
if (!(await this.handleQueryObject(and, doc))) {
|
|
738
|
-
return false;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
return true;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if (f instanceof Or) {
|
|
745
|
-
for (const or of f.or) {
|
|
746
|
-
if (await this.handleQueryObject(or, doc)) {
|
|
747
|
-
return true;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
return false;
|
|
751
|
-
}
|
|
752
|
-
return false;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
logger.info("Unsupported query type: " + f.constructor.name);
|
|
756
|
-
return false;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
private handleStringMatch(f: StringMatch, compare: string, fv: string) {
|
|
760
|
-
if (typeof fv !== "string") {
|
|
761
|
-
return false;
|
|
762
|
-
}
|
|
763
|
-
if (f.caseInsensitive) {
|
|
764
|
-
fv = fv.toLowerCase();
|
|
765
|
-
}
|
|
766
|
-
if (f.method === StringMatchMethod.exact) {
|
|
767
|
-
return fv === compare;
|
|
768
|
-
}
|
|
769
|
-
if (f.method === StringMatchMethod.prefix) {
|
|
770
|
-
return fv.startsWith(compare);
|
|
771
|
-
}
|
|
772
|
-
if (f.method === StringMatchMethod.contains) {
|
|
773
|
-
return fv.includes(compare);
|
|
774
|
-
}
|
|
775
|
-
throw new Error("Unsupported");
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Query and retrieve results with most details
|
|
780
|
-
* @param queryRequest
|
|
781
|
-
* @param options
|
|
782
|
-
* @returns
|
|
783
|
-
*/
|
|
784
|
-
public async queryDetailed(
|
|
785
|
-
queryRequest: SearchRequest,
|
|
786
|
-
options?: QueryDetailedOptions<T>
|
|
787
|
-
): Promise<Results<T>[]> {
|
|
788
|
-
const local = typeof options?.local == "boolean" ? options?.local : true;
|
|
789
|
-
let remote: RemoteQueryOptions<AbstractSearchResult<T>> | undefined =
|
|
790
|
-
undefined;
|
|
791
|
-
if (typeof options?.remote === "boolean") {
|
|
792
|
-
if (options?.remote) {
|
|
793
|
-
remote = {};
|
|
794
|
-
} else {
|
|
795
|
-
remote = undefined;
|
|
796
|
-
}
|
|
797
|
-
} else {
|
|
798
|
-
remote = options?.remote || {};
|
|
799
|
-
}
|
|
800
|
-
if (remote && remote.priority == null) {
|
|
801
|
-
// give queries higher priority than other "normal" data activities
|
|
802
|
-
// without this, we might have a scenario that a peer joina network with large amount of data to be synced, but can not query anything before that is done
|
|
803
|
-
// this will lead to bad UX as you usually want to list/expore whats going on before doing any replication work
|
|
804
|
-
remote.priority = 1;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
const promises: Promise<Results<T>[] | undefined>[] = [];
|
|
808
|
-
if (!local && !remote) {
|
|
809
|
-
throw new Error(
|
|
810
|
-
"Expecting either 'options.remote' or 'options.local' to be true"
|
|
811
|
-
);
|
|
812
|
-
}
|
|
813
|
-
const allResults: Results<T>[] = [];
|
|
814
|
-
|
|
815
|
-
if (local) {
|
|
816
|
-
const results = await this.processFetchRequest(
|
|
817
|
-
queryRequest,
|
|
818
|
-
this.node.identity.publicKey
|
|
819
|
-
);
|
|
820
|
-
if (results.results.length > 0) {
|
|
821
|
-
const resultsObject = new Results<T>({
|
|
822
|
-
results: results.results.map((r) => {
|
|
823
|
-
if (r.value.last instanceof PutOperation === false) {
|
|
824
|
-
throw new Error(
|
|
825
|
-
"Unexpected value type on local results: " +
|
|
826
|
-
(r.value.last as any)?.constructor.name || typeof r.value.last
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
return new ResultWithSource({
|
|
830
|
-
context: r.context,
|
|
831
|
-
value: r.value.value,
|
|
832
|
-
source: r.value.last.data
|
|
833
|
-
});
|
|
834
|
-
}),
|
|
835
|
-
kept: BigInt(results.kept)
|
|
836
|
-
});
|
|
837
|
-
options?.onResponse &&
|
|
838
|
-
options.onResponse(resultsObject, this.node.identity.publicKey);
|
|
839
|
-
allResults.push(resultsObject);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if (remote) {
|
|
844
|
-
const replicatorGroups = await this._log.getReplicatorUnion(
|
|
845
|
-
remote.minAge
|
|
846
|
-
);
|
|
847
|
-
if (replicatorGroups) {
|
|
848
|
-
const groupHashes: string[][] = replicatorGroups.map((x) => [x]);
|
|
849
|
-
const fn = async () => {
|
|
850
|
-
const rs: Results<T>[] = [];
|
|
851
|
-
const responseHandler = async (
|
|
852
|
-
results: RPCResponse<AbstractSearchResult<T>>[]
|
|
853
|
-
) => {
|
|
854
|
-
for (const r of await introduceEntries(
|
|
855
|
-
results,
|
|
856
|
-
this.type,
|
|
857
|
-
this._sync,
|
|
858
|
-
options
|
|
859
|
-
)) {
|
|
860
|
-
rs.push(r.response);
|
|
861
|
-
}
|
|
862
|
-
};
|
|
863
|
-
try {
|
|
864
|
-
if (queryRequest instanceof CloseIteratorRequest) {
|
|
865
|
-
// don't wait for responses
|
|
866
|
-
await this._query.request(queryRequest, { mode: remote!.mode });
|
|
867
|
-
} else {
|
|
868
|
-
await queryAll(
|
|
869
|
-
this._query,
|
|
870
|
-
groupHashes,
|
|
871
|
-
queryRequest,
|
|
872
|
-
responseHandler,
|
|
873
|
-
remote
|
|
874
|
-
);
|
|
875
|
-
}
|
|
876
|
-
} catch (error) {
|
|
877
|
-
if (error instanceof MissingResponsesError) {
|
|
878
|
-
logger.warn("Did not reciveve responses from all shard");
|
|
879
|
-
if (remote?.throwOnMissing) {
|
|
880
|
-
throw error;
|
|
881
|
-
}
|
|
882
|
-
} else {
|
|
883
|
-
throw error;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
return rs;
|
|
887
|
-
};
|
|
888
|
-
promises.push(fn());
|
|
889
|
-
} else {
|
|
890
|
-
// TODO send without direction out to the world? or just assume we can insert?
|
|
891
|
-
/* promises.push(
|
|
892
|
-
this._query
|
|
893
|
-
.request(queryRequest, remote)
|
|
894
|
-
.then((results) => introduceEntries(results, this.type, this._sync, options).then(x => x.map(y => y.response)))
|
|
895
|
-
); */
|
|
896
|
-
/* throw new Error(
|
|
897
|
-
"Missing remote replicator info for performing distributed document query"
|
|
898
|
-
); */
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
const resolved = await Promise.all(promises);
|
|
902
|
-
for (const r of resolved) {
|
|
903
|
-
if (r) {
|
|
904
|
-
if (r instanceof Array) {
|
|
905
|
-
allResults.push(...r);
|
|
906
|
-
} else {
|
|
907
|
-
allResults.push(r);
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
return allResults;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* Query and retrieve results
|
|
916
|
-
* @param queryRequest
|
|
917
|
-
* @param options
|
|
918
|
-
* @returns
|
|
919
|
-
*/
|
|
920
|
-
public async search(
|
|
921
|
-
queryRequest: SearchRequest,
|
|
922
|
-
options?: SearchOptions<T>
|
|
923
|
-
): Promise<T[]> {
|
|
924
|
-
// Set fetch to search size, or max value (default to max u32 (4294967295))
|
|
925
|
-
queryRequest.fetch = options?.size ?? 0xffffffff;
|
|
926
|
-
|
|
927
|
-
// So that the iterator is pre-fetching the right amount of entries
|
|
928
|
-
const iterator = this.iterate(queryRequest, options);
|
|
929
|
-
|
|
930
|
-
// So that this call will not do any remote requests
|
|
931
|
-
const allResults: T[] = [];
|
|
932
|
-
while (
|
|
933
|
-
iterator.done() === false &&
|
|
934
|
-
queryRequest.fetch > allResults.length
|
|
935
|
-
) {
|
|
936
|
-
// We might need to pull .next multiple time due to data message size limitations
|
|
937
|
-
for (const result of await iterator.next(
|
|
938
|
-
queryRequest.fetch - allResults.length
|
|
939
|
-
)) {
|
|
940
|
-
allResults.push(result);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
await iterator.close();
|
|
945
|
-
|
|
946
|
-
//s Deduplicate and return values directly
|
|
947
|
-
return dedup(allResults, this.indexByResolver);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* Query and retrieve documents in a iterator
|
|
952
|
-
* @param queryRequest
|
|
953
|
-
* @param options
|
|
954
|
-
* @returns
|
|
955
|
-
*/
|
|
956
|
-
public iterate(
|
|
957
|
-
queryRequest: SearchRequest,
|
|
958
|
-
options?: QueryOptions<T>
|
|
959
|
-
): ResultsIterator<T> {
|
|
960
|
-
let fetchPromise: Promise<any> | undefined = undefined;
|
|
961
|
-
const peerBufferMap: Map<
|
|
962
|
-
string,
|
|
963
|
-
{
|
|
964
|
-
kept: number;
|
|
965
|
-
buffer: {
|
|
966
|
-
value: { value: T };
|
|
967
|
-
context: Context;
|
|
968
|
-
from: PublicSignKey;
|
|
969
|
-
}[];
|
|
970
|
-
}
|
|
971
|
-
> = new Map();
|
|
972
|
-
const visited = new Set<string>();
|
|
973
|
-
|
|
974
|
-
let done = false;
|
|
975
|
-
let first = false;
|
|
976
|
-
|
|
977
|
-
// TODO handle join/leave while iterating
|
|
978
|
-
const controller = new AbortController();
|
|
979
|
-
|
|
980
|
-
const peerBuffers = (): {
|
|
981
|
-
value: { value: T };
|
|
982
|
-
from: PublicSignKey;
|
|
983
|
-
context: Context;
|
|
984
|
-
}[] => {
|
|
985
|
-
return [...peerBufferMap.values()].map((x) => x.buffer).flat();
|
|
986
|
-
};
|
|
987
|
-
|
|
988
|
-
const fetchFirst = async (n: number): Promise<boolean> => {
|
|
989
|
-
done = true; // Assume we are donne
|
|
990
|
-
queryRequest.fetch = n;
|
|
991
|
-
await this.queryDetailed(queryRequest, {
|
|
992
|
-
...options,
|
|
993
|
-
onResponse: (response, from) => {
|
|
994
|
-
if (!from) {
|
|
995
|
-
logger.error("Missing response from");
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
if (response instanceof NoAccess) {
|
|
999
|
-
logger.error("Dont have access");
|
|
1000
|
-
return;
|
|
1001
|
-
} else if (response instanceof Results) {
|
|
1002
|
-
const results = response as Results<T>;
|
|
1003
|
-
if (results.kept === 0n && results.results.length === 0) {
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
if (results.kept > 0n) {
|
|
1008
|
-
done = false; // we have more to do later!
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
peerBufferMap.set(from.hashcode(), {
|
|
1012
|
-
buffer: results.results
|
|
1013
|
-
.filter(
|
|
1014
|
-
(x) => !visited.has(asString(this.indexByResolver(x.value)))
|
|
1015
|
-
)
|
|
1016
|
-
.map((x) => {
|
|
1017
|
-
visited.add(asString(this.indexByResolver(x.value)));
|
|
1018
|
-
return {
|
|
1019
|
-
from,
|
|
1020
|
-
value: { value: x.value },
|
|
1021
|
-
context: x.context
|
|
1022
|
-
};
|
|
1023
|
-
}),
|
|
1024
|
-
kept: Number(response.kept)
|
|
1025
|
-
});
|
|
1026
|
-
} else {
|
|
1027
|
-
throw new Error(
|
|
1028
|
-
"Unsupported result type: " + response?.constructor?.name
|
|
1029
|
-
);
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
return done;
|
|
1035
|
-
};
|
|
1036
|
-
|
|
1037
|
-
const fetchAtLeast = async (n: number) => {
|
|
1038
|
-
if (done && first) {
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
await fetchPromise;
|
|
1043
|
-
|
|
1044
|
-
if (!first) {
|
|
1045
|
-
first = true;
|
|
1046
|
-
fetchPromise = fetchFirst(n);
|
|
1047
|
-
return fetchPromise;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
const promises: Promise<any>[] = [];
|
|
1051
|
-
let resultsLeft = 0;
|
|
1052
|
-
|
|
1053
|
-
for (const [peer, buffer] of peerBufferMap) {
|
|
1054
|
-
if (buffer.buffer.length < n) {
|
|
1055
|
-
if (buffer.kept === 0) {
|
|
1056
|
-
if (peerBufferMap.get(peer)?.buffer.length === 0) {
|
|
1057
|
-
peerBufferMap.delete(peer); // No more results
|
|
1058
|
-
}
|
|
1059
|
-
continue;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// TODO buffer more than deleted?
|
|
1063
|
-
// TODO batch to multiple 'to's
|
|
1064
|
-
const collectRequest = new CollectNextRequest({
|
|
1065
|
-
id: queryRequest.id,
|
|
1066
|
-
amount: n - buffer.buffer.length
|
|
1067
|
-
});
|
|
1068
|
-
// Fetch locally?
|
|
1069
|
-
if (peer === this.node.identity.publicKey.hashcode()) {
|
|
1070
|
-
promises.push(
|
|
1071
|
-
this.processFetchRequest(
|
|
1072
|
-
collectRequest,
|
|
1073
|
-
this.node.identity.publicKey
|
|
1074
|
-
)
|
|
1075
|
-
.then((results) => {
|
|
1076
|
-
resultsLeft += results.kept;
|
|
1077
|
-
|
|
1078
|
-
if (results.results.length === 0) {
|
|
1079
|
-
if (peerBufferMap.get(peer)?.buffer.length === 0) {
|
|
1080
|
-
peerBufferMap.delete(peer); // No more results
|
|
1081
|
-
}
|
|
1082
|
-
} else {
|
|
1083
|
-
const peerBuffer = peerBufferMap.get(peer);
|
|
1084
|
-
if (!peerBuffer) {
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
peerBuffer.kept = results.kept;
|
|
1088
|
-
peerBuffer.buffer.push(
|
|
1089
|
-
...results.results
|
|
1090
|
-
.filter(
|
|
1091
|
-
(x) =>
|
|
1092
|
-
!visited.has(
|
|
1093
|
-
asString(this.indexByResolver(x.value.value))
|
|
1094
|
-
)
|
|
1095
|
-
)
|
|
1096
|
-
.map((x) => {
|
|
1097
|
-
visited.add(
|
|
1098
|
-
asString(this.indexByResolver(x.value.value))
|
|
1099
|
-
);
|
|
1100
|
-
return {
|
|
1101
|
-
value: x.value,
|
|
1102
|
-
context: x.context,
|
|
1103
|
-
from: this.node.identity.publicKey
|
|
1104
|
-
};
|
|
1105
|
-
})
|
|
1106
|
-
);
|
|
1107
|
-
}
|
|
1108
|
-
})
|
|
1109
|
-
.catch((e) => {
|
|
1110
|
-
logger.error(
|
|
1111
|
-
"Failed to collect sorted results from self. " + e?.message
|
|
1112
|
-
);
|
|
1113
|
-
peerBufferMap.delete(peer);
|
|
1114
|
-
})
|
|
1115
|
-
);
|
|
1116
|
-
} else {
|
|
1117
|
-
// Fetch remotely
|
|
1118
|
-
promises.push(
|
|
1119
|
-
this._query
|
|
1120
|
-
.request(collectRequest, {
|
|
1121
|
-
...options,
|
|
1122
|
-
signal: controller.signal,
|
|
1123
|
-
priority: 1,
|
|
1124
|
-
mode: new SilentDelivery({ to: [peer], redundancy: 1 })
|
|
1125
|
-
})
|
|
1126
|
-
.then((response) =>
|
|
1127
|
-
introduceEntries(response, this.type, this._sync, options)
|
|
1128
|
-
.then((responses) => {
|
|
1129
|
-
responses.map((response) => {
|
|
1130
|
-
resultsLeft += Number(response.response.kept);
|
|
1131
|
-
if (!response.from) {
|
|
1132
|
-
logger.error("Missing from for sorted query");
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
if (response.response.results.length === 0) {
|
|
1137
|
-
if (peerBufferMap.get(peer)?.buffer.length === 0) {
|
|
1138
|
-
peerBufferMap.delete(peer); // No more results
|
|
1139
|
-
}
|
|
1140
|
-
} else {
|
|
1141
|
-
const peerBuffer = peerBufferMap.get(peer);
|
|
1142
|
-
if (!peerBuffer) {
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
peerBuffer.kept = Number(response.response.kept);
|
|
1146
|
-
peerBuffer.buffer.push(
|
|
1147
|
-
...response.response.results
|
|
1148
|
-
.filter(
|
|
1149
|
-
(x) =>
|
|
1150
|
-
!visited.has(
|
|
1151
|
-
asString(this.indexByResolver(x.value))
|
|
1152
|
-
)
|
|
1153
|
-
)
|
|
1154
|
-
.map((x) => {
|
|
1155
|
-
visited.add(
|
|
1156
|
-
asString(this.indexByResolver(x.value))
|
|
1157
|
-
);
|
|
1158
|
-
return {
|
|
1159
|
-
value: { value: x.value },
|
|
1160
|
-
context: x.context,
|
|
1161
|
-
from: response.from!
|
|
1162
|
-
};
|
|
1163
|
-
})
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
});
|
|
1167
|
-
})
|
|
1168
|
-
.catch((e) => {
|
|
1169
|
-
logger.error(
|
|
1170
|
-
"Failed to collect sorted results from: " +
|
|
1171
|
-
peer +
|
|
1172
|
-
". " +
|
|
1173
|
-
e?.message
|
|
1174
|
-
);
|
|
1175
|
-
peerBufferMap.delete(peer);
|
|
1176
|
-
})
|
|
1177
|
-
)
|
|
1178
|
-
);
|
|
1179
|
-
}
|
|
1180
|
-
} else {
|
|
1181
|
-
resultsLeft += peerBufferMap.get(peer)?.kept || 0;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return (fetchPromise = Promise.all(promises).then(() => {
|
|
1185
|
-
return resultsLeft === 0; // 0 results left to fetch and 0 pending results
|
|
1186
|
-
}));
|
|
1187
|
-
};
|
|
1188
|
-
|
|
1189
|
-
const next = async (n: number) => {
|
|
1190
|
-
if (n < 0) {
|
|
1191
|
-
throw new Error("Expecting to fetch a positive amount of element");
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
if (n === 0) {
|
|
1195
|
-
return [];
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// TODO everything below is not very optimized
|
|
1199
|
-
const fetchedAll = await fetchAtLeast(n);
|
|
1200
|
-
|
|
1201
|
-
// get n next top entries, shift and pull more results
|
|
1202
|
-
const results = await resolvedSort(
|
|
1203
|
-
peerBuffers(),
|
|
1204
|
-
this._toIndex,
|
|
1205
|
-
queryRequest.sort
|
|
1206
|
-
);
|
|
1207
|
-
|
|
1208
|
-
const pendingMoreResults = n < results.length;
|
|
1209
|
-
|
|
1210
|
-
const batch = results.splice(0, n);
|
|
1211
|
-
|
|
1212
|
-
for (const result of batch) {
|
|
1213
|
-
const arr = peerBufferMap.get(result.from.hashcode());
|
|
1214
|
-
if (!arr) {
|
|
1215
|
-
logger.error("Unexpected empty result buffer");
|
|
1216
|
-
continue;
|
|
1217
|
-
}
|
|
1218
|
-
const idx = arr.buffer.findIndex((x) => x == result);
|
|
1219
|
-
if (idx >= 0) {
|
|
1220
|
-
arr.buffer.splice(idx, 1);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
done = fetchedAll && !pendingMoreResults;
|
|
1225
|
-
return dedup(
|
|
1226
|
-
batch.map((x) => x.value.value),
|
|
1227
|
-
this.indexByResolver
|
|
1228
|
-
);
|
|
1229
|
-
};
|
|
1230
|
-
|
|
1231
|
-
const close = async () => {
|
|
1232
|
-
controller.abort(new AbortError("Iterator closed"));
|
|
1233
|
-
|
|
1234
|
-
const closeRequest = new CloseIteratorRequest({ id: queryRequest.id });
|
|
1235
|
-
const promises: Promise<any>[] = [];
|
|
1236
|
-
for (const [peer, buffer] of peerBufferMap) {
|
|
1237
|
-
if (buffer.kept === 0) {
|
|
1238
|
-
peerBufferMap.delete(peer);
|
|
1239
|
-
continue;
|
|
1240
|
-
}
|
|
1241
|
-
// Fetch locally?
|
|
1242
|
-
if (peer === this.node.identity.publicKey.hashcode()) {
|
|
1243
|
-
promises.push(
|
|
1244
|
-
this.processCloseIteratorRequest(
|
|
1245
|
-
closeRequest,
|
|
1246
|
-
this.node.identity.publicKey
|
|
1247
|
-
)
|
|
1248
|
-
);
|
|
1249
|
-
} else {
|
|
1250
|
-
// Close remote
|
|
1251
|
-
promises.push(
|
|
1252
|
-
this._query.send(closeRequest, {
|
|
1253
|
-
...options,
|
|
1254
|
-
mode: new SilentDelivery({ to: [peer], redundancy: 1 })
|
|
1255
|
-
})
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
await Promise.all(promises);
|
|
1260
|
-
};
|
|
1261
|
-
|
|
1262
|
-
return {
|
|
1263
|
-
close,
|
|
1264
|
-
next,
|
|
1265
|
-
done: () => done
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
}
|