@orion-js/dogs 4.6.2 → 4.6.5

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/dist/index.js CHANGED
@@ -48,27 +48,169 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
48
48
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
49
49
 
50
50
  // src/index.ts
51
- import { getInstance as getInstance2 } from "@orion-js/services";
51
+ import { getInstance as getInstance3 } from "@orion-js/services";
52
52
 
53
- // src/services/EventsService.ts
54
- import { logger as logger2 } from "@orion-js/logger";
55
- import { Inject, Service } from "@orion-js/services";
53
+ // src/repos/JobsHistoryRepo.ts
54
+ import { MongoCollection, Repository } from "@orion-js/mongodb";
55
+
56
+ // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/isInteger.js
57
+ function _isInteger(n) {
58
+ return n << 0 === n;
59
+ }
60
+ var isInteger = Number.isInteger || _isInteger;
61
+
62
+ // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/createPath.js
63
+ function createPath(path, delimiter = ".") {
64
+ return typeof path === "string" ? path.split(delimiter).map((x) => isInteger(x) ? Number(x) : x) : path;
65
+ }
66
+
67
+ // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/compare.js
68
+ function compare(a, b) {
69
+ return String(a) === String(b);
70
+ }
71
+
72
+ // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/includes.js
73
+ function includes(a, list) {
74
+ let index = -1;
75
+ const { length } = list;
76
+ while (++index < length)
77
+ if (compare(list[index], a))
78
+ return true;
79
+ return false;
80
+ }
81
+
82
+ // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/omit.js
83
+ function omit(propsToOmit, obj) {
84
+ if (arguments.length === 1) return (_obj) => omit(propsToOmit, _obj);
85
+ if (obj === null || obj === void 0)
86
+ return void 0;
87
+ const propsToOmitValue = createPath(propsToOmit, ",");
88
+ const willReturn = {};
89
+ for (const key in obj)
90
+ if (!includes(key, propsToOmitValue))
91
+ willReturn[key] = obj[key];
92
+ return willReturn;
93
+ }
94
+
95
+ // src/types/HistoryRecord.ts
96
+ import { schemaWithName } from "@orion-js/schema";
97
+ var HistoryRecordSchema = schemaWithName("HistoryRecord", {
98
+ _id: { type: "string" },
99
+ jobId: { type: "string" },
100
+ executionId: { type: "string" },
101
+ jobName: { type: "string" },
102
+ type: { type: "string" },
103
+ priority: { type: "number" },
104
+ tries: { type: "number" },
105
+ uniqueIdentifier: { type: "string", optional: true },
106
+ startedAt: { type: "date" },
107
+ endedAt: { type: "date" },
108
+ duration: { type: "number" },
109
+ expiresAt: { type: "date", optional: true },
110
+ status: { type: "string", enum: ["success", "error", "stale"] },
111
+ errorMessage: { type: "string", optional: true },
112
+ params: { type: "blackbox", optional: true },
113
+ result: { type: "any", optional: true }
114
+ });
115
+
116
+ // src/repos/JobsHistoryRepo.ts
117
+ var _history_dec, _JobsHistoryRepo_decorators, _init;
118
+ _JobsHistoryRepo_decorators = [Repository()], _history_dec = [MongoCollection({
119
+ name: "orionjs.jobs_dogs_history",
120
+ idGeneration: "uuid",
121
+ schema: HistoryRecordSchema,
122
+ indexes: [
123
+ {
124
+ keys: {
125
+ jobName: 1,
126
+ startedAt: 1
127
+ }
128
+ },
129
+ {
130
+ keys: {
131
+ executionId: 1
132
+ }
133
+ },
134
+ {
135
+ keys: {
136
+ expiresAt: 1
137
+ },
138
+ options: {
139
+ expireAfterSeconds: 0
140
+ }
141
+ }
142
+ ]
143
+ })];
144
+ var JobsHistoryRepo = class {
145
+ constructor() {
146
+ __publicField(this, "history", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
147
+ }
148
+ async saveExecution(record) {
149
+ await this.history.upsert(
150
+ { executionId: record.executionId },
151
+ {
152
+ $setOnInsert: {
153
+ status: record.status
154
+ },
155
+ $set: {
156
+ ...omit(["status"], record)
157
+ }
158
+ }
159
+ );
160
+ }
161
+ async getExecutions(jobName, limit, skip) {
162
+ const cursor = this.history.find({ jobName }).sort({ startedAt: -1 });
163
+ if (skip) {
164
+ cursor.skip(skip);
165
+ }
166
+ if (limit) {
167
+ cursor.limit(limit);
168
+ }
169
+ return await cursor.toArray();
170
+ }
171
+ };
172
+ _init = __decoratorStart(null);
173
+ __decorateElement(_init, 5, "history", _history_dec, JobsHistoryRepo);
174
+ JobsHistoryRepo = __decorateElement(_init, 0, "JobsHistoryRepo", _JobsHistoryRepo_decorators, JobsHistoryRepo);
175
+ __runInitializers(_init, 1, JobsHistoryRepo);
56
176
 
57
177
  // src/repos/JobsRepo.ts
58
178
  import { generateId } from "@orion-js/helpers";
59
179
  import { logger } from "@orion-js/logger";
60
- import { MongoCollection, Repository } from "@orion-js/mongodb";
180
+ import { MongoCollection as MongoCollection2, Repository as Repository2 } from "@orion-js/mongodb";
181
+
182
+ // src/services/partitions.ts
183
+ function validatePartitionsCount(nPartitions) {
184
+ if (!Number.isInteger(nPartitions) || nPartitions <= 0) {
185
+ throw new Error("nPartitions must be a positive integer");
186
+ }
187
+ }
188
+ function getRandomPartition(nPartitions) {
189
+ return Math.floor(Math.random() * nPartitions);
190
+ }
191
+ function createShuffledPartitions(nPartitions) {
192
+ const partitions = Array.from({ length: nPartitions }, (_, partition) => partition);
193
+ for (let index = partitions.length - 1; index > 0; index--) {
194
+ const swapIndex = Math.floor(Math.random() * (index + 1));
195
+ [partitions[index], partitions[swapIndex]] = [partitions[swapIndex], partitions[index]];
196
+ }
197
+ return partitions;
198
+ }
61
199
 
62
200
  // src/types/JobRecord.ts
63
- import { createEnum, schemaWithName } from "@orion-js/schema";
201
+ import { createEnum, schemaWithName as schemaWithName2 } from "@orion-js/schema";
64
202
  var JobStatusEnum = createEnum("JobStatus", ["pending", "maxTriesReached"]);
65
- var JobRecordSchema = schemaWithName("JobRecord", {
203
+ var JobRecordSchema = schemaWithName2("JobRecord", {
66
204
  _id: { type: "string" },
67
205
  jobName: { type: "string" },
68
206
  type: { type: createEnum("JobType", ["recurrent", "event"]) },
69
207
  priority: { type: "number" },
70
208
  uniqueIdentifier: { type: "string", optional: true },
71
209
  nextRunAt: { type: "date" },
210
+ /**
211
+ * Acquisition partition. Optional so records created by previous versions can be reconciled.
212
+ */
213
+ partition: { type: "number", optional: true },
72
214
  lastRunAt: { type: "date", optional: true },
73
215
  lockedUntil: { type: "date", optional: true },
74
216
  /**
@@ -76,6 +218,11 @@ var JobRecordSchema = schemaWithName("JobRecord", {
76
218
  * this value so a stale execution cannot modify a newer claim.
77
219
  */
78
220
  lockId: { type: "string", optional: true },
221
+ /**
222
+ * Internal acquisition metadata used when MongoDB supports sorted updateOne. It preserves the
223
+ * pre-update stale state because updateOne intentionally does not return the previous document.
224
+ */
225
+ claimWasStale: { type: "boolean", optional: true },
79
226
  tries: { type: "number", optional: true },
80
227
  params: { type: "blackbox", optional: true },
81
228
  /**
@@ -100,23 +247,30 @@ var DEFAULT_MAX_TRIES_REACHED_RETENTION_MS = 1e3 * 60 * 60 * 24 * 7;
100
247
  // src/repos/JobsRepo.ts
101
248
  var JOB_ACQUISITION_HINTS = {
102
249
  byJobName: {
250
+ partition: 1,
103
251
  jobName: 1,
104
252
  priority: -1,
105
253
  nextRunAt: 1
106
254
  },
107
255
  globalPriority: {
256
+ partition: 1,
108
257
  priority: -1,
109
258
  nextRunAt: 1
110
259
  }
111
260
  };
112
261
  var INITIAL_JOB_ACQUISITION_HINT = "byJobName";
113
262
  var CANDIDATE_JOB_ACQUISITION_HINT = "globalPriority";
263
+ var SORTED_UPDATE_ONE_MIN_WIRE_VERSION = 25;
264
+ var LOCK_ID_INDEX_NAME = "jobs_dogs_records_lock_id";
265
+ var PARTITION_JOB_NAME_INDEX_NAME = "jobs_dogs_records_partition_job_name_priority_next_run";
266
+ var PARTITION_PRIORITY_INDEX_NAME = "jobs_dogs_records_partition_priority_next_run";
114
267
  var JOB_ACQUISITION_SORT = {
115
268
  priority: -1,
116
269
  nextRunAt: 1
117
270
  };
118
- function getJobAcquisitionSelector(jobNames, now) {
271
+ function getJobAcquisitionSelector(jobNames, now, partition) {
119
272
  return {
273
+ partition,
120
274
  jobName: { $in: jobNames },
121
275
  nextRunAt: { $lte: now },
122
276
  $and: [
@@ -127,17 +281,19 @@ function getJobAcquisitionSelector(jobNames, now) {
127
281
  ]
128
282
  };
129
283
  }
130
- var _jobs_dec, _JobsRepo_decorators, _init;
131
- _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
284
+ var _jobs_dec, _JobsRepo_decorators, _init2;
285
+ _JobsRepo_decorators = [Repository2()], _jobs_dec = [MongoCollection2({
132
286
  idGeneration: "uuid",
133
287
  name: "orionjs.jobs_dogs_records",
134
288
  schema: JobRecordSchema,
135
289
  indexes: [
136
290
  {
137
- keys: JOB_ACQUISITION_HINTS.byJobName
291
+ keys: JOB_ACQUISITION_HINTS.byJobName,
292
+ options: { name: PARTITION_JOB_NAME_INDEX_NAME }
138
293
  },
139
294
  {
140
- keys: JOB_ACQUISITION_HINTS.globalPriority
295
+ keys: JOB_ACQUISITION_HINTS.globalPriority,
296
+ options: { name: PARTITION_PRIORITY_INDEX_NAME }
141
297
  },
142
298
  {
143
299
  keys: {
@@ -157,6 +313,15 @@ _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
157
313
  sparse: true
158
314
  }
159
315
  },
316
+ {
317
+ keys: {
318
+ lockId: 1
319
+ },
320
+ options: {
321
+ name: LOCK_ID_INDEX_NAME,
322
+ sparse: true
323
+ }
324
+ },
160
325
  {
161
326
  keys: {
162
327
  expiresAt: 1
@@ -168,14 +333,55 @@ _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
168
333
  })];
169
334
  var JobsRepo = class {
170
335
  constructor() {
171
- __publicField(this, "jobs", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
336
+ __publicField(this, "jobs", __runInitializers(_init2, 8, this)), __runInitializers(_init2, 11, this);
337
+ __publicField(this, "sortedUpdateOneSupportPromise");
338
+ __publicField(this, "partitionsByJobName", /* @__PURE__ */ new Map());
339
+ }
340
+ configureJobPartitions(jobNames, nPartitions) {
341
+ for (const jobName of jobNames) {
342
+ this.partitionsByJobName.set(jobName, nPartitions);
343
+ }
344
+ }
345
+ getJobPartitionsCount(jobName) {
346
+ return this.partitionsByJobName.get(jobName);
347
+ }
348
+ async supportsSortedUpdateOne() {
349
+ if (!this.sortedUpdateOneSupportPromise) {
350
+ this.sortedUpdateOneSupportPromise = this.detectSortedUpdateOneSupport();
351
+ }
352
+ return this.sortedUpdateOneSupportPromise;
353
+ }
354
+ async detectSortedUpdateOneSupport() {
355
+ const rawCollection = await this.jobs.getRawCollection();
356
+ await this.jobs.createIndexesPromise;
357
+ const hello = await rawCollection.db.admin().command({ hello: 1 });
358
+ const supportsSortedUpdateOne = typeof hello.maxWireVersion === "number" && hello.maxWireVersion >= SORTED_UPDATE_ONE_MIN_WIRE_VERSION;
359
+ if (!supportsSortedUpdateOne) {
360
+ logger.info("MongoDB does not support sorted updateOne; using findOneAndUpdate for jobs.");
361
+ return false;
362
+ }
363
+ const hasLockIdIndex = await rawCollection.indexExists(LOCK_ID_INDEX_NAME);
364
+ if (!hasLockIdIndex) {
365
+ logger.warn(
366
+ `MongoDB supports sorted updateOne but index "${LOCK_ID_INDEX_NAME}" is unavailable; using findOneAndUpdate for jobs.`
367
+ );
368
+ return false;
369
+ }
370
+ logger.info("MongoDB supports sorted updateOne; using it for job acquisition.");
371
+ return true;
372
+ }
373
+ async getJobAndLock(jobNames, lockTime, hint = INITIAL_JOB_ACQUISITION_HINT, partition = 0) {
374
+ if (await this.supportsSortedUpdateOne()) {
375
+ return this.getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition);
376
+ }
377
+ return this.getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition);
172
378
  }
173
- async getJobAndLock(jobNames, lockTime, hint = INITIAL_JOB_ACQUISITION_HINT) {
379
+ async getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition) {
174
380
  const executionId = generateId();
175
381
  const now = /* @__PURE__ */ new Date();
176
382
  const lockedUntil = new Date(now.getTime() + lockTime);
177
383
  const job = await this.jobs.findOneAndUpdate(
178
- getJobAcquisitionSelector(jobNames, now),
384
+ getJobAcquisitionSelector(jobNames, now, partition),
179
385
  {
180
386
  $set: { lockId: executionId, lockedUntil, lastRunAt: now },
181
387
  $inc: { tries: 1 }
@@ -191,6 +397,37 @@ var JobsRepo = class {
191
397
  if (!job) return;
192
398
  const tries = (job.tries || 0) + 1;
193
399
  const wasStale = Boolean(job.lockedUntil);
400
+ return this.toJobToRun(job, executionId, lockTime, tries, wasStale);
401
+ }
402
+ async getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition) {
403
+ const executionId = generateId();
404
+ const now = /* @__PURE__ */ new Date();
405
+ const lockedUntil = new Date(now.getTime() + lockTime);
406
+ const rawCollection = await this.jobs.getRawCollection();
407
+ const result = await rawCollection.updateOne(
408
+ getJobAcquisitionSelector(jobNames, now, partition),
409
+ [
410
+ {
411
+ $set: {
412
+ lockId: { $literal: executionId },
413
+ lockedUntil: { $literal: lockedUntil },
414
+ lastRunAt: { $literal: now },
415
+ tries: { $add: [{ $ifNull: ["$tries", 0] }, 1] },
416
+ claimWasStale: { $eq: [{ $type: "$lockedUntil" }, "date"] }
417
+ }
418
+ }
419
+ ],
420
+ {
421
+ hint: JOB_ACQUISITION_HINTS[hint],
422
+ sort: JOB_ACQUISITION_SORT
423
+ }
424
+ );
425
+ if (result.matchedCount === 0) return;
426
+ const job = await rawCollection.findOne({ lockId: executionId });
427
+ if (!job) return;
428
+ return this.toJobToRun(job, executionId, lockTime, job.tries || 0, Boolean(job.claimWasStale));
429
+ }
430
+ toJobToRun(job, executionId, lockTime, tries, wasStale) {
194
431
  if (wasStale) {
195
432
  logger.info(`Running job "${job.jobName}" that was staled`);
196
433
  }
@@ -208,9 +445,9 @@ var JobsRepo = class {
208
445
  wasStale
209
446
  };
210
447
  }
211
- async getJobAcquisitionExecutionTime(jobNames, hint) {
448
+ async getJobAcquisitionExecutionTime(jobNames, hint, partition = 0) {
212
449
  var _a;
213
- const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date()), {
450
+ const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date(), partition), {
214
451
  hint: JOB_ACQUISITION_HINTS[hint],
215
452
  maxTimeMS: 1e3,
216
453
  readPreference: "primary"
@@ -234,7 +471,7 @@ var JobsRepo = class {
234
471
  priority: options.priority,
235
472
  ...options.resetTries ? { tries: 0 } : {}
236
473
  },
237
- $unset: { lockId: "", lockedUntil: "" }
474
+ $unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
238
475
  };
239
476
  const result = await this.jobs.updateOne(
240
477
  options.lockId ? { _id: options.jobId, lockId: options.lockId } : options.jobId,
@@ -268,6 +505,7 @@ var JobsRepo = class {
268
505
  $unset: {
269
506
  lockId: "",
270
507
  lockedUntil: "",
508
+ claimWasStale: "",
271
509
  ...retentionMs === null ? { expiresAt: "" } : {}
272
510
  }
273
511
  }
@@ -324,12 +562,12 @@ var JobsRepo = class {
324
562
  lockedUntil: { $exists: true }
325
563
  },
326
564
  {
327
- $unset: { lockId: "", lockedUntil: "" }
565
+ $unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
328
566
  }
329
567
  );
330
568
  return result.modifiedCount;
331
569
  }
332
- async ensureJobRecord(job) {
570
+ async ensureJobRecord(job, partition = 0) {
333
571
  const result = await this.jobs.upsert(
334
572
  {
335
573
  jobName: job.name
@@ -343,7 +581,8 @@ var JobsRepo = class {
343
581
  status: ""
344
582
  },
345
583
  $setOnInsert: {
346
- nextRunAt: /* @__PURE__ */ new Date()
584
+ nextRunAt: /* @__PURE__ */ new Date(),
585
+ partition
347
586
  }
348
587
  }
349
588
  );
@@ -361,7 +600,8 @@ var JobsRepo = class {
361
600
  params: options.params,
362
601
  nextRunAt: options.nextRunAt,
363
602
  priority: options.priority,
364
- type: "event"
603
+ type: "event",
604
+ ...options.partition === void 0 ? {} : { partition: options.partition }
365
605
  });
366
606
  } catch (error) {
367
607
  if (error.isValidationError && Object.values(error.validationErrors).includes("notUnique") && options.uniqueIdentifier) {
@@ -389,7 +629,8 @@ var JobsRepo = class {
389
629
  params: job.params,
390
630
  nextRunAt: job.nextRunAt,
391
631
  priority: job.priority,
392
- type: "event"
632
+ type: "event",
633
+ ...job.partition === void 0 ? {} : { partition: job.partition }
393
634
  });
394
635
  scheduledCount++;
395
636
  } catch (error) {
@@ -414,11 +655,55 @@ var JobsRepo = class {
414
655
  errors
415
656
  };
416
657
  }
658
+ async reconcilePartitions(options) {
659
+ if (options.jobNames.length === 0) return 0;
660
+ const rawCollection = await this.jobs.getRawCollection();
661
+ await this.jobs.createIndexesPromise;
662
+ const now = /* @__PURE__ */ new Date();
663
+ const invalidPartition = {
664
+ $or: [
665
+ { partition: { $exists: false } },
666
+ { partition: { $lt: 0 } },
667
+ { partition: { $gte: options.nPartitions } }
668
+ ]
669
+ };
670
+ const availableLock = {
671
+ $or: [{ lockedUntil: { $exists: false } }, { lockedUntil: { $lte: now } }]
672
+ };
673
+ const runnableJob = {
674
+ $or: [{ type: { $ne: "event" } }, { status: { $ne: "maxTriesReached" } }]
675
+ };
676
+ const jobs = await rawCollection.find({
677
+ jobName: { $in: options.jobNames },
678
+ $and: [invalidPartition, availableLock, runnableJob]
679
+ }).project({ _id: 1 }).limit(options.batchSize).toArray();
680
+ if (jobs.length === 0) return 0;
681
+ const result = await rawCollection.bulkWrite(
682
+ jobs.map((job) => ({
683
+ updateOne: {
684
+ filter: {
685
+ _id: job._id,
686
+ $and: [invalidPartition, availableLock, runnableJob]
687
+ },
688
+ update: { $set: { partition: getRandomPartition(options.nPartitions) } }
689
+ }
690
+ })),
691
+ { ordered: false }
692
+ );
693
+ return result.modifiedCount;
694
+ }
417
695
  };
418
- _init = __decoratorStart(null);
419
- __decorateElement(_init, 5, "jobs", _jobs_dec, JobsRepo);
420
- JobsRepo = __decorateElement(_init, 0, "JobsRepo", _JobsRepo_decorators, JobsRepo);
421
- __runInitializers(_init, 1, JobsRepo);
696
+ _init2 = __decoratorStart(null);
697
+ __decorateElement(_init2, 5, "jobs", _jobs_dec, JobsRepo);
698
+ JobsRepo = __decorateElement(_init2, 0, "JobsRepo", _JobsRepo_decorators, JobsRepo);
699
+ __runInitializers(_init2, 1, JobsRepo);
700
+
701
+ // src/schedule.ts
702
+ import { getInstance } from "@orion-js/services";
703
+
704
+ // src/services/EventsService.ts
705
+ import { logger as logger2 } from "@orion-js/logger";
706
+ import { Inject, Service } from "@orion-js/services";
422
707
 
423
708
  // src/services/getNextRunDate.ts
424
709
  import { CronExpressionParser } from "cron-parser";
@@ -448,38 +733,53 @@ var getNextRunDate = (options) => {
448
733
  };
449
734
 
450
735
  // src/services/EventsService.ts
451
- var _jobsRepo_dec, _EventsService_decorators, _init2;
736
+ var _jobsRepo_dec, _EventsService_decorators, _init3;
452
737
  _EventsService_decorators = [Service()], _jobsRepo_dec = [Inject(() => JobsRepo)];
453
738
  var EventsService = class {
454
739
  constructor() {
455
- __publicField(this, "jobsRepo", __runInitializers(_init2, 8, this)), __runInitializers(_init2, 11, this);
740
+ __publicField(this, "jobsRepo", __runInitializers(_init3, 8, this)), __runInitializers(_init3, 11, this);
456
741
  }
457
- async scheduleJob(options) {
742
+ async scheduleJob(options, nPartitions) {
458
743
  logger2.debug("Scheduling job...", options);
744
+ const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
459
745
  await this.jobsRepo.scheduleJob({
460
746
  name: options.name,
461
747
  priority: options.priority || 100,
462
748
  nextRunAt: getNextRunDate(options),
463
749
  params: options.params || null,
464
- uniqueIdentifier: options.uniqueIdentifier
750
+ uniqueIdentifier: options.uniqueIdentifier,
751
+ partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
465
752
  });
466
753
  }
467
- async scheduleJobs(jobs) {
754
+ async scheduleJobs(jobs, nPartitions) {
468
755
  logger2.debug(`Scheduling ${jobs.length} jobs...`);
469
- const jobRecords = jobs.map((options) => ({
470
- name: options.name,
471
- priority: options.priority || 100,
472
- nextRunAt: getNextRunDate(options),
473
- params: options.params || null,
474
- uniqueIdentifier: options.uniqueIdentifier
475
- }));
756
+ const jobRecords = jobs.map((options) => {
757
+ const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
758
+ return {
759
+ name: options.name,
760
+ priority: options.priority || 100,
761
+ nextRunAt: getNextRunDate(options),
762
+ params: options.params || null,
763
+ uniqueIdentifier: options.uniqueIdentifier,
764
+ partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
765
+ };
766
+ });
476
767
  return await this.jobsRepo.scheduleJobs(jobRecords);
477
768
  }
478
769
  };
479
- _init2 = __decoratorStart(null);
480
- __decorateElement(_init2, 5, "jobsRepo", _jobsRepo_dec, EventsService);
481
- EventsService = __decorateElement(_init2, 0, "EventsService", _EventsService_decorators, EventsService);
482
- __runInitializers(_init2, 1, EventsService);
770
+ _init3 = __decoratorStart(null);
771
+ __decorateElement(_init3, 5, "jobsRepo", _jobsRepo_dec, EventsService);
772
+ EventsService = __decorateElement(_init3, 0, "EventsService", _EventsService_decorators, EventsService);
773
+ __runInitializers(_init3, 1, EventsService);
774
+
775
+ // src/schedule.ts
776
+ var eventsService = getInstance(EventsService);
777
+ function scheduleJobInternal(options, nPartitions) {
778
+ return eventsService.scheduleJob(options, nPartitions);
779
+ }
780
+ function scheduleJobsInternal(jobs, nPartitions) {
781
+ return eventsService.scheduleJobs(jobs, nPartitions);
782
+ }
483
783
 
484
784
  // src/services/WorkerService.ts
485
785
  import { sleep } from "@orion-js/helpers";
@@ -490,132 +790,6 @@ import { Inject as Inject3, Service as Service3 } from "@orion-js/services";
490
790
  import { SpanStatusCode, trace } from "@opentelemetry/api";
491
791
  import { logger as logger3, runWithOrionAsyncContext, updateOrionAsyncContext } from "@orion-js/logger";
492
792
  import { Inject as Inject2, Service as Service2 } from "@orion-js/services";
493
-
494
- // src/repos/JobsHistoryRepo.ts
495
- import { MongoCollection as MongoCollection2, Repository as Repository2 } from "@orion-js/mongodb";
496
-
497
- // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/isInteger.js
498
- function _isInteger(n) {
499
- return n << 0 === n;
500
- }
501
- var isInteger = Number.isInteger || _isInteger;
502
-
503
- // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/createPath.js
504
- function createPath(path, delimiter = ".") {
505
- return typeof path === "string" ? path.split(delimiter).map((x) => isInteger(x) ? Number(x) : x) : path;
506
- }
507
-
508
- // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/compare.js
509
- function compare(a, b) {
510
- return String(a) === String(b);
511
- }
512
-
513
- // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/includes.js
514
- function includes(a, list) {
515
- let index = -1;
516
- const { length } = list;
517
- while (++index < length)
518
- if (compare(list[index], a))
519
- return true;
520
- return false;
521
- }
522
-
523
- // ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/omit.js
524
- function omit(propsToOmit, obj) {
525
- if (arguments.length === 1) return (_obj) => omit(propsToOmit, _obj);
526
- if (obj === null || obj === void 0)
527
- return void 0;
528
- const propsToOmitValue = createPath(propsToOmit, ",");
529
- const willReturn = {};
530
- for (const key in obj)
531
- if (!includes(key, propsToOmitValue))
532
- willReturn[key] = obj[key];
533
- return willReturn;
534
- }
535
-
536
- // src/types/HistoryRecord.ts
537
- import { schemaWithName as schemaWithName2 } from "@orion-js/schema";
538
- var HistoryRecordSchema = schemaWithName2("HistoryRecord", {
539
- _id: { type: "string" },
540
- jobId: { type: "string" },
541
- executionId: { type: "string" },
542
- jobName: { type: "string" },
543
- type: { type: "string" },
544
- priority: { type: "number" },
545
- tries: { type: "number" },
546
- uniqueIdentifier: { type: "string", optional: true },
547
- startedAt: { type: "date" },
548
- endedAt: { type: "date" },
549
- duration: { type: "number" },
550
- expiresAt: { type: "date", optional: true },
551
- status: { type: "string", enum: ["success", "error", "stale"] },
552
- errorMessage: { type: "string", optional: true },
553
- params: { type: "blackbox", optional: true },
554
- result: { type: "any", optional: true }
555
- });
556
-
557
- // src/repos/JobsHistoryRepo.ts
558
- var _history_dec, _JobsHistoryRepo_decorators, _init3;
559
- _JobsHistoryRepo_decorators = [Repository2()], _history_dec = [MongoCollection2({
560
- name: "orionjs.jobs_dogs_history",
561
- idGeneration: "uuid",
562
- schema: HistoryRecordSchema,
563
- indexes: [
564
- {
565
- keys: {
566
- jobName: 1,
567
- startedAt: 1
568
- }
569
- },
570
- {
571
- keys: {
572
- executionId: 1
573
- }
574
- },
575
- {
576
- keys: {
577
- expiresAt: 1
578
- },
579
- options: {
580
- expireAfterSeconds: 0
581
- }
582
- }
583
- ]
584
- })];
585
- var JobsHistoryRepo = class {
586
- constructor() {
587
- __publicField(this, "history", __runInitializers(_init3, 8, this)), __runInitializers(_init3, 11, this);
588
- }
589
- async saveExecution(record) {
590
- await this.history.upsert(
591
- { executionId: record.executionId },
592
- {
593
- $setOnInsert: {
594
- status: record.status
595
- },
596
- $set: {
597
- ...omit(["status"], record)
598
- }
599
- }
600
- );
601
- }
602
- async getExecutions(jobName, limit, skip) {
603
- const cursor = this.history.find({ jobName }).sort({ startedAt: -1 });
604
- if (skip) {
605
- cursor.skip(skip);
606
- }
607
- if (limit) {
608
- cursor.limit(limit);
609
- }
610
- return await cursor.toArray();
611
- }
612
- };
613
- _init3 = __decoratorStart(null);
614
- __decorateElement(_init3, 5, "history", _history_dec, JobsHistoryRepo);
615
- JobsHistoryRepo = __decorateElement(_init3, 0, "JobsHistoryRepo", _JobsHistoryRepo_decorators, JobsHistoryRepo);
616
- __runInitializers(_init3, 1, JobsHistoryRepo);
617
-
618
- // src/services/Executor.ts
619
793
  var _jobsHistoryRepo_dec, _jobsRepo_dec2, _Executor_decorators, _init4;
620
794
  _Executor_decorators = [Service2()], _jobsRepo_dec2 = [Inject2(() => JobsRepo)], _jobsHistoryRepo_dec = [Inject2(() => JobsHistoryRepo)];
621
795
  var Executor = class {
@@ -955,11 +1129,16 @@ Executor = __decorateElement(_init4, 0, "Executor", _Executor_decorators, Execut
955
1129
  __runInitializers(_init4, 1, Executor);
956
1130
 
957
1131
  // src/services/WorkerService.ts
1132
+ var FILTERED_JOB_NAMES_ACQUISITION_HINT = "byJobName";
958
1133
  var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
959
1134
  _WorkerService_decorators = [Service3()], _jobsRepo_dec3 = [Inject3(() => JobsRepo)], _executor_dec = [Inject3(() => Executor)];
960
1135
  var WorkerService = class {
961
1136
  constructor() {
962
1137
  __publicField(this, "jobAcquisitionHintProbeIntervalMs", 30 * 60 * 1e3);
1138
+ __publicField(this, "partitionReconcilerIntervalMs", 5 * 60 * 1e3);
1139
+ __publicField(this, "partitionReconcilerJitterMs", 30 * 1e3);
1140
+ __publicField(this, "partitionReconcilerBatchPauseMs", 250);
1141
+ __publicField(this, "partitionReconcilerBatchSize", 500);
963
1142
  __publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
964
1143
  __publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
965
1144
  }
@@ -1044,6 +1223,15 @@ var WorkerService = class {
1044
1223
  if (capacityPromises.length === 0) return;
1045
1224
  await Promise.race([...capacityPromises, workersInstance.stopSignal]);
1046
1225
  }
1226
+ getNextPartition(config, workersInstance) {
1227
+ const partition = workersInstance.partitionOrder[workersInstance.partitionCursor];
1228
+ workersInstance.partitionCursor++;
1229
+ if (workersInstance.partitionCursor >= config.nPartitions) {
1230
+ workersInstance.partitionOrder = createShuffledPartitions(config.nPartitions);
1231
+ workersInstance.partitionCursor = 0;
1232
+ }
1233
+ return partition;
1234
+ }
1047
1235
  async runSchedulerLoop(config, workersInstance) {
1048
1236
  const jobNames = this.getJobNames(config.jobs);
1049
1237
  const executeConfig = {
@@ -1053,6 +1241,7 @@ var WorkerService = class {
1053
1241
  onMaxTriesReached: config.onMaxTriesReached
1054
1242
  };
1055
1243
  logger4.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
1244
+ let consecutivePartitionMisses = 0;
1056
1245
  while (workersInstance.running) {
1057
1246
  let didMiss = false;
1058
1247
  let allJobNamesAtCapacity = false;
@@ -1063,21 +1252,31 @@ var WorkerService = class {
1063
1252
  allJobNamesAtCapacity = workersInstance.activeExecutions.size > 0;
1064
1253
  break;
1065
1254
  }
1255
+ const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
1256
+ const partition = this.getNextPartition(config, workersInstance);
1066
1257
  const jobToRun = await this.jobsRepo.getJobAndLock(
1067
1258
  availableJobNames,
1068
1259
  config.defaultLockTime,
1069
- workersInstance.jobAcquisitionHint
1260
+ jobAcquisitionHint,
1261
+ partition
1070
1262
  );
1071
1263
  if (!jobToRun) {
1072
- logger4.debug("No job to run");
1073
- didMiss = true;
1074
- break;
1264
+ consecutivePartitionMisses++;
1265
+ logger4.debug(`No job to run in partition ${partition}`);
1266
+ if (consecutivePartitionMisses >= config.nPartitions) {
1267
+ consecutivePartitionMisses = 0;
1268
+ didMiss = true;
1269
+ break;
1270
+ }
1271
+ continue;
1075
1272
  }
1273
+ consecutivePartitionMisses = 0;
1076
1274
  this.startExecution(executeConfig, workersInstance, jobToRun);
1077
1275
  if (!workersInstance.running) break;
1078
1276
  }
1079
1277
  } catch (error) {
1080
1278
  logger4.error("Error in job scheduler.", { error });
1279
+ consecutivePartitionMisses = 0;
1081
1280
  didMiss = true;
1082
1281
  }
1083
1282
  if (!workersInstance.running) return;
@@ -1113,11 +1312,13 @@ var WorkerService = class {
1113
1312
  workersInstance.jobAcquisitionHintProbeCycle++;
1114
1313
  try {
1115
1314
  for (let sample = 0; sample < 3; sample++) {
1315
+ const partition = getRandomPartition(config.nPartitions);
1116
1316
  for (const hint of [firstHint, secondHint]) {
1117
1317
  if (!workersInstance.running) return;
1118
1318
  const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
1119
1319
  jobNames,
1120
- hint
1320
+ hint,
1321
+ partition
1121
1322
  );
1122
1323
  if (!workersInstance.running) return;
1123
1324
  byHint[hint].push(executionTimeMillis);
@@ -1189,6 +1390,44 @@ var WorkerService = class {
1189
1390
  await this.waitForNextJobAcquisitionHintProbe(workersInstance);
1190
1391
  }
1191
1392
  }
1393
+ async reconcilePartitionBatch(config) {
1394
+ if (typeof this.jobsRepo.reconcilePartitions !== "function") return 0;
1395
+ try {
1396
+ return await this.jobsRepo.reconcilePartitions({
1397
+ jobNames: this.getJobNames(config.jobs),
1398
+ nPartitions: config.nPartitions,
1399
+ batchSize: this.partitionReconcilerBatchSize
1400
+ });
1401
+ } catch (error) {
1402
+ logger4.warn("Job partition reconciliation failed; it will be retried.", { error });
1403
+ return 0;
1404
+ }
1405
+ }
1406
+ async waitForNextPartitionReconcile(workersInstance, delayMs) {
1407
+ await new Promise((resolve) => {
1408
+ let resolved = false;
1409
+ const finishWait = () => {
1410
+ if (resolved) return;
1411
+ resolved = true;
1412
+ clearTimeout(workersInstance.partitionReconcilerTimer);
1413
+ workersInstance.partitionReconcilerTimer = void 0;
1414
+ workersInstance.cancelPartitionReconcilerWait = void 0;
1415
+ resolve();
1416
+ };
1417
+ workersInstance.partitionReconcilerTimer = setTimeout(finishWait, delayMs);
1418
+ workersInstance.cancelPartitionReconcilerWait = finishWait;
1419
+ });
1420
+ }
1421
+ async runPartitionReconcilerLoop(config, workersInstance, initialReconcilePromise) {
1422
+ let modifiedCount = await initialReconcilePromise;
1423
+ while (workersInstance.running) {
1424
+ const didFillBatch = modifiedCount >= this.partitionReconcilerBatchSize;
1425
+ const delayMs = didFillBatch ? this.partitionReconcilerBatchPauseMs : this.partitionReconcilerIntervalMs + Math.floor(Math.random() * (this.partitionReconcilerJitterMs + 1));
1426
+ await this.waitForNextPartitionReconcile(workersInstance, delayMs);
1427
+ if (!workersInstance.running) return;
1428
+ modifiedCount = await this.reconcilePartitionBatch(config);
1429
+ }
1430
+ }
1192
1431
  createWorkersInstanceDefinition(config) {
1193
1432
  let signalStop;
1194
1433
  const stopSignal = new Promise((resolve) => {
@@ -1204,7 +1443,10 @@ var WorkerService = class {
1204
1443
  jobAcquisitionHintProbeCycle: 0,
1205
1444
  hasCompletedInitialJobAcquisitionHintProbe: false,
1206
1445
  pendingJobAcquisitionHintWins: 0,
1446
+ partitionOrder: createShuffledPartitions(config.nPartitions),
1447
+ partitionCursor: 0,
1207
1448
  jobAcquisitionHintProbePromise: Promise.resolve(),
1449
+ partitionReconcilerPromise: Promise.resolve(),
1208
1450
  schedulerPromise: Promise.resolve(),
1209
1451
  stopSignal,
1210
1452
  signalStop,
@@ -1214,14 +1456,16 @@ var WorkerService = class {
1214
1456
  stop: async () => {
1215
1457
  if (workersInstance.stopPromise) return workersInstance.stopPromise;
1216
1458
  workersInstance.stopPromise = (async () => {
1217
- var _a;
1459
+ var _a, _b;
1218
1460
  logger4.info("Stopping job scheduler...");
1219
1461
  workersInstance.running = false;
1220
1462
  workersInstance.signalStop();
1221
1463
  (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1464
+ (_b = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b.call(workersInstance);
1222
1465
  await Promise.all([
1223
1466
  workersInstance.schedulerPromise,
1224
- workersInstance.jobAcquisitionHintProbePromise
1467
+ workersInstance.jobAcquisitionHintProbePromise,
1468
+ workersInstance.partitionReconcilerPromise
1225
1469
  ]);
1226
1470
  const activeCapacity = Array.from(
1227
1471
  workersInstance.activeExecutions.values(),
@@ -1239,7 +1483,7 @@ var WorkerService = class {
1239
1483
  await Promise.all(
1240
1484
  jobs.filter((job) => job.type === "recurrent").map(async (job) => {
1241
1485
  logger4.debug(`Ensuring records for job "${job.name}"...`);
1242
- await this.jobsRepo.ensureJobRecord(job);
1486
+ await this.jobsRepo.ensureJobRecord(job, getRandomPartition(config.nPartitions));
1243
1487
  })
1244
1488
  );
1245
1489
  }
@@ -1256,29 +1500,41 @@ var WorkerService = class {
1256
1500
  * @param userConfig - Configuration for the scheduler. Required field: jobs
1257
1501
  */
1258
1502
  startWorkers(userConfig) {
1503
+ var _a, _b;
1259
1504
  const maxTriesReachedRetentionMs = userConfig.maxTriesReachedRetentionMs === void 0 ? DEFAULT_MAX_TRIES_REACHED_RETENTION_MS : userConfig.maxTriesReachedRetentionMs;
1260
1505
  if (maxTriesReachedRetentionMs !== null && (!Number.isFinite(maxTriesReachedRetentionMs) || maxTriesReachedRetentionMs < 0)) {
1261
1506
  throw new Error("maxTriesReachedRetentionMs must be null, zero, or a positive number");
1262
1507
  }
1508
+ const nPartitions = userConfig.nPartitions ?? 1;
1509
+ validatePartitionsCount(nPartitions);
1263
1510
  const config = {
1264
1511
  pollInterval: 3e3,
1265
1512
  workersCount: 4,
1266
1513
  defaultLockTime: 30 * 1e3,
1267
1514
  ...userConfig,
1515
+ nPartitions,
1268
1516
  maxTriesReachedRetentionMs
1269
1517
  };
1270
- setNameToJobs(config.jobs);
1518
+ setNameToJobs(config.jobs, config.nPartitions);
1519
+ (_b = (_a = this.jobsRepo).configureJobPartitions) == null ? void 0 : _b.call(_a, this.getJobNames(config.jobs), config.nPartitions);
1271
1520
  const workersInstance = this.createWorkersInstanceDefinition(config);
1272
1521
  logger4.debug("Starting job scheduler", config);
1273
1522
  workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
1274
1523
  config,
1275
1524
  workersInstance
1276
1525
  );
1526
+ const initialReconcilePromise = this.reconcilePartitionBatch(config);
1527
+ workersInstance.partitionReconcilerPromise = this.runPartitionReconcilerLoop(
1528
+ config,
1529
+ workersInstance,
1530
+ initialReconcilePromise
1531
+ );
1277
1532
  workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
1278
- var _a;
1533
+ var _a2, _b2;
1279
1534
  workersInstance.running = false;
1280
1535
  workersInstance.signalStop();
1281
- (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1536
+ (_a2 = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a2.call(workersInstance);
1537
+ (_b2 = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b2.call(workersInstance);
1282
1538
  logger4.error("Job scheduler stopped after an unexpected error.", { error });
1283
1539
  });
1284
1540
  return workersInstance;
@@ -1289,15 +1545,13 @@ __decorateElement(_init5, 5, "jobsRepo", _jobsRepo_dec3, WorkerService);
1289
1545
  __decorateElement(_init5, 5, "executor", _executor_dec, WorkerService);
1290
1546
  WorkerService = __decorateElement(_init5, 0, "WorkerService", _WorkerService_decorators, WorkerService);
1291
1547
  __runInitializers(_init5, 1, WorkerService);
1292
- function setNameToJobs(jobs) {
1548
+ function setNameToJobs(jobs, nPartitions) {
1293
1549
  for (const name of Object.keys(jobs)) {
1294
1550
  jobs[name].jobName = name;
1551
+ jobs[name].nPartitions = nPartitions;
1295
1552
  }
1296
1553
  }
1297
1554
 
1298
- // src/service/index.ts
1299
- import { getInstance, Service as Service4 } from "@orion-js/services";
1300
-
1301
1555
  // src/defineJob/index.ts
1302
1556
  import { cleanAndValidate } from "@orion-js/schema";
1303
1557
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
@@ -1314,11 +1568,14 @@ function createEventJob(options) {
1314
1568
  throw new Error("This job has not been registered in the workers");
1315
1569
  }
1316
1570
  const params = jobDefinition.params ? await cleanAndValidate(jobDefinition.params, scheduleOptions.params) : scheduleOptions.params;
1317
- return await scheduleJob({
1318
- ...scheduleOptions,
1319
- name: jobDefinition.jobName,
1320
- params
1321
- });
1571
+ return await scheduleJobInternal(
1572
+ {
1573
+ ...scheduleOptions,
1574
+ name: jobDefinition.jobName,
1575
+ params
1576
+ },
1577
+ jobDefinition.nPartitions
1578
+ );
1322
1579
  };
1323
1580
  jobDefinition.scheduleJobs = async (jobs) => {
1324
1581
  if (!jobDefinition.jobName) {
@@ -1334,7 +1591,7 @@ function createEventJob(options) {
1334
1591
  };
1335
1592
  })
1336
1593
  );
1337
- return await scheduleJobs(processedJobs);
1594
+ return await scheduleJobsInternal(processedJobs, jobDefinition.nPartitions);
1338
1595
  };
1339
1596
  return jobDefinition;
1340
1597
  }
@@ -1363,6 +1620,7 @@ var defineJob = (options) => {
1363
1620
  };
1364
1621
 
1365
1622
  // src/service/index.ts
1623
+ import { getInstance as getInstance2, Service as Service4 } from "@orion-js/services";
1366
1624
  var serviceMetadata = /* @__PURE__ */ new WeakMap();
1367
1625
  var jobsMetadata = /* @__PURE__ */ new Map();
1368
1626
  var jobEntriesByClass = /* @__PURE__ */ new Map();
@@ -1418,7 +1676,7 @@ function initializeJobsIfNeeded(instance) {
1418
1676
  jobsMetadata.set(instance, jobs);
1419
1677
  }
1420
1678
  function getServiceJobs(target) {
1421
- const instance = getInstance(target);
1679
+ const instance = getInstance2(target);
1422
1680
  if (!serviceMetadata.has(instance.constructor)) {
1423
1681
  throw new Error("You must pass a class decorated with @Jobs to getServiceJobs");
1424
1682
  }
@@ -1432,18 +1690,17 @@ function getServiceJobs(target) {
1432
1690
  }
1433
1691
 
1434
1692
  // src/index.ts
1435
- var workerService = getInstance2(WorkerService);
1436
- var eventsService = getInstance2(EventsService);
1437
- var jobsHistoryRepo = getInstance2(JobsHistoryRepo);
1438
- var jobsRepo = getInstance2(JobsRepo);
1693
+ var workerService = getInstance3(WorkerService);
1694
+ var jobsHistoryRepo = getInstance3(JobsHistoryRepo);
1695
+ var jobsRepo = getInstance3(JobsRepo);
1439
1696
  var startWorkers = (config) => {
1440
1697
  return workerService.startWorkers(config);
1441
1698
  };
1442
1699
  var scheduleJob = (options) => {
1443
- return eventsService.scheduleJob(options);
1700
+ return scheduleJobInternal(options);
1444
1701
  };
1445
1702
  var scheduleJobs = (jobs) => {
1446
- return eventsService.scheduleJobs(jobs);
1703
+ return scheduleJobsInternal(jobs);
1447
1704
  };
1448
1705
  export {
1449
1706
  DEFAULT_MAX_TRIES_REACHED_RETENTION_MS,