enqiu 0.1.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +245 -0
- package/dist/api.d.ts +236 -0
- package/dist/api.js +519 -0
- package/dist/codec.d.ts +8 -0
- package/dist/codec.js +74 -0
- package/dist/cron.d.ts +19 -0
- package/dist/cron.js +217 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/memory-scheduler.d.ts +24 -0
- package/dist/memory-scheduler.js +163 -0
- package/dist/memory.d.ts +344 -0
- package/dist/memory.js +1201 -0
- package/dist/redis.d.ts +202 -0
- package/dist/redis.js +2180 -0
- package/package.json +72 -0
package/dist/memory.js
ADDED
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
export class JobFailedError extends Error {
|
|
2
|
+
jobId;
|
|
3
|
+
constructor(jobId, message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = "JobFailedError";
|
|
6
|
+
this.jobId = jobId;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class JobCancelledError extends Error {
|
|
10
|
+
jobId;
|
|
11
|
+
constructor(jobId, message = "Job was cancelled") {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "JobCancelledError";
|
|
14
|
+
this.jobId = jobId;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class JobTimeoutError extends Error {
|
|
18
|
+
jobId;
|
|
19
|
+
timeout;
|
|
20
|
+
constructor(jobId, timeout) {
|
|
21
|
+
super(`Job "${jobId}" timed out after ${timeout}ms`);
|
|
22
|
+
this.name = "JobTimeoutError";
|
|
23
|
+
this.jobId = jobId;
|
|
24
|
+
this.timeout = timeout;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class JobExpiredError extends Error {
|
|
28
|
+
jobId;
|
|
29
|
+
constructor(jobId) {
|
|
30
|
+
super(`Job "${jobId}" expired before it could start`);
|
|
31
|
+
this.name = "JobExpiredError";
|
|
32
|
+
this.jobId = jobId;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class QueueClosedError extends Error {
|
|
36
|
+
constructor(name) {
|
|
37
|
+
super(`Queue "${name}" is closed`);
|
|
38
|
+
this.name = "QueueClosedError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class JobHandle {
|
|
42
|
+
owner;
|
|
43
|
+
job;
|
|
44
|
+
deduplicated;
|
|
45
|
+
constructor(owner, job, deduplicated) {
|
|
46
|
+
this.owner = owner;
|
|
47
|
+
this.job = job;
|
|
48
|
+
this.deduplicated = deduplicated;
|
|
49
|
+
}
|
|
50
|
+
get id() {
|
|
51
|
+
return this.job.id;
|
|
52
|
+
}
|
|
53
|
+
get name() {
|
|
54
|
+
return this.job.name;
|
|
55
|
+
}
|
|
56
|
+
get input() {
|
|
57
|
+
return this.job.input;
|
|
58
|
+
}
|
|
59
|
+
get status() {
|
|
60
|
+
return this.job.status;
|
|
61
|
+
}
|
|
62
|
+
get result() {
|
|
63
|
+
return this.owner.resultFor(this.job);
|
|
64
|
+
}
|
|
65
|
+
get accepted() {
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
}
|
|
68
|
+
cancel(reason) {
|
|
69
|
+
return this.owner.cancel(this.id, reason);
|
|
70
|
+
}
|
|
71
|
+
snapshot() {
|
|
72
|
+
return snapshot(this.job);
|
|
73
|
+
}
|
|
74
|
+
then(onFulfilled, onRejected) {
|
|
75
|
+
return this.result.then(onFulfilled, onRejected);
|
|
76
|
+
}
|
|
77
|
+
catch(onRejected) {
|
|
78
|
+
return this.result.catch(onRejected);
|
|
79
|
+
}
|
|
80
|
+
finally(onFinally) {
|
|
81
|
+
return this.result.finally(onFinally ?? undefined);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A zero-dependency, single-process job queue with strongly typed named jobs.
|
|
86
|
+
*
|
|
87
|
+
* State is intentionally kept in memory. MemoryQueue is ideal for local background
|
|
88
|
+
* work, concurrency control, API throttling, and tests. It is not durable or
|
|
89
|
+
* distributed; use a database-backed queue when jobs must survive restarts.
|
|
90
|
+
*/
|
|
91
|
+
export class MemoryQueue {
|
|
92
|
+
name;
|
|
93
|
+
handlers;
|
|
94
|
+
records = new Map();
|
|
95
|
+
keys = new Map();
|
|
96
|
+
activeKeys = new Map();
|
|
97
|
+
throttleStates = new Map();
|
|
98
|
+
debounceStates = new Map();
|
|
99
|
+
ready = new BinaryHeap(readyBefore);
|
|
100
|
+
delayed = new BinaryHeap(delayedBefore);
|
|
101
|
+
listeners = new Map();
|
|
102
|
+
defaultRetry;
|
|
103
|
+
defaultTimeout;
|
|
104
|
+
rateLimit;
|
|
105
|
+
historyLimit;
|
|
106
|
+
logLimit;
|
|
107
|
+
starts = [];
|
|
108
|
+
idleWaiters = new Set();
|
|
109
|
+
sizeWaiters = new Set();
|
|
110
|
+
_concurrency;
|
|
111
|
+
runningCount = 0;
|
|
112
|
+
sequence = 0;
|
|
113
|
+
started;
|
|
114
|
+
closed = false;
|
|
115
|
+
idleNotified = true;
|
|
116
|
+
pumpQueued = false;
|
|
117
|
+
timer;
|
|
118
|
+
policyWakeAt;
|
|
119
|
+
constructor(handlers, options = {}) {
|
|
120
|
+
if (Object.keys(handlers).length === 0) {
|
|
121
|
+
throw new TypeError("At least one job handler is required");
|
|
122
|
+
}
|
|
123
|
+
this.name = options.name?.trim() || "default";
|
|
124
|
+
this._concurrency = options.concurrency ?? Number.POSITIVE_INFINITY;
|
|
125
|
+
this.defaultRetry = normalizeRetry(options.retry);
|
|
126
|
+
this.defaultTimeout = options.timeout;
|
|
127
|
+
this.rateLimit = options.rateLimit;
|
|
128
|
+
this.historyLimit = options.historyLimit ?? 1000;
|
|
129
|
+
this.logLimit = options.logLimit ?? 100;
|
|
130
|
+
this.started = options.autoStart ?? true;
|
|
131
|
+
this.handlers = handlers;
|
|
132
|
+
validatePositiveIntegerOrInfinity("concurrency", this._concurrency);
|
|
133
|
+
validateTimeout(this.defaultTimeout);
|
|
134
|
+
validateHistoryLimit(this.historyLimit);
|
|
135
|
+
validateHistoryLimit(this.logLimit);
|
|
136
|
+
if (this.rateLimit) {
|
|
137
|
+
validatePositiveInteger("rateLimit.limit", this.rateLimit.limit);
|
|
138
|
+
validatePositiveNumber("rateLimit.interval", this.rateLimit.interval);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
get concurrency() {
|
|
142
|
+
return this._concurrency;
|
|
143
|
+
}
|
|
144
|
+
set concurrency(value) {
|
|
145
|
+
validatePositiveIntegerOrInfinity("concurrency", value);
|
|
146
|
+
this._concurrency = value;
|
|
147
|
+
this.requestPump();
|
|
148
|
+
}
|
|
149
|
+
/** Jobs waiting to start, including scheduled jobs. */
|
|
150
|
+
get size() {
|
|
151
|
+
let count = 0;
|
|
152
|
+
for (const job of this.records.values()) {
|
|
153
|
+
if (job.status === "queued" || job.status === "scheduled") {
|
|
154
|
+
count += 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return count;
|
|
158
|
+
}
|
|
159
|
+
/** Jobs currently running. */
|
|
160
|
+
get pending() {
|
|
161
|
+
return this.runningCount;
|
|
162
|
+
}
|
|
163
|
+
get isPaused() {
|
|
164
|
+
return !this.started && !this.closed;
|
|
165
|
+
}
|
|
166
|
+
get isRateLimited() {
|
|
167
|
+
if (!this.rateLimit || this.size === 0) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
this.pruneStarts(Date.now());
|
|
171
|
+
return this.starts.length >= this.rateLimit.limit;
|
|
172
|
+
}
|
|
173
|
+
get isSaturated() {
|
|
174
|
+
return (this.size > 0 &&
|
|
175
|
+
(this.runningCount >= this._concurrency || this.isRateLimited));
|
|
176
|
+
}
|
|
177
|
+
get stats() {
|
|
178
|
+
const value = {
|
|
179
|
+
queued: 0,
|
|
180
|
+
scheduled: 0,
|
|
181
|
+
running: 0,
|
|
182
|
+
succeeded: 0,
|
|
183
|
+
failed: 0,
|
|
184
|
+
cancelled: 0,
|
|
185
|
+
expired: 0,
|
|
186
|
+
total: 0,
|
|
187
|
+
};
|
|
188
|
+
for (const job of this.records.values()) {
|
|
189
|
+
value[job.status] += 1;
|
|
190
|
+
value.total += 1;
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
add(name, input, options = {}) {
|
|
195
|
+
this.assertOpen();
|
|
196
|
+
const handler = this.handlers[name];
|
|
197
|
+
if (typeof handler !== "function") {
|
|
198
|
+
throw new TypeError(`Unknown job "${name}"`);
|
|
199
|
+
}
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
this.prunePolicyState(now);
|
|
202
|
+
validateExecutionOptions(options);
|
|
203
|
+
const key = options.key ? `${String(name)}:${options.key}` : undefined;
|
|
204
|
+
if (key) {
|
|
205
|
+
const existing = this.keys.get(key);
|
|
206
|
+
if (existing) {
|
|
207
|
+
if (!isTerminal(existing.status) ||
|
|
208
|
+
(existing.keyExpiresAt !== undefined &&
|
|
209
|
+
existing.keyExpiresAt > now)) {
|
|
210
|
+
return this.handle(existing, true);
|
|
211
|
+
}
|
|
212
|
+
this.keys.delete(key);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const debounceKey = options.debounce
|
|
216
|
+
? `${String(name)}:${options.debounce.key}`
|
|
217
|
+
: undefined;
|
|
218
|
+
const debounceState = debounceKey
|
|
219
|
+
? this.debounceStates.get(debounceKey)
|
|
220
|
+
: undefined;
|
|
221
|
+
if (debounceState && options.debounce) {
|
|
222
|
+
if (options.debounce.mode === "leading" &&
|
|
223
|
+
debounceState.until > now) {
|
|
224
|
+
return this.handle(debounceState.job, true);
|
|
225
|
+
}
|
|
226
|
+
if (options.debounce.mode === "trailing" &&
|
|
227
|
+
(debounceState.job.status === "queued" ||
|
|
228
|
+
debounceState.job.status === "scheduled")) {
|
|
229
|
+
this.updateTrailingDebounce(debounceState, input, options, now);
|
|
230
|
+
return this.handle(debounceState.job, true);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const id = options.id ?? this.createId(String(name), now);
|
|
234
|
+
if (!id) {
|
|
235
|
+
throw new TypeError("Job ID must not be empty");
|
|
236
|
+
}
|
|
237
|
+
if (this.records.has(id)) {
|
|
238
|
+
throw new Error(`Job ID "${id}" already exists`);
|
|
239
|
+
}
|
|
240
|
+
const requestedRunAt = normalizeRunAt(options.delay, now);
|
|
241
|
+
const runAt = options.debounce?.mode === "trailing"
|
|
242
|
+
? Math.max(requestedRunAt, now + options.debounce.wait)
|
|
243
|
+
: requestedRunAt;
|
|
244
|
+
const priority = options.priority ?? 0;
|
|
245
|
+
const retry = options.retry === undefined
|
|
246
|
+
? this.defaultRetry
|
|
247
|
+
: normalizeRetry(options.retry);
|
|
248
|
+
const timeout = options.timeout ?? this.defaultTimeout;
|
|
249
|
+
const expiresAt = options.expiresIn === undefined ? undefined : now + options.expiresIn;
|
|
250
|
+
if (!Number.isFinite(priority)) {
|
|
251
|
+
throw new RangeError("priority must be a finite number");
|
|
252
|
+
}
|
|
253
|
+
validateTimeout(timeout);
|
|
254
|
+
if (options.expiresIn !== undefined) {
|
|
255
|
+
validatePositiveNumber("expiresIn", options.expiresIn);
|
|
256
|
+
}
|
|
257
|
+
const job = {
|
|
258
|
+
id,
|
|
259
|
+
name: String(name),
|
|
260
|
+
input,
|
|
261
|
+
status: runAt > now ? "scheduled" : "queued",
|
|
262
|
+
priority,
|
|
263
|
+
attempt: 0,
|
|
264
|
+
retry,
|
|
265
|
+
timeout,
|
|
266
|
+
key,
|
|
267
|
+
keyRetention: options.keyRetention ?? 0,
|
|
268
|
+
keyExpiresAt: undefined,
|
|
269
|
+
concurrency: options.concurrency,
|
|
270
|
+
throttle: options.throttle,
|
|
271
|
+
debounceKey,
|
|
272
|
+
createdAt: now,
|
|
273
|
+
runAt,
|
|
274
|
+
expiresAt,
|
|
275
|
+
startedAt: undefined,
|
|
276
|
+
finishedAt: undefined,
|
|
277
|
+
progress: undefined,
|
|
278
|
+
output: undefined,
|
|
279
|
+
error: undefined,
|
|
280
|
+
errorCause: undefined,
|
|
281
|
+
logs: [],
|
|
282
|
+
sequence: this.sequence++,
|
|
283
|
+
controller: undefined,
|
|
284
|
+
completion: deferred(),
|
|
285
|
+
externalSignal: options.signal,
|
|
286
|
+
abortListener: undefined,
|
|
287
|
+
};
|
|
288
|
+
this.records.set(id, job);
|
|
289
|
+
this.idleNotified = false;
|
|
290
|
+
if (key) {
|
|
291
|
+
this.keys.set(key, job);
|
|
292
|
+
}
|
|
293
|
+
if (debounceKey && options.debounce) {
|
|
294
|
+
this.debounceStates.set(debounceKey, {
|
|
295
|
+
job,
|
|
296
|
+
until: now + options.debounce.wait,
|
|
297
|
+
mode: options.debounce.mode,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (job.status === "scheduled") {
|
|
301
|
+
this.delayed.push(job);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
this.ready.push(job);
|
|
305
|
+
}
|
|
306
|
+
this.emit("added", snapshot(job));
|
|
307
|
+
this.connectSignal(job);
|
|
308
|
+
this.notifySizeWaiters();
|
|
309
|
+
this.requestPump();
|
|
310
|
+
return this.handle(job, false);
|
|
311
|
+
}
|
|
312
|
+
updateTrailingDebounce(state, input, options, now) {
|
|
313
|
+
const job = state.job;
|
|
314
|
+
this.ready.remove(job);
|
|
315
|
+
this.delayed.remove(job);
|
|
316
|
+
this.disconnectSignal(job);
|
|
317
|
+
job.input = input;
|
|
318
|
+
job.status = "scheduled";
|
|
319
|
+
job.priority = options.priority ?? job.priority;
|
|
320
|
+
job.retry =
|
|
321
|
+
options.retry === undefined
|
|
322
|
+
? job.retry
|
|
323
|
+
: normalizeRetry(options.retry);
|
|
324
|
+
job.timeout = options.timeout ?? job.timeout;
|
|
325
|
+
job.runAt = Math.max(normalizeRunAt(options.delay, now), now + (options.debounce?.wait ?? 0));
|
|
326
|
+
job.expiresAt =
|
|
327
|
+
options.expiresIn === undefined ? undefined : now + options.expiresIn;
|
|
328
|
+
job.concurrency = options.concurrency;
|
|
329
|
+
job.throttle = options.throttle;
|
|
330
|
+
job.sequence = this.sequence++;
|
|
331
|
+
job.externalSignal = options.signal;
|
|
332
|
+
state.until = now + (options.debounce?.wait ?? 0);
|
|
333
|
+
this.delayed.push(job);
|
|
334
|
+
this.connectSignal(job);
|
|
335
|
+
this.emit("added", snapshot(job));
|
|
336
|
+
this.requestPump();
|
|
337
|
+
}
|
|
338
|
+
addMany(name, inputs, options) {
|
|
339
|
+
return inputs.map((input) => this.add(name, input, options));
|
|
340
|
+
}
|
|
341
|
+
get(id) {
|
|
342
|
+
const job = this.records.get(id);
|
|
343
|
+
return job ? snapshot(job) : undefined;
|
|
344
|
+
}
|
|
345
|
+
list(status) {
|
|
346
|
+
const jobs = [];
|
|
347
|
+
for (const job of this.records.values()) {
|
|
348
|
+
if (!status || job.status === status) {
|
|
349
|
+
jobs.push(snapshot(job));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return jobs.sort((a, b) => a.createdAt - b.createdAt);
|
|
353
|
+
}
|
|
354
|
+
cancel(id, reason = "Job was cancelled") {
|
|
355
|
+
const job = this.records.get(id);
|
|
356
|
+
if (!job || isTerminal(job.status)) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const error = new JobCancelledError(id, reason);
|
|
360
|
+
job.status = "cancelled";
|
|
361
|
+
job.finishedAt = Date.now();
|
|
362
|
+
job.error = serializeError(error);
|
|
363
|
+
job.errorCause = error;
|
|
364
|
+
job.controller?.abort(error);
|
|
365
|
+
this.finish(job, "cancelled");
|
|
366
|
+
this.notifySizeWaiters();
|
|
367
|
+
this.requestPump();
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
clear(reason = "Queue was cleared") {
|
|
371
|
+
let count = 0;
|
|
372
|
+
for (const job of this.records.values()) {
|
|
373
|
+
if ((job.status === "queued" || job.status === "scheduled") &&
|
|
374
|
+
this.cancel(job.id, reason)) {
|
|
375
|
+
count += 1;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return count;
|
|
379
|
+
}
|
|
380
|
+
retry(id) {
|
|
381
|
+
this.assertOpen();
|
|
382
|
+
const job = this.records.get(id);
|
|
383
|
+
if (!job ||
|
|
384
|
+
(job.status !== "failed" &&
|
|
385
|
+
job.status !== "cancelled" &&
|
|
386
|
+
job.status !== "expired")) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
job.status = "queued";
|
|
390
|
+
job.attempt = 0;
|
|
391
|
+
job.runAt = Date.now();
|
|
392
|
+
job.expiresAt = undefined;
|
|
393
|
+
job.startedAt = undefined;
|
|
394
|
+
job.finishedAt = undefined;
|
|
395
|
+
job.progress = undefined;
|
|
396
|
+
job.output = undefined;
|
|
397
|
+
job.error = undefined;
|
|
398
|
+
job.errorCause = undefined;
|
|
399
|
+
job.logs = [];
|
|
400
|
+
job.sequence = this.sequence++;
|
|
401
|
+
job.controller = undefined;
|
|
402
|
+
job.completion = deferred();
|
|
403
|
+
this.idleNotified = false;
|
|
404
|
+
if (job.key) {
|
|
405
|
+
this.keys.set(job.key, job);
|
|
406
|
+
}
|
|
407
|
+
this.ready.push(job);
|
|
408
|
+
this.connectSignal(job);
|
|
409
|
+
this.notifySizeWaiters();
|
|
410
|
+
this.requestPump();
|
|
411
|
+
return this.handle(job, false);
|
|
412
|
+
}
|
|
413
|
+
pause() {
|
|
414
|
+
this.assertOpen();
|
|
415
|
+
this.started = false;
|
|
416
|
+
this.clearTimer();
|
|
417
|
+
return this;
|
|
418
|
+
}
|
|
419
|
+
start() {
|
|
420
|
+
this.assertOpen();
|
|
421
|
+
this.started = true;
|
|
422
|
+
this.requestPump();
|
|
423
|
+
return this;
|
|
424
|
+
}
|
|
425
|
+
async onIdle() {
|
|
426
|
+
if (this.size === 0 && this.runningCount === 0) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
await new Promise((resolve) => this.idleWaiters.add(resolve));
|
|
430
|
+
}
|
|
431
|
+
async onSizeLessThan(limit) {
|
|
432
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
433
|
+
throw new RangeError("limit must be a positive integer");
|
|
434
|
+
}
|
|
435
|
+
if (this.size < limit) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
await new Promise((resolve) => this.sizeWaiters.add({ limit, resolve }));
|
|
439
|
+
}
|
|
440
|
+
cleanup(options = {}) {
|
|
441
|
+
const olderThan = options.olderThan ?? 0;
|
|
442
|
+
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
443
|
+
if (!Number.isFinite(olderThan) || olderThan < 0) {
|
|
444
|
+
throw new RangeError("olderThan must be a non-negative finite number");
|
|
445
|
+
}
|
|
446
|
+
if ((!Number.isInteger(limit) && limit !== Number.POSITIVE_INFINITY) ||
|
|
447
|
+
limit < 0) {
|
|
448
|
+
throw new RangeError("limit must be a non-negative integer or Infinity");
|
|
449
|
+
}
|
|
450
|
+
const threshold = Date.now() - olderThan;
|
|
451
|
+
const removed = [];
|
|
452
|
+
for (const [id, job] of this.records) {
|
|
453
|
+
if (removed.length >= limit ||
|
|
454
|
+
!isTerminal(job.status) ||
|
|
455
|
+
(job.finishedAt ?? Number.POSITIVE_INFINITY) > threshold) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
this.records.delete(id);
|
|
459
|
+
removed.push(id);
|
|
460
|
+
}
|
|
461
|
+
return removed;
|
|
462
|
+
}
|
|
463
|
+
async close(options = {}) {
|
|
464
|
+
if (this.closed) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (options.drain ?? true) {
|
|
468
|
+
this.started = true;
|
|
469
|
+
this.requestPump();
|
|
470
|
+
await this.onIdle();
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
this.clear("Queue closed");
|
|
474
|
+
for (const job of this.records.values()) {
|
|
475
|
+
if (job.status === "running") {
|
|
476
|
+
this.cancel(job.id, "Queue closed");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (this.runningCount > 0) {
|
|
480
|
+
await new Promise((resolve) => this.idleWaiters.add(resolve));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
this.started = false;
|
|
484
|
+
this.closed = true;
|
|
485
|
+
this.clearTimer();
|
|
486
|
+
}
|
|
487
|
+
on(event, listener) {
|
|
488
|
+
let group = this.listeners.get(event);
|
|
489
|
+
if (!group) {
|
|
490
|
+
group = new Set();
|
|
491
|
+
this.listeners.set(event, group);
|
|
492
|
+
}
|
|
493
|
+
group.add(listener);
|
|
494
|
+
return () => {
|
|
495
|
+
group?.delete(listener);
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
/** @internal Used by the awaitable job handle. */
|
|
499
|
+
async resultFor(job) {
|
|
500
|
+
const result = isTerminal(job.status)
|
|
501
|
+
? snapshot(job)
|
|
502
|
+
: await job.completion.promise;
|
|
503
|
+
if (result.status === "succeeded") {
|
|
504
|
+
return result.output;
|
|
505
|
+
}
|
|
506
|
+
if (result.status === "cancelled") {
|
|
507
|
+
throw new JobCancelledError(job.id, result.error?.message);
|
|
508
|
+
}
|
|
509
|
+
if (result.status === "expired") {
|
|
510
|
+
throw new JobExpiredError(job.id);
|
|
511
|
+
}
|
|
512
|
+
throw new JobFailedError(job.id, result.error?.message ?? `Job "${job.id}" failed`, job.errorCause ? { cause: job.errorCause } : undefined);
|
|
513
|
+
}
|
|
514
|
+
handle(job, deduplicated) {
|
|
515
|
+
return new JobHandle(this, job, deduplicated);
|
|
516
|
+
}
|
|
517
|
+
createId(name, now) {
|
|
518
|
+
return `${this.name}:${name}:${now.toString(36)}:${this.sequence.toString(36)}`;
|
|
519
|
+
}
|
|
520
|
+
connectSignal(job) {
|
|
521
|
+
this.disconnectSignal(job);
|
|
522
|
+
const signal = job.externalSignal;
|
|
523
|
+
if (!signal) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const abort = () => {
|
|
527
|
+
this.cancel(job.id, abortMessage(signal.reason));
|
|
528
|
+
};
|
|
529
|
+
job.abortListener = abort;
|
|
530
|
+
if (signal.aborted) {
|
|
531
|
+
abort();
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
disconnectSignal(job) {
|
|
538
|
+
if (job.externalSignal && job.abortListener) {
|
|
539
|
+
job.externalSignal.removeEventListener("abort", job.abortListener);
|
|
540
|
+
job.abortListener = undefined;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
requestPump() {
|
|
544
|
+
if (this.pumpQueued || !this.started || this.closed) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
this.pumpQueued = true;
|
|
548
|
+
queueMicrotask(() => {
|
|
549
|
+
this.pumpQueued = false;
|
|
550
|
+
this.pump();
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
pump() {
|
|
554
|
+
if (!this.started || this.closed) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
this.clearTimer();
|
|
558
|
+
const now = Date.now();
|
|
559
|
+
this.policyWakeAt = undefined;
|
|
560
|
+
this.prunePolicyState(now);
|
|
561
|
+
this.expireWaiting(now);
|
|
562
|
+
this.promoteDelayed(now);
|
|
563
|
+
this.pruneStarts(now);
|
|
564
|
+
while (this.runningCount < this._concurrency &&
|
|
565
|
+
this.hasRateCapacity()) {
|
|
566
|
+
const job = this.popReady(now);
|
|
567
|
+
if (!job) {
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
this.beginExecutionPolicy(job, now);
|
|
571
|
+
if (this.rateLimit) {
|
|
572
|
+
this.starts.push(Date.now());
|
|
573
|
+
}
|
|
574
|
+
void this.execute(job);
|
|
575
|
+
}
|
|
576
|
+
this.scheduleNextWake();
|
|
577
|
+
this.notifyIdle();
|
|
578
|
+
}
|
|
579
|
+
promoteDelayed(now) {
|
|
580
|
+
while (true) {
|
|
581
|
+
const job = this.peekDelayed();
|
|
582
|
+
if (!job || job.runAt > now) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
this.delayed.pop();
|
|
586
|
+
if (job.status === "scheduled") {
|
|
587
|
+
job.status = "queued";
|
|
588
|
+
this.ready.push(job);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
expireWaiting(now) {
|
|
593
|
+
for (const job of this.records.values()) {
|
|
594
|
+
if ((job.status === "queued" || job.status === "scheduled") &&
|
|
595
|
+
job.expiresAt !== undefined &&
|
|
596
|
+
job.expiresAt <= now) {
|
|
597
|
+
const error = new JobExpiredError(job.id);
|
|
598
|
+
job.status = "expired";
|
|
599
|
+
job.finishedAt = now;
|
|
600
|
+
job.error = serializeError(error);
|
|
601
|
+
job.errorCause = error;
|
|
602
|
+
this.finish(job, "expired");
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
peekDelayed() {
|
|
607
|
+
while (true) {
|
|
608
|
+
const job = this.delayed.peek();
|
|
609
|
+
if (!job || job.status === "scheduled") {
|
|
610
|
+
return job;
|
|
611
|
+
}
|
|
612
|
+
this.delayed.pop();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
popReady(now) {
|
|
616
|
+
const blocked = [];
|
|
617
|
+
let selected;
|
|
618
|
+
while (!selected) {
|
|
619
|
+
const job = this.ready.pop();
|
|
620
|
+
if (!job) {
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
if (job.status !== "queued") {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (this.canStart(job, now)) {
|
|
627
|
+
selected = job;
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
blocked.push(job);
|
|
631
|
+
}
|
|
632
|
+
for (const job of blocked) {
|
|
633
|
+
this.ready.push(job);
|
|
634
|
+
}
|
|
635
|
+
return selected;
|
|
636
|
+
}
|
|
637
|
+
canStart(job, now) {
|
|
638
|
+
if (job.concurrency) {
|
|
639
|
+
const active = this.activeKeys.get(job.concurrency.key) ?? 0;
|
|
640
|
+
if (active >= job.concurrency.limit) {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (job.throttle) {
|
|
645
|
+
const state = this.refillThrottle(job.throttle, now);
|
|
646
|
+
if (state.tokens < 1) {
|
|
647
|
+
const refillPerMs = job.throttle.limit / job.throttle.interval;
|
|
648
|
+
const wakeAt = now + Math.ceil((1 - state.tokens) / refillPerMs);
|
|
649
|
+
this.policyWakeAt =
|
|
650
|
+
this.policyWakeAt === undefined
|
|
651
|
+
? wakeAt
|
|
652
|
+
: Math.min(this.policyWakeAt, wakeAt);
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
beginExecutionPolicy(job, now) {
|
|
659
|
+
if (job.concurrency) {
|
|
660
|
+
this.activeKeys.set(job.concurrency.key, (this.activeKeys.get(job.concurrency.key) ?? 0) + 1);
|
|
661
|
+
}
|
|
662
|
+
if (job.throttle) {
|
|
663
|
+
const state = this.refillThrottle(job.throttle, now);
|
|
664
|
+
state.tokens = Math.max(0, state.tokens - 1);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
releaseExecutionPolicy(job) {
|
|
668
|
+
if (!job.concurrency) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const active = (this.activeKeys.get(job.concurrency.key) ?? 1) - 1;
|
|
672
|
+
if (active <= 0) {
|
|
673
|
+
this.activeKeys.delete(job.concurrency.key);
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
this.activeKeys.set(job.concurrency.key, active);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
refillThrottle(policy, now) {
|
|
680
|
+
let state = this.throttleStates.get(policy.key);
|
|
681
|
+
if (!state) {
|
|
682
|
+
state = { tokens: policy.burst, updatedAt: now };
|
|
683
|
+
this.throttleStates.set(policy.key, state);
|
|
684
|
+
return state;
|
|
685
|
+
}
|
|
686
|
+
const elapsed = Math.max(0, now - state.updatedAt);
|
|
687
|
+
state.tokens = Math.min(policy.burst, state.tokens + elapsed * (policy.limit / policy.interval));
|
|
688
|
+
state.updatedAt = now;
|
|
689
|
+
return state;
|
|
690
|
+
}
|
|
691
|
+
hasReady() {
|
|
692
|
+
while (true) {
|
|
693
|
+
const job = this.ready.peek();
|
|
694
|
+
if (!job) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
if (job.status === "queued") {
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
this.ready.pop();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
hasRateCapacity() {
|
|
704
|
+
return !this.rateLimit || this.starts.length < this.rateLimit.limit;
|
|
705
|
+
}
|
|
706
|
+
pruneStarts(now) {
|
|
707
|
+
if (!this.rateLimit) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const threshold = now - this.rateLimit.interval;
|
|
711
|
+
while (this.starts.length > 0 && this.starts[0] <= threshold) {
|
|
712
|
+
this.starts.shift();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
scheduleNextWake() {
|
|
716
|
+
let wakeAt;
|
|
717
|
+
const delayed = this.peekDelayed();
|
|
718
|
+
if (delayed) {
|
|
719
|
+
wakeAt = delayed.runAt;
|
|
720
|
+
}
|
|
721
|
+
if (this.rateLimit &&
|
|
722
|
+
this.hasReady() &&
|
|
723
|
+
!this.hasRateCapacity() &&
|
|
724
|
+
this.starts[0] !== undefined) {
|
|
725
|
+
const rateWake = this.starts[0] + this.rateLimit.interval;
|
|
726
|
+
wakeAt = wakeAt === undefined ? rateWake : Math.min(wakeAt, rateWake);
|
|
727
|
+
}
|
|
728
|
+
if (this.policyWakeAt !== undefined) {
|
|
729
|
+
wakeAt =
|
|
730
|
+
wakeAt === undefined
|
|
731
|
+
? this.policyWakeAt
|
|
732
|
+
: Math.min(wakeAt, this.policyWakeAt);
|
|
733
|
+
}
|
|
734
|
+
for (const job of this.records.values()) {
|
|
735
|
+
if ((job.status === "queued" || job.status === "scheduled") &&
|
|
736
|
+
job.expiresAt !== undefined) {
|
|
737
|
+
wakeAt =
|
|
738
|
+
wakeAt === undefined
|
|
739
|
+
? job.expiresAt
|
|
740
|
+
: Math.min(wakeAt, job.expiresAt);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (wakeAt === undefined || this.runningCount >= this._concurrency) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const delay = Math.max(0, wakeAt - Date.now());
|
|
747
|
+
this.timer = setTimeout(() => {
|
|
748
|
+
this.timer = undefined;
|
|
749
|
+
this.requestPump();
|
|
750
|
+
}, Math.min(delay, 2_147_483_647));
|
|
751
|
+
}
|
|
752
|
+
async execute(job) {
|
|
753
|
+
const handler = this.handlers[job.name];
|
|
754
|
+
if (typeof handler !== "function" || job.status !== "queued") {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
job.status = "running";
|
|
758
|
+
job.attempt += 1;
|
|
759
|
+
job.startedAt = Date.now();
|
|
760
|
+
job.controller = new AbortController();
|
|
761
|
+
this.runningCount += 1;
|
|
762
|
+
this.emit("started", snapshot(job));
|
|
763
|
+
this.notifySizeWaiters();
|
|
764
|
+
let timeoutTimer;
|
|
765
|
+
try {
|
|
766
|
+
const context = {
|
|
767
|
+
id: job.id,
|
|
768
|
+
name: job.name,
|
|
769
|
+
attempt: job.attempt,
|
|
770
|
+
signal: job.controller.signal,
|
|
771
|
+
progress: (value) => {
|
|
772
|
+
if (job.status === "running") {
|
|
773
|
+
job.progress = value;
|
|
774
|
+
this.emit("progress", snapshot(job));
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
log: (entry) => {
|
|
778
|
+
if (job.status !== "running" || this.logLimit === 0) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
job.logs.push(entry);
|
|
782
|
+
if (job.logs.length > this.logLimit) {
|
|
783
|
+
job.logs.splice(0, job.logs.length - this.logLimit);
|
|
784
|
+
}
|
|
785
|
+
this.emit("log", { job: snapshot(job), entry });
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
const execution = Promise.resolve(handler(job.input, context));
|
|
789
|
+
const output = job.timeout === undefined
|
|
790
|
+
? await execution
|
|
791
|
+
: await Promise.race([
|
|
792
|
+
execution,
|
|
793
|
+
new Promise((_, reject) => {
|
|
794
|
+
timeoutTimer = setTimeout(() => {
|
|
795
|
+
const error = new JobTimeoutError(job.id, job.timeout);
|
|
796
|
+
job.controller?.abort(error);
|
|
797
|
+
reject(error);
|
|
798
|
+
}, job.timeout);
|
|
799
|
+
}),
|
|
800
|
+
]);
|
|
801
|
+
if (!isCancelled(job)) {
|
|
802
|
+
job.status = "succeeded";
|
|
803
|
+
job.output = output;
|
|
804
|
+
job.error = undefined;
|
|
805
|
+
job.errorCause = undefined;
|
|
806
|
+
job.finishedAt = Date.now();
|
|
807
|
+
this.finish(job, "succeeded");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
catch (cause) {
|
|
811
|
+
if (!isCancelled(job)) {
|
|
812
|
+
try {
|
|
813
|
+
await this.handleFailure(job, toError(cause));
|
|
814
|
+
}
|
|
815
|
+
catch (policyCause) {
|
|
816
|
+
const policyError = toError(policyCause);
|
|
817
|
+
job.error = serializeError(policyError);
|
|
818
|
+
job.errorCause = policyError;
|
|
819
|
+
job.status = "failed";
|
|
820
|
+
job.finishedAt = Date.now();
|
|
821
|
+
this.finish(job, "failed");
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
finally {
|
|
826
|
+
if (timeoutTimer !== undefined) {
|
|
827
|
+
clearTimeout(timeoutTimer);
|
|
828
|
+
}
|
|
829
|
+
job.controller = undefined;
|
|
830
|
+
this.releaseExecutionPolicy(job);
|
|
831
|
+
this.runningCount -= 1;
|
|
832
|
+
this.notifyIdle();
|
|
833
|
+
this.requestPump();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
async handleFailure(job, error) {
|
|
837
|
+
job.error = serializeError(error);
|
|
838
|
+
job.errorCause = error;
|
|
839
|
+
const shouldRetry = job.attempt <= job.retry.retries &&
|
|
840
|
+
(job.retry.when ? await job.retry.when(error, job.attempt) : true);
|
|
841
|
+
if (!shouldRetry) {
|
|
842
|
+
job.status = "failed";
|
|
843
|
+
job.finishedAt = Date.now();
|
|
844
|
+
this.finish(job, "failed");
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const delay = await backoffDelay(job.retry.backoff, job.attempt, error);
|
|
848
|
+
job.runAt = Date.now() + delay;
|
|
849
|
+
job.sequence = this.sequence++;
|
|
850
|
+
job.status = delay > 0 ? "scheduled" : "queued";
|
|
851
|
+
if (job.status === "scheduled") {
|
|
852
|
+
this.delayed.push(job);
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
this.ready.push(job);
|
|
856
|
+
}
|
|
857
|
+
this.emit("retry", { job: snapshot(job), error, delay });
|
|
858
|
+
}
|
|
859
|
+
finish(job, event) {
|
|
860
|
+
this.disconnectSignal(job);
|
|
861
|
+
if (job.key && this.keys.get(job.key) === job) {
|
|
862
|
+
if (job.keyRetention > 0) {
|
|
863
|
+
job.keyExpiresAt = Date.now() + job.keyRetention;
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
this.keys.delete(job.key);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const value = snapshot(job);
|
|
870
|
+
job.completion.resolve(value);
|
|
871
|
+
this.emit(event, value);
|
|
872
|
+
this.pruneHistory();
|
|
873
|
+
this.notifyIdle();
|
|
874
|
+
}
|
|
875
|
+
pruneHistory() {
|
|
876
|
+
let finished = 0;
|
|
877
|
+
for (const job of this.records.values()) {
|
|
878
|
+
if (isTerminal(job.status)) {
|
|
879
|
+
finished += 1;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
let remove = finished - this.historyLimit;
|
|
883
|
+
if (remove <= 0) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
for (const [id, job] of this.records) {
|
|
887
|
+
if (remove <= 0) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (isTerminal(job.status)) {
|
|
891
|
+
if (job.key && this.keys.get(job.key) === job) {
|
|
892
|
+
this.keys.delete(job.key);
|
|
893
|
+
}
|
|
894
|
+
this.records.delete(id);
|
|
895
|
+
remove -= 1;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
prunePolicyState(now) {
|
|
900
|
+
for (const [key, job] of this.keys) {
|
|
901
|
+
if (isTerminal(job.status) &&
|
|
902
|
+
(job.keyExpiresAt === undefined || job.keyExpiresAt <= now)) {
|
|
903
|
+
this.keys.delete(key);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
for (const [key, state] of this.debounceStates) {
|
|
907
|
+
if (state.until <= now &&
|
|
908
|
+
(state.mode === "leading" || isTerminal(state.job.status))) {
|
|
909
|
+
this.debounceStates.delete(key);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
notifyIdle() {
|
|
914
|
+
if (this.size !== 0 || this.runningCount !== 0) {
|
|
915
|
+
this.idleNotified = false;
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
for (const resolve of this.idleWaiters) {
|
|
919
|
+
resolve();
|
|
920
|
+
}
|
|
921
|
+
this.idleWaiters.clear();
|
|
922
|
+
if (!this.idleNotified) {
|
|
923
|
+
this.idleNotified = true;
|
|
924
|
+
this.emit("idle", this.stats);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
notifySizeWaiters() {
|
|
928
|
+
const size = this.size;
|
|
929
|
+
for (const waiter of this.sizeWaiters) {
|
|
930
|
+
if (size < waiter.limit) {
|
|
931
|
+
waiter.resolve();
|
|
932
|
+
this.sizeWaiters.delete(waiter);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
emit(event, payload) {
|
|
937
|
+
const group = this.listeners.get(event);
|
|
938
|
+
if (!group) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
for (const listener of group) {
|
|
942
|
+
try {
|
|
943
|
+
listener(payload);
|
|
944
|
+
}
|
|
945
|
+
catch {
|
|
946
|
+
// Observers cannot break queue processing.
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
clearTimer() {
|
|
951
|
+
if (this.timer !== undefined) {
|
|
952
|
+
clearTimeout(this.timer);
|
|
953
|
+
this.timer = undefined;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
assertOpen() {
|
|
957
|
+
if (this.closed) {
|
|
958
|
+
throw new QueueClosedError(this.name);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/** Create a strongly typed queue from a map of named handlers. */
|
|
963
|
+
export function memoryQueue(handlers, options) {
|
|
964
|
+
return new MemoryQueue(handlers, options);
|
|
965
|
+
}
|
|
966
|
+
class BinaryHeap {
|
|
967
|
+
before;
|
|
968
|
+
values = [];
|
|
969
|
+
constructor(before) {
|
|
970
|
+
this.before = before;
|
|
971
|
+
}
|
|
972
|
+
peek() {
|
|
973
|
+
return this.values[0];
|
|
974
|
+
}
|
|
975
|
+
push(value) {
|
|
976
|
+
this.values.push(value);
|
|
977
|
+
let index = this.values.length - 1;
|
|
978
|
+
while (index > 0) {
|
|
979
|
+
const parent = Math.floor((index - 1) / 2);
|
|
980
|
+
if (!this.before(value, this.values[parent])) {
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
this.values[index] = this.values[parent];
|
|
984
|
+
index = parent;
|
|
985
|
+
}
|
|
986
|
+
this.values[index] = value;
|
|
987
|
+
}
|
|
988
|
+
pop() {
|
|
989
|
+
const first = this.values[0];
|
|
990
|
+
const last = this.values.pop();
|
|
991
|
+
if (first === undefined || last === undefined || this.values.length === 0) {
|
|
992
|
+
return first;
|
|
993
|
+
}
|
|
994
|
+
let index = 0;
|
|
995
|
+
this.values[0] = last;
|
|
996
|
+
while (true) {
|
|
997
|
+
const left = index * 2 + 1;
|
|
998
|
+
const right = left + 1;
|
|
999
|
+
let next = index;
|
|
1000
|
+
if (left < this.values.length &&
|
|
1001
|
+
this.before(this.values[left], this.values[next])) {
|
|
1002
|
+
next = left;
|
|
1003
|
+
}
|
|
1004
|
+
if (right < this.values.length &&
|
|
1005
|
+
this.before(this.values[right], this.values[next])) {
|
|
1006
|
+
next = right;
|
|
1007
|
+
}
|
|
1008
|
+
if (next === index) {
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
[this.values[index], this.values[next]] = [
|
|
1012
|
+
this.values[next],
|
|
1013
|
+
this.values[index],
|
|
1014
|
+
];
|
|
1015
|
+
index = next;
|
|
1016
|
+
}
|
|
1017
|
+
return first;
|
|
1018
|
+
}
|
|
1019
|
+
remove(value) {
|
|
1020
|
+
const filtered = this.values.filter((entry) => entry !== value);
|
|
1021
|
+
if (filtered.length === this.values.length) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
this.values.length = 0;
|
|
1025
|
+
for (const entry of filtered) {
|
|
1026
|
+
this.push(entry);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function readyBefore(left, right) {
|
|
1031
|
+
return (left.priority > right.priority ||
|
|
1032
|
+
(left.priority === right.priority && left.sequence < right.sequence));
|
|
1033
|
+
}
|
|
1034
|
+
function delayedBefore(left, right) {
|
|
1035
|
+
return (left.runAt < right.runAt ||
|
|
1036
|
+
(left.runAt === right.runAt && readyBefore(left, right)));
|
|
1037
|
+
}
|
|
1038
|
+
function deferred() {
|
|
1039
|
+
let resolvePromise;
|
|
1040
|
+
const promise = new Promise((resolve) => {
|
|
1041
|
+
resolvePromise = resolve;
|
|
1042
|
+
});
|
|
1043
|
+
return {
|
|
1044
|
+
promise,
|
|
1045
|
+
resolve(value) {
|
|
1046
|
+
resolvePromise?.(value);
|
|
1047
|
+
},
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
function snapshot(job) {
|
|
1051
|
+
return {
|
|
1052
|
+
id: job.id,
|
|
1053
|
+
name: job.name,
|
|
1054
|
+
input: job.input,
|
|
1055
|
+
status: job.status,
|
|
1056
|
+
priority: job.priority,
|
|
1057
|
+
attempt: job.attempt,
|
|
1058
|
+
retries: job.retry.retries,
|
|
1059
|
+
createdAt: job.createdAt,
|
|
1060
|
+
runAt: job.runAt,
|
|
1061
|
+
expiresAt: job.expiresAt,
|
|
1062
|
+
startedAt: job.startedAt,
|
|
1063
|
+
finishedAt: job.finishedAt,
|
|
1064
|
+
progress: job.progress,
|
|
1065
|
+
output: job.output,
|
|
1066
|
+
error: job.error,
|
|
1067
|
+
logs: [...job.logs],
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function normalizeRetry(retry) {
|
|
1071
|
+
if (retry === undefined) {
|
|
1072
|
+
return { retries: 0, backoff: undefined, when: undefined };
|
|
1073
|
+
}
|
|
1074
|
+
if (typeof retry === "number") {
|
|
1075
|
+
validateNonNegativeInteger("retry", retry);
|
|
1076
|
+
return { retries: retry, backoff: undefined, when: undefined };
|
|
1077
|
+
}
|
|
1078
|
+
validateNonNegativeInteger("retry.retries", retry.retries);
|
|
1079
|
+
return {
|
|
1080
|
+
retries: retry.retries,
|
|
1081
|
+
backoff: retry.backoff,
|
|
1082
|
+
when: retry.when,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
async function backoffDelay(strategy, attempt, error) {
|
|
1086
|
+
if (strategy === undefined) {
|
|
1087
|
+
return 0;
|
|
1088
|
+
}
|
|
1089
|
+
if (typeof strategy === "function") {
|
|
1090
|
+
return normalizeBackoff(await strategy(attempt, error));
|
|
1091
|
+
}
|
|
1092
|
+
if (typeof strategy === "number") {
|
|
1093
|
+
return normalizeBackoff(strategy);
|
|
1094
|
+
}
|
|
1095
|
+
const base = strategy.type === "exponential"
|
|
1096
|
+
? strategy.delay * 2 ** Math.max(0, attempt - 1)
|
|
1097
|
+
: strategy.delay;
|
|
1098
|
+
const jitter = Math.min(1, Math.max(0, strategy.jitter ?? 0));
|
|
1099
|
+
return normalizeBackoff(base * (1 - Math.random() * jitter));
|
|
1100
|
+
}
|
|
1101
|
+
function normalizeBackoff(value) {
|
|
1102
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1103
|
+
throw new RangeError("backoff delay must be a non-negative finite number");
|
|
1104
|
+
}
|
|
1105
|
+
return value;
|
|
1106
|
+
}
|
|
1107
|
+
function normalizeRunAt(delay, now) {
|
|
1108
|
+
if (delay instanceof Date) {
|
|
1109
|
+
const value = delay.getTime();
|
|
1110
|
+
if (!Number.isFinite(value)) {
|
|
1111
|
+
throw new RangeError("delay date must be valid");
|
|
1112
|
+
}
|
|
1113
|
+
return Math.max(now, value);
|
|
1114
|
+
}
|
|
1115
|
+
if (delay === undefined) {
|
|
1116
|
+
return now;
|
|
1117
|
+
}
|
|
1118
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
1119
|
+
throw new RangeError("delay must be a non-negative finite number");
|
|
1120
|
+
}
|
|
1121
|
+
return now + delay;
|
|
1122
|
+
}
|
|
1123
|
+
function validateTimeout(value) {
|
|
1124
|
+
if (value !== undefined) {
|
|
1125
|
+
validatePositiveNumber("timeout", value);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function validateExecutionOptions(options) {
|
|
1129
|
+
if (options.keyRetention !== undefined) {
|
|
1130
|
+
if (!Number.isFinite(options.keyRetention) || options.keyRetention < 0) {
|
|
1131
|
+
throw new RangeError("keyRetention must be a non-negative finite number");
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (options.concurrency) {
|
|
1135
|
+
validatePositiveInteger("concurrency.limit", options.concurrency.limit);
|
|
1136
|
+
validatePolicyKey("concurrency.key", options.concurrency.key);
|
|
1137
|
+
}
|
|
1138
|
+
if (options.throttle) {
|
|
1139
|
+
validatePositiveInteger("throttle.limit", options.throttle.limit);
|
|
1140
|
+
validatePositiveNumber("throttle.interval", options.throttle.interval);
|
|
1141
|
+
validatePositiveInteger("throttle.burst", options.throttle.burst);
|
|
1142
|
+
validatePolicyKey("throttle.key", options.throttle.key);
|
|
1143
|
+
}
|
|
1144
|
+
if (options.debounce) {
|
|
1145
|
+
validatePositiveNumber("debounce.wait", options.debounce.wait);
|
|
1146
|
+
validatePolicyKey("debounce.key", options.debounce.key);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function validatePolicyKey(name, value) {
|
|
1150
|
+
if (!value.trim()) {
|
|
1151
|
+
throw new TypeError(`${name} must not be empty`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function validateHistoryLimit(value) {
|
|
1155
|
+
validateNonNegativeInteger("historyLimit", value);
|
|
1156
|
+
}
|
|
1157
|
+
function validatePositiveInteger(name, value) {
|
|
1158
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1159
|
+
throw new RangeError(`${name} must be a positive integer`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
function validateNonNegativeInteger(name, value) {
|
|
1163
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
1164
|
+
throw new RangeError(`${name} must be a non-negative integer`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function validatePositiveNumber(name, value) {
|
|
1168
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1169
|
+
throw new RangeError(`${name} must be a positive finite number`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function validatePositiveIntegerOrInfinity(name, value) {
|
|
1173
|
+
if (value !== Number.POSITIVE_INFINITY) {
|
|
1174
|
+
validatePositiveInteger(name, value);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
function isTerminal(status) {
|
|
1178
|
+
return (status === "succeeded" ||
|
|
1179
|
+
status === "failed" ||
|
|
1180
|
+
status === "cancelled" ||
|
|
1181
|
+
status === "expired");
|
|
1182
|
+
}
|
|
1183
|
+
function isCancelled(job) {
|
|
1184
|
+
return job.status === "cancelled";
|
|
1185
|
+
}
|
|
1186
|
+
function serializeError(error) {
|
|
1187
|
+
return {
|
|
1188
|
+
name: error.name,
|
|
1189
|
+
message: error.message,
|
|
1190
|
+
stack: error.stack,
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function toError(value) {
|
|
1194
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
1195
|
+
}
|
|
1196
|
+
function abortMessage(reason) {
|
|
1197
|
+
if (reason instanceof Error) {
|
|
1198
|
+
return reason.message;
|
|
1199
|
+
}
|
|
1200
|
+
return reason === undefined ? "Job was aborted" : String(reason);
|
|
1201
|
+
}
|