@zudojs/scheduler 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +6 -3
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.js +1 -1
  4. package/dist/scheduler/clock/index.d.ts +0 -1
  5. package/dist/scheduler/clock/index.js +0 -1
  6. package/dist/scheduler/clock/schedulerClock.type.d.ts +0 -1
  7. package/dist/scheduler/clock/schedulerClock.type.js +0 -1
  8. package/dist/scheduler/constants/index.d.ts +0 -1
  9. package/dist/scheduler/constants/index.js +0 -1
  10. package/dist/scheduler/constants/schedulerConstants.core.d.ts +0 -1
  11. package/dist/scheduler/constants/schedulerConstants.core.js +0 -1
  12. package/dist/scheduler/duration/duration.parser.d.ts +0 -1
  13. package/dist/scheduler/duration/duration.parser.js +0 -1
  14. package/dist/scheduler/duration/index.d.ts +0 -1
  15. package/dist/scheduler/duration/index.js +0 -1
  16. package/dist/scheduler/errors/index.d.ts +0 -1
  17. package/dist/scheduler/errors/index.js +0 -1
  18. package/dist/scheduler/errors/scheduler.errors.d.ts +0 -1
  19. package/dist/scheduler/errors/scheduler.errors.js +0 -1
  20. package/dist/scheduler/executor/index.d.ts +0 -1
  21. package/dist/scheduler/executor/index.js +0 -1
  22. package/dist/scheduler/executor/jobExecutor.core.d.ts +0 -1
  23. package/dist/scheduler/executor/jobExecutor.core.js +0 -1
  24. package/dist/scheduler/job/index.d.ts +0 -1
  25. package/dist/scheduler/job/index.js +0 -1
  26. package/dist/scheduler/job/jobContext.type.d.ts +0 -1
  27. package/dist/scheduler/job/jobContext.type.js +0 -1
  28. package/dist/scheduler/job/jobDefinition.type.d.ts +0 -1
  29. package/dist/scheduler/job/jobDefinition.type.js +0 -1
  30. package/dist/scheduler/job/jobHandler.type.d.ts +0 -1
  31. package/dist/scheduler/job/jobHandler.type.js +0 -1
  32. package/dist/scheduler/job/jobOptions.type.d.ts +0 -1
  33. package/dist/scheduler/job/jobOptions.type.js +0 -1
  34. package/dist/scheduler/priorityQueue/index.d.ts +0 -1
  35. package/dist/scheduler/priorityQueue/index.js +0 -1
  36. package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.d.ts +0 -1
  37. package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.js +0 -1
  38. package/dist/scheduler/registry/index.d.ts +0 -1
  39. package/dist/scheduler/registry/index.js +0 -1
  40. package/dist/scheduler/registry/jobRegistry.core.d.ts +0 -1
  41. package/dist/scheduler/registry/jobRegistry.core.js +0 -1
  42. package/dist/scheduler/schedule/index.d.ts +0 -1
  43. package/dist/scheduler/schedule/index.js +0 -1
  44. package/dist/scheduler/schedule/schedule.type.d.ts +0 -1
  45. package/dist/scheduler/schedule/schedule.type.js +0 -1
  46. package/dist/scheduler/schedule/scheduleOptions.type.d.ts +5 -2
  47. package/dist/scheduler/schedule/scheduleOptions.type.js +0 -1
  48. package/dist/scheduler/scheduleHandle/index.d.ts +0 -1
  49. package/dist/scheduler/scheduleHandle/index.js +0 -1
  50. package/dist/scheduler/scheduleHandle/scheduleHandle.type.d.ts +0 -1
  51. package/dist/scheduler/scheduleHandle/scheduleHandle.type.js +0 -1
  52. package/dist/scheduler/scheduler.core.d.ts +0 -1
  53. package/dist/scheduler/scheduler.core.js +0 -1
  54. package/dist/scheduler/trigger/cron.parser.d.ts +7 -4
  55. package/dist/scheduler/trigger/cron.parser.js +45 -70
  56. package/dist/scheduler/trigger/cron.zone.d.ts +50 -0
  57. package/dist/scheduler/trigger/cron.zone.js +118 -0
  58. package/dist/scheduler/trigger/index.d.ts +2 -1
  59. package/dist/scheduler/trigger/index.js +1 -1
  60. package/dist/scheduler/trigger/schedulerTrigger.core.d.ts +7 -6
  61. package/dist/scheduler/trigger/schedulerTrigger.core.js +15 -15
  62. package/dist/scheduler/trigger/trigger.type.d.ts +0 -1
  63. package/dist/scheduler/trigger/trigger.type.js +0 -1
  64. package/dist/scheduler/types/index.d.ts +0 -1
  65. package/dist/scheduler/types/index.js +0 -1
  66. package/dist/scheduler/types/schedulerTypes.core.d.ts +0 -1
  67. package/dist/scheduler/types/schedulerTypes.core.js +0 -1
  68. package/package.json +4 -4
package/README.md CHANGED
@@ -82,9 +82,12 @@ day-of-week`) with ranges, lists, steps and three-letter names, plus the
82
82
  `@daily`/`@hourly`/`@weekly`/`@monthly`/`@yearly` macros. An invalid expression
83
83
  throws where the schedule is declared, not at fire time.
84
84
 
85
- Only `timezone: "UTC"` and the system local zone are supported. Any other zone
86
- is rejected rather than silently ignored — honouring an arbitrary IANA zone
87
- needs real zone data this package does not carry.
85
+ `timezone` accepts any IANA zone name (`"Africa/Lagos"`, `"America/New_York"`,
86
+ `"Asia/Kolkata"`), `"UTC"`, or nothing for the host's local zone. Zones are
87
+ resolved through Node's `Intl` data, so daylight-saving transitions are
88
+ honoured: `0 9 * * *` in `America/New_York` fires at 09:00 wall-clock on both
89
+ sides of a DST change. An unknown zone name throws `InvalidScheduleError` where
90
+ the schedule is declared rather than being silently ignored.
88
91
 
89
92
  ## Controlling a schedule
90
93
 
package/dist/index.d.ts CHANGED
@@ -34,6 +34,8 @@ export type { ScheduleHandleBinding } from "./scheduler/scheduleHandle/index.js"
34
34
  export { DateTrigger, DelayTrigger, IntervalTrigger, CronTrigger, } from "./scheduler/trigger/index.js";
35
35
  export { parseCron, nextCronDate } from "./scheduler/trigger/index.js";
36
36
  export type { ParsedCron } from "./scheduler/trigger/index.js";
37
+ export { LOCAL_ZONE, UTC_ZONE, createIntlZone, resolveCronZone, } from "./scheduler/trigger/index.js";
38
+ export type { CronWallClock, CronZone } from "./scheduler/trigger/index.js";
37
39
  export { SystemClock, createSystemClock } from "./scheduler/clock/index.js";
38
40
  export type { Clock } from "./scheduler/clock/index.js";
39
41
  export { JobRegistry } from "./scheduler/registry/index.js";
@@ -42,4 +44,3 @@ export { PriorityQueue } from "./scheduler/priorityQueue/index.js";
42
44
  export { parseDuration } from "./scheduler/duration/index.js";
43
45
  export { Scheduler } from "./scheduler/scheduler.core.js";
44
46
  export type { SchedulerOptions, SchedulerErrorEvent, } from "./scheduler/scheduler.core.js";
45
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ export { ScheduleHandleImpl } from "./scheduler/scheduleHandle/index.js";
35
35
  // Trigger
36
36
  export { DateTrigger, DelayTrigger, IntervalTrigger, CronTrigger, } from "./scheduler/trigger/index.js";
37
37
  export { parseCron, nextCronDate } from "./scheduler/trigger/index.js";
38
+ export { LOCAL_ZONE, UTC_ZONE, createIntlZone, resolveCronZone, } from "./scheduler/trigger/index.js";
38
39
  // Clock
39
40
  export { SystemClock, createSystemClock } from "./scheduler/clock/index.js";
40
41
  // Registry
@@ -47,4 +48,3 @@ export { PriorityQueue } from "./scheduler/priorityQueue/index.js";
47
48
  export { parseDuration } from "./scheduler/duration/index.js";
48
49
  // Scheduler
49
50
  export { Scheduler } from "./scheduler/scheduler.core.js";
50
- //# sourceMappingURL=index.js.map
@@ -1,3 +1,2 @@
1
1
  export type { Clock } from "./schedulerClock.type.js";
2
2
  export { SystemClock, createSystemClock } from "./schedulerClock.type.js";
3
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { SystemClock, createSystemClock } from "./schedulerClock.type.js";
2
- //# sourceMappingURL=index.js.map
@@ -18,4 +18,3 @@ export declare class SystemClock implements Clock {
18
18
  * Creates a new system clock.
19
19
  */
20
20
  export declare function createSystemClock(): SystemClock;
21
- //# sourceMappingURL=schedulerClock.type.d.ts.map
@@ -15,4 +15,3 @@ export class SystemClock {
15
15
  export function createSystemClock() {
16
16
  return new SystemClock();
17
17
  }
18
- //# sourceMappingURL=schedulerClock.type.js.map
@@ -1,2 +1 @@
1
1
  export { DEFAULT_JOB_TIMEOUT, DEFAULT_MAX_CONCURRENCY, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY, MAX_TIMER_DELAY, MAX_JOBS, MAX_SCHEDULES, MAX_EXECUTION_HISTORY, DEFAULT_MISFIRE_POLICY, DEFAULT_OVERLAP_POLICY, } from "./schedulerConstants.core.js";
2
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { DEFAULT_JOB_TIMEOUT, DEFAULT_MAX_CONCURRENCY, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY, MAX_TIMER_DELAY, MAX_JOBS, MAX_SCHEDULES, MAX_EXECUTION_HISTORY, DEFAULT_MISFIRE_POLICY, DEFAULT_OVERLAP_POLICY, } from "./schedulerConstants.core.js";
2
- //# sourceMappingURL=index.js.map
@@ -45,4 +45,3 @@ export declare const DEFAULT_MISFIRE_POLICY: MisfirePolicy;
45
45
  * Default overlap policy.
46
46
  */
47
47
  export declare const DEFAULT_OVERLAP_POLICY: OverlapPolicy;
48
- //# sourceMappingURL=schedulerConstants.core.d.ts.map
@@ -43,4 +43,3 @@ export const DEFAULT_MISFIRE_POLICY = "run-once";
43
43
  * Default overlap policy.
44
44
  */
45
45
  export const DEFAULT_OVERLAP_POLICY = "allow";
46
- //# sourceMappingURL=schedulerConstants.core.js.map
@@ -16,4 +16,3 @@
16
16
  * @throws {InvalidDurationError} when the string is malformed or out of range.
17
17
  */
18
18
  export declare function parseDuration(duration: string): number;
19
- //# sourceMappingURL=duration.parser.d.ts.map
@@ -66,4 +66,3 @@ export function parseDuration(duration) {
66
66
  }
67
67
  return total;
68
68
  }
69
- //# sourceMappingURL=duration.parser.js.map
@@ -1,2 +1 @@
1
1
  export { parseDuration } from "./duration.parser.js";
2
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { parseDuration } from "./duration.parser.js";
2
- //# sourceMappingURL=index.js.map
@@ -1,3 +1,2 @@
1
1
  export { SchedulerError, SchedulerAlreadyStartedError, SchedulerStoppedError, SchedulerJobNotFoundError, SchedulerJobAlreadyExistsError, InvalidJobError, InvalidScheduleError, CronParseError, InvalidDurationError, SchedulerJobExecutionError, SchedulerJobTimeoutError, SchedulerJobCancelledError, createSchedulerError, isSchedulerError, } from "./scheduler.errors.js";
2
2
  export type { SchedulerErrorOptions } from "./scheduler.errors.js";
3
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { SchedulerError, SchedulerAlreadyStartedError, SchedulerStoppedError, SchedulerJobNotFoundError, SchedulerJobAlreadyExistsError, InvalidJobError, InvalidScheduleError, CronParseError, InvalidDurationError, SchedulerJobExecutionError, SchedulerJobTimeoutError, SchedulerJobCancelledError, createSchedulerError, isSchedulerError, } from "./scheduler.errors.js";
2
- //# sourceMappingURL=index.js.map
@@ -13,4 +13,3 @@
13
13
  */
14
14
  export { SchedulerError, SchedulerAlreadyStartedError, SchedulerStoppedError, SchedulerJobNotFoundError, SchedulerJobAlreadyExistsError, InvalidJobError, InvalidScheduleError, CronParseError, InvalidDurationError, SchedulerJobExecutionError, SchedulerJobTimeoutError, SchedulerJobCancelledError, createSchedulerError, isSchedulerError, } from "@zudojs/errors";
15
15
  export type { SchedulerErrorOptions } from "@zudojs/errors";
16
- //# sourceMappingURL=scheduler.errors.d.ts.map
@@ -12,4 +12,3 @@
12
12
  * @zudojs/errors for a durable scheduler built on top.
13
13
  */
14
14
  export { SchedulerError, SchedulerAlreadyStartedError, SchedulerStoppedError, SchedulerJobNotFoundError, SchedulerJobAlreadyExistsError, InvalidJobError, InvalidScheduleError, CronParseError, InvalidDurationError, SchedulerJobExecutionError, SchedulerJobTimeoutError, SchedulerJobCancelledError, createSchedulerError, isSchedulerError, } from "@zudojs/errors";
15
- //# sourceMappingURL=scheduler.errors.js.map
@@ -1,2 +1 @@
1
1
  export { JobExecutor, retryDelay } from "./jobExecutor.core.js";
2
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { JobExecutor, retryDelay } from "./jobExecutor.core.js";
2
- //# sourceMappingURL=index.js.map
@@ -40,4 +40,3 @@ export declare class JobExecutor {
40
40
  * package was written; none of it was ever applied.
41
41
  */
42
42
  export declare function retryDelay(policy: RetryPolicy | undefined, attempt: number): number;
43
- //# sourceMappingURL=jobExecutor.core.d.ts.map
@@ -176,4 +176,3 @@ function sleep(ms, signal) {
176
176
  signal.addEventListener("abort", finish, { once: true });
177
177
  });
178
178
  }
179
- //# sourceMappingURL=jobExecutor.core.js.map
@@ -4,4 +4,3 @@ export { createJobContext } from "./jobContext.type.js";
4
4
  export type { JobDefinition } from "./jobDefinition.type.js";
5
5
  export { createJobDefinition } from "./jobDefinition.type.js";
6
6
  export type { JobOptions, RetryPolicy, RetryStrategy, } from "./jobOptions.type.js";
7
- //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,2 @@
1
1
  export { createJobContext } from "./jobContext.type.js";
2
2
  export { createJobDefinition } from "./jobDefinition.type.js";
3
- //# sourceMappingURL=index.js.map
@@ -23,4 +23,3 @@ export interface JobContext<T = unknown> {
23
23
  * Creates a job context.
24
24
  */
25
25
  export declare function createJobContext<T = unknown>(jobId: string, executionId: string, scheduledAt: Date, startedAt: Date, attempt: number, data: T, signal: AbortSignal): JobContext<T>;
26
- //# sourceMappingURL=jobContext.type.d.ts.map
@@ -13,4 +13,3 @@ export function createJobContext(jobId, executionId, scheduledAt, startedAt, att
13
13
  signal,
14
14
  });
15
15
  }
16
- //# sourceMappingURL=jobContext.type.js.map
@@ -13,4 +13,3 @@ export interface JobDefinition {
13
13
  * Creates a job definition.
14
14
  */
15
15
  export declare function createJobDefinition(id: string, name: string, handler: JobHandler, options?: JobOptions): JobDefinition;
16
- //# sourceMappingURL=jobDefinition.type.d.ts.map
@@ -9,4 +9,3 @@ export function createJobDefinition(id, name, handler, options = {}) {
9
9
  options: Object.freeze(options),
10
10
  });
11
11
  }
12
- //# sourceMappingURL=jobDefinition.type.js.map
@@ -3,4 +3,3 @@ import type { JobContext } from "./jobContext.type.js";
3
3
  * Handler for a scheduled job.
4
4
  */
5
5
  export type JobHandler<T = unknown> = (context: JobContext<T>) => Promise<void> | void;
6
- //# sourceMappingURL=jobHandler.type.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=jobHandler.type.js.map
@@ -33,4 +33,3 @@ export interface RetryPolicy {
33
33
  * Retry strategy.
34
34
  */
35
35
  export type RetryStrategy = "fixed" | "linear" | "exponential";
36
- //# sourceMappingURL=jobOptions.type.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=jobOptions.type.js.map
@@ -1,2 +1 @@
1
1
  export { PriorityQueue } from "./schedulerPriorityQueue.core.js";
2
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { PriorityQueue } from "./schedulerPriorityQueue.core.js";
2
- //# sourceMappingURL=index.js.map
@@ -55,4 +55,3 @@ export declare class PriorityQueue {
55
55
  */
56
56
  private sinkDown;
57
57
  }
58
- //# sourceMappingURL=schedulerPriorityQueue.core.d.ts.map
@@ -145,4 +145,3 @@ export class PriorityQueue {
145
145
  }
146
146
  }
147
147
  }
148
- //# sourceMappingURL=schedulerPriorityQueue.core.js.map
@@ -1,2 +1 @@
1
1
  export { JobRegistry } from "./jobRegistry.core.js";
2
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { JobRegistry } from "./jobRegistry.core.js";
2
- //# sourceMappingURL=index.js.map
@@ -37,4 +37,3 @@ export declare class JobRegistry {
37
37
  */
38
38
  clear(): void;
39
39
  }
40
- //# sourceMappingURL=jobRegistry.core.d.ts.map
@@ -63,4 +63,3 @@ export class JobRegistry {
63
63
  this.jobs.clear();
64
64
  }
65
65
  }
66
- //# sourceMappingURL=jobRegistry.core.js.map
@@ -1,4 +1,3 @@
1
1
  export type { Schedule, ScheduleType } from "./schedule.type.js";
2
2
  export { createSchedule } from "./schedule.type.js";
3
3
  export type { ScheduleOptions } from "./scheduleOptions.type.js";
4
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { createSchedule } from "./schedule.type.js";
2
- //# sourceMappingURL=index.js.map
@@ -26,4 +26,3 @@ export interface Schedule {
26
26
  * @returns A frozen, active schedule.
27
27
  */
28
28
  export declare function createSchedule(id: string, jobId: string, type: ScheduleType, nextRunAt: Date, options?: ScheduleOptions, expression?: string): Schedule;
29
- //# sourceMappingURL=schedule.type.d.ts.map
@@ -20,4 +20,3 @@ export function createSchedule(id, jobId, type, nextRunAt, options = {}, express
20
20
  ...(expression === undefined ? {} : { expression }),
21
21
  });
22
22
  }
23
- //# sourceMappingURL=schedule.type.js.map
@@ -3,7 +3,11 @@ import type { MisfirePolicy, OverlapPolicy } from "../types/schedulerTypes.core.
3
3
  * Options for a schedule.
4
4
  */
5
5
  export interface ScheduleOptions {
6
- /** IANA timezone name. Only "UTC" is currently honoured by CronTrigger. */
6
+ /**
7
+ * Zone a cron expression is read in: an IANA name such as `"Africa/Lagos"`
8
+ * (DST honoured through `Intl`), `"UTC"`, or omitted for the host's local
9
+ * zone. Ignored by non-cron schedules.
10
+ */
7
11
  readonly timezone?: string;
8
12
  /**
9
13
  * How to handle a fire time that has already passed (default: "run-once").
@@ -30,4 +34,3 @@ export interface ScheduleOptions {
30
34
  /** Payload handed to the job handler as `context.data`. */
31
35
  readonly data?: unknown;
32
36
  }
33
- //# sourceMappingURL=scheduleOptions.type.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=scheduleOptions.type.js.map
@@ -1,4 +1,3 @@
1
1
  export type { ScheduleHandle } from "./scheduleHandle.type.js";
2
2
  export { ScheduleHandleImpl } from "./scheduleHandle.type.js";
3
3
  export type { ScheduleHandleBinding } from "./scheduleHandle.type.js";
4
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1 @@
1
1
  export { ScheduleHandleImpl } from "./scheduleHandle.type.js";
2
- //# sourceMappingURL=index.js.map
@@ -46,4 +46,3 @@ export declare class ScheduleHandleImpl implements ScheduleHandle {
46
46
  cancel(): Promise<void>;
47
47
  nextRun(): Date | undefined;
48
48
  }
49
- //# sourceMappingURL=scheduleHandle.type.d.ts.map
@@ -42,4 +42,3 @@ export class ScheduleHandleImpl {
42
42
  return this.binding?.getNextRun();
43
43
  }
44
44
  }
45
- //# sourceMappingURL=scheduleHandle.type.js.map
@@ -217,4 +217,3 @@ export declare class Scheduler {
217
217
  */
218
218
  private calculateDelay;
219
219
  }
220
- //# sourceMappingURL=scheduler.core.d.ts.map
@@ -665,4 +665,3 @@ function statusFor(error) {
665
665
  return "cancelled";
666
666
  return "failed";
667
667
  }
668
- //# sourceMappingURL=scheduler.core.js.map
@@ -8,6 +8,7 @@
8
8
  * comma-separated list of any of those. Month and day-of-week also accept the
9
9
  * usual three-letter names. A leading `@yearly`-style macro is expanded first.
10
10
  */
11
+ import type { CronZone } from "./cron.zone.js";
11
12
  /** A parsed cron expression: the permitted values for each field. */
12
13
  export interface ParsedCron {
13
14
  readonly minute: ReadonlySet<number>;
@@ -33,12 +34,14 @@ export declare function parseCron(expression: string): ParsedCron;
33
34
  *
34
35
  * Search is minute-by-minute with whole-field skips, bounded by
35
36
  * {@link MAX_SEARCH_YEARS} so an unsatisfiable expression (30 February) fails
36
- * rather than looping.
37
+ * rather than looping. Every skip is computed in wall-clock terms of the
38
+ * schedule's zone, so a DST transition neither skips nor repeats an hour of
39
+ * scheduling, and a half-hour zone (Asia/Kolkata) still visits every minute.
37
40
  *
38
41
  * @param parsed - The parsed expression.
39
42
  * @param after - The instant to search forward from (exclusive).
40
- * @param utc - Interpret the fields in UTC rather than local time.
43
+ * @param zone - The zone the fields are read in: a {@link CronZone}, or for
44
+ * compatibility `true` for UTC and `false` (the default) for local time.
41
45
  * @returns The next fire time, or null when none exists within the horizon.
42
46
  */
43
- export declare function nextCronDate(parsed: ParsedCron, after: Date, utc?: boolean): Date | null;
44
- //# sourceMappingURL=cron.parser.d.ts.map
47
+ export declare function nextCronDate(parsed: ParsedCron, after: Date, zone?: CronZone | boolean): Date | null;
@@ -9,6 +9,7 @@
9
9
  * usual three-letter names. A leading `@yearly`-style macro is expanded first.
10
10
  */
11
11
  import { CronParseError } from "../errors/scheduler.errors.js";
12
+ import { LOCAL_ZONE, UTC_ZONE } from "./cron.zone.js";
12
13
  /** Inclusive bounds for each cron field. */
13
14
  const FIELD_BOUNDS = [
14
15
  { name: "minute", min: 0, max: 59 },
@@ -173,50 +174,44 @@ function resolveValue(raw, bounds, expression, rawDayOfWeek = false) {
173
174
  *
174
175
  * Search is minute-by-minute with whole-field skips, bounded by
175
176
  * {@link MAX_SEARCH_YEARS} so an unsatisfiable expression (30 February) fails
176
- * rather than looping.
177
+ * rather than looping. Every skip is computed in wall-clock terms of the
178
+ * schedule's zone, so a DST transition neither skips nor repeats an hour of
179
+ * scheduling, and a half-hour zone (Asia/Kolkata) still visits every minute.
177
180
  *
178
181
  * @param parsed - The parsed expression.
179
182
  * @param after - The instant to search forward from (exclusive).
180
- * @param utc - Interpret the fields in UTC rather than local time.
183
+ * @param zone - The zone the fields are read in: a {@link CronZone}, or for
184
+ * compatibility `true` for UTC and `false` (the default) for local time.
181
185
  * @returns The next fire time, or null when none exists within the horizon.
182
186
  */
183
- export function nextCronDate(parsed, after, utc = false) {
184
- const get = {
185
- minute: (d) => (utc ? d.getUTCMinutes() : d.getMinutes()),
186
- hour: (d) => (utc ? d.getUTCHours() : d.getHours()),
187
- date: (d) => (utc ? d.getUTCDate() : d.getDate()),
188
- month: (d) => (utc ? d.getUTCMonth() : d.getMonth()) + 1,
189
- day: (d) => (utc ? d.getUTCDay() : d.getDay()),
190
- year: (d) => (utc ? d.getUTCFullYear() : d.getFullYear()),
191
- };
187
+ export function nextCronDate(parsed, after, zone = false) {
188
+ const clock = typeof zone === "boolean" ? (zone ? UTC_ZONE : LOCAL_ZONE) : zone;
192
189
  // Start at the next whole minute after `after`, with seconds cleared.
193
- const candidate = new Date(after.getTime());
194
- if (utc)
195
- candidate.setUTCSeconds(0, 0);
196
- else
197
- candidate.setSeconds(0, 0);
198
- candidate.setTime(candidate.getTime() + 60_000);
199
- const limitYear = get.year(after) + MAX_SEARCH_YEARS;
200
- while (get.year(candidate) <= limitYear) {
201
- if (!parsed.month.has(get.month(candidate))) {
202
- advanceMonth(candidate, utc);
190
+ let candidate = Math.floor(after.getTime() / 60_000) * 60_000 + 60_000;
191
+ const limitYear = clock.fields(after.getTime()).year + MAX_SEARCH_YEARS;
192
+ for (;;) {
193
+ const now = clock.fields(candidate);
194
+ if (now.year > limitYear) {
195
+ return null;
196
+ }
197
+ if (!parsed.month.has(now.month)) {
198
+ candidate = advanceTo(candidate, clock.instantOf(now.year, now.month + 1, 1, 0, 0));
203
199
  continue;
204
200
  }
205
- if (!matchesDay(parsed, candidate, get.date, get.day)) {
206
- advanceDay(candidate, utc);
201
+ if (!matchesDay(parsed, now)) {
202
+ candidate = advanceTo(candidate, clock.instantOf(now.year, now.month, now.date + 1, 0, 0));
207
203
  continue;
208
204
  }
209
- if (!parsed.hour.has(get.hour(candidate))) {
210
- advanceHour(candidate, utc);
205
+ if (!parsed.hour.has(now.hour)) {
206
+ candidate = advanceHour(candidate, now.minute, clock);
211
207
  continue;
212
208
  }
213
- if (!parsed.minute.has(get.minute(candidate))) {
214
- candidate.setTime(candidate.getTime() + 60_000);
209
+ if (!parsed.minute.has(now.minute)) {
210
+ candidate += 60_000;
215
211
  continue;
216
212
  }
217
- return candidate;
213
+ return new Date(candidate);
218
214
  }
219
- return null;
220
215
  }
221
216
  /**
222
217
  * Applies the cron day-matching rule.
@@ -226,9 +221,9 @@ export function nextCronDate(parsed, after, utc = false) {
226
221
  * implementation preserves, because `0 0 1,15 * mon` is widely used to mean
227
222
  * "the 1st, the 15th, and every Monday".
228
223
  */
229
- function matchesDay(parsed, candidate, getDate, getDay) {
230
- const domMatches = parsed.dayOfMonth.has(getDate(candidate));
231
- const dowMatches = parsed.dayOfWeek.has(getDay(candidate));
224
+ function matchesDay(parsed, now) {
225
+ const domMatches = parsed.dayOfMonth.has(now.date);
226
+ const dowMatches = parsed.dayOfWeek.has(now.day);
232
227
  if (parsed.dayOfMonthUnrestricted && parsed.dayOfWeekUnrestricted) {
233
228
  return true;
234
229
  }
@@ -240,48 +235,28 @@ function matchesDay(parsed, candidate, getDate, getDay) {
240
235
  }
241
236
  return domMatches || dowMatches;
242
237
  }
243
- /** Moves to 00:00 on the first day of the next month. */
244
- function advanceMonth(candidate, utc) {
245
- if (utc) {
246
- candidate.setUTCDate(1);
247
- candidate.setUTCHours(0, 0, 0, 0);
248
- candidate.setUTCMonth(candidate.getUTCMonth() + 1);
249
- }
250
- else {
251
- candidate.setDate(1);
252
- candidate.setHours(0, 0, 0, 0);
253
- candidate.setMonth(candidate.getMonth() + 1);
254
- }
255
- }
256
- /** Moves to 00:00 on the next day. */
257
- function advanceDay(candidate, utc) {
258
- if (utc) {
259
- candidate.setUTCHours(0, 0, 0, 0);
260
- candidate.setUTCDate(candidate.getUTCDate() + 1);
261
- }
262
- else {
263
- candidate.setHours(0, 0, 0, 0);
264
- candidate.setDate(candidate.getDate() + 1);
265
- }
238
+ /**
239
+ * Moves the search to `target`, never backwards.
240
+ *
241
+ * A wall-clock midnight that does not exist in the zone (a DST gap at
242
+ * 00:00) can resolve to an instant before the candidate; falling back to
243
+ * the next minute keeps the search advancing until the gap is behind it.
244
+ */
245
+ function advanceTo(candidate, target) {
246
+ return Number.isFinite(target) && target > candidate
247
+ ? target
248
+ : candidate + 60_000;
266
249
  }
267
250
  /**
268
251
  * Moves to the top of the next hour.
269
252
  *
270
253
  * Uses wall-clock arithmetic rather than adding an hour of milliseconds, so a
271
- * DST transition does not skip or repeat an hour of scheduling. In UTC mode
272
- * the top of the hour is taken in UTC: a host on a half-hour offset
273
- * (Asia/Kolkata) would otherwise land every skip on :30 UTC and never visit
274
- * minutes 0-29 of a restricted hour.
254
+ * DST transition does not skip or repeat an hour of scheduling. The top of the
255
+ * hour is taken in the schedule's zone: a host on a half-hour offset
256
+ * (Asia/Kolkata) evaluating a UTC schedule would otherwise land every skip
257
+ * on :30 UTC and never visit minutes 0-29 of a restricted hour.
275
258
  */
276
- function advanceHour(candidate, utc) {
277
- if (utc) {
278
- candidate.setUTCMinutes(0, 0, 0);
279
- candidate.setTime(candidate.getTime() + 3_600_000);
280
- candidate.setUTCMinutes(0, 0, 0);
281
- return;
282
- }
283
- candidate.setMinutes(0, 0, 0);
284
- candidate.setTime(candidate.getTime() + 3_600_000);
285
- candidate.setMinutes(0, 0, 0);
259
+ function advanceHour(candidate, minute, clock) {
260
+ const nextHour = candidate - minute * 60_000 + 3_600_000;
261
+ return nextHour - clock.fields(nextHour).minute * 60_000;
286
262
  }
287
- //# sourceMappingURL=cron.parser.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Wall-clock arithmetic for cron evaluation in a named time zone.
3
+ *
4
+ * A cron expression is written in wall-clock terms (09:00 on Mondays), so
5
+ * computing its next fire time needs two operations: reading the wall-clock
6
+ * fields of an instant in the schedule's zone, and turning a wall-clock time
7
+ * back into an instant. Node ships full IANA zone data through `Intl`, which
8
+ * is what makes `Africa/Lagos` or `America/New_York` work without a
9
+ * dependency.
10
+ *
11
+ * @module scheduler/trigger/cron.zone
12
+ */
13
+ /** The wall-clock fields cron matches against. Month is 1-12, day 0-6. */
14
+ export interface CronWallClock {
15
+ readonly year: number;
16
+ readonly month: number;
17
+ readonly date: number;
18
+ readonly hour: number;
19
+ readonly minute: number;
20
+ readonly day: number;
21
+ }
22
+ /** A time zone as the cron evaluator sees it. */
23
+ export interface CronZone {
24
+ /** Canonical IANA name, `"UTC"`, or `undefined` for the host's local zone. */
25
+ readonly name: string | undefined;
26
+ /** Wall-clock fields of `instant` in this zone. */
27
+ fields(instant: number): CronWallClock;
28
+ /**
29
+ * The instant at which the given wall-clock time occurs in this zone.
30
+ * A time inside a DST gap resolves to the instant the clocks jumped to; a
31
+ * repeated time resolves to its first occurrence. Overflowing fields
32
+ * (month 13, date 32) roll over like `Date.UTC`.
33
+ */
34
+ instantOf(year: number, month: number, date: number, hour: number, minute: number): number;
35
+ }
36
+ /** The host's local zone, as `Date`'s local accessors report it. */
37
+ export declare const LOCAL_ZONE: CronZone;
38
+ /** Coordinated Universal Time. */
39
+ export declare const UTC_ZONE: CronZone;
40
+ /** Builds a zone backed by `Intl` zone data; `undefined` for an unknown name. */
41
+ export declare function createIntlZone(timezone: string): CronZone | undefined;
42
+ /**
43
+ * Resolves the zone a cron schedule is evaluated in.
44
+ *
45
+ * `undefined` selects the host's local zone, `"UTC"` (any case) or
46
+ * `"Etc/UTC"` selects UTC, and any other value is looked up as an IANA name.
47
+ *
48
+ * @returns The zone, or `undefined` when the name is not a known zone.
49
+ */
50
+ export declare function resolveCronZone(timezone?: string): CronZone | undefined;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Wall-clock arithmetic for cron evaluation in a named time zone.
3
+ *
4
+ * A cron expression is written in wall-clock terms (09:00 on Mondays), so
5
+ * computing its next fire time needs two operations: reading the wall-clock
6
+ * fields of an instant in the schedule's zone, and turning a wall-clock time
7
+ * back into an instant. Node ships full IANA zone data through `Intl`, which
8
+ * is what makes `Africa/Lagos` or `America/New_York` work without a
9
+ * dependency.
10
+ *
11
+ * @module scheduler/trigger/cron.zone
12
+ */
13
+ /** Freezes a zone while keeping its methods contextually typed. */
14
+ function defineZone(zone) {
15
+ return Object.freeze(zone);
16
+ }
17
+ /** The host's local zone, as `Date`'s local accessors report it. */
18
+ export const LOCAL_ZONE = defineZone({
19
+ name: undefined,
20
+ fields(instant) {
21
+ const d = new Date(instant);
22
+ return {
23
+ year: d.getFullYear(),
24
+ month: d.getMonth() + 1,
25
+ date: d.getDate(),
26
+ hour: d.getHours(),
27
+ minute: d.getMinutes(),
28
+ day: d.getDay(),
29
+ };
30
+ },
31
+ instantOf(year, month, date, hour, minute) {
32
+ return new Date(year, month - 1, date, hour, minute).getTime();
33
+ },
34
+ });
35
+ /** Coordinated Universal Time. */
36
+ export const UTC_ZONE = defineZone({
37
+ name: "UTC",
38
+ fields(instant) {
39
+ const d = new Date(instant);
40
+ return {
41
+ year: d.getUTCFullYear(),
42
+ month: d.getUTCMonth() + 1,
43
+ date: d.getUTCDate(),
44
+ hour: d.getUTCHours(),
45
+ minute: d.getUTCMinutes(),
46
+ day: d.getUTCDay(),
47
+ };
48
+ },
49
+ instantOf(year, month, date, hour, minute) {
50
+ return Date.UTC(year, month - 1, date, hour, minute);
51
+ },
52
+ });
53
+ const PART_TYPES = ["year", "month", "day", "hour", "minute"];
54
+ /** Builds a zone backed by `Intl` zone data; `undefined` for an unknown name. */
55
+ export function createIntlZone(timezone) {
56
+ let formatter;
57
+ try {
58
+ formatter = new Intl.DateTimeFormat("en-US", {
59
+ timeZone: timezone,
60
+ hourCycle: "h23",
61
+ year: "numeric",
62
+ month: "numeric",
63
+ day: "numeric",
64
+ hour: "numeric",
65
+ minute: "numeric",
66
+ });
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ const name = formatter.resolvedOptions().timeZone;
72
+ const wallAsUtc = (instant) => {
73
+ const values = {};
74
+ for (const part of formatter.formatToParts(new Date(instant))) {
75
+ if (PART_TYPES.includes(part.type)) {
76
+ values[part.type] = Number(part.value);
77
+ }
78
+ }
79
+ return Date.UTC(values.year ?? 1970, (values.month ?? 1) - 1, values.day ?? 1, (values.hour ?? 0) % 24, values.minute ?? 0);
80
+ };
81
+ const offsetAt = (instant) => wallAsUtc(instant) - instant;
82
+ return defineZone({
83
+ name,
84
+ fields(instant) {
85
+ const wall = new Date(wallAsUtc(instant));
86
+ return {
87
+ year: wall.getUTCFullYear(),
88
+ month: wall.getUTCMonth() + 1,
89
+ date: wall.getUTCDate(),
90
+ hour: wall.getUTCHours(),
91
+ minute: wall.getUTCMinutes(),
92
+ day: wall.getUTCDay(),
93
+ };
94
+ },
95
+ instantOf(year, month, date, hour, minute) {
96
+ const wall = Date.UTC(year, month - 1, date, hour, minute);
97
+ const guess = wall - offsetAt(wall);
98
+ return wall - offsetAt(guess);
99
+ },
100
+ });
101
+ }
102
+ /**
103
+ * Resolves the zone a cron schedule is evaluated in.
104
+ *
105
+ * `undefined` selects the host's local zone, `"UTC"` (any case) or
106
+ * `"Etc/UTC"` selects UTC, and any other value is looked up as an IANA name.
107
+ *
108
+ * @returns The zone, or `undefined` when the name is not a known zone.
109
+ */
110
+ export function resolveCronZone(timezone) {
111
+ if (timezone === undefined) {
112
+ return LOCAL_ZONE;
113
+ }
114
+ if (/^utc$/i.test(timezone) || timezone === "Etc/UTC") {
115
+ return UTC_ZONE;
116
+ }
117
+ return createIntlZone(timezone);
118
+ }
@@ -2,4 +2,5 @@ export type { Trigger } from "./trigger.type.js";
2
2
  export { DateTrigger, DelayTrigger, IntervalTrigger, CronTrigger, } from "./schedulerTrigger.core.js";
3
3
  export { parseCron, nextCronDate } from "./cron.parser.js";
4
4
  export type { ParsedCron } from "./cron.parser.js";
5
- //# sourceMappingURL=index.d.ts.map
5
+ export { LOCAL_ZONE, UTC_ZONE, createIntlZone, resolveCronZone, } from "./cron.zone.js";
6
+ export type { CronWallClock, CronZone } from "./cron.zone.js";
@@ -1,3 +1,3 @@
1
1
  export { DateTrigger, DelayTrigger, IntervalTrigger, CronTrigger, } from "./schedulerTrigger.core.js";
2
2
  export { parseCron, nextCronDate } from "./cron.parser.js";
3
- //# sourceMappingURL=index.js.map
3
+ export { LOCAL_ZONE, UTC_ZONE, createIntlZone, resolveCronZone, } from "./cron.zone.js";
@@ -40,17 +40,18 @@ export declare class IntervalTrigger implements Trigger {
40
40
  export declare class CronTrigger implements Trigger {
41
41
  private readonly expression;
42
42
  private readonly parsed;
43
- private readonly utc;
43
+ private readonly zone;
44
44
  /**
45
45
  * @param expression - A five-field cron expression, or a macro such as `@daily`.
46
- * @param timezone - Pass `"UTC"` to interpret the fields in UTC. Any other
47
- * value is rejected: honouring an arbitrary IANA zone needs real zone data,
48
- * and silently ignoring the argument is what made the previous
49
- * implementation's `timezone` parameter meaningless.
46
+ * @param timezone - The zone the fields are read in: an IANA name such as
47
+ * `"Africa/Lagos"` or `"America/New_York"` (resolved through `Intl`, so
48
+ * DST is honoured), `"UTC"`, or omitted for the host's local zone. An
49
+ * unknown name is rejected rather than silently ignored.
50
50
  */
51
51
  constructor(expression: string, timezone?: string);
52
52
  /** The expression this trigger was built from. */
53
53
  get source(): string;
54
+ /** The canonical zone the fields are read in; `undefined` for local time. */
55
+ get timezone(): string | undefined;
54
56
  next(after: Date): Date | null;
55
57
  }
56
- //# sourceMappingURL=schedulerTrigger.core.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { parseCron, nextCronDate } from "./cron.parser.js";
2
+ import { resolveCronZone } from "./cron.zone.js";
2
3
  import { InvalidScheduleError } from "../errors/scheduler.errors.js";
3
4
  /**
4
5
  * Trigger that fires once at a specific date.
@@ -65,33 +66,33 @@ export class IntervalTrigger {
65
66
  export class CronTrigger {
66
67
  expression;
67
68
  parsed;
68
- utc;
69
+ zone;
69
70
  /**
70
71
  * @param expression - A five-field cron expression, or a macro such as `@daily`.
71
- * @param timezone - Pass `"UTC"` to interpret the fields in UTC. Any other
72
- * value is rejected: honouring an arbitrary IANA zone needs real zone data,
73
- * and silently ignoring the argument is what made the previous
74
- * implementation's `timezone` parameter meaningless.
72
+ * @param timezone - The zone the fields are read in: an IANA name such as
73
+ * `"Africa/Lagos"` or `"America/New_York"` (resolved through `Intl`, so
74
+ * DST is honoured), `"UTC"`, or omitted for the host's local zone. An
75
+ * unknown name is rejected rather than silently ignored.
75
76
  */
76
77
  constructor(expression, timezone) {
77
78
  this.expression = expression;
78
79
  this.parsed = parseCron(expression);
79
- if (timezone === undefined) {
80
- this.utc = false;
81
- }
82
- else if (/^utc$/i.test(timezone) || timezone === "Etc/UTC") {
83
- this.utc = true;
84
- }
85
- else {
86
- throw new InvalidScheduleError(`CronTrigger supports only "UTC" or the system local zone, got "${timezone}"`, expression);
80
+ const zone = resolveCronZone(timezone);
81
+ if (zone === undefined) {
82
+ throw new InvalidScheduleError(`CronTrigger received an unknown time zone "${timezone}"; use an IANA name such as "Africa/Lagos", or "UTC"`, expression);
87
83
  }
84
+ this.zone = zone;
88
85
  }
89
86
  /** The expression this trigger was built from. */
90
87
  get source() {
91
88
  return this.expression;
92
89
  }
90
+ /** The canonical zone the fields are read in; `undefined` for local time. */
91
+ get timezone() {
92
+ return this.zone.name;
93
+ }
93
94
  next(after) {
94
- return nextCronDate(this.parsed, after, this.utc);
95
+ return nextCronDate(this.parsed, after, this.zone);
95
96
  }
96
97
  }
97
98
  /**
@@ -114,4 +115,3 @@ function assertUsableInterval(ms, what) {
114
115
  throw new InvalidScheduleError(`${what} interval ${ms}ms exceeds the representable date range`, what);
115
116
  }
116
117
  }
117
- //# sourceMappingURL=schedulerTrigger.core.js.map
@@ -9,4 +9,3 @@
9
9
  export interface Trigger {
10
10
  next(after: Date): Date | null;
11
11
  }
12
- //# sourceMappingURL=trigger.type.d.ts.map
@@ -4,4 +4,3 @@
4
4
  * Trigger types for the scheduler package.
5
5
  */
6
6
  export {};
7
- //# sourceMappingURL=trigger.type.js.map
@@ -10,4 +10,3 @@ export type { JobOptions, RetryPolicy, RetryStrategy, JobDefinition, JobHandler,
10
10
  export type { Schedule, ScheduleOptions } from "../schedule/index.js";
11
11
  export type { ScheduleHandle } from "../scheduleHandle/index.js";
12
12
  export type { Trigger } from "../trigger/index.js";
13
- //# sourceMappingURL=index.d.ts.map
@@ -6,4 +6,3 @@
6
6
  * type a consumer imports is the one the implementation actually uses.
7
7
  */
8
8
  export {};
9
- //# sourceMappingURL=index.js.map
@@ -63,4 +63,3 @@ export interface JobExecutionResult {
63
63
  readonly success: boolean;
64
64
  readonly error?: unknown;
65
65
  }
66
- //# sourceMappingURL=schedulerTypes.core.d.ts.map
@@ -10,4 +10,3 @@
10
10
  * `scheduler.every(..., { overlap: "skip" })` did not typecheck.
11
11
  */
12
12
  export {};
13
- //# sourceMappingURL=schedulerTypes.core.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/scheduler",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Scheduled task and job infrastructure with cron-like scheduling, persistence, and worker management.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -27,9 +27,9 @@
27
27
  "node": ">=24.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "@zudojs/errors": "1.3.2",
31
- "@zudojs/constants": "1.1.4",
32
- "@zudojs/types": "1.2.0"
30
+ "@zudojs/errors": "1.4.0",
31
+ "@zudojs/constants": "1.2.0",
32
+ "@zudojs/types": "1.3.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "typescript": "7.0.2",