@peerbit/document 13.1.5 → 13.1.7

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.
@@ -40,7 +40,7 @@ import { tryProjectDocumentIndexSimple, } from "./native-rust.js";
40
40
  import { CachedIndex } from "@peerbit/indexer-cache";
41
41
  import * as indexerTypes from "@peerbit/indexer-interface";
42
42
  import { HashmapIndex } from "@peerbit/indexer-simple";
43
- import { BORSH_ENCODING, Entry } from "@peerbit/log";
43
+ import { BORSH_ENCODING } from "@peerbit/log";
44
44
  import { logger as loggerFn } from "@peerbit/logger";
45
45
  import { ClosedError, Program } from "@peerbit/program";
46
46
  import { MissingResponsesError, RPC, queryAll, } from "@peerbit/rpc";
@@ -48,7 +48,7 @@ import { SharedLog, } from "@peerbit/shared-log";
48
48
  import { DataMessage, FOREGROUND_READ_MESSAGE_PRIORITY, SilentDelivery, } from "@peerbit/stream-interface";
49
49
  import { AbortError, TimeoutError, waitFor } from "@peerbit/time";
50
50
  import pDefer, {} from "p-defer";
51
- import { concat, fromString } from "uint8arrays";
51
+ import { concat, equals, fromString } from "uint8arrays";
52
52
  import { copySerialization } from "./borsh.js";
53
53
  import { MAX_BATCH_SIZE } from "./constants.js";
54
54
  import MostCommonQueryPredictor, { idAgnosticQueryKey, } from "./most-common-query-predictor.js";
@@ -377,6 +377,7 @@ function isSubclassOf(SubClass, SuperClass) {
377
377
  const DEFAULT_TIMEOUT = 1e4;
378
378
  const DEFAULT_KEEP_REMOTE_ITERATOR_TIMEOUT = 3e5;
379
379
  const DISCOVER_TIMEOUT_FALLBACK = 500;
380
+ const CLOSE_ITERATOR_REQUEST_TIMEOUT = 5e3;
380
381
  const DEFAULT_INDEX_BY = "id";
381
382
  export const INDEX_CONTEXT_SHAPE = {
382
383
  __context: {
@@ -551,33 +552,76 @@ let DocumentIndex = (() => {
551
552
  this._prefetch.accumulator = new Prefetch();
552
553
  }
553
554
  }
554
- /** Head entry safe to borsh-serialize into ResultIndexedValue.entries over the
555
- * document RPC. Under the native block store, this._log.log.get can return a
556
- * hollow entry whose payload bytes were never materialized on the JS side;
557
- * serializing it throws and aborts the whole RPC response. The complete entry is
558
- * in the block store keyed by hash, so recover it there. No-op for pure JS. */
559
- async getSerializableHead(hash) {
560
- const head = await this._log.log.get(hash);
561
- if (!head?.hash)
562
- return head ?? undefined;
563
- try {
564
- head.getStorageBytes(); // = serialize(head); throws on a hollow native entry
565
- return head;
566
- }
567
- catch {
568
- try {
569
- return await Entry.fromMultihash(this._log.log.blocks, head.hash);
555
+ /**
556
+ * Resolve documents without bypassing the resolver/program caches. Cache and
557
+ * identity-index hits are collected first; only the remaining heads cross the
558
+ * log read boundary, and those reads stay aligned in one batch.
559
+ */
560
+ async resolveDocumentsWithBatchedHeads(values) {
561
+ const resolved = new Array(values.length);
562
+ const heads = new Array(values.length);
563
+ const unresolvedPositions = [];
564
+ for (let i = 0; i < values.length; i++) {
565
+ const value = values[i];
566
+ const cached = await this.resolveDocument({
567
+ ...value,
568
+ headEntry: null,
569
+ });
570
+ if (cached) {
571
+ resolved[i] = cached;
570
572
  }
571
- catch {
572
- return head; // block absent (e.g. pruned) — preserve today's behavior
573
+ else {
574
+ unresolvedPositions.push(i);
573
575
  }
574
576
  }
577
+ if (unresolvedPositions.length === 0) {
578
+ return { resolved, heads };
579
+ }
580
+ const unresolvedHeads = await this._log.log.getMany(unresolvedPositions.map((position) => values[position].head));
581
+ for (let i = 0; i < unresolvedPositions.length; i++) {
582
+ const position = unresolvedPositions[i];
583
+ const head = unresolvedHeads[i];
584
+ heads[position] = head;
585
+ resolved[position] = await this.resolveDocument({
586
+ ...values[position],
587
+ headEntry: head ?? null,
588
+ });
589
+ }
590
+ return { resolved, heads };
575
591
  }
576
592
  async wrapPushResults(matches, resolve) {
577
593
  if (!matches.length)
578
594
  return [];
595
+ const headsByMatch = [];
596
+ const resolvedByMatch = [];
597
+ if (!resolve) {
598
+ headsByMatch.push(...(await this._log.log.getMany(matches.map((match) => match.__context.head))));
599
+ }
600
+ else {
601
+ const indexedPositions = [];
602
+ for (let i = 0; i < matches.length; i++) {
603
+ if (!(matches[i] instanceof this.documentType)) {
604
+ indexedPositions.push(i);
605
+ }
606
+ }
607
+ const indexedBatch = indexedPositions.length
608
+ ? await this.resolveDocumentsWithBatchedHeads(indexedPositions.map((position) => {
609
+ const indexed = matches[position];
610
+ return {
611
+ indexed,
612
+ head: indexed.__context.head,
613
+ };
614
+ }))
615
+ : { resolved: [], heads: [] };
616
+ for (let i = 0; i < indexedPositions.length; i++) {
617
+ const position = indexedPositions[i];
618
+ resolvedByMatch[position] = indexedBatch.resolved[i];
619
+ headsByMatch[position] = indexedBatch.heads[i];
620
+ }
621
+ }
579
622
  const results = [];
580
- for (const match of matches) {
623
+ for (let i = 0; i < matches.length; i++) {
624
+ const match = matches[i];
581
625
  if (resolve) {
582
626
  if (match instanceof this.documentType) {
583
627
  const doc = match;
@@ -592,10 +636,7 @@ let DocumentIndex = (() => {
592
636
  continue;
593
637
  }
594
638
  const indexed = match;
595
- const resolved = await this.resolveDocument({
596
- indexed,
597
- head: indexed.__context.head,
598
- });
639
+ const resolved = resolvedByMatch[i];
599
640
  if (resolved) {
600
641
  results.push(new types.ResultValue({
601
642
  context: indexed.__context,
@@ -605,7 +646,7 @@ let DocumentIndex = (() => {
605
646
  }));
606
647
  continue;
607
648
  }
608
- const head = await this.getSerializableHead(indexed.__context.head);
649
+ const head = headsByMatch[i];
609
650
  results.push(new types.ResultIndexedValue({
610
651
  context: indexed.__context,
611
652
  source: serialize(indexed),
@@ -615,7 +656,7 @@ let DocumentIndex = (() => {
615
656
  }
616
657
  else {
617
658
  const indexed = match;
618
- const head = await this.getSerializableHead(indexed.__context.head);
659
+ const head = headsByMatch[i];
619
660
  results.push(new types.ResultIndexedValue({
620
661
  context: indexed.__context,
621
662
  source: serialize(indexed),
@@ -631,14 +672,21 @@ let DocumentIndex = (() => {
631
672
  return [];
632
673
  }
633
674
  const drained = queueEntries.splice(0);
675
+ const resolvedBatch = resolve
676
+ ? await this.resolveDocumentsWithBatchedHeads(drained.map((entry) => ({
677
+ indexed: entry.value,
678
+ head: entry.value.__context.head,
679
+ })))
680
+ : undefined;
681
+ const heads = resolvedBatch
682
+ ? resolvedBatch.heads
683
+ : await this._log.log.getMany(drained.map((entry) => entry.value.__context.head));
634
684
  const results = [];
635
- for (const entry of drained) {
685
+ for (let i = 0; i < drained.length; i++) {
686
+ const entry = drained[i];
636
687
  const indexedUnwrapped = Object.assign(Object.create(this.indexedType.prototype), entry.value);
637
688
  if (resolve) {
638
- const value = await this.resolveDocument({
639
- indexed: entry.value,
640
- head: entry.value.__context.head,
641
- });
689
+ const value = resolvedBatch.resolved[i];
642
690
  if (value) {
643
691
  results.push(new types.ResultValue({
644
692
  context: entry.value.__context,
@@ -648,7 +696,7 @@ let DocumentIndex = (() => {
648
696
  }));
649
697
  continue;
650
698
  }
651
- const head = await this.getSerializableHead(entry.value.__context.head);
699
+ const head = heads[i];
652
700
  results.push(new types.ResultIndexedValue({
653
701
  context: entry.value.__context,
654
702
  source: serialize(indexedUnwrapped),
@@ -657,7 +705,7 @@ let DocumentIndex = (() => {
657
705
  }));
658
706
  }
659
707
  else {
660
- const head = await this.getSerializableHead(entry.value.__context.head);
708
+ const head = heads[i];
661
709
  results.push(new types.ResultIndexedValue({
662
710
  context: entry.value.__context,
663
711
  source: serialize(indexedUnwrapped),
@@ -697,6 +745,7 @@ let DocumentIndex = (() => {
697
745
  let pendingAdded = added;
698
746
  do {
699
747
  const batches = [];
748
+ let claimedIds = [];
700
749
  const queued = await this.drainQueuedResults(queue.queue, resolveFlag);
701
750
  if (queued.length) {
702
751
  batches.push(...queued);
@@ -710,6 +759,13 @@ let DocumentIndex = (() => {
710
759
  const wrapped = await this.wrapPushResults(matches, resolveFlag);
711
760
  if (wrapped.length) {
712
761
  batches.push(...wrapped);
762
+ claimedIds = wrapped
763
+ .map((result) => result instanceof types.ResultValue && result.indexed
764
+ ? indexerTypes.toId(this.indexByResolver(result.indexed))
765
+ : result instanceof types.ResultIndexedValue
766
+ ? indexerTypes.toId(this.indexByResolver(result.value))
767
+ : undefined)
768
+ .filter((id) => id !== undefined);
713
769
  }
714
770
  }
715
771
  if (batches.length) {
@@ -727,6 +783,9 @@ let DocumentIndex = (() => {
727
783
  redundancy: 1,
728
784
  }),
729
785
  });
786
+ if (claimedIds.length) {
787
+ await this._resumableIterators.markYielded(_iteratorId, claimedIds);
788
+ }
730
789
  }
731
790
  pendingAdded = queue.pendingAdded ?? [];
732
791
  queue.pendingAdded = undefined;
@@ -2235,7 +2294,9 @@ let DocumentIndex = (() => {
2235
2294
  delete obj.__context;
2236
2295
  return { value: obj };
2237
2296
  }
2238
- const head = await this._log.log.get(value.head);
2297
+ const head = value.headEntry === undefined
2298
+ ? await this._log.log.get(value.head)
2299
+ : (value.headEntry ?? undefined);
2239
2300
  if (!head) {
2240
2301
  return undefined; // we could end up here if we recently pruned the document and other peers never persisted the entry
2241
2302
  // TODO update changes in index before removing entries from log entry storage
@@ -2326,7 +2387,7 @@ let DocumentIndex = (() => {
2326
2387
  }
2327
2388
  this._resultQueue.set(query.idString, prevQueued);
2328
2389
  }
2329
- const filteredResults = [];
2390
+ const candidates = [];
2330
2391
  const resolveDocumentsFlag = resolvesDocuments(fromQuery);
2331
2392
  const replicateIndexFlag = replicatesIndex(fromQuery);
2332
2393
  for (const result of toIterate) {
@@ -2342,11 +2403,24 @@ let DocumentIndex = (() => {
2342
2403
  !(await options.canRead(indexedUnwrapped, from))) {
2343
2404
  continue;
2344
2405
  }
2406
+ candidates.push({ result, indexed: indexedUnwrapped });
2407
+ }
2408
+ const resolvedBatch = resolveDocumentsFlag
2409
+ ? await this.resolveDocumentsWithBatchedHeads(candidates.map(({ result }) => ({
2410
+ indexed: result.value,
2411
+ head: result.value.__context.head,
2412
+ })))
2413
+ : undefined;
2414
+ const heads = resolvedBatch
2415
+ ? resolvedBatch.heads
2416
+ : candidates.length > 0
2417
+ ? await this._log.log.getMany(candidates.map(({ result }) => result.value.__context.head))
2418
+ : [];
2419
+ const filteredResults = [];
2420
+ for (let i = 0; i < candidates.length; i++) {
2421
+ const { result, indexed: indexedUnwrapped } = candidates[i];
2345
2422
  if (resolveDocumentsFlag) {
2346
- const value = await this.resolveDocument({
2347
- indexed: result.value,
2348
- head: result.value.__context.head,
2349
- });
2423
+ const value = resolvedBatch.resolved[i];
2350
2424
  if (!value) {
2351
2425
  continue;
2352
2426
  }
@@ -2359,7 +2433,7 @@ let DocumentIndex = (() => {
2359
2433
  }
2360
2434
  else {
2361
2435
  const context = result.value.__context;
2362
- const head = await this.getSerializableHead(context.head);
2436
+ const head = heads[i];
2363
2437
  if (replicateIndexFlag) {
2364
2438
  if (!head) {
2365
2439
  continue;
@@ -2721,6 +2795,7 @@ let DocumentIndex = (() => {
2721
2795
  };
2722
2796
  let extraPromises = undefined;
2723
2797
  const seenRemoteHashes = new Set();
2798
+ const selectedRemoteHashes = [];
2724
2799
  const groupHashes = replicatorGroups
2725
2800
  .filter((hash) => {
2726
2801
  if (hash === this.node.identity.publicKey.hashcode()) {
@@ -2735,6 +2810,7 @@ let DocumentIndex = (() => {
2735
2810
  return false;
2736
2811
  }
2737
2812
  fetchFirstForRemote?.add(hash);
2813
+ selectedRemoteHashes.push(hash);
2738
2814
  const resultAlready = this._prefetch?.accumulator.consume(queryRequest, hash);
2739
2815
  if (resultAlready) {
2740
2816
  (extraPromises || (extraPromises = [])).push((async () => {
@@ -2753,6 +2829,7 @@ let DocumentIndex = (() => {
2753
2829
  return true;
2754
2830
  })
2755
2831
  .map((x) => [x]);
2832
+ options?.onRemoteTargets?.(selectedRemoteHashes);
2756
2833
  extraPromises && (await Promise.all(extraPromises));
2757
2834
  let tearDown = undefined;
2758
2835
  const search = this;
@@ -2857,16 +2934,20 @@ let DocumentIndex = (() => {
2857
2934
  // Use an iterator so large results respect message size limits.
2858
2935
  const iterator = this.iterate(coercedRequest, searchOptions);
2859
2936
  const allResults = [];
2860
- while (iterator.done() !== true &&
2861
- coercedRequest.fetch > allResults.length) {
2862
- // We might need to pull .next multiple time due to data message size limitations
2863
- for (const result of await iterator.next(coercedRequest.fetch - allResults.length)) {
2864
- allResults.push(result);
2937
+ try {
2938
+ while (iterator.done() !== true &&
2939
+ coercedRequest.fetch > allResults.length) {
2940
+ // We might need to pull .next multiple time due to data message size limitations
2941
+ for (const result of await iterator.next(coercedRequest.fetch - allResults.length)) {
2942
+ allResults.push(result);
2943
+ }
2865
2944
  }
2945
+ // Deduplicate and return values directly
2946
+ return dedup(allResults, this.indexByResolver);
2947
+ }
2948
+ finally {
2949
+ await iterator.close();
2866
2950
  }
2867
- await iterator.close();
2868
- // Deduplicate and return values directly
2869
- return dedup(allResults, this.indexByResolver);
2870
2951
  }
2871
2952
  resolveIndexed(result, results) {
2872
2953
  if (isResultIndexedValue(result)) {
@@ -3062,6 +3143,48 @@ let DocumentIndex = (() => {
3062
3143
  return tracked;
3063
3144
  };
3064
3145
  const peerBufferMap = new Map();
3146
+ const remoteIteratorPeersToClose = new Set();
3147
+ const staleRemoteIteratorIdsToClose = new Map();
3148
+ const retiredRemoteIteratorPeers = new Set();
3149
+ const pendingMissingResponseRetryPeers = new Set();
3150
+ const missingResponseRetryAttempts = new Map();
3151
+ const maxMissingResponseRetryAttempts = 2;
3152
+ const retireRemoteIteratorPeer = (peer) => {
3153
+ remoteIteratorPeersToClose.add(peer);
3154
+ retiredRemoteIteratorPeers.add(peer);
3155
+ const peerBuffer = peerBufferMap.get(peer);
3156
+ if (!peerBuffer || peerBuffer.buffer.length === 0) {
3157
+ peerBufferMap.delete(peer);
3158
+ }
3159
+ else {
3160
+ // Keep already-received values available to the caller, but do not
3161
+ // issue another CollectNextRequest until a fresh iteration succeeds.
3162
+ peerBuffer.kept = 0;
3163
+ }
3164
+ };
3165
+ const recordMissingResponseGroups = (missingGroups) => {
3166
+ const selfHash = this.node.identity.publicKey.hashcode();
3167
+ for (const group of missingGroups) {
3168
+ for (const hash of group) {
3169
+ if (hash && hash !== selfHash) {
3170
+ retireRemoteIteratorPeer(hash);
3171
+ }
3172
+ }
3173
+ if (!retryMissingResponseGroups) {
3174
+ continue;
3175
+ }
3176
+ const target = group.find((hash) => {
3177
+ if (!hash || hash === selfHash)
3178
+ return false;
3179
+ const attempts = missingResponseRetryAttempts.get(hash) ?? 0;
3180
+ return attempts < maxMissingResponseRetryAttempts;
3181
+ });
3182
+ if (!target)
3183
+ continue;
3184
+ pendingMissingResponseRetryPeers.add(target);
3185
+ missingResponseRetryAttempts.set(target, (missingResponseRetryAttempts.get(target) ?? 0) + 1);
3186
+ }
3187
+ };
3065
3188
  const visited = new Set();
3066
3189
  let indexedPlaceholders;
3067
3190
  const ensureIndexedPlaceholders = () => {
@@ -3241,6 +3364,15 @@ let DocumentIndex = (() => {
3241
3364
  }
3242
3365
  }
3243
3366
  const fetchFirst = async (n, fetchOptions) => {
3367
+ const remoteRequestOptions = typeof options?.remote === "object" ? options.remote : undefined;
3368
+ const fetchSignals = [
3369
+ options?.signal,
3370
+ remoteRequestOptions?.signal,
3371
+ ensureController().signal,
3372
+ ].filter((signal) => signal != null);
3373
+ const fetchSignal = fetchSignals.length === 1
3374
+ ? fetchSignals[0]
3375
+ : AbortSignal.any(fetchSignals);
3244
3376
  await warmupPromise;
3245
3377
  let hasMore = false;
3246
3378
  let missingResponses = false;
@@ -3253,43 +3385,56 @@ let DocumentIndex = (() => {
3253
3385
  const skipRemoteDueToDiscovery = typeof options?.remote === "object" &&
3254
3386
  options.remote.reach?.discover &&
3255
3387
  discoveredTargetHashes?.length === 0;
3388
+ const queryRemote = options?.remote !== false && !skipRemoteDueToDiscovery;
3389
+ const remoteFrom = fetchOptions?.from ??
3390
+ initialRemoteTargets ??
3391
+ remoteRequestOptions?.from;
3392
+ if (queryRemote) {
3393
+ const selfHash = this.node.identity.publicKey.hashcode();
3394
+ for (const peer of remoteFrom ?? []) {
3395
+ if (peer !== selfHash) {
3396
+ remoteIteratorPeersToClose.add(peer);
3397
+ }
3398
+ }
3399
+ }
3256
3400
  queryRequestCoerced.fetch = n;
3257
3401
  await this.queryCommence(queryRequestCoerced, {
3258
3402
  local: fetchOptions?.from != null ? false : options?.local,
3259
- remote: options?.remote !== false && !skipRemoteDueToDiscovery
3403
+ remote: queryRemote
3260
3404
  ? {
3261
- ...(typeof options?.remote === "object"
3262
- ? options.remote
3263
- : {}),
3264
- from: fetchOptions?.from ??
3265
- initialRemoteTargets ??
3266
- (typeof options?.remote === "object"
3267
- ? options.remote.from
3268
- : undefined),
3405
+ ...remoteRequestOptions,
3406
+ from: remoteFrom,
3407
+ signal: fetchSignal,
3269
3408
  }
3270
3409
  : false,
3271
3410
  resolve,
3272
- signal: options?.signal,
3411
+ signal: fetchSignal,
3273
3412
  onResponse: async (response, from) => {
3274
3413
  if (!from) {
3275
3414
  logger.error("Missing response from");
3276
3415
  return;
3277
3416
  }
3417
+ const fromHash = from.hashcode();
3418
+ remoteIteratorPeersToClose.add(fromHash);
3419
+ retiredRemoteIteratorPeers.delete(fromHash);
3278
3420
  if (response instanceof types.NoAccess) {
3279
3421
  logger.error("Dont have access");
3280
3422
  return;
3281
3423
  }
3282
3424
  else if (isResults(response)) {
3283
3425
  const results = response;
3284
- const existingBuffer = peerBufferMap.get(from.hashcode());
3426
+ const existingBuffer = peerBufferMap.get(fromHash);
3285
3427
  const buffer = existingBuffer?.buffer || [];
3286
3428
  if (results.kept === 0n && results.results.length === 0) {
3287
3429
  if (keepRemoteAlive) {
3288
- peerBufferMap.set(from.hashcode(), {
3430
+ peerBufferMap.set(fromHash, {
3289
3431
  buffer,
3290
3432
  kept: 0,
3291
3433
  });
3292
3434
  }
3435
+ else {
3436
+ remoteIteratorPeersToClose.delete(fromHash);
3437
+ }
3293
3438
  return;
3294
3439
  }
3295
3440
  const reqFetch = queryRequestCoerced.fetch ?? 0;
@@ -3298,6 +3443,9 @@ let DocumentIndex = (() => {
3298
3443
  if (effectiveKept > 0) {
3299
3444
  hasMore = true;
3300
3445
  }
3446
+ else if (!keepRemoteAlive) {
3447
+ remoteIteratorPeersToClose.delete(fromHash);
3448
+ }
3301
3449
  for (const result of results.results) {
3302
3450
  const indexKey = indexerTypes.toId(this.indexByResolver(result.value)).primitive;
3303
3451
  if (isResultValue(result)) {
@@ -3342,7 +3490,7 @@ let DocumentIndex = (() => {
3342
3490
  ensureIndexedPlaceholders().set(indexKey, placeholder);
3343
3491
  }
3344
3492
  }
3345
- peerBufferMap.set(from.hashcode(), {
3493
+ peerBufferMap.set(fromHash, {
3346
3494
  buffer,
3347
3495
  kept: effectiveKept,
3348
3496
  });
@@ -3353,29 +3501,21 @@ let DocumentIndex = (() => {
3353
3501
  },
3354
3502
  onMissingResponses: (error) => {
3355
3503
  missingResponses = true;
3356
- if (!retryMissingResponseGroups) {
3357
- return;
3358
- }
3359
3504
  const missingGroups = error.missingGroups;
3360
3505
  if (!missingGroups?.length) {
3361
3506
  return;
3362
3507
  }
3363
- const selfHash = this.node.identity.publicKey.hashcode();
3364
- for (const group of missingGroups) {
3365
- const target = group.find((hash) => {
3366
- if (!hash || hash === selfHash)
3367
- return false;
3368
- const attempts = missingResponseRetryAttempts.get(hash) ?? 0;
3369
- return attempts < maxMissingResponseRetryAttempts;
3370
- });
3371
- if (!target)
3372
- continue;
3373
- pendingMissingResponseRetryPeers.add(target);
3374
- missingResponseRetryAttempts.set(target, (missingResponseRetryAttempts.get(target) ?? 0) + 1);
3508
+ recordMissingResponseGroups(missingGroups);
3509
+ },
3510
+ onRemoteTargets: (targets) => {
3511
+ for (const peer of targets) {
3512
+ remoteIteratorPeersToClose.add(peer);
3375
3513
  }
3376
3514
  },
3377
3515
  }, fetchOptions?.fetchedFirstForRemote);
3378
- if (missingResponses && retryMissingResponseGroups) {
3516
+ if (missingResponses &&
3517
+ retryMissingResponseGroups &&
3518
+ pendingMissingResponseRetryPeers.size > 0) {
3379
3519
  hasMore = true;
3380
3520
  unsetDone();
3381
3521
  }
@@ -3400,6 +3540,18 @@ let DocumentIndex = (() => {
3400
3540
  if (pendingMissingResponseRetryPeers.size > 0) {
3401
3541
  const retryTargets = [...pendingMissingResponseRetryPeers];
3402
3542
  pendingMissingResponseRetryPeers.clear();
3543
+ const idTranslation = this._prefetch?.accumulator.getTranslationMap(queryRequestCoerced);
3544
+ for (const peer of retryTargets) {
3545
+ const staleRemoteIteratorId = idTranslation?.get(peer);
3546
+ if (staleRemoteIteratorId) {
3547
+ const staleIds = staleRemoteIteratorIdsToClose.get(peer) ?? [];
3548
+ if (!staleIds.some((id) => equals(id, staleRemoteIteratorId))) {
3549
+ staleIds.push(staleRemoteIteratorId);
3550
+ staleRemoteIteratorIdsToClose.set(peer, staleIds);
3551
+ }
3552
+ idTranslation.delete(peer);
3553
+ }
3554
+ }
3403
3555
  return setFetchPromise(fetchFirst(n, {
3404
3556
  from: retryTargets,
3405
3557
  // retries for missing groups should not be suppressed by first-fetch dedupe
@@ -3409,6 +3561,12 @@ let DocumentIndex = (() => {
3409
3561
  const promises = [];
3410
3562
  let resultsLeft = 0;
3411
3563
  for (const [peer, buffer] of peerBufferMap) {
3564
+ if (retiredRemoteIteratorPeers.has(peer)) {
3565
+ if (buffer.buffer.length === 0) {
3566
+ peerBufferMap.delete(peer);
3567
+ }
3568
+ continue;
3569
+ }
3412
3570
  if (buffer.buffer.length < n) {
3413
3571
  const hasExistingRemoteResults = buffer.kept > 0;
3414
3572
  if (!hasExistingRemoteResults && !keepRemoteAlive) {
@@ -3503,6 +3661,7 @@ let DocumentIndex = (() => {
3503
3661
  }
3504
3662
  else {
3505
3663
  // Fetch remotely
3664
+ remoteIteratorPeersToClose.add(peer);
3506
3665
  const idTranslation = this._prefetch?.accumulator.getTranslationMap(queryRequestCoerced);
3507
3666
  let remoteCollectRequest = collectRequest;
3508
3667
  if (idTranslation) {
@@ -3511,97 +3670,118 @@ let DocumentIndex = (() => {
3511
3670
  amount: collectRequest.amount,
3512
3671
  });
3513
3672
  }
3673
+ const remoteRequestOptions = typeof options?.remote === "object" ? options.remote : undefined;
3674
+ const collectSignals = [
3675
+ options?.signal,
3676
+ remoteRequestOptions?.signal,
3677
+ ensureController().signal,
3678
+ ].filter((signal) => signal != null);
3679
+ const collectSignal = collectSignals.length === 1
3680
+ ? collectSignals[0]
3681
+ : AbortSignal.any(collectSignals);
3514
3682
  promises.push(this._query
3515
3683
  .request(remoteCollectRequest, {
3516
3684
  ...options,
3517
- signal: options?.signal
3518
- ? AbortSignal.any([
3519
- options.signal,
3520
- ensureController().signal,
3521
- ])
3522
- : ensureController().signal,
3685
+ ...remoteRequestOptions,
3686
+ signal: collectSignal,
3523
3687
  priority: getRemoteQueryPriority(options?.remote),
3524
3688
  mode: new SilentDelivery({ to: [peer], redundancy: 1 }),
3525
3689
  })
3526
- .then((response) => introduceEntries(queryRequestCoerced, response, this.documentType, this.indexedType, this._sync, options)
3527
- .then(async (responses) => {
3528
- return Promise.all(responses.map(async (response, i) => {
3529
- resultsLeft += Number(response.response.kept);
3530
- const from = responses[i].from;
3531
- if (!from) {
3532
- logger.error("Missing from for sorted query");
3533
- return;
3690
+ .then((response) => {
3691
+ if (!response.some((result) => result.from?.hashcode() === peer)) {
3692
+ const missingGroups = [[peer]];
3693
+ if (remoteRequestOptions?.throwOnMissing) {
3694
+ retireRemoteIteratorPeer(peer);
3695
+ throw new MissingResponsesError("Did not receive responses from all shards: " +
3696
+ JSON.stringify(missingGroups), missingGroups);
3534
3697
  }
3535
- if (response.response.results.length === 0) {
3698
+ recordMissingResponseGroups(missingGroups);
3699
+ return;
3700
+ }
3701
+ return introduceEntries(queryRequestCoerced, response, this.documentType, this.indexedType, this._sync, options)
3702
+ .then(async (responses) => {
3703
+ return Promise.all(responses.map(async (response, i) => {
3704
+ resultsLeft += Number(response.response.kept);
3705
+ const from = responses[i].from;
3706
+ if (!from) {
3707
+ logger.error("Missing from for sorted query");
3708
+ return;
3709
+ }
3536
3710
  if (!keepRemoteAlive &&
3537
- peerBufferMap.get(peer)?.buffer.length === 0) {
3538
- peerBufferMap.delete(peer); // No more results
3711
+ response.response.kept === 0n) {
3712
+ remoteIteratorPeersToClose.delete(peer);
3539
3713
  }
3540
- }
3541
- else {
3542
- const peerBuffer = peerBufferMap.get(peer);
3543
- if (!peerBuffer) {
3544
- return;
3714
+ if (response.response.results.length === 0) {
3715
+ if (!keepRemoteAlive &&
3716
+ peerBufferMap.get(peer)?.buffer.length === 0) {
3717
+ peerBufferMap.delete(peer); // No more results
3718
+ }
3545
3719
  }
3546
- peerBuffer.kept = Number(response.response.kept);
3547
- for (const result of response.response.results) {
3548
- const indexKey = indexerTypes.toId(this.indexByResolver(result.value)).primitive;
3549
- if (isResultValue(result)) {
3550
- const existingIndexed = indexedPlaceholders?.get(indexKey);
3551
- if (existingIndexed) {
3552
- existingIndexed.value =
3553
- result.value;
3554
- existingIndexed.context = result.context;
3555
- existingIndexed.from = from;
3556
- existingIndexed.indexed =
3557
- await this.resolveIndexed(result, response.response
3558
- .results);
3559
- indexedPlaceholders?.delete(indexKey);
3560
- continue;
3561
- }
3562
- if (visited.has(indexKey) &&
3563
- !evictStaleBuffered(indexKey, result.context)) {
3564
- continue;
3565
- }
3566
- visited.add(indexKey);
3567
- const indexed = await this.resolveIndexed(result, response.response
3568
- .results);
3569
- peerBuffer.buffer.push({
3570
- value: result.value,
3571
- context: result.context,
3572
- from: from,
3573
- indexed,
3574
- });
3720
+ else {
3721
+ const peerBuffer = peerBufferMap.get(peer);
3722
+ if (!peerBuffer) {
3723
+ return;
3575
3724
  }
3576
- else {
3577
- const indexedResult = result;
3578
- if (visited.has(indexKey) &&
3579
- !indexedPlaceholders?.has(indexKey) &&
3580
- !evictStaleBuffered(indexKey, indexedResult.context)) {
3581
- continue;
3725
+ peerBuffer.kept = Number(response.response.kept);
3726
+ for (const result of response.response.results) {
3727
+ const indexKey = indexerTypes.toId(this.indexByResolver(result.value)).primitive;
3728
+ if (isResultValue(result)) {
3729
+ const existingIndexed = indexedPlaceholders?.get(indexKey);
3730
+ if (existingIndexed) {
3731
+ existingIndexed.value =
3732
+ result.value;
3733
+ existingIndexed.context = result.context;
3734
+ existingIndexed.from = from;
3735
+ existingIndexed.indexed =
3736
+ await this.resolveIndexed(result, response.response
3737
+ .results);
3738
+ indexedPlaceholders?.delete(indexKey);
3739
+ continue;
3740
+ }
3741
+ if (visited.has(indexKey) &&
3742
+ !evictStaleBuffered(indexKey, result.context)) {
3743
+ continue;
3744
+ }
3745
+ visited.add(indexKey);
3746
+ const indexed = await this.resolveIndexed(result, response.response
3747
+ .results);
3748
+ peerBuffer.buffer.push({
3749
+ value: result.value,
3750
+ context: result.context,
3751
+ from: from,
3752
+ indexed,
3753
+ });
3754
+ }
3755
+ else {
3756
+ const indexedResult = result;
3757
+ if (visited.has(indexKey) &&
3758
+ !indexedPlaceholders?.has(indexKey) &&
3759
+ !evictStaleBuffered(indexKey, indexedResult.context)) {
3760
+ continue;
3761
+ }
3762
+ visited.add(indexKey);
3763
+ const indexed = coerceWithContext(indexedResult.value, indexedResult.context);
3764
+ const placeholder = {
3765
+ value: indexedResult.value,
3766
+ context: indexedResult.context,
3767
+ from: from,
3768
+ indexed,
3769
+ };
3770
+ peerBuffer.buffer.push(placeholder);
3771
+ ensureIndexedPlaceholders().set(indexKey, placeholder);
3582
3772
  }
3583
- visited.add(indexKey);
3584
- const indexed = coerceWithContext(indexedResult.value, indexedResult.context);
3585
- const placeholder = {
3586
- value: indexedResult.value,
3587
- context: indexedResult.context,
3588
- from: from,
3589
- indexed,
3590
- };
3591
- peerBuffer.buffer.push(placeholder);
3592
- ensureIndexedPlaceholders().set(indexKey, placeholder);
3593
3773
  }
3594
3774
  }
3595
- }
3596
- }));
3597
- })
3598
- .catch((e) => {
3599
- logger.error("Failed to collect sorted results from: " +
3600
- peer +
3601
- ". " +
3602
- e?.message);
3603
- peerBufferMap.delete(peer);
3604
- })));
3775
+ }));
3776
+ })
3777
+ .catch((e) => {
3778
+ logger.error("Failed to collect sorted results from: " +
3779
+ peer +
3780
+ ". " +
3781
+ e?.message);
3782
+ peerBufferMap.delete(peer);
3783
+ });
3784
+ }));
3605
3785
  }
3606
3786
  }
3607
3787
  else {
@@ -3624,7 +3804,7 @@ let DocumentIndex = (() => {
3624
3804
  }
3625
3805
  }
3626
3806
  }
3627
- return resultsLeft === 0; // 0 results left to fetch and 0 pending results
3807
+ return (resultsLeft === 0 && pendingMissingResponseRetryPeers.size === 0); // 0 results left to fetch and 0 pending results
3628
3808
  }));
3629
3809
  };
3630
3810
  const next = async (n) => {
@@ -3706,35 +3886,83 @@ let DocumentIndex = (() => {
3706
3886
  this.processCloseIteratorRequest(queryRequestCoerced, this.node.identity.publicKey);
3707
3887
  done = true;
3708
3888
  };
3709
- let close = async () => {
3889
+ const outerSignal = options?.signal;
3890
+ let outerAbortListener;
3891
+ let closeStarted = false;
3892
+ let closePromise;
3893
+ const performClose = async () => {
3894
+ const idTranslation = this._prefetch?.accumulator.getTranslationMap(queryRequestCoerced);
3895
+ const remoteIteratorIds = idTranslation
3896
+ ? new Map(idTranslation)
3897
+ : undefined;
3710
3898
  cleanupAndDone();
3711
3899
  // Keep-open iterators can still have active remote state even when
3712
3900
  // their pending count has already drained to zero.
3713
- const closeRequest = new types.CloseIteratorRequest({
3714
- id: queryRequestCoerced.id,
3715
- });
3716
3901
  const selfHash = this.node.identity.publicKey.hashcode();
3717
- const remotePeers = keepRemoteAlive
3902
+ const activeRemotePeers = new Set(keepRemoteAlive
3718
3903
  ? [...peerBufferMap.keys()].filter((peer) => peer !== selfHash)
3719
3904
  : [...peerBufferMap.entries()]
3720
3905
  .filter(([peer, buffer]) => peer !== selfHash && buffer.kept > 0)
3721
- .map(([peer]) => peer);
3906
+ .map(([peer]) => peer));
3907
+ for (const peer of remoteIteratorPeersToClose) {
3908
+ if (peer !== selfHash) {
3909
+ activeRemotePeers.add(peer);
3910
+ }
3911
+ }
3912
+ const staleRemoteIteratorIds = new Map([...staleRemoteIteratorIdsToClose].map(([peer, ids]) => [
3913
+ peer,
3914
+ [...ids],
3915
+ ]));
3916
+ const remotePeers = new Set([
3917
+ ...activeRemotePeers,
3918
+ ...staleRemoteIteratorIds.keys(),
3919
+ ]);
3722
3920
  peerBufferMap.clear();
3723
- await Promise.allSettled(remotePeers.map((peer) => this._query.send(closeRequest, {
3724
- ...options,
3725
- priority: getRemoteQueryPriority(options?.remote),
3726
- mode: new SilentDelivery({ to: [peer], redundancy: 1 }),
3727
- })));
3921
+ retiredRemoteIteratorPeers.clear();
3922
+ remoteIteratorPeersToClose.clear();
3923
+ staleRemoteIteratorIdsToClose.clear();
3924
+ if (remotePeers.size === 0) {
3925
+ return;
3926
+ }
3927
+ const remoteCloseOptions = typeof options?.remote === "object" ? options.remote : undefined;
3928
+ const closeSignal = AbortSignal.timeout(CLOSE_ITERATOR_REQUEST_TIMEOUT);
3929
+ await Promise.allSettled([...remotePeers].flatMap((peer) => {
3930
+ const ids = [];
3931
+ if (activeRemotePeers.has(peer)) {
3932
+ ids.push(remoteIteratorIds?.get(peer) ?? queryRequestCoerced.id);
3933
+ }
3934
+ for (const staleRemoteIteratorId of staleRemoteIteratorIds.get(peer) ?? []) {
3935
+ if (!ids.some((id) => equals(id, staleRemoteIteratorId))) {
3936
+ ids.push(staleRemoteIteratorId);
3937
+ }
3938
+ }
3939
+ return ids.map((id) => {
3940
+ const closeRequest = new types.CloseIteratorRequest({ id });
3941
+ return this._query.send(closeRequest, {
3942
+ ...remoteCloseOptions,
3943
+ signal: closeSignal,
3944
+ priority: getRemoteQueryPriority(options?.remote),
3945
+ mode: new SilentDelivery({ to: [peer], redundancy: 1 }),
3946
+ });
3947
+ });
3948
+ }));
3949
+ };
3950
+ const close = () => {
3951
+ if (closeStarted)
3952
+ return closePromise;
3953
+ closeStarted = true;
3954
+ if (outerAbortListener) {
3955
+ outerSignal?.removeEventListener("abort", outerAbortListener);
3956
+ outerAbortListener = undefined;
3957
+ }
3958
+ closePromise = performClose();
3959
+ return closePromise;
3728
3960
  };
3729
- options?.signal && options.signal.addEventListener("abort", close);
3730
3961
  let doneFn = () => {
3731
3962
  return done;
3732
3963
  };
3733
3964
  let joinListener;
3734
3965
  let fetchedFirstForRemote = undefined;
3735
- const pendingMissingResponseRetryPeers = new Set();
3736
- const missingResponseRetryAttempts = new Map();
3737
- const maxMissingResponseRetryAttempts = 2;
3738
3966
  let joinFetchesInFlight = 0;
3739
3967
  let updateDeferred;
3740
3968
  const updateWaiters = new Set();
@@ -4107,7 +4335,8 @@ let DocumentIndex = (() => {
4107
4335
  continue;
4108
4336
  }
4109
4337
  }
4110
- const id = indexerTypes.toId(this.indexByResolver(indexedCandidate)).primitive;
4338
+ const indexId = indexerTypes.toId(this.indexByResolver(indexedCandidate));
4339
+ const id = indexId.primitive;
4111
4340
  const existingIndexed = indexedPlaceholders?.get(id);
4112
4341
  if (existingIndexed) {
4113
4342
  if (resolve) {
@@ -4149,6 +4378,7 @@ let DocumentIndex = (() => {
4149
4378
  if (!resolve) {
4150
4379
  ensureIndexedPlaceholders().set(id, placeholder);
4151
4380
  }
4381
+ await this._resumableIterators.markYielded(queryRequestCoerced.idString, [indexId]);
4152
4382
  hasRelevantChange = true;
4153
4383
  }
4154
4384
  }
@@ -4385,6 +4615,15 @@ let DocumentIndex = (() => {
4385
4615
  }
4386
4616
  }
4387
4617
  };
4618
+ if (outerSignal) {
4619
+ outerAbortListener = () => {
4620
+ void close();
4621
+ };
4622
+ outerSignal.addEventListener("abort", outerAbortListener, { once: true });
4623
+ if (outerSignal.aborted) {
4624
+ void close();
4625
+ }
4626
+ }
4388
4627
  return {
4389
4628
  close,
4390
4629
  next,
@@ -4454,49 +4693,61 @@ let DocumentIndex = (() => {
4454
4693
  const drainBatchSize = replicate ? 1000 : 100;
4455
4694
  let result = [];
4456
4695
  let c = 0;
4457
- while (doneFn() !== true) {
4458
- let batch = await next(drainBatchSize);
4459
- c += batch.length;
4460
- if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
4461
- warn("Iterating for more than " +
4462
- WARNING_WHEN_ITERATING_FOR_MORE_THAN +
4463
- " results");
4464
- }
4465
- if (batch.length > 0) {
4466
- result.push(...batch);
4467
- continue;
4696
+ try {
4697
+ while (doneFn() !== true) {
4698
+ let batch = await next(drainBatchSize);
4699
+ c += batch.length;
4700
+ if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
4701
+ warn("Iterating for more than " +
4702
+ WARNING_WHEN_ITERATING_FOR_MORE_THAN +
4703
+ " results");
4704
+ }
4705
+ if (batch.length > 0) {
4706
+ result.push(...batch);
4707
+ continue;
4708
+ }
4709
+ await waitForUpdateAndResetDeferred();
4468
4710
  }
4469
- await waitForUpdateAndResetDeferred();
4711
+ return result;
4712
+ }
4713
+ finally {
4714
+ await close();
4470
4715
  }
4471
- cleanupAndDone();
4472
- return result;
4473
4716
  },
4474
4717
  first: async () => {
4475
- if (doneFn()) {
4476
- return undefined;
4718
+ try {
4719
+ if (doneFn()) {
4720
+ return undefined;
4721
+ }
4722
+ let batch = await next(1);
4723
+ return batch[0];
4724
+ }
4725
+ finally {
4726
+ await close();
4477
4727
  }
4478
- let batch = await next(1);
4479
- cleanupAndDone();
4480
- return batch[0];
4481
4728
  },
4482
4729
  [Symbol.asyncIterator]: async function* () {
4483
4730
  drain = true;
4484
4731
  const drainBatchSize = replicate ? 1000 : 100;
4485
4732
  let c = 0;
4486
- while (doneFn() !== true) {
4487
- const batch = await next(drainBatchSize);
4488
- c += batch.length;
4489
- if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
4490
- warn("Iterating for more than " +
4491
- WARNING_WHEN_ITERATING_FOR_MORE_THAN +
4492
- " results");
4493
- }
4494
- for (const entry of batch) {
4495
- yield entry;
4733
+ try {
4734
+ while (doneFn() !== true) {
4735
+ const batch = await next(drainBatchSize);
4736
+ c += batch.length;
4737
+ if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
4738
+ warn("Iterating for more than " +
4739
+ WARNING_WHEN_ITERATING_FOR_MORE_THAN +
4740
+ " results");
4741
+ }
4742
+ for (const entry of batch) {
4743
+ yield entry;
4744
+ }
4745
+ await waitForUpdateAndResetDeferred();
4496
4746
  }
4497
- await waitForUpdateAndResetDeferred();
4498
4747
  }
4499
- cleanupAndDone();
4748
+ finally {
4749
+ await close();
4750
+ }
4500
4751
  },
4501
4752
  };
4502
4753
  }