@procedures/schedule 0.0.0-stage → 0.2.3
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 +21 -0
- package/README.md +98 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/cron.d.ts +47 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +197 -0
- package/dist/cron.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +42 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +174 -0
- package/dist/module.js.map +1 -0
- package/dist/procedures/schedule/add.d.ts +17 -0
- package/dist/procedures/schedule/add.d.ts.map +1 -0
- package/dist/procedures/schedule/add.js +66 -0
- package/dist/procedures/schedule/add.js.map +1 -0
- package/dist/procedures/schedule/index.d.ts +5 -0
- package/dist/procedures/schedule/index.d.ts.map +1 -0
- package/dist/procedures/schedule/index.js +5 -0
- package/dist/procedures/schedule/index.js.map +1 -0
- package/dist/procedures/schedule/list.d.ts +12 -0
- package/dist/procedures/schedule/list.d.ts.map +1 -0
- package/dist/procedures/schedule/list.js +18 -0
- package/dist/procedures/schedule/list.js.map +1 -0
- package/dist/procedures/schedule/next.d.ts +13 -0
- package/dist/procedures/schedule/next.d.ts.map +1 -0
- package/dist/procedures/schedule/next.js +24 -0
- package/dist/procedures/schedule/next.js.map +1 -0
- package/dist/procedures/schedule/remove.d.ts +12 -0
- package/dist/procedures/schedule/remove.d.ts.map +1 -0
- package/dist/procedures/schedule/remove.js +13 -0
- package/dist/procedures/schedule/remove.js.map +1 -0
- package/dist/programs.d.ts +26 -0
- package/dist/programs.d.ts.map +1 -0
- package/dist/programs.js +34 -0
- package/dist/programs.js.map +1 -0
- package/dist/register.d.ts +6 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +8 -0
- package/dist/register.js.map +1 -0
- package/dist/scheduler.d.ts +106 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +274 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/store.d.ts +62 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +188 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +78 -3
- package/src/cron.ts +230 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +10 -0
- package/src/module.ts +227 -0
- package/src/procedures/schedule/add.ts +66 -0
- package/src/procedures/schedule/index.ts +4 -0
- package/src/procedures/schedule/list.ts +20 -0
- package/src/procedures/schedule/next.ts +25 -0
- package/src/procedures/schedule/remove.ts +15 -0
- package/src/programs.ts +43 -0
- package/src/register.ts +10 -0
- package/src/scheduler.ts +331 -0
- package/src/store.ts +213 -0
- package/src/types.ts +122 -0
package/src/module.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The procedures of @procedures/schedule as data. Importing this file registers nothing.
|
|
3
|
+
*
|
|
4
|
+
* Scaffold a procedure with: mark procedure new <name> --path packages/schedule
|
|
5
|
+
*
|
|
6
|
+
* `scheduleModule` keeps the schedules in `defaultScheduleStore` (memory). `createScheduleModule()`
|
|
7
|
+
* gives the same procedures with the store of the host, for example a `FileScheduleStore`. The
|
|
8
|
+
* host gives the same store to its `Scheduler` (roadmap R46).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import {
|
|
13
|
+
defineModule,
|
|
14
|
+
registerModules,
|
|
15
|
+
PROCEDURE_REGISTRY,
|
|
16
|
+
createProcedure,
|
|
17
|
+
zodAdapter,
|
|
18
|
+
type AnyProcedure,
|
|
19
|
+
type Effects,
|
|
20
|
+
type ProcedureContext,
|
|
21
|
+
type ProcedureModule,
|
|
22
|
+
type ProcedureRegistry,
|
|
23
|
+
} from "@procedures/client";
|
|
24
|
+
import { scheduleAdd } from "./procedures/schedule/add.js";
|
|
25
|
+
import { scheduleList } from "./procedures/schedule/list.js";
|
|
26
|
+
import { scheduleRemove } from "./procedures/schedule/remove.js";
|
|
27
|
+
import { scheduleNext } from "./procedures/schedule/next.js";
|
|
28
|
+
import { DEFAULT_SCHEDULE_SETTINGS, PROGRAM_KEY, type ScheduleSettings } from "./programs.js";
|
|
29
|
+
import type { ScheduleStore } from "./store.js";
|
|
30
|
+
import type {
|
|
31
|
+
Schedule,
|
|
32
|
+
ScheduleAddInput,
|
|
33
|
+
ScheduleAddOutput,
|
|
34
|
+
ScheduleListInput,
|
|
35
|
+
ScheduleListOutput,
|
|
36
|
+
ScheduleNextInput,
|
|
37
|
+
ScheduleNextOutput,
|
|
38
|
+
ScheduleRemoveInput,
|
|
39
|
+
ScheduleRemoveOutput,
|
|
40
|
+
} from "./types.js";
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Schemas
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
const cron = z.string().min(1).max(200).describe("The cron expression: 5 fields (minute hour day month weekday), or @hourly, @daily, @weekly, @monthly, @yearly");
|
|
47
|
+
const timeZone = z.enum(["utc", "local"]).describe("The time zone of the expression (default local)");
|
|
48
|
+
const scheduleId = z
|
|
49
|
+
.string()
|
|
50
|
+
.regex(/^[A-Za-z0-9._:-]{1,200}$/, "Letters, digits, and . _ : -")
|
|
51
|
+
.describe("The name of the schedule");
|
|
52
|
+
const programKey = z.string().max(500).regex(PROGRAM_KEY, "A dotted key: reports.weekly").describe("The dotted key of the stored program");
|
|
53
|
+
|
|
54
|
+
const run = z.object({
|
|
55
|
+
startedAt: z.number().describe("The start, in milliseconds since 1970"),
|
|
56
|
+
durationMs: z.number(),
|
|
57
|
+
outcome: z.enum(["success", "failure"]),
|
|
58
|
+
error: z.object({ code: z.string().optional(), message: z.string() }).optional(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const view = z.object({
|
|
62
|
+
id: z.string(),
|
|
63
|
+
cron: z.string(),
|
|
64
|
+
program: z.string(),
|
|
65
|
+
input: z.unknown().optional(),
|
|
66
|
+
timeZone: z.enum(["utc", "local"]),
|
|
67
|
+
createdAt: z.number().describe("When the schedule was added, in milliseconds since 1970"),
|
|
68
|
+
capability: z
|
|
69
|
+
.custom<NonNullable<Schedule["capability"]>>((value) => value !== null && typeof value === "object")
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("The capability of the caller that added it: its clearance and its permitted effects"),
|
|
72
|
+
lastRun: run.optional().describe("The last run"),
|
|
73
|
+
runs: z.number().int(),
|
|
74
|
+
failures: z.number().int(),
|
|
75
|
+
skipped: z.number().int().describe("The times that came during a run: no second run started"),
|
|
76
|
+
nextRunAt: z.string().nullable().describe("The next time as an ISO text, or null"),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const ScheduleAddInputSchema = z.object({
|
|
80
|
+
id: scheduleId.optional().describe("The name of the schedule (default: the key of the program)"),
|
|
81
|
+
cron,
|
|
82
|
+
program: programKey,
|
|
83
|
+
input: z.unknown().optional().describe("The input of the program"),
|
|
84
|
+
timeZone: timeZone.optional(),
|
|
85
|
+
replace: z.boolean().optional().describe("True: replace a schedule of the same name (default false)"),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const ScheduleAddOutputSchema = z.object({ schedule: view });
|
|
89
|
+
|
|
90
|
+
const ScheduleListInputSchema = z.object({ program: programKey.optional().describe("Only the schedules of this program") });
|
|
91
|
+
|
|
92
|
+
const ScheduleListOutputSchema = z.object({ schedules: z.array(view).describe("The schedules, sorted by name") });
|
|
93
|
+
|
|
94
|
+
const ScheduleRemoveInputSchema = z.object({ id: scheduleId });
|
|
95
|
+
|
|
96
|
+
const ScheduleRemoveOutputSchema = z.object({ removed: z.boolean().describe("True when the schedule existed") });
|
|
97
|
+
|
|
98
|
+
const ScheduleNextInputSchema = z.object({
|
|
99
|
+
cron,
|
|
100
|
+
count: z.number().int().min(1).max(100).optional().describe("The number of times, 1 to 100 (default 5)"),
|
|
101
|
+
after: z.string().max(100).optional().describe("The times come after this ISO time (default now)"),
|
|
102
|
+
timeZone: timeZone.optional(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const ScheduleNextOutputSchema = z.object({ times: z.array(z.string()).describe("The next times as ISO texts") });
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Procedures
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
/** `schedule.add`: it reads the registry and the program store, and writes the schedules (a store can be a file). */
|
|
112
|
+
const ADDS: Effects = Object.freeze({ reads: Object.freeze(["fs", "programs", "registry", "schedules"]), writes: Object.freeze(["fs", "schedules"]) });
|
|
113
|
+
/** `schedule.list`: it reads the schedules. */
|
|
114
|
+
const LISTS: Effects = Object.freeze({ reads: Object.freeze(["fs", "schedules"]), writes: Object.freeze([]) });
|
|
115
|
+
/** `schedule.remove`: it deletes a schedule. A second call changes nothing more. */
|
|
116
|
+
const REMOVES: Effects = Object.freeze({ reads: Object.freeze(["fs", "schedules"]), writes: Object.freeze(["fs", "schedules"]), destructive: true, idempotent: true });
|
|
117
|
+
/** `schedule.next`: its output comes from its input and the time. */
|
|
118
|
+
const PURE: Effects = Object.freeze({ reads: Object.freeze([]), writes: Object.freeze([]) });
|
|
119
|
+
|
|
120
|
+
/** The four procedures, with handlers that use the settings. */
|
|
121
|
+
function scheduleProcedures(settings: ScheduleSettings): AnyProcedure[] {
|
|
122
|
+
const scheduleAddProcedure = createProcedure()
|
|
123
|
+
.path(["schedule", "add"])
|
|
124
|
+
.input(zodAdapter<ScheduleAddInput>(ScheduleAddInputSchema))
|
|
125
|
+
.output(zodAdapter<ScheduleAddOutput>(ScheduleAddOutputSchema))
|
|
126
|
+
.meta({
|
|
127
|
+
description: "Add a schedule: a cron expression that starts a stored program with an input",
|
|
128
|
+
tags: ["schedule", "cron", "programs"],
|
|
129
|
+
args: ["cron", "program"],
|
|
130
|
+
shorts: { timeZone: "z" },
|
|
131
|
+
output: "json",
|
|
132
|
+
effects: ADDS,
|
|
133
|
+
})
|
|
134
|
+
.handler(async (input: ScheduleAddInput, ctx: ProcedureContext): Promise<ScheduleAddOutput> => scheduleAdd(input, ctx, settings))
|
|
135
|
+
.build();
|
|
136
|
+
|
|
137
|
+
const scheduleListProcedure = createProcedure()
|
|
138
|
+
.path(["schedule", "list"])
|
|
139
|
+
.input(zodAdapter<ScheduleListInput>(ScheduleListInputSchema))
|
|
140
|
+
.output(zodAdapter<ScheduleListOutput>(ScheduleListOutputSchema))
|
|
141
|
+
.meta({
|
|
142
|
+
description: "The schedules, with the next time and the last run of each",
|
|
143
|
+
tags: ["schedule", "cron"],
|
|
144
|
+
args: [],
|
|
145
|
+
shorts: {},
|
|
146
|
+
output: "json",
|
|
147
|
+
effects: LISTS,
|
|
148
|
+
})
|
|
149
|
+
.handler(async (input: ScheduleListInput): Promise<ScheduleListOutput> => scheduleList(input, settings))
|
|
150
|
+
.build();
|
|
151
|
+
|
|
152
|
+
const scheduleRemoveProcedure = createProcedure()
|
|
153
|
+
.path(["schedule", "remove"])
|
|
154
|
+
.input(zodAdapter<ScheduleRemoveInput>(ScheduleRemoveInputSchema))
|
|
155
|
+
.output(zodAdapter<ScheduleRemoveOutput>(ScheduleRemoveOutputSchema))
|
|
156
|
+
.meta({
|
|
157
|
+
description: "Remove a schedule",
|
|
158
|
+
tags: ["schedule", "cron"],
|
|
159
|
+
args: ["id"],
|
|
160
|
+
shorts: {},
|
|
161
|
+
output: "json",
|
|
162
|
+
effects: REMOVES,
|
|
163
|
+
})
|
|
164
|
+
.handler(async (input: ScheduleRemoveInput): Promise<ScheduleRemoveOutput> => scheduleRemove(input, settings))
|
|
165
|
+
.build();
|
|
166
|
+
|
|
167
|
+
const scheduleNextProcedure = createProcedure()
|
|
168
|
+
.path(["schedule", "next"])
|
|
169
|
+
.input(zodAdapter<ScheduleNextInput>(ScheduleNextInputSchema))
|
|
170
|
+
.output(zodAdapter<ScheduleNextOutput>(ScheduleNextOutputSchema))
|
|
171
|
+
.meta({
|
|
172
|
+
description: "The next times of a cron expression",
|
|
173
|
+
tags: ["schedule", "cron"],
|
|
174
|
+
args: ["cron"],
|
|
175
|
+
shorts: { count: "n", timeZone: "z" },
|
|
176
|
+
output: "json",
|
|
177
|
+
effects: PURE,
|
|
178
|
+
})
|
|
179
|
+
.handler(async (input: ScheduleNextInput): Promise<ScheduleNextOutput> => scheduleNext(input, settings))
|
|
180
|
+
.build();
|
|
181
|
+
|
|
182
|
+
return [scheduleAddProcedure, scheduleListProcedure, scheduleRemoveProcedure, scheduleNextProcedure];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The module of this package, for `createRegistry()`: its procedures as data, on
|
|
187
|
+
* `defaultScheduleStore`. Importing this file registers nothing.
|
|
188
|
+
*/
|
|
189
|
+
export const scheduleModule: ProcedureModule = defineModule("@procedures/schedule", scheduleProcedures(DEFAULT_SCHEDULE_SETTINGS));
|
|
190
|
+
|
|
191
|
+
export default scheduleModule;
|
|
192
|
+
|
|
193
|
+
/** The options of `createScheduleModule()`. */
|
|
194
|
+
export interface ScheduleModuleOptions {
|
|
195
|
+
/** The store of the schedules. The default is `defaultScheduleStore` (memory). */
|
|
196
|
+
readonly store?: ScheduleStore | undefined;
|
|
197
|
+
/** The wall time, in milliseconds since 1970. The default is `Date.now`. A test gives the time of its fake clock. */
|
|
198
|
+
readonly now?: (() => number) | undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* This function gives the module of this package with the store of the host. Without options, it
|
|
203
|
+
* gives `scheduleModule`. A host gives it to `createRegistry()` before its bundles, so it
|
|
204
|
+
* replaces the `scheduleModule` that a bundle requires (the first module of a name counts).
|
|
205
|
+
*
|
|
206
|
+
* ```ts
|
|
207
|
+
* const store = new FileScheduleStore("schedules.json");
|
|
208
|
+
* const registry = createRegistry(createScheduleModule({ store }), storeModule);
|
|
209
|
+
* await new Scheduler({ registry, store, policy }).start();
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export function createScheduleModule(options: ScheduleModuleOptions = {}): ProcedureModule {
|
|
213
|
+
if (Object.values(options).every((value) => value === undefined)) return scheduleModule;
|
|
214
|
+
const settings: ScheduleSettings = {
|
|
215
|
+
store: options.store ?? DEFAULT_SCHEDULE_SETTINGS.store,
|
|
216
|
+
now: options.now ?? DEFAULT_SCHEDULE_SETTINGS.now,
|
|
217
|
+
};
|
|
218
|
+
return defineModule("@procedures/schedule", scheduleProcedures(settings));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* This function registers the procedures of @procedures/schedule in a registry. The default is the
|
|
223
|
+
* global registry. The options are the options of `createScheduleModule()`.
|
|
224
|
+
*/
|
|
225
|
+
export function registerScheduleProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY, options: ScheduleModuleOptions = {}): void {
|
|
226
|
+
registerModules(registry, [createScheduleModule(options)]);
|
|
227
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedule.add procedure
|
|
3
|
+
*
|
|
4
|
+
* Add a schedule: a cron expression that starts a stored program with an input. The procedure
|
|
5
|
+
* examines the expression and the program, and keeps the capability of the caller: a run gets
|
|
6
|
+
* the capability of the host, narrowed by it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PROCEDURE_REGISTRY, type ProcedureContext } from "@procedures/client";
|
|
10
|
+
import { formatTime, nextTime, parseCron } from "../../cron.js";
|
|
11
|
+
import { ScheduleError } from "../../errors.js";
|
|
12
|
+
import { DEFAULT_SCHEDULE_SETTINGS, findProgram, type ScheduleSettings } from "../../programs.js";
|
|
13
|
+
import type { Schedule, ScheduleAddInput, ScheduleAddOutput, ScheduleView } from "../../types.js";
|
|
14
|
+
|
|
15
|
+
/** The view of a schedule: with its next time after `now`. */
|
|
16
|
+
export function viewOf(schedule: Schedule, now: number): ScheduleView {
|
|
17
|
+
let next: number | undefined;
|
|
18
|
+
try {
|
|
19
|
+
next = nextTime(parseCron(schedule.cron), now, schedule.timeZone);
|
|
20
|
+
} catch {
|
|
21
|
+
next = undefined;
|
|
22
|
+
}
|
|
23
|
+
return { ...schedule, nextRunAt: next === undefined ? null : formatTime(next, schedule.timeZone) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The capability of the caller to keep: its clearance and its permitted effects, without its deadline. */
|
|
27
|
+
function callerCapability(ctx: Pick<ProcedureContext, "capability"> | undefined): Schedule["capability"] {
|
|
28
|
+
const capability = ctx?.capability;
|
|
29
|
+
if (capability === undefined) return undefined;
|
|
30
|
+
const kept: NonNullable<Schedule["capability"]> = {};
|
|
31
|
+
if (capability.clearance !== undefined) kept.clearance = capability.clearance;
|
|
32
|
+
if (capability.effects !== undefined) kept.effects = capability.effects;
|
|
33
|
+
return Object.keys(kept).length > 0 ? kept : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add a schedule: a cron expression that starts a stored program with an input
|
|
38
|
+
*/
|
|
39
|
+
export async function scheduleAdd(input: ScheduleAddInput, ctx?: Pick<ProcedureContext, "capability" | "registry">, settings: ScheduleSettings = DEFAULT_SCHEDULE_SETTINGS): Promise<ScheduleAddOutput> {
|
|
40
|
+
const now = settings.now();
|
|
41
|
+
const timeZone = input.timeZone ?? "local";
|
|
42
|
+
const cron = parseCron(input.cron);
|
|
43
|
+
if (nextTime(cron, now, timeZone) === undefined) {
|
|
44
|
+
throw new ScheduleError("BAD_CRON", `The cron expression ${JSON.stringify(input.cron)} has no time in the next 9 years`);
|
|
45
|
+
}
|
|
46
|
+
await findProgram(ctx?.registry ?? PROCEDURE_REGISTRY, input.program);
|
|
47
|
+
const id = input.id ?? input.program;
|
|
48
|
+
const capability = callerCapability(ctx);
|
|
49
|
+
const schedule: Schedule = {
|
|
50
|
+
id,
|
|
51
|
+
cron: cron.source,
|
|
52
|
+
program: input.program,
|
|
53
|
+
...(input.input !== undefined ? { input: input.input } : {}),
|
|
54
|
+
timeZone,
|
|
55
|
+
createdAt: now,
|
|
56
|
+
...(capability !== undefined ? { capability } : {}),
|
|
57
|
+
runs: 0,
|
|
58
|
+
failures: 0,
|
|
59
|
+
skipped: 0,
|
|
60
|
+
};
|
|
61
|
+
if (input.replace !== true && (await settings.store.get(id)) !== undefined) {
|
|
62
|
+
throw new ScheduleError("EXISTS", `A schedule named ${id} exists: give replace: true, or another id`);
|
|
63
|
+
}
|
|
64
|
+
await settings.store.put(schedule);
|
|
65
|
+
return { schedule: viewOf(schedule, now) };
|
|
66
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedule.list procedure
|
|
3
|
+
*
|
|
4
|
+
* The schedules, sorted by name, with the next time, the last run and the counts of each.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_SCHEDULE_SETTINGS, type ScheduleSettings } from "../../programs.js";
|
|
8
|
+
import type { ScheduleListInput, ScheduleListOutput } from "../../types.js";
|
|
9
|
+
import { viewOf } from "./add.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The schedules, with the next time and the last run of each
|
|
13
|
+
*/
|
|
14
|
+
export async function scheduleList(input: ScheduleListInput, settings: ScheduleSettings = DEFAULT_SCHEDULE_SETTINGS): Promise<ScheduleListOutput> {
|
|
15
|
+
const now = settings.now();
|
|
16
|
+
const schedules = await settings.store.list();
|
|
17
|
+
return {
|
|
18
|
+
schedules: schedules.filter((schedule) => input.program === undefined || schedule.program === input.program).map((schedule) => viewOf(schedule, now)),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedule.next procedure
|
|
3
|
+
*
|
|
4
|
+
* The next times of a cron expression. It reads no store: the output comes from the input and
|
|
5
|
+
* from the time.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { formatTime, nextTimes, parseCron } from "../../cron.js";
|
|
9
|
+
import { ScheduleError } from "../../errors.js";
|
|
10
|
+
import { DEFAULT_SCHEDULE_SETTINGS, type ScheduleSettings } from "../../programs.js";
|
|
11
|
+
import type { ScheduleNextInput, ScheduleNextOutput } from "../../types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The next times of a cron expression
|
|
15
|
+
*/
|
|
16
|
+
export async function scheduleNext(input: ScheduleNextInput, settings: Pick<ScheduleSettings, "now"> = DEFAULT_SCHEDULE_SETTINGS): Promise<ScheduleNextOutput> {
|
|
17
|
+
const timeZone = input.timeZone ?? "local";
|
|
18
|
+
const cron = parseCron(input.cron);
|
|
19
|
+
let after = settings.now();
|
|
20
|
+
if (input.after !== undefined) {
|
|
21
|
+
after = Date.parse(input.after);
|
|
22
|
+
if (Number.isNaN(after)) throw new ScheduleError("BAD_CRON", `after must be an ISO time, not ${JSON.stringify(input.after)}`);
|
|
23
|
+
}
|
|
24
|
+
return { times: nextTimes(cron, after, input.count ?? 5, timeZone).map((time) => formatTime(time, timeZone)) };
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedule.remove procedure
|
|
3
|
+
*
|
|
4
|
+
* Remove a schedule. A run that started before stays until its end, and its record goes nowhere.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_SCHEDULE_SETTINGS, type ScheduleSettings } from "../../programs.js";
|
|
8
|
+
import type { ScheduleRemoveInput, ScheduleRemoveOutput } from "../../types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Remove a schedule
|
|
12
|
+
*/
|
|
13
|
+
export async function scheduleRemove(input: ScheduleRemoveInput, settings: ScheduleSettings = DEFAULT_SCHEDULE_SETTINGS): Promise<ScheduleRemoveOutput> {
|
|
14
|
+
return { removed: await settings.store.delete(input.id) };
|
|
15
|
+
}
|
package/src/programs.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stored programs that a schedule starts, and the settings of the procedures.
|
|
3
|
+
*
|
|
4
|
+
* A schedule names a stored program: a procedure that `procedure.define` made, or a program of
|
|
5
|
+
* the program store of the registry (`@procedures/store`). A procedure of code is not a program:
|
|
6
|
+
* a schedule cannot start `shell.run` directly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isRuntimeDefined, keyToPath, type ProcedureRegistryLike } from "@procedures/client";
|
|
10
|
+
import { getProgramStore } from "@procedures/store";
|
|
11
|
+
import { ScheduleError } from "./errors.js";
|
|
12
|
+
import { defaultScheduleStore, type ScheduleStore } from "./store.js";
|
|
13
|
+
|
|
14
|
+
/** What the procedures of a module use: the store and the wall clock. */
|
|
15
|
+
export interface ScheduleSettings {
|
|
16
|
+
readonly store: ScheduleStore;
|
|
17
|
+
/** The wall time, in milliseconds since 1970. */
|
|
18
|
+
readonly now: () => number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The settings of the default module: `defaultScheduleStore` and `Date.now`. */
|
|
22
|
+
export const DEFAULT_SCHEDULE_SETTINGS: ScheduleSettings = Object.freeze({ store: defaultScheduleStore, now: () => Date.now() });
|
|
23
|
+
|
|
24
|
+
/** The form of the key of a program: segments without spaces, joined with dots. */
|
|
25
|
+
export const PROGRAM_KEY: RegExp = /^[^.\s]+(\.[^.\s]+)*$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This function tells where the stored program of a key is: in the registry (a procedure that
|
|
29
|
+
* `procedure.define` made), or only in the program store of the registry. It throws
|
|
30
|
+
* `NOT_A_PROGRAM` for a procedure of code and `NOT_FOUND` for no program.
|
|
31
|
+
*/
|
|
32
|
+
export async function findProgram(registry: ProcedureRegistryLike, key: string): Promise<"registry" | "store"> {
|
|
33
|
+
const procedure = registry.get(keyToPath(key));
|
|
34
|
+
if (procedure !== undefined) {
|
|
35
|
+
if (!isRuntimeDefined(procedure)) {
|
|
36
|
+
throw new ScheduleError("NOT_A_PROGRAM", `${key} is a procedure of code, not a stored program. A schedule starts only a program of procedure.define or of the program store.`);
|
|
37
|
+
}
|
|
38
|
+
return "registry";
|
|
39
|
+
}
|
|
40
|
+
const programs = getProgramStore(registry);
|
|
41
|
+
if (programs !== undefined && (await programs.get(key)) !== undefined) return "store";
|
|
42
|
+
throw new ScheduleError("NOT_FOUND", `No stored program has the key ${key}: it is not in the registry and not in its program store`);
|
|
43
|
+
}
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importing this file registers the procedures of @procedures/schedule in the global registry:
|
|
3
|
+
* `import "@procedures/schedule/register"`. The package root registers nothing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { registerScheduleProcedures } from "./module.js";
|
|
7
|
+
|
|
8
|
+
export * from "./module.js";
|
|
9
|
+
|
|
10
|
+
registerScheduleProcedures();
|