@nxtedition/scheduler 2.0.4 → 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 -11
  2. package/lib/index.js +114 -65
  3. package/package.json +2 -2
package/lib/index.d.ts CHANGED
@@ -1,25 +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
- run<T, U>(fn: (opaque?: U) => Promise<T> | T, priority?: 0 | 1 | 2 | 'low' | 'normal' | 'high', opaque?: U): Promise<T>;
23
- acquire<U>(fn: (opaque?: U) => unknown, priority?: 0 | 1 | 2 | 'low' | 'normal' | 'high', opaque?: U): void;
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;
24
28
  release(): void;
25
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,15 +121,11 @@ 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
126
  run (
71
127
  fn ,
72
- priority = Scheduler.NORMAL,
128
+ priority = Scheduler.NORMAL,
73
129
  opaque ,
74
130
  ) {
75
131
  return new Promise ((resolve, reject) => {
@@ -89,46 +145,18 @@ export class Scheduler {
89
145
  })
90
146
  }
91
147
 
92
- acquire (
93
- fn ,
94
- priority = Scheduler.NORMAL,
95
- opaque ,
96
- ) {
97
- if (typeof priority === 'number' && Number.isInteger(priority) && priority >= 0) {
98
- // Do nothing
99
- } else if (priority == null) {
100
- priority = Scheduler.NORMAL
101
- } else if (typeof priority === 'string') {
102
- if (priority === 'low') {
103
- priority = Scheduler.LOW
104
- } else if (priority === 'normal') {
105
- priority = Scheduler.NORMAL
106
- } else if (priority === 'high') {
107
- priority = Scheduler.HIGH
108
- } else {
109
- throw new Error('Invalid priority')
110
- }
111
- } else {
112
- throw new Error('Invalid priority')
113
- }
114
-
115
- let queue
116
- if (priority > Scheduler.NORMAL) {
117
- queue = this.#highQueue
118
- } else if (priority < Scheduler.NORMAL) {
119
- queue = this.#lowQueue
120
- } else {
121
- queue = this.#normalQueue
122
- }
148
+ acquire (fn , priority = Scheduler.NORMAL, opaque ) {
149
+ const p = parsePriority(priority)
150
+ const queue = this.#queues[p + 3]
123
151
 
124
152
  if (this.#stateView) {
125
- if (this.#running < 1 || this.#stateView[RUNNING_INDEX] < queue.lim) {
153
+ if (this.#running < 1 || this.#stateView[RUNNING_INDEX] < this.#concurrency) {
126
154
  Atomics.add(this.#stateView, RUNNING_INDEX, 1)
127
155
  this.#running += 1
128
156
  fn(opaque)
129
157
  return
130
158
  }
131
- } else if (this.#running < 1 || this.#running < queue.lim) {
159
+ } else if (this.#running < 1 || this.#running < this.#concurrency) {
132
160
  this.#running += 1
133
161
  fn(opaque)
134
162
  return
@@ -137,6 +165,10 @@ export class Scheduler {
137
165
  queue.arr.push(fn, opaque)
138
166
  queue.cnt += 1
139
167
 
168
+ if (queue.cnt === 1) {
169
+ queue.age = fastNow
170
+ }
171
+
140
172
  this.#deferred += 1
141
173
  this.#pending += 1
142
174
  }
@@ -154,30 +186,47 @@ export class Scheduler {
154
186
  try {
155
187
  this.#releasing = true
156
188
  while (this.#pending > 0) {
157
- let queue
158
- if (this.#highQueue.cnt > 0) {
159
- queue = this.#highQueue
160
- } else if (this.#normalQueue.cnt > 0) {
161
- queue = this.#normalQueue
162
- } else if (this.#lowQueue.cnt > 0) {
163
- queue = this.#lowQueue
164
- } 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) {
165
217
  throw new Error('Invariant violation: pending > 0 but no tasks in queues')
166
218
  }
167
219
 
168
- if (this.#running > 0 && running >= queue.lim) {
220
+ if (this.#running > 0 && running >= this.#concurrency) {
169
221
  break
170
222
  }
171
223
 
172
- if ((this.#counter & 63) === 0 && this.#lowQueue.cnt > 0) {
173
- queue = this.#lowQueue
174
- } else if ((this.#counter & 15) === 0 && this.#normalQueue.cnt > 0) {
175
- queue = this.#normalQueue
176
- }
224
+ this.#counter = (this.#counter + 1) & maxInt
177
225
 
178
226
  const fn = queue.arr[queue.idx++]
179
227
  const opaque = queue.arr[queue.idx++]
180
228
  queue.cnt -= 1
229
+ queue.age = fastNow
181
230
 
182
231
  if (queue.cnt === 0) {
183
232
  queue.idx = 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/scheduler",
3
- "version": "2.0.4",
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": "aa8978d5b536477418eb6cc60255d4a36bb9d013"
23
+ "gitHead": "dc18f1fb0cf53929205b2de3cb53c08b631a4a9d"
24
24
  }