@zudojs/scheduler 0.1.0 → 1.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/LICENSE +21 -0
- package/README.md +170 -16
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -3
- package/dist/scheduler/constants/index.d.ts +1 -1
- package/dist/scheduler/constants/index.js +1 -1
- package/dist/scheduler/constants/schedulerConstants.core.d.ts +0 -4
- package/dist/scheduler/constants/schedulerConstants.core.js +0 -4
- package/dist/scheduler/duration/duration.parser.d.ts +13 -5
- package/dist/scheduler/duration/duration.parser.js +57 -19
- package/dist/scheduler/errors/index.d.ts +1 -1
- package/dist/scheduler/errors/index.js +1 -1
- package/dist/scheduler/errors/scheduler.errors.d.ts +7 -1
- package/dist/scheduler/errors/scheduler.errors.js +7 -1
- package/dist/scheduler/executor/index.d.ts +1 -1
- package/dist/scheduler/executor/index.js +1 -1
- package/dist/scheduler/executor/jobExecutor.core.d.ts +24 -4
- package/dist/scheduler/executor/jobExecutor.core.js +154 -28
- package/dist/scheduler/job/jobOptions.type.d.ts +11 -0
- package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.d.ts +12 -0
- package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.js +32 -5
- package/dist/scheduler/registry/jobRegistry.core.d.ts +8 -0
- package/dist/scheduler/registry/jobRegistry.core.js +14 -0
- package/dist/scheduler/schedule/schedule.type.d.ts +11 -6
- package/dist/scheduler/schedule/schedule.type.js +10 -1
- package/dist/scheduler/schedule/scheduleOptions.type.d.ts +24 -1
- package/dist/scheduler/scheduleHandle/index.d.ts +1 -0
- package/dist/scheduler/scheduleHandle/scheduleHandle.type.d.ts +26 -3
- package/dist/scheduler/scheduleHandle/scheduleHandle.type.js +23 -7
- package/dist/scheduler/scheduler.core.d.ts +135 -11
- package/dist/scheduler/scheduler.core.js +478 -53
- package/dist/scheduler/trigger/cron.parser.d.ts +44 -0
- package/dist/scheduler/trigger/cron.parser.js +256 -0
- package/dist/scheduler/trigger/index.d.ts +2 -0
- package/dist/scheduler/trigger/index.js +1 -0
- package/dist/scheduler/trigger/schedulerTrigger.core.d.ts +22 -1
- package/dist/scheduler/trigger/schedulerTrigger.core.js +69 -6
- package/dist/scheduler/types/index.d.ts +12 -1
- package/dist/scheduler/types/index.js +7 -0
- package/dist/scheduler/types/schedulerTypes.core.d.ts +7 -87
- package/dist/scheduler/types/schedulerTypes.core.js +7 -1
- package/package.json +26 -15
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/scheduler/clock/index.d.ts.map +0 -1
- package/dist/scheduler/clock/index.js.map +0 -1
- package/dist/scheduler/clock/schedulerClock.type.d.ts.map +0 -1
- package/dist/scheduler/clock/schedulerClock.type.js.map +0 -1
- package/dist/scheduler/constants/index.d.ts.map +0 -1
- package/dist/scheduler/constants/index.js.map +0 -1
- package/dist/scheduler/constants/schedulerConstants.core.d.ts.map +0 -1
- package/dist/scheduler/constants/schedulerConstants.core.js.map +0 -1
- package/dist/scheduler/duration/duration.parser.d.ts.map +0 -1
- package/dist/scheduler/duration/duration.parser.js.map +0 -1
- package/dist/scheduler/duration/index.d.ts.map +0 -1
- package/dist/scheduler/duration/index.js.map +0 -1
- package/dist/scheduler/errors/index.d.ts.map +0 -1
- package/dist/scheduler/errors/index.js.map +0 -1
- package/dist/scheduler/errors/scheduler.errors.d.ts.map +0 -1
- package/dist/scheduler/errors/scheduler.errors.js.map +0 -1
- package/dist/scheduler/executor/index.d.ts.map +0 -1
- package/dist/scheduler/executor/index.js.map +0 -1
- package/dist/scheduler/executor/jobExecutor.core.d.ts.map +0 -1
- package/dist/scheduler/executor/jobExecutor.core.js.map +0 -1
- package/dist/scheduler/job/index.d.ts.map +0 -1
- package/dist/scheduler/job/index.js.map +0 -1
- package/dist/scheduler/job/jobContext.type.d.ts.map +0 -1
- package/dist/scheduler/job/jobContext.type.js.map +0 -1
- package/dist/scheduler/job/jobDefinition.type.d.ts.map +0 -1
- package/dist/scheduler/job/jobDefinition.type.js.map +0 -1
- package/dist/scheduler/job/jobHandler.type.d.ts.map +0 -1
- package/dist/scheduler/job/jobHandler.type.js.map +0 -1
- package/dist/scheduler/job/jobOptions.type.d.ts.map +0 -1
- package/dist/scheduler/job/jobOptions.type.js.map +0 -1
- package/dist/scheduler/priorityQueue/index.d.ts.map +0 -1
- package/dist/scheduler/priorityQueue/index.js.map +0 -1
- package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.d.ts.map +0 -1
- package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.js.map +0 -1
- package/dist/scheduler/registry/index.d.ts.map +0 -1
- package/dist/scheduler/registry/index.js.map +0 -1
- package/dist/scheduler/registry/jobRegistry.core.d.ts.map +0 -1
- package/dist/scheduler/registry/jobRegistry.core.js.map +0 -1
- package/dist/scheduler/schedule/index.d.ts.map +0 -1
- package/dist/scheduler/schedule/index.js.map +0 -1
- package/dist/scheduler/schedule/schedule.type.d.ts.map +0 -1
- package/dist/scheduler/schedule/schedule.type.js.map +0 -1
- package/dist/scheduler/schedule/scheduleOptions.type.d.ts.map +0 -1
- package/dist/scheduler/schedule/scheduleOptions.type.js.map +0 -1
- package/dist/scheduler/scheduleHandle/index.d.ts.map +0 -1
- package/dist/scheduler/scheduleHandle/index.js.map +0 -1
- package/dist/scheduler/scheduleHandle/scheduleHandle.type.d.ts.map +0 -1
- package/dist/scheduler/scheduleHandle/scheduleHandle.type.js.map +0 -1
- package/dist/scheduler/scheduler.core.d.ts.map +0 -1
- package/dist/scheduler/scheduler.core.js.map +0 -1
- package/dist/scheduler/trigger/index.d.ts.map +0 -1
- package/dist/scheduler/trigger/index.js.map +0 -1
- package/dist/scheduler/trigger/schedulerTrigger.core.d.ts.map +0 -1
- package/dist/scheduler/trigger/schedulerTrigger.core.js.map +0 -1
- package/dist/scheduler/trigger/trigger.type.d.ts.map +0 -1
- package/dist/scheduler/trigger/trigger.type.js.map +0 -1
- package/dist/scheduler/types/index.d.ts.map +0 -1
- package/dist/scheduler/types/index.js.map +0 -1
- package/dist/scheduler/types/schedulerTypes.core.d.ts.map +0 -1
- package/dist/scheduler/types/schedulerTypes.core.js.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createJobContext } from "../job/jobContext.type.js";
|
|
2
|
-
import { SchedulerJobExecutionError, SchedulerJobCancelledError, } from "../errors/scheduler.errors.js";
|
|
3
|
-
import { DEFAULT_JOB_TIMEOUT } from "../constants/schedulerConstants.core.js";
|
|
2
|
+
import { SchedulerJobExecutionError, SchedulerJobCancelledError, SchedulerJobTimeoutError, } from "../errors/scheduler.errors.js";
|
|
3
|
+
import { DEFAULT_JOB_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY, MAX_TIMER_DELAY, } from "../constants/schedulerConstants.core.js";
|
|
4
|
+
/** Marker attached to the rejection a timeout produces, carrying its budget. */
|
|
5
|
+
const TIMEOUT = Symbol("scheduler.timeout");
|
|
4
6
|
/**
|
|
5
|
-
* Executes a job with timeout support.
|
|
7
|
+
* Executes a job with timeout, cancellation and retry support.
|
|
6
8
|
*/
|
|
7
9
|
export class JobExecutor {
|
|
8
10
|
clock;
|
|
@@ -10,41 +12,165 @@ export class JobExecutor {
|
|
|
10
12
|
this.clock = clock;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
|
-
* Executes a job.
|
|
15
|
+
* Executes a job, retrying according to its retry policy.
|
|
16
|
+
*
|
|
17
|
+
* @param job - The job definition.
|
|
18
|
+
* @param executionId - Identifier for this execution.
|
|
19
|
+
* @param scheduledAt - The time the job was scheduled to run.
|
|
20
|
+
* @param attempt - The attempt number to start from (1-based).
|
|
21
|
+
* @param signal - Signal that aborts the job and stops further retries.
|
|
22
|
+
* @param data - Optional payload handed to the handler.
|
|
23
|
+
* @returns The execution result.
|
|
14
24
|
*/
|
|
15
|
-
async execute(job, executionId, scheduledAt, attempt, signal) {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
25
|
+
async execute(job, executionId, scheduledAt, attempt, signal, data) {
|
|
26
|
+
const retry = job.options?.retry;
|
|
27
|
+
const maxAttempts = Math.max(1, retry?.attempts ?? 1);
|
|
28
|
+
let currentAttempt = attempt;
|
|
29
|
+
let lastError;
|
|
30
|
+
for (let i = 0; i < maxAttempts; i++, currentAttempt++) {
|
|
23
31
|
if (signal.aborted) {
|
|
24
32
|
throw new SchedulerJobCancelledError("Job was cancelled via signal.", job.id);
|
|
25
33
|
}
|
|
26
|
-
|
|
34
|
+
try {
|
|
35
|
+
await this.runOnce(job, executionId, scheduledAt, currentAttempt, signal, data);
|
|
36
|
+
return { success: true };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (signal.aborted) {
|
|
40
|
+
throw new SchedulerJobCancelledError("Job was cancelled via signal.", job.id);
|
|
41
|
+
}
|
|
42
|
+
lastError = this.classify(error, job.id);
|
|
43
|
+
// Last attempt, or nothing to retry with.
|
|
44
|
+
if (i === maxAttempts - 1)
|
|
45
|
+
break;
|
|
46
|
+
const delay = retryDelay(retry, currentAttempt);
|
|
47
|
+
if (delay > 0) {
|
|
48
|
+
await sleep(delay, signal);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
27
51
|
}
|
|
52
|
+
throw (lastError ??
|
|
53
|
+
new SchedulerJobExecutionError("Unknown job execution error.", job.id));
|
|
54
|
+
}
|
|
55
|
+
/** Runs the handler once, under a timeout that also aborts it. */
|
|
56
|
+
async runOnce(job, executionId, scheduledAt, attempt, signal, data) {
|
|
57
|
+
// A job registered straight into a `JobRegistry` skips `define()`'s
|
|
58
|
+
// validation; clamp so an oversized budget waits, rather than being
|
|
59
|
+
// silently reduced to 1ms by the timer subsystem.
|
|
60
|
+
const timeout = Math.min(job.options?.timeout ?? DEFAULT_JOB_TIMEOUT, MAX_TIMER_DELAY);
|
|
61
|
+
// A dedicated controller per attempt, chained to the caller's signal, so a
|
|
62
|
+
// timeout actually aborts the handler instead of only rejecting the
|
|
63
|
+
// wrapper while the work carries on.
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const onParentAbort = () => controller.abort(signal.reason);
|
|
66
|
+
if (signal.aborted) {
|
|
67
|
+
controller.abort(signal.reason);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
signal.addEventListener("abort", onParentAbort, { once: true });
|
|
71
|
+
}
|
|
72
|
+
const context = createJobContext(job.id, executionId, scheduledAt, this.clock.now(), attempt, data, controller.signal);
|
|
73
|
+
try {
|
|
74
|
+
await this.withTimeout(() => Promise.resolve(job.handler(context)), timeout, controller);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
signal.removeEventListener("abort", onParentAbort);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Maps a thrown value onto the scheduler's error taxonomy. */
|
|
81
|
+
classify(error, jobId) {
|
|
82
|
+
const budget = typeof error === "object" && error !== null
|
|
83
|
+
? error[TIMEOUT]
|
|
84
|
+
: undefined;
|
|
85
|
+
if (typeof budget === "number") {
|
|
86
|
+
// SchedulerJobTimeoutError builds its own message from the budget.
|
|
87
|
+
return new SchedulerJobTimeoutError(budget, jobId);
|
|
88
|
+
}
|
|
89
|
+
if (error instanceof SchedulerJobCancelledError) {
|
|
90
|
+
return error;
|
|
91
|
+
}
|
|
92
|
+
// Carry the original error rather than flattening it to a message string,
|
|
93
|
+
// which used to discard the stack and any nested cause. The published
|
|
94
|
+
// error class takes no options bag, so the cause is attached directly.
|
|
95
|
+
const wrapped = new SchedulerJobExecutionError(error instanceof Error ? error.message : "Unknown job execution error.", jobId);
|
|
96
|
+
Object.defineProperty(wrapped, "cause", {
|
|
97
|
+
value: error,
|
|
98
|
+
writable: true,
|
|
99
|
+
enumerable: false,
|
|
100
|
+
configurable: true,
|
|
101
|
+
});
|
|
102
|
+
return wrapped;
|
|
28
103
|
}
|
|
29
104
|
/**
|
|
30
|
-
* Wraps
|
|
105
|
+
* Wraps an operation with a timeout that aborts it.
|
|
31
106
|
*/
|
|
32
|
-
async withTimeout(operation, timeout,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
clearTimeout(timer);
|
|
41
|
-
resolve(result);
|
|
42
|
-
})
|
|
43
|
-
.catch((error) => {
|
|
44
|
-
clearTimeout(timer);
|
|
107
|
+
async withTimeout(operation, timeout, controller) {
|
|
108
|
+
let timer;
|
|
109
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
110
|
+
timer = setTimeout(() => {
|
|
111
|
+
const error = new Error(`Job timed out after ${timeout}ms.`);
|
|
112
|
+
error[TIMEOUT] = timeout;
|
|
113
|
+
// Abort first so a handler that observes its signal can wind down.
|
|
114
|
+
controller.abort(error);
|
|
45
115
|
reject(error);
|
|
46
|
-
});
|
|
116
|
+
}, timeout);
|
|
117
|
+
if (timer.unref)
|
|
118
|
+
timer.unref();
|
|
47
119
|
});
|
|
120
|
+
try {
|
|
121
|
+
return await Promise.race([operation(), timeoutPromise]);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
if (timer)
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
48
127
|
}
|
|
49
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Computes the delay before the next retry attempt.
|
|
131
|
+
*
|
|
132
|
+
* `RetryPolicy` has carried strategy, delay, maxDelay and jitter since the
|
|
133
|
+
* package was written; none of it was ever applied.
|
|
134
|
+
*/
|
|
135
|
+
export function retryDelay(policy, attempt) {
|
|
136
|
+
if (!policy)
|
|
137
|
+
return 0;
|
|
138
|
+
const base = policy.delay ?? DEFAULT_RETRY_DELAY;
|
|
139
|
+
const attempts = Math.min(attempt, DEFAULT_MAX_RETRIES * 4);
|
|
140
|
+
let delay;
|
|
141
|
+
switch (policy.strategy) {
|
|
142
|
+
case "linear":
|
|
143
|
+
delay = base * attempts;
|
|
144
|
+
break;
|
|
145
|
+
case "exponential":
|
|
146
|
+
delay = base * Math.pow(2, Math.max(0, attempts - 1));
|
|
147
|
+
break;
|
|
148
|
+
case "fixed":
|
|
149
|
+
default:
|
|
150
|
+
delay = base;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (policy.maxDelay !== undefined) {
|
|
154
|
+
delay = Math.min(delay, policy.maxDelay);
|
|
155
|
+
}
|
|
156
|
+
if (policy.jitter) {
|
|
157
|
+
// Full jitter: spreads a thundering herd of retries across the window.
|
|
158
|
+
delay = Math.random() * delay;
|
|
159
|
+
}
|
|
160
|
+
return Math.max(0, Math.round(delay));
|
|
161
|
+
}
|
|
162
|
+
/** Sleeps, resolving early if the signal aborts. */
|
|
163
|
+
function sleep(ms, signal) {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
const timer = setTimeout(finish, ms);
|
|
166
|
+
if (timer.unref)
|
|
167
|
+
timer.unref();
|
|
168
|
+
function finish() {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
signal.removeEventListener("abort", finish);
|
|
171
|
+
resolve();
|
|
172
|
+
}
|
|
173
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
174
|
+
});
|
|
175
|
+
}
|
|
50
176
|
//# sourceMappingURL=jobExecutor.core.js.map
|
|
@@ -3,9 +3,20 @@ import type { OverlapPolicy } from "../types/schedulerTypes.core.js";
|
|
|
3
3
|
* Options for a scheduled job.
|
|
4
4
|
*/
|
|
5
5
|
export interface JobOptions {
|
|
6
|
+
/** Per-execution timeout in ms. Default: 30 000. */
|
|
6
7
|
readonly timeout?: number;
|
|
8
|
+
/** Retry policy applied to a failing execution. */
|
|
7
9
|
readonly retry?: RetryPolicy;
|
|
10
|
+
/**
|
|
11
|
+
* Maximum executions of this job in flight at once, across every schedule
|
|
12
|
+
* that fires it. A fire time arriving at the ceiling is held and dispatched
|
|
13
|
+
* as soon as an execution finishes.
|
|
14
|
+
*/
|
|
8
15
|
readonly concurrency?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Default overlap policy for every schedule of this job.
|
|
18
|
+
* `ScheduleOptions.overlap` overrides it.
|
|
19
|
+
*/
|
|
9
20
|
readonly overlap?: OverlapPolicy;
|
|
10
21
|
}
|
|
11
22
|
/**
|
|
@@ -2,6 +2,10 @@ import type { Schedule } from "../schedule/schedule.type.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Priority queue using a min heap for scheduling.
|
|
4
4
|
*
|
|
5
|
+
* Ordered by fire time, with `ScheduleOptions.priority` breaking ties: a
|
|
6
|
+
* higher priority runs first among schedules due at the same instant. That
|
|
7
|
+
* option used to be documented as "reserved" and read by nothing at all.
|
|
8
|
+
*
|
|
5
9
|
* Provides O(log n) insertion and removal, and O(1) peek.
|
|
6
10
|
*/
|
|
7
11
|
export declare class PriorityQueue {
|
|
@@ -22,6 +26,8 @@ export declare class PriorityQueue {
|
|
|
22
26
|
* Removes a schedule by ID.
|
|
23
27
|
*/
|
|
24
28
|
remove(id: string): boolean;
|
|
29
|
+
/** Returns true when a schedule with the given id is queued. */
|
|
30
|
+
has(id: string): boolean;
|
|
25
31
|
/**
|
|
26
32
|
* Returns the number of schedules in the queue.
|
|
27
33
|
*/
|
|
@@ -34,6 +40,12 @@ export declare class PriorityQueue {
|
|
|
34
40
|
* Clears all schedules from the queue.
|
|
35
41
|
*/
|
|
36
42
|
clear(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Orders two schedules: earlier fire time first, then higher priority.
|
|
45
|
+
*
|
|
46
|
+
* @returns A negative number when `a` should run before `b`.
|
|
47
|
+
*/
|
|
48
|
+
private compare;
|
|
37
49
|
/**
|
|
38
50
|
* Bubble up an element to maintain heap property.
|
|
39
51
|
*/
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Priority queue using a min heap for scheduling.
|
|
3
3
|
*
|
|
4
|
+
* Ordered by fire time, with `ScheduleOptions.priority` breaking ties: a
|
|
5
|
+
* higher priority runs first among schedules due at the same instant. That
|
|
6
|
+
* option used to be documented as "reserved" and read by nothing at all.
|
|
7
|
+
*
|
|
4
8
|
* Provides O(log n) insertion and removal, and O(1) peek.
|
|
5
9
|
*/
|
|
6
10
|
export class PriorityQueue {
|
|
@@ -9,6 +13,13 @@ export class PriorityQueue {
|
|
|
9
13
|
* Inserts a schedule into the priority queue.
|
|
10
14
|
*/
|
|
11
15
|
enqueue(schedule) {
|
|
16
|
+
const time = schedule.nextRunAt?.getTime();
|
|
17
|
+
if (typeof time !== "number" || Number.isNaN(time)) {
|
|
18
|
+
// Every heap comparison against NaN is false, so an invalid date never
|
|
19
|
+
// sinks: it parks itself at the head of the queue and blocks everything
|
|
20
|
+
// genuinely due behind it.
|
|
21
|
+
throw new RangeError(`Cannot enqueue schedule "${schedule.id}": nextRunAt is not a valid date`);
|
|
22
|
+
}
|
|
12
23
|
this.heap.push(schedule);
|
|
13
24
|
this.bubbleUp(this.heap.length - 1);
|
|
14
25
|
}
|
|
@@ -47,11 +58,18 @@ export class PriorityQueue {
|
|
|
47
58
|
const last = this.heap.pop();
|
|
48
59
|
if (index < this.heap.length && last !== undefined) {
|
|
49
60
|
this.heap[index] = last;
|
|
61
|
+
// Bubble first; if it moved, the element now at `index` is an ancestor
|
|
62
|
+
// of that subtree and cannot sink, so sinking is a no-op. If it did not
|
|
63
|
+
// move, sinking is the correction that is needed.
|
|
50
64
|
this.bubbleUp(index);
|
|
51
65
|
this.sinkDown(index);
|
|
52
66
|
}
|
|
53
67
|
return true;
|
|
54
68
|
}
|
|
69
|
+
/** Returns true when a schedule with the given id is queued. */
|
|
70
|
+
has(id) {
|
|
71
|
+
return this.heap.some((schedule) => schedule.id === id);
|
|
72
|
+
}
|
|
55
73
|
/**
|
|
56
74
|
* Returns the number of schedules in the queue.
|
|
57
75
|
*/
|
|
@@ -70,6 +88,17 @@ export class PriorityQueue {
|
|
|
70
88
|
clear() {
|
|
71
89
|
this.heap.length = 0;
|
|
72
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Orders two schedules: earlier fire time first, then higher priority.
|
|
93
|
+
*
|
|
94
|
+
* @returns A negative number when `a` should run before `b`.
|
|
95
|
+
*/
|
|
96
|
+
compare(a, b) {
|
|
97
|
+
const byTime = a.nextRunAt.getTime() - b.nextRunAt.getTime();
|
|
98
|
+
if (byTime !== 0)
|
|
99
|
+
return byTime;
|
|
100
|
+
return (b.options?.priority ?? 0) - (a.options?.priority ?? 0);
|
|
101
|
+
}
|
|
73
102
|
/**
|
|
74
103
|
* Bubble up an element to maintain heap property.
|
|
75
104
|
*/
|
|
@@ -78,7 +107,7 @@ export class PriorityQueue {
|
|
|
78
107
|
const parentIndex = Math.floor((index - 1) / 2);
|
|
79
108
|
const parent = this.heap[parentIndex];
|
|
80
109
|
const current = this.heap[index];
|
|
81
|
-
if (
|
|
110
|
+
if (this.compare(current, parent) < 0) {
|
|
82
111
|
[this.heap[parentIndex], this.heap[index]] = [current, parent];
|
|
83
112
|
index = parentIndex;
|
|
84
113
|
}
|
|
@@ -96,13 +125,11 @@ export class PriorityQueue {
|
|
|
96
125
|
const left = 2 * index + 1;
|
|
97
126
|
const right = 2 * index + 2;
|
|
98
127
|
if (left < this.heap.length &&
|
|
99
|
-
this.heap[left].
|
|
100
|
-
this.heap[smallest].nextRunAt.getTime()) {
|
|
128
|
+
this.compare(this.heap[left], this.heap[smallest]) < 0) {
|
|
101
129
|
smallest = left;
|
|
102
130
|
}
|
|
103
131
|
if (right < this.heap.length &&
|
|
104
|
-
this.heap[right].
|
|
105
|
-
this.heap[smallest].nextRunAt.getTime()) {
|
|
132
|
+
this.compare(this.heap[right], this.heap[smallest]) < 0) {
|
|
106
133
|
smallest = right;
|
|
107
134
|
}
|
|
108
135
|
if (smallest !== index) {
|
|
@@ -12,6 +12,14 @@ export declare class JobRegistry {
|
|
|
12
12
|
* Retrieves a job definition by ID.
|
|
13
13
|
*/
|
|
14
14
|
get(id: string): JobDefinition | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Retrieves a job definition by ID, throwing when it is not registered.
|
|
17
|
+
*
|
|
18
|
+
* @param id - The job identifier.
|
|
19
|
+
* @returns The job definition.
|
|
20
|
+
* @throws {SchedulerJobNotFoundError} when no job is registered under `id`.
|
|
21
|
+
*/
|
|
22
|
+
getOrThrow(id: string): JobDefinition;
|
|
15
23
|
/**
|
|
16
24
|
* Determines whether a job is registered.
|
|
17
25
|
*/
|
|
@@ -24,6 +24,20 @@ export class JobRegistry {
|
|
|
24
24
|
get(id) {
|
|
25
25
|
return this.jobs.get(id);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Retrieves a job definition by ID, throwing when it is not registered.
|
|
29
|
+
*
|
|
30
|
+
* @param id - The job identifier.
|
|
31
|
+
* @returns The job definition.
|
|
32
|
+
* @throws {SchedulerJobNotFoundError} when no job is registered under `id`.
|
|
33
|
+
*/
|
|
34
|
+
getOrThrow(id) {
|
|
35
|
+
const job = this.jobs.get(id);
|
|
36
|
+
if (!job) {
|
|
37
|
+
throw new SchedulerJobNotFoundError(id);
|
|
38
|
+
}
|
|
39
|
+
return job;
|
|
40
|
+
}
|
|
27
41
|
/**
|
|
28
42
|
* Determines whether a job is registered.
|
|
29
43
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ScheduleOptions } from "./scheduleOptions.type.js";
|
|
2
|
-
import type { ScheduleState } from "../types/schedulerTypes.core.js";
|
|
2
|
+
import type { ScheduleState, ScheduleType } from "../types/schedulerTypes.core.js";
|
|
3
|
+
export type { ScheduleType };
|
|
3
4
|
/**
|
|
4
5
|
* Schedule definition.
|
|
5
6
|
*/
|
|
@@ -13,12 +14,16 @@ export interface Schedule {
|
|
|
13
14
|
readonly state: ScheduleState;
|
|
14
15
|
readonly options?: ScheduleOptions;
|
|
15
16
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Type of schedule.
|
|
18
|
-
*/
|
|
19
|
-
export type ScheduleType = "once" | "delay" | "interval" | "cron";
|
|
20
17
|
/**
|
|
21
18
|
* Creates a schedule.
|
|
19
|
+
*
|
|
20
|
+
* @param id - Schedule identifier.
|
|
21
|
+
* @param jobId - The job this schedule fires.
|
|
22
|
+
* @param type - The kind of schedule.
|
|
23
|
+
* @param nextRunAt - The first fire time.
|
|
24
|
+
* @param options - Schedule options.
|
|
25
|
+
* @param expression - The cron expression, for cron schedules.
|
|
26
|
+
* @returns A frozen, active schedule.
|
|
22
27
|
*/
|
|
23
|
-
export declare function createSchedule(id: string, jobId: string, type: ScheduleType, nextRunAt: Date, options?: ScheduleOptions): Schedule;
|
|
28
|
+
export declare function createSchedule(id: string, jobId: string, type: ScheduleType, nextRunAt: Date, options?: ScheduleOptions, expression?: string): Schedule;
|
|
24
29
|
//# sourceMappingURL=schedule.type.d.ts.map
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Creates a schedule.
|
|
3
|
+
*
|
|
4
|
+
* @param id - Schedule identifier.
|
|
5
|
+
* @param jobId - The job this schedule fires.
|
|
6
|
+
* @param type - The kind of schedule.
|
|
7
|
+
* @param nextRunAt - The first fire time.
|
|
8
|
+
* @param options - Schedule options.
|
|
9
|
+
* @param expression - The cron expression, for cron schedules.
|
|
10
|
+
* @returns A frozen, active schedule.
|
|
3
11
|
*/
|
|
4
|
-
export function createSchedule(id, jobId, type, nextRunAt, options = {}) {
|
|
12
|
+
export function createSchedule(id, jobId, type, nextRunAt, options = {}, expression) {
|
|
5
13
|
return Object.freeze({
|
|
6
14
|
id,
|
|
7
15
|
jobId,
|
|
@@ -9,6 +17,7 @@ export function createSchedule(id, jobId, type, nextRunAt, options = {}) {
|
|
|
9
17
|
nextRunAt,
|
|
10
18
|
state: "active",
|
|
11
19
|
options: Object.freeze(options),
|
|
20
|
+
...(expression === undefined ? {} : { expression }),
|
|
12
21
|
});
|
|
13
22
|
}
|
|
14
23
|
//# sourceMappingURL=schedule.type.js.map
|
|
@@ -1,10 +1,33 @@
|
|
|
1
|
-
import type { MisfirePolicy } from "../types/schedulerTypes.core.js";
|
|
1
|
+
import type { MisfirePolicy, OverlapPolicy } from "../types/schedulerTypes.core.js";
|
|
2
2
|
/**
|
|
3
3
|
* Options for a schedule.
|
|
4
4
|
*/
|
|
5
5
|
export interface ScheduleOptions {
|
|
6
|
+
/** IANA timezone name. Only "UTC" is currently honoured by CronTrigger. */
|
|
6
7
|
readonly timezone?: string;
|
|
8
|
+
/**
|
|
9
|
+
* How to handle a fire time that has already passed (default: "run-once").
|
|
10
|
+
*
|
|
11
|
+
* `"skip"` refuses the schedule, `"run-once"` fires once immediately and
|
|
12
|
+
* then resumes from now, and `"catch-up"` replays every occurrence that was
|
|
13
|
+
* missed, one per tick, until the schedule has caught up with the clock.
|
|
14
|
+
*/
|
|
7
15
|
readonly misfire?: MisfirePolicy;
|
|
16
|
+
/**
|
|
17
|
+
* How to handle a fire time arriving while a previous run is still going.
|
|
18
|
+
*
|
|
19
|
+
* `"allow"` (default) starts it anyway, `"skip"` drops it, `"queue"` holds
|
|
20
|
+
* it until the running execution finishes, and `"replace"` aborts the
|
|
21
|
+
* running execution. Overrides `JobOptions.overlap` for this schedule.
|
|
22
|
+
*/
|
|
23
|
+
readonly overlap?: OverlapPolicy;
|
|
24
|
+
/**
|
|
25
|
+
* Relative priority, breaking ties between schedules due at the same
|
|
26
|
+
* instant. Higher runs first; the default is 0. It does not let a schedule
|
|
27
|
+
* jump ahead of one that is due earlier.
|
|
28
|
+
*/
|
|
8
29
|
readonly priority?: number;
|
|
30
|
+
/** Payload handed to the job handler as `context.data`. */
|
|
31
|
+
readonly data?: unknown;
|
|
9
32
|
}
|
|
10
33
|
//# sourceMappingURL=scheduleOptions.type.d.ts.map
|
|
@@ -10,14 +10,37 @@ export interface ScheduleHandle {
|
|
|
10
10
|
cancel(): Promise<void>;
|
|
11
11
|
nextRun(): Date | undefined;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The scheduler operations a handle needs to act on its schedule.
|
|
15
|
+
*
|
|
16
|
+
* Without these the handle is a detached object: `cancel()` sets a local field
|
|
17
|
+
* and the job still runs.
|
|
18
|
+
*/
|
|
19
|
+
export interface ScheduleHandleBinding {
|
|
20
|
+
/** Applies a state transition to the live schedule. */
|
|
21
|
+
setState(state: ScheduleState): void;
|
|
22
|
+
/** Reads the live schedule's state, or undefined once it is gone. */
|
|
23
|
+
getState(): ScheduleState | undefined;
|
|
24
|
+
/** Reads the live schedule's next fire time. */
|
|
25
|
+
getNextRun(): Date | undefined;
|
|
26
|
+
/** Aborts any execution of this schedule that is currently in flight. */
|
|
27
|
+
abortRunning(): void;
|
|
28
|
+
}
|
|
13
29
|
/**
|
|
14
30
|
* Implementation of ScheduleHandle.
|
|
31
|
+
*
|
|
32
|
+
* Bound to its scheduler, so pause, resume and cancel reach the queue. An
|
|
33
|
+
* unbound handle (the two-argument form) still tracks state locally, which
|
|
34
|
+
* keeps it usable in tests that do not involve a scheduler.
|
|
15
35
|
*/
|
|
16
36
|
export declare class ScheduleHandleImpl implements ScheduleHandle {
|
|
17
37
|
readonly id: string;
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
constructor(id: string, state: ScheduleState);
|
|
38
|
+
private _state;
|
|
39
|
+
private readonly binding;
|
|
40
|
+
constructor(id: string, state: ScheduleState, binding?: ScheduleHandleBinding);
|
|
41
|
+
/** The schedule's current state, read from the scheduler when bound. */
|
|
42
|
+
get state(): ScheduleState;
|
|
43
|
+
set state(next: ScheduleState);
|
|
21
44
|
pause(): Promise<void>;
|
|
22
45
|
resume(): Promise<void>;
|
|
23
46
|
cancel(): Promise<void>;
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Implementation of ScheduleHandle.
|
|
3
|
+
*
|
|
4
|
+
* Bound to its scheduler, so pause, resume and cancel reach the queue. An
|
|
5
|
+
* unbound handle (the two-argument form) still tracks state locally, which
|
|
6
|
+
* keeps it usable in tests that do not involve a scheduler.
|
|
3
7
|
*/
|
|
4
8
|
export class ScheduleHandleImpl {
|
|
5
9
|
id;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
constructor(id, state) {
|
|
10
|
+
_state;
|
|
11
|
+
binding;
|
|
12
|
+
constructor(id, state, binding) {
|
|
9
13
|
this.id = id;
|
|
10
|
-
this.
|
|
14
|
+
this._state = state;
|
|
15
|
+
this.binding = binding;
|
|
16
|
+
}
|
|
17
|
+
/** The schedule's current state, read from the scheduler when bound. */
|
|
18
|
+
get state() {
|
|
19
|
+
return this.binding?.getState() ?? this._state;
|
|
20
|
+
}
|
|
21
|
+
set state(next) {
|
|
22
|
+
this._state = next;
|
|
23
|
+
this.binding?.setState(next);
|
|
11
24
|
}
|
|
12
25
|
pause() {
|
|
13
26
|
this.state = "paused";
|
|
@@ -18,12 +31,15 @@ export class ScheduleHandleImpl {
|
|
|
18
31
|
return Promise.resolve();
|
|
19
32
|
}
|
|
20
33
|
cancel() {
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
// Abort first: a job already running should stop, not just be removed from
|
|
35
|
+
// future scheduling.
|
|
36
|
+
this.binding?.abortRunning();
|
|
37
|
+
this._state = "cancelled";
|
|
38
|
+
this.binding?.setState("cancelled");
|
|
23
39
|
return Promise.resolve();
|
|
24
40
|
}
|
|
25
41
|
nextRun() {
|
|
26
|
-
return
|
|
42
|
+
return this.binding?.getNextRun();
|
|
27
43
|
}
|
|
28
44
|
}
|
|
29
45
|
//# sourceMappingURL=scheduleHandle.type.js.map
|