omkit 0.0.15 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -97
- package/dist/actions/chrome-page.d.ts +12 -29
- package/dist/actions/chrome-page.js +19 -32
- package/dist/actions/command.d.ts +8 -41
- package/dist/actions/command.js +15 -82
- package/dist/actions/healthcheck.d.ts +3 -11
- package/dist/actions/healthcheck.js +8 -22
- package/dist/actions/index.d.ts +6 -8
- package/dist/actions/index.js +4 -6
- package/dist/actions/prompt.d.ts +5 -19
- package/dist/actions/prompt.js +10 -31
- package/dist/actions/tail-log.d.ts +7 -21
- package/dist/actions/tail-log.js +13 -34
- package/dist/actions/until-log.d.ts +4 -7
- package/dist/actions/until-log.js +3 -3
- package/dist/actions/watch-dir.d.ts +5 -19
- package/dist/actions/watch-dir.js +13 -28
- package/dist/actions/watch.d.ts +4 -12
- package/dist/actions/watch.js +9 -16
- package/dist/core/ActionRun.d.ts +97 -0
- package/dist/core/ActionRun.js +266 -0
- package/dist/core/ExecutionTree.d.ts +88 -0
- package/dist/core/ExecutionTree.js +320 -0
- package/dist/core/action.d.ts +6 -0
- package/dist/core/action.js +72 -0
- package/dist/core/context.d.ts +9 -0
- package/dist/core/context.js +8 -0
- package/dist/core/om.d.ts +9 -0
- package/dist/core/om.js +25 -0
- package/dist/core/step.d.ts +9 -0
- package/dist/core/step.js +18 -0
- package/dist/core/types.d.ts +128 -0
- package/dist/foundation/ActionRef.d.ts +18 -0
- package/dist/foundation/ActionRef.js +5 -0
- package/dist/{utils → foundation}/AsyncQueue.d.ts +2 -4
- package/dist/{utils → foundation}/AsyncQueue.js +6 -19
- package/dist/foundation/CancelledError.d.ts +10 -0
- package/dist/foundation/CancelledError.js +13 -0
- package/dist/foundation/Deferred.d.ts +8 -0
- package/dist/foundation/Deferred.js +10 -0
- package/dist/foundation/LogEntry.d.ts +29 -0
- package/dist/{orchestration/events → foundation}/events.d.ts +2 -11
- package/dist/{orchestration/events → foundation}/events.js +1 -7
- package/dist/{utils → foundation}/format.d.ts +3 -4
- package/dist/{utils → foundation}/format.js +12 -4
- package/dist/foundation/fsSafe.d.ts +7 -0
- package/dist/foundation/fsSafe.js +7 -0
- package/dist/foundation/ids.d.ts +9 -0
- package/dist/foundation/ids.js +10 -0
- package/dist/index.d.ts +11 -22
- package/dist/index.js +10 -30
- package/dist/output/LiveRenderer.d.ts +13 -0
- package/dist/output/LiveRenderer.js +56 -0
- package/dist/output/console/ConsoleCapture.d.ts +20 -0
- package/dist/output/console/ConsoleCapture.js +47 -0
- package/dist/output/folder/RunFolder.d.ts +11 -22
- package/dist/output/folder/RunFolder.js +18 -36
- package/dist/output/log/LogStore.d.ts +22 -0
- package/dist/output/log/LogStore.js +56 -0
- package/dist/output/log/RawStream.d.ts +11 -0
- package/dist/output/log/RawStream.js +18 -0
- package/dist/output/snapshot/SnapshotStore.d.ts +6 -43
- package/dist/output/snapshot/SnapshotStore.js +12 -52
- package/dist/output/writers/NodeLogWriter.d.ts +8 -0
- package/dist/output/writers/NodeLogWriter.js +36 -0
- package/dist/output/writers/ResultWriter.d.ts +3 -0
- package/dist/output/writers/ResultWriter.js +5 -0
- package/dist/output/writers/RollupWriter.d.ts +9 -0
- package/dist/output/writers/RollupWriter.js +29 -0
- package/dist/output/writers/entryLine.d.ts +10 -0
- package/dist/output/writers/entryLine.js +39 -0
- package/dist/output/writers/views.d.ts +38 -0
- package/dist/output/writers/views.js +6 -0
- package/dist/system/fs/FolderCache.d.ts +6 -34
- package/dist/system/fs/FolderCache.js +7 -38
- package/dist/system/proc.d.ts +51 -0
- package/dist/system/proc.js +148 -0
- package/dist/system/procRegistry.d.ts +52 -0
- package/dist/system/procRegistry.js +117 -0
- package/package.json +3 -1
- package/dist/orchestration/action/action.d.ts +0 -23
- package/dist/orchestration/action/action.js +0 -256
- package/dist/orchestration/action/types.d.ts +0 -182
- package/dist/orchestration/assert.d.ts +0 -24
- package/dist/orchestration/assert.js +0 -26
- package/dist/orchestration/spin/SpinHost.d.ts +0 -111
- package/dist/orchestration/spin/SpinHost.js +0 -345
- package/dist/orchestration/spin/spin.d.ts +0 -24
- package/dist/orchestration/spin/spin.js +0 -57
- package/dist/orchestration/spin/types.d.ts +0 -63
- package/dist/output/Output.d.ts +0 -24
- package/dist/output/Output.js +0 -27
- package/dist/output/index.d.ts +0 -11
- package/dist/output/index.js +0 -11
- package/dist/output/log/LogRenderer.d.ts +0 -73
- package/dist/output/log/LogRenderer.js +0 -133
- package/dist/output/log/LogsCollector.d.ts +0 -79
- package/dist/output/log/LogsCollector.js +0 -141
- package/dist/output/log/RunLog.d.ts +0 -54
- package/dist/output/log/RunLog.js +0 -152
- package/dist/output/log/console-capture.d.ts +0 -24
- package/dist/output/log/console-capture.js +0 -53
- package/dist/system/fs/streams.d.ts +0 -11
- package/dist/system/fs/streams.js +0 -14
- package/dist/system/process/process.d.ts +0 -35
- package/dist/system/process/process.js +0 -104
- package/dist/utils/serialize.d.ts +0 -2
- package/dist/utils/serialize.js +0 -12
- /package/dist/{orchestration/action → core}/types.js +0 -0
- /package/dist/{orchestration/spin/types.js → foundation/LogEntry.js} +0 -0
package/dist/actions/watch.js
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { watch as fsWatch } from "node:fs";
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, resolve } from "node:path";
|
|
4
|
-
import { action } from "../
|
|
4
|
+
import { action } from "../core/action.js";
|
|
5
5
|
/**
|
|
6
6
|
* Watches a single file that may not exist yet, firing callbacks on its
|
|
7
|
-
* transitions.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* until the run's signal aborts, then closes the watcher.
|
|
11
|
-
*
|
|
12
|
-
* Assumes the parent directory exists; watching when an ancestor is missing is
|
|
13
|
-
* a later case.
|
|
7
|
+
* transitions. Watches the parent directory and filters by name (so a missing
|
|
8
|
+
* file can be awaited), re-`stat`ing on each event to diff against last state.
|
|
9
|
+
* Daemon: runs until the run's signal aborts, then closes the watcher.
|
|
14
10
|
*/
|
|
15
|
-
export const watch = action("
|
|
11
|
+
export const watch = action("watch").run(({ signal }, target, opts = {}) => {
|
|
16
12
|
const { onMissing, onCreate, onUpdate, onDelete, debounceMs = 50 } = opts;
|
|
17
13
|
const file = resolve(target);
|
|
18
14
|
const dir = dirname(file);
|
|
@@ -31,23 +27,23 @@ export const watch = action("Watch").run(({ logs, signal }, target, opts = {}) =
|
|
|
31
27
|
if (!present) {
|
|
32
28
|
present = true;
|
|
33
29
|
mtimeMs = stats.mtimeMs;
|
|
34
|
-
|
|
30
|
+
console.log(`created ${file}`);
|
|
35
31
|
onCreate?.({ path: file, isInitial, stats });
|
|
36
32
|
}
|
|
37
33
|
else if (stats.mtimeMs !== mtimeMs) {
|
|
38
34
|
mtimeMs = stats.mtimeMs;
|
|
39
|
-
|
|
35
|
+
console.log(`updated ${file}`);
|
|
40
36
|
onUpdate?.({ path: file, isInitial, stats });
|
|
41
37
|
}
|
|
42
38
|
}
|
|
43
39
|
else if (present) {
|
|
44
40
|
present = false;
|
|
45
41
|
mtimeMs = 0;
|
|
46
|
-
|
|
42
|
+
console.log(`deleted ${file}`);
|
|
47
43
|
onDelete?.({ path: file, isInitial });
|
|
48
44
|
}
|
|
49
45
|
else if (isInitial) {
|
|
50
|
-
|
|
46
|
+
console.log(`missing ${file}`);
|
|
51
47
|
onMissing?.({ path: file, isInitial });
|
|
52
48
|
}
|
|
53
49
|
};
|
|
@@ -57,14 +53,11 @@ export const watch = action("Watch").run(({ logs, signal }, target, opts = {}) =
|
|
|
57
53
|
clearTimeout(timer);
|
|
58
54
|
timer = setTimeout(() => void recheck(false), debounceMs);
|
|
59
55
|
};
|
|
60
|
-
// Watch the parent dir (the file may not exist yet) and filter by name.
|
|
61
|
-
// Some platforms pass a null filename — recheck anyway, it's a cheap stat.
|
|
62
56
|
const watcher = fsWatch(dir, (_event, filename) => {
|
|
63
57
|
if (filename === null || filename === name)
|
|
64
58
|
schedule();
|
|
65
59
|
});
|
|
66
60
|
void recheck(true); // report the initial state
|
|
67
|
-
// Daemon: stay alive until torn down, then close the watcher.
|
|
68
61
|
return new Promise((resolveRun) => {
|
|
69
62
|
const stop = () => {
|
|
70
63
|
if (timer)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type EventHandler } from "../foundation/events.ts";
|
|
2
|
+
import type { ActionRef } from "../foundation/ActionRef.ts";
|
|
3
|
+
import type { LogStore } from "../output/log/LogStore.ts";
|
|
4
|
+
import type { SnapshotStore } from "../output/snapshot/SnapshotStore.ts";
|
|
5
|
+
import type { Exec, InstanceEvents, NodeStatus, Outcome, RunHandle } from "./types.ts";
|
|
6
|
+
/** Everything a node needs to exist, minus its behavior. */
|
|
7
|
+
export interface NodeInit {
|
|
8
|
+
readonly store: LogStore;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly uuid: string;
|
|
11
|
+
/** `${name}_${shortId}`, or `main` for the root. */
|
|
12
|
+
readonly id: string;
|
|
13
|
+
/** Ancestry of ids joined by `/`. */
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly parentId: string | null;
|
|
16
|
+
/** The parent's signal — this node's controller chains off it. */
|
|
17
|
+
readonly parentSignal: AbortSignal | null;
|
|
18
|
+
/** The run's snapshot store — backs `ctx.snapshot`. */
|
|
19
|
+
readonly snapshots: SnapshotStore;
|
|
20
|
+
/** Absolute path to the run's output folder — backs `ctx.artifactsFolder`. */
|
|
21
|
+
readonly artifactsFolder: string;
|
|
22
|
+
/** Report an assertion outcome to the run verdict. */
|
|
23
|
+
readonly onAssert: (pass: boolean, path: string, message: string) => void;
|
|
24
|
+
/** Called when this node fails while unobserved — the tree tears down + records a fault. */
|
|
25
|
+
readonly onUnhandledFailure: (path: string, error: unknown) => void;
|
|
26
|
+
/** Bubble a milestone line into this node's parent log (no-op for the root). */
|
|
27
|
+
readonly bubble: (message: string) => void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One node in the {@link ExecutionTree}: its identity, tags, timing, and terminal
|
|
31
|
+
* state, plus the run handle (`result`/`ref`/`once`/`tag`/`cancel`). Self-contained —
|
|
32
|
+
* `run(exec, args)` executes the body inside the error boundary and settles the
|
|
33
|
+
* node; it never throws (the failure is delivered through `.result`).
|
|
34
|
+
*/
|
|
35
|
+
export declare class ActionRun<Result = unknown, Events extends object = object, Handle = unknown> implements RunHandle<Result, Events, Handle> {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly uuid: string;
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly path: string;
|
|
40
|
+
readonly parentId: string | null;
|
|
41
|
+
readonly tags: string[];
|
|
42
|
+
/** The captured args this node ran with (for the log header); set at run start. */
|
|
43
|
+
args: readonly unknown[];
|
|
44
|
+
readonly children: ActionRun[];
|
|
45
|
+
readonly controller: AbortController;
|
|
46
|
+
status: NodeStatus;
|
|
47
|
+
value: Result | undefined;
|
|
48
|
+
error: unknown;
|
|
49
|
+
startedAt: number;
|
|
50
|
+
endedAt: number;
|
|
51
|
+
duration: number;
|
|
52
|
+
private readonly store;
|
|
53
|
+
private readonly snapshots;
|
|
54
|
+
private readonly artifactsFolder;
|
|
55
|
+
private readonly onAssert;
|
|
56
|
+
private readonly onUnhandledFailure;
|
|
57
|
+
private readonly bubble;
|
|
58
|
+
private readonly emitter;
|
|
59
|
+
private readonly settled;
|
|
60
|
+
private readonly handle;
|
|
61
|
+
private failureHandler;
|
|
62
|
+
private attached;
|
|
63
|
+
private observed;
|
|
64
|
+
private refObserved;
|
|
65
|
+
private cascadeCancelled;
|
|
66
|
+
constructor(init: NodeInit);
|
|
67
|
+
/** A stable reference for headers/attribution (tags are the accumulated set now). */
|
|
68
|
+
toRef(): ActionRef;
|
|
69
|
+
/** This node's abort signal — handed to the exec as `ctx.signal`. */
|
|
70
|
+
get signal(): AbortSignal;
|
|
71
|
+
get result(): Promise<Outcome<Result>>;
|
|
72
|
+
/** The attached handle; rejects on failure/cancel. */
|
|
73
|
+
get ref(): Promise<Handle>;
|
|
74
|
+
on<K extends keyof InstanceEvents<Events, Result>>(key: K, handler: EventHandler<InstanceEvents<Events, Result>[K]>): void;
|
|
75
|
+
once<K extends keyof InstanceEvents<Events, Result>>(key: K): Promise<InstanceEvents<Events, Result>[K] | undefined>;
|
|
76
|
+
tag(name: string): this;
|
|
77
|
+
handleFailure(handler: (error: unknown) => void): this;
|
|
78
|
+
cancel(): void;
|
|
79
|
+
/** Run the body inside the error boundary, settling the node. Never rejects. */
|
|
80
|
+
run(exec: Exec<Events, Handle, Result>, args: readonly unknown[]): Promise<void>;
|
|
81
|
+
private context;
|
|
82
|
+
private assertInvariant;
|
|
83
|
+
private captureSnapshot;
|
|
84
|
+
private attach;
|
|
85
|
+
private succeed;
|
|
86
|
+
private fail;
|
|
87
|
+
/** The framework's own emits (loosely typed — sidesteps EmitArgs for void payloads). */
|
|
88
|
+
private sysEmit;
|
|
89
|
+
private finishTiming;
|
|
90
|
+
/**
|
|
91
|
+
* A lifecycle transition (`launched`, `done · ok|failed|cancelled`) — logged as an
|
|
92
|
+
* `event` on this node so it joins the unified event stream (`events.log`) and the
|
|
93
|
+
* node's own log, distinct from user emits by its `lifecycle` source.
|
|
94
|
+
*/
|
|
95
|
+
private lifecycle;
|
|
96
|
+
private log;
|
|
97
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { defer } from "../foundation/Deferred.js";
|
|
2
|
+
import { events } from "../foundation/events.js";
|
|
3
|
+
import { CancelledError } from "../foundation/CancelledError.js";
|
|
4
|
+
import { renderValue } from "../foundation/format.js";
|
|
5
|
+
import { createProc } from "../system/proc.js";
|
|
6
|
+
/**
|
|
7
|
+
* One node in the {@link ExecutionTree}: its identity, tags, timing, and terminal
|
|
8
|
+
* state, plus the run handle (`result`/`ref`/`once`/`tag`/`cancel`). Self-contained —
|
|
9
|
+
* `run(exec, args)` executes the body inside the error boundary and settles the
|
|
10
|
+
* node; it never throws (the failure is delivered through `.result`).
|
|
11
|
+
*/
|
|
12
|
+
export class ActionRun {
|
|
13
|
+
id;
|
|
14
|
+
uuid;
|
|
15
|
+
name;
|
|
16
|
+
path;
|
|
17
|
+
parentId;
|
|
18
|
+
tags = [];
|
|
19
|
+
/** The captured args this node ran with (for the log header); set at run start. */
|
|
20
|
+
args = [];
|
|
21
|
+
children = [];
|
|
22
|
+
controller = new AbortController();
|
|
23
|
+
status = "running";
|
|
24
|
+
value;
|
|
25
|
+
error;
|
|
26
|
+
startedAt = 0;
|
|
27
|
+
endedAt = 0;
|
|
28
|
+
duration = 0;
|
|
29
|
+
store;
|
|
30
|
+
snapshots;
|
|
31
|
+
artifactsFolder;
|
|
32
|
+
onAssert;
|
|
33
|
+
onUnhandledFailure;
|
|
34
|
+
bubble;
|
|
35
|
+
emitter = events();
|
|
36
|
+
settled = defer();
|
|
37
|
+
handle = defer();
|
|
38
|
+
failureHandler;
|
|
39
|
+
attached = false;
|
|
40
|
+
observed = false;
|
|
41
|
+
refObserved = false;
|
|
42
|
+
// Set only by the parent-signal abort listener (an ANCESTOR cancelling this node).
|
|
43
|
+
// Distinguishes a run/ancestor teardown from this node's own subtree-abort on failure,
|
|
44
|
+
// so the unobserved-failure check below isn't fooled by our own controller.abort().
|
|
45
|
+
cascadeCancelled = false;
|
|
46
|
+
constructor(init) {
|
|
47
|
+
this.id = init.id;
|
|
48
|
+
this.uuid = init.uuid;
|
|
49
|
+
this.name = init.name;
|
|
50
|
+
this.path = init.path;
|
|
51
|
+
this.parentId = init.parentId;
|
|
52
|
+
this.store = init.store;
|
|
53
|
+
this.snapshots = init.snapshots;
|
|
54
|
+
this.artifactsFolder = init.artifactsFolder;
|
|
55
|
+
this.onAssert = init.onAssert;
|
|
56
|
+
this.onUnhandledFailure = init.onUnhandledFailure;
|
|
57
|
+
this.bubble = init.bubble;
|
|
58
|
+
// Chain cancellation: an ancestor's abort ⇒ this subtree aborts (and is flagged as a
|
|
59
|
+
// cascade, so a concurrent own-failure isn't misread as a fresh unhandled fault).
|
|
60
|
+
const parent = init.parentSignal;
|
|
61
|
+
const onAncestorAbort = () => {
|
|
62
|
+
this.cascadeCancelled = true;
|
|
63
|
+
this.controller.abort();
|
|
64
|
+
};
|
|
65
|
+
if (parent) {
|
|
66
|
+
if (parent.aborted)
|
|
67
|
+
onAncestorAbort();
|
|
68
|
+
else
|
|
69
|
+
parent.addEventListener("abort", onAncestorAbort, { once: true });
|
|
70
|
+
}
|
|
71
|
+
// A void-handle awaiter shouldn't surface an unhandled rejection at settle.
|
|
72
|
+
void this.handle.promise.catch(() => { });
|
|
73
|
+
// An unobserved failure is routed to teardown explicitly; swallow the raw rejection
|
|
74
|
+
// here (via the field, not the `done` getter) so it isn't also an unhandledRejection.
|
|
75
|
+
void this.settled.promise.catch(() => { });
|
|
76
|
+
}
|
|
77
|
+
/** A stable reference for headers/attribution (tags are the accumulated set now). */
|
|
78
|
+
toRef() {
|
|
79
|
+
return { id: this.id, name: this.name, path: this.path, tags: [...this.tags] };
|
|
80
|
+
}
|
|
81
|
+
/** This node's abort signal — handed to the exec as `ctx.signal`. */
|
|
82
|
+
get signal() {
|
|
83
|
+
return this.controller.signal;
|
|
84
|
+
}
|
|
85
|
+
// ── Run handle surface ─────────────────────────────────────────────────────
|
|
86
|
+
get result() {
|
|
87
|
+
this.observed = true;
|
|
88
|
+
return this.settled.promise.then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
|
|
89
|
+
}
|
|
90
|
+
/** The attached handle; rejects on failure/cancel. */
|
|
91
|
+
get ref() {
|
|
92
|
+
this.refObserved = true;
|
|
93
|
+
return this.handle.promise;
|
|
94
|
+
}
|
|
95
|
+
on(key, handler) {
|
|
96
|
+
// A late `on("error")` (e.g. attached after a synchronously-throwing body already
|
|
97
|
+
// failed inline during `.exec()`) still receives the failure: the emitter snapshots
|
|
98
|
+
// the last "error" payload and replays it to a new listener.
|
|
99
|
+
if (key === "error")
|
|
100
|
+
this.observed = true;
|
|
101
|
+
this.emitter.listen(key, handler);
|
|
102
|
+
}
|
|
103
|
+
once(key) {
|
|
104
|
+
if (key === "error")
|
|
105
|
+
this.observed = true;
|
|
106
|
+
if (key === "done") {
|
|
107
|
+
return this.settled.promise.then((v) => v, () => undefined);
|
|
108
|
+
}
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
let done = false;
|
|
111
|
+
const settle = (v) => {
|
|
112
|
+
if (!done) {
|
|
113
|
+
done = true;
|
|
114
|
+
resolve(v);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const doneKey = "done";
|
|
118
|
+
this.emitter.listenOnce(key, (p) => settle(p));
|
|
119
|
+
this.emitter.listenOnce(doneKey, () => settle(undefined));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
tag(name) {
|
|
123
|
+
this.tags.push(name);
|
|
124
|
+
this.log("tag", "tag", name);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
handleFailure(handler) {
|
|
128
|
+
this.failureHandler = handler;
|
|
129
|
+
this.observed = true;
|
|
130
|
+
// A synchronously-throwing body fails inline during `.exec()`, before this attaches:
|
|
131
|
+
// deliver the already-recorded failure now (it won't fail again). Cancellation is
|
|
132
|
+
// status "cancelled", not "failed", so a cancelled node never runs the handler.
|
|
133
|
+
if (this.status === "failed")
|
|
134
|
+
handler(this.error);
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
cancel() {
|
|
138
|
+
this.controller.abort();
|
|
139
|
+
}
|
|
140
|
+
// ── Execution ──────────────────────────────────────────────────────────────
|
|
141
|
+
/** Run the body inside the error boundary, settling the node. Never rejects. */
|
|
142
|
+
async run(exec, args) {
|
|
143
|
+
this.args = args;
|
|
144
|
+
this.startedAt = Date.now();
|
|
145
|
+
this.lifecycle("launched");
|
|
146
|
+
try {
|
|
147
|
+
const result = await exec(this.context(), ...args);
|
|
148
|
+
this.succeed(result);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.fail(error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
context() {
|
|
155
|
+
return {
|
|
156
|
+
signal: this.signal,
|
|
157
|
+
logs: this.store,
|
|
158
|
+
emit: ((key, payload) => {
|
|
159
|
+
this.emitter.emit(key, payload);
|
|
160
|
+
const rendered = payload === undefined ? "" : ` · ${renderValue(payload)}`;
|
|
161
|
+
this.log("event", "event", `${String(key)}${rendered}`);
|
|
162
|
+
this.bubble(`⚡ ${String(key)}${rendered}`);
|
|
163
|
+
}),
|
|
164
|
+
attach: (handle) => this.attach(handle),
|
|
165
|
+
tag: (name) => void this.tag(name),
|
|
166
|
+
assert: (condition, message) => this.assertInvariant(condition, message),
|
|
167
|
+
snapshot: (name, value) => this.captureSnapshot(name, value),
|
|
168
|
+
artifactsFolder: this.artifactsFolder,
|
|
169
|
+
proc: createProc((source, level, message) => this.store.append({ nodeId: this.id, path: this.path, level, source, message }), this.signal),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
assertInvariant(condition, message) {
|
|
173
|
+
const line = `${condition ? "⊨ pass" : "⊭ FAIL"} · ${message}`;
|
|
174
|
+
this.log("assert", "assert", line);
|
|
175
|
+
this.bubble(line);
|
|
176
|
+
this.onAssert(condition, this.path, message);
|
|
177
|
+
}
|
|
178
|
+
async captureSnapshot(name, value) {
|
|
179
|
+
const file = await this.snapshots.write(name, value, this.id);
|
|
180
|
+
this.log("snapshot", "snapshot", `${name} → ${file}`);
|
|
181
|
+
this.bubble(`📸 ${name} → ${file}`);
|
|
182
|
+
return file;
|
|
183
|
+
}
|
|
184
|
+
attach(handle) {
|
|
185
|
+
if (this.attached)
|
|
186
|
+
return;
|
|
187
|
+
this.attached = true;
|
|
188
|
+
this.handle.resolve(handle);
|
|
189
|
+
this.sysEmit("attached");
|
|
190
|
+
this.bubble("attached");
|
|
191
|
+
}
|
|
192
|
+
succeed(result) {
|
|
193
|
+
this.finishTiming();
|
|
194
|
+
this.status = "ok";
|
|
195
|
+
this.value = result;
|
|
196
|
+
this.lifecycle("done · ok");
|
|
197
|
+
this.bubble("✓ done · ok");
|
|
198
|
+
this.settled.resolve(result);
|
|
199
|
+
this.sysEmit("done", result);
|
|
200
|
+
// Settle a never-attached void handle so `.ref` awaiters resolve instead of hang.
|
|
201
|
+
this.handle.resolve(undefined);
|
|
202
|
+
}
|
|
203
|
+
fail(error) {
|
|
204
|
+
this.finishTiming();
|
|
205
|
+
// Classify: an aborted signal ⇒ intentional teardown, not a fault. The thrown
|
|
206
|
+
// error is then just the killed-child artifact (often blank), so we drop it and
|
|
207
|
+
// record a clean stop; a genuine failure keeps its error and the ✗ styling.
|
|
208
|
+
if (this.signal.aborted) {
|
|
209
|
+
this.status = "cancelled";
|
|
210
|
+
this.error = error;
|
|
211
|
+
this.lifecycle("done · cancelled");
|
|
212
|
+
this.bubble("⊘ done · cancelled");
|
|
213
|
+
this.sysEmit("done", undefined);
|
|
214
|
+
this.settled.reject(new CancelledError());
|
|
215
|
+
this.handle.reject(new CancelledError());
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.status = "failed";
|
|
219
|
+
this.error = error;
|
|
220
|
+
this.log("error", "error", messageOf(error));
|
|
221
|
+
this.bubble(`✗ ${firstLine(messageOf(error))}`);
|
|
222
|
+
this.sysEmit("error", error);
|
|
223
|
+
this.lifecycle("done · failed");
|
|
224
|
+
this.bubble("✗ done · failed");
|
|
225
|
+
this.sysEmit("done", undefined);
|
|
226
|
+
this.settled.reject(error);
|
|
227
|
+
this.handle.reject(error);
|
|
228
|
+
// A failed action's work is over: abort its OWN subtree (children → CancelledError,
|
|
229
|
+
// procs killed). This is local cleanup, independent of the run verdict. It sets our
|
|
230
|
+
// signal.aborted, which is why the check below uses `cascadeCancelled`, not
|
|
231
|
+
// `signal.aborted`, to tell an unhandled fault from a run/ancestor teardown.
|
|
232
|
+
this.controller.abort();
|
|
233
|
+
// A registered handler owns a fire-and-forget failure (side effect only; not on cancel).
|
|
234
|
+
this.failureHandler?.(error);
|
|
235
|
+
// If, one microtask on, no one is positioned to receive this error (no `.result`/`.ref`
|
|
236
|
+
// read, no `on("error")`) and we weren't caught in an ancestor's teardown, it's
|
|
237
|
+
// unhandled — tell the tree to tear the run down and record the fault.
|
|
238
|
+
// `sysEmit("error")` above already ran, so a live error handler has set `observed`.
|
|
239
|
+
queueMicrotask(() => {
|
|
240
|
+
const observed = this.observed || (this.refObserved && !this.attached);
|
|
241
|
+
if (!observed && !this.cascadeCancelled)
|
|
242
|
+
this.onUnhandledFailure(this.path, error);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/** The framework's own emits (loosely typed — sidesteps EmitArgs for void payloads). */
|
|
246
|
+
sysEmit(key, payload) {
|
|
247
|
+
this.emitter.emit(key, payload);
|
|
248
|
+
}
|
|
249
|
+
finishTiming() {
|
|
250
|
+
this.endedAt = Date.now();
|
|
251
|
+
this.duration = this.endedAt - this.startedAt;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* A lifecycle transition (`launched`, `done · ok|failed|cancelled`) — logged as an
|
|
255
|
+
* `event` on this node so it joins the unified event stream (`events.log`) and the
|
|
256
|
+
* node's own log, distinct from user emits by its `lifecycle` source.
|
|
257
|
+
*/
|
|
258
|
+
lifecycle(message) {
|
|
259
|
+
this.log("event", "lifecycle", message);
|
|
260
|
+
}
|
|
261
|
+
log(level, source, message) {
|
|
262
|
+
this.store.append({ nodeId: this.id, path: this.path, level, source, message });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const messageOf = (error) => error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
266
|
+
const firstLine = (message) => message.split("\n", 1)[0] ?? message;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { LogStore } from "../output/log/LogStore.ts";
|
|
2
|
+
import { RunFolder } from "../output/folder/RunFolder.ts";
|
|
3
|
+
import type { RunView } from "../output/writers/views.ts";
|
|
4
|
+
import { ActionRun } from "./ActionRun.ts";
|
|
5
|
+
import type { Exec, LaunchSpec } from "./types.ts";
|
|
6
|
+
/**
|
|
7
|
+
* The run: a module singleton that owns the log store, the run folder, the node
|
|
8
|
+
* registry, and the process-wide teardown. `launch` creates a child node under the
|
|
9
|
+
* ambient current action and runs it; `runRoot` runs the `om` body as the root and
|
|
10
|
+
* drives the whole thing to `finalize`, which always produces the artifacts.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ExecutionTree {
|
|
13
|
+
static current: ExecutionTree | null;
|
|
14
|
+
/** The most recent run — kept for test/debug introspection (not nulled at finalize). */
|
|
15
|
+
static last: ExecutionTree | null;
|
|
16
|
+
/** The ambient-run guard: an action can only run inside a root `om(...)`. */
|
|
17
|
+
static require(): ExecutionTree;
|
|
18
|
+
/** For tests: drop the singleton so the next `om(...)` starts clean. */
|
|
19
|
+
static reset(): void;
|
|
20
|
+
readonly store: LogStore;
|
|
21
|
+
readonly folder: RunFolder;
|
|
22
|
+
readonly root: ActionRun;
|
|
23
|
+
private readonly rawStream;
|
|
24
|
+
private readonly consoleCapture;
|
|
25
|
+
private readonly snapshotStore;
|
|
26
|
+
private readonly registry;
|
|
27
|
+
private readonly unsettled;
|
|
28
|
+
private readonly originalLog;
|
|
29
|
+
private streaming;
|
|
30
|
+
private liveRendering;
|
|
31
|
+
private assertionsPassed;
|
|
32
|
+
private assertionsFailed;
|
|
33
|
+
private readonly faults;
|
|
34
|
+
static graceMs: number;
|
|
35
|
+
private phase;
|
|
36
|
+
private finalizing;
|
|
37
|
+
private graceTimer;
|
|
38
|
+
private readonly teardownGrace;
|
|
39
|
+
private readonly onSigint;
|
|
40
|
+
private readonly onUncaught;
|
|
41
|
+
private readonly onUnhandled;
|
|
42
|
+
constructor();
|
|
43
|
+
/** Launch an instance under the ambient current node (or the root). */
|
|
44
|
+
launch(spec: LaunchSpec): ActionRun;
|
|
45
|
+
private append;
|
|
46
|
+
/** The shared services every node is built with (log store, snapshots, assert tally). */
|
|
47
|
+
private nodeDeps;
|
|
48
|
+
private recordFault;
|
|
49
|
+
private recordAssert;
|
|
50
|
+
/** Tear the whole run down (aborts the root, cascading to every node). Idempotent. */
|
|
51
|
+
cancel(): void;
|
|
52
|
+
/**
|
|
53
|
+
* The sole teardown path: `open → closing`, narrate the reason once, abort the root
|
|
54
|
+
* (cascading to every node — procs killed, waits unblocked, daemons stopped). Later
|
|
55
|
+
* calls are no-ops. A single grace timer guarantees finalize even if a node's cleanup
|
|
56
|
+
* wedges after its proc is already dead, so the run always writes its log.
|
|
57
|
+
*/
|
|
58
|
+
teardown(reason: string): void;
|
|
59
|
+
/** An escaped throw/rejection: log it, record it (unless already tearing down), tear down. */
|
|
60
|
+
private onFatal;
|
|
61
|
+
/** Run the `om` body as the root node, then drive to finalize. */
|
|
62
|
+
runRoot(body: Exec<object, unknown, unknown>): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Track a launched node's `run()` promise until it settles (drives `drive()`'s
|
|
65
|
+
* wait). `run()` itself never rejects — the node's own constructor already
|
|
66
|
+
* swallows its settled rejection (via the private field, not the `.result` getter,
|
|
67
|
+
* so it doesn't mark the node observed and defeat the unobserved-failure check).
|
|
68
|
+
*/
|
|
69
|
+
private trackNode;
|
|
70
|
+
private drive;
|
|
71
|
+
private finalize;
|
|
72
|
+
private finalizeOnce;
|
|
73
|
+
/** The end-of-run links + assert summary, to the terminal (files are already written). */
|
|
74
|
+
private printSummary;
|
|
75
|
+
private verdictOk;
|
|
76
|
+
private nodeView;
|
|
77
|
+
/** Test seam: the projected run view (same object written to result.json). */
|
|
78
|
+
runViewForTest(): RunView;
|
|
79
|
+
private runView;
|
|
80
|
+
/**
|
|
81
|
+
* Absolute `.log` path for a node — root is `main.log`, children drop the `main/`
|
|
82
|
+
* prefix. Each id segment is sanitized so names with filesystem-illegal characters
|
|
83
|
+
* (e.g. `TSKB:root:watch:docs`) still produce a valid path.
|
|
84
|
+
*/
|
|
85
|
+
private logFile;
|
|
86
|
+
private rawStreamPath;
|
|
87
|
+
private narrate;
|
|
88
|
+
}
|