@chidchanun/bcp 0.2.7 → 0.2.9

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.
@@ -0,0 +1,1040 @@
1
+ // packages/server/src/jobs.ts
2
+ import {
3
+ randomUUID
4
+ } from "node:crypto";
5
+ function createMemoryJobQueueAdapter() {
6
+ const jobs = /* @__PURE__ */ new Map();
7
+ return {
8
+ async enqueue(job) {
9
+ if (jobs.has(job.id)) {
10
+ throw new Error(
11
+ `BCP Jobs: job id "${job.id}" already exists.`
12
+ );
13
+ }
14
+ jobs.set(
15
+ job.id,
16
+ cloneJob(job)
17
+ );
18
+ },
19
+ async reserve(now) {
20
+ const candidate = Array.from(
21
+ jobs.values()
22
+ ).filter(
23
+ (job) => job.state === "queued" && job.availableAt <= now
24
+ ).sort(
25
+ (left, right) => left.availableAt - right.availableAt || left.createdAt - right.createdAt || left.id.localeCompare(right.id)
26
+ )[0];
27
+ if (!candidate) {
28
+ return null;
29
+ }
30
+ candidate.state = "running";
31
+ candidate.attempts += 1;
32
+ candidate.startedAt = now;
33
+ candidate.error = void 0;
34
+ return cloneJob(
35
+ candidate
36
+ );
37
+ },
38
+ async complete(id, completedAt) {
39
+ const job = jobs.get(id);
40
+ if (!job || job.state === "cancelled") {
41
+ return;
42
+ }
43
+ job.state = "succeeded";
44
+ job.completedAt = completedAt;
45
+ job.error = void 0;
46
+ },
47
+ async fail(id, options) {
48
+ const job = jobs.get(id);
49
+ if (!job || job.state === "cancelled") {
50
+ return;
51
+ }
52
+ job.error = options.error;
53
+ if (options.retryAt !== void 0 && job.attempts < job.maxAttempts) {
54
+ job.state = "queued";
55
+ job.availableAt = options.retryAt;
56
+ job.startedAt = void 0;
57
+ return;
58
+ }
59
+ job.state = "failed";
60
+ job.completedAt = options.failedAt;
61
+ },
62
+ async cancel(id, cancelledAt) {
63
+ const job = jobs.get(id);
64
+ if (!job || job.state === "succeeded" || job.state === "failed" || job.state === "cancelled") {
65
+ return false;
66
+ }
67
+ job.state = "cancelled";
68
+ job.completedAt = cancelledAt;
69
+ return true;
70
+ },
71
+ async get(id) {
72
+ const job = jobs.get(id);
73
+ return job ? cloneJob(job) : null;
74
+ },
75
+ async list() {
76
+ return Array.from(
77
+ jobs.values()
78
+ ).map(cloneJob).sort(
79
+ (left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)
80
+ );
81
+ },
82
+ clear() {
83
+ jobs.clear();
84
+ }
85
+ };
86
+ }
87
+ function createJobQueue(options = {}) {
88
+ const adapter = options.adapter ?? createMemoryJobQueueAdapter();
89
+ const handlers = /* @__PURE__ */ new Map();
90
+ const workers = /* @__PURE__ */ new Set();
91
+ const now = options.now ?? Date.now;
92
+ const idFactory = options.idFactory ?? randomUUID;
93
+ const defaultMaxAttempts = normalizePositiveInteger(
94
+ options.defaultMaxAttempts ?? 3,
95
+ "defaultMaxAttempts"
96
+ );
97
+ const retryDelay = options.retryDelayMs ?? ((attempt) => Math.min(
98
+ 3e4,
99
+ 1e3 * 2 ** Math.max(
100
+ 0,
101
+ attempt - 1
102
+ )
103
+ ));
104
+ const queue = {
105
+ adapter,
106
+ register(name, handler) {
107
+ const normalizedName = normalizeJobName(name);
108
+ if (typeof handler !== "function") {
109
+ throw new TypeError(
110
+ "BCP Jobs: handler must be a function."
111
+ );
112
+ }
113
+ if (handlers.has(
114
+ normalizedName
115
+ )) {
116
+ throw new Error(
117
+ `BCP Jobs: handler "${normalizedName}" is already registered.`
118
+ );
119
+ }
120
+ const registeredHandler = handler;
121
+ handlers.set(
122
+ normalizedName,
123
+ registeredHandler
124
+ );
125
+ return () => {
126
+ if (handlers.get(
127
+ normalizedName
128
+ ) === registeredHandler) {
129
+ handlers.delete(
130
+ normalizedName
131
+ );
132
+ }
133
+ };
134
+ },
135
+ async enqueue(name, payload, enqueueOptions = {}) {
136
+ const createdAt = now();
137
+ const delayMs = normalizeNonNegativeNumber(
138
+ enqueueOptions.delayMs ?? 0,
139
+ "delayMs"
140
+ );
141
+ const maxAttempts = normalizePositiveInteger(
142
+ enqueueOptions.maxAttempts ?? defaultMaxAttempts,
143
+ "maxAttempts"
144
+ );
145
+ const id = normalizeJobId(
146
+ enqueueOptions.id ?? idFactory()
147
+ );
148
+ const job = {
149
+ id,
150
+ name: normalizeJobName(
151
+ name
152
+ ),
153
+ payload,
154
+ state: "queued",
155
+ attempts: 0,
156
+ maxAttempts,
157
+ createdAt,
158
+ availableAt: createdAt + delayMs
159
+ };
160
+ await adapter.enqueue(
161
+ job
162
+ );
163
+ return cloneJob(job);
164
+ },
165
+ get(id) {
166
+ return adapter.get(
167
+ normalizeJobId(id)
168
+ );
169
+ },
170
+ list() {
171
+ return adapter.list();
172
+ },
173
+ cancel(id) {
174
+ return adapter.cancel(
175
+ normalizeJobId(id),
176
+ now()
177
+ );
178
+ },
179
+ async processNext(signal = new AbortController().signal) {
180
+ if (signal.aborted) {
181
+ return false;
182
+ }
183
+ const job = await adapter.reserve(
184
+ now()
185
+ );
186
+ if (!job) {
187
+ return false;
188
+ }
189
+ const handler = handlers.get(
190
+ job.name
191
+ );
192
+ if (!handler) {
193
+ await adapter.fail(
194
+ job.id,
195
+ {
196
+ error: `No handler registered for job "${job.name}".`,
197
+ failedAt: now()
198
+ }
199
+ );
200
+ return true;
201
+ }
202
+ try {
203
+ await handler({
204
+ job: cloneJob(job),
205
+ payload: job.payload,
206
+ signal
207
+ });
208
+ await adapter.complete(
209
+ job.id,
210
+ now()
211
+ );
212
+ } catch (error) {
213
+ const failedAt = now();
214
+ const shouldRetry = job.attempts < job.maxAttempts;
215
+ const retryAt = shouldRetry ? failedAt + resolveRetryDelay(
216
+ retryDelay,
217
+ job.attempts
218
+ ) : void 0;
219
+ await adapter.fail(
220
+ job.id,
221
+ {
222
+ error: formatJobError(
223
+ error
224
+ ),
225
+ failedAt,
226
+ retryAt
227
+ }
228
+ );
229
+ }
230
+ return true;
231
+ },
232
+ startWorker(workerOptions = {}) {
233
+ const worker = createWorker(
234
+ queue,
235
+ workerOptions,
236
+ () => workers.delete(
237
+ worker
238
+ )
239
+ );
240
+ workers.add(worker);
241
+ return worker;
242
+ },
243
+ async close() {
244
+ await Promise.all(
245
+ Array.from(
246
+ workers,
247
+ (worker) => worker.stop()
248
+ )
249
+ );
250
+ if (adapter.close) {
251
+ await adapter.close();
252
+ }
253
+ }
254
+ };
255
+ return queue;
256
+ }
257
+ function createWorker(queue, options, onStop) {
258
+ const concurrency = normalizePositiveInteger(
259
+ options.concurrency ?? 1,
260
+ "concurrency"
261
+ );
262
+ const pollIntervalMs = normalizeNonNegativeNumber(
263
+ options.pollIntervalMs ?? 250,
264
+ "pollIntervalMs"
265
+ );
266
+ const controller = new AbortController();
267
+ let running = true;
268
+ let stopPromise = null;
269
+ const loops = Array.from(
270
+ {
271
+ length: concurrency
272
+ },
273
+ () => runWorkerLoop(
274
+ queue,
275
+ controller.signal,
276
+ pollIntervalMs
277
+ )
278
+ );
279
+ return {
280
+ get running() {
281
+ return running;
282
+ },
283
+ stop() {
284
+ if (stopPromise) {
285
+ return stopPromise;
286
+ }
287
+ running = false;
288
+ controller.abort();
289
+ stopPromise = Promise.allSettled(
290
+ loops
291
+ ).then(
292
+ () => {
293
+ onStop();
294
+ }
295
+ );
296
+ return stopPromise;
297
+ }
298
+ };
299
+ }
300
+ async function runWorkerLoop(queue, signal, pollIntervalMs) {
301
+ while (!signal.aborted) {
302
+ const processed = await queue.processNext(
303
+ signal
304
+ );
305
+ if (!processed && !signal.aborted) {
306
+ await sleep(
307
+ pollIntervalMs,
308
+ signal
309
+ );
310
+ }
311
+ }
312
+ }
313
+ function sleep(durationMs, signal) {
314
+ if (durationMs === 0 || signal.aborted) {
315
+ return Promise.resolve();
316
+ }
317
+ return new Promise(
318
+ (resolve) => {
319
+ const timeout = setTimeout(
320
+ finish,
321
+ durationMs
322
+ );
323
+ const onAbort = () => finish();
324
+ signal.addEventListener(
325
+ "abort",
326
+ onAbort,
327
+ {
328
+ once: true
329
+ }
330
+ );
331
+ function finish() {
332
+ clearTimeout(timeout);
333
+ signal.removeEventListener(
334
+ "abort",
335
+ onAbort
336
+ );
337
+ resolve();
338
+ }
339
+ }
340
+ );
341
+ }
342
+ function resolveRetryDelay(value, attempt) {
343
+ const delay = typeof value === "function" ? value(attempt) : value;
344
+ return normalizeNonNegativeNumber(
345
+ delay,
346
+ "retryDelayMs"
347
+ );
348
+ }
349
+ function normalizeJobName(value) {
350
+ const name = String(value).trim();
351
+ if (!name) {
352
+ throw new TypeError(
353
+ "BCP Jobs: job name must be a non-empty string."
354
+ );
355
+ }
356
+ if (name.length > 200) {
357
+ throw new TypeError(
358
+ "BCP Jobs: job name must not exceed 200 characters."
359
+ );
360
+ }
361
+ return name;
362
+ }
363
+ function normalizeJobId(value) {
364
+ const id = String(value).trim();
365
+ if (!id) {
366
+ throw new TypeError(
367
+ "BCP Jobs: job id must be a non-empty string."
368
+ );
369
+ }
370
+ if (id.length > 200) {
371
+ throw new TypeError(
372
+ "BCP Jobs: job id must not exceed 200 characters."
373
+ );
374
+ }
375
+ return id;
376
+ }
377
+ function normalizePositiveInteger(value, field) {
378
+ if (!Number.isInteger(value) || value <= 0) {
379
+ throw new TypeError(
380
+ `BCP Jobs: ${field} must be a positive integer.`
381
+ );
382
+ }
383
+ return value;
384
+ }
385
+ function normalizeNonNegativeNumber(value, field) {
386
+ if (!Number.isFinite(value) || value < 0) {
387
+ throw new TypeError(
388
+ `BCP Jobs: ${field} must be a non-negative finite number.`
389
+ );
390
+ }
391
+ return Math.floor(value);
392
+ }
393
+ function formatJobError(error) {
394
+ if (error instanceof Error) {
395
+ return error.message || error.name;
396
+ }
397
+ if (typeof error === "string") {
398
+ return error;
399
+ }
400
+ try {
401
+ const serialized = JSON.stringify(error);
402
+ return serialized ?? String(error);
403
+ } catch {
404
+ return String(error);
405
+ }
406
+ }
407
+ function cloneJob(job) {
408
+ return {
409
+ ...job
410
+ };
411
+ }
412
+
413
+ // packages/server/src/job-scheduler.ts
414
+ import {
415
+ randomUUID as randomUUID2
416
+ } from "node:crypto";
417
+ function createMemoryJobScheduleStore() {
418
+ const schedules = /* @__PURE__ */ new Map();
419
+ return {
420
+ async upsert(schedule) {
421
+ schedules.set(
422
+ schedule.id,
423
+ cloneSchedule(
424
+ schedule
425
+ )
426
+ );
427
+ },
428
+ async get(id) {
429
+ const schedule = schedules.get(id);
430
+ return schedule ? cloneSchedule(
431
+ schedule
432
+ ) : null;
433
+ },
434
+ async list() {
435
+ return Array.from(
436
+ schedules.values()
437
+ ).map(
438
+ cloneSchedule
439
+ ).sort(
440
+ (left, right) => left.nextRunAt - right.nextRunAt || left.id.localeCompare(
441
+ right.id
442
+ )
443
+ );
444
+ },
445
+ async remove(id) {
446
+ return schedules.delete(id);
447
+ },
448
+ async acquireDue(now, options) {
449
+ const due = Array.from(
450
+ schedules.values()
451
+ ).filter(
452
+ (schedule) => schedule.nextRunAt <= now && (schedule.leaseUntil === void 0 || schedule.leaseUntil <= now)
453
+ ).sort(
454
+ (left, right) => left.nextRunAt - right.nextRunAt || left.id.localeCompare(
455
+ right.id
456
+ )
457
+ ).slice(
458
+ 0,
459
+ options.limit
460
+ );
461
+ for (const schedule of due) {
462
+ schedule.leaseOwner = options.ownerId;
463
+ schedule.leaseUntil = now + options.leaseMs;
464
+ }
465
+ return due.map(
466
+ cloneSchedule
467
+ );
468
+ },
469
+ async complete(id, options) {
470
+ const schedule = schedules.get(id);
471
+ if (!schedule || schedule.leaseOwner !== options.ownerId) {
472
+ return;
473
+ }
474
+ schedule.lastRunAt = options.lastRunAt;
475
+ schedule.nextRunAt = options.nextRunAt;
476
+ schedule.updatedAt = options.updatedAt;
477
+ schedule.leaseOwner = void 0;
478
+ schedule.leaseUntil = void 0;
479
+ },
480
+ async release(id, ownerId) {
481
+ const schedule = schedules.get(id);
482
+ if (!schedule || schedule.leaseOwner !== ownerId) {
483
+ return;
484
+ }
485
+ schedule.leaseOwner = void 0;
486
+ schedule.leaseUntil = void 0;
487
+ },
488
+ clear() {
489
+ schedules.clear();
490
+ }
491
+ };
492
+ }
493
+ function createJobScheduler(options) {
494
+ if (!options || !options.queue) {
495
+ throw new TypeError(
496
+ "BCP Jobs: createJobScheduler requires a queue."
497
+ );
498
+ }
499
+ const queue = options.queue;
500
+ const store = options.store ?? createMemoryJobScheduleStore();
501
+ const now = options.now ?? Date.now;
502
+ const idFactory = options.idFactory ?? randomUUID2;
503
+ const ownerId = normalizeIdentifier(
504
+ options.ownerId ?? `scheduler-${randomUUID2()}`,
505
+ "scheduler owner id"
506
+ );
507
+ const runners = /* @__PURE__ */ new Set();
508
+ const scheduler = {
509
+ queue,
510
+ store,
511
+ ownerId,
512
+ async schedule(jobName, payload, scheduleOptions) {
513
+ const currentTime = normalizeTimestamp(
514
+ now(),
515
+ "current time"
516
+ );
517
+ const schedule = normalizeSchedule(
518
+ scheduleOptions
519
+ );
520
+ const id = normalizeIdentifier(
521
+ scheduleOptions.id ?? idFactory(),
522
+ "schedule id"
523
+ );
524
+ const startAt = scheduleOptions.startAt === void 0 ? void 0 : normalizeTimestamp(
525
+ scheduleOptions.startAt instanceof Date ? scheduleOptions.startAt.getTime() : scheduleOptions.startAt,
526
+ "startAt"
527
+ );
528
+ const maxAttempts = scheduleOptions.maxAttempts === void 0 ? void 0 : normalizePositiveInteger2(
529
+ scheduleOptions.maxAttempts,
530
+ "maxAttempts"
531
+ );
532
+ const nextRunAt = startAt ?? nextScheduleTime(
533
+ schedule,
534
+ currentTime
535
+ );
536
+ const record = {
537
+ id,
538
+ jobName: normalizeIdentifier(
539
+ jobName,
540
+ "job name"
541
+ ),
542
+ payload,
543
+ schedule,
544
+ createdAt: currentTime,
545
+ updatedAt: currentTime,
546
+ nextRunAt,
547
+ maxAttempts
548
+ };
549
+ await store.upsert(
550
+ record
551
+ );
552
+ return cloneSchedule(
553
+ record
554
+ );
555
+ },
556
+ get(id) {
557
+ return store.get(
558
+ normalizeIdentifier(
559
+ id,
560
+ "schedule id"
561
+ )
562
+ );
563
+ },
564
+ list() {
565
+ return store.list();
566
+ },
567
+ remove(id) {
568
+ return store.remove(
569
+ normalizeIdentifier(
570
+ id,
571
+ "schedule id"
572
+ )
573
+ );
574
+ },
575
+ async runDue(runOptions = {}) {
576
+ const currentTime = normalizeTimestamp(
577
+ now(),
578
+ "current time"
579
+ );
580
+ const limit = normalizePositiveInteger2(
581
+ runOptions.limit ?? 100,
582
+ "limit"
583
+ );
584
+ const leaseMs = normalizePositiveInteger2(
585
+ runOptions.leaseMs ?? 3e4,
586
+ "leaseMs"
587
+ );
588
+ const schedules = await store.acquireDue(
589
+ currentTime,
590
+ {
591
+ ownerId,
592
+ leaseMs,
593
+ limit
594
+ }
595
+ );
596
+ let enqueued = 0;
597
+ for (const scheduleRecord of schedules) {
598
+ const scheduledFor = scheduleRecord.nextRunAt;
599
+ const runId = createScheduledRunId(
600
+ scheduleRecord.id,
601
+ scheduledFor
602
+ );
603
+ try {
604
+ await queue.enqueue(
605
+ scheduleRecord.jobName,
606
+ scheduleRecord.payload,
607
+ {
608
+ id: runId,
609
+ maxAttempts: scheduleRecord.maxAttempts
610
+ }
611
+ );
612
+ enqueued += 1;
613
+ } catch (error) {
614
+ if (!isDuplicateJobIdError(
615
+ error
616
+ )) {
617
+ await store.release(
618
+ scheduleRecord.id,
619
+ ownerId
620
+ );
621
+ throw error;
622
+ }
623
+ }
624
+ const nextRunAt = nextScheduleTime(
625
+ scheduleRecord.schedule,
626
+ scheduledFor
627
+ );
628
+ await store.complete(
629
+ scheduleRecord.id,
630
+ {
631
+ ownerId,
632
+ lastRunAt: scheduledFor,
633
+ nextRunAt,
634
+ updatedAt: currentTime
635
+ }
636
+ );
637
+ }
638
+ return enqueued;
639
+ },
640
+ start(startOptions = {}) {
641
+ const runner = createSchedulerRunner(
642
+ scheduler,
643
+ startOptions,
644
+ () => runners.delete(
645
+ runner
646
+ )
647
+ );
648
+ runners.add(runner);
649
+ return runner;
650
+ },
651
+ async close() {
652
+ await Promise.all(
653
+ Array.from(
654
+ runners,
655
+ (runner) => runner.stop()
656
+ )
657
+ );
658
+ if (store.close) {
659
+ await store.close();
660
+ }
661
+ }
662
+ };
663
+ return scheduler;
664
+ }
665
+ function nextScheduleTime(schedule, after) {
666
+ const timestamp = normalizeTimestamp(
667
+ after,
668
+ "schedule reference time"
669
+ );
670
+ if (schedule.kind === "interval") {
671
+ return timestamp + normalizePositiveInteger2(
672
+ schedule.everyMs,
673
+ "everyMs"
674
+ );
675
+ }
676
+ return nextCronTime(
677
+ schedule.expression,
678
+ timestamp
679
+ );
680
+ }
681
+ function nextCronTime(expression, after) {
682
+ const cron = parseCronExpression(
683
+ expression
684
+ );
685
+ const afterTimestamp = normalizeTimestamp(
686
+ after,
687
+ "cron reference time"
688
+ );
689
+ const minute = 6e4;
690
+ let candidate = Math.floor(
691
+ afterTimestamp / minute
692
+ ) * minute + minute;
693
+ const maxIterations = 366 * 24 * 60 * 8;
694
+ for (let iteration = 0; iteration < maxIterations; iteration += 1) {
695
+ const date = new Date(candidate);
696
+ if (cron.month.has(
697
+ date.getUTCMonth() + 1
698
+ ) && cron.hour.has(
699
+ date.getUTCHours()
700
+ ) && cron.minute.has(
701
+ date.getUTCMinutes()
702
+ ) && matchesCronDay(
703
+ cron,
704
+ date
705
+ )) {
706
+ return candidate;
707
+ }
708
+ candidate += minute;
709
+ }
710
+ throw new RangeError(
711
+ `BCP Jobs: cron expression "${expression}" did not produce a run time within 8 years.`
712
+ );
713
+ }
714
+ function createSchedulerRunner(scheduler, options, onStop) {
715
+ const pollIntervalMs = normalizePositiveInteger2(
716
+ options.pollIntervalMs ?? 1e3,
717
+ "pollIntervalMs"
718
+ );
719
+ const runOptions = {
720
+ limit: options.limit,
721
+ leaseMs: options.leaseMs
722
+ };
723
+ const controller = new AbortController();
724
+ let running = true;
725
+ let stopPromise = null;
726
+ const loop = runSchedulerLoop(
727
+ scheduler,
728
+ controller.signal,
729
+ pollIntervalMs,
730
+ runOptions,
731
+ options.onError
732
+ );
733
+ return {
734
+ get running() {
735
+ return running;
736
+ },
737
+ stop() {
738
+ if (stopPromise) {
739
+ return stopPromise;
740
+ }
741
+ running = false;
742
+ controller.abort();
743
+ stopPromise = loop.finally(
744
+ onStop
745
+ );
746
+ return stopPromise;
747
+ }
748
+ };
749
+ }
750
+ async function runSchedulerLoop(scheduler, signal, pollIntervalMs, runOptions, onError) {
751
+ while (!signal.aborted) {
752
+ try {
753
+ await scheduler.runDue(
754
+ runOptions
755
+ );
756
+ } catch (error) {
757
+ if (onError) {
758
+ await onError(error);
759
+ }
760
+ }
761
+ if (!signal.aborted) {
762
+ await sleep2(
763
+ pollIntervalMs,
764
+ signal
765
+ );
766
+ }
767
+ }
768
+ }
769
+ function normalizeSchedule(options) {
770
+ if (!options) {
771
+ throw new TypeError(
772
+ "BCP Jobs: schedule options are required."
773
+ );
774
+ }
775
+ const hasInterval = options.everyMs !== void 0;
776
+ const hasCron = options.cron !== void 0;
777
+ if (hasInterval === hasCron) {
778
+ throw new TypeError(
779
+ "BCP Jobs: schedule requires exactly one of everyMs or cron."
780
+ );
781
+ }
782
+ if (hasInterval) {
783
+ return {
784
+ kind: "interval",
785
+ everyMs: normalizePositiveInteger2(
786
+ options.everyMs,
787
+ "everyMs"
788
+ )
789
+ };
790
+ }
791
+ const expression = String(options.cron).trim();
792
+ parseCronExpression(
793
+ expression
794
+ );
795
+ return {
796
+ kind: "cron",
797
+ expression
798
+ };
799
+ }
800
+ function parseCronExpression(expression) {
801
+ const parts = String(expression).trim().split(/\s+/);
802
+ if (parts.length !== 5) {
803
+ throw new TypeError(
804
+ "BCP Jobs: cron must contain 5 fields: minute hour day-of-month month day-of-week."
805
+ );
806
+ }
807
+ return {
808
+ minute: parseCronField(
809
+ parts[0],
810
+ 0,
811
+ 59,
812
+ "minute"
813
+ ).values,
814
+ hour: parseCronField(
815
+ parts[1],
816
+ 0,
817
+ 23,
818
+ "hour"
819
+ ).values,
820
+ dayOfMonth: parseCronField(
821
+ parts[2],
822
+ 1,
823
+ 31,
824
+ "day-of-month"
825
+ ).values,
826
+ month: parseCronField(
827
+ parts[3],
828
+ 1,
829
+ 12,
830
+ "month"
831
+ ).values,
832
+ dayOfWeek: parseCronField(
833
+ parts[4],
834
+ 0,
835
+ 7,
836
+ "day-of-week",
837
+ true
838
+ ).values,
839
+ dayOfMonthWildcard: parts[2] === "*",
840
+ dayOfWeekWildcard: parts[4] === "*"
841
+ };
842
+ }
843
+ function parseCronField(source, minimum, maximum, field, normalizeSunday = false) {
844
+ const values = /* @__PURE__ */ new Set();
845
+ for (const segment of source.split(",")) {
846
+ const [base, stepSource] = segment.split("/");
847
+ if (segment.split("/").length > 2) {
848
+ throwCronField(
849
+ field,
850
+ source
851
+ );
852
+ }
853
+ const step = stepSource === void 0 ? 1 : parseCronInteger(
854
+ stepSource,
855
+ 1,
856
+ maximum - minimum + 1,
857
+ field,
858
+ source
859
+ );
860
+ let rangeStart;
861
+ let rangeEnd;
862
+ if (base === "*") {
863
+ rangeStart = minimum;
864
+ rangeEnd = maximum;
865
+ } else if (base.includes("-")) {
866
+ const bounds = base.split("-");
867
+ if (bounds.length !== 2) {
868
+ throwCronField(
869
+ field,
870
+ source
871
+ );
872
+ }
873
+ rangeStart = parseCronInteger(
874
+ bounds[0],
875
+ minimum,
876
+ maximum,
877
+ field,
878
+ source
879
+ );
880
+ rangeEnd = parseCronInteger(
881
+ bounds[1],
882
+ minimum,
883
+ maximum,
884
+ field,
885
+ source
886
+ );
887
+ if (rangeEnd < rangeStart) {
888
+ throwCronField(
889
+ field,
890
+ source
891
+ );
892
+ }
893
+ } else {
894
+ rangeStart = parseCronInteger(
895
+ base,
896
+ minimum,
897
+ maximum,
898
+ field,
899
+ source
900
+ );
901
+ rangeEnd = rangeStart;
902
+ }
903
+ for (let value = rangeStart; value <= rangeEnd; value += step) {
904
+ values.add(
905
+ normalizeSunday && value === 7 ? 0 : value
906
+ );
907
+ }
908
+ }
909
+ if (values.size === 0) {
910
+ throwCronField(
911
+ field,
912
+ source
913
+ );
914
+ }
915
+ return {
916
+ values
917
+ };
918
+ }
919
+ function parseCronInteger(source, minimum, maximum, field, fullSource) {
920
+ if (!/^\d+$/.test(source)) {
921
+ throwCronField(
922
+ field,
923
+ fullSource
924
+ );
925
+ }
926
+ const value = Number(source);
927
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
928
+ throwCronField(
929
+ field,
930
+ fullSource
931
+ );
932
+ }
933
+ return value;
934
+ }
935
+ function throwCronField(field, source) {
936
+ throw new TypeError(
937
+ `BCP Jobs: invalid cron ${field} field "${source}".`
938
+ );
939
+ }
940
+ function matchesCronDay(cron, date) {
941
+ const dayOfMonthMatches = cron.dayOfMonth.has(
942
+ date.getUTCDate()
943
+ );
944
+ const dayOfWeekMatches = cron.dayOfWeek.has(
945
+ date.getUTCDay()
946
+ );
947
+ if (cron.dayOfMonthWildcard && cron.dayOfWeekWildcard) {
948
+ return true;
949
+ }
950
+ if (cron.dayOfMonthWildcard) {
951
+ return dayOfWeekMatches;
952
+ }
953
+ if (cron.dayOfWeekWildcard) {
954
+ return dayOfMonthMatches;
955
+ }
956
+ return dayOfMonthMatches || dayOfWeekMatches;
957
+ }
958
+ function createScheduledRunId(scheduleId, scheduledFor) {
959
+ return `schedule:${scheduleId}:${scheduledFor}`;
960
+ }
961
+ function isDuplicateJobIdError(error) {
962
+ return error instanceof Error && /job id .* already exists/i.test(
963
+ error.message
964
+ );
965
+ }
966
+ function normalizeIdentifier(value, field) {
967
+ const normalized = String(value).trim();
968
+ if (!normalized) {
969
+ throw new TypeError(
970
+ `BCP Jobs: ${field} must be a non-empty string.`
971
+ );
972
+ }
973
+ if (normalized.length > 200) {
974
+ throw new TypeError(
975
+ `BCP Jobs: ${field} must not exceed 200 characters.`
976
+ );
977
+ }
978
+ return normalized;
979
+ }
980
+ function normalizeTimestamp(value, field) {
981
+ if (!Number.isFinite(value) || value < 0) {
982
+ throw new TypeError(
983
+ `BCP Jobs: ${field} must be a non-negative finite timestamp.`
984
+ );
985
+ }
986
+ return Math.floor(value);
987
+ }
988
+ function normalizePositiveInteger2(value, field) {
989
+ if (!Number.isInteger(value) || value <= 0) {
990
+ throw new TypeError(
991
+ `BCP Jobs: ${field} must be a positive integer.`
992
+ );
993
+ }
994
+ return value;
995
+ }
996
+ function sleep2(durationMs, signal) {
997
+ if (signal.aborted) {
998
+ return Promise.resolve();
999
+ }
1000
+ return new Promise(
1001
+ (resolve) => {
1002
+ const timeout = setTimeout(
1003
+ finish,
1004
+ durationMs
1005
+ );
1006
+ const onAbort = () => finish();
1007
+ signal.addEventListener(
1008
+ "abort",
1009
+ onAbort,
1010
+ {
1011
+ once: true
1012
+ }
1013
+ );
1014
+ function finish() {
1015
+ clearTimeout(timeout);
1016
+ signal.removeEventListener(
1017
+ "abort",
1018
+ onAbort
1019
+ );
1020
+ resolve();
1021
+ }
1022
+ }
1023
+ );
1024
+ }
1025
+ function cloneSchedule(schedule) {
1026
+ return {
1027
+ ...schedule,
1028
+ schedule: {
1029
+ ...schedule.schedule
1030
+ }
1031
+ };
1032
+ }
1033
+ export {
1034
+ createJobQueue,
1035
+ createJobScheduler,
1036
+ createMemoryJobQueueAdapter,
1037
+ createMemoryJobScheduleStore,
1038
+ nextCronTime,
1039
+ nextScheduleTime
1040
+ };