@connextar/house 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +447 -0
- package/dist/jobs/counts.d.ts +82 -0
- package/dist/jobs/counts.d.ts.map +1 -0
- package/dist/jobs/counts.js +112 -0
- package/dist/jobs/counts.js.map +1 -0
- package/dist/jobs/cron-auth.d.ts +44 -0
- package/dist/jobs/cron-auth.d.ts.map +1 -0
- package/dist/jobs/cron-auth.js +20 -0
- package/dist/jobs/cron-auth.js.map +1 -0
- package/dist/jobs/health.d.ts +157 -0
- package/dist/jobs/health.d.ts.map +1 -0
- package/dist/jobs/health.js +146 -0
- package/dist/jobs/health.js.map +1 -0
- package/dist/jobs/index.d.ts +26 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +26 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/job-health-list.d.ts +66 -0
- package/dist/jobs/job-health-list.d.ts.map +1 -0
- package/dist/jobs/job-health-list.js +115 -0
- package/dist/jobs/job-health-list.js.map +1 -0
- package/dist/jobs/react-index.d.ts +19 -0
- package/dist/jobs/react-index.d.ts.map +1 -0
- package/dist/jobs/react-index.js +18 -0
- package/dist/jobs/react-index.js.map +1 -0
- package/dist/jobs/registry.d.ts +104 -0
- package/dist/jobs/registry.d.ts.map +1 -0
- package/dist/jobs/registry.js +72 -0
- package/dist/jobs/registry.js.map +1 -0
- package/dist/jobs/scheduler.d.ts +89 -0
- package/dist/jobs/scheduler.d.ts.map +1 -0
- package/dist/jobs/scheduler.js +282 -0
- package/dist/jobs/scheduler.js.map +1 -0
- package/dist/jobs/store.d.ts +94 -0
- package/dist/jobs/store.d.ts.map +1 -0
- package/dist/jobs/store.js +56 -0
- package/dist/jobs/store.js.map +1 -0
- package/dist/jobs/windows.d.ts +66 -0
- package/dist/jobs/windows.d.ts.map +1 -0
- package/dist/jobs/windows.js +130 -0
- package/dist/jobs/windows.js.map +1 -0
- package/package.json +9 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { errorMessage } from "../ux/errors.js";
|
|
2
|
+
import { countLines, countsSummary, declaredCounts } from "./counts.js";
|
|
3
|
+
import { buildJobView, effectiveTimeout, formatWhen } from "./health.js";
|
|
4
|
+
import { DEFAULT_COOLDOWN_MS, DEFAULT_TIMEOUT_MS, MAX_WINDOW_KEY, } from "./registry.js";
|
|
5
|
+
import { MANUAL_KEY_PREFIX } from "./windows.js";
|
|
6
|
+
const MAX_ERROR = 1000;
|
|
7
|
+
function messageOf(error) {
|
|
8
|
+
const message = errorMessage(error, "It failed without saying why.");
|
|
9
|
+
return message.length > MAX_ERROR ? `${message.slice(0, MAX_ERROR - 1)}…` : message;
|
|
10
|
+
}
|
|
11
|
+
const minutes = (ms) => {
|
|
12
|
+
const n = Math.max(1, Math.round(ms / 60_000));
|
|
13
|
+
return `${n} minute${n === 1 ? "" : "s"}`;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The dispatcher, and everything else that touches the ledger.
|
|
17
|
+
*
|
|
18
|
+
* Declared once in the app beside its jobs and its store adapter; the tick
|
|
19
|
+
* route, the own-schedule routes, the Run now and switch routes and the health
|
|
20
|
+
* page all call this one object.
|
|
21
|
+
*/
|
|
22
|
+
export function createScheduler(options) {
|
|
23
|
+
const { jobs, store } = options;
|
|
24
|
+
const clock = options.clock ?? (() => new Date());
|
|
25
|
+
const fallbackTimeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
26
|
+
const format = { locale: options.locale };
|
|
27
|
+
const find = (id) => jobs.find((job) => job.id === id);
|
|
28
|
+
/** Run a claimed job and record the outcome. Never throws. */
|
|
29
|
+
async function execute(job, runId, context) {
|
|
30
|
+
let counts = null;
|
|
31
|
+
let error = null;
|
|
32
|
+
try {
|
|
33
|
+
counts = declaredCounts(job.counts, await job.run(context));
|
|
34
|
+
}
|
|
35
|
+
catch (thrown) {
|
|
36
|
+
error = messageOf(thrown);
|
|
37
|
+
}
|
|
38
|
+
let unrecorded = null;
|
|
39
|
+
try {
|
|
40
|
+
await store.finishRun(runId, { finishedAt: clock(), ok: error === null, counts, error });
|
|
41
|
+
}
|
|
42
|
+
catch (thrown) {
|
|
43
|
+
// The work is done (or failed) either way; the ledger just doesn't know.
|
|
44
|
+
// It will read as "never finished" once the timeout passes, which is true.
|
|
45
|
+
unrecorded = messageOf(thrown);
|
|
46
|
+
}
|
|
47
|
+
return { counts, error, unrecorded };
|
|
48
|
+
}
|
|
49
|
+
/** The switch, the window, the claim and the run — for one job, with nothing allowed to escape. */
|
|
50
|
+
async function dispatch(job, now, switches, windowKeyFor) {
|
|
51
|
+
const base = { job: job.id };
|
|
52
|
+
try {
|
|
53
|
+
if (job.switchable ?? true) {
|
|
54
|
+
if (switches instanceof Error) {
|
|
55
|
+
return {
|
|
56
|
+
...base,
|
|
57
|
+
status: "failed",
|
|
58
|
+
error: switches.message,
|
|
59
|
+
message: `${job.label} didn't start: whether it is switched on couldn't be read (${switches.message}).`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const state = switches.get(job.id);
|
|
63
|
+
if (state?.paused) {
|
|
64
|
+
// Its window is not claimed, so switched back on inside the same window it still runs.
|
|
65
|
+
const who = state.by ? ` by ${state.by}` : "";
|
|
66
|
+
const when = state.at ? ` on ${formatWhen(state.at.toISOString())}` : "";
|
|
67
|
+
return { ...base, status: "paused", message: `${job.label} is switched off${who}${when}; not started.` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const windowKey = windowKeyFor(now);
|
|
71
|
+
if (windowKey === null)
|
|
72
|
+
return { ...base, status: "not-due", message: `${job.label} is not due.` };
|
|
73
|
+
if (windowKey.length > MAX_WINDOW_KEY) {
|
|
74
|
+
throw new Error(`Its window key is longer than ${MAX_WINDOW_KEY} characters.`);
|
|
75
|
+
}
|
|
76
|
+
const runId = await store.claimRun({ job: job.id, windowKey, tenantId: null, by: null, startedAt: now });
|
|
77
|
+
if (runId === null) {
|
|
78
|
+
return { ...base, windowKey, status: "already-ran", message: `${job.label} has already run in ${windowKey}.` };
|
|
79
|
+
}
|
|
80
|
+
const context = { now, trigger: "schedule", tenantId: null, force: false, windowKey, by: null };
|
|
81
|
+
const { counts, error, unrecorded } = await execute(job, runId, context);
|
|
82
|
+
if (error !== null) {
|
|
83
|
+
return { ...base, windowKey, status: "failed", error, message: `${job.label} failed: ${error}` };
|
|
84
|
+
}
|
|
85
|
+
const summary = countsSummary(countLines(job.counts, counts, format));
|
|
86
|
+
return {
|
|
87
|
+
...base,
|
|
88
|
+
windowKey,
|
|
89
|
+
status: "ran",
|
|
90
|
+
counts: counts ?? {},
|
|
91
|
+
...(unrecorded ? { error: unrecorded } : {}),
|
|
92
|
+
message: unrecorded
|
|
93
|
+
? `${job.label} ran, but its result couldn't be recorded: ${unrecorded}`
|
|
94
|
+
: `${job.label} ran${summary ? `: ${summary}` : ""}.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (thrown) {
|
|
98
|
+
// A store that failed to claim, a window function that threw: this job's
|
|
99
|
+
// problem, reported as this job's result. The next job still runs.
|
|
100
|
+
const error = messageOf(thrown);
|
|
101
|
+
return { ...base, status: "failed", error, message: `${job.label} didn't run: ${error}` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function readSwitches() {
|
|
105
|
+
try {
|
|
106
|
+
return await store.readSwitches();
|
|
107
|
+
}
|
|
108
|
+
catch (thrown) {
|
|
109
|
+
return new Error(messageOf(thrown));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
jobs,
|
|
114
|
+
async tick(now = clock()) {
|
|
115
|
+
const switches = await readSwitches();
|
|
116
|
+
const results = [];
|
|
117
|
+
// One at a time, in declaration order: predictable load, and the order the card lists them in.
|
|
118
|
+
for (const job of jobs) {
|
|
119
|
+
if ((job.schedule ?? "tick") !== "tick")
|
|
120
|
+
continue;
|
|
121
|
+
results.push(await dispatch(job, now, switches, (at) => job.window(at)));
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
},
|
|
125
|
+
async runOwn(jobId, now = clock()) {
|
|
126
|
+
const job = find(jobId);
|
|
127
|
+
if (!job || job.schedule !== "own") {
|
|
128
|
+
throw new Error(`"${jobId}" is not a job with its own schedule.`);
|
|
129
|
+
}
|
|
130
|
+
return dispatch(job, now, await readSwitches(), (at) => job.window ? job.window(at) : `run:${at.toISOString()}`);
|
|
131
|
+
},
|
|
132
|
+
async runNow(jobId, caller, now = clock()) {
|
|
133
|
+
const job = find(jobId);
|
|
134
|
+
if (!job)
|
|
135
|
+
return { status: "refused", job: jobId, reason: "unknown", message: "There is no scheduled job by that name." };
|
|
136
|
+
const refuse = (reason, message) => ({
|
|
137
|
+
status: "refused",
|
|
138
|
+
job: job.id,
|
|
139
|
+
reason,
|
|
140
|
+
message,
|
|
141
|
+
});
|
|
142
|
+
const rule = job.runNow;
|
|
143
|
+
if (!rule)
|
|
144
|
+
return refuse("not-runnable", `${job.label} runs on its schedule only.`);
|
|
145
|
+
if (rule.scope === "tenant" && !caller.tenantId) {
|
|
146
|
+
return refuse("out-of-scope", `${job.label} can only be run for your own organisation, and you aren't in one.`);
|
|
147
|
+
}
|
|
148
|
+
if (rule.scope === "platform" && !caller.platform) {
|
|
149
|
+
return refuse("out-of-scope", `${job.label} runs across every account, so only a platform administrator can run it.`);
|
|
150
|
+
}
|
|
151
|
+
const tenantId = rule.scope === "tenant" ? caller.tenantId : null;
|
|
152
|
+
if ((job.switchable ?? true) && (await store.readSwitches()).get(job.id)?.paused) {
|
|
153
|
+
return refuse("paused", `${job.label} is switched off. Turn it back on to run it.`);
|
|
154
|
+
}
|
|
155
|
+
// Nothing in flight that this run could overlap: the schedule's sweep covers this tenant too.
|
|
156
|
+
const tenants = tenantId === null ? [null] : [null, tenantId];
|
|
157
|
+
const latest = await store.latestRun({ job: job.id, tenants });
|
|
158
|
+
const timeout = effectiveTimeout(job, fallbackTimeout);
|
|
159
|
+
if (latest && !latest.finishedAt && now.getTime() - latest.startedAt.getTime() <= timeout) {
|
|
160
|
+
return refuse("running", `${job.label} is running now — it started ${formatWhen(latest.startedAt.toISOString())}.`);
|
|
161
|
+
}
|
|
162
|
+
const cooldown = rule.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
163
|
+
if (cooldown > 0) {
|
|
164
|
+
const last = await store.latestRun({ job: job.id, tenants: [tenantId], manualOnly: true });
|
|
165
|
+
const since = last ? now.getTime() - last.startedAt.getTime() : Infinity;
|
|
166
|
+
if (since < cooldown) {
|
|
167
|
+
return refuse("too-soon", `${job.label} was run ${minutes(since)} ago. It can be run again in ${minutes(cooldown - since)}.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Two presses in the same breath share a key, so the index lets one through.
|
|
171
|
+
const at = cooldown > 0 ? new Date(Math.floor(now.getTime() / cooldown) * cooldown) : now;
|
|
172
|
+
const windowKey = `${MANUAL_KEY_PREFIX}${tenantId === null ? "platform" : `tenant:${tenantId}`}:${at.toISOString()}`;
|
|
173
|
+
if (windowKey.length > MAX_WINDOW_KEY)
|
|
174
|
+
throw new Error(`The Run now key for ${job.id} is too long to store.`);
|
|
175
|
+
const runId = await store.claimRun({ job: job.id, windowKey, tenantId, by: caller.actor.name, startedAt: now });
|
|
176
|
+
if (runId === null)
|
|
177
|
+
return refuse("too-soon", `${job.label} has just been run.`);
|
|
178
|
+
const context = { now, trigger: "manual", tenantId, force: true, windowKey, by: caller.actor };
|
|
179
|
+
const { counts, error } = await execute(job, runId, context);
|
|
180
|
+
if (error !== null) {
|
|
181
|
+
return { status: "failed", job: job.id, windowKey, error, message: `${job.label} failed: ${error}` };
|
|
182
|
+
}
|
|
183
|
+
const lines = countLines(job.counts, counts, format);
|
|
184
|
+
const summary = countsSummary(lines);
|
|
185
|
+
return {
|
|
186
|
+
status: "ran",
|
|
187
|
+
job: job.id,
|
|
188
|
+
windowKey,
|
|
189
|
+
counts: lines,
|
|
190
|
+
message: `Ran ${job.label}${summary ? ` — ${summary}` : ""}.`,
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
async setPaused(jobId, paused, caller, now = clock()) {
|
|
194
|
+
const job = find(jobId);
|
|
195
|
+
if (!job)
|
|
196
|
+
return { status: "refused", job: jobId, reason: "unknown", message: "There is no scheduled job by that name." };
|
|
197
|
+
if (!(job.switchable ?? true)) {
|
|
198
|
+
return {
|
|
199
|
+
status: "refused",
|
|
200
|
+
job: job.id,
|
|
201
|
+
reason: "not-switchable",
|
|
202
|
+
message: `${job.label} can't be switched off.`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (!caller.platform) {
|
|
206
|
+
return {
|
|
207
|
+
status: "refused",
|
|
208
|
+
job: job.id,
|
|
209
|
+
reason: "out-of-scope",
|
|
210
|
+
message: `Switching ${job.label} off stops it for every account, so only a platform administrator can.`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const current = (await store.readSwitches()).get(job.id)?.paused ?? false;
|
|
214
|
+
if (current === paused) {
|
|
215
|
+
return {
|
|
216
|
+
status: "unchanged",
|
|
217
|
+
job: job.id,
|
|
218
|
+
paused,
|
|
219
|
+
message: paused ? `${job.label} is already paused.` : `${job.label} is already on.`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
await store.writeSwitch({ job: job.id, label: job.label, paused, at: now, by: caller.actor });
|
|
223
|
+
return {
|
|
224
|
+
status: "switched",
|
|
225
|
+
job: job.id,
|
|
226
|
+
paused,
|
|
227
|
+
message: paused
|
|
228
|
+
? `${job.label} is paused. A run already under way finishes; no more start until you turn it back on.`
|
|
229
|
+
: `${job.label} is back on. It runs at its next scheduled time.`,
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
async views(audience, now = clock()) {
|
|
233
|
+
const switches = await store.readSwitches();
|
|
234
|
+
const tenants = audience.tenantId ? [null, audience.tenantId] : [null];
|
|
235
|
+
return Promise.all(jobs.map(async (job) => {
|
|
236
|
+
const [latest, lastWholeSuccess, lastVisibleSuccess] = await Promise.all([
|
|
237
|
+
store.latestRun({ job: job.id, tenants }),
|
|
238
|
+
store.latestRun({ job: job.id, tenants: [null], ok: true }),
|
|
239
|
+
tenants.length > 1 ? store.latestRun({ job: job.id, tenants, ok: true }) : null,
|
|
240
|
+
]);
|
|
241
|
+
return buildJobView(job, {
|
|
242
|
+
switch: switches.get(job.id) ?? null,
|
|
243
|
+
latest,
|
|
244
|
+
lastWholeSuccess,
|
|
245
|
+
lastVisibleSuccess: tenants.length > 1 ? lastVisibleSuccess : lastWholeSuccess,
|
|
246
|
+
}, audience, { now, timeoutMs: fallbackTimeout, locale: options.locale });
|
|
247
|
+
}));
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** The tick in one line: "2 ran, 1 failed, 1 paused, 5 not due". */
|
|
252
|
+
export function summariseTick(results) {
|
|
253
|
+
const order = [
|
|
254
|
+
["ran", "ran"],
|
|
255
|
+
["failed", "failed"],
|
|
256
|
+
["paused", "paused"],
|
|
257
|
+
["already-ran", "already ran"],
|
|
258
|
+
["not-due", "not due"],
|
|
259
|
+
];
|
|
260
|
+
const parts = order.flatMap(([status, words]) => {
|
|
261
|
+
const n = results.filter((result) => result.status === status).length;
|
|
262
|
+
return n ? [`${n} ${words}`] : [];
|
|
263
|
+
});
|
|
264
|
+
return parts.length ? parts.join(", ") : "No scheduled jobs";
|
|
265
|
+
}
|
|
266
|
+
/** The HTTP status a refusal maps to, so every app answers the same refusal the same way. */
|
|
267
|
+
export function refusalStatus(reason) {
|
|
268
|
+
switch (reason) {
|
|
269
|
+
case "unknown":
|
|
270
|
+
return 404;
|
|
271
|
+
case "not-runnable":
|
|
272
|
+
case "not-switchable":
|
|
273
|
+
case "out-of-scope":
|
|
274
|
+
return 403;
|
|
275
|
+
case "paused":
|
|
276
|
+
case "running":
|
|
277
|
+
return 409;
|
|
278
|
+
case "too-soon":
|
|
279
|
+
return 429;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/jobs/scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { JobActor } from "./registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Where runs and switches are kept — the app's, behind this interface.
|
|
4
|
+
*
|
|
5
|
+
* The package owns no schema. Both apps already have a `cron_run` table with
|
|
6
|
+
* the same eight columns, and AltEd keeps its switches in a settings table it
|
|
7
|
+
* shares with the harvest's own screen; a package migration would fight both.
|
|
8
|
+
* The README carries a reference Prisma model and adapter that both existing
|
|
9
|
+
* tables map onto, adding two nullable columns and losing nothing.
|
|
10
|
+
*/
|
|
11
|
+
/** A ledger row as the app stores it. `counts` is whatever JSON is there; it is read through the job's declarations. */
|
|
12
|
+
export interface JobRunRecord {
|
|
13
|
+
id: string;
|
|
14
|
+
job: string;
|
|
15
|
+
windowKey: string;
|
|
16
|
+
/** `null`: a run over every tenant. Otherwise the one tenant a Run now swept. */
|
|
17
|
+
tenantId: string | null;
|
|
18
|
+
/** Who pressed Run now, by name; `null` for the schedule. */
|
|
19
|
+
by: string | null;
|
|
20
|
+
startedAt: Date;
|
|
21
|
+
finishedAt: Date | null;
|
|
22
|
+
ok: boolean;
|
|
23
|
+
counts: unknown;
|
|
24
|
+
error: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface JobRunClaim {
|
|
27
|
+
job: string;
|
|
28
|
+
windowKey: string;
|
|
29
|
+
tenantId: string | null;
|
|
30
|
+
by: string | null;
|
|
31
|
+
startedAt: Date;
|
|
32
|
+
}
|
|
33
|
+
export interface JobRunOutcome {
|
|
34
|
+
finishedAt: Date;
|
|
35
|
+
ok: boolean;
|
|
36
|
+
counts: Record<string, number> | null;
|
|
37
|
+
error: string | null;
|
|
38
|
+
}
|
|
39
|
+
/** "The most recent run of this job, among these tenants" — `null` in the list meaning runs over every tenant. */
|
|
40
|
+
export interface JobRunQuery {
|
|
41
|
+
job: string;
|
|
42
|
+
tenants: readonly (string | null)[];
|
|
43
|
+
/** Successful runs only. */
|
|
44
|
+
ok?: true;
|
|
45
|
+
/** Run now runs only (their key starts `manual:`). */
|
|
46
|
+
manualOnly?: true;
|
|
47
|
+
}
|
|
48
|
+
/** Whether a job is switched off, and the latest change. No entry means on. */
|
|
49
|
+
export interface JobSwitchState {
|
|
50
|
+
paused: boolean;
|
|
51
|
+
at: Date | null;
|
|
52
|
+
by: string | null;
|
|
53
|
+
}
|
|
54
|
+
export interface JobSwitchChange {
|
|
55
|
+
job: string;
|
|
56
|
+
label: string;
|
|
57
|
+
paused: boolean;
|
|
58
|
+
at: Date;
|
|
59
|
+
by: JobActor;
|
|
60
|
+
}
|
|
61
|
+
export interface JobStore {
|
|
62
|
+
/**
|
|
63
|
+
* Insert the run row, claiming `(job, windowKey)`. Resolve with its id, or
|
|
64
|
+
* with `null` when the pair already exists — the unique index refusing it is
|
|
65
|
+
* the idempotency, so that refusal is an answer, not an error. Anything else
|
|
66
|
+
* throws.
|
|
67
|
+
*/
|
|
68
|
+
claimRun(claim: JobRunClaim): Promise<string | null>;
|
|
69
|
+
finishRun(id: string, outcome: JobRunOutcome): Promise<void>;
|
|
70
|
+
latestRun(query: JobRunQuery): Promise<JobRunRecord | null>;
|
|
71
|
+
readSwitches(): Promise<ReadonlyMap<string, JobSwitchState>>;
|
|
72
|
+
/**
|
|
73
|
+
* Record a switch. **Write the audit entry here too, in the same
|
|
74
|
+
* transaction:** the switch keeps only the latest change, and a job paused and
|
|
75
|
+
* forgotten — billing reminders, say — is found by asking who, and when.
|
|
76
|
+
*/
|
|
77
|
+
writeSwitch(change: JobSwitchChange): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A stored switch value, read the way AltEd learnt to: a row that exists but
|
|
81
|
+
* does not read as `{ paused: boolean }` is **paused**. Somebody stored it, and a
|
|
82
|
+
* switch nobody can read should not send a job out spending.
|
|
83
|
+
*/
|
|
84
|
+
export declare function pausedFromStored(value: unknown): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* The whole contract in memory — for tests, the app's included. It keeps the
|
|
87
|
+
* unique `(job, windowKey)` rule exactly as the database would.
|
|
88
|
+
*/
|
|
89
|
+
export declare function createMemoryJobStore(): JobStore & {
|
|
90
|
+
runs: JobRunRecord[];
|
|
91
|
+
switches: Map<string, JobSwitchState>;
|
|
92
|
+
switchLog: JobSwitchChange[];
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/jobs/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG9C;;;;;;;;GAQG;AAEH,wHAAwH;AACxH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,6DAA6D;IAC7D,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,IAAI,CAAC;IACjB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACtC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,kHAAkH;AAClH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IACpC,4BAA4B;IAC5B,EAAE,CAAC,EAAE,IAAI,CAAC;IACV,sDAAsD;IACtD,UAAU,CAAC,EAAE,IAAI,CAAC;CACnB;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAC;IAChB,EAAE,EAAE,IAAI,GAAG,IAAI,CAAC;IAChB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,EAAE,EAAE,IAAI,CAAC;IACT,EAAE,EAAE,QAAQ,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrD,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC5D,YAAY,IAAI,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAC7D;;;;OAIG;IACH,WAAW,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAKxD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,QAAQ,GAAG;IACjD,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACtC,SAAS,EAAE,eAAe,EAAE,CAAC;CAC9B,CA2CA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isManualKey } from "./windows.js";
|
|
2
|
+
/**
|
|
3
|
+
* A stored switch value, read the way AltEd learnt to: a row that exists but
|
|
4
|
+
* does not read as `{ paused: boolean }` is **paused**. Somebody stored it, and a
|
|
5
|
+
* switch nobody can read should not send a job out spending.
|
|
6
|
+
*/
|
|
7
|
+
export function pausedFromStored(value) {
|
|
8
|
+
if (value && typeof value === "object" && typeof value.paused === "boolean") {
|
|
9
|
+
return value.paused;
|
|
10
|
+
}
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The whole contract in memory — for tests, the app's included. It keeps the
|
|
15
|
+
* unique `(job, windowKey)` rule exactly as the database would.
|
|
16
|
+
*/
|
|
17
|
+
export function createMemoryJobStore() {
|
|
18
|
+
const runs = [];
|
|
19
|
+
const switches = new Map();
|
|
20
|
+
const switchLog = [];
|
|
21
|
+
let next = 0;
|
|
22
|
+
return {
|
|
23
|
+
runs,
|
|
24
|
+
switches,
|
|
25
|
+
switchLog,
|
|
26
|
+
async claimRun(claim) {
|
|
27
|
+
if (runs.some((run) => run.job === claim.job && run.windowKey === claim.windowKey))
|
|
28
|
+
return null;
|
|
29
|
+
const id = `run-${++next}`;
|
|
30
|
+
runs.push({ id, ...claim, finishedAt: null, ok: false, counts: null, error: null });
|
|
31
|
+
return id;
|
|
32
|
+
},
|
|
33
|
+
async finishRun(id, outcome) {
|
|
34
|
+
const run = runs.find((row) => row.id === id);
|
|
35
|
+
if (!run)
|
|
36
|
+
throw new Error(`No run ${id}`);
|
|
37
|
+
Object.assign(run, outcome);
|
|
38
|
+
},
|
|
39
|
+
async latestRun(query) {
|
|
40
|
+
const matches = runs.filter((run) => run.job === query.job &&
|
|
41
|
+
query.tenants.includes(run.tenantId) &&
|
|
42
|
+
(!query.ok || run.ok) &&
|
|
43
|
+
(!query.manualOnly || isManualKey(run.windowKey)));
|
|
44
|
+
// Latest by start; on a tie, the one inserted last.
|
|
45
|
+
return matches.reduce((latest, run) => (!latest || run.startedAt.getTime() >= latest.startedAt.getTime() ? run : latest), null);
|
|
46
|
+
},
|
|
47
|
+
async readSwitches() {
|
|
48
|
+
return new Map(switches);
|
|
49
|
+
},
|
|
50
|
+
async writeSwitch(change) {
|
|
51
|
+
switches.set(change.job, { paused: change.paused, at: change.at, by: change.by.name });
|
|
52
|
+
switchLog.push(change);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/jobs/store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAuF3C;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAQ,KAA8B,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACtG,OAAQ,KAA6B,CAAC,MAAM,CAAC;IAC/C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAKlC,MAAM,IAAI,GAAmB,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA0B,CAAC;IACnD,MAAM,SAAS,GAAsB,EAAE,CAAC;IACxC,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,OAAO;QACL,IAAI;QACJ,QAAQ;QACR,SAAS;QACT,KAAK,CAAC,QAAQ,CAAC,KAAK;YAClB,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,KAAK,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,CAAC;gBAAE,OAAO,IAAI,CAAC;YAChG,MAAM,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACpF,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,OAAO;YACzB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9C,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAC1C,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC9B,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,KAAK;YACnB,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CACzB,CAAC,GAAG,EAAE,EAAE,CACN,GAAG,CAAC,GAAG,KAAK,KAAK,CAAC,GAAG;gBACrB,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACpC,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,CAAC;gBACrB,CAAC,CAAC,KAAK,CAAC,UAAU,IAAI,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CACpD,CAAC;YACF,oDAAoD;YACpD,OAAO,OAAO,CAAC,MAAM,CACnB,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,EAClG,IAAI,CACL,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,YAAY;YAChB,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC3B,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,MAAM;YACtB,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;YACvF,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run windows: which window a moment falls in, or none.
|
|
3
|
+
*
|
|
4
|
+
* A window is a function from "now" to a key, or `null` when the job is not due
|
|
5
|
+
* at all. The key is what the ledger's unique index is on, together with the
|
|
6
|
+
* job, so **one run per job per key** is the whole of idempotency: a second tick
|
|
7
|
+
* in the same window finds the key taken and does nothing.
|
|
8
|
+
*
|
|
9
|
+
* Both apps wrote this as two functions — `attempt(now)` and `windowKey(now)` —
|
|
10
|
+
* which could disagree. One function cannot.
|
|
11
|
+
*
|
|
12
|
+
* The helpers carry one lesson each:
|
|
13
|
+
*
|
|
14
|
+
* - **A day is a day somewhere.** AltEd keyed its tracker job on the UTC date
|
|
15
|
+
* while the job took "yesterday" from the UK calendar; in summer a tick after
|
|
16
|
+
* 23:00 UTC and the next at 00:00 UTC fell in different windows but the same
|
|
17
|
+
* UK day, so that day's misses were sent twice and the next day's never. Every
|
|
18
|
+
* calendar helper takes the `timeZone` the job's own idea of a day comes from.
|
|
19
|
+
* - **Not before a civil hour.** `from` keeps a morning email out of midnight —
|
|
20
|
+
* AltEd's weekly digest waits for 07:00 "so nobody's phone lights up".
|
|
21
|
+
* - **A missed start is caught up, not lost.** Quickie's monthly report ran only
|
|
22
|
+
* on the 1st, so a day of failed ticks on the 1st lost the month. `weekly` and
|
|
23
|
+
* `monthly` stay open from their start to the end of their period (after
|
|
24
|
+
* `from` each day), and the key keeps it to one run.
|
|
25
|
+
*
|
|
26
|
+
* Keys match what both apps already store (`2026-09-30`, `2026-09-30T14`,
|
|
27
|
+
* `2026-09-30Q2`, `month:2026-09`), so moving a job onto a helper does not run
|
|
28
|
+
* it a second time in the window it moves in.
|
|
29
|
+
*/
|
|
30
|
+
export type JobWindow = (now: Date) => string | null;
|
|
31
|
+
export interface CalendarWindowOptions {
|
|
32
|
+
/** The first hour of the day the window is open, in `timeZone`. Default 0. */
|
|
33
|
+
from?: number;
|
|
34
|
+
/** Whose calendar. Default `UTC`. A job that reasons about "today" in the UK says `Europe/London`. */
|
|
35
|
+
timeZone?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Once an hour. Key `2026-09-30T14` (UTC) — Quickie's hourly key. */
|
|
38
|
+
export declare function hourly(): JobWindow;
|
|
39
|
+
/**
|
|
40
|
+
* Once every `hours` hours, in blocks from UTC midnight. Key `2026-09-30Q2` —
|
|
41
|
+
* with 6, exactly Quickie's four-a-day bank feed key.
|
|
42
|
+
*/
|
|
43
|
+
export declare function everyHours(hours: number): JobWindow;
|
|
44
|
+
/** Once a day, from `from` o'clock to midnight in `timeZone`. Key `2026-09-30`, the date there. */
|
|
45
|
+
export declare function daily(options?: CalendarWindowOptions): JobWindow;
|
|
46
|
+
/**
|
|
47
|
+
* Once a week, opening on `weekday` (1 = Monday) at `from` and staying open —
|
|
48
|
+
* from `from` each day — until the next one opens. Key `week:2026-09-28`, the
|
|
49
|
+
* date it opened.
|
|
50
|
+
*/
|
|
51
|
+
export declare function weekly(options?: CalendarWindowOptions & {
|
|
52
|
+
weekday?: number;
|
|
53
|
+
}): JobWindow;
|
|
54
|
+
/**
|
|
55
|
+
* Once a month, opening on `day` at `from` and staying open — from `from` each
|
|
56
|
+
* day — to the month's end. Key `month:2026-09` — Quickie's monthly key.
|
|
57
|
+
* `day` is 1–28, so the window opens in February too.
|
|
58
|
+
*/
|
|
59
|
+
export declare function monthly(options?: CalendarWindowOptions & {
|
|
60
|
+
day?: number;
|
|
61
|
+
}): JobWindow;
|
|
62
|
+
/** The prefix every Run now key starts with, in both apps' existing rows and in this module's. */
|
|
63
|
+
export declare const MANUAL_KEY_PREFIX = "manual:";
|
|
64
|
+
/** Whether a stored window key was a Run now rather than the schedule. */
|
|
65
|
+
export declare function isManualKey(windowKey: string): boolean;
|
|
66
|
+
//# sourceMappingURL=windows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows.d.ts","sourceRoot":"","sources":["../../src/jobs/windows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,IAAI,KAAK,MAAM,GAAG,IAAI,CAAC;AAoDrD,MAAM,WAAW,qBAAqB;IACpC,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,sEAAsE;AACtE,wBAAgB,MAAM,IAAI,SAAS,CAElC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAKnD;AAED,mGAAmG;AACnG,wBAAgB,KAAK,CAAC,OAAO,GAAE,qBAA0B,GAAG,SAAS,CAOpE;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,qBAAqB,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CAY5F;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,OAAO,GAAE,qBAAqB,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,CASzF;AAED,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEtD"}
|