@lodestar/beacon-node 1.40.0-rc.2 → 1.40.0

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.
@@ -24,6 +24,8 @@ export class JobItemQueue<Args extends any[], R> {
24
24
  private readonly metrics?: QueueMetrics;
25
25
  private runningJobs = 0;
26
26
  private lastYield = 0;
27
+ /** Resolvers waiting for space in the queue */
28
+ private spaceWaiters: (() => void)[] = [];
27
29
 
28
30
  constructor(
29
31
  private readonly itemProcessor: (...args: Args) => Promise<R>,
@@ -72,12 +74,57 @@ export class JobItemQueue<Args extends any[], R> {
72
74
  });
73
75
  }
74
76
 
77
+ /**
78
+ * Returns a promise that resolves when there is space in the queue.
79
+ * If the queue already has space, resolves immediately (noop).
80
+ * Use this to apply backpressure when the caller should wait rather than
81
+ * have push() throw QUEUE_MAX_LENGTH.
82
+ */
83
+ async waitForSpace(): Promise<void> {
84
+ if (this.opts.signal.aborted) {
85
+ throw new QueueError({code: QueueErrorCode.QUEUE_ABORTED});
86
+ }
87
+
88
+ if (this.jobs.length < this.opts.maxLength) {
89
+ return;
90
+ }
91
+
92
+ return new Promise<void>((resolve, reject) => {
93
+ let settled = false;
94
+
95
+ const onAbort = (): void => {
96
+ if (settled) return;
97
+ settled = true;
98
+ const index = this.spaceWaiters.indexOf(wrappedResolve);
99
+ if (index >= 0) {
100
+ this.spaceWaiters.splice(index, 1);
101
+ }
102
+ reject(new QueueError({code: QueueErrorCode.QUEUE_ABORTED}));
103
+ };
104
+
105
+ const wrappedResolve = (): void => {
106
+ if (settled) return;
107
+ settled = true;
108
+ this.opts.signal.removeEventListener("abort", onAbort);
109
+ resolve();
110
+ };
111
+
112
+ this.spaceWaiters.push(wrappedResolve);
113
+ this.opts.signal.addEventListener("abort", onAbort, {once: true});
114
+
115
+ // Re-check after attaching listener to close the race window where
116
+ // signal.abort() fires between the initial check and addEventListener
117
+ if (this.opts.signal.aborted) onAbort();
118
+ });
119
+ }
120
+
75
121
  getItems(): {args: Args; addedTimeMs: number}[] {
76
122
  return this.jobs.map((job) => ({args: job.args, addedTimeMs: job.addedTimeMs}));
77
123
  }
78
124
 
79
125
  dropAllJobs = (): void => {
80
126
  this.jobs.clear();
127
+ this.notifySpaceWaiters();
81
128
  };
82
129
 
83
130
  private runJob = async (): Promise<void> => {
@@ -115,10 +162,25 @@ export class JobItemQueue<Args extends any[], R> {
115
162
 
116
163
  this.runningJobs = Math.max(0, this.runningJobs - 1);
117
164
 
165
+ // Notify any waiters that space is available
166
+ this.notifySpaceWaiters();
167
+
118
168
  // Potentially run a new job
119
169
  void this.runJob();
120
170
  };
121
171
 
172
+ private notifySpaceWaiters(): void {
173
+ // Compute available slots once to avoid thundering herd: resolved waiters
174
+ // won't push() until the next microtask, so jobs.length doesn't change
175
+ // inside this loop. Without the cap we'd wake ALL waiters on a single slot.
176
+ let available = this.opts.maxLength - this.jobs.length;
177
+ while (available > 0 && this.spaceWaiters.length > 0) {
178
+ const resolve = this.spaceWaiters.shift();
179
+ if (resolve) resolve();
180
+ available--;
181
+ }
182
+ }
183
+
122
184
  private abortAllJobs = (): void => {
123
185
  while (this.jobs.length > 0) {
124
186
  const job = this.jobs.pop();