@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
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@pristine-ts/local-scheduling",
3
+ "version": "4.0.2",
4
+ "description": "In-process cron scheduling for long-running Pristine applications.",
5
+ "module": "dist/lib/esm/local-scheduling.module.js",
6
+ "main": "dist/lib/cjs/local-scheduling.module.js",
7
+ "types": "dist/types/local-scheduling.module.d.ts",
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json",
10
+ "prepublish": "npm run build",
11
+ "test": "jest",
12
+ "test:cov": "jest --coverage"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@pristine-ts/common": "^4.0.2",
24
+ "@pristine-ts/core": "^4.0.2",
25
+ "@pristine-ts/logging": "^4.0.2",
26
+ "@pristine-ts/scheduling": "^4.0.2"
27
+ },
28
+ "jest": {
29
+ "transform": {
30
+ ".(ts|tsx)": "ts-jest"
31
+ },
32
+ "globals": {
33
+ "ts-jest": {
34
+ "tsconfig": {
35
+ "strictNullChecks": false
36
+ }
37
+ }
38
+ },
39
+ "testEnvironment": "node",
40
+ "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
41
+ "moduleFileExtensions": [
42
+ "ts",
43
+ "tsx",
44
+ "js"
45
+ ],
46
+ "coveragePathIgnorePatterns": [
47
+ "/node_modules/",
48
+ "/test/"
49
+ ],
50
+ "coverageThreshold": {
51
+ "global": {
52
+ "branches": 90,
53
+ "functions": 95,
54
+ "lines": 95,
55
+ "statements": 95
56
+ }
57
+ },
58
+ "collectCoverageFrom": [
59
+ "src/*.{js,ts}"
60
+ ]
61
+ },
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "https://github.com/magieno/pristine-ts.git",
65
+ "directory": "packages/local-scheduling"
66
+ },
67
+ "gitHead": "ff1d17406b92dc73891b7091840c72dcfe84aea6"
68
+ }
package/readme.md ADDED
@@ -0,0 +1,307 @@
1
+ # @pristine-ts/local-scheduling
2
+
3
+ In-process scheduling for long-running Pristine applications.
4
+
5
+ `@pristine-ts/scheduling` is deliberately trigger-agnostic — a `ScheduledTaskInterface` is
6
+ just work (`run()`), and *something external* decides when it runs: `SchedulerManager.runTasks()`
7
+ is fired by AWS EventBridge (via `@pristine-ts/aws-scheduling`), an HTTP request, or a manual
8
+ call. That model is perfect for a serverless function invoked on a cron rule.
9
+
10
+ This module solves the other case: an **always-up Node process** (an HTTP server, a worker)
11
+ that must run tasks on their own schedules from *inside* the process, with **no external
12
+ trigger**. It owns the clock: it reads each task's declared schedule and arms an in-process
13
+ timer for it. Schedules are polymorphic (a recurring cron, a one-off date, ...) and can also
14
+ be created, edited, and deleted at runtime — which suits a consumer that loads user-defined
15
+ schedules from a database and mutates them from HTTP controllers.
16
+
17
+ It is platform-neutral (nothing AWS-specific) and has **zero third-party runtime
18
+ dependencies** — the cron engine is written in-house. It builds on `@pristine-ts/scheduling`
19
+ (a schedulable task *is* a scheduled task, see below) and coexists with
20
+ `@pristine-ts/aws-scheduling` and the other drivers; none of them interfere.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @pristine-ts/local-scheduling
26
+ ```
27
+
28
+ Import the module into your application module:
29
+
30
+ ```typescript
31
+ import {AppModuleInterface} from "@pristine-ts/common";
32
+ import {LocalSchedulingModule} from "@pristine-ts/local-scheduling";
33
+
34
+ export const AppModule: AppModuleInterface = {
35
+ keyname: "app",
36
+ importModules: [
37
+ LocalSchedulingModule,
38
+ // ...
39
+ ],
40
+ providerRegistrations: [],
41
+ };
42
+ ```
43
+
44
+ ## Two ways to schedule
45
+
46
+ A driver — not the task — decides when a task runs. This module is that driver for an
47
+ in-process daemon, and it sources schedules two ways that share one id space and one set of
48
+ timers:
49
+
50
+ 1. **Statically**, from tasks that declare their own schedule (`SchedulableInterface`),
51
+ auto-registered when the scheduler starts.
52
+ 2. **Dynamically**, through `LocalSchedulerManager`'s runtime API (typically fed from a
53
+ database at bootstrap and mutated by HTTP controllers).
54
+
55
+ You do not start the scheduler yourself: the module registers a `LocalSchedulerRuntimeServer`
56
+ (a `RuntimeServerInterface`), so **`pristine start` starts it — arming every tagged task — and
57
+ stops it on shutdown**, alongside your HTTP/gRPC servers. Resolve `LocalSchedulerManager` only
58
+ when you need the dynamic API; call `start()` / `stop()` by hand only if you embed the kernel
59
+ yourself instead of using `pristine start`.
60
+
61
+ ### Static: a task that declares its schedule
62
+
63
+ A `SchedulableInterface` is a `ScheduledTaskInterface` (from `@pristine-ts/scheduling`) that
64
+ *additionally* declares *when* it should run. Keeping it a separate interface makes it
65
+ explicit that a driver must be present to honour the schedule. Tag the class with
66
+ `@tag(ServiceDefinitionTagEnum.Schedulable)`; every tagged class is discovered and
67
+ **auto-registered on `start()`**.
68
+
69
+ ```typescript
70
+ import "reflect-metadata";
71
+ import {injectable} from "tsyringe";
72
+ import {injectConfig, tag, ServiceDefinitionTagEnum} from "@pristine-ts/common";
73
+ import {
74
+ SchedulableInterface, ScheduleInterface, CronSchedule,
75
+ } from "@pristine-ts/local-scheduling";
76
+
77
+ // Typed configuration keys for your app, mirroring each package's `XxxConfigurationKeys`
78
+ // (e.g. `JwtConfigurationKeys`) — a const object, not a magic string at the call site.
79
+ export const AppConfigurationKeys = {
80
+ CleanupCron: "app.cleanup.cron",
81
+ } as const;
82
+
83
+ @tag(ServiceDefinitionTagEnum.Schedulable)
84
+ @injectable()
85
+ export class NightlyCleanupTask implements SchedulableInterface {
86
+ // Because getSchedules() is a method, the schedule can be computed from any injected
87
+ // dependency — here a configuration value resolved by its key.
88
+ constructor(@injectConfig(AppConfigurationKeys.CleanupCron) private readonly cleanupCron: string) {}
89
+
90
+ getSchedules(): ScheduleInterface[] {
91
+ return [new CronSchedule(this.cleanupCron)];
92
+ }
93
+
94
+ async run(eventId?: string): Promise<void> {
95
+ // ...the work. `eventId` correlates logs to the specific occurrence.
96
+ }
97
+ }
98
+ ```
99
+
100
+ The key is declared in your application module's `configurationDefinitions`, so the
101
+ configuration system resolves it (from an environment variable, a default, etc.) — the same
102
+ way `@pristine-ts/jwt` declares its own keys:
103
+
104
+ ```typescript
105
+ import {EnvironmentVariableResolver} from "@pristine-ts/configuration";
106
+
107
+ // in your AppModule:
108
+ configurationDefinitions: [
109
+ {
110
+ parameterName: AppConfigurationKeys.CleanupCron,
111
+ isRequired: false,
112
+ defaultValue: "0 3 * * *",
113
+ defaultResolvers: [new EnvironmentVariableResolver("APP_CLEANUP_CRON")],
114
+ },
115
+ ],
116
+ ```
117
+
118
+ A tagged task is registered under an id derived from its **class name** (suffixed `#0`, `#1`,
119
+ … when it declares more than one schedule). Names are used as-is, so keep them distinct and,
120
+ if you minify server code, preserve class names — or use the dynamic API below, where ids are
121
+ explicit. A tagged task whose id is already taken, or whose `getSchedules()` throws, is logged
122
+ and skipped — it never prevents the others, or the scheduler, from starting. Static tasks run
123
+ with the default policies; for per-schedule policies (overlap, catch-up) use the dynamic API.
124
+
125
+ ### Dynamic: schedules as runtime state
126
+
127
+ Inject `LocalSchedulerManager` (by class, or by the `"LocalSchedulerInterface"` token) wherever
128
+ you load or mutate schedules — a service that reads them from a database at bootstrap, an HTTP
129
+ controller that edits them. There is no manual `start()`: `schedule()` arms immediately when the
130
+ scheduler is already running (it is, under `pristine start`), otherwise on start.
131
+
132
+ ```typescript
133
+ import {injectable, inject} from "tsyringe";
134
+ import {LocalSchedulerInterface} from "@pristine-ts/local-scheduling";
135
+
136
+ @injectable()
137
+ export class RoutineScheduler {
138
+ constructor(
139
+ @inject("LocalSchedulerInterface") private readonly scheduler: LocalSchedulerInterface,
140
+ @inject("RoutineRunnerInterface") private readonly runner: RoutineRunnerInterface,
141
+ ) {}
142
+
143
+ // Register a schedule loaded from your database.
144
+ register(routineId: string, scheduleId: string, cronExpression: string): void {
145
+ this.scheduler.schedule(
146
+ `routine:${routineId}:${scheduleId}`,
147
+ cronExpression, // a cron string is shorthand for a CronSchedule
148
+ (eventId) => this.runner.execute(routineId, eventId),
149
+ );
150
+ }
151
+
152
+ // Edit or remove them at runtime — e.g. from an HTTP controller.
153
+ reschedule(routineId: string, scheduleId: string, cronExpression: string): void {
154
+ this.scheduler.reschedule(`routine:${routineId}:${scheduleId}`, cronExpression);
155
+ }
156
+
157
+ unregister(routineId: string, scheduleId: string): void {
158
+ this.scheduler.unschedule(`routine:${routineId}:${scheduleId}`);
159
+ }
160
+ }
161
+ ```
162
+
163
+ ## Schedules
164
+
165
+ A schedule answers one question — *when does this next run?* — and is polymorphic, so the
166
+ scheduler drives any kind through the same contract (`ScheduleInterface`):
167
+
168
+ | Schedule | When it fires |
169
+ |---|---|
170
+ | `new CronSchedule("0 3 * * *")` | Recurring, on a cron expression (string or a `CronExpression`). |
171
+ | `new DateSchedule(new Date("2026-12-31T23:59:00"))` | Once, at a fixed instant; afterwards it is left unarmed. |
172
+
173
+ Both are accepted anywhere a schedule is expected (`schedule()`, `reschedule()`, a task's
174
+ `getSchedules()`), and a plain cron **string** is accepted as shorthand for a `CronSchedule`.
175
+ New schedule kinds only need to implement `ScheduleInterface` — the scheduler needs no change.
176
+
177
+ ## Building expressions
178
+
179
+ Prefer not to hand-write cron? `CronExpressionBuilder` composes an expression from typed,
180
+ self-describing calls — each sets one field, and a fresh builder is already "every minute":
181
+
182
+ ```typescript
183
+ import {CronExpressionBuilder, MonthEnum, DayOfWeekEnum} from "@pristine-ts/local-scheduling";
184
+
185
+ new CronExpressionBuilder().dailyAt(3).toString(); // "0 3 * * *"
186
+ new CronExpressionBuilder().weeklyOn(DayOfWeekEnum.Sunday, 2, 30).toString(); // "30 2 * * 0"
187
+
188
+ const schedule = new CronExpressionBuilder() // every 15 min, 09:00-17:00, Mon-Fri
189
+ .everyMinutes(15)
190
+ .hoursBetween(9, 17)
191
+ .daysOfWeekBetween(DayOfWeekEnum.Monday, DayOfWeekEnum.Friday)
192
+ .toSchedule(); // -> CronSchedule, ready for schedule()/getSchedules()
193
+ ```
194
+
195
+ - **Per-field setters** — `atMinute(...)` / `atHour(...)` / `onDayOfMonth(...)` / `inMonth(...)` /
196
+ `onDayOfWeek(...)` (variadic lists), `everyMinutes(n)` / `everyHours(n)` / … (steps), and
197
+ `minutesBetween(a, b, step?)` / `hoursBetween(a, b, step?)` / … (ranges). `atSecond(...)` /
198
+ `everySeconds(n)` add the optional sixth seconds field.
199
+ - **High-level helpers** — `hourlyAtMinute(m)`, `dailyAt(hour, minute?)`,
200
+ `weeklyOn(dayOfWeek, hour, minute?)`, `monthlyOn(dayOfMonth, hour, minute?)`.
201
+ - **Terminals** — `toString()` (the raw string), `build()` (a validated `CronExpression`, which
202
+ throws `InvalidCronExpressionError` on an out-of-range value), and `toSchedule()` (a
203
+ `CronSchedule`).
204
+ - `MonthEnum` and `DayOfWeekEnum` are exported so `inMonth(MonthEnum.January)` and
205
+ `onDayOfWeek(DayOfWeekEnum.Monday)` read intent instead of raw numbers.
206
+
207
+ ## `LocalSchedulerManager`
208
+
209
+ Injectable, module-scoped, and tagged `"LocalSchedulerInterface"` (inject by class or by that
210
+ token). It is a **process-wide singleton**, so the code that starts it at bootstrap and the
211
+ controllers that mutate schedules at runtime share the same instance and timers.
212
+
213
+ | Method | Description |
214
+ |---|---|
215
+ | `schedule(id, schedule, task, options?)` | Register a task. `schedule` is a `ScheduleInterface` or a cron string. Arms immediately if already started, otherwise on `start()`. Throws `ScheduleAlreadyExistsError` on a duplicate id, `InvalidCronExpressionError` on a bad cron string. |
216
+ | `unschedule(id)` | Cancel and remove a schedule. Returns `true` if one was removed. |
217
+ | `reschedule(id, schedule)` | Replace a schedule (task and options preserved). Throws `ScheduleNotFoundError` if absent. |
218
+ | `has(id)` | Whether a schedule is registered. |
219
+ | `list()` | A snapshot (`ScheduleDescriptor[]`) of every schedule, with its `schedule`, next execution date, and running state. |
220
+ | `getNextExecutionDate(id)` | The next armed date for a schedule. |
221
+ | `start()` | Register tagged tasks, then arm every schedule. Idempotent. |
222
+ | `stop()` | Cancel every timer synchronously (no fire happens after `stop()` returns) and resolve once in-flight tasks have settled — `await` it for a graceful shutdown. |
223
+ | `isStarted` | Whether the scheduler is started. |
224
+
225
+ The `task` receives the `eventId` the scheduler generates for each occurrence
226
+ (`"<id>:<scheduled-ISO-date>"`), mirroring `ScheduledTaskInterface.run`. A plain
227
+ `() => Promise<void>` is assignable too, so the argument is optional to consume.
228
+
229
+ ### Per-schedule options
230
+
231
+ ```typescript
232
+ scheduler.schedule("report", "0 * * * *", task, {
233
+ allowOverlap: false, // default: skip a fire if the previous invocation is still running
234
+ catchUp: false, // default: skip occurrences missed while asleep/stalled
235
+ missedExecutionThresholdInMilliseconds: 5000, // how late counts as "missed"
236
+ });
237
+ ```
238
+
239
+ - **Overlap** — by default a schedule never runs concurrently with itself: if it fires while
240
+ the previous invocation is still running, that fire is skipped (and logged). Set
241
+ `allowOverlap: true` to run anyway.
242
+ - **Missed fires** — if the host sleeps or the event loop stalls past an occurrence, the
243
+ default is to skip the missed occurrence and arm for the next future one. Set
244
+ `catchUp: true` to fire **at most one** catch-up on wake (multiple missed occurrences still
245
+ collapse into one).
246
+ - **Task errors** — a task that throws or rejects is caught and logged via the `LogHandler`;
247
+ the scheduling loop is never broken.
248
+
249
+ ### Timer strategy
250
+
251
+ Each schedule owns a single chained `setTimeout`, re-armed from `Date.now()` after every fire
252
+ (so it never drifts) and **chunked** past the `2^31-1` ms (~24.8 day) ceiling. Re-arming
253
+ happens *before* the task runs, so a long task never delays subsequent occurrences.
254
+
255
+ ## `CronExpression`
256
+
257
+ Parses and validates a standard **5-field** cron expression (with an optional leading **6th
258
+ seconds field**) and computes upcoming dates. `CronSchedule` wraps it; you can also use it
259
+ directly to preview runs or validate user input before persisting it:
260
+
261
+ ```typescript
262
+ import {CronExpression} from "@pristine-ts/local-scheduling";
263
+
264
+ new CronExpression("*/5 9-17 * * 1-5").getNextExecutionDates(new Date(), 3);
265
+
266
+ CronExpression.isValid(userInput); // -> boolean, no try/catch
267
+ ```
268
+
269
+ ```
270
+ ┌───────────── second (0-59) (optional 6th field)
271
+ │ ┌─────────── minute (0-59)
272
+ │ │ ┌───────── hour (0-23)
273
+ │ │ │ ┌─────── day-of-month (1-31)
274
+ │ │ │ │ ┌───── month (1-12 or JAN-DEC)
275
+ │ │ │ │ │ ┌─── day-of-week (0-7 or SUN-SAT; 0 and 7 are both Sunday)
276
+ * * * * * *
277
+ ```
278
+
279
+ - Each field supports `*`, single values, ranges (`a-b`), lists (`a,b,c`), and steps (`*/n`,
280
+ `a-b/n`, `a/n`). Month and day-of-week accept case-insensitive names.
281
+ - Ranges must be ascending; write wrap-arounds as lists (`FRI,SAT,SUN`).
282
+ - **Day-of-month / day-of-week OR rule:** when both are restricted (neither is `*`), the
283
+ expression matches when *either* matches — e.g. `30 4 1,15 * 5` runs at 04:30 on the 1st and
284
+ 15th **and** every Friday.
285
+
286
+ | Member | Description |
287
+ |---|---|
288
+ | `new CronExpression(expr)` | Parse/validate. Throws `InvalidCronExpressionError` (HTTP 400) on bad input. |
289
+ | `getNextExecutionDate(from?)` | Next occurrence strictly after `from` (default now), or `undefined` if it never occurs (e.g. `0 0 30 2 *`). |
290
+ | `getNextExecutionDates(from, count)` | Up to `count` successive occurrences. |
291
+ | `CronExpression.isValid(expr)` | Boolean validity check (no throw). |
292
+
293
+ ### Time semantics
294
+
295
+ All computations use the host's **system local time**. Across a spring-forward DST transition
296
+ a non-existent local time is skipped; across a fall-back transition a repeated local time
297
+ fires once. The API is shaped so an IANA timezone can be added later as an optional argument
298
+ without changing existing signatures.
299
+
300
+ ## Errors
301
+
302
+ All extend the framework's `PristineError`, so they surface with the right HTTP status if they
303
+ reach a controller boundary:
304
+
305
+ - `InvalidCronExpressionError` — **400**; carries the offending `expression` and `field`.
306
+ - `ScheduleNotFoundError` — **404**.
307
+ - `ScheduleAlreadyExistsError` — **409**.