@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/src/index.ts CHANGED
@@ -637,6 +637,13 @@ export type NativeBackboneCoordinateRollback<R extends "u32" | "u64"> = {
637
637
  hashes: Set<string>;
638
638
  entries: Map<string, ResidentCoordinateEntry<R>>;
639
639
  generations: Map<string, number>;
640
+ /**
641
+ * Set by `settleResidentCoordinateSnapshot` once the token's holds on the
642
+ * mutation-generation map have been released. Load-bearing: it makes the
643
+ * settle idempotent, so a second settle of this token cannot consume a
644
+ * different token's hold on a shared hash.
645
+ */
646
+ settled?: boolean;
640
647
  };
641
648
 
642
649
  export type RepairDispatchEntry<R extends "u32" | "u64"> =
@@ -2031,7 +2038,58 @@ export class SharedLog<
2031
2038
 
2032
2039
  /* private _totalParticipation!: number; */
2033
2040
 
2034
- // gid -> coordinate -> publicKeyHash list (of owners)
2041
+ // gid -> set of publicKeyHashes known to hold that gid's entries.
2042
+ //
2043
+ // This is a suppression memo, not a source of truth. A present row lets the
2044
+ // rebalance and repair paths skip re-sending an entry to a peer that already
2045
+ // has it. Every read is `?.has(peer)` guarded, and a MISSING row always
2046
+ // means "assume nothing is known", which produces strictly MORE work --
2047
+ // redundant unchecked delivery in the rebalance loop, redundant queueing in
2048
+ // the repair planner -- and never a wrong prune, a wrong quorum, or data
2049
+ // loss. Losing a row costs bandwidth; keeping a stale row costs a little
2050
+ // memory. That asymmetry is what the rest of this note turns on.
2051
+ //
2052
+ // GROWTH SHAPE. Rows are released by `deleteGidPeerHistory` on the two prune
2053
+ // paths, by `removePeerFromGidPeerHistory` once a gid's last peer drops (the
2054
+ // routine disconnect outcome), by `rebalanceAll({ clearCache: true })`, and
2055
+ // wholesale on close/reset. Nothing on the TRIM path releases a row, so a
2056
+ // node that bounds its log with trim rather than prune accumulates one row
2057
+ // per distinct gid it has ever held. A gid names a graph, not an entry: an
2058
+ // entry with `meta.next` inherits `min(next.meta.gid)` (see
2059
+ // packages/log/src/entry-v0.ts), so document updates fold into the gid of
2060
+ // the first put and the row count tracks distinct chain roots -- distinct
2061
+ // document ids -- rather than entry count. Insert-only workloads mint a
2062
+ // fresh gid per append and so do grow one row per entry. Merges are a
2063
+ // smaller second source: when a join links two graphs the losing entries
2064
+ // keep their own `meta.gid` on disk, and shared-log does not subscribe to
2065
+ // the log's `onGidRemoved`, so the shadowed gid's row lingers too.
2066
+ //
2067
+ // WHY TRIM DOES NOT SIMPLY CALL `deleteGidPeerHistory` AS WELL. Both prune
2068
+ // callers delete a whole row from a single entry's gid, and under the
2069
+ // default hash domain that is correct by construction: the coordinate is a
2070
+ // pure function of the gid (replication-domain-hash.ts sha256s
2071
+ // `entry.meta.gid`), so identical gid => identical coordinates => identical
2072
+ // leader set => every local sibling of that gid is prune-eligible in the
2073
+ // same batch. The gid really is finished locally. Trim offers no such
2074
+ // guarantee. It walks oldest-first against a length/bytelength/age bound and
2075
+ // stops the instant the bound is met (packages/log/src/trim.ts); its only
2076
+ // use of gid is memoizing the caller's `canTrim` verdict, never grouping
2077
+ // deletes. So trim routinely removes the OLDEST entry of a gid while newer
2078
+ // siblings -- same gid, same coordinates, still local, still replicated --
2079
+ // remain. Copying the prune call onto trim would therefore delete a LIVE row
2080
+ // on the common path, paying for the freed memory in repeated re-delivery of
2081
+ // entries that are still here. That trade is not worth it.
2082
+ //
2083
+ // Bounding this correctly requires a per-gid count of locally held entries,
2084
+ // dropping the row only when it reaches zero -- a real reverse index, not a
2085
+ // one-line delete. Deliberately not built: the growth is bounded by distinct
2086
+ // gids, and the cheap version is a bandwidth regression.
2087
+ //
2088
+ // Existing, deliberate imprecision: under the time domain the coordinate is
2089
+ // `meta.clock.timestamp.wallTime` (replication-domain-time.ts) and is
2090
+ // gid-independent, so siblings of one gid can carry different leader sets
2091
+ // and prune's whole-row delete is already over-eager there. The cost is the
2092
+ // same bounded extra traffic, never a wrong prune.
2035
2093
  _gidPeersHistory!: Map<string, Set<string>>;
2036
2094
 
2037
2095
  private _onSubscriptionFn!: (arg: any) => any;
@@ -3114,9 +3172,23 @@ export class SharedLog<
3114
3172
  };
3115
3173
  for (const coordinate of intent.coordinates) {
3116
3174
  rollback.hashes.add(coordinate.hash);
3117
- const generation =
3118
- (mutationGenerations.get(coordinate.hash) ?? 0) + 1;
3119
- mutationGenerations.set(coordinate.hash, generation);
3175
+ // Same hold-counted row shape the coordinator's own
3176
+ // snapshot writes: one hold per hash, released by the
3177
+ // settle after the replay consumes the token below.
3178
+ // RELIES ON `intent.coordinates` HOLDING UNIQUE HASHES —
3179
+ // it is built from the token's `hashes` Set (see
3180
+ // setNativeStrictDurableTransactionOperation). Holds are
3181
+ // taken per element here but released per unique hash by
3182
+ // the settle, so a duplicate would take two and release
3183
+ // one and retain that row forever. That is the safe
3184
+ // direction (a retained row, never a fail-open rollback),
3185
+ // but keep the source a Set.
3186
+ const row = mutationGenerations.get(coordinate.hash);
3187
+ const generation = (row?.generation ?? 0) + 1;
3188
+ mutationGenerations.set(coordinate.hash, {
3189
+ generation,
3190
+ holds: (row?.holds ?? 0) + 1,
3191
+ });
3120
3192
  rollback.generations.set(coordinate.hash, generation);
3121
3193
  if (coordinate.value) {
3122
3194
  const number = (value: string) =>
@@ -3142,6 +3214,9 @@ export class SharedLog<
3142
3214
  "",
3143
3215
  rollback,
3144
3216
  );
3217
+ // The replay fabricated these generations itself one turn
3218
+ // earlier and has now consumed them, so the rows are dead.
3219
+ this._coordinates.settleResidentCoordinateSnapshot(rollback);
3145
3220
  }
3146
3221
  for (const document of intent.documents) {
3147
3222
  this.restoreNativeBackboneDocument({
@@ -10979,12 +11054,21 @@ export class SharedLog<
10979
11054
  return rollbackLowerPublication(error);
10980
11055
  }
10981
11056
  if (!result) {
11057
+ // Abandon arm: the token was minted but nothing downstream can
11058
+ // roll it back from here.
11059
+ this._coordinates.settleResidentCoordinateSnapshot(
11060
+ lowerPublicationRollback?.coordinateEntries,
11061
+ );
10982
11062
  return this.completeNativeStrictDurableTransaction(
10983
11063
  nativeStrictTransaction,
10984
11064
  ).then(() => undefined);
10985
11065
  }
10986
11066
  return mapMaybePromise(result, async (prepared) => {
10987
11067
  if (!prepared) {
11068
+ // Abandon arm: same shape as the `!result` arm above.
11069
+ this._coordinates.settleResidentCoordinateSnapshot(
11070
+ lowerPublicationRollback?.coordinateEntries,
11071
+ );
10988
11072
  await this.completeNativeStrictDurableTransaction(
10989
11073
  nativeStrictTransaction,
10990
11074
  );
@@ -11018,6 +11102,11 @@ export class SharedLog<
11018
11102
  prepared.appendFacts.hash,
11019
11103
  lowerPublicationRollback?.coordinateEntries,
11020
11104
  );
11105
+ // Terminal: this is the last rollback consumer for the
11106
+ // token. A throw above leaves the row, which is safe.
11107
+ this._coordinates.settleResidentCoordinateSnapshot(
11108
+ lowerPublicationRollback?.coordinateEntries,
11109
+ );
11021
11110
  for (const document of lowerPublicationRollback?.documents ?? []) {
11022
11111
  this.restoreNativeBackboneDocument(document);
11023
11112
  }
@@ -11088,6 +11177,14 @@ export class SharedLog<
11088
11177
  } catch (error) {
11089
11178
  return rollback(error);
11090
11179
  }
11180
+ // Success seam. The last await inside the protected try is the
11181
+ // finalizer acknowledge; `finish()` is synchronous, so no async
11182
+ // boundary separates the catch above from this statement and
11183
+ // `rollback` can no longer fire. Nothing downstream rolls back
11184
+ // (the retire below only warns), so the token is terminal here.
11185
+ this._coordinates.settleResidentCoordinateSnapshot(
11186
+ lowerPublicationRollback?.coordinateEntries,
11187
+ );
11091
11188
  this.applyPreparedAppendFactsWithDeferredCoordinateDeletes(
11092
11189
  prepared.appendFacts,
11093
11190
  prepared.removed,
@@ -11544,6 +11641,12 @@ export class SharedLog<
11544
11641
  }
11545
11642
  return mapMaybePromise(result, async (prepared) => {
11546
11643
  if (!prepared || !backboneAppend) {
11644
+ // Abandon arm: the token was minted inside
11645
+ // `prepareBackboneAppend` and nothing downstream of
11646
+ // this return can roll it back.
11647
+ this._coordinates.settleResidentCoordinateSnapshot(
11648
+ nativeCoordinateRollback,
11649
+ );
11547
11650
  await this.completeNativeStrictDurableTransaction(
11548
11651
  nativeStrictTransaction,
11549
11652
  );
@@ -11635,6 +11738,10 @@ export class SharedLog<
11635
11738
  prepared.appendFacts.hash,
11636
11739
  rollbackCoordinateEntries,
11637
11740
  );
11741
+ // Terminal: last rollback consumer for the token.
11742
+ this._coordinates.settleResidentCoordinateSnapshot(
11743
+ rollbackCoordinateEntries,
11744
+ );
11638
11745
  } catch (rollbackError) {
11639
11746
  rollbackFailures.push(rollbackError);
11640
11747
  }
@@ -11711,6 +11818,12 @@ export class SharedLog<
11711
11818
  } catch (error) {
11712
11819
  return rollback(error);
11713
11820
  }
11821
+ // Success seam: the finalizer acknowledge above is the last
11822
+ // await inside the protected try, so `rollback` can no
11823
+ // longer fire and the retire below only warns.
11824
+ this._coordinates.settleResidentCoordinateSnapshot(
11825
+ rollbackCoordinateEntries,
11826
+ );
11714
11827
  this.applyPreparedAppendFactsWithDeferredCoordinateDeletes(
11715
11828
  prepared.appendFacts,
11716
11829
  prepared.removed,
@@ -12630,6 +12743,10 @@ export class SharedLog<
12630
12743
  throw error;
12631
12744
  }
12632
12745
  if (!appended || !backboneAppends) {
12746
+ // Abandon arm: no consumer downstream of this return.
12747
+ this._coordinates.settleResidentCoordinateSnapshot(
12748
+ batchCoordinateRollback,
12749
+ );
12633
12750
  await this.completeNativeStrictDurableTransaction(
12634
12751
  nativeStrictTransaction,
12635
12752
  );
@@ -12667,6 +12784,10 @@ export class SharedLog<
12667
12784
  appended.appendFacts[0]?.hash ?? "",
12668
12785
  batchCoordinateRollback,
12669
12786
  );
12787
+ // Terminal: last rollback consumer for the batch token.
12788
+ this._coordinates.settleResidentCoordinateSnapshot(
12789
+ batchCoordinateRollback,
12790
+ );
12670
12791
  } catch (rollbackError) {
12671
12792
  rollbackFailures.push(rollbackError);
12672
12793
  }
@@ -12798,6 +12919,11 @@ export class SharedLog<
12798
12919
  } catch (error) {
12799
12920
  return rollbackBatch(error);
12800
12921
  }
12922
+ // Success seam: `rollbackBatch` has exactly one call site (the catch
12923
+ // above), and everything from here on escapes without any rollback.
12924
+ this._coordinates.settleResidentCoordinateSnapshot(
12925
+ batchCoordinateRollback,
12926
+ );
12801
12927
 
12802
12928
  this.throwIfReplicationOwnershipLifecycleInactive(
12803
12929
  ownershipLifecycleController,
@@ -18907,292 +19033,305 @@ export class SharedLog<
18907
19033
  reusableCoordinatePersistItems,
18908
19034
  )
18909
19035
  : undefined;
18910
- const nativePreparedJoinCommit = canUsePreparedAppendFacts
18911
- ? this._coordinates.createNativeBackbonePreparedJoinCommit(
18912
- nativeReceiveCoordinateBatch,
18913
- (batch) => {
18914
- nativePreparedCoordinateBatch = batch;
18915
- },
18916
- nativeCommitVerifyHashes,
18917
- nativeCommitVerifyAllHashes,
18918
- syncProfile,
18919
- (committedHashes) => {
18920
- nativePreparedCommittedHashes = new Set(committedHashes);
18921
- },
18922
- )
18923
- : undefined;
18924
- const finishNativePreparedCoordinates = async (properties: {
18925
- nativePreparedCommitted: boolean;
18926
- }) => {
18927
- if (
18928
- !properties.nativePreparedCommitted ||
18929
- !nativePreparedCoordinateBatch
18930
- ) {
18931
- return;
18932
- }
18933
- try {
18934
- nativeBackboneOnlyPersistedHashes =
18935
- await this._coordinates.finishBackboneOnlyReceiveCoordinateBatch(
18936
- nativePreparedCoordinateBatch,
19036
+ try {
19037
+ const nativePreparedJoinCommit = canUsePreparedAppendFacts
19038
+ ? this._coordinates.createNativeBackbonePreparedJoinCommit(
19039
+ nativeReceiveCoordinateBatch,
19040
+ (batch) => {
19041
+ nativePreparedCoordinateBatch = batch;
19042
+ },
19043
+ nativeCommitVerifyHashes,
19044
+ nativeCommitVerifyAllHashes,
18937
19045
  syncProfile,
19046
+ (committedHashes) => {
19047
+ nativePreparedCommittedHashes = new Set(committedHashes);
19048
+ },
19049
+ )
19050
+ : undefined;
19051
+ const finishNativePreparedCoordinates = async (properties: {
19052
+ nativePreparedCommitted: boolean;
19053
+ }) => {
19054
+ if (
19055
+ !properties.nativePreparedCommitted ||
19056
+ !nativePreparedCoordinateBatch
19057
+ ) {
19058
+ return;
19059
+ }
19060
+ try {
19061
+ nativeBackboneOnlyPersistedHashes =
19062
+ await this._coordinates.finishBackboneOnlyReceiveCoordinateBatch(
19063
+ nativePreparedCoordinateBatch,
19064
+ syncProfile,
19065
+ );
19066
+ nativePreparedCoordinatesFinished = true;
19067
+ } catch (error) {
19068
+ this._coordinates.rollbackBackboneOnlyReceiveCoordinateBatch(
19069
+ nativePreparedCoordinateBatch,
18938
19070
  );
18939
- nativePreparedCoordinatesFinished = true;
18940
- } catch (error) {
18941
- this._coordinates.rollbackBackboneOnlyReceiveCoordinateBatch(
18942
- nativePreparedCoordinateBatch,
18943
- );
18944
- throw error;
19071
+ throw error;
19072
+ }
19073
+ };
19074
+ const preparedAppendCanValidateAppend =
19075
+ canAppendAlreadyValidated ||
19076
+ (nativeCommitCanValidateAppend && !!nativePreparedJoinCommit);
19077
+ if (!preparedAppendCanValidateAppend) {
19078
+ canUsePreparedAppendFacts = false;
18945
19079
  }
18946
- };
18947
- const preparedAppendCanValidateAppend =
18948
- canAppendAlreadyValidated ||
18949
- (nativeCommitCanValidateAppend && !!nativePreparedJoinCommit);
18950
- if (!preparedAppendCanValidateAppend) {
18951
- canUsePreparedAppendFacts = false;
18952
- }
18953
- const nativePreparedJoinCommitValidatesPlan =
18954
- !!nativePreparedJoinCommit &&
18955
- (nativeCommitVerifyHashes && nativeCommitVerifyHashes.length > 0
18956
- ? nativeCommitVerifyAllHashes
18957
- ? !!this._nativeBackbone?.graph
18958
- .commitVerifiedAllPreparedRawReceiveJoinBatch ||
18959
- !!this._nativeBackbone?.graph
18960
- .commitVerifiedPreparedRawReceiveJoinBatch
19080
+ const nativePreparedJoinCommitValidatesPlan =
19081
+ !!nativePreparedJoinCommit &&
19082
+ (nativeCommitVerifyHashes && nativeCommitVerifyHashes.length > 0
19083
+ ? nativeCommitVerifyAllHashes
19084
+ ? !!this._nativeBackbone?.graph
19085
+ .commitVerifiedAllPreparedRawReceiveJoinBatch ||
19086
+ !!this._nativeBackbone?.graph
19087
+ .commitVerifiedPreparedRawReceiveJoinBatch
19088
+ : !!this._nativeBackbone?.graph
19089
+ .commitVerifiedPreparedRawReceiveJoinBatch
18961
19090
  : !!this._nativeBackbone?.graph
18962
- .commitVerifiedPreparedRawReceiveJoinBatch
18963
- : !!this._nativeBackbone?.graph
18964
- .commitPreparedRawReceiveJoinBatch);
18965
- const trustedLowerLog = this.log as unknown as TrustedLowerLog<T>;
18966
- // With a program-level onChange consumer the hash-only
18967
- // sink is not used: the lower-log join dispatches the
18968
- // change event (lazy entry views over the prepared raw
18969
- // facts) so per-entry consumers observe every commit.
18970
- const joinOnAppendHashes = programOnChange
18971
- ? undefined
18972
- : onAppendHashes;
18973
- const joinedPreparedFacts =
18974
- canUsePreparedAppendFacts &&
18975
- (await trustedLowerLog.joinPreparedAppendFactsBatch(
18976
- preparedAppendFacts,
18977
- {
19091
+ .commitPreparedRawReceiveJoinBatch);
19092
+ const trustedLowerLog = this.log as unknown as TrustedLowerLog<T>;
19093
+ // With a program-level onChange consumer the hash-only
19094
+ // sink is not used: the lower-log join dispatches the
19095
+ // change event (lazy entry views over the prepared raw
19096
+ // facts) so per-entry consumers observe every commit.
19097
+ const joinOnAppendHashes = programOnChange
19098
+ ? undefined
19099
+ : onAppendHashes;
19100
+ const joinedPreparedFacts =
19101
+ canUsePreparedAppendFacts &&
19102
+ (await trustedLowerLog.joinPreparedAppendFactsBatch(
19103
+ preparedAppendFacts,
19104
+ {
19105
+ __peerbitEntriesAlreadyMissing: true,
19106
+ __peerbitCanAppendAlreadyValidated: true,
19107
+ __peerbitDeferIndexWrite: true,
19108
+ __peerbitOnAppendHashes: joinOnAppendHashes,
19109
+ __peerbitProfile: syncProfile,
19110
+ __peerbitNativePreparedJoinCommit: nativePreparedJoinCommit,
19111
+ __peerbitNativePreparedJoinCommitValidatesPlan:
19112
+ nativePreparedJoinCommitValidatesPlan,
19113
+ __peerbitOnPreparedJoinCommitted: nativePreparedJoinCommit
19114
+ ? finishNativePreparedCoordinates
19115
+ : undefined,
19116
+ },
19117
+ ));
19118
+ if (!joinedPreparedFacts) {
19119
+ await trustedLowerLog.join(materializeAllToMergeEntries(), {
19120
+ __peerbitBatchIndependent: true,
18978
19121
  __peerbitEntriesAlreadyMissing: true,
18979
- __peerbitCanAppendAlreadyValidated: true,
19122
+ __peerbitCanAppendAlreadyValidated:
19123
+ fallbackCanAppendAlreadyValidated,
18980
19124
  __peerbitDeferIndexWrite: true,
18981
19125
  __peerbitOnAppendHashes: joinOnAppendHashes,
18982
19126
  __peerbitProfile: syncProfile,
18983
- __peerbitNativePreparedJoinCommit: nativePreparedJoinCommit,
18984
- __peerbitNativePreparedJoinCommitValidatesPlan:
18985
- nativePreparedJoinCommitValidatesPlan,
18986
- __peerbitOnPreparedJoinCommitted: nativePreparedJoinCommit
18987
- ? finishNativePreparedCoordinates
18988
- : undefined,
18989
- },
18990
- ));
18991
- if (!joinedPreparedFacts) {
18992
- await trustedLowerLog.join(materializeAllToMergeEntries(), {
18993
- __peerbitBatchIndependent: true,
18994
- __peerbitEntriesAlreadyMissing: true,
18995
- __peerbitCanAppendAlreadyValidated:
18996
- fallbackCanAppendAlreadyValidated,
18997
- __peerbitDeferIndexWrite: true,
18998
- __peerbitOnAppendHashes: joinOnAppendHashes,
18999
- __peerbitProfile: syncProfile,
19000
- });
19001
- }
19002
- // A recursive lower-log join can resolve successfully while declining
19003
- // an individual top-level entry (for example, when one of its parents
19004
- // is temporarily unavailable). The public Log.join() API intentionally
19005
- // does not expose that per-entry result, so make local index presence the
19006
- // authority before publishing any SharedLog-side effects. A successful
19007
- // prepared-facts batch is atomic and already proves every input hash.
19008
- const admittedHashes = joinedPreparedFacts
19009
- ? new Set(allToMergeHashes)
19010
- : await this.log.hasMany(allToMergeHashes);
19011
- admittedMergeHashes = admittedHashes;
19012
- const admittedShallowEntries =
19013
- admittedHashes.size === allToMergeShallowEntries.length
19014
- ? allToMergeShallowEntries
19015
- : allToMergeShallowEntries.filter((entry) =>
19127
+ });
19128
+ }
19129
+ // A recursive lower-log join can resolve successfully while declining
19130
+ // an individual top-level entry (for example, when one of its parents
19131
+ // is temporarily unavailable). The public Log.join() API intentionally
19132
+ // does not expose that per-entry result, so make local index presence the
19133
+ // authority before publishing any SharedLog-side effects. A successful
19134
+ // prepared-facts batch is atomic and already proves every input hash.
19135
+ const admittedHashes = joinedPreparedFacts
19136
+ ? new Set(allToMergeHashes)
19137
+ : await this.log.hasMany(allToMergeHashes);
19138
+ admittedMergeHashes = admittedHashes;
19139
+ const admittedShallowEntries =
19140
+ admittedHashes.size === allToMergeShallowEntries.length
19141
+ ? allToMergeShallowEntries
19142
+ : allToMergeShallowEntries.filter((entry) =>
19143
+ admittedHashes.has(entry.hash),
19144
+ );
19145
+ if (!joinedPreparedFacts) {
19146
+ reusableCoordinatePersistItems =
19147
+ reusableCoordinatePersistItems.filter((item) =>
19148
+ admittedHashes.has(item.entry.hash),
19149
+ );
19150
+ coordinatePersistFallbackEntries =
19151
+ coordinatePersistFallbackEntries.filter((entry) =>
19016
19152
  admittedHashes.has(entry.hash),
19017
19153
  );
19018
- if (!joinedPreparedFacts) {
19019
- reusableCoordinatePersistItems =
19020
- reusableCoordinatePersistItems.filter((item) =>
19021
- admittedHashes.has(item.entry.hash),
19022
- );
19023
- coordinatePersistFallbackEntries =
19024
- coordinatePersistFallbackEntries.filter((entry) =>
19025
- admittedHashes.has(entry.hash),
19026
- );
19027
- }
19028
- const reusableCoordinatePersistItemCount =
19029
- reusableCoordinatePersistItems.length;
19030
- if (syncProfile) {
19031
- emitSyncProfileDuration(syncProfile, lowerLogJoinStartedAt, {
19032
- name: "sharedLog.receive.lowerLogJoin",
19033
- component: "shared-log",
19034
- entries: allToMerge.length,
19035
- messages: 1,
19036
- details: {
19037
- hashOnlyEntryAdded,
19038
- batchHashOnlyEntryAdded,
19039
- programOnChange,
19040
- joinedPreparedFacts,
19041
- admittedEntries: admittedHashes.size,
19042
- nativePreparedCoordinatesFinished,
19043
- },
19044
- });
19045
- }
19046
- const coordinatePersistStartedAt = syncProfileStart(syncProfile);
19047
- if (nativePreparedCoordinatesFinished) {
19048
- // The lower-log prepared receive transaction already finished
19049
- // the native coordinate mirror/journal after entry-index commit.
19050
- } else if (nativePreparedCoordinateBatch) {
19051
- try {
19052
- nativeBackboneOnlyPersistedHashes =
19053
- await this._coordinates.finishBackboneOnlyReceiveCoordinateBatch(
19154
+ }
19155
+ const reusableCoordinatePersistItemCount =
19156
+ reusableCoordinatePersistItems.length;
19157
+ if (syncProfile) {
19158
+ emitSyncProfileDuration(syncProfile, lowerLogJoinStartedAt, {
19159
+ name: "sharedLog.receive.lowerLogJoin",
19160
+ component: "shared-log",
19161
+ entries: allToMerge.length,
19162
+ messages: 1,
19163
+ details: {
19164
+ hashOnlyEntryAdded,
19165
+ batchHashOnlyEntryAdded,
19166
+ programOnChange,
19167
+ joinedPreparedFacts,
19168
+ admittedEntries: admittedHashes.size,
19169
+ nativePreparedCoordinatesFinished,
19170
+ },
19171
+ });
19172
+ }
19173
+ const coordinatePersistStartedAt = syncProfileStart(syncProfile);
19174
+ if (nativePreparedCoordinatesFinished) {
19175
+ // The lower-log prepared receive transaction already finished
19176
+ // the native coordinate mirror/journal after entry-index commit.
19177
+ } else if (nativePreparedCoordinateBatch) {
19178
+ try {
19179
+ nativeBackboneOnlyPersistedHashes =
19180
+ await this._coordinates.finishBackboneOnlyReceiveCoordinateBatch(
19181
+ nativePreparedCoordinateBatch,
19182
+ syncProfile,
19183
+ );
19184
+ } catch (error) {
19185
+ this._coordinates.rollbackBackboneOnlyReceiveCoordinateBatch(
19054
19186
  nativePreparedCoordinateBatch,
19055
- syncProfile,
19056
19187
  );
19057
- } catch (error) {
19058
- this._coordinates.rollbackBackboneOnlyReceiveCoordinateBatch(
19059
- nativePreparedCoordinateBatch,
19060
- );
19061
- throw error;
19188
+ throw error;
19189
+ }
19190
+ } else {
19191
+ nativeBackboneOnlyPersistedHashes =
19192
+ await this._coordinates.persistBackboneOnlyReceiveCoordinateBatch(
19193
+ reusableCoordinatePersistItems,
19194
+ );
19062
19195
  }
19063
- } else {
19064
- nativeBackboneOnlyPersistedHashes =
19065
- await this._coordinates.persistBackboneOnlyReceiveCoordinateBatch(
19066
- reusableCoordinatePersistItems,
19067
- );
19068
- }
19069
- if (
19070
- nativeBackboneOnlyPersistedHashes &&
19071
- nativeBackboneOnlyPersistedHashes.size > 0
19072
- ) {
19073
- for (
19074
- let i = reusableCoordinatePersistItems.length - 1;
19075
- i >= 0;
19076
- i--
19196
+ if (
19197
+ nativeBackboneOnlyPersistedHashes &&
19198
+ nativeBackboneOnlyPersistedHashes.size > 0
19077
19199
  ) {
19078
- if (
19079
- nativeBackboneOnlyPersistedHashes.has(
19080
- reusableCoordinatePersistItems[i]!.entry.hash,
19081
- )
19200
+ for (
19201
+ let i = reusableCoordinatePersistItems.length - 1;
19202
+ i >= 0;
19203
+ i--
19082
19204
  ) {
19083
- reusableCoordinatePersistItems.splice(i, 1);
19205
+ if (
19206
+ nativeBackboneOnlyPersistedHashes.has(
19207
+ reusableCoordinatePersistItems[i]!.entry.hash,
19208
+ )
19209
+ ) {
19210
+ reusableCoordinatePersistItems.splice(i, 1);
19211
+ }
19084
19212
  }
19085
19213
  }
19086
- }
19087
- if (reusableCoordinatePersistItems.length > 0) {
19088
- await this._coordinates.persistCoordinatesBatch(
19089
- reusableCoordinatePersistItems,
19090
- );
19091
- }
19092
- if (coordinatePersistFallbackEntries.length > 0) {
19093
- await this.planEntryLeaderBatch(
19094
- coordinatePersistFallbackEntries.map((entry) => ({
19095
- entry,
19096
- replicas:
19097
- receiveReplicaCounts.get(entry.hash) ??
19098
- decodeReplicas(entry).getValue(this),
19099
- options: { roleAge: 0, persist: {} },
19100
- })),
19101
- );
19102
- }
19103
- if (syncProfile) {
19104
- emitSyncProfileDuration(syncProfile, coordinatePersistStartedAt, {
19105
- name: "sharedLog.receive.coordinatePersist",
19106
- component: "shared-log",
19107
- entries: entriesToPersist.length,
19108
- messages: 1,
19109
- details: {
19110
- reusedLeaderPlans: reusableCoordinatePersistItemCount,
19111
- nativeBackboneOnly:
19112
- nativeBackboneOnlyPersistedHashes?.size ?? 0,
19113
- },
19114
- });
19115
- }
19116
- for (const hash of admittedHashes) {
19117
- confirmedHashes.add(hash);
19118
- }
19119
- const checkedPruneStartedAt = syncProfileStart(syncProfile);
19120
- const ownershipChangedDuringReceive =
19121
- !this.isReceiveOwnershipSnapshotStable(receiveOwnershipRevision);
19122
- if (ownershipChangedDuringReceive) {
19123
- const freshAuditRevision =
19124
- this._instanceLifecycle?._receiveOwnershipRevision ?? 0;
19125
- const armFreshAuditRetry = () => {
19126
- for (const entry of admittedShallowEntries) {
19127
- this.scheduleCheckedPruneRetry(
19128
- { entry, leaders: new Map() },
19214
+ if (reusableCoordinatePersistItems.length > 0) {
19215
+ await this._coordinates.persistCoordinatesBatch(
19216
+ reusableCoordinatePersistItems,
19217
+ );
19218
+ }
19219
+ if (coordinatePersistFallbackEntries.length > 0) {
19220
+ await this.planEntryLeaderBatch(
19221
+ coordinatePersistFallbackEntries.map((entry) => ({
19222
+ entry,
19223
+ replicas:
19224
+ receiveReplicaCounts.get(entry.hash) ??
19225
+ decodeReplicas(entry).getValue(this),
19226
+ options: { roleAge: 0, persist: {} },
19227
+ })),
19228
+ );
19229
+ }
19230
+ if (syncProfile) {
19231
+ emitSyncProfileDuration(syncProfile, coordinatePersistStartedAt, {
19232
+ name: "sharedLog.receive.coordinatePersist",
19233
+ component: "shared-log",
19234
+ entries: entriesToPersist.length,
19235
+ messages: 1,
19236
+ details: {
19237
+ reusedLeaderPlans: reusableCoordinatePersistItemCount,
19238
+ nativeBackboneOnly:
19239
+ nativeBackboneOnlyPersistedHashes?.size ?? 0,
19240
+ },
19241
+ });
19242
+ }
19243
+ for (const hash of admittedHashes) {
19244
+ confirmedHashes.add(hash);
19245
+ }
19246
+ const checkedPruneStartedAt = syncProfileStart(syncProfile);
19247
+ const ownershipChangedDuringReceive =
19248
+ !this.isReceiveOwnershipSnapshotStable(receiveOwnershipRevision);
19249
+ if (ownershipChangedDuringReceive) {
19250
+ const freshAuditRevision =
19251
+ this._instanceLifecycle?._receiveOwnershipRevision ?? 0;
19252
+ const armFreshAuditRetry = () => {
19253
+ for (const entry of admittedShallowEntries) {
19254
+ this.scheduleCheckedPruneRetry(
19255
+ { entry, leaders: new Map() },
19256
+ receiveOwnershipLifecycleController,
19257
+ );
19258
+ }
19259
+ };
19260
+ try {
19261
+ await this.pruneJoinedEntriesNoLongerLed(
19262
+ admittedShallowEntries,
19263
+ {
19264
+ decodedReplicaCounts: receiveReplicaCounts,
19265
+ freshReceiveOwnerAudit: true,
19266
+ preserveExistingPruneOnLocalResult: true,
19267
+ profile: syncProfile,
19268
+ },
19129
19269
  receiveOwnershipLifecycleController,
19130
19270
  );
19271
+ this.throwIfReplicationOwnershipLifecycleInactive(
19272
+ receiveOwnershipLifecycleController,
19273
+ );
19274
+ if (
19275
+ !this.isReceiveOwnershipSnapshotStable(freshAuditRevision)
19276
+ ) {
19277
+ armFreshAuditRetry();
19278
+ }
19279
+ } catch {
19280
+ // The lower-log and coordinate commits are already durable. A
19281
+ // sender retry will filter these hashes as present, so retain a
19282
+ // bounded local obligation instead of failing the admitted receive.
19283
+ this.throwIfReplicationOwnershipLifecycleInactive(
19284
+ receiveOwnershipLifecycleController,
19285
+ );
19286
+ armFreshAuditRetry();
19131
19287
  }
19132
- };
19133
- try {
19288
+ } else {
19134
19289
  await this.pruneJoinedEntriesNoLongerLed(
19135
19290
  admittedShallowEntries,
19136
19291
  {
19137
19292
  decodedReplicaCounts: receiveReplicaCounts,
19138
- freshReceiveOwnerAudit: true,
19139
19293
  preserveExistingPruneOnLocalResult: true,
19294
+ reusableLeaderPlans: reusableCoordinatePlans,
19140
19295
  profile: syncProfile,
19141
19296
  },
19142
19297
  receiveOwnershipLifecycleController,
19143
19298
  );
19144
- this.throwIfReplicationOwnershipLifecycleInactive(
19145
- receiveOwnershipLifecycleController,
19146
- );
19147
- if (
19148
- !this.isReceiveOwnershipSnapshotStable(freshAuditRevision)
19149
- ) {
19150
- armFreshAuditRetry();
19151
- }
19152
- } catch {
19153
- // The lower-log and coordinate commits are already durable. A
19154
- // sender retry will filter these hashes as present, so retain a
19155
- // bounded local obligation instead of failing the admitted receive.
19156
- this.throwIfReplicationOwnershipLifecycleInactive(
19157
- receiveOwnershipLifecycleController,
19158
- );
19159
- armFreshAuditRetry();
19160
19299
  }
19161
- } else {
19162
- await this.pruneJoinedEntriesNoLongerLed(
19163
- admittedShallowEntries,
19164
- {
19165
- decodedReplicaCounts: receiveReplicaCounts,
19166
- preserveExistingPruneOnLocalResult: true,
19167
- reusableLeaderPlans: reusableCoordinatePlans,
19168
- profile: syncProfile,
19169
- },
19170
- receiveOwnershipLifecycleController,
19171
- );
19172
- }
19173
- if (syncProfile) {
19174
- emitSyncProfileDuration(syncProfile, checkedPruneStartedAt, {
19175
- name: "sharedLog.receive.checkedPrune",
19176
- component: "shared-log",
19177
- entries: allToMerge.length,
19178
- messages: 1,
19179
- });
19180
- }
19300
+ if (syncProfile) {
19301
+ emitSyncProfileDuration(syncProfile, checkedPruneStartedAt, {
19302
+ name: "sharedLog.receive.checkedPrune",
19303
+ component: "shared-log",
19304
+ entries: allToMerge.length,
19305
+ messages: 1,
19306
+ });
19307
+ }
19181
19308
 
19182
- for (const plan of joinPlans) {
19183
- plan.toDelete
19184
- ?.filter((entry) => admittedMergeHashes.has(entry.hash))
19185
- .map((entry) =>
19186
- this.pruneDebouncedFnAddIfNotKeeping({
19187
- key: entry.hash,
19188
- value: {
19189
- entry,
19190
- leaders: plan.leaders as Map<string, any>,
19191
- },
19192
- }),
19193
- );
19309
+ for (const plan of joinPlans) {
19310
+ plan.toDelete
19311
+ ?.filter((entry) => admittedMergeHashes.has(entry.hash))
19312
+ .map((entry) =>
19313
+ this.pruneDebouncedFnAddIfNotKeeping({
19314
+ key: entry.hash,
19315
+ value: {
19316
+ entry,
19317
+ leaders: plan.leaders as Map<string, any>,
19318
+ },
19319
+ }),
19320
+ );
19321
+ }
19322
+ this.rebalanceParticipationDebounced?.call();
19323
+ } finally {
19324
+ // Settle seam for the receive token. Every consumer that can
19325
+ // roll it back runs inline before control leaves this block: the
19326
+ // prepared-join callback resolves during the join await, and the
19327
+ // late finish/rollback arm runs above. This `finally` is what
19328
+ // closes the abandon arms (no prepared-join commit, a declined
19329
+ // native commit, a downgrade to the plain join) without having
19330
+ // to enumerate them.
19331
+ this._coordinates.settleResidentCoordinateSnapshot(
19332
+ nativeReceiveCoordinateBatch?.rollbackCoordinateEntries,
19333
+ );
19194
19334
  }
19195
- this.rebalanceParticipationDebounced?.call();
19196
19335
  }
19197
19336
 
19198
19337
  for (const plan of joinPlans) {