@peerbit/shared-log 13.2.16 → 13.2.18

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/shared-log",
3
- "version": "13.2.16",
3
+ "version": "13.2.18",
4
4
  "description": "Shared log",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -62,25 +62,25 @@
62
62
  "pino": "^9.4.0",
63
63
  "uint8arrays": "^5.1.0",
64
64
  "@peerbit/any-store": "2.2.13",
65
- "@peerbit/blocks": "4.2.7",
65
+ "@peerbit/blocks": "4.2.8",
66
66
  "@peerbit/crypto": "3.1.4",
67
- "@peerbit/diagnostics": "0.0.1",
68
67
  "@peerbit/cache": "3.1.1",
69
- "@peerbit/indexer-interface": "3.0.8",
70
- "@peerbit/blocks-interface": "2.1.3",
68
+ "@peerbit/blocks-interface": "2.1.4",
69
+ "@peerbit/diagnostics": "0.0.1",
71
70
  "@peerbit/indexer-sqlite3": "3.0.11",
72
- "@peerbit/log": "6.2.10",
71
+ "@peerbit/indexer-interface": "3.0.8",
72
+ "@peerbit/log": "6.2.11",
73
73
  "@peerbit/logger": "2.0.1",
74
- "@peerbit/rpc": "6.1.9",
75
- "@peerbit/pubsub-interface": "5.1.7",
74
+ "@peerbit/program": "6.0.42",
75
+ "@peerbit/pubsub": "5.3.7",
76
76
  "@peerbit/riblt": "1.2.0",
77
- "@peerbit/program": "6.0.41",
77
+ "@peerbit/pubsub-interface": "5.1.8",
78
+ "@peerbit/rpc": "6.1.10",
78
79
  "@peerbit/time": "3.0.1",
79
- "@peerbit/stream-interface": "6.0.13",
80
- "@peerbit/pubsub": "5.3.6"
80
+ "@peerbit/stream-interface": "6.0.14"
81
81
  },
82
82
  "optionalDependencies": {
83
- "@peerbit/native-backbone": "0.2.3",
83
+ "@peerbit/native-backbone": "0.2.4",
84
84
  "@peerbit/shared-log-rust": "0.1.3"
85
85
  },
86
86
  "devDependencies": {
@@ -88,9 +88,9 @@
88
88
  "@types/pidusage": "^2.0.5",
89
89
  "uuid": "^11.1.1",
90
90
  "@peerbit/any-store-rust": "0.1.3",
91
- "@peerbit/indexer-rust": "1.0.6",
92
- "@peerbit/test-utils": "3.1.13",
93
- "peerbit": "5.3.13"
91
+ "@peerbit/test-utils": "3.1.14",
92
+ "peerbit": "5.3.14",
93
+ "@peerbit/indexer-rust": "1.0.6"
94
94
  },
95
95
  "repository": {
96
96
  "type": "git",
@@ -26,11 +26,17 @@ export type CheckedPrunePendingDelete = {
26
26
 
27
27
  export type CheckedPruneRetryState<T, R extends "u32" | "u64"> = {
28
28
  attempts: number;
29
+ generation: number;
29
30
  timer?: ReturnType<typeof setTimeout>;
30
31
  entry: CheckedPruneEntry<T, R>;
31
32
  leaders: CheckedPruneLeaderMap | Set<string>;
32
33
  };
33
34
 
35
+ export type CheckedPruneRetryIdentity<T, R extends "u32" | "u64"> = {
36
+ state: CheckedPruneRetryState<T, R>;
37
+ generation: number;
38
+ };
39
+
34
40
  export type CheckedPruneInvalidatedGeneration<T, R extends "u32" | "u64"> = {
35
41
  hash: string;
36
42
  pending: CheckedPrunePendingDelete;
@@ -109,6 +115,7 @@ export class CheckedPruneCoordinator<T, R extends "u32" | "u64"> {
109
115
  this.requestIPruneSent.has(hash) ||
110
116
  this.responseReplicatorSet.has(hash) ||
111
117
  this.retries.has(hash) ||
118
+ this.restartReservations.has(hash) ||
112
119
  this.candidateTokens.has(hash)
113
120
  ) {
114
121
  return;
@@ -219,6 +226,11 @@ export class CheckedPruneCoordinator<T, R extends "u32" | "u64"> {
219
226
  return this.retries.get(hash);
220
227
  }
221
228
 
229
+ getRetryIdentity(hash: string): CheckedPruneRetryIdentity<T, R> | undefined {
230
+ const state = this.retries.get(hash);
231
+ return state ? { state, generation: state.generation } : undefined;
232
+ }
233
+
222
234
  hasRetry(hash: string) {
223
235
  return this.retries.has(hash);
224
236
  }
@@ -232,10 +244,14 @@ export class CheckedPruneCoordinator<T, R extends "u32" | "u64"> {
232
244
  session.phase = "retrying";
233
245
  }
234
246
 
235
- clearRetry(hash: string) {
236
- this.restartReservations.delete(hash);
237
- this.candidateTokens.delete(hash);
247
+ clearRetry(hash: string, expected?: CheckedPruneRetryIdentity<T, R>) {
238
248
  const state = this.retries.get(hash);
249
+ if (
250
+ expected &&
251
+ (state !== expected.state || state.generation !== expected.generation)
252
+ ) {
253
+ return false;
254
+ }
239
255
  if (state?.timer) {
240
256
  clearTimeout(state.timer);
241
257
  }
@@ -245,6 +261,7 @@ export class CheckedPruneCoordinator<T, R extends "u32" | "u64"> {
245
261
  session.retry = undefined;
246
262
  }
247
263
  this.deleteSessionIfIdle(hash);
264
+ return true;
248
265
  }
249
266
 
250
267
  clearRetryTimer(hash: string) {
@@ -477,12 +494,14 @@ export class CheckedPruneCoordinator<T, R extends "u32" | "u64"> {
477
494
  return false;
478
495
  }
479
496
  this.restartReservations.delete(hash);
497
+ this.deleteSessionIfIdle(hash);
480
498
  return true;
481
499
  }
482
500
 
483
501
  cancelRestartReservation(hash: string, reservation: object) {
484
502
  if (this.restartReservations.get(hash) === reservation) {
485
503
  this.restartReservations.delete(hash);
504
+ this.deleteSessionIfIdle(hash);
486
505
  }
487
506
  }
488
507
 
package/src/index.ts CHANGED
@@ -132,6 +132,7 @@ import {
132
132
  type CheckedPruneLeaderMap,
133
133
  type CheckedPrunePendingDelete,
134
134
  type CheckedPruneRestartCandidate,
135
+ type CheckedPruneRetryIdentity,
135
136
  type CheckedPruneRetryState,
136
137
  } from "./checked-prune.js";
137
138
  import { type CPUUsage, CPUUsageIntervalLag } from "./cpu.js";
@@ -3349,6 +3350,13 @@ export class SharedLog<
3349
3350
  // before a liveness eviction but reach the per-peer apply lane after it. Unlike
3350
3351
  // message timestamps, these tokens never compare clocks from different peers.
3351
3352
  private _replicationInfoReceiveEpochByPeer!: Map<string, object>;
3353
+ // Receive-side ownership plans may span lower-log joins that invoke user code.
3354
+ // Increment synchronously with leader-cache invalidation so the handler can
3355
+ // detect whether its pre-join plan needs one fresh post-persist audit.
3356
+ private _receiveOwnershipRevision = 0;
3357
+ // Count ownership-changing range mutations from queue admission through
3358
+ // settlement, including mutations already pending when a receive starts.
3359
+ private _receiveOwnershipMutationAdmissions = 0;
3352
3360
  // Subscription callbacks can overlap because removing a replicator mutates the
3353
3361
  // replication index asynchronously. Keep that lifecycle separate from message
3354
3362
  // timestamps so a reconnect can synchronously revoke an older unsubscribe.
@@ -5921,9 +5929,17 @@ export class SharedLog<
5921
5929
  }
5922
5930
 
5923
5931
  private invalidateLeaderSelectionContextCache() {
5932
+ this._receiveOwnershipRevision++;
5924
5933
  this._leaderSelectionContextCache = undefined;
5925
5934
  }
5926
5935
 
5936
+ private isReceiveOwnershipSnapshotStable(revision: number): boolean {
5937
+ return (
5938
+ this._receiveOwnershipMutationAdmissions === 0 &&
5939
+ this._receiveOwnershipRevision === revision
5940
+ );
5941
+ }
5942
+
5927
5943
  private canCacheLeaderSelectionContext(options?: {
5928
5944
  roleAge?: number;
5929
5945
  candidates?: Iterable<string>;
@@ -7706,7 +7722,7 @@ export class SharedLog<
7706
7722
  let deleted: ReplicationRangeIndexable<R>[] = [];
7707
7723
  let ownerHasRanges = false;
7708
7724
  let mutationError: unknown;
7709
- const mutationCommitted = await this.withReplicationRangeMutationQueue(
7725
+ const mutationCommitted = await this.withReceiveOwnershipMutationQueue(
7710
7726
  async () => {
7711
7727
  if (
7712
7728
  !ownsReplicationOwnershipLifecycle() ||
@@ -7934,7 +7950,7 @@ export class SharedLog<
7934
7950
  this.throwIfReplicationOwnershipLifecycleInactive(
7935
7951
  replicationOwnershipLifecycleController,
7936
7952
  );
7937
- return this.withReplicationRangeMutationQueue(
7953
+ return this.withReceiveOwnershipMutationQueue(
7938
7954
  () => this.removeReplicationRangesUnlocked(ranges, from, options),
7939
7955
  replicationOwnershipLifecycleController,
7940
7956
  );
@@ -8106,7 +8122,7 @@ export class SharedLog<
8106
8122
  this.throwIfReplicationOwnershipLifecycleInactive(
8107
8123
  replicationOwnershipLifecycleController,
8108
8124
  );
8109
- return this.withReplicationRangeMutationQueue(
8125
+ return this.withReceiveOwnershipMutationQueue(
8110
8126
  () =>
8111
8127
  this.addReplicationRangeUnlocked(
8112
8128
  ranges,
@@ -11005,6 +11021,8 @@ export class SharedLog<
11005
11021
  entries: ShallowOrFullEntry<T>[],
11006
11022
  options?: {
11007
11023
  decodedReplicaCounts?: DecodedReplicaCountMap;
11024
+ freshReceiveOwnerAudit?: boolean;
11025
+ preserveExistingPruneOnLocalResult?: boolean;
11008
11026
  reusableLeaderPlans?: ReadonlyMap<
11009
11027
  string,
11010
11028
  Pick<ReusableReceiveCoordinatePlan<R>, "plan" | "replicas">
@@ -11020,11 +11038,17 @@ export class SharedLog<
11020
11038
  return;
11021
11039
  }
11022
11040
  const selfHash = this.node.identity.publicKey.hashcode();
11041
+ const freshReceiveRoleAge = options?.freshReceiveOwnerAudit
11042
+ ? await this.getDefaultMinRoleAge()
11043
+ : undefined;
11044
+ this.throwIfReplicationOwnershipLifecycleInactive(
11045
+ ownershipLifecycleController,
11046
+ );
11023
11047
  const plans = new Array<EntryLeaderPlan<R> | undefined>(entries.length);
11024
11048
  const leaderItems: Array<{
11025
11049
  entry: ShallowOrFullEntry<T>;
11026
11050
  replicas: number;
11027
- options: { roleAge: number; persist: false };
11051
+ options: LeaderSelectionOptions<R>;
11028
11052
  }> = [];
11029
11053
  const leaderItemIndexes: number[] = [];
11030
11054
  let reusableLeaderPlanHits = 0;
@@ -11033,7 +11057,9 @@ export class SharedLog<
11033
11057
  const replicas =
11034
11058
  options?.decodedReplicaCounts?.get(entry.hash) ??
11035
11059
  decodeReplicas(entry).getValue(this);
11036
- const reusablePlan = options?.reusableLeaderPlans?.get(entry.hash);
11060
+ const reusablePlan = options?.freshReceiveOwnerAudit
11061
+ ? undefined
11062
+ : options?.reusableLeaderPlans?.get(entry.hash);
11037
11063
  if (reusablePlan && reusablePlan.replicas === replicas) {
11038
11064
  plans[i] = reusablePlan.plan;
11039
11065
  reusableLeaderPlanHits++;
@@ -11042,7 +11068,9 @@ export class SharedLog<
11042
11068
  leaderItems.push({
11043
11069
  entry,
11044
11070
  replicas,
11045
- options: { roleAge: 0, persist: false },
11071
+ options: options?.freshReceiveOwnerAudit
11072
+ ? { roleAge: freshReceiveRoleAge!, persist: false }
11073
+ : { roleAge: 0, persist: false },
11046
11074
  });
11047
11075
  leaderItemIndexes.push(i);
11048
11076
  }
@@ -11116,6 +11144,7 @@ export class SharedLog<
11116
11144
  const loopStartedAt = syncProfileStart(options?.profile);
11117
11145
  let enqueuedPrune = 0;
11118
11146
  let cancelledLocalLeader = 0;
11147
+ let localLeaderResults = 0;
11119
11148
  for (let i = 0; i < entries.length; i++) {
11120
11149
  if (!this.isRepairLifecycleActive(ownershipLifecycleController)) {
11121
11150
  continue;
@@ -11124,11 +11153,14 @@ export class SharedLog<
11124
11153
  const leaders = plans[i]?.leaders ?? new Map();
11125
11154
 
11126
11155
  if (leaders.has(selfHash)) {
11127
- await this.cancelCheckedPruneForLocalLeader(entry.hash);
11128
- this.throwIfReplicationOwnershipLifecycleInactive(
11129
- ownershipLifecycleController,
11130
- );
11131
- cancelledLocalLeader++;
11156
+ if (!options?.preserveExistingPruneOnLocalResult) {
11157
+ await this.cancelCheckedPruneForLocalLeader(entry.hash);
11158
+ this.throwIfReplicationOwnershipLifecycleInactive(
11159
+ ownershipLifecycleController,
11160
+ );
11161
+ cancelledLocalLeader++;
11162
+ }
11163
+ localLeaderResults++;
11132
11164
  continue;
11133
11165
  }
11134
11166
 
@@ -11162,7 +11194,7 @@ export class SharedLog<
11162
11194
  entries: entries.length,
11163
11195
  count: enqueuedPrune,
11164
11196
  messages: 1,
11165
- details: { cancelledLocalLeader },
11197
+ details: { cancelledLocalLeader, localLeaderResults },
11166
11198
  });
11167
11199
  }
11168
11200
 
@@ -11482,9 +11514,11 @@ export class SharedLog<
11482
11514
  checkedPruneCoordinator.getRetry(hash) ??
11483
11515
  ({
11484
11516
  attempts: 0,
11517
+ generation: 0,
11485
11518
  entry: args.entry,
11486
11519
  leaders: args.leaders,
11487
11520
  } satisfies CheckedPruneRetryState<T, R>);
11521
+ state.generation++;
11488
11522
  state.entry = args.entry;
11489
11523
  state.leaders = args.leaders;
11490
11524
 
@@ -11507,6 +11541,7 @@ export class SharedLog<
11507
11541
 
11508
11542
  state.attempts = attempt;
11509
11543
  const timer = setTimeout(() => {
11544
+ const generation = state.generation;
11510
11545
  const run = async () => {
11511
11546
  const st = checkedPruneCoordinator.getRetry(hash);
11512
11547
  if (st !== state || state.timer !== timer || !isCurrent()) {
@@ -11516,6 +11551,7 @@ export class SharedLog<
11516
11551
  const isExactAttempt = () =>
11517
11552
  isCurrent() &&
11518
11553
  checkedPruneCoordinator.getRetry(hash) === state &&
11554
+ state.generation === generation &&
11519
11555
  state.timer === undefined;
11520
11556
  if (checkedPruneCoordinator.hasPendingDelete(hash)) return;
11521
11557
  const retryEntry = state.entry;
@@ -11567,25 +11603,31 @@ export class SharedLog<
11567
11603
  ) {
11568
11604
  // A keep policy is terminal for this background prune. Do not
11569
11605
  // retain a retry record with neither a timer nor pending work.
11570
- checkedPruneCoordinator.clearRetry(hash);
11606
+ checkedPruneCoordinator.clearRetry(hash, {
11607
+ state,
11608
+ generation,
11609
+ });
11571
11610
  }
11572
11611
  };
11573
11612
  void run().catch((error) => {
11574
11613
  if (isCurrent() && !isNotStartedError(error as Error)) {
11575
11614
  logger.error(error);
11576
- const retry = checkedPruneCoordinator.getRetry(hash);
11577
11615
  if (
11578
- retry === state &&
11579
- !retry.timer &&
11616
+ checkedPruneCoordinator.getRetry(hash) === state &&
11617
+ state.generation === generation &&
11618
+ !state.timer &&
11580
11619
  !checkedPruneCoordinator.hasPendingDelete(hash)
11581
11620
  ) {
11582
11621
  try {
11583
11622
  this.scheduleCheckedPruneRetry(
11584
- { entry: retry.entry, leaders: retry.leaders },
11623
+ { entry: state.entry, leaders: state.leaders },
11585
11624
  ownershipLifecycleController,
11586
11625
  );
11587
11626
  } catch {
11588
- checkedPruneCoordinator.clearRetry(hash);
11627
+ checkedPruneCoordinator.clearRetry(hash, {
11628
+ state,
11629
+ generation,
11630
+ });
11589
11631
  }
11590
11632
  }
11591
11633
  }
@@ -15689,6 +15731,8 @@ export class SharedLog<
15689
15731
  this._checkedPruneRemoveBlocksLocalRangeMutationAdmission = 0;
15690
15732
  this._checkedPruneRemovalCallbackInvocationDepth = 0;
15691
15733
  this._localReplicationRoleGeneration = 0;
15734
+ this._receiveOwnershipRevision = 0;
15735
+ this._receiveOwnershipMutationAdmissions = 0;
15692
15736
  this._pruneRemovesClosing = false;
15693
15737
  this._replicationRangeMutationFailure = undefined;
15694
15738
  this.startRepairLifecycle();
@@ -16123,7 +16167,9 @@ export class SharedLog<
16123
16167
  continue;
16124
16168
  }
16125
16169
  if (checkedPruneLeaders.localLeader) {
16126
- const retry = checkedPruneCoordinator.getRetry(hash);
16170
+ const retryIdentity =
16171
+ checkedPruneCoordinator.getRetryIdentity(hash);
16172
+ const retry = retryIdentity?.state;
16127
16173
  await this.cancelCheckedPruneForLocalLeader(hash, {
16128
16174
  preserveRetry: retry != null,
16129
16175
  });
@@ -16133,7 +16179,7 @@ export class SharedLog<
16133
16179
  if (retry) {
16134
16180
  // A delayed confirmation of local ownership is terminal.
16135
16181
  // Future ownership mutations will enqueue fresh work.
16136
- checkedPruneCoordinator.clearRetry(hash);
16182
+ checkedPruneCoordinator.clearRetry(hash, retryIdentity);
16137
16183
  } else {
16138
16184
  // A first positive view can be a transient membership
16139
16185
  // snapshot. Confirm it once after the normal retry delay
@@ -16182,6 +16228,15 @@ export class SharedLog<
16182
16228
  pruneOwnershipLifecycleController,
16183
16229
  );
16184
16230
  } catch {
16231
+ if (
16232
+ value.workToken != null &&
16233
+ checkedPruneCoordinator.isCandidateTokenCurrent(
16234
+ hash,
16235
+ value.workToken,
16236
+ )
16237
+ ) {
16238
+ checkedPruneCoordinator.invalidateCandidateToken(hash);
16239
+ }
16185
16240
  checkedPruneCoordinator.clearRetry(hash);
16186
16241
  }
16187
16242
  }
@@ -16982,7 +17037,7 @@ export class SharedLog<
16982
17037
  ) {
16983
17038
  const ownershipLifecycleController =
16984
17039
  this.captureReplicationOwnershipLifecycle();
16985
- return this.withReplicationRangeMutationQueue(async () => {
17040
+ return this.withReceiveOwnershipMutationQueue(async () => {
16986
17041
  this.throwIfReplicationOwnershipLifecycleInactive(
16987
17042
  ownershipLifecycleController,
16988
17043
  );
@@ -19157,6 +19212,9 @@ export class SharedLog<
19157
19212
  if (!releasePeerReceiveLease) {
19158
19213
  return;
19159
19214
  }
19215
+ const receiveOwnershipRevision = this._receiveOwnershipRevision;
19216
+ const receiveOwnershipLifecycleController =
19217
+ this.captureReplicationOwnershipLifecycle();
19160
19218
  const receiveReplicationInfoReceiveEpoch =
19161
19219
  this.getReplicationInfoReceiveEpoch(receiveFromHash);
19162
19220
  if (msg instanceof ResponseRoleMessage) {
@@ -19440,6 +19498,7 @@ export class SharedLog<
19440
19498
  heads,
19441
19499
  hashes,
19442
19500
  ),
19501
+ receiveOwnershipRevision,
19443
19502
  }),
19444
19503
  selectPreparedRawReceiveHashes: rawIsRepairHint
19445
19504
  ? undefined
@@ -19454,6 +19513,7 @@ export class SharedLog<
19454
19513
  heads,
19455
19514
  hashes,
19456
19515
  ),
19516
+ receiveOwnershipRevision,
19457
19517
  }),
19458
19518
  },
19459
19519
  );
@@ -20150,6 +20210,7 @@ export class SharedLog<
20150
20210
  !isReplicating &&
20151
20211
  !this.keep &&
20152
20212
  !isRepairHint &&
20213
+ this.isReceiveOwnershipSnapshotStable(receiveOwnershipRevision) &&
20153
20214
  receiveGroups.length > 0 &&
20154
20215
  receiveGroups.every(
20155
20216
  (group) =>
@@ -20894,11 +20955,60 @@ export class SharedLog<
20894
20955
  confirmedHashes.add(hash);
20895
20956
  }
20896
20957
  const checkedPruneStartedAt = syncProfileStart(syncProfile);
20897
- await this.pruneJoinedEntriesNoLongerLed(admittedShallowEntries, {
20898
- decodedReplicaCounts: receiveReplicaCounts,
20899
- reusableLeaderPlans: reusableCoordinatePlans,
20900
- profile: syncProfile,
20901
- });
20958
+ const ownershipChangedDuringReceive =
20959
+ !this.isReceiveOwnershipSnapshotStable(
20960
+ receiveOwnershipRevision,
20961
+ );
20962
+ if (ownershipChangedDuringReceive) {
20963
+ const freshAuditRevision = this._receiveOwnershipRevision;
20964
+ const armFreshAuditRetry = () => {
20965
+ for (const entry of admittedShallowEntries) {
20966
+ this.scheduleCheckedPruneRetry(
20967
+ { entry, leaders: new Map() },
20968
+ receiveOwnershipLifecycleController,
20969
+ );
20970
+ }
20971
+ };
20972
+ try {
20973
+ await this.pruneJoinedEntriesNoLongerLed(
20974
+ admittedShallowEntries,
20975
+ {
20976
+ decodedReplicaCounts: receiveReplicaCounts,
20977
+ freshReceiveOwnerAudit: true,
20978
+ preserveExistingPruneOnLocalResult: true,
20979
+ profile: syncProfile,
20980
+ },
20981
+ receiveOwnershipLifecycleController,
20982
+ );
20983
+ this.throwIfReplicationOwnershipLifecycleInactive(
20984
+ receiveOwnershipLifecycleController,
20985
+ );
20986
+ if (
20987
+ !this.isReceiveOwnershipSnapshotStable(freshAuditRevision)
20988
+ ) {
20989
+ armFreshAuditRetry();
20990
+ }
20991
+ } catch {
20992
+ // The lower-log and coordinate commits are already durable. A
20993
+ // sender retry will filter these hashes as present, so retain a
20994
+ // bounded local obligation instead of failing the admitted receive.
20995
+ this.throwIfReplicationOwnershipLifecycleInactive(
20996
+ receiveOwnershipLifecycleController,
20997
+ );
20998
+ armFreshAuditRetry();
20999
+ }
21000
+ } else {
21001
+ await this.pruneJoinedEntriesNoLongerLed(
21002
+ admittedShallowEntries,
21003
+ {
21004
+ decodedReplicaCounts: receiveReplicaCounts,
21005
+ preserveExistingPruneOnLocalResult: true,
21006
+ reusableLeaderPlans: reusableCoordinatePlans,
21007
+ profile: syncProfile,
21008
+ },
21009
+ receiveOwnershipLifecycleController,
21010
+ );
21011
+ }
20902
21012
  if (syncProfile) {
20903
21013
  emitSyncProfileDuration(syncProfile, checkedPruneStartedAt, {
20904
21014
  name: "sharedLog.receive.checkedPrune",
@@ -24490,8 +24600,10 @@ export class SharedLog<
24490
24600
  fromIsSelf: boolean;
24491
24601
  syncProfile?: SyncProfileFn;
24492
24602
  selection?: NativeBackboneRawReceiveSelectionPlan;
24603
+ receiveOwnershipRevision: number;
24493
24604
  }): Promise<boolean> {
24494
24605
  const backbone = this._nativeBackbone;
24606
+ const ownershipRevision = properties.receiveOwnershipRevision;
24495
24607
  if (!backbone || !this.syncronizer.onReceivedEntryHashes) {
24496
24608
  return false;
24497
24609
  }
@@ -24499,7 +24611,11 @@ export class SharedLog<
24499
24611
  const selection =
24500
24612
  properties.selection ??
24501
24613
  (await this.planNativePreparedRawReceiveSelection(properties));
24502
- if (!selection || selection.retainedHashes.length > 0) {
24614
+ if (
24615
+ !selection ||
24616
+ selection.retainedHashes.length > 0 ||
24617
+ !this.isReceiveOwnershipSnapshotStable(ownershipRevision)
24618
+ ) {
24503
24619
  return false;
24504
24620
  }
24505
24621
 
@@ -24534,6 +24650,9 @@ export class SharedLog<
24534
24650
  hashes: selection.droppedHashes,
24535
24651
  from: properties.from,
24536
24652
  });
24653
+ if (!this.isReceiveOwnershipSnapshotStable(ownershipRevision)) {
24654
+ return false;
24655
+ }
24537
24656
  if (properties.syncProfile) {
24538
24657
  emitSyncProfileDuration(properties.syncProfile, notifyStartedAt, {
24539
24658
  name: "sharedLog.receive.notifySynchronizer",
@@ -24572,7 +24691,9 @@ export class SharedLog<
24572
24691
  fromIsSelf: boolean;
24573
24692
  syncProfile?: SyncProfileFn;
24574
24693
  selection?: NativeBackboneRawReceiveSelectionPlan;
24694
+ receiveOwnershipRevision: number;
24575
24695
  }): Promise<RawReceiveHashSelection | undefined> {
24696
+ const ownershipRevision = properties.receiveOwnershipRevision;
24576
24697
  if (!this.syncronizer.onReceivedEntryHashes) {
24577
24698
  return undefined;
24578
24699
  }
@@ -24580,7 +24701,10 @@ export class SharedLog<
24580
24701
  const selection =
24581
24702
  properties.selection ??
24582
24703
  (await this.planNativePreparedRawReceiveSelection(properties));
24583
- if (!selection) {
24704
+ if (
24705
+ !selection ||
24706
+ !this.isReceiveOwnershipSnapshotStable(ownershipRevision)
24707
+ ) {
24584
24708
  return undefined;
24585
24709
  }
24586
24710
  if (selection.droppedHashes.length === 0) {
@@ -24598,6 +24722,9 @@ export class SharedLog<
24598
24722
  hashes: selection.droppedHashes,
24599
24723
  from: properties.from,
24600
24724
  });
24725
+ if (!this.isReceiveOwnershipSnapshotStable(ownershipRevision)) {
24726
+ return undefined;
24727
+ }
24601
24728
  emitSyncProfileDuration(properties.syncProfile, notifyStartedAt, {
24602
24729
  name: "sharedLog.receive.notifySynchronizer",
24603
24730
  component: "shared-log",
@@ -25465,6 +25592,7 @@ export class SharedLog<
25465
25592
  private withReplicationRangeMutationQueue<T>(
25466
25593
  fn: () => Promise<T>,
25467
25594
  replicationOwnershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
25595
+ options?: { affectsReceiveOwnership?: boolean },
25468
25596
  ): Promise<T> {
25469
25597
  try {
25470
25598
  this.throwIfReplicationOwnershipLifecycleInactive(
@@ -25480,6 +25608,15 @@ export class SharedLog<
25480
25608
  ),
25481
25609
  );
25482
25610
  }
25611
+ let releaseReceiveOwnershipMutationAdmission: (() => void) | undefined;
25612
+ if (options?.affectsReceiveOwnership) {
25613
+ this._receiveOwnershipMutationAdmissions++;
25614
+ this.invalidateLeaderSelectionContextCache();
25615
+ releaseReceiveOwnershipMutationAdmission = () => {
25616
+ this._receiveOwnershipMutationAdmissions--;
25617
+ this.invalidateLeaderSelectionContextCache();
25618
+ };
25619
+ }
25483
25620
  const run = (this._replicationRangeMutationTail ?? Promise.resolve())
25484
25621
  .catch(() => {
25485
25622
  // A failed predecessor must not leave the queue permanently rejected.
@@ -25493,11 +25630,25 @@ export class SharedLog<
25493
25630
  );
25494
25631
  return fn();
25495
25632
  });
25496
- this._replicationRangeMutationTail = run.then(
25633
+ const fencedRun = releaseReceiveOwnershipMutationAdmission
25634
+ ? run.finally(releaseReceiveOwnershipMutationAdmission)
25635
+ : run;
25636
+ this._replicationRangeMutationTail = fencedRun.then(
25497
25637
  () => undefined,
25498
25638
  () => undefined,
25499
25639
  );
25500
- return run;
25640
+ return fencedRun;
25641
+ }
25642
+
25643
+ private withReceiveOwnershipMutationQueue<T>(
25644
+ fn: () => Promise<T>,
25645
+ replicationOwnershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
25646
+ ): Promise<T> {
25647
+ return this.withReplicationRangeMutationQueue(
25648
+ fn,
25649
+ replicationOwnershipLifecycleController,
25650
+ { affectsReceiveOwnership: true },
25651
+ );
25501
25652
  }
25502
25653
 
25503
25654
  private acquireReplicationRangeMutationTerminalFence(): {
@@ -26428,12 +26579,13 @@ export class SharedLog<
26428
26579
  const restartCandidates: Array<{
26429
26580
  candidate: CheckedPruneRestartCandidate<T, R>;
26430
26581
  reservation: object;
26431
- retry?: CheckedPruneRetryState<T, R>;
26582
+ retryIdentity?: CheckedPruneRetryIdentity<T, R>;
26432
26583
  }> = [];
26433
26584
  for (const { hash } of admitted) {
26434
26585
  const candidate = checkedPruneCoordinator.getRestartCandidate(hash);
26435
26586
  const pending = checkedPruneCoordinator.getPendingDelete(hash);
26436
- const retry = checkedPruneCoordinator.getRetry(hash);
26587
+ const retryIdentity = checkedPruneCoordinator.getRetryIdentity(hash);
26588
+ const retry = retryIdentity?.state;
26437
26589
  const hadQueuedCandidate = this.pruneDebouncedFn.has(hash);
26438
26590
  const hadInFlightCandidate = checkedPruneCoordinator.hasCandidate(hash);
26439
26591
  const shouldRestart =
@@ -26476,7 +26628,7 @@ export class SharedLog<
26476
26628
  restartCandidates.push({
26477
26629
  candidate,
26478
26630
  reservation: checkedPruneCoordinator.reserveRestart(hash),
26479
- retry,
26631
+ retryIdentity,
26480
26632
  });
26481
26633
  }
26482
26634
  }
@@ -26512,7 +26664,7 @@ export class SharedLog<
26512
26664
  for (const {
26513
26665
  candidate,
26514
26666
  reservation,
26515
- retry,
26667
+ retryIdentity,
26516
26668
  } of admission.restartCandidates) {
26517
26669
  if (
26518
26670
  this._checkedPrune !== checkedPruneCoordinator ||
@@ -26522,11 +26674,8 @@ export class SharedLog<
26522
26674
  candidate.hash,
26523
26675
  reservation,
26524
26676
  );
26525
- if (
26526
- retry &&
26527
- checkedPruneCoordinator.getRetry(candidate.hash) === retry
26528
- ) {
26529
- checkedPruneCoordinator.clearRetry(candidate.hash);
26677
+ if (retryIdentity) {
26678
+ checkedPruneCoordinator.clearRetry(candidate.hash, retryIdentity);
26530
26679
  }
26531
26680
  continue;
26532
26681
  }