@peerbit/shared-log 16.0.24 → 16.0.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/shared-log",
3
- "version": "16.0.24",
3
+ "version": "16.0.25",
4
4
  "description": "Shared log",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -62,35 +62,35 @@
62
62
  "pidusage": "^4.0.1",
63
63
  "pino": "^9.4.0",
64
64
  "uint8arrays": "^5.1.0",
65
- "@peerbit/any-store": "2.2.17",
66
65
  "@peerbit/blocks": "4.2.16",
67
- "@peerbit/blocks-interface": "2.1.8",
68
66
  "@peerbit/cache": "3.1.1",
69
67
  "@peerbit/crypto": "3.1.6",
68
+ "@peerbit/blocks-interface": "2.1.8",
70
69
  "@peerbit/diagnostics": "0.0.1",
70
+ "@peerbit/any-store": "2.2.17",
71
71
  "@peerbit/indexer-interface": "3.0.13",
72
- "@peerbit/log": "6.2.31",
73
72
  "@peerbit/logger": "2.0.2",
74
- "@peerbit/indexer-sqlite3": "3.0.20",
75
73
  "@peerbit/program": "6.0.58",
76
- "@peerbit/riblt": "1.2.0",
77
- "@peerbit/time": "3.0.1",
74
+ "@peerbit/indexer-sqlite3": "3.0.20",
75
+ "@peerbit/pubsub": "5.4.5",
76
+ "@peerbit/pubsub-interface": "5.2.2",
77
+ "@peerbit/log": "6.2.31",
78
78
  "@peerbit/rpc": "6.1.26",
79
+ "@peerbit/riblt": "1.2.0",
79
80
  "@peerbit/stream-interface": "6.0.16",
80
- "@peerbit/pubsub-interface": "5.2.2",
81
- "@peerbit/pubsub": "5.4.5"
81
+ "@peerbit/time": "3.0.1"
82
82
  },
83
83
  "optionalDependencies": {
84
- "@peerbit/shared-log-rust": "0.1.7",
85
- "@peerbit/native-backbone": "0.2.12"
84
+ "@peerbit/native-backbone": "0.2.12",
85
+ "@peerbit/shared-log-rust": "0.1.7"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@types/libsodium-wrappers": "^0.7.14",
89
89
  "@types/pidusage": "^2.0.5",
90
90
  "uuid": "^11.1.1",
91
- "@peerbit/any-store-rust": "0.1.7",
92
- "@peerbit/indexer-rust": "1.0.14",
93
91
  "@peerbit/indexer-simple": "1.2.17",
92
+ "@peerbit/indexer-rust": "1.0.14",
93
+ "@peerbit/any-store-rust": "0.1.7",
94
94
  "@peerbit/test-utils": "3.1.38",
95
95
  "peerbit": "5.3.38"
96
96
  },
@@ -383,6 +383,15 @@ export const SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM = 1 << 4;
383
383
  */
384
384
  export const SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS = 1 << 5;
385
385
 
386
+ /**
387
+ * Transient authenticated recovery hint: the receiver of this exact
388
+ * `SyncCapabilitiesMessage` should re-arm its current replication-info V2
389
+ * receiver and issue a fresh Full request. This bit is deliberately omitted
390
+ * from steady capability advertisements and must never be retained as a
391
+ * negotiated capability. Older peers safely ignore the unknown bit.
392
+ */
393
+ export const SYNC_CAPABILITY_REPLICATION_INFO_V2_REARM = 1 << 6;
394
+
386
395
  /**
387
396
  * One-shot capability advertisement, sent to a peer when it (or we) subscribe
388
397
  * to the program topic. Peers that do not know this message drop it as an
package/src/index.ts CHANGED
@@ -169,6 +169,7 @@ import {
169
169
  SYNC_CAPABILITY_REPLICATION_INFO_V2_APPLY,
170
170
  SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM,
171
171
  SYNC_CAPABILITY_REPLICATION_INFO_V2_DECODE,
172
+ SYNC_CAPABILITY_REPLICATION_INFO_V2_REARM,
172
173
  SYNC_CAPABILITY_REPLICATION_INFO_V2_SEND,
173
174
  StashBackedRawExchangeHeadsMessage,
174
175
  SyncCapabilitiesMessage,
@@ -4071,6 +4072,7 @@ export class SharedLog<
4071
4072
  peerSession: PeerSession;
4072
4073
  receiveEpoch: object | null;
4073
4074
  signal: AbortSignal;
4075
+ requestRemoteFullRearm?: boolean;
4074
4076
  }): Promise<
4075
4077
  { receiverTransportSession: bigint; requestNotBeforeMs: number } | undefined
4076
4078
  > {
@@ -4078,7 +4080,11 @@ export class SharedLog<
4078
4080
  const receiverTransportSession = this.ownTransportSession();
4079
4081
  await this.rpc.send(
4080
4082
  new SyncCapabilitiesMessage({
4081
- capabilities: this.replicationInfoV2ReceiveCapabilities(),
4083
+ capabilities:
4084
+ this.replicationInfoV2ReceiveCapabilities() |
4085
+ (properties.requestRemoteFullRearm
4086
+ ? SYNC_CAPABILITY_REPLICATION_INFO_V2_REARM
4087
+ : 0),
4082
4088
  }),
4083
4089
  {
4084
4090
  mode: new AcknowledgeDelivery({
@@ -4145,6 +4151,7 @@ export class SharedLog<
4145
4151
  peerSession: properties.peerSession as PeerSession,
4146
4152
  receiveEpoch: properties.receiveEpoch,
4147
4153
  signal: properties.signal,
4154
+ requestRemoteFullRearm: properties.requestRemoteFullRearm,
4148
4155
  }),
4149
4156
  onRequestError: (error) => {
4150
4157
  if (
@@ -5851,7 +5858,13 @@ export class SharedLog<
5851
5858
 
5852
5859
  private persistedReceiptPeerSession(
5853
5860
  peerHash: string,
5854
- ): { capabilitySession: bigint; peerSession: PeerSession } | undefined {
5861
+ ):
5862
+ | {
5863
+ capabilitySession: bigint;
5864
+ peerSession: PeerSession;
5865
+ receiveEpoch: object | null;
5866
+ }
5867
+ | undefined {
5855
5868
  // This is a hot receipt/transfer-loop predicate. Keep it allocation-light,
5856
5869
  // while mirroring every exact-session gate in
5857
5870
  // persistedReceiptReadinessCandidate (which additionally creates public
@@ -5884,7 +5897,7 @@ export class SharedLog<
5884
5897
  ) {
5885
5898
  return undefined;
5886
5899
  }
5887
- return { capabilitySession, peerSession };
5900
+ return { capabilitySession, peerSession, receiveEpoch };
5888
5901
  }
5889
5902
 
5890
5903
  private async waitPersistedReceiptRetry(
@@ -23168,6 +23181,17 @@ export class SharedLog<
23168
23181
  if (!context.from.equals(this.node.identity.publicKey)) {
23169
23182
  const capabilityTransportSession = context.message?.header?.session;
23170
23183
  const capabilityTimestamp = context.message?.header?.timestamp;
23184
+ const previousCapabilitySession =
23185
+ this._peerSyncCapabilitySessions.get(receiveFromHash);
23186
+ const previousCapabilityTimestamp =
23187
+ this._peerSyncCapabilityTimestamps.get(receiveFromHash);
23188
+ const requestsRemoteFullRearm =
23189
+ (msg.capabilities & SYNC_CAPABILITY_REPLICATION_INFO_V2_REARM) !==
23190
+ 0;
23191
+ // The rearm bit is a one-shot authenticated command, not a sticky
23192
+ // negotiated capability. Store and promote only the steady bits.
23193
+ const steadyCapabilities =
23194
+ msg.capabilities & ~SYNC_CAPABILITY_REPLICATION_INFO_V2_REARM;
23171
23195
  // No await separates this from the capture above, so the captured
23172
23196
  // window state is exact: the legacy re-read of the opening map here
23173
23197
  // could never observe a different value.
@@ -23180,7 +23204,7 @@ export class SharedLog<
23180
23204
  // cleanup cannot erase it before the opening transition commits.
23181
23205
  this.observePeerSyncCapabilities({
23182
23206
  peerHash: receiveFromHash,
23183
- capabilities: msg.capabilities,
23207
+ capabilities: steadyCapabilities,
23184
23208
  transportSession: capabilityTransportSession,
23185
23209
  timestamp: capabilityTimestamp,
23186
23210
  openingSession: receiveSession!,
@@ -23188,7 +23212,7 @@ export class SharedLog<
23188
23212
  } else {
23189
23213
  const observed = this.observePeerSyncCapabilities({
23190
23214
  peerHash: receiveFromHash,
23191
- capabilities: msg.capabilities,
23215
+ capabilities: steadyCapabilities,
23192
23216
  transportSession: capabilityTransportSession,
23193
23217
  timestamp: capabilityTimestamp,
23194
23218
  });
@@ -23197,6 +23221,33 @@ export class SharedLog<
23197
23221
  context.from,
23198
23222
  receiveSession,
23199
23223
  );
23224
+ const freshExactRearm =
23225
+ requestsRemoteFullRearm &&
23226
+ capabilityTransportSession !== undefined &&
23227
+ capabilityTimestamp !== undefined &&
23228
+ previousCapabilitySession === capabilityTransportSession &&
23229
+ previousCapabilityTimestamp !== undefined &&
23230
+ capabilityTimestamp > previousCapabilityTimestamp;
23231
+ if (
23232
+ freshExactRearm &&
23233
+ this._v2Receive.isCurrentActive({
23234
+ peerHash: receiveFromHash,
23235
+ peerSession: receiveSession,
23236
+ receiveEpoch:
23237
+ this._peerSessions.receiveEpoch(receiveFromHash),
23238
+ senderTransportSession: capabilityTransportSession,
23239
+ })
23240
+ ) {
23241
+ // Rotate the receiver grant/challenge before requesting Full. A
23242
+ // sender rebuilt from no state starts at sequence one, which an
23243
+ // active receiver's old sequence fence must otherwise reject.
23244
+ this._v2Receive.advanceRecovery({
23245
+ peerHash: receiveFromHash,
23246
+ peerSession: receiveSession,
23247
+ receiveEpoch:
23248
+ this._peerSessions.receiveEpoch(receiveFromHash),
23249
+ });
23250
+ }
23200
23251
  } else if (observed && receiveSession === null) {
23201
23252
  // A capability can arrive before the sender's topic Subscribe after
23202
23253
  // reconnect. Ask that authenticated peer for its authoritative
@@ -24327,8 +24378,11 @@ export class SharedLog<
24327
24378
  throwIfInactive();
24328
24379
  }
24329
24380
 
24330
- private nudgePersistedReceiptPeerReadiness(publicKey: PublicSignKey): void {
24331
- if (this.closed) return;
24381
+ private nudgePersistedReceiptPeerReadiness(
24382
+ publicKey: PublicSignKey,
24383
+ signal: AbortSignal,
24384
+ ): void {
24385
+ if (this.closed || signal.aborted) return;
24332
24386
  const peerHash = publicKey.hashcode();
24333
24387
  const peerSession = this._peerSessions.current(peerHash);
24334
24388
  if (
@@ -24359,6 +24413,22 @@ export class SharedLog<
24359
24413
  peerSession,
24360
24414
  receiveEpoch,
24361
24415
  });
24416
+ const receiptTarget = this.persistedReceiptPeerSession(peerHash);
24417
+ if (
24418
+ receiptTarget &&
24419
+ !this._v2Send.hasCurrentStateForPeer({
24420
+ peerHash,
24421
+ peerSession: receiptTarget.peerSession,
24422
+ receiverTransportSession: receiptTarget.capabilitySession,
24423
+ })
24424
+ ) {
24425
+ this._v2Receive.reAdvertiseLocalCapabilityForRemoteFull({
24426
+ peerHash,
24427
+ peerSession: receiptTarget.peerSession,
24428
+ receiveEpoch: receiptTarget.receiveEpoch,
24429
+ signal,
24430
+ });
24431
+ }
24362
24432
  this.scheduleReplicationInfoV2Recovery(publicKey);
24363
24433
  }
24364
24434
 
@@ -24627,18 +24697,26 @@ export class SharedLog<
24627
24697
  throw new AbortError("Persisted-receipt readiness wait settled");
24628
24698
  }
24629
24699
  };
24700
+ const recoveryDelayMs = Math.max(
24701
+ 50,
24702
+ Math.min(1_000, this.waitForReplicatorRequestIntervalMs),
24703
+ );
24630
24704
  const armRecoveryTick = () => {
24631
24705
  if (settled || recoveryTimer) return;
24632
- const delayMs = Math.max(
24633
- 50,
24634
- Math.min(1_000, this.waitForReplicatorRequestIntervalMs),
24635
- );
24636
24706
  recoveryTimer = setTimeout(() => {
24637
24707
  recoveryTimer = undefined;
24638
24708
  if (!continueWait()) return;
24639
- this.nudgePersistedReceiptPeerReadiness(key);
24709
+ this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
24710
+ if (checkInFlight) {
24711
+ // Confirmation can legitimately span several recovery intervals.
24712
+ // Keep repairing the exact current generation without aborting its
24713
+ // one application-confirmation waiter on every tick.
24714
+ rerun = true;
24715
+ armRecoveryTick();
24716
+ return;
24717
+ }
24640
24718
  scheduleCheck();
24641
- }, delayMs);
24719
+ }, recoveryDelayMs);
24642
24720
  recoveryTimer.unref?.();
24643
24721
  };
24644
24722
  const runCheck = async () => {
@@ -24671,6 +24749,11 @@ export class SharedLog<
24671
24749
  ) {
24672
24750
  const target = this.persistedReceiptPeerSession(peerHash);
24673
24751
  if (target) {
24752
+ // A confirmation waiter cannot create an outbound V2 stream when
24753
+ // that exact session state is missing. Nudge first, then keep the
24754
+ // recovery tick alive while the single confirmation wait remains.
24755
+ this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
24756
+ armRecoveryTick();
24674
24757
  const currentConfirmationController = new AbortController();
24675
24758
  confirmationController = currentConfirmationController;
24676
24759
  try {
@@ -24715,7 +24798,7 @@ export class SharedLog<
24715
24798
  }
24716
24799
  }
24717
24800
  if (!continueWait()) return;
24718
- this.nudgePersistedReceiptPeerReadiness(key);
24801
+ this.nudgePersistedReceiptPeerReadiness(key, operationSignal);
24719
24802
  } catch (error) {
24720
24803
  if (!settled) reject(error);
24721
24804
  } finally {
@@ -112,6 +112,11 @@ export type ReplicationInfoV2LocalCapabilityAdvertisement = {
112
112
  firstAttempt?: Promise<void>;
113
113
  };
114
114
 
115
+ type ReplicationInfoV2RemoteFullRearm = {
116
+ lastAttemptAt: number;
117
+ inFlight?: Promise<void>;
118
+ };
119
+
115
120
  export type ReplicationInfoV2LocalCapabilityRefresh = {
116
121
  receiverTransportSession: bigint;
117
122
  requestNotBeforeMs: number;
@@ -183,6 +188,7 @@ export type ReplicationInfoV2ReceiveDeps = {
183
188
  peerSession: object;
184
189
  receiveEpoch: object | null;
185
190
  signal: AbortSignal;
191
+ requestRemoteFullRearm?: boolean;
186
192
  }) => Promise<ReplicationInfoV2LocalCapabilityRefresh | undefined>;
187
193
  onRequestError?: (error: unknown) => void;
188
194
  onLocalCapabilityError?: (error: unknown) => void;
@@ -217,6 +223,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
217
223
  string,
218
224
  ReplicationInfoV2LocalCapabilityAdvertisement
219
225
  >;
226
+ _remoteFullRearmBySession!: WeakMap<object, ReplicationInfoV2RemoteFullRearm>;
220
227
  _reservedAdmissionsByPeer!: Map<string, ReplicationInfoV2ReceiveAdmission>;
221
228
 
222
229
  private readonly now: () => number;
@@ -243,6 +250,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
243
250
  this._localCapabilityReadyBySession = new WeakMap();
244
251
  this._localCapabilityContextBySession = new WeakMap();
245
252
  this._localCapabilityAdvertisementsByPeer = new Map();
253
+ this._remoteFullRearmBySession = new WeakMap();
246
254
  this._reservedAdmissionsByPeer = new Map();
247
255
  }
248
256
 
@@ -253,6 +261,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
253
261
  this._localCapabilityReadyBySession = new WeakMap();
254
262
  this._localCapabilityContextBySession = new WeakMap();
255
263
  this._localCapabilityAdvertisementsByPeer = new Map();
264
+ this._remoteFullRearmBySession = new WeakMap();
256
265
  this._reservedAdmissionsByPeer = new Map();
257
266
  }
258
267
 
@@ -270,6 +279,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
270
279
  this._cutoverPeerSessions = new WeakSet();
271
280
  this._localCapabilityReadyBySession = new WeakMap();
272
281
  this._localCapabilityContextBySession = new WeakMap();
282
+ this._remoteFullRearmBySession = new WeakMap();
273
283
  }
274
284
 
275
285
  clearPeer(peerHash: string, expectedSession?: object): void {
@@ -291,6 +301,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
291
301
  if (expectedSession) {
292
302
  this._localCapabilityReadyBySession.delete(expectedSession);
293
303
  this._localCapabilityContextBySession.delete(expectedSession);
304
+ this._remoteFullRearmBySession.delete(expectedSession);
294
305
  this._cutoverPeerSessions.delete(expectedSession);
295
306
  }
296
307
  }
@@ -625,6 +636,117 @@ export class ReplicationInfoV2ReceiveCoordinator {
625
636
  };
626
637
  }
627
638
 
639
+ /**
640
+ * Send one transient, authenticated rearm hint for the exact current local
641
+ * receive grant. The hint asks a patched remote receiver to rotate its
642
+ * challenge and issue another Full request, rebuilding an outbound sender
643
+ * stream that was lost while both directions otherwise remained current.
644
+ *
645
+ * This deliberately does not mutate the steady advertisement worker. One
646
+ * attempt is coalesced per PeerSession and bound to the caller's signal; the
647
+ * readiness wait's bounded recovery tick decides whether another attempt is
648
+ * needed.
649
+ */
650
+ reAdvertiseLocalCapabilityForRemoteFull(properties: {
651
+ peerHash: string;
652
+ peerSession: object;
653
+ receiveEpoch: object | null;
654
+ signal: AbortSignal;
655
+ }): boolean {
656
+ const context = this._localCapabilityContextBySession.get(
657
+ properties.peerSession,
658
+ );
659
+ const ready = this._localCapabilityReadyBySession.get(
660
+ properties.peerSession,
661
+ );
662
+ if (
663
+ properties.signal.aborted ||
664
+ !context ||
665
+ context.peerHash !== properties.peerHash ||
666
+ context.lifecycleSignal.aborted ||
667
+ this.deps.isClosed() ||
668
+ !this.deps.isPeerStateCurrent(
669
+ properties.peerHash,
670
+ properties.peerSession,
671
+ properties.receiveEpoch,
672
+ ) ||
673
+ !ready ||
674
+ ready.peerHash !== properties.peerHash ||
675
+ ready.receiveEpoch !== properties.receiveEpoch ||
676
+ ready.receiverTransportSession !== this.deps.getReceiverTransportSession()
677
+ ) {
678
+ return false;
679
+ }
680
+
681
+ const now = this.now();
682
+ let rearm = this._remoteFullRearmBySession.get(properties.peerSession);
683
+ if (rearm?.inFlight) {
684
+ return true;
685
+ }
686
+ if (
687
+ rearm !== undefined &&
688
+ now - rearm.lastAttemptAt < this.requestRetryMs
689
+ ) {
690
+ return true;
691
+ }
692
+ if (!rearm) {
693
+ rearm = { lastAttemptAt: now };
694
+ this._remoteFullRearmBySession.set(properties.peerSession, rearm);
695
+ } else {
696
+ rearm.lastAttemptAt = now;
697
+ }
698
+
699
+ const operationSignal = AbortSignal.any([
700
+ context.lifecycleSignal,
701
+ properties.signal,
702
+ ]);
703
+ let operation: Promise<void>;
704
+ const detach = () => {
705
+ if (rearm?.inFlight === operation) {
706
+ rearm.inFlight = undefined;
707
+ }
708
+ };
709
+ operation = Promise.resolve()
710
+ .then(() => {
711
+ if (operationSignal.aborted) {
712
+ throw operationSignal.reason;
713
+ }
714
+ return this.deps.refreshLocalCapability({
715
+ peerHash: properties.peerHash,
716
+ target: context.target,
717
+ peerSession: properties.peerSession,
718
+ receiveEpoch: properties.receiveEpoch,
719
+ signal: operationSignal,
720
+ requestRemoteFullRearm: true,
721
+ });
722
+ })
723
+ .then(() => undefined)
724
+ .catch((error) => {
725
+ if (
726
+ !operationSignal.aborted &&
727
+ !this.deps.isClosed() &&
728
+ this.deps.isPeerStateCurrent(
729
+ properties.peerHash,
730
+ properties.peerSession,
731
+ properties.receiveEpoch,
732
+ )
733
+ ) {
734
+ this.deps.onLocalCapabilityError?.(error);
735
+ }
736
+ })
737
+ .finally(() => {
738
+ operationSignal.removeEventListener("abort", detach);
739
+ detach();
740
+ });
741
+ rearm.inFlight = operation;
742
+ operationSignal.addEventListener("abort", detach, { once: true });
743
+ if (operationSignal.aborted) {
744
+ detach();
745
+ }
746
+ void operation;
747
+ return true;
748
+ }
749
+
628
750
  private promoteLocalCapabilityAdvertisement(
629
751
  state: ReplicationInfoV2LocalCapabilityAdvertisement,
630
752
  ): boolean {
@@ -561,6 +561,22 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
561
561
  );
562
562
  }
563
563
 
564
+ /**
565
+ * Whether the exact current destination generation already has an outbound
566
+ * V2 stream. A missing/stale stream cannot be repaired by confirmation
567
+ * retries alone: the remote receiver must issue another Full request first.
568
+ */
569
+ hasCurrentStateForPeer(target: ApplicationConfirmationTarget): boolean {
570
+ const state = this._sendStates.get(target.peerHash);
571
+ return (
572
+ state !== undefined &&
573
+ state.peerSession === target.peerSession &&
574
+ state.receiverTransportSession === target.receiverTransportSession &&
575
+ this.isCurrent(state) &&
576
+ this.supportsApplicationConfirmation(state)
577
+ );
578
+ }
579
+
564
580
  private trackRetiringWorker(state: ReplicationInfoV2SendState): void {
565
581
  const worker = state.worker;
566
582
  if (!worker) {