@nxtedition/scheduler 2.0.3 → 3.0.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.
Files changed (3) hide show
  1. package/lib/index.d.ts +15 -10
  2. package/lib/index.js +138 -65
  3. package/package.json +2 -2
package/lib/index.d.ts CHANGED
@@ -1,24 +1,29 @@
1
+ export type Priority = -3 | -2 | -1 | 0 | 1 | 2 | 3 | 'lowest' | 'lower' | 'low' | 'normal' | 'high' | 'higher' | 'highest';
2
+ export declare function parsePriority(p: Priority | string | number): number;
1
3
  export declare class Scheduler {
2
4
  #private;
3
- static LOW: 0;
4
- static NORMAL: 1;
5
- static HIGH: 2;
5
+ static LOWEST: Priority;
6
+ static LOWER: Priority;
7
+ static LOW: Priority;
8
+ static NORMAL: Priority;
9
+ static HIGH: Priority;
10
+ static HIGHER: Priority;
11
+ static HIGHEST: Priority;
6
12
  get concurrency(): number;
7
13
  get stats(): {
8
14
  deferred: number;
9
15
  running: number;
10
16
  pending: number;
11
- lowCount: number;
12
- normalCount: number;
13
- highCount: number;
14
- lowLimit: number;
15
- normalLimit: number;
16
- highLimit: number;
17
+ queues: {
18
+ count: number;
19
+ age: number;
20
+ }[];
17
21
  };
18
22
  static makeSharedState(concurrency: number): SharedArrayBuffer;
19
23
  constructor(opts: SharedArrayBuffer | {
20
24
  concurrency?: number;
21
25
  });
22
- acquire(fn: (opaque?: any) => any, priority?: 0 | 1 | 2 | 'low' | 'normal' | 'high', opaque?: any): any;
26
+ run<T, U>(fn: (opaque?: U) => Promise<T> | T, priority?: Priority, opaque?: U): Promise<T>;
27
+ acquire<U>(fn: (opaque?: U) => unknown, priority?: Priority, opaque?: U): void;
23
28
  release(): void;
24
29
  }
package/lib/index.js CHANGED
@@ -1,17 +1,76 @@
1
1
  const RUNNING_INDEX = 0
2
2
  const CONCURRENCY_INDEX = 1
3
3
 
4
+ const maxInt = 2147483647
5
+
6
+ let fastNow = Date.now()
7
+ setInterval(() => {
8
+ fastNow = Date.now()
9
+ }, 1e3).unref()
10
+
4
11
  class FastQueue {
5
12
  idx = 0
6
13
  cnt = 0
7
14
  arr = []
8
- lim = 0
15
+ age = fastNow
16
+ }
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+ export function parsePriority(p ) {
35
+ if (typeof p === 'number') {
36
+ // Do nothing...
37
+ } else if (p === 'lowest') {
38
+ return -3
39
+ } else if (p === 'lower') {
40
+ return -2
41
+ } else if (p === 'low') {
42
+ return -1
43
+ } else if (p === 'normal') {
44
+ return 0
45
+ } else if (p === 'high') {
46
+ return 1
47
+ } else if (p === 'higher') {
48
+ return 2
49
+ } else if (p === 'highest') {
50
+ return 3
51
+ } else {
52
+ p = Number(p)
53
+ }
54
+
55
+ if (typeof p !== 'number') {
56
+ return 0
57
+ } else if (p < -3) {
58
+ return -3
59
+ } else if (p > 3) {
60
+ return 3
61
+ } else {
62
+ return Math.trunc(p)
63
+ }
9
64
  }
10
65
 
11
66
  export class Scheduler {
12
- static LOW = 0
13
- static NORMAL = 1
14
- static HIGH = 2
67
+ static LOWEST = -3
68
+ static LOWER = -2
69
+ static LOW = -1
70
+ static NORMAL = 0
71
+ static HIGH = 1
72
+ static HIGHER = 2
73
+ static HIGHEST = 3
15
74
 
16
75
  #concurrency
17
76
  #stateView
@@ -22,9 +81,15 @@ export class Scheduler {
22
81
  #releasing = false
23
82
  #deferred = 0
24
83
 
25
- #lowQueue = new FastQueue()
26
- #normalQueue = new FastQueue()
27
- #highQueue = new FastQueue()
84
+ #queues = [
85
+ new FastQueue(), // 0 lowest
86
+ new FastQueue(), // 1 lower
87
+ new FastQueue(), // 2 low
88
+ new FastQueue(), // 3 normal
89
+ new FastQueue(), // 4 high
90
+ new FastQueue(), // 5 higher
91
+ new FastQueue(), // 6 highest
92
+ ]
28
93
 
29
94
  get concurrency() {
30
95
  return this.#concurrency
@@ -35,12 +100,7 @@ export class Scheduler {
35
100
  deferred: this.#deferred,
36
101
  running: this.#running,
37
102
  pending: this.#pending,
38
- lowCount: this.#lowQueue.cnt,
39
- normalCount: this.#normalQueue.cnt,
40
- highCount: this.#highQueue.cnt,
41
- lowLimit: this.#lowQueue.lim,
42
- normalLimit: this.#normalQueue.lim,
43
- highLimit: this.#highQueue.lim,
103
+ queues: this.#queues.map((q) => ({ count: q.cnt, age: q.age })),
44
104
  }
45
105
  }
46
106
 
@@ -61,58 +121,54 @@ export class Scheduler {
61
121
  } else {
62
122
  this.#concurrency = opts?.concurrency || Infinity
63
123
  }
64
-
65
- this.#lowQueue.lim = this.#concurrency / 4
66
- this.#normalQueue.lim = this.#concurrency - this.#lowQueue.lim
67
- this.#highQueue.lim = this.#concurrency
68
124
  }
69
125
 
70
- acquire(
71
- fn ,
72
- priority = Scheduler.NORMAL,
73
- opaque ,
74
- ) {
75
- if (typeof priority === 'number' && Number.isInteger(priority) && priority >= 0) {
76
- // Do nothing
77
- } else if (priority == null) {
78
- priority = Scheduler.NORMAL
79
- } else if (typeof priority === 'string') {
80
- if (priority === 'low') {
81
- priority = Scheduler.LOW
82
- } else if (priority === 'normal') {
83
- priority = Scheduler.NORMAL
84
- } else if (priority === 'high') {
85
- priority = Scheduler.HIGH
86
- } else {
87
- throw new Error('Invalid priority')
88
- }
89
- } else {
90
- throw new Error('Invalid priority')
91
- }
126
+ run (
127
+ fn ,
128
+ priority = Scheduler.NORMAL,
129
+ opaque ,
130
+ ) {
131
+ return new Promise ((resolve, reject) => {
132
+ this.acquire(
133
+ async (o) => {
134
+ try {
135
+ resolve(await fn(o))
136
+ } catch (err) {
137
+ reject(err)
138
+ } finally {
139
+ this.release()
140
+ }
141
+ },
142
+ priority,
143
+ opaque,
144
+ )
145
+ })
146
+ }
92
147
 
93
- let queue
94
- if (priority > Scheduler.NORMAL) {
95
- queue = this.#highQueue
96
- } else if (priority < Scheduler.NORMAL) {
97
- queue = this.#lowQueue
98
- } else {
99
- queue = this.#normalQueue
100
- }
148
+ acquire (fn , priority = Scheduler.NORMAL, opaque ) {
149
+ const p = parsePriority(priority)
150
+ const queue = this.#queues[p + 3]
101
151
 
102
152
  if (this.#stateView) {
103
- if (this.#running < 1 || this.#stateView[RUNNING_INDEX] < queue.lim) {
153
+ if (this.#running < 1 || this.#stateView[RUNNING_INDEX] < this.#concurrency) {
104
154
  Atomics.add(this.#stateView, RUNNING_INDEX, 1)
105
155
  this.#running += 1
106
- return fn(opaque)
156
+ fn(opaque)
157
+ return
107
158
  }
108
- } else if (this.#running < 1 || this.#running < queue.lim) {
159
+ } else if (this.#running < 1 || this.#running < this.#concurrency) {
109
160
  this.#running += 1
110
- return fn(opaque)
161
+ fn(opaque)
162
+ return
111
163
  }
112
164
 
113
165
  queue.arr.push(fn, opaque)
114
166
  queue.cnt += 1
115
167
 
168
+ if (queue.cnt === 1) {
169
+ queue.age = fastNow
170
+ }
171
+
116
172
  this.#deferred += 1
117
173
  this.#pending += 1
118
174
  }
@@ -130,30 +186,47 @@ export class Scheduler {
130
186
  try {
131
187
  this.#releasing = true
132
188
  while (this.#pending > 0) {
133
- let queue
134
- if (this.#highQueue.cnt > 0) {
135
- queue = this.#highQueue
136
- } else if (this.#normalQueue.cnt > 0) {
137
- queue = this.#normalQueue
138
- } else if (this.#lowQueue.cnt > 0) {
139
- queue = this.#lowQueue
140
- } else {
189
+ let queue = null
190
+
191
+ // Avoid starvation by randomizing the starting queue.
192
+
193
+ let idx = 6 // highest: 50%
194
+ if (this.#counter & 0b0000010) {
195
+ idx = 5 // higher: 25%
196
+ } else if (this.#counter & 0b0000100) {
197
+ idx = 4 // high: 12.5%
198
+ } else if (this.#counter & 0b0001000) {
199
+ idx = 3 // normal: 6.25%
200
+ } else if (this.#counter & 0b0010000) {
201
+ idx = 2 // low: 3.125%
202
+ } else if (this.#counter & 0b0100000) {
203
+ idx = 1 // lower: 1.5625%
204
+ } else if (this.#counter & 0b1000000) {
205
+ idx = 0 // lowest: 0.78%
206
+ }
207
+
208
+ for (let n = idx; n >= 0 && (queue == null || queue.cnt === 0); n--) {
209
+ queue = this.#queues[n]
210
+ }
211
+
212
+ for (let n = 6; n > idx && (queue == null || queue.cnt === 0); n--) {
213
+ queue = this.#queues[n]
214
+ }
215
+
216
+ if (queue == null || queue.cnt === 0) {
141
217
  throw new Error('Invariant violation: pending > 0 but no tasks in queues')
142
218
  }
143
219
 
144
- if (this.#running > 0 && running >= queue.lim) {
220
+ if (this.#running > 0 && running >= this.#concurrency) {
145
221
  break
146
222
  }
147
223
 
148
- if ((this.#counter & 63) === 0 && this.#lowQueue.cnt > 0) {
149
- queue = this.#lowQueue
150
- } else if ((this.#counter & 15) === 0 && this.#normalQueue.cnt > 0) {
151
- queue = this.#normalQueue
152
- }
224
+ this.#counter = (this.#counter + 1) & maxInt
153
225
 
154
226
  const fn = queue.arr[queue.idx++]
155
227
  const opaque = queue.arr[queue.idx++]
156
228
  queue.cnt -= 1
229
+ queue.age = fastNow
157
230
 
158
231
  if (queue.cnt === 0) {
159
232
  queue.idx = 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/scheduler",
3
- "version": "2.0.3",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -20,5 +20,5 @@
20
20
  "rimraf": "^6.1.2",
21
21
  "typescript": "^5.9.3"
22
22
  },
23
- "gitHead": "c9cfc87959862c94ed18d15b6090eff035ed271a"
23
+ "gitHead": "dc18f1fb0cf53929205b2de3cb53c08b631a4a9d"
24
24
  }