@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.
package/src/search.ts CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  import { CachedIndex, type QueryCacheOptions } from "@peerbit/indexer-cache";
25
25
  import * as indexerTypes from "@peerbit/indexer-interface";
26
26
  import { HashmapIndex } from "@peerbit/indexer-simple";
27
- import { BORSH_ENCODING, type Encoding, Entry } from "@peerbit/log";
27
+ import { BORSH_ENCODING, type Encoding, type Entry } from "@peerbit/log";
28
28
  import { logger as loggerFn } from "@peerbit/logger";
29
29
  import { ClosedError, Program } from "@peerbit/program";
30
30
  import {
@@ -47,7 +47,7 @@ import {
47
47
  } from "@peerbit/stream-interface";
48
48
  import { AbortError, TimeoutError, waitFor } from "@peerbit/time";
49
49
  import pDefer, { type DeferredPromise } from "p-defer";
50
- import { concat, fromString } from "uint8arrays";
50
+ import { concat, equals, fromString } from "uint8arrays";
51
51
  import { copySerialization } from "./borsh.js";
52
52
  import { MAX_BATCH_SIZE } from "./constants.js";
53
53
  import type { DocumentEvents, DocumentsChange } from "./events.js";
@@ -511,6 +511,7 @@ type QueryDetailedOptions<
511
511
  from: PublicSignKey,
512
512
  ) => void | Promise<void>;
513
513
  onMissingResponses?: (error: MissingResponsesError) => void | Promise<void>;
514
+ onRemoteTargets?: (targets: string[]) => void;
514
515
  remote?: {
515
516
  from?: string[]; // if specified, only query these peers
516
517
  };
@@ -742,6 +743,7 @@ function isSubclassOf(
742
743
  const DEFAULT_TIMEOUT = 1e4;
743
744
  const DEFAULT_KEEP_REMOTE_ITERATOR_TIMEOUT = 3e5;
744
745
  const DISCOVER_TIMEOUT_FALLBACK = 500;
746
+ const CLOSE_ITERATOR_REQUEST_TIMEOUT = 5e3;
745
747
 
746
748
  const DEFAULT_INDEX_BY = "id";
747
749
 
@@ -1179,26 +1181,56 @@ export class DocumentIndex<
1179
1181
  }
1180
1182
  }
1181
1183
 
1182
- /** Head entry safe to borsh-serialize into ResultIndexedValue.entries over the
1183
- * document RPC. Under the native block store, this._log.log.get can return a
1184
- * hollow entry whose payload bytes were never materialized on the JS side;
1185
- * serializing it throws and aborts the whole RPC response. The complete entry is
1186
- * in the block store keyed by hash, so recover it there. No-op for pure JS. */
1187
- private async getSerializableHead(
1188
- hash: string,
1189
- ): Promise<Entry<any> | undefined> {
1190
- const head = await this._log.log.get(hash);
1191
- if (!head?.hash) return head ?? undefined;
1192
- try {
1193
- head.getStorageBytes(); // = serialize(head); throws on a hollow native entry
1194
- return head;
1195
- } catch {
1196
- try {
1197
- return await Entry.fromMultihash(this._log.log.blocks, head.hash);
1198
- } catch {
1199
- return head; // block absent (e.g. pruned) — preserve today's behavior
1184
+ /**
1185
+ * Resolve documents without bypassing the resolver/program caches. Cache and
1186
+ * identity-index hits are collected first; only the remaining heads cross the
1187
+ * log read boundary, and those reads stay aligned in one batch.
1188
+ */
1189
+ private async resolveDocumentsWithBatchedHeads(
1190
+ values: Array<{
1191
+ id?: indexerTypes.IdPrimitive;
1192
+ indexed: I;
1193
+ head: string;
1194
+ }>,
1195
+ ): Promise<{
1196
+ resolved: Array<{ value: T } | undefined>;
1197
+ heads: Array<Entry<Operation> | undefined>;
1198
+ }> {
1199
+ const resolved: Array<{ value: T } | undefined> = new Array(values.length);
1200
+ const heads: Array<Entry<Operation> | undefined> = new Array(values.length);
1201
+ const unresolvedPositions: number[] = [];
1202
+
1203
+ for (let i = 0; i < values.length; i++) {
1204
+ const value = values[i]!;
1205
+ const cached = await this.resolveDocument({
1206
+ ...value,
1207
+ headEntry: null,
1208
+ });
1209
+ if (cached) {
1210
+ resolved[i] = cached;
1211
+ } else {
1212
+ unresolvedPositions.push(i);
1200
1213
  }
1201
1214
  }
1215
+
1216
+ if (unresolvedPositions.length === 0) {
1217
+ return { resolved, heads };
1218
+ }
1219
+
1220
+ const unresolvedHeads = await this._log.log.getMany(
1221
+ unresolvedPositions.map((position) => values[position]!.head),
1222
+ );
1223
+ for (let i = 0; i < unresolvedPositions.length; i++) {
1224
+ const position = unresolvedPositions[i]!;
1225
+ const head = unresolvedHeads[i];
1226
+ heads[position] = head;
1227
+ resolved[position] = await this.resolveDocument({
1228
+ ...values[position]!,
1229
+ headEntry: head ?? null,
1230
+ });
1231
+ }
1232
+
1233
+ return { resolved, heads };
1202
1234
  }
1203
1235
 
1204
1236
  private async wrapPushResults(
@@ -1206,8 +1238,41 @@ export class DocumentIndex<
1206
1238
  resolve: boolean,
1207
1239
  ): Promise<types.Result[]> {
1208
1240
  if (!matches.length) return [];
1241
+ const headsByMatch: Array<Entry<Operation> | undefined> = [];
1242
+ const resolvedByMatch: Array<{ value: T } | undefined> = [];
1243
+ if (!resolve) {
1244
+ headsByMatch.push(
1245
+ ...(await this._log.log.getMany(
1246
+ matches.map((match) => match.__context.head),
1247
+ )),
1248
+ );
1249
+ } else {
1250
+ const indexedPositions: number[] = [];
1251
+ for (let i = 0; i < matches.length; i++) {
1252
+ if (!(matches[i] instanceof this.documentType)) {
1253
+ indexedPositions.push(i);
1254
+ }
1255
+ }
1256
+ const indexedBatch = indexedPositions.length
1257
+ ? await this.resolveDocumentsWithBatchedHeads(
1258
+ indexedPositions.map((position) => {
1259
+ const indexed = matches[position] as WithContext<I>;
1260
+ return {
1261
+ indexed,
1262
+ head: indexed.__context.head,
1263
+ };
1264
+ }),
1265
+ )
1266
+ : { resolved: [], heads: [] };
1267
+ for (let i = 0; i < indexedPositions.length; i++) {
1268
+ const position = indexedPositions[i]!;
1269
+ resolvedByMatch[position] = indexedBatch.resolved[i];
1270
+ headsByMatch[position] = indexedBatch.heads[i];
1271
+ }
1272
+ }
1209
1273
  const results: types.Result[] = [];
1210
- for (const match of matches) {
1274
+ for (let i = 0; i < matches.length; i++) {
1275
+ const match = matches[i]!;
1211
1276
  if (resolve) {
1212
1277
  if (match instanceof this.documentType) {
1213
1278
  const doc = match as WithContext<T>;
@@ -1225,10 +1290,7 @@ export class DocumentIndex<
1225
1290
  }
1226
1291
 
1227
1292
  const indexed = match as WithContext<I>;
1228
- const resolved = await this.resolveDocument({
1229
- indexed,
1230
- head: indexed.__context.head,
1231
- });
1293
+ const resolved = resolvedByMatch[i];
1232
1294
 
1233
1295
  if (resolved) {
1234
1296
  results.push(
@@ -1242,7 +1304,7 @@ export class DocumentIndex<
1242
1304
  continue;
1243
1305
  }
1244
1306
 
1245
- const head = await this.getSerializableHead(indexed.__context.head);
1307
+ const head = headsByMatch[i];
1246
1308
  results.push(
1247
1309
  new types.ResultIndexedValue({
1248
1310
  context: indexed.__context,
@@ -1253,7 +1315,7 @@ export class DocumentIndex<
1253
1315
  );
1254
1316
  } else {
1255
1317
  const indexed = match as WithContext<I>;
1256
- const head = await this.getSerializableHead(indexed.__context.head);
1318
+ const head = headsByMatch[i];
1257
1319
  results.push(
1258
1320
  new types.ResultIndexedValue({
1259
1321
  context: indexed.__context,
@@ -1275,17 +1337,28 @@ export class DocumentIndex<
1275
1337
  return [];
1276
1338
  }
1277
1339
  const drained = queueEntries.splice(0);
1340
+ const resolvedBatch = resolve
1341
+ ? await this.resolveDocumentsWithBatchedHeads(
1342
+ drained.map((entry) => ({
1343
+ indexed: entry.value,
1344
+ head: entry.value.__context.head,
1345
+ })),
1346
+ )
1347
+ : undefined;
1348
+ const heads = resolvedBatch
1349
+ ? resolvedBatch.heads
1350
+ : await this._log.log.getMany(
1351
+ drained.map((entry) => entry.value.__context.head),
1352
+ );
1278
1353
  const results: types.Result[] = [];
1279
- for (const entry of drained) {
1354
+ for (let i = 0; i < drained.length; i++) {
1355
+ const entry = drained[i]!;
1280
1356
  const indexedUnwrapped = Object.assign(
1281
1357
  Object.create(this.indexedType.prototype),
1282
1358
  entry.value,
1283
1359
  );
1284
1360
  if (resolve) {
1285
- const value = await this.resolveDocument({
1286
- indexed: entry.value,
1287
- head: entry.value.__context.head,
1288
- });
1361
+ const value = resolvedBatch!.resolved[i];
1289
1362
  if (value) {
1290
1363
  results.push(
1291
1364
  new types.ResultValue({
@@ -1298,7 +1371,7 @@ export class DocumentIndex<
1298
1371
  continue;
1299
1372
  }
1300
1373
 
1301
- const head = await this.getSerializableHead(entry.value.__context.head);
1374
+ const head = heads[i];
1302
1375
  results.push(
1303
1376
  new types.ResultIndexedValue({
1304
1377
  context: entry.value.__context,
@@ -1308,7 +1381,7 @@ export class DocumentIndex<
1308
1381
  }),
1309
1382
  );
1310
1383
  } else {
1311
- const head = await this.getSerializableHead(entry.value.__context.head);
1384
+ const head = heads[i];
1312
1385
  results.push(
1313
1386
  new types.ResultIndexedValue({
1314
1387
  context: entry.value.__context,
@@ -1358,6 +1431,7 @@ export class DocumentIndex<
1358
1431
  let pendingAdded = added;
1359
1432
  do {
1360
1433
  const batches: types.Result[] = [];
1434
+ let claimedIds: indexerTypes.IdKey[] = [];
1361
1435
  const queued = await this.drainQueuedResults(
1362
1436
  queue.queue,
1363
1437
  resolveFlag,
@@ -1379,6 +1453,21 @@ export class DocumentIndex<
1379
1453
  const wrapped = await this.wrapPushResults(matches, resolveFlag);
1380
1454
  if (wrapped.length) {
1381
1455
  batches.push(...wrapped);
1456
+ claimedIds = wrapped
1457
+ .map((result) =>
1458
+ result instanceof types.ResultValue && result.indexed
1459
+ ? indexerTypes.toId(
1460
+ this.indexByResolver(result.indexed),
1461
+ )
1462
+ : result instanceof types.ResultIndexedValue
1463
+ ? indexerTypes.toId(
1464
+ this.indexByResolver(result.value),
1465
+ )
1466
+ : undefined,
1467
+ )
1468
+ .filter(
1469
+ (id): id is indexerTypes.IdKey => id !== undefined,
1470
+ );
1382
1471
  }
1383
1472
  }
1384
1473
  if (batches.length) {
@@ -1396,6 +1485,12 @@ export class DocumentIndex<
1396
1485
  redundancy: 1,
1397
1486
  }),
1398
1487
  });
1488
+ if (claimedIds.length) {
1489
+ await this._resumableIterators.markYielded(
1490
+ _iteratorId,
1491
+ claimedIds,
1492
+ );
1493
+ }
1399
1494
  }
1400
1495
  pendingAdded = queue.pendingAdded ?? [];
1401
1496
  queue.pendingAdded = undefined;
@@ -3474,6 +3569,7 @@ export class DocumentIndex<
3474
3569
  id?: indexerTypes.IdPrimitive;
3475
3570
  indexed: I;
3476
3571
  head: string;
3572
+ headEntry?: Entry<Operation> | null;
3477
3573
  }): Promise<{ value: T } | undefined> {
3478
3574
  const id =
3479
3575
  value.id ??
@@ -3495,7 +3591,10 @@ export class DocumentIndex<
3495
3591
  return { value: obj as T };
3496
3592
  }
3497
3593
 
3498
- const head = await this._log.log.get(value.head);
3594
+ const head =
3595
+ value.headEntry === undefined
3596
+ ? await this._log.log.get(value.head)
3597
+ : (value.headEntry ?? undefined);
3499
3598
  if (!head) {
3500
3599
  return undefined; // we could end up here if we recently pruned the document and other peers never persisted the entry
3501
3600
  // TODO update changes in index before removing entries from log entry storage
@@ -3633,7 +3732,10 @@ export class DocumentIndex<
3633
3732
  this._resultQueue.set(query.idString, prevQueued);
3634
3733
  }
3635
3734
 
3636
- const filteredResults: types.Result[] = [];
3735
+ const candidates: Array<{
3736
+ result: indexerTypes.IndexedResult<WithContext<I>>;
3737
+ indexed: I;
3738
+ }> = [];
3637
3739
  const resolveDocumentsFlag = resolvesDocuments(fromQuery);
3638
3740
  const replicateIndexFlag = replicatesIndex(fromQuery);
3639
3741
  for (const result of toIterate) {
@@ -3655,11 +3757,29 @@ export class DocumentIndex<
3655
3757
  ) {
3656
3758
  continue;
3657
3759
  }
3760
+ candidates.push({ result, indexed: indexedUnwrapped });
3761
+ }
3762
+
3763
+ const resolvedBatch = resolveDocumentsFlag
3764
+ ? await this.resolveDocumentsWithBatchedHeads(
3765
+ candidates.map(({ result }) => ({
3766
+ indexed: result.value,
3767
+ head: result.value.__context.head,
3768
+ })),
3769
+ )
3770
+ : undefined;
3771
+ const heads = resolvedBatch
3772
+ ? resolvedBatch.heads
3773
+ : candidates.length > 0
3774
+ ? await this._log.log.getMany(
3775
+ candidates.map(({ result }) => result.value.__context.head),
3776
+ )
3777
+ : [];
3778
+ const filteredResults: types.Result[] = [];
3779
+ for (let i = 0; i < candidates.length; i++) {
3780
+ const { result, indexed: indexedUnwrapped } = candidates[i]!;
3658
3781
  if (resolveDocumentsFlag) {
3659
- const value = await this.resolveDocument({
3660
- indexed: result.value,
3661
- head: result.value.__context.head,
3662
- });
3782
+ const value = resolvedBatch!.resolved[i];
3663
3783
 
3664
3784
  if (!value) {
3665
3785
  continue;
@@ -3675,7 +3795,7 @@ export class DocumentIndex<
3675
3795
  );
3676
3796
  } else {
3677
3797
  const context = result.value.__context;
3678
- const head = await this.getSerializableHead(context.head);
3798
+ const head = heads[i];
3679
3799
  if (replicateIndexFlag) {
3680
3800
  if (!head) {
3681
3801
  continue;
@@ -4165,6 +4285,7 @@ export class DocumentIndex<
4165
4285
  let extraPromises: Promise<void>[] | undefined = undefined;
4166
4286
 
4167
4287
  const seenRemoteHashes = new Set<string>();
4288
+ const selectedRemoteHashes: string[] = [];
4168
4289
  const groupHashes: string[][] = replicatorGroups
4169
4290
  .filter((hash) => {
4170
4291
  if (hash === this.node.identity.publicKey.hashcode()) {
@@ -4181,6 +4302,7 @@ export class DocumentIndex<
4181
4302
  return false;
4182
4303
  }
4183
4304
  fetchFirstForRemote?.add(hash);
4305
+ selectedRemoteHashes.push(hash);
4184
4306
 
4185
4307
  const resultAlready = this._prefetch?.accumulator.consume(
4186
4308
  queryRequest,
@@ -4206,6 +4328,7 @@ export class DocumentIndex<
4206
4328
  })
4207
4329
  .map((x) => [x]);
4208
4330
 
4331
+ options?.onRemoteTargets?.(selectedRemoteHashes);
4209
4332
  extraPromises && (await Promise.all(extraPromises));
4210
4333
  let tearDown: (() => void) | undefined = undefined;
4211
4334
  const search = this;
@@ -4364,23 +4487,25 @@ export class DocumentIndex<
4364
4487
 
4365
4488
  const allResults: ValueTypeFromRequest<Resolve, T, I>[] = [];
4366
4489
 
4367
- while (
4368
- iterator.done() !== true &&
4369
- coercedRequest.fetch > allResults.length
4370
- ) {
4371
- // We might need to pull .next multiple time due to data message size limitations
4490
+ try {
4491
+ while (
4492
+ iterator.done() !== true &&
4493
+ coercedRequest.fetch > allResults.length
4494
+ ) {
4495
+ // We might need to pull .next multiple time due to data message size limitations
4372
4496
 
4373
- for (const result of await iterator.next(
4374
- coercedRequest.fetch - allResults.length,
4375
- )) {
4376
- allResults.push(result as ValueTypeFromRequest<Resolve, T, I>);
4497
+ for (const result of await iterator.next(
4498
+ coercedRequest.fetch - allResults.length,
4499
+ )) {
4500
+ allResults.push(result as ValueTypeFromRequest<Resolve, T, I>);
4501
+ }
4377
4502
  }
4378
- }
4379
-
4380
- await iterator.close();
4381
4503
 
4382
- // Deduplicate and return values directly
4383
- return dedup(allResults, this.indexByResolver);
4504
+ // Deduplicate and return values directly
4505
+ return dedup(allResults, this.indexByResolver);
4506
+ } finally {
4507
+ await iterator.close();
4508
+ }
4384
4509
  }
4385
4510
 
4386
4511
  private resolveIndexed<R>(
@@ -4672,6 +4797,50 @@ export class DocumentIndex<
4672
4797
  buffer: BufferedResult<types.ResultTypeFromRequest<R, T, I> | I, I>[];
4673
4798
  }
4674
4799
  > = new Map();
4800
+ const remoteIteratorPeersToClose = new Set<string>();
4801
+ const staleRemoteIteratorIdsToClose = new Map<string, Uint8Array[]>();
4802
+ const retiredRemoteIteratorPeers = new Set<string>();
4803
+ const pendingMissingResponseRetryPeers = new Set<string>();
4804
+ const missingResponseRetryAttempts = new Map<string, number>();
4805
+ const maxMissingResponseRetryAttempts = 2;
4806
+ const retireRemoteIteratorPeer = (peer: string) => {
4807
+ remoteIteratorPeersToClose.add(peer);
4808
+ retiredRemoteIteratorPeers.add(peer);
4809
+ const peerBuffer = peerBufferMap.get(peer);
4810
+ if (!peerBuffer || peerBuffer.buffer.length === 0) {
4811
+ peerBufferMap.delete(peer);
4812
+ } else {
4813
+ // Keep already-received values available to the caller, but do not
4814
+ // issue another CollectNextRequest until a fresh iteration succeeds.
4815
+ peerBuffer.kept = 0;
4816
+ }
4817
+ };
4818
+ const recordMissingResponseGroups = (missingGroups: string[][]) => {
4819
+ const selfHash = this.node.identity.publicKey.hashcode();
4820
+ for (const group of missingGroups) {
4821
+ for (const hash of group) {
4822
+ if (hash && hash !== selfHash) {
4823
+ retireRemoteIteratorPeer(hash);
4824
+ }
4825
+ }
4826
+
4827
+ if (!retryMissingResponseGroups) {
4828
+ continue;
4829
+ }
4830
+
4831
+ const target = group.find((hash) => {
4832
+ if (!hash || hash === selfHash) return false;
4833
+ const attempts = missingResponseRetryAttempts.get(hash) ?? 0;
4834
+ return attempts < maxMissingResponseRetryAttempts;
4835
+ });
4836
+ if (!target) continue;
4837
+ pendingMissingResponseRetryPeers.add(target);
4838
+ missingResponseRetryAttempts.set(
4839
+ target,
4840
+ (missingResponseRetryAttempts.get(target) ?? 0) + 1,
4841
+ );
4842
+ }
4843
+ };
4675
4844
  const visited = new Set<indexerTypes.IdPrimitive>();
4676
4845
  let indexedPlaceholders:
4677
4846
  | Map<
@@ -4903,6 +5072,17 @@ export class DocumentIndex<
4903
5072
  n: number,
4904
5073
  fetchOptions?: { from?: string[]; fetchedFirstForRemote?: Set<string> },
4905
5074
  ): Promise<boolean> => {
5075
+ const remoteRequestOptions =
5076
+ typeof options?.remote === "object" ? options.remote : undefined;
5077
+ const fetchSignals = [
5078
+ options?.signal,
5079
+ remoteRequestOptions?.signal,
5080
+ ensureController().signal,
5081
+ ].filter((signal): signal is AbortSignal => signal != null);
5082
+ const fetchSignal =
5083
+ fetchSignals.length === 1
5084
+ ? fetchSignals[0]
5085
+ : AbortSignal.any(fetchSignals);
4906
5086
  await warmupPromise;
4907
5087
  let hasMore = false;
4908
5088
  let missingResponses = false;
@@ -4918,33 +5098,43 @@ export class DocumentIndex<
4918
5098
  typeof options?.remote === "object" &&
4919
5099
  options.remote.reach?.discover &&
4920
5100
  discoveredTargetHashes?.length === 0;
5101
+ const queryRemote =
5102
+ options?.remote !== false && !skipRemoteDueToDiscovery;
5103
+ const remoteFrom =
5104
+ fetchOptions?.from ??
5105
+ initialRemoteTargets ??
5106
+ remoteRequestOptions?.from;
5107
+ if (queryRemote) {
5108
+ const selfHash = this.node.identity.publicKey.hashcode();
5109
+ for (const peer of remoteFrom ?? []) {
5110
+ if (peer !== selfHash) {
5111
+ remoteIteratorPeersToClose.add(peer);
5112
+ }
5113
+ }
5114
+ }
4921
5115
 
4922
5116
  queryRequestCoerced.fetch = n;
4923
5117
  await this.queryCommence(
4924
5118
  queryRequestCoerced,
4925
5119
  {
4926
5120
  local: fetchOptions?.from != null ? false : options?.local,
4927
- remote:
4928
- options?.remote !== false && !skipRemoteDueToDiscovery
4929
- ? {
4930
- ...(typeof options?.remote === "object"
4931
- ? options.remote
4932
- : {}),
4933
- from:
4934
- fetchOptions?.from ??
4935
- initialRemoteTargets ??
4936
- (typeof options?.remote === "object"
4937
- ? options.remote.from
4938
- : undefined),
4939
- }
4940
- : false,
5121
+ remote: queryRemote
5122
+ ? {
5123
+ ...remoteRequestOptions,
5124
+ from: remoteFrom,
5125
+ signal: fetchSignal,
5126
+ }
5127
+ : false,
4941
5128
  resolve,
4942
- signal: options?.signal,
5129
+ signal: fetchSignal,
4943
5130
  onResponse: async (response, from) => {
4944
5131
  if (!from) {
4945
5132
  logger.error("Missing response from");
4946
5133
  return;
4947
5134
  }
5135
+ const fromHash = from.hashcode();
5136
+ remoteIteratorPeersToClose.add(fromHash);
5137
+ retiredRemoteIteratorPeers.delete(fromHash);
4948
5138
  if (response instanceof types.NoAccess) {
4949
5139
  logger.error("Dont have access");
4950
5140
  return;
@@ -4953,7 +5143,7 @@ export class DocumentIndex<
4953
5143
  types.ResultTypeFromRequest<R, T, I>
4954
5144
  >;
4955
5145
 
4956
- const existingBuffer = peerBufferMap.get(from.hashcode());
5146
+ const existingBuffer = peerBufferMap.get(fromHash);
4957
5147
  const buffer: BufferedResult<
4958
5148
  types.ResultTypeFromRequest<R, T, I> | I,
4959
5149
  I
@@ -4961,10 +5151,12 @@ export class DocumentIndex<
4961
5151
 
4962
5152
  if (results.kept === 0n && results.results.length === 0) {
4963
5153
  if (keepRemoteAlive) {
4964
- peerBufferMap.set(from.hashcode(), {
5154
+ peerBufferMap.set(fromHash, {
4965
5155
  buffer,
4966
5156
  kept: 0,
4967
5157
  });
5158
+ } else {
5159
+ remoteIteratorPeersToClose.delete(fromHash);
4968
5160
  }
4969
5161
  return;
4970
5162
  }
@@ -4979,6 +5171,8 @@ export class DocumentIndex<
4979
5171
 
4980
5172
  if (effectiveKept > 0) {
4981
5173
  hasMore = true;
5174
+ } else if (!keepRemoteAlive) {
5175
+ remoteIteratorPeersToClose.delete(fromHash);
4982
5176
  }
4983
5177
 
4984
5178
  for (const result of results.results) {
@@ -5042,7 +5236,7 @@ export class DocumentIndex<
5042
5236
  }
5043
5237
  }
5044
5238
 
5045
- peerBufferMap.set(from.hashcode(), {
5239
+ peerBufferMap.set(fromHash, {
5046
5240
  buffer,
5047
5241
  kept: effectiveKept,
5048
5242
  });
@@ -5054,9 +5248,6 @@ export class DocumentIndex<
5054
5248
  },
5055
5249
  onMissingResponses: (error) => {
5056
5250
  missingResponses = true;
5057
- if (!retryMissingResponseGroups) {
5058
- return;
5059
- }
5060
5251
  const missingGroups = (
5061
5252
  error as MissingResponsesError & {
5062
5253
  missingGroups?: string[][];
@@ -5065,27 +5256,22 @@ export class DocumentIndex<
5065
5256
  if (!missingGroups?.length) {
5066
5257
  return;
5067
5258
  }
5068
-
5069
- const selfHash = this.node.identity.publicKey.hashcode();
5070
- for (const group of missingGroups) {
5071
- const target = group.find((hash) => {
5072
- if (!hash || hash === selfHash) return false;
5073
- const attempts = missingResponseRetryAttempts.get(hash) ?? 0;
5074
- return attempts < maxMissingResponseRetryAttempts;
5075
- });
5076
- if (!target) continue;
5077
- pendingMissingResponseRetryPeers.add(target);
5078
- missingResponseRetryAttempts.set(
5079
- target,
5080
- (missingResponseRetryAttempts.get(target) ?? 0) + 1,
5081
- );
5259
+ recordMissingResponseGroups(missingGroups);
5260
+ },
5261
+ onRemoteTargets: (targets) => {
5262
+ for (const peer of targets) {
5263
+ remoteIteratorPeersToClose.add(peer);
5082
5264
  }
5083
5265
  },
5084
5266
  },
5085
5267
  fetchOptions?.fetchedFirstForRemote,
5086
5268
  );
5087
5269
 
5088
- if (missingResponses && retryMissingResponseGroups) {
5270
+ if (
5271
+ missingResponses &&
5272
+ retryMissingResponseGroups &&
5273
+ pendingMissingResponseRetryPeers.size > 0
5274
+ ) {
5089
5275
  hasMore = true;
5090
5276
  unsetDone();
5091
5277
  }
@@ -5117,6 +5303,19 @@ export class DocumentIndex<
5117
5303
  if (pendingMissingResponseRetryPeers.size > 0) {
5118
5304
  const retryTargets = [...pendingMissingResponseRetryPeers];
5119
5305
  pendingMissingResponseRetryPeers.clear();
5306
+ const idTranslation =
5307
+ this._prefetch?.accumulator.getTranslationMap(queryRequestCoerced);
5308
+ for (const peer of retryTargets) {
5309
+ const staleRemoteIteratorId = idTranslation?.get(peer);
5310
+ if (staleRemoteIteratorId) {
5311
+ const staleIds = staleRemoteIteratorIdsToClose.get(peer) ?? [];
5312
+ if (!staleIds.some((id) => equals(id, staleRemoteIteratorId))) {
5313
+ staleIds.push(staleRemoteIteratorId);
5314
+ staleRemoteIteratorIdsToClose.set(peer, staleIds);
5315
+ }
5316
+ idTranslation!.delete(peer);
5317
+ }
5318
+ }
5120
5319
  return setFetchPromise(
5121
5320
  fetchFirst(n, {
5122
5321
  from: retryTargets,
@@ -5130,6 +5329,12 @@ export class DocumentIndex<
5130
5329
  let resultsLeft = 0;
5131
5330
 
5132
5331
  for (const [peer, buffer] of peerBufferMap) {
5332
+ if (retiredRemoteIteratorPeers.has(peer)) {
5333
+ if (buffer.buffer.length === 0) {
5334
+ peerBufferMap.delete(peer);
5335
+ }
5336
+ continue;
5337
+ }
5133
5338
  if (buffer.buffer.length < n) {
5134
5339
  const hasExistingRemoteResults = buffer.kept > 0;
5135
5340
  if (!hasExistingRemoteResults && !keepRemoteAlive) {
@@ -5272,10 +5477,11 @@ export class DocumentIndex<
5272
5477
  "Failed to collect sorted results from self. " + e?.message,
5273
5478
  );
5274
5479
  peerBufferMap.delete(peer);
5275
- }),
5480
+ }),
5276
5481
  );
5277
5482
  } else {
5278
5483
  // Fetch remotely
5484
+ remoteIteratorPeersToClose.add(peer);
5279
5485
  const idTranslation =
5280
5486
  this._prefetch?.accumulator.getTranslationMap(
5281
5487
  queryRequestCoerced,
@@ -5287,22 +5493,45 @@ export class DocumentIndex<
5287
5493
  amount: collectRequest.amount,
5288
5494
  });
5289
5495
  }
5496
+ const remoteRequestOptions =
5497
+ typeof options?.remote === "object" ? options.remote : undefined;
5498
+ const collectSignals = [
5499
+ options?.signal,
5500
+ remoteRequestOptions?.signal,
5501
+ ensureController().signal,
5502
+ ].filter((signal): signal is AbortSignal => signal != null);
5503
+ const collectSignal =
5504
+ collectSignals.length === 1
5505
+ ? collectSignals[0]
5506
+ : AbortSignal.any(collectSignals);
5290
5507
 
5291
5508
  promises.push(
5292
5509
  this._query
5293
5510
  .request(remoteCollectRequest, {
5294
5511
  ...options,
5295
- signal: options?.signal
5296
- ? AbortSignal.any([
5297
- options.signal,
5298
- ensureController().signal,
5299
- ])
5300
- : ensureController().signal,
5512
+ ...remoteRequestOptions,
5513
+ signal: collectSignal,
5301
5514
  priority: getRemoteQueryPriority(options?.remote),
5302
5515
  mode: new SilentDelivery({ to: [peer], redundancy: 1 }),
5303
5516
  })
5304
- .then((response) =>
5305
- introduceEntries(
5517
+ .then((response) => {
5518
+ if (
5519
+ !response.some((result) => result.from?.hashcode() === peer)
5520
+ ) {
5521
+ const missingGroups = [[peer]];
5522
+ if (remoteRequestOptions?.throwOnMissing) {
5523
+ retireRemoteIteratorPeer(peer);
5524
+ throw new MissingResponsesError(
5525
+ "Did not receive responses from all shards: " +
5526
+ JSON.stringify(missingGroups),
5527
+ missingGroups,
5528
+ );
5529
+ }
5530
+ recordMissingResponseGroups(missingGroups);
5531
+ return;
5532
+ }
5533
+
5534
+ return introduceEntries(
5306
5535
  queryRequestCoerced,
5307
5536
  response,
5308
5537
  this.documentType,
@@ -5319,6 +5548,12 @@ export class DocumentIndex<
5319
5548
  logger.error("Missing from for sorted query");
5320
5549
  return;
5321
5550
  }
5551
+ if (
5552
+ !keepRemoteAlive &&
5553
+ response.response.kept === 0n
5554
+ ) {
5555
+ remoteIteratorPeersToClose.delete(peer);
5556
+ }
5322
5557
 
5323
5558
  if (response.response.results.length === 0) {
5324
5559
  if (
@@ -5439,8 +5674,8 @@ export class DocumentIndex<
5439
5674
  e?.message,
5440
5675
  );
5441
5676
  peerBufferMap.delete(peer);
5442
- }),
5443
- ),
5677
+ });
5678
+ }),
5444
5679
  );
5445
5680
  }
5446
5681
  } else {
@@ -5466,7 +5701,9 @@ export class DocumentIndex<
5466
5701
  }
5467
5702
  }
5468
5703
  }
5469
- return resultsLeft === 0; // 0 results left to fetch and 0 pending results
5704
+ return (
5705
+ resultsLeft === 0 && pendingMissingResponseRetryPeers.size === 0
5706
+ ); // 0 results left to fetch and 0 pending results
5470
5707
  }),
5471
5708
  );
5472
5709
  };
@@ -5587,32 +5824,89 @@ export class DocumentIndex<
5587
5824
  done = true;
5588
5825
  };
5589
5826
 
5590
- let close = async () => {
5827
+ const outerSignal = options?.signal;
5828
+ let outerAbortListener: (() => void) | undefined;
5829
+ let closeStarted = false;
5830
+ let closePromise: Promise<void> | undefined;
5831
+ const performClose = async () => {
5832
+ const idTranslation =
5833
+ this._prefetch?.accumulator.getTranslationMap(queryRequestCoerced);
5834
+ const remoteIteratorIds = idTranslation
5835
+ ? new Map(idTranslation)
5836
+ : undefined;
5591
5837
  cleanupAndDone();
5592
5838
 
5593
5839
  // Keep-open iterators can still have active remote state even when
5594
5840
  // their pending count has already drained to zero.
5595
- const closeRequest = new types.CloseIteratorRequest({
5596
- id: queryRequestCoerced.id,
5597
- });
5598
5841
  const selfHash = this.node.identity.publicKey.hashcode();
5599
- const remotePeers = keepRemoteAlive
5600
- ? [...peerBufferMap.keys()].filter((peer) => peer !== selfHash)
5601
- : [...peerBufferMap.entries()]
5602
- .filter(([peer, buffer]) => peer !== selfHash && buffer.kept > 0)
5603
- .map(([peer]) => peer);
5842
+ const activeRemotePeers = new Set(
5843
+ keepRemoteAlive
5844
+ ? [...peerBufferMap.keys()].filter((peer) => peer !== selfHash)
5845
+ : [...peerBufferMap.entries()]
5846
+ .filter(
5847
+ ([peer, buffer]) => peer !== selfHash && buffer.kept > 0,
5848
+ )
5849
+ .map(([peer]) => peer),
5850
+ );
5851
+ for (const peer of remoteIteratorPeersToClose) {
5852
+ if (peer !== selfHash) {
5853
+ activeRemotePeers.add(peer);
5854
+ }
5855
+ }
5856
+ const staleRemoteIteratorIds = new Map(
5857
+ [...staleRemoteIteratorIdsToClose].map(([peer, ids]) => [
5858
+ peer,
5859
+ [...ids],
5860
+ ]),
5861
+ );
5862
+ const remotePeers = new Set([
5863
+ ...activeRemotePeers,
5864
+ ...staleRemoteIteratorIds.keys(),
5865
+ ]);
5604
5866
  peerBufferMap.clear();
5867
+ retiredRemoteIteratorPeers.clear();
5868
+ remoteIteratorPeersToClose.clear();
5869
+ staleRemoteIteratorIdsToClose.clear();
5870
+ if (remotePeers.size === 0) {
5871
+ return;
5872
+ }
5873
+ const remoteCloseOptions =
5874
+ typeof options?.remote === "object" ? options.remote : undefined;
5875
+ const closeSignal = AbortSignal.timeout(CLOSE_ITERATOR_REQUEST_TIMEOUT);
5605
5876
  await Promise.allSettled(
5606
- remotePeers.map((peer) =>
5607
- this._query.send(closeRequest, {
5608
- ...options,
5609
- priority: getRemoteQueryPriority(options?.remote),
5610
- mode: new SilentDelivery({ to: [peer], redundancy: 1 }),
5611
- }),
5612
- ),
5877
+ [...remotePeers].flatMap((peer) => {
5878
+ const ids: Uint8Array[] = [];
5879
+ if (activeRemotePeers.has(peer)) {
5880
+ ids.push(remoteIteratorIds?.get(peer) ?? queryRequestCoerced.id);
5881
+ }
5882
+ for (const staleRemoteIteratorId of
5883
+ staleRemoteIteratorIds.get(peer) ?? []) {
5884
+ if (!ids.some((id) => equals(id, staleRemoteIteratorId))) {
5885
+ ids.push(staleRemoteIteratorId);
5886
+ }
5887
+ }
5888
+ return ids.map((id) => {
5889
+ const closeRequest = new types.CloseIteratorRequest({ id });
5890
+ return this._query.send(closeRequest, {
5891
+ ...remoteCloseOptions,
5892
+ signal: closeSignal,
5893
+ priority: getRemoteQueryPriority(options?.remote),
5894
+ mode: new SilentDelivery({ to: [peer], redundancy: 1 }),
5895
+ });
5896
+ });
5897
+ }),
5613
5898
  );
5614
5899
  };
5615
- options?.signal && options.signal.addEventListener("abort", close);
5900
+ const close = () => {
5901
+ if (closeStarted) return closePromise!;
5902
+ closeStarted = true;
5903
+ if (outerAbortListener) {
5904
+ outerSignal?.removeEventListener("abort", outerAbortListener);
5905
+ outerAbortListener = undefined;
5906
+ }
5907
+ closePromise = performClose();
5908
+ return closePromise;
5909
+ };
5616
5910
 
5617
5911
  let doneFn = () => {
5618
5912
  return done;
@@ -5621,9 +5915,6 @@ export class DocumentIndex<
5621
5915
  let joinListener: (() => void) | undefined;
5622
5916
 
5623
5917
  let fetchedFirstForRemote: Set<string> | undefined = undefined;
5624
- const pendingMissingResponseRetryPeers = new Set<string>();
5625
- const missingResponseRetryAttempts = new Map<string, number>();
5626
- const maxMissingResponseRetryAttempts = 2;
5627
5918
  let joinFetchesInFlight = 0;
5628
5919
 
5629
5920
  let updateDeferred: ReturnType<typeof pDefer> | undefined;
@@ -6144,9 +6435,10 @@ export class DocumentIndex<
6144
6435
  continue;
6145
6436
  }
6146
6437
  }
6147
- const id = indexerTypes.toId(
6438
+ const indexId = indexerTypes.toId(
6148
6439
  this.indexByResolver(indexedCandidate),
6149
- ).primitive;
6440
+ );
6441
+ const id = indexId.primitive;
6150
6442
  const existingIndexed = indexedPlaceholders?.get(id);
6151
6443
  if (existingIndexed) {
6152
6444
  if (resolve) {
@@ -6187,6 +6479,10 @@ export class DocumentIndex<
6187
6479
  if (!resolve) {
6188
6480
  ensureIndexedPlaceholders().set(id, placeholder);
6189
6481
  }
6482
+ await this._resumableIterators.markYielded(
6483
+ queryRequestCoerced.idString,
6484
+ [indexId],
6485
+ );
6190
6486
  hasRelevantChange = true;
6191
6487
  }
6192
6488
  }
@@ -6458,6 +6754,16 @@ export class DocumentIndex<
6458
6754
  }
6459
6755
  };
6460
6756
 
6757
+ if (outerSignal) {
6758
+ outerAbortListener = () => {
6759
+ void close();
6760
+ };
6761
+ outerSignal.addEventListener("abort", outerAbortListener, { once: true });
6762
+ if (outerSignal.aborted) {
6763
+ void close();
6764
+ }
6765
+ }
6766
+
6461
6767
  return {
6462
6768
  close,
6463
6769
  next,
@@ -6531,53 +6837,62 @@ export class DocumentIndex<
6531
6837
  const drainBatchSize = replicate ? 1000 : 100;
6532
6838
  let result: ValueTypeFromRequest<Resolve, T, I>[] = [];
6533
6839
  let c = 0;
6534
- while (doneFn() !== true) {
6535
- let batch = await next(drainBatchSize);
6536
- c += batch.length;
6537
- if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
6538
- warn(
6539
- "Iterating for more than " +
6540
- WARNING_WHEN_ITERATING_FOR_MORE_THAN +
6541
- " results",
6542
- );
6543
- }
6544
- if (batch.length > 0) {
6545
- result.push(...batch);
6546
- continue;
6840
+ try {
6841
+ while (doneFn() !== true) {
6842
+ let batch = await next(drainBatchSize);
6843
+ c += batch.length;
6844
+ if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
6845
+ warn(
6846
+ "Iterating for more than " +
6847
+ WARNING_WHEN_ITERATING_FOR_MORE_THAN +
6848
+ " results",
6849
+ );
6850
+ }
6851
+ if (batch.length > 0) {
6852
+ result.push(...batch);
6853
+ continue;
6854
+ }
6855
+ await waitForUpdateAndResetDeferred();
6547
6856
  }
6548
- await waitForUpdateAndResetDeferred();
6857
+ return result;
6858
+ } finally {
6859
+ await close();
6549
6860
  }
6550
- cleanupAndDone();
6551
- return result;
6552
6861
  },
6553
6862
  first: async () => {
6554
- if (doneFn()) {
6555
- return undefined;
6863
+ try {
6864
+ if (doneFn()) {
6865
+ return undefined;
6866
+ }
6867
+ let batch = await next(1);
6868
+ return batch[0];
6869
+ } finally {
6870
+ await close();
6556
6871
  }
6557
- let batch = await next(1);
6558
- cleanupAndDone();
6559
- return batch[0];
6560
6872
  },
6561
6873
  [Symbol.asyncIterator]: async function* () {
6562
6874
  drain = true;
6563
6875
  const drainBatchSize = replicate ? 1000 : 100;
6564
6876
  let c = 0;
6565
- while (doneFn() !== true) {
6566
- const batch = await next(drainBatchSize);
6567
- c += batch.length;
6568
- if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
6569
- warn(
6570
- "Iterating for more than " +
6571
- WARNING_WHEN_ITERATING_FOR_MORE_THAN +
6572
- " results",
6573
- );
6574
- }
6575
- for (const entry of batch) {
6576
- yield entry;
6877
+ try {
6878
+ while (doneFn() !== true) {
6879
+ const batch = await next(drainBatchSize);
6880
+ c += batch.length;
6881
+ if (c > WARNING_WHEN_ITERATING_FOR_MORE_THAN) {
6882
+ warn(
6883
+ "Iterating for more than " +
6884
+ WARNING_WHEN_ITERATING_FOR_MORE_THAN +
6885
+ " results",
6886
+ );
6887
+ }
6888
+ for (const entry of batch) {
6889
+ yield entry;
6890
+ }
6891
+ await waitForUpdateAndResetDeferred();
6577
6892
  }
6578
- await waitForUpdateAndResetDeferred();
6893
+ } finally {
6894
+ await close();
6579
6895
  }
6580
- cleanupAndDone();
6581
6896
  },
6582
6897
  };
6583
6898
  }