@nxtedition/scheduler 3.0.3 → 3.0.5
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/LICENSE +21 -0
- package/README.md +162 -0
- package/lib/index.js +10 -1
- package/package.json +12 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) nxtedition
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# @nxtedition/scheduler
|
|
2
|
+
|
|
3
|
+
A high-performance, priority-based task scheduler for Node.js with support for concurrency limiting and multi-worker coordination via `SharedArrayBuffer`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @nxtedition/scheduler
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { Scheduler } from '@nxtedition/scheduler'
|
|
17
|
+
|
|
18
|
+
const scheduler = new Scheduler({ concurrency: 4 })
|
|
19
|
+
|
|
20
|
+
const result = await scheduler.run(async () => {
|
|
21
|
+
const res = await fetch('https://example.com')
|
|
22
|
+
return res.json()
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Priority
|
|
27
|
+
|
|
28
|
+
Seven priority levels are available:
|
|
29
|
+
|
|
30
|
+
| Name | Value |
|
|
31
|
+
| --------- | ----- |
|
|
32
|
+
| `lowest` | -3 |
|
|
33
|
+
| `lower` | -2 |
|
|
34
|
+
| `low` | -1 |
|
|
35
|
+
| `normal` | 0 |
|
|
36
|
+
| `high` | 1 |
|
|
37
|
+
| `higher` | 2 |
|
|
38
|
+
| `highest` | 3 |
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// Using string priority
|
|
42
|
+
await scheduler.run(() => importantWork(), 'high')
|
|
43
|
+
|
|
44
|
+
// Using static constants
|
|
45
|
+
await scheduler.run(() => backgroundWork(), Scheduler.LOW)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Low-Level API
|
|
49
|
+
|
|
50
|
+
For more control, use `acquire` / `release` directly:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
scheduler.acquire(
|
|
54
|
+
(opaque) => {
|
|
55
|
+
try {
|
|
56
|
+
doWork(opaque)
|
|
57
|
+
} finally {
|
|
58
|
+
scheduler.release()
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
Scheduler.NORMAL,
|
|
62
|
+
opaqueData,
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Multi-Worker Coordination
|
|
67
|
+
|
|
68
|
+
Share a concurrency limit across worker threads using `SharedArrayBuffer`:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
// Main thread
|
|
72
|
+
import { Scheduler } from '@nxtedition/scheduler'
|
|
73
|
+
import { Worker } from 'node:worker_threads'
|
|
74
|
+
|
|
75
|
+
const sharedState = Scheduler.makeSharedState(8)
|
|
76
|
+
const worker = new Worker('./worker.js', { workerData: sharedState })
|
|
77
|
+
|
|
78
|
+
// Worker thread
|
|
79
|
+
import { Scheduler } from '@nxtedition/scheduler'
|
|
80
|
+
import { workerData } from 'node:worker_threads'
|
|
81
|
+
|
|
82
|
+
const scheduler = new Scheduler(workerData)
|
|
83
|
+
await scheduler.run(() => work())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### UV Thread Pool Scheduling
|
|
87
|
+
|
|
88
|
+
A practical use case is coordinating access to the libuv thread pool (`UV_THREADPOOL_SIZE`) across multiple worker threads. For example, several HTTP file-serving workers can share a scheduler so that file-system operations (which consume UV thread pool slots) are prioritized and throttled globally:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// main.js
|
|
92
|
+
import { Scheduler } from '@nxtedition/scheduler'
|
|
93
|
+
import { Worker } from 'node:worker_threads'
|
|
94
|
+
|
|
95
|
+
const UV_THREADPOOL_SIZE = parseInt(process.env.UV_THREADPOOL_SIZE || '4', 10)
|
|
96
|
+
const sharedState = Scheduler.makeSharedState(UV_THREADPOOL_SIZE)
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < 4; i++) {
|
|
99
|
+
new Worker('./http-worker.js', { workerData: { sharedState } })
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// http-worker.js
|
|
105
|
+
import { Scheduler, parsePriority } from '@nxtedition/scheduler'
|
|
106
|
+
import { workerData } from 'node:worker_threads'
|
|
107
|
+
import fs from 'node:fs/promises'
|
|
108
|
+
|
|
109
|
+
const scheduler = new Scheduler(workerData.sharedState)
|
|
110
|
+
|
|
111
|
+
async function handleRequest(req, res) {
|
|
112
|
+
// Derive priority from a request header, e.g. "X-Priority: high"
|
|
113
|
+
const priority = parsePriority(req.headers['x-priority'] || 'normal')
|
|
114
|
+
|
|
115
|
+
const data = await scheduler.run(() => fs.readFile(filePath), priority)
|
|
116
|
+
res.end(data)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This ensures that high-priority requests get file-system access first, while low-priority background work (thumbnails, transcoding, etc.) yields thread pool capacity without starving entirely — thanks to the built-in starvation prevention.
|
|
121
|
+
|
|
122
|
+
### Monitoring
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
const { running, pending, deferred, queues } = scheduler.stats
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- `running` — currently executing tasks
|
|
129
|
+
- `pending` — tasks waiting in queues
|
|
130
|
+
- `deferred` — total tasks that were queued (lifetime counter)
|
|
131
|
+
- `queues` — per-priority queue counts
|
|
132
|
+
|
|
133
|
+
## API
|
|
134
|
+
|
|
135
|
+
### `new Scheduler(opts)`
|
|
136
|
+
|
|
137
|
+
- `opts.concurrency` — max concurrent tasks (default: `Infinity`)
|
|
138
|
+
- `opts` may also be a `SharedArrayBuffer` created by `Scheduler.makeSharedState()`
|
|
139
|
+
|
|
140
|
+
### `scheduler.run(fn, priority?, opaque?): Promise<T>`
|
|
141
|
+
|
|
142
|
+
Execute `fn` within the scheduler. Returns a promise that resolves with the return value of `fn`.
|
|
143
|
+
|
|
144
|
+
### `scheduler.acquire(fn, priority?, opaque?): void`
|
|
145
|
+
|
|
146
|
+
Low-level task acquisition. You **must** call `scheduler.release()` when done.
|
|
147
|
+
|
|
148
|
+
### `scheduler.release(): void`
|
|
149
|
+
|
|
150
|
+
Signal task completion. Dequeues the next pending task if concurrency allows.
|
|
151
|
+
|
|
152
|
+
### `Scheduler.makeSharedState(concurrency): SharedArrayBuffer`
|
|
153
|
+
|
|
154
|
+
Create shared state for cross-worker scheduling.
|
|
155
|
+
|
|
156
|
+
### `parsePriority(value): number`
|
|
157
|
+
|
|
158
|
+
Parse a string or number into a normalized priority value.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
package/lib/index.js
CHANGED
|
@@ -145,12 +145,21 @@ export class Scheduler {
|
|
|
145
145
|
const queue = this.#queues[p + 3]
|
|
146
146
|
|
|
147
147
|
if (this.#stateView) {
|
|
148
|
-
|
|
148
|
+
// Make sure we are always running at least one local job even if we might globally over subscribe.
|
|
149
|
+
if (this.#running < 1) {
|
|
149
150
|
Atomics.add(this.#stateView, RUNNING_INDEX, 1)
|
|
150
151
|
this.#running += 1
|
|
151
152
|
fn(opaque)
|
|
152
153
|
return
|
|
153
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
|
+
}
|
|
154
163
|
} else if ((this.#running < 1 && this.#concurrency > 0) || this.#running < this.#concurrency) {
|
|
155
164
|
this.#running += 1
|
|
156
165
|
fn(opaque)
|
package/package.json
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/scheduler",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
-
"lib"
|
|
8
|
+
"lib",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
9
11
|
],
|
|
10
|
-
"license": "
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
11
16
|
"scripts": {
|
|
12
17
|
"build": "rimraf lib && tsc && amaroc ./src/index.ts && mv src/index.js lib/",
|
|
13
18
|
"prepublishOnly": "yarn build",
|
|
14
19
|
"typecheck": "tsc --noEmit",
|
|
15
|
-
"test": "node --test",
|
|
16
|
-
"test:ci": "node --test"
|
|
20
|
+
"test": "yarn build && node --test",
|
|
21
|
+
"test:ci": "yarn build && node --test"
|
|
17
22
|
},
|
|
18
23
|
"devDependencies": {
|
|
19
24
|
"@types/node": "^25.2.3",
|
|
@@ -21,5 +26,6 @@
|
|
|
21
26
|
"oxlint-tsgolint": "^0.12.2",
|
|
22
27
|
"rimraf": "^6.1.2",
|
|
23
28
|
"typescript": "^5.9.3"
|
|
24
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"gitHead": "6174bfadecfd6232d4fd5c526fecf17d24528ff3"
|
|
25
31
|
}
|