@peerbit/shared-log 16.0.12 → 16.0.14

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.
Files changed (32) hide show
  1. package/README.md +14 -0
  2. package/dist/src/exchange-heads.d.ts +6 -0
  3. package/dist/src/exchange-heads.d.ts.map +1 -1
  4. package/dist/src/exchange-heads.js +6 -0
  5. package/dist/src/exchange-heads.js.map +1 -1
  6. package/dist/src/index.d.ts +13 -1
  7. package/dist/src/index.d.ts.map +1 -1
  8. package/dist/src/index.js +178 -33
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/receive-signature-verification.d.ts +9 -0
  11. package/dist/src/receive-signature-verification.d.ts.map +1 -0
  12. package/dist/src/receive-signature-verification.js +54 -0
  13. package/dist/src/receive-signature-verification.js.map +1 -0
  14. package/dist/src/replication-info-v2-receive.d.ts +12 -1
  15. package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
  16. package/dist/src/replication-info-v2-receive.js +30 -1
  17. package/dist/src/replication-info-v2-receive.js.map +1 -1
  18. package/dist/src/replication-info-v2-send.d.ts +61 -1
  19. package/dist/src/replication-info-v2-send.d.ts.map +1 -1
  20. package/dist/src/replication-info-v2-send.js +335 -9
  21. package/dist/src/replication-info-v2-send.js.map +1 -1
  22. package/dist/src/replication.d.ts +31 -0
  23. package/dist/src/replication.d.ts.map +1 -1
  24. package/dist/src/replication.js +115 -0
  25. package/dist/src/replication.js.map +1 -1
  26. package/package.json +17 -15
  27. package/src/exchange-heads.ts +7 -0
  28. package/src/index.ts +240 -33
  29. package/src/receive-signature-verification.ts +66 -0
  30. package/src/replication-info-v2-receive.ts +42 -0
  31. package/src/replication-info-v2-send.ts +452 -11
  32. package/src/replication.ts +69 -0
package/src/index.ts CHANGED
@@ -164,6 +164,7 @@ import {
164
164
  ResponseIPruneV2,
165
165
  SYNC_CAPABILITY_RAW_EXCHANGE_HEADS,
166
166
  SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY,
167
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM,
167
168
  SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE,
168
169
  SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND,
169
170
  StashBackedRawExchangeHeadsMessage,
@@ -231,6 +232,11 @@ import {
231
232
  shouldAssigneToRangeBoundary as shouldAssignToRangeBoundary,
232
233
  toRebalance,
233
234
  } from "./ranges.js";
235
+ import {
236
+ type ReceiveSignatureVerificationFact,
237
+ receiveSignatureVerificationResult as getReceiveSignatureVerificationResult,
238
+ withReceiveSignatureVerificationFacts as withSignatureVerificationFacts,
239
+ } from "./receive-signature-verification.js";
234
240
  import { ReplicationAnnouncementCoordinator } from "./replication-announcement.js";
235
241
  import {
236
242
  type ReplicationDomainHash,
@@ -251,7 +257,10 @@ import type {
251
257
  FullReplicationInfoMutation,
252
258
  } from "./replication-info-mutation.js";
253
259
  import { ReplicationInfoV2ReceiveCoordinator } from "./replication-info-v2-receive.js";
254
- import { ReplicationInfoV2SendCoordinator } from "./replication-info-v2-send.js";
260
+ import {
261
+ type ReplicationInfoV2ConfirmationOptions,
262
+ ReplicationInfoV2SendCoordinator,
263
+ } from "./replication-info-v2-send.js";
255
264
  import {
256
265
  type ReplicationStatus,
257
266
  classifyReplicationStatus,
@@ -264,10 +273,12 @@ import {
264
273
  FullReplicationInfoV2Message,
265
274
  MinReplicas,
266
275
  ReplicationError,
276
+ ReplicationInfoV2AppliedMessage,
267
277
  type ReplicationInfoV2Message,
268
278
  type ReplicationLimits,
269
279
  ReplicationPingMessage,
270
280
  RequestReplicationInfoMessage,
281
+ RequestReplicationInfoV2AppliedMessage,
271
282
  RequestReplicationInfoV2Message,
272
283
  ResponseRoleMessage,
273
284
  StoppedReplicating,
@@ -487,25 +498,36 @@ const joinNativeCoordinateDirectory = (
487
498
  `${nodeDirectory.replace(/[/\\]+$/, "")}/coordinates/${fsSafeLogId}`;
488
499
 
489
500
  type DurableBlockSublevelStore = {
501
+ readonly supportsCrashSafeJournalCheckpoint?: boolean;
490
502
  sublevel(
491
503
  name: string,
492
504
  options?: {
493
505
  compactOnClose?: boolean;
494
506
  compactOnCloseMinJournalBytes?: number;
507
+ compactMaxJournalBytes?: number;
495
508
  durability?: "normal" | "strict";
496
509
  },
497
510
  ): MaybePromise<AnyStore>;
498
511
  };
499
512
 
513
+ const defaultNativeEntryBlockCompactMaxJournalBytes = 64 * 1024 * 1024;
514
+
500
515
  const createNativeDurableBlockStore = async (
501
516
  storage: DurableBlockSublevelStore,
502
517
  ): Promise<AnyBlockStore> =>
503
518
  new AnyBlockStore(
504
519
  await storage.sublevel("blocks", {
505
- // Strict mirrors remain WAL-backed across close. The available snapshot
506
- // rewrite is not a crash-atomic generation protocol, so thresholds must
507
- // not re-enable it behind this acknowledgement boundary.
520
+ // Strict mirrors remain WAL-backed. On POSIX Node, checkpoint only the
521
+ // historical suffix through the Rust store's fsync + atomic-rename path;
522
+ // browsers/custom backends and Windows retain the prior unbounded WAL until
523
+ // they expose an equally strong directory durability barrier.
508
524
  compactOnClose: false,
525
+ ...(storage.supportsCrashSafeJournalCheckpoint === true
526
+ ? {
527
+ compactMaxJournalBytes:
528
+ defaultNativeEntryBlockCompactMaxJournalBytes,
529
+ }
530
+ : {}),
509
531
  // A native append is acknowledged only after this mirror resolves. The
510
532
  // Rust store's normal immutable fast path may resolve before its WAL write;
511
533
  // strict mode waits for the journal write and sync, closing the SIGKILL gap.
@@ -1065,6 +1087,10 @@ export type ReplicationOptions<R extends "u32" | "u64" = any> =
1065
1087
  | NewReplicationOptions<R>
1066
1088
  | ExistingReplicationOptions<R>;
1067
1089
 
1090
+ /** Opt-in proof that a local replication-role update applied remotely. */
1091
+ export type ReplicationConfirmationOptions =
1092
+ ReplicationInfoV2ConfirmationOptions;
1093
+
1068
1094
  export { BlocksMessage };
1069
1095
 
1070
1096
  const isAdaptiveReplicatorOption = (
@@ -2179,6 +2205,16 @@ export class SharedLog<
2179
2205
  private _logProperties?: LogProperties<T> &
2180
2206
  LogEvents<T> &
2181
2207
  SharedLogOptions<T, D, R>;
2208
+ // A successful native signature batch is only an authorization input for the
2209
+ // receive operation that requested it. Facts retain the exact verified bytes
2210
+ // and are leased around the lower-log join; byte comparison prevents an
2211
+ // earlier application callback from mutating a later entry underneath the
2212
+ // batch result. The array form also makes overlapping leases for the same
2213
+ // object safe without making verification sticky across retries.
2214
+ private _receiveSignatureVerificationFacts = new WeakMap<
2215
+ Entry<T>,
2216
+ ReceiveSignatureVerificationFact<T>[]
2217
+ >();
2182
2218
  private _closeController!: AbortController;
2183
2219
  private _respondToIHaveTimeout!: any;
2184
2220
  private _checkedPrune!: CheckedPruneCoordinator<T, R>;
@@ -3561,6 +3597,12 @@ export class SharedLog<
3561
3597
  this.captureReplicationOwnershipLifecycle(),
3562
3598
  isReplicationOwnershipLifecycleActive: (controller) =>
3563
3599
  this.isRepairLifecycleActive(controller),
3600
+ supportsApplicationConfirmation: (peerHash, receiverTransportSession) =>
3601
+ this._peerSyncCapabilitySessions.get(peerHash) ===
3602
+ receiverTransportSession &&
3603
+ ((this._peerSyncCapabilities.get(peerHash) ?? 0) &
3604
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM) !==
3605
+ 0,
3564
3606
  });
3565
3607
  }
3566
3608
 
@@ -3569,6 +3611,7 @@ export class SharedLog<
3569
3611
  SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE |
3570
3612
  SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND |
3571
3613
  SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY |
3614
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM |
3572
3615
  (this._logProperties?.sync?.rawExchangeHeads === true
3573
3616
  ? SYNC_CAPABILITY_RAW_EXCHANGE_HEADS
3574
3617
  : 0)
@@ -6187,6 +6230,8 @@ export class SharedLog<
6187
6230
  checkDuplicates?: boolean;
6188
6231
  rebalance?: boolean;
6189
6232
  mergeSegments?: boolean;
6233
+ /** Wait for explicit remote durable application; omitted is unchanged. */
6234
+ confirm?: boolean | ReplicationConfirmationOptions;
6190
6235
  announce?: (
6191
6236
  msg: FullReplicationInfoMutation | AddedReplicationInfoMutation,
6192
6237
  ) => void;
@@ -6197,7 +6242,13 @@ export class SharedLog<
6197
6242
  ? this.replicationController?.maxMemoryLimit
6198
6243
  : undefined;
6199
6244
  try {
6200
- return await this.applyReplicationRole(rangeOrEntry, options);
6245
+ const ranges = await this.applyReplicationRole(rangeOrEntry, options);
6246
+ if (options?.confirm) {
6247
+ await this._v2Send.confirmLatest(
6248
+ typeof options.confirm === "object" ? options.confirm : undefined,
6249
+ );
6250
+ }
6251
+ return ranges;
6201
6252
  } finally {
6202
6253
  const storageObjective = this._isAdaptiveReplicating
6203
6254
  ? this.replicationController?.maxMemoryLimit
@@ -6329,7 +6380,10 @@ export class SharedLog<
6329
6380
  );
6330
6381
  }
6331
6382
 
6332
- async unreplicate(rangeOrEntry?: Entry<T> | { id: Uint8Array }[]) {
6383
+ async unreplicate(
6384
+ rangeOrEntry?: Entry<T> | { id: Uint8Array }[],
6385
+ options?: { confirm?: boolean | ReplicationConfirmationOptions },
6386
+ ) {
6333
6387
  const wasAdaptiveReplicating = this._isAdaptiveReplicating;
6334
6388
  const previousStorageObjective = wasAdaptiveReplicating
6335
6389
  ? this.replicationController?.maxMemoryLimit
@@ -6340,10 +6394,16 @@ export class SharedLog<
6340
6394
  const replicationOwnershipLifecycleController =
6341
6395
  this.captureReplicationOwnershipLifecycle();
6342
6396
  try {
6343
- return await this._unreplicate(
6397
+ const result = await this._unreplicate(
6344
6398
  rangeOrEntry,
6345
6399
  replicationOwnershipLifecycleController,
6346
6400
  );
6401
+ if (options?.confirm) {
6402
+ await this._v2Send.confirmLatest(
6403
+ typeof options.confirm === "object" ? options.confirm : undefined,
6404
+ );
6405
+ }
6406
+ return result;
6347
6407
  } finally {
6348
6408
  const storageObjective = this._isAdaptiveReplicating
6349
6409
  ? this.replicationController?.maxMemoryLimit
@@ -14600,6 +14660,7 @@ export class SharedLog<
14600
14660
  : undefined,
14601
14661
  };
14602
14662
  this._logProperties = options;
14663
+ this._receiveSignatureVerificationFacts = new WeakMap();
14603
14664
 
14604
14665
  this.domain = options?.domain
14605
14666
  ? (options.domain(this) as unknown as D)
@@ -17039,6 +17100,93 @@ export class SharedLog<
17039
17100
  });
17040
17101
  }
17041
17102
 
17103
+ private receiveSignatureVerificationResult(
17104
+ entry: Entry<T>,
17105
+ ): boolean | undefined {
17106
+ return getReceiveSignatureVerificationResult(
17107
+ this._receiveSignatureVerificationFacts,
17108
+ entry,
17109
+ );
17110
+ }
17111
+
17112
+ private async preverifyReceiveSignaturesBatch(
17113
+ entries: Entry<T>[],
17114
+ profile?: SyncProfileFn,
17115
+ ): Promise<ReceiveSignatureVerificationFact<T>[] | undefined> {
17116
+ const candidates = entries.filter(
17117
+ (entry) => !entry.createdLocally && !hasPreverifiedSignature(entry),
17118
+ );
17119
+ if (candidates.length < NATIVE_ED25519_VERIFY_BATCH_MIN_ENTRIES) {
17120
+ return undefined;
17121
+ }
17122
+
17123
+ let storageBytes: Uint8Array[];
17124
+ let signableInputs: Ed25519VerifyBatchInput[] | undefined;
17125
+ try {
17126
+ // Capture the callback-visible storage state and native signable inputs
17127
+ // before the first await so the result is bound to one entry state.
17128
+ storageBytes = candidates.map((entry) => entry.getStorageBytes().slice());
17129
+ signableInputs =
17130
+ this.prepareReceiveNativeEd25519VerificationBatch(candidates);
17131
+ } catch {
17132
+ return undefined;
17133
+ }
17134
+ if (!signableInputs) {
17135
+ return undefined;
17136
+ }
17137
+
17138
+ const verifyStartedAt = syncProfileStart(profile);
17139
+ let verified: boolean[] | undefined;
17140
+ try {
17141
+ verified = await verifyEd25519Batch(signableInputs);
17142
+ } catch {
17143
+ verified = undefined;
17144
+ }
17145
+ if (!verified || verified.length !== candidates.length) {
17146
+ return undefined;
17147
+ }
17148
+ if (profile) {
17149
+ emitSyncProfileDuration(profile, verifyStartedAt, {
17150
+ name: "sharedLog.canAppendBatch.verifySignatures",
17151
+ component: "shared-log",
17152
+ entries: candidates.length,
17153
+ messages: 1,
17154
+ details: {
17155
+ native: true,
17156
+ mode: "signable",
17157
+ programCanAppendDeferred: true,
17158
+ },
17159
+ });
17160
+ }
17161
+ return candidates.map((entry, index) => ({
17162
+ entry,
17163
+ storageBytes: storageBytes[index]!,
17164
+ verified: verified[index]!,
17165
+ }));
17166
+ }
17167
+
17168
+ private prepareReceiveNativeEd25519VerificationBatch(
17169
+ entries: Entry<T>[],
17170
+ ): Ed25519VerifyBatchInput[] | undefined {
17171
+ const inputs = this.prepareNativeEd25519VerificationBatch(entries);
17172
+ return inputs?.map((input) => ({
17173
+ signature: input.signature.slice(),
17174
+ publicKey: input.publicKey.slice(),
17175
+ message: input.message.slice(),
17176
+ }));
17177
+ }
17178
+
17179
+ private async withReceiveSignatureVerificationFacts<R>(
17180
+ facts: ReceiveSignatureVerificationFact<T>[] | undefined,
17181
+ operation: () => Promise<R>,
17182
+ ): Promise<R> {
17183
+ return withSignatureVerificationFacts(
17184
+ this._receiveSignatureVerificationFacts,
17185
+ facts,
17186
+ operation,
17187
+ );
17188
+ }
17189
+
17042
17190
  async canAppend(entry: Entry<T>) {
17043
17191
  try {
17044
17192
  if (!entry.meta.data) {
@@ -17052,10 +17200,19 @@ export class SharedLog<
17052
17200
 
17053
17201
  checkMinReplicasLimit(replicas);
17054
17202
 
17055
- // Locally-created entries were signed before append.
17203
+ const receiveSignatureVerification =
17204
+ this.receiveSignatureVerificationResult(entry);
17205
+ if (receiveSignatureVerification === false) {
17206
+ return false;
17207
+ }
17208
+
17209
+ // Locally-created entries were signed before append. Raw prepared entries
17210
+ // can carry a durable verifier fact from their decoder; plain receive facts
17211
+ // are scoped to this lower-log join and checked against the exact bytes.
17056
17212
  if (
17057
17213
  !entry.createdLocally &&
17058
17214
  !hasPreverifiedSignature(entry) &&
17215
+ receiveSignatureVerification !== true &&
17059
17216
  !(await entry.verifySignatures())
17060
17217
  ) {
17061
17218
  return false;
@@ -19608,6 +19765,12 @@ export class SharedLog<
19608
19765
  // entry views.
19609
19766
  const programCanAppend = !!this._logProperties?.canAppend;
19610
19767
  const programOnChange = !!this._logProperties?.onChange;
19768
+ const receiveSignatureVerificationFacts = programCanAppend
19769
+ ? await this.preverifyReceiveSignaturesBatch(
19770
+ materializeAllToMergeEntries(),
19771
+ syncProfile,
19772
+ )
19773
+ : undefined;
19611
19774
  const nativeBackboneCommitValidation = programCanAppend
19612
19775
  ? undefined
19613
19776
  : this.validatePreparedRawReceiveHeadsMetadataWithNativeBackbone(
@@ -19811,35 +19974,43 @@ export class SharedLog<
19811
19974
  const joinOnAppendHashes = programOnChange
19812
19975
  ? undefined
19813
19976
  : onAppendHashes;
19814
- const joinedPreparedFacts =
19815
- canUsePreparedAppendFacts &&
19816
- (await trustedLowerLog.joinPreparedAppendFactsBatch(
19817
- preparedAppendFacts,
19818
- {
19977
+ let joinedPreparedFacts = false;
19978
+ const joinLowerLog = async () => {
19979
+ joinedPreparedFacts =
19980
+ canUsePreparedAppendFacts &&
19981
+ (await trustedLowerLog.joinPreparedAppendFactsBatch(
19982
+ preparedAppendFacts,
19983
+ {
19984
+ __peerbitEntriesAlreadyMissing: true,
19985
+ __peerbitCanAppendAlreadyValidated: true,
19986
+ __peerbitDeferIndexWrite: true,
19987
+ __peerbitOnAppendHashes: joinOnAppendHashes,
19988
+ __peerbitProfile: syncProfile,
19989
+ __peerbitNativePreparedJoinCommit:
19990
+ nativePreparedJoinCommit,
19991
+ __peerbitNativePreparedJoinCommitValidatesPlan:
19992
+ nativePreparedJoinCommitValidatesPlan,
19993
+ __peerbitOnPreparedJoinCommitted: nativePreparedJoinCommit
19994
+ ? finishNativePreparedCoordinates
19995
+ : undefined,
19996
+ },
19997
+ ));
19998
+ if (!joinedPreparedFacts) {
19999
+ await trustedLowerLog.join(materializeAllToMergeEntries(), {
20000
+ __peerbitBatchIndependent: true,
19819
20001
  __peerbitEntriesAlreadyMissing: true,
19820
- __peerbitCanAppendAlreadyValidated: true,
20002
+ __peerbitCanAppendAlreadyValidated:
20003
+ fallbackCanAppendAlreadyValidated,
19821
20004
  __peerbitDeferIndexWrite: true,
19822
20005
  __peerbitOnAppendHashes: joinOnAppendHashes,
19823
20006
  __peerbitProfile: syncProfile,
19824
- __peerbitNativePreparedJoinCommit: nativePreparedJoinCommit,
19825
- __peerbitNativePreparedJoinCommitValidatesPlan:
19826
- nativePreparedJoinCommitValidatesPlan,
19827
- __peerbitOnPreparedJoinCommitted: nativePreparedJoinCommit
19828
- ? finishNativePreparedCoordinates
19829
- : undefined,
19830
- },
19831
- ));
19832
- if (!joinedPreparedFacts) {
19833
- await trustedLowerLog.join(materializeAllToMergeEntries(), {
19834
- __peerbitBatchIndependent: true,
19835
- __peerbitEntriesAlreadyMissing: true,
19836
- __peerbitCanAppendAlreadyValidated:
19837
- fallbackCanAppendAlreadyValidated,
19838
- __peerbitDeferIndexWrite: true,
19839
- __peerbitOnAppendHashes: joinOnAppendHashes,
19840
- __peerbitProfile: syncProfile,
19841
- });
19842
- }
20007
+ });
20008
+ }
20009
+ };
20010
+ await this.withReceiveSignatureVerificationFacts(
20011
+ receiveSignatureVerificationFacts,
20012
+ joinLowerLog,
20013
+ );
19843
20014
  // A recursive lower-log join can resolve successfully while declining
19844
20015
  // an individual top-level entry (for example, when one of its parents
19845
20016
  // is temporarily unavailable). The public Log.join() API intentionally
@@ -20153,6 +20324,42 @@ export class SharedLog<
20153
20324
  this.markEntriesKnownByPeer(msg.hashes, context.from.hashcode());
20154
20325
  this.clearRepairFrontierHashes(context.from.hashcode(), msg.hashes);
20155
20326
  return;
20327
+ } else if (msg instanceof RequestReplicationInfoV2AppliedMessage) {
20328
+ if (
20329
+ receiveSession?.phase === "open" &&
20330
+ this._peerSyncCapabilitySessions.get(receiveFromHash) ===
20331
+ context.message.header.session &&
20332
+ ((this._peerSyncCapabilities.get(receiveFromHash) ?? 0) &
20333
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM) !==
20334
+ 0
20335
+ ) {
20336
+ const applied = this._v2Receive.confirmApplied(msg, {
20337
+ from: context.from,
20338
+ peerSession: receiveSession,
20339
+ receiveEpoch: receiveReplicationInfoReceiveEpoch,
20340
+ senderTransportSession: context.message.header.session,
20341
+ });
20342
+ if (applied) {
20343
+ await this.rpc.send(applied, {
20344
+ mode: new SilentDelivery({
20345
+ to: [context.from],
20346
+ redundancy: 1,
20347
+ }),
20348
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
20349
+ signal: AbortSignal.any([
20350
+ this._closeController.signal,
20351
+ lane.ownershipLifecycleController.signal,
20352
+ ]),
20353
+ });
20354
+ }
20355
+ }
20356
+ return;
20357
+ } else if (msg instanceof ReplicationInfoV2AppliedMessage) {
20358
+ this._v2Send.acceptApplied(msg, {
20359
+ from: context.from,
20360
+ receiverTransportSession: context.message.header.session,
20361
+ });
20362
+ return;
20156
20363
  } else if (isReplicationInfoV2Message(msg)) {
20157
20364
  await this.handleReplicationInfoV2Announcement(
20158
20365
  msg,
@@ -0,0 +1,66 @@
1
+ import type { Entry } from "@peerbit/log";
2
+ import { equals as bytesEqual } from "uint8arrays";
3
+
4
+ export type ReceiveSignatureVerificationFact<T> = {
5
+ entry: Entry<T>;
6
+ storageBytes: Uint8Array;
7
+ verified: boolean;
8
+ };
9
+
10
+ export const receiveSignatureVerificationResult = <T>(
11
+ factsByEntry: WeakMap<Entry<T>, ReceiveSignatureVerificationFact<T>[]>,
12
+ entry: Entry<T>,
13
+ ): boolean | undefined => {
14
+ const facts = factsByEntry.get(entry);
15
+ if (!facts || facts.length === 0) {
16
+ return undefined;
17
+ }
18
+ let storageBytes: Uint8Array;
19
+ try {
20
+ storageBytes = entry.getStorageBytes();
21
+ } catch {
22
+ return undefined;
23
+ }
24
+ for (let i = facts.length - 1; i >= 0; i--) {
25
+ const fact = facts[i]!;
26
+ if (bytesEqual(storageBytes, fact.storageBytes)) {
27
+ return fact.verified;
28
+ }
29
+ }
30
+ return undefined;
31
+ };
32
+
33
+ export const withReceiveSignatureVerificationFacts = async <T, R>(
34
+ factsByEntry: WeakMap<Entry<T>, ReceiveSignatureVerificationFact<T>[]>,
35
+ facts: ReceiveSignatureVerificationFact<T>[] | undefined,
36
+ operation: () => Promise<R>,
37
+ ): Promise<R> => {
38
+ if (!facts || facts.length === 0) {
39
+ return operation();
40
+ }
41
+ for (const fact of facts) {
42
+ const active = factsByEntry.get(fact.entry);
43
+ if (active) {
44
+ active.push(fact);
45
+ } else {
46
+ factsByEntry.set(fact.entry, [fact]);
47
+ }
48
+ }
49
+ try {
50
+ return await operation();
51
+ } finally {
52
+ for (const fact of facts) {
53
+ const active = factsByEntry.get(fact.entry);
54
+ if (!active) {
55
+ continue;
56
+ }
57
+ const index = active.indexOf(fact);
58
+ if (index >= 0) {
59
+ active.splice(index, 1);
60
+ }
61
+ if (active.length === 0) {
62
+ factsByEntry.delete(fact.entry);
63
+ }
64
+ }
65
+ }
66
+ };
@@ -10,7 +10,9 @@ import {
10
10
  AddedReplicationSegmentMessage,
11
11
  AllReplicatingSegmentsMessage,
12
12
  FullReplicationInfoV2Message,
13
+ ReplicationInfoV2AppliedMessage,
13
14
  type ReplicationInfoV2Message,
15
+ RequestReplicationInfoV2AppliedMessage,
14
16
  RequestReplicationInfoV2Message,
15
17
  StoppedReplicating,
16
18
  StoppedReplicationInfoV2Message,
@@ -1250,6 +1252,46 @@ export class ReplicationInfoV2ReceiveCoordinator {
1250
1252
  return true;
1251
1253
  }
1252
1254
 
1255
+ /**
1256
+ * Answer only after the exact authenticated receive generation has committed
1257
+ * the queried sequence (or a newer one). No pending query is retained: the
1258
+ * sender's bounded retry loop reissues an authoritative snapshot and query.
1259
+ */
1260
+ confirmApplied(
1261
+ request: RequestReplicationInfoV2AppliedMessage,
1262
+ properties: {
1263
+ from: PublicSignKey;
1264
+ peerSession: object;
1265
+ receiveEpoch: object | null;
1266
+ senderTransportSession: bigint;
1267
+ },
1268
+ ): ReplicationInfoV2AppliedMessage | undefined {
1269
+ const state = this._receiveStates.get(properties.from.hashcode());
1270
+ if (
1271
+ !state ||
1272
+ state.peerSession !== properties.peerSession ||
1273
+ state.receiveEpoch !== properties.receiveEpoch ||
1274
+ state.senderTransportSession !== properties.senderTransportSession ||
1275
+ !state.target.equals(properties.from) ||
1276
+ !state.receiverBinding ||
1277
+ !bytesEqual(request.receiverChallenge, state.receiverBinding) ||
1278
+ !state.senderEpoch ||
1279
+ !bytesEqual(request.senderEpoch, state.senderEpoch) ||
1280
+ request.sequence <= 0n ||
1281
+ state.lastSequence === undefined ||
1282
+ state.lastSequence < request.sequence ||
1283
+ !this.isStateCurrent(state)
1284
+ ) {
1285
+ return undefined;
1286
+ }
1287
+ return new ReplicationInfoV2AppliedMessage({
1288
+ receiverChallenge: request.receiverChallenge.slice(),
1289
+ senderEpoch: request.senderEpoch.slice(),
1290
+ sequence: request.sequence,
1291
+ revision: request.revision,
1292
+ });
1293
+ }
1294
+
1253
1295
  requireFullAfterFailure(
1254
1296
  admission: ReplicationInfoV2ReceiveAdmission,
1255
1297
  ): boolean {