@peerbit/document 13.0.21 → 13.0.23

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,11 @@ 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(() => {
3739
+ return resultsLeft === 0; // 0 results left to fetch and 0 pending results
3740
+ }),
3741
+ );
3702
3742
  };
3703
3743
 
3704
3744
  const next = async (n: number) => {
@@ -3853,8 +3893,10 @@ export class DocumentIndex<
3853
3893
  const pendingMissingResponseRetryPeers = new Set<string>();
3854
3894
  const missingResponseRetryAttempts = new Map<string, number>();
3855
3895
  const maxMissingResponseRetryAttempts = 2;
3896
+ let joinFetchesInFlight = 0;
3856
3897
 
3857
3898
  let updateDeferred: ReturnType<typeof pDefer> | undefined;
3899
+ const updateWaiters = new Set<ReturnType<typeof pDefer<void>>>();
3858
3900
  const onLateResultsQueue =
3859
3901
  options?.outOfOrder?.mode === "queue" &&
3860
3902
  typeof options?.outOfOrder?.handle === "function"
@@ -3970,9 +4012,20 @@ export class DocumentIndex<
3970
4012
  runNotify(reason);
3971
4013
  }
3972
4014
  updateDeferred?.resolve();
4015
+ for (const waiter of updateWaiters) {
4016
+ waiter.resolve();
4017
+ }
4018
+ updateWaiters.clear();
3973
4019
  };
3974
4020
  const _waitForUpdate = () =>
3975
4021
  updateDeferred ? updateDeferred.promise : Promise.resolve();
4022
+ const waitForAnyUpdate = () => {
4023
+ const waiter = pDefer<void>();
4024
+ updateWaiters.add(waiter);
4025
+ return waiter.promise.finally(() => {
4026
+ updateWaiters.delete(waiter);
4027
+ });
4028
+ };
3976
4029
 
3977
4030
  // ---------------- Live updates wiring (sorted-only with optional filter) ----------------
3978
4031
  const updateCallbacks = updateCallbacksRaw;
@@ -4435,11 +4488,143 @@ export class DocumentIndex<
4435
4488
  const keepRemoteWaitOpen =
4436
4489
  !!remoteConfig?.wait &&
4437
4490
  remoteWaitBehavior === "keep-open";
4491
+ let fetchLateJoinPeers = async (
4492
+ _candidateHashes?: Iterable<string>,
4493
+ _candidateKeys?: Map<string, PublicSignKey>,
4494
+ ) => false;
4438
4495
 
4439
4496
  if (keepRemoteWaitOpen) {
4440
4497
  // was used to account for missed results when a peer joins; omitted in this minimal handler
4441
4498
 
4442
4499
  updateDeferred = pDefer<void>();
4500
+ const lateJoinFetchesInFlight = new Set<string>();
4501
+
4502
+ fetchLateJoinPeers = async (
4503
+ candidateHashes?: Iterable<string>,
4504
+ candidateKeys?: Map<string, PublicSignKey>,
4505
+ ) => {
4506
+ if (totalFetchedCounter === 0) {
4507
+ return false;
4508
+ }
4509
+
4510
+ if (done) {
4511
+ unsetDone();
4512
+ }
4513
+
4514
+ const selfHash = this.node.identity.publicKey.hashcode();
4515
+ const knownCandidateKeys = candidateKeys
4516
+ ? new Map(candidateKeys)
4517
+ : new Map<string, PublicSignKey>();
4518
+ const hashes = candidateHashes
4519
+ ? [...candidateHashes]
4520
+ : [...(await this._log.getReplicators()).keys()];
4521
+ let missing = hashes.filter((hash) => {
4522
+ if (hash === selfHash) return false;
4523
+ if (peerBufferMap.has(hash)) return false;
4524
+ if (fetchedFirstForRemote!.has(hash)) return false;
4525
+ if (lateJoinFetchesInFlight.has(hash)) return false;
4526
+ return true;
4527
+ });
4528
+ if (missing.length === 0 && !candidateHashes) {
4529
+ const connectedPeers = (this.node.services.pubsub as any)?.peers as
4530
+ | Map<string, unknown>
4531
+ | undefined;
4532
+ if (connectedPeers?.size) {
4533
+ const connectedCandidates = [...connectedPeers.keys()].filter(
4534
+ (hash) =>
4535
+ hash !== selfHash &&
4536
+ !hashes.includes(hash) &&
4537
+ !peerBufferMap.has(hash) &&
4538
+ !fetchedFirstForRemote!.has(hash) &&
4539
+ !lateJoinFetchesInFlight.has(hash),
4540
+ );
4541
+ if (connectedCandidates.length > 0) {
4542
+ const discovered = await Promise.all(
4543
+ connectedCandidates.slice(0, 8).map(async (hash) => {
4544
+ const pk = await this.node.services.pubsub.getPublicKey(hash);
4545
+ if (!pk) {
4546
+ return undefined;
4547
+ }
4548
+ try {
4549
+ await this._log.waitForReplicator(pk, {
4550
+ signal: ensureController().signal,
4551
+ eager: true,
4552
+ timeout: 250,
4553
+ });
4554
+ knownCandidateKeys.set(hash, pk);
4555
+ return hash;
4556
+ } catch {
4557
+ return undefined;
4558
+ }
4559
+ }),
4560
+ );
4561
+ missing = discovered.filter((hash): hash is string => !!hash);
4562
+ }
4563
+ }
4564
+ }
4565
+ if (missing.length === 0) {
4566
+ return false;
4567
+ }
4568
+
4569
+ missing.forEach((hash) => lateJoinFetchesInFlight.add(hash));
4570
+ joinFetchesInFlight += missing.length;
4571
+
4572
+ try {
4573
+ const unresolved = missing.filter((hash) => {
4574
+ if (peerBufferMap.has(hash)) return false;
4575
+ if (fetchedFirstForRemote!.has(hash)) return false;
4576
+ return true;
4577
+ });
4578
+
4579
+ if (unresolved.length === 0) {
4580
+ return false;
4581
+ }
4582
+
4583
+ const lateJoinFetchPromise = trackFetch(
4584
+ fetchFirst(totalFetchedCounter, {
4585
+ from: unresolved,
4586
+ fetchedFirstForRemote,
4587
+ }),
4588
+ );
4589
+ await lateJoinFetchPromise;
4590
+ for (const hash of unresolved) {
4591
+ if (!peerBufferMap.has(hash)) {
4592
+ fetchedFirstForRemote?.delete(hash);
4593
+ }
4594
+ }
4595
+
4596
+ if (onLateResultsQueue || onLateResultsDrop) {
4597
+ for (const hash of unresolved) {
4598
+ const pending = peerBufferMap.get(hash)?.buffer;
4599
+ if (!pending || pending.length === 0) {
4600
+ continue;
4601
+ }
4602
+
4603
+ const peer = knownCandidateKeys.get(hash);
4604
+ if (lastDeliveredIndexed) {
4605
+ const delivered = lastDeliveredIndexed;
4606
+ const lateItems = pending.filter(
4607
+ (item) => compareIndexed(item.indexed, delivered) < 0,
4608
+ );
4609
+ if (lateItems.length > 0) {
4610
+ notifyLateResults?.(lateItems.length, peer, lateItems);
4611
+ }
4612
+ } else {
4613
+ notifyLateResults?.(pending.length, peer, pending);
4614
+ }
4615
+ }
4616
+ }
4617
+
4618
+ if (!pendingBatchReason) {
4619
+ pendingBatchReason = "join";
4620
+ }
4621
+ signalUpdate("join");
4622
+ return true;
4623
+ } finally {
4624
+ missing.forEach((hash) => lateJoinFetchesInFlight.delete(hash));
4625
+ joinFetchesInFlight -= missing.length;
4626
+ }
4627
+ };
4443
4628
 
4444
4629
  const waitForTime = remoteWaitPolicy?.timeout;
4445
4630
 
@@ -4462,36 +4647,7 @@ export class DocumentIndex<
4462
4647
  onPeer: async (pk) => {
4463
4648
  if (done) return;
4464
4649
  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");
4650
+ await fetchLateJoinPeers([hash], new Map([[hash, pk]]));
4495
4651
  },
4496
4652
  });
4497
4653
  const cleanupDefault = cleanup;
@@ -4531,27 +4687,68 @@ export class DocumentIndex<
4531
4687
  next,
4532
4688
  done: doneFn,
4533
4689
  pending: async () => {
4690
+ const countPending = () => {
4691
+ let total = 0;
4692
+ for (const buffer of peerBufferMap.values()) {
4693
+ total += buffer.kept + buffer.buffer.length;
4694
+ }
4695
+ return total;
4696
+ };
4697
+
4534
4698
  try {
4699
+ let pendingTotal = countPending();
4700
+ if (remoteWaitActive && first) {
4701
+ if (pendingTotal === 0) {
4702
+ await fetchLateJoinPeers();
4703
+ pendingTotal = countPending();
4704
+ }
4705
+
4706
+ const shouldPrimePending =
4707
+ !done &&
4708
+ keepRemoteAlive &&
4709
+ (!pushUpdates || !first) &&
4710
+ pendingTotal === 0 &&
4711
+ joinFetchesInFlight === 0;
4712
+ if (shouldPrimePending && fetchesInFlight === 0) {
4713
+ const primePending = fetchAtLeast(1).catch((error) => {
4714
+ warn("Failed to prime keep-open iterator pending state", error);
4715
+ });
4716
+ if (remoteWaitActive) {
4717
+ await Promise.race([primePending, waitForAnyUpdate()]);
4718
+ } else {
4719
+ await primePending;
4720
+ }
4721
+ }
4722
+ return countPending();
4723
+ }
4724
+
4535
4725
  await fetchPromise;
4726
+ pendingTotal = countPending();
4536
4727
  // In push-update mode, remotes will stream new results proactively.
4537
4728
  // After the iterator has been primed (`first === true`), calling
4538
4729
  // `fetchAtLeast(1)` from `pending()` can double-count by pulling from
4539
4730
  // the remote iterator while we also have pushed results buffered locally.
4540
4731
  //
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)) {
4732
+ // In keep-open remote-wait mode, we also avoid starting another remote
4733
+ // collect while a late-join fetch is already in flight or when we already
4734
+ // have buffered results to report. This keeps `pending()` observational
4735
+ // enough to avoid starving late joins behind unrelated long-poll collects,
4736
+ // while preserving the existing "pull one more" behavior when there is
4737
+ // nothing buffered yet.
4738
+ const shouldPrimePending =
4739
+ !done &&
4740
+ keepRemoteAlive &&
4741
+ (!pushUpdates || !first) &&
4742
+ pendingTotal === 0 &&
4743
+ !(remoteWaitActive && first && joinFetchesInFlight > 0);
4744
+ if (shouldPrimePending) {
4544
4745
  await fetchAtLeast(1);
4545
4746
  }
4546
4747
  } catch (error) {
4547
4748
  warn("Failed to refresh iterator pending state", error);
4548
4749
  }
4549
4750
 
4550
- let total = 0;
4551
- for (const buffer of peerBufferMap.values()) {
4552
- total += buffer.kept + buffer.buffer.length;
4553
- }
4554
- return total;
4751
+ return countPending();
4555
4752
  },
4556
4753
  all: async () => {
4557
4754
  drain = true;