@nxtedition/scheduler 3.0.4 → 3.0.6

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 (2) hide show
  1. package/lib/index.js +38 -34
  2. package/package.json +4 -3
package/lib/index.js CHANGED
@@ -1,15 +1,8 @@
1
- import os from 'node:os'
2
-
3
1
  const RUNNING_INDEX = 0
4
2
  const CONCURRENCY_INDEX = 1
5
3
 
6
4
  const maxInt = 2147483647
7
5
 
8
- // On x86/x64, aligned 32-bit reads do not tear, so a plain array access is safe
9
- // and avoids the overhead of Atomics.load() in V8. On other architectures (e.g. ARM),
10
- // use Atomics.load() to prevent tearing.
11
- const useAtomicLoad = os.arch() !== 'x64' && os.arch() !== 'ia32'
12
-
13
6
  class FastQueue {
14
7
  idx = 0
15
8
  cnt = 0
@@ -110,17 +103,17 @@ export class Scheduler {
110
103
  throw new Error('Invalid concurrency')
111
104
  }
112
105
  const stateBuffer = new SharedArrayBuffer(64)
113
- const stateView = new Uint32Array(stateBuffer)
106
+ const stateView = new Int32Array(stateBuffer)
114
107
  Atomics.store(stateView, CONCURRENCY_INDEX, concurrency ?? 0)
115
108
  return stateBuffer
116
109
  }
117
110
 
118
111
  constructor(opts ) {
119
112
  if (opts instanceof SharedArrayBuffer) {
120
- this.#stateView = new Uint32Array(opts)
113
+ this.#stateView = new Int32Array(opts)
121
114
  this.#concurrency = Atomics.load(this.#stateView, CONCURRENCY_INDEX) || Infinity
122
115
  } else {
123
- this.#concurrency = opts?.concurrency || Infinity
116
+ this.#concurrency = opts?.concurrency ?? Infinity
124
117
  }
125
118
  }
126
119
 
@@ -152,22 +145,21 @@ export class Scheduler {
152
145
  const queue = this.#queues[p + 3]
153
146
 
154
147
  if (this.#stateView) {
155
- // NOTE: The read of stateView followed by Atomics.add is a TOCTOU race. Multiple
156
- // workers may simultaneously read a value below the concurrency limit and all
157
- // proceed, briefly exceeding it by up to N-1 (where N is the number of workers).
158
- // This is by design — the concurrency limit is a soft / best-effort constraint.
159
- // Small, transient over-subscriptions are acceptable and self-correcting on the
160
- // next release() cycle.
161
- // Plain array read on x86 (aligned 32-bit reads don't tear); Atomics.load elsewhere.
162
- const running = useAtomicLoad
163
- ? Atomics.load(this.#stateView, RUNNING_INDEX)
164
- : this.#stateView[RUNNING_INDEX]
165
- if (this.#running < 1 || running < this.#concurrency) {
148
+ // Make sure we are always running at least one local job even if we might globally over subscribe.
149
+ if (this.#running < 1) {
166
150
  Atomics.add(this.#stateView, RUNNING_INDEX, 1)
167
151
  this.#running += 1
168
152
  fn(opaque)
169
153
  return
170
154
  }
155
+
156
+ if (Atomics.add(this.#stateView, RUNNING_INDEX, 1) >= this.#concurrency) {
157
+ Atomics.sub(this.#stateView, RUNNING_INDEX, 1)
158
+ } else {
159
+ this.#running += 1
160
+ fn(opaque)
161
+ return
162
+ }
171
163
  } else if ((this.#running < 1 && this.#concurrency > 0) || this.#running < this.#concurrency) {
172
164
  this.#running += 1
173
165
  fn(opaque)
@@ -182,10 +174,16 @@ export class Scheduler {
182
174
  }
183
175
 
184
176
  release() {
185
- let running = this.#stateView
186
- ? Atomics.sub(this.#stateView, RUNNING_INDEX, 1) - 1
187
- : this.#running - 1
188
- this.#running -= 1
177
+ let running
178
+ if (this.#running > 0) {
179
+ running = this.#stateView
180
+ ? Atomics.sub(this.#stateView, RUNNING_INDEX, 1) - 1
181
+ : this.#running - 1
182
+ this.#running -= 1
183
+ } else {
184
+ // Gracefully handle user error...
185
+ running = this.#stateView ? Atomics.load(this.#stateView, RUNNING_INDEX) : this.#running
186
+ }
189
187
 
190
188
  if (this.#pending === 0 || this.#releasing) {
191
189
  return
@@ -230,33 +228,39 @@ export class Scheduler {
230
228
  throw new Error('Invariant violation: pending > 0 but no tasks in queues')
231
229
  }
232
230
 
233
- const fn = queue.arr[queue.idx++]
234
- const opaque = queue.arr[queue.idx++]
231
+ const fn = queue.arr[queue.idx]
232
+ queue.arr[queue.idx++] = null
233
+ const opaque = queue.arr[queue.idx]
234
+ queue.arr[queue.idx++] = null
235
235
  queue.cnt -= 1
236
236
 
237
237
  if (queue.cnt === 0) {
238
238
  queue.idx = 0
239
239
  queue.arr.length = 0
240
240
  } else if (queue.idx > 1024) {
241
- queue.idx = 0
242
241
  queue.arr.splice(0, queue.idx)
242
+ queue.idx = 0
243
243
  }
244
244
 
245
- running = this.#stateView
246
- ? Atomics.add(this.#stateView, RUNNING_INDEX, 1) + 1
247
- : this.#running + 1
248
-
249
- this.#counter += 1
250
245
  this.#pending -= 1
251
246
  this.#running += 1
247
+
248
+ if (this.#stateView) {
249
+ Atomics.add(this.#stateView, RUNNING_INDEX, 1)
250
+ }
251
+
252
252
  fn(opaque)
253
+
254
+ // Re-read running after fn() in case it synchronously called release()
255
+ running = this.#stateView ? Atomics.load(this.#stateView, RUNNING_INDEX) : this.#running
253
256
  }
254
- this.#releasing = false
255
257
  } catch (err) {
256
258
  // Throwing here is undefined behavior...
257
259
  queueMicrotask(() => {
258
260
  throw new Error('Scheduler task error', { cause: err })
259
261
  })
262
+ } finally {
263
+ this.#releasing = false
260
264
  }
261
265
  }
262
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/scheduler",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -18,7 +18,8 @@
18
18
  "prepublishOnly": "yarn build",
19
19
  "typecheck": "tsc --noEmit",
20
20
  "test": "yarn build && node --test",
21
- "test:ci": "yarn build && node --test"
21
+ "test:ci": "yarn build && node --test",
22
+ "test:coverage": "npx tsx --test --experimental-test-coverage src/index.test.ts"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@types/node": "^25.2.3",
@@ -27,5 +28,5 @@
27
28
  "rimraf": "^6.1.2",
28
29
  "typescript": "^5.9.3"
29
30
  },
30
- "gitHead": "17807cefcac092e20ebf99befddf0e742e5fc0e2"
31
+ "gitHead": "80ace3245dc851f38a288b0c3b167ca0024f0469"
31
32
  }