@peerbit/shared-log 13.2.34 → 13.2.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +7 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +223 -38
- package/dist/src/index.js.map +1 -1
- package/dist/src/peer-session.d.ts +2 -1
- package/dist/src/peer-session.d.ts.map +1 -1
- package/dist/src/peer-session.js +8 -2
- package/dist/src/peer-session.js.map +1 -1
- package/dist/src/replication-announcement.d.ts +1 -0
- package/dist/src/replication-announcement.d.ts.map +1 -1
- package/dist/src/replication-announcement.js +16 -2
- package/dist/src/replication-announcement.js.map +1 -1
- package/dist/src/replication-info-v2-receive.d.ts +11 -0
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +75 -5
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +22 -1
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/dist/src/sync/factory.js +1 -1
- package/dist/src/sync/factory.js.map +1 -1
- package/package.json +12 -12
- package/src/index.ts +268 -48
- package/src/peer-session.ts +8 -0
- package/src/replication-announcement.ts +17 -2
- package/src/replication-info-v2-receive.ts +96 -5
- package/src/replication-info-v2-send.ts +21 -1
- package/src/sync/factory.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -2086,7 +2086,11 @@ export class SharedLog<
|
|
|
2086
2086
|
// reconnect barrier commits. See PeerSessionRegistry._replicationInfoBlockedPeers.
|
|
2087
2087
|
private _replicationInfoRequestByPeer!: Map<
|
|
2088
2088
|
string,
|
|
2089
|
-
{
|
|
2089
|
+
{
|
|
2090
|
+
attempts: number;
|
|
2091
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
2092
|
+
peerSession?: PeerSession;
|
|
2093
|
+
}
|
|
2090
2094
|
>;
|
|
2091
2095
|
private _replicationInfoApplyQueueByPeer!: Map<string, Promise<void>>;
|
|
2092
2096
|
// Range ids are global primary keys while receive lanes are per peer. Keep
|
|
@@ -3382,6 +3386,7 @@ export class SharedLog<
|
|
|
3382
3386
|
error,
|
|
3383
3387
|
),
|
|
3384
3388
|
enqueueReplicationInfoV2: (message) => this._v2Send.enqueue(message),
|
|
3389
|
+
isLegacyReplicationInfoEnabled: () => this.legacyReplicationInfoEnabled,
|
|
3385
3390
|
isClosed: () => this.closed,
|
|
3386
3391
|
getCloseSignal: () => this._closeController.signal,
|
|
3387
3392
|
getMyReplicationSegments: () => this.getMyReplicationSegments(),
|
|
@@ -3740,6 +3745,14 @@ export class SharedLog<
|
|
|
3740
3745
|
return this._logProperties?.compatibility;
|
|
3741
3746
|
}
|
|
3742
3747
|
|
|
3748
|
+
/**
|
|
3749
|
+
* Legacy replication-info is an explicit compatibility fallback. Current
|
|
3750
|
+
* logs never infer or re-enable it from a remote peer's capabilities.
|
|
3751
|
+
*/
|
|
3752
|
+
private get legacyReplicationInfoEnabled(): boolean {
|
|
3753
|
+
return this.compatibility !== undefined && this.compatibility < 10;
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3743
3756
|
get isAdaptiveReplicating() {
|
|
3744
3757
|
return this._isAdaptiveReplicating;
|
|
3745
3758
|
}
|
|
@@ -14163,7 +14176,9 @@ export class SharedLog<
|
|
|
14163
14176
|
this.domain = options?.domain
|
|
14164
14177
|
? (options.domain(this) as unknown as D)
|
|
14165
14178
|
: (createReplicationDomainHash(
|
|
14166
|
-
options?.compatibility && options
|
|
14179
|
+
options?.compatibility !== undefined && options.compatibility < 10
|
|
14180
|
+
? "u32"
|
|
14181
|
+
: "u64",
|
|
14167
14182
|
)(this) as unknown as D);
|
|
14168
14183
|
this.indexableDomain = createIndexableDomainFromResolution(
|
|
14169
14184
|
this.domain.resolution,
|
|
@@ -14309,8 +14324,12 @@ export class SharedLog<
|
|
|
14309
14324
|
}
|
|
14310
14325
|
|
|
14311
14326
|
this._closeController = new AbortController();
|
|
14312
|
-
this.
|
|
14313
|
-
|
|
14327
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
14328
|
+
this._announcements.setupReplicationAnnouncementRetryFunction();
|
|
14329
|
+
this._announcements.setupReplicationAnnouncementRepairFunction();
|
|
14330
|
+
} else {
|
|
14331
|
+
this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
|
|
14332
|
+
}
|
|
14314
14333
|
this._closeController.signal.addEventListener("abort", () => {
|
|
14315
14334
|
for (const [_peer, state] of this._replicationInfoRequestByPeer) {
|
|
14316
14335
|
if (state.timer) clearTimeout(state.timer);
|
|
@@ -15512,7 +15531,14 @@ export class SharedLog<
|
|
|
15512
15531
|
|
|
15513
15532
|
async afterOpen(): Promise<void> {
|
|
15514
15533
|
await super.afterOpen();
|
|
15515
|
-
|
|
15534
|
+
// Start the broader discovery eagerly, in parallel with rebalance, for its
|
|
15535
|
+
// routing/cache side effects. It also contains connected/provider/fanout
|
|
15536
|
+
// candidates that have not subscribed to this log, so only the authoritative
|
|
15537
|
+
// pubsub snapshot below may create subscription fallback sessions.
|
|
15538
|
+
const subscriberDiscoveryPromise = this._getTopicSubscribers(this.topic);
|
|
15539
|
+
const existingSubscribersPromise = this.node.services.pubsub.getSubscribers(
|
|
15540
|
+
this.topic,
|
|
15541
|
+
);
|
|
15516
15542
|
const replicationLifecycleController =
|
|
15517
15543
|
this._instanceLifecycle?.membershipLifecycleController;
|
|
15518
15544
|
|
|
@@ -15533,6 +15559,7 @@ export class SharedLog<
|
|
|
15533
15559
|
this._liveness.startReplicatorLivenessSweep();
|
|
15534
15560
|
|
|
15535
15561
|
await this.rebalanceParticipation();
|
|
15562
|
+
await subscriberDiscoveryPromise;
|
|
15536
15563
|
|
|
15537
15564
|
// Take into account existing subscription
|
|
15538
15565
|
(await existingSubscribersPromise)?.forEach((v) => {
|
|
@@ -15542,6 +15569,14 @@ export class SharedLog<
|
|
|
15542
15569
|
if (this.closed) {
|
|
15543
15570
|
return;
|
|
15544
15571
|
}
|
|
15572
|
+
// The live subscribe event and this after-open snapshot can report the
|
|
15573
|
+
// same initial transport generation. The live callback rotates its
|
|
15574
|
+
// PeerSession synchronously, so any current session here proves the
|
|
15575
|
+
// fallback is stale/duplicate. Rotating again would erase the signed
|
|
15576
|
+
// capability binding that the first callback just established.
|
|
15577
|
+
if (this._peerSessions.current(v.hashcode()) !== null) {
|
|
15578
|
+
return;
|
|
15579
|
+
}
|
|
15545
15580
|
void this.runSubscriptionChangeCallback(() =>
|
|
15546
15581
|
this.handleSubscriptionChange(v, [this.topic], true),
|
|
15547
15582
|
);
|
|
@@ -15754,7 +15789,9 @@ export class SharedLog<
|
|
|
15754
15789
|
const peerSession = this._peerSessions.current(peerHash);
|
|
15755
15790
|
const preserveV2Session =
|
|
15756
15791
|
peerSession?.phase === "open" && peerSession.isActive();
|
|
15757
|
-
this.
|
|
15792
|
+
if (this.legacyReplicationInfoEnabled || !preserveV2Session) {
|
|
15793
|
+
this.cancelReplicationInfoRequests(peerHash);
|
|
15794
|
+
}
|
|
15758
15795
|
this._liveness._replicatorLivenessFailures.delete(peerHash);
|
|
15759
15796
|
this._liveness._replicatorLastActivityAt.delete(peerHash);
|
|
15760
15797
|
if (!preserveV2Session) {
|
|
@@ -16925,15 +16962,18 @@ export class SharedLog<
|
|
|
16925
16962
|
}, 2_000);
|
|
16926
16963
|
try {
|
|
16927
16964
|
const reset = new AllReplicatingSegmentsMessage({ segments: [] });
|
|
16928
|
-
|
|
16929
|
-
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
16933
|
-
|
|
16934
|
-
|
|
16935
|
-
|
|
16936
|
-
|
|
16965
|
+
const resets = [this._v2Send.sendTerminalReset(abort.signal)];
|
|
16966
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
16967
|
+
resets.push(
|
|
16968
|
+
this.rpc
|
|
16969
|
+
.send(reset, {
|
|
16970
|
+
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
16971
|
+
signal: abort.signal,
|
|
16972
|
+
})
|
|
16973
|
+
.catch(() => {}),
|
|
16974
|
+
);
|
|
16975
|
+
}
|
|
16976
|
+
await Promise.all(resets);
|
|
16937
16977
|
} finally {
|
|
16938
16978
|
clearTimeout(abortTimer);
|
|
16939
16979
|
}
|
|
@@ -17055,15 +17095,18 @@ export class SharedLog<
|
|
|
17055
17095
|
}, 2_000);
|
|
17056
17096
|
try {
|
|
17057
17097
|
const reset = new AllReplicatingSegmentsMessage({ segments: [] });
|
|
17058
|
-
|
|
17059
|
-
|
|
17060
|
-
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
|
|
17065
|
-
|
|
17066
|
-
|
|
17098
|
+
const resets = [this._v2Send.sendTerminalReset(abort.signal)];
|
|
17099
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
17100
|
+
resets.push(
|
|
17101
|
+
this.rpc
|
|
17102
|
+
.send(reset, {
|
|
17103
|
+
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
17104
|
+
signal: abort.signal,
|
|
17105
|
+
})
|
|
17106
|
+
.catch(() => {}),
|
|
17107
|
+
);
|
|
17108
|
+
}
|
|
17109
|
+
await Promise.all(resets);
|
|
17067
17110
|
} finally {
|
|
17068
17111
|
clearTimeout(abortTimer);
|
|
17069
17112
|
}
|
|
@@ -17589,6 +17632,19 @@ export class SharedLog<
|
|
|
17589
17632
|
if (!context.from) {
|
|
17590
17633
|
throw new Error("Missing from in update role message");
|
|
17591
17634
|
}
|
|
17635
|
+
if (
|
|
17636
|
+
!this.legacyReplicationInfoEnabled &&
|
|
17637
|
+
(msg instanceof RequestReplicationInfoMessage ||
|
|
17638
|
+
msg instanceof ResponseRoleMessage ||
|
|
17639
|
+
msg instanceof AllReplicatingSegmentsMessage ||
|
|
17640
|
+
msg instanceof AddedReplicationSegmentMessage ||
|
|
17641
|
+
msg instanceof StoppedReplicating)
|
|
17642
|
+
) {
|
|
17643
|
+
// These variants remain registered decode tombstones, but current logs
|
|
17644
|
+
// fail closed before leases, synchronizer work, liveness, watermarks or
|
|
17645
|
+
// mutations. Only an explicit pre-v10 compatibility open admits them.
|
|
17646
|
+
return;
|
|
17647
|
+
}
|
|
17592
17648
|
// Snapshot receive ownership before any async handler gets a chance to
|
|
17593
17649
|
// yield. Replication-info messages reach their branch only after the
|
|
17594
17650
|
// synchronizer declines them, and a U/S transition can happen meanwhile.
|
|
@@ -19310,19 +19366,37 @@ export class SharedLog<
|
|
|
19310
19366
|
timestamp: capabilityTimestamp,
|
|
19311
19367
|
openingSession: receiveSession!,
|
|
19312
19368
|
});
|
|
19313
|
-
} else
|
|
19314
|
-
this.observePeerSyncCapabilities({
|
|
19369
|
+
} else {
|
|
19370
|
+
const observed = this.observePeerSyncCapabilities({
|
|
19315
19371
|
peerHash: receiveFromHash,
|
|
19316
19372
|
capabilities: msg.capabilities,
|
|
19317
19373
|
transportSession: capabilityTransportSession,
|
|
19318
19374
|
timestamp: capabilityTimestamp,
|
|
19319
|
-
})
|
|
19320
|
-
receiveSession?.phase === "open"
|
|
19321
|
-
|
|
19322
|
-
|
|
19323
|
-
|
|
19324
|
-
|
|
19325
|
-
)
|
|
19375
|
+
});
|
|
19376
|
+
if (observed && receiveSession?.phase === "open") {
|
|
19377
|
+
this.promoteReplicationInfoV2ReceiveCapability(
|
|
19378
|
+
context.from,
|
|
19379
|
+
receiveSession,
|
|
19380
|
+
);
|
|
19381
|
+
} else if (observed && receiveSession === null) {
|
|
19382
|
+
// A capability can arrive before the sender's topic Subscribe after
|
|
19383
|
+
// reconnect. Ask that authenticated peer for its authoritative
|
|
19384
|
+
// subscriber snapshot; the resulting Subscribe creates the real
|
|
19385
|
+
// PeerSession and completes the symmetric capability handshake.
|
|
19386
|
+
// Never synthesize membership from capability traffic alone.
|
|
19387
|
+
void Promise.resolve()
|
|
19388
|
+
.then(() =>
|
|
19389
|
+
this.node.services.pubsub.requestSubscribers(
|
|
19390
|
+
this.topic,
|
|
19391
|
+
context.from,
|
|
19392
|
+
),
|
|
19393
|
+
)
|
|
19394
|
+
.catch((error) => {
|
|
19395
|
+
if (!isNotStartedError(error as Error)) {
|
|
19396
|
+
logger.error(error?.toString?.() ?? String(error));
|
|
19397
|
+
}
|
|
19398
|
+
});
|
|
19399
|
+
}
|
|
19326
19400
|
}
|
|
19327
19401
|
}
|
|
19328
19402
|
return;
|
|
@@ -19889,7 +19963,10 @@ export class SharedLog<
|
|
|
19889
19963
|
return;
|
|
19890
19964
|
}
|
|
19891
19965
|
this._liveness.markReplicatorActivity(fromHash);
|
|
19892
|
-
if (
|
|
19966
|
+
if (
|
|
19967
|
+
msg instanceof FullReplicationInfoV2Message &&
|
|
19968
|
+
this.legacyReplicationInfoEnabled
|
|
19969
|
+
) {
|
|
19893
19970
|
this.cancelReplicationInfoRequests(fromHash);
|
|
19894
19971
|
}
|
|
19895
19972
|
});
|
|
@@ -20553,6 +20630,7 @@ export class SharedLog<
|
|
|
20553
20630
|
}, timeoutMs);
|
|
20554
20631
|
|
|
20555
20632
|
let requestAttempts = 0;
|
|
20633
|
+
let subscriberSnapshotInFlight: Promise<void> | undefined;
|
|
20556
20634
|
const requestIntervalMs = this.waitForReplicatorRequestIntervalMs;
|
|
20557
20635
|
const maxRequestAttempts =
|
|
20558
20636
|
this.waitForReplicatorRequestMaxAttempts ??
|
|
@@ -20560,6 +20638,26 @@ export class SharedLog<
|
|
|
20560
20638
|
WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS,
|
|
20561
20639
|
Math.ceil(timeoutMs / requestIntervalMs),
|
|
20562
20640
|
);
|
|
20641
|
+
const requestSubscriberSnapshot = () => {
|
|
20642
|
+
if (subscriberSnapshotInFlight) {
|
|
20643
|
+
return;
|
|
20644
|
+
}
|
|
20645
|
+
subscriberSnapshotInFlight = Promise.resolve()
|
|
20646
|
+
.then(async () => {
|
|
20647
|
+
if (settled || this.closed) {
|
|
20648
|
+
return;
|
|
20649
|
+
}
|
|
20650
|
+
await this.node.services.pubsub.requestSubscribers(this.topic, key);
|
|
20651
|
+
})
|
|
20652
|
+
.catch((error) => {
|
|
20653
|
+
if (!isNotStartedError(error as Error)) {
|
|
20654
|
+
logger.error(error?.toString?.() ?? String(error));
|
|
20655
|
+
}
|
|
20656
|
+
})
|
|
20657
|
+
.finally(() => {
|
|
20658
|
+
subscriberSnapshotInFlight = undefined;
|
|
20659
|
+
});
|
|
20660
|
+
};
|
|
20563
20661
|
|
|
20564
20662
|
const requestReplicationInfo = () => {
|
|
20565
20663
|
if (settled || this.closed) {
|
|
@@ -20572,17 +20670,35 @@ export class SharedLog<
|
|
|
20572
20670
|
|
|
20573
20671
|
requestAttempts++;
|
|
20574
20672
|
|
|
20575
|
-
this.
|
|
20576
|
-
.
|
|
20577
|
-
|
|
20578
|
-
|
|
20579
|
-
|
|
20580
|
-
|
|
20581
|
-
|
|
20582
|
-
|
|
20583
|
-
|
|
20584
|
-
|
|
20585
|
-
|
|
20673
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
20674
|
+
this.rpc
|
|
20675
|
+
.send(new RequestReplicationInfoMessage(), {
|
|
20676
|
+
mode: new AcknowledgeDelivery({ redundancy: 1, to: [key] }),
|
|
20677
|
+
})
|
|
20678
|
+
.catch((e) => {
|
|
20679
|
+
// Best-effort: missing peers / unopened RPC should not fail the wait logic.
|
|
20680
|
+
if (isNotStartedError(e as Error)) {
|
|
20681
|
+
return;
|
|
20682
|
+
}
|
|
20683
|
+
logger.error(e?.toString?.() ?? String(e));
|
|
20684
|
+
});
|
|
20685
|
+
} else {
|
|
20686
|
+
const peerHash = key.hashcode();
|
|
20687
|
+
const peerSession = this._peerSessions.current(peerHash);
|
|
20688
|
+
if (peerSession?.phase === "open") {
|
|
20689
|
+
this._v2Receive.resumeParkedRequest({
|
|
20690
|
+
peerHash,
|
|
20691
|
+
peerSession,
|
|
20692
|
+
receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
|
|
20693
|
+
});
|
|
20694
|
+
} else if (peerSession === null || peerSession.phase === "departing") {
|
|
20695
|
+
// A peer can be known to routing before its SharedLog topic
|
|
20696
|
+
// subscription has been observed. Legacy requests used to bootstrap
|
|
20697
|
+
// that case directly; V2 needs an authoritative Subscribe snapshot
|
|
20698
|
+
// before it can create a fenced PeerSession and request a Full.
|
|
20699
|
+
requestSubscriberSnapshot();
|
|
20700
|
+
}
|
|
20701
|
+
}
|
|
20586
20702
|
|
|
20587
20703
|
if (requestAttempts < maxRequestAttempts) {
|
|
20588
20704
|
requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
|
|
@@ -23316,6 +23432,73 @@ export class SharedLog<
|
|
|
23316
23432
|
this._replicationInfoRequestByPeer.delete(peerHash);
|
|
23317
23433
|
}
|
|
23318
23434
|
|
|
23435
|
+
private scheduleReplicationInfoV2Recovery(
|
|
23436
|
+
peer: PublicSignKey,
|
|
23437
|
+
replicationLifecycleController = this._instanceLifecycle
|
|
23438
|
+
?.membershipLifecycleController,
|
|
23439
|
+
) {
|
|
23440
|
+
if (
|
|
23441
|
+
!replicationLifecycleController ||
|
|
23442
|
+
!this.isReplicationLifecycleActive(replicationLifecycleController)
|
|
23443
|
+
) {
|
|
23444
|
+
return;
|
|
23445
|
+
}
|
|
23446
|
+
const peerHash = peer.hashcode();
|
|
23447
|
+
const peerSession = this._peerSessions.current(peerHash);
|
|
23448
|
+
if (!peerSession || peerSession.phase !== "open") {
|
|
23449
|
+
return;
|
|
23450
|
+
}
|
|
23451
|
+
const requestStates = this._replicationInfoRequestByPeer;
|
|
23452
|
+
const existing = requestStates.get(peerHash);
|
|
23453
|
+
if (existing?.peerSession === peerSession) {
|
|
23454
|
+
return;
|
|
23455
|
+
}
|
|
23456
|
+
if (existing) {
|
|
23457
|
+
if (existing.timer) {
|
|
23458
|
+
clearTimeout(existing.timer);
|
|
23459
|
+
}
|
|
23460
|
+
requestStates.delete(peerHash);
|
|
23461
|
+
}
|
|
23462
|
+
const state: {
|
|
23463
|
+
attempts: number;
|
|
23464
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
23465
|
+
peerSession: PeerSession;
|
|
23466
|
+
} = {
|
|
23467
|
+
attempts: 0,
|
|
23468
|
+
peerSession,
|
|
23469
|
+
};
|
|
23470
|
+
requestStates.set(peerHash, state);
|
|
23471
|
+
const cancel = () => {
|
|
23472
|
+
if (requestStates.get(peerHash) !== state) {
|
|
23473
|
+
return;
|
|
23474
|
+
}
|
|
23475
|
+
if (state.timer) {
|
|
23476
|
+
clearTimeout(state.timer);
|
|
23477
|
+
}
|
|
23478
|
+
requestStates.delete(peerHash);
|
|
23479
|
+
};
|
|
23480
|
+
const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
|
|
23481
|
+
const tick = () => {
|
|
23482
|
+
if (
|
|
23483
|
+
!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
23484
|
+
peerSession.phase !== "open" ||
|
|
23485
|
+
!this._peerSessions.isCurrent(peerHash, peerSession)
|
|
23486
|
+
) {
|
|
23487
|
+
cancel();
|
|
23488
|
+
return;
|
|
23489
|
+
}
|
|
23490
|
+
this._v2Receive.resumeParkedRequest({
|
|
23491
|
+
peerHash,
|
|
23492
|
+
peerSession,
|
|
23493
|
+
receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
|
|
23494
|
+
});
|
|
23495
|
+
state.attempts++;
|
|
23496
|
+
state.timer = setTimeout(tick, intervalMs);
|
|
23497
|
+
state.timer.unref?.();
|
|
23498
|
+
};
|
|
23499
|
+
tick();
|
|
23500
|
+
}
|
|
23501
|
+
|
|
23319
23502
|
private scheduleReplicationInfoRequests(
|
|
23320
23503
|
peer: PublicSignKey,
|
|
23321
23504
|
replicationLifecycleController = this._instanceLifecycle
|
|
@@ -23327,6 +23510,13 @@ export class SharedLog<
|
|
|
23327
23510
|
) {
|
|
23328
23511
|
return;
|
|
23329
23512
|
}
|
|
23513
|
+
if (!this.legacyReplicationInfoEnabled) {
|
|
23514
|
+
this.scheduleReplicationInfoV2Recovery(
|
|
23515
|
+
peer,
|
|
23516
|
+
replicationLifecycleController,
|
|
23517
|
+
);
|
|
23518
|
+
return;
|
|
23519
|
+
}
|
|
23330
23520
|
const peerHash = peer.hashcode();
|
|
23331
23521
|
const requestStates = this._replicationInfoRequestByPeer;
|
|
23332
23522
|
if (requestStates.has(peerHash)) {
|
|
@@ -23360,7 +23550,6 @@ export class SharedLog<
|
|
|
23360
23550
|
cancel();
|
|
23361
23551
|
return;
|
|
23362
23552
|
}
|
|
23363
|
-
|
|
23364
23553
|
state.attempts++;
|
|
23365
23554
|
|
|
23366
23555
|
this.rpc
|
|
@@ -23397,6 +23586,7 @@ export class SharedLog<
|
|
|
23397
23586
|
topics: string[],
|
|
23398
23587
|
subscribed: boolean,
|
|
23399
23588
|
subscriptionEpoch?: PeerSession,
|
|
23589
|
+
subscriptionTransportSession?: bigint,
|
|
23400
23590
|
) {
|
|
23401
23591
|
if (!topics.includes(this.topic)) {
|
|
23402
23592
|
return;
|
|
@@ -23419,13 +23609,31 @@ export class SharedLog<
|
|
|
23419
23609
|
if (!ownsSubscriptionEpoch()) {
|
|
23420
23610
|
return;
|
|
23421
23611
|
}
|
|
23612
|
+
// A reconnect can arrive before the previous exact-session recovery tick
|
|
23613
|
+
// observes its stale session. Retire that job synchronously so it cannot
|
|
23614
|
+
// suppress the replacement session's scheduler in the shared peer slot.
|
|
23615
|
+
this.cancelReplicationInfoRequests(peerHash);
|
|
23422
23616
|
// A destination stream is scoped to exactly one topic-subscription
|
|
23423
23617
|
// session. Abort the predecessor synchronously before either barrier can
|
|
23424
23618
|
// yield; a late queue completion must never enter the new session.
|
|
23425
23619
|
this._v2Receive.clearPeer(peerHash);
|
|
23426
23620
|
this._v2Send.clearPeer(peerHash);
|
|
23427
|
-
|
|
23428
|
-
|
|
23621
|
+
const capabilityTransportSession =
|
|
23622
|
+
this._peerSyncCapabilitySessions.get(peerHash);
|
|
23623
|
+
const canInheritCapability =
|
|
23624
|
+
subscribed &&
|
|
23625
|
+
(subscriptionTransportSession !== undefined
|
|
23626
|
+
? capabilityTransportSession === subscriptionTransportSession
|
|
23627
|
+
: !expectedSubscriptionEpoch.hasPredecessor);
|
|
23628
|
+
if (!canInheritCapability) {
|
|
23629
|
+
// Departures and successor openings revoke the signed transport binding.
|
|
23630
|
+
// A live successor may inherit a capability that arrived just before its
|
|
23631
|
+
// Subscribe only when both signed frames belong to the same transport
|
|
23632
|
+
// generation. Snapshot fallbacks lack that proof, so only their first
|
|
23633
|
+
// opening may inherit pre-opening capability state.
|
|
23634
|
+
this._peerSyncCapabilitySessions.delete(peerHash);
|
|
23635
|
+
this._peerSyncCapabilityTimestamps.delete(peerHash);
|
|
23636
|
+
}
|
|
23429
23637
|
if (subscribed) {
|
|
23430
23638
|
const pendingOpeningCapabilities =
|
|
23431
23639
|
this._openingSyncCapabilitiesByPeer.get(peerHash);
|
|
@@ -23577,6 +23785,17 @@ export class SharedLog<
|
|
|
23577
23785
|
receiveEpoch,
|
|
23578
23786
|
signal: replicationLifecycleController.signal,
|
|
23579
23787
|
});
|
|
23788
|
+
if (!this.legacyReplicationInfoEnabled) {
|
|
23789
|
+
// Current logs have no legacy startup work to order ahead of RequestV2.
|
|
23790
|
+
// Releasing is synchronous and exact-session fenced; the ACK may still
|
|
23791
|
+
// arrive later and promote readiness through the coordinator.
|
|
23792
|
+
localCapabilityAdvertisement.releaseLegacyBarrier();
|
|
23793
|
+
this.scheduleReplicationInfoV2Recovery(
|
|
23794
|
+
publicKey,
|
|
23795
|
+
replicationLifecycleController,
|
|
23796
|
+
);
|
|
23797
|
+
return;
|
|
23798
|
+
}
|
|
23580
23799
|
|
|
23581
23800
|
try {
|
|
23582
23801
|
let replicationSegments: ReplicationRangeIndexable<R>[];
|
|
@@ -25105,6 +25324,7 @@ export class SharedLog<
|
|
|
25105
25324
|
evt.detail.topics,
|
|
25106
25325
|
true,
|
|
25107
25326
|
subscriptionEpoch,
|
|
25327
|
+
evt.detail.session,
|
|
25108
25328
|
);
|
|
25109
25329
|
}
|
|
25110
25330
|
|
package/src/peer-session.ts
CHANGED
|
@@ -31,6 +31,11 @@ export type PeerReceiveAdmissionOptions = {
|
|
|
31
31
|
export class PeerSession {
|
|
32
32
|
readonly peerHash: string;
|
|
33
33
|
readonly kind: PeerSessionKind;
|
|
34
|
+
// True when rotate() superseded an earlier subscription generation. This is
|
|
35
|
+
// deliberately independent of the predecessor's phase/kind: a newer
|
|
36
|
+
// transport can announce a subscription without first delivering an
|
|
37
|
+
// unsubscribe, and must not inherit the old transport's signed capability.
|
|
38
|
+
readonly hasPredecessor: boolean;
|
|
34
39
|
// The lifecycle controller live at rotation. All current seams pair the
|
|
35
40
|
// epoch check with a lifecycle check against a controller captured in the
|
|
36
41
|
// same synchronous window as the epoch advance; capturing it here
|
|
@@ -59,10 +64,12 @@ export class PeerSession {
|
|
|
59
64
|
peerHash: string,
|
|
60
65
|
kind: PeerSessionKind,
|
|
61
66
|
replicationLifecycleController: AbortController | undefined,
|
|
67
|
+
hasPredecessor: boolean,
|
|
62
68
|
) {
|
|
63
69
|
this.peerHash = peerHash;
|
|
64
70
|
this.kind = kind;
|
|
65
71
|
this.replicationLifecycleController = replicationLifecycleController;
|
|
72
|
+
this.hasPredecessor = hasPredecessor;
|
|
66
73
|
this.phase = kind;
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -278,6 +285,7 @@ export class PeerSessionRegistry {
|
|
|
278
285
|
peerHash,
|
|
279
286
|
kind,
|
|
280
287
|
this.deps.getReplicationLifecycleController(),
|
|
288
|
+
previous !== undefined,
|
|
281
289
|
);
|
|
282
290
|
this.sessions.set(peerHash, next);
|
|
283
291
|
return next;
|
|
@@ -204,9 +204,10 @@ export type ReplicationAnnouncementDeps<R extends "u32" | "u64"> = {
|
|
|
204
204
|
// Owner-routed so coordinator spies keep observing re-entrant queueing.
|
|
205
205
|
queueCurrentReplicationStateAnnouncementRepair: () => void;
|
|
206
206
|
queueCurrentReplicationStateAnnouncementRetry: (error: unknown) => boolean;
|
|
207
|
-
//
|
|
208
|
-
// retains its exact rejection/retry semantics
|
|
207
|
+
// V2 is always current. Legacy publication is enabled only for an explicit
|
|
208
|
+
// compatibility open and retains its exact rejection/retry semantics there.
|
|
209
209
|
enqueueReplicationInfoV2: (message: LegacyReplicationInfoMessage) => void;
|
|
210
|
+
isLegacyReplicationInfoEnabled: () => boolean;
|
|
210
211
|
};
|
|
211
212
|
|
|
212
213
|
type LegacyReplicationInfoMessage =
|
|
@@ -270,6 +271,7 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
|
|
|
270
271
|
|
|
271
272
|
queueCurrentReplicationStateAnnouncementRetry(error: unknown): boolean {
|
|
272
273
|
if (
|
|
274
|
+
!this.deps.isLegacyReplicationInfoEnabled() ||
|
|
273
275
|
this.deps.isClosed() ||
|
|
274
276
|
this.deps.getCloseSignal().aborted ||
|
|
275
277
|
this._replicationAnnouncementRetryController.signal.aborted ||
|
|
@@ -362,6 +364,7 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
|
|
|
362
364
|
|
|
363
365
|
queueCurrentReplicationStateAnnouncementRepair(): void {
|
|
364
366
|
if (
|
|
367
|
+
!this.deps.isLegacyReplicationInfoEnabled() ||
|
|
365
368
|
this.deps.isClosed() ||
|
|
366
369
|
this.deps.getCloseSignal().aborted ||
|
|
367
370
|
this._replicationAnnouncementRepairController.signal.aborted ||
|
|
@@ -437,6 +440,11 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
|
|
|
437
440
|
async repairCurrentReplicationStateAnnouncement(
|
|
438
441
|
context?: ReplicationAnnouncementRepairWorkerContext,
|
|
439
442
|
): Promise<void> {
|
|
443
|
+
if (!this.deps.isLegacyReplicationInfoEnabled()) {
|
|
444
|
+
this._announcementRepairBinding.pending = false;
|
|
445
|
+
this._announcementRepairBinding.targets.clear();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
440
448
|
if (!this._announcementRepairBinding.pending) {
|
|
441
449
|
return;
|
|
442
450
|
}
|
|
@@ -645,6 +653,9 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
|
|
|
645
653
|
this.rotateAnnouncementSession();
|
|
646
654
|
this.advanceCurrentReplicationStateAnnouncementRepairGeneration();
|
|
647
655
|
this.deps.enqueueReplicationInfoV2(message);
|
|
656
|
+
if (!this.deps.isLegacyReplicationInfoEnabled()) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
648
659
|
try {
|
|
649
660
|
await this.deps.getRpc().send(message, {
|
|
650
661
|
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
@@ -677,6 +688,10 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
|
|
|
677
688
|
}
|
|
678
689
|
|
|
679
690
|
async retryCurrentReplicationStateAnnouncement(): Promise<void> {
|
|
691
|
+
if (!this.deps.isLegacyReplicationInfoEnabled()) {
|
|
692
|
+
this._replicationAnnouncementRetryPending = false;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
680
695
|
const session = this._announcementSession;
|
|
681
696
|
const controller = this._replicationAnnouncementRetryController;
|
|
682
697
|
try {
|