@monque/core 1.1.0 → 1.1.2

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,267 @@
1
+ import type { Document } from 'mongodb';
2
+
3
+ import {
4
+ type EnqueueOptions,
5
+ type Job,
6
+ JobStatus,
7
+ type PersistedJob,
8
+ type ScheduleOptions,
9
+ } from '@/jobs';
10
+ import { ConnectionError, getNextCronDate, MonqueError } from '@/shared';
11
+
12
+ import type { SchedulerContext } from './types.js';
13
+
14
+ /**
15
+ * Internal service for job scheduling operations.
16
+ *
17
+ * Handles enqueueing new jobs, immediate dispatch, and cron scheduling.
18
+ * All operations are atomic and support deduplication via uniqueKey.
19
+ *
20
+ * @internal Not part of public API - use Monque class methods instead.
21
+ */
22
+ export class JobScheduler {
23
+ constructor(private readonly ctx: SchedulerContext) {}
24
+
25
+ /**
26
+ * Enqueue a job for processing.
27
+ *
28
+ * Jobs are stored in MongoDB and processed by registered workers. Supports
29
+ * delayed execution via `runAt` and deduplication via `uniqueKey`.
30
+ *
31
+ * When a `uniqueKey` is provided, only one pending or processing job with that key
32
+ * can exist. Completed or failed jobs don't block new jobs with the same key.
33
+ *
34
+ * Failed jobs are automatically retried with exponential backoff up to `maxRetries`
35
+ * (default: 10 attempts). The delay between retries is calculated as `2^failCount × baseRetryInterval`.
36
+ *
37
+ * @template T - The job data payload type (must be JSON-serializable)
38
+ * @param name - Job type identifier, must match a registered worker
39
+ * @param data - Job payload, will be passed to the worker handler
40
+ * @param options - Scheduling and deduplication options
41
+ * @returns Promise resolving to the created or existing job document
42
+ * @throws {ConnectionError} If database operation fails or scheduler not initialized
43
+ *
44
+ * @example Basic job enqueueing
45
+ * ```typescript
46
+ * await monque.enqueue('send-email', {
47
+ * to: 'user@example.com',
48
+ * subject: 'Welcome!',
49
+ * body: 'Thanks for signing up.'
50
+ * });
51
+ * ```
52
+ *
53
+ * @example Delayed execution
54
+ * ```typescript
55
+ * const oneHourLater = new Date(Date.now() + 3600000);
56
+ * await monque.enqueue('reminder', { message: 'Check in!' }, {
57
+ * runAt: oneHourLater
58
+ * });
59
+ * ```
60
+ *
61
+ * @example Prevent duplicates with unique key
62
+ * ```typescript
63
+ * await monque.enqueue('sync-user', { userId: '123' }, {
64
+ * uniqueKey: 'sync-user-123'
65
+ * });
66
+ * // Subsequent enqueues with same uniqueKey return existing pending/processing job
67
+ * ```
68
+ */
69
+ async enqueue<T>(name: string, data: T, options: EnqueueOptions = {}): Promise<PersistedJob<T>> {
70
+ const now = new Date();
71
+ const job: Omit<Job<T>, '_id'> = {
72
+ name,
73
+ data,
74
+ status: JobStatus.PENDING,
75
+ nextRunAt: options.runAt ?? now,
76
+ failCount: 0,
77
+ createdAt: now,
78
+ updatedAt: now,
79
+ };
80
+
81
+ if (options.uniqueKey) {
82
+ job.uniqueKey = options.uniqueKey;
83
+ }
84
+
85
+ try {
86
+ if (options.uniqueKey) {
87
+ // Use upsert with $setOnInsert for deduplication (scoped by name + uniqueKey)
88
+ const result = await this.ctx.collection.findOneAndUpdate(
89
+ {
90
+ name,
91
+ uniqueKey: options.uniqueKey,
92
+ status: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },
93
+ },
94
+ {
95
+ $setOnInsert: job,
96
+ },
97
+ {
98
+ upsert: true,
99
+ returnDocument: 'after',
100
+ },
101
+ );
102
+
103
+ if (!result) {
104
+ throw new ConnectionError('Failed to enqueue job: findOneAndUpdate returned no document');
105
+ }
106
+
107
+ return this.ctx.documentToPersistedJob<T>(result);
108
+ }
109
+
110
+ const result = await this.ctx.collection.insertOne(job as Document);
111
+
112
+ return { ...job, _id: result.insertedId } as PersistedJob<T>;
113
+ } catch (error) {
114
+ if (error instanceof ConnectionError) {
115
+ throw error;
116
+ }
117
+ const message = error instanceof Error ? error.message : 'Unknown error during enqueue';
118
+ throw new ConnectionError(
119
+ `Failed to enqueue job: ${message}`,
120
+ error instanceof Error ? { cause: error } : undefined,
121
+ );
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Enqueue a job for immediate processing.
127
+ *
128
+ * Convenience method equivalent to `enqueue(name, data, { runAt: new Date() })`.
129
+ * Jobs are picked up on the next poll cycle (typically within 1 second based on `pollInterval`).
130
+ *
131
+ * @template T - The job data payload type (must be JSON-serializable)
132
+ * @param name - Job type identifier, must match a registered worker
133
+ * @param data - Job payload, will be passed to the worker handler
134
+ * @returns Promise resolving to the created job document
135
+ * @throws {ConnectionError} If database operation fails or scheduler not initialized
136
+ *
137
+ * @example Send email immediately
138
+ * ```typescript
139
+ * await monque.now('send-email', {
140
+ * to: 'admin@example.com',
141
+ * subject: 'Alert',
142
+ * body: 'Immediate attention required'
143
+ * });
144
+ * ```
145
+ *
146
+ * @example Process order in background
147
+ * ```typescript
148
+ * const order = await createOrder(data);
149
+ * await monque.now('process-order', { orderId: order.id });
150
+ * return order; // Return immediately, processing happens async
151
+ * ```
152
+ */
153
+ async now<T>(name: string, data: T): Promise<PersistedJob<T>> {
154
+ return this.enqueue(name, data, { runAt: new Date() });
155
+ }
156
+
157
+ /**
158
+ * Schedule a recurring job with a cron expression.
159
+ *
160
+ * Creates a job that automatically re-schedules itself based on the cron pattern.
161
+ * Uses standard 5-field cron format: minute, hour, day of month, month, day of week.
162
+ * Also supports predefined expressions like `@daily`, `@weekly`, `@monthly`, etc.
163
+ * After successful completion, the job is reset to `pending` status and scheduled
164
+ * for its next run based on the cron expression.
165
+ *
166
+ * When a `uniqueKey` is provided, only one pending or processing job with that key
167
+ * can exist. This prevents duplicate scheduled jobs on application restart.
168
+ *
169
+ * @template T - The job data payload type (must be JSON-serializable)
170
+ * @param cron - Cron expression (5 fields or predefined expression)
171
+ * @param name - Job type identifier, must match a registered worker
172
+ * @param data - Job payload, will be passed to the worker handler on each run
173
+ * @param options - Scheduling options (uniqueKey for deduplication)
174
+ * @returns Promise resolving to the created job document with `repeatInterval` set
175
+ * @throws {InvalidCronError} If cron expression is invalid
176
+ * @throws {ConnectionError} If database operation fails or scheduler not initialized
177
+ *
178
+ * @example Hourly cleanup job
179
+ * ```typescript
180
+ * await monque.schedule('0 * * * *', 'cleanup-temp-files', {
181
+ * directory: '/tmp/uploads'
182
+ * });
183
+ * ```
184
+ *
185
+ * @example Prevent duplicate scheduled jobs with unique key
186
+ * ```typescript
187
+ * await monque.schedule('0 * * * *', 'hourly-report', { type: 'sales' }, {
188
+ * uniqueKey: 'hourly-report-sales'
189
+ * });
190
+ * // Subsequent calls with same uniqueKey return existing pending/processing job
191
+ * ```
192
+ *
193
+ * @example Daily report at midnight (using predefined expression)
194
+ * ```typescript
195
+ * await monque.schedule('@daily', 'daily-report', {
196
+ * reportType: 'sales',
197
+ * recipients: ['analytics@example.com']
198
+ * });
199
+ * ```
200
+ */
201
+ async schedule<T>(
202
+ cron: string,
203
+ name: string,
204
+ data: T,
205
+ options: ScheduleOptions = {},
206
+ ): Promise<PersistedJob<T>> {
207
+ // Validate cron and get next run date (throws InvalidCronError if invalid)
208
+ const nextRunAt = getNextCronDate(cron);
209
+
210
+ const now = new Date();
211
+ const job: Omit<Job<T>, '_id'> = {
212
+ name,
213
+ data,
214
+ status: JobStatus.PENDING,
215
+ nextRunAt,
216
+ repeatInterval: cron,
217
+ failCount: 0,
218
+ createdAt: now,
219
+ updatedAt: now,
220
+ };
221
+
222
+ if (options.uniqueKey) {
223
+ job.uniqueKey = options.uniqueKey;
224
+ }
225
+
226
+ try {
227
+ if (options.uniqueKey) {
228
+ // Use upsert with $setOnInsert for deduplication (scoped by name + uniqueKey)
229
+ const result = await this.ctx.collection.findOneAndUpdate(
230
+ {
231
+ name,
232
+ uniqueKey: options.uniqueKey,
233
+ status: { $in: [JobStatus.PENDING, JobStatus.PROCESSING] },
234
+ },
235
+ {
236
+ $setOnInsert: job,
237
+ },
238
+ {
239
+ upsert: true,
240
+ returnDocument: 'after',
241
+ },
242
+ );
243
+
244
+ if (!result) {
245
+ throw new ConnectionError(
246
+ 'Failed to schedule job: findOneAndUpdate returned no document',
247
+ );
248
+ }
249
+
250
+ return this.ctx.documentToPersistedJob<T>(result);
251
+ }
252
+
253
+ const result = await this.ctx.collection.insertOne(job as Document);
254
+
255
+ return { ...job, _id: result.insertedId } as PersistedJob<T>;
256
+ } catch (error) {
257
+ if (error instanceof MonqueError) {
258
+ throw error;
259
+ }
260
+ const message = error instanceof Error ? error.message : 'Unknown error during schedule';
261
+ throw new ConnectionError(
262
+ `Failed to schedule job: ${message}`,
263
+ error instanceof Error ? { cause: error } : undefined,
264
+ );
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,48 @@
1
+ import type { Collection, Document, WithId } from 'mongodb';
2
+
3
+ import type { MonqueEventMap } from '@/events';
4
+ import type { PersistedJob } from '@/jobs';
5
+ import type { WorkerRegistration } from '@/workers';
6
+
7
+ import type { MonqueOptions } from '../types.js';
8
+
9
+ /**
10
+ * Resolved Monque options with all defaults applied.
11
+ *
12
+ * Required options have their defaults filled in, while truly optional
13
+ * options (`maxBackoffDelay`, `jobRetention`) remain optional.
14
+ */
15
+ export interface ResolvedMonqueOptions
16
+ extends Required<Omit<MonqueOptions, 'maxBackoffDelay' | 'jobRetention'>>,
17
+ Pick<MonqueOptions, 'maxBackoffDelay' | 'jobRetention'> {}
18
+ /**
19
+ * Shared context provided to all internal Monque services.
20
+ *
21
+ * Contains references to shared state, configuration, and utilities
22
+ * needed by service methods. Passed to service constructors to enable
23
+ * access to the collection, options, and event emission.
24
+ *
25
+ * @internal Not part of public API.
26
+ */
27
+ export interface SchedulerContext {
28
+ /** MongoDB collection for jobs */
29
+ collection: Collection<Document>;
30
+
31
+ /** Resolved scheduler options with defaults applied */
32
+ options: ResolvedMonqueOptions;
33
+
34
+ /** Unique identifier for this scheduler instance (for claiming jobs) */
35
+ instanceId: string;
36
+
37
+ /** Registered workers by job name */
38
+ workers: Map<string, WorkerRegistration>;
39
+
40
+ /** Whether the scheduler is currently running */
41
+ isRunning: () => boolean;
42
+
43
+ /** Type-safe event emitter */
44
+ emit: <K extends keyof MonqueEventMap>(event: K, payload: MonqueEventMap[K]) => boolean;
45
+
46
+ /** Convert MongoDB document to typed PersistedJob */
47
+ documentToPersistedJob: <T>(doc: WithId<Document>) => PersistedJob<T>;
48
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Configuration options for the Monque scheduler.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * const monque = new Monque(db, {
7
+ * collectionName: 'jobs',
8
+ * pollInterval: 1000,
9
+ * maxRetries: 10,
10
+ * baseRetryInterval: 1000,
11
+ * shutdownTimeout: 30000,
12
+ * defaultConcurrency: 5,
13
+ * });
14
+ * ```
15
+ */
16
+ export interface MonqueOptions {
17
+ /**
18
+ * Name of the MongoDB collection for storing jobs.
19
+ * @default 'monque_jobs'
20
+ */
21
+ collectionName?: string;
22
+
23
+ /**
24
+ * Interval in milliseconds between polling for new jobs.
25
+ * @default 1000
26
+ */
27
+ pollInterval?: number;
28
+
29
+ /**
30
+ * Maximum number of retry attempts before marking a job as permanently failed.
31
+ * @default 10
32
+ */
33
+ maxRetries?: number;
34
+
35
+ /**
36
+ * Base interval in milliseconds for exponential backoff calculation.
37
+ * Actual delay = 2^failCount * baseRetryInterval
38
+ * @default 1000
39
+ */
40
+ baseRetryInterval?: number;
41
+
42
+ /**
43
+ * Maximum delay in milliseconds for exponential backoff.
44
+ * If calculated delay exceeds this value, it will be capped.
45
+ *
46
+ * Defaults to 24 hours to prevent unbounded delays.
47
+ * @default 86400000 (24 hours)
48
+ */
49
+ maxBackoffDelay?: number | undefined;
50
+
51
+ /**
52
+ * Timeout in milliseconds for graceful shutdown.
53
+ * @default 30000
54
+ */
55
+ shutdownTimeout?: number;
56
+
57
+ /**
58
+ * Default number of concurrent jobs per worker.
59
+ * @default 5
60
+ */
61
+ defaultConcurrency?: number;
62
+
63
+ /**
64
+ * Maximum time in milliseconds a job can be in 'processing' status before
65
+ * being considered stale and eligible for recovery.
66
+ *
67
+ * Stale recovery uses `lockedAt` as the source of truth; this is an absolute
68
+ * “time locked” limit, not a heartbeat timeout.
69
+ * @default 1800000 (30 minutes)
70
+ */
71
+ lockTimeout?: number;
72
+
73
+ /**
74
+ * Unique identifier for this scheduler instance.
75
+ * Used for atomic job claiming - each instance uses this ID to claim jobs.
76
+ * Defaults to a randomly generated UUID v4.
77
+ * @default crypto.randomUUID()
78
+ */
79
+ schedulerInstanceId?: string;
80
+
81
+ /**
82
+ * Interval in milliseconds for heartbeat updates during job processing.
83
+ * The scheduler periodically updates `lastHeartbeat` for all jobs it is processing
84
+ * to indicate liveness for monitoring/debugging.
85
+ *
86
+ * Note: stale recovery is based on `lockedAt` + `lockTimeout`, not `lastHeartbeat`.
87
+ * @default 30000 (30 seconds)
88
+ */
89
+ heartbeatInterval?: number;
90
+
91
+ /**
92
+ * Whether to recover stale processing jobs on scheduler startup.
93
+ * When true, jobs with lockedAt older than lockTimeout will be reset to pending.
94
+ * @default true
95
+ */
96
+ recoverStaleJobs?: boolean;
97
+
98
+ /**
99
+ * Configuration for automatic cleanup of completed and failed jobs.
100
+ * If undefined, no cleanup is performed.
101
+ */
102
+ jobRetention?:
103
+ | {
104
+ /**
105
+ * Age in milliseconds after which completed jobs are deleted.
106
+ * Cleaned up based on 'updatedAt' timestamp.
107
+ */
108
+ completed?: number;
109
+
110
+ /**
111
+ * Age in milliseconds after which failed jobs are deleted.
112
+ * Cleaned up based on 'updatedAt' timestamp.
113
+ */
114
+ failed?: number;
115
+
116
+ /**
117
+ * Interval in milliseconds for running the cleanup job.
118
+ * @default 3600000 (1 hour)
119
+ */
120
+ interval?: number;
121
+ }
122
+ | undefined;
123
+ }
@@ -0,0 +1,225 @@
1
+ import type { Job } from '@/jobs';
2
+
3
+ /**
4
+ * Base error class for all Monque-related errors.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * try {
9
+ * await monque.enqueue('job', data);
10
+ * } catch (error) {
11
+ * if (error instanceof MonqueError) {
12
+ * console.error('Monque error:', error.message);
13
+ * }
14
+ * }
15
+ * ```
16
+ */
17
+ export class MonqueError extends Error {
18
+ constructor(message: string) {
19
+ super(message);
20
+ this.name = 'MonqueError';
21
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
22
+ /* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
23
+ if (Error.captureStackTrace) {
24
+ Error.captureStackTrace(this, MonqueError);
25
+ }
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Error thrown when an invalid cron expression is provided.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * try {
35
+ * await monque.schedule('invalid cron', 'job', data);
36
+ * } catch (error) {
37
+ * if (error instanceof InvalidCronError) {
38
+ * console.error('Invalid expression:', error.expression);
39
+ * }
40
+ * }
41
+ * ```
42
+ */
43
+ export class InvalidCronError extends MonqueError {
44
+ constructor(
45
+ public readonly expression: string,
46
+ message: string,
47
+ ) {
48
+ super(message);
49
+ this.name = 'InvalidCronError';
50
+ /* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
51
+ if (Error.captureStackTrace) {
52
+ Error.captureStackTrace(this, InvalidCronError);
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Error thrown when there's a database connection issue.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * try {
63
+ * await monque.enqueue('job', data);
64
+ * } catch (error) {
65
+ * if (error instanceof ConnectionError) {
66
+ * console.error('Database connection lost');
67
+ * }
68
+ * }
69
+ * ```
70
+ */
71
+ export class ConnectionError extends MonqueError {
72
+ constructor(message: string, options?: { cause?: Error }) {
73
+ super(message);
74
+ this.name = 'ConnectionError';
75
+ if (options?.cause) {
76
+ this.cause = options.cause;
77
+ }
78
+ /* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
79
+ if (Error.captureStackTrace) {
80
+ Error.captureStackTrace(this, ConnectionError);
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Error thrown when graceful shutdown times out.
87
+ * Includes information about jobs that were still in progress.
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * try {
92
+ * await monque.stop();
93
+ * } catch (error) {
94
+ * if (error instanceof ShutdownTimeoutError) {
95
+ * console.error('Incomplete jobs:', error.incompleteJobs.length);
96
+ * }
97
+ * }
98
+ * ```
99
+ */
100
+ export class ShutdownTimeoutError extends MonqueError {
101
+ constructor(
102
+ message: string,
103
+ public readonly incompleteJobs: Job[],
104
+ ) {
105
+ super(message);
106
+ this.name = 'ShutdownTimeoutError';
107
+ /* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
108
+ if (Error.captureStackTrace) {
109
+ Error.captureStackTrace(this, ShutdownTimeoutError);
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Error thrown when attempting to register a worker for a job name
116
+ * that already has a registered worker, without explicitly allowing replacement.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * try {
121
+ * monque.register('send-email', handler1);
122
+ * monque.register('send-email', handler2); // throws
123
+ * } catch (error) {
124
+ * if (error instanceof WorkerRegistrationError) {
125
+ * console.error('Worker already registered for:', error.jobName);
126
+ * }
127
+ * }
128
+ *
129
+ * // To intentionally replace a worker:
130
+ * monque.register('send-email', handler2, { replace: true });
131
+ * ```
132
+ */
133
+ export class WorkerRegistrationError extends MonqueError {
134
+ constructor(
135
+ message: string,
136
+ public readonly jobName: string,
137
+ ) {
138
+ super(message);
139
+ this.name = 'WorkerRegistrationError';
140
+ /* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
141
+ if (Error.captureStackTrace) {
142
+ Error.captureStackTrace(this, WorkerRegistrationError);
143
+ }
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Error thrown when a state transition is invalid.
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * try {
153
+ * await monque.cancelJob(jobId);
154
+ * } catch (error) {
155
+ * if (error instanceof JobStateError) {
156
+ * console.error(`Cannot cancel job in state: ${error.currentStatus}`);
157
+ * }
158
+ * }
159
+ * ```
160
+ */
161
+ export class JobStateError extends MonqueError {
162
+ constructor(
163
+ message: string,
164
+ public readonly jobId: string,
165
+ public readonly currentStatus: string,
166
+ public readonly attemptedAction: 'cancel' | 'retry' | 'reschedule',
167
+ ) {
168
+ super(message);
169
+ this.name = 'JobStateError';
170
+ /* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
171
+ if (Error.captureStackTrace) {
172
+ Error.captureStackTrace(this, JobStateError);
173
+ }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Error thrown when a pagination cursor is invalid or malformed.
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * try {
183
+ * await monque.listJobs({ cursor: 'invalid-cursor' });
184
+ * } catch (error) {
185
+ * if (error instanceof InvalidCursorError) {
186
+ * console.error('Invalid cursor provided');
187
+ * }
188
+ * }
189
+ * ```
190
+ */
191
+ export class InvalidCursorError extends MonqueError {
192
+ constructor(message: string) {
193
+ super(message);
194
+ this.name = 'InvalidCursorError';
195
+ /* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
196
+ if (Error.captureStackTrace) {
197
+ Error.captureStackTrace(this, InvalidCursorError);
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Error thrown when a statistics aggregation times out.
204
+ *
205
+ * @example
206
+ * ```typescript
207
+ * try {
208
+ * const stats = await monque.getQueueStats();
209
+ * } catch (error) {
210
+ * if (error instanceof AggregationTimeoutError) {
211
+ * console.error('Stats took too long to calculate');
212
+ * }
213
+ * }
214
+ * ```
215
+ */
216
+ export class AggregationTimeoutError extends MonqueError {
217
+ constructor(message: string = 'Statistics aggregation exceeded 30 second timeout') {
218
+ super(message);
219
+ this.name = 'AggregationTimeoutError';
220
+ /* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
221
+ if (Error.captureStackTrace) {
222
+ Error.captureStackTrace(this, AggregationTimeoutError);
223
+ }
224
+ }
225
+ }
@@ -0,0 +1,18 @@
1
+ export {
2
+ AggregationTimeoutError,
3
+ ConnectionError,
4
+ InvalidCronError,
5
+ InvalidCursorError,
6
+ JobStateError,
7
+ MonqueError,
8
+ ShutdownTimeoutError,
9
+ WorkerRegistrationError,
10
+ } from './errors.js';
11
+ export {
12
+ calculateBackoff,
13
+ calculateBackoffDelay,
14
+ DEFAULT_BASE_INTERVAL,
15
+ DEFAULT_MAX_BACKOFF_DELAY,
16
+ getNextCronDate,
17
+ validateCronExpression,
18
+ } from './utils/index.js';