@peerbit/shared-log 13.2.23 → 13.2.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.
Files changed (175) hide show
  1. package/dist/src/checked-prune.d.ts +12 -0
  2. package/dist/src/checked-prune.d.ts.map +1 -1
  3. package/dist/src/checked-prune.js +33 -0
  4. package/dist/src/checked-prune.js.map +1 -1
  5. package/dist/src/coordinate-persistence.d.ts +211 -0
  6. package/dist/src/coordinate-persistence.d.ts.map +1 -0
  7. package/dist/src/coordinate-persistence.js +1133 -0
  8. package/dist/src/coordinate-persistence.js.map +1 -0
  9. package/dist/src/errors.d.ts +1 -0
  10. package/dist/src/errors.d.ts.map +1 -1
  11. package/dist/src/errors.js +19 -0
  12. package/dist/src/errors.js.map +1 -1
  13. package/dist/src/index.d.ts +139 -133
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +1491 -4265
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/instance-lifecycle.d.ts +117 -0
  18. package/dist/src/instance-lifecycle.d.ts.map +1 -0
  19. package/dist/src/instance-lifecycle.js +254 -0
  20. package/dist/src/instance-lifecycle.js.map +1 -0
  21. package/dist/src/join-warmup.d.ts +86 -0
  22. package/dist/src/join-warmup.d.ts.map +1 -0
  23. package/dist/src/join-warmup.js +351 -0
  24. package/dist/src/join-warmup.js.map +1 -0
  25. package/dist/src/leader-plan-cache.d.ts +33 -0
  26. package/dist/src/leader-plan-cache.d.ts.map +1 -0
  27. package/dist/src/leader-plan-cache.js +49 -0
  28. package/dist/src/leader-plan-cache.js.map +1 -0
  29. package/dist/src/native-write-through-block-store.d.ts +123 -0
  30. package/dist/src/native-write-through-block-store.d.ts.map +1 -0
  31. package/dist/src/native-write-through-block-store.js +989 -0
  32. package/dist/src/native-write-through-block-store.js.map +1 -0
  33. package/dist/src/peer-session.d.ts +117 -0
  34. package/dist/src/peer-session.d.ts.map +1 -0
  35. package/dist/src/peer-session.js +259 -0
  36. package/dist/src/peer-session.js.map +1 -0
  37. package/dist/src/ranges.d.ts +2 -2
  38. package/dist/src/ranges.d.ts.map +1 -1
  39. package/dist/src/ranges.js +10 -8
  40. package/dist/src/ranges.js.map +1 -1
  41. package/dist/src/replication-announcement.d.ts +116 -0
  42. package/dist/src/replication-announcement.d.ts.map +1 -0
  43. package/dist/src/replication-announcement.js +515 -0
  44. package/dist/src/replication-announcement.js.map +1 -0
  45. package/dist/src/replicator-liveness.d.ts +59 -0
  46. package/dist/src/replicator-liveness.d.ts.map +1 -0
  47. package/dist/src/replicator-liveness.js +304 -0
  48. package/dist/src/replicator-liveness.js.map +1 -0
  49. package/dist/src/sync/dispatch-lifecycle.d.ts +48 -0
  50. package/dist/src/sync/dispatch-lifecycle.d.ts.map +1 -0
  51. package/dist/src/sync/dispatch-lifecycle.js +141 -0
  52. package/dist/src/sync/dispatch-lifecycle.js.map +1 -0
  53. package/dist/src/sync/factory.d.ts +29 -0
  54. package/dist/src/sync/factory.d.ts.map +1 -0
  55. package/dist/src/sync/factory.js +87 -0
  56. package/dist/src/sync/factory.js.map +1 -0
  57. package/dist/src/sync/pending-sync-store.d.ts +105 -0
  58. package/dist/src/sync/pending-sync-store.d.ts.map +1 -0
  59. package/dist/src/sync/pending-sync-store.js +877 -0
  60. package/dist/src/sync/pending-sync-store.js.map +1 -0
  61. package/dist/src/sync/rateless-iblt.d.ts +4 -1
  62. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  63. package/dist/src/sync/rateless-iblt.js +102 -106
  64. package/dist/src/sync/rateless-iblt.js.map +1 -1
  65. package/dist/src/sync/simple.d.ts +10 -41
  66. package/dist/src/sync/simple.d.ts.map +1 -1
  67. package/dist/src/sync/simple.js +259 -952
  68. package/dist/src/sync/simple.js.map +1 -1
  69. package/dist/src/sync/sync-peer-state.d.ts +16 -0
  70. package/dist/src/sync/sync-peer-state.d.ts.map +1 -0
  71. package/dist/src/sync/sync-peer-state.js +82 -0
  72. package/dist/src/sync/sync-peer-state.js.map +1 -0
  73. package/package.json +22 -21
  74. package/src/checked-prune.ts +35 -0
  75. package/src/coordinate-persistence.ts +1796 -0
  76. package/src/errors.ts +21 -0
  77. package/src/index.ts +6367 -10052
  78. package/src/instance-lifecycle.ts +351 -0
  79. package/src/join-warmup.ts +507 -0
  80. package/src/leader-plan-cache.ts +61 -0
  81. package/src/native-write-through-block-store.ts +1170 -0
  82. package/src/peer-session.ts +336 -0
  83. package/src/ranges.ts +22 -16
  84. package/src/replication-announcement.ts +744 -0
  85. package/src/replicator-liveness.ts +439 -0
  86. package/src/sync/dispatch-lifecycle.ts +230 -0
  87. package/src/sync/factory.ts +154 -0
  88. package/src/sync/pending-sync-store.ts +1085 -0
  89. package/src/sync/rateless-iblt.ts +122 -132
  90. package/src/sync/simple.ts +334 -1144
  91. package/src/sync/sync-peer-state.ts +107 -0
  92. package/dist/benchmark/adaptive-ingest.d.ts +0 -2
  93. package/dist/benchmark/adaptive-ingest.d.ts.map +0 -1
  94. package/dist/benchmark/adaptive-ingest.js +0 -275
  95. package/dist/benchmark/adaptive-ingest.js.map +0 -1
  96. package/dist/benchmark/get-samples.d.ts +0 -2
  97. package/dist/benchmark/get-samples.d.ts.map +0 -1
  98. package/dist/benchmark/get-samples.js +0 -194
  99. package/dist/benchmark/get-samples.js.map +0 -1
  100. package/dist/benchmark/index.d.ts +0 -2
  101. package/dist/benchmark/index.d.ts.map +0 -1
  102. package/dist/benchmark/index.js +0 -224
  103. package/dist/benchmark/index.js.map +0 -1
  104. package/dist/benchmark/join-backfill-repair.d.ts +0 -2
  105. package/dist/benchmark/join-backfill-repair.d.ts.map +0 -1
  106. package/dist/benchmark/join-backfill-repair.js +0 -288
  107. package/dist/benchmark/join-backfill-repair.js.map +0 -1
  108. package/dist/benchmark/memory/child.d.ts +0 -2
  109. package/dist/benchmark/memory/child.d.ts.map +0 -1
  110. package/dist/benchmark/memory/child.js +0 -204
  111. package/dist/benchmark/memory/child.js.map +0 -1
  112. package/dist/benchmark/memory/index.d.ts +0 -2
  113. package/dist/benchmark/memory/index.d.ts.map +0 -1
  114. package/dist/benchmark/memory/index.js +0 -81
  115. package/dist/benchmark/memory/index.js.map +0 -1
  116. package/dist/benchmark/memory/utils.d.ts +0 -13
  117. package/dist/benchmark/memory/utils.d.ts.map +0 -1
  118. package/dist/benchmark/memory/utils.js +0 -2
  119. package/dist/benchmark/memory/utils.js.map +0 -1
  120. package/dist/benchmark/native-graph.d.ts +0 -2
  121. package/dist/benchmark/native-graph.d.ts.map +0 -1
  122. package/dist/benchmark/native-graph.js +0 -359
  123. package/dist/benchmark/native-graph.js.map +0 -1
  124. package/dist/benchmark/network-preset-e2e.d.ts +0 -2
  125. package/dist/benchmark/network-preset-e2e.d.ts.map +0 -1
  126. package/dist/benchmark/network-preset-e2e.js +0 -497
  127. package/dist/benchmark/network-preset-e2e.js.map +0 -1
  128. package/dist/benchmark/partial-sync.d.ts +0 -3
  129. package/dist/benchmark/partial-sync.d.ts.map +0 -1
  130. package/dist/benchmark/partial-sync.js +0 -121
  131. package/dist/benchmark/partial-sync.js.map +0 -1
  132. package/dist/benchmark/pid-convergence.d.ts +0 -2
  133. package/dist/benchmark/pid-convergence.d.ts.map +0 -1
  134. package/dist/benchmark/pid-convergence.js +0 -138
  135. package/dist/benchmark/pid-convergence.js.map +0 -1
  136. package/dist/benchmark/rateless-iblt-sender-startsync.d.ts +0 -2
  137. package/dist/benchmark/rateless-iblt-sender-startsync.d.ts.map +0 -1
  138. package/dist/benchmark/rateless-iblt-sender-startsync.js +0 -104
  139. package/dist/benchmark/rateless-iblt-sender-startsync.js.map +0 -1
  140. package/dist/benchmark/rateless-iblt-startsync-cache.d.ts +0 -2
  141. package/dist/benchmark/rateless-iblt-startsync-cache.d.ts.map +0 -1
  142. package/dist/benchmark/rateless-iblt-startsync-cache.js +0 -112
  143. package/dist/benchmark/rateless-iblt-startsync-cache.js.map +0 -1
  144. package/dist/benchmark/receive-prune.d.ts +0 -2
  145. package/dist/benchmark/receive-prune.d.ts.map +0 -1
  146. package/dist/benchmark/receive-prune.js +0 -818
  147. package/dist/benchmark/receive-prune.js.map +0 -1
  148. package/dist/benchmark/replication-prune.d.ts +0 -2
  149. package/dist/benchmark/replication-prune.d.ts.map +0 -1
  150. package/dist/benchmark/replication-prune.js +0 -105
  151. package/dist/benchmark/replication-prune.js.map +0 -1
  152. package/dist/benchmark/replication.d.ts +0 -2
  153. package/dist/benchmark/replication.d.ts.map +0 -1
  154. package/dist/benchmark/replication.js +0 -88
  155. package/dist/benchmark/replication.js.map +0 -1
  156. package/dist/benchmark/sync-batch-sweep.d.ts +0 -2
  157. package/dist/benchmark/sync-batch-sweep.d.ts.map +0 -1
  158. package/dist/benchmark/sync-batch-sweep.js +0 -353
  159. package/dist/benchmark/sync-batch-sweep.js.map +0 -1
  160. package/dist/benchmark/sync-catchup.d.ts +0 -2
  161. package/dist/benchmark/sync-catchup.d.ts.map +0 -1
  162. package/dist/benchmark/sync-catchup.js +0 -277
  163. package/dist/benchmark/sync-catchup.js.map +0 -1
  164. package/dist/benchmark/sync-phase-profile.d.ts +0 -2
  165. package/dist/benchmark/sync-phase-profile.d.ts.map +0 -1
  166. package/dist/benchmark/sync-phase-profile.js +0 -303
  167. package/dist/benchmark/sync-phase-profile.js.map +0 -1
  168. package/dist/benchmark/to-rebalance.d.ts +0 -2
  169. package/dist/benchmark/to-rebalance.d.ts.map +0 -1
  170. package/dist/benchmark/to-rebalance.js +0 -117
  171. package/dist/benchmark/to-rebalance.js.map +0 -1
  172. package/dist/benchmark/utils.d.ts +0 -24
  173. package/dist/benchmark/utils.d.ts.map +0 -1
  174. package/dist/benchmark/utils.js +0 -47
  175. package/dist/benchmark/utils.js.map +0 -1
@@ -0,0 +1,507 @@
1
+ const JOIN_WARMUP_SEND_SPACING_MS = 250;
2
+
3
+ /**
4
+ * One WarmupSession per join-warmup attempt window for a target. The
5
+ * instance IS the legacy opaque generation token — always compared by
6
+ * identity, never inspected — now carrying diagnostics. Rotation points are
7
+ * unchanged: created on demand, dropped at cancelJoinWarmupTarget so the
8
+ * next touch mints a fresh identity. Deliberately NOT hosted on
9
+ * PeerSession: warmup windows rotate independently of subscription epochs
10
+ * (poison and committed removals leave the subscription session alone), and
11
+ * a warmup target may have no session at all — auto-creating one as a
12
+ * warmup side effect would flip `isCurrent(peer, null)` from true to false
13
+ * at every receive-admission seat.
14
+ */
15
+ export class WarmupSession {
16
+ readonly target: string;
17
+ readonly createdAt = Date.now(); // diagnostics only
18
+
19
+ constructor(target: string) {
20
+ this.target = target;
21
+ }
22
+ }
23
+
24
+ export type JoinWarmupSendState<E> = {
25
+ bypassKnownPeerHints: boolean;
26
+ entries: Map<string, E>;
27
+ // Per-TARGET send state adopts the session it is currently draining for.
28
+ // lastCompletedAt spacing survives session rotation by design; do NOT
29
+ // move send state onto the session.
30
+ session: WarmupSession;
31
+ lastCompletedAt: number;
32
+ pending: boolean;
33
+ running: boolean;
34
+ };
35
+
36
+ export type JoinWarmupRetryTimer = {
37
+ handle: ReturnType<typeof setTimeout>;
38
+ resolve?: () => void;
39
+ };
40
+
41
+ export type JoinWarmupScheduledRetryBatch<E> = {
42
+ bypassKnownPeerHints: boolean;
43
+ entries: Map<string, E>;
44
+ remainingAttempts: number;
45
+ };
46
+
47
+ export type JoinWarmupScheduledRetryCohort<E> = {
48
+ batches: JoinWarmupScheduledRetryBatch<E>[];
49
+ dueAt: number;
50
+ };
51
+
52
+ export type JoinWarmupScheduledRetrySlot<E> = {
53
+ cohorts: JoinWarmupScheduledRetryCohort<E>[];
54
+ head: number;
55
+ timer?: JoinWarmupRetryTimer;
56
+ timerDueAt?: number;
57
+ };
58
+
59
+ export type JoinWarmupScheduledRetries<E> = {
60
+ session: WarmupSession;
61
+ slotsByDelay: Map<number, JoinWarmupScheduledRetrySlot<E>>;
62
+ };
63
+
64
+ export type JoinWarmupDeps<E> = {
65
+ isLifecycleActive: (controller: AbortController) => boolean;
66
+ getCurrentLifecycleController: () => AbortController;
67
+ getRepairRetryTimers: () => Set<ReturnType<typeof setTimeout>>;
68
+ isClosed: () => boolean;
69
+ onTargetCancelled: (target: string) => void;
70
+ bumpSimpleFallbackPasses: () => void;
71
+ sendEntriesSimple: (
72
+ target: string,
73
+ entries: ReadonlyMap<string, E>,
74
+ options: {
75
+ bypassKnownPeers: boolean;
76
+ bypassRecentKnownPeers: boolean;
77
+ isStillCurrent: () => boolean;
78
+ signal: AbortSignal;
79
+ },
80
+ ) => Promise<void>;
81
+ logError: (error: any) => void;
82
+ };
83
+
84
+ export class JoinWarmupCoordinator<E> {
85
+ _warmupSessionsByTarget!: Map<string, WarmupSession>;
86
+ _joinWarmupSendStateByTarget!: Map<string, JoinWarmupSendState<E>>;
87
+ _joinWarmupRetryTimersByTarget!: Map<string, Set<JoinWarmupRetryTimer>>;
88
+ _joinWarmupScheduledRetriesByTarget!: Map<
89
+ string,
90
+ JoinWarmupScheduledRetries<E>
91
+ >;
92
+ _repairSweepWarmupSessionByTarget!: Map<string, WarmupSession>;
93
+
94
+ constructor(private readonly deps: JoinWarmupDeps<E>) {
95
+ this.reset();
96
+ }
97
+
98
+ reset() {
99
+ this._warmupSessionsByTarget = new Map();
100
+ this._joinWarmupSendStateByTarget = new Map();
101
+ this._joinWarmupRetryTimersByTarget = new Map();
102
+ this._joinWarmupScheduledRetriesByTarget = new Map();
103
+ this._repairSweepWarmupSessionByTarget = new Map();
104
+ }
105
+
106
+ ensureWarmupSession(target: string): WarmupSession {
107
+ let session = this._warmupSessionsByTarget.get(target);
108
+ if (!session) {
109
+ session = new WarmupSession(target);
110
+ this._warmupSessionsByTarget.set(target, session);
111
+ }
112
+ return session;
113
+ }
114
+
115
+
116
+ clearRepairSweepWarmupSessions(): void {
117
+ this._repairSweepWarmupSessionByTarget.clear();
118
+ }
119
+
120
+ trackJoinWarmupTimer(target: string, timer: JoinWarmupRetryTimer) {
121
+ let timers = this._joinWarmupRetryTimersByTarget.get(target);
122
+ if (!timers) {
123
+ timers = new Set();
124
+ this._joinWarmupRetryTimersByTarget.set(target, timers);
125
+ }
126
+ timers.add(timer);
127
+ this.deps.getRepairRetryTimers().add(timer.handle);
128
+ }
129
+
130
+ untrackJoinWarmupTimer(target: string, timer: JoinWarmupRetryTimer) {
131
+ this.deps.getRepairRetryTimers().delete(timer.handle);
132
+ const timers = this._joinWarmupRetryTimersByTarget.get(target);
133
+ if (!timers) {
134
+ return;
135
+ }
136
+ timers.delete(timer);
137
+ if (timers.size === 0) {
138
+ this._joinWarmupRetryTimersByTarget.delete(target);
139
+ }
140
+ }
141
+
142
+ cancelJoinWarmupTimers(target: string) {
143
+ const timers = this._joinWarmupRetryTimersByTarget.get(target);
144
+ if (!timers) {
145
+ return;
146
+ }
147
+ for (const timer of [...timers]) {
148
+ clearTimeout(timer.handle);
149
+ timer.resolve?.();
150
+ this.untrackJoinWarmupTimer(target, timer);
151
+ }
152
+ }
153
+
154
+ cancelJoinWarmupTarget(target: string) {
155
+ this._warmupSessionsByTarget.delete(target);
156
+ this.deps.onTargetCancelled(target);
157
+ this._repairSweepWarmupSessionByTarget.delete(target);
158
+ this.cancelJoinWarmupTimers(target);
159
+ this._joinWarmupScheduledRetriesByTarget.delete(target);
160
+ const state = this._joinWarmupSendStateByTarget.get(target);
161
+ if (!state) {
162
+ return;
163
+ }
164
+ state.bypassKnownPeerHints = false;
165
+ state.entries.clear();
166
+ state.pending = false;
167
+ if (!state.running) {
168
+ this._joinWarmupSendStateByTarget.delete(target);
169
+ }
170
+ }
171
+
172
+ cancelAllJoinWarmupTargets() {
173
+ const targets = new Set([
174
+ ...this._warmupSessionsByTarget.keys(),
175
+ ...this._joinWarmupRetryTimersByTarget.keys(),
176
+ ...this._joinWarmupScheduledRetriesByTarget.keys(),
177
+ ...this._joinWarmupSendStateByTarget.keys(),
178
+ ]);
179
+ for (const target of targets) {
180
+ this.cancelJoinWarmupTarget(target);
181
+ }
182
+ }
183
+
184
+ async sleepJoinWarmupTracked(
185
+ target: string,
186
+ delayMs: number,
187
+ repairLifecycleController: AbortController,
188
+ ) {
189
+ if (delayMs <= 0) {
190
+ return;
191
+ }
192
+ await new Promise<void>((resolve) => {
193
+ let settled = false;
194
+ let trackedTimer!: JoinWarmupRetryTimer;
195
+ const settle = () => {
196
+ if (settled) {
197
+ return;
198
+ }
199
+ settled = true;
200
+ if (
201
+ repairLifecycleController ===
202
+ this.deps.getCurrentLifecycleController()
203
+ ) {
204
+ this.untrackJoinWarmupTimer(target, trackedTimer);
205
+ }
206
+ resolve();
207
+ };
208
+ const handle = setTimeout(settle, delayMs);
209
+ handle.unref?.();
210
+ trackedTimer = { handle, resolve: settle };
211
+ this.trackJoinWarmupTimer(target, trackedTimer);
212
+ });
213
+ }
214
+
215
+ scheduleJoinWarmupRetries(
216
+ target: string,
217
+ session: WarmupSession,
218
+ delaysMs: Iterable<number>,
219
+ entries: ReadonlyMap<string, E>,
220
+ bypassKnownPeerHints: boolean,
221
+ repairLifecycleController: AbortController = this.deps.getCurrentLifecycleController(),
222
+ ) {
223
+ if (
224
+ !this.deps.isLifecycleActive(repairLifecycleController) ||
225
+ this._warmupSessionsByTarget.get(target) !== session
226
+ ) {
227
+ return;
228
+ }
229
+ let scheduled = this._joinWarmupScheduledRetriesByTarget.get(target);
230
+ if (scheduled?.session !== session) {
231
+ this.cancelJoinWarmupTimers(target);
232
+ this._joinWarmupScheduledRetriesByTarget.delete(target);
233
+ scheduled = undefined;
234
+ }
235
+ if (!scheduled) {
236
+ scheduled = {
237
+ session,
238
+ slotsByDelay: new Map(),
239
+ };
240
+ this._joinWarmupScheduledRetriesByTarget.set(target, scheduled);
241
+ }
242
+ const delays = [...new Set(delaysMs)];
243
+ const batch: JoinWarmupScheduledRetryBatch<E> = {
244
+ bypassKnownPeerHints,
245
+ entries: new Map(entries),
246
+ remainingAttempts: delays.length,
247
+ };
248
+ const now = Date.now();
249
+ for (const delayMs of delays) {
250
+ let slot = scheduled.slotsByDelay.get(delayMs);
251
+ if (!slot) {
252
+ slot = { cohorts: [], head: 0 };
253
+ scheduled.slotsByDelay.set(delayMs, slot);
254
+ }
255
+ const tail = slot.cohorts.at(-1);
256
+ const dueAt = Math.max(tail?.dueAt ?? 0, now + delayMs);
257
+ if (tail?.dueAt === dueAt) {
258
+ tail.batches.push(batch);
259
+ } else {
260
+ slot.cohorts.push({
261
+ batches: [batch],
262
+ dueAt,
263
+ });
264
+ }
265
+ this.armJoinWarmupRetrySlot(
266
+ target,
267
+ scheduled,
268
+ delayMs,
269
+ slot,
270
+ repairLifecycleController,
271
+ );
272
+ }
273
+ }
274
+
275
+ armJoinWarmupRetrySlot(
276
+ target: string,
277
+ scheduled: JoinWarmupScheduledRetries<E>,
278
+ delayMs: number,
279
+ slot: JoinWarmupScheduledRetrySlot<E>,
280
+ repairLifecycleController: AbortController,
281
+ ) {
282
+ if (!this.deps.isLifecycleActive(repairLifecycleController)) {
283
+ return;
284
+ }
285
+ const nextDueAt = slot.cohorts[slot.head]?.dueAt;
286
+ if (nextDueAt == null) {
287
+ return;
288
+ }
289
+ if (slot.timer && slot.timerDueAt === nextDueAt) {
290
+ return;
291
+ }
292
+ if (slot.timer) {
293
+ clearTimeout(slot.timer.handle);
294
+ this.untrackJoinWarmupTimer(target, slot.timer);
295
+ }
296
+ let trackedTimer!: JoinWarmupRetryTimer;
297
+ const handle = setTimeout(
298
+ () => {
299
+ if (!this.deps.isLifecycleActive(repairLifecycleController)) {
300
+ return;
301
+ }
302
+ this.untrackJoinWarmupTimer(target, trackedTimer);
303
+ if (slot.timer !== trackedTimer) {
304
+ return;
305
+ }
306
+ slot.timer = undefined;
307
+ slot.timerDueAt = undefined;
308
+ const current = this._joinWarmupScheduledRetriesByTarget.get(target);
309
+ if (
310
+ current !== scheduled ||
311
+ current.slotsByDelay.get(delayMs) !== slot
312
+ ) {
313
+ return;
314
+ }
315
+
316
+ const dueEntries = new Map<string, E>();
317
+ let bypassKnownPeerHints = false;
318
+ const now = Date.now();
319
+ while (
320
+ slot.head < slot.cohorts.length &&
321
+ slot.cohorts[slot.head].dueAt <= now
322
+ ) {
323
+ const cohort = slot.cohorts[slot.head++];
324
+ for (const batch of cohort.batches) {
325
+ for (const [hash, entry] of batch.entries) {
326
+ dueEntries.set(hash, entry);
327
+ }
328
+ bypassKnownPeerHints ||= batch.bypassKnownPeerHints;
329
+ batch.remainingAttempts -= 1;
330
+ if (batch.remainingAttempts === 0) {
331
+ batch.entries.clear();
332
+ }
333
+ }
334
+ cohort.batches.length = 0;
335
+ }
336
+ if (
337
+ dueEntries.size > 0 &&
338
+ !this.deps.isClosed() &&
339
+ this._warmupSessionsByTarget.get(target) === scheduled.session
340
+ ) {
341
+ this.queueJoinWarmupSend(
342
+ target,
343
+ scheduled.session,
344
+ dueEntries,
345
+ bypassKnownPeerHints,
346
+ repairLifecycleController,
347
+ );
348
+ }
349
+ if (slot.head === slot.cohorts.length) {
350
+ current.slotsByDelay.delete(delayMs);
351
+ if (current.slotsByDelay.size === 0) {
352
+ this._joinWarmupScheduledRetriesByTarget.delete(target);
353
+ }
354
+ return;
355
+ }
356
+ if (slot.head >= 1_024 && slot.head * 2 >= slot.cohorts.length) {
357
+ slot.cohorts = slot.cohorts.slice(slot.head);
358
+ slot.head = 0;
359
+ }
360
+ this.armJoinWarmupRetrySlot(
361
+ target,
362
+ current,
363
+ delayMs,
364
+ slot,
365
+ repairLifecycleController,
366
+ );
367
+ },
368
+ Math.max(0, nextDueAt - Date.now()),
369
+ );
370
+ handle.unref?.();
371
+ trackedTimer = { handle };
372
+ slot.timer = trackedTimer;
373
+ slot.timerDueAt = nextDueAt;
374
+ this.trackJoinWarmupTimer(target, trackedTimer);
375
+ }
376
+
377
+ queueJoinWarmupSend(
378
+ target: string,
379
+ session: WarmupSession,
380
+ entries: ReadonlyMap<string, E>,
381
+ bypassKnownPeerHints: boolean,
382
+ repairLifecycleController: AbortController = this.deps.getCurrentLifecycleController(),
383
+ ) {
384
+ if (
385
+ !this.deps.isLifecycleActive(repairLifecycleController) ||
386
+ this._warmupSessionsByTarget.get(target) !== session
387
+ ) {
388
+ return;
389
+ }
390
+ let state = this._joinWarmupSendStateByTarget.get(target);
391
+ if (!state) {
392
+ state = {
393
+ bypassKnownPeerHints: false,
394
+ entries: new Map(),
395
+ session,
396
+ lastCompletedAt: Number.NEGATIVE_INFINITY,
397
+ pending: false,
398
+ running: false,
399
+ };
400
+ this._joinWarmupSendStateByTarget.set(target, state);
401
+ } else if (state.session !== session) {
402
+ state.bypassKnownPeerHints = false;
403
+ state.entries.clear();
404
+ state.pending = false;
405
+ }
406
+ for (const [hash, entry] of entries) {
407
+ state.entries.set(hash, entry);
408
+ }
409
+ state.bypassKnownPeerHints ||= bypassKnownPeerHints;
410
+ state.session = session;
411
+ state.pending = true;
412
+ if (state.running) {
413
+ return;
414
+ }
415
+ void this.drainJoinWarmupSends(
416
+ target,
417
+ state,
418
+ repairLifecycleController,
419
+ ).catch((error: any) => {
420
+ if (this.deps.isLifecycleActive(repairLifecycleController)) {
421
+ this.deps.logError(error);
422
+ }
423
+ });
424
+ }
425
+
426
+ async drainJoinWarmupSends(
427
+ target: string,
428
+ state: JoinWarmupSendState<E>,
429
+ repairLifecycleController: AbortController,
430
+ ) {
431
+ if (
432
+ state.running ||
433
+ !this.deps.isLifecycleActive(repairLifecycleController)
434
+ ) {
435
+ return;
436
+ }
437
+ state.running = true;
438
+ try {
439
+ while (state.pending) {
440
+ if (!this.deps.isLifecycleActive(repairLifecycleController)) {
441
+ return;
442
+ }
443
+ state.pending = false;
444
+ const session = state.session;
445
+ const entries = new Map(state.entries);
446
+ state.entries.clear();
447
+ const bypassKnownPeerHints = state.bypassKnownPeerHints;
448
+ state.bypassKnownPeerHints = false;
449
+ const spacingMs = Math.max(
450
+ 0,
451
+ state.lastCompletedAt + JOIN_WARMUP_SEND_SPACING_MS - Date.now(),
452
+ );
453
+ await this.sleepJoinWarmupTracked(
454
+ target,
455
+ spacingMs,
456
+ repairLifecycleController,
457
+ );
458
+ if (
459
+ !this.deps.isLifecycleActive(repairLifecycleController) ||
460
+ state.session !== session ||
461
+ this._warmupSessionsByTarget.get(target) !== session
462
+ ) {
463
+ continue;
464
+ }
465
+ if (entries.size === 0) {
466
+ continue;
467
+ }
468
+ this.deps.bumpSimpleFallbackPasses();
469
+ try {
470
+ await this.deps.sendEntriesSimple(target, entries, {
471
+ bypassKnownPeers: bypassKnownPeerHints,
472
+ bypassRecentKnownPeers: bypassKnownPeerHints,
473
+ isStillCurrent: () =>
474
+ this.deps.isLifecycleActive(repairLifecycleController),
475
+ signal: repairLifecycleController.signal,
476
+ });
477
+ } catch (error: any) {
478
+ if (this.deps.isLifecycleActive(repairLifecycleController)) {
479
+ this.deps.logError(error);
480
+ }
481
+ } finally {
482
+ if (this.deps.isLifecycleActive(repairLifecycleController)) {
483
+ state.lastCompletedAt = Date.now();
484
+ }
485
+ }
486
+ }
487
+ } finally {
488
+ state.running = false;
489
+ if (this._joinWarmupSendStateByTarget.get(target) === state) {
490
+ const currentRepairLifecycleController =
491
+ this.deps.getCurrentLifecycleController();
492
+ if (
493
+ state.pending &&
494
+ this.deps.isLifecycleActive(currentRepairLifecycleController)
495
+ ) {
496
+ void this.drainJoinWarmupSends(
497
+ target,
498
+ state,
499
+ currentRepairLifecycleController,
500
+ ).catch((error: any) => this.deps.logError(error));
501
+ } else if (!this._warmupSessionsByTarget.has(target)) {
502
+ this._joinWarmupSendStateByTarget.delete(target);
503
+ }
504
+ }
505
+ }
506
+ }
507
+ }
@@ -0,0 +1,61 @@
1
+ import { Cache } from "@peerbit/cache";
2
+
3
+ type LeaderMap = Map<string, { intersecting: boolean }>;
4
+
5
+ const cloneLeaders = (leaders: LeaderMap): LeaderMap => {
6
+ const cloned: LeaderMap = new Map();
7
+ for (const [hash, meta] of leaders) {
8
+ cloned.set(hash, { intersecting: meta.intersecting });
9
+ }
10
+ return cloned;
11
+ };
12
+
13
+ /**
14
+ * Memoizes leader-election plans between replication-topology changes.
15
+ *
16
+ * Coherence is structural, not temporal: a private monotonic version is
17
+ * bumped (and the store wiped) on every invalidation, and an async fill is
18
+ * only stored when the version captured before the computation still holds —
19
+ * a plan computed across a topology change is returned to its caller but
20
+ * never cached. The TTL bounds wall-clock maturity flips that fire no event
21
+ * (a range re-crossing maturity after the dynamic roleAge regrew has no
22
+ * pending timer), so it is load-bearing for that edge.
23
+ *
24
+ * Callers mutate returned maps and their value objects, so every hit and
25
+ * every store clones.
26
+ */
27
+ export class LeaderPlanCache {
28
+ private cache: Cache<{ leaders: LeaderMap }>;
29
+ private version = 0;
30
+
31
+ constructor(options: { max: number; ttl: number }) {
32
+ this.cache = new Cache(options);
33
+ }
34
+
35
+ invalidate(): void {
36
+ this.version++;
37
+ this.cache.clear();
38
+ }
39
+
40
+ get(key: string): LeaderMap | undefined {
41
+ const entry = this.cache.get(key);
42
+ return entry ? cloneLeaders(entry.leaders) : undefined;
43
+ }
44
+
45
+ /** Capture the version before an awaited plan computation. */
46
+ capture(): number {
47
+ return this.version;
48
+ }
49
+
50
+ /** Store only if no invalidation happened since `capturedVersion`. */
51
+ put(key: string, leaders: LeaderMap, capturedVersion: number): void {
52
+ if (capturedVersion !== this.version) {
53
+ return;
54
+ }
55
+ this.cache.add(
56
+ key,
57
+ { leaders: cloneLeaders(leaders) },
58
+ Math.max(1, leaders.size),
59
+ );
60
+ }
61
+ }