@apisurf/ngn-core 0.3.0 → 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 +78 -9
- package/dist/core.js +155 -80
- 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,8 +635,17 @@ interface ExecuteSchedule {
|
|
|
607
635
|
/** When the next tick is due, epoch ms. */
|
|
608
636
|
nextAt?: number | null;
|
|
609
637
|
}
|
|
610
|
-
/** The `task_runs.id` of the run that threw `error`, when a
|
|
638
|
+
/** The `task_runs.id` of the run that threw `error`, when a run recorded it. */
|
|
611
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;
|
|
612
649
|
declare class Task {
|
|
613
650
|
private taskCallbacks;
|
|
614
651
|
private startRun;
|
|
@@ -654,8 +691,26 @@ declare class Task {
|
|
|
654
691
|
hasTaskExport(): boolean;
|
|
655
692
|
hasTimingExport(): boolean;
|
|
656
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
|
+
*/
|
|
657
699
|
private loadAndRegister;
|
|
658
|
-
|
|
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;
|
|
659
714
|
/**
|
|
660
715
|
* Run the task for one tick, retrying as its `retry` export allows. A
|
|
661
716
|
* scheduled tick passes when it fired and when the next one is due, which
|
|
@@ -682,6 +737,17 @@ declare class Task {
|
|
|
682
737
|
*/
|
|
683
738
|
private holdBack;
|
|
684
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
|
+
*/
|
|
685
751
|
private executeOnce;
|
|
686
752
|
}
|
|
687
753
|
|
|
@@ -750,9 +816,12 @@ declare function scheduleTask(task: Task, pattern: string, onError: (error: unkn
|
|
|
750
816
|
declare function setupDbClient(dbPath: string): Promise<Client>;
|
|
751
817
|
declare function getDbClient(): Client;
|
|
752
818
|
|
|
753
|
-
declare function createControlsGenerator({ dbClient, env, }: {
|
|
819
|
+
declare function createControlsGenerator({ dbClient, env, report, }: {
|
|
754
820
|
dbClient: Client;
|
|
755
|
-
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;
|
|
756
825
|
}): BuildCompiledTaskConfigFn;
|
|
757
826
|
|
|
758
|
-
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 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, 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 = [
|
|
@@ -1172,6 +1180,9 @@ function breakerSkips(failedInARow, breaker) {
|
|
|
1172
1180
|
var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1173
1181
|
var failedRuns = /* @__PURE__ */ new WeakMap();
|
|
1174
1182
|
function failedRunId(error) {
|
|
1183
|
+
return runFailure(error)?.runId ?? null;
|
|
1184
|
+
}
|
|
1185
|
+
function runFailure(error) {
|
|
1175
1186
|
return error && typeof error === "object" ? failedRuns.get(error) ?? null : null;
|
|
1176
1187
|
}
|
|
1177
1188
|
var Task = class {
|
|
@@ -1284,12 +1295,15 @@ var Task = class {
|
|
|
1284
1295
|
await this.entry.load();
|
|
1285
1296
|
this.entryExports = await this.entry.getExports();
|
|
1286
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
|
+
*/
|
|
1287
1303
|
async loadAndRegister() {
|
|
1288
1304
|
invariant5(this.entry, "Entry not initialized.");
|
|
1289
|
-
await this.
|
|
1305
|
+
await this.entry.load();
|
|
1290
1306
|
invariant5(this.entry.isLoaded(), "Entry failed to load.");
|
|
1291
|
-
invariant5(this.entryExports, "Entry failed to get exports.");
|
|
1292
|
-
this.taskCallbacks?.onTaskExportsLoaded?.();
|
|
1293
1307
|
const loadResult = await this.taskCallbacks?.onTaskLoadComplete?.({
|
|
1294
1308
|
paths: this.entry.paths,
|
|
1295
1309
|
md5Hash: this.entry.codeHash,
|
|
@@ -1298,22 +1312,44 @@ var Task = class {
|
|
|
1298
1312
|
this.meta.fileTaskId = loadResult?.fileTaskId ?? -1;
|
|
1299
1313
|
this.meta.fileTaskVersionId = loadResult?.fileTaskVersionId ?? -1;
|
|
1300
1314
|
}
|
|
1301
|
-
|
|
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) {
|
|
1302
1348
|
invariant5(this.entry, "Entry not initialized.");
|
|
1303
|
-
const path = this.entry.paths.relativeEntry;
|
|
1304
1349
|
try {
|
|
1305
|
-
await
|
|
1306
|
-
|
|
1307
|
-
await run.callbacks.
|
|
1308
|
-
await hooks.onSuccess?.(taskContext);
|
|
1309
|
-
return taskReturnValue;
|
|
1310
|
-
} catch (err) {
|
|
1311
|
-
if (err && typeof err === "object") failedRuns.set(err, run.id);
|
|
1312
|
-
await run.callbacks.onFailure?.({ path });
|
|
1313
|
-
await hooks.onError?.(err, taskContext);
|
|
1314
|
-
throw err;
|
|
1315
|
-
} finally {
|
|
1316
|
-
await hooks.onComplete?.(taskContext);
|
|
1350
|
+
await hook();
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
await run.callbacks.onHookError?.({ path: this.entry.paths.relativeEntry, stage, error });
|
|
1317
1353
|
}
|
|
1318
1354
|
}
|
|
1319
1355
|
/**
|
|
@@ -1403,27 +1439,38 @@ var Task = class {
|
|
|
1403
1439
|
queue: (name, options = {}) => openQueue(options.db?.client ?? this.sqlite.client, name, options, { nextAt, signal })
|
|
1404
1440
|
};
|
|
1405
1441
|
}
|
|
1406
|
-
|
|
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) {
|
|
1407
1447
|
invariant5(this.entry, "Entry not initialized.");
|
|
1408
1448
|
await this.loadAndRegister();
|
|
1409
|
-
invariant5(this.entryExports, "Entry failed to get exports.");
|
|
1410
|
-
const exports = this.entryExports;
|
|
1411
1449
|
const meta = { ...this.meta };
|
|
1412
1450
|
const path = this.entry.paths.relativeEntry;
|
|
1413
|
-
const budgetMs = exports.budget == null ? null : toMs(exports.budget, `budget for ${path}`);
|
|
1414
|
-
const exclusiveLocks = exclusiveNames(exports.exclusive, path);
|
|
1415
|
-
const retry = resolveRetry(exports.retry, path);
|
|
1416
|
-
tick2.retry = retry;
|
|
1417
1451
|
const run = await this.startRun({ retryOf: tick2.firstRunId });
|
|
1418
1452
|
const firstAttempt = tick2.firstRunId == null;
|
|
1419
1453
|
tick2.firstRunId ??= run.id;
|
|
1420
|
-
|
|
1421
|
-
await
|
|
1422
|
-
|
|
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;
|
|
1423
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;
|
|
1424
1470
|
const deadline = runDeadline({ startedAt: Date.now(), scheduledAt, nextAt, budgetMs });
|
|
1425
1471
|
const { signal, dispose } = deadlineSignal(deadline);
|
|
1426
1472
|
const taskContext = this.buildContext(run, meta, { scheduledAt, nextAt, deadline }, signal);
|
|
1473
|
+
let stage = "start";
|
|
1427
1474
|
let exclusiveHeld = null;
|
|
1428
1475
|
try {
|
|
1429
1476
|
const gate = await this.holdBack(run, {
|
|
@@ -1437,20 +1484,29 @@ var Task = class {
|
|
|
1437
1484
|
return;
|
|
1438
1485
|
}
|
|
1439
1486
|
exclusiveHeld = gate.held;
|
|
1487
|
+
stage = "shouldSkip";
|
|
1440
1488
|
if (await exports.shouldSkip?.(taskContext)) {
|
|
1441
1489
|
await run.callbacks.onSkip?.({ path });
|
|
1442
1490
|
return;
|
|
1443
1491
|
}
|
|
1492
|
+
stage = "start";
|
|
1493
|
+
await run.callbacks.onStart?.({ path });
|
|
1494
|
+
stage = "task";
|
|
1444
1495
|
tick2.failedContext = taskContext;
|
|
1445
|
-
const
|
|
1446
|
-
const value = await this.runTask(
|
|
1447
|
-
exports.task,
|
|
1448
|
-
taskContext,
|
|
1449
|
-
{ onSuccess, onError, onComplete },
|
|
1450
|
-
run
|
|
1451
|
-
);
|
|
1496
|
+
const value = await task(taskContext);
|
|
1452
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));
|
|
1453
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;
|
|
1454
1510
|
} finally {
|
|
1455
1511
|
dispose();
|
|
1456
1512
|
await exclusiveHeld?.release();
|
|
@@ -1698,6 +1754,7 @@ import {
|
|
|
1698
1754
|
logTaskConsoleWarn
|
|
1699
1755
|
} from "@apisurf/ngn-os";
|
|
1700
1756
|
import invariant11 from "tiny-invariant";
|
|
1757
|
+
import { errorSummary, failureLine } from "@apisurf/ngn-schema/describe";
|
|
1701
1758
|
|
|
1702
1759
|
// src/db/services/fileTask.ts
|
|
1703
1760
|
import { $fileTaskInsert } from "@apisurf/ngn-schema";
|
|
@@ -2089,9 +2146,17 @@ var LockService = class {
|
|
|
2089
2146
|
};
|
|
2090
2147
|
|
|
2091
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
|
+
}
|
|
2092
2156
|
function createControlsGenerator({
|
|
2093
2157
|
dbClient,
|
|
2094
|
-
env
|
|
2158
|
+
env,
|
|
2159
|
+
report
|
|
2095
2160
|
}) {
|
|
2096
2161
|
const taskRunService = new TaskRunService(dbClient);
|
|
2097
2162
|
const logService = new LogService(dbClient);
|
|
@@ -2134,10 +2199,6 @@ function createControlsGenerator({
|
|
|
2134
2199
|
retry_of: retryOf
|
|
2135
2200
|
});
|
|
2136
2201
|
const callbacks = {
|
|
2137
|
-
async onNotFound() {
|
|
2138
|
-
await logService.logError(taskId, runId, "Task not found");
|
|
2139
|
-
logTaskCallback(`Task not found: ${taskId}, run ID: ${runId}`);
|
|
2140
|
-
},
|
|
2141
2202
|
async onSkip({ reason }) {
|
|
2142
2203
|
await taskRunService.skipTaskRun(runId, reason ?? null);
|
|
2143
2204
|
await logService.logInfo(taskId, runId, "Task skipped");
|
|
@@ -2149,10 +2210,22 @@ function createControlsGenerator({
|
|
|
2149
2210
|
await logService.logInfo(taskId, runId, "Task started");
|
|
2150
2211
|
logTaskCallback(`Task started: ${taskId}, run ID: ${runId}`);
|
|
2151
2212
|
},
|
|
2152
|
-
async onFailure() {
|
|
2153
|
-
await
|
|
2154
|
-
|
|
2155
|
-
|
|
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 });
|
|
2156
2229
|
},
|
|
2157
2230
|
async onSuccess({ result }) {
|
|
2158
2231
|
await taskRunService.endTaskRun(runId, "success", serializeResult(result));
|
|
@@ -2200,7 +2273,7 @@ function createControlsGenerator({
|
|
|
2200
2273
|
return taskRunService.countSkipsSince(fileTaskId, reason, afterId);
|
|
2201
2274
|
}
|
|
2202
2275
|
},
|
|
2203
|
-
env,
|
|
2276
|
+
env: env ?? {},
|
|
2204
2277
|
// Scoped to this task's own folder — see createTaskSqlite
|
|
2205
2278
|
sqlite: createTaskSqlite(sourcePath),
|
|
2206
2279
|
kv: {
|
|
@@ -2233,6 +2306,8 @@ export {
|
|
|
2233
2306
|
getDbClient,
|
|
2234
2307
|
initDbFileIfNotExists,
|
|
2235
2308
|
isCronPatternValid,
|
|
2309
|
+
releaseTaskDatabases,
|
|
2310
|
+
runFailure,
|
|
2236
2311
|
scheduleTask,
|
|
2237
2312
|
setupDbClient
|
|
2238
2313
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apisurf/ngn-core",
|
|
3
|
-
"version": "0.3.
|
|
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-os": "0.3.
|
|
42
|
-
"@apisurf/ngn-
|
|
43
|
-
"@apisurf/ngn-
|
|
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",
|