@apisurf/ngn-core 0.4.0 → 0.5.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.
Files changed (3) hide show
  1. package/dist/core.d.ts +461 -80
  2. package/dist/core.js +1627 -192
  3. package/package.json +4 -4
package/dist/core.d.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { RunStage } from '@apisurf/ngn-schema/describe';
2
- export { RunStage } from '@apisurf/ngn-schema/describe';
3
- import { FsNodeFileDescriptor } from '@apisurf/ngn-os';
4
1
  import { Client, InArgs, ResultSet, InStatement, InValue } from '@apisurf/ngn-persistence';
5
2
  export { initDbFileIfNotExists } from '@apisurf/ngn-persistence';
6
- import { Cron } from 'croner';
3
+ import { ScheduleFields, ScheduleEventName, TickOrigin, RunStage } from '@apisurf/ngn-schema/describe';
4
+ export { RunStage } from '@apisurf/ngn-schema/describe';
5
+ import { FsNodeFileDescriptor } from '@apisurf/ngn-os';
7
6
 
8
7
  type CompilerOptions = {
9
8
  /** Minify output */
@@ -59,6 +58,95 @@ declare class Compiler {
59
58
  */
60
59
  declare function executeEsm(code: string): Promise<Record<string, unknown>>;
61
60
 
61
+ /**
62
+ * Named leases in `locks`.
63
+ *
64
+ * Taking one is a single upsert that only overwrites a row whose lease has
65
+ * run out, so two processes on one file cannot both win, and a lease left by
66
+ * a process that died is free once it expires. Every acquisition gets its own
67
+ * holder token: two runs of one task in one process are two holders.
68
+ */
69
+ declare class LockService {
70
+ private db;
71
+ constructor(db: Client);
72
+ /** The holder token on success, null when someone else holds an unexpired lease. */
73
+ tryAcquire(name: string, ttlMs: number, taskRunId: number | null): Promise<string | null>;
74
+ /** Push the lease out; false when it was lost (it expired and someone took it). */
75
+ renew(name: string, holder: string, ttlMs: number): Promise<boolean>;
76
+ /** Who holds `name`, and until when; null when nobody has it. Expired leases included. */
77
+ holderOf(name: string): Promise<{
78
+ holder: string;
79
+ taskRunId: number | null;
80
+ expiresAt: number;
81
+ } | null>;
82
+ /** Record which run a held lease is working for. */
83
+ setRun(name: string, holder: string, taskRunId: number): Promise<void>;
84
+ release(name: string, holder: string): Promise<void>;
85
+ }
86
+
87
+ /**
88
+ * A length of time as tasks write it: milliseconds, or a string such as
89
+ * `"500ms"`, `"30s"`, `"4m"`, `"2h"` or `"1d"`.
90
+ */
91
+ type Duration = number | `${number}${"ms" | "s" | "m" | "h" | "d"}`;
92
+
93
+ type Edge = "leading" | "trailing";
94
+ /** The object form of the `timing` export: a period instead of a cron pattern. */
95
+ interface IntervalTiming {
96
+ /** How far apart runs are. At least 1s. */
97
+ every: Duration;
98
+ /**
99
+ * `"leading"` (default): a fixed grid, `every` apart from the planned start
100
+ * of the last run. `"trailing"`: `every` after the last run ended.
101
+ */
102
+ edge?: Edge;
103
+ /** The shortest delay a run may choose. At least 1s; defaults to `every`. */
104
+ min?: Duration;
105
+ /** The longest delay anything may choose. Defaults to `every × 32`. */
106
+ max?: Duration;
107
+ /**
108
+ * How failures slow the task down: `every × factor^failedInARow`, capped at
109
+ * `max`, give or take `jitter` of it. `false` keeps failures on `every`.
110
+ * Defaults to `{ factor: 2, jitter: 0.1 }`.
111
+ */
112
+ backoff?: TimingBackoff | false;
113
+ }
114
+ interface TimingBackoff {
115
+ /** What each failure in a row multiplies the delay by. At least 1. */
116
+ factor?: number;
117
+ /** How far the delay may stray either way, as a fraction of it, from 0 to 1. */
118
+ jitter?: number;
119
+ }
120
+ /** What a task's `timing` export can be. */
121
+ type TimingExport = string | IntervalTiming;
122
+ type ResolvedTiming = {
123
+ kind: "cron";
124
+ pattern: string;
125
+ } | {
126
+ kind: "interval";
127
+ everyMs: number;
128
+ edge: Edge;
129
+ minMs: number;
130
+ maxMs: number;
131
+ backoff: Required<TimingBackoff> | null;
132
+ };
133
+ /**
134
+ * The `timing` export, checked: a cron pattern, or `{ every, edge, min, max, backoff }`. Throws,
135
+ * naming the task, on anything else, so a typo fails at schedule time.
136
+ */
137
+ declare function resolveTiming(value: unknown, taskPath: string): ResolvedTiming;
138
+ /**
139
+ * The timing `--timing` takes on the command line: a duration (`5m`) is
140
+ * `{ every }` on the leading edge, anything else a cron pattern. Null when it is
141
+ * neither.
142
+ */
143
+ declare function timingFromFlag(flag: string): TimingExport | null;
144
+ /** What the `next` hook chose, checked: a delay in ms or one of the task's own, and why. */
145
+ interface NextChoice {
146
+ delay: number | "every" | "min" | "max";
147
+ reason: string | null;
148
+ }
149
+
62
150
  /**
63
151
  * A database migration
64
152
  */
@@ -130,30 +218,6 @@ declare function closeTaskDatabases(): void;
130
218
  */
131
219
  declare function releaseTaskDatabases(taskSourcePath: string): void;
132
220
 
133
- /**
134
- * A length of time as tasks write it: milliseconds, or a string such as
135
- * `"500ms"`, `"30s"`, `"4m"`, `"2h"` or `"1d"`.
136
- */
137
- type Duration = number | `${number}${"ms" | "s" | "m" | "h" | "d"}`;
138
-
139
- /**
140
- * Named leases in `locks`.
141
- *
142
- * Taking one is a single upsert that only overwrites a row whose lease has
143
- * run out, so two processes on one file cannot both win, and a lease left by
144
- * a process that died is free once it expires. Every acquisition gets its own
145
- * holder token: two runs of one task in one process are two holders.
146
- */
147
- declare class LockService {
148
- private db;
149
- constructor(db: Client);
150
- /** The holder token on success, null when someone else holds an unexpired lease. */
151
- tryAcquire(name: string, ttlMs: number, taskRunId: number | null): Promise<string | null>;
152
- /** Push the lease out; false when it was lost (it expired and someone took it). */
153
- renew(name: string, holder: string, ttlMs: number): Promise<boolean>;
154
- release(name: string, holder: string): Promise<void>;
155
- }
156
-
157
221
  interface LockOptions {
158
222
  /**
159
223
  * How long the lease lasts if this process stops renewing it (it dies, or
@@ -451,6 +515,15 @@ declare class Queue<T = unknown> {
451
515
  stats(): Promise<QueueStats>;
452
516
  }
453
517
 
518
+ interface ResolvedRetry {
519
+ attempts: number;
520
+ delayMs: number;
521
+ breaker: {
522
+ after: number;
523
+ maxSkip: number;
524
+ } | null;
525
+ }
526
+
454
527
  /**
455
528
  * The run this context belongs to, and how long it has. Times are epoch
456
529
  * milliseconds, as everywhere in ngn's database.
@@ -458,9 +531,13 @@ declare class Queue<T = unknown> {
458
531
  interface TaskRun {
459
532
  /** This run's `task_runs.id`. */
460
533
  id: number;
461
- /** When the tick that started it fired; null for a run with no schedule. */
534
+ /** When the tick that started it was planned for; null for a run with no schedule. */
462
535
  scheduledAt: number | null;
463
- /** When the next tick is due; null for a run with no schedule. */
536
+ /**
537
+ * When the next tick is due: the next slot of a cron pattern or the leading
538
+ * edge. Null on the trailing edge, whose next run depends on when this one
539
+ * ends, and for a run with no schedule.
540
+ */
464
541
  nextAt: number | null;
465
542
  /**
466
543
  * When to stop starting new work: shortly before `nextAt`, or sooner with a
@@ -527,11 +604,118 @@ interface TaskContext {
527
604
  sqlite: TaskSqlite;
528
605
  }
529
606
  /**
530
- * What a scheduled tick does when the task's previous run is still going:
531
- * `"skip"` records the tick as a skipped run, `"allow"` starts another run
532
- * beside it.
607
+ * ngn runs one run of a task at a time, so `"skip"` is the only policy: a run
608
+ * that comes due while one is going waits for it. `"allow"` fails at schedule
609
+ * time; it is kept in the type so the message can say why.
533
610
  */
534
611
  type OverlapPolicy = "skip" | "allow";
612
+ interface NextOutcomeBase {
613
+ /** Failed ticks in a row, this one included. */
614
+ failedInARow: number;
615
+ /** The delay that led to this tick, in ms; null when there was none to measure. */
616
+ previousDelayMs: number | null;
617
+ /** The `task_runs.id` of the tick's final run. */
618
+ runId: number;
619
+ }
620
+ /** What the `next` hook is told about the tick that just ended. */
621
+ type NextOutcome<Result = any> = NextOutcomeBase & ({
622
+ status: "success";
623
+ result: Result;
624
+ error: undefined;
625
+ skipReason: null;
626
+ } | {
627
+ status: "failure";
628
+ result: undefined;
629
+ error: Error;
630
+ skipReason: null;
631
+ } | {
632
+ status: "skipped";
633
+ result: undefined;
634
+ error: undefined;
635
+ /** Why ngn skipped it; null for a skip by the task's own `shouldSkip`. */
636
+ skipReason: string | null;
637
+ });
638
+ /**
639
+ * When to run next: a duration from when the tick ended, or the task's own
640
+ * `every`, `min` or `max`. ngn keeps it within `[min, max]`.
641
+ */
642
+ type NextDelay = Duration | "every" | "min" | "max";
643
+ /** What the `next` hook returns: undefined leaves it to the default policy. */
644
+ type NextDecision = undefined | void | NextDelay | {
645
+ in: NextDelay;
646
+ reason?: string;
647
+ };
648
+
649
+ interface ScheduleRow extends ScheduleFields {
650
+ file_task_id: number;
651
+ updated_at: number;
652
+ }
653
+ /** What a transition writes, besides the state the table gives it. */
654
+ interface Transition {
655
+ event: ScheduleEventName;
656
+ at: number;
657
+ /** Row fields to set; anything left out keeps its value. */
658
+ set?: Partial<Omit<ScheduleFields, "state">>;
659
+ origin?: TickOrigin | null;
660
+ firstRunId?: number | null;
661
+ runId?: number | null;
662
+ /** Only apply while the row's holder is this one (a tick's own lease, or none). */
663
+ holder?: string | null;
664
+ /** The tick's final run, to record the next run and its reason on. */
665
+ finalRunId?: number | null;
666
+ /**
667
+ * Only apply while the row's pending request is this one, so a request made
668
+ * after it was read is not lost.
669
+ */
670
+ requestIs?: string | null;
671
+ /**
672
+ * A second transition to make in the same write, from the state this one
673
+ * leaves: `paused` after the decision of a tick a pause was requested during.
674
+ */
675
+ followedBy?: Omit<Transition, "followedBy" | "holder" | "requestIs" | "finalRunId">;
676
+ }
677
+ /** Why a transition was not applied. */
678
+ type Refusal = {
679
+ refused: string;
680
+ };
681
+ /**
682
+ * The schedule rows and their event log. Every change is one write batch: the
683
+ * row update, conditional on the state the event comes from, then the event,
684
+ * copied from the row it left, inserted only if the update applied. The row is
685
+ * therefore always the fold of its events. A write the state table does not
686
+ * allow, or one that lost a race with another process, is refused and the row
687
+ * is left as it was.
688
+ */
689
+ declare class ScheduleStore {
690
+ private db;
691
+ constructor(db: Client);
692
+ read(taskId: number): Promise<ScheduleRow | null>;
693
+ /**
694
+ * Apply `change` to the task's schedule, as it stands now. The new row, or
695
+ * why it was refused.
696
+ */
697
+ transition(taskId: number, change: Transition): Promise<ScheduleRow | Refusal>;
698
+ private insert;
699
+ /**
700
+ * The row update, conditional on the state it comes from (and the holder or
701
+ * request the change names). `after`: the time of the transition this one
702
+ * follows in the same write, which it applies only on top of.
703
+ */
704
+ private update;
705
+ /** The next run and its reason, on the tick's final run, written only if the event was. */
706
+ private finalRun;
707
+ /** The event, copied from the row the update left, written only if it applied. */
708
+ private event;
709
+ /**
710
+ * Close the task's runs that never recorded an end, as failed in
711
+ * `interrupted` with `why` as the error, in one write. `fromRunId` limits it
712
+ * to one tick's runs. Returns the ids closed, oldest first.
713
+ */
714
+ closeOpenRuns(taskId: number, why: string, { fromRunId, at }: {
715
+ fromRunId?: number;
716
+ at: number;
717
+ }): Promise<number[]>;
718
+ }
535
719
 
536
720
  interface CompiledTaskCallbacks {
537
721
  onTaskLoadComplete?: ({ paths, md5Hash, compiledCode, }: {
@@ -627,13 +811,74 @@ type TaskConfig = {
627
811
  env: TaskContext["env"];
628
812
  kv: TaskContext["kv"];
629
813
  sqlite: TaskContext["sqlite"];
814
+ /** The run database's schedules and leases, for whatever schedules or starts the task. */
815
+ scheduling: {
816
+ store: ScheduleStore;
817
+ locks: LockService;
818
+ };
630
819
  };
631
820
 
821
+ /**
822
+ * What time it is, and how to wait: the one seam the scheduler takes time
823
+ * through, so a test can drive grids, sleeps and restarts without patching
824
+ * global timers (which task code, in its own VM context, never sees anyway).
825
+ */
826
+ interface Clock {
827
+ now(): number;
828
+ /** Resolve after `ms`, or as soon as `signal` aborts. Never rejects. */
829
+ sleep(ms: number, signal?: AbortSignal): Promise<void>;
830
+ }
831
+ declare const systemClock: Clock;
832
+
632
833
  interface ExecuteSchedule {
633
834
  /** When the tick fired, epoch ms. */
634
835
  scheduledAt?: number | null;
635
836
  /** When the next tick is due, epoch ms. */
636
837
  nextAt?: number | null;
838
+ /**
839
+ * For a run with no next tick: its deadline when it has no budget, measured
840
+ * from its start (the trailing edge's `every`).
841
+ */
842
+ periodMs?: number | null;
843
+ /** Where the tick reads the time and waits between retries. */
844
+ clock?: Clock;
845
+ /** Aborts every attempt's `ctx.run.signal`: ngn is stopping, or lost the task's lease. */
846
+ signal?: AbortSignal;
847
+ /** Told each attempt's run id as its row is opened. */
848
+ onRunStarted?: (runId: number) => void;
849
+ /**
850
+ * Ask the task's `next` hook, if it has one, when to run next, once the
851
+ * tick has ended: a scheduled tick of an interval task passes this.
852
+ */
853
+ next?: {
854
+ previousDelayMs: number | null;
855
+ timeoutMs?: number;
856
+ };
857
+ }
858
+ type TickStatus = "success" | "failure" | "skipped";
859
+ /**
860
+ * How one tick ended, every way it can: the task ran and returned, it failed
861
+ * (in any stage, after any retries), or ngn or `shouldSkip` held it back.
862
+ * Whatever decides when the task runs next reads this.
863
+ */
864
+ interface TickOutcome {
865
+ status: TickStatus;
866
+ /** What the task returned, for a success. */
867
+ result: unknown;
868
+ /** What the final attempt threw, for a failure. */
869
+ error: unknown;
870
+ /** Why ngn skipped the tick; null for a skip by the task's own `shouldSkip`, or no skip. */
871
+ skipReason: string | null;
872
+ /** The `task_runs.id` of the tick's first attempt; null when it failed before a run existed. */
873
+ firstRunId: number | null;
874
+ /** The `task_runs.id` of the tick's final attempt; null when it failed before a run existed. */
875
+ runId: number | null;
876
+ /** Attempts made, the first included. */
877
+ attempts: number;
878
+ /** When the final attempt ended, on the tick's clock. */
879
+ endedAt: number;
880
+ /** What the `next` hook chose; null when it was not asked, or left it to the policy. */
881
+ next: NextChoice | null;
637
882
  }
638
883
  /** The `task_runs.id` of the run that threw `error`, when a run recorded it. */
639
884
  declare function failedRunId(error: unknown): number | null;
@@ -654,10 +899,9 @@ declare class Task {
654
899
  private kv;
655
900
  private sqlite;
656
901
  private meta;
902
+ private schedulingServices;
657
903
  private entry;
658
904
  private entryExports;
659
- /** Executions started and not yet settled. */
660
- private inFlight;
661
905
  constructor({ entryParams, buildConfigFn, }: {
662
906
  entryParams: EntryParams;
663
907
  buildConfigFn: BuildCompiledTaskConfigFn;
@@ -669,25 +913,28 @@ declare class Task {
669
913
  */
670
914
  get taskPath(): string;
671
915
  get codeHash(): string | null;
672
- get timingPattern(): string | undefined;
673
- /**
674
- * The task's `overlap` export, `"skip"` when it has none. Throws on anything
675
- * else, naming the task, so a typo fails at schedule time.
676
- */
677
- get overlapPolicy(): OverlapPolicy;
916
+ /** The `timing` export, as the latest run (or `loadEntry`) read it. */
917
+ get timing(): TimingExport | undefined;
918
+ /** The `next` export, as loaded: unchecked, for whoever schedules the task to check. */
919
+ get nextHook(): unknown;
678
920
  /**
679
921
  * Check the exports that shape how the task is scheduled, so a bad value
680
- * fails when the task is scheduled rather than on its first tick.
922
+ * fails when the task is scheduled rather than on its first tick. ngn runs
923
+ * one run of a task at a time, so `overlap` may only say so.
681
924
  */
682
925
  validateExports(): void;
683
- /** Whether an execution of this task is in progress. */
684
- get isRunning(): boolean;
926
+ /** The `retry` export, checked. */
927
+ get retryPolicy(): ResolvedRetry;
928
+ /** The schedule and lease services this task's runs are recorded with. */
929
+ get scheduling(): {
930
+ store: ScheduleStore;
931
+ locks: LockService;
932
+ };
685
933
  /**
686
- * Record a run that ngn decided not to start, without loading or calling any
687
- * of the task's code. Needs the task to have been registered by an earlier
688
- * execution, which is always so for a run skipped because one is in progress.
934
+ * Load the task file and register it and its version, without running it:
935
+ * its schedule is kept against the task's id. Returns that id.
689
936
  */
690
- skip(reason: string): Promise<void>;
937
+ register(): Promise<number>;
691
938
  hasTaskExport(): boolean;
692
939
  hasTimingExport(): boolean;
693
940
  loadEntry(): Promise<void>;
@@ -712,18 +959,39 @@ declare class Task {
712
959
  */
713
960
  private runHook;
714
961
  /**
715
- * Run the task for one tick, retrying as its `retry` export allows. A
716
- * scheduled tick passes when it fired and when the next one is due, which
717
- * `ctx.run.deadline` and the retry window are worked out from.
962
+ * Run the task for one tick, retrying as its `retry` export allows, and say
963
+ * how it ended. A scheduled tick passes when it fired and when the next one
964
+ * is due, which `ctx.run.deadline` and the retry window are worked out from.
965
+ * Never throws: a failure is an outcome, its error recorded on its run.
966
+ */
967
+ executeTick(schedule?: ExecuteSchedule): Promise<TickOutcome>;
968
+ /**
969
+ * One tick, for a caller that wants the task's value: what it returned, or
970
+ * undefined for a skip. Throws what the tick failed with.
718
971
  */
719
- execute(schedule?: ExecuteSchedule): Promise<void>;
972
+ execute(schedule?: ExecuteSchedule): Promise<unknown>;
720
973
  private executeWithRetries;
974
+ /**
975
+ * Ask the `next` hook when to run next. What it throws, returns that it may
976
+ * not, or a hook that takes too long, is recorded on the tick's final run in
977
+ * stage `next`, and leaves the decision to the policy, as does a tick that
978
+ * never got as far as loading the task's exports.
979
+ */
980
+ private askNext;
981
+ /** The tick's attempts, retrying as `retry` allows, and how the last one ended. */
982
+ private attempts;
721
983
  /**
722
984
  * Retry only a run that got as far as running: a module that will not load
723
985
  * fails the same way every time. Then it has to fit before the next tick,
724
986
  * and the task's `shouldRetry` gets the last word.
725
987
  */
726
988
  private shouldRetry;
989
+ /**
990
+ * The task's latest ticks that failed in a row, from its run history, so a
991
+ * restart does not reset the count. Ticks closed as interrupted count, since
992
+ * their runs were closed as failed; skipped ticks neither break nor extend it.
993
+ */
994
+ failedInARow(): Promise<number>;
727
995
  /**
728
996
  * Whether the breaker holds this tick back: after `after` failed ticks in a
729
997
  * row it skips 1, 2, 4… ticks (counted since the latest failure), letting
@@ -765,27 +1033,132 @@ declare class TaskLibrary {
765
1033
  tasksRootDir: string;
766
1034
  buildConfigFn: BuildCompiledTaskConfigFn;
767
1035
  }): void;
768
- execute(taskPath: string): Promise<void>;
1036
+ execute(taskPath: string): Promise<unknown>;
769
1037
  getTasks(): Task[];
770
1038
  }
771
1039
 
772
- type Options$1 = {
773
- taskLibrary: TaskLibrary;
774
- /** Called when a scheduled run throws, with the run it was recorded as when there is one. */
775
- onRunError?: (path: string, error: unknown, runId: number | null) => void;
1040
+ /**
1041
+ * What `ngn pause`, `ngn resume` and `ngn next` do to a task's schedule, and
1042
+ * what ngnui's buttons do through `ngn run`: each one transition from the
1043
+ * state table, written straight to the database with source `cli`. No live
1044
+ * runtime is needed; a running scheduler sees the changed row within a step.
1045
+ *
1046
+ * | Command | waiting, blocked | running | paused |
1047
+ * | -------- | ---------------- | ------------------ | ------------------- |
1048
+ * | pause | paused | pause_requested | no change |
1049
+ * | resume | no change | no change | resumed |
1050
+ * | next | rescheduled | override_requested | refused |
1051
+ *
1052
+ * A request made during a run waits in `schedules.request` for the run to end.
1053
+ */
1054
+
1055
+ type ScheduleCommand = {
1056
+ kind: "pause";
1057
+ } | {
1058
+ kind: "resume";
1059
+ }
1060
+ /** `delayMs` from now; 0 for `now`. */
1061
+ | {
1062
+ kind: "next";
1063
+ delayMs: number;
776
1064
  };
777
- /** What `runNow()` did: started a run, or why it did not. */
1065
+ interface ScheduleControlTarget {
1066
+ store: ScheduleStore;
1067
+ taskId: number;
1068
+ /** The task as the person named it, for the messages. */
1069
+ name: string;
1070
+ /** The task's cron pattern, when it runs on one: resuming it takes the next slot. */
1071
+ pattern: string | null;
1072
+ now: number;
1073
+ }
1074
+ type ScheduleControlResult = {
1075
+ applied: true;
1076
+ event: ScheduleEventName;
1077
+ row: ScheduleRow;
1078
+ message: string;
1079
+ }
1080
+ /** `error`: the command could not do what was asked (as opposed to there being nothing to do). */
1081
+ | {
1082
+ applied: false;
1083
+ error: boolean;
1084
+ message: string;
1085
+ };
1086
+ /**
1087
+ * Apply `command` to the task's schedule as it stands. If the scheduler moves
1088
+ * the row on between the read and the write, the command reads it again and
1089
+ * acts on the new state, once.
1090
+ */
1091
+ declare function controlSchedule(target: ScheduleControlTarget, command: ScheduleCommand): Promise<ScheduleControlResult>;
1092
+
1093
+ /**
1094
+ * When a task runs next, for each kind of timing. The scheduler's loop is the
1095
+ * same for all of them; only these answers differ.
1096
+ *
1097
+ * The leading edge and cron both run on slots: the leading edge's are `every`
1098
+ * apart from an anchor, a pattern's are wherever it matches. After a tick, the
1099
+ * next run is the first slot that fits: the slot after the tick's planned
1100
+ * time, or, when the run overran it, the first slot after it ended. Missed
1101
+ * slots are never made up with back-to-back runs.
1102
+ */
1103
+
1104
+ /** The window a run gets: what `ctx.run` and the retry window are worked out from. */
1105
+ interface RunWindow {
1106
+ scheduledAt: number;
1107
+ nextAt: number | null;
1108
+ /** For the trailing edge: the run's deadline when it has no budget. */
1109
+ periodMs: number | null;
1110
+ }
1111
+
1112
+ /** What Run now did: started a run, or why it did not. */
778
1113
  type RunNowOutcome = {
779
1114
  started: true;
780
1115
  } | {
781
1116
  started: false;
782
1117
  reason: string;
783
1118
  };
784
- /** What `start()` did with each task: scheduled on its pattern, or left out and why. */
1119
+ /** How long `stop()` waits for runs in progress before closing them as interrupted. */
1120
+ declare const STOP_GRACE_MS = 10000;
1121
+ /** The services a manual run uses, and the task's id they are keyed by. */
1122
+ interface ManualRunDeps {
1123
+ task: Task;
1124
+ taskId: number;
1125
+ clock: Clock;
1126
+ leaseTtlMs?: number;
1127
+ /** The run's window, when it has one: Run now's is the schedule's next run. */
1128
+ window?: Pick<RunWindow, "nextAt" | "periodMs">;
1129
+ onProblem: (message: string) => void;
1130
+ }
1131
+ /**
1132
+ * `ngn run x --once`: one run now, through the task's lease, from a process of
1133
+ * its own. Refused, writing nothing, while another run of the task is going.
1134
+ */
1135
+ declare function runOnce(task: Task, { clock, leaseTtlMs, onProblem }: Pick<ManualRunDeps, "clock" | "leaseTtlMs" | "onProblem">): Promise<TickOutcome | {
1136
+ busy: string;
1137
+ }>;
1138
+
1139
+ type Options$1 = {
1140
+ taskLibrary: TaskLibrary;
1141
+ /** Called when a scheduled run throws, with the run it was recorded as when there is one. */
1142
+ onRunError?: (path: string, error: unknown, runId: number | null) => void;
1143
+ /** Something went wrong with a task's schedule itself (a refused transition, a bad timing change). */
1144
+ onScheduleProblem?: (path: string, message: string) => void;
1145
+ /** Schedule every task on this instead of its own `timing` (`ngn run x --timing`). */
1146
+ timing?: TimingExport;
1147
+ /** Where the scheduler reads the time and waits. Tests pass their own. */
1148
+ clock?: Clock;
1149
+ /** How long a task's lease outlives a process that stopped renewing it. */
1150
+ leaseTtlMs?: number;
1151
+ /** Where backoff's jitter comes from. Tests pass their own. */
1152
+ random?: () => number;
1153
+ /** How long a task's `next` hook may take. Tests pass their own. */
1154
+ nextTimeoutMs?: number;
1155
+ };
1156
+
1157
+ /** What `start()` did with each task: scheduled on its timing, or left out and why. */
785
1158
  interface ScheduleReport {
786
1159
  scheduled: {
787
1160
  path: string;
788
- pattern: string;
1161
+ timing: TimingExport;
789
1162
  }[];
790
1163
  failed: {
791
1164
  path: string;
@@ -794,25 +1167,38 @@ interface ScheduleReport {
794
1167
  }
795
1168
  declare class TaskLibraryScheduler {
796
1169
  private taskLibrary;
797
- private onRunError;
1170
+ private options;
798
1171
  private scheduledTasks;
799
1172
  constructor(options: Options$1);
800
1173
  start(): Promise<ScheduleReport>;
801
- stop(): Promise<void>;
802
- scheduleTask(task: Task): Promise<void>;
803
- /**
804
- * Start one run of a scheduled task now, outside its schedule, the way a tick
805
- * would: retries, budget, locks and hooks all apply, and errors go where a
806
- * tick's go. It is not a tick, so the breaker does not hold it back. Resolves
807
- * once the run is under way, not when it ends: a run can take longer than
808
- * anyone waits on a button.
1174
+ /**
1175
+ * Stop starting runs, and give the runs in progress up to `graceMs` to end
1176
+ * before closing them as interrupted.
1177
+ */
1178
+ stop(options?: {
1179
+ graceMs?: number;
1180
+ }): Promise<void>;
1181
+ /** Stop as a process that died would, writing nothing. For tests standing in for a crash. */
1182
+ abandon(): void;
1183
+ /**
1184
+ * Check a loaded task's exports and start its schedule. The timing it is
1185
+ * scheduled on is returned; anything wrong with the exports is thrown,
1186
+ * naming the task, so it fails now rather than on the first tick.
1187
+ */
1188
+ scheduleTask(task: Task): Promise<TimingExport>;
1189
+ /**
1190
+ * Start one run of a scheduled task now, outside its schedule: retries,
1191
+ * budget, locks and hooks all apply, and errors go where a tick's go. It is
1192
+ * not a tick, so the breaker does not hold it back, and it leaves the next
1193
+ * run where it was. Resolves once the run is under way, not when it ends.
809
1194
  *
810
- * A run still going is reported rather than recorded as a skip, since nothing
811
- * was due: the person asking can simply wait.
1195
+ * A run of the task still going, in this process or another, is reported
1196
+ * rather than recorded as a skip: the person asking can simply wait.
812
1197
  */
813
- runNow(taskPath: string): RunNowOutcome;
814
- unscheduleTask(task: Task): void;
815
- isCronPatternValid(cronPattern: string): boolean;
1198
+ runNow(taskPath: string): Promise<RunNowOutcome>;
1199
+ /** Pause, resume or move a scheduled task's next run, as `ngn pause`, `resume` and `next` do. */
1200
+ control(taskPath: string, command: ScheduleCommand): Promise<ScheduleControlResult>;
1201
+ unscheduleTask(task: Task): Promise<void>;
816
1202
  }
817
1203
 
818
1204
  type Options = {
@@ -825,11 +1211,6 @@ declare class TaskOnceRunner {
825
1211
  }
826
1212
 
827
1213
  declare function isCronPatternValid(pattern: string): boolean;
828
- /**
829
- * Schedule a loaded task on `pattern`. The task's exports are checked here, so
830
- * a bad `overlap` or `budget` fails now rather than on the first tick.
831
- */
832
- declare function scheduleTask(task: Task, pattern: string, onError: (error: unknown) => void): Cron<undefined>;
833
1214
 
834
1215
  declare function setupDbClient(dbPath: string): Promise<Client>;
835
1216
  declare function getDbClient(): Client;
@@ -842,4 +1223,4 @@ declare function createControlsGenerator({ dbClient, env, report, }: {
842
1223
  report?: (problem: RunProblem) => void;
843
1224
  }): BuildCompiledTaskConfigFn;
844
1225
 
845
- export { type AddJob, type Backoff, type ClaimOptions, Compiler, type DBInstance, type Duration, type InitDBOptions, type Job, type JobFilter, type JobStatus, LockBusyError, type LockOptions, type Migration, type OverlapPolicy, Queue, type QueueOptions, type QueueStats, type RunNowOutcome, type RunProblem, type ScheduleReport, Task, type TaskContext, TaskLibrary, TaskLibraryScheduler, TaskOnceRunner, type TaskRun, type TaskSqlite, type WorkHandler, type WorkOptions, type WorkSummary, type WorkerHandle, closeTaskDatabases, createControlsGenerator, executeEsm, failedRunId, getDbClient, isCronPatternValid, releaseTaskDatabases, runFailure, scheduleTask, setupDbClient };
1226
+ export { type AddJob, type Backoff, type ClaimOptions, type Clock, Compiler, type DBInstance, type Duration, type Edge, type ExecuteSchedule, type InitDBOptions, type IntervalTiming, type Job, type JobFilter, type JobStatus, LockBusyError, type LockOptions, type Migration, type NextDecision, type NextDelay, type NextOutcome, type OverlapPolicy, Queue, type QueueOptions, type QueueStats, type ResolvedTiming, type RunNowOutcome, type RunProblem, STOP_GRACE_MS, type ScheduleCommand, type ScheduleControlResult, type ScheduleControlTarget, type ScheduleReport, ScheduleStore, Task, type TaskContext, TaskLibrary, TaskLibraryScheduler, TaskOnceRunner, type TaskRun, type TaskSqlite, type TickOutcome, type TickStatus, type TimingBackoff, type TimingExport, type WorkHandler, type WorkOptions, type WorkSummary, type WorkerHandle, closeTaskDatabases, controlSchedule, createControlsGenerator, executeEsm, failedRunId, getDbClient, isCronPatternValid, releaseTaskDatabases, resolveTiming, runFailure, runOnce, setupDbClient, systemClock, timingFromFlag };