@peerbit/shared-log 13.2.32 → 13.2.34
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 +6 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +613 -123
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-info-v2-binding.d.ts +16 -0
- package/dist/src/replication-info-v2-binding.d.ts.map +1 -0
- package/dist/src/replication-info-v2-binding.js +30 -0
- package/dist/src/replication-info-v2-binding.js.map +1 -0
- package/dist/src/replication-info-v2-receive.d.ts +250 -0
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -0
- package/dist/src/replication-info-v2-receive.js +1089 -0
- package/dist/src/replication-info-v2-receive.js.map +1 -0
- package/dist/src/replication-info-v2-send.d.ts +25 -17
- package/dist/src/replication-info-v2-send.d.ts.map +1 -1
- package/dist/src/replication-info-v2-send.js +170 -86
- package/dist/src/replication-info-v2-send.js.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +897 -260
- package/src/replication-info-v2-binding.ts +44 -0
- package/src/replication-info-v2-receive.ts +1577 -0
- package/src/replication-info-v2-send.ts +223 -109
|
@@ -0,0 +1,1577 @@
|
|
|
1
|
+
import { serialize } from "@dao-xyz/borsh";
|
|
2
|
+
import { type PublicSignKey, randomBytes, sha256Sync } from "@peerbit/crypto";
|
|
3
|
+
import {
|
|
4
|
+
SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE,
|
|
5
|
+
SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND,
|
|
6
|
+
} from "./exchange-heads.js";
|
|
7
|
+
import { deriveReplicationInfoV2ReceiverBinding } from "./replication-info-v2-binding.js";
|
|
8
|
+
import {
|
|
9
|
+
AddedReplicationInfoV2Message,
|
|
10
|
+
AddedReplicationSegmentMessage,
|
|
11
|
+
AllReplicatingSegmentsMessage,
|
|
12
|
+
FullReplicationInfoV2Message,
|
|
13
|
+
type ReplicationInfoV2Message,
|
|
14
|
+
RequestReplicationInfoV2Message,
|
|
15
|
+
StoppedReplicating,
|
|
16
|
+
StoppedReplicationInfoV2Message,
|
|
17
|
+
} from "./replication.js";
|
|
18
|
+
|
|
19
|
+
const REQUIRED_SENDER_CAPABILITIES =
|
|
20
|
+
SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
|
|
21
|
+
SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND;
|
|
22
|
+
|
|
23
|
+
const DEFAULT_REQUEST_RETRY_MS = 1_000;
|
|
24
|
+
const DEFAULT_MAX_REQUEST_RETRY_MS = 30_000;
|
|
25
|
+
const DEFAULT_REQUEST_MAX_ATTEMPTS = 7;
|
|
26
|
+
const DEFAULT_LEGACY_FALLBACK_DELAY_MS = 5_000;
|
|
27
|
+
const MAX_U64 = (1n << 64n) - 1n;
|
|
28
|
+
const MAX_BACKOFF_EXPONENT = 20;
|
|
29
|
+
|
|
30
|
+
const bytesEqual = (left: Uint8Array, right: Uint8Array): boolean => {
|
|
31
|
+
if (left.byteLength !== right.byteLength) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
for (let index = 0; index < left.byteLength; index++) {
|
|
35
|
+
if (left[index] !== right[index]) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type LegacyReplicationInfoMessage =
|
|
43
|
+
| AllReplicatingSegmentsMessage
|
|
44
|
+
| AddedReplicationSegmentMessage
|
|
45
|
+
| StoppedReplicating;
|
|
46
|
+
|
|
47
|
+
const replicationInfoPayloadFingerprint = (
|
|
48
|
+
message: ReplicationInfoV2Message | LegacyReplicationInfoMessage,
|
|
49
|
+
): Uint8Array => {
|
|
50
|
+
const canonical =
|
|
51
|
+
message instanceof FullReplicationInfoV2Message
|
|
52
|
+
? new AllReplicatingSegmentsMessage({ segments: message.segments })
|
|
53
|
+
: message instanceof AddedReplicationInfoV2Message
|
|
54
|
+
? new AddedReplicationSegmentMessage({ segments: message.segments })
|
|
55
|
+
: message instanceof StoppedReplicationInfoV2Message
|
|
56
|
+
? new StoppedReplicating({ segmentIds: message.segmentIds })
|
|
57
|
+
: message;
|
|
58
|
+
return sha256Sync(serialize(canonical));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type ReplicationInfoV2ReceivePhase =
|
|
62
|
+
| "awaiting-full"
|
|
63
|
+
| "active"
|
|
64
|
+
| "resync";
|
|
65
|
+
|
|
66
|
+
type LocalCapabilityReady = {
|
|
67
|
+
peerHash: string;
|
|
68
|
+
receiveEpoch: object | null;
|
|
69
|
+
receiverTransportSession: bigint;
|
|
70
|
+
/**
|
|
71
|
+
* The local capability envelope and the later RequestV2 envelope use the
|
|
72
|
+
* same millisecond clock. The sender deliberately requires the request to
|
|
73
|
+
* be strictly newer, so never construct it in this captured millisecond.
|
|
74
|
+
*/
|
|
75
|
+
requestNotBeforeMs: number;
|
|
76
|
+
advertisement?: ReplicationInfoV2LocalCapabilityAdvertisement;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type LocalCapabilityReadyProperties = {
|
|
80
|
+
peerHash: string;
|
|
81
|
+
peerSession: object;
|
|
82
|
+
receiveEpoch: object | null;
|
|
83
|
+
receiverTransportSession: bigint;
|
|
84
|
+
requestNotBeforeMs: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type ReplicationInfoV2LocalCapabilityAdvertisementHandle = {
|
|
88
|
+
firstAttempt: Promise<void>;
|
|
89
|
+
releaseLegacyBarrier(): void;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type ReplicationInfoV2LocalCapabilityContext = {
|
|
93
|
+
peerHash: string;
|
|
94
|
+
target: PublicSignKey;
|
|
95
|
+
lifecycleSignal: AbortSignal;
|
|
96
|
+
legacyBarrierReleased: boolean;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type ReplicationInfoV2LocalCapabilityAdvertisement = {
|
|
100
|
+
peerHash: string;
|
|
101
|
+
target: PublicSignKey;
|
|
102
|
+
peerSession: object;
|
|
103
|
+
receiveEpoch: object | null;
|
|
104
|
+
lifecycleSignal: AbortSignal;
|
|
105
|
+
onLifecycleAbort: () => void;
|
|
106
|
+
controller: AbortController;
|
|
107
|
+
context: ReplicationInfoV2LocalCapabilityContext;
|
|
108
|
+
attempts: number;
|
|
109
|
+
ready: boolean;
|
|
110
|
+
acknowledgedReady?: LocalCapabilityReady;
|
|
111
|
+
receiverTransportSession?: bigint;
|
|
112
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
113
|
+
inFlight?: Promise<void>;
|
|
114
|
+
firstAttempt?: Promise<void>;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type ReplicationInfoV2LocalCapabilityRefresh = {
|
|
118
|
+
receiverTransportSession: bigint;
|
|
119
|
+
requestNotBeforeMs: number;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type ReplicationInfoV2ReceiveState = {
|
|
123
|
+
peerHash: string;
|
|
124
|
+
target: PublicSignKey;
|
|
125
|
+
peerSession: object;
|
|
126
|
+
receiveEpoch: object | null;
|
|
127
|
+
capabilities: number;
|
|
128
|
+
capabilityTimestamp: bigint;
|
|
129
|
+
senderTransportSession: bigint;
|
|
130
|
+
receiverTransportSession?: bigint;
|
|
131
|
+
receiverRequestChallenge: Uint8Array;
|
|
132
|
+
receiverBinding?: Uint8Array;
|
|
133
|
+
senderEpoch?: Uint8Array;
|
|
134
|
+
lastSequence?: bigint;
|
|
135
|
+
phase: ReplicationInfoV2ReceivePhase;
|
|
136
|
+
version: number;
|
|
137
|
+
controller: AbortController;
|
|
138
|
+
requestTimer?: ReturnType<typeof setTimeout>;
|
|
139
|
+
requestInFlight?: Promise<void>;
|
|
140
|
+
requestAttempts: number;
|
|
141
|
+
requestsSinceCapabilityRefresh: number;
|
|
142
|
+
requestParked: boolean;
|
|
143
|
+
capabilityRefreshRequired: boolean;
|
|
144
|
+
legacyFallbackTimer?: ReturnType<typeof setTimeout>;
|
|
145
|
+
legacyFallbackFingerprint?: Uint8Array;
|
|
146
|
+
legacyFallbackTimestamp?: bigint;
|
|
147
|
+
legacyFallbackAmbiguous: boolean;
|
|
148
|
+
lastCommittedTransportTimestamp?: bigint;
|
|
149
|
+
recentCommittedPayloads: Array<{
|
|
150
|
+
fingerprint: Uint8Array;
|
|
151
|
+
transportTimestamp: bigint;
|
|
152
|
+
}>;
|
|
153
|
+
lastLegacyObservationTimestamp?: bigint;
|
|
154
|
+
lastLegacyObservationFingerprint?: Uint8Array;
|
|
155
|
+
lastLegacyObservationAmbiguous: boolean;
|
|
156
|
+
reservedAdmission?: ReplicationInfoV2ReceiveAdmission;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export type ReplicationInfoV2ReceiveAdmission = {
|
|
160
|
+
state: ReplicationInfoV2ReceiveState;
|
|
161
|
+
version: number;
|
|
162
|
+
receiveEpoch: object | null;
|
|
163
|
+
message: ReplicationInfoV2Message;
|
|
164
|
+
kind: "full" | "added" | "stopped";
|
|
165
|
+
payloadFingerprint: Uint8Array;
|
|
166
|
+
transportTimestamp: bigint;
|
|
167
|
+
committed: boolean;
|
|
168
|
+
resyncAfterRelease?: boolean;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export type ReplicationInfoV2ReceiveDeps = {
|
|
172
|
+
getSelfKey: () => PublicSignKey;
|
|
173
|
+
getReceiverTransportSession: () => bigint;
|
|
174
|
+
isClosed: () => boolean;
|
|
175
|
+
isPeerSessionCurrent: (peerHash: string, peerSession: object) => boolean;
|
|
176
|
+
isReceiveEpochCurrent: (
|
|
177
|
+
peerHash: string,
|
|
178
|
+
receiveEpoch: object | null,
|
|
179
|
+
) => boolean;
|
|
180
|
+
isPeerStateCurrent: (
|
|
181
|
+
peerHash: string,
|
|
182
|
+
peerSession: object,
|
|
183
|
+
receiveEpoch: object | null,
|
|
184
|
+
) => boolean;
|
|
185
|
+
isSenderTransportSessionCurrent: (
|
|
186
|
+
peerHash: string,
|
|
187
|
+
senderTransportSession: bigint,
|
|
188
|
+
) => boolean;
|
|
189
|
+
sendRequest: (
|
|
190
|
+
request: RequestReplicationInfoV2Message,
|
|
191
|
+
target: PublicSignKey,
|
|
192
|
+
signal: AbortSignal,
|
|
193
|
+
) => Promise<void>;
|
|
194
|
+
refreshLocalCapability: (properties: {
|
|
195
|
+
peerHash: string;
|
|
196
|
+
target: PublicSignKey;
|
|
197
|
+
peerSession: object;
|
|
198
|
+
receiveEpoch: object | null;
|
|
199
|
+
signal: AbortSignal;
|
|
200
|
+
}) => Promise<ReplicationInfoV2LocalCapabilityRefresh | undefined>;
|
|
201
|
+
onRequestError?: (error: unknown) => void;
|
|
202
|
+
onLocalCapabilityError?: (error: unknown) => void;
|
|
203
|
+
now?: () => number;
|
|
204
|
+
requestRetryMs?: number;
|
|
205
|
+
maxRequestRetryMs?: number;
|
|
206
|
+
requestMaxAttempts?: number;
|
|
207
|
+
legacyFallbackDelayMs?: number;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Authenticated receive grants and sender-authoritative ordering for
|
|
212
|
+
* replication-info V2. State is bounded to one entry per subscribed peer and
|
|
213
|
+
* is always scoped to the exact PeerSession object.
|
|
214
|
+
*/
|
|
215
|
+
export class ReplicationInfoV2ReceiveCoordinator {
|
|
216
|
+
_receiveStates!: Map<string, ReplicationInfoV2ReceiveState>;
|
|
217
|
+
_cutoverPeerSessions!: WeakSet<object>;
|
|
218
|
+
_localCapabilityReadyBySession!: WeakMap<object, LocalCapabilityReady>;
|
|
219
|
+
_localCapabilityContextBySession!: WeakMap<
|
|
220
|
+
object,
|
|
221
|
+
ReplicationInfoV2LocalCapabilityContext
|
|
222
|
+
>;
|
|
223
|
+
_localCapabilityAdvertisementsByPeer!: Map<
|
|
224
|
+
string,
|
|
225
|
+
ReplicationInfoV2LocalCapabilityAdvertisement
|
|
226
|
+
>;
|
|
227
|
+
_reservedAdmissionsByPeer!: Map<string, ReplicationInfoV2ReceiveAdmission>;
|
|
228
|
+
|
|
229
|
+
private readonly now: () => number;
|
|
230
|
+
private readonly requestRetryMs: number;
|
|
231
|
+
private readonly maxRequestRetryMs: number;
|
|
232
|
+
private readonly requestMaxAttempts: number;
|
|
233
|
+
private readonly legacyFallbackDelayMs: number;
|
|
234
|
+
|
|
235
|
+
constructor(private readonly deps: ReplicationInfoV2ReceiveDeps) {
|
|
236
|
+
this.now = deps.now ?? Date.now;
|
|
237
|
+
this.requestRetryMs = Math.max(
|
|
238
|
+
1,
|
|
239
|
+
deps.requestRetryMs ?? DEFAULT_REQUEST_RETRY_MS,
|
|
240
|
+
);
|
|
241
|
+
this.maxRequestRetryMs = Math.max(
|
|
242
|
+
this.requestRetryMs,
|
|
243
|
+
deps.maxRequestRetryMs ?? DEFAULT_MAX_REQUEST_RETRY_MS,
|
|
244
|
+
);
|
|
245
|
+
this.requestMaxAttempts = Math.max(
|
|
246
|
+
1,
|
|
247
|
+
Math.floor(deps.requestMaxAttempts ?? DEFAULT_REQUEST_MAX_ATTEMPTS),
|
|
248
|
+
);
|
|
249
|
+
this.legacyFallbackDelayMs = Math.max(
|
|
250
|
+
this.requestRetryMs,
|
|
251
|
+
deps.legacyFallbackDelayMs ?? DEFAULT_LEGACY_FALLBACK_DELAY_MS,
|
|
252
|
+
);
|
|
253
|
+
this._receiveStates = new Map();
|
|
254
|
+
this._cutoverPeerSessions = new WeakSet();
|
|
255
|
+
this._localCapabilityReadyBySession = new WeakMap();
|
|
256
|
+
this._localCapabilityContextBySession = new WeakMap();
|
|
257
|
+
this._localCapabilityAdvertisementsByPeer = new Map();
|
|
258
|
+
this._reservedAdmissionsByPeer = new Map();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
resetForOpen(): void {
|
|
262
|
+
this.clearForClose();
|
|
263
|
+
this._receiveStates = new Map();
|
|
264
|
+
this._cutoverPeerSessions = new WeakSet();
|
|
265
|
+
this._localCapabilityReadyBySession = new WeakMap();
|
|
266
|
+
this._localCapabilityContextBySession = new WeakMap();
|
|
267
|
+
this._localCapabilityAdvertisementsByPeer = new Map();
|
|
268
|
+
this._reservedAdmissionsByPeer = new Map();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
clearForClose(): void {
|
|
272
|
+
for (const advertisement of [
|
|
273
|
+
...(this._localCapabilityAdvertisementsByPeer?.values() ?? []),
|
|
274
|
+
]) {
|
|
275
|
+
this.clearLocalCapabilityAdvertisement(advertisement);
|
|
276
|
+
}
|
|
277
|
+
this._localCapabilityAdvertisementsByPeer?.clear();
|
|
278
|
+
for (const state of this._receiveStates?.values() ?? []) {
|
|
279
|
+
this.clearState(state);
|
|
280
|
+
}
|
|
281
|
+
this._receiveStates?.clear();
|
|
282
|
+
this._cutoverPeerSessions = new WeakSet();
|
|
283
|
+
this._localCapabilityReadyBySession = new WeakMap();
|
|
284
|
+
this._localCapabilityContextBySession = new WeakMap();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
clearPeer(peerHash: string, expectedSession?: object): void {
|
|
288
|
+
const advertisement =
|
|
289
|
+
this._localCapabilityAdvertisementsByPeer.get(peerHash);
|
|
290
|
+
if (
|
|
291
|
+
advertisement &&
|
|
292
|
+
(!expectedSession || advertisement.peerSession === expectedSession)
|
|
293
|
+
) {
|
|
294
|
+
this.clearLocalCapabilityAdvertisement(advertisement);
|
|
295
|
+
}
|
|
296
|
+
const state = this._receiveStates.get(peerHash);
|
|
297
|
+
if (state && (!expectedSession || state.peerSession === expectedSession)) {
|
|
298
|
+
this.clearState(state);
|
|
299
|
+
this._localCapabilityReadyBySession.delete(state.peerSession);
|
|
300
|
+
this._localCapabilityContextBySession.delete(state.peerSession);
|
|
301
|
+
this._cutoverPeerSessions.delete(state.peerSession);
|
|
302
|
+
}
|
|
303
|
+
if (expectedSession) {
|
|
304
|
+
this._localCapabilityReadyBySession.delete(expectedSession);
|
|
305
|
+
this._localCapabilityContextBySession.delete(expectedSession);
|
|
306
|
+
this._cutoverPeerSessions.delete(expectedSession);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Revoke an unauthenticated or downgraded capability generation. */
|
|
311
|
+
revokePeerCapability(peerHash: string, reopenLegacy = true): void {
|
|
312
|
+
const state = this._receiveStates.get(peerHash);
|
|
313
|
+
if (!state) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.clearState(state);
|
|
317
|
+
if (reopenLegacy) {
|
|
318
|
+
this._cutoverPeerSessions.delete(state.peerSession);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private clearState(state: ReplicationInfoV2ReceiveState): void {
|
|
323
|
+
if (state.requestTimer) {
|
|
324
|
+
clearTimeout(state.requestTimer);
|
|
325
|
+
state.requestTimer = undefined;
|
|
326
|
+
}
|
|
327
|
+
if (state.legacyFallbackTimer) {
|
|
328
|
+
clearTimeout(state.legacyFallbackTimer);
|
|
329
|
+
state.legacyFallbackTimer = undefined;
|
|
330
|
+
}
|
|
331
|
+
state.controller.abort();
|
|
332
|
+
state.version++;
|
|
333
|
+
if (this._receiveStates.get(state.peerHash) === state) {
|
|
334
|
+
this._receiveStates.delete(state.peerHash);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private clearLocalCapabilityAdvertisement(
|
|
339
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
340
|
+
options?: { preserveContext?: boolean },
|
|
341
|
+
): void {
|
|
342
|
+
if (state.timer) {
|
|
343
|
+
clearTimeout(state.timer);
|
|
344
|
+
state.timer = undefined;
|
|
345
|
+
}
|
|
346
|
+
state.lifecycleSignal.removeEventListener("abort", state.onLifecycleAbort);
|
|
347
|
+
state.controller.abort();
|
|
348
|
+
const wasMapped =
|
|
349
|
+
this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state;
|
|
350
|
+
if (wasMapped) {
|
|
351
|
+
this._localCapabilityAdvertisementsByPeer.delete(state.peerHash);
|
|
352
|
+
}
|
|
353
|
+
const ready = this._localCapabilityReadyBySession.get(state.peerSession);
|
|
354
|
+
if (ready?.advertisement === state) {
|
|
355
|
+
this._localCapabilityReadyBySession.delete(state.peerSession);
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
wasMapped &&
|
|
359
|
+
!options?.preserveContext &&
|
|
360
|
+
this._localCapabilityContextBySession.get(state.peerSession) ===
|
|
361
|
+
state.context
|
|
362
|
+
) {
|
|
363
|
+
this._localCapabilityContextBySession.delete(state.peerSession);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private isLocalCapabilityAdvertisementOwnerCurrent(
|
|
368
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
369
|
+
): boolean {
|
|
370
|
+
return (
|
|
371
|
+
this._localCapabilityAdvertisementsByPeer.get(state.peerHash) === state &&
|
|
372
|
+
this._localCapabilityContextBySession.get(state.peerSession) ===
|
|
373
|
+
state.context &&
|
|
374
|
+
state.context.peerHash === state.peerHash &&
|
|
375
|
+
state.context.lifecycleSignal === state.lifecycleSignal &&
|
|
376
|
+
state.context.target.equals(state.target) &&
|
|
377
|
+
!state.controller.signal.aborted &&
|
|
378
|
+
!state.lifecycleSignal.aborted &&
|
|
379
|
+
!this.deps.isClosed() &&
|
|
380
|
+
this.deps.isPeerSessionCurrent(state.peerHash, state.peerSession) &&
|
|
381
|
+
(state.receiverTransportSession === undefined ||
|
|
382
|
+
this.deps.getReceiverTransportSession() ===
|
|
383
|
+
state.receiverTransportSession)
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private isLocalCapabilityAdvertisementGenerationCurrent(
|
|
388
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
389
|
+
): boolean {
|
|
390
|
+
return (
|
|
391
|
+
this.isLocalCapabilityAdvertisementOwnerCurrent(state) &&
|
|
392
|
+
this.deps.isReceiveEpochCurrent(state.peerHash, state.receiveEpoch)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private isLocalCapabilityAdvertisementReadyOpen(
|
|
397
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
398
|
+
): boolean {
|
|
399
|
+
return (
|
|
400
|
+
this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
|
|
401
|
+
this.deps.isPeerStateCurrent(
|
|
402
|
+
state.peerHash,
|
|
403
|
+
state.peerSession,
|
|
404
|
+
state.receiveEpoch,
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private localCapabilityRetryDelay(
|
|
410
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
411
|
+
): number {
|
|
412
|
+
const exponent = Math.max(0, state.attempts - 1);
|
|
413
|
+
return Math.min(
|
|
414
|
+
this.maxRequestRetryMs,
|
|
415
|
+
this.requestRetryMs * 2 ** Math.min(exponent, MAX_BACKOFF_EXPONENT),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private armLocalCapabilityAdvertisement(
|
|
420
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
421
|
+
): void {
|
|
422
|
+
if (state.timer || state.inFlight || state.ready) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (state.acknowledgedReady && !state.context.legacyBarrierReleased) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
429
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
state.timer = setTimeout(() => {
|
|
436
|
+
state.timer = undefined;
|
|
437
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
438
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
|
|
445
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
void this.runLocalCapabilityAdvertisement(state);
|
|
449
|
+
}, this.localCapabilityRetryDelay(state));
|
|
450
|
+
state.timer.unref?.();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private async runLocalCapabilityAdvertisement(
|
|
454
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
455
|
+
): Promise<void> {
|
|
456
|
+
if (state.inFlight) {
|
|
457
|
+
await state.inFlight;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (state.ready) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
464
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!this.isLocalCapabilityAdvertisementGenerationCurrent(state)) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (state.acknowledgedReady) {
|
|
471
|
+
if (state.context.legacyBarrierReleased) {
|
|
472
|
+
this.promoteLocalCapabilityAdvertisement(state);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
|
|
477
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
state.attempts = Math.min(state.attempts + 1, MAX_BACKOFF_EXPONENT + 1);
|
|
481
|
+
let operation: Promise<void>;
|
|
482
|
+
operation = (async () => {
|
|
483
|
+
const refreshed = await this.deps.refreshLocalCapability({
|
|
484
|
+
peerHash: state.peerHash,
|
|
485
|
+
target: state.target,
|
|
486
|
+
peerSession: state.peerSession,
|
|
487
|
+
receiveEpoch: state.receiveEpoch,
|
|
488
|
+
signal: AbortSignal.any([
|
|
489
|
+
state.controller.signal,
|
|
490
|
+
state.lifecycleSignal,
|
|
491
|
+
]),
|
|
492
|
+
});
|
|
493
|
+
if (
|
|
494
|
+
!refreshed ||
|
|
495
|
+
!this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
|
|
496
|
+
this.deps.getReceiverTransportSession() !==
|
|
497
|
+
refreshed.receiverTransportSession
|
|
498
|
+
) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
state.receiverTransportSession = refreshed.receiverTransportSession;
|
|
502
|
+
state.acknowledgedReady = {
|
|
503
|
+
peerHash: state.peerHash,
|
|
504
|
+
receiveEpoch: state.receiveEpoch,
|
|
505
|
+
receiverTransportSession: refreshed.receiverTransportSession,
|
|
506
|
+
requestNotBeforeMs: refreshed.requestNotBeforeMs,
|
|
507
|
+
advertisement: state,
|
|
508
|
+
};
|
|
509
|
+
state.attempts = 0;
|
|
510
|
+
this.promoteLocalCapabilityAdvertisement(state);
|
|
511
|
+
})()
|
|
512
|
+
.catch((error) => {
|
|
513
|
+
if (
|
|
514
|
+
!state.controller.signal.aborted &&
|
|
515
|
+
!state.lifecycleSignal.aborted &&
|
|
516
|
+
!this.deps.isClosed()
|
|
517
|
+
) {
|
|
518
|
+
this.deps.onLocalCapabilityError?.(error);
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
.finally(() => {
|
|
522
|
+
if (state.inFlight === operation) {
|
|
523
|
+
state.inFlight = undefined;
|
|
524
|
+
}
|
|
525
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
526
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
527
|
+
} else if (
|
|
528
|
+
this.isLocalCapabilityAdvertisementGenerationCurrent(state) &&
|
|
529
|
+
!state.ready
|
|
530
|
+
) {
|
|
531
|
+
if (!state.acknowledgedReady || state.context.legacyBarrierReleased) {
|
|
532
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
state.inFlight = operation;
|
|
537
|
+
await operation;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Start local authenticated-apply advertisement independently of the legacy
|
|
542
|
+
* join path. ACK and legacy publication are a two-phase barrier: the first
|
|
543
|
+
* attempt always settles independently, while readiness is promoted only
|
|
544
|
+
* after the host releases the legacy barrier. Failed attempts leave one
|
|
545
|
+
* exact-session worker retrying with capped exponential backoff.
|
|
546
|
+
*/
|
|
547
|
+
advertiseLocalCapability(properties: {
|
|
548
|
+
target: PublicSignKey;
|
|
549
|
+
peerSession: object;
|
|
550
|
+
receiveEpoch: object | null;
|
|
551
|
+
signal: AbortSignal;
|
|
552
|
+
}): ReplicationInfoV2LocalCapabilityAdvertisementHandle {
|
|
553
|
+
const peerHash = properties.target.hashcode();
|
|
554
|
+
if (
|
|
555
|
+
properties.signal.aborted ||
|
|
556
|
+
this.deps.isClosed() ||
|
|
557
|
+
!this.deps.isPeerSessionCurrent(peerHash, properties.peerSession) ||
|
|
558
|
+
!this.deps.isReceiveEpochCurrent(peerHash, properties.receiveEpoch)
|
|
559
|
+
) {
|
|
560
|
+
return {
|
|
561
|
+
firstAttempt: Promise.resolve(),
|
|
562
|
+
releaseLegacyBarrier: () => {},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
let context = this._localCapabilityContextBySession.get(
|
|
566
|
+
properties.peerSession,
|
|
567
|
+
);
|
|
568
|
+
if (
|
|
569
|
+
context &&
|
|
570
|
+
(context.peerHash !== peerHash ||
|
|
571
|
+
!context.target.equals(properties.target) ||
|
|
572
|
+
context.lifecycleSignal !== properties.signal)
|
|
573
|
+
) {
|
|
574
|
+
return {
|
|
575
|
+
firstAttempt: Promise.resolve(),
|
|
576
|
+
releaseLegacyBarrier: () => {},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
if (!context) {
|
|
580
|
+
context = {
|
|
581
|
+
peerHash,
|
|
582
|
+
target: properties.target,
|
|
583
|
+
lifecycleSignal: properties.signal,
|
|
584
|
+
legacyBarrierReleased: false,
|
|
585
|
+
};
|
|
586
|
+
this._localCapabilityContextBySession.set(
|
|
587
|
+
properties.peerSession,
|
|
588
|
+
context,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
let state = this._localCapabilityAdvertisementsByPeer.get(peerHash);
|
|
592
|
+
if (
|
|
593
|
+
state &&
|
|
594
|
+
(state.peerSession !== properties.peerSession ||
|
|
595
|
+
state.context !== context ||
|
|
596
|
+
state.lifecycleSignal !== properties.signal ||
|
|
597
|
+
!state.target.equals(properties.target))
|
|
598
|
+
) {
|
|
599
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
600
|
+
state = undefined;
|
|
601
|
+
}
|
|
602
|
+
if (state && state.receiveEpoch !== properties.receiveEpoch) {
|
|
603
|
+
this.clearLocalCapabilityAdvertisement(state, { preserveContext: true });
|
|
604
|
+
state = undefined;
|
|
605
|
+
}
|
|
606
|
+
if (state && !this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
607
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
608
|
+
state = undefined;
|
|
609
|
+
}
|
|
610
|
+
if (
|
|
611
|
+
this._localCapabilityContextBySession.get(properties.peerSession) !==
|
|
612
|
+
context
|
|
613
|
+
) {
|
|
614
|
+
return {
|
|
615
|
+
firstAttempt: Promise.resolve(),
|
|
616
|
+
releaseLegacyBarrier: () => {},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
if (!state) {
|
|
620
|
+
const controller = new AbortController();
|
|
621
|
+
const advertisement: ReplicationInfoV2LocalCapabilityAdvertisement = {
|
|
622
|
+
peerHash,
|
|
623
|
+
target: properties.target,
|
|
624
|
+
peerSession: properties.peerSession,
|
|
625
|
+
receiveEpoch: properties.receiveEpoch,
|
|
626
|
+
lifecycleSignal: properties.signal,
|
|
627
|
+
onLifecycleAbort: () => {},
|
|
628
|
+
controller,
|
|
629
|
+
context,
|
|
630
|
+
attempts: 0,
|
|
631
|
+
ready: false,
|
|
632
|
+
receiverTransportSession: this.deps.getReceiverTransportSession(),
|
|
633
|
+
};
|
|
634
|
+
advertisement.onLifecycleAbort = () =>
|
|
635
|
+
this.clearLocalCapabilityAdvertisement(advertisement);
|
|
636
|
+
properties.signal.addEventListener(
|
|
637
|
+
"abort",
|
|
638
|
+
advertisement.onLifecycleAbort,
|
|
639
|
+
{
|
|
640
|
+
once: true,
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
this._localCapabilityAdvertisementsByPeer.set(peerHash, advertisement);
|
|
644
|
+
state = advertisement;
|
|
645
|
+
}
|
|
646
|
+
const firstAttempt =
|
|
647
|
+
state.firstAttempt ??
|
|
648
|
+
(state.firstAttempt = this.runLocalCapabilityAdvertisement(state));
|
|
649
|
+
const advertisement = state;
|
|
650
|
+
return {
|
|
651
|
+
firstAttempt,
|
|
652
|
+
releaseLegacyBarrier: () =>
|
|
653
|
+
this.releaseLocalCapabilityLegacyBarrier(
|
|
654
|
+
advertisement.peerSession,
|
|
655
|
+
context,
|
|
656
|
+
),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private releaseLocalCapabilityLegacyBarrier(
|
|
661
|
+
peerSession: object,
|
|
662
|
+
context: ReplicationInfoV2LocalCapabilityContext,
|
|
663
|
+
): void {
|
|
664
|
+
if (context.legacyBarrierReleased) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (
|
|
668
|
+
this._localCapabilityContextBySession.get(peerSession) !== context ||
|
|
669
|
+
context.lifecycleSignal.aborted ||
|
|
670
|
+
this.deps.isClosed() ||
|
|
671
|
+
!this.deps.isPeerSessionCurrent(context.peerHash, peerSession)
|
|
672
|
+
) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
context.legacyBarrierReleased = true;
|
|
676
|
+
const state = this._localCapabilityAdvertisementsByPeer.get(
|
|
677
|
+
context.peerHash,
|
|
678
|
+
);
|
|
679
|
+
if (state?.peerSession === peerSession && state.context === context) {
|
|
680
|
+
if (!this.isLocalCapabilityAdvertisementOwnerCurrent(state)) {
|
|
681
|
+
this.clearLocalCapabilityAdvertisement(state);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.promoteLocalCapabilityAdvertisement(state);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private promoteLocalCapabilityAdvertisement(
|
|
689
|
+
state: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
690
|
+
): boolean {
|
|
691
|
+
const ready = state.acknowledgedReady;
|
|
692
|
+
if (
|
|
693
|
+
state.ready ||
|
|
694
|
+
!state.context.legacyBarrierReleased ||
|
|
695
|
+
!ready ||
|
|
696
|
+
ready.receiveEpoch !== state.receiveEpoch ||
|
|
697
|
+
ready.advertisement !== state ||
|
|
698
|
+
!this.isLocalCapabilityAdvertisementOwnerCurrent(state) ||
|
|
699
|
+
!this.isLocalCapabilityAdvertisementGenerationCurrent(state) ||
|
|
700
|
+
this.deps.getReceiverTransportSession() !== ready.receiverTransportSession
|
|
701
|
+
) {
|
|
702
|
+
return state.ready;
|
|
703
|
+
}
|
|
704
|
+
if (!this.isLocalCapabilityAdvertisementReadyOpen(state)) {
|
|
705
|
+
this.armLocalCapabilityAdvertisement(state);
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
if (
|
|
709
|
+
!this.recordLocalCapabilityReady(
|
|
710
|
+
{
|
|
711
|
+
peerHash: state.peerHash,
|
|
712
|
+
peerSession: state.peerSession,
|
|
713
|
+
receiveEpoch: state.receiveEpoch,
|
|
714
|
+
receiverTransportSession: ready.receiverTransportSession,
|
|
715
|
+
requestNotBeforeMs: ready.requestNotBeforeMs,
|
|
716
|
+
},
|
|
717
|
+
state,
|
|
718
|
+
)
|
|
719
|
+
) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
state.ready = true;
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Re-advertise one exact current recovery epoch from the stable membership
|
|
728
|
+
* context captured during opening. The opening handle owns barrier release;
|
|
729
|
+
* recovery never bypasses a legacy snapshot or role publication still in
|
|
730
|
+
* progress.
|
|
731
|
+
*/
|
|
732
|
+
reAdvertiseLocalCapabilityForRecovery(properties: {
|
|
733
|
+
peerHash: string;
|
|
734
|
+
peerSession: object;
|
|
735
|
+
receiveEpoch: object | null;
|
|
736
|
+
}): boolean {
|
|
737
|
+
const context = this._localCapabilityContextBySession.get(
|
|
738
|
+
properties.peerSession,
|
|
739
|
+
);
|
|
740
|
+
if (
|
|
741
|
+
!context ||
|
|
742
|
+
context.peerHash !== properties.peerHash ||
|
|
743
|
+
context.lifecycleSignal.aborted ||
|
|
744
|
+
this.deps.isClosed() ||
|
|
745
|
+
!this.deps.isPeerSessionCurrent(
|
|
746
|
+
properties.peerHash,
|
|
747
|
+
properties.peerSession,
|
|
748
|
+
) ||
|
|
749
|
+
!this.deps.isReceiveEpochCurrent(
|
|
750
|
+
properties.peerHash,
|
|
751
|
+
properties.receiveEpoch,
|
|
752
|
+
)
|
|
753
|
+
) {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
const state = this._receiveStates.get(properties.peerHash);
|
|
757
|
+
if (
|
|
758
|
+
state?.peerSession === properties.peerSession &&
|
|
759
|
+
state.receiveEpoch === properties.receiveEpoch &&
|
|
760
|
+
state.receiverBinding !== undefined
|
|
761
|
+
) {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
const ready = this._localCapabilityReadyBySession.get(
|
|
765
|
+
properties.peerSession,
|
|
766
|
+
);
|
|
767
|
+
if (
|
|
768
|
+
ready?.peerHash === properties.peerHash &&
|
|
769
|
+
ready.receiveEpoch === properties.receiveEpoch &&
|
|
770
|
+
ready.receiverTransportSession === this.deps.getReceiverTransportSession()
|
|
771
|
+
) {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
this.advertiseLocalCapability({
|
|
775
|
+
target: context.target,
|
|
776
|
+
peerSession: properties.peerSession,
|
|
777
|
+
receiveEpoch: properties.receiveEpoch,
|
|
778
|
+
signal: context.lifecycleSignal,
|
|
779
|
+
});
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Record success of this session's ACKed local APPLY advertisement. */
|
|
784
|
+
markLocalCapabilityReady(
|
|
785
|
+
properties: LocalCapabilityReadyProperties,
|
|
786
|
+
): boolean {
|
|
787
|
+
return this.recordLocalCapabilityReady(properties);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private recordLocalCapabilityReady(
|
|
791
|
+
properties: LocalCapabilityReadyProperties,
|
|
792
|
+
advertisement?: ReplicationInfoV2LocalCapabilityAdvertisement,
|
|
793
|
+
): boolean {
|
|
794
|
+
const { peerHash, peerSession, receiverTransportSession } = properties;
|
|
795
|
+
const state = this._receiveStates.get(peerHash);
|
|
796
|
+
if (
|
|
797
|
+
this.deps.isClosed() ||
|
|
798
|
+
!this.deps.isPeerStateCurrent(
|
|
799
|
+
peerHash,
|
|
800
|
+
peerSession,
|
|
801
|
+
properties.receiveEpoch,
|
|
802
|
+
) ||
|
|
803
|
+
this.deps.getReceiverTransportSession() !== receiverTransportSession
|
|
804
|
+
) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const ready: LocalCapabilityReady = {
|
|
809
|
+
peerHash,
|
|
810
|
+
receiveEpoch: properties.receiveEpoch,
|
|
811
|
+
receiverTransportSession,
|
|
812
|
+
requestNotBeforeMs: properties.requestNotBeforeMs,
|
|
813
|
+
advertisement,
|
|
814
|
+
};
|
|
815
|
+
this._localCapabilityReadyBySession.set(peerSession, ready);
|
|
816
|
+
if (
|
|
817
|
+
state?.peerSession === peerSession &&
|
|
818
|
+
state.receiveEpoch === properties.receiveEpoch
|
|
819
|
+
) {
|
|
820
|
+
if (
|
|
821
|
+
state.receiverTransportSession !== undefined &&
|
|
822
|
+
state.receiverTransportSession !== receiverTransportSession
|
|
823
|
+
) {
|
|
824
|
+
this.clearState(state);
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
this.bindLocalCapability(state, ready);
|
|
828
|
+
state.requestAttempts = 0;
|
|
829
|
+
state.requestsSinceCapabilityRefresh = 0;
|
|
830
|
+
state.requestParked = false;
|
|
831
|
+
this.armRequest(state, 0);
|
|
832
|
+
}
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Promote one signed capability generation after the opening barrier has
|
|
838
|
+
* committed. Repeated same-session advertisements refresh freshness only;
|
|
839
|
+
* they never reset sequence state.
|
|
840
|
+
*/
|
|
841
|
+
observeCapability(properties: {
|
|
842
|
+
peerHash: string;
|
|
843
|
+
target: PublicSignKey;
|
|
844
|
+
peerSession: object;
|
|
845
|
+
receiveEpoch: object | null;
|
|
846
|
+
capabilities: number;
|
|
847
|
+
senderTransportSession: bigint;
|
|
848
|
+
capabilityTimestamp: bigint;
|
|
849
|
+
}): boolean {
|
|
850
|
+
const {
|
|
851
|
+
peerHash,
|
|
852
|
+
target,
|
|
853
|
+
peerSession,
|
|
854
|
+
receiveEpoch,
|
|
855
|
+
capabilities,
|
|
856
|
+
senderTransportSession,
|
|
857
|
+
capabilityTimestamp,
|
|
858
|
+
} = properties;
|
|
859
|
+
if (
|
|
860
|
+
target.equals(this.deps.getSelfKey()) ||
|
|
861
|
+
this.deps.isClosed() ||
|
|
862
|
+
!this.deps.isPeerStateCurrent(peerHash, peerSession, receiveEpoch)
|
|
863
|
+
) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const senderReady =
|
|
868
|
+
(capabilities & REQUIRED_SENDER_CAPABILITIES) ===
|
|
869
|
+
REQUIRED_SENDER_CAPABILITIES;
|
|
870
|
+
let state = this._receiveStates.get(peerHash);
|
|
871
|
+
if (
|
|
872
|
+
state &&
|
|
873
|
+
(state.peerSession !== peerSession ||
|
|
874
|
+
state.senderTransportSession !== senderTransportSession ||
|
|
875
|
+
!state.target.equals(target))
|
|
876
|
+
) {
|
|
877
|
+
const preserveCutover = state.peerSession === peerSession && senderReady;
|
|
878
|
+
this.clearState(state);
|
|
879
|
+
if (!preserveCutover) {
|
|
880
|
+
this._cutoverPeerSessions.delete(state.peerSession);
|
|
881
|
+
}
|
|
882
|
+
state = undefined;
|
|
883
|
+
}
|
|
884
|
+
if (!senderReady) {
|
|
885
|
+
if (state) {
|
|
886
|
+
this.clearState(state);
|
|
887
|
+
this._cutoverPeerSessions.delete(peerSession);
|
|
888
|
+
}
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (state) {
|
|
893
|
+
if (capabilityTimestamp < state.capabilityTimestamp) {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
const previousCapabilities = state.capabilities;
|
|
897
|
+
const previousTimestamp = state.capabilityTimestamp;
|
|
898
|
+
const receiveEpochChanged = state.receiveEpoch !== receiveEpoch;
|
|
899
|
+
const addsCapabilities = (capabilities & ~previousCapabilities) !== 0;
|
|
900
|
+
if (
|
|
901
|
+
capabilityTimestamp === previousTimestamp &&
|
|
902
|
+
!addsCapabilities &&
|
|
903
|
+
!receiveEpochChanged
|
|
904
|
+
) {
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
state.capabilities |= capabilities;
|
|
908
|
+
state.capabilityTimestamp = capabilityTimestamp;
|
|
909
|
+
if (receiveEpochChanged) {
|
|
910
|
+
state.receiveEpoch = receiveEpoch;
|
|
911
|
+
this.transitionToResync(state, {
|
|
912
|
+
force: true,
|
|
913
|
+
refreshCapability: true,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
const ready = this._localCapabilityReadyBySession.get(peerSession);
|
|
917
|
+
if (
|
|
918
|
+
ready?.peerHash === peerHash &&
|
|
919
|
+
ready.receiveEpoch === receiveEpoch &&
|
|
920
|
+
state.receiverBinding === undefined
|
|
921
|
+
) {
|
|
922
|
+
this.bindLocalCapability(state, ready);
|
|
923
|
+
}
|
|
924
|
+
if (state.phase !== "active") {
|
|
925
|
+
state.requestAttempts = 0;
|
|
926
|
+
state.requestParked = false;
|
|
927
|
+
this.armRequest(state, 0);
|
|
928
|
+
}
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const retainedCutover = this._cutoverPeerSessions.has(peerSession);
|
|
933
|
+
state = {
|
|
934
|
+
peerHash,
|
|
935
|
+
target,
|
|
936
|
+
peerSession,
|
|
937
|
+
receiveEpoch,
|
|
938
|
+
capabilities,
|
|
939
|
+
capabilityTimestamp,
|
|
940
|
+
senderTransportSession,
|
|
941
|
+
receiverRequestChallenge: randomBytes(32),
|
|
942
|
+
phase: retainedCutover ? "resync" : "awaiting-full",
|
|
943
|
+
version: 0,
|
|
944
|
+
controller: new AbortController(),
|
|
945
|
+
requestAttempts: 0,
|
|
946
|
+
requestsSinceCapabilityRefresh: 0,
|
|
947
|
+
requestParked: false,
|
|
948
|
+
capabilityRefreshRequired: retainedCutover,
|
|
949
|
+
legacyFallbackAmbiguous: false,
|
|
950
|
+
recentCommittedPayloads: [],
|
|
951
|
+
lastLegacyObservationAmbiguous: false,
|
|
952
|
+
};
|
|
953
|
+
this._receiveStates.set(peerHash, state);
|
|
954
|
+
const ready = this._localCapabilityReadyBySession.get(peerSession);
|
|
955
|
+
if (ready?.peerHash === peerHash && ready.receiveEpoch === receiveEpoch) {
|
|
956
|
+
this.bindLocalCapability(state, ready);
|
|
957
|
+
this.armRequest(state, 0);
|
|
958
|
+
}
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
private bindLocalCapability(
|
|
963
|
+
state: ReplicationInfoV2ReceiveState,
|
|
964
|
+
ready: LocalCapabilityReady,
|
|
965
|
+
): void {
|
|
966
|
+
state.receiverTransportSession = ready.receiverTransportSession;
|
|
967
|
+
state.receiverBinding = deriveReplicationInfoV2ReceiverBinding({
|
|
968
|
+
receiverChallenge: state.receiverRequestChallenge,
|
|
969
|
+
receiver: this.deps.getSelfKey(),
|
|
970
|
+
receiverTransportSession: ready.receiverTransportSession,
|
|
971
|
+
sender: state.target,
|
|
972
|
+
senderTransportSession: state.senderTransportSession,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Require a fresh capability-bound grant and authoritative Full. */
|
|
977
|
+
advanceRecovery(properties: {
|
|
978
|
+
peerHash: string;
|
|
979
|
+
peerSession: object;
|
|
980
|
+
receiveEpoch: object | null;
|
|
981
|
+
}): boolean {
|
|
982
|
+
const state = this._receiveStates.get(properties.peerHash);
|
|
983
|
+
if (!state || state.peerSession !== properties.peerSession) {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
state.receiveEpoch = properties.receiveEpoch;
|
|
987
|
+
this.transitionToResync(state, {
|
|
988
|
+
force: true,
|
|
989
|
+
refreshCapability: true,
|
|
990
|
+
});
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
private transitionToResync(
|
|
995
|
+
state: ReplicationInfoV2ReceiveState,
|
|
996
|
+
options?: { force?: boolean; refreshCapability?: boolean },
|
|
997
|
+
): void {
|
|
998
|
+
const shouldRestart =
|
|
999
|
+
state.phase !== "resync" ||
|
|
1000
|
+
options?.force === true ||
|
|
1001
|
+
state.requestParked;
|
|
1002
|
+
if (state.phase !== "resync" || options?.force === true) {
|
|
1003
|
+
state.phase = "resync";
|
|
1004
|
+
state.version++;
|
|
1005
|
+
}
|
|
1006
|
+
if (options?.refreshCapability) {
|
|
1007
|
+
state.capabilityRefreshRequired = true;
|
|
1008
|
+
}
|
|
1009
|
+
if (shouldRestart) {
|
|
1010
|
+
state.requestAttempts = 0;
|
|
1011
|
+
state.requestParked = false;
|
|
1012
|
+
this.armRequest(state, 0);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
isLegacyCutover(peerSession: object | null): boolean {
|
|
1017
|
+
return peerSession !== null && this._cutoverPeerSessions.has(peerSession);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
prepare(
|
|
1021
|
+
message: ReplicationInfoV2Message,
|
|
1022
|
+
properties: {
|
|
1023
|
+
from: PublicSignKey;
|
|
1024
|
+
peerSession: object;
|
|
1025
|
+
receiveEpoch: object | null;
|
|
1026
|
+
senderTransportSession: bigint;
|
|
1027
|
+
transportTimestamp: bigint;
|
|
1028
|
+
},
|
|
1029
|
+
): ReplicationInfoV2ReceiveAdmission | undefined {
|
|
1030
|
+
const peerHash = properties.from.hashcode();
|
|
1031
|
+
const state = this._receiveStates.get(peerHash);
|
|
1032
|
+
if (
|
|
1033
|
+
!state ||
|
|
1034
|
+
state.peerSession !== properties.peerSession ||
|
|
1035
|
+
state.receiveEpoch !== properties.receiveEpoch ||
|
|
1036
|
+
state.senderTransportSession !== properties.senderTransportSession ||
|
|
1037
|
+
!state.target.equals(properties.from) ||
|
|
1038
|
+
!state.receiverBinding ||
|
|
1039
|
+
!bytesEqual(message.receiverChallenge, state.receiverBinding) ||
|
|
1040
|
+
message.sequence <= 0n ||
|
|
1041
|
+
!this.isStateCurrent(state)
|
|
1042
|
+
) {
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const kind =
|
|
1047
|
+
message instanceof FullReplicationInfoV2Message
|
|
1048
|
+
? "full"
|
|
1049
|
+
: message instanceof AddedReplicationInfoV2Message
|
|
1050
|
+
? "added"
|
|
1051
|
+
: message instanceof StoppedReplicationInfoV2Message
|
|
1052
|
+
? "stopped"
|
|
1053
|
+
: undefined;
|
|
1054
|
+
if (!kind) {
|
|
1055
|
+
return undefined;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (state.senderEpoch === undefined) {
|
|
1059
|
+
if (kind !== "full") {
|
|
1060
|
+
return undefined;
|
|
1061
|
+
}
|
|
1062
|
+
} else if (!bytesEqual(message.senderEpoch, state.senderEpoch)) {
|
|
1063
|
+
return undefined;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const lastSequence = state.lastSequence;
|
|
1067
|
+
if (kind === "full") {
|
|
1068
|
+
if (lastSequence !== undefined && message.sequence <= lastSequence) {
|
|
1069
|
+
return undefined;
|
|
1070
|
+
}
|
|
1071
|
+
} else {
|
|
1072
|
+
if (
|
|
1073
|
+
state.phase !== "active" ||
|
|
1074
|
+
lastSequence === undefined ||
|
|
1075
|
+
message.sequence !== lastSequence + 1n
|
|
1076
|
+
) {
|
|
1077
|
+
if (
|
|
1078
|
+
state.phase === "active" &&
|
|
1079
|
+
lastSequence !== undefined &&
|
|
1080
|
+
message.sequence > lastSequence + 1n
|
|
1081
|
+
) {
|
|
1082
|
+
this.transitionToResync(state);
|
|
1083
|
+
}
|
|
1084
|
+
return undefined;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return {
|
|
1089
|
+
state,
|
|
1090
|
+
version: state.version,
|
|
1091
|
+
receiveEpoch: state.receiveEpoch,
|
|
1092
|
+
message,
|
|
1093
|
+
kind,
|
|
1094
|
+
payloadFingerprint: replicationInfoPayloadFingerprint(message),
|
|
1095
|
+
transportTimestamp: properties.transportTimestamp,
|
|
1096
|
+
committed: false,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** Reserve at most one decoded V2 frame per peer ahead of the apply lane. */
|
|
1101
|
+
reserve(
|
|
1102
|
+
message: ReplicationInfoV2Message,
|
|
1103
|
+
properties: {
|
|
1104
|
+
from: PublicSignKey;
|
|
1105
|
+
peerSession: object;
|
|
1106
|
+
receiveEpoch: object | null;
|
|
1107
|
+
senderTransportSession: bigint;
|
|
1108
|
+
transportTimestamp: bigint;
|
|
1109
|
+
},
|
|
1110
|
+
): ReplicationInfoV2ReceiveAdmission | undefined {
|
|
1111
|
+
const peerHash = properties.from.hashcode();
|
|
1112
|
+
const reserved = this._reservedAdmissionsByPeer.get(peerHash);
|
|
1113
|
+
if (reserved) {
|
|
1114
|
+
const state = reserved.state;
|
|
1115
|
+
const currentState = this._receiveStates.get(peerHash);
|
|
1116
|
+
const knownMessage =
|
|
1117
|
+
message instanceof FullReplicationInfoV2Message ||
|
|
1118
|
+
message instanceof AddedReplicationInfoV2Message ||
|
|
1119
|
+
message instanceof StoppedReplicationInfoV2Message;
|
|
1120
|
+
if (
|
|
1121
|
+
knownMessage &&
|
|
1122
|
+
this._receiveStates.get(peerHash) === state &&
|
|
1123
|
+
state.peerSession === properties.peerSession &&
|
|
1124
|
+
state.receiveEpoch === properties.receiveEpoch &&
|
|
1125
|
+
state.senderTransportSession === properties.senderTransportSession &&
|
|
1126
|
+
state.target.equals(properties.from) &&
|
|
1127
|
+
state.receiverBinding !== undefined &&
|
|
1128
|
+
bytesEqual(message.receiverChallenge, state.receiverBinding) &&
|
|
1129
|
+
bytesEqual(message.senderEpoch, reserved.message.senderEpoch) &&
|
|
1130
|
+
message.sequence > reserved.message.sequence &&
|
|
1131
|
+
this.isStateCurrent(state)
|
|
1132
|
+
) {
|
|
1133
|
+
// Transport ACKs precede application. Do not invalidate the frame
|
|
1134
|
+
// already applying, and do not retain an unbounded successor queue.
|
|
1135
|
+
// Commit the reservation, then request one authoritative Full.
|
|
1136
|
+
reserved.resyncAfterRelease = true;
|
|
1137
|
+
} else if (
|
|
1138
|
+
knownMessage &&
|
|
1139
|
+
currentState !== undefined &&
|
|
1140
|
+
currentState !== state &&
|
|
1141
|
+
this.prepare(message, properties)?.state === currentState
|
|
1142
|
+
) {
|
|
1143
|
+
// A previous generation can still be parked in the host apply lane.
|
|
1144
|
+
// Transport already ACKed this current-generation frame, so wake the
|
|
1145
|
+
// current state once the peer-global reservation is finally released.
|
|
1146
|
+
reserved.resyncAfterRelease = true;
|
|
1147
|
+
}
|
|
1148
|
+
return undefined;
|
|
1149
|
+
}
|
|
1150
|
+
const admission = this.prepare(message, properties);
|
|
1151
|
+
if (!admission) {
|
|
1152
|
+
return undefined;
|
|
1153
|
+
}
|
|
1154
|
+
const { state } = admission;
|
|
1155
|
+
this._reservedAdmissionsByPeer.set(peerHash, admission);
|
|
1156
|
+
state.reservedAdmission = admission;
|
|
1157
|
+
return admission;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
release(admission: ReplicationInfoV2ReceiveAdmission): void {
|
|
1161
|
+
const { state } = admission;
|
|
1162
|
+
if (this._reservedAdmissionsByPeer.get(state.peerHash) !== admission) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
this._reservedAdmissionsByPeer.delete(state.peerHash);
|
|
1166
|
+
if (state.reservedAdmission === admission) {
|
|
1167
|
+
state.reservedAdmission = undefined;
|
|
1168
|
+
}
|
|
1169
|
+
const currentState = this._receiveStates.get(state.peerHash);
|
|
1170
|
+
if (
|
|
1171
|
+
currentState &&
|
|
1172
|
+
this.isStateCurrent(currentState) &&
|
|
1173
|
+
(admission.resyncAfterRelease ||
|
|
1174
|
+
(currentState !== state && currentState.phase !== "active"))
|
|
1175
|
+
) {
|
|
1176
|
+
this.transitionToResync(currentState, { force: true });
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* B9 can lose its sender grant while keeping the topic session open. Its
|
|
1182
|
+
* unmatched legacy sidecar is the compatibility signal to re-handshake; a
|
|
1183
|
+
* matching V2 payload before or after the legacy copy suppresses the timer.
|
|
1184
|
+
*/
|
|
1185
|
+
noteLegacyAnnouncement(properties: {
|
|
1186
|
+
peerHash: string;
|
|
1187
|
+
peerSession: object;
|
|
1188
|
+
receiveEpoch: object | null;
|
|
1189
|
+
senderTransportSession: bigint;
|
|
1190
|
+
transportTimestamp: bigint;
|
|
1191
|
+
message: LegacyReplicationInfoMessage;
|
|
1192
|
+
}): boolean {
|
|
1193
|
+
const state = this._receiveStates.get(properties.peerHash);
|
|
1194
|
+
if (
|
|
1195
|
+
!state ||
|
|
1196
|
+
state.peerSession !== properties.peerSession ||
|
|
1197
|
+
state.receiveEpoch !== properties.receiveEpoch ||
|
|
1198
|
+
state.senderTransportSession !== properties.senderTransportSession ||
|
|
1199
|
+
!this._cutoverPeerSessions.has(properties.peerSession) ||
|
|
1200
|
+
!this.isStateCurrent(state)
|
|
1201
|
+
) {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
const fingerprint = replicationInfoPayloadFingerprint(properties.message);
|
|
1205
|
+
if (
|
|
1206
|
+
state.lastCommittedTransportTimestamp !== undefined &&
|
|
1207
|
+
properties.transportTimestamp < state.lastCommittedTransportTimestamp
|
|
1208
|
+
) {
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
if (
|
|
1212
|
+
state.recentCommittedPayloads.some(
|
|
1213
|
+
(committed) =>
|
|
1214
|
+
properties.transportTimestamp <= committed.transportTimestamp &&
|
|
1215
|
+
bytesEqual(committed.fingerprint, fingerprint),
|
|
1216
|
+
)
|
|
1217
|
+
) {
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
if (state.lastLegacyObservationTimestamp !== undefined) {
|
|
1221
|
+
if (
|
|
1222
|
+
properties.transportTimestamp < state.lastLegacyObservationTimestamp
|
|
1223
|
+
) {
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
if (
|
|
1227
|
+
properties.transportTimestamp === state.lastLegacyObservationTimestamp
|
|
1228
|
+
) {
|
|
1229
|
+
if (
|
|
1230
|
+
state.lastLegacyObservationAmbiguous ||
|
|
1231
|
+
(state.lastLegacyObservationFingerprint !== undefined &&
|
|
1232
|
+
bytesEqual(state.lastLegacyObservationFingerprint, fingerprint))
|
|
1233
|
+
) {
|
|
1234
|
+
return true;
|
|
1235
|
+
}
|
|
1236
|
+
state.lastLegacyObservationAmbiguous = true;
|
|
1237
|
+
} else {
|
|
1238
|
+
state.lastLegacyObservationTimestamp = properties.transportTimestamp;
|
|
1239
|
+
state.lastLegacyObservationFingerprint = fingerprint.slice();
|
|
1240
|
+
state.lastLegacyObservationAmbiguous = false;
|
|
1241
|
+
}
|
|
1242
|
+
} else {
|
|
1243
|
+
state.lastLegacyObservationTimestamp = properties.transportTimestamp;
|
|
1244
|
+
state.lastLegacyObservationFingerprint = fingerprint.slice();
|
|
1245
|
+
state.lastLegacyObservationAmbiguous = false;
|
|
1246
|
+
}
|
|
1247
|
+
const reserved = this._reservedAdmissionsByPeer.get(properties.peerHash);
|
|
1248
|
+
if (
|
|
1249
|
+
reserved?.state === state &&
|
|
1250
|
+
!bytesEqual(reserved.payloadFingerprint, fingerprint)
|
|
1251
|
+
) {
|
|
1252
|
+
// A legacy sidecar observed while a different V2 payload is applying
|
|
1253
|
+
// cannot be erased by that commit. The transport has already ACKed the
|
|
1254
|
+
// sidecar, so require one authoritative successor after release.
|
|
1255
|
+
reserved.resyncAfterRelease = true;
|
|
1256
|
+
}
|
|
1257
|
+
if (!state.legacyFallbackFingerprint) {
|
|
1258
|
+
state.legacyFallbackFingerprint = fingerprint;
|
|
1259
|
+
state.legacyFallbackTimestamp = properties.transportTimestamp;
|
|
1260
|
+
state.legacyFallbackAmbiguous = false;
|
|
1261
|
+
} else {
|
|
1262
|
+
if (!bytesEqual(state.legacyFallbackFingerprint, fingerprint)) {
|
|
1263
|
+
state.legacyFallbackAmbiguous = true;
|
|
1264
|
+
}
|
|
1265
|
+
if (
|
|
1266
|
+
state.legacyFallbackTimestamp === undefined ||
|
|
1267
|
+
properties.transportTimestamp > state.legacyFallbackTimestamp
|
|
1268
|
+
) {
|
|
1269
|
+
state.legacyFallbackTimestamp = properties.transportTimestamp;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (state.phase !== "active") {
|
|
1273
|
+
if (state.requestParked) {
|
|
1274
|
+
this.transitionToResync(state, {
|
|
1275
|
+
force: true,
|
|
1276
|
+
refreshCapability: true,
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
if (state.legacyFallbackTimer) {
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
state.legacyFallbackTimer = setTimeout(() => {
|
|
1285
|
+
state.legacyFallbackTimer = undefined;
|
|
1286
|
+
if (this.isStateCurrent(state) && state.phase === "active") {
|
|
1287
|
+
this.transitionToResync(state, {
|
|
1288
|
+
force: true,
|
|
1289
|
+
refreshCapability: true,
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}, this.legacyFallbackDelayMs);
|
|
1293
|
+
state.legacyFallbackTimer.unref?.();
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
private clearLegacyFallback(state: ReplicationInfoV2ReceiveState): void {
|
|
1298
|
+
if (state.legacyFallbackTimer) {
|
|
1299
|
+
clearTimeout(state.legacyFallbackTimer);
|
|
1300
|
+
state.legacyFallbackTimer = undefined;
|
|
1301
|
+
}
|
|
1302
|
+
state.legacyFallbackFingerprint = undefined;
|
|
1303
|
+
state.legacyFallbackTimestamp = undefined;
|
|
1304
|
+
state.legacyFallbackAmbiguous = false;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
isAdmissionCurrent(admission: ReplicationInfoV2ReceiveAdmission): boolean {
|
|
1308
|
+
const { state } = admission;
|
|
1309
|
+
return (
|
|
1310
|
+
state.version === admission.version &&
|
|
1311
|
+
state.receiveEpoch === admission.receiveEpoch &&
|
|
1312
|
+
this.isStateCurrent(state)
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
commit(admission: ReplicationInfoV2ReceiveAdmission): boolean {
|
|
1317
|
+
if (admission.committed) {
|
|
1318
|
+
return this.isAdmissionCurrent(admission);
|
|
1319
|
+
}
|
|
1320
|
+
if (!this.isAdmissionCurrent(admission)) {
|
|
1321
|
+
this.release(admission);
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
const { state, message } = admission;
|
|
1325
|
+
if (admission.kind === "full") {
|
|
1326
|
+
if (state.senderEpoch === undefined) {
|
|
1327
|
+
state.senderEpoch = message.senderEpoch.slice();
|
|
1328
|
+
this._cutoverPeerSessions.add(state.peerSession);
|
|
1329
|
+
} else if (!bytesEqual(state.senderEpoch, message.senderEpoch)) {
|
|
1330
|
+
this.release(admission);
|
|
1331
|
+
return false;
|
|
1332
|
+
}
|
|
1333
|
+
} else if (
|
|
1334
|
+
state.senderEpoch === undefined ||
|
|
1335
|
+
!bytesEqual(state.senderEpoch, message.senderEpoch)
|
|
1336
|
+
) {
|
|
1337
|
+
this.release(admission);
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
state.lastSequence = message.sequence;
|
|
1341
|
+
state.phase = "active";
|
|
1342
|
+
state.requestAttempts = 0;
|
|
1343
|
+
state.requestsSinceCapabilityRefresh = 0;
|
|
1344
|
+
state.requestParked = false;
|
|
1345
|
+
state.capabilityRefreshRequired = false;
|
|
1346
|
+
state.lastCommittedTransportTimestamp = admission.transportTimestamp;
|
|
1347
|
+
state.recentCommittedPayloads.push({
|
|
1348
|
+
fingerprint: admission.payloadFingerprint.slice(),
|
|
1349
|
+
transportTimestamp: admission.transportTimestamp,
|
|
1350
|
+
});
|
|
1351
|
+
if (state.recentCommittedPayloads.length > 8) {
|
|
1352
|
+
state.recentCommittedPayloads.shift();
|
|
1353
|
+
}
|
|
1354
|
+
if (
|
|
1355
|
+
(state.legacyFallbackTimestamp !== undefined &&
|
|
1356
|
+
admission.transportTimestamp > state.legacyFallbackTimestamp) ||
|
|
1357
|
+
(!state.legacyFallbackAmbiguous &&
|
|
1358
|
+
state.legacyFallbackFingerprint !== undefined &&
|
|
1359
|
+
bytesEqual(
|
|
1360
|
+
state.legacyFallbackFingerprint,
|
|
1361
|
+
admission.payloadFingerprint,
|
|
1362
|
+
))
|
|
1363
|
+
) {
|
|
1364
|
+
this.clearLegacyFallback(state);
|
|
1365
|
+
}
|
|
1366
|
+
if (state.requestTimer) {
|
|
1367
|
+
clearTimeout(state.requestTimer);
|
|
1368
|
+
state.requestTimer = undefined;
|
|
1369
|
+
}
|
|
1370
|
+
state.version++;
|
|
1371
|
+
admission.version = state.version;
|
|
1372
|
+
admission.receiveEpoch = state.receiveEpoch;
|
|
1373
|
+
admission.committed = true;
|
|
1374
|
+
this.release(admission);
|
|
1375
|
+
return true;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
requireFullAfterFailure(
|
|
1379
|
+
admission: ReplicationInfoV2ReceiveAdmission,
|
|
1380
|
+
): boolean {
|
|
1381
|
+
if (!this.isAdmissionCurrent(admission)) {
|
|
1382
|
+
this.release(admission);
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
this.release(admission);
|
|
1386
|
+
this.transitionToResync(admission.state, { force: true });
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
private isStateCurrent(state: ReplicationInfoV2ReceiveState): boolean {
|
|
1391
|
+
return (
|
|
1392
|
+
this._receiveStates.get(state.peerHash) === state &&
|
|
1393
|
+
!state.controller.signal.aborted &&
|
|
1394
|
+
!this.deps.isClosed() &&
|
|
1395
|
+
state.receiverTransportSession !== undefined &&
|
|
1396
|
+
this.deps.getReceiverTransportSession() ===
|
|
1397
|
+
state.receiverTransportSession &&
|
|
1398
|
+
this.deps.isSenderTransportSessionCurrent(
|
|
1399
|
+
state.peerHash,
|
|
1400
|
+
state.senderTransportSession,
|
|
1401
|
+
) &&
|
|
1402
|
+
this.deps.isPeerStateCurrent(
|
|
1403
|
+
state.peerHash,
|
|
1404
|
+
state.peerSession,
|
|
1405
|
+
state.receiveEpoch,
|
|
1406
|
+
)
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
private armRequest(
|
|
1411
|
+
state: ReplicationInfoV2ReceiveState,
|
|
1412
|
+
delayMs: number,
|
|
1413
|
+
): void {
|
|
1414
|
+
if (state.requestTimer) {
|
|
1415
|
+
clearTimeout(state.requestTimer);
|
|
1416
|
+
}
|
|
1417
|
+
if (
|
|
1418
|
+
this._receiveStates.get(state.peerHash) !== state ||
|
|
1419
|
+
state.controller.signal.aborted ||
|
|
1420
|
+
this.deps.isClosed() ||
|
|
1421
|
+
(this._reservedAdmissionsByPeer.has(state.peerHash) &&
|
|
1422
|
+
this._reservedAdmissionsByPeer.get(state.peerHash)?.state !== state) ||
|
|
1423
|
+
state.receiverBinding === undefined ||
|
|
1424
|
+
state.phase === "active" ||
|
|
1425
|
+
state.requestParked ||
|
|
1426
|
+
state.lastSequence === MAX_U64
|
|
1427
|
+
) {
|
|
1428
|
+
state.requestTimer = undefined;
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
state.requestTimer = setTimeout(
|
|
1432
|
+
() => {
|
|
1433
|
+
state.requestTimer = undefined;
|
|
1434
|
+
void this.runRequest(state);
|
|
1435
|
+
},
|
|
1436
|
+
Math.max(0, delayMs),
|
|
1437
|
+
);
|
|
1438
|
+
state.requestTimer.unref?.();
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
private requestRetryDelay(state: ReplicationInfoV2ReceiveState): number {
|
|
1442
|
+
const exponent = Math.max(0, state.requestAttempts - 1);
|
|
1443
|
+
return Math.min(
|
|
1444
|
+
this.maxRequestRetryMs,
|
|
1445
|
+
this.requestRetryMs * 2 ** Math.min(exponent, 20),
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
private async refreshGrant(
|
|
1450
|
+
state: ReplicationInfoV2ReceiveState,
|
|
1451
|
+
): Promise<boolean> {
|
|
1452
|
+
const refreshed = await this.deps.refreshLocalCapability({
|
|
1453
|
+
peerHash: state.peerHash,
|
|
1454
|
+
target: state.target,
|
|
1455
|
+
peerSession: state.peerSession,
|
|
1456
|
+
receiveEpoch: state.receiveEpoch,
|
|
1457
|
+
signal: state.controller.signal,
|
|
1458
|
+
});
|
|
1459
|
+
if (
|
|
1460
|
+
!refreshed ||
|
|
1461
|
+
!this.isStateCurrent(state) ||
|
|
1462
|
+
this.deps.getReceiverTransportSession() !==
|
|
1463
|
+
refreshed.receiverTransportSession
|
|
1464
|
+
) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const ready: LocalCapabilityReady = {
|
|
1469
|
+
peerHash: state.peerHash,
|
|
1470
|
+
receiveEpoch: state.receiveEpoch,
|
|
1471
|
+
receiverTransportSession: refreshed.receiverTransportSession,
|
|
1472
|
+
requestNotBeforeMs: refreshed.requestNotBeforeMs,
|
|
1473
|
+
};
|
|
1474
|
+
this._localCapabilityReadyBySession.set(state.peerSession, ready);
|
|
1475
|
+
state.receiverRequestChallenge = randomBytes(32);
|
|
1476
|
+
state.senderEpoch = undefined;
|
|
1477
|
+
state.lastSequence = undefined;
|
|
1478
|
+
state.phase = "resync";
|
|
1479
|
+
state.capabilityRefreshRequired = false;
|
|
1480
|
+
state.requestsSinceCapabilityRefresh = 0;
|
|
1481
|
+
state.version++;
|
|
1482
|
+
this.bindLocalCapability(state, ready);
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
private async runRequest(
|
|
1487
|
+
state: ReplicationInfoV2ReceiveState,
|
|
1488
|
+
): Promise<void> {
|
|
1489
|
+
if (state.requestInFlight) {
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (!this.isStateCurrent(state)) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
if (
|
|
1496
|
+
state.phase === "active" ||
|
|
1497
|
+
state.requestParked ||
|
|
1498
|
+
state.lastSequence === MAX_U64
|
|
1499
|
+
) {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (state.requestAttempts >= this.requestMaxAttempts) {
|
|
1503
|
+
state.requestParked = true;
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
let operation: Promise<void>;
|
|
1508
|
+
operation = (async () => {
|
|
1509
|
+
if (state.capabilityRefreshRequired) {
|
|
1510
|
+
state.requestAttempts++;
|
|
1511
|
+
if (!(await this.refreshGrant(state))) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (!this.isStateCurrent(state)) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
const ready = this._localCapabilityReadyBySession.get(state.peerSession);
|
|
1519
|
+
if (
|
|
1520
|
+
!ready ||
|
|
1521
|
+
ready.peerHash !== state.peerHash ||
|
|
1522
|
+
ready.receiveEpoch !== state.receiveEpoch ||
|
|
1523
|
+
ready.receiverTransportSession !== state.receiverTransportSession
|
|
1524
|
+
) {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const now = this.now();
|
|
1528
|
+
if (now <= ready.requestNotBeforeMs) {
|
|
1529
|
+
this.armRequest(state, ready.requestNotBeforeMs - now + 1);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
if (state.requestAttempts >= this.requestMaxAttempts) {
|
|
1533
|
+
state.requestParked = true;
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
state.requestAttempts++;
|
|
1537
|
+
state.requestsSinceCapabilityRefresh++;
|
|
1538
|
+
const request = new RequestReplicationInfoV2Message({
|
|
1539
|
+
receiverChallenge: state.receiverRequestChallenge.slice(),
|
|
1540
|
+
intendedSender: state.target,
|
|
1541
|
+
senderSession: state.senderTransportSession,
|
|
1542
|
+
});
|
|
1543
|
+
await this.deps.sendRequest(
|
|
1544
|
+
request,
|
|
1545
|
+
state.target,
|
|
1546
|
+
state.controller.signal,
|
|
1547
|
+
);
|
|
1548
|
+
})()
|
|
1549
|
+
.catch((error) => {
|
|
1550
|
+
if (!state.controller.signal.aborted && !this.deps.isClosed()) {
|
|
1551
|
+
this.deps.onRequestError?.(error);
|
|
1552
|
+
}
|
|
1553
|
+
})
|
|
1554
|
+
.finally(() => {
|
|
1555
|
+
if (state.requestInFlight === operation) {
|
|
1556
|
+
state.requestInFlight = undefined;
|
|
1557
|
+
}
|
|
1558
|
+
if (
|
|
1559
|
+
this._receiveStates.get(state.peerHash) === state &&
|
|
1560
|
+
!state.controller.signal.aborted &&
|
|
1561
|
+
!state.requestTimer &&
|
|
1562
|
+
state.phase !== "active"
|
|
1563
|
+
) {
|
|
1564
|
+
if (state.requestsSinceCapabilityRefresh >= 3) {
|
|
1565
|
+
state.capabilityRefreshRequired = true;
|
|
1566
|
+
}
|
|
1567
|
+
if (state.requestAttempts >= this.requestMaxAttempts) {
|
|
1568
|
+
state.requestParked = true;
|
|
1569
|
+
} else {
|
|
1570
|
+
this.armRequest(state, this.requestRetryDelay(state));
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
state.requestInFlight = operation;
|
|
1575
|
+
await operation;
|
|
1576
|
+
}
|
|
1577
|
+
}
|