@boringnode/queue 0.0.1-alpha.4 → 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/build/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  parse
3
- } from "./chunk-5PDDRF5O.js";
3
+ } from "./chunk-NPQKBCCY.js";
4
4
  import {
5
5
  Locator,
6
6
  QueueManager,
7
7
  debug_default
8
- } from "./chunk-CD45GT6E.js";
8
+ } from "./chunk-US7THLSZ.js";
9
9
  import {
10
10
  DEFAULT_ERROR_RETRY_DELAY,
11
11
  DEFAULT_IDLE_DELAY,
@@ -13,12 +13,14 @@ import {
13
13
  DEFAULT_STALLED_INTERVAL,
14
14
  DEFAULT_STALLED_THRESHOLD,
15
15
  E_INVALID_BASE_DELAY,
16
+ E_INVALID_CRON_EXPRESSION,
16
17
  E_INVALID_MAX_DELAY,
17
18
  E_INVALID_MULTIPLIER,
19
+ E_INVALID_SCHEDULE_CONFIG,
18
20
  E_JOB_MAX_ATTEMPTS_REACHED,
19
21
  E_JOB_TIMEOUT,
20
22
  exceptions_exports
21
- } from "./chunk-HMGNQSSG.js";
23
+ } from "./chunk-SMOKFZ46.js";
22
24
 
23
25
  // src/job_dispatcher.ts
24
26
  import { randomUUID } from "crypto";
@@ -120,11 +122,11 @@ var JobDispatcher = class {
120
122
  /**
121
123
  * Dispatch the job to the queue.
122
124
  *
123
- * @returns The unique job ID
125
+ * @returns A DispatchResult containing the jobId
124
126
  *
125
127
  * @example
126
128
  * ```typescript
127
- * const jobId = await SendEmailJob.dispatch(payload).run()
129
+ * const { jobId } = await SendEmailJob.dispatch(payload).run()
128
130
  * console.log(`Dispatched job: ${jobId}`)
129
131
  * ```
130
132
  */
@@ -145,7 +147,9 @@ var JobDispatcher = class {
145
147
  } else {
146
148
  await adapter.pushOn(this.#queue, payload);
147
149
  }
148
- return id;
150
+ return {
151
+ jobId: id
152
+ };
149
153
  }
150
154
  /**
151
155
  * Thenable implementation for auto-dispatch when awaited.
@@ -154,7 +158,7 @@ var JobDispatcher = class {
154
158
  *
155
159
  * @param onFulfilled - Success callback
156
160
  * @param onRejected - Error callback
157
- * @returns Promise resolving to the job ID
161
+ * @returns Promise resolving to the DispatchResult
158
162
  */
159
163
  then(onFulfilled, onRejected) {
160
164
  return this.run().then(onFulfilled, onRejected);
@@ -170,6 +174,159 @@ var JobDispatcher = class {
170
174
  }
171
175
  };
172
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
+
173
330
  // src/job.ts
174
331
  var Job = class {
175
332
  #payload;
@@ -248,6 +405,34 @@ var Job = class {
248
405
  }
249
406
  return dispatcher;
250
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
+ }
251
436
  };
252
437
 
253
438
  // src/worker.ts
@@ -510,6 +695,7 @@ var Worker = class {
510
695
  while (this.#running) {
511
696
  try {
512
697
  await this.#checkStalledJobs(queues);
698
+ await this.#dispatchDueSchedules();
513
699
  yield* this.#fillPool(queues);
514
700
  if (this.#pool.isEmpty()) {
515
701
  yield { type: "idle", suggestedDelay: this.#idleDelay };
@@ -678,6 +864,156 @@ var Worker = class {
678
864
  this.#shutdownHandler = void 0;
679
865
  }
680
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;
1016
+ }
681
1017
  };
682
1018
 
683
1019
  // src/strategies/backoff_strategy.ts
@@ -829,6 +1165,8 @@ export {
829
1165
  Job,
830
1166
  Locator,
831
1167
  QueueManager,
1168
+ Schedule,
1169
+ ScheduleBuilder,
832
1170
  Worker,
833
1171
  customBackoff,
834
1172
  exceptions_exports as errors,