@peerbit/shared-log 13.2.30 → 13.2.32
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/exchange-heads.d.ts +20 -3
- package/dist/src/exchange-heads.d.ts.map +1 -1
- package/dist/src/exchange-heads.js +20 -3
- package/dist/src/exchange-heads.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +200 -31
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-announcement.d.ts +3 -1
- package/dist/src/replication-announcement.d.ts.map +1 -1
- package/dist/src/replication-announcement.js +1 -0
- package/dist/src/replication-announcement.js.map +1 -1
- package/dist/src/replication-info-v2-send.d.ts +107 -0
- package/dist/src/replication-info-v2-send.d.ts.map +1 -0
- package/dist/src/replication-info-v2-send.js +415 -0
- package/dist/src/replication-info-v2-send.js.map +1 -0
- package/dist/src/replication.d.ts +66 -0
- package/dist/src/replication.d.ts.map +1 -1
- package/dist/src/replication.js +219 -2
- package/dist/src/replication.js.map +1 -1
- package/package.json +9 -9
- package/src/exchange-heads.ts +23 -3
- package/src/index.ts +255 -34
- package/src/replication-announcement.ts +10 -4
- package/src/replication-info-v2-send.ts +568 -0
- package/src/replication.ts +138 -1
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { serialize } from "@dao-xyz/borsh";
|
|
2
|
+
import { type PublicSignKey, randomBytes, sha256Sync } from "@peerbit/crypto";
|
|
3
|
+
import { logger as loggerFn } from "@peerbit/logger";
|
|
4
|
+
import type { RPC } from "@peerbit/rpc";
|
|
5
|
+
import {
|
|
6
|
+
AcknowledgeDelivery,
|
|
7
|
+
CONVERGENCE_MESSAGE_PRIORITY,
|
|
8
|
+
} from "@peerbit/stream-interface";
|
|
9
|
+
import type { TransportMessage } from "./message.js";
|
|
10
|
+
import type { ReplicationRangeIndexable } from "./ranges.js";
|
|
11
|
+
import { concat, fromString } from "uint8arrays";
|
|
12
|
+
import {
|
|
13
|
+
AddedReplicationInfoV2Message,
|
|
14
|
+
AddedReplicationSegmentMessage,
|
|
15
|
+
AllReplicatingSegmentsMessage,
|
|
16
|
+
FullReplicationInfoV2Message,
|
|
17
|
+
RequestReplicationInfoV2Message,
|
|
18
|
+
StoppedReplicating,
|
|
19
|
+
StoppedReplicationInfoV2Message,
|
|
20
|
+
} from "./replication.js";
|
|
21
|
+
|
|
22
|
+
const logger = loggerFn("peerbit:shared-log:replication-info-v2-send");
|
|
23
|
+
|
|
24
|
+
const MAX_U64 = (1n << 64n) - 1n;
|
|
25
|
+
const RECEIVER_BINDING_DOMAIN = fromString(
|
|
26
|
+
"peerbit/shared-log/replication-info-v2/receiver-binding/v1",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const lengthPrefixed = (bytes: Uint8Array): Uint8Array => {
|
|
30
|
+
const length = new Uint8Array(4);
|
|
31
|
+
new DataView(length.buffer).setUint32(0, bytes.byteLength, true);
|
|
32
|
+
return concat([length, bytes]);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const u64LittleEndian = (value: bigint): Uint8Array => {
|
|
36
|
+
const bytes = new Uint8Array(8);
|
|
37
|
+
new DataView(bytes.buffer).setBigUint64(0, value, true);
|
|
38
|
+
return bytes;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Derive the token echoed by V2 data frames. Delivery recipients are unsigned,
|
|
43
|
+
* so the receiver's nonce alone is not a destination binding. Folding both
|
|
44
|
+
* authenticated identities and signed transport sessions into the 32-byte
|
|
45
|
+
* field prevents a copied request nonce from creating an interchangeable
|
|
46
|
+
* stream for another receiver.
|
|
47
|
+
*/
|
|
48
|
+
export const deriveReplicationInfoV2ReceiverBinding = (properties: {
|
|
49
|
+
receiverChallenge: Uint8Array;
|
|
50
|
+
receiver: PublicSignKey;
|
|
51
|
+
receiverTransportSession: bigint;
|
|
52
|
+
sender: PublicSignKey;
|
|
53
|
+
senderTransportSession: bigint;
|
|
54
|
+
}): Uint8Array =>
|
|
55
|
+
sha256Sync(
|
|
56
|
+
concat([
|
|
57
|
+
lengthPrefixed(RECEIVER_BINDING_DOMAIN),
|
|
58
|
+
lengthPrefixed(properties.receiverChallenge),
|
|
59
|
+
lengthPrefixed(serialize(properties.receiver)),
|
|
60
|
+
u64LittleEndian(properties.receiverTransportSession),
|
|
61
|
+
lengthPrefixed(serialize(properties.sender)),
|
|
62
|
+
u64LittleEndian(properties.senderTransportSession),
|
|
63
|
+
]),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export type LegacyReplicationInfoMessage =
|
|
67
|
+
| AllReplicatingSegmentsMessage
|
|
68
|
+
| AddedReplicationSegmentMessage
|
|
69
|
+
| StoppedReplicating;
|
|
70
|
+
|
|
71
|
+
type SendRequest =
|
|
72
|
+
| { kind: "snapshot" }
|
|
73
|
+
| { kind: "message"; message: LegacyReplicationInfoMessage };
|
|
74
|
+
|
|
75
|
+
export type ReplicationInfoV2SendState = {
|
|
76
|
+
peerHash: string;
|
|
77
|
+
target: PublicSignKey;
|
|
78
|
+
peerSession: object;
|
|
79
|
+
receiverTransportSession: bigint;
|
|
80
|
+
senderTransportSession: bigint;
|
|
81
|
+
lastRequestTimestamp: bigint;
|
|
82
|
+
receiverRequestChallenge: Uint8Array;
|
|
83
|
+
receiverChallenge: Uint8Array;
|
|
84
|
+
senderEpoch: Uint8Array;
|
|
85
|
+
ownershipLifecycleController: AbortController;
|
|
86
|
+
nextSequence: bigint;
|
|
87
|
+
established: boolean;
|
|
88
|
+
suspended: boolean;
|
|
89
|
+
inFlightSequence?: bigint;
|
|
90
|
+
controller: AbortController;
|
|
91
|
+
pending?: SendRequest;
|
|
92
|
+
worker?: Promise<void>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type ReplicationInfoV2SendDeps<R extends "u32" | "u64"> = {
|
|
96
|
+
getRpc: () => RPC<TransportMessage, TransportMessage>;
|
|
97
|
+
getSelfKey: () => PublicSignKey;
|
|
98
|
+
getSenderTransportSession: () => bigint;
|
|
99
|
+
getMyReplicationSegments: () => Promise<ReplicationRangeIndexable<R>[]>;
|
|
100
|
+
validatePersistedReplicationRangeSnapshot: (
|
|
101
|
+
ranges: readonly { mode: unknown }[],
|
|
102
|
+
) => void;
|
|
103
|
+
isClosed: () => boolean;
|
|
104
|
+
isPeerSessionOpen: (peerHash: string, peerSession: object) => boolean;
|
|
105
|
+
captureReplicationOwnershipLifecycle: () => AbortController;
|
|
106
|
+
isReplicationOwnershipLifecycleActive: (
|
|
107
|
+
controller: AbortController,
|
|
108
|
+
) => boolean;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const bytesEqual = (left: Uint8Array, right: Uint8Array): boolean => {
|
|
112
|
+
if (left.byteLength !== right.byteLength) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
for (let index = 0; index < left.byteLength; index++) {
|
|
116
|
+
if (left[index] !== right[index]) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Per-destination V2 sender streams. Each stream retains at most one in-flight
|
|
125
|
+
* operation and one pending operation. A second pending mutation coalesces to
|
|
126
|
+
* a freshly collected authoritative snapshot, bounding memory at O(peers).
|
|
127
|
+
*/
|
|
128
|
+
export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
|
|
129
|
+
_senderEpoch!: Uint8Array;
|
|
130
|
+
_sendStates!: Map<string, ReplicationInfoV2SendState>;
|
|
131
|
+
_spentPeerSessions!: WeakSet<object>;
|
|
132
|
+
_retiringWorkersByPeer!: Map<string, Promise<void>>;
|
|
133
|
+
|
|
134
|
+
constructor(private readonly deps: ReplicationInfoV2SendDeps<R>) {
|
|
135
|
+
this._senderEpoch = randomBytes(32);
|
|
136
|
+
this._sendStates = new Map();
|
|
137
|
+
this._spentPeerSessions = new WeakSet();
|
|
138
|
+
this._retiringWorkersByPeer = new Map();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
resetForOpen(): void {
|
|
142
|
+
this.clearForClose();
|
|
143
|
+
this._senderEpoch = randomBytes(32);
|
|
144
|
+
this._sendStates = new Map();
|
|
145
|
+
this._spentPeerSessions = new WeakSet();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
clearForClose(): void {
|
|
149
|
+
for (const state of [...(this._sendStates?.values() ?? [])]) {
|
|
150
|
+
this.clearState(state);
|
|
151
|
+
}
|
|
152
|
+
this._sendStates?.clear();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
clearPeer(peerHash: string, expectedSession?: object): void {
|
|
156
|
+
const state = this._sendStates.get(peerHash);
|
|
157
|
+
if (!state || (expectedSession && state.peerSession !== expectedSession)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.clearState(state);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
advancePeerCapability(peerHash: string): void {
|
|
164
|
+
const state = this._sendStates.get(peerHash);
|
|
165
|
+
if (state) {
|
|
166
|
+
this.clearState(state);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private clearState(state: ReplicationInfoV2SendState): void {
|
|
171
|
+
this.trackRetiringWorker(state);
|
|
172
|
+
state.controller.abort();
|
|
173
|
+
if (this._sendStates.get(state.peerHash) === state) {
|
|
174
|
+
this._sendStates.delete(state.peerHash);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private trackRetiringWorker(state: ReplicationInfoV2SendState): void {
|
|
179
|
+
const worker = state.worker;
|
|
180
|
+
if (!worker) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const previous = this._retiringWorkersByPeer.get(state.peerHash);
|
|
184
|
+
if (previous === worker) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const retirement = previous
|
|
188
|
+
? Promise.allSettled([previous, worker]).then(() => undefined)
|
|
189
|
+
: worker;
|
|
190
|
+
this._retiringWorkersByPeer.set(state.peerHash, retirement);
|
|
191
|
+
const forget = () => {
|
|
192
|
+
if (this._retiringWorkersByPeer.get(state.peerHash) === retirement) {
|
|
193
|
+
this._retiringWorkersByPeer.delete(state.peerHash);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
void retirement.then(forget, forget);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private isDestinationCurrent(state: ReplicationInfoV2SendState): boolean {
|
|
200
|
+
return (
|
|
201
|
+
!this.deps.isClosed() &&
|
|
202
|
+
!state.controller.signal.aborted &&
|
|
203
|
+
this._sendStates.get(state.peerHash) === state &&
|
|
204
|
+
this.deps.isPeerSessionOpen(state.peerHash, state.peerSession) &&
|
|
205
|
+
this.deps.getSenderTransportSession() === state.senderTransportSession
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private isCurrent(state: ReplicationInfoV2SendState): boolean {
|
|
210
|
+
return (
|
|
211
|
+
this.isDestinationCurrent(state) &&
|
|
212
|
+
this.deps.isReplicationOwnershipLifecycleActive(
|
|
213
|
+
state.ownershipLifecycleController,
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Accept a signed receiver request. An exact newer retry asks for another
|
|
220
|
+
* full snapshot without resetting the epoch/sequence. A different challenge
|
|
221
|
+
* cannot replace the first binding within one PeerSession; this prevents a
|
|
222
|
+
* signed request flood from spawning unbounded orphan snapshot reads.
|
|
223
|
+
*/
|
|
224
|
+
acceptRequest(
|
|
225
|
+
request: RequestReplicationInfoV2Message,
|
|
226
|
+
properties: {
|
|
227
|
+
from: PublicSignKey;
|
|
228
|
+
peerSession: object;
|
|
229
|
+
receiverTransportSession: bigint;
|
|
230
|
+
requestTimestamp: bigint;
|
|
231
|
+
},
|
|
232
|
+
): boolean {
|
|
233
|
+
const self = this.deps.getSelfKey();
|
|
234
|
+
const senderTransportSession = this.deps.getSenderTransportSession();
|
|
235
|
+
if (
|
|
236
|
+
properties.from.equals(self) ||
|
|
237
|
+
!request.intendedSender.equals(self) ||
|
|
238
|
+
request.senderSession !== senderTransportSession
|
|
239
|
+
) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const peerHash = properties.from.hashcode();
|
|
244
|
+
if (
|
|
245
|
+
this._retiringWorkersByPeer.has(peerHash) ||
|
|
246
|
+
this._spentPeerSessions.has(properties.peerSession) ||
|
|
247
|
+
!this.deps.isPeerSessionOpen(peerHash, properties.peerSession)
|
|
248
|
+
) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let previous = this._sendStates.get(peerHash);
|
|
253
|
+
if (
|
|
254
|
+
previous &&
|
|
255
|
+
(previous.peerSession !== properties.peerSession ||
|
|
256
|
+
previous.senderTransportSession !== senderTransportSession)
|
|
257
|
+
) {
|
|
258
|
+
this.clearState(previous);
|
|
259
|
+
previous = undefined;
|
|
260
|
+
if (this._retiringWorkersByPeer.has(peerHash)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (previous) {
|
|
265
|
+
const sameBinding =
|
|
266
|
+
previous.peerSession === properties.peerSession &&
|
|
267
|
+
previous.receiverTransportSession ===
|
|
268
|
+
properties.receiverTransportSession &&
|
|
269
|
+
bytesEqual(
|
|
270
|
+
previous.receiverRequestChallenge,
|
|
271
|
+
request.receiverChallenge,
|
|
272
|
+
);
|
|
273
|
+
if (sameBinding) {
|
|
274
|
+
if (properties.requestTimestamp <= previous.lastRequestTimestamp) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
previous.lastRequestTimestamp = properties.requestTimestamp;
|
|
278
|
+
previous.suspended = false;
|
|
279
|
+
this.enqueueState(previous, { kind: "snapshot" });
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const ownershipLifecycleController =
|
|
287
|
+
this.deps.captureReplicationOwnershipLifecycle();
|
|
288
|
+
const state: ReplicationInfoV2SendState = {
|
|
289
|
+
peerHash,
|
|
290
|
+
target: properties.from,
|
|
291
|
+
peerSession: properties.peerSession,
|
|
292
|
+
receiverTransportSession: properties.receiverTransportSession,
|
|
293
|
+
senderTransportSession,
|
|
294
|
+
lastRequestTimestamp: properties.requestTimestamp,
|
|
295
|
+
receiverRequestChallenge: request.receiverChallenge.slice(),
|
|
296
|
+
receiverChallenge: deriveReplicationInfoV2ReceiverBinding({
|
|
297
|
+
receiverChallenge: request.receiverChallenge,
|
|
298
|
+
receiver: properties.from,
|
|
299
|
+
receiverTransportSession: properties.receiverTransportSession,
|
|
300
|
+
sender: self,
|
|
301
|
+
senderTransportSession,
|
|
302
|
+
}),
|
|
303
|
+
senderEpoch: this._senderEpoch.slice(),
|
|
304
|
+
nextSequence: 1n,
|
|
305
|
+
established: false,
|
|
306
|
+
suspended: false,
|
|
307
|
+
controller: new AbortController(),
|
|
308
|
+
ownershipLifecycleController,
|
|
309
|
+
};
|
|
310
|
+
this._sendStates.set(peerHash, state);
|
|
311
|
+
this.enqueueState(state, { kind: "snapshot" });
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
enqueue(message: LegacyReplicationInfoMessage): void {
|
|
316
|
+
for (const state of [...this._sendStates.values()]) {
|
|
317
|
+
this.enqueueState(state, { kind: "message", message });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
enqueueSnapshotForPeer(peerHash: string): void {
|
|
322
|
+
const state = this._sendStates.get(peerHash);
|
|
323
|
+
if (state) {
|
|
324
|
+
this.enqueueState(state, { kind: "snapshot" });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private enqueueState(
|
|
329
|
+
state: ReplicationInfoV2SendState,
|
|
330
|
+
request: SendRequest,
|
|
331
|
+
): void {
|
|
332
|
+
if (!this.isCurrent(state)) {
|
|
333
|
+
state.pending = undefined;
|
|
334
|
+
if (
|
|
335
|
+
!this.isDestinationCurrent(state) ||
|
|
336
|
+
this.deps.isReplicationOwnershipLifecycleActive(
|
|
337
|
+
state.ownershipLifecycleController,
|
|
338
|
+
)
|
|
339
|
+
) {
|
|
340
|
+
this.clearState(state);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (state.suspended) {
|
|
345
|
+
state.pending = undefined;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!state.worker) {
|
|
350
|
+
state.pending = request;
|
|
351
|
+
let worker: Promise<void>;
|
|
352
|
+
worker = Promise.resolve()
|
|
353
|
+
.then(() => this.runWorker(state))
|
|
354
|
+
.catch((error) => {
|
|
355
|
+
const ownershipActive =
|
|
356
|
+
this.deps.isReplicationOwnershipLifecycleActive(
|
|
357
|
+
state.ownershipLifecycleController,
|
|
358
|
+
);
|
|
359
|
+
if (
|
|
360
|
+
ownershipActive &&
|
|
361
|
+
!state.controller.signal.aborted &&
|
|
362
|
+
!this.deps.isClosed()
|
|
363
|
+
) {
|
|
364
|
+
logger.trace(
|
|
365
|
+
"Replication-info V2 destination stream failed for %s: %s",
|
|
366
|
+
state.peerHash,
|
|
367
|
+
(error as Error)?.message ?? String(error),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
state.pending = undefined;
|
|
371
|
+
const ordinaryFailure =
|
|
372
|
+
ownershipActive && this.isDestinationCurrent(state);
|
|
373
|
+
if (ordinaryFailure) {
|
|
374
|
+
if (state.inFlightSequence !== undefined) {
|
|
375
|
+
state.inFlightSequence = undefined;
|
|
376
|
+
if (state.nextSequence > MAX_U64) {
|
|
377
|
+
this._spentPeerSessions.add(state.peerSession);
|
|
378
|
+
this.clearState(state);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// A delivery error is ambiguous: the receiver may already have
|
|
383
|
+
// applied this sequence. Retain the exact grant, stop ordinary
|
|
384
|
+
// deltas, and require a newer same-challenge request to resume
|
|
385
|
+
// with an authoritative Full at the next safe sequence.
|
|
386
|
+
state.suspended = true;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (!this.isDestinationCurrent(state)) {
|
|
390
|
+
this.clearState(state);
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
.finally(() => {
|
|
394
|
+
if (state.worker === worker) {
|
|
395
|
+
state.worker = undefined;
|
|
396
|
+
// An enqueue can land after runWorker observes an empty slot but
|
|
397
|
+
// before this promise reaction clears `worker`. Re-arm that item
|
|
398
|
+
// here so the one-slot bound cannot become a stranded queue.
|
|
399
|
+
const pending = state.pending;
|
|
400
|
+
if (pending && this.isCurrent(state)) {
|
|
401
|
+
state.pending = undefined;
|
|
402
|
+
this.enqueueState(state, pending);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
state.worker = worker;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!state.pending) {
|
|
411
|
+
state.pending = request;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// One pending item is the hard bound. Once another mutation arrives,
|
|
416
|
+
// replace the pending delta with a current authoritative snapshot.
|
|
417
|
+
state.pending = { kind: "snapshot" };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private async createMessage(
|
|
421
|
+
state: ReplicationInfoV2SendState,
|
|
422
|
+
request: SendRequest,
|
|
423
|
+
): Promise<
|
|
424
|
+
| FullReplicationInfoV2Message
|
|
425
|
+
| AddedReplicationInfoV2Message
|
|
426
|
+
| StoppedReplicationInfoV2Message
|
|
427
|
+
> {
|
|
428
|
+
const common = {
|
|
429
|
+
receiverChallenge: state.receiverChallenge.slice(),
|
|
430
|
+
senderEpoch: state.senderEpoch.slice(),
|
|
431
|
+
sequence: state.nextSequence,
|
|
432
|
+
};
|
|
433
|
+
if (request.kind === "snapshot") {
|
|
434
|
+
const segments = (await this.deps.getMyReplicationSegments()).map(
|
|
435
|
+
(range) => range.toReplicationRange(),
|
|
436
|
+
);
|
|
437
|
+
this.deps.validatePersistedReplicationRangeSnapshot(segments);
|
|
438
|
+
return new FullReplicationInfoV2Message({ ...common, segments });
|
|
439
|
+
}
|
|
440
|
+
if (request.message instanceof AllReplicatingSegmentsMessage) {
|
|
441
|
+
const segments = request.message.segments;
|
|
442
|
+
this.deps.validatePersistedReplicationRangeSnapshot(segments);
|
|
443
|
+
return new FullReplicationInfoV2Message({ ...common, segments });
|
|
444
|
+
}
|
|
445
|
+
if (request.message instanceof AddedReplicationSegmentMessage) {
|
|
446
|
+
return new AddedReplicationInfoV2Message({
|
|
447
|
+
...common,
|
|
448
|
+
segments: request.message.segments,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return new StoppedReplicationInfoV2Message({
|
|
452
|
+
...common,
|
|
453
|
+
segmentIds: request.message.segmentIds,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private async runWorker(state: ReplicationInfoV2SendState): Promise<void> {
|
|
458
|
+
while (this.isCurrent(state)) {
|
|
459
|
+
const request = state.pending;
|
|
460
|
+
if (!request) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
state.pending = undefined;
|
|
464
|
+
if (state.nextSequence > MAX_U64) {
|
|
465
|
+
this.clearState(state);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const message = await this.createMessage(state, request);
|
|
470
|
+
if (!this.isCurrent(state)) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// Consume the sequence before the transport attempt. From this point on
|
|
474
|
+
// delivery is ambiguous even if ownership aborts before the await
|
|
475
|
+
// continuation runs, so `nextSequence` always remains the next value that
|
|
476
|
+
// has never been attempted with different content.
|
|
477
|
+
state.inFlightSequence = state.nextSequence;
|
|
478
|
+
state.nextSequence += 1n;
|
|
479
|
+
await this.deps.getRpc().send(message, {
|
|
480
|
+
mode: new AcknowledgeDelivery({
|
|
481
|
+
to: [state.target],
|
|
482
|
+
redundancy: 1,
|
|
483
|
+
}),
|
|
484
|
+
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
485
|
+
signal: AbortSignal.any([
|
|
486
|
+
state.controller.signal,
|
|
487
|
+
state.ownershipLifecycleController.signal,
|
|
488
|
+
]),
|
|
489
|
+
});
|
|
490
|
+
state.inFlightSequence = undefined;
|
|
491
|
+
if (!this.isCurrent(state)) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
state.established = true;
|
|
495
|
+
if (state.nextSequence > MAX_U64) {
|
|
496
|
+
this._spentPeerSessions.add(state.peerSession);
|
|
497
|
+
this.clearState(state);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Terminal close/drop runs after the normal ownership generation is aborted.
|
|
505
|
+
* Send an authoritative empty Full directly from the retained destination
|
|
506
|
+
* bindings. `nextSequence` is consumed before every transport attempt, so it
|
|
507
|
+
* is always safe for different terminal content even if a just-aborted frame
|
|
508
|
+
* was delivered. Receivers admit Full gaps as authoritative resynchronization.
|
|
509
|
+
*/
|
|
510
|
+
async sendTerminalReset(signal: AbortSignal): Promise<void> {
|
|
511
|
+
const sends: Promise<unknown>[] = [];
|
|
512
|
+
for (const state of [...this._sendStates.values()]) {
|
|
513
|
+
if (
|
|
514
|
+
signal.aborted ||
|
|
515
|
+
!this.isDestinationCurrent(state) ||
|
|
516
|
+
state.nextSequence > MAX_U64
|
|
517
|
+
) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
state.pending = undefined;
|
|
521
|
+
const message = new FullReplicationInfoV2Message({
|
|
522
|
+
receiverChallenge: state.receiverChallenge.slice(),
|
|
523
|
+
senderEpoch: state.senderEpoch.slice(),
|
|
524
|
+
sequence: state.nextSequence,
|
|
525
|
+
segments: [],
|
|
526
|
+
});
|
|
527
|
+
sends.push(
|
|
528
|
+
this.deps.getRpc().send(message, {
|
|
529
|
+
mode: new AcknowledgeDelivery({
|
|
530
|
+
to: [state.target],
|
|
531
|
+
redundancy: 1,
|
|
532
|
+
}),
|
|
533
|
+
priority: CONVERGENCE_MESSAGE_PRIORITY,
|
|
534
|
+
signal,
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
await Promise.allSettled(sends);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async drain(signal?: AbortSignal): Promise<void> {
|
|
542
|
+
while (!signal?.aborted) {
|
|
543
|
+
const workers = [
|
|
544
|
+
...[...this._sendStates.values()]
|
|
545
|
+
.map((state) => state.worker)
|
|
546
|
+
.filter((worker): worker is Promise<void> => worker != null),
|
|
547
|
+
...this._retiringWorkersByPeer.values(),
|
|
548
|
+
];
|
|
549
|
+
const uniqueWorkers = [...new Set(workers)];
|
|
550
|
+
if (uniqueWorkers.length === 0) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const settled = Promise.allSettled(uniqueWorkers).then(() => undefined);
|
|
554
|
+
if (!signal) {
|
|
555
|
+
await settled;
|
|
556
|
+
} else {
|
|
557
|
+
await new Promise<void>((resolve) => {
|
|
558
|
+
const onAbort = () => resolve();
|
|
559
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
560
|
+
void settled.then(() => {
|
|
561
|
+
signal.removeEventListener("abort", onAbort);
|
|
562
|
+
resolve();
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|