@boringnode/queue 0.0.1-alpha.3 → 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/README.md +255 -35
- package/build/chunk-NPQKBCCY.js +26 -0
- package/build/chunk-NPQKBCCY.js.map +1 -0
- package/build/chunk-SMOKFZ46.js +117 -0
- package/build/chunk-SMOKFZ46.js.map +1 -0
- package/build/chunk-US7THLSZ.js +357 -0
- package/build/chunk-US7THLSZ.js.map +1 -0
- package/build/index-2Ng_OpVK.d.ts +1013 -0
- package/build/index.d.ts +429 -4
- package/build/index.js +725 -118
- package/build/index.js.map +1 -1
- package/build/src/contracts/adapter.d.ts +1 -1
- package/build/src/drivers/knex_adapter.d.ts +9 -1
- package/build/src/drivers/knex_adapter.js +253 -42
- package/build/src/drivers/knex_adapter.js.map +1 -1
- package/build/src/drivers/redis_adapter.d.ts +8 -2
- package/build/src/drivers/redis_adapter.js +265 -27
- package/build/src/drivers/redis_adapter.js.map +1 -1
- package/build/src/drivers/sync_adapter.d.ts +13 -3
- package/build/src/drivers/sync_adapter.js +43 -8
- package/build/src/drivers/sync_adapter.js.map +1 -1
- package/build/src/types/index.d.ts +1 -0
- package/build/src/types/index.js +1 -0
- package/build/src/types/index.js.map +1 -0
- package/build/src/types/main.d.ts +1 -1
- package/package.json +16 -3
- package/build/chunk-Y6KR3UIR.js +0 -99
- package/build/chunk-Y6KR3UIR.js.map +0 -1
- package/build/job-Bd_c2lFK.d.ts +0 -149
package/build/index.js
CHANGED
|
@@ -1,116 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
parse
|
|
3
|
+
} from "./chunk-NPQKBCCY.js";
|
|
4
|
+
import {
|
|
5
|
+
Locator,
|
|
6
|
+
QueueManager,
|
|
7
|
+
debug_default
|
|
8
|
+
} from "./chunk-US7THLSZ.js";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_ERROR_RETRY_DELAY,
|
|
11
|
+
DEFAULT_IDLE_DELAY,
|
|
12
|
+
DEFAULT_PRIORITY,
|
|
13
|
+
DEFAULT_STALLED_INTERVAL,
|
|
14
|
+
DEFAULT_STALLED_THRESHOLD,
|
|
3
15
|
E_INVALID_BASE_DELAY,
|
|
4
|
-
|
|
16
|
+
E_INVALID_CRON_EXPRESSION,
|
|
5
17
|
E_INVALID_MAX_DELAY,
|
|
6
18
|
E_INVALID_MULTIPLIER,
|
|
19
|
+
E_INVALID_SCHEDULE_CONFIG,
|
|
7
20
|
E_JOB_MAX_ATTEMPTS_REACHED,
|
|
8
21
|
E_JOB_TIMEOUT,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./chunk-Y6KR3UIR.js";
|
|
22
|
+
exceptions_exports
|
|
23
|
+
} from "./chunk-SMOKFZ46.js";
|
|
12
24
|
|
|
13
25
|
// src/job_dispatcher.ts
|
|
14
26
|
import { randomUUID } from "crypto";
|
|
15
|
-
|
|
16
|
-
// src/queue_manager.ts
|
|
17
|
-
var QueueManagerSingleton = class {
|
|
18
|
-
#defaultAdapter;
|
|
19
|
-
#adapters = {};
|
|
20
|
-
#adapterInstances = /* @__PURE__ */ new Map();
|
|
21
|
-
#globalRetryConfig;
|
|
22
|
-
#queueConfigs = /* @__PURE__ */ new Map();
|
|
23
|
-
async init(config) {
|
|
24
|
-
debug_default("initializing queue manager with config: %O", config);
|
|
25
|
-
this.#validateConfig(config);
|
|
26
|
-
this.#adapterInstances.clear();
|
|
27
|
-
this.#defaultAdapter = config.default;
|
|
28
|
-
this.#adapters = config.adapters;
|
|
29
|
-
this.#globalRetryConfig = config.retry;
|
|
30
|
-
if (config.queues) {
|
|
31
|
-
for (const [queue, queueConfig] of Object.entries(config.queues)) {
|
|
32
|
-
this.#queueConfigs.set(queue, queueConfig);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
await Locator.registerFromGlob(config.locations);
|
|
36
|
-
return this;
|
|
37
|
-
}
|
|
38
|
-
use(adapter) {
|
|
39
|
-
if (!adapter) {
|
|
40
|
-
adapter = this.#defaultAdapter;
|
|
41
|
-
}
|
|
42
|
-
const cached = this.#adapterInstances.get(adapter);
|
|
43
|
-
if (cached) {
|
|
44
|
-
return cached;
|
|
45
|
-
}
|
|
46
|
-
const adapterFactory = this.#adapters[adapter];
|
|
47
|
-
if (!adapterFactory) {
|
|
48
|
-
throw new E_CONFIGURATION_ERROR([`Adapter "${adapter}" is not registered`]);
|
|
49
|
-
}
|
|
50
|
-
debug_default('using adapter "%s"', adapter);
|
|
51
|
-
try {
|
|
52
|
-
const instance = adapterFactory();
|
|
53
|
-
this.#adapterInstances.set(adapter, instance);
|
|
54
|
-
return instance;
|
|
55
|
-
} catch (error) {
|
|
56
|
-
throw new Error();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Priority: job > queue > global
|
|
61
|
-
*/
|
|
62
|
-
getMergedRetryConfig(queue, jobRetryConfig) {
|
|
63
|
-
const queueConfig = this.#queueConfigs.get(queue);
|
|
64
|
-
const queueRetryConfig = queueConfig?.retry || {};
|
|
65
|
-
let maxRetries = jobRetryConfig?.maxRetries || queueRetryConfig.maxRetries || this.#globalRetryConfig?.maxRetries || 0;
|
|
66
|
-
let backoff = jobRetryConfig?.backoff || queueRetryConfig.backoff || this.#globalRetryConfig?.backoff;
|
|
67
|
-
return { maxRetries, backoff };
|
|
68
|
-
}
|
|
69
|
-
#validateConfig(config) {
|
|
70
|
-
if (!config.adapters || Object.keys(config.adapters).length === 0) {
|
|
71
|
-
throw new E_CONFIGURATION_ERROR(["At least one adapter must be configured"]);
|
|
72
|
-
}
|
|
73
|
-
if (!config.default) {
|
|
74
|
-
throw new E_CONFIGURATION_ERROR(["Default adapter must be specified"]);
|
|
75
|
-
}
|
|
76
|
-
if (!config.locations || config.locations.length === 0) {
|
|
77
|
-
throw new E_CONFIGURATION_ERROR(["Job locations must be specified"]);
|
|
78
|
-
}
|
|
79
|
-
if (!config.adapters[config.default]) {
|
|
80
|
-
throw new E_CONFIGURATION_ERROR([
|
|
81
|
-
`Default adapter "${config.default}" not found in adapters configuration`
|
|
82
|
-
]);
|
|
83
|
-
}
|
|
84
|
-
for (const [name, factory] of Object.entries(config.adapters)) {
|
|
85
|
-
if (typeof factory !== "function") {
|
|
86
|
-
throw new E_CONFIGURATION_ERROR([`Adapter "${name}" must be a factory function`]);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async destroy() {
|
|
91
|
-
for (const [name, adapter] of this.#adapterInstances) {
|
|
92
|
-
debug_default('destroying adapter "%s"', name);
|
|
93
|
-
await adapter.destroy();
|
|
94
|
-
}
|
|
95
|
-
this.#adapterInstances.clear();
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
var QueueManager = new QueueManagerSingleton();
|
|
99
|
-
|
|
100
|
-
// src/utils.ts
|
|
101
|
-
import { parse as parseDuration } from "@lukeed/ms";
|
|
102
|
-
function parse(duration) {
|
|
103
|
-
if (typeof duration === "number") {
|
|
104
|
-
return duration;
|
|
105
|
-
}
|
|
106
|
-
const milliseconds = parseDuration(duration);
|
|
107
|
-
if (typeof milliseconds === "undefined") {
|
|
108
|
-
throw new E_INVALID_DURATION_EXPRESSION([duration]);
|
|
109
|
-
}
|
|
110
|
-
return milliseconds;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// src/job_dispatcher.ts
|
|
114
27
|
var JobDispatcher = class {
|
|
115
28
|
#name;
|
|
116
29
|
#payload;
|
|
@@ -118,26 +31,105 @@ var JobDispatcher = class {
|
|
|
118
31
|
#adapter;
|
|
119
32
|
#delay;
|
|
120
33
|
#priority;
|
|
34
|
+
/**
|
|
35
|
+
* Create a new job dispatcher.
|
|
36
|
+
*
|
|
37
|
+
* @param name - The job class name (used to locate the class at runtime)
|
|
38
|
+
* @param payload - The data to pass to the job
|
|
39
|
+
*/
|
|
121
40
|
constructor(name, payload) {
|
|
122
41
|
this.#name = name;
|
|
123
42
|
this.#payload = payload;
|
|
124
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Set the target queue for this job.
|
|
46
|
+
*
|
|
47
|
+
* @param queue - Queue name (default: 'default')
|
|
48
|
+
* @returns This dispatcher for chaining
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* await SendEmailJob.dispatch(payload).toQueue('emails')
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
125
55
|
toQueue(queue) {
|
|
126
56
|
this.#queue = queue;
|
|
127
57
|
return this;
|
|
128
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Delay the job execution.
|
|
61
|
+
*
|
|
62
|
+
* The job will be stored in a delayed state and moved to pending
|
|
63
|
+
* after the delay expires.
|
|
64
|
+
*
|
|
65
|
+
* @param delay - Delay as milliseconds or duration string ('5s', '1h', '7d')
|
|
66
|
+
* @returns This dispatcher for chaining
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* // Send reminder in 24 hours
|
|
71
|
+
* await ReminderJob.dispatch(payload).in('24h')
|
|
72
|
+
*
|
|
73
|
+
* // Process in 5 minutes
|
|
74
|
+
* await CleanupJob.dispatch(payload).in('5m')
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
129
77
|
in(delay) {
|
|
130
78
|
this.#delay = delay;
|
|
131
79
|
return this;
|
|
132
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Set the job priority.
|
|
83
|
+
*
|
|
84
|
+
* Lower numbers = higher priority. Jobs with lower priority values
|
|
85
|
+
* are processed before jobs with higher values.
|
|
86
|
+
*
|
|
87
|
+
* @param priority - Priority level (1-10, default: 5)
|
|
88
|
+
* @returns This dispatcher for chaining
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* // High priority job
|
|
93
|
+
* await UrgentJob.dispatch(payload).priority(1)
|
|
94
|
+
*
|
|
95
|
+
* // Low priority job
|
|
96
|
+
* await BackgroundJob.dispatch(payload).priority(10)
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
133
99
|
priority(priority) {
|
|
134
100
|
this.#priority = priority;
|
|
135
101
|
return this;
|
|
136
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Use a specific adapter for this job.
|
|
105
|
+
*
|
|
106
|
+
* @param adapter - Adapter name or factory function
|
|
107
|
+
* @returns This dispatcher for chaining
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* // Use named adapter
|
|
112
|
+
* await Job.dispatch(payload).with('redis')
|
|
113
|
+
*
|
|
114
|
+
* // Use custom adapter instance
|
|
115
|
+
* await Job.dispatch(payload).with(() => new CustomAdapter())
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
137
118
|
with(adapter) {
|
|
138
119
|
this.#adapter = adapter;
|
|
139
120
|
return this;
|
|
140
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Dispatch the job to the queue.
|
|
124
|
+
*
|
|
125
|
+
* @returns A DispatchResult containing the jobId
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* const { jobId } = await SendEmailJob.dispatch(payload).run()
|
|
130
|
+
* console.log(`Dispatched job: ${jobId}`)
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
141
133
|
async run() {
|
|
142
134
|
const id = randomUUID();
|
|
143
135
|
debug_default("dispatching job %s with id %s using payload %s", this.#name, id, this.#payload);
|
|
@@ -155,8 +147,19 @@ var JobDispatcher = class {
|
|
|
155
147
|
} else {
|
|
156
148
|
await adapter.pushOn(this.#queue, payload);
|
|
157
149
|
}
|
|
158
|
-
return
|
|
150
|
+
return {
|
|
151
|
+
jobId: id
|
|
152
|
+
};
|
|
159
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Thenable implementation for auto-dispatch when awaited.
|
|
156
|
+
*
|
|
157
|
+
* Allows `await Job.dispatch(payload)` without explicit `.run()`.
|
|
158
|
+
*
|
|
159
|
+
* @param onFulfilled - Success callback
|
|
160
|
+
* @param onRejected - Error callback
|
|
161
|
+
* @returns Promise resolving to the DispatchResult
|
|
162
|
+
*/
|
|
160
163
|
then(onFulfilled, onRejected) {
|
|
161
164
|
return this.run().then(onFulfilled, onRejected);
|
|
162
165
|
}
|
|
@@ -171,16 +174,221 @@ var JobDispatcher = class {
|
|
|
171
174
|
}
|
|
172
175
|
};
|
|
173
176
|
|
|
177
|
+
// src/schedule_builder.ts
|
|
178
|
+
import { CronExpressionParser } from "cron-parser";
|
|
179
|
+
var ScheduleBuilder = class {
|
|
180
|
+
#jobName;
|
|
181
|
+
#payload;
|
|
182
|
+
#id;
|
|
183
|
+
#cronExpression;
|
|
184
|
+
#everyMs;
|
|
185
|
+
#timezone = "UTC";
|
|
186
|
+
#from;
|
|
187
|
+
#to;
|
|
188
|
+
#limit;
|
|
189
|
+
constructor(jobName, payload) {
|
|
190
|
+
this.#jobName = jobName;
|
|
191
|
+
this.#payload = payload;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Set a custom schedule ID.
|
|
195
|
+
* If not specified, defaults to the job name.
|
|
196
|
+
* If a schedule with this ID exists, it will be updated (upsert).
|
|
197
|
+
*/
|
|
198
|
+
id(scheduleId) {
|
|
199
|
+
this.#id = scheduleId;
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Set a cron expression for the schedule.
|
|
204
|
+
* Mutually exclusive with `every()`.
|
|
205
|
+
*/
|
|
206
|
+
cron(expression) {
|
|
207
|
+
this.#cronExpression = expression;
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Set a repeating interval for the schedule.
|
|
212
|
+
* Mutually exclusive with `cron()`.
|
|
213
|
+
*/
|
|
214
|
+
every(interval) {
|
|
215
|
+
this.#everyMs = parse(interval);
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Set the timezone for cron evaluation.
|
|
220
|
+
* @default 'UTC'
|
|
221
|
+
*/
|
|
222
|
+
timezone(tz) {
|
|
223
|
+
this.#timezone = tz;
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Set the start boundary for the schedule.
|
|
228
|
+
* No jobs will be dispatched before this date.
|
|
229
|
+
*/
|
|
230
|
+
from(date) {
|
|
231
|
+
this.#from = date;
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Set the end boundary for the schedule.
|
|
236
|
+
* No jobs will be dispatched after this date.
|
|
237
|
+
*/
|
|
238
|
+
to(date) {
|
|
239
|
+
this.#to = date;
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Set both start and end boundaries for the schedule.
|
|
244
|
+
* Shorthand for `.from(start).to(end)`.
|
|
245
|
+
*/
|
|
246
|
+
between(from, to) {
|
|
247
|
+
return this.from(from).to(to);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Set the maximum number of runs for this schedule.
|
|
251
|
+
*/
|
|
252
|
+
limit(maxRuns) {
|
|
253
|
+
this.#limit = maxRuns;
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Create the schedule and return the schedule ID.
|
|
258
|
+
*/
|
|
259
|
+
async run() {
|
|
260
|
+
if (!this.#cronExpression && !this.#everyMs) {
|
|
261
|
+
throw new E_INVALID_SCHEDULE_CONFIG([
|
|
262
|
+
"Schedule must have either a cron expression or an interval"
|
|
263
|
+
]);
|
|
264
|
+
}
|
|
265
|
+
if (this.#cronExpression && this.#everyMs) {
|
|
266
|
+
throw new E_INVALID_SCHEDULE_CONFIG([
|
|
267
|
+
"Schedule cannot have both a cron expression and an interval"
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
if (this.#cronExpression) {
|
|
271
|
+
try {
|
|
272
|
+
CronExpressionParser.parse(this.#cronExpression, { tz: this.#timezone });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new E_INVALID_CRON_EXPRESSION([this.#cronExpression, error.message]);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const config = {
|
|
278
|
+
id: this.#id ?? this.#jobName,
|
|
279
|
+
jobName: this.#jobName,
|
|
280
|
+
payload: this.#payload,
|
|
281
|
+
cronExpression: this.#cronExpression,
|
|
282
|
+
everyMs: this.#everyMs,
|
|
283
|
+
timezone: this.#timezone,
|
|
284
|
+
from: this.#from,
|
|
285
|
+
to: this.#to,
|
|
286
|
+
limit: this.#limit
|
|
287
|
+
};
|
|
288
|
+
const adapter = QueueManager.use();
|
|
289
|
+
const scheduleId = await adapter.createSchedule(config);
|
|
290
|
+
const nextRunAt = this.#calculateNextRunAt();
|
|
291
|
+
await adapter.updateSchedule(scheduleId, { nextRunAt });
|
|
292
|
+
return { scheduleId };
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Calculate the next run time based on cron or interval.
|
|
296
|
+
*/
|
|
297
|
+
#calculateNextRunAt() {
|
|
298
|
+
const now = /* @__PURE__ */ new Date();
|
|
299
|
+
let nextRun;
|
|
300
|
+
if (this.#cronExpression) {
|
|
301
|
+
const cron = CronExpressionParser.parse(this.#cronExpression, {
|
|
302
|
+
currentDate: now,
|
|
303
|
+
tz: this.#timezone
|
|
304
|
+
});
|
|
305
|
+
nextRun = cron.next().toDate();
|
|
306
|
+
} else {
|
|
307
|
+
nextRun = new Date(now.getTime() + this.#everyMs);
|
|
308
|
+
}
|
|
309
|
+
if (this.#from && nextRun < this.#from) {
|
|
310
|
+
if (this.#cronExpression) {
|
|
311
|
+
const cron = CronExpressionParser.parse(this.#cronExpression, {
|
|
312
|
+
currentDate: this.#from,
|
|
313
|
+
tz: this.#timezone
|
|
314
|
+
});
|
|
315
|
+
nextRun = cron.next().toDate();
|
|
316
|
+
} else {
|
|
317
|
+
nextRun = this.#from;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return nextRun;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Implement PromiseLike to allow `await builder.every('5m')` syntax.
|
|
324
|
+
*/
|
|
325
|
+
then(onfulfilled, onrejected) {
|
|
326
|
+
return this.run().then(onfulfilled, onrejected);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
174
330
|
// src/job.ts
|
|
175
331
|
var Job = class {
|
|
176
332
|
#payload;
|
|
333
|
+
#context;
|
|
334
|
+
/** Static options for this job class (queue, retries, timeout, etc.) */
|
|
177
335
|
static options = {};
|
|
336
|
+
/** The payload data passed to this job instance */
|
|
178
337
|
get payload() {
|
|
179
338
|
return this.#payload;
|
|
180
339
|
}
|
|
181
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Context information for the current job execution.
|
|
342
|
+
*
|
|
343
|
+
* Provides metadata such as job ID, current attempt number,
|
|
344
|
+
* queue name, priority, and timing information.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```typescript
|
|
348
|
+
* async execute() {
|
|
349
|
+
* if (this.context.attempt > 1) {
|
|
350
|
+
* console.log(`Retry attempt ${this.context.attempt}`)
|
|
351
|
+
* }
|
|
352
|
+
* console.log(`Processing job ${this.context.jobId} on queue ${this.context.queue}`)
|
|
353
|
+
* }
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
get context() {
|
|
357
|
+
return this.#context;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Create a new job instance.
|
|
361
|
+
*
|
|
362
|
+
* @param payload - The data to be processed by this job
|
|
363
|
+
* @param context - The job execution context (provided by the worker)
|
|
364
|
+
*/
|
|
365
|
+
constructor(payload, context) {
|
|
182
366
|
this.#payload = payload;
|
|
367
|
+
this.#context = Object.freeze(context);
|
|
183
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Dispatch this job to the queue.
|
|
371
|
+
*
|
|
372
|
+
* Returns a JobDispatcher for fluent configuration before dispatching.
|
|
373
|
+
* The job is not actually dispatched until `.run()` is called or the
|
|
374
|
+
* dispatcher is awaited.
|
|
375
|
+
*
|
|
376
|
+
* @param payload - The data to pass to the job
|
|
377
|
+
* @returns A JobDispatcher for fluent configuration
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```typescript
|
|
381
|
+
* // Simple dispatch
|
|
382
|
+
* await SendEmailJob.dispatch({ to: 'user@example.com', subject: 'Hello' })
|
|
383
|
+
*
|
|
384
|
+
* // With options
|
|
385
|
+
* await SendEmailJob.dispatch({ to: 'user@example.com' })
|
|
386
|
+
* .toQueue('high-priority')
|
|
387
|
+
* .priority(1)
|
|
388
|
+
* .in('5m')
|
|
389
|
+
* .run()
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
184
392
|
static dispatch(payload) {
|
|
185
393
|
const dispatcher = new JobDispatcher(
|
|
186
394
|
this.jobName,
|
|
@@ -197,6 +405,34 @@ var Job = class {
|
|
|
197
405
|
}
|
|
198
406
|
return dispatcher;
|
|
199
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Create a schedule for this job.
|
|
410
|
+
*
|
|
411
|
+
* Returns a ScheduleBuilder for fluent configuration before creating the schedule.
|
|
412
|
+
* The schedule is not actually created until `.run()` is called or the
|
|
413
|
+
* builder is awaited.
|
|
414
|
+
*
|
|
415
|
+
* @param payload - The data to pass to the job on each run
|
|
416
|
+
* @returns A ScheduleBuilder for fluent configuration
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```typescript
|
|
420
|
+
* // Cron schedule
|
|
421
|
+
* await CleanupJob.schedule({ days: 30 })
|
|
422
|
+
* .id('cleanup-daily')
|
|
423
|
+
* .cron('0 0 * * *')
|
|
424
|
+
* .timezone('Europe/Paris')
|
|
425
|
+
* .run()
|
|
426
|
+
*
|
|
427
|
+
* // Interval schedule
|
|
428
|
+
* await SyncJob.schedule({ source: 'api' })
|
|
429
|
+
* .every('5m')
|
|
430
|
+
* .run()
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
433
|
+
static schedule(payload) {
|
|
434
|
+
return new ScheduleBuilder(this.jobName, payload);
|
|
435
|
+
}
|
|
200
436
|
};
|
|
201
437
|
|
|
202
438
|
// src/worker.ts
|
|
@@ -206,18 +442,45 @@ import { setTimeout } from "timers/promises";
|
|
|
206
442
|
// src/job_pool.ts
|
|
207
443
|
var JobPool = class {
|
|
208
444
|
#activeJobs = /* @__PURE__ */ new Map();
|
|
445
|
+
/** Number of currently running jobs */
|
|
209
446
|
get size() {
|
|
210
447
|
return this.#activeJobs.size;
|
|
211
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* Check if the pool has no running jobs.
|
|
451
|
+
*
|
|
452
|
+
* @returns True if no jobs are running
|
|
453
|
+
*/
|
|
212
454
|
isEmpty() {
|
|
213
455
|
return this.#activeJobs.size === 0;
|
|
214
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Check if the pool can accept more jobs.
|
|
459
|
+
*
|
|
460
|
+
* @param concurrency - Maximum number of concurrent jobs
|
|
461
|
+
* @returns True if there's room for more jobs
|
|
462
|
+
*/
|
|
215
463
|
hasCapacity(concurrency) {
|
|
216
464
|
return this.#activeJobs.size < concurrency;
|
|
217
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Add a job to the pool.
|
|
468
|
+
*
|
|
469
|
+
* @param job - The acquired job data
|
|
470
|
+
* @param queue - The queue the job came from
|
|
471
|
+
* @param promise - Promise that resolves when the job completes
|
|
472
|
+
*/
|
|
218
473
|
add(job, queue, promise) {
|
|
219
474
|
this.#activeJobs.set(job.id, { promise, job, queue });
|
|
220
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Wait for the next job to complete and return it.
|
|
478
|
+
*
|
|
479
|
+
* Uses `Promise.race()` internally, so the fastest job wins.
|
|
480
|
+
* The completed job is removed from the pool.
|
|
481
|
+
*
|
|
482
|
+
* @returns The first job to complete (success or failure)
|
|
483
|
+
*/
|
|
221
484
|
async waitForNextCompletion() {
|
|
222
485
|
const completedJobId = await Promise.race(
|
|
223
486
|
[...this.#activeJobs.entries()].map(async ([id, { promise }]) => {
|
|
@@ -232,6 +495,12 @@ var JobPool = class {
|
|
|
232
495
|
this.#activeJobs.delete(completedJobId);
|
|
233
496
|
return completed;
|
|
234
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Wait for all running jobs to complete.
|
|
500
|
+
*
|
|
501
|
+
* Used during graceful shutdown to ensure no jobs are abandoned.
|
|
502
|
+
* Clears the pool after all jobs finish.
|
|
503
|
+
*/
|
|
235
504
|
async drain() {
|
|
236
505
|
const promises = [...this.#activeJobs.values()].map(async ({ promise }) => {
|
|
237
506
|
try {
|
|
@@ -248,19 +517,46 @@ var JobPool = class {
|
|
|
248
517
|
var Worker = class {
|
|
249
518
|
#id;
|
|
250
519
|
#config;
|
|
520
|
+
#idleDelay;
|
|
521
|
+
#stalledInterval;
|
|
522
|
+
#stalledThreshold;
|
|
523
|
+
#maxStalledCount;
|
|
524
|
+
#concurrency;
|
|
525
|
+
#gracefulShutdown;
|
|
526
|
+
#onShutdownSignal;
|
|
251
527
|
#adapter;
|
|
252
528
|
#running = false;
|
|
253
529
|
#initialized = false;
|
|
254
530
|
#generator;
|
|
255
531
|
#pool;
|
|
532
|
+
#lastStalledCheck = 0;
|
|
533
|
+
#shutdownHandler;
|
|
534
|
+
/** Unique identifier for this worker instance */
|
|
256
535
|
get id() {
|
|
257
536
|
return this.#id;
|
|
258
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Create a new worker instance.
|
|
540
|
+
*
|
|
541
|
+
* @param config - Queue configuration including adapter and worker settings
|
|
542
|
+
*/
|
|
259
543
|
constructor(config) {
|
|
260
544
|
this.#config = config;
|
|
261
545
|
this.#id = randomUUID2();
|
|
546
|
+
this.#idleDelay = parse(config.worker?.idleDelay ?? DEFAULT_IDLE_DELAY);
|
|
547
|
+
this.#stalledInterval = parse(config.worker?.stalledInterval ?? DEFAULT_STALLED_INTERVAL);
|
|
548
|
+
this.#stalledThreshold = parse(config.worker?.stalledThreshold ?? DEFAULT_STALLED_THRESHOLD);
|
|
549
|
+
this.#maxStalledCount = config.worker?.maxStalledCount ?? 1;
|
|
550
|
+
this.#concurrency = config.worker?.concurrency ?? 1;
|
|
551
|
+
this.#gracefulShutdown = config.worker?.gracefulShutdown ?? true;
|
|
552
|
+
this.#onShutdownSignal = config.worker?.onShutdownSignal;
|
|
262
553
|
debug_default("created worker with id %s and config %O", this.#id, config);
|
|
263
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Initialize the worker (called automatically by `start()`).
|
|
557
|
+
*
|
|
558
|
+
* Sets up the QueueManager and adapter connection.
|
|
559
|
+
*/
|
|
264
560
|
async init() {
|
|
265
561
|
if (this.#initialized) {
|
|
266
562
|
return;
|
|
@@ -272,6 +568,23 @@ var Worker = class {
|
|
|
272
568
|
this.#initialized = true;
|
|
273
569
|
debug_default("worker %s initialized", this.#id);
|
|
274
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* Start processing jobs from the specified queues.
|
|
573
|
+
*
|
|
574
|
+
* This method blocks until the worker is stopped (via `stop()` or signal).
|
|
575
|
+
* Jobs are processed concurrently up to the configured concurrency limit.
|
|
576
|
+
*
|
|
577
|
+
* @param queues - Queue names to process (default: ['default'])
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* ```typescript
|
|
581
|
+
* // Process single queue
|
|
582
|
+
* await worker.start()
|
|
583
|
+
*
|
|
584
|
+
* // Process multiple queues (priority order)
|
|
585
|
+
* await worker.start(['high-priority', 'default', 'low-priority'])
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
275
588
|
async start(queues = ["default"]) {
|
|
276
589
|
await this.init();
|
|
277
590
|
if (this.#running) {
|
|
@@ -280,7 +593,7 @@ var Worker = class {
|
|
|
280
593
|
}
|
|
281
594
|
this.#running = true;
|
|
282
595
|
debug_default("starting worker %s on queues: %O", this.#id, queues);
|
|
283
|
-
|
|
596
|
+
this.#setupGracefulShutdown();
|
|
284
597
|
for await (const cycle of this.process(queues)) {
|
|
285
598
|
if (["started", "completed"].includes(cycle.type)) {
|
|
286
599
|
continue;
|
|
@@ -296,6 +609,12 @@ var Worker = class {
|
|
|
296
609
|
}
|
|
297
610
|
}
|
|
298
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Stop the worker gracefully.
|
|
614
|
+
*
|
|
615
|
+
* Waits for all running jobs to complete before shutting down.
|
|
616
|
+
* Called automatically on SIGINT/SIGTERM if gracefulShutdown is enabled.
|
|
617
|
+
*/
|
|
299
618
|
async stop() {
|
|
300
619
|
debug_default("stopping worker %s", this.#id);
|
|
301
620
|
this.#running = false;
|
|
@@ -306,7 +625,29 @@ var Worker = class {
|
|
|
306
625
|
if (this.#adapter) {
|
|
307
626
|
await this.#adapter.destroy();
|
|
308
627
|
}
|
|
628
|
+
this.#removeShutdownHandlers();
|
|
309
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Process a single cycle and return the result.
|
|
632
|
+
*
|
|
633
|
+
* Useful for testing or when you need fine-grained control.
|
|
634
|
+
* Each cycle may start new jobs, complete a job, or return idle.
|
|
635
|
+
*
|
|
636
|
+
* @param queues - Queue names to process
|
|
637
|
+
* @returns The cycle result, or null if the worker was stopped
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* ```typescript
|
|
641
|
+
* const worker = new Worker(config)
|
|
642
|
+
*
|
|
643
|
+
* // Process cycles manually
|
|
644
|
+
* let cycle = await worker.processCycle(['default'])
|
|
645
|
+
* while (cycle) {
|
|
646
|
+
* console.log('Cycle:', cycle.type)
|
|
647
|
+
* cycle = await worker.processCycle(['default'])
|
|
648
|
+
* }
|
|
649
|
+
* ```
|
|
650
|
+
*/
|
|
310
651
|
async processCycle(queues) {
|
|
311
652
|
await this.init();
|
|
312
653
|
this.#running = true;
|
|
@@ -320,26 +661,59 @@ var Worker = class {
|
|
|
320
661
|
}
|
|
321
662
|
return result.value;
|
|
322
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Generator that yields worker cycle events.
|
|
666
|
+
*
|
|
667
|
+
* Low-level API for processing jobs. Yields events for:
|
|
668
|
+
* - `started`: A new job began execution
|
|
669
|
+
* - `completed`: A job finished (success or failure)
|
|
670
|
+
* - `idle`: No jobs available, suggest waiting
|
|
671
|
+
* - `error`: An error occurred during processing
|
|
672
|
+
*
|
|
673
|
+
* @param queues - Queue names to process
|
|
674
|
+
* @yields WorkerCycle events
|
|
675
|
+
*
|
|
676
|
+
* @example
|
|
677
|
+
* ```typescript
|
|
678
|
+
* for await (const cycle of worker.process(['default'])) {
|
|
679
|
+
* switch (cycle.type) {
|
|
680
|
+
* case 'started':
|
|
681
|
+
* console.log(`Started job ${cycle.job.id}`)
|
|
682
|
+
* break
|
|
683
|
+
* case 'completed':
|
|
684
|
+
* console.log(`Completed job ${cycle.job.id}`)
|
|
685
|
+
* break
|
|
686
|
+
* case 'idle':
|
|
687
|
+
* await sleep(cycle.suggestedDelay)
|
|
688
|
+
* break
|
|
689
|
+
* }
|
|
690
|
+
* }
|
|
691
|
+
* ```
|
|
692
|
+
*/
|
|
323
693
|
async *process(queues) {
|
|
324
|
-
const pollingInterval = parse(this.#config.worker?.pollingInterval || "2s");
|
|
325
694
|
this.#pool = new JobPool();
|
|
326
695
|
while (this.#running) {
|
|
327
696
|
try {
|
|
697
|
+
await this.#checkStalledJobs(queues);
|
|
698
|
+
await this.#dispatchDueSchedules();
|
|
328
699
|
yield* this.#fillPool(queues);
|
|
329
700
|
if (this.#pool.isEmpty()) {
|
|
330
|
-
yield { type: "idle", suggestedDelay:
|
|
701
|
+
yield { type: "idle", suggestedDelay: this.#idleDelay };
|
|
331
702
|
continue;
|
|
332
703
|
}
|
|
333
704
|
const completed = await this.#pool.waitForNextCompletion();
|
|
334
705
|
yield { type: "completed", queue: completed.queue, job: completed.job };
|
|
335
706
|
} catch (error) {
|
|
336
|
-
yield {
|
|
707
|
+
yield {
|
|
708
|
+
type: "error",
|
|
709
|
+
error,
|
|
710
|
+
suggestedDelay: parse(DEFAULT_ERROR_RETRY_DELAY)
|
|
711
|
+
};
|
|
337
712
|
}
|
|
338
713
|
}
|
|
339
714
|
}
|
|
340
715
|
async *#fillPool(queues) {
|
|
341
|
-
const
|
|
342
|
-
const slotsAvailable = concurrency - this.#pool.size;
|
|
716
|
+
const slotsAvailable = this.#concurrency - this.#pool.size;
|
|
343
717
|
if (slotsAvailable <= 0) return;
|
|
344
718
|
const popPromises = Array.from({ length: slotsAvailable }, () => this.#acquireNextJob(queues));
|
|
345
719
|
const results = await Promise.all(popPromises);
|
|
@@ -400,7 +774,17 @@ var Worker = class {
|
|
|
400
774
|
async #initJob(job, queue) {
|
|
401
775
|
try {
|
|
402
776
|
const JobClass = Locator.getOrThrow(job.name);
|
|
403
|
-
const
|
|
777
|
+
const context = Object.freeze({
|
|
778
|
+
jobId: job.id,
|
|
779
|
+
name: job.name,
|
|
780
|
+
attempt: job.attempts + 1,
|
|
781
|
+
queue,
|
|
782
|
+
priority: job.priority ?? DEFAULT_PRIORITY,
|
|
783
|
+
acquiredAt: new Date(job.acquiredAt),
|
|
784
|
+
stalledCount: job.stalledCount ?? 0
|
|
785
|
+
});
|
|
786
|
+
const jobFactory = QueueManager.getJobFactory();
|
|
787
|
+
const instance = jobFactory ? await jobFactory(JobClass, job.payload, context) : new JobClass(job.payload, context);
|
|
404
788
|
const options = JobClass.options || {};
|
|
405
789
|
const timeout = this.#getJobTimeout(options);
|
|
406
790
|
return { instance, options, timeout };
|
|
@@ -442,14 +826,193 @@ var Worker = class {
|
|
|
442
826
|
}
|
|
443
827
|
return null;
|
|
444
828
|
}
|
|
445
|
-
async #
|
|
446
|
-
const
|
|
829
|
+
async #checkStalledJobs(queues) {
|
|
830
|
+
const now = Date.now();
|
|
831
|
+
if (now - this.#lastStalledCheck < this.#stalledInterval) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
this.#lastStalledCheck = now;
|
|
835
|
+
for (const queue of queues) {
|
|
836
|
+
const recovered = await this.#adapter.recoverStalledJobs(
|
|
837
|
+
queue,
|
|
838
|
+
this.#stalledThreshold,
|
|
839
|
+
this.#maxStalledCount
|
|
840
|
+
);
|
|
841
|
+
if (recovered > 0) {
|
|
842
|
+
debug_default("worker %s: recovered %d stalled jobs from queue %s", this.#id, recovered, queue);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
#setupGracefulShutdown() {
|
|
847
|
+
if (!this.#gracefulShutdown) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
this.#shutdownHandler = async () => {
|
|
447
851
|
debug_default("received shutdown signal, stopping worker...");
|
|
852
|
+
if (this.#onShutdownSignal) {
|
|
853
|
+
await this.#onShutdownSignal();
|
|
854
|
+
}
|
|
448
855
|
await this.stop();
|
|
449
|
-
process.exit(0);
|
|
450
856
|
};
|
|
451
|
-
process.on("SIGINT",
|
|
452
|
-
process.on("SIGTERM",
|
|
857
|
+
process.on("SIGINT", this.#shutdownHandler);
|
|
858
|
+
process.on("SIGTERM", this.#shutdownHandler);
|
|
859
|
+
}
|
|
860
|
+
#removeShutdownHandlers() {
|
|
861
|
+
if (this.#shutdownHandler) {
|
|
862
|
+
process.off("SIGINT", this.#shutdownHandler);
|
|
863
|
+
process.off("SIGTERM", this.#shutdownHandler);
|
|
864
|
+
this.#shutdownHandler = void 0;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Dispatch any due scheduled jobs.
|
|
869
|
+
*
|
|
870
|
+
* Claims due schedules from the adapter and dispatches the corresponding
|
|
871
|
+
* jobs to their configured queues.
|
|
872
|
+
*/
|
|
873
|
+
async #dispatchDueSchedules() {
|
|
874
|
+
while (true) {
|
|
875
|
+
const schedule = await this.#adapter.claimDueSchedule();
|
|
876
|
+
if (!schedule) {
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
debug_default(
|
|
880
|
+
"worker %s: dispatching scheduled job %s (schedule: %s, runCount: %d)",
|
|
881
|
+
this.#id,
|
|
882
|
+
schedule.jobName,
|
|
883
|
+
schedule.id,
|
|
884
|
+
schedule.runCount + 1
|
|
885
|
+
);
|
|
886
|
+
const JobClass = Locator.get(schedule.jobName);
|
|
887
|
+
const queue = JobClass?.options?.queue ?? "default";
|
|
888
|
+
await this.#adapter.pushOn(queue, {
|
|
889
|
+
id: randomUUID2(),
|
|
890
|
+
name: schedule.jobName,
|
|
891
|
+
payload: schedule.payload,
|
|
892
|
+
attempts: 0,
|
|
893
|
+
priority: JobClass?.options?.priority
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// src/schedule.ts
|
|
900
|
+
var Schedule = class _Schedule {
|
|
901
|
+
#data;
|
|
902
|
+
constructor(data) {
|
|
903
|
+
this.#data = data;
|
|
904
|
+
}
|
|
905
|
+
get id() {
|
|
906
|
+
return this.#data.id;
|
|
907
|
+
}
|
|
908
|
+
get jobName() {
|
|
909
|
+
return this.#data.jobName;
|
|
910
|
+
}
|
|
911
|
+
get payload() {
|
|
912
|
+
return this.#data.payload;
|
|
913
|
+
}
|
|
914
|
+
get cronExpression() {
|
|
915
|
+
return this.#data.cronExpression;
|
|
916
|
+
}
|
|
917
|
+
get everyMs() {
|
|
918
|
+
return this.#data.everyMs;
|
|
919
|
+
}
|
|
920
|
+
get timezone() {
|
|
921
|
+
return this.#data.timezone;
|
|
922
|
+
}
|
|
923
|
+
get from() {
|
|
924
|
+
return this.#data.from;
|
|
925
|
+
}
|
|
926
|
+
get to() {
|
|
927
|
+
return this.#data.to;
|
|
928
|
+
}
|
|
929
|
+
get limit() {
|
|
930
|
+
return this.#data.limit;
|
|
931
|
+
}
|
|
932
|
+
get runCount() {
|
|
933
|
+
return this.#data.runCount;
|
|
934
|
+
}
|
|
935
|
+
get nextRunAt() {
|
|
936
|
+
return this.#data.nextRunAt;
|
|
937
|
+
}
|
|
938
|
+
get lastRunAt() {
|
|
939
|
+
return this.#data.lastRunAt;
|
|
940
|
+
}
|
|
941
|
+
get status() {
|
|
942
|
+
return this.#data.status;
|
|
943
|
+
}
|
|
944
|
+
get createdAt() {
|
|
945
|
+
return this.#data.createdAt;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Find a schedule by ID.
|
|
949
|
+
*
|
|
950
|
+
* @param id - The schedule ID
|
|
951
|
+
* @returns The schedule instance, or null if not found
|
|
952
|
+
*/
|
|
953
|
+
static async find(id) {
|
|
954
|
+
const adapter = QueueManager.use();
|
|
955
|
+
const data = await adapter.getSchedule(id);
|
|
956
|
+
if (!data) return null;
|
|
957
|
+
return new _Schedule(data);
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* List all schedules matching the given options.
|
|
961
|
+
*
|
|
962
|
+
* @param options - Optional filters for listing
|
|
963
|
+
* @returns Array of schedule instances
|
|
964
|
+
*/
|
|
965
|
+
static async list(options) {
|
|
966
|
+
const adapter = QueueManager.use();
|
|
967
|
+
const schedules = await adapter.listSchedules(options);
|
|
968
|
+
return schedules.map((data) => new _Schedule(data));
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Pause this schedule.
|
|
972
|
+
* No jobs will be dispatched while paused.
|
|
973
|
+
*/
|
|
974
|
+
async pause() {
|
|
975
|
+
const adapter = QueueManager.use();
|
|
976
|
+
await adapter.updateSchedule(this.#data.id, { status: "paused" });
|
|
977
|
+
this.#data.status = "paused";
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Resume this schedule.
|
|
981
|
+
* Jobs will be dispatched according to the schedule.
|
|
982
|
+
*/
|
|
983
|
+
async resume() {
|
|
984
|
+
const adapter = QueueManager.use();
|
|
985
|
+
await adapter.updateSchedule(this.#data.id, { status: "active" });
|
|
986
|
+
this.#data.status = "active";
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Delete this schedule permanently.
|
|
990
|
+
*/
|
|
991
|
+
async delete() {
|
|
992
|
+
const adapter = QueueManager.use();
|
|
993
|
+
await adapter.deleteSchedule(this.#data.id);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Trigger immediate execution of this schedule's job.
|
|
997
|
+
* Also updates runCount and lastRunAt.
|
|
998
|
+
*
|
|
999
|
+
* If the schedule has reached its limit, the job will not be dispatched.
|
|
1000
|
+
*/
|
|
1001
|
+
async trigger() {
|
|
1002
|
+
if (this.#data.limit !== null && this.#data.runCount >= this.#data.limit) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const adapter = QueueManager.use();
|
|
1006
|
+
const dispatcher = new JobDispatcher(this.#data.jobName, this.#data.payload);
|
|
1007
|
+
await dispatcher.run();
|
|
1008
|
+
const now = /* @__PURE__ */ new Date();
|
|
1009
|
+
const newRunCount = this.#data.runCount + 1;
|
|
1010
|
+
await adapter.updateSchedule(this.#data.id, {
|
|
1011
|
+
runCount: newRunCount,
|
|
1012
|
+
lastRunAt: now
|
|
1013
|
+
});
|
|
1014
|
+
this.#data.runCount = newRunCount;
|
|
1015
|
+
this.#data.lastRunAt = now;
|
|
453
1016
|
}
|
|
454
1017
|
};
|
|
455
1018
|
|
|
@@ -458,10 +1021,33 @@ import { RuntimeException } from "@poppinss/utils";
|
|
|
458
1021
|
import { assertUnreachable } from "@poppinss/utils/assert";
|
|
459
1022
|
var BackoffStrategy = class {
|
|
460
1023
|
#config;
|
|
1024
|
+
/**
|
|
1025
|
+
* Create a new backoff strategy.
|
|
1026
|
+
*
|
|
1027
|
+
* @param config - Backoff configuration
|
|
1028
|
+
* @throws {E_INVALID_BASE_DELAY} If baseDelay is not positive
|
|
1029
|
+
* @throws {E_INVALID_MAX_DELAY} If maxDelay is invalid
|
|
1030
|
+
* @throws {E_INVALID_MULTIPLIER} If multiplier is invalid
|
|
1031
|
+
*/
|
|
461
1032
|
constructor(config) {
|
|
462
1033
|
this.#config = config;
|
|
463
1034
|
this.#validateConfig();
|
|
464
1035
|
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Calculate the delay for a given attempt number.
|
|
1038
|
+
*
|
|
1039
|
+
* @param attempt - The attempt number (1-based)
|
|
1040
|
+
* @returns Delay in milliseconds
|
|
1041
|
+
* @throws {RuntimeException} If attempt is less than 1
|
|
1042
|
+
*
|
|
1043
|
+
* @example
|
|
1044
|
+
* ```typescript
|
|
1045
|
+
* // Exponential: 1s, 2s, 4s, 8s, 16s, ...
|
|
1046
|
+
* strategy.calculateDelay(1) // 1000
|
|
1047
|
+
* strategy.calculateDelay(2) // 2000
|
|
1048
|
+
* strategy.calculateDelay(3) // 4000
|
|
1049
|
+
* ```
|
|
1050
|
+
*/
|
|
465
1051
|
calculateDelay(attempt) {
|
|
466
1052
|
if (attempt < 1) {
|
|
467
1053
|
throw new RuntimeException("Attempt number must be >= 1");
|
|
@@ -489,10 +1075,27 @@ var BackoffStrategy = class {
|
|
|
489
1075
|
}
|
|
490
1076
|
return Math.floor(delay);
|
|
491
1077
|
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Get the Date when the next retry should occur.
|
|
1080
|
+
*
|
|
1081
|
+
* @param attempt - The attempt number (1-based)
|
|
1082
|
+
* @returns Date for the next retry
|
|
1083
|
+
*
|
|
1084
|
+
* @example
|
|
1085
|
+
* ```typescript
|
|
1086
|
+
* const nextRetry = strategy.getNextRetryAt(3)
|
|
1087
|
+
* console.log(`Retry at: ${nextRetry.toISOString()}`)
|
|
1088
|
+
* ```
|
|
1089
|
+
*/
|
|
492
1090
|
getNextRetryAt(attempt) {
|
|
493
1091
|
const delay = this.calculateDelay(attempt);
|
|
494
1092
|
return new Date(Date.now() + delay);
|
|
495
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Get a frozen copy of the configuration.
|
|
1096
|
+
*
|
|
1097
|
+
* @returns Readonly configuration object
|
|
1098
|
+
*/
|
|
496
1099
|
getConfig() {
|
|
497
1100
|
return Object.freeze({ ...this.#config });
|
|
498
1101
|
}
|
|
@@ -560,9 +1163,13 @@ function customBackoff(config) {
|
|
|
560
1163
|
}
|
|
561
1164
|
export {
|
|
562
1165
|
Job,
|
|
1166
|
+
Locator,
|
|
563
1167
|
QueueManager,
|
|
1168
|
+
Schedule,
|
|
1169
|
+
ScheduleBuilder,
|
|
564
1170
|
Worker,
|
|
565
1171
|
customBackoff,
|
|
1172
|
+
exceptions_exports as errors,
|
|
566
1173
|
exponentialBackoff,
|
|
567
1174
|
fixedBackoff,
|
|
568
1175
|
linearBackoff
|