@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.
@@ -25,34 +25,65 @@ import pDefer, {} from "p-defer";
25
25
  import { concat, fromString } from "uint8arrays";
26
26
  import { copySerialization } from "./borsh.js";
27
27
  import { MAX_BATCH_SIZE } from "./constants.js";
28
- import MostCommonQueryPredictor from "./most-common-query-predictor.js";
28
+ import MostCommonQueryPredictor, { idAgnosticQueryKey, } from "./most-common-query-predictor.js";
29
29
  import { isPutOperation } from "./operation.js";
30
30
  import { Prefetch } from "./prefetch.js";
31
31
  import { ResumableIterators } from "./resumable-iterator.js";
32
32
  const WARNING_WHEN_ITERATING_FOR_MORE_THAN = 1e5;
33
33
  const logger = loggerFn({ module: "document-index" });
34
- const coerceQuery = (query, options) => {
35
- let replicate = typeof options?.remote !== "boolean" ? options?.remote?.replicate : false;
34
+ const coerceQuery = (query, options, compatibility) => {
35
+ const replicate = typeof options?.remote !== "boolean" ? options?.remote?.replicate : false;
36
+ const shouldResolve = options?.resolve !== false;
37
+ const useLegacyRequests = compatibility != null && compatibility <= 9;
36
38
  if (query instanceof types.SearchRequestIndexed &&
37
39
  query.replicate === false &&
38
40
  replicate) {
39
41
  query.replicate = true;
40
42
  return query;
41
43
  }
42
- if (query instanceof types.SearchRequest) {
44
+ if (query instanceof types.SearchRequest ||
45
+ query instanceof types.SearchRequestIndexed) {
46
+ return query;
47
+ }
48
+ if (query instanceof types.IterationRequest) {
49
+ if (useLegacyRequests) {
50
+ if (query.resolve === false) {
51
+ return new types.SearchRequestIndexed({
52
+ query: query.query,
53
+ sort: query.sort,
54
+ fetch: query.fetch,
55
+ replicate: query.replicate ?? replicate,
56
+ });
57
+ }
58
+ return new types.SearchRequest({
59
+ query: query.query,
60
+ sort: query.sort,
61
+ fetch: query.fetch,
62
+ });
63
+ }
43
64
  return query;
44
65
  }
45
66
  const queryObject = query;
46
- return options?.resolve || options?.resolve == null
47
- ? new types.SearchRequest({
48
- query: indexerTypes.toQuery(queryObject.query),
49
- sort: indexerTypes.toSort(query.sort),
50
- })
51
- : new types.SearchRequestIndexed({
67
+ if (useLegacyRequests) {
68
+ if (shouldResolve) {
69
+ return new types.SearchRequest({
70
+ query: indexerTypes.toQuery(queryObject.query),
71
+ sort: indexerTypes.toSort(queryObject.sort),
72
+ });
73
+ }
74
+ return new types.SearchRequestIndexed({
52
75
  query: indexerTypes.toQuery(queryObject.query),
53
- sort: indexerTypes.toSort(query.sort),
76
+ sort: indexerTypes.toSort(queryObject.sort),
54
77
  replicate,
55
78
  });
79
+ }
80
+ return new types.IterationRequest({
81
+ query: indexerTypes.toQuery(queryObject.query),
82
+ sort: indexerTypes.toSort(queryObject.sort),
83
+ fetch: 10,
84
+ resolve: shouldResolve,
85
+ replicate: shouldResolve ? false : replicate,
86
+ });
56
87
  };
57
88
  const introduceEntries = async (queryRequest, responses, documentType, indexedType, sync, options) => {
58
89
  const results = [];
@@ -94,6 +125,30 @@ const dedup = (allResult, dedupBy) => {
94
125
  }
95
126
  return dedup;
96
127
  };
128
+ const resolvesDocuments = (req) => {
129
+ if (!req) {
130
+ return true;
131
+ }
132
+ if (req instanceof types.SearchRequestIndexed) {
133
+ return false;
134
+ }
135
+ if (req instanceof types.IterationRequest) {
136
+ return req.resolve !== false;
137
+ }
138
+ return true;
139
+ };
140
+ const replicatesIndex = (req) => {
141
+ if (!req) {
142
+ return false;
143
+ }
144
+ if (req instanceof types.SearchRequestIndexed) {
145
+ return req.replicate === true;
146
+ }
147
+ if (req instanceof types.IterationRequest) {
148
+ return req.replicate === true;
149
+ }
150
+ return false;
151
+ };
97
152
  function isSubclassOf(SubClass, SuperClass) {
98
153
  // Start with the immediate parent of SubClass
99
154
  let proto = Object.getPrototypeOf(SubClass);
@@ -106,6 +161,7 @@ function isSubclassOf(SubClass, SuperClass) {
106
161
  return false;
107
162
  }
108
163
  const DEFAULT_TIMEOUT = 1e4;
164
+ const DISCOVER_TIMEOUT_FALLBACK = 500;
109
165
  const DEFAULT_INDEX_BY = "id";
110
166
  const isTransformerWithFunction = (options) => {
111
167
  return options.transform != null;
@@ -154,9 +210,11 @@ let DocumentIndex = class DocumentIndex extends Program {
154
210
  documentEvents;
155
211
  _joinListener;
156
212
  _resultQueue;
213
+ iteratorKeepAliveTimers;
157
214
  constructor(properties) {
158
215
  super();
159
216
  this._query = properties?.query || new RPC();
217
+ this.iteratorKeepAliveTimers = new Map();
160
218
  }
161
219
  get valueEncoding() {
162
220
  return this._valueEncoding;
@@ -317,7 +375,8 @@ let DocumentIndex = class DocumentIndex extends Program {
317
375
  }
318
376
  if (this.prefetch?.predictor &&
319
377
  (query instanceof types.SearchRequest ||
320
- query instanceof types.SearchRequestIndexed)) {
378
+ query instanceof types.SearchRequestIndexed ||
379
+ query instanceof types.IterationRequest)) {
321
380
  const { ignore } = this.prefetch.predictor.onRequest(query, {
322
381
  from: ctx.from,
323
382
  });
@@ -334,6 +393,7 @@ let DocumentIndex = class DocumentIndex extends Program {
334
393
  async handleSearchRequest(query, ctx) {
335
394
  if (this.canSearch &&
336
395
  (query instanceof types.SearchRequest ||
396
+ query instanceof types.IterationRequest ||
337
397
  query instanceof types.CollectNextRequest) &&
338
398
  !(await this.canSearch(query, ctx.from))) {
339
399
  return new types.NoAccess();
@@ -342,11 +402,13 @@ let DocumentIndex = class DocumentIndex extends Program {
342
402
  this.processCloseIteratorRequest(query, ctx.from);
343
403
  }
344
404
  else {
345
- const shouldIncludedIndexedResults = this.includeIndexed &&
346
- (query instanceof types.SearchRequest ||
347
- (query instanceof types.CollectNextRequest &&
348
- this._resultQueue.get(query.idString)?.fromQuery instanceof
349
- types.SearchRequest)); // we do this check here because this._resultQueue might be emptied when this.processQuery is called
405
+ const fromQueued = query instanceof types.CollectNextRequest
406
+ ? this._resultQueue.get(query.idString)?.fromQuery
407
+ : undefined;
408
+ const queryResolvesDocuments = query instanceof types.CollectNextRequest
409
+ ? resolvesDocuments(fromQueued)
410
+ : resolvesDocuments(query);
411
+ const shouldIncludedIndexedResults = this.includeIndexed && queryResolvesDocuments;
350
412
  const results = await this.processQuery(query, ctx.from, false, {
351
413
  canRead: this.canRead,
352
414
  });
@@ -585,22 +647,29 @@ let DocumentIndex = class DocumentIndex extends Program {
585
647
  };
586
648
  }
587
649
  let results;
650
+ const runAndClose = async (req) => {
651
+ const response = await this.queryCommence(req, coercedOptions);
652
+ this._resumableIterators.close({ idString: req.idString });
653
+ this.cancelIteratorKeepAlive(req.idString);
654
+ return response;
655
+ };
588
656
  const resolve = coercedOptions?.resolve || coercedOptions?.resolve == null;
589
657
  let requestClazz = resolve
590
658
  ? types.SearchRequest
591
659
  : types.SearchRequestIndexed;
592
660
  if (key instanceof Uint8Array) {
593
- results = await this.queryCommence(new requestClazz({
661
+ const request = new requestClazz({
594
662
  query: [
595
663
  new indexerTypes.ByteMatchQuery({ key: this.indexBy, value: key }),
596
664
  ],
597
- }), coercedOptions);
665
+ });
666
+ results = await runAndClose(request);
598
667
  }
599
668
  else {
600
669
  const indexableKey = indexerTypes.toIdeable(key);
601
670
  if (typeof indexableKey === "number" ||
602
671
  typeof indexableKey === "bigint") {
603
- results = await this.queryCommence(new requestClazz({
672
+ const request = new requestClazz({
604
673
  query: [
605
674
  new indexerTypes.IntegerCompare({
606
675
  key: this.indexBy,
@@ -608,27 +677,44 @@ let DocumentIndex = class DocumentIndex extends Program {
608
677
  value: indexableKey,
609
678
  }),
610
679
  ],
611
- }), coercedOptions);
680
+ });
681
+ results = await runAndClose(request);
612
682
  }
613
683
  else if (typeof indexableKey === "string") {
614
- results = await this.queryCommence(new requestClazz({
684
+ const request = new requestClazz({
615
685
  query: [
616
686
  new indexerTypes.StringMatch({
617
687
  key: this.indexBy,
618
688
  value: indexableKey,
619
689
  }),
620
690
  ],
621
- }), coercedOptions);
691
+ });
692
+ results = await runAndClose(request);
622
693
  }
623
694
  else if (indexableKey instanceof Uint8Array) {
624
- results = await this.queryCommence(new requestClazz({
695
+ const request = new requestClazz({
625
696
  query: [
626
697
  new indexerTypes.ByteMatchQuery({
627
698
  key: this.indexBy,
628
699
  value: indexableKey,
629
700
  }),
630
701
  ],
631
- }), coercedOptions);
702
+ });
703
+ results = await runAndClose(request);
704
+ }
705
+ else if (indexableKey instanceof ArrayBuffer) {
706
+ const request = new requestClazz({
707
+ query: [
708
+ new indexerTypes.ByteMatchQuery({
709
+ key: this.indexBy,
710
+ value: new Uint8Array(indexableKey),
711
+ }),
712
+ ],
713
+ });
714
+ results = await runAndClose(request);
715
+ }
716
+ else {
717
+ throw new Error("Unsupported key type");
632
718
  }
633
719
  }
634
720
  // if we are to resolve the document we need to go through all results and replace the results with the resolved values
@@ -711,23 +797,42 @@ let DocumentIndex = class DocumentIndex extends Program {
711
797
  }
712
798
  let indexedResult = undefined;
713
799
  let fromQuery;
800
+ let keepAliveRequest;
714
801
  if (query instanceof types.SearchRequest ||
715
- query instanceof types.SearchRequestIndexed) {
802
+ query instanceof types.SearchRequestIndexed ||
803
+ query instanceof types.IterationRequest) {
716
804
  fromQuery = query;
717
- indexedResult = await this._resumableIterators.iterateAndFetch(query);
805
+ if (!isLocal &&
806
+ query instanceof types.IterationRequest &&
807
+ query.keepAliveTtl != null) {
808
+ keepAliveRequest = query;
809
+ }
810
+ indexedResult = await this._resumableIterators.iterateAndFetch(query, {
811
+ keepAlive: keepAliveRequest !== undefined,
812
+ });
718
813
  }
719
814
  else if (query instanceof types.CollectNextRequest) {
720
- fromQuery =
721
- prevQueued?.fromQuery ||
722
- this._resumableIterators.queues.get(query.idString)?.request;
815
+ const cachedRequest = prevQueued?.fromQuery ||
816
+ this._resumableIterators.queues.get(query.idString)?.request;
817
+ fromQuery = cachedRequest;
818
+ if (!isLocal &&
819
+ cachedRequest instanceof types.IterationRequest &&
820
+ cachedRequest.keepAliveTtl != null) {
821
+ keepAliveRequest = cachedRequest;
822
+ }
723
823
  indexedResult =
724
824
  prevQueued?.keptInIndex === 0
725
825
  ? []
726
- : await this._resumableIterators.next(query);
826
+ : await this._resumableIterators.next(query, {
827
+ keepAlive: keepAliveRequest !== undefined,
828
+ });
727
829
  }
728
830
  else {
729
831
  throw new Error("Unsupported");
730
832
  }
833
+ if (!isLocal && keepAliveRequest) {
834
+ this.scheduleIteratorKeepAlive(query.idString, keepAliveRequest.keepAliveTtl);
835
+ }
731
836
  let resultSize = 0;
732
837
  let toIterate = prevQueued
733
838
  ? [...prevQueued.queue, ...indexedResult]
@@ -751,6 +856,8 @@ let DocumentIndex = class DocumentIndex extends Program {
751
856
  this._resultQueue.set(query.idString, prevQueued);
752
857
  }
753
858
  const filteredResults = [];
859
+ const resolveDocumentsFlag = resolvesDocuments(fromQuery);
860
+ const replicateIndexFlag = replicatesIndex(fromQuery);
754
861
  for (const result of toIterate) {
755
862
  if (!isLocal) {
756
863
  resultSize += result.value.__context.size;
@@ -764,7 +871,7 @@ let DocumentIndex = class DocumentIndex extends Program {
764
871
  !(await options.canRead(indexedUnwrapped, from))) {
765
872
  continue;
766
873
  }
767
- if (fromQuery instanceof types.SearchRequest) {
874
+ if (resolveDocumentsFlag) {
768
875
  const value = await this.resolveDocument({
769
876
  indexed: result.value,
770
877
  head: result.value.__context.head,
@@ -779,11 +886,11 @@ let DocumentIndex = class DocumentIndex extends Program {
779
886
  indexed: indexedUnwrapped,
780
887
  }));
781
888
  }
782
- else if (fromQuery instanceof types.SearchRequestIndexed) {
889
+ else {
783
890
  const context = result.value.__context;
784
891
  const head = await this._log.log.get(context.head);
785
892
  // assume remote peer will start to replicate (TODO is this ideal?)
786
- if (fromQuery.replicate) {
893
+ if (replicateIndexFlag) {
787
894
  this._log.addPeersToGidPeerHistory(context.gid, [from.hashcode()]);
788
895
  }
789
896
  filteredResults.push(new types.ResultIndexedValue({
@@ -798,11 +905,53 @@ let DocumentIndex = class DocumentIndex extends Program {
798
905
  results: filteredResults,
799
906
  kept: BigInt(kept + (prevQueued?.queue.length || 0)),
800
907
  });
908
+ /* console.debug("[DocumentIndex] processQuery", {
909
+ id: query.idString,
910
+ isLocal,
911
+ batchLength: filteredResults.length,
912
+ kept: results.kept,
913
+ source: from.hashcode(),
914
+ }); */
801
915
  if (!isLocal && results.kept === 0n) {
802
916
  this.clearResultsQueue(query);
803
917
  }
804
918
  return results;
805
919
  }
920
+ scheduleIteratorKeepAlive(idString, ttl) {
921
+ if (ttl == null) {
922
+ return;
923
+ }
924
+ const ttlNumber = Number(ttl);
925
+ if (!Number.isFinite(ttlNumber) || ttlNumber <= 0) {
926
+ return;
927
+ }
928
+ // Cap max timeout to 1 day (TODO make configurable?)
929
+ const delay = Math.max(1, Math.min(ttlNumber, 86400000));
930
+ this.cancelIteratorKeepAlive(idString);
931
+ const timers = this.iteratorKeepAliveTimers ??
932
+ (this.iteratorKeepAliveTimers = new Map());
933
+ const timer = setTimeout(() => {
934
+ timers.delete(idString);
935
+ const queued = this._resultQueue.get(idString);
936
+ if (queued) {
937
+ clearTimeout(queued.timeout);
938
+ this._resultQueue.delete(idString);
939
+ }
940
+ this._resumableIterators.close({ idString });
941
+ }, delay);
942
+ timers.set(idString, timer);
943
+ }
944
+ cancelIteratorKeepAlive(idString) {
945
+ const timers = this.iteratorKeepAliveTimers;
946
+ if (!timers) {
947
+ return;
948
+ }
949
+ const timer = timers.get(idString);
950
+ if (timer) {
951
+ clearTimeout(timer);
952
+ timers.delete(idString);
953
+ }
954
+ }
806
955
  clearResultsQueue(query) {
807
956
  const queue = this._resultQueue.get(query.idString);
808
957
  if (queue) {
@@ -817,6 +966,7 @@ let DocumentIndex = class DocumentIndex extends Program {
817
966
  for (const [key, queue] of this._resultQueue) {
818
967
  clearTimeout(queue.timeout);
819
968
  this._resultQueue.delete(key);
969
+ this.cancelIteratorKeepAlive(key);
820
970
  this._resumableIterators.close({ idString: key });
821
971
  }
822
972
  }
@@ -950,6 +1100,7 @@ let DocumentIndex = class DocumentIndex extends Program {
950
1100
  logger.info("Ignoring close iterator request from different peer");
951
1101
  return;
952
1102
  }
1103
+ this.cancelIteratorKeepAlive(query.idString);
953
1104
  this.clearResultsQueue(query);
954
1105
  return this._resumableIterators.close(query);
955
1106
  }
@@ -1126,7 +1277,7 @@ let DocumentIndex = class DocumentIndex extends Program {
1126
1277
  */
1127
1278
  async search(queryRequest, options) {
1128
1279
  // Set fetch to search size, or max value (default to max u32 (4294967295))
1129
- const coercedRequest = coerceQuery(queryRequest, options);
1280
+ const coercedRequest = coerceQuery(queryRequest, options, this.compatibility);
1130
1281
  coercedRequest.fetch = coercedRequest.fetch ?? 0xffffffff;
1131
1282
  // So that the iterator is pre-fetching the right amount of entries
1132
1283
  const iterator = this.iterate(coercedRequest, options);
@@ -1168,19 +1319,42 @@ let DocumentIndex = class DocumentIndex extends Program {
1168
1319
  /**
1169
1320
  * Query and retrieve documents in a iterator
1170
1321
  * @param queryRequest
1171
- * @param options
1322
+ * @param optionsArg
1172
1323
  * @returns
1173
1324
  */
1174
- iterate(queryRequest, options) {
1325
+ iterate(queryRequest, optionsArg) {
1326
+ let options = optionsArg;
1175
1327
  if (queryRequest instanceof types.SearchRequest &&
1176
1328
  options?.resolve === false) {
1177
1329
  throw new Error("Cannot use resolve=false with SearchRequest"); // TODO make this work
1178
1330
  }
1179
- let queryRequestCoerced = coerceQuery(queryRequest ?? {}, options);
1331
+ let queryRequestCoerced = coerceQuery(queryRequest ?? {}, options, this.compatibility);
1332
+ const { mergePolicy, push: pushUpdates, callbacks: updateCallbacksRaw, } = normalizeUpdatesOption(options?.updates);
1333
+ const hasLiveUpdates = mergePolicy !== undefined;
1334
+ const originalRemote = options?.remote;
1335
+ let remoteOptions = typeof originalRemote === "boolean"
1336
+ ? originalRemote
1337
+ : originalRemote
1338
+ ? { ...originalRemote }
1339
+ : undefined;
1340
+ if (pushUpdates && remoteOptions !== false) {
1341
+ if (typeof remoteOptions === "object") {
1342
+ if (remoteOptions.replicate !== true) {
1343
+ remoteOptions.replicate = true;
1344
+ }
1345
+ }
1346
+ else if (remoteOptions === undefined || remoteOptions === true) {
1347
+ remoteOptions = { replicate: true };
1348
+ }
1349
+ }
1350
+ if (remoteOptions !== originalRemote) {
1351
+ options = Object.assign({}, options, { remote: remoteOptions });
1352
+ }
1180
1353
  let resolve = options?.resolve !== false;
1181
- if (options?.remote &&
1354
+ if (!(queryRequestCoerced instanceof types.IterationRequest) &&
1355
+ options?.remote &&
1182
1356
  typeof options.remote !== "boolean" &&
1183
- options.remote.replicate &&
1357
+ (options.remote.replicate || pushUpdates) &&
1184
1358
  options?.resolve !== false) {
1185
1359
  if ((queryRequest instanceof types.SearchRequestIndexed === false &&
1186
1360
  this.compatibility == null) ||
@@ -1195,7 +1369,7 @@ let DocumentIndex = class DocumentIndex extends Program {
1195
1369
  }
1196
1370
  let replicate = options?.remote &&
1197
1371
  typeof options.remote !== "boolean" &&
1198
- options.remote.replicate;
1372
+ (options.remote.replicate || pushUpdates);
1199
1373
  if (replicate &&
1200
1374
  queryRequestCoerced instanceof types.SearchRequestIndexed) {
1201
1375
  queryRequestCoerced.replicate = true;
@@ -1237,6 +1411,7 @@ let DocumentIndex = class DocumentIndex extends Program {
1237
1411
  this.clearResultsQueue(queryRequestCoerced);
1238
1412
  };
1239
1413
  let warmupPromise = undefined;
1414
+ let discoveredTargetHashes;
1240
1415
  if (typeof options?.remote === "object") {
1241
1416
  let waitForTime = undefined;
1242
1417
  if (options.remote.wait) {
@@ -1271,11 +1446,25 @@ let DocumentIndex = class DocumentIndex extends Program {
1271
1446
  };
1272
1447
  }
1273
1448
  if (options.remote.reach?.discover) {
1274
- warmupPromise = this.waitFor(options.remote.reach.discover, {
1449
+ const discoverTimeout = waitForTime ??
1450
+ (options.remote.wait ? DEFAULT_TIMEOUT : DISCOVER_TIMEOUT_FALLBACK);
1451
+ const discoverPromise = this.waitFor(options.remote.reach.discover, {
1275
1452
  signal: ensureController().signal,
1276
1453
  seek: "present",
1277
- timeout: waitForTime ?? DEFAULT_TIMEOUT,
1454
+ timeout: discoverTimeout,
1455
+ })
1456
+ .then((hashes) => {
1457
+ discoveredTargetHashes = hashes;
1458
+ })
1459
+ .catch((error) => {
1460
+ if (error instanceof TimeoutError || error instanceof AbortError) {
1461
+ discoveredTargetHashes = [];
1462
+ return;
1463
+ }
1464
+ throw error;
1278
1465
  });
1466
+ const prior = warmupPromise ?? Promise.resolve();
1467
+ warmupPromise = prior.then(() => discoverPromise);
1279
1468
  options.remote.reach.eager = true; // include the results from the discovered peer even if it is not mature
1280
1469
  }
1281
1470
  const waitPolicy = typeof options.remote.wait === "object"
@@ -1299,15 +1488,24 @@ let DocumentIndex = class DocumentIndex extends Program {
1299
1488
  const fetchFirst = async (n, fetchOptions) => {
1300
1489
  await warmupPromise;
1301
1490
  let hasMore = false;
1491
+ const discoverTargets = typeof options?.remote === "object"
1492
+ ? options.remote.reach?.discover
1493
+ : undefined;
1494
+ const initialRemoteTargets = discoveredTargetHashes !== undefined
1495
+ ? discoveredTargetHashes
1496
+ : discoverTargets?.map((pk) => pk.hashcode().toString());
1497
+ const skipRemoteDueToDiscovery = typeof options?.remote === "object" &&
1498
+ options.remote.reach?.discover &&
1499
+ discoveredTargetHashes?.length === 0;
1302
1500
  queryRequestCoerced.fetch = n;
1303
1501
  await this.queryCommence(queryRequestCoerced, {
1304
1502
  local: fetchOptions?.from != null ? false : options?.local,
1305
- remote: options?.remote !== false
1503
+ remote: options?.remote !== false && !skipRemoteDueToDiscovery
1306
1504
  ? {
1307
1505
  ...(typeof options?.remote === "object"
1308
1506
  ? options.remote
1309
1507
  : {}),
1310
- from: fetchOptions?.from,
1508
+ from: fetchOptions?.from ?? initialRemoteTargets,
1311
1509
  }
1312
1510
  : false,
1313
1511
  resolve,
@@ -1322,13 +1520,20 @@ let DocumentIndex = class DocumentIndex extends Program {
1322
1520
  }
1323
1521
  else if (response instanceof types.Results) {
1324
1522
  const results = response;
1523
+ const existingBuffer = peerBufferMap.get(from.hashcode());
1524
+ const buffer = existingBuffer?.buffer || [];
1325
1525
  if (results.kept === 0n && results.results.length === 0) {
1526
+ if (keepRemoteAlive) {
1527
+ peerBufferMap.set(from.hashcode(), {
1528
+ buffer,
1529
+ kept: Number(response.kept),
1530
+ });
1531
+ }
1326
1532
  return;
1327
1533
  }
1328
1534
  if (results.kept > 0n) {
1329
1535
  hasMore = true;
1330
1536
  }
1331
- const buffer = peerBufferMap.get(from.hashcode())?.buffer || [];
1332
1537
  for (const result of results.results) {
1333
1538
  const indexKey = indexerTypes.toId(this.indexByResolver(result.value)).primitive;
1334
1539
  if (result instanceof types.ResultValue) {
@@ -1380,6 +1585,15 @@ let DocumentIndex = class DocumentIndex extends Program {
1380
1585
  }
1381
1586
  },
1382
1587
  }, fetchOptions?.fetchedFirstForRemote);
1588
+ /* console.debug(
1589
+ "[DocumentIndex] fetchFirst",
1590
+ {
1591
+ id: queryRequestCoerced.idString,
1592
+ requestedFrom: fetchOptions?.from,
1593
+ initialRemoteTargets,
1594
+ keepRemoteAlive,
1595
+ },
1596
+ ); */
1383
1597
  if (!hasMore) {
1384
1598
  maybeSetDone();
1385
1599
  }
@@ -1403,7 +1617,8 @@ let DocumentIndex = class DocumentIndex extends Program {
1403
1617
  let resultsLeft = 0;
1404
1618
  for (const [peer, buffer] of peerBufferMap) {
1405
1619
  if (buffer.buffer.length < n) {
1406
- if (buffer.kept === 0) {
1620
+ const hasExistingRemoteResults = buffer.kept > 0;
1621
+ if (!hasExistingRemoteResults && !keepRemoteAlive) {
1407
1622
  if (peerBufferMap.get(peer)?.buffer.length === 0) {
1408
1623
  peerBufferMap.delete(peer); // No more results
1409
1624
  }
@@ -1411,9 +1626,21 @@ let DocumentIndex = class DocumentIndex extends Program {
1411
1626
  }
1412
1627
  // TODO buffer more than deleted?
1413
1628
  // TODO batch to multiple 'to's
1629
+ const lacking = n - buffer.buffer.length;
1630
+ const amount = lacking > 0 ? lacking : keepRemoteAlive ? 1 : 0;
1631
+ /* console.debug("[DocumentIndex] fetchAtLeast loop", {
1632
+ peer,
1633
+ bufferLength: buffer.buffer.length,
1634
+ bufferKept: buffer.kept,
1635
+ amount,
1636
+ keepRemoteAlive,
1637
+ }); */
1638
+ if (amount <= 0) {
1639
+ continue;
1640
+ }
1414
1641
  const collectRequest = new types.CollectNextRequest({
1415
1642
  id: queryRequestCoerced.id,
1416
- amount: n - buffer.buffer.length,
1643
+ amount,
1417
1644
  });
1418
1645
  // Fetch locally?
1419
1646
  if (peer === this.node.identity.publicKey.hashcode()) {
@@ -1424,7 +1651,8 @@ let DocumentIndex = class DocumentIndex extends Program {
1424
1651
  .then(async (results) => {
1425
1652
  resultsLeft += Number(results.kept);
1426
1653
  if (results.results.length === 0) {
1427
- if (peerBufferMap.get(peer)?.buffer.length === 0) {
1654
+ if (!keepRemoteAlive &&
1655
+ peerBufferMap.get(peer)?.buffer.length === 0) {
1428
1656
  peerBufferMap.delete(peer); // No more results
1429
1657
  }
1430
1658
  }
@@ -1516,7 +1744,8 @@ let DocumentIndex = class DocumentIndex extends Program {
1516
1744
  return;
1517
1745
  }
1518
1746
  if (response.response.results.length === 0) {
1519
- if (peerBufferMap.get(peer)?.buffer.length === 0) {
1747
+ if (!keepRemoteAlive &&
1748
+ peerBufferMap.get(peer)?.buffer.length === 0) {
1520
1749
  peerBufferMap.delete(peer); // No more results
1521
1750
  }
1522
1751
  }
@@ -1684,32 +1913,75 @@ let DocumentIndex = class DocumentIndex extends Program {
1684
1913
  let joinListener;
1685
1914
  let fetchedFirstForRemote = undefined;
1686
1915
  let updateDeferred;
1687
- const signalUpdate = () => updateDeferred?.resolve();
1916
+ const signalUpdate = (reason) => {
1917
+ if (reason) {
1918
+ /* console.debug("[DocumentIndex] signalUpdate", {
1919
+ id: queryRequestCoerced.idString,
1920
+ reason,
1921
+ }); */
1922
+ }
1923
+ updateDeferred?.resolve();
1924
+ };
1688
1925
  const _waitForUpdate = () => updateDeferred ? updateDeferred.promise : Promise.resolve();
1689
1926
  // ---------------- Live updates wiring (sorted-only with optional filter) ----------------
1690
- const normalizeUpdatesOption = (u) => {
1691
- if (u == null || u === false)
1692
- return undefined;
1693
- if (u === true)
1927
+ function normalizeUpdatesOption(u) {
1928
+ const identityFilter = (evt) => evt;
1929
+ const buildMergePolicy = (merge, defaultEnabled) => {
1930
+ const effective = merge === undefined ? (defaultEnabled ? true : undefined) : merge;
1931
+ if (effective === undefined || effective === false) {
1932
+ return undefined;
1933
+ }
1934
+ if (effective === true) {
1935
+ return {
1936
+ merge: {
1937
+ filter: identityFilter,
1938
+ },
1939
+ };
1940
+ }
1694
1941
  return {
1695
1942
  merge: {
1696
- filter: (evt) => evt,
1943
+ filter: effective.filter ?? identityFilter,
1697
1944
  },
1698
1945
  };
1946
+ };
1947
+ if (u == null || u === false) {
1948
+ return { push: false };
1949
+ }
1950
+ if (u === true) {
1951
+ return {
1952
+ mergePolicy: buildMergePolicy(true, true),
1953
+ push: false,
1954
+ };
1955
+ }
1956
+ if (typeof u === "string") {
1957
+ if (u === "remote") {
1958
+ return { push: true };
1959
+ }
1960
+ if (u === "local") {
1961
+ return {
1962
+ mergePolicy: buildMergePolicy(true, true),
1963
+ push: false,
1964
+ };
1965
+ }
1966
+ if (u === "all") {
1967
+ return {
1968
+ mergePolicy: buildMergePolicy(true, true),
1969
+ push: true,
1970
+ };
1971
+ }
1972
+ }
1699
1973
  if (typeof u === "object") {
1974
+ const hasMergeProp = Object.prototype.hasOwnProperty.call(u, "merge");
1975
+ const mergeValue = hasMergeProp ? u.merge : undefined;
1700
1976
  return {
1701
- merge: u.merge
1702
- ? {
1703
- filter: typeof u.merge === "object" ? u.merge.filter : (evt) => evt,
1704
- }
1705
- : {},
1977
+ mergePolicy: buildMergePolicy(mergeValue, !hasMergeProp || mergeValue === undefined),
1978
+ push: Boolean(u.push),
1979
+ callbacks: u,
1706
1980
  };
1707
1981
  }
1708
- return undefined;
1709
- };
1710
- const updateCallbacks = typeof options?.updates === "object" ? options.updates : undefined;
1711
- const mergePolicy = normalizeUpdatesOption(options?.updates);
1712
- const hasLiveUpdates = mergePolicy !== undefined;
1982
+ return { push: false };
1983
+ }
1984
+ const updateCallbacks = updateCallbacksRaw;
1713
1985
  let pendingResultsReason;
1714
1986
  let hasDeliveredResults = false;
1715
1987
  const emitOnResults = async (batch, defaultReason) => {
@@ -1735,6 +2007,130 @@ let DocumentIndex = class DocumentIndex extends Program {
1735
2007
  if (hasLiveUpdates && !updateDeferred) {
1736
2008
  updateDeferred = pDefer();
1737
2009
  }
2010
+ const keepRemoteAlive = (options?.closePolicy === "manual" || hasLiveUpdates || pushUpdates) &&
2011
+ remoteOptions !== false;
2012
+ if (queryRequestCoerced instanceof types.IterationRequest) {
2013
+ queryRequestCoerced.resolve = resolve;
2014
+ queryRequestCoerced.fetch = queryRequestCoerced.fetch ?? 10;
2015
+ const replicateFlag = !resolve && replicate ? true : false;
2016
+ queryRequestCoerced.replicate = replicateFlag;
2017
+ const ttlSource = typeof remoteOptions === "object" &&
2018
+ typeof remoteOptions?.wait === "object"
2019
+ ? (remoteOptions.wait.timeout ?? DEFAULT_TIMEOUT)
2020
+ : DEFAULT_TIMEOUT;
2021
+ queryRequestCoerced.keepAliveTtl = keepRemoteAlive
2022
+ ? BigInt(ttlSource)
2023
+ : undefined;
2024
+ queryRequestCoerced.pushUpdates = pushUpdates ? true : undefined;
2025
+ queryRequestCoerced.mergeUpdates = mergePolicy?.merge ? true : undefined;
2026
+ }
2027
+ if (pushUpdates && this.prefetch?.accumulator) {
2028
+ const targetPrefetchKey = idAgnosticQueryKey(queryRequestCoerced);
2029
+ const mergePrefetchedResults = async (from, results) => {
2030
+ const peerHash = from.hashcode();
2031
+ const existingBuffer = peerBufferMap.get(peerHash);
2032
+ const buffer = existingBuffer?.buffer || [];
2033
+ if (results.kept === 0n && results.results.length === 0) {
2034
+ peerBufferMap.set(peerHash, {
2035
+ buffer,
2036
+ kept: Number(results.kept),
2037
+ });
2038
+ return;
2039
+ }
2040
+ for (const result of results.results) {
2041
+ const indexKey = indexerTypes.toId(this.indexByResolver(result.value)).primitive;
2042
+ if (result instanceof types.ResultValue) {
2043
+ const existingIndexed = indexedPlaceholders?.get(indexKey);
2044
+ if (existingIndexed) {
2045
+ existingIndexed.value =
2046
+ result.value;
2047
+ existingIndexed.context = result.context;
2048
+ existingIndexed.from = from;
2049
+ existingIndexed.indexed = await this.resolveIndexed(result, results.results);
2050
+ indexedPlaceholders?.delete(indexKey);
2051
+ continue;
2052
+ }
2053
+ if (visited.has(indexKey)) {
2054
+ continue;
2055
+ }
2056
+ visited.add(indexKey);
2057
+ buffer.push({
2058
+ value: result.value,
2059
+ context: result.context,
2060
+ from,
2061
+ indexed: await this.resolveIndexed(result, results.results),
2062
+ });
2063
+ }
2064
+ else {
2065
+ if (visited.has(indexKey) && !indexedPlaceholders?.has(indexKey)) {
2066
+ continue;
2067
+ }
2068
+ visited.add(indexKey);
2069
+ const indexed = coerceWithContext(result.indexed || result.value, result.context);
2070
+ const placeholder = {
2071
+ value: result.value,
2072
+ context: result.context,
2073
+ from,
2074
+ indexed,
2075
+ };
2076
+ buffer.push(placeholder);
2077
+ ensureIndexedPlaceholders().set(indexKey, placeholder);
2078
+ }
2079
+ }
2080
+ peerBufferMap.set(peerHash, {
2081
+ buffer,
2082
+ kept: Number(results.kept),
2083
+ });
2084
+ };
2085
+ const consumePrefetch = async (consumable) => {
2086
+ const request = consumable.response?.request;
2087
+ if (!request) {
2088
+ return;
2089
+ }
2090
+ if (idAgnosticQueryKey(request) !== targetPrefetchKey) {
2091
+ return;
2092
+ }
2093
+ /* console.debug("[DocumentIndex] prefetch match", {
2094
+ iterator: queryRequestCoerced.idString,
2095
+ source: consumable.from?.hashcode(),
2096
+ });
2097
+ */
2098
+ try {
2099
+ const prepared = await introduceEntries(queryRequestCoerced, [
2100
+ {
2101
+ response: consumable.response.results,
2102
+ from: consumable.from,
2103
+ },
2104
+ ], this.documentType, this.indexedType, this._sync, options);
2105
+ for (const response of prepared) {
2106
+ if (!response.from) {
2107
+ continue;
2108
+ }
2109
+ const payload = response.response;
2110
+ if (!(payload instanceof types.Results)) {
2111
+ continue;
2112
+ }
2113
+ await mergePrefetchedResults(response.from, payload);
2114
+ }
2115
+ if (!pendingResultsReason) {
2116
+ pendingResultsReason = "change";
2117
+ }
2118
+ signalUpdate("prefetch-add");
2119
+ }
2120
+ catch (error) {
2121
+ logger.warn("Failed to merge prefetched results", error);
2122
+ }
2123
+ };
2124
+ const onPrefetchAdd = (evt) => {
2125
+ void consumePrefetch(evt.detail.consumable);
2126
+ };
2127
+ this.prefetch.accumulator.addEventListener("add", onPrefetchAdd);
2128
+ const cleanupDefault = cleanup;
2129
+ cleanup = () => {
2130
+ this.prefetch?.accumulator.removeEventListener("add", onPrefetchAdd);
2131
+ return cleanupDefault();
2132
+ };
2133
+ }
1738
2134
  let updatesCleanup;
1739
2135
  if (hasLiveUpdates) {
1740
2136
  const localHash = this.node.identity.publicKey.hashcode();
@@ -1767,6 +2163,11 @@ let DocumentIndex = class DocumentIndex extends Program {
1767
2163
  return value;
1768
2164
  };
1769
2165
  const onChange = async (evt) => {
2166
+ /* console.debug("[DocumentIndex] onChange event", {
2167
+ id: queryRequestCoerced.idString,
2168
+ added: evt.detail.added?.length,
2169
+ removed: evt.detail.removed?.length,
2170
+ }); */
1770
2171
  // Optional filter to mutate/suppress change events
1771
2172
  let filtered = evt.detail;
1772
2173
  if (mergePolicy?.merge?.filter) {
@@ -1862,6 +2263,12 @@ let DocumentIndex = class DocumentIndex extends Program {
1862
2263
  indexed: indexedCandidate,
1863
2264
  };
1864
2265
  buf.buffer.push(placeholder);
2266
+ /* console.debug("[DocumentIndex] buffered change", {
2267
+ id: queryRequestCoerced.idString,
2268
+ placeholderId: (valueForBuffer as any)?.id,
2269
+ peer: localHash,
2270
+ bufferSize: buf.buffer.length,
2271
+ }); */
1865
2272
  if (!resolve) {
1866
2273
  ensureIndexedPlaceholders().set(id, placeholder);
1867
2274
  }
@@ -1875,7 +2282,13 @@ let DocumentIndex = class DocumentIndex extends Program {
1875
2282
  }
1876
2283
  if (changeForCallback.added.length > 0 ||
1877
2284
  changeForCallback.removed.length > 0) {
2285
+ /* console.debug("[DocumentIndex] changeForCallback", {
2286
+ id: queryRequestCoerced.idString,
2287
+ added: changeForCallback.added.map((x) => (x as any)?.id),
2288
+ removed: changeForCallback.removed.map((x) => (x as any)?.id),
2289
+ }); */
1878
2290
  updateCallbacks?.onChange?.(changeForCallback);
2291
+ signalUpdate("change");
1879
2292
  }
1880
2293
  }
1881
2294
  }
@@ -1962,8 +2375,8 @@ let DocumentIndex = class DocumentIndex extends Program {
1962
2375
  return cleanupDefault();
1963
2376
  };
1964
2377
  }
1965
- if (options?.closePolicy === "manual") {
1966
- let prevMaybeSetDone = maybeSetDone;
2378
+ if (keepRemoteAlive) {
2379
+ const prevMaybeSetDone = maybeSetDone;
1967
2380
  maybeSetDone = () => {
1968
2381
  if (drain) {
1969
2382
  prevMaybeSetDone();
@@ -1985,7 +2398,16 @@ let DocumentIndex = class DocumentIndex extends Program {
1985
2398
  close,
1986
2399
  next,
1987
2400
  done: doneFn,
1988
- pending: () => {
2401
+ pending: async () => {
2402
+ try {
2403
+ await fetchPromise;
2404
+ if (!done && keepRemoteAlive) {
2405
+ await fetchAtLeast(1);
2406
+ }
2407
+ }
2408
+ catch (error) {
2409
+ logger.warn("Failed to refresh iterator pending state", error);
2410
+ }
1989
2411
  let pendingCount = 0;
1990
2412
  for (const buffer of peerBufferMap.values()) {
1991
2413
  pendingCount += buffer.kept + buffer.buffer.length;