@peerbit/document 15.0.4 → 15.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/document",
3
- "version": "15.0.4",
3
+ "version": "15.0.6",
4
4
  "description": "Document store implementation",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -55,40 +55,40 @@
55
55
  "dependencies": {
56
56
  "@chainsafe/libp2p-yamux": "^8.0.1",
57
57
  "@dao-xyz/borsh": "^6.0.0",
58
- "@libp2p/interface": "^3.1.1",
59
- "@libp2p/tcp": "^11.0.14",
60
- "@multiformats/multiaddr": "^13.0.1",
58
+ "@libp2p/interface": "^3.2.5",
59
+ "@libp2p/tcp": "^11.0.26",
60
+ "@multiformats/multiaddr": "^13.0.3",
61
61
  "p-defer": "^4.0.0",
62
62
  "uint8arrays": "^5.1.0",
63
63
  "@peerbit/cache": "3.1.1",
64
- "@peerbit/crypto": "3.1.5",
65
- "@peerbit/document-interface": "3.2.60",
66
- "@peerbit/indexer-cache": "0.2.12",
67
- "@peerbit/indexer-interface": "3.0.9",
68
- "@peerbit/indexer-simple": "1.2.13",
69
- "@peerbit/indexer-sqlite3": "3.0.14",
70
- "@peerbit/log": "6.2.17",
71
- "@peerbit/logger": "2.0.1",
72
- "@peerbit/program": "6.0.49",
73
- "@peerbit/rpc": "6.1.17",
74
- "@peerbit/shared-log": "16.0.4",
75
- "@peerbit/time": "3.0.1",
76
- "@peerbit/pubsub": "5.4.1",
77
- "@peerbit/stream-interface": "6.0.15"
64
+ "@peerbit/crypto": "3.1.6",
65
+ "@peerbit/document-interface": "3.2.62",
66
+ "@peerbit/indexer-interface": "3.0.10",
67
+ "@peerbit/indexer-cache": "0.2.13",
68
+ "@peerbit/indexer-simple": "1.2.14",
69
+ "@peerbit/indexer-sqlite3": "3.0.15",
70
+ "@peerbit/logger": "2.0.2",
71
+ "@peerbit/program": "6.0.51",
72
+ "@peerbit/pubsub": "5.4.2",
73
+ "@peerbit/log": "6.2.19",
74
+ "@peerbit/rpc": "6.1.19",
75
+ "@peerbit/shared-log": "16.0.6",
76
+ "@peerbit/stream-interface": "6.0.16",
77
+ "@peerbit/time": "3.0.1"
78
78
  },
79
79
  "optionalDependencies": {
80
80
  "@peerbit/document-rust": "0.1.1"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@chainsafe/libp2p-noise": "^17.0.0",
84
- "@libp2p/websockets": "^10.1.7",
84
+ "@libp2p/websockets": "^10.1.19",
85
85
  "@types/pidusage": "^2.0.5",
86
86
  "pidusage": "^4.0.1",
87
87
  "uuid": "^11.1.1",
88
88
  "@peerbit/log-rust": "1.1.3",
89
- "@peerbit/native-backbone": "0.2.5",
90
- "@peerbit/test-utils": "3.1.23",
91
- "peerbit": "5.3.23"
89
+ "@peerbit/native-backbone": "0.2.6",
90
+ "@peerbit/test-utils": "3.1.25",
91
+ "peerbit": "5.3.25"
92
92
  },
93
93
  "repository": {
94
94
  "type": "git",
package/src/program.ts CHANGED
@@ -1991,6 +1991,75 @@ export class Documents<
1991
1991
  >;
1992
1992
  }
1993
1993
 
1994
+ /**
1995
+ * The change-delivery variant of `getLocalIndexedContext`, and the ONLY caller
1996
+ * allowed to treat a closed index as "no prior version".
1997
+ *
1998
+ * `getLocalIndexedContext` reaches the backing indexer directly, bypassing
1999
+ * `DocumentIndex`'s guarded wrappers. `terminalChildOrder` now tears the index
2000
+ * down after the log, but a change can still be delivered against a closed
2001
+ * index on paths that do not go through that ordering: a `DocumentIndex` closed
2002
+ * on its own rather than through its parent, and a log teardown that fails,
2003
+ * which opens the rank gate without having drained. For a change that the log
2004
+ * has ALREADY committed, "there is no prior version to compare against" is the
2005
+ * right reading, and the write leg - which already returns without persisting
2006
+ * once the index is closed, see `DocumentIndex.putWithContext` - drops it.
2007
+ *
2008
+ * WHY THIS IS NOT ON THE SHARED HELPER. It was, and that was wrong. The helper
2009
+ * has five callers and only this one is a post-commit change:
2010
+ *
2011
+ * :2689 resolveCanPerformDeleteValue - feeds the user's canPerform policy
2012
+ * :2794 _canAppend PUT - immutable-overwrite protection
2013
+ * :2863 _canAppend DELETE - delete-history validation
2014
+ * :3124 putCompatDocumentBackend - the user-initiated put() path
2015
+ *
2016
+ * Swallowing there is actively harmful rather than merely broad. On :3124 a
2017
+ * put() against a closed index returned SUCCESS with the entry committed to the
2018
+ * log and silently absent from the index - the exact log/index divergence this
2019
+ * recovery exists to avoid, manufactured on the public write path with no error
2020
+ * signal. On :2863 a swallowed read reads as "already deleted" and returns
2021
+ * `assume ok`, turning a REJECT into an ALLOW for inbound remote deletes.
2022
+ *
2023
+ * Those callers must keep throwing: outside change delivery, a closed index is
2024
+ * a caller error, not a state to paper over.
2025
+ */
2026
+ private getLocalIndexedContextForChange(
2027
+ key: indexerTypes.IdKey,
2028
+ ): Promise<indexerTypes.IndexedResult<IndexedContextOnly<I>> | undefined> {
2029
+ // Not every backing index resolves asynchronously (the native one answers
2030
+ // synchronously, and may throw synchronously), so guard both shapes without
2031
+ // forcing a promise on the fast path.
2032
+ type LocalIndexedContext =
2033
+ | indexerTypes.IndexedResult<IndexedContextOnly<I>>
2034
+ | undefined;
2035
+ try {
2036
+ const result = this.getLocalIndexedContext(
2037
+ key,
2038
+ ) as MaybePromise<LocalIndexedContext>;
2039
+ return (
2040
+ isPromiseLike(result)
2041
+ ? result.catch((error) => this.recoverClosedIndexContextRead(error))
2042
+ : result
2043
+ ) as Promise<LocalIndexedContext>;
2044
+ } catch (error) {
2045
+ return this.recoverClosedIndexContextRead(
2046
+ error,
2047
+ ) as unknown as Promise<LocalIndexedContext>;
2048
+ }
2049
+ }
2050
+
2051
+ /**
2052
+ * Deliberately narrow: only `NotStartedError`, and only once the document index
2053
+ * is actually closed. An indexer that breaks while the index is open still
2054
+ * throws. Reachable only from `getLocalIndexedContextForChange`.
2055
+ */
2056
+ private recoverClosedIndexContextRead(error: unknown): undefined {
2057
+ if (error instanceof indexerTypes.NotStartedError && this._index.closed) {
2058
+ return undefined;
2059
+ }
2060
+ throw error;
2061
+ }
2062
+
1994
2063
  private getNativeEntrySignerPublicKeys(
1995
2064
  hashes: string[],
1996
2065
  ): Array<Uint8Array | undefined> | undefined {
@@ -2442,6 +2511,27 @@ export class Documents<
2442
2511
  return this.log.recover();
2443
2512
  }
2444
2513
 
2514
+ /**
2515
+ * `Documents` owns two sub-programs: `log` (the source of changes) and
2516
+ * `_index` (a view derived from it, backed by an indexer). `Program` tears
2517
+ * children down concurrently by default, so nothing stopped
2518
+ * `DocumentIndex.close()` -> `indexer.stop()` from completing while a
2519
+ * `Log.joinRecursively()` was still in flight. That join calls `handleChanges`
2520
+ * with an entry it has *already* committed to the log, and `handleChanges`
2521
+ * reads the index (`getLocalIndexedContext`) before it writes - so the read hit
2522
+ * an indexer in state "closed" and threw `NotStartedError` out of a join that
2523
+ * had no business failing.
2524
+ *
2525
+ * Ranking the index behind the log fixes the ordering rather than the symptom:
2526
+ * `SharedLog.close()` drains its in-flight receives and then closes the lower
2527
+ * `Log`, so by the time the index is torn down no further change can be
2528
+ * dispatched, and every change already committed to the log has been applied to
2529
+ * the index. Nothing is dropped.
2530
+ */
2531
+ protected terminalChildOrder(child: Program): number {
2532
+ return child === (this._index as Program) ? 1 : 0;
2533
+ }
2534
+
2445
2535
  private async _resolveEntry(
2446
2536
  history: Entry<Operation> | string,
2447
2537
  options?: {
@@ -5369,7 +5459,7 @@ export class Documents<
5369
5459
  ? reference.existing
5370
5460
  : this.isNativeMode()
5371
5461
  ? this.getNativeModeIndexedContext(key) || null
5372
- : (await this.getLocalIndexedContext(key)) || null;
5462
+ : (await this.getLocalIndexedContextForChange(key)) || null;
5373
5463
  if (!this.strictHistory && existing) {
5374
5464
  // if immutable use oldest, else use newest
5375
5465
  let shouldIgnoreChange = this.immutable
package/src/search.ts CHANGED
@@ -15,12 +15,6 @@ import {
15
15
  sha256Base64Sync,
16
16
  } from "@peerbit/crypto";
17
17
  import * as types from "@peerbit/document-interface";
18
- import {
19
- type SimpleDocumentFieldExtractionPlan,
20
- type SimpleDocumentProjectionContext,
21
- type SimpleDocumentProjectionPlan,
22
- tryProjectDocumentIndexSimple,
23
- } from "./native-rust.js";
24
18
  import { CachedIndex, type QueryCacheOptions } from "@peerbit/indexer-cache";
25
19
  import * as indexerTypes from "@peerbit/indexer-interface";
26
20
  import { HashmapIndex } from "@peerbit/indexer-simple";
@@ -56,6 +50,12 @@ import type { QueryPredictor } from "./most-common-query-predictor.js";
56
50
  import MostCommonQueryPredictor, {
57
51
  idAgnosticQueryKey,
58
52
  } from "./most-common-query-predictor.js";
53
+ import {
54
+ type SimpleDocumentFieldExtractionPlan,
55
+ type SimpleDocumentProjectionContext,
56
+ type SimpleDocumentProjectionPlan,
57
+ tryProjectDocumentIndexSimple,
58
+ } from "./native-rust.js";
59
59
  import { type Operation, isPutOperation } from "./operation.js";
60
60
  import { Prefetch } from "./prefetch.js";
61
61
  import type { ExtractArgs } from "./program.js";
@@ -67,13 +67,13 @@ import {
67
67
  } from "./result-shape.js";
68
68
  import { ResumableIterators } from "./resumable-iterator.js";
69
69
  import {
70
+ type DocumentTransformDescriptor,
71
+ type DocumentTransformFacts,
72
+ type DocumentTransformer,
70
73
  canPrepareDocumentTransformBeforeAppend,
71
74
  canPrepareDocumentTransformWithAppendFacts,
72
75
  documentTransformPreservesFieldPath,
73
76
  getDocumentTransformDescriptor,
74
- type DocumentTransformFacts,
75
- type DocumentTransformDescriptor,
76
- type DocumentTransformer,
77
77
  } from "./transform.js";
78
78
 
79
79
  const WARNING_WHEN_ITERATING_FOR_MORE_THAN = 1e5;
@@ -1221,10 +1221,9 @@ export class DocumentIndex<
1221
1221
  } catch {
1222
1222
  // A native-backed head may be hollow until read from the block store.
1223
1223
  }
1224
- return Entry.fromMultihash<Operation>(
1225
- this._log.log.blocks,
1226
- hash,
1227
- ).catch(() => head);
1224
+ return Entry.fromMultihash<Operation>(this._log.log.blocks, hash).catch(
1225
+ () => head,
1226
+ );
1228
1227
  }
1229
1228
  return Entry.fromMultihash<Operation>(this._log.log.blocks, hash);
1230
1229
  }
@@ -1458,18 +1457,12 @@ export class DocumentIndex<
1458
1457
  claimedIds = wrapped
1459
1458
  .map((result) =>
1460
1459
  result instanceof types.ResultValue && result.indexed
1461
- ? indexerTypes.toId(
1462
- this.indexByResolver(result.indexed),
1463
- )
1460
+ ? indexerTypes.toId(this.indexByResolver(result.indexed))
1464
1461
  : result instanceof types.ResultIndexedValue
1465
- ? indexerTypes.toId(
1466
- this.indexByResolver(result.value),
1467
- )
1462
+ ? indexerTypes.toId(this.indexByResolver(result.value))
1468
1463
  : undefined,
1469
1464
  )
1470
- .filter(
1471
- (id): id is indexerTypes.IdKey => id !== undefined,
1472
- );
1465
+ .filter((id): id is indexerTypes.IdKey => id !== undefined);
1473
1466
  }
1474
1467
  }
1475
1468
  if (batches.length) {
@@ -1792,9 +1785,7 @@ export class DocumentIndex<
1792
1785
  private canPrepareNativeBackboneDocumentIndexCommit(): boolean {
1793
1786
  return (
1794
1787
  (this.transformerIsIdentity && this.indexedTypeIsDocumentType) ||
1795
- canPrepareDocumentTransformBeforeAppend(
1796
- this.nativeTransformDescriptor,
1797
- )
1788
+ canPrepareDocumentTransformBeforeAppend(this.nativeTransformDescriptor)
1798
1789
  );
1799
1790
  }
1800
1791
 
@@ -1802,9 +1793,7 @@ export class DocumentIndex<
1802
1793
  return (
1803
1794
  this.canPrepareNativeBackboneDocumentIndexCommit() ||
1804
1795
  !!this.nativeTransformProjectionPlan ||
1805
- canPrepareDocumentTransformWithAppendFacts(
1806
- this.nativeTransformDescriptor,
1807
- )
1796
+ canPrepareDocumentTransformWithAppendFacts(this.nativeTransformDescriptor)
1808
1797
  );
1809
1798
  }
1810
1799
 
@@ -1865,9 +1854,7 @@ export class DocumentIndex<
1865
1854
  };
1866
1855
  }
1867
1856
  if (
1868
- !canPrepareDocumentTransformBeforeAppend(
1869
- this.nativeTransformDescriptor,
1870
- )
1857
+ !canPrepareDocumentTransformBeforeAppend(this.nativeTransformDescriptor)
1871
1858
  ) {
1872
1859
  return;
1873
1860
  }
@@ -1950,9 +1937,7 @@ export class DocumentIndex<
1950
1937
  !canPrepareDocumentTransformWithAppendFacts(
1951
1938
  this.nativeTransformDescriptor,
1952
1939
  ) &&
1953
- !canPrepareDocumentTransformBeforeAppend(
1954
- this.nativeTransformDescriptor,
1955
- )
1940
+ !canPrepareDocumentTransformBeforeAppend(this.nativeTransformDescriptor)
1956
1941
  ) {
1957
1942
  return;
1958
1943
  }
@@ -1991,9 +1976,7 @@ export class DocumentIndex<
1991
1976
  !canPrepareDocumentTransformWithAppendFacts(
1992
1977
  this.nativeTransformDescriptor,
1993
1978
  ) &&
1994
- !canPrepareDocumentTransformBeforeAppend(
1995
- this.nativeTransformDescriptor,
1996
- )
1979
+ !canPrepareDocumentTransformBeforeAppend(this.nativeTransformDescriptor)
1997
1980
  ) {
1998
1981
  return;
1999
1982
  }
@@ -2488,9 +2471,7 @@ export class DocumentIndex<
2488
2471
  return indexed?.id;
2489
2472
  }
2490
2473
 
2491
- private getIndexedKeyByHead(
2492
- head: string,
2493
- ): indexerTypes.IdKey | undefined {
2474
+ private getIndexedKeyByHead(head: string): indexerTypes.IdKey | undefined {
2494
2475
  const getIdByHead = (this.index as ContextHeadIndex<I>).getIdByContextHead;
2495
2476
  return typeof getIdByHead === "function"
2496
2477
  ? getIdByHead.call(this.index, head)
@@ -2507,9 +2488,10 @@ export class DocumentIndex<
2507
2488
  return heads.map((head) => getIdByHead.call(this.index, head));
2508
2489
  }
2509
2490
 
2510
- public tryGetIdentityIndexedKeyByHead(
2511
- head: string,
2512
- ): { supported: boolean; key?: indexerTypes.IdKey } {
2491
+ public tryGetIdentityIndexedKeyByHead(head: string): {
2492
+ supported: boolean;
2493
+ key?: indexerTypes.IdKey;
2494
+ } {
2513
2495
  const getIdByHead = (this.index as ContextHeadIndex<I>).getIdByContextHead;
2514
2496
  if (typeof getIdByHead !== "function") {
2515
2497
  return { supported: false };
@@ -2849,12 +2831,18 @@ export class DocumentIndex<
2849
2831
  return false;
2850
2832
  }
2851
2833
  return isPromiseLike(persistResult)
2852
- ? persistResult.then(() => true, (error: unknown) => {
2853
- if (error instanceof indexerTypes.NotStartedError && this.closed) {
2854
- return true;
2855
- }
2856
- throw error;
2857
- })
2834
+ ? persistResult.then(
2835
+ () => true,
2836
+ (error: unknown) => {
2837
+ if (
2838
+ error instanceof indexerTypes.NotStartedError &&
2839
+ this.closed
2840
+ ) {
2841
+ return true;
2842
+ }
2843
+ throw error;
2844
+ },
2845
+ )
2858
2846
  : true;
2859
2847
  } catch (error) {
2860
2848
  if (error instanceof indexerTypes.NotStartedError && this.closed) {
@@ -2974,9 +2962,7 @@ export class DocumentIndex<
2974
2962
  }
2975
2963
  const batchValues: Array<{
2976
2964
  id: indexerTypes.IdKey;
2977
- encodedValueParts: NonNullable<
2978
- ContextualPutOptions["encodedValueParts"]
2979
- >;
2965
+ encodedValueParts: NonNullable<ContextualPutOptions["encodedValueParts"]>;
2980
2966
  options?: { replace?: boolean };
2981
2967
  }> = [];
2982
2968
  for (const item of values) {
@@ -3341,6 +3327,29 @@ export class DocumentIndex<
3341
3327
  });
3342
3328
  }
3343
3329
 
3330
+ /**
3331
+ * The removal counterpart of `putWithContext`'s guard. Once this index is
3332
+ * closed there is nothing left to remove from, so a `NotStartedError` marks
3333
+ * the teardown window rather than a failure.
3334
+ *
3335
+ * This exists because the guard used to be one-legged: the read leg
3336
+ * (`Documents.getLocalIndexedContextForChange`) tolerated a closed index while
3337
+ * the remove leg did not, so a DELETE-carrying change still threw out of a
3338
+ * join in exactly the window the read guard was written for. A guard that
3339
+ * covers one direction of the same operation is worse than none, because it
3340
+ * reads as covered.
3341
+ *
3342
+ * Narrow in the same way as the read leg: only `NotStartedError`, and only
3343
+ * once this index is actually closed. An indexer that breaks while open still
3344
+ * throws.
3345
+ */
3346
+ private recoverClosedRemoval(error: unknown): void {
3347
+ if (error instanceof indexerTypes.NotStartedError && this.closed) {
3348
+ return;
3349
+ }
3350
+ throw error;
3351
+ }
3352
+
3344
3353
  public async delMany(keys: indexerTypes.IdKey[]): Promise<void> {
3345
3354
  if (keys.length === 0) {
3346
3355
  return;
@@ -3348,17 +3357,21 @@ export class DocumentIndex<
3348
3357
  for (const key of keys) {
3349
3358
  this.deleteResolvedCacheForKey(key);
3350
3359
  }
3351
- const delIdsNoReturn = (this.index as ExactDeleteIndex).delIdsNoReturn;
3352
- if (delIdsNoReturn) {
3353
- await delIdsNoReturn.call(this.index, keys);
3354
- return;
3355
- }
3356
- const delIds = (this.index as ExactDeleteIndex).delIds;
3357
- if (delIds) {
3358
- await delIds.call(this.index, keys);
3359
- return;
3360
+ try {
3361
+ const delIdsNoReturn = (this.index as ExactDeleteIndex).delIdsNoReturn;
3362
+ if (delIdsNoReturn) {
3363
+ await delIdsNoReturn.call(this.index, keys);
3364
+ return;
3365
+ }
3366
+ const delIds = (this.index as ExactDeleteIndex).delIds;
3367
+ if (delIds) {
3368
+ await delIds.call(this.index, keys);
3369
+ return;
3370
+ }
3371
+ await Promise.all(keys.map((key) => this.del(key)));
3372
+ } catch (error) {
3373
+ this.recoverClosedRemoval(error);
3360
3374
  }
3361
- await Promise.all(keys.map((key) => this.del(key)));
3362
3375
  }
3363
3376
 
3364
3377
  public clearResolvedCacheForKeys(keys: indexerTypes.IdKey[]): void {
@@ -3374,17 +3387,28 @@ export class DocumentIndex<
3374
3387
  for (const key of keys) {
3375
3388
  this.deleteResolvedCacheForKey(key);
3376
3389
  }
3377
- const delIdsNoReturn = (this.index as ExactDeleteIndex).delIdsNoReturn;
3378
- if (delIdsNoReturn) {
3379
- const result = delIdsNoReturn.call(this.index, keys);
3380
- return isPromiseLike(result) ? result.then(() => undefined) : undefined;
3381
- }
3382
- const delIds = (this.index as ExactDeleteIndex).delIds;
3383
- if (delIds) {
3384
- const result = delIds.call(this.index, keys);
3385
- return isPromiseLike(result) ? result.then(() => undefined) : undefined;
3390
+ // The native index answers synchronously and may throw synchronously, so
3391
+ // both shapes need the guard - see recoverClosedRemoval.
3392
+ const guard = (result: MaybePromise<unknown>): MaybePromise<void> =>
3393
+ isPromiseLike(result)
3394
+ ? result.then(
3395
+ () => undefined,
3396
+ (error: unknown) => this.recoverClosedRemoval(error),
3397
+ )
3398
+ : undefined;
3399
+ try {
3400
+ const delIdsNoReturn = (this.index as ExactDeleteIndex).delIdsNoReturn;
3401
+ if (delIdsNoReturn) {
3402
+ return guard(delIdsNoReturn.call(this.index, keys));
3403
+ }
3404
+ const delIds = (this.index as ExactDeleteIndex).delIds;
3405
+ if (delIds) {
3406
+ return guard(delIds.call(this.index, keys));
3407
+ }
3408
+ return guard(Promise.all(keys.map((key) => this.del(key))));
3409
+ } catch (error) {
3410
+ return this.recoverClosedRemoval(error);
3386
3411
  }
3387
- return Promise.all(keys.map((key) => this.del(key))).then(() => undefined);
3388
3412
  }
3389
3413
 
3390
3414
  public async getDetailed<
@@ -5457,7 +5481,7 @@ export class DocumentIndex<
5457
5481
  "Failed to collect sorted results from self. " + e?.message,
5458
5482
  );
5459
5483
  peerBufferMap.delete(peer);
5460
- }),
5484
+ }),
5461
5485
  );
5462
5486
  } else {
5463
5487
  // Fetch remotely
@@ -5823,9 +5847,7 @@ export class DocumentIndex<
5823
5847
  keepRemoteAlive
5824
5848
  ? [...peerBufferMap.keys()].filter((peer) => peer !== selfHash)
5825
5849
  : [...peerBufferMap.entries()]
5826
- .filter(
5827
- ([peer, buffer]) => peer !== selfHash && buffer.kept > 0,
5828
- )
5850
+ .filter(([peer, buffer]) => peer !== selfHash && buffer.kept > 0)
5829
5851
  .map(([peer]) => peer),
5830
5852
  );
5831
5853
  for (const peer of remoteIteratorPeersToClose) {
@@ -5859,8 +5881,9 @@ export class DocumentIndex<
5859
5881
  if (activeRemotePeers.has(peer)) {
5860
5882
  ids.push(remoteIteratorIds?.get(peer) ?? queryRequestCoerced.id);
5861
5883
  }
5862
- for (const staleRemoteIteratorId of
5863
- staleRemoteIteratorIds.get(peer) ?? []) {
5884
+ for (const staleRemoteIteratorId of staleRemoteIteratorIds.get(
5885
+ peer,
5886
+ ) ?? []) {
5864
5887
  if (!ids.some((id) => equals(id, staleRemoteIteratorId))) {
5865
5888
  ids.push(staleRemoteIteratorId);
5866
5889
  }