@descryy/runtime-controller 0.3.8 → 0.3.9
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/attach-fencing.d.ts +109 -0
- package/dist/attach-fencing.d.ts.map +1 -0
- package/dist/attach-fencing.js +215 -0
- package/dist/attach-fencing.js.map +1 -0
- package/dist/capability-registry.d.ts +9 -0
- package/dist/capability-registry.d.ts.map +1 -0
- package/dist/capability-registry.js +32 -0
- package/dist/capability-registry.js.map +1 -0
- package/dist/collector-version.d.ts +3 -0
- package/dist/collector-version.d.ts.map +1 -0
- package/dist/collector-version.js +5 -0
- package/dist/collector-version.js.map +1 -0
- package/dist/container-sandbox.d.ts +104 -0
- package/dist/container-sandbox.d.ts.map +1 -0
- package/dist/container-sandbox.js +152 -0
- package/dist/container-sandbox.js.map +1 -0
- package/dist/controller.d.ts +138 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +449 -0
- package/dist/controller.js.map +1 -0
- package/dist/dependency-version-check.d.ts +36 -0
- package/dist/dependency-version-check.d.ts.map +1 -0
- package/dist/dependency-version-check.js +72 -0
- package/dist/dependency-version-check.js.map +1 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +12 -0
- package/dist/env.js.map +1 -0
- package/dist/environment-metadata.d.ts +15 -0
- package/dist/environment-metadata.d.ts.map +1 -0
- package/dist/environment-metadata.js +63 -0
- package/dist/environment-metadata.js.map +1 -0
- package/dist/environment-version-check.d.ts +40 -0
- package/dist/environment-version-check.d.ts.map +1 -0
- package/dist/environment-version-check.js +176 -0
- package/dist/environment-version-check.js.map +1 -0
- package/dist/execution-safety.d.ts +96 -0
- package/dist/execution-safety.d.ts.map +1 -0
- package/dist/execution-safety.js +140 -0
- package/dist/execution-safety.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestration.d.ts +55 -0
- package/dist/orchestration.d.ts.map +1 -0
- package/dist/orchestration.js +219 -0
- package/dist/orchestration.js.map +1 -0
- package/dist/preflight.d.ts +122 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +177 -0
- package/dist/preflight.js.map +1 -0
- package/dist/process-collector.d.ts +20 -0
- package/dist/process-collector.d.ts.map +1 -0
- package/dist/process-collector.js +116 -0
- package/dist/process-collector.js.map +1 -0
- package/dist/process-identity.d.ts +46 -0
- package/dist/process-identity.d.ts.map +1 -0
- package/dist/process-identity.js +186 -0
- package/dist/process-identity.js.map +1 -0
- package/dist/process-manager.d.ts +137 -0
- package/dist/process-manager.d.ts.map +1 -0
- package/dist/process-manager.js +365 -0
- package/dist/process-manager.js.map +1 -0
- package/dist/readiness.d.ts +122 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +214 -0
- package/dist/readiness.js.map +1 -0
- package/dist/sandbox.d.ts +94 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +228 -0
- package/dist/sandbox.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §21's execution boundary, container backend -- the macOS/Windows half of the
|
|
3
|
+
* sandbox-isolation lane (`sandbox.ts` covers Linux via bubblewrap).
|
|
4
|
+
*
|
|
5
|
+
* **Not a second native sandbox.** `sandbox-exec`/Seatbelt (macOS) and Job
|
|
6
|
+
* Objects/AppContainer/WFP (Windows) were researched and rejected: `sandbox-exec` is
|
|
7
|
+
* deprecated with no public API replacement, Endpoint Security needs an Apple
|
|
8
|
+
* entitlement this project doesn't have, and hand-building Windows equivalents is a
|
|
9
|
+
* second and third bespoke sandbox for a problem already solved once.
|
|
10
|
+
*
|
|
11
|
+
* **Mechanism: route the spawned process through a real container instead**, via
|
|
12
|
+
* `docker run` (Docker Desktop's Linux VM on macOS, WSL2/Docker Desktop on Windows).
|
|
13
|
+
* The container's own rootfs is the isolation boundary; no bwrap needed inside it.
|
|
14
|
+
*
|
|
15
|
+
* **Verified for real** (Linux dev machine, real Docker daemon 29.6.1, not mocked):
|
|
16
|
+
* a file outside bind-mounts is unreachable (`ENOENT`); a network call under
|
|
17
|
+
* `--network none` is refused against a real listener; both together, through the
|
|
18
|
+
* full `ExecutionController.run()` path. See `container-sandbox.test.ts`,
|
|
19
|
+
* `controller.test.ts`'s container-backend e2e test.
|
|
20
|
+
*
|
|
21
|
+
* **UNVERIFIED on real macOS/Windows hardware** (none available here): Docker
|
|
22
|
+
* Desktop running-detection before spawn, path translation across the Docker
|
|
23
|
+
* Desktop VM boundary (macOS) and WSL2 (Windows) -- this module's bind-mount assumes
|
|
24
|
+
* host path == in-container path (`-v hostPath:hostPath`), true on Linux and for
|
|
25
|
+
* Docker Desktop's Linux VM when already inside its shared-drive mapping, untested
|
|
26
|
+
* for an arbitrary Windows path. Mechanism exists and is tested where it can be;
|
|
27
|
+
* cross-platform verification needs hardware nobody here has -- not a bug, not
|
|
28
|
+
* claimed otherwise.
|
|
29
|
+
*
|
|
30
|
+
* **Scope boundary** (same treatment as attach-mode elsewhere): a target that can't
|
|
31
|
+
* run containerized at all (deep native OS integration, GUI, native deps absent
|
|
32
|
+
* from a Linux image) is out of scope on any platform, with no native-sandbox
|
|
33
|
+
* fallback -- disclosed, not silently unsupported.
|
|
34
|
+
*
|
|
35
|
+
* **Measured Docker/bwrap difference** (found via a mutation check removing
|
|
36
|
+
* `--network none` and rerunning): Docker gives every container its own network
|
|
37
|
+
* namespace regardless of `--network none` -- unlike bwrap, which shares the host's
|
|
38
|
+
* network unless `networkPolicy` is declared. So a container can never reach the
|
|
39
|
+
* HOST's loopback either way, but it CAN still reach the public internet via
|
|
40
|
+
* Docker's default bridge NAT unless `--network none` is set. The dedicated
|
|
41
|
+
* mutation-sensitive test in `container-sandbox.test.ts` checks reachability to an
|
|
42
|
+
* external host (not loopback) to isolate exactly this.
|
|
43
|
+
*
|
|
44
|
+
* **Not solved here, disclosed:** `ResourceLimits` composition -- `sandbox.ts` wraps
|
|
45
|
+
* bwrap with `applyResourceLimits`; this module does not wrap Docker's
|
|
46
|
+
* `--memory`/`--cpus`/`--pids-limit` equivalents, so a `resourceLimits`-declared
|
|
47
|
+
* execution on this backend runs unlimited inside the container. Selective network
|
|
48
|
+
* allow/deny-listing is unimplemented for the same reason as `sandbox.ts` (needs DNS
|
|
49
|
+
* interception + IP filtering) -- `unsupportedContainerNetworkPolicyReason` reports
|
|
50
|
+
* it the same way, matching posture rather than overclaiming.
|
|
51
|
+
*/
|
|
52
|
+
import { execFileSync } from "node:child_process";
|
|
53
|
+
import { randomUUID } from "node:crypto";
|
|
54
|
+
import { basename } from "node:path";
|
|
55
|
+
/** Real check: working Docker CLI + reachable daemon, not just a `docker` binary on PATH. `docker version` talks to the daemon, so a CLI with no daemon running fails the same way a missing binary does -- both mean "cannot enforce." */
|
|
56
|
+
export function containerRuntimeCapability(env = process.env) {
|
|
57
|
+
try {
|
|
58
|
+
execFileSync("docker", ["version", "--format", "{{.Server.Version}}"], { env, stdio: "ignore" });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return {
|
|
62
|
+
availability: "unavailable",
|
|
63
|
+
reason: "docker is not on PATH, or no Docker daemon is reachable -- container-based isolation cannot be enforced without a real, running Docker",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { availability: "available", reason: null };
|
|
67
|
+
}
|
|
68
|
+
// Mirrors sandbox.ts's filesystem/network capability split: one real mechanism
|
|
69
|
+
// (container boundary), two named capabilities since the policies are declared
|
|
70
|
+
// and reasoned about independently.
|
|
71
|
+
export function containerFilesystemIsolationCapability(env = process.env) {
|
|
72
|
+
return containerRuntimeCapability(env);
|
|
73
|
+
}
|
|
74
|
+
export function containerNetworkIsolationCapability(env = process.env) {
|
|
75
|
+
return containerRuntimeCapability(env);
|
|
76
|
+
}
|
|
77
|
+
/** Same posture as `sandbox.ts`'s `unsupportedNetworkPolicyReason`: only full denial (`--network none`) is enforced. A shape bwrap discloses as unsupported isn't silently claimed here just because the mechanism changed. */
|
|
78
|
+
export function unsupportedContainerNetworkPolicyReason(policy) {
|
|
79
|
+
if (policy.mode === "allow" && policy.hosts.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
return ('only full network denial ({ mode: "allow", hosts: [] }) is enforced -- selective allow- or deny-listing of specific ' +
|
|
82
|
+
"hosts would need a custom docker network with DNS interception and IP filtering inside it, not built in this " +
|
|
83
|
+
"iteration (the same posture the bwrap backend already declares for the same shapes)");
|
|
84
|
+
}
|
|
85
|
+
// Minimal official -slim/-jre images for this runtime's fixture targets, keyed by
|
|
86
|
+
// interpreter basename so a full path still resolves. Not a general registry --
|
|
87
|
+
// other runtimes pass `containerImage` explicitly instead of extending this table.
|
|
88
|
+
const KNOWN_RUNTIME_IMAGES = {
|
|
89
|
+
node: "node:22-slim",
|
|
90
|
+
python: "python:3.12-slim",
|
|
91
|
+
python3: "python:3.12-slim",
|
|
92
|
+
java: "eclipse-temurin:21-jre",
|
|
93
|
+
};
|
|
94
|
+
export function resolveContainerImage(interpreterCommand) {
|
|
95
|
+
return KNOWN_RUNTIME_IMAGES[basename(interpreterCommand)] ?? null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Wraps `command`/`args` with `docker run` so a container boundary enforces
|
|
99
|
+
* `filesystemPolicy`/`networkPolicy`. Neither declared returns them unchanged and
|
|
100
|
+
* never invokes docker -- same non-regression contract as `sandbox.ts`'s
|
|
101
|
+
* `applySandbox`.
|
|
102
|
+
*
|
|
103
|
+
* **Throws rather than silently spawning unconstrained** when a policy can't
|
|
104
|
+
* actually be backed (no working Docker, unimplemented `networkPolicy` shape).
|
|
105
|
+
*
|
|
106
|
+
* **Returns a real, unique `containerName` whenever it wraps.** Found via real
|
|
107
|
+
* escape testing: `process-manager.ts`'s group-kill targets the local `docker` CLI
|
|
108
|
+
* process's group, which is right for bwrap but not Docker -- the container runs
|
|
109
|
+
* under `dockerd`, not as the CLI's child, so killing the CLI's group left an
|
|
110
|
+
* orphaned `node:22-slim` container running in testing. `containerName` lets the
|
|
111
|
+
* caller issue an explicit `docker stop <name>` against the container itself.
|
|
112
|
+
*/
|
|
113
|
+
export function applyContainerSandbox(options) {
|
|
114
|
+
const { filesystemPolicy, networkPolicy } = options;
|
|
115
|
+
if (filesystemPolicy === undefined && networkPolicy === undefined) {
|
|
116
|
+
return { command: options.command, args: options.args };
|
|
117
|
+
}
|
|
118
|
+
const env = options.env ?? process.env;
|
|
119
|
+
if (networkPolicy !== undefined) {
|
|
120
|
+
const shapeReason = unsupportedContainerNetworkPolicyReason(networkPolicy);
|
|
121
|
+
if (shapeReason !== null) {
|
|
122
|
+
throw new Error(`networkPolicy was configured but cannot be enforced: ${shapeReason}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const capability = containerRuntimeCapability(env);
|
|
126
|
+
if (capability.availability !== "available") {
|
|
127
|
+
throw new Error(`container sandbox was requested but cannot be enforced: ${capability.reason}`);
|
|
128
|
+
}
|
|
129
|
+
const image = options.containerImage ?? resolveContainerImage(options.interpreterCommand);
|
|
130
|
+
if (image === null) {
|
|
131
|
+
throw new Error(`container sandbox was requested but no base image is known for interpreter "${options.interpreterCommand}" -- pass containerImage explicitly`);
|
|
132
|
+
}
|
|
133
|
+
const containerName = `descry-sandbox-${randomUUID()}`;
|
|
134
|
+
const dockerArgs = ["run", "--rm", "--name", containerName];
|
|
135
|
+
if (networkPolicy !== undefined) {
|
|
136
|
+
dockerArgs.push("--network", "none");
|
|
137
|
+
}
|
|
138
|
+
dockerArgs.push("-v", `${options.cwd}:${options.cwd}`, "-w", options.cwd);
|
|
139
|
+
if (filesystemPolicy !== undefined) {
|
|
140
|
+
for (const root of filesystemPolicy.allowedRoots) {
|
|
141
|
+
dockerArgs.push("-v", `${root}:${root}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const [key, value] of Object.entries(options.processEnv ?? {})) {
|
|
145
|
+
if (key === "PATH" || value === undefined)
|
|
146
|
+
continue;
|
|
147
|
+
dockerArgs.push("-e", `${key}=${value}`);
|
|
148
|
+
}
|
|
149
|
+
dockerArgs.push(image, options.command, ...options.args);
|
|
150
|
+
return { command: "docker", args: dockerArgs, containerName };
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=container-sandbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container-sandbox.js","sourceRoot":"","sources":["../src/container-sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime controller (§4/§5/§6/§45 "Core"). Owns Execution lifecycle, spawns every
|
|
3
|
+
* configured service, waits for each one's real readiness (never "process exists"),
|
|
4
|
+
* guarantees cleanup on partial/total failure and timeout. Knows nothing about any
|
|
5
|
+
* specific language or framework (principle 8).
|
|
6
|
+
*/
|
|
7
|
+
import { type Execution, type ProcessHandle } from "@descryy/runtime-contracts";
|
|
8
|
+
import { type ProcessOutputReader, type ReadinessCheck, type ReadinessResult } from "./readiness.ts";
|
|
9
|
+
import type { ExecutionConfiguration } from "@descryy/runtime-contracts";
|
|
10
|
+
export interface CreateExecutionInput {
|
|
11
|
+
readonly application: string;
|
|
12
|
+
readonly repository: string;
|
|
13
|
+
readonly commit: string;
|
|
14
|
+
readonly configuration: ExecutionConfiguration;
|
|
15
|
+
}
|
|
16
|
+
export interface ServiceReadinessSpec {
|
|
17
|
+
/**
|
|
18
|
+
* `outputReader` is a real `ProcessOutputReader` bound to THIS spawned (or
|
|
19
|
+
* attached) process's own output — the only way to build a `log-pattern`
|
|
20
|
+
* check that type-checks (`readiness.ts`'s `ProcessOutputReader` brand).
|
|
21
|
+
* RT-024: this closes "log-pattern only carries the stronger this-run
|
|
22
|
+
* guarantee as caller discipline, not enforced by the type" by making the
|
|
23
|
+
* discipline the only path the type allows.
|
|
24
|
+
*/
|
|
25
|
+
readonly checks: (info: {
|
|
26
|
+
readonly port: number;
|
|
27
|
+
readonly outputReader: ProcessOutputReader;
|
|
28
|
+
}) => readonly ReadinessCheck[];
|
|
29
|
+
readonly timeoutMs: number;
|
|
30
|
+
}
|
|
31
|
+
export interface RunOptions {
|
|
32
|
+
/** One entry required per key in `configuration.services` -- refuses rather than defaulting a missing one to "spawn success is enough" (RT-024/RT-032). */
|
|
33
|
+
readonly readiness: Readonly<Record<string, ServiceReadinessSpec>>;
|
|
34
|
+
/**
|
|
35
|
+
* Optional live observer for `ProcessLifecycleEvent`, called synchronously at each
|
|
36
|
+
* transition for the lifetime of this execution (including after `run()` returns).
|
|
37
|
+
* A throwing observer is not caught here -- same "must not hang, may not silently
|
|
38
|
+
* swallow its own failure" discipline `Collector` imposes on every callback here.
|
|
39
|
+
*/
|
|
40
|
+
readonly onProcessLifecycleEvent?: (event: ProcessLifecycleEvent) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface ServiceStartResult {
|
|
43
|
+
readonly serviceName: string;
|
|
44
|
+
readonly stage: "spawn" | "readiness" | "not-attempted";
|
|
45
|
+
readonly succeeded: boolean;
|
|
46
|
+
/** Set when `stage === "readiness"`. RT-024/RT-031 apply per entry -- N of these is N liveness claims, never a joint identity proof. */
|
|
47
|
+
readonly readiness: ReadinessResult | null;
|
|
48
|
+
/** Set when `stage === "spawn"` and spawning itself threw. */
|
|
49
|
+
readonly error: string | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fires at the exact moments `run()` observes a service's lifecycle -- spawned,
|
|
53
|
+
* readiness settled either way, reaped. A pure notification channel; nothing here
|
|
54
|
+
* decides whether a transition becomes Evidence.
|
|
55
|
+
*
|
|
56
|
+
* Needed because `Execution.processes`/`ServiceStartResults` are only complete once
|
|
57
|
+
* `run()` returns, but a service still running when `run()` resolves hasn't exited
|
|
58
|
+
* yet -- its eventual `process-exited` lies in the future. Only this live channel
|
|
59
|
+
* can observe that exit; reading the final `Execution` later cannot.
|
|
60
|
+
*/
|
|
61
|
+
export type ProcessLifecycleEvent = {
|
|
62
|
+
readonly kind: "process-started";
|
|
63
|
+
readonly serviceName: string;
|
|
64
|
+
readonly handle: ProcessHandle;
|
|
65
|
+
} | {
|
|
66
|
+
readonly kind: "process-ready";
|
|
67
|
+
readonly serviceName: string;
|
|
68
|
+
readonly handle: ProcessHandle;
|
|
69
|
+
readonly readiness: ReadinessResult;
|
|
70
|
+
} | {
|
|
71
|
+
readonly kind: "process-ready-failed";
|
|
72
|
+
readonly serviceName: string;
|
|
73
|
+
readonly handle: ProcessHandle;
|
|
74
|
+
readonly readiness: ReadinessResult;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `phase` distinguishes "readiness hadn't yet succeeded when it exited"
|
|
78
|
+
* (`"starting"`) from "had already reached READY" (`"running"`) -- the
|
|
79
|
+
* process-level echo of `FAILED_START` vs `FAILED` (§30). Computed from this
|
|
80
|
+
* service's own readiness history, not `Execution.state` at exit time -- a crash
|
|
81
|
+
* mid-readiness-poll fires before the execution's own state has caught up.
|
|
82
|
+
*/
|
|
83
|
+
| {
|
|
84
|
+
readonly kind: "process-exited";
|
|
85
|
+
readonly serviceName: string;
|
|
86
|
+
readonly handle: ProcessHandle;
|
|
87
|
+
readonly phase: "starting" | "running";
|
|
88
|
+
};
|
|
89
|
+
export declare class ExecutionController {
|
|
90
|
+
#private;
|
|
91
|
+
private constructor();
|
|
92
|
+
static create(input: CreateExecutionInput): ExecutionController;
|
|
93
|
+
get execution(): Execution;
|
|
94
|
+
/** One entry per configured service, in spawn order, including never-attempted ones (`stage: "not-attempted"`). Empty until `run()` returns its first result. */
|
|
95
|
+
get serviceStartResults(): readonly ServiceStartResult[];
|
|
96
|
+
/** Non-null only when `run()` refused before spawning anything. */
|
|
97
|
+
get validationError(): string | null;
|
|
98
|
+
/** Combined stdout+stderr of the named service's process so far, or "" if it was never spawned. */
|
|
99
|
+
readProcessOutput(serviceName: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* Starts every service in dependency order, spawned (each on its own ephemeral
|
|
102
|
+
* port unless fixed) or attached (`service.attach`, for an already-running
|
|
103
|
+
* pid/log file). Reaches RUNNING only if every service starts and reaches
|
|
104
|
+
* readiness. On first failure, every started service is killed in reverse order
|
|
105
|
+
* (no-op for attached ones) and remaining services are recorded "not-attempted" --
|
|
106
|
+
* `serviceStartResults` always distinguishes "never tried" from "tried and failed".
|
|
107
|
+
*
|
|
108
|
+
* Configuration is validated atomically first -- a bad `dependsOn`, a cycle, an
|
|
109
|
+
* undeclared placeholder, ambiguous `command`/`attach`, or a missing readiness
|
|
110
|
+
* entry refuses before anything spawns.
|
|
111
|
+
*
|
|
112
|
+
* Every **spawned** service gets `PORT` (its resolved port) unconditionally --
|
|
113
|
+
* load-bearing (matches the fixture apps), not incidental: a command reading its
|
|
114
|
+
* port from anywhere else won't be reachable via readiness checks built from the
|
|
115
|
+
* same `port`. An **attached** service gets no env/port allocation -- it's already
|
|
116
|
+
* running on whatever it bound at launch.
|
|
117
|
+
*/
|
|
118
|
+
run(options: RunOptions): Promise<Execution>;
|
|
119
|
+
stop(): Promise<Execution>;
|
|
120
|
+
/**
|
|
121
|
+
* Stop everything and record `TIMED_OUT` -- same transition `#onTimeout` makes,
|
|
122
|
+
* for a caller that knows the budget expired before the internal timer noticed.
|
|
123
|
+
*
|
|
124
|
+
* **Why not `stop()`:** the orchestrator clamps its observation window to
|
|
125
|
+
* whatever's left of `timeoutMs`, making `TIMED_OUT` a race between `#onTimeout`
|
|
126
|
+
* and `stop()` at the deadline -- measured: the same case reported `TIMED_OUT` at
|
|
127
|
+
* 1534ms and `COMPLETED` at 1515ms. `execution.state` is what `observe_runtime`
|
|
128
|
+
* reads to choose `ok` vs `timed_out`, so losing that race silently hides a real
|
|
129
|
+
* cutoff. A caller that already decided it truncated the run shouldn't have to
|
|
130
|
+
* win a race to report it.
|
|
131
|
+
*
|
|
132
|
+
* Idempotent and terminal-safe like `cancel()`: if the timer fired first, returns
|
|
133
|
+
* the execution it already finished.
|
|
134
|
+
*/
|
|
135
|
+
timeout(): Promise<Execution>;
|
|
136
|
+
cancel(): Promise<Execution>;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
|