@peerbit/shared-log 16.0.34 → 16.0.35

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.
@@ -75,8 +75,8 @@ export type SyncOptions<R extends "u32" | "u64"> = {
75
75
  /**
76
76
  * Optional profiling callback. It is only invoked when provided, and should
77
77
  * avoid blocking because it runs inline during open, provider resolution,
78
- * and sync paths. Open-phase events describe setup progress only; they are
79
- * not replication or durable-write readiness barriers.
78
+ * and sync paths, including bounded persisted-delivery observations.
79
+ * Profiling events are not replication/readiness barriers or durable receipts.
80
80
  */
81
81
  profile?: SyncProfileFn;
82
82
  };
@@ -0,0 +1,18 @@
1
+ import { type DiagnosticValue } from "@peerbit/diagnostics";
2
+ import type { SyncProfileFn } from "./profile.js";
3
+ export declare const PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT = 16;
4
+ /** Bounded, invocation-owned diagnostics. No retained entries or peer objects. */
5
+ export declare const createPersistedDeliveryProfile: (sink: SyncProfileFn | undefined, entries: number, minAcks: number, leaderDegree: number) => {
6
+ now: () => number;
7
+ emit: (name: string, details: Record<string, DiagnosticValue>, peer?: string, phaseStartedAt?: number) => void;
8
+ phase: (phase: string, options: {
9
+ round: number;
10
+ peer: string;
11
+ requestedEntries: number;
12
+ attempt?: number;
13
+ timeoutMs?: number;
14
+ startedAt?: number;
15
+ }) => (outcome: string, acceptedEntries?: number) => void;
16
+ finish: (details: Record<string, DiagnosticValue>) => void;
17
+ } | undefined;
18
+ //# sourceMappingURL=persisted-delivery-profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persisted-delivery-profile.d.ts","sourceRoot":"","sources":["../../../src/sync/persisted-delivery-profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAiB,MAAM,sBAAsB,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAKlD,eAAO,MAAM,sCAAsC,KAAK,CAAC;AAEzD,kFAAkF;AAClF,eAAO,MAAM,8BAA8B,GAC1C,MAAM,aAAa,GAAG,SAAS,EAC/B,SAAS,MAAM,EACf,SAAS,MAAM,EACf,cAAc,MAAM;;iBAqDb,MAAM,WACH,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,SACjC,MAAM,mBACI,MAAM;mBAcf,MAAM,WACJ;QACR,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACnB,MASO,SAAS,MAAM,EAAE,kBAAkB,MAAM;sBAQhC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC;aAUlD,CAAC"}
@@ -0,0 +1,77 @@
1
+ import { diagnosticNow } from "@peerbit/diagnostics";
2
+ // Process-local correlation only, never a receipt, wire id or persistent key.
3
+ let nextTrace = 0;
4
+ const MAX_EVENTS = 256;
5
+ export const PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT = 16;
6
+ /** Bounded, invocation-owned diagnostics. No retained entries or peer objects. */
7
+ export const createPersistedDeliveryProfile = (sink, entries, minAcks, leaderDegree) => {
8
+ if (!sink)
9
+ return undefined;
10
+ nextTrace = (nextTrace % Number.MAX_SAFE_INTEGER) + 1;
11
+ const traceId = `persisted:${nextTrace}`;
12
+ const startedAt = diagnosticNow();
13
+ let emitted = 0;
14
+ let dropped = 0;
15
+ let closed = false;
16
+ const send = (name, details, peer, phaseStartedAt = startedAt) => {
17
+ try {
18
+ const now = diagnosticNow();
19
+ const result = sink({
20
+ name: `sharedLog.persistedDelivery.${name}`,
21
+ component: "shared-log",
22
+ traceId,
23
+ entries,
24
+ peer,
25
+ durationMs: Math.max(0, now - phaseStartedAt),
26
+ details: {
27
+ ...details,
28
+ v: 1,
29
+ minAcks,
30
+ leaderDegree,
31
+ entrySampleWindow: Math.min(entries, PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT),
32
+ entriesOutsideSampleWindow: Math.max(0, entries - PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT),
33
+ elapsedMs: Math.max(0, now - startedAt),
34
+ },
35
+ });
36
+ // A void callback can still be async in TypeScript. Never await it, and
37
+ // own its rejection just as we isolate a synchronous diagnostic throw.
38
+ if (result &&
39
+ typeof result.then === "function") {
40
+ void Promise.resolve(result).catch(() => undefined);
41
+ }
42
+ }
43
+ catch {
44
+ // Diagnostics must not enter protocol recovery or replace its outcome.
45
+ }
46
+ };
47
+ const emit = (name, details, peer, phaseStartedAt) => {
48
+ if (closed)
49
+ return;
50
+ if (emitted >= MAX_EVENTS) {
51
+ dropped = Math.min(Number.MAX_SAFE_INTEGER, dropped + 1);
52
+ return;
53
+ }
54
+ emitted++;
55
+ send(name, details, peer, phaseStartedAt);
56
+ };
57
+ return {
58
+ now: diagnosticNow,
59
+ emit,
60
+ phase: (phase, options) => {
61
+ const { peer, startedAt = diagnosticNow(), ...details } = options;
62
+ emit("peerPhase", { ...details, phase, edge: "start", outcome: "pending" }, peer, startedAt);
63
+ return (outcome, acceptedEntries) => emit("peerPhase", { ...details, phase, edge: "end", outcome, acceptedEntries }, peer, startedAt);
64
+ },
65
+ finish: (details) => {
66
+ if (closed)
67
+ return;
68
+ closed = true;
69
+ send("settle", {
70
+ ...details,
71
+ emittedEvents: emitted + 1,
72
+ droppedEvents: dropped,
73
+ });
74
+ },
75
+ };
76
+ };
77
+ //# sourceMappingURL=persisted-delivery-profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persisted-delivery-profile.js","sourceRoot":"","sources":["../../../src/sync/persisted-delivery-profile.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/shared-log",
3
- "version": "16.0.34",
3
+ "version": "16.0.35",
4
4
  "description": "Shared log",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -63,36 +63,36 @@
63
63
  "pino": "^9.4.0",
64
64
  "uint8arrays": "^5.1.0",
65
65
  "@peerbit/any-store": "2.2.17",
66
- "@peerbit/blocks-interface": "2.2.1",
66
+ "@peerbit/cache": "3.1.1",
67
67
  "@peerbit/blocks": "4.3.2",
68
+ "@peerbit/blocks-interface": "2.2.1",
68
69
  "@peerbit/crypto": "3.1.6",
69
- "@peerbit/cache": "3.1.1",
70
+ "@peerbit/indexer-interface": "3.1.0",
70
71
  "@peerbit/diagnostics": "0.0.1",
71
- "@peerbit/indexer-interface": "3.0.13",
72
- "@peerbit/log": "6.2.34",
73
- "@peerbit/indexer-sqlite3": "3.0.20",
74
- "@peerbit/program": "6.0.62",
75
- "@peerbit/pubsub-interface": "5.2.2",
72
+ "@peerbit/log": "6.2.35",
76
73
  "@peerbit/logger": "2.0.2",
77
- "@peerbit/pubsub": "5.4.8",
74
+ "@peerbit/indexer-sqlite3": "3.0.21",
75
+ "@peerbit/program": "6.0.63",
78
76
  "@peerbit/riblt": "1.2.0",
79
- "@peerbit/rpc": "6.1.30",
77
+ "@peerbit/pubsub": "5.4.8",
78
+ "@peerbit/rpc": "6.1.31",
79
+ "@peerbit/pubsub-interface": "5.2.2",
80
80
  "@peerbit/stream-interface": "6.0.16",
81
81
  "@peerbit/time": "3.0.1"
82
82
  },
83
83
  "optionalDependencies": {
84
- "@peerbit/native-backbone": "0.2.16",
85
- "@peerbit/shared-log-rust": "0.1.7"
84
+ "@peerbit/native-backbone": "0.2.17",
85
+ "@peerbit/shared-log-rust": "0.1.8"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@types/libsodium-wrappers": "^0.7.14",
89
89
  "@types/pidusage": "^2.0.5",
90
90
  "uuid": "^11.1.1",
91
91
  "@peerbit/any-store-rust": "0.1.7",
92
- "@peerbit/indexer-simple": "1.2.17",
93
- "@peerbit/test-utils": "3.1.43",
94
- "peerbit": "5.4.4",
95
- "@peerbit/indexer-rust": "1.0.14"
92
+ "@peerbit/indexer-rust": "1.0.15",
93
+ "@peerbit/indexer-simple": "1.3.0",
94
+ "@peerbit/test-utils": "3.1.44",
95
+ "peerbit": "5.4.5"
96
96
  },
97
97
  "repository": {
98
98
  "type": "git",
package/src/index.ts CHANGED
@@ -307,6 +307,10 @@ import type {
307
307
  SynchronizerConstructor,
308
308
  Syncronizer,
309
309
  } from "./sync/index.js";
310
+ import {
311
+ PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT,
312
+ createPersistedDeliveryProfile,
313
+ } from "./sync/persisted-delivery-profile.js";
310
314
  import {
311
315
  emitSyncProfileDuration,
312
316
  emitSyncProfileEvent,
@@ -6356,6 +6360,18 @@ export class SharedLog<
6356
6360
  records.size,
6357
6361
  );
6358
6362
  const signal = deadline.signal;
6363
+ const profile = createPersistedDeliveryProfile(
6364
+ this._logProperties?.sync?.profile,
6365
+ records.size,
6366
+ minAcks,
6367
+ replicas,
6368
+ );
6369
+ const profiledHashes = profile
6370
+ ? committedHashes.slice(0, PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT)
6371
+ : undefined;
6372
+ let profileRounds = 0;
6373
+ let profileOutcome = "failed";
6374
+ let profileReason: string | undefined;
6359
6375
  const recoveryController = new AbortController();
6360
6376
  const recoverySignal = AbortSignal.any([signal, recoveryController.signal]);
6361
6377
  const recoveryByPeer = new Map<
@@ -6469,6 +6485,7 @@ export class SharedLog<
6469
6485
  };
6470
6486
  try {
6471
6487
  while (true) {
6488
+ const profileRound = profile ? ++profileRounds : 0;
6472
6489
  if (signal.aborted) {
6473
6490
  throw signal.reason ?? new AbortError();
6474
6491
  }
@@ -6524,13 +6541,20 @@ export class SharedLog<
6524
6541
  const selfHash = this.node.identity.publicKey.hashcode();
6525
6542
  if (needsInitialLeaderCheck) {
6526
6543
  needsInitialLeaderCheck = false;
6527
- if (
6528
- leadersByEntry.some(
6529
- (leaders) =>
6530
- leaders.size === 0 ||
6531
- (leaders.size === 1 && leaders.has(selfHash)),
6532
- )
6533
- ) {
6544
+ const noRemoteIndex = leadersByEntry.findIndex(
6545
+ (leaders) =>
6546
+ leaders.size === 0 ||
6547
+ (leaders.size === 1 && leaders.has(selfHash)),
6548
+ );
6549
+ if (noRemoteIndex >= 0) {
6550
+ if (noRemoteIndex < PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT)
6551
+ profile?.emit("plan", {
6552
+ round: profileRound,
6553
+ entryIndex: noRemoteIndex,
6554
+ remoteLeaderCount: 0,
6555
+ selectedRequestPeerCount: 0,
6556
+ carriedAckCount: 0,
6557
+ });
6534
6558
  throw new NoPeersError(this.rpc.topic, "persisted");
6535
6559
  }
6536
6560
  }
@@ -6542,6 +6566,10 @@ export class SharedLog<
6542
6566
  }
6543
6567
  if (!isRoundOwnershipCurrent()) break;
6544
6568
  const acknowledgements = carriedAcknowledgements.get(hash)!;
6569
+ const entryProfile =
6570
+ index < PERSISTED_DELIVERY_PROFILE_ENTRY_LIMIT
6571
+ ? profile
6572
+ : undefined;
6545
6573
  for (const [peer, captured] of acknowledgements) {
6546
6574
  const current = this.persistedReceiptPeerSession(peer);
6547
6575
  if (
@@ -6551,19 +6579,67 @@ export class SharedLog<
6551
6579
  current.peerSession !== captured.peerSession
6552
6580
  ) {
6553
6581
  acknowledgements.delete(peer);
6582
+ } else {
6583
+ entryProfile?.emit(
6584
+ "candidate",
6585
+ {
6586
+ round: profileRound,
6587
+ entryIndex: index,
6588
+ status: "carried-receipt",
6589
+ },
6590
+ peer,
6591
+ );
6554
6592
  }
6555
6593
  }
6556
- if (acknowledgements.size >= minAcks) continue;
6594
+ if (acknowledgements.size >= minAcks) {
6595
+ entryProfile?.emit("plan", {
6596
+ round: profileRound,
6597
+ entryIndex: index,
6598
+ remoteLeaderCount: leaders.size - Number(leaders.has(selfHash)),
6599
+ selectedRequestPeerCount: 0,
6600
+ carriedAckCount: acknowledgements.size,
6601
+ });
6602
+ continue;
6603
+ }
6604
+ let selectedRequestPeerCount = 0;
6557
6605
  for (const peer of leaders.keys()) {
6558
6606
  if (peer === selfHash) continue;
6559
6607
  recoveryCandidates.add(peer);
6560
6608
  const current = this.persistedReceiptPeerSession(peer);
6561
- if (!current) continue;
6609
+ if (!current) {
6610
+ entryProfile?.emit(
6611
+ "candidate",
6612
+ {
6613
+ round: profileRound,
6614
+ entryIndex: index,
6615
+ status: "leader-no-current-session",
6616
+ },
6617
+ peer,
6618
+ );
6619
+ continue;
6620
+ }
6562
6621
  if (acknowledgements.has(peer)) continue;
6563
6622
  const hashes = hashesByPeer.get(peer) ?? [];
6564
6623
  hashes.push(hash);
6565
6624
  hashesByPeer.set(peer, hashes);
6625
+ selectedRequestPeerCount++;
6626
+ entryProfile?.emit(
6627
+ "candidate",
6628
+ {
6629
+ round: profileRound,
6630
+ entryIndex: index,
6631
+ status: "selected-for-request",
6632
+ },
6633
+ peer,
6634
+ );
6566
6635
  }
6636
+ entryProfile?.emit("plan", {
6637
+ round: profileRound,
6638
+ entryIndex: index,
6639
+ remoteLeaderCount: leaders.size - Number(leaders.has(selfHash)),
6640
+ selectedRequestPeerCount,
6641
+ carriedAckCount: acknowledgements.size,
6642
+ });
6567
6643
  }
6568
6644
  if (!isRoundOwnershipCurrent()) continue;
6569
6645
  recoverSelectedPeers(recoveryCandidates);
@@ -6644,17 +6720,30 @@ export class SharedLog<
6644
6720
  signal: roundSignal,
6645
6721
  });
6646
6722
  }
6647
- await this._v2Send.confirmLatestForPeer(
6648
- {
6649
- peerHash: peer,
6650
- peerSession: captured.peerSession,
6651
- receiverTransportSession: captured.capabilitySession,
6652
- },
6653
- {
6654
- timeout: getAttemptTimeout(),
6655
- signal: roundSignal,
6656
- },
6657
- );
6723
+ const confirmationTimeout = getAttemptTimeout();
6724
+ const endConfirmation = profile?.phase("confirmation", {
6725
+ round: profileRound,
6726
+ requestedEntries: hashes.length,
6727
+ timeoutMs: confirmationTimeout,
6728
+ peer,
6729
+ });
6730
+ try {
6731
+ await this._v2Send.confirmLatestForPeer(
6732
+ {
6733
+ peerHash: peer,
6734
+ peerSession: captured.peerSession,
6735
+ receiverTransportSession: captured.capabilitySession,
6736
+ },
6737
+ {
6738
+ timeout: confirmationTimeout,
6739
+ signal: roundSignal,
6740
+ },
6741
+ );
6742
+ endConfirmation?.("fulfilled");
6743
+ } catch (error) {
6744
+ endConfirmation?.("rejected");
6745
+ throw error;
6746
+ }
6658
6747
  } catch {
6659
6748
  // The exact receiver generation did not prove that it applied
6660
6749
  // our latest role state. Replan instead of transferring to a
@@ -6691,13 +6780,32 @@ export class SharedLog<
6691
6780
  }
6692
6781
  },
6693
6782
  onChunkSent: async (chunk) => {
6694
- return this.waitForPersistedTransferAdmission(
6783
+ const startedAt = profile?.now();
6784
+ const pending = this.waitForPersistedTransferAdmission(
6695
6785
  peer,
6696
6786
  chunk,
6697
6787
  captured,
6698
6788
  roundSignal,
6699
6789
  isPeerRoundCurrent,
6700
6790
  );
6791
+ const endAdmission = profile?.phase(
6792
+ "transfer-admission",
6793
+ {
6794
+ round: profileRound,
6795
+ peer,
6796
+ requestedEntries: chunk.length,
6797
+ startedAt,
6798
+ },
6799
+ );
6800
+ if (endAdmission)
6801
+ void pending.then(
6802
+ (admitted) =>
6803
+ endAdmission(
6804
+ admitted ? "fulfilled" : "not-admitted",
6805
+ ),
6806
+ () => endAdmission("rejected"),
6807
+ );
6808
+ return pending;
6701
6809
  },
6702
6810
  operationQueue,
6703
6811
  priority: delivery.priority,
@@ -6742,19 +6850,37 @@ export class SharedLog<
6742
6850
  break;
6743
6851
  }
6744
6852
  let responses;
6853
+ const attempt = offset / PERSISTED_RECEIPT_CHUNK_SIZE + 1;
6854
+ let requestStartedAt: number | undefined;
6855
+ let endRequest:
6856
+ | ((outcome: string, acceptedEntries?: number) => void)
6857
+ | undefined;
6745
6858
  try {
6746
- await this.waitForPersistedReceiptEgressAdmission(
6859
+ const endEgress = profile?.phase("receipt-egress", {
6860
+ round: profileRound,
6861
+ attempt,
6862
+ requestedEntries: requestedHashes.length,
6747
6863
  peer,
6748
- captured.capabilitySession,
6749
- requestedHashes.length,
6750
- roundSignal,
6751
- );
6864
+ });
6865
+ try {
6866
+ await this.waitForPersistedReceiptEgressAdmission(
6867
+ peer,
6868
+ captured.capabilitySession,
6869
+ requestedHashes.length,
6870
+ roundSignal,
6871
+ );
6872
+ endEgress?.("fulfilled");
6873
+ } catch (error) {
6874
+ endEgress?.("rejected");
6875
+ throw error;
6876
+ }
6752
6877
  if (!isPeerRoundCurrent()) break;
6753
6878
  const attemptTimeout = getAttemptTimeout();
6754
6879
  responses =
6755
6880
  (await operationQueue.add(async () => {
6756
6881
  if (!isPeerRoundCurrent()) return [];
6757
- return this.rpc.request(
6882
+ requestStartedAt = profile?.now();
6883
+ const pending = this.rpc.request(
6758
6884
  new RequestPersistedEntriesV1({
6759
6885
  expectedReceiverSession: captured.capabilitySession,
6760
6886
  hashes: requestedHashes,
@@ -6770,14 +6896,25 @@ export class SharedLog<
6770
6896
  signal: roundSignal,
6771
6897
  },
6772
6898
  );
6899
+ endRequest = profile?.phase("receipt-request", {
6900
+ round: profileRound,
6901
+ attempt,
6902
+ requestedEntries: requestedHashes.length,
6903
+ timeoutMs: attemptTimeout,
6904
+ peer,
6905
+ startedAt: requestStartedAt,
6906
+ });
6907
+ return pending;
6773
6908
  })) ?? [];
6774
6909
  } catch {
6910
+ endRequest?.("rejected", 0);
6775
6911
  if (roundSignal.aborted) break;
6776
6912
  // A peer can disconnect or miss this retry while the overall
6777
6913
  // quorum deadline remains active. Replan on the next round.
6778
6914
  break;
6779
6915
  }
6780
6916
  if (!isRoundOwnershipCurrent()) {
6917
+ endRequest?.("stale", 0);
6781
6918
  roundController.abort();
6782
6919
  break;
6783
6920
  }
@@ -6788,6 +6925,7 @@ export class SharedLog<
6788
6925
  current.peerSession !== captured.peerSession
6789
6926
  ) {
6790
6927
  purgePeerDeliveryState(peer);
6928
+ endRequest?.("stale", 0);
6791
6929
  break;
6792
6930
  }
6793
6931
  const requested = new Set(requestedHashes);
@@ -6824,6 +6962,26 @@ export class SharedLog<
6824
6962
  }
6825
6963
  }
6826
6964
  }
6965
+ endRequest?.("fulfilled", confirmed.size);
6966
+ for (
6967
+ let entryIndex = 0;
6968
+ entryIndex < (profiledHashes?.length ?? 0);
6969
+ entryIndex++
6970
+ ) {
6971
+ const hash = profiledHashes![entryIndex]!;
6972
+ if (confirmed.has(hash))
6973
+ profile?.emit(
6974
+ "progress",
6975
+ {
6976
+ round: profileRound,
6977
+ attempt,
6978
+ entryIndex,
6979
+ carriedAckCount:
6980
+ carriedAcknowledgements.get(hash)!.size,
6981
+ },
6982
+ peer,
6983
+ );
6984
+ }
6827
6985
  }
6828
6986
  })()
6829
6987
  .then(() => undefined)
@@ -6891,10 +7049,12 @@ export class SharedLog<
6891
7049
  while (requests.size > 0) {
6892
7050
  await Promise.race(requests);
6893
7051
  if (await roundComplete()) {
7052
+ profileOutcome = "quorum-validated";
6894
7053
  return;
6895
7054
  }
6896
7055
  }
6897
7056
  if (await roundComplete()) {
7057
+ profileOutcome = "quorum-validated";
6898
7058
  return;
6899
7059
  }
6900
7060
  } finally {
@@ -6922,6 +7082,16 @@ export class SharedLog<
6922
7082
  );
6923
7083
  }
6924
7084
  } catch (error) {
7085
+ profileReason =
7086
+ signal.reason instanceof TimeoutError
7087
+ ? "timeout"
7088
+ : signal.aborted
7089
+ ? "signal"
7090
+ : error instanceof NoPeersError
7091
+ ? "no-peers"
7092
+ : "error";
7093
+ profileOutcome =
7094
+ signal.aborted && profileReason !== "timeout" ? "aborted" : "failed";
6925
7095
  if (error instanceof PersistedDeliveryError) {
6926
7096
  throw error;
6927
7097
  }
@@ -6933,6 +7103,11 @@ export class SharedLog<
6933
7103
  state.controller.abort();
6934
7104
  }
6935
7105
  if (ownedDeadline) deadline.dispose();
7106
+ profile?.finish({
7107
+ outcome: profileOutcome,
7108
+ reason: profileReason,
7109
+ round: profileRounds,
7110
+ });
6936
7111
  }
6937
7112
  }
6938
7113
 
@@ -27046,8 +27221,9 @@ export class SharedLog<
27046
27221
  );
27047
27222
  }
27048
27223
 
27224
+ let fullReplicaPlan;
27049
27225
  if (!options?.candidates) {
27050
- const fullReplicaLeaders = await this.findFullReplicaLeaders(
27226
+ fullReplicaPlan = await this.findFullReplicaLeaderPlan(
27051
27227
  cursors.length,
27052
27228
  context.roleAge,
27053
27229
  peerFilter,
@@ -27055,8 +27231,8 @@ export class SharedLog<
27055
27231
  this.throwIfReplicationOwnershipLifecycleInactive(
27056
27232
  ownershipLifecycleController,
27057
27233
  );
27058
- if (fullReplicaLeaders) {
27059
- return fullReplicaLeaders;
27234
+ if (fullReplicaPlan?.complete) {
27235
+ return fullReplicaPlan.leaders;
27060
27236
  }
27061
27237
  }
27062
27238
 
@@ -27073,6 +27249,16 @@ export class SharedLog<
27073
27249
  this.throwIfReplicationOwnershipLifecycleInactive(
27074
27250
  ownershipLifecycleController,
27075
27251
  );
27252
+ if (fullReplicaPlan) {
27253
+ // Retain the existing mature global fallback (including strict owners).
27254
+ // Young owners still need coordinate sampling before the plan is complete.
27255
+ for (const [hash, leader] of leaders) {
27256
+ if (!fullReplicaPlan.leaders.has(hash)) {
27257
+ fullReplicaPlan.leaders.set(hash, leader);
27258
+ }
27259
+ }
27260
+ return fullReplicaPlan.leaders;
27261
+ }
27076
27262
  return leaders;
27077
27263
  }
27078
27264
 
@@ -27117,16 +27303,20 @@ export class SharedLog<
27117
27303
  return peerFilter;
27118
27304
  }
27119
27305
 
27120
- private async findFullReplicaLeaders(
27306
+ private async findFullReplicaLeaderPlan(
27121
27307
  replicas: number,
27122
27308
  roleAge: number,
27123
27309
  peerFilter?: Set<string>,
27124
- ): Promise<Map<string, { intersecting: boolean }> | undefined> {
27310
+ ): Promise<
27311
+ | { leaders: Map<string, { intersecting: boolean }>; complete: boolean }
27312
+ | undefined
27313
+ > {
27125
27314
  const now = Date.now();
27126
27315
  const leaders = new Map<string, { intersecting: boolean }>();
27127
27316
  // Strict-only peers are not global fallbacks, but may still own an entry's
27128
27317
  // coordinates. Remember them so a partial fallback cannot bypass sampling.
27129
27318
  const excludedStrictPeers = new Set<string>();
27319
+ const youngPeers = new Set<string>();
27130
27320
  const includeStrict =
27131
27321
  this._logProperties?.strictFullReplicaFallback !== false;
27132
27322
  const iterator = this.replicationIndex.iterate(
@@ -27150,6 +27340,7 @@ export class SharedLog<
27150
27340
  continue;
27151
27341
  }
27152
27342
  if (!isMatured(range, now, roleAge)) {
27343
+ youngPeers.add(range.hash);
27153
27344
  continue;
27154
27345
  }
27155
27346
  leaders.set(range.hash, { intersecting: true });
@@ -27168,7 +27359,12 @@ export class SharedLog<
27168
27359
  }
27169
27360
  }
27170
27361
 
27171
- return leaders.size > 0 ? leaders : undefined;
27362
+ return leaders.size > 0
27363
+ ? {
27364
+ leaders,
27365
+ complete: [...youngPeers].every((hash) => leaders.has(hash)),
27366
+ }
27367
+ : undefined;
27172
27368
  }
27173
27369
 
27174
27370
  private async findEntryReplicatedLeaderBatch(
package/src/sync/index.ts CHANGED
@@ -94,8 +94,8 @@ export type SyncOptions<R extends "u32" | "u64"> = {
94
94
  /**
95
95
  * Optional profiling callback. It is only invoked when provided, and should
96
96
  * avoid blocking because it runs inline during open, provider resolution,
97
- * and sync paths. Open-phase events describe setup progress only; they are
98
- * not replication or durable-write readiness barriers.
97
+ * and sync paths, including bounded persisted-delivery observations.
98
+ * Profiling events are not replication/readiness barriers or durable receipts.
99
99
  */
100
100
  profile?: SyncProfileFn;
101
101
  };