@apisurf/ngn-core 0.2.9 → 0.3.1
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/dist/core.d.ts +94 -9
- package/dist/core.js +177 -81
- package/package.json +4 -4
package/dist/core.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RunStage } from '@apisurf/ngn-schema/describe';
|
|
2
|
+
export { RunStage } from '@apisurf/ngn-schema/describe';
|
|
1
3
|
import { FsNodeFileDescriptor } from '@apisurf/ngn-os';
|
|
2
4
|
import { Client, InArgs, ResultSet, InStatement, InValue } from '@apisurf/ngn-persistence';
|
|
3
5
|
export { initDbFileIfNotExists } from '@apisurf/ngn-persistence';
|
|
@@ -120,6 +122,13 @@ interface TaskSqlite {
|
|
|
120
122
|
* Close every database opened by a task. Called by the CLI on shutdown.
|
|
121
123
|
*/
|
|
122
124
|
declare function closeTaskDatabases(): void;
|
|
125
|
+
/**
|
|
126
|
+
* Let go of the databases one task file opened, closing those no other task
|
|
127
|
+
* holds. For a one-off run inside a process that keeps scheduling others (the
|
|
128
|
+
* editor's): `closeTaskDatabases` there would close the connections of
|
|
129
|
+
* scheduled runs still in progress, and their next query would fail.
|
|
130
|
+
*/
|
|
131
|
+
declare function releaseTaskDatabases(taskSourcePath: string): void;
|
|
123
132
|
|
|
124
133
|
/**
|
|
125
134
|
* A length of time as tasks write it: milliseconds, or a string such as
|
|
@@ -484,7 +493,8 @@ interface TaskContext {
|
|
|
484
493
|
file: FsNodeFileDescriptor;
|
|
485
494
|
tasksRootDir: string;
|
|
486
495
|
};
|
|
487
|
-
|
|
496
|
+
/** The variables from the config's `envFile`; empty when there is none. */
|
|
497
|
+
env: Record<string, string>;
|
|
488
498
|
kv: {
|
|
489
499
|
set(key: string, value: string): Promise<void>;
|
|
490
500
|
get(key: string): Promise<string | null>;
|
|
@@ -541,9 +551,6 @@ interface CompiledTaskCallbacks {
|
|
|
541
551
|
onTaskExportsLoaded?: () => Promise<void>;
|
|
542
552
|
}
|
|
543
553
|
interface TaskRuntimeCallbacks {
|
|
544
|
-
onNotFound?: (meta: {
|
|
545
|
-
path: string;
|
|
546
|
-
}) => Promise<void>;
|
|
547
554
|
/** `reason` is set when ngn skipped the run, rather than the task's `shouldSkip`. */
|
|
548
555
|
onSkip?: (meta: {
|
|
549
556
|
path: string;
|
|
@@ -556,10 +563,31 @@ interface TaskRuntimeCallbacks {
|
|
|
556
563
|
path: string;
|
|
557
564
|
result?: unknown;
|
|
558
565
|
}) => Promise<void>;
|
|
566
|
+
/** The run failed: `error` was thrown in `stage`. Never throws itself. */
|
|
559
567
|
onFailure?: (meta: {
|
|
560
568
|
path: string;
|
|
569
|
+
stage: RunStage;
|
|
570
|
+
error: unknown;
|
|
571
|
+
}) => Promise<void>;
|
|
572
|
+
/** A hook threw; the run's outcome stands. Never throws itself. */
|
|
573
|
+
onHookError?: (meta: {
|
|
574
|
+
path: string;
|
|
575
|
+
stage: RunStage;
|
|
576
|
+
error: unknown;
|
|
561
577
|
}) => Promise<void>;
|
|
562
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Something a run threw, once it is recorded against the run: what the CLI
|
|
581
|
+
* prints. `fatal` is false for a hook's error, which leaves the run's outcome
|
|
582
|
+
* as it was.
|
|
583
|
+
*/
|
|
584
|
+
interface RunProblem {
|
|
585
|
+
path: string;
|
|
586
|
+
runId: number;
|
|
587
|
+
stage: RunStage;
|
|
588
|
+
error: unknown;
|
|
589
|
+
fatal: boolean;
|
|
590
|
+
}
|
|
563
591
|
/**
|
|
564
592
|
* One run's bookkeeping: its row in `task_runs`, and everything that writes
|
|
565
593
|
* against that row. Made fresh for every execution, so two runs of one task
|
|
@@ -607,6 +635,17 @@ interface ExecuteSchedule {
|
|
|
607
635
|
/** When the next tick is due, epoch ms. */
|
|
608
636
|
nextAt?: number | null;
|
|
609
637
|
}
|
|
638
|
+
/** The `task_runs.id` of the run that threw `error`, when a run recorded it. */
|
|
639
|
+
declare function failedRunId(error: unknown): number | null;
|
|
640
|
+
/**
|
|
641
|
+
* The run that recorded `error` and the stage it was thrown in; null for an
|
|
642
|
+
* error no run recorded (a task file that did not compile, a database ngn
|
|
643
|
+
* could not open), which only its thrower knows about.
|
|
644
|
+
*/
|
|
645
|
+
declare function runFailure(error: unknown): {
|
|
646
|
+
runId: number;
|
|
647
|
+
stage: RunStage;
|
|
648
|
+
} | null;
|
|
610
649
|
declare class Task {
|
|
611
650
|
private taskCallbacks;
|
|
612
651
|
private startRun;
|
|
@@ -652,8 +691,26 @@ declare class Task {
|
|
|
652
691
|
hasTaskExport(): boolean;
|
|
653
692
|
hasTimingExport(): boolean;
|
|
654
693
|
loadEntry(): Promise<void>;
|
|
694
|
+
/**
|
|
695
|
+
* Load the task file's code and register its version. A failure here has no
|
|
696
|
+
* run to mark yet, so it is thrown to whoever called execute(). Evaluating
|
|
697
|
+
* the code comes after the run exists — see `readExports`.
|
|
698
|
+
*/
|
|
655
699
|
private loadAndRegister;
|
|
656
|
-
|
|
700
|
+
/**
|
|
701
|
+
* Evaluate the task file and check the exports a run depends on. Throws
|
|
702
|
+
* what the module threw, or what is wrong with an export, for the run to
|
|
703
|
+
* record as failed in `load`.
|
|
704
|
+
*/
|
|
705
|
+
private readExports;
|
|
706
|
+
/** Mark the run failed in `stage`, remembering where for whoever reports the error. */
|
|
707
|
+
private failRun;
|
|
708
|
+
/**
|
|
709
|
+
* Call one of the task's hooks. What it throws is recorded on the run and
|
|
710
|
+
* goes no further: the run's outcome was decided before the hook ran, and
|
|
711
|
+
* `onError`'s error must not replace the one it was called with.
|
|
712
|
+
*/
|
|
713
|
+
private runHook;
|
|
657
714
|
/**
|
|
658
715
|
* Run the task for one tick, retrying as its `retry` export allows. A
|
|
659
716
|
* scheduled tick passes when it fired and when the next one is due, which
|
|
@@ -680,6 +737,17 @@ declare class Task {
|
|
|
680
737
|
*/
|
|
681
738
|
private holdBack;
|
|
682
739
|
private buildContext;
|
|
740
|
+
/**
|
|
741
|
+
* Load the task file, open the attempt's `task_runs` row, then evaluate the
|
|
742
|
+
* file's code; what that throws is recorded on the row as failed in `load`.
|
|
743
|
+
*/
|
|
744
|
+
private openRun;
|
|
745
|
+
/**
|
|
746
|
+
* One attempt: a `task_runs` row, then the task, with every failure recorded
|
|
747
|
+
* against that row as the stage it was thrown in. Only a task file whose code
|
|
748
|
+
* cannot be loaded at all, or a run row that cannot be written, fails before
|
|
749
|
+
* there is a row to record it on.
|
|
750
|
+
*/
|
|
683
751
|
private executeOnce;
|
|
684
752
|
}
|
|
685
753
|
|
|
@@ -703,12 +771,26 @@ declare class TaskLibrary {
|
|
|
703
771
|
|
|
704
772
|
type Options$1 = {
|
|
705
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;
|
|
706
776
|
};
|
|
777
|
+
/** What `start()` did with each task: scheduled on its pattern, or left out and why. */
|
|
778
|
+
interface ScheduleReport {
|
|
779
|
+
scheduled: {
|
|
780
|
+
path: string;
|
|
781
|
+
pattern: string;
|
|
782
|
+
}[];
|
|
783
|
+
failed: {
|
|
784
|
+
path: string;
|
|
785
|
+
reason: string;
|
|
786
|
+
}[];
|
|
787
|
+
}
|
|
707
788
|
declare class TaskLibraryScheduler {
|
|
708
789
|
private taskLibrary;
|
|
790
|
+
private onRunError;
|
|
709
791
|
private scheduledTasks;
|
|
710
792
|
constructor(options: Options$1);
|
|
711
|
-
start(): Promise<
|
|
793
|
+
start(): Promise<ScheduleReport>;
|
|
712
794
|
stop(): Promise<void>;
|
|
713
795
|
scheduleTask(task: Task): Promise<void>;
|
|
714
796
|
unscheduleTask(task: Task): void;
|
|
@@ -734,9 +816,12 @@ declare function scheduleTask(task: Task, pattern: string, onError: (error: unkn
|
|
|
734
816
|
declare function setupDbClient(dbPath: string): Promise<Client>;
|
|
735
817
|
declare function getDbClient(): Client;
|
|
736
818
|
|
|
737
|
-
declare function createControlsGenerator({ dbClient, env, }: {
|
|
819
|
+
declare function createControlsGenerator({ dbClient, env, report, }: {
|
|
738
820
|
dbClient: Client;
|
|
739
|
-
env
|
|
821
|
+
/** A missing env file is null from the readers; tasks always get an object. */
|
|
822
|
+
env: TaskContext["env"] | null | undefined;
|
|
823
|
+
/** Told about everything a run throws, after it is recorded. The CLI prints it. */
|
|
824
|
+
report?: (problem: RunProblem) => void;
|
|
740
825
|
}): BuildCompiledTaskConfigFn;
|
|
741
826
|
|
|
742
|
-
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, Task, type TaskContext, TaskLibrary, TaskLibraryScheduler, TaskOnceRunner, type TaskRun, type TaskSqlite, type WorkHandler, type WorkOptions, type WorkSummary, type WorkerHandle, closeTaskDatabases, createControlsGenerator, executeEsm, getDbClient, isCronPatternValid, scheduleTask, setupDbClient };
|
|
827
|
+
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 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 };
|
package/dist/core.js
CHANGED
|
@@ -297,36 +297,36 @@ var Entry = class {
|
|
|
297
297
|
isLoaded() {
|
|
298
298
|
return this.entry.isLoaded();
|
|
299
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Evaluate the task file and read its exports. Whatever the module throws
|
|
302
|
+
* while it is evaluated is thrown from here, so the run it was for can
|
|
303
|
+
* record it.
|
|
304
|
+
*/
|
|
300
305
|
async getExports() {
|
|
301
306
|
invariant4(this.entry.code, "Entry code not loaded. Load entry first.");
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
taskFunction = taskExports.task;
|
|
312
|
-
}
|
|
313
|
-
return {
|
|
314
|
-
task: taskFunction,
|
|
315
|
-
timing: taskExports.timing,
|
|
316
|
-
overlap: taskExports.overlap,
|
|
317
|
-
budget: taskExports.budget,
|
|
318
|
-
exclusive: taskExports.exclusive,
|
|
319
|
-
retry: taskExports.retry,
|
|
320
|
-
shouldSkip: taskExports.shouldSkip,
|
|
321
|
-
shouldRetry: taskExports.shouldRetry,
|
|
322
|
-
onSuccess: taskExports.onSuccess,
|
|
323
|
-
onError: taskExports.onError,
|
|
324
|
-
onComplete: taskExports.onComplete
|
|
325
|
-
};
|
|
326
|
-
} catch (error) {
|
|
327
|
-
console.error("Error getting task exports:", error);
|
|
328
|
-
return null;
|
|
307
|
+
const taskExports = await executeEsm(this.entry.code);
|
|
308
|
+
this.callbacks.onTaskCodeLoaded?.(this.entry.codeHash);
|
|
309
|
+
let taskFunction;
|
|
310
|
+
if (typeof taskExports.default === "function") {
|
|
311
|
+
taskFunction = taskExports.default;
|
|
312
|
+
} else if (taskExports.default && typeof taskExports.default === "object" && "task" in taskExports.default) {
|
|
313
|
+
taskFunction = taskExports.default.task;
|
|
314
|
+
} else {
|
|
315
|
+
taskFunction = taskExports.task;
|
|
329
316
|
}
|
|
317
|
+
return {
|
|
318
|
+
task: taskFunction,
|
|
319
|
+
timing: taskExports.timing,
|
|
320
|
+
overlap: taskExports.overlap,
|
|
321
|
+
budget: taskExports.budget,
|
|
322
|
+
exclusive: taskExports.exclusive,
|
|
323
|
+
retry: taskExports.retry,
|
|
324
|
+
shouldSkip: taskExports.shouldSkip,
|
|
325
|
+
shouldRetry: taskExports.shouldRetry,
|
|
326
|
+
onSuccess: taskExports.onSuccess,
|
|
327
|
+
onError: taskExports.onError,
|
|
328
|
+
onComplete: taskExports.onComplete
|
|
329
|
+
};
|
|
330
330
|
}
|
|
331
331
|
get paths() {
|
|
332
332
|
return this.entry.path;
|
|
@@ -530,27 +530,28 @@ function resolveTaskDbPath(taskDir, file) {
|
|
|
530
530
|
}
|
|
531
531
|
return resolved;
|
|
532
532
|
}
|
|
533
|
-
function getClient(absolutePath) {
|
|
533
|
+
function getClient(absolutePath, owner) {
|
|
534
534
|
const existing = openClients.get(absolutePath);
|
|
535
535
|
if (existing) {
|
|
536
|
-
|
|
536
|
+
existing.owners.add(owner);
|
|
537
|
+
return existing.client;
|
|
537
538
|
}
|
|
538
539
|
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
539
540
|
const client = openDbClient(`file:${absolutePath}`);
|
|
540
|
-
openClients.set(absolutePath, client);
|
|
541
|
+
openClients.set(absolutePath, { client, owners: /* @__PURE__ */ new Set([owner]) });
|
|
541
542
|
return client;
|
|
542
543
|
}
|
|
543
544
|
function closeClient(absolutePath) {
|
|
544
|
-
const
|
|
545
|
-
if (
|
|
546
|
-
client.close();
|
|
545
|
+
const open = openClients.get(absolutePath);
|
|
546
|
+
if (open) {
|
|
547
|
+
open.client.close();
|
|
547
548
|
openClients.delete(absolutePath);
|
|
548
549
|
}
|
|
549
550
|
}
|
|
550
551
|
function createTaskSqlite(taskSourcePath) {
|
|
551
552
|
const taskDir = dirname(taskSourcePath);
|
|
552
553
|
const defaultDbPath = join(taskDir, `${basename(taskSourcePath, extname(taskSourcePath))}.db`);
|
|
553
|
-
const defaultClient = () => getClient(defaultDbPath);
|
|
554
|
+
const defaultClient = () => getClient(defaultDbPath, taskSourcePath);
|
|
554
555
|
return {
|
|
555
556
|
get client() {
|
|
556
557
|
return defaultClient();
|
|
@@ -562,7 +563,7 @@ function createTaskSqlite(taskSourcePath) {
|
|
|
562
563
|
batch: (statements) => defaultClient().batch(statements),
|
|
563
564
|
async initDB(options) {
|
|
564
565
|
const dbPath = resolveTaskDbPath(taskDir, options.file);
|
|
565
|
-
const client = getClient(dbPath);
|
|
566
|
+
const client = getClient(dbPath, taskSourcePath);
|
|
566
567
|
if (options.migrations?.length) {
|
|
567
568
|
await runMigrations(client, options.migrations);
|
|
568
569
|
}
|
|
@@ -582,11 +583,18 @@ function createTaskSqlite(taskSourcePath) {
|
|
|
582
583
|
};
|
|
583
584
|
}
|
|
584
585
|
function closeTaskDatabases() {
|
|
585
|
-
for (const client of openClients.values()) {
|
|
586
|
+
for (const { client } of openClients.values()) {
|
|
586
587
|
client.close();
|
|
587
588
|
}
|
|
588
589
|
openClients.clear();
|
|
589
590
|
}
|
|
591
|
+
function releaseTaskDatabases(taskSourcePath) {
|
|
592
|
+
for (const [path, { client, owners }] of openClients) {
|
|
593
|
+
if (!owners.delete(taskSourcePath) || owners.size > 0) continue;
|
|
594
|
+
client.close();
|
|
595
|
+
openClients.delete(path);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
590
598
|
|
|
591
599
|
// src/runtime/queue/schema.ts
|
|
592
600
|
var QUEUE_MIGRATIONS = [
|
|
@@ -1170,6 +1178,13 @@ function breakerSkips(failedInARow, breaker) {
|
|
|
1170
1178
|
|
|
1171
1179
|
// src/runtime/task.ts
|
|
1172
1180
|
var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1181
|
+
var failedRuns = /* @__PURE__ */ new WeakMap();
|
|
1182
|
+
function failedRunId(error) {
|
|
1183
|
+
return runFailure(error)?.runId ?? null;
|
|
1184
|
+
}
|
|
1185
|
+
function runFailure(error) {
|
|
1186
|
+
return error && typeof error === "object" ? failedRuns.get(error) ?? null : null;
|
|
1187
|
+
}
|
|
1173
1188
|
var Task = class {
|
|
1174
1189
|
taskCallbacks;
|
|
1175
1190
|
startRun;
|
|
@@ -1280,12 +1295,15 @@ var Task = class {
|
|
|
1280
1295
|
await this.entry.load();
|
|
1281
1296
|
this.entryExports = await this.entry.getExports();
|
|
1282
1297
|
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Load the task file's code and register its version. A failure here has no
|
|
1300
|
+
* run to mark yet, so it is thrown to whoever called execute(). Evaluating
|
|
1301
|
+
* the code comes after the run exists — see `readExports`.
|
|
1302
|
+
*/
|
|
1283
1303
|
async loadAndRegister() {
|
|
1284
1304
|
invariant5(this.entry, "Entry not initialized.");
|
|
1285
|
-
await this.
|
|
1305
|
+
await this.entry.load();
|
|
1286
1306
|
invariant5(this.entry.isLoaded(), "Entry failed to load.");
|
|
1287
|
-
invariant5(this.entryExports, "Entry failed to get exports.");
|
|
1288
|
-
this.taskCallbacks?.onTaskExportsLoaded?.();
|
|
1289
1307
|
const loadResult = await this.taskCallbacks?.onTaskLoadComplete?.({
|
|
1290
1308
|
paths: this.entry.paths,
|
|
1291
1309
|
md5Hash: this.entry.codeHash,
|
|
@@ -1294,21 +1312,44 @@ var Task = class {
|
|
|
1294
1312
|
this.meta.fileTaskId = loadResult?.fileTaskId ?? -1;
|
|
1295
1313
|
this.meta.fileTaskVersionId = loadResult?.fileTaskVersionId ?? -1;
|
|
1296
1314
|
}
|
|
1297
|
-
|
|
1315
|
+
/**
|
|
1316
|
+
* Evaluate the task file and check the exports a run depends on. Throws
|
|
1317
|
+
* what the module threw, or what is wrong with an export, for the run to
|
|
1318
|
+
* record as failed in `load`.
|
|
1319
|
+
*/
|
|
1320
|
+
async readExports(path) {
|
|
1321
|
+
invariant5(this.entry, "Entry not initialized.");
|
|
1322
|
+
const exports = await this.entry.getExports();
|
|
1323
|
+
this.entryExports = exports;
|
|
1324
|
+
this.taskCallbacks?.onTaskExportsLoaded?.();
|
|
1325
|
+
if (typeof exports.task !== "function") {
|
|
1326
|
+
throw new Error(`${path} exports no task function`);
|
|
1327
|
+
}
|
|
1328
|
+
return {
|
|
1329
|
+
exports,
|
|
1330
|
+
task: exports.task,
|
|
1331
|
+
budgetMs: exports.budget == null ? null : toMs(exports.budget, `budget for ${path}`),
|
|
1332
|
+
exclusiveLocks: exclusiveNames(exports.exclusive, path),
|
|
1333
|
+
retry: resolveRetry(exports.retry, path)
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
/** Mark the run failed in `stage`, remembering where for whoever reports the error. */
|
|
1337
|
+
async failRun(run, stage, error) {
|
|
1338
|
+
invariant5(this.entry, "Entry not initialized.");
|
|
1339
|
+
if (error && typeof error === "object") failedRuns.set(error, { runId: run.id, stage });
|
|
1340
|
+
await run.callbacks.onFailure?.({ path: this.entry.paths.relativeEntry, stage, error });
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Call one of the task's hooks. What it throws is recorded on the run and
|
|
1344
|
+
* goes no further: the run's outcome was decided before the hook ran, and
|
|
1345
|
+
* `onError`'s error must not replace the one it was called with.
|
|
1346
|
+
*/
|
|
1347
|
+
async runHook(run, stage, hook) {
|
|
1298
1348
|
invariant5(this.entry, "Entry not initialized.");
|
|
1299
|
-
const path = this.entry.paths.relativeEntry;
|
|
1300
1349
|
try {
|
|
1301
|
-
await
|
|
1302
|
-
|
|
1303
|
-
await run.callbacks.
|
|
1304
|
-
await hooks.onSuccess?.(taskContext);
|
|
1305
|
-
return taskReturnValue;
|
|
1306
|
-
} catch (err) {
|
|
1307
|
-
await run.callbacks.onFailure?.({ path });
|
|
1308
|
-
await hooks.onError?.(err, taskContext);
|
|
1309
|
-
throw err;
|
|
1310
|
-
} finally {
|
|
1311
|
-
await hooks.onComplete?.(taskContext);
|
|
1350
|
+
await hook();
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
await run.callbacks.onHookError?.({ path: this.entry.paths.relativeEntry, stage, error });
|
|
1312
1353
|
}
|
|
1313
1354
|
}
|
|
1314
1355
|
/**
|
|
@@ -1398,27 +1439,38 @@ var Task = class {
|
|
|
1398
1439
|
queue: (name, options = {}) => openQueue(options.db?.client ?? this.sqlite.client, name, options, { nextAt, signal })
|
|
1399
1440
|
};
|
|
1400
1441
|
}
|
|
1401
|
-
|
|
1442
|
+
/**
|
|
1443
|
+
* Load the task file, open the attempt's `task_runs` row, then evaluate the
|
|
1444
|
+
* file's code; what that throws is recorded on the row as failed in `load`.
|
|
1445
|
+
*/
|
|
1446
|
+
async openRun(tick2) {
|
|
1402
1447
|
invariant5(this.entry, "Entry not initialized.");
|
|
1403
1448
|
await this.loadAndRegister();
|
|
1404
|
-
invariant5(this.entryExports, "Entry failed to get exports.");
|
|
1405
|
-
const exports = this.entryExports;
|
|
1406
1449
|
const meta = { ...this.meta };
|
|
1407
1450
|
const path = this.entry.paths.relativeEntry;
|
|
1408
|
-
const budgetMs = exports.budget == null ? null : toMs(exports.budget, `budget for ${path}`);
|
|
1409
|
-
const exclusiveLocks = exclusiveNames(exports.exclusive, path);
|
|
1410
|
-
const retry = resolveRetry(exports.retry, path);
|
|
1411
|
-
tick2.retry = retry;
|
|
1412
1451
|
const run = await this.startRun({ retryOf: tick2.firstRunId });
|
|
1413
1452
|
const firstAttempt = tick2.firstRunId == null;
|
|
1414
1453
|
tick2.firstRunId ??= run.id;
|
|
1415
|
-
|
|
1416
|
-
await
|
|
1417
|
-
|
|
1454
|
+
try {
|
|
1455
|
+
return { run, meta, path, firstAttempt, ...await this.readExports(path) };
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
await this.failRun(run, "load", error);
|
|
1458
|
+
throw error;
|
|
1418
1459
|
}
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* One attempt: a `task_runs` row, then the task, with every failure recorded
|
|
1463
|
+
* against that row as the stage it was thrown in. Only a task file whose code
|
|
1464
|
+
* cannot be loaded at all, or a run row that cannot be written, fails before
|
|
1465
|
+
* there is a row to record it on.
|
|
1466
|
+
*/
|
|
1467
|
+
async executeOnce({ scheduledAt = null, nextAt = null }, tick2) {
|
|
1468
|
+
const { run, meta, path, firstAttempt, exports, task, budgetMs, exclusiveLocks, retry } = await this.openRun(tick2);
|
|
1469
|
+
tick2.retry = retry;
|
|
1419
1470
|
const deadline = runDeadline({ startedAt: Date.now(), scheduledAt, nextAt, budgetMs });
|
|
1420
1471
|
const { signal, dispose } = deadlineSignal(deadline);
|
|
1421
1472
|
const taskContext = this.buildContext(run, meta, { scheduledAt, nextAt, deadline }, signal);
|
|
1473
|
+
let stage = "start";
|
|
1422
1474
|
let exclusiveHeld = null;
|
|
1423
1475
|
try {
|
|
1424
1476
|
const gate = await this.holdBack(run, {
|
|
@@ -1432,20 +1484,29 @@ var Task = class {
|
|
|
1432
1484
|
return;
|
|
1433
1485
|
}
|
|
1434
1486
|
exclusiveHeld = gate.held;
|
|
1487
|
+
stage = "shouldSkip";
|
|
1435
1488
|
if (await exports.shouldSkip?.(taskContext)) {
|
|
1436
1489
|
await run.callbacks.onSkip?.({ path });
|
|
1437
1490
|
return;
|
|
1438
1491
|
}
|
|
1492
|
+
stage = "start";
|
|
1493
|
+
await run.callbacks.onStart?.({ path });
|
|
1494
|
+
stage = "task";
|
|
1439
1495
|
tick2.failedContext = taskContext;
|
|
1440
|
-
const
|
|
1441
|
-
const value = await this.runTask(
|
|
1442
|
-
exports.task,
|
|
1443
|
-
taskContext,
|
|
1444
|
-
{ onSuccess, onError, onComplete },
|
|
1445
|
-
run
|
|
1446
|
-
);
|
|
1496
|
+
const value = await task(taskContext);
|
|
1447
1497
|
tick2.failedContext = null;
|
|
1498
|
+
stage = "record";
|
|
1499
|
+
await run.callbacks.onSuccess?.({ path, result: value });
|
|
1500
|
+
await this.runHook(run, "onSuccess", () => exports.onSuccess?.(taskContext));
|
|
1501
|
+
await this.runHook(run, "onComplete", () => exports.onComplete?.(taskContext));
|
|
1448
1502
|
return value;
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
await this.failRun(run, stage, error);
|
|
1505
|
+
if (stage === "task" || stage === "record") {
|
|
1506
|
+
await this.runHook(run, "onError", () => exports.onError?.(error, taskContext));
|
|
1507
|
+
await this.runHook(run, "onComplete", () => exports.onComplete?.(taskContext));
|
|
1508
|
+
}
|
|
1509
|
+
throw error;
|
|
1449
1510
|
} finally {
|
|
1450
1511
|
dispose();
|
|
1451
1512
|
await exclusiveHeld?.release();
|
|
@@ -1553,23 +1614,30 @@ function scheduleTask(task, pattern, onError) {
|
|
|
1553
1614
|
// src/runtime/taskLibraryScheduler.ts
|
|
1554
1615
|
var TaskLibraryScheduler = class {
|
|
1555
1616
|
taskLibrary;
|
|
1617
|
+
onRunError;
|
|
1556
1618
|
scheduledTasks = /* @__PURE__ */ new Map();
|
|
1557
1619
|
constructor(options) {
|
|
1558
1620
|
this.taskLibrary = options.taskLibrary;
|
|
1621
|
+
this.onRunError = options.onRunError;
|
|
1559
1622
|
}
|
|
1560
1623
|
async start() {
|
|
1561
1624
|
logTaskLibraryScheduler("Starting scheduler");
|
|
1625
|
+
const report = { scheduled: [], failed: [] };
|
|
1562
1626
|
const scheduledTasks = this.taskLibrary.getTasks().map(async (task) => {
|
|
1563
1627
|
try {
|
|
1564
|
-
|
|
1565
|
-
|
|
1628
|
+
await this.scheduleTask(task);
|
|
1629
|
+
report.scheduled.push({ path: task.taskPath, pattern: task.timingPattern ?? "" });
|
|
1566
1630
|
} catch (error) {
|
|
1567
1631
|
logTaskLibrarySchedulerError(`Error scheduling task: ${task.taskPath}`);
|
|
1568
1632
|
logTaskLibrarySchedulerError(error);
|
|
1633
|
+
report.failed.push({ path: task.taskPath, reason: reasonOf(error, task.taskPath) });
|
|
1569
1634
|
}
|
|
1570
1635
|
});
|
|
1571
1636
|
await Promise.all(scheduledTasks);
|
|
1572
1637
|
logTaskLibraryScheduler("Scheduler started");
|
|
1638
|
+
report.scheduled.sort(byPath);
|
|
1639
|
+
report.failed.sort(byPath);
|
|
1640
|
+
return report;
|
|
1573
1641
|
}
|
|
1574
1642
|
async stop() {
|
|
1575
1643
|
logTaskLibraryScheduler("Stopping scheduler");
|
|
@@ -1591,6 +1659,7 @@ var TaskLibraryScheduler = class {
|
|
|
1591
1659
|
const scheduledTask = scheduleTask(task, task.timingPattern, (error) => {
|
|
1592
1660
|
logTaskLibrarySchedulerError(`Run failed: ${task.taskPath}`);
|
|
1593
1661
|
logTaskLibrarySchedulerError(error);
|
|
1662
|
+
this.onRunError?.(task.taskPath, error, failedRunId(error));
|
|
1594
1663
|
});
|
|
1595
1664
|
this.scheduledTasks.set(task.taskPath, scheduledTask);
|
|
1596
1665
|
logTaskLibraryScheduler(`Scheduled: ${task.taskPath}`);
|
|
@@ -1623,6 +1692,13 @@ var TaskLibraryScheduler = class {
|
|
|
1623
1692
|
// task.stop();
|
|
1624
1693
|
// }
|
|
1625
1694
|
};
|
|
1695
|
+
function byPath(a, b) {
|
|
1696
|
+
return a.path.localeCompare(b.path);
|
|
1697
|
+
}
|
|
1698
|
+
function reasonOf(error, path) {
|
|
1699
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1700
|
+
return message.replace(/^Invariant failed: /, "").replace(` for ${path}`, "").replace(`: ${path}`, "");
|
|
1701
|
+
}
|
|
1626
1702
|
|
|
1627
1703
|
// src/runtime/taskOnceRunner.ts
|
|
1628
1704
|
import invariant9 from "tiny-invariant";
|
|
@@ -1678,6 +1754,7 @@ import {
|
|
|
1678
1754
|
logTaskConsoleWarn
|
|
1679
1755
|
} from "@apisurf/ngn-os";
|
|
1680
1756
|
import invariant11 from "tiny-invariant";
|
|
1757
|
+
import { errorSummary, failureLine } from "@apisurf/ngn-schema/describe";
|
|
1681
1758
|
|
|
1682
1759
|
// src/db/services/fileTask.ts
|
|
1683
1760
|
import { $fileTaskInsert } from "@apisurf/ngn-schema";
|
|
@@ -2069,9 +2146,17 @@ var LockService = class {
|
|
|
2069
2146
|
};
|
|
2070
2147
|
|
|
2071
2148
|
// src/runtime/generateControls.ts
|
|
2149
|
+
async function recordSafely(runId, write) {
|
|
2150
|
+
try {
|
|
2151
|
+
await write();
|
|
2152
|
+
} catch (error) {
|
|
2153
|
+
logTaskCallback(`Could not record what run ${runId} threw: ${errorSummary(error)}`);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2072
2156
|
function createControlsGenerator({
|
|
2073
2157
|
dbClient,
|
|
2074
|
-
env
|
|
2158
|
+
env,
|
|
2159
|
+
report
|
|
2075
2160
|
}) {
|
|
2076
2161
|
const taskRunService = new TaskRunService(dbClient);
|
|
2077
2162
|
const logService = new LogService(dbClient);
|
|
@@ -2114,10 +2199,6 @@ function createControlsGenerator({
|
|
|
2114
2199
|
retry_of: retryOf
|
|
2115
2200
|
});
|
|
2116
2201
|
const callbacks = {
|
|
2117
|
-
async onNotFound() {
|
|
2118
|
-
await logService.logError(taskId, runId, "Task not found");
|
|
2119
|
-
logTaskCallback(`Task not found: ${taskId}, run ID: ${runId}`);
|
|
2120
|
-
},
|
|
2121
2202
|
async onSkip({ reason }) {
|
|
2122
2203
|
await taskRunService.skipTaskRun(runId, reason ?? null);
|
|
2123
2204
|
await logService.logInfo(taskId, runId, "Task skipped");
|
|
@@ -2129,10 +2210,22 @@ function createControlsGenerator({
|
|
|
2129
2210
|
await logService.logInfo(taskId, runId, "Task started");
|
|
2130
2211
|
logTaskCallback(`Task started: ${taskId}, run ID: ${runId}`);
|
|
2131
2212
|
},
|
|
2132
|
-
async onFailure() {
|
|
2133
|
-
await
|
|
2134
|
-
|
|
2135
|
-
|
|
2213
|
+
async onFailure({ path, stage, error }) {
|
|
2214
|
+
await recordSafely(runId, async () => {
|
|
2215
|
+
await taskRunService.endTaskRun(runId, "failure");
|
|
2216
|
+
await logService.logError(taskId, runId, failureLine(stage, error));
|
|
2217
|
+
await logService.logError(taskId, runId, "Task failed");
|
|
2218
|
+
});
|
|
2219
|
+
logTaskCallback(`Task failed in ${stage}: ${taskId}, run ID: ${runId}`);
|
|
2220
|
+
report?.({ path, runId, stage, error, fatal: true });
|
|
2221
|
+
},
|
|
2222
|
+
async onHookError({ path, stage, error }) {
|
|
2223
|
+
await recordSafely(
|
|
2224
|
+
runId,
|
|
2225
|
+
() => logService.logError(taskId, runId, failureLine(stage, error))
|
|
2226
|
+
);
|
|
2227
|
+
logTaskCallback(`Hook ${stage} threw: ${taskId}, run ID: ${runId}`);
|
|
2228
|
+
report?.({ path, runId, stage, error, fatal: false });
|
|
2136
2229
|
},
|
|
2137
2230
|
async onSuccess({ result }) {
|
|
2138
2231
|
await taskRunService.endTaskRun(runId, "success", serializeResult(result));
|
|
@@ -2180,7 +2273,7 @@ function createControlsGenerator({
|
|
|
2180
2273
|
return taskRunService.countSkipsSince(fileTaskId, reason, afterId);
|
|
2181
2274
|
}
|
|
2182
2275
|
},
|
|
2183
|
-
env,
|
|
2276
|
+
env: env ?? {},
|
|
2184
2277
|
// Scoped to this task's own folder — see createTaskSqlite
|
|
2185
2278
|
sqlite: createTaskSqlite(sourcePath),
|
|
2186
2279
|
kv: {
|
|
@@ -2209,9 +2302,12 @@ export {
|
|
|
2209
2302
|
closeTaskDatabases,
|
|
2210
2303
|
createControlsGenerator,
|
|
2211
2304
|
executeEsm,
|
|
2305
|
+
failedRunId,
|
|
2212
2306
|
getDbClient,
|
|
2213
2307
|
initDbFileIfNotExists,
|
|
2214
2308
|
isCronPatternValid,
|
|
2309
|
+
releaseTaskDatabases,
|
|
2310
|
+
runFailure,
|
|
2215
2311
|
scheduleTask,
|
|
2216
2312
|
setupDbClient
|
|
2217
2313
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apisurf/ngn-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "ngn runtime: task compilation, scheduling and the run/log/timing services.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Luka Vidakovic",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"esbuild": "^0.27.1",
|
|
39
39
|
"tiny-invariant": "^1.3.3",
|
|
40
40
|
"zod": "^3.24.2",
|
|
41
|
-
"@apisurf/ngn-
|
|
42
|
-
"@apisurf/ngn-
|
|
43
|
-
"@apisurf/ngn-persistence": "0.
|
|
41
|
+
"@apisurf/ngn-os": "0.3.1",
|
|
42
|
+
"@apisurf/ngn-schema": "0.3.1",
|
|
43
|
+
"@apisurf/ngn-persistence": "0.3.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^22.8.1",
|