@peerbit/shared-log 16.0.22 → 16.0.23
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 +78 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +559 -16
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts +6 -0
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +15 -0
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/package.json +11 -11
- package/src/index.ts +796 -18
- package/src/replication-info-v2-send.ts +18 -0
package/src/index.ts
CHANGED
|
@@ -1752,6 +1752,71 @@ export type DeliveryOptions = {
|
|
|
1752
1752
|
signal?: AbortSignal;
|
|
1753
1753
|
};
|
|
1754
1754
|
|
|
1755
|
+
export type PersistedReceiptPeerReadinessPendingReason =
|
|
1756
|
+
| "closed"
|
|
1757
|
+
| "no-current-session"
|
|
1758
|
+
| "session-opening"
|
|
1759
|
+
| "capability-pending"
|
|
1760
|
+
| "replication-state-pending"
|
|
1761
|
+
| "replication-confirmation-pending"
|
|
1762
|
+
| "not-replicating"
|
|
1763
|
+
| "not-entry-leader"
|
|
1764
|
+
| "ownership-changing";
|
|
1765
|
+
|
|
1766
|
+
export type PersistedReceiptPeerReadinessUnsupportedReason =
|
|
1767
|
+
| "persisted-receipts-unsupported"
|
|
1768
|
+
| "replication-confirmation-unsupported";
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* Detached view of one public key's current persisted-receipt generation.
|
|
1772
|
+
* `generation` is opaque: callers may compare it for equality, but must not
|
|
1773
|
+
* interpret its contents or use it as a future-session capability. Equal
|
|
1774
|
+
* generations mean the connection/receive/capability binding is unchanged;
|
|
1775
|
+
* leadership and outbound confirmation can still change within a generation.
|
|
1776
|
+
*/
|
|
1777
|
+
export type PersistedReceiptPeerReadiness =
|
|
1778
|
+
| Readonly<{
|
|
1779
|
+
status: "ready";
|
|
1780
|
+
generation: string;
|
|
1781
|
+
}>
|
|
1782
|
+
| Readonly<{
|
|
1783
|
+
status: "pending";
|
|
1784
|
+
reason: PersistedReceiptPeerReadinessPendingReason;
|
|
1785
|
+
generation?: string;
|
|
1786
|
+
}>
|
|
1787
|
+
| Readonly<{
|
|
1788
|
+
status: "unsupported";
|
|
1789
|
+
reason: PersistedReceiptPeerReadinessUnsupportedReason;
|
|
1790
|
+
generation: string;
|
|
1791
|
+
}>;
|
|
1792
|
+
|
|
1793
|
+
export type PersistedReceiptPeerReady = Extract<
|
|
1794
|
+
PersistedReceiptPeerReadiness,
|
|
1795
|
+
{ status: "ready" }
|
|
1796
|
+
>;
|
|
1797
|
+
|
|
1798
|
+
export type PersistedReceiptPeerReadinessOptions<
|
|
1799
|
+
T,
|
|
1800
|
+
R extends "u32" | "u64",
|
|
1801
|
+
> = Readonly<{
|
|
1802
|
+
/** Require this peer to be a freshly planned leader for every entry. */
|
|
1803
|
+
entries?: readonly (ShallowOrFullEntry<T> | EntryReplicated<R>)[];
|
|
1804
|
+
/**
|
|
1805
|
+
* Total leader-plan replica degree used for `entries`; defaults to this log's
|
|
1806
|
+
* configured minimum. This is not the persisted delivery `minAcks` count.
|
|
1807
|
+
*/
|
|
1808
|
+
replicas?: number;
|
|
1809
|
+
}>;
|
|
1810
|
+
|
|
1811
|
+
export type WaitForPersistedReceiptPeerReadinessOptions<
|
|
1812
|
+
T,
|
|
1813
|
+
R extends "u32" | "u64",
|
|
1814
|
+
> = PersistedReceiptPeerReadinessOptions<T, R> &
|
|
1815
|
+
Readonly<{
|
|
1816
|
+
timeout?: number;
|
|
1817
|
+
signal?: AbortSignal;
|
|
1818
|
+
}>;
|
|
1819
|
+
|
|
1755
1820
|
type PersistedDeliveryOptions = Readonly<{
|
|
1756
1821
|
reliability: "persisted";
|
|
1757
1822
|
minAcks: number;
|
|
@@ -1793,6 +1858,7 @@ const PERSISTED_RECEIPT_RETRY_MS = 50;
|
|
|
1793
1858
|
const MAX_PERSISTED_RECEIPT_ATTEMPT_MS = 2_000;
|
|
1794
1859
|
const MAX_PERSISTED_RECEIPT_REQUESTS_GLOBAL = 8;
|
|
1795
1860
|
const MAX_PERSISTED_RECEIPT_REQUESTS_PER_PEER = 2;
|
|
1861
|
+
const MAX_PERSISTED_RECEIPT_READINESS_WAITERS = 1_024;
|
|
1796
1862
|
const PERSISTED_RECEIPT_INGRESS_PEER_REQUEST_CAPACITY = 16;
|
|
1797
1863
|
const PERSISTED_RECEIPT_INGRESS_PEER_HASH_CAPACITY = 8_192;
|
|
1798
1864
|
const PERSISTED_RECEIPT_INGRESS_PEER_REQUESTS_PER_SECOND = 8;
|
|
@@ -2167,6 +2233,10 @@ export type ReplicatorLeaveEvent = { publicKey: PublicSignKey };
|
|
|
2167
2233
|
export type ReplicationChangeEvent = { publicKey: PublicSignKey };
|
|
2168
2234
|
export type ReplicatorMatureEvent = { publicKey: PublicSignKey };
|
|
2169
2235
|
export type ReplicationStatusEvent = ReplicationStatus;
|
|
2236
|
+
/** `peerHash` is the result of `PublicSignKey.hashcode()`. */
|
|
2237
|
+
export type PersistedReceiptPeerReadinessEvent = Readonly<{
|
|
2238
|
+
peerHash: string;
|
|
2239
|
+
}>;
|
|
2170
2240
|
|
|
2171
2241
|
class ReplicationStatusSnapshotChangedError extends Error {
|
|
2172
2242
|
constructor() {
|
|
@@ -2189,6 +2259,12 @@ export interface SharedLogEvents extends ProgramEvents {
|
|
|
2189
2259
|
"replication:change": CustomEvent<ReplicationChangeEvent>;
|
|
2190
2260
|
"replicator:mature": CustomEvent<ReplicatorMatureEvent>;
|
|
2191
2261
|
"replication:status": CustomEvent<ReplicationStatusEvent>;
|
|
2262
|
+
/**
|
|
2263
|
+
* Non-exhaustive wake hint that a peer may now produce a new readiness
|
|
2264
|
+
* snapshot. Consumers must re-read the snapshot; this event is deliberately
|
|
2265
|
+
* not a durable transition log and a `ready` result remains advisory.
|
|
2266
|
+
*/
|
|
2267
|
+
"persisted-receipt:readiness": CustomEvent<PersistedReceiptPeerReadinessEvent>;
|
|
2192
2268
|
}
|
|
2193
2269
|
|
|
2194
2270
|
export type SharedLogRuntimeSnapshot = Readonly<{
|
|
@@ -3742,6 +3818,23 @@ export class SharedLog<
|
|
|
3742
3818
|
// parallel map so existing capability-number consumers remain unchanged.
|
|
3743
3819
|
private _peerSyncCapabilitySessions!: Map<string, bigint>;
|
|
3744
3820
|
private _peerSyncCapabilityTimestamps!: Map<string, bigint>;
|
|
3821
|
+
// design-note: these fields cache a stable, public diagnostics token for the
|
|
3822
|
+
// composite of PeerSession identity, receive epoch, and signed capability
|
|
3823
|
+
// session. They are not consulted to admit or fence asynchronous work. A
|
|
3824
|
+
// separate opaque token is necessary because exposing any of those internal
|
|
3825
|
+
// identities would leak protocol/session values, while PeerSession alone does
|
|
3826
|
+
// not change when receive or capability state is replaced.
|
|
3827
|
+
private _persistedReceiptReadinessGenerations!: WeakMap<
|
|
3828
|
+
PeerSession,
|
|
3829
|
+
{
|
|
3830
|
+
receiveEpoch: object | null;
|
|
3831
|
+
capabilitySession?: bigint;
|
|
3832
|
+
generation: string;
|
|
3833
|
+
}
|
|
3834
|
+
>;
|
|
3835
|
+
private _persistedReceiptReadinessGenerationPrefix!: string;
|
|
3836
|
+
private _persistedReceiptReadinessGenerationCounter!: number;
|
|
3837
|
+
private _persistedReceiptReadinessWaiters!: Set<object>;
|
|
3745
3838
|
private _persistedReceiptStorage?: PersistedReceiptStorage;
|
|
3746
3839
|
private _persistedReceiptRequestsInFlight!: Map<string, number>;
|
|
3747
3840
|
private _persistedReceiptRequestsInFlightTotal!: number;
|
|
@@ -4099,6 +4192,12 @@ export class SharedLog<
|
|
|
4099
4192
|
this._peerSyncCapabilities = new Map();
|
|
4100
4193
|
this._peerSyncCapabilitySessions = new Map();
|
|
4101
4194
|
this._peerSyncCapabilityTimestamps = new Map();
|
|
4195
|
+
this._persistedReceiptReadinessGenerations = new WeakMap();
|
|
4196
|
+
this._persistedReceiptReadinessGenerationPrefix = toHexString(
|
|
4197
|
+
randomBytes(8),
|
|
4198
|
+
);
|
|
4199
|
+
this._persistedReceiptReadinessGenerationCounter = 0;
|
|
4200
|
+
this._persistedReceiptReadinessWaiters = new Set();
|
|
4102
4201
|
this._persistedReceiptStorage = undefined;
|
|
4103
4202
|
this._persistedReceiptRequestsInFlight = new Map();
|
|
4104
4203
|
this._persistedReceiptRequestsInFlightTotal = 0;
|
|
@@ -5061,15 +5160,23 @@ export class SharedLog<
|
|
|
5061
5160
|
) {
|
|
5062
5161
|
return false;
|
|
5063
5162
|
}
|
|
5163
|
+
const nextCapabilities = previous.capabilities | capabilities;
|
|
5164
|
+
const nextTimestamp =
|
|
5165
|
+
previous.timestamp === undefined || timestamp > previous.timestamp
|
|
5166
|
+
? timestamp
|
|
5167
|
+
: previous.timestamp;
|
|
5064
5168
|
this._openingSyncCapabilitiesByPeer.set(peerHash, {
|
|
5065
5169
|
epoch: openingSession,
|
|
5066
|
-
capabilities:
|
|
5170
|
+
capabilities: nextCapabilities,
|
|
5067
5171
|
transportSession,
|
|
5068
|
-
timestamp:
|
|
5069
|
-
previous.timestamp === undefined || timestamp > previous.timestamp
|
|
5070
|
-
? timestamp
|
|
5071
|
-
: previous.timestamp,
|
|
5172
|
+
timestamp: nextTimestamp,
|
|
5072
5173
|
});
|
|
5174
|
+
if (
|
|
5175
|
+
previous.capabilities !== nextCapabilities ||
|
|
5176
|
+
previous.timestamp === undefined
|
|
5177
|
+
) {
|
|
5178
|
+
this.dispatchPersistedReceiptReadinessChange(peerHash);
|
|
5179
|
+
}
|
|
5073
5180
|
return true;
|
|
5074
5181
|
}
|
|
5075
5182
|
this._openingSyncCapabilitiesByPeer.set(peerHash, {
|
|
@@ -5078,17 +5185,25 @@ export class SharedLog<
|
|
|
5078
5185
|
transportSession,
|
|
5079
5186
|
timestamp,
|
|
5080
5187
|
});
|
|
5188
|
+
this.dispatchPersistedReceiptReadinessChange(peerHash);
|
|
5081
5189
|
return true;
|
|
5082
5190
|
}
|
|
5083
5191
|
|
|
5084
5192
|
if (transportSession === undefined || timestamp === undefined) {
|
|
5085
5193
|
// Test/in-process synthetic contexts predate signed envelope captures.
|
|
5086
5194
|
// They may exercise capability-number behavior, but can never authorize V2.
|
|
5195
|
+
const readinessChanged =
|
|
5196
|
+
this._peerSyncCapabilities.get(peerHash) !== capabilities ||
|
|
5197
|
+
this._peerSyncCapabilitySessions.has(peerHash) ||
|
|
5198
|
+
this._peerSyncCapabilityTimestamps.has(peerHash);
|
|
5087
5199
|
this._peerSyncCapabilities.set(peerHash, capabilities);
|
|
5088
5200
|
this._peerSyncCapabilitySessions.delete(peerHash);
|
|
5089
5201
|
this._peerSyncCapabilityTimestamps.delete(peerHash);
|
|
5090
5202
|
this._v2Send.advancePeerCapability(peerHash);
|
|
5091
5203
|
this._v2Receive.revokePeerCapability(peerHash);
|
|
5204
|
+
if (readinessChanged) {
|
|
5205
|
+
this.dispatchPersistedReceiptReadinessChange(peerHash);
|
|
5206
|
+
}
|
|
5092
5207
|
return true;
|
|
5093
5208
|
}
|
|
5094
5209
|
|
|
@@ -5113,6 +5228,10 @@ export class SharedLog<
|
|
|
5113
5228
|
!sameTransportSession ||
|
|
5114
5229
|
(previousCapabilities & senderGrantCapabilityMask) !==
|
|
5115
5230
|
(nextCapabilities & senderGrantCapabilityMask);
|
|
5231
|
+
const readinessChanged =
|
|
5232
|
+
!sameTransportSession ||
|
|
5233
|
+
previousTimestamp === undefined ||
|
|
5234
|
+
previousCapabilities !== nextCapabilities;
|
|
5116
5235
|
this._peerSyncCapabilities.set(peerHash, nextCapabilities);
|
|
5117
5236
|
this._peerSyncCapabilitySessions.set(peerHash, transportSession);
|
|
5118
5237
|
this._peerSyncCapabilityTimestamps.set(
|
|
@@ -5129,6 +5248,9 @@ export class SharedLog<
|
|
|
5129
5248
|
// recovery re-solicitation may restart from the base interval.
|
|
5130
5249
|
this.resetReplicationInfoV2RecoveryEscalation(peerHash);
|
|
5131
5250
|
}
|
|
5251
|
+
if (readinessChanged) {
|
|
5252
|
+
this.dispatchPersistedReceiptReadinessChange(peerHash);
|
|
5253
|
+
}
|
|
5132
5254
|
return true;
|
|
5133
5255
|
}
|
|
5134
5256
|
|
|
@@ -5478,9 +5600,148 @@ export class SharedLog<
|
|
|
5478
5600
|
return this.sendFusedRawExchangeHeadsPlan(plan, to, options);
|
|
5479
5601
|
}
|
|
5480
5602
|
|
|
5603
|
+
private persistedReceiptReadinessGeneration(
|
|
5604
|
+
peerSession: PeerSession,
|
|
5605
|
+
receiveEpoch: object | null,
|
|
5606
|
+
capabilitySession: bigint | undefined,
|
|
5607
|
+
): string {
|
|
5608
|
+
const current = this._persistedReceiptReadinessGenerations.get(peerSession);
|
|
5609
|
+
if (
|
|
5610
|
+
current?.receiveEpoch === receiveEpoch &&
|
|
5611
|
+
current.capabilitySession === capabilitySession
|
|
5612
|
+
) {
|
|
5613
|
+
return current.generation;
|
|
5614
|
+
}
|
|
5615
|
+
const generation = `${this._persistedReceiptReadinessGenerationPrefix}:${(++this
|
|
5616
|
+
._persistedReceiptReadinessGenerationCounter).toString(36)}`;
|
|
5617
|
+
this._persistedReceiptReadinessGenerations.set(peerSession, {
|
|
5618
|
+
receiveEpoch,
|
|
5619
|
+
capabilitySession,
|
|
5620
|
+
generation,
|
|
5621
|
+
});
|
|
5622
|
+
return generation;
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5625
|
+
private pendingPersistedReceiptReadiness(
|
|
5626
|
+
reason: PersistedReceiptPeerReadinessPendingReason,
|
|
5627
|
+
generation?: string,
|
|
5628
|
+
): PersistedReceiptPeerReadiness {
|
|
5629
|
+
return Object.freeze({
|
|
5630
|
+
status: "pending" as const,
|
|
5631
|
+
reason,
|
|
5632
|
+
...(generation === undefined ? {} : { generation }),
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
private unsupportedPersistedReceiptReadiness(
|
|
5637
|
+
reason: PersistedReceiptPeerReadinessUnsupportedReason,
|
|
5638
|
+
generation: string,
|
|
5639
|
+
): PersistedReceiptPeerReadiness {
|
|
5640
|
+
return Object.freeze({
|
|
5641
|
+
status: "unsupported" as const,
|
|
5642
|
+
reason,
|
|
5643
|
+
generation,
|
|
5644
|
+
});
|
|
5645
|
+
}
|
|
5646
|
+
|
|
5647
|
+
private dispatchPersistedReceiptReadinessChange(peerHash: string): void {
|
|
5648
|
+
this.events.dispatchEvent(
|
|
5649
|
+
new CustomEvent<PersistedReceiptPeerReadinessEvent>(
|
|
5650
|
+
"persisted-receipt:readiness",
|
|
5651
|
+
{ detail: Object.freeze({ peerHash }) },
|
|
5652
|
+
),
|
|
5653
|
+
);
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5656
|
+
private persistedReceiptReadinessCandidate(peerHash: string):
|
|
5657
|
+
| {
|
|
5658
|
+
capabilitySession: bigint;
|
|
5659
|
+
peerSession: PeerSession;
|
|
5660
|
+
receiveEpoch: object | null;
|
|
5661
|
+
generation: string;
|
|
5662
|
+
}
|
|
5663
|
+
| PersistedReceiptPeerReadiness {
|
|
5664
|
+
if (this.closed) {
|
|
5665
|
+
return this.pendingPersistedReceiptReadiness("closed");
|
|
5666
|
+
}
|
|
5667
|
+
const peerSession = this._peerSessions.current(peerHash);
|
|
5668
|
+
if (!peerSession) {
|
|
5669
|
+
return this.pendingPersistedReceiptReadiness("no-current-session");
|
|
5670
|
+
}
|
|
5671
|
+
const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
|
|
5672
|
+
const capabilitySession = this._peerSyncCapabilitySessions.get(peerHash);
|
|
5673
|
+
const generation = this.persistedReceiptReadinessGeneration(
|
|
5674
|
+
peerSession,
|
|
5675
|
+
receiveEpoch,
|
|
5676
|
+
capabilitySession,
|
|
5677
|
+
);
|
|
5678
|
+
if (
|
|
5679
|
+
peerSession.phase !== "open" ||
|
|
5680
|
+
!peerSession.isActive() ||
|
|
5681
|
+
this._peerSessions.isReplicationInfoBlocked(peerHash) ||
|
|
5682
|
+
!this._peerSessions.isReceiveCleanupGateOpen(peerHash)
|
|
5683
|
+
) {
|
|
5684
|
+
return this.pendingPersistedReceiptReadiness(
|
|
5685
|
+
"session-opening",
|
|
5686
|
+
generation,
|
|
5687
|
+
);
|
|
5688
|
+
}
|
|
5689
|
+
if (
|
|
5690
|
+
capabilitySession === undefined ||
|
|
5691
|
+
!this._peerSyncCapabilityTimestamps.has(peerHash)
|
|
5692
|
+
) {
|
|
5693
|
+
return this.pendingPersistedReceiptReadiness(
|
|
5694
|
+
"capability-pending",
|
|
5695
|
+
generation,
|
|
5696
|
+
);
|
|
5697
|
+
}
|
|
5698
|
+
const capabilities = this._peerSyncCapabilities.get(peerHash) ?? 0;
|
|
5699
|
+
if ((capabilities & SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS) === 0) {
|
|
5700
|
+
return this.unsupportedPersistedReceiptReadiness(
|
|
5701
|
+
"persisted-receipts-unsupported",
|
|
5702
|
+
generation,
|
|
5703
|
+
);
|
|
5704
|
+
}
|
|
5705
|
+
if ((capabilities & SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM) === 0) {
|
|
5706
|
+
return this.unsupportedPersistedReceiptReadiness(
|
|
5707
|
+
"replication-confirmation-unsupported",
|
|
5708
|
+
generation,
|
|
5709
|
+
);
|
|
5710
|
+
}
|
|
5711
|
+
if (
|
|
5712
|
+
!this._v2Receive.isCurrentActive({
|
|
5713
|
+
peerHash,
|
|
5714
|
+
peerSession,
|
|
5715
|
+
receiveEpoch,
|
|
5716
|
+
senderTransportSession: capabilitySession,
|
|
5717
|
+
})
|
|
5718
|
+
) {
|
|
5719
|
+
return this.pendingPersistedReceiptReadiness(
|
|
5720
|
+
"replication-state-pending",
|
|
5721
|
+
generation,
|
|
5722
|
+
);
|
|
5723
|
+
}
|
|
5724
|
+
if (!this.uniqueReplicators.has(peerHash)) {
|
|
5725
|
+
return this.pendingPersistedReceiptReadiness(
|
|
5726
|
+
"not-replicating",
|
|
5727
|
+
generation,
|
|
5728
|
+
);
|
|
5729
|
+
}
|
|
5730
|
+
return {
|
|
5731
|
+
capabilitySession,
|
|
5732
|
+
peerSession,
|
|
5733
|
+
receiveEpoch,
|
|
5734
|
+
generation,
|
|
5735
|
+
};
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5481
5738
|
private persistedReceiptPeerSession(
|
|
5482
5739
|
peerHash: string,
|
|
5483
5740
|
): { capabilitySession: bigint; peerSession: PeerSession } | undefined {
|
|
5741
|
+
// This is a hot receipt/transfer-loop predicate. Keep it allocation-light,
|
|
5742
|
+
// while mirroring every exact-session gate in
|
|
5743
|
+
// persistedReceiptReadinessCandidate (which additionally creates public
|
|
5744
|
+
// reason/generation snapshots).
|
|
5484
5745
|
const capabilitySession = this._peerSyncCapabilitySessions.get(peerHash);
|
|
5485
5746
|
const peerSession = this._peerSessions.current(peerHash);
|
|
5486
5747
|
const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
|
|
@@ -5488,10 +5749,14 @@ export class SharedLog<
|
|
|
5488
5749
|
SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS |
|
|
5489
5750
|
SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM;
|
|
5490
5751
|
if (
|
|
5752
|
+
this.closed ||
|
|
5491
5753
|
capabilitySession == null ||
|
|
5492
5754
|
!peerSession ||
|
|
5493
5755
|
peerSession.phase !== "open" ||
|
|
5494
|
-
!
|
|
5756
|
+
!peerSession.isActive() ||
|
|
5757
|
+
this._peerSessions.isReplicationInfoBlocked(peerHash) ||
|
|
5758
|
+
!this._peerSessions.isReceiveCleanupGateOpen(peerHash) ||
|
|
5759
|
+
!this.uniqueReplicators.has(peerHash) ||
|
|
5495
5760
|
!this._peerSyncCapabilityTimestamps.has(peerHash) ||
|
|
5496
5761
|
((this._peerSyncCapabilities.get(peerHash) ?? 0) &
|
|
5497
5762
|
requiredCapabilities) !==
|
|
@@ -8181,8 +8446,11 @@ export class SharedLog<
|
|
|
8181
8446
|
? checkedPruneCoordinator.fencePeerRemoval(keyHash)
|
|
8182
8447
|
: undefined;
|
|
8183
8448
|
const blockPeerReceiveAdmission = () => {
|
|
8184
|
-
releaseReceiveCleanupGate
|
|
8185
|
-
|
|
8449
|
+
if (!releaseReceiveCleanupGate) {
|
|
8450
|
+
releaseReceiveCleanupGate =
|
|
8451
|
+
this._peerSessions.acquireReceiveCleanupGate(keyHash);
|
|
8452
|
+
this.dispatchPersistedReceiptReadinessChange(keyHash);
|
|
8453
|
+
}
|
|
8186
8454
|
};
|
|
8187
8455
|
if (!isMe && !isSpeculativePeerRemoval) {
|
|
8188
8456
|
// Revoke this peer's receipts synchronously, before this removal can
|
|
@@ -8423,7 +8691,10 @@ export class SharedLog<
|
|
|
8423
8691
|
});
|
|
8424
8692
|
removalCallCompleted = true;
|
|
8425
8693
|
} finally {
|
|
8426
|
-
releaseReceiveCleanupGate
|
|
8694
|
+
if (releaseReceiveCleanupGate) {
|
|
8695
|
+
releaseReceiveCleanupGate();
|
|
8696
|
+
this.dispatchPersistedReceiptReadinessChange(keyHash);
|
|
8697
|
+
}
|
|
8427
8698
|
if (
|
|
8428
8699
|
replicationInfoRecoveryEpochAdvanced &&
|
|
8429
8700
|
ownsReplicationOwnershipLifecycle() &&
|
|
@@ -16904,6 +17175,12 @@ export class SharedLog<
|
|
|
16904
17175
|
this._peerSyncCapabilities = new Map();
|
|
16905
17176
|
this._peerSyncCapabilitySessions = new Map();
|
|
16906
17177
|
this._peerSyncCapabilityTimestamps = new Map();
|
|
17178
|
+
this._persistedReceiptReadinessGenerations = new WeakMap();
|
|
17179
|
+
this._persistedReceiptReadinessGenerationPrefix = toHexString(
|
|
17180
|
+
randomBytes(8),
|
|
17181
|
+
);
|
|
17182
|
+
this._persistedReceiptReadinessGenerationCounter = 0;
|
|
17183
|
+
this._persistedReceiptReadinessWaiters = new Set();
|
|
16907
17184
|
this._persistedReceiptStorage = undefined;
|
|
16908
17185
|
this._persistedReceiptRequestsInFlight = new Map();
|
|
16909
17186
|
this._persistedReceiptRequestsInFlightTotal = 0;
|
|
@@ -18580,6 +18857,7 @@ export class SharedLog<
|
|
|
18580
18857
|
ownershipLifecycleController,
|
|
18581
18858
|
this._checkedPrune,
|
|
18582
18859
|
);
|
|
18860
|
+
this.dispatchPersistedReceiptReadinessChange(peerHash);
|
|
18583
18861
|
}
|
|
18584
18862
|
|
|
18585
18863
|
private cleanupPendingIHavePeer(peerHash: string) {
|
|
@@ -18604,6 +18882,7 @@ export class SharedLog<
|
|
|
18604
18882
|
receiveEpoch,
|
|
18605
18883
|
});
|
|
18606
18884
|
}
|
|
18885
|
+
this.dispatchPersistedReceiptReadinessChange(peerHash);
|
|
18607
18886
|
}
|
|
18608
18887
|
|
|
18609
18888
|
private async resolveCandidatePeersForHash(
|
|
@@ -20045,6 +20324,7 @@ export class SharedLog<
|
|
|
20045
20324
|
this._peerSyncCapabilities?.clear();
|
|
20046
20325
|
this._peerSyncCapabilitySessions?.clear();
|
|
20047
20326
|
this._peerSyncCapabilityTimestamps?.clear();
|
|
20327
|
+
this._persistedReceiptReadinessGenerations = new WeakMap();
|
|
20048
20328
|
this._persistedReceiptStorage = undefined;
|
|
20049
20329
|
this._persistedReceiptRequestsInFlight?.clear();
|
|
20050
20330
|
this._persistedReceiptRequestsInFlightTotal = 0;
|
|
@@ -22601,10 +22881,14 @@ export class SharedLog<
|
|
|
22601
22881
|
}
|
|
22602
22882
|
return;
|
|
22603
22883
|
} else if (msg instanceof ReplicationInfoV2AppliedMessage) {
|
|
22604
|
-
|
|
22605
|
-
|
|
22606
|
-
|
|
22607
|
-
|
|
22884
|
+
if (
|
|
22885
|
+
this._v2Send.acceptApplied(msg, {
|
|
22886
|
+
from: context.from,
|
|
22887
|
+
receiverTransportSession: context.message.header.session,
|
|
22888
|
+
})
|
|
22889
|
+
) {
|
|
22890
|
+
this.dispatchPersistedReceiptReadinessChange(receiveFromHash);
|
|
22891
|
+
}
|
|
22608
22892
|
return;
|
|
22609
22893
|
} else if (isReplicationInfoV2Message(msg)) {
|
|
22610
22894
|
await this.handleReplicationInfoV2Announcement(
|
|
@@ -23420,6 +23704,7 @@ export class SharedLog<
|
|
|
23420
23704
|
// A committed V2 announcement is applied progress: the peer answers,
|
|
23421
23705
|
// so recovery re-solicitation may restart from the base interval.
|
|
23422
23706
|
this.resetReplicationInfoV2RecoveryEscalation(fromHash);
|
|
23707
|
+
this.dispatchPersistedReceiptReadinessChange(fromHash);
|
|
23423
23708
|
});
|
|
23424
23709
|
} finally {
|
|
23425
23710
|
this._v2Receive.release(admission);
|
|
@@ -23774,6 +24059,465 @@ export class SharedLog<
|
|
|
23774
24059
|
throwIfInactive();
|
|
23775
24060
|
}
|
|
23776
24061
|
|
|
24062
|
+
private nudgePersistedReceiptPeerReadiness(publicKey: PublicSignKey): void {
|
|
24063
|
+
if (this.closed) return;
|
|
24064
|
+
const peerHash = publicKey.hashcode();
|
|
24065
|
+
const peerSession = this._peerSessions.current(peerHash);
|
|
24066
|
+
if (
|
|
24067
|
+
!peerSession ||
|
|
24068
|
+
peerSession.phase === "departing" ||
|
|
24069
|
+
(peerSession.phase === "opening" &&
|
|
24070
|
+
!peerSession.openingBarrierActive)
|
|
24071
|
+
) {
|
|
24072
|
+
// A barrier rejection deliberately leaves the current session in its
|
|
24073
|
+
// fail-closed opening phase after the barrier window has settled. Ask the
|
|
24074
|
+
// authenticated peer for a fresh subscriber snapshot so the replacement
|
|
24075
|
+
// session can recover; never rotate a barrier that is still in flight.
|
|
24076
|
+
this.requestSubscriberSnapshotForCapability(publicKey);
|
|
24077
|
+
return;
|
|
24078
|
+
}
|
|
24079
|
+
if (peerSession.phase !== "open" || !peerSession.isActive()) {
|
|
24080
|
+
return;
|
|
24081
|
+
}
|
|
24082
|
+
const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
|
|
24083
|
+
this.promoteReplicationInfoV2ReceiveCapability(publicKey, peerSession);
|
|
24084
|
+
this._v2Receive.reAdvertiseLocalCapabilityForRecovery({
|
|
24085
|
+
peerHash,
|
|
24086
|
+
peerSession,
|
|
24087
|
+
receiveEpoch,
|
|
24088
|
+
});
|
|
24089
|
+
this._v2Receive.ensureRequestProgress({
|
|
24090
|
+
peerHash,
|
|
24091
|
+
peerSession,
|
|
24092
|
+
receiveEpoch,
|
|
24093
|
+
});
|
|
24094
|
+
this.scheduleReplicationInfoV2Recovery(publicKey);
|
|
24095
|
+
}
|
|
24096
|
+
|
|
24097
|
+
/**
|
|
24098
|
+
* Inspect whether one public key's exact current connection generation can
|
|
24099
|
+
* supply persisted-receipt evidence. The returned object is frozen and never
|
|
24100
|
+
* exposes the internal PeerSession token. When `entries` are supplied, the
|
|
24101
|
+
* peer must also be present in a fresh leader plan for every entry.
|
|
24102
|
+
*
|
|
24103
|
+
* This is advisory preflight state. Persisted delivery repeats every
|
|
24104
|
+
* generation, leadership, ownership and storage check at receipt time; a
|
|
24105
|
+
* `ready` snapshot is never itself authority to dispose a source copy.
|
|
24106
|
+
*/
|
|
24107
|
+
async getPersistedReceiptPeerReadiness(
|
|
24108
|
+
key: PublicSignKey,
|
|
24109
|
+
options: PersistedReceiptPeerReadinessOptions<T, R> = {},
|
|
24110
|
+
): Promise<PersistedReceiptPeerReadiness> {
|
|
24111
|
+
return this.inspectPersistedReceiptPeerReadiness(key, options);
|
|
24112
|
+
}
|
|
24113
|
+
|
|
24114
|
+
private async inspectPersistedReceiptPeerReadiness(
|
|
24115
|
+
key: PublicSignKey,
|
|
24116
|
+
options: PersistedReceiptPeerReadinessOptions<T, R>,
|
|
24117
|
+
assertContinue?: () => void,
|
|
24118
|
+
): Promise<PersistedReceiptPeerReadiness> {
|
|
24119
|
+
// Capture and validate caller-owned planning input before consulting live
|
|
24120
|
+
// peer state. Invalid options must not appear to work merely because the
|
|
24121
|
+
// peer is currently absent, then fail later when the same session connects.
|
|
24122
|
+
const entries = options.entries ? [...options.entries] : [];
|
|
24123
|
+
const replicas =
|
|
24124
|
+
options.replicas ??
|
|
24125
|
+
(entries.length > 0 ? this.replicas.min.getValue(this) : undefined);
|
|
24126
|
+
if (replicas !== undefined) {
|
|
24127
|
+
if (!Number.isSafeInteger(replicas) || replicas <= 0) {
|
|
24128
|
+
throw new RangeError(
|
|
24129
|
+
"Persisted-receipt readiness replicas must be a positive integer",
|
|
24130
|
+
);
|
|
24131
|
+
}
|
|
24132
|
+
checkMinReplicasLimit(replicas);
|
|
24133
|
+
}
|
|
24134
|
+
|
|
24135
|
+
const peerHash = key.hashcode();
|
|
24136
|
+
const captured = this.persistedReceiptReadinessCandidate(peerHash);
|
|
24137
|
+
if ("status" in captured) {
|
|
24138
|
+
return captured;
|
|
24139
|
+
}
|
|
24140
|
+
assertContinue?.();
|
|
24141
|
+
|
|
24142
|
+
if (entries.length > 0) {
|
|
24143
|
+
const ownershipLifecycleController =
|
|
24144
|
+
this.captureReplicationOwnershipLifecycle();
|
|
24145
|
+
const ownershipRevision =
|
|
24146
|
+
this._instanceLifecycle?._receiveOwnershipRevision ?? 0;
|
|
24147
|
+
if (!this.isReceiveOwnershipSnapshotStable(ownershipRevision)) {
|
|
24148
|
+
return this.pendingPersistedReceiptReadiness(
|
|
24149
|
+
"ownership-changing",
|
|
24150
|
+
captured.generation,
|
|
24151
|
+
);
|
|
24152
|
+
}
|
|
24153
|
+
for (const entry of entries) {
|
|
24154
|
+
assertContinue?.();
|
|
24155
|
+
const leaders = await this.findLeadersFromEntry(
|
|
24156
|
+
entry,
|
|
24157
|
+
replicas!,
|
|
24158
|
+
{ freshLeaderPlan: true },
|
|
24159
|
+
ownershipLifecycleController,
|
|
24160
|
+
);
|
|
24161
|
+
assertContinue?.();
|
|
24162
|
+
if (!this.isReceiveOwnershipSnapshotStable(ownershipRevision)) {
|
|
24163
|
+
return this.pendingPersistedReceiptReadiness(
|
|
24164
|
+
"ownership-changing",
|
|
24165
|
+
captured.generation,
|
|
24166
|
+
);
|
|
24167
|
+
}
|
|
24168
|
+
const current = this.persistedReceiptReadinessCandidate(peerHash);
|
|
24169
|
+
if ("status" in current) {
|
|
24170
|
+
return current;
|
|
24171
|
+
}
|
|
24172
|
+
if (
|
|
24173
|
+
current.peerSession !== captured.peerSession ||
|
|
24174
|
+
current.receiveEpoch !== captured.receiveEpoch ||
|
|
24175
|
+
current.capabilitySession !== captured.capabilitySession
|
|
24176
|
+
) {
|
|
24177
|
+
return this.pendingPersistedReceiptReadiness(
|
|
24178
|
+
"replication-state-pending",
|
|
24179
|
+
current.generation,
|
|
24180
|
+
);
|
|
24181
|
+
}
|
|
24182
|
+
if (!leaders.has(peerHash)) {
|
|
24183
|
+
return this.pendingPersistedReceiptReadiness(
|
|
24184
|
+
"not-entry-leader",
|
|
24185
|
+
captured.generation,
|
|
24186
|
+
);
|
|
24187
|
+
}
|
|
24188
|
+
}
|
|
24189
|
+
}
|
|
24190
|
+
|
|
24191
|
+
assertContinue?.();
|
|
24192
|
+
const current = this.persistedReceiptReadinessCandidate(peerHash);
|
|
24193
|
+
if ("status" in current) {
|
|
24194
|
+
return current;
|
|
24195
|
+
}
|
|
24196
|
+
if (
|
|
24197
|
+
current.peerSession !== captured.peerSession ||
|
|
24198
|
+
current.receiveEpoch !== captured.receiveEpoch ||
|
|
24199
|
+
current.capabilitySession !== captured.capabilitySession
|
|
24200
|
+
) {
|
|
24201
|
+
return this.pendingPersistedReceiptReadiness(
|
|
24202
|
+
"replication-state-pending",
|
|
24203
|
+
current.generation,
|
|
24204
|
+
);
|
|
24205
|
+
}
|
|
24206
|
+
if (
|
|
24207
|
+
!this._v2Send.isLatestConfirmedForPeer({
|
|
24208
|
+
peerHash,
|
|
24209
|
+
peerSession: captured.peerSession,
|
|
24210
|
+
receiverTransportSession: captured.capabilitySession,
|
|
24211
|
+
})
|
|
24212
|
+
) {
|
|
24213
|
+
return this.pendingPersistedReceiptReadiness(
|
|
24214
|
+
"replication-confirmation-pending",
|
|
24215
|
+
captured.generation,
|
|
24216
|
+
);
|
|
24217
|
+
}
|
|
24218
|
+
return Object.freeze({
|
|
24219
|
+
status: "ready" as const,
|
|
24220
|
+
generation: captured.generation,
|
|
24221
|
+
});
|
|
24222
|
+
}
|
|
24223
|
+
|
|
24224
|
+
/**
|
|
24225
|
+
* Wait for a public key's current (or replacement) connection generation to
|
|
24226
|
+
* become persisted-receipt ready. Transition listeners are installed before
|
|
24227
|
+
* the first asynchronous inspection, and a bounded recovery tick repairs
|
|
24228
|
+
* missed subscriber/capability wakes without retaining stale PeerSessions.
|
|
24229
|
+
* This waiter is advisory only; the following persisted delivery remains the
|
|
24230
|
+
* operation that proves the requested remote durability quorum.
|
|
24231
|
+
*/
|
|
24232
|
+
async waitForPersistedReceiptPeerReadiness(
|
|
24233
|
+
key: PublicSignKey,
|
|
24234
|
+
options: WaitForPersistedReceiptPeerReadinessOptions<T, R> = {},
|
|
24235
|
+
): Promise<PersistedReceiptPeerReady> {
|
|
24236
|
+
if (this.closed) {
|
|
24237
|
+
throw new ClosedError();
|
|
24238
|
+
}
|
|
24239
|
+
const timeoutMs = options.timeout ?? this.waitForReplicatorTimeout;
|
|
24240
|
+
if (
|
|
24241
|
+
!Number.isSafeInteger(timeoutMs) ||
|
|
24242
|
+
timeoutMs <= 0 ||
|
|
24243
|
+
timeoutMs > MAX_PERSISTED_DELIVERY_TIMEOUT_MS
|
|
24244
|
+
) {
|
|
24245
|
+
throw new RangeError(
|
|
24246
|
+
`Persisted-receipt readiness timeout must be an integer from 1 to ${MAX_PERSISTED_DELIVERY_TIMEOUT_MS} milliseconds`,
|
|
24247
|
+
);
|
|
24248
|
+
}
|
|
24249
|
+
if (options.signal?.aborted) {
|
|
24250
|
+
throw options.signal.reason instanceof Error
|
|
24251
|
+
? options.signal.reason
|
|
24252
|
+
: new AbortError("Persisted-receipt readiness wait aborted");
|
|
24253
|
+
}
|
|
24254
|
+
|
|
24255
|
+
// Capture caller-owned inputs before reserving a waiter slot. A throwing
|
|
24256
|
+
// iterator/key implementation must not strand capacity permanently.
|
|
24257
|
+
const entries = options.entries ? [...options.entries] : undefined;
|
|
24258
|
+
const inspectOptions: PersistedReceiptPeerReadinessOptions<T, R> = {
|
|
24259
|
+
...(entries ? { entries } : {}),
|
|
24260
|
+
...(options.replicas === undefined ? {} : { replicas: options.replicas }),
|
|
24261
|
+
};
|
|
24262
|
+
const peerHash = key.hashcode();
|
|
24263
|
+
const waiterSet = this._persistedReceiptReadinessWaiters;
|
|
24264
|
+
if (waiterSet.size >= MAX_PERSISTED_RECEIPT_READINESS_WAITERS) {
|
|
24265
|
+
throw new RangeError(
|
|
24266
|
+
`Too many pending persisted-receipt readiness waits (maximum ${MAX_PERSISTED_RECEIPT_READINESS_WAITERS})`,
|
|
24267
|
+
);
|
|
24268
|
+
}
|
|
24269
|
+
const waiterToken = {};
|
|
24270
|
+
waiterSet.add(waiterToken);
|
|
24271
|
+
const deadline = Date.now() + timeoutMs;
|
|
24272
|
+
const closeSignal = this._closeController.signal;
|
|
24273
|
+
const operationController = new AbortController();
|
|
24274
|
+
const operationSignal = AbortSignal.any(
|
|
24275
|
+
[options.signal, closeSignal, operationController.signal].filter(
|
|
24276
|
+
(value): value is AbortSignal => value !== undefined,
|
|
24277
|
+
),
|
|
24278
|
+
);
|
|
24279
|
+
const deferred = pDefer<PersistedReceiptPeerReady>();
|
|
24280
|
+
let settled = false;
|
|
24281
|
+
let checkScheduled = false;
|
|
24282
|
+
let checkInFlight = false;
|
|
24283
|
+
let rerun = false;
|
|
24284
|
+
let recoveryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
24285
|
+
let confirmationController: AbortController | undefined;
|
|
24286
|
+
let lastSnapshot: PersistedReceiptPeerReadiness | undefined;
|
|
24287
|
+
const createTimeoutError = () => {
|
|
24288
|
+
const suffix = lastSnapshot
|
|
24289
|
+
? ` (last status: ${lastSnapshot.status}${
|
|
24290
|
+
"reason" in lastSnapshot ? `/${lastSnapshot.reason}` : ""
|
|
24291
|
+
})`
|
|
24292
|
+
: "";
|
|
24293
|
+
return new TimeoutError(
|
|
24294
|
+
`Timeout waiting for persisted-receipt readiness from ${peerHash}${suffix}`,
|
|
24295
|
+
);
|
|
24296
|
+
};
|
|
24297
|
+
|
|
24298
|
+
const cleanup = () => {
|
|
24299
|
+
waiterSet.delete(waiterToken);
|
|
24300
|
+
this.events.removeEventListener(
|
|
24301
|
+
"persisted-receipt:readiness",
|
|
24302
|
+
onReadinessChange,
|
|
24303
|
+
);
|
|
24304
|
+
this.events.removeEventListener("replication:change", onRoleChange);
|
|
24305
|
+
this.events.removeEventListener("replicator:mature", onRoleChange);
|
|
24306
|
+
options.signal?.removeEventListener("abort", onCallerAbort);
|
|
24307
|
+
closeSignal.removeEventListener("abort", onClose);
|
|
24308
|
+
if (recoveryTimer) {
|
|
24309
|
+
clearTimeout(recoveryTimer);
|
|
24310
|
+
recoveryTimer = undefined;
|
|
24311
|
+
}
|
|
24312
|
+
confirmationController?.abort(
|
|
24313
|
+
new AbortError("Persisted-receipt readiness generation changed"),
|
|
24314
|
+
);
|
|
24315
|
+
confirmationController = undefined;
|
|
24316
|
+
operationController.abort(
|
|
24317
|
+
new AbortError("Persisted-receipt readiness wait settled"),
|
|
24318
|
+
);
|
|
24319
|
+
};
|
|
24320
|
+
const resolve = (snapshot: PersistedReceiptPeerReady) => {
|
|
24321
|
+
if (settled) return;
|
|
24322
|
+
settled = true;
|
|
24323
|
+
cleanup();
|
|
24324
|
+
deferred.resolve(snapshot);
|
|
24325
|
+
};
|
|
24326
|
+
const reject = (error: unknown) => {
|
|
24327
|
+
if (settled) return;
|
|
24328
|
+
settled = true;
|
|
24329
|
+
cleanup();
|
|
24330
|
+
deferred.reject(
|
|
24331
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
24332
|
+
);
|
|
24333
|
+
};
|
|
24334
|
+
const onCallerAbort = () =>
|
|
24335
|
+
reject(
|
|
24336
|
+
options.signal?.reason instanceof Error
|
|
24337
|
+
? options.signal.reason
|
|
24338
|
+
: new AbortError("Persisted-receipt readiness wait aborted"),
|
|
24339
|
+
);
|
|
24340
|
+
const onClose = () => reject(new ClosedError());
|
|
24341
|
+
const continueWait = () => {
|
|
24342
|
+
if (settled) return false;
|
|
24343
|
+
if (closeSignal.aborted) {
|
|
24344
|
+
onClose();
|
|
24345
|
+
return false;
|
|
24346
|
+
}
|
|
24347
|
+
if (options.signal?.aborted) {
|
|
24348
|
+
onCallerAbort();
|
|
24349
|
+
return false;
|
|
24350
|
+
}
|
|
24351
|
+
if (Date.now() >= deadline) {
|
|
24352
|
+
reject(createTimeoutError());
|
|
24353
|
+
return false;
|
|
24354
|
+
}
|
|
24355
|
+
return true;
|
|
24356
|
+
};
|
|
24357
|
+
const assertInspectionCurrent = () => {
|
|
24358
|
+
if (!continueWait()) {
|
|
24359
|
+
throw new AbortError("Persisted-receipt readiness wait settled");
|
|
24360
|
+
}
|
|
24361
|
+
};
|
|
24362
|
+
const armRecoveryTick = () => {
|
|
24363
|
+
if (settled || recoveryTimer) return;
|
|
24364
|
+
const delayMs = Math.max(
|
|
24365
|
+
50,
|
|
24366
|
+
Math.min(1_000, this.waitForReplicatorRequestIntervalMs),
|
|
24367
|
+
);
|
|
24368
|
+
recoveryTimer = setTimeout(() => {
|
|
24369
|
+
recoveryTimer = undefined;
|
|
24370
|
+
if (!continueWait()) return;
|
|
24371
|
+
this.nudgePersistedReceiptPeerReadiness(key);
|
|
24372
|
+
scheduleCheck();
|
|
24373
|
+
}, delayMs);
|
|
24374
|
+
recoveryTimer.unref?.();
|
|
24375
|
+
};
|
|
24376
|
+
const runCheck = async () => {
|
|
24377
|
+
checkScheduled = false;
|
|
24378
|
+
if (!continueWait()) return;
|
|
24379
|
+
if (checkInFlight) {
|
|
24380
|
+
rerun = true;
|
|
24381
|
+
return;
|
|
24382
|
+
}
|
|
24383
|
+
checkInFlight = true;
|
|
24384
|
+
try {
|
|
24385
|
+
let snapshot = await this.inspectPersistedReceiptPeerReadiness(
|
|
24386
|
+
key,
|
|
24387
|
+
inspectOptions,
|
|
24388
|
+
assertInspectionCurrent,
|
|
24389
|
+
);
|
|
24390
|
+
lastSnapshot = snapshot;
|
|
24391
|
+
if (!continueWait()) return;
|
|
24392
|
+
if (rerun) return;
|
|
24393
|
+
if (snapshot.status === "ready") {
|
|
24394
|
+
// A wake observed while the asynchronous inspection was running may
|
|
24395
|
+
// already have invalidated this snapshot. Drain that coalesced wake
|
|
24396
|
+
// before publishing readiness.
|
|
24397
|
+
resolve(snapshot);
|
|
24398
|
+
return;
|
|
24399
|
+
}
|
|
24400
|
+
if (
|
|
24401
|
+
snapshot.status === "pending" &&
|
|
24402
|
+
snapshot.reason === "replication-confirmation-pending"
|
|
24403
|
+
) {
|
|
24404
|
+
const target = this.persistedReceiptPeerSession(peerHash);
|
|
24405
|
+
if (target) {
|
|
24406
|
+
const currentConfirmationController = new AbortController();
|
|
24407
|
+
confirmationController = currentConfirmationController;
|
|
24408
|
+
try {
|
|
24409
|
+
await this._v2Send.confirmLatestForPeer(
|
|
24410
|
+
{
|
|
24411
|
+
peerHash,
|
|
24412
|
+
peerSession: target.peerSession,
|
|
24413
|
+
receiverTransportSession: target.capabilitySession,
|
|
24414
|
+
},
|
|
24415
|
+
{
|
|
24416
|
+
timeout: Math.max(1, deadline - Date.now()),
|
|
24417
|
+
signal: AbortSignal.any([
|
|
24418
|
+
operationSignal,
|
|
24419
|
+
currentConfirmationController.signal,
|
|
24420
|
+
]),
|
|
24421
|
+
},
|
|
24422
|
+
);
|
|
24423
|
+
} catch (error) {
|
|
24424
|
+
if (!continueWait()) return;
|
|
24425
|
+
if (!(error instanceof AbortError)) {
|
|
24426
|
+
throw error;
|
|
24427
|
+
}
|
|
24428
|
+
rerun = true;
|
|
24429
|
+
} finally {
|
|
24430
|
+
if (confirmationController === currentConfirmationController) {
|
|
24431
|
+
confirmationController = undefined;
|
|
24432
|
+
}
|
|
24433
|
+
}
|
|
24434
|
+
if (!continueWait()) return;
|
|
24435
|
+
snapshot = await this.inspectPersistedReceiptPeerReadiness(
|
|
24436
|
+
key,
|
|
24437
|
+
inspectOptions,
|
|
24438
|
+
assertInspectionCurrent,
|
|
24439
|
+
);
|
|
24440
|
+
lastSnapshot = snapshot;
|
|
24441
|
+
if (!continueWait()) return;
|
|
24442
|
+
if (rerun) return;
|
|
24443
|
+
if (snapshot.status === "ready") {
|
|
24444
|
+
resolve(snapshot);
|
|
24445
|
+
return;
|
|
24446
|
+
}
|
|
24447
|
+
}
|
|
24448
|
+
}
|
|
24449
|
+
if (!continueWait()) return;
|
|
24450
|
+
this.nudgePersistedReceiptPeerReadiness(key);
|
|
24451
|
+
} catch (error) {
|
|
24452
|
+
if (!settled) reject(error);
|
|
24453
|
+
} finally {
|
|
24454
|
+
checkInFlight = false;
|
|
24455
|
+
if (!settled && rerun) {
|
|
24456
|
+
rerun = false;
|
|
24457
|
+
scheduleCheck();
|
|
24458
|
+
} else {
|
|
24459
|
+
armRecoveryTick();
|
|
24460
|
+
}
|
|
24461
|
+
}
|
|
24462
|
+
};
|
|
24463
|
+
const scheduleCheck = (interruptConfirmation = false) => {
|
|
24464
|
+
if (settled) return;
|
|
24465
|
+
if (recoveryTimer) {
|
|
24466
|
+
clearTimeout(recoveryTimer);
|
|
24467
|
+
recoveryTimer = undefined;
|
|
24468
|
+
}
|
|
24469
|
+
if (checkInFlight) {
|
|
24470
|
+
rerun = true;
|
|
24471
|
+
if (interruptConfirmation) {
|
|
24472
|
+
confirmationController?.abort(
|
|
24473
|
+
new AbortError(
|
|
24474
|
+
"Persisted-receipt readiness changed during confirmation",
|
|
24475
|
+
),
|
|
24476
|
+
);
|
|
24477
|
+
}
|
|
24478
|
+
return;
|
|
24479
|
+
}
|
|
24480
|
+
if (checkScheduled) return;
|
|
24481
|
+
checkScheduled = true;
|
|
24482
|
+
void Promise.resolve().then(runCheck);
|
|
24483
|
+
};
|
|
24484
|
+
const onReadinessChange = (
|
|
24485
|
+
event: CustomEvent<PersistedReceiptPeerReadinessEvent>,
|
|
24486
|
+
) => {
|
|
24487
|
+
if (event.detail.peerHash === peerHash) scheduleCheck(true);
|
|
24488
|
+
};
|
|
24489
|
+
const onRoleChange = (event: CustomEvent<ReplicationChangeEvent>) => {
|
|
24490
|
+
if (
|
|
24491
|
+
(entries?.length ?? 0) > 0 ||
|
|
24492
|
+
event.detail.publicKey.hashcode() === peerHash
|
|
24493
|
+
) {
|
|
24494
|
+
scheduleCheck(true);
|
|
24495
|
+
}
|
|
24496
|
+
};
|
|
24497
|
+
|
|
24498
|
+
// Register wake sources before the first state inspection. EventTarget does
|
|
24499
|
+
// not replay a transition that fired between an async check and registration.
|
|
24500
|
+
this.events.addEventListener(
|
|
24501
|
+
"persisted-receipt:readiness",
|
|
24502
|
+
onReadinessChange,
|
|
24503
|
+
);
|
|
24504
|
+
this.events.addEventListener("replication:change", onRoleChange);
|
|
24505
|
+
this.events.addEventListener("replicator:mature", onRoleChange);
|
|
24506
|
+
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
24507
|
+
closeSignal.addEventListener("abort", onClose, { once: true });
|
|
24508
|
+
if (options.signal?.aborted) {
|
|
24509
|
+
onCallerAbort();
|
|
24510
|
+
} else if (closeSignal.aborted) {
|
|
24511
|
+
onClose();
|
|
24512
|
+
} else {
|
|
24513
|
+
scheduleCheck();
|
|
24514
|
+
}
|
|
24515
|
+
|
|
24516
|
+
const timeout = setTimeout(() => reject(createTimeoutError()), timeoutMs);
|
|
24517
|
+
timeout.unref?.();
|
|
24518
|
+
return deferred.promise.finally(() => clearTimeout(timeout));
|
|
24519
|
+
}
|
|
24520
|
+
|
|
23777
24521
|
async waitForReplicator(
|
|
23778
24522
|
key: PublicSignKey,
|
|
23779
24523
|
options?: {
|
|
@@ -23783,19 +24527,28 @@ export class SharedLog<
|
|
|
23783
24527
|
timeout?: number;
|
|
23784
24528
|
},
|
|
23785
24529
|
) {
|
|
24530
|
+
if (options?.signal?.aborted) {
|
|
24531
|
+
throw new AbortError();
|
|
24532
|
+
}
|
|
23786
24533
|
const deferred = pDefer<void>();
|
|
23787
24534
|
const timeoutMs = options?.timeout ?? this.waitForReplicatorTimeout;
|
|
23788
24535
|
const resolvedRoleAge = options?.eager
|
|
23789
24536
|
? undefined
|
|
23790
24537
|
: (options?.roleAge ?? (await this.getDefaultMinRoleAge()));
|
|
24538
|
+
if (options?.signal?.aborted) {
|
|
24539
|
+
throw new AbortError();
|
|
24540
|
+
}
|
|
23791
24541
|
|
|
23792
24542
|
let settled = false;
|
|
23793
24543
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
23794
24544
|
let requestTimer: ReturnType<typeof setTimeout> | undefined;
|
|
24545
|
+
let checkInFlight = false;
|
|
24546
|
+
let checkAgain = false;
|
|
23795
24547
|
|
|
23796
24548
|
const clear = () => {
|
|
23797
|
-
|
|
23798
|
-
this.events.removeEventListener("
|
|
24549
|
+
checkAgain = false;
|
|
24550
|
+
this.events.removeEventListener("replicator:mature", runCheck);
|
|
24551
|
+
this.events.removeEventListener("replication:change", runCheck);
|
|
23799
24552
|
options?.signal?.removeEventListener("abort", onAbort);
|
|
23800
24553
|
if (timer != null) {
|
|
23801
24554
|
clearTimeout(timer);
|
|
@@ -23931,11 +24684,34 @@ export class SharedLog<
|
|
|
23931
24684
|
await iterator?.close();
|
|
23932
24685
|
}
|
|
23933
24686
|
};
|
|
24687
|
+
const runCheck = () => {
|
|
24688
|
+
if (settled) return;
|
|
24689
|
+
if (checkInFlight) {
|
|
24690
|
+
checkAgain = true;
|
|
24691
|
+
return;
|
|
24692
|
+
}
|
|
24693
|
+
// Reserve synchronously before `check()` can dispatch/re-enter from an
|
|
24694
|
+
// index implementation's first `next()` call.
|
|
24695
|
+
checkInFlight = true;
|
|
24696
|
+
void check()
|
|
24697
|
+
.catch((error) =>
|
|
24698
|
+
reject(error instanceof Error ? error : new Error(String(error))),
|
|
24699
|
+
)
|
|
24700
|
+
.finally(() => {
|
|
24701
|
+
checkInFlight = false;
|
|
24702
|
+
if (!settled && checkAgain) {
|
|
24703
|
+
checkAgain = false;
|
|
24704
|
+
runCheck();
|
|
24705
|
+
}
|
|
24706
|
+
});
|
|
24707
|
+
};
|
|
23934
24708
|
|
|
24709
|
+
// Register before the first asynchronous index read. EventTarget does not
|
|
24710
|
+
// replay a maturity/change event that fires while that read is in flight.
|
|
24711
|
+
this.events.addEventListener("replicator:mature", runCheck);
|
|
24712
|
+
this.events.addEventListener("replication:change", runCheck);
|
|
23935
24713
|
requestReplicationInfo();
|
|
23936
|
-
|
|
23937
|
-
this.events.addEventListener("replicator:mature", check);
|
|
23938
|
-
this.events.addEventListener("replication:change", check);
|
|
24714
|
+
runCheck();
|
|
23939
24715
|
|
|
23940
24716
|
return deferred.promise.finally(clear);
|
|
23941
24717
|
}
|
|
@@ -26825,6 +27601,7 @@ export class SharedLog<
|
|
|
26825
27601
|
if (!ownsSubscriptionEpoch()) {
|
|
26826
27602
|
return;
|
|
26827
27603
|
}
|
|
27604
|
+
this.dispatchPersistedReceiptReadinessChange(peerHash);
|
|
26828
27605
|
// A reconnect can arrive before the previous exact-session recovery tick
|
|
26829
27606
|
// observes its stale session. Retire that job synchronously so it cannot
|
|
26830
27607
|
// suppress the replacement session's scheduler in the shared peer slot.
|
|
@@ -26997,6 +27774,7 @@ export class SharedLog<
|
|
|
26997
27774
|
publicKey,
|
|
26998
27775
|
replicationLifecycleController,
|
|
26999
27776
|
);
|
|
27777
|
+
this.dispatchPersistedReceiptReadinessChange(peerHash);
|
|
27000
27778
|
}
|
|
27001
27779
|
|
|
27002
27780
|
private getClampedReplicas(customValue?: MinReplicas) {
|