@peerbit/document 9.4.2 → 9.4.3-fb47029

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/search.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  sha256Base64Sync,
9
9
  } from "@peerbit/crypto";
10
10
  import * as types from "@peerbit/document-interface";
11
+ import { CachedIndex, type QueryCacheOptions } from "@peerbit/indexer-cache";
11
12
  import * as indexerTypes from "@peerbit/indexer-interface";
12
13
  import { HashmapIndex } from "@peerbit/indexer-simple";
13
14
  import { BORSH_ENCODING, type Encoding, Entry } from "@peerbit/log";
@@ -25,12 +26,15 @@ import {
25
26
  type ReplicationDomain,
26
27
  SharedLog,
27
28
  } from "@peerbit/shared-log";
28
- import { SilentDelivery } from "@peerbit/stream-interface";
29
+ import { DataMessage, SilentDelivery } from "@peerbit/stream-interface";
29
30
  import { AbortError, waitFor } from "@peerbit/time";
30
31
  import { concat, fromString } from "uint8arrays";
31
32
  import { copySerialization } from "./borsh.js";
32
33
  import { MAX_BATCH_SIZE } from "./constants.js";
34
+ import type { QueryPredictor } from "./most-common-query-predictor.js";
35
+ import MostCommonQueryPredictor from "./most-common-query-predictor.js";
33
36
  import { type Operation, isPutOperation } from "./operation.js";
37
+ import { Prefetch } from "./prefetch.js";
34
38
  import type { ExtractArgs } from "./program.js";
35
39
  import { ResumableIterators } from "./resumable-iterator.js";
36
40
 
@@ -43,7 +47,7 @@ type BufferedResult<T, I extends Record<string, any>> = {
43
47
  from: PublicSignKey;
44
48
  };
45
49
 
46
- export type RemoteQueryOptions<R, D> = RPCRequestAllOptions<R> & {
50
+ export type RemoteQueryOptions<Q, R, D> = RPCRequestAllOptions<Q, R> & {
47
51
  replicate?: boolean;
48
52
  minAge?: number;
49
53
  throwOnMissing?: boolean;
@@ -58,7 +62,13 @@ export type RemoteQueryOptions<R, D> = RPCRequestAllOptions<R> & {
58
62
  eager?: boolean; // whether to query newly joined peers before they have matured
59
63
  };
60
64
  export type QueryOptions<R, D, Resolve extends boolean | undefined> = {
61
- remote?: boolean | RemoteQueryOptions<types.AbstractSearchResult, D>;
65
+ remote?:
66
+ | boolean
67
+ | RemoteQueryOptions<
68
+ types.AbstractSearchRequest,
69
+ types.AbstractSearchResult,
70
+ D
71
+ >;
62
72
  local?: boolean;
63
73
  resolve?: Resolve;
64
74
  };
@@ -141,7 +151,7 @@ const introduceEntries = async <
141
151
  R extends types.SearchRequest | types.SearchRequestIndexed,
142
152
  >(
143
153
  queryRequest: R,
144
- responses: RPCResponse<types.AbstractSearchResult>[],
154
+ responses: { response: types.AbstractSearchResult; from?: PublicSignKey }[],
145
155
  documentType: AbstractType<T>,
146
156
  indexedType: AbstractType<I>,
147
157
  sync: (
@@ -259,6 +269,19 @@ const isTransformerWithFunction = <T, I>(
259
269
  return (options as TransformerAsFunction<T, I>).transform != null;
260
270
  };
261
271
 
272
+ export type PrefetchOptions = {
273
+ predictor?: QueryPredictor;
274
+ ttl: number;
275
+ accumulator: Prefetch;
276
+
277
+ /* When `true` we assume every peer supports prefetch routing,
278
+ * so it is safe to drop SearchRequests that the predictor marks
279
+ * as `ignore === true`.
280
+ *
281
+ * Default: `false` – be conservative.
282
+ */
283
+ strict?: boolean;
284
+ };
262
285
  export type OpenOptions<
263
286
  T,
264
287
  I,
@@ -281,9 +304,13 @@ export type OpenOptions<
281
304
  ) => Promise<void>;
282
305
  indexBy?: string | string[];
283
306
  transform?: TransformOptions<T, I>;
284
- cacheSize?: number;
307
+ cache?: {
308
+ resolver?: number;
309
+ query?: QueryCacheOptions;
310
+ };
285
311
  compatibility: 6 | 7 | 8 | undefined;
286
312
  maybeOpen: (value: T & Program) => Promise<T & Program>;
313
+ prefetch?: boolean | Partial<PrefetchOptions>;
287
314
  };
288
315
 
289
316
  type IndexableClass<I> = new (
@@ -332,6 +359,7 @@ export class DocumentIndex<
332
359
  private indexByResolver: (obj: any) => string | Uint8Array;
333
360
  index: indexerTypes.Index<WithContext<I>>;
334
361
  private _resumableIterators: ResumableIterators<WithContext<I>>;
362
+ private _prefetch?: PrefetchOptions | undefined;
335
363
 
336
364
  compatibility: 6 | 7 | 8 | undefined;
337
365
 
@@ -351,6 +379,9 @@ export class DocumentIndex<
351
379
  private _resolverCache?: Cache<T>;
352
380
  private isProgramValued: boolean;
353
381
  private _maybeOpen: (value: T & Program) => Promise<T & Program>;
382
+ private canSearch?: CanSearch;
383
+ private canRead?: CanRead<I>;
384
+ private _joinListener?: (e: { detail: PublicSignKey }) => Promise<void>;
354
385
 
355
386
  private _resultQueue: Map<
356
387
  string,
@@ -386,6 +417,21 @@ export class DocumentIndex<
386
417
  }
387
418
  async open(properties: OpenOptions<T, I, D>) {
388
419
  this._log = properties.log;
420
+ let prefectOptions =
421
+ typeof properties.prefetch === "object"
422
+ ? properties.prefetch
423
+ : properties.prefetch
424
+ ? {}
425
+ : undefined;
426
+ this._prefetch = prefectOptions
427
+ ? {
428
+ ...prefectOptions,
429
+ predictor:
430
+ prefectOptions.predictor || new MostCommonQueryPredictor(3),
431
+ ttl: prefectOptions.ttl ?? 5e3,
432
+ accumulator: prefectOptions.accumulator || new Prefetch(),
433
+ }
434
+ : undefined;
389
435
 
390
436
  this.documentType = properties.documentType;
391
437
  this.indexedTypeIsDocumentType =
@@ -393,6 +439,8 @@ export class DocumentIndex<
393
439
  properties.transform?.type === properties.documentType;
394
440
 
395
441
  this.compatibility = properties.compatibility;
442
+ this.canRead = properties.canRead;
443
+ this.canSearch = properties.canSearch;
396
444
 
397
445
  @variant(0)
398
446
  class IndexedClassWithContext {
@@ -418,7 +466,30 @@ export class DocumentIndex<
418
466
  this.isProgramValued = isSubclassOf(this.documentType, Program);
419
467
  this.dbType = properties.dbType;
420
468
  this._resultQueue = new Map();
421
- this._sync = (request, results) => properties.replicate(request, results);
469
+ this._sync = (request, results) => {
470
+ let rq: types.SearchRequest | types.SearchRequestIndexed;
471
+ let rs: types.Results<
472
+ types.ResultTypeFromRequest<
473
+ types.SearchRequest | types.SearchRequestIndexed,
474
+ T,
475
+ I
476
+ >
477
+ >;
478
+ if (request instanceof types.PredictedSearchRequest) {
479
+ rq = request.request;
480
+ rs = request.results;
481
+ } else {
482
+ rq = request;
483
+ rs = results as types.Results<
484
+ types.ResultTypeFromRequest<
485
+ types.SearchRequest | types.SearchRequestIndexed,
486
+ T,
487
+ I
488
+ >
489
+ >;
490
+ }
491
+ return properties.replicate(rq, rs);
492
+ };
422
493
 
423
494
  const transformOptions = properties.transform;
424
495
  this.transformer = transformOptions
@@ -437,9 +508,9 @@ export class DocumentIndex<
437
508
  this._valueEncoding = BORSH_ENCODING(this.documentType);
438
509
 
439
510
  this._resolverCache =
440
- properties.cacheSize === 0
511
+ properties.cache?.resolver === 0
441
512
  ? undefined
442
- : new Cache({ max: properties.cacheSize ?? 100 }); // TODO choose limit better by default (adaptive)
513
+ : new Cache({ max: properties.cache?.resolver ?? 100 }); // TODO choose limit better by default (adaptive)
443
514
 
444
515
  this.index =
445
516
  (await (
@@ -455,6 +526,10 @@ export class DocumentIndex<
455
526
  /* maxBatchSize: MAX_BATCH_SIZE */
456
527
  })) || new HashmapIndex<WithContext<I>>();
457
528
 
529
+ if (properties.cache?.query) {
530
+ this.index = new CachedIndex(this.index, properties.cache.query);
531
+ }
532
+
458
533
  this._resumableIterators = new ResumableIterators(this.index);
459
534
  this._maybeOpen = properties.maybeOpen;
460
535
  if (this.isProgramValued) {
@@ -465,49 +540,103 @@ export class DocumentIndex<
465
540
  topic: sha256Base64Sync(
466
541
  concat([this._log.log.id, fromString("/document")]),
467
542
  ),
468
- responseHandler: async (query, ctx) => {
469
- if (!ctx.from) {
470
- logger.info("Receieved query without from");
471
- return;
472
- }
543
+ responseHandler: this.queryRPCResponseHandler.bind(this),
544
+ responseType: types.AbstractSearchResult,
545
+ queryType: types.AbstractSearchRequest,
546
+ });
547
+ }
473
548
 
474
- if (
475
- properties.canSearch &&
476
- (query instanceof types.SearchRequest ||
477
- query instanceof types.CollectNextRequest) &&
478
- !(await properties.canSearch(
479
- query as types.SearchRequest | types.CollectNextRequest,
480
- ctx.from,
481
- ))
482
- ) {
483
- return new types.NoAccess();
484
- }
549
+ get prefetch() {
550
+ return this._prefetch;
551
+ }
485
552
 
486
- if (query instanceof types.CloseIteratorRequest) {
487
- this.processCloseIteratorRequest(query, ctx.from);
488
- } else {
489
- const results = await this.processQuery(
490
- query as
491
- | types.SearchRequest
492
- | types.SearchRequestIndexed
493
- | types.CollectNextRequest,
494
- ctx.from,
495
- false,
496
- {
497
- canRead: properties.canRead,
498
- },
499
- );
553
+ private async queryRPCResponseHandler(
554
+ query: types.AbstractSearchRequest,
555
+ ctx: { from?: PublicSignKey; message: DataMessage },
556
+ ) {
557
+ if (!ctx.from) {
558
+ logger.info("Receieved query without from");
559
+ return;
560
+ }
561
+ if (query instanceof types.PredictedSearchRequest) {
562
+ // put results in a waiting cache so that we eventually in the future will query a matching thing, we already have results available
563
+ this._prefetch?.accumulator.add(
564
+ {
565
+ message: ctx.message,
566
+ response: query,
567
+ from: ctx.from,
568
+ },
569
+ ctx.from!.hashcode(),
570
+ );
571
+ return;
572
+ }
500
573
 
501
- return new types.Results({
502
- // Even if results might have length 0, respond, because then we now at least there are no matching results
503
- results: results.results,
504
- kept: results.kept,
505
- });
574
+ if (
575
+ this.prefetch?.predictor &&
576
+ (query instanceof types.SearchRequest ||
577
+ query instanceof types.SearchRequestIndexed)
578
+ ) {
579
+ const { ignore } = this.prefetch.predictor.onRequest(query, {
580
+ from: ctx.from!,
581
+ });
582
+
583
+ if (ignore) {
584
+ if (this.prefetch.strict) {
585
+ return;
506
586
  }
587
+ }
588
+ }
589
+
590
+ return this.handleSearchRequest(
591
+ query as
592
+ | types.SearchRequest
593
+ | types.SearchRequestIndexed
594
+ | types.CollectNextRequest,
595
+ {
596
+ from: ctx.from!,
507
597
  },
508
- responseType: types.AbstractSearchResult,
509
- queryType: types.AbstractSearchRequest,
510
- });
598
+ );
599
+ }
600
+ private async handleSearchRequest(
601
+ query:
602
+ | types.SearchRequest
603
+ | types.SearchRequestIndexed
604
+ | types.CollectNextRequest,
605
+ ctx: { from: PublicSignKey },
606
+ ) {
607
+ if (
608
+ this.canSearch &&
609
+ (query instanceof types.SearchRequest ||
610
+ query instanceof types.CollectNextRequest) &&
611
+ !(await this.canSearch(
612
+ query as types.SearchRequest | types.CollectNextRequest,
613
+ ctx.from,
614
+ ))
615
+ ) {
616
+ return new types.NoAccess();
617
+ }
618
+
619
+ if (query instanceof types.CloseIteratorRequest) {
620
+ this.processCloseIteratorRequest(query, ctx.from);
621
+ } else {
622
+ const results = await this.processQuery(
623
+ query as
624
+ | types.SearchRequest
625
+ | types.SearchRequestIndexed
626
+ | types.CollectNextRequest,
627
+ ctx.from,
628
+ false,
629
+ {
630
+ canRead: this.canRead,
631
+ },
632
+ );
633
+
634
+ return new types.Results({
635
+ // Even if results might have length 0, respond, because then we now at least there are no matching results
636
+ results: results.results,
637
+ kept: results.kept,
638
+ });
639
+ }
511
640
  }
512
641
 
513
642
  async afterOpen(): Promise<void> {
@@ -531,6 +660,35 @@ export class DocumentIndex<
531
660
  this._resolverProgramCache!.set(id.primitive, programValue.value as T);
532
661
  }
533
662
  }
663
+
664
+ if (this.prefetch?.predictor) {
665
+ const predictor = this.prefetch.predictor;
666
+ this._joinListener = async (e: { detail: PublicSignKey }) => {
667
+ // create an iterator and send the peer the results
668
+ let request = predictor.predictedQuery(e.detail);
669
+
670
+ if (!request) {
671
+ return;
672
+ }
673
+ const results = await this.handleSearchRequest(request, {
674
+ from: e.detail,
675
+ });
676
+
677
+ if (results instanceof types.Results) {
678
+ // start a resumable iterator for the peer
679
+ const query = new types.PredictedSearchRequest({
680
+ id: request.id,
681
+ request,
682
+ results,
683
+ });
684
+ await this._query.send(query, {
685
+ mode: new SilentDelivery({ to: [e.detail], redundancy: 1 }),
686
+ });
687
+ }
688
+ };
689
+ this._query.events.addEventListener("join", this._joinListener);
690
+ }
691
+
534
692
  return super.afterOpen();
535
693
  }
536
694
  async getPending(cursorId: string): Promise<number | undefined> {
@@ -545,6 +703,7 @@ export class DocumentIndex<
545
703
  async close(from?: Program): Promise<boolean> {
546
704
  const closed = await super.close(from);
547
705
  if (closed) {
706
+ this._query.events.removeEventListener("join", this._joinListener);
548
707
  this.clearAllResultQueues();
549
708
  await this.index.stop?.();
550
709
  }
@@ -660,15 +819,13 @@ export class DocumentIndex<
660
819
  ): Promise<types.Results<RT>[] | undefined> {
661
820
  let coercedOptions = options;
662
821
  if (options?.remote && typeof options.remote !== "boolean") {
663
- {
664
- coercedOptions = {
665
- ...options,
666
- remote: {
667
- ...options.remote,
668
- strategy: options.remote?.strategy ?? "fallback",
669
- },
670
- };
671
- }
822
+ coercedOptions = {
823
+ ...options,
824
+ remote: {
825
+ ...options.remote,
826
+ strategy: options.remote?.strategy ?? "fallback",
827
+ },
828
+ };
672
829
  } else if (options?.remote === undefined) {
673
830
  coercedOptions = {
674
831
  ...options,
@@ -990,6 +1147,10 @@ export class DocumentIndex<
990
1147
  }
991
1148
  }
992
1149
 
1150
+ get countIteratorsInProgress() {
1151
+ return this._resumableIterators.queues.size;
1152
+ }
1153
+
993
1154
  private clearAllResultQueues() {
994
1155
  for (const [key, queue] of this._resultQueue) {
995
1156
  clearTimeout(queue.timeout);
@@ -1025,8 +1186,13 @@ export class DocumentIndex<
1025
1186
  options?: QueryDetailedOptions<T, D, boolean | undefined>,
1026
1187
  ): Promise<types.Results<RT>[]> {
1027
1188
  const local = typeof options?.local === "boolean" ? options?.local : true;
1028
- let remote: RemoteQueryOptions<types.AbstractSearchResult, D> | undefined =
1029
- undefined;
1189
+ let remote:
1190
+ | RemoteQueryOptions<
1191
+ types.AbstractSearchRequest,
1192
+ types.AbstractSearchResult,
1193
+ D
1194
+ >
1195
+ | undefined = undefined;
1030
1196
  if (typeof options?.remote === "boolean") {
1031
1197
  if (options?.remote) {
1032
1198
  remote = {};
@@ -1066,6 +1232,11 @@ export class DocumentIndex<
1066
1232
 
1067
1233
  let resolved: types.Results<types.ResultTypeFromRequest<R, T, I>>[] = [];
1068
1234
  if (remote && (remote.strategy !== "fallback" || allResults.length === 0)) {
1235
+ if (queryRequest instanceof types.CloseIteratorRequest) {
1236
+ // don't wait for responses
1237
+ throw new Error("Unexpected");
1238
+ }
1239
+
1069
1240
  const replicatorGroups = await this._log.getCover(
1070
1241
  remote.domain ?? { args: undefined },
1071
1242
  {
@@ -1075,9 +1246,11 @@ export class DocumentIndex<
1075
1246
  );
1076
1247
 
1077
1248
  if (replicatorGroups) {
1078
- const groupHashes: string[][] = replicatorGroups.map((x) => [x]);
1079
1249
  const responseHandler = async (
1080
- results: RPCResponse<types.AbstractSearchResult>[],
1250
+ results: {
1251
+ response: types.AbstractSearchResult;
1252
+ from?: PublicSignKey;
1253
+ }[],
1081
1254
  ) => {
1082
1255
  const resultInitialized = await introduceEntries(
1083
1256
  queryRequest,
@@ -1091,19 +1264,105 @@ export class DocumentIndex<
1091
1264
  resolved.push(r.response);
1092
1265
  }
1093
1266
  };
1267
+
1268
+ let extraPromises: Promise<void>[] | undefined = undefined;
1269
+
1270
+ const groupHashes: string[][] = replicatorGroups
1271
+ .filter((hash) => {
1272
+ if (hash === this.node.identity.publicKey.hashcode()) {
1273
+ return false;
1274
+ }
1275
+ const resultAlready = this._prefetch?.accumulator.consume(
1276
+ queryRequest,
1277
+ hash,
1278
+ );
1279
+ if (resultAlready) {
1280
+ (extraPromises || (extraPromises = [])).push(
1281
+ (async () => {
1282
+ let from = await this.node.services.pubsub.getPublicKey(hash);
1283
+ if (from) {
1284
+ return responseHandler([
1285
+ {
1286
+ response: resultAlready.response.results,
1287
+ from,
1288
+ },
1289
+ ]);
1290
+ }
1291
+ })(),
1292
+ );
1293
+ return false;
1294
+ }
1295
+ return true;
1296
+ })
1297
+ .map((x) => [x]);
1298
+
1299
+ extraPromises && (await Promise.all(extraPromises));
1300
+ let tearDown: (() => void) | undefined = undefined;
1301
+ const search = this;
1302
+
1094
1303
  try {
1095
- if (queryRequest instanceof types.CloseIteratorRequest) {
1096
- // don't wait for responses
1097
- await this._query.request(queryRequest, { mode: remote!.mode });
1098
- } else {
1099
- await queryAll(
1304
+ groupHashes.length > 0 &&
1305
+ (await queryAll(
1100
1306
  this._query,
1101
1307
  groupHashes,
1102
1308
  queryRequest,
1103
1309
  responseHandler,
1104
- remote,
1105
- );
1106
- }
1310
+ search._prefetch?.accumulator
1311
+ ? {
1312
+ ...remote,
1313
+ responseInterceptor(fn) {
1314
+ const listener = (evt: {
1315
+ detail: {
1316
+ consumable: RPCResponse<
1317
+ types.PredictedSearchRequest<any>
1318
+ >;
1319
+ };
1320
+ }) => {
1321
+ const consumable =
1322
+ search._prefetch?.accumulator.consume(
1323
+ queryRequest,
1324
+ evt.detail.consumable.from!.hashcode(),
1325
+ );
1326
+
1327
+ if (consumable) {
1328
+ fn({
1329
+ message: consumable.message,
1330
+ response: consumable.response.results,
1331
+ from: consumable.from,
1332
+ });
1333
+ }
1334
+ };
1335
+
1336
+ for (const groups of groupHashes) {
1337
+ for (const hash of groups) {
1338
+ const consumable =
1339
+ search._prefetch?.accumulator.consume(
1340
+ queryRequest,
1341
+ hash,
1342
+ );
1343
+ if (consumable) {
1344
+ fn({
1345
+ message: consumable.message,
1346
+ response: consumable.response.results,
1347
+ from: consumable.from,
1348
+ });
1349
+ }
1350
+ }
1351
+ }
1352
+ search.prefetch?.accumulator.addEventListener(
1353
+ "add",
1354
+ listener,
1355
+ );
1356
+ tearDown = () => {
1357
+ search.prefetch?.accumulator.removeEventListener(
1358
+ "add",
1359
+ listener,
1360
+ );
1361
+ };
1362
+ },
1363
+ }
1364
+ : remote,
1365
+ ));
1107
1366
  } catch (error) {
1108
1367
  if (error instanceof MissingResponsesError) {
1109
1368
  logger.warn("Did not reciveve responses from all shard");
@@ -1113,6 +1372,8 @@ export class DocumentIndex<
1113
1372
  } else {
1114
1373
  throw error;
1115
1374
  }
1375
+ } finally {
1376
+ tearDown && (tearDown as any)();
1116
1377
  }
1117
1378
  } else {
1118
1379
  // TODO send without direction out to the world? or just assume we can insert?
@@ -1400,6 +1661,7 @@ export class DocumentIndex<
1400
1661
 
1401
1662
  // TODO buffer more than deleted?
1402
1663
  // TODO batch to multiple 'to's
1664
+
1403
1665
  const collectRequest = new types.CollectNextRequest({
1404
1666
  id: queryRequestCoerced.id,
1405
1667
  amount: n - buffer.buffer.length,
@@ -1464,9 +1726,21 @@ export class DocumentIndex<
1464
1726
  );
1465
1727
  } else {
1466
1728
  // Fetch remotely
1729
+ const idTranslation =
1730
+ this._prefetch?.accumulator.getTranslationMap(
1731
+ queryRequestCoerced,
1732
+ );
1733
+ let remoteCollectRequest: types.CollectNextRequest = collectRequest;
1734
+ if (idTranslation) {
1735
+ remoteCollectRequest = new types.CollectNextRequest({
1736
+ id: idTranslation.get(peer) || collectRequest.id,
1737
+ amount: collectRequest.amount,
1738
+ });
1739
+ }
1740
+
1467
1741
  promises.push(
1468
1742
  this._query
1469
- .request(collectRequest, {
1743
+ .request(remoteCollectRequest, {
1470
1744
  ...options,
1471
1745
  signal: controller.signal,
1472
1746
  priority: 1,
@@ -1627,6 +1901,7 @@ export class DocumentIndex<
1627
1901
  const closeRequest = new types.CloseIteratorRequest({
1628
1902
  id: queryRequestCoerced.id,
1629
1903
  });
1904
+ this.prefetch?.accumulator.clear(queryRequestCoerced);
1630
1905
  const promises: Promise<any>[] = [];
1631
1906
  for (const [peer, buffer] of peerBufferMap) {
1632
1907
  if (buffer.kept === 0) {