@peerbit/document 12.3.4 → 12.3.5-000e3f1

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
@@ -289,6 +289,7 @@ type QueryDetailedOptions<
289
289
  response: types.AbstractSearchResult,
290
290
  from: PublicSignKey,
291
291
  ) => void | Promise<void>;
292
+ onMissingResponses?: (error: MissingResponsesError) => void | Promise<void>;
292
293
  remote?: {
293
294
  from?: string[]; // if specified, only query these peers
294
295
  };
@@ -1338,7 +1339,23 @@ export class DocumentIndex<
1338
1339
  );
1339
1340
  }
1340
1341
  this.clearAllResultQueues();
1341
- await this.index?.stop?.();
1342
+ await this._resumableIterators.clearAll();
1343
+ if (this.iteratorKeepAliveTimers) {
1344
+ for (const timer of this.iteratorKeepAliveTimers.values()) {
1345
+ clearTimeout(timer);
1346
+ }
1347
+ this.iteratorKeepAliveTimers.clear();
1348
+ }
1349
+ try {
1350
+ await this.index?.stop?.();
1351
+ } catch (error) {
1352
+ // Be defensive during teardown: stopping an already-stopped index shouldn't
1353
+ // prevent closing the program and releasing timers/iterators.
1354
+ if (error instanceof indexerTypes.NotStartedError) {
1355
+ return closed;
1356
+ }
1357
+ throw error;
1358
+ }
1342
1359
  }
1343
1360
  return closed;
1344
1361
  }
@@ -1351,8 +1368,27 @@ export class DocumentIndex<
1351
1368
  this.handleDocumentChange,
1352
1369
  );
1353
1370
  this.clearAllResultQueues();
1354
- await this.index?.drop?.();
1355
- await this.index?.stop?.();
1371
+ await this._resumableIterators.clearAll();
1372
+ if (this.iteratorKeepAliveTimers) {
1373
+ for (const timer of this.iteratorKeepAliveTimers.values()) {
1374
+ clearTimeout(timer);
1375
+ }
1376
+ this.iteratorKeepAliveTimers.clear();
1377
+ }
1378
+ try {
1379
+ await this.index?.drop?.();
1380
+ } catch (error) {
1381
+ if (!(error instanceof indexerTypes.NotStartedError)) {
1382
+ throw error;
1383
+ }
1384
+ }
1385
+ try {
1386
+ await this.index?.stop?.();
1387
+ } catch (error) {
1388
+ if (!(error instanceof indexerTypes.NotStartedError)) {
1389
+ throw error;
1390
+ }
1391
+ }
1356
1392
  }
1357
1393
  return dropped;
1358
1394
  }
@@ -1367,17 +1403,24 @@ export class DocumentIndex<
1367
1403
  options?: Options,
1368
1404
  ): Promise<WithContext<I>>;
1369
1405
 
1370
- public async get<
1371
- Options extends GetOptions<T, I, D, Resolve>,
1372
- Resolve extends boolean | undefined = ExtractResolveFromOptions<Options>,
1373
- >(key: indexerTypes.Ideable | indexerTypes.IdKey, options?: Options) {
1374
- let deferred:
1375
- | DeferredPromise<WithIndexedContext<T, I> | WithContext<I>>
1376
- | undefined;
1377
-
1378
- // Normalize the id key early so listeners can use it
1379
- let idKey =
1380
- key instanceof indexerTypes.IdKey ? key : indexerTypes.toId(key);
1406
+ public async get<
1407
+ Options extends GetOptions<T, I, D, Resolve>,
1408
+ Resolve extends boolean | undefined = ExtractResolveFromOptions<Options>,
1409
+ >(key: indexerTypes.Ideable | indexerTypes.IdKey, options?: Options) {
1410
+ let deferred:
1411
+ | DeferredPromise<WithIndexedContext<T, I> | WithContext<I>>
1412
+ | undefined;
1413
+ let baseRemote:
1414
+ | RemoteQueryOptions<
1415
+ types.AbstractSearchRequest,
1416
+ types.AbstractSearchResult,
1417
+ D
1418
+ >
1419
+ | undefined;
1420
+
1421
+ // Normalize the id key early so listeners can use it
1422
+ let idKey =
1423
+ key instanceof indexerTypes.IdKey ? key : indexerTypes.toId(key);
1381
1424
 
1382
1425
  if (options?.waitFor) {
1383
1426
  // add change listener before query because we might get a concurrent change that matches the query,
@@ -1410,16 +1453,16 @@ export class DocumentIndex<
1410
1453
 
1411
1454
  let timeout = setTimeout(resolveUndefined, options.waitFor);
1412
1455
  this.events.addEventListener("close", resolveUndefined);
1413
- this.documentEvents.addEventListener("change", listener);
1414
- deferred.promise.then(cleanup);
1415
-
1416
- // Prepare remote options without mutating caller options
1417
- const baseRemote =
1418
- options?.remote === false
1419
- ? undefined
1420
- : typeof options?.remote === "object"
1421
- ? { ...options.remote }
1422
- : {};
1456
+ this.documentEvents.addEventListener("change", listener);
1457
+ deferred.promise.then(cleanup);
1458
+
1459
+ // Prepare remote options without mutating caller options
1460
+ baseRemote =
1461
+ options?.remote === false
1462
+ ? undefined
1463
+ : typeof options?.remote === "object"
1464
+ ? { ...options.remote }
1465
+ : {};
1423
1466
  if (baseRemote) {
1424
1467
  const waitPolicy = baseRemote.wait;
1425
1468
  if (
@@ -1455,16 +1498,20 @@ export class DocumentIndex<
1455
1498
  deferred!.resolve(first.value as any);
1456
1499
  }
1457
1500
  },
1458
- });
1501
+ });
1502
+ }
1459
1503
  }
1460
- }
1461
1504
 
1462
- const result = (await this.getDetailed(idKey, options))?.[0]?.results[0];
1505
+ const initialOptions = baseRemote
1506
+ ? ({ ...(options as any), remote: baseRemote } as Options)
1507
+ : options;
1508
+ const result =
1509
+ (await this.getDetailed(idKey, initialOptions))?.[0]?.results[0];
1463
1510
 
1464
- // if no results, and we have remote joining options, we wait for the timout and if there are joining peers we re-query
1465
- if (!result) {
1466
- return deferred?.promise;
1467
- } else if (deferred) {
1511
+ // if no results, and we have remote joining options, we wait for the timout and if there are joining peers we re-query
1512
+ if (!result) {
1513
+ return deferred?.promise;
1514
+ } else if (deferred) {
1468
1515
  deferred.resolve(undefined);
1469
1516
  }
1470
1517
  return result?.value;
@@ -1855,24 +1902,26 @@ export class DocumentIndex<
1855
1902
  const resolveFlag = resolvesDocuments(
1856
1903
  (fromQuery || query) as AnyIterationRequest,
1857
1904
  );
1858
- prevQueued = {
1859
- from,
1860
- queue: [],
1861
- timeout: setTimeout(() => {
1862
- this._resultQueue.delete(query.idString);
1863
- }, 6e4),
1864
- keptInIndex: kept,
1865
- fromQuery: (fromQuery || query) as
1866
- | types.SearchRequest
1867
- | types.SearchRequestIndexed
1868
- | types.IterationRequest,
1869
- resolveResults: resolveFlag,
1870
- };
1871
- if (
1872
- fromQuery instanceof types.IterationRequest &&
1873
- fromQuery.pushUpdates
1874
- ) {
1875
- prevQueued.pushMode = fromQuery.pushUpdates;
1905
+ prevQueued = {
1906
+ from,
1907
+ queue: [],
1908
+ timeout: setTimeout(() => {
1909
+ this._resultQueue.delete(query.idString);
1910
+ }, 6e4),
1911
+ keptInIndex: kept,
1912
+ fromQuery: (fromQuery || query) as
1913
+ | types.SearchRequest
1914
+ | types.SearchRequestIndexed
1915
+ | types.IterationRequest,
1916
+ resolveResults: resolveFlag,
1917
+ };
1918
+ // Don't keep Node alive just to GC old remote iterator state.
1919
+ prevQueued.timeout.unref?.();
1920
+ if (
1921
+ fromQuery instanceof types.IterationRequest &&
1922
+ fromQuery.pushUpdates
1923
+ ) {
1924
+ prevQueued.pushMode = fromQuery.pushUpdates;
1876
1925
  }
1877
1926
  this._resultQueue.set(query.idString, prevQueued);
1878
1927
  }
@@ -1970,17 +2019,19 @@ export class DocumentIndex<
1970
2019
  string,
1971
2020
  ReturnType<typeof setTimeout>
1972
2021
  >());
1973
- const timer = setTimeout(() => {
1974
- timers.delete(idString);
1975
- const queued = this._resultQueue.get(idString);
1976
- if (queued) {
1977
- clearTimeout(queued.timeout);
1978
- this._resultQueue.delete(idString);
1979
- }
1980
- this._resumableIterators.close({ idString });
1981
- }, delay);
1982
- timers.set(idString, timer);
1983
- }
2022
+ const timer = setTimeout(() => {
2023
+ timers.delete(idString);
2024
+ const queued = this._resultQueue.get(idString);
2025
+ if (queued) {
2026
+ clearTimeout(queued.timeout);
2027
+ this._resultQueue.delete(idString);
2028
+ }
2029
+ this._resumableIterators.close({ idString });
2030
+ }, delay);
2031
+ // This is a best-effort cleanup timer; it should not keep Node alive.
2032
+ timer.unref?.();
2033
+ timers.set(idString, timer);
2034
+ }
1984
2035
 
1985
2036
  private cancelIteratorKeepAlive(idString: string) {
1986
2037
  const timers = this.iteratorKeepAliveTimers;
@@ -2213,30 +2264,35 @@ export class DocumentIndex<
2213
2264
  queryRequest: R,
2214
2265
  options?: QueryDetailedOptions<T, I, D, boolean | undefined>,
2215
2266
  fetchFirstForRemote?: Set<string>,
2216
- ): Promise<types.Results<RT>[]> {
2217
- const local = typeof options?.local === "boolean" ? options?.local : true;
2218
- let remote:
2219
- | RemoteQueryOptions<
2220
- types.AbstractSearchRequest,
2221
- types.AbstractSearchResult,
2222
- D
2223
- >
2224
- | undefined = undefined;
2225
- if (typeof options?.remote === "boolean") {
2226
- if (options?.remote) {
2227
- remote = {};
2267
+ ): Promise<types.Results<RT>[]> {
2268
+ const local = typeof options?.local === "boolean" ? options?.local : true;
2269
+ let remote:
2270
+ | RemoteQueryOptions<
2271
+ types.AbstractSearchRequest,
2272
+ types.AbstractSearchResult,
2273
+ D
2274
+ >
2275
+ | undefined = undefined;
2276
+ if (typeof options?.remote === "boolean") {
2277
+ remote = options.remote ? {} : undefined;
2228
2278
  } else {
2229
- remote = undefined;
2279
+ remote = options?.remote || {};
2230
2280
  }
2231
- } else {
2232
- remote = options?.remote || {};
2233
- }
2234
2281
  if (remote && remote.priority == null) {
2235
2282
  // give queries higher priority than other "normal" data activities
2236
2283
  // without this, we might have a scenario that a peer joina network with large amount of data to be synced, but can not query anything before that is done
2237
2284
  // this will lead to bad UX as you usually want to list/expore whats going on before doing any replication work
2238
2285
  remote.priority = 2;
2239
2286
  }
2287
+ if (remote && remote.timeout == null && options?.remote) {
2288
+ const waitPolicy =
2289
+ typeof options.remote === "object" ? options.remote.wait : undefined;
2290
+ const waitTimeout =
2291
+ typeof waitPolicy === "object" ? waitPolicy.timeout : undefined;
2292
+ if (waitTimeout != null) {
2293
+ remote.timeout = waitTimeout;
2294
+ }
2295
+ }
2240
2296
 
2241
2297
  if (!local && !remote) {
2242
2298
  throw new Error(
@@ -2266,19 +2322,76 @@ export class DocumentIndex<
2266
2322
  throw new Error("Unexpected");
2267
2323
  }
2268
2324
 
2269
- const replicatorGroups = options?.remote?.from
2270
- ? options?.remote?.from
2271
- : await this._log.getCover(remote.domain ?? { args: undefined }, {
2272
- roleAge: remote.minAge,
2273
- eager: remote.reach?.eager,
2274
- reachableOnly: !!remote.wait, // when we want to merge joining we can ignore pending to be online peers and instead consider them once they become online
2275
- signal: options?.signal,
2276
- });
2325
+ const coverProps = remote.domain ?? { args: undefined };
2326
+ const isDefaultDomainArgs =
2327
+ !("range" in coverProps) &&
2328
+ (!("args" in coverProps) || (coverProps as any).args == null);
2329
+
2330
+ let replicatorGroups = options?.remote?.from
2331
+ ? options?.remote?.from
2332
+ : await this._log.getCover(coverProps, {
2333
+ roleAge: remote.minAge,
2334
+ eager: remote.reach?.eager,
2335
+ reachableOnly: !!remote.wait, // when we want to merge joining we can ignore pending to be online peers and instead consider them once they become online
2336
+ signal: options?.signal,
2337
+ });
2338
+
2339
+ // Cold start: cover can be temporarily empty/self-only while replication metadata
2340
+ // converges. For remote search, it's sometimes better to at least try currently
2341
+ // connected peers, but only if we have evidence that a remote replicator exists.
2342
+ if (!options?.remote?.from && isDefaultDomainArgs) {
2343
+ const selfHash = this.node.identity.publicKey.hashcode();
2344
+ const remoteCount = replicatorGroups.filter((h) => h !== selfHash).length;
2345
+ if (remoteCount === 0) {
2346
+ const waitEnabled = Boolean(remote.wait);
2347
+ const coverIsSelfOnly =
2348
+ replicatorGroups.length === 1 && replicatorGroups[0] === selfHash;
2349
+
2350
+ // If the cover is explicitly empty (no shards), don't override it unless
2351
+ // the caller requested waiting for joins (e.g. get(waitFor)).
2352
+ if (!waitEnabled && !coverIsSelfOnly) {
2353
+ // no-op
2354
+ } else {
2355
+ let hasKnownRemoteReplicator = false;
2356
+ if (!waitEnabled) {
2357
+ try {
2358
+ const replicators = await this._log.getReplicators();
2359
+ for (const hash of replicators.keys()) {
2360
+ if (hash !== selfHash) {
2361
+ hasKnownRemoteReplicator = true;
2362
+ break;
2363
+ }
2364
+ }
2365
+ } catch {
2366
+ // Best-effort only.
2367
+ }
2368
+ }
2369
+
2370
+ if (waitEnabled || hasKnownRemoteReplicator) {
2371
+ const peerMap: Map<string, unknown> | undefined = (this.node.services
2372
+ .pubsub as any)?.peers;
2373
+ if (peerMap?.keys) {
2374
+ const extra: string[] = [];
2375
+ for (const hash of peerMap.keys()) {
2376
+ if (!hash || hash === selfHash) continue;
2377
+ extra.push(hash);
2378
+ if (extra.length >= 8) break;
2379
+ }
2380
+ if (extra.length > 0) {
2381
+ replicatorGroups = [
2382
+ ...new Set([...replicatorGroups, ...extra]),
2383
+ ];
2384
+ }
2385
+ }
2386
+ }
2387
+ }
2388
+ }
2389
+ }
2277
2390
 
2278
- if (replicatorGroups) {
2279
- const responseHandler = async (
2280
- results: {
2281
- response: types.AbstractSearchResult;
2391
+ if (replicatorGroups) {
2392
+ const responseHandler = async (
2393
+ results: {
2394
+ response: types.AbstractSearchResult;
2282
2395
  from?: PublicSignKey;
2283
2396
  }[],
2284
2397
  ) => {
@@ -2403,6 +2516,9 @@ export class DocumentIndex<
2403
2516
  } catch (error) {
2404
2517
  if (error instanceof MissingResponsesError) {
2405
2518
  warn("Did not reciveve responses from all shard");
2519
+ if (options?.onMissingResponses) {
2520
+ await options.onMissingResponses(error);
2521
+ }
2406
2522
  if (remote?.throwOnMissing) {
2407
2523
  throw error;
2408
2524
  }
@@ -2464,18 +2580,17 @@ export class DocumentIndex<
2464
2580
  options?: O,
2465
2581
  ): Promise<ValueTypeFromRequest<Resolve, T, I>[]> {
2466
2582
  // Set fetch to search size, or max value (default to max u32 (4294967295))
2467
- const coercedRequest = coerceQuery(
2468
- queryRequest,
2469
- options,
2470
- this.compatibility,
2471
- );
2472
- coercedRequest.fetch = coercedRequest.fetch ?? 0xffffffff;
2583
+ const coercedRequest = coerceQuery(
2584
+ queryRequest,
2585
+ options,
2586
+ this.compatibility,
2587
+ );
2588
+ coercedRequest.fetch = coercedRequest.fetch ?? 0xffffffff;
2473
2589
 
2474
- // So that the iterator is pre-fetching the right amount of entries
2475
- const iterator = this.iterate<Resolve>(coercedRequest, options);
2590
+ // Use an iterator so large results respect message size limits.
2591
+ const iterator = this.iterate<Resolve>(coercedRequest, options);
2476
2592
 
2477
- // So that this call will not do any remote requests
2478
- const allResults: ValueTypeFromRequest<Resolve, T, I>[] = [];
2593
+ const allResults: ValueTypeFromRequest<Resolve, T, I>[] = [];
2479
2594
 
2480
2595
  while (
2481
2596
  iterator.done() !== true &&
@@ -2876,37 +2991,43 @@ export class DocumentIndex<
2876
2991
 
2877
2992
  if (typeof options?.remote === "object") {
2878
2993
  let waitForTime: number | undefined = undefined;
2994
+ const waitPolicy =
2995
+ typeof options.remote.wait === "object"
2996
+ ? options.remote.wait
2997
+ : undefined;
2998
+ const waitBehavior: WaitBehavior = waitPolicy?.behavior ?? "keep-open";
2879
2999
  if (options.remote.wait) {
2880
- let t0 = +new Date();
2881
-
2882
3000
  waitForTime =
2883
3001
  typeof options.remote.wait === "boolean"
2884
3002
  ? DEFAULT_TIMEOUT
2885
3003
  : (options.remote.wait.timeout ?? DEFAULT_TIMEOUT);
2886
- let setDoneIfTimeout = false;
2887
- maybeSetDone = () => {
2888
- if (t0 + waitForTime! < +new Date()) {
2889
- cleanup();
2890
- done = true;
2891
- } else {
2892
- setDoneIfTimeout = true;
2893
- }
2894
- };
2895
- unsetDone = () => {
2896
- setDoneIfTimeout = false;
2897
- done = false;
2898
- };
2899
- let timeout = setTimeout(() => {
2900
- if (setDoneIfTimeout) {
2901
- cleanup();
2902
- done = true;
2903
- }
2904
- }, waitForTime);
3004
+ if (waitBehavior === "keep-open") {
3005
+ let t0 = +new Date();
3006
+ let setDoneIfTimeout = false;
3007
+ maybeSetDone = () => {
3008
+ if (t0 + waitForTime! < +new Date()) {
3009
+ cleanup();
3010
+ done = true;
3011
+ } else {
3012
+ setDoneIfTimeout = true;
3013
+ }
3014
+ };
3015
+ unsetDone = () => {
3016
+ setDoneIfTimeout = false;
3017
+ done = false;
3018
+ };
3019
+ let timeout = setTimeout(() => {
3020
+ if (setDoneIfTimeout) {
3021
+ cleanup();
3022
+ done = true;
3023
+ }
3024
+ }, waitForTime);
2905
3025
 
2906
- cleanup = () => {
2907
- this.clearResultsQueue(queryRequestCoerced);
2908
- clearTimeout(timeout);
2909
- };
3026
+ cleanup = () => {
3027
+ this.clearResultsQueue(queryRequestCoerced);
3028
+ clearTimeout(timeout);
3029
+ };
3030
+ }
2910
3031
  }
2911
3032
 
2912
3033
  if (options.remote.reach?.discover) {
@@ -2933,10 +3054,6 @@ export class DocumentIndex<
2933
3054
  options.remote.reach.eager = true; // include the results from the discovered peer even if it is not mature
2934
3055
  }
2935
3056
 
2936
- const waitPolicy =
2937
- typeof options.remote.wait === "object"
2938
- ? options.remote.wait
2939
- : undefined;
2940
3057
  if (
2941
3058
  waitPolicy?.behavior === "block" &&
2942
3059
  (waitPolicy.until ?? "any") === "any"
@@ -2961,6 +3078,7 @@ export class DocumentIndex<
2961
3078
  ): Promise<boolean> => {
2962
3079
  await warmupPromise;
2963
3080
  let hasMore = false;
3081
+ let missingResponses = false;
2964
3082
  const discoverTargets =
2965
3083
  typeof options?.remote === "object"
2966
3084
  ? options.remote.reach?.discover
@@ -3096,10 +3214,38 @@ export class DocumentIndex<
3096
3214
  );
3097
3215
  }
3098
3216
  },
3217
+ onMissingResponses: (error) => {
3218
+ missingResponses = true;
3219
+ const missingGroups = (error as MissingResponsesError & {
3220
+ missingGroups?: string[][];
3221
+ }).missingGroups;
3222
+ if (!missingGroups?.length) {
3223
+ return;
3224
+ }
3225
+
3226
+ const selfHash = this.node.identity.publicKey.hashcode();
3227
+ for (const group of missingGroups) {
3228
+ const target = group.find((hash) => {
3229
+ if (!hash || hash === selfHash) return false;
3230
+ const attempts = missingResponseRetryAttempts.get(hash) ?? 0;
3231
+ return attempts < maxMissingResponseRetryAttempts;
3232
+ });
3233
+ if (!target) continue;
3234
+ pendingMissingResponseRetryPeers.add(target);
3235
+ missingResponseRetryAttempts.set(
3236
+ target,
3237
+ (missingResponseRetryAttempts.get(target) ?? 0) + 1,
3238
+ );
3239
+ }
3240
+ },
3099
3241
  },
3100
3242
  fetchOptions?.fetchedFirstForRemote,
3101
3243
  );
3102
3244
 
3245
+ if (missingResponses) {
3246
+ hasMore = true;
3247
+ unsetDone();
3248
+ }
3103
3249
  if (!hasMore) {
3104
3250
  maybeSetDone();
3105
3251
  }
@@ -3126,6 +3272,17 @@ export class DocumentIndex<
3126
3272
  return fetchPromise;
3127
3273
  }
3128
3274
 
3275
+ if (pendingMissingResponseRetryPeers.size > 0) {
3276
+ const retryTargets = [...pendingMissingResponseRetryPeers];
3277
+ pendingMissingResponseRetryPeers.clear();
3278
+ fetchPromise = fetchFirst(n, {
3279
+ from: retryTargets,
3280
+ // retries for missing groups should not be suppressed by first-fetch dedupe
3281
+ fetchedFirstForRemote: undefined,
3282
+ });
3283
+ return fetchPromise;
3284
+ }
3285
+
3129
3286
  const promises: Promise<any>[] = [];
3130
3287
  let resultsLeft = 0;
3131
3288
 
@@ -3533,32 +3690,27 @@ export class DocumentIndex<
3533
3690
  done = true;
3534
3691
  };
3535
3692
 
3536
- let close = async () => {
3537
- cleanupAndDone();
3538
-
3539
- // send close to remote
3540
- const closeRequest = new types.CloseIteratorRequest({
3541
- id: queryRequestCoerced.id,
3542
- });
3543
- const promises: Promise<any>[] = [];
3693
+ let close = async () => {
3694
+ cleanupAndDone();
3544
3695
 
3545
- for (const [peer, buffer] of peerBufferMap) {
3546
- if (buffer.kept === 0) {
3547
- peerBufferMap.delete(peer);
3548
- continue;
3549
- }
3550
- if (peer !== this.node.identity.publicKey.hashcode()) {
3551
- // Close remote
3552
- promises.push(
3696
+ // send close to remote (only peers that actually served results / had an active buffer)
3697
+ const closeRequest = new types.CloseIteratorRequest({
3698
+ id: queryRequestCoerced.id,
3699
+ });
3700
+ const selfHash = this.node.identity.publicKey.hashcode();
3701
+ const remotePeers = [...peerBufferMap.entries()]
3702
+ .filter(([peer, buffer]) => peer !== selfHash && buffer.kept > 0)
3703
+ .map(([peer]) => peer);
3704
+ peerBufferMap.clear();
3705
+ await Promise.allSettled(
3706
+ remotePeers.map((peer) =>
3553
3707
  this._query.send(closeRequest, {
3554
3708
  ...options,
3555
3709
  mode: new SilentDelivery({ to: [peer], redundancy: 1 }),
3556
3710
  }),
3557
- );
3558
- }
3559
- }
3560
- await Promise.all(promises);
3561
- };
3711
+ ),
3712
+ );
3713
+ };
3562
3714
  options?.signal && options.signal.addEventListener("abort", close);
3563
3715
 
3564
3716
  let doneFn = () => {
@@ -3568,6 +3720,9 @@ export class DocumentIndex<
3568
3720
  let joinListener: (() => void) | undefined;
3569
3721
 
3570
3722
  let fetchedFirstForRemote: Set<string> | undefined = undefined;
3723
+ const pendingMissingResponseRetryPeers = new Set<string>();
3724
+ const missingResponseRetryAttempts = new Map<string, number>();
3725
+ const maxMissingResponseRetryAttempts = 2;
3571
3726
 
3572
3727
  let updateDeferred: ReturnType<typeof pDefer> | undefined;
3573
3728
  const onLateResultsQueue =
@@ -4144,13 +4299,24 @@ export class DocumentIndex<
4144
4299
  };
4145
4300
  }
4146
4301
 
4147
- if (typeof options?.remote === "object" && options?.remote.wait) {
4302
+ const remoteConfig =
4303
+ options && typeof options.remote === "object" ? options.remote : undefined;
4304
+ const remoteWaitPolicy =
4305
+ remoteConfig && typeof remoteConfig.wait === "object"
4306
+ ? remoteConfig.wait
4307
+ : undefined;
4308
+ const remoteWaitBehavior: WaitBehavior =
4309
+ remoteWaitPolicy?.behavior ?? "keep-open";
4310
+ const keepRemoteWaitOpen =
4311
+ !!remoteConfig?.wait &&
4312
+ remoteWaitBehavior === "keep-open";
4313
+
4314
+ if (keepRemoteWaitOpen) {
4148
4315
  // was used to account for missed results when a peer joins; omitted in this minimal handler
4149
4316
 
4150
4317
  updateDeferred = pDefer<void>();
4151
4318
 
4152
- const waitForTime =
4153
- typeof options.remote.wait === "object" && options.remote.wait.timeout;
4319
+ const waitForTime = remoteWaitPolicy?.timeout;
4154
4320
 
4155
4321
  const prevMaybeSetDone = maybeSetDone;
4156
4322
  maybeSetDone = () => {
@@ -4167,7 +4333,7 @@ export class DocumentIndex<
4167
4333
  fetchedFirstForRemote = new Set<string>();
4168
4334
  joinListener = this.createReplicatorJoinListener({
4169
4335
  signal: ensureController().signal,
4170
- eager: options.remote.reach?.eager,
4336
+ eager: remoteConfig?.reach?.eager,
4171
4337
  onPeer: async (pk) => {
4172
4338
  if (done) return;
4173
4339
  const hash = pk.hashcode();
@@ -4240,8 +4406,7 @@ export class DocumentIndex<
4240
4406
  }
4241
4407
  };
4242
4408
  }
4243
- const remoteWaitActive =
4244
- typeof options?.remote === "object" && !!options.remote.wait;
4409
+ const remoteWaitActive = keepRemoteWaitOpen;
4245
4410
 
4246
4411
  const waitForUpdateAndResetDeferred = async () => {
4247
4412
  if (remoteWaitActive) {