@peerbit/document 13.0.22 → 13.0.24

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
@@ -2300,8 +2300,7 @@ export class DocumentIndex<
2300
2300
  onPeer: (pk: PublicSignKey) => Promise<void> | void;
2301
2301
  }): () => void {
2302
2302
  const active = new Set<string>();
2303
- const listener = async (e: { detail: PublicSignKey }) => {
2304
- const pk = e.detail;
2303
+ const handlePeer = async (pk: PublicSignKey) => {
2305
2304
  const hash = pk.hashcode();
2306
2305
  if (hash === this.node.identity.publicKey.hashcode()) return;
2307
2306
  if (params.signal?.aborted) return;
@@ -2323,8 +2322,35 @@ export class DocumentIndex<
2323
2322
  }
2324
2323
  };
2325
2324
 
2326
- this._query.events.addEventListener("join", listener);
2327
- return () => this._query.events.removeEventListener("join", listener);
2325
+ const onQueryJoin = (e: { detail: PublicSignKey }) => {
2326
+ void handlePeer(e.detail);
2327
+ };
2328
+ const onReplicatorEvent = (e: {
2329
+ detail: { publicKey: PublicSignKey };
2330
+ }) => {
2331
+ void handlePeer(e.detail.publicKey);
2332
+ };
2333
+
2334
+ this._query.events.addEventListener("join", onQueryJoin);
2335
+ this._log?.events?.addEventListener("replicator:join", onReplicatorEvent);
2336
+ this._log?.events?.addEventListener("replicator:mature", onReplicatorEvent);
2337
+ this._log?.events?.addEventListener("replication:change", onReplicatorEvent);
2338
+
2339
+ return () => {
2340
+ this._query.events.removeEventListener("join", onQueryJoin);
2341
+ this._log?.events?.removeEventListener(
2342
+ "replicator:join",
2343
+ onReplicatorEvent,
2344
+ );
2345
+ this._log?.events?.removeEventListener(
2346
+ "replicator:mature",
2347
+ onReplicatorEvent,
2348
+ );
2349
+ this._log?.events?.removeEventListener(
2350
+ "replication:change",
2351
+ onReplicatorEvent,
2352
+ );
2353
+ };
2328
2354
  }
2329
2355
 
2330
2356
  processCloseIteratorRequest(
@@ -2992,6 +3018,18 @@ export class DocumentIndex<
2992
3018
  });
2993
3019
 
2994
3020
  let fetchPromise: Promise<any> | undefined = undefined;
3021
+ let fetchesInFlight = 0;
3022
+ const trackFetch = <T>(promise: Promise<T>): Promise<T> => {
3023
+ fetchesInFlight++;
3024
+ return promise.finally(() => {
3025
+ fetchesInFlight--;
3026
+ });
3027
+ };
3028
+ const setFetchPromise = <T>(promise: Promise<T>): Promise<T> => {
3029
+ const tracked = trackFetch(promise);
3030
+ fetchPromise = tracked;
3031
+ return tracked;
3032
+ };
2995
3033
  const peerBufferMap: Map<
2996
3034
  string,
2997
3035
  {
@@ -3378,19 +3416,19 @@ export class DocumentIndex<
3378
3416
 
3379
3417
  if (!first) {
3380
3418
  first = true;
3381
- fetchPromise = fetchFirst(n);
3382
- return fetchPromise;
3419
+ return setFetchPromise(fetchFirst(n));
3383
3420
  }
3384
3421
 
3385
3422
  if (pendingMissingResponseRetryPeers.size > 0) {
3386
3423
  const retryTargets = [...pendingMissingResponseRetryPeers];
3387
3424
  pendingMissingResponseRetryPeers.clear();
3388
- fetchPromise = fetchFirst(n, {
3389
- from: retryTargets,
3390
- // retries for missing groups should not be suppressed by first-fetch dedupe
3391
- fetchedFirstForRemote: undefined,
3392
- });
3393
- return fetchPromise;
3425
+ return setFetchPromise(
3426
+ fetchFirst(n, {
3427
+ from: retryTargets,
3428
+ // retries for missing groups should not be suppressed by first-fetch dedupe
3429
+ fetchedFirstForRemote: undefined,
3430
+ }),
3431
+ );
3394
3432
  }
3395
3433
 
3396
3434
  const promises: Promise<any>[] = [];
@@ -3696,9 +3734,28 @@ export class DocumentIndex<
3696
3734
  resultsLeft += peerBufferMap.get(peer)?.kept || 0;
3697
3735
  }
3698
3736
  }
3699
- return (fetchPromise = Promise.all(promises).then(() => {
3700
- return resultsLeft === 0; // 0 results left to fetch and 0 pending results
3701
- }));
3737
+ return setFetchPromise(
3738
+ Promise.all(promises).then(async () => {
3739
+ if (keepRemoteWaitOpen && resultsLeft === 0) {
3740
+ const bufferedAfterCollect = peerBuffers().length;
3741
+ const hasObservedResults = visited.size > 0;
3742
+ // When the initial cover drains before satisfying the requested batch,
3743
+ // probe any already-known replicators we have not queried yet instead of
3744
+ // waiting only for a future join/update event.
3745
+ if (
3746
+ hasObservedResults &&
3747
+ bufferedAfterCollect < n &&
3748
+ joinFetchesInFlight === 0
3749
+ ) {
3750
+ const recoveredLatePeers = await fetchLateJoinPeers();
3751
+ if (recoveredLatePeers) {
3752
+ return false;
3753
+ }
3754
+ }
3755
+ }
3756
+ return resultsLeft === 0; // 0 results left to fetch and 0 pending results
3757
+ }),
3758
+ );
3702
3759
  };
3703
3760
 
3704
3761
  const next = async (n: number) => {
@@ -3853,8 +3910,10 @@ export class DocumentIndex<
3853
3910
  const pendingMissingResponseRetryPeers = new Set<string>();
3854
3911
  const missingResponseRetryAttempts = new Map<string, number>();
3855
3912
  const maxMissingResponseRetryAttempts = 2;
3913
+ let joinFetchesInFlight = 0;
3856
3914
 
3857
3915
  let updateDeferred: ReturnType<typeof pDefer> | undefined;
3916
+ const updateWaiters = new Set<ReturnType<typeof pDefer<void>>>();
3858
3917
  const onLateResultsQueue =
3859
3918
  options?.outOfOrder?.mode === "queue" &&
3860
3919
  typeof options?.outOfOrder?.handle === "function"
@@ -3970,9 +4029,20 @@ export class DocumentIndex<
3970
4029
  runNotify(reason);
3971
4030
  }
3972
4031
  updateDeferred?.resolve();
4032
+ for (const waiter of updateWaiters) {
4033
+ waiter.resolve();
4034
+ }
4035
+ updateWaiters.clear();
3973
4036
  };
3974
4037
  const _waitForUpdate = () =>
3975
4038
  updateDeferred ? updateDeferred.promise : Promise.resolve();
4039
+ const waitForAnyUpdate = () => {
4040
+ const waiter = pDefer<void>();
4041
+ updateWaiters.add(waiter);
4042
+ return waiter.promise.finally(() => {
4043
+ updateWaiters.delete(waiter);
4044
+ });
4045
+ };
3976
4046
 
3977
4047
  // ---------------- Live updates wiring (sorted-only with optional filter) ----------------
3978
4048
  const updateCallbacks = updateCallbacksRaw;
@@ -4435,11 +4505,175 @@ export class DocumentIndex<
4435
4505
  const keepRemoteWaitOpen =
4436
4506
  !!remoteConfig?.wait &&
4437
4507
  remoteWaitBehavior === "keep-open";
4508
+ let fetchLateJoinPeers = async (
4509
+ _candidateHashes?: Iterable<string>,
4510
+ _candidateKeys?: Map<string, PublicSignKey>,
4511
+ ) => false;
4512
+
4513
+ if (keepRemoteWaitOpen) {
4514
+ // was used to account for missed results when a peer joins; omitted in this minimal handler
4515
+
4516
+ updateDeferred = pDefer<void>();
4517
+ const lateJoinFetchesInFlight = new Set<string>();
4518
+ const shouldIgnoreLateJoinFetchError = (error: unknown) => {
4519
+ if (
4520
+ this.closed ||
4521
+ ensureController().signal.aborted ||
4522
+ error instanceof ClosedError ||
4523
+ error instanceof AbortError
4524
+ ) {
4525
+ return true;
4526
+ }
4527
+ return (
4528
+ error instanceof Error &&
4529
+ error.message.trim().toLowerCase() === "closed"
4530
+ );
4531
+ };
4438
4532
 
4439
- if (keepRemoteWaitOpen) {
4440
- // was used to account for missed results when a peer joins; omitted in this minimal handler
4533
+ fetchLateJoinPeers = async (
4534
+ candidateHashes?: Iterable<string>,
4535
+ candidateKeys?: Map<string, PublicSignKey>,
4536
+ ) => {
4537
+ if (totalFetchedCounter === 0) {
4538
+ return false;
4539
+ }
4540
+ if (this.closed || ensureController().signal.aborted) {
4541
+ return false;
4542
+ }
4441
4543
 
4442
- updateDeferred = pDefer<void>();
4544
+ if (done) {
4545
+ unsetDone();
4546
+ }
4547
+
4548
+ const selfHash = this.node.identity.publicKey.hashcode();
4549
+ const knownCandidateKeys = candidateKeys
4550
+ ? new Map(candidateKeys)
4551
+ : new Map<string, PublicSignKey>();
4552
+ let hashes: string[];
4553
+ try {
4554
+ hashes = candidateHashes
4555
+ ? [...candidateHashes]
4556
+ : [...(await this._log.getReplicators()).keys()];
4557
+ } catch (error) {
4558
+ if (shouldIgnoreLateJoinFetchError(error)) {
4559
+ return false;
4560
+ }
4561
+ throw error;
4562
+ }
4563
+ let missing = hashes.filter((hash) => {
4564
+ if (hash === selfHash) return false;
4565
+ if (peerBufferMap.has(hash)) return false;
4566
+ if (fetchedFirstForRemote!.has(hash)) return false;
4567
+ if (lateJoinFetchesInFlight.has(hash)) return false;
4568
+ return true;
4569
+ });
4570
+ if (missing.length === 0 && !candidateHashes) {
4571
+ const connectedPeers = (this.node.services.pubsub as any)?.peers as
4572
+ | Map<string, unknown>
4573
+ | undefined;
4574
+ if (connectedPeers?.size) {
4575
+ const connectedCandidates = [...connectedPeers.keys()].filter(
4576
+ (hash) =>
4577
+ hash !== selfHash &&
4578
+ !hashes.includes(hash) &&
4579
+ !peerBufferMap.has(hash) &&
4580
+ !fetchedFirstForRemote!.has(hash) &&
4581
+ !lateJoinFetchesInFlight.has(hash),
4582
+ );
4583
+ if (connectedCandidates.length > 0) {
4584
+ const discovered = await Promise.all(
4585
+ connectedCandidates.slice(0, 8).map(async (hash) => {
4586
+ const pk = await this.node.services.pubsub.getPublicKey(hash);
4587
+ if (!pk) {
4588
+ return undefined;
4589
+ }
4590
+ try {
4591
+ await this._log.waitForReplicator(pk, {
4592
+ signal: ensureController().signal,
4593
+ eager: true,
4594
+ timeout: 250,
4595
+ });
4596
+ knownCandidateKeys.set(hash, pk);
4597
+ return hash;
4598
+ } catch {
4599
+ return undefined;
4600
+ }
4601
+ }),
4602
+ );
4603
+ missing = discovered.filter((hash): hash is string => !!hash);
4604
+ }
4605
+ }
4606
+ }
4607
+ if (missing.length === 0) {
4608
+ return false;
4609
+ }
4610
+
4611
+ missing.forEach((hash) => lateJoinFetchesInFlight.add(hash));
4612
+ joinFetchesInFlight += missing.length;
4613
+
4614
+ try {
4615
+ const unresolved = missing.filter((hash) => {
4616
+ if (peerBufferMap.has(hash)) return false;
4617
+ if (fetchedFirstForRemote!.has(hash)) return false;
4618
+ return true;
4619
+ });
4620
+
4621
+ if (unresolved.length === 0) {
4622
+ return false;
4623
+ }
4624
+
4625
+ const lateJoinFetchPromise = trackFetch(
4626
+ fetchFirst(totalFetchedCounter, {
4627
+ from: unresolved,
4628
+ fetchedFirstForRemote,
4629
+ }),
4630
+ );
4631
+ try {
4632
+ await lateJoinFetchPromise;
4633
+ } catch (error) {
4634
+ if (shouldIgnoreLateJoinFetchError(error)) {
4635
+ return false;
4636
+ }
4637
+ throw error;
4638
+ }
4639
+ for (const hash of unresolved) {
4640
+ if (!peerBufferMap.has(hash)) {
4641
+ fetchedFirstForRemote?.delete(hash);
4642
+ }
4643
+ }
4644
+
4645
+ if (onLateResultsQueue || onLateResultsDrop) {
4646
+ for (const hash of unresolved) {
4647
+ const pending = peerBufferMap.get(hash)?.buffer;
4648
+ if (!pending || pending.length === 0) {
4649
+ continue;
4650
+ }
4651
+
4652
+ const peer = knownCandidateKeys.get(hash);
4653
+ if (lastDeliveredIndexed) {
4654
+ const delivered = lastDeliveredIndexed;
4655
+ const lateItems = pending.filter(
4656
+ (item) => compareIndexed(item.indexed, delivered) < 0,
4657
+ );
4658
+ if (lateItems.length > 0) {
4659
+ notifyLateResults?.(lateItems.length, peer, lateItems);
4660
+ }
4661
+ } else {
4662
+ notifyLateResults?.(pending.length, peer, pending);
4663
+ }
4664
+ }
4665
+ }
4666
+
4667
+ if (!pendingBatchReason) {
4668
+ pendingBatchReason = "join";
4669
+ }
4670
+ signalUpdate("join");
4671
+ return true;
4672
+ } finally {
4673
+ missing.forEach((hash) => lateJoinFetchesInFlight.delete(hash));
4674
+ joinFetchesInFlight -= missing.length;
4675
+ }
4676
+ };
4443
4677
 
4444
4678
  const waitForTime = remoteWaitPolicy?.timeout;
4445
4679
 
@@ -4462,36 +4696,7 @@ export class DocumentIndex<
4462
4696
  onPeer: async (pk) => {
4463
4697
  if (done) return;
4464
4698
  const hash = pk.hashcode();
4465
- await fetchPromise; // ensure fetches in flight are done
4466
- if (peerBufferMap.has(hash)) return;
4467
- if (fetchedFirstForRemote!.has(hash)) return;
4468
- if (totalFetchedCounter > 0) {
4469
- fetchPromise = fetchFirst(totalFetchedCounter, {
4470
- from: [hash],
4471
- fetchedFirstForRemote,
4472
- });
4473
- await fetchPromise;
4474
- if (onLateResultsQueue || onLateResultsDrop) {
4475
- const pending = peerBufferMap.get(hash)?.buffer;
4476
- if (pending && pending.length > 0) {
4477
- if (lastDeliveredIndexed) {
4478
- const delivered = lastDeliveredIndexed;
4479
- const lateItems = pending.filter(
4480
- (item) => compareIndexed(item.indexed, delivered) < 0,
4481
- );
4482
- if (lateItems.length > 0) {
4483
- notifyLateResults?.(lateItems.length, pk, lateItems);
4484
- }
4485
- } else {
4486
- notifyLateResults?.(pending.length, pk, pending);
4487
- }
4488
- }
4489
- }
4490
- }
4491
- if (!pendingBatchReason) {
4492
- pendingBatchReason = "join";
4493
- }
4494
- signalUpdate("join");
4699
+ await fetchLateJoinPeers([hash], new Map([[hash, pk]]));
4495
4700
  },
4496
4701
  });
4497
4702
  const cleanupDefault = cleanup;
@@ -4531,27 +4736,68 @@ export class DocumentIndex<
4531
4736
  next,
4532
4737
  done: doneFn,
4533
4738
  pending: async () => {
4739
+ const countPending = () => {
4740
+ let total = 0;
4741
+ for (const buffer of peerBufferMap.values()) {
4742
+ total += buffer.kept + buffer.buffer.length;
4743
+ }
4744
+ return total;
4745
+ };
4746
+
4534
4747
  try {
4748
+ let pendingTotal = countPending();
4749
+ if (remoteWaitActive && first) {
4750
+ if (pendingTotal === 0) {
4751
+ await fetchLateJoinPeers();
4752
+ pendingTotal = countPending();
4753
+ }
4754
+
4755
+ const shouldPrimePending =
4756
+ !done &&
4757
+ keepRemoteAlive &&
4758
+ (!pushUpdates || !first) &&
4759
+ pendingTotal === 0 &&
4760
+ joinFetchesInFlight === 0;
4761
+ if (shouldPrimePending && fetchesInFlight === 0) {
4762
+ const primePending = fetchAtLeast(1).catch((error) => {
4763
+ warn("Failed to prime keep-open iterator pending state", error);
4764
+ });
4765
+ if (remoteWaitActive) {
4766
+ await Promise.race([primePending, waitForAnyUpdate()]);
4767
+ } else {
4768
+ await primePending;
4769
+ }
4770
+ }
4771
+ return countPending();
4772
+ }
4773
+
4535
4774
  await fetchPromise;
4775
+ pendingTotal = countPending();
4536
4776
  // In push-update mode, remotes will stream new results proactively.
4537
4777
  // After the iterator has been primed (`first === true`), calling
4538
4778
  // `fetchAtLeast(1)` from `pending()` can double-count by pulling from
4539
4779
  // the remote iterator while we also have pushed results buffered locally.
4540
4780
  //
4541
- // We still need to prime the iterator at least once so `pending()` is meaningful
4542
- // even before the first `next(...)` call.
4543
- if (!done && keepRemoteAlive && (!pushUpdates || !first)) {
4781
+ // In keep-open remote-wait mode, we also avoid starting another remote
4782
+ // collect while a late-join fetch is already in flight or when we already
4783
+ // have buffered results to report. This keeps `pending()` observational
4784
+ // enough to avoid starving late joins behind unrelated long-poll collects,
4785
+ // while preserving the existing "pull one more" behavior when there is
4786
+ // nothing buffered yet.
4787
+ const shouldPrimePending =
4788
+ !done &&
4789
+ keepRemoteAlive &&
4790
+ (!pushUpdates || !first) &&
4791
+ pendingTotal === 0 &&
4792
+ !(remoteWaitActive && first && joinFetchesInFlight > 0);
4793
+ if (shouldPrimePending) {
4544
4794
  await fetchAtLeast(1);
4545
4795
  }
4546
4796
  } catch (error) {
4547
4797
  warn("Failed to refresh iterator pending state", error);
4548
4798
  }
4549
4799
 
4550
- let total = 0;
4551
- for (const buffer of peerBufferMap.values()) {
4552
- total += buffer.kept + buffer.buffer.length;
4553
- }
4554
- return total;
4800
+ return countPending();
4555
4801
  },
4556
4802
  all: async () => {
4557
4803
  drain = true;