@peerbit/document 9.13.10 → 10.0.0-954957e

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";
@@ -46,7 +48,9 @@ import { ResumableIterators } from "./resumable-iterator.js";
46
48
 
47
49
  const WARNING_WHEN_ITERATING_FOR_MORE_THAN = 1e5;
48
50
 
49
- const logger = loggerFn({ module: "document-index" });
51
+ const logger: ReturnType<typeof loggerFn> = loggerFn({
52
+ module: "document-index",
53
+ });
50
54
 
51
55
  type BufferedResult<T, I extends Record<string, any>> = {
52
56
  value: T;
@@ -95,11 +99,16 @@ export type UpdateCallbacks<
95
99
  * Unified update options for iterate()/search()/get() and hooks.
96
100
  * If you pass `true`, defaults to `{ merge: "sorted" }`.
97
101
  */
102
+ export type UpdateModeShortcut = "local" | "remote" | "all";
103
+
98
104
  export type UpdateOptions<T, I, Resolve extends boolean | undefined> =
99
105
  | boolean
106
+ | UpdateModeShortcut
100
107
  | ({
101
108
  /** Live update behavior. Only sorted merging is supported; optional filter can mutate/ignore events. */
102
109
  merge?: UpdateMergeStrategy<T, I, Resolve>;
110
+ /** Request push-style notifications backed by the prefetch channel. */
111
+ push?: boolean;
103
112
  } & UpdateCallbacks<T, I, Resolve>);
104
113
 
105
114
  export type JoiningTargets = {
@@ -221,7 +230,7 @@ export type ResultsIterator<T> = {
221
230
  next: (number: number) => Promise<T[]>;
222
231
  done: () => boolean;
223
232
  all: () => Promise<T[]>;
224
- pending: () => number | undefined;
233
+ pending: () => MaybePromise<number | undefined>;
225
234
  first: () => Promise<T | undefined>;
226
235
  [Symbol.asyncIterator]: () => AsyncIterator<T>;
227
236
  };
@@ -255,11 +264,21 @@ type ExtractResolveFromOptions<O> =
255
264
  : true; // if R isn't QueryLike at all, default to true
256
265
 
257
266
  const coerceQuery = <Resolve extends boolean | undefined>(
258
- query: types.SearchRequest | types.SearchRequestIndexed | QueryLike,
267
+ query:
268
+ | types.SearchRequest
269
+ | types.SearchRequestIndexed
270
+ | types.IterationRequest
271
+ | QueryLike,
259
272
  options?: QueryOptions<any, any, any, Resolve>,
260
- ) => {
261
- let replicate =
273
+ compatibility?: number,
274
+ ):
275
+ | types.SearchRequest
276
+ | types.SearchRequestIndexed
277
+ | types.IterationRequest => {
278
+ const replicate =
262
279
  typeof options?.remote !== "boolean" ? options?.remote?.replicate : false;
280
+ const shouldResolve = options?.resolve !== false;
281
+ const useLegacyRequests = compatibility != null && compatibility <= 9;
263
282
 
264
283
  if (
265
284
  query instanceof types.SearchRequestIndexed &&
@@ -269,29 +288,66 @@ const coerceQuery = <Resolve extends boolean | undefined>(
269
288
  query.replicate = true;
270
289
  return query;
271
290
  }
272
- if (query instanceof types.SearchRequest) {
291
+
292
+ if (
293
+ query instanceof types.SearchRequest ||
294
+ query instanceof types.SearchRequestIndexed
295
+ ) {
296
+ return query;
297
+ }
298
+
299
+ if (query instanceof types.IterationRequest) {
300
+ if (useLegacyRequests) {
301
+ if (query.resolve === false) {
302
+ return new types.SearchRequestIndexed({
303
+ query: query.query,
304
+ sort: query.sort,
305
+ fetch: query.fetch,
306
+ replicate: query.replicate ?? replicate,
307
+ });
308
+ }
309
+ return new types.SearchRequest({
310
+ query: query.query,
311
+ sort: query.sort,
312
+ fetch: query.fetch,
313
+ });
314
+ }
273
315
  return query;
274
316
  }
275
317
 
276
318
  const queryObject = query as QueryLike;
277
319
 
278
- return options?.resolve || options?.resolve == null
279
- ? new types.SearchRequest({
320
+ if (useLegacyRequests) {
321
+ if (shouldResolve) {
322
+ return new types.SearchRequest({
280
323
  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,
324
+ sort: indexerTypes.toSort(queryObject.sort),
287
325
  });
326
+ }
327
+ return new types.SearchRequestIndexed({
328
+ query: indexerTypes.toQuery(queryObject.query),
329
+ sort: indexerTypes.toSort(queryObject.sort),
330
+ replicate,
331
+ });
332
+ }
333
+
334
+ return new types.IterationRequest({
335
+ query: indexerTypes.toQuery(queryObject.query),
336
+ sort: indexerTypes.toSort(queryObject.sort),
337
+ fetch: 10,
338
+ resolve: shouldResolve,
339
+ replicate: shouldResolve ? false : replicate,
340
+ });
288
341
  };
289
342
 
290
343
  const introduceEntries = async <
291
344
  T,
292
345
  I,
293
346
  D,
294
- R extends types.SearchRequest | types.SearchRequestIndexed,
347
+ R extends
348
+ | types.SearchRequest
349
+ | types.SearchRequestIndexed
350
+ | types.IterationRequest,
295
351
  >(
296
352
  queryRequest: R,
297
353
  responses: { response: types.AbstractSearchResult; from?: PublicSignKey }[],
@@ -347,6 +403,37 @@ const dedup = <T>(
347
403
  return dedup;
348
404
  };
349
405
 
406
+ type AnyIterationRequest =
407
+ | types.SearchRequest
408
+ | types.SearchRequestIndexed
409
+ | types.IterationRequest;
410
+
411
+ const resolvesDocuments = (req?: AnyIterationRequest) => {
412
+ if (!req) {
413
+ return true;
414
+ }
415
+ if (req instanceof types.SearchRequestIndexed) {
416
+ return false;
417
+ }
418
+ if (req instanceof types.IterationRequest) {
419
+ return req.resolve !== false;
420
+ }
421
+ return true;
422
+ };
423
+
424
+ const replicatesIndex = (req?: AnyIterationRequest) => {
425
+ if (!req) {
426
+ return false;
427
+ }
428
+ if (req instanceof types.SearchRequestIndexed) {
429
+ return req.replicate === true;
430
+ }
431
+ if (req instanceof types.IterationRequest) {
432
+ return req.replicate === true;
433
+ }
434
+ return false;
435
+ };
436
+
350
437
  function isSubclassOf(
351
438
  SubClass: AbstractType<any>,
352
439
  SuperClass: AbstractType<any>,
@@ -365,11 +452,15 @@ function isSubclassOf(
365
452
  }
366
453
 
367
454
  const DEFAULT_TIMEOUT = 1e4;
455
+ const DISCOVER_TIMEOUT_FALLBACK = 500;
368
456
 
369
457
  const DEFAULT_INDEX_BY = "id";
370
458
 
371
459
  export type CanSearch = (
372
- request: types.SearchRequest | types.CollectNextRequest,
460
+ request:
461
+ | types.SearchRequest
462
+ | types.IterationRequest
463
+ | types.CollectNextRequest,
373
464
  from: PublicSignKey,
374
465
  ) => Promise<boolean> | boolean;
375
466
 
@@ -443,10 +534,15 @@ export type OpenOptions<
443
534
  canRead?: CanRead<I>;
444
535
  canSearch?: CanSearch;
445
536
  replicate: (
446
- request: types.SearchRequest | types.SearchRequestIndexed,
537
+ request:
538
+ | types.SearchRequest
539
+ | types.SearchRequestIndexed
540
+ | types.IterationRequest,
447
541
  results: types.Results<
448
542
  types.ResultTypeFromRequest<
449
- types.SearchRequest | types.SearchRequestIndexed,
543
+ | types.SearchRequest
544
+ | types.SearchRequestIndexed
545
+ | types.IterationRequest,
450
546
  T,
451
547
  I
452
548
  >
@@ -458,7 +554,7 @@ export type OpenOptions<
458
554
  resolver?: number;
459
555
  query?: QueryCacheOptions;
460
556
  };
461
- compatibility: 6 | 7 | 8 | undefined;
557
+ compatibility: 6 | 7 | 8 | 9 | undefined;
462
558
  maybeOpen: (value: T & Program) => Promise<T & Program>;
463
559
  prefetch?: boolean | Partial<PrefetchOptions>;
464
560
  includeIndexed?: boolean; // if true, indexed representations will always be included in the search results
@@ -518,7 +614,7 @@ export class DocumentIndex<
518
614
  private _prefetch?: PrefetchOptions | undefined;
519
615
  private includeIndexed: boolean | undefined = undefined;
520
616
 
521
- compatibility: 6 | 7 | 8 | undefined;
617
+ compatibility: 6 | 7 | 8 | 9 | undefined;
522
618
 
523
619
  // Transformation, indexer
524
620
  /* fields: IndexableFields<T, I>; */
@@ -526,7 +622,10 @@ export class DocumentIndex<
526
622
  private _valueEncoding: Encoding<T>;
527
623
 
528
624
  private _sync: <V extends types.ResultValue<T> | types.ResultIndexedValue<I>>(
529
- request: types.SearchRequest | types.SearchRequestIndexed,
625
+ request:
626
+ | types.SearchRequest
627
+ | types.SearchRequestIndexed
628
+ | types.IterationRequest,
530
629
  results: types.Results<V>,
531
630
  ) => Promise<void>;
532
631
 
@@ -550,15 +649,20 @@ export class DocumentIndex<
550
649
  keptInIndex: number;
551
650
  timeout: ReturnType<typeof setTimeout>;
552
651
  queue: indexerTypes.IndexedResult<WithContext<I>>[];
553
- fromQuery: types.SearchRequest | types.SearchRequestIndexed;
652
+ fromQuery:
653
+ | types.SearchRequest
654
+ | types.SearchRequestIndexed
655
+ | types.IterationRequest;
554
656
  }
555
657
  >;
658
+ private iteratorKeepAliveTimers?: Map<string, ReturnType<typeof setTimeout>>;
556
659
 
557
660
  constructor(properties?: {
558
661
  query?: RPC<types.AbstractSearchRequest, types.AbstractSearchResult>;
559
662
  }) {
560
663
  super();
561
664
  this._query = properties?.query || new RPC();
665
+ this.iteratorKeepAliveTimers = new Map();
562
666
  }
563
667
 
564
668
  get valueEncoding() {
@@ -628,10 +732,15 @@ export class DocumentIndex<
628
732
  this.dbType = properties.dbType;
629
733
  this._resultQueue = new Map();
630
734
  this._sync = (request, results) => {
631
- let rq: types.SearchRequest | types.SearchRequestIndexed;
735
+ let rq:
736
+ | types.SearchRequest
737
+ | types.SearchRequestIndexed
738
+ | types.IterationRequest;
632
739
  let rs: types.Results<
633
740
  types.ResultTypeFromRequest<
634
- types.SearchRequest | types.SearchRequestIndexed,
741
+ | types.SearchRequest
742
+ | types.SearchRequestIndexed
743
+ | types.IterationRequest,
635
744
  T,
636
745
  I
637
746
  >
@@ -643,7 +752,9 @@ export class DocumentIndex<
643
752
  rq = request;
644
753
  rs = results as types.Results<
645
754
  types.ResultTypeFromRequest<
646
- types.SearchRequest | types.SearchRequestIndexed,
755
+ | types.SearchRequest
756
+ | types.SearchRequestIndexed
757
+ | types.IterationRequest,
647
758
  T,
648
759
  I
649
760
  >
@@ -775,7 +886,8 @@ export class DocumentIndex<
775
886
  if (
776
887
  this.prefetch?.predictor &&
777
888
  (query instanceof types.SearchRequest ||
778
- query instanceof types.SearchRequestIndexed)
889
+ query instanceof types.SearchRequestIndexed ||
890
+ query instanceof types.IterationRequest)
779
891
  ) {
780
892
  const { ignore } = this.prefetch.predictor.onRequest(query, {
781
893
  from: ctx.from!,
@@ -792,6 +904,7 @@ export class DocumentIndex<
792
904
  query as
793
905
  | types.SearchRequest
794
906
  | types.SearchRequestIndexed
907
+ | types.IterationRequest
795
908
  | types.CollectNextRequest,
796
909
  {
797
910
  from: ctx.from!,
@@ -802,15 +915,20 @@ export class DocumentIndex<
802
915
  query:
803
916
  | types.SearchRequest
804
917
  | types.SearchRequestIndexed
918
+ | types.IterationRequest
805
919
  | types.CollectNextRequest,
806
920
  ctx: { from: PublicSignKey },
807
921
  ) {
808
922
  if (
809
923
  this.canSearch &&
810
924
  (query instanceof types.SearchRequest ||
925
+ query instanceof types.IterationRequest ||
811
926
  query instanceof types.CollectNextRequest) &&
812
927
  !(await this.canSearch(
813
- query as types.SearchRequest | types.CollectNextRequest,
928
+ query as
929
+ | types.SearchRequest
930
+ | types.IterationRequest
931
+ | types.CollectNextRequest,
814
932
  ctx.from,
815
933
  ))
816
934
  ) {
@@ -820,17 +938,23 @@ export class DocumentIndex<
820
938
  if (query instanceof types.CloseIteratorRequest) {
821
939
  this.processCloseIteratorRequest(query, ctx.from);
822
940
  } else {
941
+ const fromQueued =
942
+ query instanceof types.CollectNextRequest
943
+ ? this._resultQueue.get(query.idString)?.fromQuery
944
+ : undefined;
945
+ const queryResolvesDocuments =
946
+ query instanceof types.CollectNextRequest
947
+ ? resolvesDocuments(fromQueued)
948
+ : resolvesDocuments(query as AnyIterationRequest);
949
+
823
950
  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
951
+ this.includeIndexed && queryResolvesDocuments;
829
952
 
830
953
  const results = await this.processQuery(
831
954
  query as
832
955
  | types.SearchRequest
833
956
  | types.SearchRequestIndexed
957
+ | types.IterationRequest
834
958
  | types.CollectNextRequest,
835
959
  ctx.from,
836
960
  false,
@@ -1156,19 +1280,29 @@ export class DocumentIndex<
1156
1280
  | types.ResultIndexedValue<WithContext<I>>
1157
1281
  >[]
1158
1282
  | undefined;
1283
+
1284
+ const runAndClose = async (
1285
+ req: types.SearchRequest | types.SearchRequestIndexed,
1286
+ ): Promise<typeof results> => {
1287
+ const response = await this.queryCommence(
1288
+ req,
1289
+ coercedOptions as QueryDetailedOptions<T, I, D, boolean | undefined>,
1290
+ );
1291
+ this._resumableIterators.close({ idString: req.idString });
1292
+ this.cancelIteratorKeepAlive(req.idString);
1293
+ return response as typeof results;
1294
+ };
1159
1295
  const resolve = coercedOptions?.resolve || coercedOptions?.resolve == null;
1160
1296
  let requestClazz = resolve
1161
1297
  ? types.SearchRequest
1162
1298
  : types.SearchRequestIndexed;
1163
1299
  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
- );
1300
+ const request = new requestClazz({
1301
+ query: [
1302
+ new indexerTypes.ByteMatchQuery({ key: this.indexBy, value: key }),
1303
+ ],
1304
+ });
1305
+ results = await runAndClose(request);
1172
1306
  } else {
1173
1307
  const indexableKey = indexerTypes.toIdeable(key);
1174
1308
 
@@ -1176,42 +1310,48 @@ export class DocumentIndex<
1176
1310
  typeof indexableKey === "number" ||
1177
1311
  typeof indexableKey === "bigint"
1178
1312
  ) {
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
- );
1313
+ const request = new requestClazz({
1314
+ query: [
1315
+ new indexerTypes.IntegerCompare({
1316
+ key: this.indexBy,
1317
+ compare: indexerTypes.Compare.Equal,
1318
+ value: indexableKey,
1319
+ }),
1320
+ ],
1321
+ });
1322
+ results = await runAndClose(request);
1191
1323
  } 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
- );
1324
+ const request = new requestClazz({
1325
+ query: [
1326
+ new indexerTypes.StringMatch({
1327
+ key: this.indexBy,
1328
+ value: indexableKey,
1329
+ }),
1330
+ ],
1331
+ });
1332
+ results = await runAndClose(request);
1203
1333
  } 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
- );
1334
+ const request = new requestClazz({
1335
+ query: [
1336
+ new indexerTypes.ByteMatchQuery({
1337
+ key: this.indexBy,
1338
+ value: indexableKey,
1339
+ }),
1340
+ ],
1341
+ });
1342
+ results = await runAndClose(request);
1343
+ } else if ((indexableKey as any) instanceof ArrayBuffer) {
1344
+ const request = new requestClazz({
1345
+ query: [
1346
+ new indexerTypes.ByteMatchQuery({
1347
+ key: this.indexBy,
1348
+ value: new Uint8Array(indexableKey),
1349
+ }),
1350
+ ],
1351
+ });
1352
+ results = await runAndClose(request);
1353
+ } else {
1354
+ throw new Error("Unsupported key type");
1215
1355
  }
1216
1356
  }
1217
1357
 
@@ -1317,6 +1457,7 @@ export class DocumentIndex<
1317
1457
  R extends
1318
1458
  | types.SearchRequest
1319
1459
  | types.SearchRequestIndexed
1460
+ | types.IterationRequest
1320
1461
  | types.CollectNextRequest,
1321
1462
  >(
1322
1463
  query: R,
@@ -1338,25 +1479,57 @@ export class DocumentIndex<
1338
1479
  let indexedResult: indexerTypes.IndexedResults<WithContext<I>> | undefined =
1339
1480
  undefined;
1340
1481
 
1341
- let fromQuery: types.SearchRequest | types.SearchRequestIndexed | undefined;
1482
+ let fromQuery:
1483
+ | types.SearchRequest
1484
+ | types.SearchRequestIndexed
1485
+ | types.IterationRequest
1486
+ | undefined;
1487
+ let keepAliveRequest: types.IterationRequest | undefined;
1342
1488
  if (
1343
1489
  query instanceof types.SearchRequest ||
1344
- query instanceof types.SearchRequestIndexed
1490
+ query instanceof types.SearchRequestIndexed ||
1491
+ query instanceof types.IterationRequest
1345
1492
  ) {
1346
1493
  fromQuery = query;
1347
- indexedResult = await this._resumableIterators.iterateAndFetch(query);
1494
+ if (
1495
+ !isLocal &&
1496
+ query instanceof types.IterationRequest &&
1497
+ query.keepAliveTtl != null
1498
+ ) {
1499
+ keepAliveRequest = query;
1500
+ }
1501
+ indexedResult = await this._resumableIterators.iterateAndFetch(query, {
1502
+ keepAlive: keepAliveRequest !== undefined,
1503
+ });
1348
1504
  } else if (query instanceof types.CollectNextRequest) {
1349
- fromQuery =
1505
+ const cachedRequest =
1350
1506
  prevQueued?.fromQuery ||
1351
1507
  this._resumableIterators.queues.get(query.idString)?.request;
1508
+ fromQuery = cachedRequest;
1509
+ if (
1510
+ !isLocal &&
1511
+ cachedRequest instanceof types.IterationRequest &&
1512
+ cachedRequest.keepAliveTtl != null
1513
+ ) {
1514
+ keepAliveRequest = cachedRequest;
1515
+ }
1352
1516
  indexedResult =
1353
1517
  prevQueued?.keptInIndex === 0
1354
1518
  ? []
1355
- : await this._resumableIterators.next(query);
1519
+ : await this._resumableIterators.next(query, {
1520
+ keepAlive: keepAliveRequest !== undefined,
1521
+ });
1356
1522
  } else {
1357
1523
  throw new Error("Unsupported");
1358
1524
  }
1359
1525
 
1526
+ if (!isLocal && keepAliveRequest) {
1527
+ this.scheduleIteratorKeepAlive(
1528
+ query.idString,
1529
+ keepAliveRequest.keepAliveTtl,
1530
+ );
1531
+ }
1532
+
1360
1533
  let resultSize = 0;
1361
1534
 
1362
1535
  let toIterate = prevQueued
@@ -1381,13 +1554,15 @@ export class DocumentIndex<
1381
1554
  keptInIndex: kept,
1382
1555
  fromQuery: (fromQuery || query) as
1383
1556
  | types.SearchRequest
1384
- | types.SearchRequestIndexed,
1557
+ | types.SearchRequestIndexed
1558
+ | types.IterationRequest,
1385
1559
  };
1386
1560
  this._resultQueue.set(query.idString, prevQueued);
1387
1561
  }
1388
1562
 
1389
1563
  const filteredResults: types.Result[] = [];
1390
-
1564
+ const resolveDocumentsFlag = resolvesDocuments(fromQuery);
1565
+ const replicateIndexFlag = replicatesIndex(fromQuery);
1391
1566
  for (const result of toIterate) {
1392
1567
  if (!isLocal) {
1393
1568
  resultSize += result.value.__context.size;
@@ -1407,7 +1582,7 @@ export class DocumentIndex<
1407
1582
  ) {
1408
1583
  continue;
1409
1584
  }
1410
- if (fromQuery instanceof types.SearchRequest) {
1585
+ if (resolveDocumentsFlag) {
1411
1586
  const value = await this.resolveDocument({
1412
1587
  indexed: result.value,
1413
1588
  head: result.value.__context.head,
@@ -1425,11 +1600,11 @@ export class DocumentIndex<
1425
1600
  indexed: indexedUnwrapped,
1426
1601
  }),
1427
1602
  );
1428
- } else if (fromQuery instanceof types.SearchRequestIndexed) {
1603
+ } else {
1429
1604
  const context = result.value.__context;
1430
1605
  const head = await this._log.log.get(context.head);
1431
1606
  // assume remote peer will start to replicate (TODO is this ideal?)
1432
- if (fromQuery.replicate) {
1607
+ if (replicateIndexFlag) {
1433
1608
  this._log.addPeersToGidPeerHistory(context.gid, [from.hashcode()]);
1434
1609
  }
1435
1610
 
@@ -1456,10 +1631,53 @@ export class DocumentIndex<
1456
1631
  return results;
1457
1632
  }
1458
1633
 
1634
+ private scheduleIteratorKeepAlive(idString: string, ttl?: bigint) {
1635
+ if (ttl == null) {
1636
+ return;
1637
+ }
1638
+ const ttlNumber = Number(ttl);
1639
+ if (!Number.isFinite(ttlNumber) || ttlNumber <= 0) {
1640
+ return;
1641
+ }
1642
+
1643
+ // Cap max timeout to 1 day (TODO make configurable?)
1644
+ const delay = Math.max(1, Math.min(ttlNumber, 86400000));
1645
+ this.cancelIteratorKeepAlive(idString);
1646
+ const timers =
1647
+ this.iteratorKeepAliveTimers ??
1648
+ (this.iteratorKeepAliveTimers = new Map<
1649
+ string,
1650
+ ReturnType<typeof setTimeout>
1651
+ >());
1652
+ const timer = setTimeout(() => {
1653
+ timers.delete(idString);
1654
+ const queued = this._resultQueue.get(idString);
1655
+ if (queued) {
1656
+ clearTimeout(queued.timeout);
1657
+ this._resultQueue.delete(idString);
1658
+ }
1659
+ this._resumableIterators.close({ idString });
1660
+ }, delay);
1661
+ timers.set(idString, timer);
1662
+ }
1663
+
1664
+ private cancelIteratorKeepAlive(idString: string) {
1665
+ const timers = this.iteratorKeepAliveTimers;
1666
+ if (!timers) {
1667
+ return;
1668
+ }
1669
+ const timer = timers.get(idString);
1670
+ if (timer) {
1671
+ clearTimeout(timer);
1672
+ timers.delete(idString);
1673
+ }
1674
+ }
1675
+
1459
1676
  private clearResultsQueue(
1460
1677
  query:
1461
1678
  | types.SearchRequest
1462
1679
  | types.SearchRequestIndexed
1680
+ | types.IterationRequest
1463
1681
  | types.CollectNextRequest
1464
1682
  | types.CloseIteratorRequest,
1465
1683
  ) {
@@ -1478,6 +1696,7 @@ export class DocumentIndex<
1478
1696
  for (const [key, queue] of this._resultQueue) {
1479
1697
  clearTimeout(queue.timeout);
1480
1698
  this._resultQueue.delete(key);
1699
+ this.cancelIteratorKeepAlive(key);
1481
1700
  this._resumableIterators.close({ idString: key });
1482
1701
  }
1483
1702
  }
@@ -1644,6 +1863,7 @@ export class DocumentIndex<
1644
1863
  logger.info("Ignoring close iterator request from different peer");
1645
1864
  return;
1646
1865
  }
1866
+ this.cancelIteratorKeepAlive(query.idString);
1647
1867
  this.clearResultsQueue(query);
1648
1868
  return this._resumableIterators.close(query);
1649
1869
  }
@@ -1655,7 +1875,10 @@ export class DocumentIndex<
1655
1875
  * @returns
1656
1876
  */
1657
1877
  private async queryCommence<
1658
- R extends types.SearchRequest | types.SearchRequestIndexed,
1878
+ R extends
1879
+ | types.SearchRequest
1880
+ | types.SearchRequestIndexed
1881
+ | types.IterationRequest,
1659
1882
  RT extends types.Result = types.ResultTypeFromRequest<R, T, I>,
1660
1883
  >(
1661
1884
  queryRequest: R,
@@ -1899,7 +2122,11 @@ export class DocumentIndex<
1899
2122
  * @returns
1900
2123
  */
1901
2124
  public async search<
1902
- R extends types.SearchRequest | types.SearchRequestIndexed | QueryLike,
2125
+ R extends
2126
+ | types.SearchRequest
2127
+ | types.SearchRequestIndexed
2128
+ | types.IterationRequest
2129
+ | QueryLike,
1903
2130
  O extends SearchOptions<T, I, D, Resolve>,
1904
2131
  Resolve extends boolean = ExtractResolveFromOptions<O>,
1905
2132
  >(
@@ -1907,8 +2134,11 @@ export class DocumentIndex<
1907
2134
  options?: O,
1908
2135
  ): Promise<ValueTypeFromRequest<Resolve, T, I>[]> {
1909
2136
  // Set fetch to search size, or max value (default to max u32 (4294967295))
1910
- const coercedRequest: types.SearchRequest | types.SearchRequestIndexed =
1911
- coerceQuery(queryRequest, options);
2137
+ const coercedRequest = coerceQuery(
2138
+ queryRequest,
2139
+ options,
2140
+ this.compatibility,
2141
+ );
1912
2142
  coercedRequest.fetch = coercedRequest.fetch ?? 0xffffffff;
1913
2143
 
1914
2144
  // So that the iterator is pre-fetching the right amount of entries
@@ -1987,7 +2217,7 @@ export class DocumentIndex<
1987
2217
  /**
1988
2218
  * Query and retrieve documents in a iterator
1989
2219
  * @param queryRequest
1990
- * @param options
2220
+ * @param optionsArg
1991
2221
  * @returns
1992
2222
  */
1993
2223
  public iterate<
@@ -1996,8 +2226,9 @@ export class DocumentIndex<
1996
2226
  Resolve extends boolean | undefined = ExtractResolveFromOptions<O>,
1997
2227
  >(
1998
2228
  queryRequest?: R,
1999
- options?: QueryOptions<T, I, D, Resolve>,
2229
+ optionsArg?: QueryOptions<T, I, D, Resolve>,
2000
2230
  ): ResultsIterator<ValueTypeFromRequest<Resolve, T, I>> {
2231
+ let options = optionsArg;
2001
2232
  if (
2002
2233
  queryRequest instanceof types.SearchRequest &&
2003
2234
  options?.resolve === false
@@ -2005,14 +2236,44 @@ export class DocumentIndex<
2005
2236
  throw new Error("Cannot use resolve=false with SearchRequest"); // TODO make this work
2006
2237
  }
2007
2238
 
2008
- let queryRequestCoerced: types.SearchRequest | types.SearchRequestIndexed =
2009
- coerceQuery(queryRequest ?? {}, options);
2239
+ let queryRequestCoerced = coerceQuery(
2240
+ queryRequest ?? {},
2241
+ options,
2242
+ this.compatibility,
2243
+ );
2244
+
2245
+ const {
2246
+ mergePolicy,
2247
+ push: pushUpdates,
2248
+ callbacks: updateCallbacksRaw,
2249
+ } = normalizeUpdatesOption(options?.updates);
2250
+ const hasLiveUpdates = mergePolicy !== undefined;
2251
+ const originalRemote = options?.remote;
2252
+ let remoteOptions =
2253
+ typeof originalRemote === "boolean"
2254
+ ? originalRemote
2255
+ : originalRemote
2256
+ ? { ...originalRemote }
2257
+ : undefined;
2258
+ if (pushUpdates && remoteOptions !== false) {
2259
+ if (typeof remoteOptions === "object") {
2260
+ if (remoteOptions.replicate !== true) {
2261
+ remoteOptions.replicate = true;
2262
+ }
2263
+ } else if (remoteOptions === undefined || remoteOptions === true) {
2264
+ remoteOptions = { replicate: true };
2265
+ }
2266
+ }
2267
+ if (remoteOptions !== originalRemote) {
2268
+ options = Object.assign({}, options, { remote: remoteOptions });
2269
+ }
2010
2270
 
2011
2271
  let resolve = options?.resolve !== false;
2012
2272
  if (
2273
+ !(queryRequestCoerced instanceof types.IterationRequest) &&
2013
2274
  options?.remote &&
2014
2275
  typeof options.remote !== "boolean" &&
2015
- options.remote.replicate &&
2276
+ (options.remote.replicate || pushUpdates) &&
2016
2277
  options?.resolve !== false
2017
2278
  ) {
2018
2279
  if (
@@ -2032,7 +2293,7 @@ export class DocumentIndex<
2032
2293
  let replicate =
2033
2294
  options?.remote &&
2034
2295
  typeof options.remote !== "boolean" &&
2035
- options.remote.replicate;
2296
+ (options.remote.replicate || pushUpdates);
2036
2297
  if (
2037
2298
  replicate &&
2038
2299
  queryRequestCoerced instanceof types.SearchRequestIndexed
@@ -2109,6 +2370,8 @@ export class DocumentIndex<
2109
2370
 
2110
2371
  let warmupPromise: Promise<any> | undefined = undefined;
2111
2372
 
2373
+ let discoveredTargetHashes: string[] | undefined;
2374
+
2112
2375
  if (typeof options?.remote === "object") {
2113
2376
  let waitForTime: number | undefined = undefined;
2114
2377
  if (options.remote.wait) {
@@ -2145,11 +2408,26 @@ export class DocumentIndex<
2145
2408
  }
2146
2409
 
2147
2410
  if (options.remote.reach?.discover) {
2148
- warmupPromise = this.waitFor(options.remote.reach.discover, {
2411
+ const discoverTimeout =
2412
+ waitForTime ??
2413
+ (options.remote.wait ? DEFAULT_TIMEOUT : DISCOVER_TIMEOUT_FALLBACK);
2414
+ const discoverPromise = this.waitFor(options.remote.reach.discover, {
2149
2415
  signal: ensureController().signal,
2150
2416
  seek: "present",
2151
- timeout: waitForTime ?? DEFAULT_TIMEOUT,
2152
- });
2417
+ timeout: discoverTimeout,
2418
+ })
2419
+ .then((hashes) => {
2420
+ discoveredTargetHashes = hashes;
2421
+ })
2422
+ .catch((error) => {
2423
+ if (error instanceof TimeoutError || error instanceof AbortError) {
2424
+ discoveredTargetHashes = [];
2425
+ return;
2426
+ }
2427
+ throw error;
2428
+ });
2429
+ const prior = warmupPromise ?? Promise.resolve();
2430
+ warmupPromise = prior.then(() => discoverPromise);
2153
2431
  options.remote.reach.eager = true; // include the results from the discovered peer even if it is not mature
2154
2432
  }
2155
2433
 
@@ -2181,6 +2459,18 @@ export class DocumentIndex<
2181
2459
  ): Promise<boolean> => {
2182
2460
  await warmupPromise;
2183
2461
  let hasMore = false;
2462
+ const discoverTargets =
2463
+ typeof options?.remote === "object"
2464
+ ? options.remote.reach?.discover
2465
+ : undefined;
2466
+ const initialRemoteTargets =
2467
+ discoveredTargetHashes !== undefined
2468
+ ? discoveredTargetHashes
2469
+ : discoverTargets?.map((pk) => pk.hashcode().toString());
2470
+ const skipRemoteDueToDiscovery =
2471
+ typeof options?.remote === "object" &&
2472
+ options.remote.reach?.discover &&
2473
+ discoveredTargetHashes?.length === 0;
2184
2474
 
2185
2475
  queryRequestCoerced.fetch = n;
2186
2476
  await this.queryCommence(
@@ -2188,12 +2478,12 @@ export class DocumentIndex<
2188
2478
  {
2189
2479
  local: fetchOptions?.from != null ? false : options?.local,
2190
2480
  remote:
2191
- options?.remote !== false
2481
+ options?.remote !== false && !skipRemoteDueToDiscovery
2192
2482
  ? {
2193
2483
  ...(typeof options?.remote === "object"
2194
2484
  ? options.remote
2195
2485
  : {}),
2196
- from: fetchOptions?.from,
2486
+ from: fetchOptions?.from ?? initialRemoteTargets,
2197
2487
  }
2198
2488
  : false,
2199
2489
  resolve,
@@ -2210,17 +2500,25 @@ export class DocumentIndex<
2210
2500
  types.ResultTypeFromRequest<R, T, I>
2211
2501
  >;
2212
2502
 
2503
+ const existingBuffer = peerBufferMap.get(from.hashcode());
2504
+ const buffer: BufferedResult<
2505
+ types.ResultTypeFromRequest<R, T, I> | I,
2506
+ I
2507
+ >[] = existingBuffer?.buffer || [];
2508
+
2213
2509
  if (results.kept === 0n && results.results.length === 0) {
2510
+ if (keepRemoteAlive) {
2511
+ peerBufferMap.set(from.hashcode(), {
2512
+ buffer,
2513
+ kept: Number(response.kept),
2514
+ });
2515
+ }
2214
2516
  return;
2215
2517
  }
2216
2518
 
2217
2519
  if (results.kept > 0n) {
2218
2520
  hasMore = true;
2219
2521
  }
2220
- const buffer: BufferedResult<
2221
- types.ResultTypeFromRequest<R, T, I> | I,
2222
- I
2223
- >[] = peerBufferMap.get(from.hashcode())?.buffer || [];
2224
2522
 
2225
2523
  for (const result of results.results) {
2226
2524
  const indexKey = indexerTypes.toId(
@@ -2322,7 +2620,8 @@ export class DocumentIndex<
2322
2620
 
2323
2621
  for (const [peer, buffer] of peerBufferMap) {
2324
2622
  if (buffer.buffer.length < n) {
2325
- if (buffer.kept === 0) {
2623
+ const hasExistingRemoteResults = buffer.kept > 0;
2624
+ if (!hasExistingRemoteResults && !keepRemoteAlive) {
2326
2625
  if (peerBufferMap.get(peer)?.buffer.length === 0) {
2327
2626
  peerBufferMap.delete(peer); // No more results
2328
2627
  }
@@ -2332,9 +2631,16 @@ export class DocumentIndex<
2332
2631
  // TODO buffer more than deleted?
2333
2632
  // TODO batch to multiple 'to's
2334
2633
 
2634
+ const lacking = n - buffer.buffer.length;
2635
+ const amount = lacking > 0 ? lacking : keepRemoteAlive ? 1 : 0;
2636
+
2637
+ if (amount <= 0) {
2638
+ continue;
2639
+ }
2640
+
2335
2641
  const collectRequest = new types.CollectNextRequest({
2336
2642
  id: queryRequestCoerced.id,
2337
- amount: n - buffer.buffer.length,
2643
+ amount,
2338
2644
  });
2339
2645
  // Fetch locally?
2340
2646
  if (peer === this.node.identity.publicKey.hashcode()) {
@@ -2351,7 +2657,10 @@ export class DocumentIndex<
2351
2657
  resultsLeft += Number(results.kept);
2352
2658
 
2353
2659
  if (results.results.length === 0) {
2354
- if (peerBufferMap.get(peer)?.buffer.length === 0) {
2660
+ if (
2661
+ !keepRemoteAlive &&
2662
+ peerBufferMap.get(peer)?.buffer.length === 0
2663
+ ) {
2355
2664
  peerBufferMap.delete(peer); // No more results
2356
2665
  }
2357
2666
  } else {
@@ -2492,7 +2801,10 @@ export class DocumentIndex<
2492
2801
  }
2493
2802
 
2494
2803
  if (response.response.results.length === 0) {
2495
- if (peerBufferMap.get(peer)?.buffer.length === 0) {
2804
+ if (
2805
+ !keepRemoteAlive &&
2806
+ peerBufferMap.get(peer)?.buffer.length === 0
2807
+ ) {
2496
2808
  peerBufferMap.delete(peer); // No more results
2497
2809
  }
2498
2810
  } else {
@@ -2745,48 +3057,96 @@ export class DocumentIndex<
2745
3057
  let fetchedFirstForRemote: Set<string> | undefined = undefined;
2746
3058
 
2747
3059
  let updateDeferred: ReturnType<typeof pDefer> | undefined;
2748
- const signalUpdate = () => updateDeferred?.resolve();
3060
+ const signalUpdate = (reason?: string) => {
3061
+ updateDeferred?.resolve();
3062
+ };
2749
3063
  const _waitForUpdate = () =>
2750
3064
  updateDeferred ? updateDeferred.promise : Promise.resolve();
2751
3065
 
2752
3066
  // ---------------- 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)
3067
+ function normalizeUpdatesOption(u?: UpdateOptions<T, I, Resolve>): {
3068
+ mergePolicy?: {
3069
+ merge?:
3070
+ | {
3071
+ filter?: (
3072
+ evt: DocumentsChange<T, I>,
3073
+ ) => MaybePromise<DocumentsChange<T, I> | void>;
3074
+ }
3075
+ | undefined;
3076
+ };
3077
+ push: boolean;
3078
+ callbacks?: UpdateCallbacks<T, I, Resolve>;
3079
+ } {
3080
+ const identityFilter = (evt: DocumentsChange<T, I>) => evt;
3081
+ const buildMergePolicy = (
3082
+ merge: UpdateMergeStrategy<T, I, Resolve> | undefined,
3083
+ defaultEnabled: boolean,
3084
+ ) => {
3085
+ const effective =
3086
+ merge === undefined ? (defaultEnabled ? true : undefined) : merge;
3087
+ if (effective === undefined || effective === false) {
3088
+ return undefined;
3089
+ }
3090
+ if (effective === true) {
3091
+ return {
3092
+ merge: {
3093
+ filter: identityFilter,
3094
+ },
3095
+ };
3096
+ }
2768
3097
  return {
2769
3098
  merge: {
2770
- filter: (evt) => evt,
3099
+ filter: effective.filter ?? identityFilter,
2771
3100
  },
2772
3101
  };
3102
+ };
3103
+
3104
+ if (u == null || u === false) {
3105
+ return { push: false };
3106
+ }
3107
+
3108
+ if (u === true) {
3109
+ return {
3110
+ mergePolicy: buildMergePolicy(true, true),
3111
+ push: false,
3112
+ };
3113
+ }
3114
+
3115
+ if (typeof u === "string") {
3116
+ if (u === "remote") {
3117
+ return { push: true };
3118
+ }
3119
+ if (u === "local") {
3120
+ return {
3121
+ mergePolicy: buildMergePolicy(true, true),
3122
+ push: false,
3123
+ };
3124
+ }
3125
+ if (u === "all") {
3126
+ return {
3127
+ mergePolicy: buildMergePolicy(true, true),
3128
+ push: true,
3129
+ };
3130
+ }
3131
+ }
3132
+
2773
3133
  if (typeof u === "object") {
3134
+ const hasMergeProp = Object.prototype.hasOwnProperty.call(u, "merge");
3135
+ const mergeValue = hasMergeProp ? u.merge : undefined;
2774
3136
  return {
2775
- merge: u.merge
2776
- ? {
2777
- filter:
2778
- typeof u.merge === "object" ? u.merge.filter : (evt) => evt,
2779
- }
2780
- : {},
3137
+ mergePolicy: buildMergePolicy(
3138
+ mergeValue,
3139
+ !hasMergeProp || mergeValue === undefined,
3140
+ ),
3141
+ push: Boolean(u.push),
3142
+ callbacks: u,
2781
3143
  };
2782
3144
  }
2783
- return undefined;
2784
- };
2785
3145
 
2786
- const updateCallbacks =
2787
- typeof options?.updates === "object" ? options.updates : undefined;
2788
- const mergePolicy = normalizeUpdatesOption(options?.updates);
2789
- const hasLiveUpdates = mergePolicy !== undefined;
3146
+ return { push: false };
3147
+ }
3148
+
3149
+ const updateCallbacks = updateCallbacksRaw;
2790
3150
  let pendingResultsReason:
2791
3151
  | Extract<ResultBatchReason, "join" | "change">
2792
3152
  | undefined;
@@ -2819,6 +3179,174 @@ export class DocumentIndex<
2819
3179
  updateDeferred = pDefer<void>();
2820
3180
  }
2821
3181
 
3182
+ const keepRemoteAlive =
3183
+ (options?.closePolicy === "manual" || hasLiveUpdates || pushUpdates) &&
3184
+ remoteOptions !== false;
3185
+
3186
+ if (queryRequestCoerced instanceof types.IterationRequest) {
3187
+ queryRequestCoerced.resolve = resolve;
3188
+ queryRequestCoerced.fetch = queryRequestCoerced.fetch ?? 10;
3189
+ const replicateFlag = !resolve && replicate ? true : false;
3190
+ queryRequestCoerced.replicate = replicateFlag;
3191
+ const ttlSource =
3192
+ typeof remoteOptions === "object" &&
3193
+ typeof remoteOptions?.wait === "object"
3194
+ ? (remoteOptions.wait.timeout ?? DEFAULT_TIMEOUT)
3195
+ : DEFAULT_TIMEOUT;
3196
+ queryRequestCoerced.keepAliveTtl = keepRemoteAlive
3197
+ ? BigInt(ttlSource)
3198
+ : undefined;
3199
+ queryRequestCoerced.pushUpdates = pushUpdates ? true : undefined;
3200
+ queryRequestCoerced.mergeUpdates = mergePolicy?.merge ? true : undefined;
3201
+ }
3202
+
3203
+ if (pushUpdates && this.prefetch?.accumulator) {
3204
+ const targetPrefetchKey = idAgnosticQueryKey(queryRequestCoerced);
3205
+ const mergePrefetchedResults = async (
3206
+ from: PublicSignKey,
3207
+ results: types.Results<types.ResultTypeFromRequest<R, T, I>>,
3208
+ ) => {
3209
+ const peerHash = from.hashcode();
3210
+ const existingBuffer = peerBufferMap.get(peerHash);
3211
+ const buffer: BufferedResult<
3212
+ types.ResultTypeFromRequest<R, T, I> | I,
3213
+ I
3214
+ >[] = existingBuffer?.buffer || [];
3215
+
3216
+ if (results.kept === 0n && results.results.length === 0) {
3217
+ peerBufferMap.set(peerHash, {
3218
+ buffer,
3219
+ kept: Number(results.kept),
3220
+ });
3221
+ return;
3222
+ }
3223
+
3224
+ for (const result of results.results) {
3225
+ const indexKey = indexerTypes.toId(
3226
+ this.indexByResolver(result.value),
3227
+ ).primitive;
3228
+ if (result instanceof types.ResultValue) {
3229
+ const existingIndexed = indexedPlaceholders?.get(indexKey);
3230
+ if (existingIndexed) {
3231
+ existingIndexed.value =
3232
+ result.value as types.ResultTypeFromRequest<R, T, I>;
3233
+ existingIndexed.context = result.context;
3234
+ existingIndexed.from = from;
3235
+ existingIndexed.indexed = await this.resolveIndexed<R>(
3236
+ result,
3237
+ results.results as types.ResultTypeFromRequest<R, T, I>[],
3238
+ );
3239
+ indexedPlaceholders?.delete(indexKey);
3240
+ continue;
3241
+ }
3242
+ if (visited.has(indexKey)) {
3243
+ continue;
3244
+ }
3245
+ visited.add(indexKey);
3246
+ buffer.push({
3247
+ value: result.value as types.ResultTypeFromRequest<R, T, I>,
3248
+ context: result.context,
3249
+ from,
3250
+ indexed: await this.resolveIndexed<R>(
3251
+ result,
3252
+ results.results as types.ResultTypeFromRequest<R, T, I>[],
3253
+ ),
3254
+ });
3255
+ } else {
3256
+ if (visited.has(indexKey) && !indexedPlaceholders?.has(indexKey)) {
3257
+ continue;
3258
+ }
3259
+ visited.add(indexKey);
3260
+ const indexed = coerceWithContext(
3261
+ result.indexed || result.value,
3262
+ result.context,
3263
+ );
3264
+ const placeholder = {
3265
+ value: result.value,
3266
+ context: result.context,
3267
+ from,
3268
+ indexed,
3269
+ };
3270
+ buffer.push(placeholder);
3271
+ ensureIndexedPlaceholders().set(indexKey, placeholder);
3272
+ }
3273
+ }
3274
+
3275
+ peerBufferMap.set(peerHash, {
3276
+ buffer,
3277
+ kept: Number(results.kept),
3278
+ });
3279
+ };
3280
+
3281
+ const consumePrefetch = async (
3282
+ consumable: RPCResponse<types.PredictedSearchRequest<any>>,
3283
+ ) => {
3284
+ const request = consumable.response?.request;
3285
+ if (!request) {
3286
+ return;
3287
+ }
3288
+ if (idAgnosticQueryKey(request) !== targetPrefetchKey) {
3289
+ return;
3290
+ }
3291
+ try {
3292
+ const prepared = await introduceEntries(
3293
+ queryRequestCoerced,
3294
+ [
3295
+ {
3296
+ response: consumable.response.results,
3297
+ from: consumable.from,
3298
+ },
3299
+ ],
3300
+ this.documentType,
3301
+ this.indexedType,
3302
+ this._sync,
3303
+ options as QueryDetailedOptions<T, I, D, any>,
3304
+ );
3305
+
3306
+ for (const response of prepared) {
3307
+ if (!response.from) {
3308
+ continue;
3309
+ }
3310
+ const payload = response.response;
3311
+ if (!(payload instanceof types.Results)) {
3312
+ continue;
3313
+ }
3314
+ await mergePrefetchedResults(
3315
+ response.from,
3316
+ payload as types.Results<types.ResultTypeFromRequest<R, T, I>>,
3317
+ );
3318
+ }
3319
+
3320
+ if (!pendingResultsReason) {
3321
+ pendingResultsReason = "change";
3322
+ }
3323
+ signalUpdate("prefetch-add");
3324
+ } catch (error) {
3325
+ logger.warn("Failed to merge prefetched results", error);
3326
+ }
3327
+ };
3328
+
3329
+ const onPrefetchAdd = (
3330
+ evt: CustomEvent<{
3331
+ consumable: RPCResponse<types.PredictedSearchRequest<any>>;
3332
+ }>,
3333
+ ) => {
3334
+ void consumePrefetch(evt.detail.consumable);
3335
+ };
3336
+ this.prefetch.accumulator.addEventListener(
3337
+ "add",
3338
+ onPrefetchAdd as EventListener,
3339
+ );
3340
+ const cleanupDefault = cleanup;
3341
+ cleanup = () => {
3342
+ this.prefetch?.accumulator.removeEventListener(
3343
+ "add",
3344
+ onPrefetchAdd as EventListener,
3345
+ );
3346
+ return cleanupDefault();
3347
+ };
3348
+ }
3349
+
2822
3350
  let updatesCleanup: (() => void) | undefined;
2823
3351
  if (hasLiveUpdates) {
2824
3352
  const localHash = this.node.identity.publicKey.hashcode();
@@ -2998,6 +3526,7 @@ export class DocumentIndex<
2998
3526
  changeForCallback.removed.length > 0
2999
3527
  ) {
3000
3528
  updateCallbacks?.onChange?.(changeForCallback);
3529
+ signalUpdate("change");
3001
3530
  }
3002
3531
  }
3003
3532
  }
@@ -3099,8 +3628,8 @@ export class DocumentIndex<
3099
3628
  };
3100
3629
  }
3101
3630
 
3102
- if (options?.closePolicy === "manual") {
3103
- let prevMaybeSetDone = maybeSetDone;
3631
+ if (keepRemoteAlive) {
3632
+ const prevMaybeSetDone = maybeSetDone;
3104
3633
  maybeSetDone = () => {
3105
3634
  if (drain) {
3106
3635
  prevMaybeSetDone();
@@ -3126,7 +3655,16 @@ export class DocumentIndex<
3126
3655
  close,
3127
3656
  next,
3128
3657
  done: doneFn,
3129
- pending: () => {
3658
+ pending: async () => {
3659
+ try {
3660
+ await fetchPromise;
3661
+ if (!done && keepRemoteAlive) {
3662
+ await fetchAtLeast(1);
3663
+ }
3664
+ } catch (error) {
3665
+ logger.warn("Failed to refresh iterator pending state", error);
3666
+ }
3667
+
3130
3668
  let pendingCount = 0;
3131
3669
  for (const buffer of peerBufferMap.values()) {
3132
3670
  pendingCount += buffer.kept + buffer.buffer.length;