omkit 0.0.8 → 0.0.10
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 +254 -0
- package/dist/{core/action.d.ts → orchestration/action/types.d.ts} +37 -26
- 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 +21 -5
- package/dist/{core/log-collector → output/log}/LogsCollector.js +41 -10
- 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 -188
- 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 -57
- package/dist/core/log-collector/render.js +0 -111
- package/dist/core/markers.d.ts +0 -9
- package/dist/core/markers.js +0 -13
- package/dist/core/output.d.ts +0 -64
- package/dist/core/output.js +0 -190
- package/dist/core/run.d.ts +0 -88
- package/dist/core/run.js +0 -240
- package/dist/core/spin.d.ts +0 -72
- package/dist/core/spin.js +0 -36
- /package/dist/{core/helpers → utils}/AsyncQueue.d.ts +0 -0
- /package/dist/{core/helpers → utils}/AsyncQueue.js +0 -0
|
@@ -41,4 +41,4 @@ export interface ChromePageOptions {
|
|
|
41
41
|
* teardown it closes the CDP connection *only* when it opened one — a handed-in
|
|
42
42
|
* browser or window is left untouched for its owner to close.
|
|
43
43
|
*/
|
|
44
|
-
export declare const chromePage: import("../
|
|
44
|
+
export declare const chromePage: import("../index.ts").Action<[source: ChromePageSource | Promise<ChromePageSource>, opts?: ChromePageOptions | undefined], void, ChromePageEvents, Page>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { chromium } from "playwright-core";
|
|
2
|
-
import { action } from "../
|
|
2
|
+
import { action } from "../orchestration/action/action.js";
|
|
3
3
|
// Playwright's classes are structurally distinct by these methods; only Browser
|
|
4
4
|
// has `contexts()`, only Page has `mainFrame()`, and a BrowserContext has
|
|
5
5
|
// `newPage()` without either — enough to tell a handed-in handle apart.
|
package/dist/actions/command.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { action } from "../
|
|
4
|
-
import { killTree } from "../
|
|
5
|
-
import { captureSnapshot } from "../core/output.js";
|
|
3
|
+
import { action } from "../orchestration/action/action.js";
|
|
4
|
+
import { killTree } from "../system/process/process.js";
|
|
6
5
|
/**
|
|
7
6
|
* Build the literal part of a zx tagged template from a command string, so it
|
|
8
7
|
* runs verbatim. zx only quotes interpolated args, not the template's `pieces` —
|
|
@@ -15,7 +14,7 @@ export function command(name, cmdOrFile, argsOrOptions, maybeOptions) {
|
|
|
15
14
|
const direct = Array.isArray(argsOrOptions);
|
|
16
15
|
const options = (direct ? maybeOptions : argsOrOptions) ?? {};
|
|
17
16
|
if (direct) {
|
|
18
|
-
return action(name).run((ctx) => spawnDirect(ctx.logs, ctx.signal, name, cmdOrFile, argsOrOptions, options))();
|
|
17
|
+
return action(name).run((ctx) => spawnDirect(ctx.output.snapshots, ctx.logs, ctx.signal, name, cmdOrFile, argsOrOptions, options))();
|
|
19
18
|
}
|
|
20
19
|
return action(name).run(({ proc }) => {
|
|
21
20
|
// path.resolve keeps an absolute cwd as-is and resolves a relative one
|
|
@@ -35,7 +34,7 @@ export function command(name, cmdOrFile, argsOrOptions, maybeOptions) {
|
|
|
35
34
|
* log, and resolves when it exits (0 = ok; a non-zero exit is a genuine failure,
|
|
36
35
|
* unless the run is tearing down, where the abort-kill is the reason).
|
|
37
36
|
*/
|
|
38
|
-
const spawnDirect = (logs, signal, name, file, args, options) => {
|
|
37
|
+
const spawnDirect = (snapshots, logs, signal, name, file, args, options) => {
|
|
39
38
|
const cwd = path.resolve(options.cwd ?? ".");
|
|
40
39
|
const source = options.label ?? name;
|
|
41
40
|
// windowsHide keeps a stray console window from flashing for the child.
|
|
@@ -43,7 +42,7 @@ const spawnDirect = (logs, signal, name, file, args, options) => {
|
|
|
43
42
|
const child = spawn(file, args, { cwd, windowsHide: true, env: options.env });
|
|
44
43
|
// Mirror proc's launch line: a filterable milestone on the timeline, with the
|
|
45
44
|
// structured args snapshotted and linked (see process.ts for the rationale).
|
|
46
|
-
const snap =
|
|
45
|
+
const snap = snapshots.captureJson(`proc-${name}-start`, { cmd: file, args, cwd });
|
|
47
46
|
logs.append({
|
|
48
47
|
source: name,
|
|
49
48
|
level: "event",
|
|
@@ -52,4 +52,4 @@ export interface HealthcheckEvents {
|
|
|
52
52
|
* stall the loop) and the whole wait by the run's abort signal and an optional
|
|
53
53
|
* `timeoutMs`, so teardown unblocks a pending probe instead of hanging.
|
|
54
54
|
*/
|
|
55
|
-
export declare const healthcheck: import("../
|
|
55
|
+
export declare const healthcheck: import("../index.ts").Action<[opts?: HealthcheckOptions | undefined], HealthcheckResult, HealthcheckEvents, void>;
|
package/dist/actions/prompt.d.ts
CHANGED
|
@@ -50,5 +50,5 @@ export interface PromptEvents {
|
|
|
50
50
|
* `answer` when settled (carrying whether it came from input, the default, or a
|
|
51
51
|
* timeout).
|
|
52
52
|
*/
|
|
53
|
-
export declare const prompt: import("../
|
|
53
|
+
export declare const prompt: import("../index.ts").Action<[opts?: PromptOptions | undefined], string, PromptEvents, string>;
|
|
54
54
|
export {};
|
package/dist/actions/prompt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import { action } from "../
|
|
2
|
+
import { action } from "../orchestration/action/action.js";
|
|
3
3
|
/** Normalize choices to `{ label, value }`. */
|
|
4
4
|
const asChoices = (choices) => choices.map((c) => (typeof c === "string" ? { label: c, value: c } : c));
|
|
5
5
|
/**
|
|
@@ -27,4 +27,4 @@ export interface TailLogOptions {
|
|
|
27
27
|
* filters by name, like {@link import("./watch.ts").watch}. Runs until the run's
|
|
28
28
|
* signal aborts, then closes the watcher (flushing any trailing partial line).
|
|
29
29
|
*/
|
|
30
|
-
export declare const tailLog: import("../
|
|
30
|
+
export declare const tailLog: import("../index.ts").Action<[target: string, opts?: TailLogOptions | undefined], void, import("../index.ts").NoEvents, void>;
|
package/dist/actions/tail-log.js
CHANGED
|
@@ -2,7 +2,7 @@ import { watch as fsWatch } from "node:fs";
|
|
|
2
2
|
import { open, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, resolve } from "node:path";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
|
-
import { action } from "../
|
|
5
|
+
import { action } from "../orchestration/action/action.js";
|
|
6
6
|
/**
|
|
7
7
|
* Tails a (possibly not-yet-existing) text/log file and streams each appended
|
|
8
8
|
* line into the run's combined log — the way `proc` streams a child's stdout, but
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LogEntry } from "../
|
|
1
|
+
import type { LogEntry } from "../output/log/LogsCollector.ts";
|
|
2
2
|
/** Predicate over a log entry — return `true` for the entry to wait for. */
|
|
3
3
|
export type LogMatcher = (entry: LogEntry) => boolean;
|
|
4
4
|
export interface UntilLogOptions {
|
|
@@ -21,4 +21,4 @@ export interface UntilLogEvents {
|
|
|
21
21
|
* run's abort signal (and an optional timeout), so teardown unblocks a pending
|
|
22
22
|
* wait instead of hanging.
|
|
23
23
|
*/
|
|
24
|
-
export declare const untilLog: import("../
|
|
24
|
+
export declare const untilLog: import("../index.ts").Action<[matcher: LogMatcher, (UntilLogOptions | undefined)?], LogEntry, UntilLogEvents, void>;
|
|
@@ -32,4 +32,4 @@ export interface WatchDirEvents {
|
|
|
32
32
|
* event the *parent* fires for the target's name — when it does and the target
|
|
33
33
|
* still exists, it was replaced (`removed` + `ready`).
|
|
34
34
|
*/
|
|
35
|
-
export declare const watchDir: import("../
|
|
35
|
+
export declare const watchDir: import("../index.ts").Action<[target: string, opts?: WatchDirOptions | undefined], void, WatchDirEvents, void>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, watch as fsWatch } from "node:fs";
|
|
2
2
|
import { readdir, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { action } from "../
|
|
4
|
+
import { action } from "../orchestration/action/action.js";
|
|
5
5
|
/**
|
|
6
6
|
* Watches a directory and emits per-file events on changes, surviving the folder
|
|
7
7
|
* being deleted and recreated (e.g. a build that `rm -rf`s it first).
|
package/dist/actions/watch.d.ts
CHANGED
|
@@ -30,4 +30,4 @@ export interface WatchOptions {
|
|
|
30
30
|
* Assumes the parent directory exists; watching when an ancestor is missing is
|
|
31
31
|
* a later case.
|
|
32
32
|
*/
|
|
33
|
-
export declare const watch: import("../
|
|
33
|
+
export declare const watch: import("../index.ts").Action<[target: string, opts?: WatchOptions | undefined], void, import("../index.ts").NoEvents, void>;
|
package/dist/actions/watch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 "../orchestration/action/action.js";
|
|
5
5
|
/**
|
|
6
6
|
* Watches a single file that may not exist yet, firing callbacks on its
|
|
7
7
|
* transitions. Since `fs.watch` can't watch a missing path, it watches the
|
package/dist/index.d.ts
CHANGED
|
@@ -8,20 +8,16 @@
|
|
|
8
8
|
* This is a curated public surface — internal helpers (the async queue, the log
|
|
9
9
|
* renderer, marker prefixing) are intentionally not exported.
|
|
10
10
|
*/
|
|
11
|
-
export { action } from "./
|
|
12
|
-
export type { Action, ActionBuilder, ActionInstance, AnyActionInstance, ActionContext, SystemGlobal, SystemEvents, InstanceEvents, NoEvents, Awaitable, Outcome, } from "./
|
|
13
|
-
export {
|
|
14
|
-
export type {
|
|
15
|
-
export {
|
|
16
|
-
export type {
|
|
17
|
-
export {
|
|
18
|
-
export type {
|
|
19
|
-
export {
|
|
20
|
-
export type {
|
|
21
|
-
export {
|
|
22
|
-
export type {
|
|
23
|
-
export {
|
|
24
|
-
export { snapshot, captureSnapshot } from "./core/output.ts";
|
|
25
|
-
export type { SnapshotRef } from "./core/output.ts";
|
|
26
|
-
export { artifactsFolder } from "./core/output.ts";
|
|
27
|
-
export { fingerprint, cacheDir } from "./core/folder-cache.ts";
|
|
11
|
+
export { action } from "./orchestration/action/action.ts";
|
|
12
|
+
export type { Action, ActionBuilder, ActionInstance, AnyActionInstance, ActionContext, SystemGlobal, SystemEvents, InstanceEvents, NoEvents, Awaitable, Outcome, } from "./orchestration/action/types.ts";
|
|
13
|
+
export { spin } from "./orchestration/spin/spin.ts";
|
|
14
|
+
export type { Nod, SpinContext, SpinBody } from "./orchestration/spin/types.ts";
|
|
15
|
+
export { events } from "./orchestration/events/events.ts";
|
|
16
|
+
export type { Emitter, EventHandler, EventMeta } from "./orchestration/events/events.ts";
|
|
17
|
+
export { createProc } from "./system/process/process.ts";
|
|
18
|
+
export type { Proc } from "./system/process/process.ts";
|
|
19
|
+
export { LogsCollector } from "./output/log/LogsCollector.ts";
|
|
20
|
+
export type { Logger, LogEntry, LogInput, SubscribeOptions, } from "./output/log/LogsCollector.ts";
|
|
21
|
+
export { Output } from "./output/index.ts";
|
|
22
|
+
export type { SnapshotRef } from "./output/index.ts";
|
|
23
|
+
export { FolderCache } from "./system/fs/FolderCache.ts";
|
package/dist/index.js
CHANGED
|
@@ -9,26 +9,24 @@
|
|
|
9
9
|
* renderer, marker prefixing) are intentionally not exported.
|
|
10
10
|
*/
|
|
11
11
|
// ── Actions ────────────────────────────────────────────────────────────────
|
|
12
|
-
export { action } from "./
|
|
13
|
-
// ── Spin (the run handle) ────────────────────────────────────────────────────
|
|
14
|
-
export { run } from "./core/run.js";
|
|
12
|
+
export { action } from "./orchestration/action/action.js";
|
|
15
13
|
// ── Spin (linear orchestration) ──────────────────────────────────────────────
|
|
16
|
-
export { spin } from "./
|
|
14
|
+
export { spin } from "./orchestration/spin/spin.js";
|
|
17
15
|
// ── Events ─────────────────────────────────────────────────────────────────
|
|
18
|
-
export { events } from "./
|
|
16
|
+
export { events } from "./orchestration/events/events.js";
|
|
19
17
|
// ── Child processes ────────────────────────────────────────────────────────
|
|
20
|
-
export { createProc } from "./
|
|
18
|
+
export { createProc } from "./system/process/process.js";
|
|
21
19
|
// ── The log ────────────────────────────────────────────────────────────────
|
|
22
|
-
export { LogsCollector } from "./
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
export { artifactsFolder } from "./core/output.js";
|
|
20
|
+
export { LogsCollector } from "./output/log/LogsCollector.js";
|
|
21
|
+
// ── Snapshots & artifacts ────────────────────────────────────────────────────
|
|
22
|
+
// The output subsystem is owned per-run by the spin and reached through context —
|
|
23
|
+
// `ctx.output` (folder / snapshots / runLog) inside an action, `ctx.snapshot` and
|
|
24
|
+
// `ctx.artifactsFolder` inside a spin body. The classes are exported for typing and
|
|
25
|
+
// advanced use; there is no process-global singleton or free-function form.
|
|
26
|
+
export { Output } from "./output/index.js";
|
|
30
27
|
// ── Folder cache ─────────────────────────────────────────────────────────────
|
|
31
28
|
// Backs the `.withCache(...inputs)` method on instances (fingerprints an
|
|
32
|
-
// action's input files/folders, not its outputs);
|
|
33
|
-
//
|
|
34
|
-
|
|
29
|
+
// action's input files/folders, not its outputs); exposed for scripts that want
|
|
30
|
+
// to inspect or manually invalidate the cache — `FolderCache.fingerprint(...)`,
|
|
31
|
+
// `FolderCache.dir`.
|
|
32
|
+
export { FolderCache } from "./system/fs/FolderCache.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ActionBuilder, AnyActionInstance, NoEvents } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* If `value` is some action instance's `.ref` promise, the instance that owns it
|
|
4
|
+
* (the producer of that handle); otherwise `undefined`. Used by the run to verify
|
|
5
|
+
* a `.ref` dependency was launched before the action that consumes it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const producerOfRef: (value: unknown) => AnyActionInstance | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Defines an action. Always returns a builder; provide the implementation via
|
|
10
|
+
* `.run(…)`, optionally after declaring events and/or a handle:
|
|
11
|
+
*
|
|
12
|
+
* action("Build").run(async (ctx, opts) => { … }) // no declared events / handle
|
|
13
|
+
* action("Watch") // typed events + handle
|
|
14
|
+
* .emits<{ create: string }>()
|
|
15
|
+
* .ref<{ rescan(): void }>()
|
|
16
|
+
* .run((ctx, opts) => { ctx.emit(…); ctx.attach(…); … })
|
|
17
|
+
*
|
|
18
|
+
* `.run(…)` yields a callable that, when invoked, constructs an
|
|
19
|
+
* {@link ActionInstance} (deferred — the run executes it). Instances expose
|
|
20
|
+
* chainable `.on` / `.once` (declared events plus `done`/`error`); the attached
|
|
21
|
+
* handle is awaited via `instance.ref`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function action(name: string): ActionBuilder<NoEvents, void>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { events } from "../events/events.js";
|
|
2
|
+
import { createProc } from "../../system/process/process.js";
|
|
3
|
+
import { FolderCache } from "../../system/fs/FolderCache.js";
|
|
4
|
+
// Maps an instance's `.ref` promise back to the instance that produces it, so the
|
|
5
|
+
// run can catch a consumer launched before (or without) its `.ref` producer —
|
|
6
|
+
// which would otherwise await a handle that never arrives and hang silently.
|
|
7
|
+
const refOwners = new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* If `value` is some action instance's `.ref` promise, the instance that owns it
|
|
10
|
+
* (the producer of that handle); otherwise `undefined`. Used by the run to verify
|
|
11
|
+
* a `.ref` dependency was launched before the action that consumes it.
|
|
12
|
+
*/
|
|
13
|
+
export const producerOfRef = (value) => typeof value === "object" && value !== null ? refOwners.get(value) : undefined;
|
|
14
|
+
/**
|
|
15
|
+
* A constructed-but-not-yet-run action — everything a single launch needs, in one
|
|
16
|
+
* object: its own event bus, its set-once handle (`ref`) and Outcome (`done`), and
|
|
17
|
+
* the `start` that runs the impl and settles them. Self-contained: `start()`
|
|
18
|
+
* resolves with the {@link Outcome} and **never rejects** (a failing action is
|
|
19
|
+
* data, not a throw), so a launcher just awaits `.done`.
|
|
20
|
+
*/
|
|
21
|
+
class ActionRun {
|
|
22
|
+
name;
|
|
23
|
+
args;
|
|
24
|
+
/** The attached handle, awaited via `.ref` (settled by `attach`, or by `start` on settle). */
|
|
25
|
+
ref;
|
|
26
|
+
/** The action's Outcome — resolves (never rejects) once it settles. */
|
|
27
|
+
done;
|
|
28
|
+
exec;
|
|
29
|
+
// The bus covers declared + system events; `ctx.emit` is narrowed to the declared
|
|
30
|
+
// ones, while the framework emits attached/done/error through the same bus.
|
|
31
|
+
emitter;
|
|
32
|
+
handle;
|
|
33
|
+
settled;
|
|
34
|
+
attached = false;
|
|
35
|
+
constructor(name, exec, args) {
|
|
36
|
+
this.name = name;
|
|
37
|
+
this.exec = exec;
|
|
38
|
+
this.args = args;
|
|
39
|
+
this.emitter = events(name);
|
|
40
|
+
this.handle = ActionRun.defer();
|
|
41
|
+
this.settled = ActionRun.defer();
|
|
42
|
+
this.ref = this.handle.promise;
|
|
43
|
+
this.done = this.settled.promise;
|
|
44
|
+
// Most actions have a `void` handle and no one awaits `ref`; mark the promise
|
|
45
|
+
// handled so a settle-time rejection on such an instance doesn't surface as an
|
|
46
|
+
// unhandled rejection. Real awaiters of `ref` still receive the rejection.
|
|
47
|
+
void this.handle.promise.catch(() => { });
|
|
48
|
+
// Register this instance as the producer of its `.ref`, so the run can catch a
|
|
49
|
+
// consumer built with this `.ref` that gets launched before this instance.
|
|
50
|
+
refOwners.set(this.ref, this);
|
|
51
|
+
}
|
|
52
|
+
start(system) {
|
|
53
|
+
// `ctx.emit` is the declared-events view of the same bus.
|
|
54
|
+
const emit = this.emitter.emit;
|
|
55
|
+
// The bus is pure pub/sub; logging each emit onto the timeline is layered here,
|
|
56
|
+
// through this action's *scoped* logger — so an emit's line inherits the action's
|
|
57
|
+
// path (`parent › child › event`) just like its other output, and the bus stays
|
|
58
|
+
// free of any global. Registered once per run of this instance.
|
|
59
|
+
this.emitter.onAny((key, payload) => ActionRun.logEmit(system, this.name, String(key), payload));
|
|
60
|
+
// `Promise.resolve().then` normalizes a sync result and routes a sync throw to
|
|
61
|
+
// the `error` path, so exec may be sync or async.
|
|
62
|
+
return Promise.resolve()
|
|
63
|
+
.then(() => this.exec({
|
|
64
|
+
...system,
|
|
65
|
+
emit,
|
|
66
|
+
attach: (value) => this.attach(value),
|
|
67
|
+
proc: createProc(system.logs, system.signal, system.output.snapshots),
|
|
68
|
+
artifactsFolder: system.output.folder.artifacts(),
|
|
69
|
+
}, ...this.args))
|
|
70
|
+
.then((result) => {
|
|
71
|
+
const outcome = { ok: true, value: result };
|
|
72
|
+
this.settled.resolve(outcome);
|
|
73
|
+
this.systemEmit("done", outcome);
|
|
74
|
+
// Settle `ref` if the action finished without attaching, so awaiters
|
|
75
|
+
// resolve (with `undefined` for the default void handle) rather than
|
|
76
|
+
// hang. Idempotent — a no-op when a handle was already attached.
|
|
77
|
+
this.handle.resolve(undefined);
|
|
78
|
+
return outcome;
|
|
79
|
+
}, (error) => {
|
|
80
|
+
const exitCode = ActionRun.exitCodeOf(error);
|
|
81
|
+
const outcome = exitCode === undefined ? { ok: false, error } : { ok: false, error, exitCode };
|
|
82
|
+
// Emit `error` (for `.on("error")` handlers), then settle `done` with the
|
|
83
|
+
// Outcome — the canonical failure signal that `.done`/`once` read.
|
|
84
|
+
this.systemEmit("error", error);
|
|
85
|
+
this.settled.resolve(outcome);
|
|
86
|
+
this.systemEmit("done", outcome);
|
|
87
|
+
// The action failed, so its handle will never arrive — reject `ref` with
|
|
88
|
+
// the same error instead of leaving awaiters hung. No-op if already attached.
|
|
89
|
+
this.handle.reject(error);
|
|
90
|
+
// Resolve (never rethrow): failure is the Outcome, not a rejection.
|
|
91
|
+
return outcome;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
on(key, handler) {
|
|
95
|
+
this.emitter.listen(key, handler);
|
|
96
|
+
}
|
|
97
|
+
once(key) {
|
|
98
|
+
// `done` always fires with the Outcome and never "fails" the await, so it reads
|
|
99
|
+
// straight from the settled promise; other keys race via awaitEvent (which
|
|
100
|
+
// resolves `undefined` if the action settles before the event).
|
|
101
|
+
return (key === "done" ? this.settled.promise : ActionRun.awaitEvent(this.emitter, key));
|
|
102
|
+
}
|
|
103
|
+
withCache(...paths) {
|
|
104
|
+
// Validate + canonicalize eagerly here (at the call site), not lazily inside the
|
|
105
|
+
// run — a relative path fails fast where it's written, and the resolved paths
|
|
106
|
+
// give a stable cache key regardless of spelling.
|
|
107
|
+
const targets = FolderCache.resolvePaths(paths);
|
|
108
|
+
const { name } = this;
|
|
109
|
+
// Wrap this instance in a fresh one (reusing the engine for its own
|
|
110
|
+
// emitter/ref/on/once) that fingerprints `paths` and either skips or
|
|
111
|
+
// runs+records. The wrapper keeps this action's `name`, so log lines and the
|
|
112
|
+
// `launch <name>` line are unchanged; it forwards its injected logs/signal
|
|
113
|
+
// straight into this inner instance's `start`.
|
|
114
|
+
return action(name).run(async (ctx) => {
|
|
115
|
+
const fp = await FolderCache.fingerprint(targets);
|
|
116
|
+
if ((await FolderCache.read(targets)) === fp) {
|
|
117
|
+
ctx.logs.append({ source: name, level: "info", message: "cached, skipping" });
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const outcome = await this.start({
|
|
121
|
+
logs: ctx.logs,
|
|
122
|
+
signal: ctx.signal,
|
|
123
|
+
nod: ctx.nod,
|
|
124
|
+
output: ctx.output,
|
|
125
|
+
});
|
|
126
|
+
// Re-throw the inner failure so the wrapper fails too (the framework re-wraps
|
|
127
|
+
// it into this wrapper's own Outcome); only record on success.
|
|
128
|
+
if (!outcome.ok)
|
|
129
|
+
throw outcome.error;
|
|
130
|
+
await FolderCache.write(targets, fp);
|
|
131
|
+
return outcome.value;
|
|
132
|
+
})();
|
|
133
|
+
}
|
|
134
|
+
/** One-time lifecycle moment: resolve `ref` and emit `attached`. Guarded (second attach no-ops). */
|
|
135
|
+
attach(value) {
|
|
136
|
+
if (this.attached)
|
|
137
|
+
return;
|
|
138
|
+
this.attached = true;
|
|
139
|
+
this.handle.resolve(value);
|
|
140
|
+
this.systemEmit("attached");
|
|
141
|
+
}
|
|
142
|
+
/** The framework's own emits (loosely typed — sidesteps EmitArgs for void results). */
|
|
143
|
+
systemEmit(key, payload) {
|
|
144
|
+
this.emitter.emit(key, payload);
|
|
145
|
+
}
|
|
146
|
+
static defer() {
|
|
147
|
+
let resolve;
|
|
148
|
+
let reject;
|
|
149
|
+
const promise = new Promise((res, rej) => {
|
|
150
|
+
resolve = res;
|
|
151
|
+
reject = rej;
|
|
152
|
+
});
|
|
153
|
+
return { promise, resolve, reject };
|
|
154
|
+
}
|
|
155
|
+
/** Duck-type an exit code off a thrown error (zx's ProcessOutput, command's exit error). */
|
|
156
|
+
static exitCodeOf(error) {
|
|
157
|
+
const code = error?.exitCode;
|
|
158
|
+
return typeof code === "number" ? code : undefined;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Log one emit onto the run timeline, through the action's scoped `logs` so the
|
|
162
|
+
* line carries its path. Fields are `·`-delimited — action name · key · payload.
|
|
163
|
+
* A string payload rides inline; a richer one is written to a snapshot file and
|
|
164
|
+
* linked (`→ <rel>`) so the durable record keeps it without bloating the line.
|
|
165
|
+
* This is the sole place emits become log lines — the bus itself does no logging.
|
|
166
|
+
*/
|
|
167
|
+
static logEmit(system, name, key, payload) {
|
|
168
|
+
const fields = [name, key];
|
|
169
|
+
if (typeof payload === "string") {
|
|
170
|
+
fields.push(payload);
|
|
171
|
+
}
|
|
172
|
+
else if (payload !== undefined) {
|
|
173
|
+
fields.push(`→ ${system.output.snapshots.captureJson(`event-${name}-${key}`, payload).rel}`);
|
|
174
|
+
}
|
|
175
|
+
system.logs.append({ source: "event", level: "event", message: fields.join(" · ") });
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* A promise for the next emit of `key`. It **never rejects**: it resolves with
|
|
179
|
+
* the event's payload, or with `undefined` if the action settles (`done` — which
|
|
180
|
+
* always fires) before `key` ever does. A retained snapshot resolves it
|
|
181
|
+
* immediately. (Not used for `key === "done"`, which reads the settled Outcome.)
|
|
182
|
+
*/
|
|
183
|
+
static awaitEvent(emitter, key) {
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const offs = [];
|
|
186
|
+
let settled = false;
|
|
187
|
+
// First of [the event | done] to fire wins; the rest are unsubscribed.
|
|
188
|
+
const settle = (act) => {
|
|
189
|
+
if (settled)
|
|
190
|
+
return;
|
|
191
|
+
settled = true;
|
|
192
|
+
for (const off of offs)
|
|
193
|
+
off();
|
|
194
|
+
act();
|
|
195
|
+
};
|
|
196
|
+
// listenOnce is keyed by E; the system `done` key is always present on an
|
|
197
|
+
// instance's event map, so reach it through a loosened view.
|
|
198
|
+
const listen = emitter.listenOnce;
|
|
199
|
+
offs.push(listen(key, (payload) => settle(() => resolve(payload))));
|
|
200
|
+
// `done` fires once on any settle (success or failure); if it beats `key`,
|
|
201
|
+
// the event will never come — resolve `undefined` rather than reject.
|
|
202
|
+
if (key !== "done") {
|
|
203
|
+
offs.push(listen("done", () => settle(() => resolve(undefined))));
|
|
204
|
+
}
|
|
205
|
+
// A retained snapshot fires a listen synchronously during registration; if
|
|
206
|
+
// that already settled us, drop any listeners registered afterwards.
|
|
207
|
+
if (settled)
|
|
208
|
+
for (const off of offs)
|
|
209
|
+
off();
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The step between `action(name)` and an {@link Action}: declare typed events
|
|
215
|
+
* and/or an imperative handle (phantom type transitions — each returns a builder
|
|
216
|
+
* carrying the same `name`), then `run(exec)` to bind the implementation.
|
|
217
|
+
*/
|
|
218
|
+
class Builder {
|
|
219
|
+
name;
|
|
220
|
+
constructor(name) {
|
|
221
|
+
this.name = name;
|
|
222
|
+
}
|
|
223
|
+
emits() {
|
|
224
|
+
return new Builder(this.name);
|
|
225
|
+
}
|
|
226
|
+
ref() {
|
|
227
|
+
return new Builder(this.name);
|
|
228
|
+
}
|
|
229
|
+
run(exec) {
|
|
230
|
+
const { name } = this;
|
|
231
|
+
// Invoking the returned callable constructs a fresh instance (deferred — the run
|
|
232
|
+
// executes it). Each call is its own {@link ActionRun}.
|
|
233
|
+
const create = (...args) => new ActionRun(name, exec, args);
|
|
234
|
+
return Object.assign(create, { actionName: name });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Defines an action. Always returns a builder; provide the implementation via
|
|
239
|
+
* `.run(…)`, optionally after declaring events and/or a handle:
|
|
240
|
+
*
|
|
241
|
+
* action("Build").run(async (ctx, opts) => { … }) // no declared events / handle
|
|
242
|
+
* action("Watch") // typed events + handle
|
|
243
|
+
* .emits<{ create: string }>()
|
|
244
|
+
* .ref<{ rescan(): void }>()
|
|
245
|
+
* .run((ctx, opts) => { ctx.emit(…); ctx.attach(…); … })
|
|
246
|
+
*
|
|
247
|
+
* `.run(…)` yields a callable that, when invoked, constructs an
|
|
248
|
+
* {@link ActionInstance} (deferred — the run executes it). Instances expose
|
|
249
|
+
* chainable `.on` / `.once` (declared events plus `done`/`error`); the attached
|
|
250
|
+
* handle is awaited via `instance.ref`.
|
|
251
|
+
*/
|
|
252
|
+
export function action(name) {
|
|
253
|
+
return new Builder(name);
|
|
254
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { Logger } from "
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import type { Logger } from "../../output/log/LogsCollector.ts";
|
|
2
|
+
import type { Output } from "../../output/Output.ts";
|
|
3
|
+
import type { Emitter, EventHandler } from "../events/events.ts";
|
|
4
|
+
import type { Proc } from "../../system/process/process.ts";
|
|
4
5
|
/** An action that declares no events. */
|
|
5
6
|
export type NoEvents = Record<never, never>;
|
|
6
7
|
/** A value an action's `exec` may return: a result, or a promise of one. */
|
|
@@ -9,7 +10,7 @@ export type Awaitable<T> = T | Promise<T>;
|
|
|
9
10
|
* How an action settles. An action **never throws into the spin body** — its
|
|
10
11
|
* failure is data, not control flow: it completes with `{ ok: false, error }`
|
|
11
12
|
* instead of rejecting. On success it carries the exec's `value`. `exitCode` is
|
|
12
|
-
* present when the failure came from a process (see {@link import("
|
|
13
|
+
* present when the failure came from a process (see {@link import("../../../actions/command.ts").command}).
|
|
13
14
|
* Await it via `instance.done` or `once("done")`; both resolve, never reject.
|
|
14
15
|
*/
|
|
15
16
|
export type Outcome<T> = {
|
|
@@ -35,16 +36,41 @@ export interface SystemEvents<Result> {
|
|
|
35
36
|
}
|
|
36
37
|
/** The full event map a constructed instance exposes: the declared events plus the system ones. */
|
|
37
38
|
export type InstanceEvents<Events extends object, Result> = Events & SystemEvents<Result>;
|
|
39
|
+
/**
|
|
40
|
+
* Launch an action into the spin and get the same instance back, so you can
|
|
41
|
+
* `await` its `.done` (its {@link Outcome} — resolves with `{ ok, value } |
|
|
42
|
+
* { ok, error }`, **never throws**), its `.ref` (the attached handle; rejects on
|
|
43
|
+
* failure, so it's for data deps consumed inside other actions), or subscribe with
|
|
44
|
+
* `.on` / `.once`. The spin body waves an action in with a nod; an action nods
|
|
45
|
+
* **child** actions the same way, and their log output is prefixed with the
|
|
46
|
+
* parent's path (see `SpinHost`).
|
|
47
|
+
*/
|
|
48
|
+
export type Nod = <I extends AnyActionInstance>(instance: I) => I;
|
|
38
49
|
/** System services the pipeline injects into every action when it runs. */
|
|
39
50
|
export interface SystemGlobal {
|
|
40
51
|
/** A logger bound to this action — feeds it directly or via `proc`. */
|
|
41
52
|
readonly logs: Logger;
|
|
42
53
|
/** The run's abort signal — aborted on teardown/cancel; honor it to stop early. */
|
|
43
54
|
readonly signal: AbortSignal;
|
|
55
|
+
/**
|
|
56
|
+
* Launch a **child** action into the same spin (returns it for awaiting/chaining).
|
|
57
|
+
* The child's log output is prefixed with this action's path (`parent › child`),
|
|
58
|
+
* and the child lives in the spin — it keeps the run alive until it settles,
|
|
59
|
+
* independent of when this action finishes. `await ctx.nod(x).done` to wait on it.
|
|
60
|
+
*/
|
|
61
|
+
readonly nod: Nod;
|
|
62
|
+
/**
|
|
63
|
+
* This run's output subsystem — the folder, snapshot store, and run-log writer,
|
|
64
|
+
* owned by the {@link SpinHost} and threaded to every action (rather than reached
|
|
65
|
+
* as a module singleton). The framework uses it to place `ctx.artifactsFolder`
|
|
66
|
+
* and to snapshot event/proc payloads; actions can reach `output.snapshots` to
|
|
67
|
+
* capture their own artifacts.
|
|
68
|
+
*/
|
|
69
|
+
readonly output: Output;
|
|
44
70
|
}
|
|
45
71
|
/** What an action's implementation receives: the system bag, plus `emit`, `attach`, and `proc`. */
|
|
46
72
|
export interface ActionContext<Events extends object, Handle = void> extends SystemGlobal {
|
|
47
|
-
/** Emit one of this action's declared events (also
|
|
73
|
+
/** Emit one of this action's declared events (also logged onto the timeline under this action's path). */
|
|
48
74
|
readonly emit: Emitter<Events>["emit"];
|
|
49
75
|
/** Publish this action's imperative handle, resolving its instance's `.ref`. */
|
|
50
76
|
readonly attach: (handle: Handle) => void;
|
|
@@ -91,12 +117,13 @@ export interface ActionInstance<Result = unknown, Events extends object = NoEven
|
|
|
91
117
|
/** Subscribe to a declared or system event (`done`/`error`) with a handler. */
|
|
92
118
|
on<K extends keyof InstanceEvents<Events, Result>>(key: K, handler: EventHandler<InstanceEvents<Events, Result>[K]>): void;
|
|
93
119
|
/**
|
|
94
|
-
* A promise for the next occurrence of an event
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
120
|
+
* A promise for the next occurrence of an event. It **never rejects**: it
|
|
121
|
+
* resolves with the event's payload, or with `undefined` if the action settles
|
|
122
|
+
* (`done`) before the event ever fires — so awaiting it in a spin body can't
|
|
123
|
+
* throw and can't hang. `once("done")` yields the {@link Outcome}. Use {@link on}
|
|
124
|
+
* for ongoing/handler subscriptions; `once` is the awaitable, one-value form.
|
|
98
125
|
*/
|
|
99
|
-
once<K extends keyof InstanceEvents<Events, Result>>(key: K): Promise<InstanceEvents<Events, Result>[K]>;
|
|
126
|
+
once<K extends keyof InstanceEvents<Events, Result>>(key: K): Promise<InstanceEvents<Events, Result>[K] | undefined>;
|
|
100
127
|
/**
|
|
101
128
|
* Gate this action on its **inputs** — the files/folders it *reads* (its
|
|
102
129
|
* sources), **not** what it produces. `paths` (each a file or a folder) are
|
|
@@ -139,19 +166,3 @@ export interface ActionBuilder<Events extends object, Handle = void> {
|
|
|
139
166
|
/** Provide the implementation. `ctx` carries `logs`, `signal`, `emit`, and `attach`. */
|
|
140
167
|
run<Args extends unknown[], Result>(exec: (ctx: ActionContext<Events, Handle>, ...args: Args) => Awaitable<Result>): Action<Args, Result, Events, Handle>;
|
|
141
168
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Defines an action. Always returns a builder; provide the implementation via
|
|
144
|
-
* `.run(…)`, optionally after declaring events and/or a handle:
|
|
145
|
-
*
|
|
146
|
-
* action("Build").run(async (ctx, opts) => { … }) // no declared events / handle
|
|
147
|
-
* action("Watch") // typed events + handle
|
|
148
|
-
* .emits<{ create: string }>()
|
|
149
|
-
* .ref<{ rescan(): void }>()
|
|
150
|
-
* .run((ctx, opts) => { ctx.emit(…); ctx.attach(…); … })
|
|
151
|
-
*
|
|
152
|
-
* `.run(…)` yields a callable that, when invoked, constructs an
|
|
153
|
-
* {@link ActionInstance} (deferred — the run executes it). Instances expose
|
|
154
|
-
* chainable `.on` / `.once` (declared events plus `done`/`error`); the attached
|
|
155
|
-
* handle is awaited via `instance.ref`.
|
|
156
|
-
*/
|
|
157
|
-
export declare function action(name: string): ActionBuilder<NoEvents, void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|