@peerbit/document 6.0.7 → 7.0.0-3a75d6e

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