Hook
import { Hook, LoggerHook } from "@ecosy/schedule/hook";
A hook is where run outcomes go. Optional — without one, a finished task reports nowhere.
Schedule()
.hook(Hook.combine(LoggerHook(), TelegramHook, MailHook))
Hook#
interface Hook {
notify(event: TaskEvent): Promisable<void>;
}
One hook, not a list — combine turns several into one, so nothing downstream
ever branches on how many there are.
.hook() takes a class; the scheduler constructs it at start().
class TelegramHook implements Hook {
async notify(event: TaskEvent) {
if (event.ok) return;
await fetch(`https://api.telegram.org/bot${TOKEN}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: CHAT_ID,
text: `${event.key} failed (${event.reason}): ${event.detail ?? ""}`,
}),
});
}
}
The hook is notified once per run, with the final attempt's event — not
once per retry. For per-attempt reporting use
.onError().
LoggerHook(logger?)#
function LoggerHook(logger?: { info(...a: unknown[]): void; error(...a: unknown[]): void }): ClassType<Hook>
Writes each outcome as a log line. Defaults to console, so it works before
real logging is wired up.
Schedule().hook(LoggerHook())
Schedule().hook(LoggerHook(appLogger))
[schedule] session.cleanup ok in 42ms
[schedule] report failed (timeout) after 30001ms on attempt 3: request exceeded 30000ms
Note it is a factory — LoggerHook() returns the class. Passing
LoggerHook itself would hand the scheduler a factory to construct instead of a
hook.
Anything with info and error works, including a
@ecosy/logger instance.
Hook.combine#
Hook.combine(...hooks: ClassType<Hook>[]): ClassType<Hook>
Fans one event out to several hooks, as a single hook class.
Schedule().hook(Hook.combine(LoggerHook(), TelegramHook, MetricsHook))
Hooks run concurrently, each isolated:
- One that throws is caught and warned about; the others still run.
- One that hangs is abandoned after 10 seconds; the others still complete.
A Telegram call that fails must not stop the log line from being written, and a slow one must not hold the scheduler — the task has already finished, and reporting is separate work.
The 10-second hook deadline is fixed and independent of the task's own
timeout.
combine flattens, so combine(a, combine(b, c)) and combine(a, b, c)
behave identically. With no arguments it is a valid no-op, which makes it a
usable default rather than something to guard against:
Schedule().hook(Hook.combine(...(isProd ? [TelegramHook] : [])))
TaskEvent#
interface TaskEvent {
key: string;
scheduledFor: Date; // the fire time from the expression, not `now`
startedAt: Date;
durationMs: number;
attempt: number; // 1 for the first try
ok: boolean;
reason?: TaskFailure;
detail?: string;
raw?: unknown;
}
type TaskFailure = "throw" | "status" | "timeout" | "exit" | "unknown";
reason |
|
|---|---|
"throw" |
a handler threw |
"status" |
an api target answered outside 2xx |
"timeout" |
the deadline passed |
"exit" |
a file target exited non-zero, or was killed |
"unknown" |
anything else, including an unregistered handler key |
Normalised across all three target kinds, so a hook never branches on target
type. raw keeps the original — a return value, a response body, or stdout.
scheduledFor is the computed fire time, so two instances a few milliseconds
apart report the same value for the same run.
Schedule-level events#
The scheduler reports its own problems through
.onError(), not through the hook, using the reserved key
@schedule:
Schedule().onError((event, task) => {
if (event.key === "@schedule") {
alert(`scheduler problem: ${event.detail}`);
return;
}
record(event);
});
That covers a source read that failed, an unparseable entry, and a source that returned nothing while tasks are live.