@peerbit/shared-log 16.0.2 → 16.0.4

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/dist/src/index.js CHANGED
@@ -652,7 +652,58 @@ let SharedLog = (() => {
652
652
  uniqueReplicators;
653
653
  _replicatorJoinEmitted;
654
654
  /* private _totalParticipation!: number; */
655
- // gid -> coordinate -> publicKeyHash list (of owners)
655
+ // gid -> set of publicKeyHashes known to hold that gid's entries.
656
+ //
657
+ // This is a suppression memo, not a source of truth. A present row lets the
658
+ // rebalance and repair paths skip re-sending an entry to a peer that already
659
+ // has it. Every read is `?.has(peer)` guarded, and a MISSING row always
660
+ // means "assume nothing is known", which produces strictly MORE work --
661
+ // redundant unchecked delivery in the rebalance loop, redundant queueing in
662
+ // the repair planner -- and never a wrong prune, a wrong quorum, or data
663
+ // loss. Losing a row costs bandwidth; keeping a stale row costs a little
664
+ // memory. That asymmetry is what the rest of this note turns on.
665
+ //
666
+ // GROWTH SHAPE. Rows are released by `deleteGidPeerHistory` on the two prune
667
+ // paths, by `removePeerFromGidPeerHistory` once a gid's last peer drops (the
668
+ // routine disconnect outcome), by `rebalanceAll({ clearCache: true })`, and
669
+ // wholesale on close/reset. Nothing on the TRIM path releases a row, so a
670
+ // node that bounds its log with trim rather than prune accumulates one row
671
+ // per distinct gid it has ever held. A gid names a graph, not an entry: an
672
+ // entry with `meta.next` inherits `min(next.meta.gid)` (see
673
+ // packages/log/src/entry-v0.ts), so document updates fold into the gid of
674
+ // the first put and the row count tracks distinct chain roots -- distinct
675
+ // document ids -- rather than entry count. Insert-only workloads mint a
676
+ // fresh gid per append and so do grow one row per entry. Merges are a
677
+ // smaller second source: when a join links two graphs the losing entries
678
+ // keep their own `meta.gid` on disk, and shared-log does not subscribe to
679
+ // the log's `onGidRemoved`, so the shadowed gid's row lingers too.
680
+ //
681
+ // WHY TRIM DOES NOT SIMPLY CALL `deleteGidPeerHistory` AS WELL. Both prune
682
+ // callers delete a whole row from a single entry's gid, and under the
683
+ // default hash domain that is correct by construction: the coordinate is a
684
+ // pure function of the gid (replication-domain-hash.ts sha256s
685
+ // `entry.meta.gid`), so identical gid => identical coordinates => identical
686
+ // leader set => every local sibling of that gid is prune-eligible in the
687
+ // same batch. The gid really is finished locally. Trim offers no such
688
+ // guarantee. It walks oldest-first against a length/bytelength/age bound and
689
+ // stops the instant the bound is met (packages/log/src/trim.ts); its only
690
+ // use of gid is memoizing the caller's `canTrim` verdict, never grouping
691
+ // deletes. So trim routinely removes the OLDEST entry of a gid while newer
692
+ // siblings -- same gid, same coordinates, still local, still replicated --
693
+ // remain. Copying the prune call onto trim would therefore delete a LIVE row
694
+ // on the common path, paying for the freed memory in repeated re-delivery of
695
+ // entries that are still here. That trade is not worth it.
696
+ //
697
+ // Bounding this correctly requires a per-gid count of locally held entries,
698
+ // dropping the row only when it reaches zero -- a real reverse index, not a
699
+ // one-line delete. Deliberately not built: the growth is bounded by distinct
700
+ // gids, and the cheap version is a bandwidth regression.
701
+ //
702
+ // Existing, deliberate imprecision: under the time domain the coordinate is
703
+ // `meta.clock.timestamp.wallTime` (replication-domain-time.ts) and is
704
+ // gid-independent, so siblings of one gid can carry different leader sets
705
+ // and prune's whole-row delete is already over-eager there. The cost is the
706
+ // same bounded extra traffic, never a wrong prune.
656
707
  _gidPeersHistory;
657
708
  _onSubscriptionFn;
658
709
  _onUnsubscriptionFn;
@@ -1518,8 +1569,23 @@ let SharedLog = (() => {
1518
1569
  };
1519
1570
  for (const coordinate of intent.coordinates) {
1520
1571
  rollback.hashes.add(coordinate.hash);
1521
- const generation = (mutationGenerations.get(coordinate.hash) ?? 0) + 1;
1522
- mutationGenerations.set(coordinate.hash, generation);
1572
+ // Same hold-counted row shape the coordinator's own
1573
+ // snapshot writes: one hold per hash, released by the
1574
+ // settle after the replay consumes the token below.
1575
+ // RELIES ON `intent.coordinates` HOLDING UNIQUE HASHES —
1576
+ // it is built from the token's `hashes` Set (see
1577
+ // setNativeStrictDurableTransactionOperation). Holds are
1578
+ // taken per element here but released per unique hash by
1579
+ // the settle, so a duplicate would take two and release
1580
+ // one and retain that row forever. That is the safe
1581
+ // direction (a retained row, never a fail-open rollback),
1582
+ // but keep the source a Set.
1583
+ const row = mutationGenerations.get(coordinate.hash);
1584
+ const generation = (row?.generation ?? 0) + 1;
1585
+ mutationGenerations.set(coordinate.hash, {
1586
+ generation,
1587
+ holds: (row?.holds ?? 0) + 1,
1588
+ });
1523
1589
  rollback.generations.set(coordinate.hash, generation);
1524
1590
  if (coordinate.value) {
1525
1591
  const number = (value) => (this.domain.resolution === "u32"
@@ -1537,6 +1603,9 @@ let SharedLog = (() => {
1537
1603
  }
1538
1604
  }
1539
1605
  await this._coordinates.rollbackNativeBackboneCoordinateAppendDurably("", rollback);
1606
+ // The replay fabricated these generations itself one turn
1607
+ // earlier and has now consumed them, so the rows are dead.
1608
+ this._coordinates.settleResidentCoordinateSnapshot(rollback);
1540
1609
  }
1541
1610
  for (const document of intent.documents) {
1542
1611
  this.restoreNativeBackboneDocument({
@@ -7081,10 +7150,15 @@ let SharedLog = (() => {
7081
7150
  return rollbackLowerPublication(error);
7082
7151
  }
7083
7152
  if (!result) {
7153
+ // Abandon arm: the token was minted but nothing downstream can
7154
+ // roll it back from here.
7155
+ this._coordinates.settleResidentCoordinateSnapshot(lowerPublicationRollback?.coordinateEntries);
7084
7156
  return this.completeNativeStrictDurableTransaction(nativeStrictTransaction).then(() => undefined);
7085
7157
  }
7086
7158
  return mapMaybePromise(result, async (prepared) => {
7087
7159
  if (!prepared) {
7160
+ // Abandon arm: same shape as the `!result` arm above.
7161
+ this._coordinates.settleResidentCoordinateSnapshot(lowerPublicationRollback?.coordinateEntries);
7088
7162
  await this.completeNativeStrictDurableTransaction(nativeStrictTransaction);
7089
7163
  return undefined;
7090
7164
  }
@@ -7105,6 +7179,9 @@ let SharedLog = (() => {
7105
7179
  }
7106
7180
  try {
7107
7181
  await this._coordinates.rollbackNativeBackboneCoordinateAppendDurably(prepared.appendFacts.hash, lowerPublicationRollback?.coordinateEntries);
7182
+ // Terminal: this is the last rollback consumer for the
7183
+ // token. A throw above leaves the row, which is safe.
7184
+ this._coordinates.settleResidentCoordinateSnapshot(lowerPublicationRollback?.coordinateEntries);
7108
7185
  for (const document of lowerPublicationRollback?.documents ?? []) {
7109
7186
  this.restoreNativeBackboneDocument(document);
7110
7187
  }
@@ -7164,6 +7241,12 @@ let SharedLog = (() => {
7164
7241
  catch (error) {
7165
7242
  return rollback(error);
7166
7243
  }
7244
+ // Success seam. The last await inside the protected try is the
7245
+ // finalizer acknowledge; `finish()` is synchronous, so no async
7246
+ // boundary separates the catch above from this statement and
7247
+ // `rollback` can no longer fire. Nothing downstream rolls back
7248
+ // (the retire below only warns), so the token is terminal here.
7249
+ this._coordinates.settleResidentCoordinateSnapshot(lowerPublicationRollback?.coordinateEntries);
7167
7250
  this.applyPreparedAppendFactsWithDeferredCoordinateDeletes(prepared.appendFacts, prepared.removed, prepared.materializeEntry, { removedHashes: prepared.removedHashes });
7168
7251
  try {
7169
7252
  await this.completeNativeStrictDurableTransaction(nativeStrictTransaction);
@@ -7466,6 +7549,10 @@ let SharedLog = (() => {
7466
7549
  }
7467
7550
  return mapMaybePromise(result, async (prepared) => {
7468
7551
  if (!prepared || !backboneAppend) {
7552
+ // Abandon arm: the token was minted inside
7553
+ // `prepareBackboneAppend` and nothing downstream of
7554
+ // this return can roll it back.
7555
+ this._coordinates.settleResidentCoordinateSnapshot(nativeCoordinateRollback);
7469
7556
  await this.completeNativeStrictDurableTransaction(nativeStrictTransaction);
7470
7557
  return undefined;
7471
7558
  }
@@ -7533,6 +7620,8 @@ let SharedLog = (() => {
7533
7620
  }
7534
7621
  try {
7535
7622
  await this._coordinates.rollbackNativeBackboneCoordinateAppendDurably(prepared.appendFacts.hash, rollbackCoordinateEntries);
7623
+ // Terminal: last rollback consumer for the token.
7624
+ this._coordinates.settleResidentCoordinateSnapshot(rollbackCoordinateEntries);
7536
7625
  }
7537
7626
  catch (rollbackError) {
7538
7627
  rollbackFailures.push(rollbackError);
@@ -7594,6 +7683,10 @@ let SharedLog = (() => {
7594
7683
  catch (error) {
7595
7684
  return rollback(error);
7596
7685
  }
7686
+ // Success seam: the finalizer acknowledge above is the last
7687
+ // await inside the protected try, so `rollback` can no
7688
+ // longer fire and the retire below only warns.
7689
+ this._coordinates.settleResidentCoordinateSnapshot(rollbackCoordinateEntries);
7597
7690
  this.applyPreparedAppendFactsWithDeferredCoordinateDeletes(prepared.appendFacts, prepared.removed, prepared.materializeEntry, {
7598
7691
  forgetNativeCoordinates: false,
7599
7692
  removedHashes: prepared.removedHashes,
@@ -8148,6 +8241,8 @@ let SharedLog = (() => {
8148
8241
  throw error;
8149
8242
  }
8150
8243
  if (!appended || !backboneAppends) {
8244
+ // Abandon arm: no consumer downstream of this return.
8245
+ this._coordinates.settleResidentCoordinateSnapshot(batchCoordinateRollback);
8151
8246
  await this.completeNativeStrictDurableTransaction(nativeStrictTransaction);
8152
8247
  this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
8153
8248
  return undefined;
@@ -8170,6 +8265,8 @@ let SharedLog = (() => {
8170
8265
  }
8171
8266
  try {
8172
8267
  await this._coordinates.rollbackNativeBackboneCoordinateAppendDurably(appended.appendFacts[0]?.hash ?? "", batchCoordinateRollback);
8268
+ // Terminal: last rollback consumer for the batch token.
8269
+ this._coordinates.settleResidentCoordinateSnapshot(batchCoordinateRollback);
8173
8270
  }
8174
8271
  catch (rollbackError) {
8175
8272
  rollbackFailures.push(rollbackError);
@@ -8260,6 +8357,9 @@ let SharedLog = (() => {
8260
8357
  catch (error) {
8261
8358
  return rollbackBatch(error);
8262
8359
  }
8360
+ // Success seam: `rollbackBatch` has exactly one call site (the catch
8361
+ // above), and everything from here on escapes without any rollback.
8362
+ this._coordinates.settleResidentCoordinateSnapshot(batchCoordinateRollback);
8263
8363
  this.throwIfReplicationOwnershipLifecycleInactive(ownershipLifecycleController);
8264
8364
  const appendCommits = [];
8265
8365
  for (let i = 0; i < coordinateRows.length; i++) {
@@ -12925,221 +13025,233 @@ let SharedLog = (() => {
12925
13025
  const nativeReceiveCoordinateBatch = canUsePreparedAppendFacts
12926
13026
  ? this._coordinates.createBackboneOnlyReceiveCoordinateBatch(reusableCoordinatePersistItems)
12927
13027
  : undefined;
12928
- const nativePreparedJoinCommit = canUsePreparedAppendFacts
12929
- ? this._coordinates.createNativeBackbonePreparedJoinCommit(nativeReceiveCoordinateBatch, (batch) => {
12930
- nativePreparedCoordinateBatch = batch;
12931
- }, nativeCommitVerifyHashes, nativeCommitVerifyAllHashes, syncProfile, (committedHashes) => {
12932
- nativePreparedCommittedHashes = new Set(committedHashes);
12933
- })
12934
- : undefined;
12935
- const finishNativePreparedCoordinates = async (properties) => {
12936
- if (!properties.nativePreparedCommitted ||
12937
- !nativePreparedCoordinateBatch) {
12938
- return;
12939
- }
12940
- try {
12941
- nativeBackboneOnlyPersistedHashes =
12942
- await this._coordinates.finishBackboneOnlyReceiveCoordinateBatch(nativePreparedCoordinateBatch, syncProfile);
12943
- nativePreparedCoordinatesFinished = true;
12944
- }
12945
- catch (error) {
12946
- this._coordinates.rollbackBackboneOnlyReceiveCoordinateBatch(nativePreparedCoordinateBatch);
12947
- throw error;
13028
+ try {
13029
+ const nativePreparedJoinCommit = canUsePreparedAppendFacts
13030
+ ? this._coordinates.createNativeBackbonePreparedJoinCommit(nativeReceiveCoordinateBatch, (batch) => {
13031
+ nativePreparedCoordinateBatch = batch;
13032
+ }, nativeCommitVerifyHashes, nativeCommitVerifyAllHashes, syncProfile, (committedHashes) => {
13033
+ nativePreparedCommittedHashes = new Set(committedHashes);
13034
+ })
13035
+ : undefined;
13036
+ const finishNativePreparedCoordinates = async (properties) => {
13037
+ if (!properties.nativePreparedCommitted ||
13038
+ !nativePreparedCoordinateBatch) {
13039
+ return;
13040
+ }
13041
+ try {
13042
+ nativeBackboneOnlyPersistedHashes =
13043
+ await this._coordinates.finishBackboneOnlyReceiveCoordinateBatch(nativePreparedCoordinateBatch, syncProfile);
13044
+ nativePreparedCoordinatesFinished = true;
13045
+ }
13046
+ catch (error) {
13047
+ this._coordinates.rollbackBackboneOnlyReceiveCoordinateBatch(nativePreparedCoordinateBatch);
13048
+ throw error;
13049
+ }
13050
+ };
13051
+ const preparedAppendCanValidateAppend = canAppendAlreadyValidated ||
13052
+ (nativeCommitCanValidateAppend && !!nativePreparedJoinCommit);
13053
+ if (!preparedAppendCanValidateAppend) {
13054
+ canUsePreparedAppendFacts = false;
12948
13055
  }
12949
- };
12950
- const preparedAppendCanValidateAppend = canAppendAlreadyValidated ||
12951
- (nativeCommitCanValidateAppend && !!nativePreparedJoinCommit);
12952
- if (!preparedAppendCanValidateAppend) {
12953
- canUsePreparedAppendFacts = false;
12954
- }
12955
- const nativePreparedJoinCommitValidatesPlan = !!nativePreparedJoinCommit &&
12956
- (nativeCommitVerifyHashes && nativeCommitVerifyHashes.length > 0
12957
- ? nativeCommitVerifyAllHashes
12958
- ? !!this._nativeBackbone?.graph
12959
- .commitVerifiedAllPreparedRawReceiveJoinBatch ||
12960
- !!this._nativeBackbone?.graph
13056
+ const nativePreparedJoinCommitValidatesPlan = !!nativePreparedJoinCommit &&
13057
+ (nativeCommitVerifyHashes && nativeCommitVerifyHashes.length > 0
13058
+ ? nativeCommitVerifyAllHashes
13059
+ ? !!this._nativeBackbone?.graph
13060
+ .commitVerifiedAllPreparedRawReceiveJoinBatch ||
13061
+ !!this._nativeBackbone?.graph
13062
+ .commitVerifiedPreparedRawReceiveJoinBatch
13063
+ : !!this._nativeBackbone?.graph
12961
13064
  .commitVerifiedPreparedRawReceiveJoinBatch
12962
13065
  : !!this._nativeBackbone?.graph
12963
- .commitVerifiedPreparedRawReceiveJoinBatch
12964
- : !!this._nativeBackbone?.graph
12965
- .commitPreparedRawReceiveJoinBatch);
12966
- const trustedLowerLog = this.log;
12967
- // With a program-level onChange consumer the hash-only
12968
- // sink is not used: the lower-log join dispatches the
12969
- // change event (lazy entry views over the prepared raw
12970
- // facts) so per-entry consumers observe every commit.
12971
- const joinOnAppendHashes = programOnChange
12972
- ? undefined
12973
- : onAppendHashes;
12974
- const joinedPreparedFacts = canUsePreparedAppendFacts &&
12975
- (await trustedLowerLog.joinPreparedAppendFactsBatch(preparedAppendFacts, {
12976
- __peerbitEntriesAlreadyMissing: true,
12977
- __peerbitCanAppendAlreadyValidated: true,
12978
- __peerbitDeferIndexWrite: true,
12979
- __peerbitOnAppendHashes: joinOnAppendHashes,
12980
- __peerbitProfile: syncProfile,
12981
- __peerbitNativePreparedJoinCommit: nativePreparedJoinCommit,
12982
- __peerbitNativePreparedJoinCommitValidatesPlan: nativePreparedJoinCommitValidatesPlan,
12983
- __peerbitOnPreparedJoinCommitted: nativePreparedJoinCommit
12984
- ? finishNativePreparedCoordinates
12985
- : undefined,
12986
- }));
12987
- if (!joinedPreparedFacts) {
12988
- await trustedLowerLog.join(materializeAllToMergeEntries(), {
12989
- __peerbitBatchIndependent: true,
12990
- __peerbitEntriesAlreadyMissing: true,
12991
- __peerbitCanAppendAlreadyValidated: fallbackCanAppendAlreadyValidated,
12992
- __peerbitDeferIndexWrite: true,
12993
- __peerbitOnAppendHashes: joinOnAppendHashes,
12994
- __peerbitProfile: syncProfile,
12995
- });
12996
- }
12997
- // A recursive lower-log join can resolve successfully while declining
12998
- // an individual top-level entry (for example, when one of its parents
12999
- // is temporarily unavailable). The public Log.join() API intentionally
13000
- // does not expose that per-entry result, so make local index presence the
13001
- // authority before publishing any SharedLog-side effects. A successful
13002
- // prepared-facts batch is atomic and already proves every input hash.
13003
- const admittedHashes = joinedPreparedFacts
13004
- ? new Set(allToMergeHashes)
13005
- : await this.log.hasMany(allToMergeHashes);
13006
- admittedMergeHashes = admittedHashes;
13007
- const admittedShallowEntries = admittedHashes.size === allToMergeShallowEntries.length
13008
- ? allToMergeShallowEntries
13009
- : allToMergeShallowEntries.filter((entry) => admittedHashes.has(entry.hash));
13010
- if (!joinedPreparedFacts) {
13011
- reusableCoordinatePersistItems =
13012
- reusableCoordinatePersistItems.filter((item) => admittedHashes.has(item.entry.hash));
13013
- coordinatePersistFallbackEntries =
13014
- coordinatePersistFallbackEntries.filter((entry) => admittedHashes.has(entry.hash));
13015
- }
13016
- const reusableCoordinatePersistItemCount = reusableCoordinatePersistItems.length;
13017
- if (syncProfile) {
13018
- emitSyncProfileDuration(syncProfile, lowerLogJoinStartedAt, {
13019
- name: "sharedLog.receive.lowerLogJoin",
13020
- component: "shared-log",
13021
- entries: allToMerge.length,
13022
- messages: 1,
13023
- details: {
13024
- hashOnlyEntryAdded,
13025
- batchHashOnlyEntryAdded,
13026
- programOnChange,
13027
- joinedPreparedFacts,
13028
- admittedEntries: admittedHashes.size,
13029
- nativePreparedCoordinatesFinished,
13030
- },
13031
- });
13032
- }
13033
- const coordinatePersistStartedAt = syncProfileStart(syncProfile);
13034
- if (nativePreparedCoordinatesFinished) {
13035
- // The lower-log prepared receive transaction already finished
13036
- // the native coordinate mirror/journal after entry-index commit.
13037
- }
13038
- else if (nativePreparedCoordinateBatch) {
13039
- try {
13040
- nativeBackboneOnlyPersistedHashes =
13041
- await this._coordinates.finishBackboneOnlyReceiveCoordinateBatch(nativePreparedCoordinateBatch, syncProfile);
13066
+ .commitPreparedRawReceiveJoinBatch);
13067
+ const trustedLowerLog = this.log;
13068
+ // With a program-level onChange consumer the hash-only
13069
+ // sink is not used: the lower-log join dispatches the
13070
+ // change event (lazy entry views over the prepared raw
13071
+ // facts) so per-entry consumers observe every commit.
13072
+ const joinOnAppendHashes = programOnChange
13073
+ ? undefined
13074
+ : onAppendHashes;
13075
+ const joinedPreparedFacts = canUsePreparedAppendFacts &&
13076
+ (await trustedLowerLog.joinPreparedAppendFactsBatch(preparedAppendFacts, {
13077
+ __peerbitEntriesAlreadyMissing: true,
13078
+ __peerbitCanAppendAlreadyValidated: true,
13079
+ __peerbitDeferIndexWrite: true,
13080
+ __peerbitOnAppendHashes: joinOnAppendHashes,
13081
+ __peerbitProfile: syncProfile,
13082
+ __peerbitNativePreparedJoinCommit: nativePreparedJoinCommit,
13083
+ __peerbitNativePreparedJoinCommitValidatesPlan: nativePreparedJoinCommitValidatesPlan,
13084
+ __peerbitOnPreparedJoinCommitted: nativePreparedJoinCommit
13085
+ ? finishNativePreparedCoordinates
13086
+ : undefined,
13087
+ }));
13088
+ if (!joinedPreparedFacts) {
13089
+ await trustedLowerLog.join(materializeAllToMergeEntries(), {
13090
+ __peerbitBatchIndependent: true,
13091
+ __peerbitEntriesAlreadyMissing: true,
13092
+ __peerbitCanAppendAlreadyValidated: fallbackCanAppendAlreadyValidated,
13093
+ __peerbitDeferIndexWrite: true,
13094
+ __peerbitOnAppendHashes: joinOnAppendHashes,
13095
+ __peerbitProfile: syncProfile,
13096
+ });
13042
13097
  }
13043
- catch (error) {
13044
- this._coordinates.rollbackBackboneOnlyReceiveCoordinateBatch(nativePreparedCoordinateBatch);
13045
- throw error;
13098
+ // A recursive lower-log join can resolve successfully while declining
13099
+ // an individual top-level entry (for example, when one of its parents
13100
+ // is temporarily unavailable). The public Log.join() API intentionally
13101
+ // does not expose that per-entry result, so make local index presence the
13102
+ // authority before publishing any SharedLog-side effects. A successful
13103
+ // prepared-facts batch is atomic and already proves every input hash.
13104
+ const admittedHashes = joinedPreparedFacts
13105
+ ? new Set(allToMergeHashes)
13106
+ : await this.log.hasMany(allToMergeHashes);
13107
+ admittedMergeHashes = admittedHashes;
13108
+ const admittedShallowEntries = admittedHashes.size === allToMergeShallowEntries.length
13109
+ ? allToMergeShallowEntries
13110
+ : allToMergeShallowEntries.filter((entry) => admittedHashes.has(entry.hash));
13111
+ if (!joinedPreparedFacts) {
13112
+ reusableCoordinatePersistItems =
13113
+ reusableCoordinatePersistItems.filter((item) => admittedHashes.has(item.entry.hash));
13114
+ coordinatePersistFallbackEntries =
13115
+ coordinatePersistFallbackEntries.filter((entry) => admittedHashes.has(entry.hash));
13046
13116
  }
13047
- }
13048
- else {
13049
- nativeBackboneOnlyPersistedHashes =
13050
- await this._coordinates.persistBackboneOnlyReceiveCoordinateBatch(reusableCoordinatePersistItems);
13051
- }
13052
- if (nativeBackboneOnlyPersistedHashes &&
13053
- nativeBackboneOnlyPersistedHashes.size > 0) {
13054
- for (let i = reusableCoordinatePersistItems.length - 1; i >= 0; i--) {
13055
- if (nativeBackboneOnlyPersistedHashes.has(reusableCoordinatePersistItems[i].entry.hash)) {
13056
- reusableCoordinatePersistItems.splice(i, 1);
13117
+ const reusableCoordinatePersistItemCount = reusableCoordinatePersistItems.length;
13118
+ if (syncProfile) {
13119
+ emitSyncProfileDuration(syncProfile, lowerLogJoinStartedAt, {
13120
+ name: "sharedLog.receive.lowerLogJoin",
13121
+ component: "shared-log",
13122
+ entries: allToMerge.length,
13123
+ messages: 1,
13124
+ details: {
13125
+ hashOnlyEntryAdded,
13126
+ batchHashOnlyEntryAdded,
13127
+ programOnChange,
13128
+ joinedPreparedFacts,
13129
+ admittedEntries: admittedHashes.size,
13130
+ nativePreparedCoordinatesFinished,
13131
+ },
13132
+ });
13133
+ }
13134
+ const coordinatePersistStartedAt = syncProfileStart(syncProfile);
13135
+ if (nativePreparedCoordinatesFinished) {
13136
+ // The lower-log prepared receive transaction already finished
13137
+ // the native coordinate mirror/journal after entry-index commit.
13138
+ }
13139
+ else if (nativePreparedCoordinateBatch) {
13140
+ try {
13141
+ nativeBackboneOnlyPersistedHashes =
13142
+ await this._coordinates.finishBackboneOnlyReceiveCoordinateBatch(nativePreparedCoordinateBatch, syncProfile);
13143
+ }
13144
+ catch (error) {
13145
+ this._coordinates.rollbackBackboneOnlyReceiveCoordinateBatch(nativePreparedCoordinateBatch);
13146
+ throw error;
13057
13147
  }
13058
13148
  }
13059
- }
13060
- if (reusableCoordinatePersistItems.length > 0) {
13061
- await this._coordinates.persistCoordinatesBatch(reusableCoordinatePersistItems);
13062
- }
13063
- if (coordinatePersistFallbackEntries.length > 0) {
13064
- await this.planEntryLeaderBatch(coordinatePersistFallbackEntries.map((entry) => ({
13065
- entry,
13066
- replicas: receiveReplicaCounts.get(entry.hash) ??
13067
- decodeReplicas(entry).getValue(this),
13068
- options: { roleAge: 0, persist: {} },
13069
- })));
13070
- }
13071
- if (syncProfile) {
13072
- emitSyncProfileDuration(syncProfile, coordinatePersistStartedAt, {
13073
- name: "sharedLog.receive.coordinatePersist",
13074
- component: "shared-log",
13075
- entries: entriesToPersist.length,
13076
- messages: 1,
13077
- details: {
13078
- reusedLeaderPlans: reusableCoordinatePersistItemCount,
13079
- nativeBackboneOnly: nativeBackboneOnlyPersistedHashes?.size ?? 0,
13080
- },
13081
- });
13082
- }
13083
- for (const hash of admittedHashes) {
13084
- confirmedHashes.add(hash);
13085
- }
13086
- const checkedPruneStartedAt = syncProfileStart(syncProfile);
13087
- const ownershipChangedDuringReceive = !this.isReceiveOwnershipSnapshotStable(receiveOwnershipRevision);
13088
- if (ownershipChangedDuringReceive) {
13089
- const freshAuditRevision = this._instanceLifecycle?._receiveOwnershipRevision ?? 0;
13090
- const armFreshAuditRetry = () => {
13091
- for (const entry of admittedShallowEntries) {
13092
- this.scheduleCheckedPruneRetry({ entry, leaders: new Map() }, receiveOwnershipLifecycleController);
13149
+ else {
13150
+ nativeBackboneOnlyPersistedHashes =
13151
+ await this._coordinates.persistBackboneOnlyReceiveCoordinateBatch(reusableCoordinatePersistItems);
13152
+ }
13153
+ if (nativeBackboneOnlyPersistedHashes &&
13154
+ nativeBackboneOnlyPersistedHashes.size > 0) {
13155
+ for (let i = reusableCoordinatePersistItems.length - 1; i >= 0; i--) {
13156
+ if (nativeBackboneOnlyPersistedHashes.has(reusableCoordinatePersistItems[i].entry.hash)) {
13157
+ reusableCoordinatePersistItems.splice(i, 1);
13158
+ }
13093
13159
  }
13094
- };
13095
- try {
13160
+ }
13161
+ if (reusableCoordinatePersistItems.length > 0) {
13162
+ await this._coordinates.persistCoordinatesBatch(reusableCoordinatePersistItems);
13163
+ }
13164
+ if (coordinatePersistFallbackEntries.length > 0) {
13165
+ await this.planEntryLeaderBatch(coordinatePersistFallbackEntries.map((entry) => ({
13166
+ entry,
13167
+ replicas: receiveReplicaCounts.get(entry.hash) ??
13168
+ decodeReplicas(entry).getValue(this),
13169
+ options: { roleAge: 0, persist: {} },
13170
+ })));
13171
+ }
13172
+ if (syncProfile) {
13173
+ emitSyncProfileDuration(syncProfile, coordinatePersistStartedAt, {
13174
+ name: "sharedLog.receive.coordinatePersist",
13175
+ component: "shared-log",
13176
+ entries: entriesToPersist.length,
13177
+ messages: 1,
13178
+ details: {
13179
+ reusedLeaderPlans: reusableCoordinatePersistItemCount,
13180
+ nativeBackboneOnly: nativeBackboneOnlyPersistedHashes?.size ?? 0,
13181
+ },
13182
+ });
13183
+ }
13184
+ for (const hash of admittedHashes) {
13185
+ confirmedHashes.add(hash);
13186
+ }
13187
+ const checkedPruneStartedAt = syncProfileStart(syncProfile);
13188
+ const ownershipChangedDuringReceive = !this.isReceiveOwnershipSnapshotStable(receiveOwnershipRevision);
13189
+ if (ownershipChangedDuringReceive) {
13190
+ const freshAuditRevision = this._instanceLifecycle?._receiveOwnershipRevision ?? 0;
13191
+ const armFreshAuditRetry = () => {
13192
+ for (const entry of admittedShallowEntries) {
13193
+ this.scheduleCheckedPruneRetry({ entry, leaders: new Map() }, receiveOwnershipLifecycleController);
13194
+ }
13195
+ };
13196
+ try {
13197
+ await this.pruneJoinedEntriesNoLongerLed(admittedShallowEntries, {
13198
+ decodedReplicaCounts: receiveReplicaCounts,
13199
+ freshReceiveOwnerAudit: true,
13200
+ preserveExistingPruneOnLocalResult: true,
13201
+ profile: syncProfile,
13202
+ }, receiveOwnershipLifecycleController);
13203
+ this.throwIfReplicationOwnershipLifecycleInactive(receiveOwnershipLifecycleController);
13204
+ if (!this.isReceiveOwnershipSnapshotStable(freshAuditRevision)) {
13205
+ armFreshAuditRetry();
13206
+ }
13207
+ }
13208
+ catch {
13209
+ // The lower-log and coordinate commits are already durable. A
13210
+ // sender retry will filter these hashes as present, so retain a
13211
+ // bounded local obligation instead of failing the admitted receive.
13212
+ this.throwIfReplicationOwnershipLifecycleInactive(receiveOwnershipLifecycleController);
13213
+ armFreshAuditRetry();
13214
+ }
13215
+ }
13216
+ else {
13096
13217
  await this.pruneJoinedEntriesNoLongerLed(admittedShallowEntries, {
13097
13218
  decodedReplicaCounts: receiveReplicaCounts,
13098
- freshReceiveOwnerAudit: true,
13099
13219
  preserveExistingPruneOnLocalResult: true,
13220
+ reusableLeaderPlans: reusableCoordinatePlans,
13100
13221
  profile: syncProfile,
13101
13222
  }, receiveOwnershipLifecycleController);
13102
- this.throwIfReplicationOwnershipLifecycleInactive(receiveOwnershipLifecycleController);
13103
- if (!this.isReceiveOwnershipSnapshotStable(freshAuditRevision)) {
13104
- armFreshAuditRetry();
13105
- }
13106
13223
  }
13107
- catch {
13108
- // The lower-log and coordinate commits are already durable. A
13109
- // sender retry will filter these hashes as present, so retain a
13110
- // bounded local obligation instead of failing the admitted receive.
13111
- this.throwIfReplicationOwnershipLifecycleInactive(receiveOwnershipLifecycleController);
13112
- armFreshAuditRetry();
13224
+ if (syncProfile) {
13225
+ emitSyncProfileDuration(syncProfile, checkedPruneStartedAt, {
13226
+ name: "sharedLog.receive.checkedPrune",
13227
+ component: "shared-log",
13228
+ entries: allToMerge.length,
13229
+ messages: 1,
13230
+ });
13113
13231
  }
13232
+ for (const plan of joinPlans) {
13233
+ plan.toDelete
13234
+ ?.filter((entry) => admittedMergeHashes.has(entry.hash))
13235
+ .map((entry) => this.pruneDebouncedFnAddIfNotKeeping({
13236
+ key: entry.hash,
13237
+ value: {
13238
+ entry,
13239
+ leaders: plan.leaders,
13240
+ },
13241
+ }));
13242
+ }
13243
+ this.rebalanceParticipationDebounced?.call();
13114
13244
  }
13115
- else {
13116
- await this.pruneJoinedEntriesNoLongerLed(admittedShallowEntries, {
13117
- decodedReplicaCounts: receiveReplicaCounts,
13118
- preserveExistingPruneOnLocalResult: true,
13119
- reusableLeaderPlans: reusableCoordinatePlans,
13120
- profile: syncProfile,
13121
- }, receiveOwnershipLifecycleController);
13122
- }
13123
- if (syncProfile) {
13124
- emitSyncProfileDuration(syncProfile, checkedPruneStartedAt, {
13125
- name: "sharedLog.receive.checkedPrune",
13126
- component: "shared-log",
13127
- entries: allToMerge.length,
13128
- messages: 1,
13129
- });
13130
- }
13131
- for (const plan of joinPlans) {
13132
- plan.toDelete
13133
- ?.filter((entry) => admittedMergeHashes.has(entry.hash))
13134
- .map((entry) => this.pruneDebouncedFnAddIfNotKeeping({
13135
- key: entry.hash,
13136
- value: {
13137
- entry,
13138
- leaders: plan.leaders,
13139
- },
13140
- }));
13245
+ finally {
13246
+ // Settle seam for the receive token. Every consumer that can
13247
+ // roll it back runs inline before control leaves this block: the
13248
+ // prepared-join callback resolves during the join await, and the
13249
+ // late finish/rollback arm runs above. This `finally` is what
13250
+ // closes the abandon arms (no prepared-join commit, a declined
13251
+ // native commit, a downgrade to the plain join) without having
13252
+ // to enumerate them.
13253
+ this._coordinates.settleResidentCoordinateSnapshot(nativeReceiveCoordinateBatch?.rollbackCoordinateEntries);
13141
13254
  }
13142
- this.rebalanceParticipationDebounced?.call();
13143
13255
  }
13144
13256
  for (const plan of joinPlans) {
13145
13257
  if (!plan.maybeDelete) {