@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/coordinate-persistence.d.ts +18 -1
- package/dist/src/coordinate-persistence.d.ts.map +1 -1
- package/dist/src/coordinate-persistence.js +61 -4
- package/dist/src/coordinate-persistence.js.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +313 -201
- package/dist/src/index.js.map +1 -1
- package/package.json +13 -13
- package/src/coordinate-persistence.ts +66 -5
- package/src/index.ts +397 -258
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 ->
|
|
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
|
-
|
|
1522
|
-
|
|
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
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
!
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
|
|
12943
|
-
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
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
|
-
|
|
12951
|
-
|
|
12952
|
-
|
|
12953
|
-
|
|
12954
|
-
|
|
12955
|
-
|
|
12956
|
-
|
|
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
|
-
.
|
|
12964
|
-
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
12972
|
-
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
|
|
12993
|
-
|
|
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
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
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
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
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
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
|
|
13111
|
-
|
|
13112
|
-
|
|
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
|
-
|
|
13116
|
-
|
|
13117
|
-
|
|
13118
|
-
|
|
13119
|
-
|
|
13120
|
-
|
|
13121
|
-
|
|
13122
|
-
|
|
13123
|
-
|
|
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) {
|