omkit 0.0.9 → 0.0.11
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/actions/chrome-page.d.ts +1 -1
- package/dist/actions/chrome-page.js +1 -1
- package/dist/actions/command.d.ts +1 -1
- package/dist/actions/command.js +5 -6
- package/dist/actions/healthcheck.d.ts +1 -1
- package/dist/actions/healthcheck.js +1 -1
- package/dist/actions/prompt.d.ts +1 -1
- package/dist/actions/prompt.js +1 -1
- package/dist/actions/tail-log.d.ts +1 -1
- package/dist/actions/tail-log.js +1 -1
- package/dist/actions/until-log.d.ts +2 -2
- package/dist/actions/until-log.js +1 -1
- package/dist/actions/watch-dir.d.ts +1 -1
- package/dist/actions/watch-dir.js +1 -1
- package/dist/actions/watch.d.ts +1 -1
- package/dist/actions/watch.js +1 -1
- package/dist/index.d.ts +13 -17
- package/dist/index.js +15 -17
- package/dist/orchestration/action/action.d.ts +23 -0
- package/dist/orchestration/action/action.js +255 -0
- package/dist/{core/action.d.ts → orchestration/action/types.d.ts} +38 -27
- package/dist/orchestration/action/types.js +1 -0
- package/dist/{core → orchestration/events}/events.d.ts +9 -1
- package/dist/{core → orchestration/events}/events.js +14 -22
- package/dist/orchestration/spin/SpinHost.d.ts +105 -0
- package/dist/orchestration/spin/SpinHost.js +303 -0
- package/dist/orchestration/spin/spin.d.ts +24 -0
- package/dist/orchestration/spin/spin.js +56 -0
- package/dist/orchestration/spin/types.d.ts +56 -0
- package/dist/orchestration/spin/types.js +1 -0
- package/dist/output/Output.d.ts +24 -0
- package/dist/output/Output.js +27 -0
- package/dist/output/folder/RunFolder.d.ts +29 -0
- package/dist/output/folder/RunFolder.js +56 -0
- package/dist/output/index.d.ts +11 -0
- package/dist/output/index.js +11 -0
- package/dist/output/log/LogRenderer.d.ts +73 -0
- package/dist/output/log/LogRenderer.js +131 -0
- package/dist/{core/log-collector → output/log}/LogsCollector.d.ts +14 -0
- package/dist/{core/log-collector → output/log}/LogsCollector.js +24 -1
- package/dist/output/log/RunLog.d.ts +44 -0
- package/dist/output/log/RunLog.js +119 -0
- package/dist/output/snapshot/SnapshotStore.d.ts +49 -0
- package/dist/output/snapshot/SnapshotStore.js +61 -0
- package/dist/system/fs/FolderCache.d.ts +47 -0
- package/dist/system/fs/FolderCache.js +117 -0
- package/dist/system/fs/streams.d.ts +11 -0
- package/dist/system/fs/streams.js +14 -0
- package/dist/{core → system/process}/process.d.ts +9 -8
- package/dist/{core → system/process}/process.js +8 -9
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.js +9 -0
- package/dist/utils/serialize.d.ts +2 -0
- package/dist/utils/serialize.js +12 -0
- package/package.json +1 -1
- package/dist/core/action.js +0 -201
- package/dist/core/folder-cache.d.ts +0 -39
- package/dist/core/folder-cache.js +0 -109
- package/dist/core/log-collector/global.d.ts +0 -7
- package/dist/core/log-collector/global.js +0 -7
- package/dist/core/log-collector/render.d.ts +0 -68
- package/dist/core/log-collector/render.js +0 -132
- package/dist/core/markers.d.ts +0 -9
- package/dist/core/markers.js +0 -13
- package/dist/core/output.d.ts +0 -68
- package/dist/core/output.js +0 -213
- package/dist/core/run.d.ts +0 -88
- package/dist/core/run.js +0 -257
- package/dist/core/spin.d.ts +0 -72
- package/dist/core/spin.js +0 -45
- /package/dist/{core/helpers → utils}/AsyncQueue.d.ts +0 -0
- /package/dist/{core/helpers → utils}/AsyncQueue.js +0 -0
package/dist/core/run.d.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { type AnyActionInstance } from "./action.ts";
|
|
2
|
-
import { log as logs } from "./log-collector/global.ts";
|
|
3
|
-
/**
|
|
4
|
-
* A single action's failure, captured for the spin's verdict. Cascade failures —
|
|
5
|
-
* actions that settled `{ ok: false }` only because teardown aborted them — are
|
|
6
|
-
* deliberately excluded (see `launch`'s outcome handling), so this records
|
|
7
|
-
* genuine faults, not fallout.
|
|
8
|
-
*/
|
|
9
|
-
export interface ActionFailure {
|
|
10
|
-
/** The failing action's name. */
|
|
11
|
-
readonly action: string;
|
|
12
|
-
/** Whatever it failed with (the Outcome's `error`). */
|
|
13
|
-
readonly error: unknown;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* The terminal verdict of a spin: did everything that mattered succeed, and if
|
|
17
|
-
* not, what failed. {@link Spin.done} resolves (never rejects) with this, so a
|
|
18
|
-
* single `await` yields the outcome instead of forcing a try/catch on awaiters.
|
|
19
|
-
* A failing action is *recorded* here (so `ok` is false and the exit code is 1)
|
|
20
|
-
* but does **not** tear the spin down — see {@link Spin}.
|
|
21
|
-
*/
|
|
22
|
-
export interface SpinResult {
|
|
23
|
-
/** True when no genuine failure was recorded. */
|
|
24
|
-
readonly ok: boolean;
|
|
25
|
-
/** The genuine failures, in the order they surfaced. Empty when `ok`. */
|
|
26
|
-
readonly failures: readonly ActionFailure[];
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* The spin's lifecycle. It only ever moves forward:
|
|
30
|
-
*
|
|
31
|
-
* open ──▶ closing ──▶ closed
|
|
32
|
-
*
|
|
33
|
-
* - **open** — launching and running actions; `.run(…)` is accepted.
|
|
34
|
-
* - **closing** — teardown has begun (cancel, Ctrl-C, natural completion, or an
|
|
35
|
-
* uncaught fatal). The signal is aborted, in-flight actions are unwinding, and
|
|
36
|
-
* new `.run(…)` is refused. Note: a failing *action* does not begin teardown.
|
|
37
|
-
* - **closed** — everything has settled, the log is flushed, the verdict is final.
|
|
38
|
-
*/
|
|
39
|
-
export type SpinState = "open" | "closing" | "closed";
|
|
40
|
-
/** A live spin: a set of actions sharing one abort controller and the global log. */
|
|
41
|
-
export interface Spin {
|
|
42
|
-
/** The shared global log (output + events for everything in this spin). */
|
|
43
|
-
readonly logs: typeof logs;
|
|
44
|
-
/** The current lifecycle state — see {@link SpinState}. */
|
|
45
|
-
readonly state: SpinState;
|
|
46
|
-
/** Settles once the spin reaches `closed`, carrying the terminal {@link SpinResult}. */
|
|
47
|
-
readonly done: Promise<SpinResult>;
|
|
48
|
-
/** The spin's abort signal — for consumers that want to observe teardown. */
|
|
49
|
-
readonly signal: AbortSignal;
|
|
50
|
-
/**
|
|
51
|
-
* Launch more actions into this same spin — same controller, same log. Chainable.
|
|
52
|
-
*
|
|
53
|
-
* Throws if the spin is no longer `open`: launching into a closing/closed spin
|
|
54
|
-
* would bind the actions to an already-aborted signal, so they'd start and
|
|
55
|
-
* immediately unwind, silently doing nothing. Launch before teardown, or from
|
|
56
|
-
* a lifecycle event of an action that is still running (those fire while the
|
|
57
|
-
* spin is still `open`).
|
|
58
|
-
*/
|
|
59
|
-
run(...actions: AnyActionInstance[]): Spin;
|
|
60
|
-
/** Begin teardown: abort the signal (killing procs, unblocking waits, stopping daemons). */
|
|
61
|
-
cancel(): void;
|
|
62
|
-
/**
|
|
63
|
-
* Start streaming the global log live to a writer (console by default) in the
|
|
64
|
-
* background; it stops on its own when the spin settles. Returns the spin, so it
|
|
65
|
-
* chains. Await `.done` for the verdict.
|
|
66
|
-
*/
|
|
67
|
-
drain(write?: (message: string) => void): Spin;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Starts the spin: launches the given actions in parallel under one abort
|
|
71
|
-
* controller, against the process-wide global log. Returns a {@link Spin} — add
|
|
72
|
-
* more actions on demand with `.run(…)`, observe `.done` for the verdict, stop
|
|
73
|
-
* with `.cancel()`.
|
|
74
|
-
*
|
|
75
|
-
* **Callable once per process.** A spin *is* the world being modelled, and there's
|
|
76
|
-
* one global log per world; a second call would silently share that log, so it
|
|
77
|
-
* throws instead. Attach further actions — including event-driven ones — to the
|
|
78
|
-
* returned {@link Spin} via its `.run(…)` method, not by calling this again.
|
|
79
|
-
*
|
|
80
|
-
* Each action runs against its own child logger (which headers and indents its
|
|
81
|
-
* lines into the global log) and the spin's shared abort signal — both injected
|
|
82
|
-
* into its context, where the framework also binds a `proc` to them so child
|
|
83
|
-
* processes stream into the right log and die on teardown. A **failing action
|
|
84
|
-
* never tears the spin down** — its `{ ok: false }` outcome is recorded in the
|
|
85
|
-
* verdict (and sets exit code 1) and the spin keeps running. Teardown to `closed`
|
|
86
|
-
* happens only on Ctrl-C, `.cancel()`, natural completion, or an uncaught fatal.
|
|
87
|
-
*/
|
|
88
|
-
export declare function run(...actions: AnyActionInstance[]): Spin;
|
package/dist/core/run.js
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
|
-
import { producerOfRef } from "./action.js";
|
|
3
|
-
import { log as logs } from "./log-collector/global.js";
|
|
4
|
-
import { createRenderer } from "./log-collector/render.js";
|
|
5
|
-
import { streamLog, writeLog } from "./output.js";
|
|
6
|
-
import { marker } from "./markers.js";
|
|
7
|
-
/** One spin per process — a single world over the one global log. See {@link run}. */
|
|
8
|
-
let started = false;
|
|
9
|
-
/** Narrate a run-lifecycle milestone into the global log (renders as a `[run]` line). */
|
|
10
|
-
const narrate = (message) => logs.append({ source: "run", level: "run", message });
|
|
11
|
-
/** The entry script, relative to where node was invoked (so the log says what produced it). */
|
|
12
|
-
const scriptPath = () => {
|
|
13
|
-
const entry = process.argv[1];
|
|
14
|
-
return entry ? relative(process.cwd(), entry) : "(unknown)";
|
|
15
|
-
};
|
|
16
|
-
/** A constructed instance is an object carrying a `start` function. */
|
|
17
|
-
const isActionInstance = (value) => typeof value === "object" &&
|
|
18
|
-
value !== null &&
|
|
19
|
-
typeof value.start === "function";
|
|
20
|
-
/** An *uncalled* action: the callable `action(name).run(…)` returns before it's invoked. */
|
|
21
|
-
const isUncalledAction = (value) => typeof value === "function" && typeof value.actionName === "string";
|
|
22
|
-
/**
|
|
23
|
-
* Guard each launch argument: an action *instance* is required, not the action
|
|
24
|
-
* itself. Forgetting the trailing `()` is an easy slip (the callable has no
|
|
25
|
-
* `.start`), and silently treating it as something else would report a false
|
|
26
|
-
* green — so fail loudly with the fix.
|
|
27
|
-
*/
|
|
28
|
-
function assertInstance(value) {
|
|
29
|
-
if (isActionInstance(value))
|
|
30
|
-
return;
|
|
31
|
-
const hint = isUncalledAction(value)
|
|
32
|
-
? ` — that looks like an action that wasn't invoked; construct an instance by calling it: action("…").run(…)()`
|
|
33
|
-
: "";
|
|
34
|
-
throw new Error(`run() expects action instances; received ${typeof value}${hint}`);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Starts the spin: launches the given actions in parallel under one abort
|
|
38
|
-
* controller, against the process-wide global log. Returns a {@link Spin} — add
|
|
39
|
-
* more actions on demand with `.run(…)`, observe `.done` for the verdict, stop
|
|
40
|
-
* with `.cancel()`.
|
|
41
|
-
*
|
|
42
|
-
* **Callable once per process.** A spin *is* the world being modelled, and there's
|
|
43
|
-
* one global log per world; a second call would silently share that log, so it
|
|
44
|
-
* throws instead. Attach further actions — including event-driven ones — to the
|
|
45
|
-
* returned {@link Spin} via its `.run(…)` method, not by calling this again.
|
|
46
|
-
*
|
|
47
|
-
* Each action runs against its own child logger (which headers and indents its
|
|
48
|
-
* lines into the global log) and the spin's shared abort signal — both injected
|
|
49
|
-
* into its context, where the framework also binds a `proc` to them so child
|
|
50
|
-
* processes stream into the right log and die on teardown. A **failing action
|
|
51
|
-
* never tears the spin down** — its `{ ok: false }` outcome is recorded in the
|
|
52
|
-
* verdict (and sets exit code 1) and the spin keeps running. Teardown to `closed`
|
|
53
|
-
* happens only on Ctrl-C, `.cancel()`, natural completion, or an uncaught fatal.
|
|
54
|
-
*/
|
|
55
|
-
export function run(...actions) {
|
|
56
|
-
if (started) {
|
|
57
|
-
throw new Error("run() may only be called once per process — attach more actions to the existing run with its .run(…) method.");
|
|
58
|
-
}
|
|
59
|
-
// Validate eagerly, before the one-run latch flips, so a bad argument throws
|
|
60
|
-
// cleanly and leaves the process's single run slot still usable.
|
|
61
|
-
for (const instance of actions)
|
|
62
|
-
assertInstance(instance);
|
|
63
|
-
started = true;
|
|
64
|
-
const controller = new AbortController();
|
|
65
|
-
// The log stays open through teardown and is closed once at finalize (after the
|
|
66
|
-
// last entry), so `streamLog` captures the whole run — including lines logged
|
|
67
|
-
// *during* teardown — and its run.jsonl is complete before `writeLog` reads it.
|
|
68
|
-
// ── Lifecycle state ──────────────────────────────────────────────────────
|
|
69
|
-
// The single source of truth for "where in its life is this run". Only
|
|
70
|
-
// `beginClosing` and the finalize step below mutate it, and only forward.
|
|
71
|
-
let state = "open";
|
|
72
|
-
// Genuine failures (not teardown cascade). Frozen into the verdict at close.
|
|
73
|
-
const failures = [];
|
|
74
|
-
// Every launched action's promise lives here until it settles; the drain loop
|
|
75
|
-
// empties this set. Daemons keep it non-empty until the controller aborts.
|
|
76
|
-
const inFlight = new Set();
|
|
77
|
-
/**
|
|
78
|
-
* Move `open → closing`: abort the signal (unwinding procs, waits, and
|
|
79
|
-
* daemons), narrate the cause once, and stop accepting new actions. Idempotent
|
|
80
|
-
* — only the first caller transitions; later triggers (a second failure, a
|
|
81
|
-
* `cancel()` during teardown) are no-ops. This is the *only* path to abort, so
|
|
82
|
-
* every teardown reason flows through one place.
|
|
83
|
-
*/
|
|
84
|
-
const beginClosing = (reason) => {
|
|
85
|
-
if (state !== "open")
|
|
86
|
-
return;
|
|
87
|
-
state = "closing";
|
|
88
|
-
narrate(`tearing down · ${reason}`);
|
|
89
|
-
controller.abort();
|
|
90
|
-
};
|
|
91
|
-
/**
|
|
92
|
-
* Record a genuine fault in the verdict. It does **not** tear the spin down — a
|
|
93
|
-
* failing action is data, and the spin keeps running (only Ctrl-C / cancel /
|
|
94
|
-
* completion / an uncaught fatal close it).
|
|
95
|
-
*/
|
|
96
|
-
const recordFailure = (action, error) => {
|
|
97
|
-
failures.push({ action, error });
|
|
98
|
-
};
|
|
99
|
-
// Ctrl-C tears the spin down gracefully through the lifecycle, like cancel().
|
|
100
|
-
const onSigint = () => beginClosing("interrupted (SIGINT)");
|
|
101
|
-
process.once("SIGINT", onSigint);
|
|
102
|
-
// A throw or rejection that escaped every action's own handling. Without a
|
|
103
|
-
// handler Node prints it and exits *immediately* — before the run can flush its
|
|
104
|
-
// log, which leaves a silent, empty log directory and no verdict. So catch it,
|
|
105
|
-
// log it, fold it into the verdict, and drive teardown so the log still flushes
|
|
106
|
-
// and the exit code reflects the fault — degrading a stray throw to a recorded
|
|
107
|
-
// failure instead of a hard crash. Removed at finalize, so it only governs
|
|
108
|
-
// while this run owns the process.
|
|
109
|
-
const onFatal = (kind) => (error) => {
|
|
110
|
-
logs.append({
|
|
111
|
-
source: kind,
|
|
112
|
-
level: "error",
|
|
113
|
-
message: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
114
|
-
});
|
|
115
|
-
// Genuine fault vs. teardown fallout: only one that surfaces *before* the
|
|
116
|
-
// signal aborts is the spin's own (mirrors `launch`'s cascade rule); a throw
|
|
117
|
-
// during unwind is logged but doesn't pollute the verdict. Either way tear
|
|
118
|
-
// down — unlike an action's own failure, an *uncaught* fatal leaves the
|
|
119
|
-
// process state undefined, so we always finish and flush.
|
|
120
|
-
if (!controller.signal.aborted)
|
|
121
|
-
failures.push({ action: kind, error });
|
|
122
|
-
beginClosing(kind);
|
|
123
|
-
};
|
|
124
|
-
const onUncaught = onFatal("uncaughtException");
|
|
125
|
-
const onUnhandled = onFatal("unhandledRejection");
|
|
126
|
-
process.on("uncaughtException", onUncaught);
|
|
127
|
-
process.on("unhandledRejection", onUnhandled);
|
|
128
|
-
narrate(`run started · ${scriptPath()}`);
|
|
129
|
-
// Start streaming log entries to disk immediately — incremental writes survive
|
|
130
|
-
// hard kills where the final writeLog never runs. Kept so finalize can await it
|
|
131
|
-
// *before* writeLog rewrites the same files: otherwise a late flush from this
|
|
132
|
-
// stream lands past writeLog's truncation and the OS zero-fills the gap (a
|
|
133
|
-
// multi-MB NUL run in run.log).
|
|
134
|
-
const streaming = streamLog(logs).catch(() => { });
|
|
135
|
-
// Instances launched so far — used to verify a `.ref` dependency was launched
|
|
136
|
-
// before the action that consumes its handle.
|
|
137
|
-
const launched = new WeakSet();
|
|
138
|
-
const launch = (instance) => {
|
|
139
|
-
// A consumer built with another action's `.ref` (e.g. `chromePage(driver.ref)`)
|
|
140
|
-
// must be launched *after* that producer — otherwise it awaits a handle that
|
|
141
|
-
// never arrives and the spin hangs silently. Catch it and say how to fix it.
|
|
142
|
-
for (const arg of instance.args) {
|
|
143
|
-
const producer = producerOfRef(arg);
|
|
144
|
-
if (producer && !launched.has(producer)) {
|
|
145
|
-
throw new Error(`${instance.name} was launched before its dependency ${producer.name}: it ` +
|
|
146
|
-
`consumes ${producer.name}'s .ref, so nod ${producer.name} first.`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
launched.add(instance);
|
|
150
|
-
narrate(`launch ${instance.name}`);
|
|
151
|
-
// The action logs into the one shared store (entries carry their `source`;
|
|
152
|
-
// grouping is applied at render time) and gets the spin's signal directly; the
|
|
153
|
-
// framework injects a `proc` bound to both, so there's no ambient context.
|
|
154
|
-
// `start()` resolves with the action's Outcome — it never rejects for the
|
|
155
|
-
// action's own failure — so a failure is handled here as data, not a throw.
|
|
156
|
-
const record = (error) => {
|
|
157
|
-
// Always record the fault in the action's own log, under its name.
|
|
158
|
-
logs.append({
|
|
159
|
-
source: instance.name,
|
|
160
|
-
level: "error",
|
|
161
|
-
message: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
162
|
-
});
|
|
163
|
-
// Genuine failure vs. teardown fallout: if the signal was already aborted
|
|
164
|
-
// when this surfaced, the failure is almost certainly the abort killing the
|
|
165
|
-
// action (a cascade), not its own fault — so it must not pollute the verdict.
|
|
166
|
-
if (!controller.signal.aborted)
|
|
167
|
-
recordFailure(instance.name, error);
|
|
168
|
-
};
|
|
169
|
-
const promise = instance
|
|
170
|
-
.start({ logs, signal: controller.signal })
|
|
171
|
-
.then((outcome) => {
|
|
172
|
-
if (!outcome.ok)
|
|
173
|
-
record(outcome.error);
|
|
174
|
-
})
|
|
175
|
-
// Defensive: `start()` shouldn't reject, but if the framework itself throws,
|
|
176
|
-
// treat it as a fault rather than losing it to an unhandled rejection.
|
|
177
|
-
.catch(record);
|
|
178
|
-
inFlight.add(promise);
|
|
179
|
-
void promise.finally(() => inFlight.delete(promise));
|
|
180
|
-
};
|
|
181
|
-
for (const instance of actions)
|
|
182
|
-
launch(instance);
|
|
183
|
-
// Drives the run to `closed`. The loop re-checks `inFlight` because actions can
|
|
184
|
-
// be added (or daemons can settle on abort) while we await. A synchronous
|
|
185
|
-
// `.run(…)` from a settling action's lifecycle event lands before that action's
|
|
186
|
-
// `finally` removes it, so `inFlight` never spuriously hits 0 mid-handoff.
|
|
187
|
-
const done = (async () => {
|
|
188
|
-
for (;;) {
|
|
189
|
-
if (inFlight.size === 0) {
|
|
190
|
-
// Nothing left to run. If still open, every action finished on its own —
|
|
191
|
-
// natural completion; tear down so daemons/listeners unwind uniformly.
|
|
192
|
-
// If already closing, teardown is simply complete.
|
|
193
|
-
if (state === "open")
|
|
194
|
-
beginClosing("completed");
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
await Promise.allSettled([...inFlight]);
|
|
198
|
-
}
|
|
199
|
-
// ── Finalize (reached exactly once) ──────────────────────────────────────
|
|
200
|
-
state = "closed";
|
|
201
|
-
process.off("SIGINT", onSigint);
|
|
202
|
-
process.off("uncaughtException", onUncaught);
|
|
203
|
-
process.off("unhandledRejection", onUnhandled);
|
|
204
|
-
narrate("finished");
|
|
205
|
-
// Close the log now (after the last entry) so `streamLog` drains and finishes
|
|
206
|
-
// writing the complete run.jsonl, then wait for it to fully close its files —
|
|
207
|
-
// both so its last flush can't corrupt run.log later, and so run.jsonl is done
|
|
208
|
-
// before `writeLog` streams it into the final collapsed run.log.
|
|
209
|
-
logs.close();
|
|
210
|
-
await streaming;
|
|
211
|
-
const file = await writeLog();
|
|
212
|
-
// Direct to the terminal — the file's already written, so this line isn't in it.
|
|
213
|
-
console.log(marker("run", `log → ${file}`));
|
|
214
|
-
const result = { ok: failures.length === 0, failures };
|
|
215
|
-
// One process = one run = one verdict; reflect pass/fail in the exit code so
|
|
216
|
-
// shells and CI see it without parsing the log. Only set it on failure, so a
|
|
217
|
-
// code a failing action's own handler may have set isn't clobbered to 0.
|
|
218
|
-
if (!result.ok)
|
|
219
|
-
process.exitCode = 1;
|
|
220
|
-
return result;
|
|
221
|
-
})();
|
|
222
|
-
// `done` resolves with the verdict and never rejects; guard anyway so a stray
|
|
223
|
-
// finalize error can't surface as an unhandled rejection.
|
|
224
|
-
done.catch(() => { });
|
|
225
|
-
const drain = (write = (message) => console.log(message)) => {
|
|
226
|
-
void (async () => {
|
|
227
|
-
// A renderer applies grouping/indentation as entries stream by; its state
|
|
228
|
-
// spans the replayed history and live entries as one continuous stream. The
|
|
229
|
-
// subscription ends itself when the run tears down (the collector closes it),
|
|
230
|
-
// so this loop simply runs to completion.
|
|
231
|
-
const render = createRenderer();
|
|
232
|
-
for await (const entry of logs.subscribe({ replay: true }))
|
|
233
|
-
write(render(entry));
|
|
234
|
-
})().catch(() => { });
|
|
235
|
-
return self;
|
|
236
|
-
};
|
|
237
|
-
const self = {
|
|
238
|
-
logs,
|
|
239
|
-
get state() {
|
|
240
|
-
return state;
|
|
241
|
-
},
|
|
242
|
-
done,
|
|
243
|
-
signal: controller.signal,
|
|
244
|
-
run(...more) {
|
|
245
|
-
if (state !== "open") {
|
|
246
|
-
throw new Error(`spin.run(…) rejected: the spin is "${state}", not "open". Launch actions before ` +
|
|
247
|
-
`teardown, or from a lifecycle event of an action that is still running.`);
|
|
248
|
-
}
|
|
249
|
-
for (const instance of more)
|
|
250
|
-
launch(instance);
|
|
251
|
-
return self;
|
|
252
|
-
},
|
|
253
|
-
cancel: () => beginClosing("cancelled"),
|
|
254
|
-
drain,
|
|
255
|
-
};
|
|
256
|
-
return self;
|
|
257
|
-
}
|
package/dist/core/spin.d.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { type AnyActionInstance } from "./action.ts";
|
|
2
|
-
import { type Spin } from "./run.ts";
|
|
3
|
-
/**
|
|
4
|
-
* Launch an action into the spin and get the same instance back, so you can
|
|
5
|
-
* `await` its `.done` (its {@link import("./action.ts").Outcome} — resolves with
|
|
6
|
-
* `{ ok, value } | { ok, error }`, **never throws**), its `.ref` (the attached
|
|
7
|
-
* handle; rejects on failure, so it's for data deps consumed inside other
|
|
8
|
-
* actions), or subscribe with `.on` / `.once`. A nod is how the body waves an
|
|
9
|
-
* action in.
|
|
10
|
-
*/
|
|
11
|
-
export type Nod = <I extends AnyActionInstance>(instance: I) => I;
|
|
12
|
-
/** Tunes a {@link spin} — currently just how it drains. */
|
|
13
|
-
export interface SpinOptions {
|
|
14
|
-
/**
|
|
15
|
-
* Stream the log to the console automatically from the start. Default `true`.
|
|
16
|
-
* Set `false` when you need to drain later via {@link SpinContext.drain} —
|
|
17
|
-
* e.g. after an interactive `prompt`, so the stream doesn't write over it.
|
|
18
|
-
*/
|
|
19
|
-
drain?: boolean;
|
|
20
|
-
}
|
|
21
|
-
/** What a {@link spin} body receives. */
|
|
22
|
-
export interface SpinContext {
|
|
23
|
-
/** Launch an action into the spin; returns the instance for awaiting/chaining. */
|
|
24
|
-
readonly nod: Nod;
|
|
25
|
-
/**
|
|
26
|
-
* Start streaming the spin's log to the console now. Only needed when the spin
|
|
27
|
-
* was created with `drain: false`; idempotent, so it's safe to call regardless.
|
|
28
|
-
*/
|
|
29
|
-
readonly drain: Spin["drain"];
|
|
30
|
-
/**
|
|
31
|
-
* Tear the spin down (abort the signal, stopping daemons). Use it to end a spin
|
|
32
|
-
* whose work is finished but which is held open by a daemon — e.g. once a
|
|
33
|
-
* one-shot build completes while a watcher is still running.
|
|
34
|
-
*/
|
|
35
|
-
readonly cancel: Spin["cancel"];
|
|
36
|
-
/** The spin's abort signal — honor it to stop early. */
|
|
37
|
-
readonly signal: AbortSignal;
|
|
38
|
-
/**
|
|
39
|
-
* Absolute path to this spin's output folder (`logs/<name>/<date>/<time>/`),
|
|
40
|
-
* created up front. Hand it to actions (or use `ctx.artifactsFolder` inside
|
|
41
|
-
* one) to write artifacts beside `run.log`.
|
|
42
|
-
*/
|
|
43
|
-
readonly artifactsFolder: string;
|
|
44
|
-
}
|
|
45
|
-
/** The body of a {@link spin}: any (async) function — it isn't named or wrapped by you. */
|
|
46
|
-
export type SpinBody = (ctx: SpinContext) => void | Promise<void>;
|
|
47
|
-
/**
|
|
48
|
-
* Runs a linear orchestration. You pass a plain (async) function; omkit hosts it
|
|
49
|
-
* in a single in-flight action and hands it a {@link Nod}. Because that host
|
|
50
|
-
* stays in-flight while your body `await`s, the spin never idles shut between
|
|
51
|
-
* steps — so you write `await` / `if` control flow instead of nested callbacks.
|
|
52
|
-
* When the body returns, the daemons it nodded in keep the spin alive until
|
|
53
|
-
* teardown.
|
|
54
|
-
*
|
|
55
|
-
* A failing action **never throws into the body** — `await nod(x).done` yields an
|
|
56
|
-
* {@link import("./action.ts").Outcome} you inspect, and the spin keeps going
|
|
57
|
-
* (the failure is recorded in the verdict and sets exit code 1):
|
|
58
|
-
*
|
|
59
|
-
* spin({ drain: false }, async ({ nod }) => {
|
|
60
|
-
* const ans = await nod(prompt({ … })).done; // Outcome — never throws
|
|
61
|
-
* if (ans.ok && ans.value === "yes") await nod(test).done;
|
|
62
|
-
* nod(devServer); // fire-and-forget daemon
|
|
63
|
-
* const chrome = nod(chromedriver({ … }));
|
|
64
|
-
* nod(chromePage(chrome.ref)); // data deps via .ref
|
|
65
|
-
* });
|
|
66
|
-
*
|
|
67
|
-
* Auto-drains by default; pass `drain: false` and call `ctx.drain()` yourself to
|
|
68
|
-
* stream the log only from a chosen point (e.g. after a `prompt`). Returns the
|
|
69
|
-
* {@link Spin} — `await` its `.done` for the verdict.
|
|
70
|
-
*/
|
|
71
|
-
export declare function spin(body: SpinBody): Spin;
|
|
72
|
-
export declare function spin(options: SpinOptions, body: SpinBody): Spin;
|
package/dist/core/spin.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { action } from "./action.js";
|
|
2
|
-
import { run } from "./run.js";
|
|
3
|
-
import { artifactsFolder } from "./output.js";
|
|
4
|
-
export function spin(a, b) {
|
|
5
|
-
const [options, body] = typeof a === "function" ? [{}, a] : [a, b];
|
|
6
|
-
const { drain: autoDrain = true } = options;
|
|
7
|
-
// `run(...)` below assigns `host` synchronously, before the action's async body
|
|
8
|
-
// ever runs — so the closures here capture the spin safely.
|
|
9
|
-
let host;
|
|
10
|
-
// One-shot drain: auto-drain and a manual ctx.drain() can't double-subscribe.
|
|
11
|
-
let drained = false;
|
|
12
|
-
const startDrain = (write) => {
|
|
13
|
-
if (!drained) {
|
|
14
|
-
drained = true;
|
|
15
|
-
host.drain(write);
|
|
16
|
-
}
|
|
17
|
-
return host;
|
|
18
|
-
};
|
|
19
|
-
const hosted = action("Spin").run(async ({ signal }) => {
|
|
20
|
-
const nod = (instance) => {
|
|
21
|
-
host.run(instance);
|
|
22
|
-
return instance;
|
|
23
|
-
};
|
|
24
|
-
await body({
|
|
25
|
-
nod,
|
|
26
|
-
drain: startDrain,
|
|
27
|
-
cancel: () => host.cancel(),
|
|
28
|
-
signal,
|
|
29
|
-
artifactsFolder: artifactsFolder(),
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
const hostInstance = hosted();
|
|
33
|
-
host = run(hostInstance);
|
|
34
|
-
// The body is the orchestrator, not a peer action: if it throws, tear the whole
|
|
35
|
-
// spin down. Regular action failures only get recorded (they don't stop the
|
|
36
|
-
// spin), but the orchestrator crashing must end the run — otherwise any daemon
|
|
37
|
-
// it already nodded keeps the process alive forever on a pipeline bug.
|
|
38
|
-
void hostInstance.done.then((outcome) => {
|
|
39
|
-
if (!outcome.ok)
|
|
40
|
-
host.cancel();
|
|
41
|
-
});
|
|
42
|
-
if (autoDrain)
|
|
43
|
-
startDrain();
|
|
44
|
-
return host;
|
|
45
|
-
}
|
|
File without changes
|
|
File without changes
|