@peerbit/shared-log 13.2.33 → 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 +284 -91
- 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 +84 -4
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +441 -7
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts +20 -0
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +177 -55
- 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 +11 -11
- package/src/index.ts +350 -123
- package/src/peer-session.ts +8 -0
- package/src/replication-announcement.ts +17 -2
- package/src/replication-info-v2-receive.ts +634 -11
- package/src/replication-info-v2-send.ts +227 -65
- 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(),
|
|
@@ -3415,6 +3420,8 @@ export class SharedLog<
|
|
|
3415
3420
|
validatePersistedReplicationRangeSnapshot: (ranges) =>
|
|
3416
3421
|
this.validatePersistedReplicationRangeSnapshot(ranges),
|
|
3417
3422
|
isClosed: () => this.closed,
|
|
3423
|
+
isPeerSessionCurrent: (peerHash, peerSession) =>
|
|
3424
|
+
this._peerSessions.isCurrent(peerHash, peerSession),
|
|
3418
3425
|
isPeerSessionOpen: (peerHash, peerSession) =>
|
|
3419
3426
|
this._peerSessions.isCurrent(peerHash, peerSession) &&
|
|
3420
3427
|
(peerSession as PeerSession).phase === "open" &&
|
|
@@ -3495,6 +3502,12 @@ export class SharedLog<
|
|
|
3495
3502
|
(this.node.services.pubsub as unknown as { session: number }).session,
|
|
3496
3503
|
),
|
|
3497
3504
|
isClosed: () => this.closed,
|
|
3505
|
+
isPeerSessionCurrent: (peerHash, peerSession) =>
|
|
3506
|
+
this._peerSessions.isCurrent(peerHash, peerSession) &&
|
|
3507
|
+
(peerSession as PeerSession).phase === "open" &&
|
|
3508
|
+
(peerSession as PeerSession).isActive(),
|
|
3509
|
+
isReceiveEpochCurrent: (peerHash, receiveEpoch) =>
|
|
3510
|
+
this._peerSessions.isReceiveEpochCurrent(peerHash, receiveEpoch),
|
|
3498
3511
|
isPeerStateCurrent: (peerHash, peerSession, receiveEpoch) =>
|
|
3499
3512
|
this._peerSessions.isCurrent(peerHash, peerSession) &&
|
|
3500
3513
|
(peerSession as PeerSession).phase === "open" &&
|
|
@@ -3532,6 +3545,8 @@ export class SharedLog<
|
|
|
3532
3545
|
}`,
|
|
3533
3546
|
);
|
|
3534
3547
|
},
|
|
3548
|
+
onLocalCapabilityError: (error) =>
|
|
3549
|
+
this.handleReplicationLifecycleSendError(error),
|
|
3535
3550
|
});
|
|
3536
3551
|
}
|
|
3537
3552
|
|
|
@@ -3730,6 +3745,14 @@ export class SharedLog<
|
|
|
3730
3745
|
return this._logProperties?.compatibility;
|
|
3731
3746
|
}
|
|
3732
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
|
+
|
|
3733
3756
|
get isAdaptiveReplicating() {
|
|
3734
3757
|
return this._isAdaptiveReplicating;
|
|
3735
3758
|
}
|
|
@@ -6527,10 +6550,16 @@ export class SharedLog<
|
|
|
6527
6550
|
// deliver the authoritative Full that completes recovery.
|
|
6528
6551
|
const peerSession = this._peerSessions.current(keyHash);
|
|
6529
6552
|
if (peerSession?.phase === "open") {
|
|
6553
|
+
const receiveEpoch = this._peerSessions.receiveEpoch(keyHash);
|
|
6530
6554
|
this._v2Receive.advanceRecovery({
|
|
6531
6555
|
peerHash: keyHash,
|
|
6532
6556
|
peerSession,
|
|
6533
|
-
receiveEpoch
|
|
6557
|
+
receiveEpoch,
|
|
6558
|
+
});
|
|
6559
|
+
this._v2Receive.reAdvertiseLocalCapabilityForRecovery({
|
|
6560
|
+
peerHash: keyHash,
|
|
6561
|
+
peerSession,
|
|
6562
|
+
receiveEpoch,
|
|
6534
6563
|
});
|
|
6535
6564
|
}
|
|
6536
6565
|
}
|
|
@@ -14147,7 +14176,9 @@ export class SharedLog<
|
|
|
14147
14176
|
this.domain = options?.domain
|
|
14148
14177
|
? (options.domain(this) as unknown as D)
|
|
14149
14178
|
: (createReplicationDomainHash(
|
|
14150
|
-
options?.compatibility && options
|
|
14179
|
+
options?.compatibility !== undefined && options.compatibility < 10
|
|
14180
|
+
? "u32"
|
|
14181
|
+
: "u64",
|
|
14151
14182
|
)(this) as unknown as D);
|
|
14152
14183
|
this.indexableDomain = createIndexableDomainFromResolution(
|
|
14153
14184
|
this.domain.resolution,
|
|
@@ -14293,8 +14324,12 @@ export class SharedLog<
|
|
|
14293
14324
|
}
|
|
14294
14325
|
|
|
14295
14326
|
this._closeController = new AbortController();
|
|
14296
|
-
this.
|
|
14297
|
-
|
|
14327
|
+
if (this.legacyReplicationInfoEnabled) {
|
|
14328
|
+
this._announcements.setupReplicationAnnouncementRetryFunction();
|
|
14329
|
+
this._announcements.setupReplicationAnnouncementRepairFunction();
|
|
14330
|
+
} else {
|
|
14331
|
+
this._announcements.cancelCurrentReplicationStateAnnouncementRetry();
|
|
14332
|
+
}
|
|
14298
14333
|
this._closeController.signal.addEventListener("abort", () => {
|
|
14299
14334
|
for (const [_peer, state] of this._replicationInfoRequestByPeer) {
|
|
14300
14335
|
if (state.timer) clearTimeout(state.timer);
|
|
@@ -15496,7 +15531,14 @@ export class SharedLog<
|
|
|
15496
15531
|
|
|
15497
15532
|
async afterOpen(): Promise<void> {
|
|
15498
15533
|
await super.afterOpen();
|
|
15499
|
-
|
|
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
|
+
);
|
|
15500
15542
|
const replicationLifecycleController =
|
|
15501
15543
|
this._instanceLifecycle?.membershipLifecycleController;
|
|
15502
15544
|
|
|
@@ -15517,6 +15559,7 @@ export class SharedLog<
|
|
|
15517
15559
|
this._liveness.startReplicatorLivenessSweep();
|
|
15518
15560
|
|
|
15519
15561
|
await this.rebalanceParticipation();
|
|
15562
|
+
await subscriberDiscoveryPromise;
|
|
15520
15563
|
|
|
15521
15564
|
// Take into account existing subscription
|
|
15522
15565
|
(await existingSubscribersPromise)?.forEach((v) => {
|
|
@@ -15526,6 +15569,14 @@ export class SharedLog<
|
|
|
15526
15569
|
if (this.closed) {
|
|
15527
15570
|
return;
|
|
15528
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
|
+
}
|
|
15529
15580
|
void this.runSubscriptionChangeCallback(() =>
|
|
15530
15581
|
this.handleSubscriptionChange(v, [this.topic], true),
|
|
15531
15582
|
);
|
|
@@ -15738,7 +15789,9 @@ export class SharedLog<
|
|
|
15738
15789
|
const peerSession = this._peerSessions.current(peerHash);
|
|
15739
15790
|
const preserveV2Session =
|
|
15740
15791
|
peerSession?.phase === "open" && peerSession.isActive();
|
|
15741
|
-
this.
|
|
15792
|
+
if (this.legacyReplicationInfoEnabled || !preserveV2Session) {
|
|
15793
|
+
this.cancelReplicationInfoRequests(peerHash);
|
|
15794
|
+
}
|
|
15742
15795
|
this._liveness._replicatorLivenessFailures.delete(peerHash);
|
|
15743
15796
|
this._liveness._replicatorLastActivityAt.delete(peerHash);
|
|
15744
15797
|
if (!preserveV2Session) {
|
|
@@ -16909,15 +16962,18 @@ export class SharedLog<
|
|
|
16909
16962
|
}, 2_000);
|
|
16910
16963
|
try {
|
|
16911
16964
|
const reset = new AllReplicatingSegmentsMessage({ segments: [] });
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
16916
|
-
|
|
16917
|
-
|
|
16918
|
-
|
|
16919
|
-
|
|
16920
|
-
|
|
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);
|
|
16921
16977
|
} finally {
|
|
16922
16978
|
clearTimeout(abortTimer);
|
|
16923
16979
|
}
|
|
@@ -17039,15 +17095,18 @@ export class SharedLog<
|
|
|
17039
17095
|
}, 2_000);
|
|
17040
17096
|
try {
|
|
17041
17097
|
const reset = new AllReplicatingSegmentsMessage({ segments: [] });
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
|
|
17050
|
-
|
|
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);
|
|
17051
17110
|
} finally {
|
|
17052
17111
|
clearTimeout(abortTimer);
|
|
17053
17112
|
}
|
|
@@ -17573,6 +17632,19 @@ export class SharedLog<
|
|
|
17573
17632
|
if (!context.from) {
|
|
17574
17633
|
throw new Error("Missing from in update role message");
|
|
17575
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
|
+
}
|
|
17576
17648
|
// Snapshot receive ownership before any async handler gets a chance to
|
|
17577
17649
|
// yield. Replication-info messages reach their branch only after the
|
|
17578
17650
|
// synchronizer declines them, and a U/S transition can happen meanwhile.
|
|
@@ -19294,19 +19366,37 @@ export class SharedLog<
|
|
|
19294
19366
|
timestamp: capabilityTimestamp,
|
|
19295
19367
|
openingSession: receiveSession!,
|
|
19296
19368
|
});
|
|
19297
|
-
} else
|
|
19298
|
-
this.observePeerSyncCapabilities({
|
|
19369
|
+
} else {
|
|
19370
|
+
const observed = this.observePeerSyncCapabilities({
|
|
19299
19371
|
peerHash: receiveFromHash,
|
|
19300
19372
|
capabilities: msg.capabilities,
|
|
19301
19373
|
transportSession: capabilityTransportSession,
|
|
19302
19374
|
timestamp: capabilityTimestamp,
|
|
19303
|
-
})
|
|
19304
|
-
receiveSession?.phase === "open"
|
|
19305
|
-
|
|
19306
|
-
|
|
19307
|
-
|
|
19308
|
-
|
|
19309
|
-
)
|
|
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
|
+
}
|
|
19310
19400
|
}
|
|
19311
19401
|
}
|
|
19312
19402
|
return;
|
|
@@ -19873,7 +19963,10 @@ export class SharedLog<
|
|
|
19873
19963
|
return;
|
|
19874
19964
|
}
|
|
19875
19965
|
this._liveness.markReplicatorActivity(fromHash);
|
|
19876
|
-
if (
|
|
19966
|
+
if (
|
|
19967
|
+
msg instanceof FullReplicationInfoV2Message &&
|
|
19968
|
+
this.legacyReplicationInfoEnabled
|
|
19969
|
+
) {
|
|
19877
19970
|
this.cancelReplicationInfoRequests(fromHash);
|
|
19878
19971
|
}
|
|
19879
19972
|
});
|
|
@@ -20537,6 +20630,7 @@ export class SharedLog<
|
|
|
20537
20630
|
}, timeoutMs);
|
|
20538
20631
|
|
|
20539
20632
|
let requestAttempts = 0;
|
|
20633
|
+
let subscriberSnapshotInFlight: Promise<void> | undefined;
|
|
20540
20634
|
const requestIntervalMs = this.waitForReplicatorRequestIntervalMs;
|
|
20541
20635
|
const maxRequestAttempts =
|
|
20542
20636
|
this.waitForReplicatorRequestMaxAttempts ??
|
|
@@ -20544,6 +20638,26 @@ export class SharedLog<
|
|
|
20544
20638
|
WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS,
|
|
20545
20639
|
Math.ceil(timeoutMs / requestIntervalMs),
|
|
20546
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
|
+
};
|
|
20547
20661
|
|
|
20548
20662
|
const requestReplicationInfo = () => {
|
|
20549
20663
|
if (settled || this.closed) {
|
|
@@ -20556,17 +20670,35 @@ export class SharedLog<
|
|
|
20556
20670
|
|
|
20557
20671
|
requestAttempts++;
|
|
20558
20672
|
|
|
20559
|
-
this.
|
|
20560
|
-
.
|
|
20561
|
-
|
|
20562
|
-
|
|
20563
|
-
|
|
20564
|
-
|
|
20565
|
-
|
|
20566
|
-
|
|
20567
|
-
|
|
20568
|
-
|
|
20569
|
-
|
|
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
|
+
}
|
|
20570
20702
|
|
|
20571
20703
|
if (requestAttempts < maxRequestAttempts) {
|
|
20572
20704
|
requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
|
|
@@ -23300,6 +23432,73 @@ export class SharedLog<
|
|
|
23300
23432
|
this._replicationInfoRequestByPeer.delete(peerHash);
|
|
23301
23433
|
}
|
|
23302
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
|
+
|
|
23303
23502
|
private scheduleReplicationInfoRequests(
|
|
23304
23503
|
peer: PublicSignKey,
|
|
23305
23504
|
replicationLifecycleController = this._instanceLifecycle
|
|
@@ -23311,6 +23510,13 @@ export class SharedLog<
|
|
|
23311
23510
|
) {
|
|
23312
23511
|
return;
|
|
23313
23512
|
}
|
|
23513
|
+
if (!this.legacyReplicationInfoEnabled) {
|
|
23514
|
+
this.scheduleReplicationInfoV2Recovery(
|
|
23515
|
+
peer,
|
|
23516
|
+
replicationLifecycleController,
|
|
23517
|
+
);
|
|
23518
|
+
return;
|
|
23519
|
+
}
|
|
23314
23520
|
const peerHash = peer.hashcode();
|
|
23315
23521
|
const requestStates = this._replicationInfoRequestByPeer;
|
|
23316
23522
|
if (requestStates.has(peerHash)) {
|
|
@@ -23344,7 +23550,6 @@ export class SharedLog<
|
|
|
23344
23550
|
cancel();
|
|
23345
23551
|
return;
|
|
23346
23552
|
}
|
|
23347
|
-
|
|
23348
23553
|
state.attempts++;
|
|
23349
23554
|
|
|
23350
23555
|
this.rpc
|
|
@@ -23381,6 +23586,7 @@ export class SharedLog<
|
|
|
23381
23586
|
topics: string[],
|
|
23382
23587
|
subscribed: boolean,
|
|
23383
23588
|
subscriptionEpoch?: PeerSession,
|
|
23589
|
+
subscriptionTransportSession?: bigint,
|
|
23384
23590
|
) {
|
|
23385
23591
|
if (!topics.includes(this.topic)) {
|
|
23386
23592
|
return;
|
|
@@ -23403,13 +23609,31 @@ export class SharedLog<
|
|
|
23403
23609
|
if (!ownsSubscriptionEpoch()) {
|
|
23404
23610
|
return;
|
|
23405
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);
|
|
23406
23616
|
// A destination stream is scoped to exactly one topic-subscription
|
|
23407
23617
|
// session. Abort the predecessor synchronously before either barrier can
|
|
23408
23618
|
// yield; a late queue completion must never enter the new session.
|
|
23409
23619
|
this._v2Receive.clearPeer(peerHash);
|
|
23410
23620
|
this._v2Send.clearPeer(peerHash);
|
|
23411
|
-
|
|
23412
|
-
|
|
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
|
+
}
|
|
23413
23637
|
if (subscribed) {
|
|
23414
23638
|
const pendingOpeningCapabilities =
|
|
23415
23639
|
this._openingSyncCapabilitiesByPeer.get(peerHash);
|
|
@@ -23548,76 +23772,57 @@ export class SharedLog<
|
|
|
23548
23772
|
expectedSubscriptionEpoch,
|
|
23549
23773
|
);
|
|
23550
23774
|
|
|
23551
|
-
// Decode, sender and authenticated apply readiness are separate bits.
|
|
23552
|
-
//
|
|
23553
|
-
//
|
|
23554
|
-
//
|
|
23555
|
-
//
|
|
23775
|
+
// Decode, sender and authenticated apply readiness are separate bits. An
|
|
23776
|
+
// ACKed capability advert is the local half of receiver-led negotiation;
|
|
23777
|
+
// readiness is promoted only after the legacy startup path has completed.
|
|
23778
|
+
// This preserves mixed-version discovery without putting capability ACK
|
|
23779
|
+
// latency on the subscription callback's critical path.
|
|
23556
23780
|
const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
|
|
23557
23781
|
const localCapabilityAdvertisement =
|
|
23558
|
-
this.
|
|
23782
|
+
this._v2Receive.advertiseLocalCapability({
|
|
23559
23783
|
target: publicKey,
|
|
23560
23784
|
peerSession: expectedSubscriptionEpoch,
|
|
23561
23785
|
receiveEpoch,
|
|
23562
23786
|
signal: replicationLifecycleController.signal,
|
|
23563
|
-
}).catch((error) => {
|
|
23564
|
-
this.handleReplicationLifecycleSendError(
|
|
23565
|
-
error,
|
|
23566
|
-
replicationLifecycleController,
|
|
23567
|
-
);
|
|
23568
|
-
return undefined;
|
|
23569
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
|
+
}
|
|
23570
23799
|
|
|
23571
|
-
let replicationSegments: ReplicationRangeIndexable<R>[];
|
|
23572
23800
|
try {
|
|
23573
|
-
replicationSegments
|
|
23574
|
-
|
|
23575
|
-
|
|
23576
|
-
|
|
23577
|
-
|
|
23578
|
-
|
|
23579
|
-
|
|
23801
|
+
let replicationSegments: ReplicationRangeIndexable<R>[];
|
|
23802
|
+
try {
|
|
23803
|
+
replicationSegments = await this.getMyReplicationSegments();
|
|
23804
|
+
} catch (error) {
|
|
23805
|
+
if (
|
|
23806
|
+
!this.isReplicationLifecycleActive(replicationLifecycleController) &&
|
|
23807
|
+
isNotStartedError(error as Error)
|
|
23808
|
+
) {
|
|
23809
|
+
return;
|
|
23810
|
+
}
|
|
23811
|
+
throw error;
|
|
23580
23812
|
}
|
|
23581
|
-
throw error;
|
|
23582
|
-
}
|
|
23583
|
-
if (
|
|
23584
|
-
!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
23585
|
-
!ownsSubscriptionEpoch()
|
|
23586
|
-
) {
|
|
23587
|
-
return;
|
|
23588
|
-
}
|
|
23589
|
-
if (replicationSegments.length > 0) {
|
|
23590
|
-
const segments = replicationSegments.map((x) => x.toReplicationRange());
|
|
23591
|
-
this.validatePersistedReplicationRangeSnapshot(segments);
|
|
23592
|
-
await this.rpc
|
|
23593
|
-
.send(
|
|
23594
|
-
new AllReplicatingSegmentsMessage({
|
|
23595
|
-
segments,
|
|
23596
|
-
}),
|
|
23597
|
-
{
|
|
23598
|
-
mode: new AcknowledgeDelivery({ redundancy: 1, to: [publicKey] }),
|
|
23599
|
-
signal: replicationLifecycleController.signal,
|
|
23600
|
-
},
|
|
23601
|
-
)
|
|
23602
|
-
.catch((error) =>
|
|
23603
|
-
this.handleReplicationLifecycleSendError(
|
|
23604
|
-
error,
|
|
23605
|
-
replicationLifecycleController,
|
|
23606
|
-
),
|
|
23607
|
-
);
|
|
23608
23813
|
if (
|
|
23609
23814
|
!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
23610
23815
|
!ownsSubscriptionEpoch()
|
|
23611
23816
|
) {
|
|
23612
23817
|
return;
|
|
23613
23818
|
}
|
|
23614
|
-
|
|
23615
|
-
|
|
23616
|
-
|
|
23819
|
+
if (replicationSegments.length > 0) {
|
|
23820
|
+
const segments = replicationSegments.map((x) => x.toReplicationRange());
|
|
23821
|
+
this.validatePersistedReplicationRangeSnapshot(segments);
|
|
23617
23822
|
await this.rpc
|
|
23618
23823
|
.send(
|
|
23619
|
-
new
|
|
23620
|
-
|
|
23824
|
+
new AllReplicatingSegmentsMessage({
|
|
23825
|
+
segments,
|
|
23621
23826
|
}),
|
|
23622
23827
|
{
|
|
23623
23828
|
mode: new AcknowledgeDelivery({
|
|
@@ -23633,30 +23838,51 @@ export class SharedLog<
|
|
|
23633
23838
|
replicationLifecycleController,
|
|
23634
23839
|
),
|
|
23635
23840
|
);
|
|
23636
|
-
|
|
23637
|
-
|
|
23841
|
+
if (
|
|
23842
|
+
!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
23843
|
+
!ownsSubscriptionEpoch()
|
|
23844
|
+
) {
|
|
23845
|
+
return;
|
|
23846
|
+
}
|
|
23638
23847
|
|
|
23639
|
-
|
|
23640
|
-
|
|
23641
|
-
|
|
23642
|
-
|
|
23643
|
-
|
|
23644
|
-
|
|
23645
|
-
|
|
23646
|
-
|
|
23647
|
-
|
|
23648
|
-
|
|
23649
|
-
|
|
23650
|
-
|
|
23848
|
+
if (this.v8Behaviour) {
|
|
23849
|
+
// for backwards compatibility
|
|
23850
|
+
await this.rpc
|
|
23851
|
+
.send(
|
|
23852
|
+
new ResponseRoleMessage({
|
|
23853
|
+
role: this.getRoleFromReplicationSegments(replicationSegments),
|
|
23854
|
+
}),
|
|
23855
|
+
{
|
|
23856
|
+
mode: new AcknowledgeDelivery({
|
|
23857
|
+
redundancy: 1,
|
|
23858
|
+
to: [publicKey],
|
|
23859
|
+
}),
|
|
23860
|
+
signal: replicationLifecycleController.signal,
|
|
23861
|
+
},
|
|
23862
|
+
)
|
|
23863
|
+
.catch((error) =>
|
|
23864
|
+
this.handleReplicationLifecycleSendError(
|
|
23865
|
+
error,
|
|
23866
|
+
replicationLifecycleController,
|
|
23867
|
+
),
|
|
23868
|
+
);
|
|
23869
|
+
}
|
|
23870
|
+
}
|
|
23651
23871
|
|
|
23652
|
-
|
|
23653
|
-
|
|
23654
|
-
|
|
23655
|
-
|
|
23656
|
-
|
|
23657
|
-
|
|
23658
|
-
|
|
23659
|
-
|
|
23872
|
+
// Keep legacy request-based discovery independent of the capability ACK.
|
|
23873
|
+
// This makes mixed-version joins resilient to timing-sensitive delivery/order
|
|
23874
|
+
// issues where we may miss the remote peer's initial announcement.
|
|
23875
|
+
if (
|
|
23876
|
+
this.isReplicationLifecycleActive(replicationLifecycleController) &&
|
|
23877
|
+
ownsSubscriptionEpoch()
|
|
23878
|
+
) {
|
|
23879
|
+
this.scheduleReplicationInfoRequests(
|
|
23880
|
+
publicKey,
|
|
23881
|
+
replicationLifecycleController,
|
|
23882
|
+
);
|
|
23883
|
+
}
|
|
23884
|
+
} finally {
|
|
23885
|
+
localCapabilityAdvertisement.releaseLegacyBarrier();
|
|
23660
23886
|
}
|
|
23661
23887
|
}
|
|
23662
23888
|
|
|
@@ -25098,6 +25324,7 @@ export class SharedLog<
|
|
|
25098
25324
|
evt.detail.topics,
|
|
25099
25325
|
true,
|
|
25100
25326
|
subscriptionEpoch,
|
|
25327
|
+
evt.detail.session,
|
|
25101
25328
|
);
|
|
25102
25329
|
}
|
|
25103
25330
|
|