@peerbit/document 9.13.10 → 10.0.0

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
@@ -38,7 +38,9 @@ import { copySerialization } from "./borsh.js";
38
38
  import { MAX_BATCH_SIZE } from "./constants.js";
39
39
  import type { DocumentEvents, DocumentsChange } from "./events.js";
40
40
  import type { QueryPredictor } from "./most-common-query-predictor.js";
41
- import MostCommonQueryPredictor from "./most-common-query-predictor.js";
41
+ import MostCommonQueryPredictor, {
42
+ idAgnosticQueryKey,
43
+ } from "./most-common-query-predictor.js";
42
44
  import { type Operation, isPutOperation } from "./operation.js";
43
45
  import { Prefetch } from "./prefetch.js";
44
46
  import type { ExtractArgs } from "./program.js";
@@ -95,11 +97,16 @@ export type UpdateCallbacks<
95
97
  * Unified update options for iterate()/search()/get() and hooks.
96
98
  * If you pass `true`, defaults to `{ merge: "sorted" }`.
97
99
  */
100
+ export type UpdateModeShortcut = "local" | "remote" | "all";
101
+
98
102
  export type UpdateOptions<T, I, Resolve extends boolean | undefined> =
99
103
  | boolean
104
+ | UpdateModeShortcut
100
105
  | ({
101
106
  /** Live update behavior. Only sorted merging is supported; optional filter can mutate/ignore events. */
102
107
  merge?: UpdateMergeStrategy<T, I, Resolve>;
108
+ /** Request push-style notifications backed by the prefetch channel. */
109
+ push?: boolean;
103
110
  } & UpdateCallbacks<T, I, Resolve>);
104
111
 
105
112
  export type JoiningTargets = {
@@ -221,7 +228,7 @@ export type ResultsIterator<T> = {
221
228
  next: (number: number) => Promise<T[]>;
222
229
  done: () => boolean;
223
230
  all: () => Promise<T[]>;
224
- pending: () => number | undefined;
231
+ pending: () => MaybePromise<number | undefined>;
225
232
  first: () => Promise<T | undefined>;
226
233
  [Symbol.asyncIterator]: () => AsyncIterator<T>;
227
234
  };
@@ -255,11 +262,21 @@ type ExtractResolveFromOptions<O> =
255
262
  : true; // if R isn't QueryLike at all, default to true
256
263
 
257
264
  const coerceQuery = <Resolve extends boolean | undefined>(
258
- query: types.SearchRequest | types.SearchRequestIndexed | QueryLike,
265
+ query:
266
+ | types.SearchRequest
267
+ | types.SearchRequestIndexed
268
+ | types.IterationRequest
269
+ | QueryLike,
259
270
  options?: QueryOptions<any, any, any, Resolve>,
260
- ) => {
261
- let replicate =
271
+ compatibility?: number,
272
+ ):
273
+ | types.SearchRequest
274
+ | types.SearchRequestIndexed
275
+ | types.IterationRequest => {
276
+ const replicate =
262
277
  typeof options?.remote !== "boolean" ? options?.remote?.replicate : false;
278
+ const shouldResolve = options?.resolve !== false;
279
+ const useLegacyRequests = compatibility != null && compatibility <= 9;
263
280
 
264
281
  if (
265
282
  query instanceof types.SearchRequestIndexed &&
@@ -269,29 +286,66 @@ const coerceQuery = <Resolve extends boolean | undefined>(
269
286
  query.replicate = true;
270
287
  return query;
271
288
  }
272
- if (query instanceof types.SearchRequest) {
289
+
290
+ if (
291
+ query instanceof types.SearchRequest ||
292
+ query instanceof types.SearchRequestIndexed
293
+ ) {
294
+ return query;
295
+ }
296
+
297
+ if (query instanceof types.IterationRequest) {
298
+ if (useLegacyRequests) {
299
+ if (query.resolve === false) {
300
+ return new types.SearchRequestIndexed({
301
+ query: query.query,
302
+ sort: query.sort,
303
+ fetch: query.fetch,
304
+ replicate: query.replicate ?? replicate,
305
+ });
306
+ }
307
+ return new types.SearchRequest({
308
+ query: query.query,
309
+ sort: query.sort,
310
+ fetch: query.fetch,
311
+ });
312
+ }
273
313
  return query;
274
314
  }
275
315
 
276
316
  const queryObject = query as QueryLike;
277
317
 
278
- return options?.resolve || options?.resolve == null
279
- ? new types.SearchRequest({
318
+ if (useLegacyRequests) {
319
+ if (shouldResolve) {
320
+ return new types.SearchRequest({
280
321
  query: indexerTypes.toQuery(queryObject.query),
281
- sort: indexerTypes.toSort(query.sort),
282
- })
283
- : new types.SearchRequestIndexed({
284
- query: indexerTypes.toQuery(queryObject.query),
285
- sort: indexerTypes.toSort(query.sort),
286
- replicate,
322
+ sort: indexerTypes.toSort(queryObject.sort),
287
323
  });
324
+ }
325
+ return new types.SearchRequestIndexed({
326
+ query: indexerTypes.toQuery(queryObject.query),
327
+ sort: indexerTypes.toSort(queryObject.sort),
328
+ replicate,
329
+ });
330
+ }
331
+
332
+ return new types.IterationRequest({
333
+ query: indexerTypes.toQuery(queryObject.query),
334
+ sort: indexerTypes.toSort(queryObject.sort),
335
+ fetch: 10,
336
+ resolve: shouldResolve,
337
+ replicate: shouldResolve ? false : replicate,
338
+ });
288
339
  };
289
340
 
290
341
  const introduceEntries = async <
291
342
  T,
292
343
  I,
293
344
  D,
294
- R extends types.SearchRequest | types.SearchRequestIndexed,
345
+ R extends
346
+ | types.SearchRequest
347
+ | types.SearchRequestIndexed
348
+ | types.IterationRequest,
295
349
  >(
296
350
  queryRequest: R,
297
351
  responses: { response: types.AbstractSearchResult; from?: PublicSignKey }[],
@@ -347,6 +401,37 @@ const dedup = <T>(
347
401
  return dedup;
348
402
  };
349
403
 
404
+ type AnyIterationRequest =
405
+ | types.SearchRequest
406
+ | types.SearchRequestIndexed
407
+ | types.IterationRequest;
408
+
409
+ const resolvesDocuments = (req?: AnyIterationRequest) => {
410
+ if (!req) {
411
+ return true;
412
+ }
413
+ if (req instanceof types.SearchRequestIndexed) {
414
+ return false;
415
+ }
416
+ if (req instanceof types.IterationRequest) {
417
+ return req.resolve !== false;
418
+ }
419
+ return true;
420
+ };
421
+
422
+ const replicatesIndex = (req?: AnyIterationRequest) => {
423
+ if (!req) {
424
+ return false;
425
+ }
426
+ if (req instanceof types.SearchRequestIndexed) {
427
+ return req.replicate === true;
428
+ }
429
+ if (req instanceof types.IterationRequest) {
430
+ return req.replicate === true;
431
+ }
432
+ return false;
433
+ };
434
+
350
435
  function isSubclassOf(
351
436
  SubClass: AbstractType<any>,
352
437
  SuperClass: AbstractType<any>,
@@ -365,11 +450,15 @@ function isSubclassOf(
365
450
  }
366
451
 
367
452
  const DEFAULT_TIMEOUT = 1e4;
453
+ const DISCOVER_TIMEOUT_FALLBACK = 500;
368
454
 
369
455
  const DEFAULT_INDEX_BY = "id";
370
456
 
371
457
  export type CanSearch = (
372
- request: types.SearchRequest | types.CollectNextRequest,
458
+ request:
459
+ | types.SearchRequest
460
+ | types.IterationRequest
461
+ | types.CollectNextRequest,
373
462
  from: PublicSignKey,
374
463
  ) => Promise<boolean> | boolean;
375
464
 
@@ -443,10 +532,15 @@ export type OpenOptions<
443
532
  canRead?: CanRead<I>;
444
533
  canSearch?: CanSearch;
445
534
  replicate: (
446
- request: types.SearchRequest | types.SearchRequestIndexed,
535
+ request:
536
+ | types.SearchRequest
537
+ | types.SearchRequestIndexed
538
+ | types.IterationRequest,
447
539
  results: types.Results<
448
540
  types.ResultTypeFromRequest<
449
- types.SearchRequest | types.SearchRequestIndexed,
541
+ | types.SearchRequest
542
+ | types.SearchRequestIndexed
543
+ | types.IterationRequest,
450
544
  T,
451
545
  I
452
546
  >
@@ -458,7 +552,7 @@ export type OpenOptions<
458
552
  resolver?: number;
459
553
  query?: QueryCacheOptions;
460
554
  };
461
- compatibility: 6 | 7 | 8 | undefined;
555
+ compatibility: 6 | 7 | 8 | 9 | undefined;
462
556
  maybeOpen: (value: T & Program) => Promise<T & Program>;
463
557
  prefetch?: boolean | Partial<PrefetchOptions>;
464
558
  includeIndexed?: boolean; // if true, indexed representations will always be included in the search results
@@ -518,7 +612,7 @@ export class DocumentIndex<
518
612
  private _prefetch?: PrefetchOptions | undefined;
519
613
  private includeIndexed: boolean | undefined = undefined;
520
614
 
521
- compatibility: 6 | 7 | 8 | undefined;
615
+ compatibility: 6 | 7 | 8 | 9 | undefined;
522
616
 
523
617
  // Transformation, indexer
524
618
  /* fields: IndexableFields<T, I>; */
@@ -526,7 +620,10 @@ export class DocumentIndex<
526
620
  private _valueEncoding: Encoding<T>;
527
621
 
528
622
  private _sync: <V extends types.ResultValue<T> | types.ResultIndexedValue<I>>(
529
- request: types.SearchRequest | types.SearchRequestIndexed,
623
+ request:
624
+ | types.SearchRequest
625
+ | types.SearchRequestIndexed
626
+ | types.IterationRequest,
530
627
  results: types.Results<V>,
531
628
  ) => Promise<void>;
532
629
 
@@ -550,15 +647,20 @@ export class DocumentIndex<
550
647
  keptInIndex: number;
551
648
  timeout: ReturnType<typeof setTimeout>;
552
649
  queue: indexerTypes.IndexedResult<WithContext<I>>[];
553
- fromQuery: types.SearchRequest | types.SearchRequestIndexed;
650
+ fromQuery:
651
+ | types.SearchRequest
652
+ | types.SearchRequestIndexed
653
+ | types.IterationRequest;
554
654
  }
555
655
  >;
656
+ private iteratorKeepAliveTimers?: Map<string, ReturnType<typeof setTimeout>>;
556
657
 
557
658
  constructor(properties?: {
558
659
  query?: RPC<types.AbstractSearchRequest, types.AbstractSearchResult>;
559
660
  }) {
560
661
  super();
561
662
  this._query = properties?.query || new RPC();
663
+ this.iteratorKeepAliveTimers = new Map();
562
664
  }
563
665
 
564
666
  get valueEncoding() {
@@ -628,10 +730,15 @@ export class DocumentIndex<
628
730
  this.dbType = properties.dbType;
629
731
  this._resultQueue = new Map();
630
732
  this._sync = (request, results) => {
631
- let rq: types.SearchRequest | types.SearchRequestIndexed;
733
+ let rq:
734
+ | types.SearchRequest
735
+ | types.SearchRequestIndexed
736
+ | types.IterationRequest;
632
737
  let rs: types.Results<
633
738
  types.ResultTypeFromRequest<
634
- types.SearchRequest | types.SearchRequestIndexed,
739
+ | types.SearchRequest
740
+ | types.SearchRequestIndexed
741
+ | types.IterationRequest,
635
742
  T,
636
743
  I
637
744
  >
@@ -643,7 +750,9 @@ export class DocumentIndex<
643
750
  rq = request;
644
751
  rs = results as types.Results<
645
752
  types.ResultTypeFromRequest<
646
- types.SearchRequest | types.SearchRequestIndexed,
753
+ | types.SearchRequest
754
+ | types.SearchRequestIndexed
755
+ | types.IterationRequest,
647
756
  T,
648
757
  I
649
758
  >
@@ -775,7 +884,8 @@ export class DocumentIndex<
775
884
  if (
776
885
  this.prefetch?.predictor &&
777
886
  (query instanceof types.SearchRequest ||
778
- query instanceof types.SearchRequestIndexed)
887
+ query instanceof types.SearchRequestIndexed ||
888
+ query instanceof types.IterationRequest)
779
889
  ) {
780
890
  const { ignore } = this.prefetch.predictor.onRequest(query, {
781
891
  from: ctx.from!,
@@ -792,6 +902,7 @@ export class DocumentIndex<
792
902
  query as
793
903
  | types.SearchRequest
794
904
  | types.SearchRequestIndexed
905
+ | types.IterationRequest
795
906
  | types.CollectNextRequest,
796
907
  {
797
908
  from: ctx.from!,
@@ -802,15 +913,20 @@ export class DocumentIndex<
802
913
  query:
803
914
  | types.SearchRequest
804
915
  | types.SearchRequestIndexed
916
+ | types.IterationRequest
805
917
  | types.CollectNextRequest,
806
918
  ctx: { from: PublicSignKey },
807
919
  ) {
808
920
  if (
809
921
  this.canSearch &&
810
922
  (query instanceof types.SearchRequest ||
923
+ query instanceof types.IterationRequest ||
811
924
  query instanceof types.CollectNextRequest) &&
812
925
  !(await this.canSearch(
813
- query as types.SearchRequest | types.CollectNextRequest,
926
+ query as
927
+ | types.SearchRequest
928
+ | types.IterationRequest
929
+ | types.CollectNextRequest,
814
930
  ctx.from,
815
931
  ))
816
932
  ) {
@@ -820,17 +936,23 @@ export class DocumentIndex<
820
936
  if (query instanceof types.CloseIteratorRequest) {
821
937
  this.processCloseIteratorRequest(query, ctx.from);
822
938
  } else {
939
+ const fromQueued =
940
+ query instanceof types.CollectNextRequest
941
+ ? this._resultQueue.get(query.idString)?.fromQuery
942
+ : undefined;
943
+ const queryResolvesDocuments =
944
+ query instanceof types.CollectNextRequest
945
+ ? resolvesDocuments(fromQueued)
946
+ : resolvesDocuments(query as AnyIterationRequest);
947
+
823
948
  const shouldIncludedIndexedResults =
824
- this.includeIndexed &&
825
- (query instanceof types.SearchRequest ||
826
- (query instanceof types.CollectNextRequest &&
827
- this._resultQueue.get(query.idString)?.fromQuery instanceof
828
- types.SearchRequest)); // we do this check here because this._resultQueue might be emptied when this.processQuery is called
949
+ this.includeIndexed && queryResolvesDocuments;
829
950
 
830
951
  const results = await this.processQuery(
831
952
  query as
832
953
  | types.SearchRequest
833
954
  | types.SearchRequestIndexed
955
+ | types.IterationRequest
834
956
  | types.CollectNextRequest,
835
957
  ctx.from,
836
958
  false,
@@ -1156,19 +1278,29 @@ export class DocumentIndex<
1156
1278
  | types.ResultIndexedValue<WithContext<I>>
1157
1279
  >[]
1158
1280
  | undefined;
1281
+
1282
+ const runAndClose = async (
1283
+ req: types.SearchRequest | types.SearchRequestIndexed,
1284
+ ): Promise<typeof results> => {
1285
+ const response = await this.queryCommence(
1286
+ req,
1287
+ coercedOptions as QueryDetailedOptions<T, I, D, boolean | undefined>,
1288
+ );
1289
+ this._resumableIterators.close({ idString: req.idString });
1290
+ this.cancelIteratorKeepAlive(req.idString);
1291
+ return response as typeof results;
1292
+ };
1159
1293
  const resolve = coercedOptions?.resolve || coercedOptions?.resolve == null;
1160
1294
  let requestClazz = resolve
1161
1295
  ? types.SearchRequest
1162
1296
  : types.SearchRequestIndexed;
1163
1297
  if (key instanceof Uint8Array) {
1164
- results = await this.queryCommence(
1165
- new requestClazz({
1166
- query: [
1167
- new indexerTypes.ByteMatchQuery({ key: this.indexBy, value: key }),
1168
- ],
1169
- }),
1170
- coercedOptions as QueryDetailedOptions<T, I, D, boolean | undefined>,
1171
- );
1298
+ const request = new requestClazz({
1299
+ query: [
1300
+ new indexerTypes.ByteMatchQuery({ key: this.indexBy, value: key }),
1301
+ ],
1302
+ });
1303
+ results = await runAndClose(request);
1172
1304
  } else {
1173
1305
  const indexableKey = indexerTypes.toIdeable(key);
1174
1306
 
@@ -1176,42 +1308,48 @@ export class DocumentIndex<
1176
1308
  typeof indexableKey === "number" ||
1177
1309
  typeof indexableKey === "bigint"
1178
1310
  ) {
1179
- results = await this.queryCommence(
1180
- new requestClazz({
1181
- query: [
1182
- new indexerTypes.IntegerCompare({
1183
- key: this.indexBy,
1184
- compare: indexerTypes.Compare.Equal,
1185
- value: indexableKey,
1186
- }),
1187
- ],
1188
- }),
1189
- coercedOptions as QueryDetailedOptions<T, I, D, boolean | undefined>,
1190
- );
1311
+ const request = new requestClazz({
1312
+ query: [
1313
+ new indexerTypes.IntegerCompare({
1314
+ key: this.indexBy,
1315
+ compare: indexerTypes.Compare.Equal,
1316
+ value: indexableKey,
1317
+ }),
1318
+ ],
1319
+ });
1320
+ results = await runAndClose(request);
1191
1321
  } else if (typeof indexableKey === "string") {
1192
- results = await this.queryCommence(
1193
- new requestClazz({
1194
- query: [
1195
- new indexerTypes.StringMatch({
1196
- key: this.indexBy,
1197
- value: indexableKey,
1198
- }),
1199
- ],
1200
- }),
1201
- coercedOptions as QueryDetailedOptions<T, I, D, boolean | undefined>,
1202
- );
1322
+ const request = new requestClazz({
1323
+ query: [
1324
+ new indexerTypes.StringMatch({
1325
+ key: this.indexBy,
1326
+ value: indexableKey,
1327
+ }),
1328
+ ],
1329
+ });
1330
+ results = await runAndClose(request);
1203
1331
  } else if (indexableKey instanceof Uint8Array) {
1204
- results = await this.queryCommence(
1205
- new requestClazz({
1206
- query: [
1207
- new indexerTypes.ByteMatchQuery({
1208
- key: this.indexBy,
1209
- value: indexableKey,
1210
- }),
1211
- ],
1212
- }),
1213
- coercedOptions as QueryDetailedOptions<T, I, D, boolean | undefined>,
1214
- );
1332
+ const request = new requestClazz({
1333
+ query: [
1334
+ new indexerTypes.ByteMatchQuery({
1335
+ key: this.indexBy,
1336
+ value: indexableKey,
1337
+ }),
1338
+ ],
1339
+ });
1340
+ results = await runAndClose(request);
1341
+ } else if ((indexableKey as any) instanceof ArrayBuffer) {
1342
+ const request = new requestClazz({
1343
+ query: [
1344
+ new indexerTypes.ByteMatchQuery({
1345
+ key: this.indexBy,
1346
+ value: new Uint8Array(indexableKey),
1347
+ }),
1348
+ ],
1349
+ });
1350
+ results = await runAndClose(request);
1351
+ } else {
1352
+ throw new Error("Unsupported key type");
1215
1353
  }
1216
1354
  }
1217
1355
 
@@ -1317,6 +1455,7 @@ export class DocumentIndex<
1317
1455
  R extends
1318
1456
  | types.SearchRequest
1319
1457
  | types.SearchRequestIndexed
1458
+ | types.IterationRequest
1320
1459
  | types.CollectNextRequest,
1321
1460
  >(
1322
1461
  query: R,
@@ -1338,25 +1477,57 @@ export class DocumentIndex<
1338
1477
  let indexedResult: indexerTypes.IndexedResults<WithContext<I>> | undefined =
1339
1478
  undefined;
1340
1479
 
1341
- let fromQuery: types.SearchRequest | types.SearchRequestIndexed | undefined;
1480
+ let fromQuery:
1481
+ | types.SearchRequest
1482
+ | types.SearchRequestIndexed
1483
+ | types.IterationRequest
1484
+ | undefined;
1485
+ let keepAliveRequest: types.IterationRequest | undefined;
1342
1486
  if (
1343
1487
  query instanceof types.SearchRequest ||
1344
- query instanceof types.SearchRequestIndexed
1488
+ query instanceof types.SearchRequestIndexed ||
1489
+ query instanceof types.IterationRequest
1345
1490
  ) {
1346
1491
  fromQuery = query;
1347
- indexedResult = await this._resumableIterators.iterateAndFetch(query);
1492
+ if (
1493
+ !isLocal &&
1494
+ query instanceof types.IterationRequest &&
1495
+ query.keepAliveTtl != null
1496
+ ) {
1497
+ keepAliveRequest = query;
1498
+ }
1499
+ indexedResult = await this._resumableIterators.iterateAndFetch(query, {
1500
+ keepAlive: keepAliveRequest !== undefined,
1501
+ });
1348
1502
  } else if (query instanceof types.CollectNextRequest) {
1349
- fromQuery =
1503
+ const cachedRequest =
1350
1504
  prevQueued?.fromQuery ||
1351
1505
  this._resumableIterators.queues.get(query.idString)?.request;
1506
+ fromQuery = cachedRequest;
1507
+ if (
1508
+ !isLocal &&
1509
+ cachedRequest instanceof types.IterationRequest &&
1510
+ cachedRequest.keepAliveTtl != null
1511
+ ) {
1512
+ keepAliveRequest = cachedRequest;
1513
+ }
1352
1514
  indexedResult =
1353
1515
  prevQueued?.keptInIndex === 0
1354
1516
  ? []
1355
- : await this._resumableIterators.next(query);
1517
+ : await this._resumableIterators.next(query, {
1518
+ keepAlive: keepAliveRequest !== undefined,
1519
+ });
1356
1520
  } else {
1357
1521
  throw new Error("Unsupported");
1358
1522
  }
1359
1523
 
1524
+ if (!isLocal && keepAliveRequest) {
1525
+ this.scheduleIteratorKeepAlive(
1526
+ query.idString,
1527
+ keepAliveRequest.keepAliveTtl,
1528
+ );
1529
+ }
1530
+
1360
1531
  let resultSize = 0;
1361
1532
 
1362
1533
  let toIterate = prevQueued
@@ -1381,13 +1552,15 @@ export class DocumentIndex<
1381
1552
  keptInIndex: kept,
1382
1553
  fromQuery: (fromQuery || query) as
1383
1554
  | types.SearchRequest
1384
- | types.SearchRequestIndexed,
1555
+ | types.SearchRequestIndexed
1556
+ | types.IterationRequest,
1385
1557
  };
1386
1558
  this._resultQueue.set(query.idString, prevQueued);
1387
1559
  }
1388
1560
 
1389
1561
  const filteredResults: types.Result[] = [];
1390
-
1562
+ const resolveDocumentsFlag = resolvesDocuments(fromQuery);
1563
+ const replicateIndexFlag = replicatesIndex(fromQuery);
1391
1564
  for (const result of toIterate) {
1392
1565
  if (!isLocal) {
1393
1566
  resultSize += result.value.__context.size;
@@ -1407,7 +1580,7 @@ export class DocumentIndex<
1407
1580
  ) {
1408
1581
  continue;
1409
1582
  }
1410
- if (fromQuery instanceof types.SearchRequest) {
1583
+ if (resolveDocumentsFlag) {
1411
1584
  const value = await this.resolveDocument({
1412
1585
  indexed: result.value,
1413
1586
  head: result.value.__context.head,
@@ -1425,11 +1598,11 @@ export class DocumentIndex<
1425
1598
  indexed: indexedUnwrapped,
1426
1599
  }),
1427
1600
  );
1428
- } else if (fromQuery instanceof types.SearchRequestIndexed) {
1601
+ } else {
1429
1602
  const context = result.value.__context;
1430
1603
  const head = await this._log.log.get(context.head);
1431
1604
  // assume remote peer will start to replicate (TODO is this ideal?)
1432
- if (fromQuery.replicate) {
1605
+ if (replicateIndexFlag) {
1433
1606
  this._log.addPeersToGidPeerHistory(context.gid, [from.hashcode()]);
1434
1607
  }
1435
1608
 
@@ -1448,6 +1621,13 @@ export class DocumentIndex<
1448
1621
  results: filteredResults,
1449
1622
  kept: BigInt(kept + (prevQueued?.queue.length || 0)),
1450
1623
  });
1624
+ /* console.debug("[DocumentIndex] processQuery", {
1625
+ id: query.idString,
1626
+ isLocal,
1627
+ batchLength: filteredResults.length,
1628
+ kept: results.kept,
1629
+ source: from.hashcode(),
1630
+ }); */
1451
1631
 
1452
1632
  if (!isLocal && results.kept === 0n) {
1453
1633
  this.clearResultsQueue(query);
@@ -1456,10 +1636,53 @@ export class DocumentIndex<
1456
1636
  return results;
1457
1637
  }
1458
1638
 
1639
+ private scheduleIteratorKeepAlive(idString: string, ttl?: bigint) {
1640
+ if (ttl == null) {
1641
+ return;
1642
+ }
1643
+ const ttlNumber = Number(ttl);
1644
+ if (!Number.isFinite(ttlNumber) || ttlNumber <= 0) {
1645
+ return;
1646
+ }
1647
+
1648
+ // Cap max timeout to 1 day (TODO make configurable?)
1649
+ const delay = Math.max(1, Math.min(ttlNumber, 86400000));
1650
+ this.cancelIteratorKeepAlive(idString);
1651
+ const timers =
1652
+ this.iteratorKeepAliveTimers ??
1653
+ (this.iteratorKeepAliveTimers = new Map<
1654
+ string,
1655
+ ReturnType<typeof setTimeout>
1656
+ >());
1657
+ const timer = setTimeout(() => {
1658
+ timers.delete(idString);
1659
+ const queued = this._resultQueue.get(idString);
1660
+ if (queued) {
1661
+ clearTimeout(queued.timeout);
1662
+ this._resultQueue.delete(idString);
1663
+ }
1664
+ this._resumableIterators.close({ idString });
1665
+ }, delay);
1666
+ timers.set(idString, timer);
1667
+ }
1668
+
1669
+ private cancelIteratorKeepAlive(idString: string) {
1670
+ const timers = this.iteratorKeepAliveTimers;
1671
+ if (!timers) {
1672
+ return;
1673
+ }
1674
+ const timer = timers.get(idString);
1675
+ if (timer) {
1676
+ clearTimeout(timer);
1677
+ timers.delete(idString);
1678
+ }
1679
+ }
1680
+
1459
1681
  private clearResultsQueue(
1460
1682
  query:
1461
1683
  | types.SearchRequest
1462
1684
  | types.SearchRequestIndexed
1685
+ | types.IterationRequest
1463
1686
  | types.CollectNextRequest
1464
1687
  | types.CloseIteratorRequest,
1465
1688
  ) {
@@ -1478,6 +1701,7 @@ export class DocumentIndex<
1478
1701
  for (const [key, queue] of this._resultQueue) {
1479
1702
  clearTimeout(queue.timeout);
1480
1703
  this._resultQueue.delete(key);
1704
+ this.cancelIteratorKeepAlive(key);
1481
1705
  this._resumableIterators.close({ idString: key });
1482
1706
  }
1483
1707
  }
@@ -1644,6 +1868,7 @@ export class DocumentIndex<
1644
1868
  logger.info("Ignoring close iterator request from different peer");
1645
1869
  return;
1646
1870
  }
1871
+ this.cancelIteratorKeepAlive(query.idString);
1647
1872
  this.clearResultsQueue(query);
1648
1873
  return this._resumableIterators.close(query);
1649
1874
  }
@@ -1655,7 +1880,10 @@ export class DocumentIndex<
1655
1880
  * @returns
1656
1881
  */
1657
1882
  private async queryCommence<
1658
- R extends types.SearchRequest | types.SearchRequestIndexed,
1883
+ R extends
1884
+ | types.SearchRequest
1885
+ | types.SearchRequestIndexed
1886
+ | types.IterationRequest,
1659
1887
  RT extends types.Result = types.ResultTypeFromRequest<R, T, I>,
1660
1888
  >(
1661
1889
  queryRequest: R,
@@ -1899,7 +2127,11 @@ export class DocumentIndex<
1899
2127
  * @returns
1900
2128
  */
1901
2129
  public async search<
1902
- R extends types.SearchRequest | types.SearchRequestIndexed | QueryLike,
2130
+ R extends
2131
+ | types.SearchRequest
2132
+ | types.SearchRequestIndexed
2133
+ | types.IterationRequest
2134
+ | QueryLike,
1903
2135
  O extends SearchOptions<T, I, D, Resolve>,
1904
2136
  Resolve extends boolean = ExtractResolveFromOptions<O>,
1905
2137
  >(
@@ -1907,8 +2139,11 @@ export class DocumentIndex<
1907
2139
  options?: O,
1908
2140
  ): Promise<ValueTypeFromRequest<Resolve, T, I>[]> {
1909
2141
  // Set fetch to search size, or max value (default to max u32 (4294967295))
1910
- const coercedRequest: types.SearchRequest | types.SearchRequestIndexed =
1911
- coerceQuery(queryRequest, options);
2142
+ const coercedRequest = coerceQuery(
2143
+ queryRequest,
2144
+ options,
2145
+ this.compatibility,
2146
+ );
1912
2147
  coercedRequest.fetch = coercedRequest.fetch ?? 0xffffffff;
1913
2148
 
1914
2149
  // So that the iterator is pre-fetching the right amount of entries
@@ -1987,7 +2222,7 @@ export class DocumentIndex<
1987
2222
  /**
1988
2223
  * Query and retrieve documents in a iterator
1989
2224
  * @param queryRequest
1990
- * @param options
2225
+ * @param optionsArg
1991
2226
  * @returns
1992
2227
  */
1993
2228
  public iterate<
@@ -1996,8 +2231,9 @@ export class DocumentIndex<
1996
2231
  Resolve extends boolean | undefined = ExtractResolveFromOptions<O>,
1997
2232
  >(
1998
2233
  queryRequest?: R,
1999
- options?: QueryOptions<T, I, D, Resolve>,
2234
+ optionsArg?: QueryOptions<T, I, D, Resolve>,
2000
2235
  ): ResultsIterator<ValueTypeFromRequest<Resolve, T, I>> {
2236
+ let options = optionsArg;
2001
2237
  if (
2002
2238
  queryRequest instanceof types.SearchRequest &&
2003
2239
  options?.resolve === false
@@ -2005,14 +2241,44 @@ export class DocumentIndex<
2005
2241
  throw new Error("Cannot use resolve=false with SearchRequest"); // TODO make this work
2006
2242
  }
2007
2243
 
2008
- let queryRequestCoerced: types.SearchRequest | types.SearchRequestIndexed =
2009
- coerceQuery(queryRequest ?? {}, options);
2244
+ let queryRequestCoerced = coerceQuery(
2245
+ queryRequest ?? {},
2246
+ options,
2247
+ this.compatibility,
2248
+ );
2249
+
2250
+ const {
2251
+ mergePolicy,
2252
+ push: pushUpdates,
2253
+ callbacks: updateCallbacksRaw,
2254
+ } = normalizeUpdatesOption(options?.updates);
2255
+ const hasLiveUpdates = mergePolicy !== undefined;
2256
+ const originalRemote = options?.remote;
2257
+ let remoteOptions =
2258
+ typeof originalRemote === "boolean"
2259
+ ? originalRemote
2260
+ : originalRemote
2261
+ ? { ...originalRemote }
2262
+ : undefined;
2263
+ if (pushUpdates && remoteOptions !== false) {
2264
+ if (typeof remoteOptions === "object") {
2265
+ if (remoteOptions.replicate !== true) {
2266
+ remoteOptions.replicate = true;
2267
+ }
2268
+ } else if (remoteOptions === undefined || remoteOptions === true) {
2269
+ remoteOptions = { replicate: true };
2270
+ }
2271
+ }
2272
+ if (remoteOptions !== originalRemote) {
2273
+ options = Object.assign({}, options, { remote: remoteOptions });
2274
+ }
2010
2275
 
2011
2276
  let resolve = options?.resolve !== false;
2012
2277
  if (
2278
+ !(queryRequestCoerced instanceof types.IterationRequest) &&
2013
2279
  options?.remote &&
2014
2280
  typeof options.remote !== "boolean" &&
2015
- options.remote.replicate &&
2281
+ (options.remote.replicate || pushUpdates) &&
2016
2282
  options?.resolve !== false
2017
2283
  ) {
2018
2284
  if (
@@ -2032,7 +2298,7 @@ export class DocumentIndex<
2032
2298
  let replicate =
2033
2299
  options?.remote &&
2034
2300
  typeof options.remote !== "boolean" &&
2035
- options.remote.replicate;
2301
+ (options.remote.replicate || pushUpdates);
2036
2302
  if (
2037
2303
  replicate &&
2038
2304
  queryRequestCoerced instanceof types.SearchRequestIndexed
@@ -2109,6 +2375,8 @@ export class DocumentIndex<
2109
2375
 
2110
2376
  let warmupPromise: Promise<any> | undefined = undefined;
2111
2377
 
2378
+ let discoveredTargetHashes: string[] | undefined;
2379
+
2112
2380
  if (typeof options?.remote === "object") {
2113
2381
  let waitForTime: number | undefined = undefined;
2114
2382
  if (options.remote.wait) {
@@ -2145,11 +2413,26 @@ export class DocumentIndex<
2145
2413
  }
2146
2414
 
2147
2415
  if (options.remote.reach?.discover) {
2148
- warmupPromise = this.waitFor(options.remote.reach.discover, {
2416
+ const discoverTimeout =
2417
+ waitForTime ??
2418
+ (options.remote.wait ? DEFAULT_TIMEOUT : DISCOVER_TIMEOUT_FALLBACK);
2419
+ const discoverPromise = this.waitFor(options.remote.reach.discover, {
2149
2420
  signal: ensureController().signal,
2150
2421
  seek: "present",
2151
- timeout: waitForTime ?? DEFAULT_TIMEOUT,
2152
- });
2422
+ timeout: discoverTimeout,
2423
+ })
2424
+ .then((hashes) => {
2425
+ discoveredTargetHashes = hashes;
2426
+ })
2427
+ .catch((error) => {
2428
+ if (error instanceof TimeoutError || error instanceof AbortError) {
2429
+ discoveredTargetHashes = [];
2430
+ return;
2431
+ }
2432
+ throw error;
2433
+ });
2434
+ const prior = warmupPromise ?? Promise.resolve();
2435
+ warmupPromise = prior.then(() => discoverPromise);
2153
2436
  options.remote.reach.eager = true; // include the results from the discovered peer even if it is not mature
2154
2437
  }
2155
2438
 
@@ -2181,6 +2464,18 @@ export class DocumentIndex<
2181
2464
  ): Promise<boolean> => {
2182
2465
  await warmupPromise;
2183
2466
  let hasMore = false;
2467
+ const discoverTargets =
2468
+ typeof options?.remote === "object"
2469
+ ? options.remote.reach?.discover
2470
+ : undefined;
2471
+ const initialRemoteTargets =
2472
+ discoveredTargetHashes !== undefined
2473
+ ? discoveredTargetHashes
2474
+ : discoverTargets?.map((pk) => pk.hashcode().toString());
2475
+ const skipRemoteDueToDiscovery =
2476
+ typeof options?.remote === "object" &&
2477
+ options.remote.reach?.discover &&
2478
+ discoveredTargetHashes?.length === 0;
2184
2479
 
2185
2480
  queryRequestCoerced.fetch = n;
2186
2481
  await this.queryCommence(
@@ -2188,12 +2483,12 @@ export class DocumentIndex<
2188
2483
  {
2189
2484
  local: fetchOptions?.from != null ? false : options?.local,
2190
2485
  remote:
2191
- options?.remote !== false
2486
+ options?.remote !== false && !skipRemoteDueToDiscovery
2192
2487
  ? {
2193
2488
  ...(typeof options?.remote === "object"
2194
2489
  ? options.remote
2195
2490
  : {}),
2196
- from: fetchOptions?.from,
2491
+ from: fetchOptions?.from ?? initialRemoteTargets,
2197
2492
  }
2198
2493
  : false,
2199
2494
  resolve,
@@ -2210,17 +2505,25 @@ export class DocumentIndex<
2210
2505
  types.ResultTypeFromRequest<R, T, I>
2211
2506
  >;
2212
2507
 
2508
+ const existingBuffer = peerBufferMap.get(from.hashcode());
2509
+ const buffer: BufferedResult<
2510
+ types.ResultTypeFromRequest<R, T, I> | I,
2511
+ I
2512
+ >[] = existingBuffer?.buffer || [];
2513
+
2213
2514
  if (results.kept === 0n && results.results.length === 0) {
2515
+ if (keepRemoteAlive) {
2516
+ peerBufferMap.set(from.hashcode(), {
2517
+ buffer,
2518
+ kept: Number(response.kept),
2519
+ });
2520
+ }
2214
2521
  return;
2215
2522
  }
2216
2523
 
2217
2524
  if (results.kept > 0n) {
2218
2525
  hasMore = true;
2219
2526
  }
2220
- const buffer: BufferedResult<
2221
- types.ResultTypeFromRequest<R, T, I> | I,
2222
- I
2223
- >[] = peerBufferMap.get(from.hashcode())?.buffer || [];
2224
2527
 
2225
2528
  for (const result of results.results) {
2226
2529
  const indexKey = indexerTypes.toId(
@@ -2290,6 +2593,15 @@ export class DocumentIndex<
2290
2593
  },
2291
2594
  fetchOptions?.fetchedFirstForRemote,
2292
2595
  );
2596
+ /* console.debug(
2597
+ "[DocumentIndex] fetchFirst",
2598
+ {
2599
+ id: queryRequestCoerced.idString,
2600
+ requestedFrom: fetchOptions?.from,
2601
+ initialRemoteTargets,
2602
+ keepRemoteAlive,
2603
+ },
2604
+ ); */
2293
2605
 
2294
2606
  if (!hasMore) {
2295
2607
  maybeSetDone();
@@ -2322,7 +2634,8 @@ export class DocumentIndex<
2322
2634
 
2323
2635
  for (const [peer, buffer] of peerBufferMap) {
2324
2636
  if (buffer.buffer.length < n) {
2325
- if (buffer.kept === 0) {
2637
+ const hasExistingRemoteResults = buffer.kept > 0;
2638
+ if (!hasExistingRemoteResults && !keepRemoteAlive) {
2326
2639
  if (peerBufferMap.get(peer)?.buffer.length === 0) {
2327
2640
  peerBufferMap.delete(peer); // No more results
2328
2641
  }
@@ -2332,9 +2645,22 @@ export class DocumentIndex<
2332
2645
  // TODO buffer more than deleted?
2333
2646
  // TODO batch to multiple 'to's
2334
2647
 
2648
+ const lacking = n - buffer.buffer.length;
2649
+ const amount = lacking > 0 ? lacking : keepRemoteAlive ? 1 : 0;
2650
+ /* console.debug("[DocumentIndex] fetchAtLeast loop", {
2651
+ peer,
2652
+ bufferLength: buffer.buffer.length,
2653
+ bufferKept: buffer.kept,
2654
+ amount,
2655
+ keepRemoteAlive,
2656
+ }); */
2657
+ if (amount <= 0) {
2658
+ continue;
2659
+ }
2660
+
2335
2661
  const collectRequest = new types.CollectNextRequest({
2336
2662
  id: queryRequestCoerced.id,
2337
- amount: n - buffer.buffer.length,
2663
+ amount,
2338
2664
  });
2339
2665
  // Fetch locally?
2340
2666
  if (peer === this.node.identity.publicKey.hashcode()) {
@@ -2351,7 +2677,10 @@ export class DocumentIndex<
2351
2677
  resultsLeft += Number(results.kept);
2352
2678
 
2353
2679
  if (results.results.length === 0) {
2354
- if (peerBufferMap.get(peer)?.buffer.length === 0) {
2680
+ if (
2681
+ !keepRemoteAlive &&
2682
+ peerBufferMap.get(peer)?.buffer.length === 0
2683
+ ) {
2355
2684
  peerBufferMap.delete(peer); // No more results
2356
2685
  }
2357
2686
  } else {
@@ -2492,7 +2821,10 @@ export class DocumentIndex<
2492
2821
  }
2493
2822
 
2494
2823
  if (response.response.results.length === 0) {
2495
- if (peerBufferMap.get(peer)?.buffer.length === 0) {
2824
+ if (
2825
+ !keepRemoteAlive &&
2826
+ peerBufferMap.get(peer)?.buffer.length === 0
2827
+ ) {
2496
2828
  peerBufferMap.delete(peer); // No more results
2497
2829
  }
2498
2830
  } else {
@@ -2745,48 +3077,102 @@ export class DocumentIndex<
2745
3077
  let fetchedFirstForRemote: Set<string> | undefined = undefined;
2746
3078
 
2747
3079
  let updateDeferred: ReturnType<typeof pDefer> | undefined;
2748
- const signalUpdate = () => updateDeferred?.resolve();
3080
+ const signalUpdate = (reason?: string) => {
3081
+ if (reason) {
3082
+ /* console.debug("[DocumentIndex] signalUpdate", {
3083
+ id: queryRequestCoerced.idString,
3084
+ reason,
3085
+ }); */
3086
+ }
3087
+ updateDeferred?.resolve();
3088
+ };
2749
3089
  const _waitForUpdate = () =>
2750
3090
  updateDeferred ? updateDeferred.promise : Promise.resolve();
2751
3091
 
2752
3092
  // ---------------- Live updates wiring (sorted-only with optional filter) ----------------
2753
- const normalizeUpdatesOption = (
2754
- u?: UpdateOptions<T, I, Resolve>,
2755
- ):
2756
- | {
2757
- merge?:
2758
- | {
2759
- filter?: (
2760
- evt: DocumentsChange<T, I>,
2761
- ) => MaybePromise<DocumentsChange<T, I> | void>;
2762
- }
2763
- | undefined;
2764
- }
2765
- | undefined => {
2766
- if (u == null || u === false) return undefined;
2767
- if (u === true)
3093
+ function normalizeUpdatesOption(u?: UpdateOptions<T, I, Resolve>): {
3094
+ mergePolicy?: {
3095
+ merge?:
3096
+ | {
3097
+ filter?: (
3098
+ evt: DocumentsChange<T, I>,
3099
+ ) => MaybePromise<DocumentsChange<T, I> | void>;
3100
+ }
3101
+ | undefined;
3102
+ };
3103
+ push: boolean;
3104
+ callbacks?: UpdateCallbacks<T, I, Resolve>;
3105
+ } {
3106
+ const identityFilter = (evt: DocumentsChange<T, I>) => evt;
3107
+ const buildMergePolicy = (
3108
+ merge: UpdateMergeStrategy<T, I, Resolve> | undefined,
3109
+ defaultEnabled: boolean,
3110
+ ) => {
3111
+ const effective =
3112
+ merge === undefined ? (defaultEnabled ? true : undefined) : merge;
3113
+ if (effective === undefined || effective === false) {
3114
+ return undefined;
3115
+ }
3116
+ if (effective === true) {
3117
+ return {
3118
+ merge: {
3119
+ filter: identityFilter,
3120
+ },
3121
+ };
3122
+ }
2768
3123
  return {
2769
3124
  merge: {
2770
- filter: (evt) => evt,
3125
+ filter: effective.filter ?? identityFilter,
2771
3126
  },
2772
3127
  };
3128
+ };
3129
+
3130
+ if (u == null || u === false) {
3131
+ return { push: false };
3132
+ }
3133
+
3134
+ if (u === true) {
3135
+ return {
3136
+ mergePolicy: buildMergePolicy(true, true),
3137
+ push: false,
3138
+ };
3139
+ }
3140
+
3141
+ if (typeof u === "string") {
3142
+ if (u === "remote") {
3143
+ return { push: true };
3144
+ }
3145
+ if (u === "local") {
3146
+ return {
3147
+ mergePolicy: buildMergePolicy(true, true),
3148
+ push: false,
3149
+ };
3150
+ }
3151
+ if (u === "all") {
3152
+ return {
3153
+ mergePolicy: buildMergePolicy(true, true),
3154
+ push: true,
3155
+ };
3156
+ }
3157
+ }
3158
+
2773
3159
  if (typeof u === "object") {
3160
+ const hasMergeProp = Object.prototype.hasOwnProperty.call(u, "merge");
3161
+ const mergeValue = hasMergeProp ? u.merge : undefined;
2774
3162
  return {
2775
- merge: u.merge
2776
- ? {
2777
- filter:
2778
- typeof u.merge === "object" ? u.merge.filter : (evt) => evt,
2779
- }
2780
- : {},
3163
+ mergePolicy: buildMergePolicy(
3164
+ mergeValue,
3165
+ !hasMergeProp || mergeValue === undefined,
3166
+ ),
3167
+ push: Boolean(u.push),
3168
+ callbacks: u,
2781
3169
  };
2782
3170
  }
2783
- return undefined;
2784
- };
2785
3171
 
2786
- const updateCallbacks =
2787
- typeof options?.updates === "object" ? options.updates : undefined;
2788
- const mergePolicy = normalizeUpdatesOption(options?.updates);
2789
- const hasLiveUpdates = mergePolicy !== undefined;
3172
+ return { push: false };
3173
+ }
3174
+
3175
+ const updateCallbacks = updateCallbacksRaw;
2790
3176
  let pendingResultsReason:
2791
3177
  | Extract<ResultBatchReason, "join" | "change">
2792
3178
  | undefined;
@@ -2819,6 +3205,180 @@ export class DocumentIndex<
2819
3205
  updateDeferred = pDefer<void>();
2820
3206
  }
2821
3207
 
3208
+ const keepRemoteAlive =
3209
+ (options?.closePolicy === "manual" || hasLiveUpdates || pushUpdates) &&
3210
+ remoteOptions !== false;
3211
+
3212
+ if (queryRequestCoerced instanceof types.IterationRequest) {
3213
+ queryRequestCoerced.resolve = resolve;
3214
+ queryRequestCoerced.fetch = queryRequestCoerced.fetch ?? 10;
3215
+ const replicateFlag = !resolve && replicate ? true : false;
3216
+ queryRequestCoerced.replicate = replicateFlag;
3217
+ const ttlSource =
3218
+ typeof remoteOptions === "object" &&
3219
+ typeof remoteOptions?.wait === "object"
3220
+ ? (remoteOptions.wait.timeout ?? DEFAULT_TIMEOUT)
3221
+ : DEFAULT_TIMEOUT;
3222
+ queryRequestCoerced.keepAliveTtl = keepRemoteAlive
3223
+ ? BigInt(ttlSource)
3224
+ : undefined;
3225
+ queryRequestCoerced.pushUpdates = pushUpdates ? true : undefined;
3226
+ queryRequestCoerced.mergeUpdates = mergePolicy?.merge ? true : undefined;
3227
+ }
3228
+
3229
+ if (pushUpdates && this.prefetch?.accumulator) {
3230
+ const targetPrefetchKey = idAgnosticQueryKey(queryRequestCoerced);
3231
+ const mergePrefetchedResults = async (
3232
+ from: PublicSignKey,
3233
+ results: types.Results<types.ResultTypeFromRequest<R, T, I>>,
3234
+ ) => {
3235
+ const peerHash = from.hashcode();
3236
+ const existingBuffer = peerBufferMap.get(peerHash);
3237
+ const buffer: BufferedResult<
3238
+ types.ResultTypeFromRequest<R, T, I> | I,
3239
+ I
3240
+ >[] = existingBuffer?.buffer || [];
3241
+
3242
+ if (results.kept === 0n && results.results.length === 0) {
3243
+ peerBufferMap.set(peerHash, {
3244
+ buffer,
3245
+ kept: Number(results.kept),
3246
+ });
3247
+ return;
3248
+ }
3249
+
3250
+ for (const result of results.results) {
3251
+ const indexKey = indexerTypes.toId(
3252
+ this.indexByResolver(result.value),
3253
+ ).primitive;
3254
+ if (result instanceof types.ResultValue) {
3255
+ const existingIndexed = indexedPlaceholders?.get(indexKey);
3256
+ if (existingIndexed) {
3257
+ existingIndexed.value =
3258
+ result.value as types.ResultTypeFromRequest<R, T, I>;
3259
+ existingIndexed.context = result.context;
3260
+ existingIndexed.from = from;
3261
+ existingIndexed.indexed = await this.resolveIndexed<R>(
3262
+ result,
3263
+ results.results as types.ResultTypeFromRequest<R, T, I>[],
3264
+ );
3265
+ indexedPlaceholders?.delete(indexKey);
3266
+ continue;
3267
+ }
3268
+ if (visited.has(indexKey)) {
3269
+ continue;
3270
+ }
3271
+ visited.add(indexKey);
3272
+ buffer.push({
3273
+ value: result.value as types.ResultTypeFromRequest<R, T, I>,
3274
+ context: result.context,
3275
+ from,
3276
+ indexed: await this.resolveIndexed<R>(
3277
+ result,
3278
+ results.results as types.ResultTypeFromRequest<R, T, I>[],
3279
+ ),
3280
+ });
3281
+ } else {
3282
+ if (visited.has(indexKey) && !indexedPlaceholders?.has(indexKey)) {
3283
+ continue;
3284
+ }
3285
+ visited.add(indexKey);
3286
+ const indexed = coerceWithContext(
3287
+ result.indexed || result.value,
3288
+ result.context,
3289
+ );
3290
+ const placeholder = {
3291
+ value: result.value,
3292
+ context: result.context,
3293
+ from,
3294
+ indexed,
3295
+ };
3296
+ buffer.push(placeholder);
3297
+ ensureIndexedPlaceholders().set(indexKey, placeholder);
3298
+ }
3299
+ }
3300
+
3301
+ peerBufferMap.set(peerHash, {
3302
+ buffer,
3303
+ kept: Number(results.kept),
3304
+ });
3305
+ };
3306
+
3307
+ const consumePrefetch = async (
3308
+ consumable: RPCResponse<types.PredictedSearchRequest<any>>,
3309
+ ) => {
3310
+ const request = consumable.response?.request;
3311
+ if (!request) {
3312
+ return;
3313
+ }
3314
+ if (idAgnosticQueryKey(request) !== targetPrefetchKey) {
3315
+ return;
3316
+ }
3317
+
3318
+ /* console.debug("[DocumentIndex] prefetch match", {
3319
+ iterator: queryRequestCoerced.idString,
3320
+ source: consumable.from?.hashcode(),
3321
+ });
3322
+ */
3323
+ try {
3324
+ const prepared = await introduceEntries(
3325
+ queryRequestCoerced,
3326
+ [
3327
+ {
3328
+ response: consumable.response.results,
3329
+ from: consumable.from,
3330
+ },
3331
+ ],
3332
+ this.documentType,
3333
+ this.indexedType,
3334
+ this._sync,
3335
+ options as QueryDetailedOptions<T, I, D, any>,
3336
+ );
3337
+
3338
+ for (const response of prepared) {
3339
+ if (!response.from) {
3340
+ continue;
3341
+ }
3342
+ const payload = response.response;
3343
+ if (!(payload instanceof types.Results)) {
3344
+ continue;
3345
+ }
3346
+ await mergePrefetchedResults(
3347
+ response.from,
3348
+ payload as types.Results<types.ResultTypeFromRequest<R, T, I>>,
3349
+ );
3350
+ }
3351
+
3352
+ if (!pendingResultsReason) {
3353
+ pendingResultsReason = "change";
3354
+ }
3355
+ signalUpdate("prefetch-add");
3356
+ } catch (error) {
3357
+ logger.warn("Failed to merge prefetched results", error);
3358
+ }
3359
+ };
3360
+
3361
+ const onPrefetchAdd = (
3362
+ evt: CustomEvent<{
3363
+ consumable: RPCResponse<types.PredictedSearchRequest<any>>;
3364
+ }>,
3365
+ ) => {
3366
+ void consumePrefetch(evt.detail.consumable);
3367
+ };
3368
+ this.prefetch.accumulator.addEventListener(
3369
+ "add",
3370
+ onPrefetchAdd as EventListener,
3371
+ );
3372
+ const cleanupDefault = cleanup;
3373
+ cleanup = () => {
3374
+ this.prefetch?.accumulator.removeEventListener(
3375
+ "add",
3376
+ onPrefetchAdd as EventListener,
3377
+ );
3378
+ return cleanupDefault();
3379
+ };
3380
+ }
3381
+
2822
3382
  let updatesCleanup: (() => void) | undefined;
2823
3383
  if (hasLiveUpdates) {
2824
3384
  const localHash = this.node.identity.publicKey.hashcode();
@@ -2864,6 +3424,11 @@ export class DocumentIndex<
2864
3424
  };
2865
3425
 
2866
3426
  const onChange = async (evt: CustomEvent<DocumentsChange<T, I>>) => {
3427
+ /* console.debug("[DocumentIndex] onChange event", {
3428
+ id: queryRequestCoerced.idString,
3429
+ added: evt.detail.added?.length,
3430
+ removed: evt.detail.removed?.length,
3431
+ }); */
2867
3432
  // Optional filter to mutate/suppress change events
2868
3433
  let filtered: DocumentsChange<T, I> | void = evt.detail;
2869
3434
  if (mergePolicy?.merge?.filter) {
@@ -2981,6 +3546,12 @@ export class DocumentIndex<
2981
3546
  indexed: indexedCandidate,
2982
3547
  };
2983
3548
  buf.buffer.push(placeholder);
3549
+ /* console.debug("[DocumentIndex] buffered change", {
3550
+ id: queryRequestCoerced.idString,
3551
+ placeholderId: (valueForBuffer as any)?.id,
3552
+ peer: localHash,
3553
+ bufferSize: buf.buffer.length,
3554
+ }); */
2984
3555
  if (!resolve) {
2985
3556
  ensureIndexedPlaceholders().set(id, placeholder);
2986
3557
  }
@@ -2997,7 +3568,13 @@ export class DocumentIndex<
2997
3568
  changeForCallback.added.length > 0 ||
2998
3569
  changeForCallback.removed.length > 0
2999
3570
  ) {
3571
+ /* console.debug("[DocumentIndex] changeForCallback", {
3572
+ id: queryRequestCoerced.idString,
3573
+ added: changeForCallback.added.map((x) => (x as any)?.id),
3574
+ removed: changeForCallback.removed.map((x) => (x as any)?.id),
3575
+ }); */
3000
3576
  updateCallbacks?.onChange?.(changeForCallback);
3577
+ signalUpdate("change");
3001
3578
  }
3002
3579
  }
3003
3580
  }
@@ -3099,8 +3676,8 @@ export class DocumentIndex<
3099
3676
  };
3100
3677
  }
3101
3678
 
3102
- if (options?.closePolicy === "manual") {
3103
- let prevMaybeSetDone = maybeSetDone;
3679
+ if (keepRemoteAlive) {
3680
+ const prevMaybeSetDone = maybeSetDone;
3104
3681
  maybeSetDone = () => {
3105
3682
  if (drain) {
3106
3683
  prevMaybeSetDone();
@@ -3126,7 +3703,16 @@ export class DocumentIndex<
3126
3703
  close,
3127
3704
  next,
3128
3705
  done: doneFn,
3129
- pending: () => {
3706
+ pending: async () => {
3707
+ try {
3708
+ await fetchPromise;
3709
+ if (!done && keepRemoteAlive) {
3710
+ await fetchAtLeast(1);
3711
+ }
3712
+ } catch (error) {
3713
+ logger.warn("Failed to refresh iterator pending state", error);
3714
+ }
3715
+
3130
3716
  let pendingCount = 0;
3131
3717
  for (const buffer of peerBufferMap.values()) {
3132
3718
  pendingCount += buffer.kept + buffer.buffer.length;