@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.
- package/LICENSE +201 -0
- package/dist/lib/cjs/builders/builders.js +18 -0
- package/dist/lib/cjs/builders/builders.js.map +1 -0
- package/dist/lib/cjs/builders/cron-expression.builder.js +159 -0
- package/dist/lib/cjs/builders/cron-expression.builder.js.map +1 -0
- package/dist/lib/cjs/enums/day-of-week.enum.js +19 -0
- package/dist/lib/cjs/enums/day-of-week.enum.js.map +1 -0
- package/dist/lib/cjs/enums/enums.js +19 -0
- package/dist/lib/cjs/enums/enums.js.map +1 -0
- package/dist/lib/cjs/enums/month.enum.js +23 -0
- package/dist/lib/cjs/enums/month.enum.js.map +1 -0
- package/dist/lib/cjs/errors/errors.js +20 -0
- package/dist/lib/cjs/errors/errors.js.map +1 -0
- package/dist/lib/cjs/errors/invalid-cron-expression.error.js +30 -0
- package/dist/lib/cjs/errors/invalid-cron-expression.error.js.map +1 -0
- package/dist/lib/cjs/errors/schedule-already-exists.error.js +25 -0
- package/dist/lib/cjs/errors/schedule-already-exists.error.js.map +1 -0
- package/dist/lib/cjs/errors/schedule-not-found.error.js +22 -0
- package/dist/lib/cjs/errors/schedule-not-found.error.js.map +1 -0
- package/dist/lib/cjs/interfaces/interfaces.js +22 -0
- package/dist/lib/cjs/interfaces/interfaces.js.map +1 -0
- package/dist/lib/cjs/interfaces/local-scheduler.interface.js +3 -0
- package/dist/lib/cjs/interfaces/local-scheduler.interface.js.map +1 -0
- package/dist/lib/cjs/interfaces/schedulable.interface.js +3 -0
- package/dist/lib/cjs/interfaces/schedulable.interface.js.map +1 -0
- package/dist/lib/cjs/interfaces/schedule-descriptor.interface.js +3 -0
- package/dist/lib/cjs/interfaces/schedule-descriptor.interface.js.map +1 -0
- package/dist/lib/cjs/interfaces/schedule-options.interface.js +3 -0
- package/dist/lib/cjs/interfaces/schedule-options.interface.js.map +1 -0
- package/dist/lib/cjs/interfaces/schedule-state.interface.js +3 -0
- package/dist/lib/cjs/interfaces/schedule-state.interface.js.map +1 -0
- package/dist/lib/cjs/interfaces/schedule.interface.js +3 -0
- package/dist/lib/cjs/interfaces/schedule.interface.js.map +1 -0
- package/dist/lib/cjs/local-scheduling.module.js +85 -0
- package/dist/lib/cjs/local-scheduling.module.js.map +1 -0
- package/dist/lib/cjs/local-scheduling.module.keyname.js +5 -0
- package/dist/lib/cjs/local-scheduling.module.keyname.js.map +1 -0
- package/dist/lib/cjs/managers/local-scheduler.manager.js +361 -0
- package/dist/lib/cjs/managers/local-scheduler.manager.js.map +1 -0
- package/dist/lib/cjs/managers/managers.js +18 -0
- package/dist/lib/cjs/managers/managers.js.map +1 -0
- package/dist/lib/cjs/models/cron-expression.model.js +304 -0
- package/dist/lib/cjs/models/cron-expression.model.js.map +1 -0
- package/dist/lib/cjs/models/models.js +18 -0
- package/dist/lib/cjs/models/models.js.map +1 -0
- package/dist/lib/cjs/schedules/cron.schedule.js +34 -0
- package/dist/lib/cjs/schedules/cron.schedule.js.map +1 -0
- package/dist/lib/cjs/schedules/date.schedule.js +27 -0
- package/dist/lib/cjs/schedules/date.schedule.js.map +1 -0
- package/dist/lib/cjs/schedules/schedules.js +19 -0
- package/dist/lib/cjs/schedules/schedules.js.map +1 -0
- package/dist/lib/cjs/servers/local-scheduler.runtime-server.js +69 -0
- package/dist/lib/cjs/servers/local-scheduler.runtime-server.js.map +1 -0
- package/dist/lib/cjs/servers/servers.js +18 -0
- package/dist/lib/cjs/servers/servers.js.map +1 -0
- package/dist/lib/cjs/types/scheduled-task-function.type.js +3 -0
- package/dist/lib/cjs/types/scheduled-task-function.type.js.map +1 -0
- package/dist/lib/cjs/types/types.js +18 -0
- package/dist/lib/cjs/types/types.js.map +1 -0
- package/dist/lib/esm/builders/builders.js +2 -0
- package/dist/lib/esm/builders/builders.js.map +1 -0
- package/dist/lib/esm/builders/cron-expression.builder.js +155 -0
- package/dist/lib/esm/builders/cron-expression.builder.js.map +1 -0
- package/dist/lib/esm/enums/day-of-week.enum.js +16 -0
- package/dist/lib/esm/enums/day-of-week.enum.js.map +1 -0
- package/dist/lib/esm/enums/enums.js +3 -0
- package/dist/lib/esm/enums/enums.js.map +1 -0
- package/dist/lib/esm/enums/month.enum.js +20 -0
- package/dist/lib/esm/enums/month.enum.js.map +1 -0
- package/dist/lib/esm/errors/errors.js +4 -0
- package/dist/lib/esm/errors/errors.js.map +1 -0
- package/dist/lib/esm/errors/invalid-cron-expression.error.js +26 -0
- package/dist/lib/esm/errors/invalid-cron-expression.error.js.map +1 -0
- package/dist/lib/esm/errors/schedule-already-exists.error.js +21 -0
- package/dist/lib/esm/errors/schedule-already-exists.error.js.map +1 -0
- package/dist/lib/esm/errors/schedule-not-found.error.js +18 -0
- package/dist/lib/esm/errors/schedule-not-found.error.js.map +1 -0
- package/dist/lib/esm/interfaces/interfaces.js +6 -0
- package/dist/lib/esm/interfaces/interfaces.js.map +1 -0
- package/dist/lib/esm/interfaces/local-scheduler.interface.js +2 -0
- package/dist/lib/esm/interfaces/local-scheduler.interface.js.map +1 -0
- package/dist/lib/esm/interfaces/schedulable.interface.js +2 -0
- package/dist/lib/esm/interfaces/schedulable.interface.js.map +1 -0
- package/dist/lib/esm/interfaces/schedule-descriptor.interface.js +2 -0
- package/dist/lib/esm/interfaces/schedule-descriptor.interface.js.map +1 -0
- package/dist/lib/esm/interfaces/schedule-options.interface.js +2 -0
- package/dist/lib/esm/interfaces/schedule-options.interface.js.map +1 -0
- package/dist/lib/esm/interfaces/schedule-state.interface.js +2 -0
- package/dist/lib/esm/interfaces/schedule-state.interface.js.map +1 -0
- package/dist/lib/esm/interfaces/schedule.interface.js +2 -0
- package/dist/lib/esm/interfaces/schedule.interface.js.map +1 -0
- package/dist/lib/esm/local-scheduling.module.js +68 -0
- package/dist/lib/esm/local-scheduling.module.js.map +1 -0
- package/dist/lib/esm/local-scheduling.module.keyname.js +2 -0
- package/dist/lib/esm/local-scheduling.module.keyname.js.map +1 -0
- package/dist/lib/esm/managers/local-scheduler.manager.js +358 -0
- package/dist/lib/esm/managers/local-scheduler.manager.js.map +1 -0
- package/dist/lib/esm/managers/managers.js +2 -0
- package/dist/lib/esm/managers/managers.js.map +1 -0
- package/dist/lib/esm/models/cron-expression.model.js +300 -0
- package/dist/lib/esm/models/cron-expression.model.js.map +1 -0
- package/dist/lib/esm/models/models.js +2 -0
- package/dist/lib/esm/models/models.js.map +1 -0
- package/dist/lib/esm/schedules/cron.schedule.js +30 -0
- package/dist/lib/esm/schedules/cron.schedule.js.map +1 -0
- package/dist/lib/esm/schedules/date.schedule.js +23 -0
- package/dist/lib/esm/schedules/date.schedule.js.map +1 -0
- package/dist/lib/esm/schedules/schedules.js +3 -0
- package/dist/lib/esm/schedules/schedules.js.map +1 -0
- package/dist/lib/esm/servers/local-scheduler.runtime-server.js +66 -0
- package/dist/lib/esm/servers/local-scheduler.runtime-server.js.map +1 -0
- package/dist/lib/esm/servers/servers.js +2 -0
- package/dist/lib/esm/servers/servers.js.map +1 -0
- package/dist/lib/esm/types/scheduled-task-function.type.js +2 -0
- package/dist/lib/esm/types/scheduled-task-function.type.js.map +1 -0
- package/dist/lib/esm/types/types.js +2 -0
- package/dist/lib/esm/types/types.js.map +1 -0
- package/dist/lib/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/lib/tsconfig.tsbuildinfo +1 -0
- package/dist/types/builders/builders.d.ts +1 -0
- package/dist/types/builders/cron-expression.builder.d.ts +69 -0
- package/dist/types/enums/day-of-week.enum.d.ts +14 -0
- package/dist/types/enums/enums.d.ts +2 -0
- package/dist/types/enums/month.enum.d.ts +18 -0
- package/dist/types/errors/errors.d.ts +3 -0
- package/dist/types/errors/invalid-cron-expression.error.d.ts +20 -0
- package/dist/types/errors/schedule-already-exists.error.d.ts +15 -0
- package/dist/types/errors/schedule-not-found.error.d.ts +12 -0
- package/dist/types/interfaces/interfaces.d.ts +5 -0
- package/dist/types/interfaces/local-scheduler.interface.d.ts +57 -0
- package/dist/types/interfaces/schedulable.interface.d.ts +43 -0
- package/dist/types/interfaces/schedule-descriptor.interface.d.ts +22 -0
- package/dist/types/interfaces/schedule-options.interface.d.ts +37 -0
- package/dist/types/interfaces/schedule-state.interface.d.ts +24 -0
- package/dist/types/interfaces/schedule.interface.d.ts +20 -0
- package/dist/types/local-scheduling.module.d.ts +30 -0
- package/dist/types/local-scheduling.module.keyname.d.ts +1 -0
- package/dist/types/managers/local-scheduler.manager.d.ts +110 -0
- package/dist/types/managers/managers.d.ts +1 -0
- package/dist/types/models/cron-expression.model.d.ts +120 -0
- package/dist/types/models/models.d.ts +1 -0
- package/dist/types/schedules/cron.schedule.d.ts +22 -0
- package/dist/types/schedules/date.schedule.d.ts +17 -0
- package/dist/types/schedules/schedules.d.ts +2 -0
- package/dist/types/servers/local-scheduler.runtime-server.d.ts +24 -0
- package/dist/types/servers/servers.d.ts +1 -0
- package/dist/types/types/scheduled-task-function.type.d.ts +10 -0
- package/dist/types/types/types.d.ts +1 -0
- package/package.json +68 -0
- 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**.
|