hanzio 1.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.
- package/README.md +510 -0
- package/dist/api-wrapper/index.d.ts +84 -0
- package/dist/api-wrapper/index.js +250 -0
- package/dist/array/index.d.ts +22 -0
- package/dist/cache/index.d.ts +19 -0
- package/dist/date/index.d.ts +11 -0
- package/dist/gzip/index.d.ts +2 -0
- package/dist/gzip/index.js +16 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1565 -0
- package/dist/jwt/index.d.ts +3 -0
- package/dist/jwt/index.js +19 -0
- package/dist/math/index.d.ts +2 -0
- package/dist/math/linearRegressionTrend.d.ts +5 -0
- package/dist/math/metricsSeriesColors.d.ts +42 -0
- package/dist/p-queue/index.d.ts +95 -0
- package/dist/p-queue/index.js +439 -0
- package/dist/promise/index.d.ts +21 -0
- package/dist/sitemap/index.d.ts +14 -0
- package/dist/sitemap/index.js +552 -0
- package/dist/state/index.d.ts +101 -0
- package/dist/string/index.d.ts +8 -0
- package/dist/typedswitch/index.d.ts +124 -0
- package/dist/url/index.d.ts +1 -0
- package/dist/zod/index.d.ts +8 -0
- package/dist/zod/index.js +210 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +6 -0
- package/dist/zod/zodMinMaxFilter.d.ts +14 -0
- package/dist/zod/zodToTypeString.d.ts +9 -0
- package/package.json +102 -0
- package/src/api-wrapper/index.ts +492 -0
- package/src/array/index.ts +187 -0
- package/src/cache/index.ts +154 -0
- package/src/date/index.ts +54 -0
- package/src/gzip/index.ts +29 -0
- package/src/index.ts +12 -0
- package/src/jwt/index.ts +25 -0
- package/src/math/index.ts +11 -0
- package/src/math/linearRegressionTrend.ts +36 -0
- package/src/math/metricsSeriesColors.ts +208 -0
- package/src/p-queue/index.ts +690 -0
- package/src/promise/index.ts +73 -0
- package/src/sitemap/index.ts +195 -0
- package/src/state/index.ts +331 -0
- package/src/string/index.ts +86 -0
- package/src/typedswitch/index.ts +309 -0
- package/src/url/index.ts +5 -0
- package/src/zod/index.ts +29 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -0
- package/src/zod/zodMinMaxFilter.ts +28 -0
- package/src/zod/zodToTypeString.ts +222 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
export type RunFunction = () => Promise<unknown> | unknown
|
|
2
|
+
|
|
3
|
+
export type Queue<Element, Options> = {
|
|
4
|
+
size: number
|
|
5
|
+
filter: (options: Readonly<Partial<Options>>) => Element[]
|
|
6
|
+
dequeue: () => Element | undefined
|
|
7
|
+
enqueue: (run: Element, options?: Partial<Options>) => void
|
|
8
|
+
setPriority: (id: string, priority: number) => void
|
|
9
|
+
remove?: (id: string) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TaskOptions = {
|
|
13
|
+
readonly signal?: AbortSignal | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type TimeoutOptions = {
|
|
17
|
+
timeout?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type QueueAddOptions = {
|
|
21
|
+
readonly priority?: number
|
|
22
|
+
id?: string
|
|
23
|
+
} & TaskOptions &
|
|
24
|
+
TimeoutOptions
|
|
25
|
+
|
|
26
|
+
export type Options<
|
|
27
|
+
QueueType extends Queue<RunFunction, QueueOptions> = PriorityQueue,
|
|
28
|
+
QueueOptions extends QueueAddOptions = QueueAddOptions
|
|
29
|
+
> = {
|
|
30
|
+
readonly concurrency?: number
|
|
31
|
+
readonly autoStart?: boolean
|
|
32
|
+
readonly queueClass?: new () => QueueType
|
|
33
|
+
readonly intervalCap?: number
|
|
34
|
+
readonly interval?: number
|
|
35
|
+
readonly carryoverIntervalCount?: boolean
|
|
36
|
+
readonly carryoverConcurrencyCount?: boolean
|
|
37
|
+
readonly strict?: boolean
|
|
38
|
+
} & TimeoutOptions
|
|
39
|
+
|
|
40
|
+
type Task<TaskResultType> =
|
|
41
|
+
| ((options: TaskOptions) => PromiseLike<TaskResultType>)
|
|
42
|
+
| ((options: TaskOptions) => TaskResultType)
|
|
43
|
+
|
|
44
|
+
type EventMap = {
|
|
45
|
+
active: []
|
|
46
|
+
idle: []
|
|
47
|
+
empty: []
|
|
48
|
+
add: []
|
|
49
|
+
next: []
|
|
50
|
+
completed: [unknown]
|
|
51
|
+
error: [unknown]
|
|
52
|
+
pendingZero: []
|
|
53
|
+
rateLimit: []
|
|
54
|
+
rateLimitCleared: []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type EventName = keyof EventMap
|
|
58
|
+
type Listener<Event extends EventName> = (
|
|
59
|
+
...arguments_: EventMap[Event]
|
|
60
|
+
) => void
|
|
61
|
+
|
|
62
|
+
export class TimeoutError extends Error {
|
|
63
|
+
constructor(message: string) {
|
|
64
|
+
super(message)
|
|
65
|
+
this.name = 'TimeoutError'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type PriorityQueueOptions = {
|
|
70
|
+
priority?: number
|
|
71
|
+
} & QueueAddOptions
|
|
72
|
+
|
|
73
|
+
export class PriorityQueue implements Queue<RunFunction, PriorityQueueOptions> {
|
|
74
|
+
readonly #queue: Array<PriorityQueueOptions & { run: RunFunction }> = []
|
|
75
|
+
|
|
76
|
+
enqueue(run: RunFunction, options: Partial<PriorityQueueOptions> = {}): void {
|
|
77
|
+
const element = {
|
|
78
|
+
priority: options.priority ?? 0,
|
|
79
|
+
id: options.id,
|
|
80
|
+
run
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const index = this.#queue.findIndex(
|
|
84
|
+
(item) => item.priority! < element.priority
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if (index === -1) {
|
|
88
|
+
this.#queue.push(element)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.#queue.splice(index, 0, element)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
dequeue(): RunFunction | undefined {
|
|
96
|
+
return this.#queue.shift()?.run
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
filter(options: Readonly<Partial<PriorityQueueOptions>>): RunFunction[] {
|
|
100
|
+
return this.#queue
|
|
101
|
+
.filter((item) =>
|
|
102
|
+
Object.entries(options).every(
|
|
103
|
+
([key, value]) => item[key as keyof PriorityQueueOptions] === value
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
.map((item) => item.run)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setPriority(id: string, priority: number): void {
|
|
110
|
+
const index = this.#queue.findIndex((item) => item.id === id)
|
|
111
|
+
|
|
112
|
+
if (index === -1) {
|
|
113
|
+
throw new ReferenceError(
|
|
114
|
+
`No promise function with the id "${id}" exists in the queue.`
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const [item] = this.#queue.splice(index, 1)
|
|
119
|
+
this.enqueue(item!.run, { ...item, priority })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
remove(id: string): void {
|
|
123
|
+
const index = this.#queue.findIndex((item) => item.id === id)
|
|
124
|
+
|
|
125
|
+
if (index !== -1) {
|
|
126
|
+
this.#queue.splice(index, 1)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get size(): number {
|
|
131
|
+
return this.#queue.length
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default class PQueue<
|
|
136
|
+
QueueType extends Queue<RunFunction, EnqueueOptionsType> = PriorityQueue,
|
|
137
|
+
EnqueueOptionsType extends QueueAddOptions = QueueAddOptions
|
|
138
|
+
> {
|
|
139
|
+
readonly #listeners = new Map<EventName, Set<Listener<EventName>>>()
|
|
140
|
+
#queue: QueueType
|
|
141
|
+
readonly #queueClass: new () => QueueType
|
|
142
|
+
#pending = 0
|
|
143
|
+
#concurrency: number
|
|
144
|
+
#isPaused: boolean
|
|
145
|
+
readonly #intervalCap: number
|
|
146
|
+
readonly #interval: number
|
|
147
|
+
readonly #carryoverIntervalCount: boolean
|
|
148
|
+
readonly #strict: boolean
|
|
149
|
+
#intervalCount = 0
|
|
150
|
+
#intervalEnd = 0
|
|
151
|
+
#intervalTimer?: ReturnType<typeof setTimeout>
|
|
152
|
+
#strictTimestamps: number[] = []
|
|
153
|
+
#rateLimited = false
|
|
154
|
+
#idCounter = 1n
|
|
155
|
+
readonly #runningTasks = new Map<
|
|
156
|
+
symbol,
|
|
157
|
+
{
|
|
158
|
+
id?: string
|
|
159
|
+
priority: number
|
|
160
|
+
startTime: number
|
|
161
|
+
timeout?: number
|
|
162
|
+
}
|
|
163
|
+
>()
|
|
164
|
+
timeout?: number
|
|
165
|
+
|
|
166
|
+
constructor(options: Options<QueueType, EnqueueOptionsType> = {}) {
|
|
167
|
+
const {
|
|
168
|
+
concurrency = Number.POSITIVE_INFINITY,
|
|
169
|
+
autoStart = true,
|
|
170
|
+
queueClass = PriorityQueue as unknown as new () => QueueType,
|
|
171
|
+
intervalCap = Number.POSITIVE_INFINITY,
|
|
172
|
+
interval = 0,
|
|
173
|
+
carryoverIntervalCount,
|
|
174
|
+
carryoverConcurrencyCount,
|
|
175
|
+
strict = false,
|
|
176
|
+
timeout
|
|
177
|
+
} = options
|
|
178
|
+
|
|
179
|
+
if (!(typeof concurrency === 'number' && concurrency >= 1)) {
|
|
180
|
+
throw new TypeError('Expected `concurrency` to be a number from 1 and up')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!(typeof intervalCap === 'number' && intervalCap >= 1)) {
|
|
184
|
+
throw new TypeError('Expected `intervalCap` to be a number from 1 and up')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
!(
|
|
189
|
+
typeof interval === 'number' &&
|
|
190
|
+
Number.isFinite(interval) &&
|
|
191
|
+
interval >= 0
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
throw new TypeError('Expected `interval` to be a finite number >= 0')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (strict && interval === 0) {
|
|
198
|
+
throw new TypeError('The `strict` option requires a non-zero `interval`')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (strict && intervalCap === Number.POSITIVE_INFINITY) {
|
|
202
|
+
throw new TypeError('The `strict` option requires a finite `intervalCap`')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (timeout !== undefined && !(Number.isFinite(timeout) && timeout > 0)) {
|
|
206
|
+
throw new TypeError('Expected `timeout` to be a positive finite number')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.#queueClass = queueClass
|
|
210
|
+
this.#queue = new queueClass()
|
|
211
|
+
this.#concurrency = concurrency
|
|
212
|
+
this.#isPaused = !autoStart
|
|
213
|
+
this.#intervalCap = intervalCap
|
|
214
|
+
this.#interval = interval
|
|
215
|
+
this.#carryoverIntervalCount =
|
|
216
|
+
carryoverIntervalCount ?? carryoverConcurrencyCount ?? false
|
|
217
|
+
this.#strict = strict
|
|
218
|
+
this.timeout = timeout
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
on<Event extends EventName>(event: Event, listener: Listener<Event>): this {
|
|
222
|
+
const listeners = this.#listeners.get(event) ?? new Set()
|
|
223
|
+
listeners.add(listener as Listener<EventName>)
|
|
224
|
+
this.#listeners.set(event, listeners)
|
|
225
|
+
return this
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
off<Event extends EventName>(event: Event, listener: Listener<Event>): this {
|
|
229
|
+
this.#listeners.get(event)?.delete(listener as Listener<EventName>)
|
|
230
|
+
return this
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
emit<Event extends EventName>(
|
|
234
|
+
event: Event,
|
|
235
|
+
...arguments_: EventMap[Event]
|
|
236
|
+
): boolean {
|
|
237
|
+
const listeners = this.#listeners.get(event)
|
|
238
|
+
|
|
239
|
+
if (!listeners || listeners.size === 0) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const listener of [...listeners]) {
|
|
244
|
+
listener(...(arguments_ as EventMap[EventName]))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get concurrency(): number {
|
|
251
|
+
return this.#concurrency
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
set concurrency(newConcurrency: number) {
|
|
255
|
+
if (!(typeof newConcurrency === 'number' && newConcurrency >= 1)) {
|
|
256
|
+
throw new TypeError('Expected `concurrency` to be a number from 1 and up')
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.#concurrency = newConcurrency
|
|
260
|
+
this.#processQueue()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
get size(): number {
|
|
264
|
+
return this.#queue.size
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
sizeBy(options: Readonly<Partial<EnqueueOptionsType>>): number {
|
|
268
|
+
return this.#queue.filter(options).length
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
get pending(): number {
|
|
272
|
+
return this.#pending
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get isPaused(): boolean {
|
|
276
|
+
return this.#isPaused
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get isRateLimited(): boolean {
|
|
280
|
+
this.#updateRateLimitState()
|
|
281
|
+
return this.#rateLimited
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
get isSaturated(): boolean {
|
|
285
|
+
return (
|
|
286
|
+
(this.#pending >= this.#concurrency && this.#queue.size > 0) ||
|
|
287
|
+
(this.isRateLimited && this.#queue.size > 0)
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
get runningTasks(): ReadonlyArray<{
|
|
292
|
+
readonly id?: string
|
|
293
|
+
readonly priority: number
|
|
294
|
+
readonly startTime: number
|
|
295
|
+
readonly timeout?: number
|
|
296
|
+
}> {
|
|
297
|
+
return [...this.#runningTasks.values()].map((task) => ({ ...task }))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
setPriority(id: string, priority: number): void {
|
|
301
|
+
if (typeof priority !== 'number' || !Number.isFinite(priority)) {
|
|
302
|
+
throw new TypeError('Expected `priority` to be a finite number')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.#queue.setPriority(id, priority)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async add<TaskResultType>(
|
|
309
|
+
function_: Task<TaskResultType>,
|
|
310
|
+
options: Partial<EnqueueOptionsType> = {}
|
|
311
|
+
): Promise<TaskResultType> {
|
|
312
|
+
const taskOptions = {
|
|
313
|
+
timeout: this.timeout,
|
|
314
|
+
...options,
|
|
315
|
+
id: options.id ?? (this.#idCounter++).toString()
|
|
316
|
+
} as Partial<EnqueueOptionsType> & Required<Pick<QueueAddOptions, 'id'>>
|
|
317
|
+
|
|
318
|
+
return new Promise<TaskResultType>((resolve, reject) => {
|
|
319
|
+
const taskSymbol = Symbol(taskOptions.id)
|
|
320
|
+
let didRun = false
|
|
321
|
+
let queuedAbortHandler: (() => void) | undefined
|
|
322
|
+
let runningAbortHandler: (() => void) | undefined
|
|
323
|
+
|
|
324
|
+
const cleanupQueuedAbort = () => {
|
|
325
|
+
if (queuedAbortHandler) {
|
|
326
|
+
taskOptions.signal?.removeEventListener('abort', queuedAbortHandler)
|
|
327
|
+
queuedAbortHandler = undefined
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const run = async () => {
|
|
332
|
+
didRun = true
|
|
333
|
+
cleanupQueuedAbort()
|
|
334
|
+
this.#pending++
|
|
335
|
+
this.#runningTasks.set(taskSymbol, {
|
|
336
|
+
id: taskOptions.id,
|
|
337
|
+
priority: taskOptions.priority ?? 0,
|
|
338
|
+
startTime: Date.now(),
|
|
339
|
+
timeout: taskOptions.timeout
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
taskOptions.signal?.throwIfAborted()
|
|
344
|
+
const operation = Promise.resolve(
|
|
345
|
+
function_({ signal: taskOptions.signal })
|
|
346
|
+
)
|
|
347
|
+
const result = await this.#withTimeoutAndAbort(
|
|
348
|
+
operation,
|
|
349
|
+
taskOptions,
|
|
350
|
+
(listener) => {
|
|
351
|
+
runningAbortHandler = listener
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
resolve(result)
|
|
355
|
+
this.emit('completed', result)
|
|
356
|
+
} catch (error) {
|
|
357
|
+
reject(error)
|
|
358
|
+
this.emit('error', error)
|
|
359
|
+
} finally {
|
|
360
|
+
if (runningAbortHandler) {
|
|
361
|
+
taskOptions.signal?.removeEventListener(
|
|
362
|
+
'abort',
|
|
363
|
+
runningAbortHandler
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.#runningTasks.delete(taskSymbol)
|
|
368
|
+
queueMicrotask(() => {
|
|
369
|
+
this.#next()
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (taskOptions.signal?.aborted) {
|
|
375
|
+
reject(taskOptions.signal.reason)
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (taskOptions.signal) {
|
|
380
|
+
queuedAbortHandler = () => {
|
|
381
|
+
if (didRun) {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
this.#queue.remove?.(taskOptions.id)
|
|
386
|
+
reject(taskOptions.signal?.reason)
|
|
387
|
+
this.emit('next')
|
|
388
|
+
this.#tryToStartAnother()
|
|
389
|
+
}
|
|
390
|
+
taskOptions.signal.addEventListener('abort', queuedAbortHandler, {
|
|
391
|
+
once: true
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this.#queue.enqueue(run, taskOptions)
|
|
396
|
+
this.emit('add')
|
|
397
|
+
this.#processQueue()
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async addAll<TaskResultsType>(
|
|
402
|
+
functions: ReadonlyArray<Task<TaskResultsType>>,
|
|
403
|
+
options?: Partial<EnqueueOptionsType>
|
|
404
|
+
): Promise<TaskResultsType[]> {
|
|
405
|
+
return Promise.all(
|
|
406
|
+
functions.map(async (function_) => this.add(function_, options))
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
start(): this {
|
|
411
|
+
if (!this.#isPaused) {
|
|
412
|
+
return this
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.#isPaused = false
|
|
416
|
+
this.#processQueue()
|
|
417
|
+
return this
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
pause(): void {
|
|
421
|
+
this.#isPaused = true
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
clear(): void {
|
|
425
|
+
this.#queue = new this.#queueClass()
|
|
426
|
+
this.#clearIntervalTimer()
|
|
427
|
+
this.#updateRateLimitState()
|
|
428
|
+
this.emit('empty')
|
|
429
|
+
|
|
430
|
+
if (this.#pending === 0) {
|
|
431
|
+
this.emit('idle')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
this.emit('next')
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async onEmpty(): Promise<void> {
|
|
438
|
+
if (this.#queue.size === 0) {
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
await this.#onEvent('empty')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async onIdle(): Promise<void> {
|
|
446
|
+
if (this.#queue.size === 0 && this.#pending === 0) {
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
await this.#onEvent('idle')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async onPendingZero(): Promise<void> {
|
|
454
|
+
if (this.#pending === 0) {
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
await this.#onEvent('pendingZero')
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async onRateLimit(): Promise<void> {
|
|
462
|
+
if (this.isRateLimited) {
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
await this.#onEvent('rateLimit')
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async onRateLimitCleared(): Promise<void> {
|
|
470
|
+
if (!this.isRateLimited) {
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
await this.#onEvent('rateLimitCleared')
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
onError(): Promise<never> {
|
|
478
|
+
return new Promise<never>((_resolve, reject) => {
|
|
479
|
+
const listener = (error: unknown) => {
|
|
480
|
+
this.off('error', listener)
|
|
481
|
+
reject(error)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.on('error', listener)
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async onSizeLessThan(limit: number): Promise<void> {
|
|
489
|
+
if (this.#queue.size < limit) {
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
await this.#onEvent('next', () => this.#queue.size < limit)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async #onEvent(event: EventName, filter?: () => boolean): Promise<void> {
|
|
497
|
+
return new Promise((resolve) => {
|
|
498
|
+
const listener = () => {
|
|
499
|
+
if (filter && !filter()) {
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
this.off(event, listener)
|
|
504
|
+
resolve()
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
this.on(event, listener)
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async #withTimeoutAndAbort<TaskResultType>(
|
|
512
|
+
operation: Promise<TaskResultType>,
|
|
513
|
+
options: Partial<EnqueueOptionsType>,
|
|
514
|
+
setAbortListener: (listener: () => void) => void
|
|
515
|
+
): Promise<TaskResultType> {
|
|
516
|
+
const races: Array<Promise<TaskResultType>> = [operation]
|
|
517
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
518
|
+
|
|
519
|
+
if (options.timeout) {
|
|
520
|
+
races.push(
|
|
521
|
+
new Promise<never>((_resolve, reject) => {
|
|
522
|
+
timeoutId = setTimeout(() => {
|
|
523
|
+
reject(
|
|
524
|
+
new TimeoutError(`Task timed out after ${options.timeout}ms`)
|
|
525
|
+
)
|
|
526
|
+
}, options.timeout)
|
|
527
|
+
})
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (options.signal) {
|
|
532
|
+
races.push(
|
|
533
|
+
new Promise<never>((_resolve, reject) => {
|
|
534
|
+
const listener = () => {
|
|
535
|
+
reject(options.signal?.reason)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
setAbortListener(listener)
|
|
539
|
+
options.signal?.addEventListener('abort', listener, { once: true })
|
|
540
|
+
})
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
return await Promise.race(races)
|
|
546
|
+
} finally {
|
|
547
|
+
if (timeoutId) {
|
|
548
|
+
clearTimeout(timeoutId)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#next(): void {
|
|
554
|
+
this.#pending--
|
|
555
|
+
|
|
556
|
+
if (this.#pending === 0) {
|
|
557
|
+
this.emit('pendingZero')
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
this.#processQueue()
|
|
561
|
+
this.emit('next')
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#processQueue(): void {
|
|
565
|
+
while (this.#tryToStartAnother()) {}
|
|
566
|
+
this.#updateRateLimitState()
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#tryToStartAnother(): boolean {
|
|
570
|
+
if (this.#queue.size === 0) {
|
|
571
|
+
this.#clearIntervalTimer()
|
|
572
|
+
this.emit('empty')
|
|
573
|
+
|
|
574
|
+
if (this.#pending === 0) {
|
|
575
|
+
this.emit('idle')
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return false
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (
|
|
582
|
+
this.#isPaused ||
|
|
583
|
+
this.#pending >= this.#concurrency ||
|
|
584
|
+
!this.#doesIntervalAllowAnother()
|
|
585
|
+
) {
|
|
586
|
+
return false
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const job = this.#queue.dequeue()
|
|
590
|
+
|
|
591
|
+
if (!job) {
|
|
592
|
+
return false
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
this.#consumeIntervalSlot()
|
|
596
|
+
this.emit('active')
|
|
597
|
+
job()
|
|
598
|
+
return true
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#doesIntervalAllowAnother(): boolean {
|
|
602
|
+
if (
|
|
603
|
+
this.#intervalCap === Number.POSITIVE_INFINITY ||
|
|
604
|
+
this.#interval === 0
|
|
605
|
+
) {
|
|
606
|
+
return true
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const now = Date.now()
|
|
610
|
+
|
|
611
|
+
if (this.#strict) {
|
|
612
|
+
this.#strictTimestamps = this.#strictTimestamps.filter(
|
|
613
|
+
(timestamp) => now - timestamp < this.#interval
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
if (this.#strictTimestamps.length < this.#intervalCap) {
|
|
617
|
+
return true
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
this.#scheduleIntervalResume(
|
|
621
|
+
this.#interval - (now - this.#strictTimestamps[0]!)
|
|
622
|
+
)
|
|
623
|
+
return false
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (this.#intervalEnd === 0 || now >= this.#intervalEnd) {
|
|
627
|
+
this.#intervalCount = this.#carryoverIntervalCount ? this.#pending : 0
|
|
628
|
+
this.#intervalEnd = now + this.#interval
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (this.#intervalCount < this.#intervalCap) {
|
|
632
|
+
return true
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
this.#scheduleIntervalResume(this.#intervalEnd - now)
|
|
636
|
+
return false
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
#consumeIntervalSlot(): void {
|
|
640
|
+
if (
|
|
641
|
+
this.#intervalCap === Number.POSITIVE_INFINITY ||
|
|
642
|
+
this.#interval === 0
|
|
643
|
+
) {
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (this.#strict) {
|
|
648
|
+
this.#strictTimestamps.push(Date.now())
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
this.#intervalCount++
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
#scheduleIntervalResume(delay: number): void {
|
|
656
|
+
if (this.#intervalTimer) {
|
|
657
|
+
return
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
this.#intervalTimer = setTimeout(
|
|
661
|
+
() => {
|
|
662
|
+
this.#intervalTimer = undefined
|
|
663
|
+
this.#processQueue()
|
|
664
|
+
},
|
|
665
|
+
Math.max(0, delay)
|
|
666
|
+
)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#clearIntervalTimer(): void {
|
|
670
|
+
if (this.#intervalTimer) {
|
|
671
|
+
clearTimeout(this.#intervalTimer)
|
|
672
|
+
this.#intervalTimer = undefined
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
#updateRateLimitState(): void {
|
|
677
|
+
const previous = this.#rateLimited
|
|
678
|
+
const isRateLimited =
|
|
679
|
+
this.#queue.size > 0 &&
|
|
680
|
+
this.#intervalCap !== Number.POSITIVE_INFINITY &&
|
|
681
|
+
this.#interval > 0 &&
|
|
682
|
+
!this.#doesIntervalAllowAnother()
|
|
683
|
+
|
|
684
|
+
this.#rateLimited = isRateLimited
|
|
685
|
+
|
|
686
|
+
if (previous !== isRateLimited) {
|
|
687
|
+
this.emit(isRateLimited ? 'rateLimit' : 'rateLimitCleared')
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|