@pristine-ts/local-scheduling 4.0.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.
Files changed (150) hide show
  1. package/LICENSE +201 -0
  2. package/dist/lib/cjs/builders/builders.js +18 -0
  3. package/dist/lib/cjs/builders/builders.js.map +1 -0
  4. package/dist/lib/cjs/builders/cron-expression.builder.js +159 -0
  5. package/dist/lib/cjs/builders/cron-expression.builder.js.map +1 -0
  6. package/dist/lib/cjs/enums/day-of-week.enum.js +19 -0
  7. package/dist/lib/cjs/enums/day-of-week.enum.js.map +1 -0
  8. package/dist/lib/cjs/enums/enums.js +19 -0
  9. package/dist/lib/cjs/enums/enums.js.map +1 -0
  10. package/dist/lib/cjs/enums/month.enum.js +23 -0
  11. package/dist/lib/cjs/enums/month.enum.js.map +1 -0
  12. package/dist/lib/cjs/errors/errors.js +20 -0
  13. package/dist/lib/cjs/errors/errors.js.map +1 -0
  14. package/dist/lib/cjs/errors/invalid-cron-expression.error.js +30 -0
  15. package/dist/lib/cjs/errors/invalid-cron-expression.error.js.map +1 -0
  16. package/dist/lib/cjs/errors/schedule-already-exists.error.js +25 -0
  17. package/dist/lib/cjs/errors/schedule-already-exists.error.js.map +1 -0
  18. package/dist/lib/cjs/errors/schedule-not-found.error.js +22 -0
  19. package/dist/lib/cjs/errors/schedule-not-found.error.js.map +1 -0
  20. package/dist/lib/cjs/interfaces/interfaces.js +22 -0
  21. package/dist/lib/cjs/interfaces/interfaces.js.map +1 -0
  22. package/dist/lib/cjs/interfaces/local-scheduler.interface.js +3 -0
  23. package/dist/lib/cjs/interfaces/local-scheduler.interface.js.map +1 -0
  24. package/dist/lib/cjs/interfaces/schedulable.interface.js +3 -0
  25. package/dist/lib/cjs/interfaces/schedulable.interface.js.map +1 -0
  26. package/dist/lib/cjs/interfaces/schedule-descriptor.interface.js +3 -0
  27. package/dist/lib/cjs/interfaces/schedule-descriptor.interface.js.map +1 -0
  28. package/dist/lib/cjs/interfaces/schedule-options.interface.js +3 -0
  29. package/dist/lib/cjs/interfaces/schedule-options.interface.js.map +1 -0
  30. package/dist/lib/cjs/interfaces/schedule-state.interface.js +3 -0
  31. package/dist/lib/cjs/interfaces/schedule-state.interface.js.map +1 -0
  32. package/dist/lib/cjs/interfaces/schedule.interface.js +3 -0
  33. package/dist/lib/cjs/interfaces/schedule.interface.js.map +1 -0
  34. package/dist/lib/cjs/local-scheduling.module.js +85 -0
  35. package/dist/lib/cjs/local-scheduling.module.js.map +1 -0
  36. package/dist/lib/cjs/local-scheduling.module.keyname.js +5 -0
  37. package/dist/lib/cjs/local-scheduling.module.keyname.js.map +1 -0
  38. package/dist/lib/cjs/managers/local-scheduler.manager.js +361 -0
  39. package/dist/lib/cjs/managers/local-scheduler.manager.js.map +1 -0
  40. package/dist/lib/cjs/managers/managers.js +18 -0
  41. package/dist/lib/cjs/managers/managers.js.map +1 -0
  42. package/dist/lib/cjs/models/cron-expression.model.js +304 -0
  43. package/dist/lib/cjs/models/cron-expression.model.js.map +1 -0
  44. package/dist/lib/cjs/models/models.js +18 -0
  45. package/dist/lib/cjs/models/models.js.map +1 -0
  46. package/dist/lib/cjs/schedules/cron.schedule.js +34 -0
  47. package/dist/lib/cjs/schedules/cron.schedule.js.map +1 -0
  48. package/dist/lib/cjs/schedules/date.schedule.js +27 -0
  49. package/dist/lib/cjs/schedules/date.schedule.js.map +1 -0
  50. package/dist/lib/cjs/schedules/schedules.js +19 -0
  51. package/dist/lib/cjs/schedules/schedules.js.map +1 -0
  52. package/dist/lib/cjs/servers/local-scheduler.runtime-server.js +69 -0
  53. package/dist/lib/cjs/servers/local-scheduler.runtime-server.js.map +1 -0
  54. package/dist/lib/cjs/servers/servers.js +18 -0
  55. package/dist/lib/cjs/servers/servers.js.map +1 -0
  56. package/dist/lib/cjs/types/scheduled-task-function.type.js +3 -0
  57. package/dist/lib/cjs/types/scheduled-task-function.type.js.map +1 -0
  58. package/dist/lib/cjs/types/types.js +18 -0
  59. package/dist/lib/cjs/types/types.js.map +1 -0
  60. package/dist/lib/esm/builders/builders.js +2 -0
  61. package/dist/lib/esm/builders/builders.js.map +1 -0
  62. package/dist/lib/esm/builders/cron-expression.builder.js +155 -0
  63. package/dist/lib/esm/builders/cron-expression.builder.js.map +1 -0
  64. package/dist/lib/esm/enums/day-of-week.enum.js +16 -0
  65. package/dist/lib/esm/enums/day-of-week.enum.js.map +1 -0
  66. package/dist/lib/esm/enums/enums.js +3 -0
  67. package/dist/lib/esm/enums/enums.js.map +1 -0
  68. package/dist/lib/esm/enums/month.enum.js +20 -0
  69. package/dist/lib/esm/enums/month.enum.js.map +1 -0
  70. package/dist/lib/esm/errors/errors.js +4 -0
  71. package/dist/lib/esm/errors/errors.js.map +1 -0
  72. package/dist/lib/esm/errors/invalid-cron-expression.error.js +26 -0
  73. package/dist/lib/esm/errors/invalid-cron-expression.error.js.map +1 -0
  74. package/dist/lib/esm/errors/schedule-already-exists.error.js +21 -0
  75. package/dist/lib/esm/errors/schedule-already-exists.error.js.map +1 -0
  76. package/dist/lib/esm/errors/schedule-not-found.error.js +18 -0
  77. package/dist/lib/esm/errors/schedule-not-found.error.js.map +1 -0
  78. package/dist/lib/esm/interfaces/interfaces.js +6 -0
  79. package/dist/lib/esm/interfaces/interfaces.js.map +1 -0
  80. package/dist/lib/esm/interfaces/local-scheduler.interface.js +2 -0
  81. package/dist/lib/esm/interfaces/local-scheduler.interface.js.map +1 -0
  82. package/dist/lib/esm/interfaces/schedulable.interface.js +2 -0
  83. package/dist/lib/esm/interfaces/schedulable.interface.js.map +1 -0
  84. package/dist/lib/esm/interfaces/schedule-descriptor.interface.js +2 -0
  85. package/dist/lib/esm/interfaces/schedule-descriptor.interface.js.map +1 -0
  86. package/dist/lib/esm/interfaces/schedule-options.interface.js +2 -0
  87. package/dist/lib/esm/interfaces/schedule-options.interface.js.map +1 -0
  88. package/dist/lib/esm/interfaces/schedule-state.interface.js +2 -0
  89. package/dist/lib/esm/interfaces/schedule-state.interface.js.map +1 -0
  90. package/dist/lib/esm/interfaces/schedule.interface.js +2 -0
  91. package/dist/lib/esm/interfaces/schedule.interface.js.map +1 -0
  92. package/dist/lib/esm/local-scheduling.module.js +68 -0
  93. package/dist/lib/esm/local-scheduling.module.js.map +1 -0
  94. package/dist/lib/esm/local-scheduling.module.keyname.js +2 -0
  95. package/dist/lib/esm/local-scheduling.module.keyname.js.map +1 -0
  96. package/dist/lib/esm/managers/local-scheduler.manager.js +358 -0
  97. package/dist/lib/esm/managers/local-scheduler.manager.js.map +1 -0
  98. package/dist/lib/esm/managers/managers.js +2 -0
  99. package/dist/lib/esm/managers/managers.js.map +1 -0
  100. package/dist/lib/esm/models/cron-expression.model.js +300 -0
  101. package/dist/lib/esm/models/cron-expression.model.js.map +1 -0
  102. package/dist/lib/esm/models/models.js +2 -0
  103. package/dist/lib/esm/models/models.js.map +1 -0
  104. package/dist/lib/esm/schedules/cron.schedule.js +30 -0
  105. package/dist/lib/esm/schedules/cron.schedule.js.map +1 -0
  106. package/dist/lib/esm/schedules/date.schedule.js +23 -0
  107. package/dist/lib/esm/schedules/date.schedule.js.map +1 -0
  108. package/dist/lib/esm/schedules/schedules.js +3 -0
  109. package/dist/lib/esm/schedules/schedules.js.map +1 -0
  110. package/dist/lib/esm/servers/local-scheduler.runtime-server.js +66 -0
  111. package/dist/lib/esm/servers/local-scheduler.runtime-server.js.map +1 -0
  112. package/dist/lib/esm/servers/servers.js +2 -0
  113. package/dist/lib/esm/servers/servers.js.map +1 -0
  114. package/dist/lib/esm/types/scheduled-task-function.type.js +2 -0
  115. package/dist/lib/esm/types/scheduled-task-function.type.js.map +1 -0
  116. package/dist/lib/esm/types/types.js +2 -0
  117. package/dist/lib/esm/types/types.js.map +1 -0
  118. package/dist/lib/tsconfig.cjs.tsbuildinfo +1 -0
  119. package/dist/lib/tsconfig.tsbuildinfo +1 -0
  120. package/dist/types/builders/builders.d.ts +1 -0
  121. package/dist/types/builders/cron-expression.builder.d.ts +69 -0
  122. package/dist/types/enums/day-of-week.enum.d.ts +14 -0
  123. package/dist/types/enums/enums.d.ts +2 -0
  124. package/dist/types/enums/month.enum.d.ts +18 -0
  125. package/dist/types/errors/errors.d.ts +3 -0
  126. package/dist/types/errors/invalid-cron-expression.error.d.ts +20 -0
  127. package/dist/types/errors/schedule-already-exists.error.d.ts +15 -0
  128. package/dist/types/errors/schedule-not-found.error.d.ts +12 -0
  129. package/dist/types/interfaces/interfaces.d.ts +5 -0
  130. package/dist/types/interfaces/local-scheduler.interface.d.ts +57 -0
  131. package/dist/types/interfaces/schedulable.interface.d.ts +43 -0
  132. package/dist/types/interfaces/schedule-descriptor.interface.d.ts +22 -0
  133. package/dist/types/interfaces/schedule-options.interface.d.ts +37 -0
  134. package/dist/types/interfaces/schedule-state.interface.d.ts +24 -0
  135. package/dist/types/interfaces/schedule.interface.d.ts +20 -0
  136. package/dist/types/local-scheduling.module.d.ts +30 -0
  137. package/dist/types/local-scheduling.module.keyname.d.ts +1 -0
  138. package/dist/types/managers/local-scheduler.manager.d.ts +110 -0
  139. package/dist/types/managers/managers.d.ts +1 -0
  140. package/dist/types/models/cron-expression.model.d.ts +120 -0
  141. package/dist/types/models/models.d.ts +1 -0
  142. package/dist/types/schedules/cron.schedule.d.ts +22 -0
  143. package/dist/types/schedules/date.schedule.d.ts +17 -0
  144. package/dist/types/schedules/schedules.d.ts +2 -0
  145. package/dist/types/servers/local-scheduler.runtime-server.d.ts +24 -0
  146. package/dist/types/servers/servers.d.ts +1 -0
  147. package/dist/types/types/scheduled-task-function.type.d.ts +10 -0
  148. package/dist/types/types/types.d.ts +1 -0
  149. package/package.json +68 -0
  150. package/readme.md +307 -0
@@ -0,0 +1,20 @@
1
+ import { BadRequestError } from "@pristine-ts/common";
2
+ /**
3
+ * Thrown when a cron expression cannot be parsed or is semantically invalid.
4
+ *
5
+ * It extends {@link BadRequestError} so that, if it propagates to an HTTP boundary (for
6
+ * example a controller that validates a user-submitted schedule), the framework's HTTP
7
+ * responder surfaces it as a **400 Bad Request** automatically. The `message` is written
8
+ * to be safe and precise enough to return verbatim to the caller, and the offending
9
+ * `expression` (plus optional field context) is carried in the error `details` bag.
10
+ */
11
+ export declare class InvalidCronExpressionError extends BadRequestError {
12
+ readonly expression: string;
13
+ readonly field?: string | undefined;
14
+ /**
15
+ * @param message A precise, caller-safe description of what is wrong with the expression.
16
+ * @param expression The raw cron expression that failed to parse.
17
+ * @param field The name of the offending field (e.g. `"minute"`), when known.
18
+ */
19
+ constructor(message: string, expression: string, field?: string | undefined);
20
+ }
@@ -0,0 +1,15 @@
1
+ import { ConflictError } from "@pristine-ts/common";
2
+ /**
3
+ * Thrown by `schedule(id, ...)` when the id is already registered. Registration is
4
+ * intentionally not idempotent so that a duplicate id — almost always a bug in the
5
+ * consumer's bootstrap — surfaces loudly instead of silently double-arming a timer. To
6
+ * replace an existing schedule, call `unschedule(id)` first, or `reschedule(id, ...)` to
7
+ * only change its expression.
8
+ *
9
+ * It extends {@link ConflictError} so it surfaces as a **409 Conflict** if it reaches an
10
+ * HTTP boundary.
11
+ */
12
+ export declare class ScheduleAlreadyExistsError extends ConflictError {
13
+ readonly id: string;
14
+ constructor(id: string);
15
+ }
@@ -0,0 +1,12 @@
1
+ import { NotFoundError } from "@pristine-ts/common";
2
+ /**
3
+ * Thrown when an operation references a schedule id that is not registered (for example
4
+ * `reschedule(id, ...)` or `getNextExecutionDate(id)` on an unknown id).
5
+ *
6
+ * It extends {@link NotFoundError} so it surfaces as a **404 Not Found** if it reaches an
7
+ * HTTP boundary — convenient when a controller mutates schedules by id.
8
+ */
9
+ export declare class ScheduleNotFoundError extends NotFoundError {
10
+ readonly id: string;
11
+ constructor(id: string);
12
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./local-scheduler.interface";
2
+ export * from "./schedulable.interface";
3
+ export * from "./schedule.interface";
4
+ export * from "./schedule-descriptor.interface";
5
+ export * from "./schedule-options.interface";
@@ -0,0 +1,57 @@
1
+ import { ScheduleInterface } from "./schedule.interface";
2
+ import { ScheduledTaskFunction } from "../types/scheduled-task-function.type";
3
+ import { ScheduleOptions } from "./schedule-options.interface";
4
+ import { ScheduleDescriptor } from "./schedule-descriptor.interface";
5
+ /**
6
+ * The contract implemented by {@link LocalSchedulerManager}. Depend on this interface (via the
7
+ * `"LocalSchedulerInterface"` injection token) when you want the scheduler mockable.
8
+ *
9
+ * Schedules come from two places that share one id space: statically-tagged
10
+ * {@link SchedulableInterface} tasks (auto-registered on `start()`), and the dynamic methods
11
+ * below — ordinary runtime state, typically loaded from a database at bootstrap and mutated by
12
+ * HTTP controllers. Schedules may be registered before `start()`; they arm when it is called.
13
+ */
14
+ export interface LocalSchedulerInterface {
15
+ /** Whether the scheduler is currently started (armed). */
16
+ readonly isStarted: boolean;
17
+ /**
18
+ * Registers a task under `id`, run on `schedule` — a {@link ScheduleInterface} (e.g. a
19
+ * {@link CronSchedule} or {@link DateSchedule}) or a cron string as shorthand for a
20
+ * {@link CronSchedule}. If the scheduler is already started, the schedule arms immediately;
21
+ * otherwise it arms on `start()`.
22
+ * @throws {ScheduleAlreadyExistsError} if `id` is already registered.
23
+ * @throws {InvalidCronExpressionError} if `schedule` is an invalid cron string.
24
+ */
25
+ schedule(id: string, schedule: ScheduleInterface | string, task: ScheduledTaskFunction, options?: ScheduleOptions): void;
26
+ /**
27
+ * Removes the schedule registered under `id`, cancelling its timer. An in-flight invocation
28
+ * is left to finish.
29
+ * @returns `true` if a schedule was removed, `false` if none existed.
30
+ */
31
+ unschedule(id: string): boolean;
32
+ /**
33
+ * Replaces the schedule of an existing registration, re-arming it (options and task are
34
+ * preserved). Accepts a {@link ScheduleInterface} or a cron string.
35
+ * @throws {ScheduleNotFoundError} if `id` is not registered.
36
+ * @throws {InvalidCronExpressionError} if `schedule` is an invalid cron string.
37
+ */
38
+ reschedule(id: string, schedule: ScheduleInterface | string): void;
39
+ /** Whether a schedule is registered under `id`. */
40
+ has(id: string): boolean;
41
+ /** Returns a snapshot of every registered schedule. */
42
+ list(): ScheduleDescriptor[];
43
+ /**
44
+ * Returns the next armed execution date for `id` (or, if the scheduler is stopped, computes
45
+ * it from now).
46
+ * @throws {ScheduleNotFoundError} if `id` is not registered.
47
+ */
48
+ getNextExecutionDate(id: string): Date | undefined;
49
+ /** Arms every registered schedule. Idempotent — calling it while started is a no-op. */
50
+ start(): void;
51
+ /**
52
+ * Cancels every schedule's timer synchronously (no further fires occur after `stop()`
53
+ * returns) and resolves once any in-flight task invocations have settled, so it can be
54
+ * awaited for a graceful shutdown.
55
+ */
56
+ stop(): Promise<void>;
57
+ }
@@ -0,0 +1,43 @@
1
+ import { ScheduledTaskInterface } from "@pristine-ts/scheduling";
2
+ import { ScheduleInterface } from "./schedule.interface";
3
+ /**
4
+ * A {@link ScheduledTaskInterface} that additionally declares *when* it should run, so a
5
+ * scheduler driver can execute it automatically.
6
+ *
7
+ * A scheduled task is just work — `run()` — and, by design in `@pristine-ts/scheduling`,
8
+ * something external decides when it runs (AWS EventBridge, an HTTP trigger, a manual call).
9
+ * Implementing `SchedulableInterface` is how a task carries its own schedule so an in-process
10
+ * driver ({@link LocalSchedulerManager}) can own the clock and run it with no external
11
+ * trigger. Keeping this a separate interface — rather than adding a schedule to every
12
+ * scheduled task — makes it explicit that a driver must be present to honour the schedule.
13
+ *
14
+ * Tag the implementing class with `@tag(ServiceDefinitionTagEnum.Schedulable)` (and `@injectable()`); the local
15
+ * scheduler injects every tagged class and, on `start()`, arms one timer per schedule the
16
+ * task returns.
17
+ *
18
+ * ```ts
19
+ * @tag(ServiceDefinitionTagEnum.Schedulable)
20
+ * @injectable()
21
+ * export class NightlyCleanupTask implements SchedulableInterface {
22
+ * constructor(@inject("MyConfig") private readonly config: MyConfig) {}
23
+ *
24
+ * getSchedules(): ScheduleInterface[] {
25
+ * return [new CronSchedule(this.config.cleanupCron)];
26
+ * }
27
+ *
28
+ * async run(eventId?: string): Promise<void> {
29
+ * // ...
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ export interface SchedulableInterface extends ScheduledTaskInterface {
35
+ /**
36
+ * The schedule(s) on which this task runs. Called once when the scheduler starts, so the
37
+ * task may compute them from injected dependencies. Return more than one to run the same
38
+ * task on several schedules (for example a recurring cron plus a one-off date), mixing
39
+ * schedule kinds freely. Any error thrown here isolates this task — it is logged and does
40
+ * not prevent other tasks, or the scheduler, from starting.
41
+ */
42
+ getSchedules(): ScheduleInterface[];
43
+ }
@@ -0,0 +1,22 @@
1
+ import { ScheduleInterface } from "./schedule.interface";
2
+ /**
3
+ * A read-only snapshot of a registered schedule, returned by `LocalSchedulerManager.list()`.
4
+ * Useful for building a "next runs" view or an admin listing of active schedules.
5
+ */
6
+ export interface ScheduleDescriptor {
7
+ /** The id the schedule is registered under. */
8
+ id: string;
9
+ /**
10
+ * The schedule that determines when the task runs — a {@link CronSchedule},
11
+ * {@link DateSchedule}, or any other {@link ScheduleInterface}. Its `toString()` gives a
12
+ * human-readable form.
13
+ */
14
+ schedule: ScheduleInterface;
15
+ /** Whether an invocation of this schedule is currently in-flight. */
16
+ isRunning: boolean;
17
+ /**
18
+ * The next armed execution date, or `undefined` when the scheduler is stopped or the
19
+ * schedule has no further occurrence.
20
+ */
21
+ nextExecutionDate?: Date;
22
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Per-schedule policy options. Every field is optional and defaults to a conservative,
3
+ * safe behaviour, so `schedule(id, expression, task)` with no options is the common case.
4
+ *
5
+ * The type is intentionally an open interface so that future options (for example an IANA
6
+ * `timezone`) can be added without breaking existing call sites.
7
+ */
8
+ export interface ScheduleOptions {
9
+ /**
10
+ * What to do when an occurrence fires while the previous invocation of the same schedule
11
+ * is still running.
12
+ *
13
+ * - `false` (default): **skip** the overlapping occurrence (a warning is logged) and wait
14
+ * for the next one. A schedule therefore never runs concurrently with itself.
15
+ * - `true`: run the new invocation anyway, concurrently with the in-flight one.
16
+ */
17
+ allowOverlap?: boolean;
18
+ /**
19
+ * What to do when an occurrence is missed because the process could not fire on time
20
+ * (for example the host slept or the event loop stalled past the occurrence).
21
+ *
22
+ * - `false` (default): **skip** the missed occurrence and arm for the next future one.
23
+ * - `true`: fire **at most one** catch-up invocation on wake, then resume the normal
24
+ * schedule. Multiple missed occurrences still collapse into a single catch-up.
25
+ */
26
+ catchUp?: boolean;
27
+ /**
28
+ * How late (in milliseconds) a fire may be before it is considered a *missed* occurrence
29
+ * rather than normal jitter. A fire later than this past its scheduled time is subject to
30
+ * the {@link catchUp} policy; a fire within this tolerance always runs.
31
+ *
32
+ * Defaults to `5000` (5 seconds) — comfortably above event-loop jitter, far below any
33
+ * real sleep. Lower it for sub-minute schedules that must be punctual; raise it on a
34
+ * heavily loaded host to avoid skipping merely-jittery fires.
35
+ */
36
+ missedExecutionThresholdInMilliseconds?: number;
37
+ }
@@ -0,0 +1,24 @@
1
+ import { ScheduleInterface } from "./schedule.interface";
2
+ import { ScheduledTaskFunction } from "../types/scheduled-task-function.type";
3
+ /**
4
+ * The fully-resolved internal state of one registered schedule, held by
5
+ * `LocalSchedulerManager`. This is an implementation detail — it is intentionally **not**
6
+ * re-exported from the module's public barrel; {@link ScheduleDescriptor} is the public,
7
+ * read-only view of a schedule.
8
+ */
9
+ export interface ScheduleState {
10
+ id: string;
11
+ schedule: ScheduleInterface;
12
+ task: ScheduledTaskFunction;
13
+ allowOverlap: boolean;
14
+ catchUp: boolean;
15
+ missedExecutionThresholdInMilliseconds: number;
16
+ /** The currently-armed timer handle, if any. */
17
+ timeout?: ReturnType<typeof setTimeout>;
18
+ /** The epoch (ms) of the occurrence the timer is currently working towards. */
19
+ armedTargetEpoch?: number;
20
+ /** The next armed execution date (mirror of {@link ScheduleState.armedTargetEpoch} as a `Date`). */
21
+ nextExecutionDate?: Date;
22
+ /** Whether an invocation of this schedule is currently in-flight. */
23
+ running: boolean;
24
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A schedule tells the scheduler *when* a task should next run. It is deliberately
3
+ * polymorphic: a {@link CronSchedule} recurs on a cron expression, a {@link DateSchedule}
4
+ * fires once at a fixed instant, and future schedule kinds (a fixed interval, calendar
5
+ * rules, ...) can implement the same contract without any change to the scheduler.
6
+ *
7
+ * The scheduler drives a schedule purely through {@link getNextExecutionDate}: it asks for
8
+ * the next occurrence, arms a timer for it, and — after firing — asks again. A schedule that
9
+ * returns `undefined` has no further occurrence and is left unarmed (a one-shot that has
10
+ * already fired, or a cron that never matches).
11
+ */
12
+ export interface ScheduleInterface {
13
+ /**
14
+ * The next occurrence strictly after `from`, or `undefined` if this schedule has no
15
+ * further occurrence.
16
+ */
17
+ getNextExecutionDate(from: Date): Date | undefined;
18
+ /** A short, human-readable description of the schedule, used in logs and `list()`. */
19
+ toString(): string;
20
+ }
@@ -0,0 +1,30 @@
1
+ import { ModuleInterface } from "@pristine-ts/common";
2
+ export * from "./builders/builders";
3
+ export * from "./enums/enums";
4
+ export * from "./errors/errors";
5
+ export * from "./interfaces/interfaces";
6
+ export * from "./managers/managers";
7
+ export * from "./models/models";
8
+ export * from "./schedules/schedules";
9
+ export * from "./servers/servers";
10
+ export * from "./types/types";
11
+ export * from "./local-scheduling.module.keyname";
12
+ /**
13
+ * The local-scheduling module. Import it into your application module to make
14
+ * {@link LocalSchedulerManager} injectable and to have every {@link SchedulableInterface} you
15
+ * tag with `@tag(ServiceDefinitionTagEnum.Schedulable)` auto-registered when the scheduler
16
+ * starts.
17
+ *
18
+ * You do not have to start the scheduler yourself: {@link LocalSchedulerRuntimeServer} is
19
+ * tagged as a {@link RuntimeServerInterface}, so `pristine start` starts it (and stops it on
20
+ * shutdown, via `onShutdown` below) alongside the HTTP/gRPC servers. Resolve
21
+ * {@link LocalSchedulerManager} directly only when you need the dynamic API (adding schedules
22
+ * from a database, mutating them from controllers).
23
+ *
24
+ * It builds on `@pristine-ts/scheduling` — a {@link SchedulableInterface} is a
25
+ * {@link ScheduledTaskInterface} that also declares its schedule — and is otherwise
26
+ * platform-neutral: it depends only on core, logging, and scheduling, and registers no event
27
+ * mappers or handlers. It coexists with `@pristine-ts/aws-scheduling` and the other scheduling
28
+ * drivers — none of them interfere with the others.
29
+ */
30
+ export declare const LocalSchedulingModule: ModuleInterface;
@@ -0,0 +1 @@
1
+ export declare const LocalSchedulingModuleKeyname: string;
@@ -0,0 +1,110 @@
1
+ import { LogHandlerInterface } from "@pristine-ts/logging";
2
+ import { LocalSchedulerInterface } from "../interfaces/local-scheduler.interface";
3
+ import { SchedulableInterface } from "../interfaces/schedulable.interface";
4
+ import { ScheduleInterface } from "../interfaces/schedule.interface";
5
+ import { ScheduleOptions } from "../interfaces/schedule-options.interface";
6
+ import { ScheduleDescriptor } from "../interfaces/schedule-descriptor.interface";
7
+ import { ScheduledTaskFunction } from "../types/scheduled-task-function.type";
8
+ /**
9
+ * In-process scheduler driver for long-running Pristine applications (an always-up server, a
10
+ * worker). Unlike {@link SchedulerManager} from `@pristine-ts/scheduling` — which runs tagged
11
+ * tasks when an external trigger (e.g. AWS EventBridge) fires — this manager owns the clock
12
+ * itself and runs each task on its own {@link ScheduleInterface} (cron, one-off date, ...)
13
+ * from inside the Node process.
14
+ *
15
+ * It offers two registration paths that share one id space and one set of timers:
16
+ *
17
+ * - **Dynamic** — schedules registered, replaced, and removed at runtime by id, which suits a
18
+ * consumer that loads user-defined schedules from a database and mutates them from HTTP
19
+ * controllers.
20
+ * - **Static** — any class tagged {@link ServiceDefinitionTagEnum.Schedulable} that implements
21
+ * {@link SchedulableInterface} is discovered via `@injectAll` and **auto-registered on
22
+ * `start()`** from the schedules it declares.
23
+ *
24
+ * **Timer strategy:** each schedule owns a single chained `setTimeout` armed for its next
25
+ * occurrence, re-armed after every fire. The delay is always recomputed from the current time
26
+ * (never accumulated), so the schedule does not drift; delays beyond the `setTimeout` maximum
27
+ * are chunked. See {@link ScheduleOptions} for the overlap and missed-fire policies.
28
+ *
29
+ * It is tagged `"LocalSchedulerInterface"` and module-scoped to the local-scheduling module,
30
+ * so it can be injected either by class or by interface token. It is a `@singleton()`: there
31
+ * is exactly one instance per process, so the code that starts it at bootstrap and the HTTP
32
+ * controllers that mutate schedules at runtime all share the same set of timers.
33
+ */
34
+ export declare class LocalSchedulerManager implements LocalSchedulerInterface {
35
+ private readonly logHandler;
36
+ private readonly schedulables;
37
+ /**
38
+ * The largest delay a single `setTimeout` can represent: 2^31 - 1 milliseconds (~24.8
39
+ * days). Longer waits are chunked into successive timeouts.
40
+ */
41
+ private static readonly MAX_TIMEOUT_DELAY;
42
+ /** Default value for {@link ScheduleOptions.missedExecutionThresholdInMilliseconds}. */
43
+ private static readonly DEFAULT_MISSED_EXECUTION_THRESHOLD_MS;
44
+ private readonly states;
45
+ /** Every task invocation that has been started and not yet settled — awaited by `stop()`. */
46
+ private readonly inFlightInvocations;
47
+ private started;
48
+ /**
49
+ * @param logHandler The log handler used to report skips, task errors, and lifecycle.
50
+ * @param schedulables Every class tagged {@link ServiceDefinitionTagEnum.Schedulable}. Injected optionally, so the
51
+ * collection is simply empty when none is tagged. They are registered on `start()`.
52
+ * Defaults to `[]` so the manager can also be constructed directly (e.g. in tests)
53
+ * without wiring the collection.
54
+ */
55
+ constructor(logHandler: LogHandlerInterface, schedulables?: SchedulableInterface[]);
56
+ get isStarted(): boolean;
57
+ schedule(id: string, schedule: ScheduleInterface | string, task: ScheduledTaskFunction, options?: ScheduleOptions): void;
58
+ unschedule(id: string): boolean;
59
+ reschedule(id: string, schedule: ScheduleInterface | string): void;
60
+ has(id: string): boolean;
61
+ list(): ScheduleDescriptor[];
62
+ getNextExecutionDate(id: string): Date | undefined;
63
+ start(): void;
64
+ stop(): Promise<void>;
65
+ /**
66
+ * Registers each statically-tagged {@link SchedulableInterface} from the schedules it
67
+ * declares. Called once by `start()`. Each task's registration id derives from its class
68
+ * name (suffixed with the schedule index when a task declares more than one schedule). A
69
+ * task whose id is already registered (a collision with a dynamic schedule, or a repeat call
70
+ * after stop→start) is skipped, and a task whose `getSchedules()` throws is logged and
71
+ * skipped — neither prevents the others, or the scheduler, from starting.
72
+ */
73
+ private registerSchedulables;
74
+ private resolveSchedule;
75
+ /**
76
+ * Computes the next occurrence from now and arms the timer for it. Any existing timer is
77
+ * cleared first.
78
+ */
79
+ private arm;
80
+ /**
81
+ * Computes the next occurrence strictly after `baseEpoch` and arms the timer for it. If the
82
+ * schedule has no upcoming occurrence, it is left unarmed (a one-shot that already fired, or
83
+ * a cron that never matches).
84
+ */
85
+ private armFrom;
86
+ /**
87
+ * Arms a `setTimeout` for the currently-armed target, chunked so it never exceeds the
88
+ * platform maximum. Re-computing the delay from `Date.now()` on each (re-)arm keeps the
89
+ * schedule drift-free.
90
+ */
91
+ private setChunkedTimeout;
92
+ /**
93
+ * Fires when a (possibly chunked) timeout elapses. If the real target is still in the future
94
+ * — because the delay was chunked — it re-arms for the remaining time; otherwise it fires
95
+ * the occurrence.
96
+ */
97
+ private onTimer;
98
+ /**
99
+ * Decides whether the occurrence at `target` should run (applying the missed-fire and
100
+ * overlap policies), re-arms for the next occurrence, and executes the task if warranted.
101
+ */
102
+ private fire;
103
+ /**
104
+ * Runs the task, isolating any error so it can never break the scheduling loop, and tracks
105
+ * the invocation so `stop()` can await it. The task receives an `eventId` correlating logs
106
+ * to this specific occurrence.
107
+ */
108
+ private execute;
109
+ private clearTimer;
110
+ }
@@ -0,0 +1 @@
1
+ export * from "./local-scheduler.manager";
@@ -0,0 +1,120 @@
1
+ /**
2
+ * A parsed, validated standard cron expression, and the engine that computes its upcoming
3
+ * execution dates.
4
+ *
5
+ * **Supported syntax** (standard 5-field cron, plus an optional leading seconds field):
6
+ *
7
+ * ```
8
+ * ┌───────────── second (0-59) (optional 6th field; omit for classic 5-field cron)
9
+ * │ ┌─────────── minute (0-59)
10
+ * │ │ ┌───────── hour (0-23)
11
+ * │ │ │ ┌─────── day-of-month (1-31)
12
+ * │ │ │ │ ┌───── month (1-12 or JAN-DEC)
13
+ * │ │ │ │ │ ┌─── day-of-week (0-7 or SUN-SAT; 0 and 7 are both Sunday)
14
+ * │ │ │ │ │ │
15
+ * * * * * * *
16
+ * ```
17
+ *
18
+ * Each field accepts `*`, single values, ranges (`a-b`), lists (`a,b,c`), and steps
19
+ * (`* / n`, `a-b/n`, `a/n` meaning "from a to the maximum, every n"). Month and day-of-week
20
+ * values may be given as case-insensitive names. Ranges must be ascending; wrap-around
21
+ * ranges such as `FRI-SUN` are not supported — express them as a list (`FRI,SAT,SUN`).
22
+ *
23
+ * **Day-of-month / day-of-week OR rule:** when *both* the day-of-month and day-of-week
24
+ * fields are restricted (neither is `*`), the expression matches when *either* field
25
+ * matches — the classic Vixie-cron behaviour. For example `30 4 1,15 * 5` runs at 04:30 on
26
+ * the 1st and 15th of the month **and** every Friday. When at least one of the two is `*`,
27
+ * the fields combine with AND (the `*` one matching everything). A field is considered
28
+ * "restricted" whenever its text is not exactly `*` — so a starred field carrying a step
29
+ * (every-N) still counts as restricted.
30
+ *
31
+ * **Time semantics:** all computations use the host's **system local time** (via the
32
+ * standard `Date`). Across a spring-forward DST transition, a local time that does not
33
+ * exist is skipped; across a fall-back transition, a repeated local time fires once. A
34
+ * future revision may accept an IANA timezone without changing this class's method
35
+ * signatures (a timezone would be supplied as an additional, optional constructor option).
36
+ */
37
+ export declare class CronExpression {
38
+ /**
39
+ * Case-insensitive month names accepted in the month field, mapped to their `1-12` value.
40
+ */
41
+ private static readonly MONTH_NAMES;
42
+ /**
43
+ * Case-insensitive day-of-week names accepted in the day-of-week field, mapped to their
44
+ * `0-6` value where `0` is Sunday (the classic cron convention).
45
+ */
46
+ private static readonly DAY_OF_WEEK_NAMES;
47
+ /**
48
+ * The upper bound (in years past the `from` date) of the forward scan performed when
49
+ * computing the next execution. Eight years is the largest gap between two consecutive
50
+ * occurrences of Feb 29 (e.g. 2096 → 2104, because 2100 is not a leap year), so any
51
+ * expression that ever matches is guaranteed to match within this window. An expression
52
+ * that does not match within it is treated as never-matching (e.g. `0 0 30 2 *`).
53
+ */
54
+ private static readonly MAX_SCAN_YEARS;
55
+ /**
56
+ * A hard iteration ceiling on the forward scan, as a defensive backstop against an
57
+ * unforeseen non-advancing state. The scan advances by at least one calendar field per
58
+ * iteration and is really bounded by {@link CronExpression.MAX_SCAN_YEARS}; this ceiling
59
+ * only ever trips on a bug, never on legitimate input.
60
+ */
61
+ private static readonly MAX_SCAN_ITERATIONS;
62
+ /** The normalized expression (trimmed, single-spaced). */
63
+ readonly expression: string;
64
+ /** Whether this expression carries a leading seconds field (6-field form). */
65
+ readonly hasSecondsField: boolean;
66
+ private readonly seconds;
67
+ private readonly minutes;
68
+ private readonly hours;
69
+ private readonly daysOfMonth;
70
+ private readonly months;
71
+ private readonly daysOfWeek;
72
+ private readonly dayOfMonthRestricted;
73
+ private readonly dayOfWeekRestricted;
74
+ /**
75
+ * Parses and validates the expression.
76
+ * @param expression A 5-field (or 6-field, seconds-leading) standard cron expression.
77
+ * @throws {InvalidCronExpressionError} if the expression is malformed or out of range.
78
+ * The error carries an HTTP 400 status and a precise message.
79
+ */
80
+ constructor(expression: string);
81
+ /**
82
+ * Returns whether an expression is valid without throwing — convenient for create-time
83
+ * validation of user input where a boolean is more ergonomic than a try/catch.
84
+ */
85
+ static isValid(expression: string): boolean;
86
+ /**
87
+ * Computes the next execution strictly after `from`.
88
+ * @param from The reference instant. Defaults to now.
89
+ * @returns The next matching `Date`, or `undefined` if the expression has no occurrence
90
+ * within the {@link CronExpression.MAX_SCAN_YEARS} scan window (i.e. it never matches,
91
+ * such as `0 0 30 2 *`).
92
+ */
93
+ getNextExecutionDate(from?: Date): Date | undefined;
94
+ /**
95
+ * Computes up to `count` successive execution dates starting strictly after `from`.
96
+ * Returns fewer than `count` (possibly zero) entries only if the expression stops
97
+ * matching within the scan window.
98
+ */
99
+ getNextExecutionDates(from: Date, count: number): Date[];
100
+ /** Returns the normalized expression. */
101
+ toString(): string;
102
+ /**
103
+ * Applies the day-of-month / day-of-week OR rule for a candidate date.
104
+ */
105
+ private matchesDay;
106
+ /**
107
+ * Parses the day-of-week field, which is special-cased for the `0-7` range where both
108
+ * `0` and `7` mean Sunday. Values are parsed against `0-7` and then `7` is normalized to
109
+ * `0` so the resulting set aligns with `Date.getDay()`.
110
+ */
111
+ private parseDayOfWeek;
112
+ /**
113
+ * Parses a single cron field into the set of numeric values it allows.
114
+ */
115
+ private parseField;
116
+ /**
117
+ * Parses one atomic token (a number or a name) and validates it is within range.
118
+ */
119
+ private parseValue;
120
+ }
@@ -0,0 +1 @@
1
+ export * from "./cron-expression.model";
@@ -0,0 +1,22 @@
1
+ import { ScheduleInterface } from "../interfaces/schedule.interface";
2
+ import { CronExpression } from "../models/cron-expression.model";
3
+ /**
4
+ * A recurring {@link ScheduleInterface} backed by a standard cron expression. Construct it
5
+ * from a cron string (parsed and validated immediately, throwing an
6
+ * {@link InvalidCronExpressionError} on bad input) or from an already-built
7
+ * {@link CronExpression}.
8
+ *
9
+ * ```ts
10
+ * new CronSchedule("0 3 * * *"); // 03:00 every day
11
+ * new CronSchedule("0/15 9-17 * * 1-5"); // every 15 minutes, 09:00-17:00, Mon-Fri
12
+ * ```
13
+ */
14
+ export declare class CronSchedule implements ScheduleInterface {
15
+ /** The parsed cron expression backing this schedule. */
16
+ readonly cronExpression: CronExpression;
17
+ constructor(cronExpression: string | CronExpression);
18
+ getNextExecutionDate(from: Date): Date | undefined;
19
+ /** Up to `count` successive occurrences after `from`. */
20
+ getNextExecutionDates(from: Date, count: number): Date[];
21
+ toString(): string;
22
+ }
@@ -0,0 +1,17 @@
1
+ import { ScheduleInterface } from "../interfaces/schedule.interface";
2
+ /**
3
+ * A one-shot {@link ScheduleInterface} that fires a single time at a fixed instant. Once that
4
+ * instant has passed, {@link getNextExecutionDate} returns `undefined`, so the scheduler
5
+ * fires it once and then leaves it unarmed — it remains listed, with no next execution date.
6
+ *
7
+ * ```ts
8
+ * new DateSchedule(new Date("2026-12-31T23:59:00"));
9
+ * ```
10
+ */
11
+ export declare class DateSchedule implements ScheduleInterface {
12
+ /** The single instant at which this schedule fires. */
13
+ readonly date: Date;
14
+ constructor(date: Date);
15
+ getNextExecutionDate(from: Date): Date | undefined;
16
+ toString(): string;
17
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./cron.schedule";
2
+ export * from "./date.schedule";
@@ -0,0 +1,24 @@
1
+ import { RuntimeServerInterface } from "@pristine-ts/core";
2
+ import { LogHandlerInterface } from "@pristine-ts/logging";
3
+ import { LocalSchedulerInterface } from "../interfaces/local-scheduler.interface";
4
+ /**
5
+ * Adapts {@link LocalSchedulerManager} to the `pristine start` lifecycle so tagged tasks run
6
+ * with no manual bootstrap.
7
+ *
8
+ * It is tagged {@link ServiceDefinitionTagEnum.RuntimeServer}, so `pristine start` resolves it
9
+ * alongside the HTTP/gRPC servers and calls `start()` — which registers every tagged
10
+ * {@link SchedulableInterface} and arms its timers. Graceful shutdown is wired via the
11
+ * module's `onShutdown`, which calls `stop()` to cancel timers and drain in-flight tasks.
12
+ *
13
+ * A scheduler has no socket, so the `--port` / `--address` overrides `pristine start`
14
+ * propagates are ignored.
15
+ */
16
+ export declare class LocalSchedulerRuntimeServer implements RuntimeServerInterface {
17
+ private readonly scheduler;
18
+ private readonly logHandler;
19
+ /** Stable label for `pristine start` log lines and diagnostics. */
20
+ readonly name: string;
21
+ constructor(scheduler: LocalSchedulerInterface, logHandler: LogHandlerInterface);
22
+ start(): Promise<void>;
23
+ stop(): Promise<void>;
24
+ }
@@ -0,0 +1 @@
1
+ export * from "./local-scheduler.runtime-server";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The function a dynamically-registered schedule runs on each fire.
3
+ *
4
+ * It receives the `eventId` the scheduler generates for the occurrence (correlating logs to
5
+ * a specific fire), mirroring {@link ScheduledTaskInterface.run}. The argument is optional to
6
+ * consume: a plain `() => Promise<void>` (or a synchronous `() => void`) is assignable to
7
+ * this type, so the common case stays terse. A returned promise is awaited, which is what
8
+ * lets the overlap policy detect a still-running invocation.
9
+ */
10
+ export type ScheduledTaskFunction = (eventId?: string) => void | Promise<void>;
@@ -0,0 +1 @@
1
+ export * from "./scheduled-task-function.type";