@emmvish/stable-request 2.5.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,8 @@ A stability-first production-grade TypeScript framework for resilient API integr
12
12
  - [stableApiGateway](#stableapigateway)
13
13
  - [stableWorkflow](#stableworkflow)
14
14
  - [stableWorkflowGraph](#stableworkflowgraph)
15
+ - [StableScheduler](#stablescheduler)
16
+ - [Stable Runner](#stable-runner)
15
17
  - [Resilience Mechanisms](#resilience-mechanisms)
16
18
  - [Retry Strategies](#retry-strategies)
17
19
  - [Circuit Breaker](#circuit-breaker)
@@ -53,8 +55,9 @@ A stability-first production-grade TypeScript framework for resilient API integr
53
55
  2. **Phased workflows** via `stableWorkflow` for array-based multi-phase execution with dynamic control flow
54
56
  3. **Graph-based workflows** via `stableWorkflowGraph` for DAG execution with higher parallelism
55
57
  4. **Generic function execution** via `stableFunction`, inheriting all resilience guards
58
+ 5. **Queue based scheduling** via `StableScheduler`, with option to preserve scheduler state and recover from saved state
56
59
 
57
- All five execution modes support the same resilience stack: retries, jitter, circuit breaking, caching, rate/concurrency limits, config cascading, shared buffers, trial mode, comprehensive hooks, and metrics. This uniformity makes it trivial to compose requests and functions in any topology.
60
+ All six core modules support the same resilience stack: retries, jitter, circuit breaking, caching, rate/concurrency limits, config cascading, shared buffers, trial mode, comprehensive hooks, and metrics. This uniformity makes it trivial to compose requests and functions in any topology. Finally, `Stable Runner` executes jobs from config.
58
61
 
59
62
  ---
60
63
 
@@ -484,6 +487,21 @@ console.log(`Graph workflow success: ${result.success}`);
484
487
  - Provide deterministic execution order
485
488
  - Offer higher parallelism than phased workflows for complex topologies
486
489
 
490
+ ### StableScheduler
491
+
492
+ Queue-based scheduler for cron/interval/timestamp execution with concurrency limits and recoverable state via custom persistence handlers.
493
+
494
+ **Key responsibilities:**
495
+ - Enforce max-parallel job execution
496
+ - Schedule jobs with cron, interval, or timestamp(s)
497
+ - Persist and restore scheduler state via user-provided handlers
498
+
499
+ ---
500
+
501
+ ## Stable Runner
502
+
503
+ Config-driven runner that executes core module jobs from JSON/ESM configs and can use StableScheduler for scheduled jobs.
504
+
487
505
  ---
488
506
 
489
507
  ## Resilience Mechanisms
@@ -3,4 +3,5 @@ export { stableFunction } from './stable-function.js';
3
3
  export { stableApiGateway } from './stable-api-gateway.js';
4
4
  export { stableWorkflow } from './stable-workflow.js';
5
5
  export { stableWorkflowGraph } from './stable-workflow-graph.js';
6
+ export { StableScheduler } from './stable-scheduler.js';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
@@ -3,4 +3,5 @@ export { stableFunction } from './stable-function.js';
3
3
  export { stableApiGateway } from './stable-api-gateway.js';
4
4
  export { stableWorkflow } from './stable-workflow.js';
5
5
  export { stableWorkflowGraph } from './stable-workflow-graph.js';
6
+ export { StableScheduler } from './stable-scheduler.js';
6
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { SchedulerConfig, SchedulerRetryConfig, SchedulerSchedule, SchedulerState, SchedulerJobHandler } from '../types/index.js';
2
+ export declare class StableScheduler<TJob extends {
3
+ id?: string;
4
+ schedule?: SchedulerSchedule;
5
+ retry?: SchedulerRetryConfig;
6
+ executionTimeoutMs?: number;
7
+ }> {
8
+ private readonly config;
9
+ private readonly handler;
10
+ private readonly jobs;
11
+ private readonly queue;
12
+ private readonly queued;
13
+ private timer;
14
+ private persistTimer;
15
+ private persistQueued;
16
+ private runningCount;
17
+ private completed;
18
+ private failed;
19
+ private dropped;
20
+ private sequence;
21
+ constructor(config: SchedulerConfig, handler: SchedulerJobHandler<TJob>);
22
+ addJobs(jobs: TJob[]): void;
23
+ setJobs(jobs: TJob[]): void;
24
+ addJob(job: TJob): string;
25
+ start(): void;
26
+ stop(): void;
27
+ tick(): void;
28
+ getStats(): {
29
+ queued: number;
30
+ running: number;
31
+ completed: number;
32
+ failed: number;
33
+ dropped: number;
34
+ totalJobs: number;
35
+ };
36
+ getState(): SchedulerState<TJob>;
37
+ restoreState(state?: SchedulerState<TJob>): Promise<boolean>;
38
+ private dispatch;
39
+ private getRetryConfig;
40
+ private getExecutionTimeoutMs;
41
+ private scheduleRetryIfEnabled;
42
+ private withTimeout;
43
+ private initializeSchedule;
44
+ private updateNextRun;
45
+ private parseTimestamp;
46
+ private getNextCronTime;
47
+ private parseCronField;
48
+ private isValidInteger;
49
+ private getCronDateParts;
50
+ private createId;
51
+ private generateUuid;
52
+ private persistStateIfEnabled;
53
+ }
54
+ //# sourceMappingURL=stable-scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stable-scheduler.d.ts","sourceRoot":"","sources":["../../src/core/stable-scheduler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EAEpB,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EAEpB,MAAM,mBAAmB,CAAC;AAE3B,qBAAa,eAAe,CAC1B,IAAI,SAAS;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE;IAErH,OAAO,CAAC,QAAQ,CAAC,MAAM,CAarB;IACF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,QAAQ,CAAK;gBAET,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,mBAAmB,CAAC,IAAI,CAAC;IAkBvE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI;IAK3B,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI;IAc3B,MAAM,CAAC,GAAG,EAAE,IAAI,GAAG,MAAM;IAqBzB,KAAK,IAAI,IAAI;IAQb,IAAI,IAAI,IAAI;IAOZ,IAAI,IAAI,IAAI;IAiCZ,QAAQ;;;;;;;;IAWR,QAAQ,IAAI,cAAc,CAAC,IAAI,CAAC;IAuB1B,YAAY,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IA+ClE,OAAO,CAAC,QAAQ;IA8ChB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,sBAAsB;IAwB9B,OAAO,CAAC,WAAW;IAsBnB,OAAO,CAAC,kBAAkB;IAoC1B,OAAO,CAAC,aAAa;IA8BrB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,eAAe;IA+CvB,OAAO,CAAC,cAAc;IA6DtB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,gBAAgB;IAuExB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,YAAY;YAuBN,qBAAqB;CA0BpC"}
@@ -0,0 +1,536 @@
1
+ import { ScheduleTypes } from '../enums/index.js';
2
+ export class StableScheduler {
3
+ config;
4
+ handler;
5
+ jobs = new Map();
6
+ queue = [];
7
+ queued = new Set();
8
+ timer = null;
9
+ persistTimer = null;
10
+ persistQueued = false;
11
+ runningCount = 0;
12
+ completed = 0;
13
+ failed = 0;
14
+ dropped = 0;
15
+ sequence = 0;
16
+ constructor(config, handler) {
17
+ this.config = {
18
+ maxParallel: config.maxParallel ?? 2,
19
+ tickIntervalMs: config.tickIntervalMs ?? 500,
20
+ queueLimit: config.queueLimit ?? 1000,
21
+ timezone: config.timezone,
22
+ persistence: {
23
+ enabled: config.persistence?.enabled ?? false,
24
+ saveState: config.persistence?.saveState,
25
+ loadState: config.persistence?.loadState
26
+ },
27
+ retry: config.retry,
28
+ executionTimeoutMs: config.executionTimeoutMs ?? 86400000,
29
+ persistenceDebounceMs: config.persistenceDebounceMs ?? 1000
30
+ };
31
+ this.handler = handler;
32
+ }
33
+ addJobs(jobs) {
34
+ jobs.forEach((job) => this.addJob(job));
35
+ void this.persistStateIfEnabled();
36
+ }
37
+ setJobs(jobs) {
38
+ this.stop();
39
+ this.jobs.clear();
40
+ this.queue.length = 0;
41
+ this.queued.clear();
42
+ this.runningCount = 0;
43
+ this.completed = 0;
44
+ this.failed = 0;
45
+ this.dropped = 0;
46
+ this.addJobs(jobs);
47
+ this.start();
48
+ void this.persistStateIfEnabled();
49
+ }
50
+ addJob(job) {
51
+ const id = job.id ?? this.createId('job');
52
+ const schedule = job.schedule;
53
+ const now = Date.now();
54
+ const { nextRunAt, runOnce, remainingTimestamps } = this.initializeSchedule(schedule, now);
55
+ const scheduledJob = {
56
+ id,
57
+ job: { ...job, id },
58
+ schedule,
59
+ nextRunAt,
60
+ lastRunAt: null,
61
+ remainingTimestamps,
62
+ runOnce,
63
+ isRunning: false,
64
+ retryAttempts: 0
65
+ };
66
+ this.jobs.set(id, scheduledJob);
67
+ void this.persistStateIfEnabled();
68
+ return id;
69
+ }
70
+ start() {
71
+ if (this.timer) {
72
+ return;
73
+ }
74
+ this.timer = setInterval(() => this.tick(), Math.max(50, this.config.tickIntervalMs));
75
+ this.tick();
76
+ }
77
+ stop() {
78
+ if (this.timer) {
79
+ clearInterval(this.timer);
80
+ this.timer = null;
81
+ }
82
+ }
83
+ tick() {
84
+ const now = Date.now();
85
+ let stateChanged = false;
86
+ for (const [id, job] of this.jobs.entries()) {
87
+ if (job.isRunning || this.queued.has(id) || job.nextRunAt === null) {
88
+ continue;
89
+ }
90
+ if (job.nextRunAt <= now) {
91
+ if (this.queue.length >= this.config.queueLimit) {
92
+ this.dropped += 1;
93
+ continue;
94
+ }
95
+ this.queue.push(id);
96
+ this.queued.add(id);
97
+ stateChanged = true;
98
+ }
99
+ }
100
+ while (this.runningCount < this.config.maxParallel && this.queue.length > 0) {
101
+ const id = this.queue.shift();
102
+ if (!id)
103
+ break;
104
+ this.queued.delete(id);
105
+ const job = this.jobs.get(id);
106
+ if (!job)
107
+ continue;
108
+ this.dispatch(job);
109
+ stateChanged = true;
110
+ }
111
+ if (stateChanged) {
112
+ void this.persistStateIfEnabled();
113
+ }
114
+ }
115
+ getStats() {
116
+ return {
117
+ queued: this.queue.length,
118
+ running: this.runningCount,
119
+ completed: this.completed,
120
+ failed: this.failed,
121
+ dropped: this.dropped,
122
+ totalJobs: this.jobs.size
123
+ };
124
+ }
125
+ getState() {
126
+ return {
127
+ jobs: Array.from(this.jobs.values()).map((job) => ({
128
+ id: job.id,
129
+ job: job.job,
130
+ schedule: job.schedule,
131
+ nextRunAt: job.nextRunAt,
132
+ lastRunAt: job.lastRunAt,
133
+ remainingTimestamps: job.remainingTimestamps ? [...job.remainingTimestamps] : null,
134
+ runOnce: job.runOnce,
135
+ isRunning: job.isRunning,
136
+ retryAttempts: job.retryAttempts
137
+ })),
138
+ queue: [...this.queue],
139
+ stats: {
140
+ completed: this.completed,
141
+ failed: this.failed,
142
+ dropped: this.dropped,
143
+ sequence: this.sequence
144
+ }
145
+ };
146
+ }
147
+ async restoreState(state) {
148
+ let resolvedState = state;
149
+ if (!resolvedState && this.config.persistence.loadState) {
150
+ resolvedState = await this.config.persistence.loadState();
151
+ }
152
+ if (!resolvedState) {
153
+ return false;
154
+ }
155
+ this.stop();
156
+ this.jobs.clear();
157
+ this.queue.length = 0;
158
+ this.queued.clear();
159
+ this.runningCount = 0;
160
+ this.completed = resolvedState.stats.completed;
161
+ this.failed = resolvedState.stats.failed;
162
+ this.dropped = resolvedState.stats.dropped;
163
+ this.sequence = resolvedState.stats.sequence;
164
+ resolvedState.jobs.forEach((jobState) => {
165
+ const restored = {
166
+ id: jobState.id,
167
+ job: jobState.job,
168
+ schedule: jobState.schedule,
169
+ nextRunAt: jobState.nextRunAt,
170
+ lastRunAt: jobState.lastRunAt,
171
+ remainingTimestamps: jobState.remainingTimestamps ? [...jobState.remainingTimestamps] : null,
172
+ runOnce: jobState.runOnce,
173
+ isRunning: false,
174
+ retryAttempts: jobState.retryAttempts ?? 0
175
+ };
176
+ this.jobs.set(jobState.id, restored);
177
+ });
178
+ resolvedState.queue.forEach((id) => {
179
+ if (this.jobs.has(id)) {
180
+ this.queue.push(id);
181
+ this.queued.add(id);
182
+ }
183
+ });
184
+ this.tick();
185
+ void this.persistStateIfEnabled();
186
+ return true;
187
+ }
188
+ dispatch(job) {
189
+ this.runningCount += 1;
190
+ job.isRunning = true;
191
+ void this.persistStateIfEnabled();
192
+ const startedAt = Date.now();
193
+ const context = {
194
+ runId: this.createId('run'),
195
+ jobId: job.id,
196
+ scheduledAt: new Date(job.nextRunAt ?? startedAt).toISOString(),
197
+ startedAt: new Date(startedAt).toISOString(),
198
+ schedule: job.schedule
199
+ };
200
+ const retryConfig = this.getRetryConfig(job);
201
+ if (retryConfig) {
202
+ job.retryAttempts += 1;
203
+ }
204
+ let jobError = null;
205
+ const handlerPromise = this.handler(job.job, context);
206
+ const executionPromise = this.withTimeout(handlerPromise, this.getExecutionTimeoutMs(job));
207
+ void executionPromise
208
+ .then(() => {
209
+ this.completed += 1;
210
+ job.retryAttempts = 0;
211
+ })
212
+ .catch((error) => {
213
+ this.failed += 1;
214
+ jobError = error;
215
+ })
216
+ .finally(() => {
217
+ const scheduledRetry = this.scheduleRetryIfEnabled(job, startedAt, jobError);
218
+ job.isRunning = false;
219
+ job.lastRunAt = startedAt;
220
+ this.runningCount -= 1;
221
+ if (!scheduledRetry) {
222
+ job.retryAttempts = 0;
223
+ this.updateNextRun(job, startedAt);
224
+ }
225
+ this.tick();
226
+ void this.persistStateIfEnabled();
227
+ });
228
+ }
229
+ getRetryConfig(job) {
230
+ return job.job.retry ?? this.config.retry;
231
+ }
232
+ getExecutionTimeoutMs(job) {
233
+ return job.job.executionTimeoutMs ?? this.config.executionTimeoutMs;
234
+ }
235
+ scheduleRetryIfEnabled(job, startedAt, error) {
236
+ if (!error) {
237
+ return false;
238
+ }
239
+ const retryConfig = this.getRetryConfig(job);
240
+ if (!retryConfig) {
241
+ return false;
242
+ }
243
+ const maxAttempts = retryConfig.maxAttempts ?? 1;
244
+ if (maxAttempts <= 1 || job.retryAttempts >= maxAttempts) {
245
+ return false;
246
+ }
247
+ const baseDelay = retryConfig.delayMs ?? 1000;
248
+ const backoff = retryConfig.backoffMultiplier ?? 1;
249
+ const calculatedDelay = baseDelay * Math.pow(backoff, Math.max(job.retryAttempts - 1, 0));
250
+ const delay = retryConfig.maxDelayMs ? Math.min(calculatedDelay, retryConfig.maxDelayMs) : calculatedDelay;
251
+ job.nextRunAt = startedAt + Math.max(0, delay);
252
+ return true;
253
+ }
254
+ withTimeout(promise, timeoutMs) {
255
+ if (!timeoutMs || timeoutMs <= 0) {
256
+ return promise;
257
+ }
258
+ return new Promise((resolve, reject) => {
259
+ const timeoutId = setTimeout(() => {
260
+ reject(new Error(`Scheduler job timed out after ${timeoutMs}ms`));
261
+ }, timeoutMs);
262
+ promise
263
+ .then((value) => {
264
+ clearTimeout(timeoutId);
265
+ resolve(value);
266
+ })
267
+ .catch((error) => {
268
+ clearTimeout(timeoutId);
269
+ reject(error);
270
+ });
271
+ });
272
+ }
273
+ initializeSchedule(schedule, now) {
274
+ if (!schedule) {
275
+ return { nextRunAt: now, runOnce: true, remainingTimestamps: null };
276
+ }
277
+ if (schedule.type === ScheduleTypes.INTERVAL) {
278
+ const startAt = this.parseTimestamp(schedule.startAt);
279
+ if (startAt !== null && startAt > now) {
280
+ return { nextRunAt: startAt, runOnce: false, remainingTimestamps: null };
281
+ }
282
+ return { nextRunAt: now, runOnce: false, remainingTimestamps: null };
283
+ }
284
+ if (schedule.type === ScheduleTypes.CRON) {
285
+ const nextRunAt = this.getNextCronTime(schedule.expression, now, schedule.timezone);
286
+ return { nextRunAt, runOnce: false, remainingTimestamps: null };
287
+ }
288
+ if (schedule.type === ScheduleTypes.TIMESTAMP) {
289
+ const at = this.parseTimestamp(schedule.at);
290
+ return { nextRunAt: at, runOnce: true, remainingTimestamps: null };
291
+ }
292
+ const timestamps = schedule.at
293
+ .map((value) => this.parseTimestamp(value))
294
+ .filter((value) => value !== null)
295
+ .sort((a, b) => a - b);
296
+ const nextRunAt = timestamps.length > 0 ? timestamps[0] : null;
297
+ return { nextRunAt, runOnce: false, remainingTimestamps: timestamps };
298
+ }
299
+ updateNextRun(job, lastRunAt) {
300
+ const schedule = job.schedule;
301
+ if (!schedule) {
302
+ job.nextRunAt = job.runOnce ? null : lastRunAt;
303
+ return;
304
+ }
305
+ if (schedule.type === ScheduleTypes.INTERVAL) {
306
+ job.nextRunAt = lastRunAt + schedule.everyMs;
307
+ return;
308
+ }
309
+ if (schedule.type === ScheduleTypes.CRON) {
310
+ job.nextRunAt = this.getNextCronTime(schedule.expression, lastRunAt, schedule.timezone);
311
+ return;
312
+ }
313
+ if (schedule.type === ScheduleTypes.TIMESTAMP) {
314
+ job.nextRunAt = null;
315
+ return;
316
+ }
317
+ const remaining = job.remainingTimestamps ?? [];
318
+ while (remaining.length > 0 && remaining[0] <= lastRunAt) {
319
+ remaining.shift();
320
+ }
321
+ job.remainingTimestamps = remaining;
322
+ job.nextRunAt = remaining.length > 0 ? remaining[0] : null;
323
+ }
324
+ parseTimestamp(value) {
325
+ if (typeof value === 'number') {
326
+ return Number.isFinite(value) ? value : null;
327
+ }
328
+ if (typeof value === 'string') {
329
+ const parsed = Date.parse(value);
330
+ return Number.isNaN(parsed) ? null : parsed;
331
+ }
332
+ return null;
333
+ }
334
+ getNextCronTime(expression, fromMs, timezone) {
335
+ const fields = expression.trim().split(/\s+/);
336
+ if (fields.length < 5 || fields.length > 6) {
337
+ return null;
338
+ }
339
+ const hasSeconds = fields.length === 6;
340
+ const [secField, minField, hourField, dayField, monthField, dowField] = hasSeconds
341
+ ? fields
342
+ : ['0', ...fields];
343
+ const seconds = this.parseCronField(secField, 0, 59, true);
344
+ const minutes = this.parseCronField(minField, 0, 59, true);
345
+ const hours = this.parseCronField(hourField, 0, 23, true);
346
+ const days = this.parseCronField(dayField, 1, 31, true);
347
+ const months = this.parseCronField(monthField, 1, 12, true);
348
+ const dows = this.parseCronField(dowField, 0, 6, true);
349
+ if (!seconds || !minutes || !hours || !days || !months || !dows) {
350
+ return null;
351
+ }
352
+ const maxIterations = 366 * 24 * 60 * 60;
353
+ let candidate = new Date(fromMs + 1000);
354
+ for (let i = 0; i < maxIterations; i += 1) {
355
+ const candidateDate = candidate;
356
+ const parts = this.getCronDateParts(candidateDate, timezone);
357
+ if (!parts) {
358
+ return null;
359
+ }
360
+ const match = seconds.has(parts.second) &&
361
+ minutes.has(parts.minute) &&
362
+ hours.has(parts.hour) &&
363
+ days.has(parts.day) &&
364
+ months.has(parts.month) &&
365
+ dows.has(parts.dow);
366
+ if (match) {
367
+ return candidateDate.getTime();
368
+ }
369
+ candidate = new Date(candidateDate.getTime() + 1000);
370
+ }
371
+ return null;
372
+ }
373
+ parseCronField(field, min, max, strict) {
374
+ const values = new Set();
375
+ const segments = field.split(',');
376
+ let hasValidSegment = false;
377
+ segments.forEach((segment) => {
378
+ const trimmed = segment.trim();
379
+ if (!trimmed)
380
+ return;
381
+ const [rangePart, stepPart] = trimmed.split('/');
382
+ if (stepPart !== undefined && !this.isValidInteger(stepPart)) {
383
+ return;
384
+ }
385
+ const step = stepPart ? Number(stepPart) : 1;
386
+ const safeStep = Number.isFinite(step) && step > 0 ? step : null;
387
+ if (!safeStep) {
388
+ return;
389
+ }
390
+ let rangeStart;
391
+ let rangeEnd;
392
+ if (rangePart === '*') {
393
+ rangeStart = min;
394
+ rangeEnd = max;
395
+ }
396
+ else if (rangePart.includes('-')) {
397
+ const [startRaw, endRaw] = rangePart.split('-');
398
+ if (!this.isValidInteger(startRaw) || !this.isValidInteger(endRaw)) {
399
+ return;
400
+ }
401
+ rangeStart = Number(startRaw);
402
+ rangeEnd = Number(endRaw);
403
+ }
404
+ else {
405
+ if (!this.isValidInteger(rangePart)) {
406
+ return;
407
+ }
408
+ rangeStart = Number(rangePart);
409
+ rangeEnd = rangeStart;
410
+ }
411
+ if (rangeStart < min || rangeEnd > max || rangeStart > rangeEnd) {
412
+ return;
413
+ }
414
+ for (let value = rangeStart; value <= rangeEnd; value += safeStep) {
415
+ values.add(value);
416
+ }
417
+ hasValidSegment = true;
418
+ });
419
+ if (values.size === 0) {
420
+ if (strict) {
421
+ return null;
422
+ }
423
+ for (let value = min; value <= max; value += 1) {
424
+ values.add(value);
425
+ }
426
+ }
427
+ return values;
428
+ }
429
+ isValidInteger(value) {
430
+ return /^\d+$/.test(value);
431
+ }
432
+ getCronDateParts(date, timezone) {
433
+ if (!timezone) {
434
+ return {
435
+ second: date.getSeconds(),
436
+ minute: date.getMinutes(),
437
+ hour: date.getHours(),
438
+ day: date.getDate(),
439
+ month: date.getMonth() + 1,
440
+ dow: date.getDay()
441
+ };
442
+ }
443
+ try {
444
+ const formatter = new Intl.DateTimeFormat('en-US', {
445
+ timeZone: timezone,
446
+ hour12: false,
447
+ weekday: 'short',
448
+ year: 'numeric',
449
+ month: '2-digit',
450
+ day: '2-digit',
451
+ hour: '2-digit',
452
+ minute: '2-digit',
453
+ second: '2-digit'
454
+ });
455
+ const parts = formatter.formatToParts(date);
456
+ const partMap = new Map(parts.map((part) => [part.type, part.value]));
457
+ const month = Number(partMap.get('month'));
458
+ const day = Number(partMap.get('day'));
459
+ const hour = Number(partMap.get('hour'));
460
+ const minute = Number(partMap.get('minute'));
461
+ const second = Number(partMap.get('second'));
462
+ const weekday = partMap.get('weekday');
463
+ if (Number.isNaN(month) ||
464
+ Number.isNaN(day) ||
465
+ Number.isNaN(hour) ||
466
+ Number.isNaN(minute) ||
467
+ Number.isNaN(second) ||
468
+ !weekday) {
469
+ return null;
470
+ }
471
+ const weekdayIndex = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(weekday);
472
+ if (weekdayIndex === -1) {
473
+ return null;
474
+ }
475
+ return {
476
+ second,
477
+ minute,
478
+ hour,
479
+ day,
480
+ month,
481
+ dow: weekdayIndex
482
+ };
483
+ }
484
+ catch {
485
+ return null;
486
+ }
487
+ }
488
+ createId(prefix) {
489
+ return `${prefix}-${this.generateUuid()}-${Date.now()}`;
490
+ }
491
+ generateUuid() {
492
+ if (typeof globalThis.crypto?.randomUUID === 'function') {
493
+ return globalThis.crypto.randomUUID();
494
+ }
495
+ const bytes = new Uint8Array(16);
496
+ if (typeof globalThis.crypto?.getRandomValues === 'function') {
497
+ globalThis.crypto.getRandomValues(bytes);
498
+ }
499
+ else {
500
+ for (let i = 0; i < bytes.length; i += 1) {
501
+ bytes[i] = Math.floor(Math.random() * 256);
502
+ }
503
+ }
504
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
505
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
506
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));
507
+ return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex
508
+ .slice(6, 8)
509
+ .join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;
510
+ }
511
+ async persistStateIfEnabled() {
512
+ if (!this.config.persistence.enabled || !this.config.persistence.saveState) {
513
+ return;
514
+ }
515
+ const debounceMs = this.config.persistenceDebounceMs ?? 0;
516
+ if (debounceMs > 0) {
517
+ if (this.persistTimer) {
518
+ this.persistQueued = true;
519
+ return;
520
+ }
521
+ this.persistQueued = false;
522
+ this.persistTimer = setTimeout(async () => {
523
+ this.persistTimer = null;
524
+ const state = this.getState();
525
+ await this.config.persistence.saveState?.(state);
526
+ if (this.persistQueued) {
527
+ void this.persistStateIfEnabled();
528
+ }
529
+ }, debounceMs);
530
+ return;
531
+ }
532
+ const state = this.getState();
533
+ await this.config.persistence.saveState(state);
534
+ }
535
+ }
536
+ //# sourceMappingURL=stable-scheduler.js.map