@executablemd/runtime 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/apis.js +119 -24
- package/esm/config.js +46 -17
- package/esm/duration.js +44 -0
- package/esm/files.js +558 -0
- package/esm/host-files.js +496 -0
- package/esm/mod.js +13 -7
- package/esm/service.js +126 -0
- package/esm/test/mod.js +2 -1
- package/esm/test/stubs.js +38 -6
- package/package.json +10 -4
- package/types/apis.d.ts +91 -18
- package/types/config.d.ts +30 -12
- package/types/duration.d.ts +18 -0
- package/types/files.d.ts +299 -0
- package/types/host-files.d.ts +80 -0
- package/types/mod.d.ts +17 -7
- package/types/service.d.ts +77 -0
- package/types/test/mod.d.ts +2 -1
- package/types/test/stubs.d.ts +9 -0
- package/esm/find-free-port.js +0 -33
- package/types/find-free-port.d.ts +0 -9
package/esm/test/stubs.js
CHANGED
|
@@ -19,7 +19,25 @@
|
|
|
19
19
|
* });
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
|
+
import { Stdio } from "@effectionx/process";
|
|
22
23
|
import { API } from "../apis.js";
|
|
24
|
+
import { SERVICE_HOSTNAME } from "../service.js";
|
|
25
|
+
/** Install a provider-neutral scoped service attachment stub. */
|
|
26
|
+
export function* useStubService(endpoint) {
|
|
27
|
+
if (endpoint.hostname !== SERVICE_HOSTNAME) {
|
|
28
|
+
throw new Error("stub service endpoint must use 127.0.0.1");
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isInteger(endpoint.port) || endpoint.port < 1 || endpoint.port > 65_535) {
|
|
31
|
+
throw new Error("stub service endpoint port must be an integer from 1 through 65535");
|
|
32
|
+
}
|
|
33
|
+
const exact = Object.freeze({ hostname: SERVICE_HOSTNAME, port: endpoint.port });
|
|
34
|
+
yield* API.Service.around({
|
|
35
|
+
// deno-lint-ignore require-yield
|
|
36
|
+
*start() {
|
|
37
|
+
return { endpoint: exact };
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
23
41
|
/**
|
|
24
42
|
* Install an in-memory filesystem stub.
|
|
25
43
|
*
|
|
@@ -57,15 +75,20 @@ export function* useStubFs(files) {
|
|
|
57
75
|
*
|
|
58
76
|
* Recognizes `bash -c "echo ..."` and returns the echo'd text as stdout.
|
|
59
77
|
* All other commands return the script text as stdout with exit code 0.
|
|
78
|
+
*
|
|
79
|
+
* It answers like a real child: the text is written to the stdio chain as it
|
|
80
|
+
* "arrives", so whatever encloses the call sees it the way it would see a
|
|
81
|
+
* child's, and it is retained in the outcome only when the caller asked
|
|
82
|
+
* for retention. A stub that always returned strings would let a document
|
|
83
|
+
* render output the contract says was already displayed.
|
|
60
84
|
*/
|
|
61
85
|
export function* useEchoExec() {
|
|
62
86
|
yield* API.Process.around({
|
|
63
87
|
*exec([options], _next) {
|
|
64
88
|
const script = (options.command[2] ?? "").trim();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
return { exitCode: 0, stdout: script + "\n", stderr: "" };
|
|
89
|
+
const text = script.startsWith("echo ") ? script.slice(5) + "\n" : script + "\n";
|
|
90
|
+
yield* Stdio.operations.stdout(encoder.encode(text));
|
|
91
|
+
return retained(options, { exitCode: 0, stdout: text, stderr: "" });
|
|
69
92
|
},
|
|
70
93
|
});
|
|
71
94
|
}
|
|
@@ -76,8 +99,17 @@ export function* useEchoExec() {
|
|
|
76
99
|
*/
|
|
77
100
|
export function* useFailingExec(exitCode, stderr = "command failed") {
|
|
78
101
|
yield* API.Process.around({
|
|
79
|
-
*exec(
|
|
80
|
-
|
|
102
|
+
*exec([options], _next) {
|
|
103
|
+
yield* Stdio.operations.stderr(encoder.encode(stderr));
|
|
104
|
+
return retained(options, { exitCode, stdout: "", stderr });
|
|
81
105
|
},
|
|
82
106
|
});
|
|
83
107
|
}
|
|
108
|
+
const encoder = new TextEncoder();
|
|
109
|
+
/** What the caller asked to keep, so a stub cannot retain more than a call would. */
|
|
110
|
+
function retained(options, outcome) {
|
|
111
|
+
if (options.retain === false) {
|
|
112
|
+
return { exitCode: outcome.exitCode, stdout: undefined, stderr: undefined };
|
|
113
|
+
}
|
|
114
|
+
return outcome;
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@executablemd/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Runtime host APIs for executable.md documents.",
|
|
5
5
|
"homepage": "https://executable.md",
|
|
6
6
|
"repository": {
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
"default": "./esm/mod.js"
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
|
+
"./files": {
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./types/files.d.ts",
|
|
26
|
+
"default": "./esm/files.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
23
29
|
"./test": {
|
|
24
30
|
"import": {
|
|
25
31
|
"types": "./types/test/mod.d.ts",
|
|
@@ -30,11 +36,11 @@
|
|
|
30
36
|
"scripts": {},
|
|
31
37
|
"dependencies": {
|
|
32
38
|
"@effectionx/context-api": "0.6.0",
|
|
33
|
-
"@effectionx/fetch": "0.2.
|
|
39
|
+
"@effectionx/fetch": "0.2.1",
|
|
34
40
|
"@effectionx/fs": "0.3.0",
|
|
35
|
-
"@effectionx/node": "0.2.4",
|
|
36
41
|
"@effectionx/process": "0.8.1",
|
|
37
|
-
"effection": "4.1.0
|
|
42
|
+
"effection": "4.1.0",
|
|
43
|
+
"@effectionx/node": "0.2.4"
|
|
38
44
|
},
|
|
39
45
|
"_generatedBy": "dnt@dev"
|
|
40
46
|
}
|
package/types/apis.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime Context APIs — platform I/O operations with pluggable middleware.
|
|
3
3
|
*
|
|
4
|
-
* Five
|
|
4
|
+
* Five host-backed domain APIs plus the provider-neutral Service Api, built on
|
|
5
|
+
* `@effectionx/context-api`.
|
|
5
6
|
* Each API provides default Node.js implementations. Use `.around()` to
|
|
6
7
|
* install middleware (mocking, instrumentation, sandboxing) scoped to the
|
|
7
8
|
* current Effection scope.
|
|
@@ -25,14 +26,19 @@
|
|
|
25
26
|
* });
|
|
26
27
|
* ```
|
|
27
28
|
*
|
|
28
|
-
* ## Why
|
|
29
|
+
* ## Why separate APIs?
|
|
29
30
|
*
|
|
30
31
|
* - **Process** — subprocess lifecycle has its own cancellation semantics
|
|
31
32
|
* (killing processes on scope teardown). Middleware targets exec only.
|
|
32
|
-
* - **Fs** — reading, writing, and inspecting
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* - **Fs** — the low-level host file surface: reading, writing, and inspecting
|
|
34
|
+
* paths the engine itself resolves, for component lookup, replay guards, and
|
|
35
|
+
* the root document. It is the host adapter's own dependency, not the
|
|
36
|
+
* boundary a document's paths cross.
|
|
37
|
+
* - **Files** — document filesystem access, in whole semantic operations
|
|
38
|
+
* (`files.ts`). `<File>`, `<Glob>`, and `<TempDir>` speak only this Api, so
|
|
39
|
+
* the same document means the same thing whether its paths resolve in the
|
|
40
|
+
* caller's filesystem or in a run-owned logical one. Its terminal handler
|
|
41
|
+
* throws: an uninstalled provider must not silently reach the host.
|
|
36
42
|
* - **Fetch** — HTTP has distinct timeout/body/abort semantics. Merging
|
|
37
43
|
* with Fs or Process would blur cancellation boundaries.
|
|
38
44
|
* - **Env** — the host itself: metadata (env vars, platform) plus the two
|
|
@@ -41,6 +47,8 @@
|
|
|
41
47
|
* use `.around()` to mock platform/env for deterministic replay; an
|
|
42
48
|
* entrypoint installs its `command` and `compile` with `{ at: "min" }` so
|
|
43
49
|
* ordinary middleware can wrap them.
|
|
50
|
+
* - **Service** — scoped service attachment. Its terminal handler requires an
|
|
51
|
+
* explicit host provider and never detects or imports a runtime.
|
|
44
52
|
*
|
|
45
53
|
* ## Middleware
|
|
46
54
|
*
|
|
@@ -59,10 +67,13 @@
|
|
|
59
67
|
* ## Test stubs
|
|
60
68
|
*
|
|
61
69
|
* Common stubs are provided by `@executablemd/runtime/test`:
|
|
62
|
-
* `useStubFs(files)`, `useEchoExec()`, `useFailingExec(code, stderr)
|
|
70
|
+
* `useStubFs(files)`, `useEchoExec()`, `useFailingExec(code, stderr)`,
|
|
71
|
+
* `useStubService(endpoint)`.
|
|
63
72
|
*/
|
|
64
73
|
import { type Api } from "@effectionx/context-api";
|
|
65
74
|
import type { Operation } from "effection";
|
|
75
|
+
import { Files } from "./files.js";
|
|
76
|
+
import { Service } from "./service.js";
|
|
66
77
|
/**
|
|
67
78
|
* Result of a `stat` call.
|
|
68
79
|
*
|
|
@@ -95,17 +106,34 @@ export interface RuntimeFetchResponse {
|
|
|
95
106
|
/** Read the response body as text. */
|
|
96
107
|
text(): Operation<string>;
|
|
97
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* What a finished child process reports.
|
|
111
|
+
*
|
|
112
|
+
* `stdout` and `stderr` are the text a caller asked to be retained. A caller
|
|
113
|
+
* that asked for none gets `undefined` rather than an empty string: nothing was
|
|
114
|
+
* accumulated, and "no output" and "not retained" are different answers.
|
|
115
|
+
*/
|
|
116
|
+
export interface ProcessOutcome {
|
|
117
|
+
exitCode: number;
|
|
118
|
+
stdout: string | undefined;
|
|
119
|
+
stderr: string | undefined;
|
|
120
|
+
}
|
|
121
|
+
export interface ProcessExecOptions {
|
|
122
|
+
command: string[];
|
|
123
|
+
cwd?: string;
|
|
124
|
+
env?: Record<string, string>;
|
|
125
|
+
timeout?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Retain the child's output in the outcome. Default `true`.
|
|
128
|
+
*
|
|
129
|
+
* Retention is the caller's explicit choice, never an inference: a run that
|
|
130
|
+
* keeps no diagnostic record asks for `false` and the text is forwarded to
|
|
131
|
+
* whatever is displaying it without ever being accumulated here.
|
|
132
|
+
*/
|
|
133
|
+
retain?: boolean;
|
|
134
|
+
}
|
|
98
135
|
interface ProcessHandler {
|
|
99
|
-
exec(options:
|
|
100
|
-
command: string[];
|
|
101
|
-
cwd?: string;
|
|
102
|
-
env?: Record<string, string>;
|
|
103
|
-
timeout?: number;
|
|
104
|
-
}): Operation<{
|
|
105
|
-
exitCode: number;
|
|
106
|
-
stdout: string;
|
|
107
|
-
stderr: string;
|
|
108
|
-
}>;
|
|
136
|
+
exec(options: ProcessExecOptions): Operation<ProcessOutcome>;
|
|
109
137
|
}
|
|
110
138
|
interface FsHandler {
|
|
111
139
|
readTextFile(path: string): Operation<string>;
|
|
@@ -178,10 +206,35 @@ interface EnvHandler {
|
|
|
178
206
|
export declare const API: {
|
|
179
207
|
Process: Api<ProcessHandler>;
|
|
180
208
|
Fs: Api<FsHandler>;
|
|
209
|
+
Files: typeof Files;
|
|
181
210
|
Fetch: Api<FetchHandler>;
|
|
182
211
|
Env: Api<EnvHandler>;
|
|
212
|
+
Service: typeof Service;
|
|
183
213
|
};
|
|
184
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Run a child process.
|
|
216
|
+
*
|
|
217
|
+
* A caller that says nothing about retention keeps the output, which is what
|
|
218
|
+
* every caller with something to read wants and what callers have always had.
|
|
219
|
+
* Asking for `retain: false` keeps the exit status alone, and the overloads say
|
|
220
|
+
* so: there is no string to read on that path, and the type refuses to pretend
|
|
221
|
+
* otherwise. Core, which decides retention per block, calls the Api operation
|
|
222
|
+
* directly and handles both.
|
|
223
|
+
*/
|
|
224
|
+
export declare function exec(options: ProcessExecOptions & {
|
|
225
|
+
retain: false;
|
|
226
|
+
}): Operation<{
|
|
227
|
+
exitCode: number;
|
|
228
|
+
stdout: undefined;
|
|
229
|
+
stderr: undefined;
|
|
230
|
+
}>;
|
|
231
|
+
export declare function exec(options: ProcessExecOptions & {
|
|
232
|
+
retain?: true;
|
|
233
|
+
}): Operation<{
|
|
234
|
+
exitCode: number;
|
|
235
|
+
stdout: string;
|
|
236
|
+
stderr: string;
|
|
237
|
+
}>;
|
|
185
238
|
export declare const readTextFile: typeof API.Fs.operations.readTextFile;
|
|
186
239
|
export declare const stat: typeof API.Fs.operations.stat;
|
|
187
240
|
export declare const glob: typeof API.Fs.operations.glob;
|
|
@@ -196,4 +249,24 @@ export declare const cwd: typeof API.Env.operations.cwd;
|
|
|
196
249
|
export declare const platform: typeof API.Env.operations.platform;
|
|
197
250
|
export declare const command: typeof API.Env.operations.command;
|
|
198
251
|
export declare const compile: typeof API.Env.operations.compile;
|
|
252
|
+
/**
|
|
253
|
+
* Discard the standard output of subprocesses started in this scope.
|
|
254
|
+
*
|
|
255
|
+
* For a caller whose subprocess output is an *answer* rather than something to
|
|
256
|
+
* show: a command whose stdout is parsed and returned would otherwise also
|
|
257
|
+
* print itself into whatever the process was rendering. `stderr` is left alone,
|
|
258
|
+
* because that is where a failing command explains itself and a diagnostic is
|
|
259
|
+
* worth seeing.
|
|
260
|
+
*
|
|
261
|
+
* It lives here because reaching the process Api's stdio directly is host
|
|
262
|
+
* behavior, and modules held to the runtime-neutral boundary may not import a
|
|
263
|
+
* host process module of their own.
|
|
264
|
+
*
|
|
265
|
+
* Installed at the display boundary, where the host's own writer sits: not
|
|
266
|
+
* showing something and not knowing it are different, and a caller that asked
|
|
267
|
+
* for the answer must still be given it. Anything upstream — this adapter's
|
|
268
|
+
* retention, a document's capture, a run's record — reads the bytes first and
|
|
269
|
+
* only the host is left out.
|
|
270
|
+
*/
|
|
271
|
+
export declare function useQuietProcessOutput(): Operation<void>;
|
|
199
272
|
export {};
|
package/types/config.d.ts
CHANGED
|
@@ -1,24 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Config Api — shared execution configuration with pluggable middleware.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* timeout
|
|
4
|
+
* Three timeouts, three owners, no defaults:
|
|
5
|
+
*
|
|
6
|
+
* - `timeout` is the deadline for the whole run — preparation and execution
|
|
7
|
+
* together — and only the outer run boundary consumes it.
|
|
8
|
+
* - `timeoutExec` is what each exec block gets, and only exec blocks and the
|
|
9
|
+
* built-in `timeout` modifier consume it.
|
|
10
|
+
* - `timeoutFetch` is what each Fetch gets, and only Fetch consumes it.
|
|
11
|
+
*
|
|
12
|
+
* `undefined` means no timeout, and it is what every field starts as. An
|
|
13
|
+
* operation nobody bounded runs until it finishes or the run's own deadline
|
|
14
|
+
* cancels it; a general "shared timeout" that quietly bounded processes,
|
|
15
|
+
* requests, prompts, and services alike is what this replaces. Override a
|
|
16
|
+
* field for a scope with:
|
|
7
17
|
*
|
|
8
18
|
* ```typescript
|
|
9
|
-
* yield* Config.around({
|
|
19
|
+
* yield* Config.around({ timeoutExec: () => 30_000 }, { at: "min" });
|
|
10
20
|
* ```
|
|
21
|
+
*
|
|
22
|
+
* Installing at `min` is what lets a nested override win: a block's own
|
|
23
|
+
* `timeout=` outranks the value the command line established for the run.
|
|
24
|
+
* Omitting a field inherits the enclosing value rather than clearing it.
|
|
11
25
|
*/
|
|
12
26
|
import { type Api } from "@effectionx/context-api";
|
|
13
27
|
import type { Operation } from "effection";
|
|
14
28
|
export interface ConfigApi {
|
|
15
|
-
/**
|
|
16
|
-
timeout: number;
|
|
29
|
+
/** Deadline for the entire run, in milliseconds; undefined for none. */
|
|
30
|
+
timeout: number | undefined;
|
|
31
|
+
/** Default timeout for each exec block, in milliseconds; undefined for none. */
|
|
32
|
+
timeoutExec: number | undefined;
|
|
33
|
+
/** Default timeout for each Fetch, in milliseconds; undefined for none. */
|
|
34
|
+
timeoutFetch: number | undefined;
|
|
17
35
|
}
|
|
18
36
|
export declare const Config: Api<ConfigApi>;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*/
|
|
24
|
-
export declare const
|
|
37
|
+
/** The validated run deadline. Read by the run boundary and nothing else. */
|
|
38
|
+
export declare const timeout: Operation<number | undefined>;
|
|
39
|
+
/** The validated default timeout for an exec block. */
|
|
40
|
+
export declare const timeoutExec: Operation<number | undefined>;
|
|
41
|
+
/** The validated default timeout for a Fetch. */
|
|
42
|
+
export declare const timeoutFetch: Operation<number | undefined>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The duration grammar, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Every timeout a caller or a document writes is spelled the same way: the
|
|
5
|
+
* three CLI options, and the `timeout=` modifier a block declares. A duration
|
|
6
|
+
* is a positive whole number with a unit — `500ms`, `30s`, `5min`, `20min` —
|
|
7
|
+
* or bare digits, which are milliseconds.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here substitutes a value. An empty, zero, negative, or malformed
|
|
10
|
+
* duration is refused where it was written, because the alternative is a run
|
|
11
|
+
* bounded by a number nobody asked for.
|
|
12
|
+
*/
|
|
13
|
+
/** Milliseconds, or `undefined` when `text` is not a duration. */
|
|
14
|
+
export declare function asDuration(text: string): number | undefined;
|
|
15
|
+
/** What a rejected duration says, with `label` naming where it was written. */
|
|
16
|
+
export declare function durationError(label: string, text: string): Error;
|
|
17
|
+
/** Milliseconds. Throws when `text` is not a duration this grammar accepts. */
|
|
18
|
+
export declare function parseDuration(text: string, label: string): number;
|
package/types/files.d.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `API.Files` — the document filesystem boundary.
|
|
3
|
+
*
|
|
4
|
+
* A document names files with a path relative to the contextual working
|
|
5
|
+
* directory, and every one of those operations arrives here. What is on the
|
|
6
|
+
* other side is a provider's choice: `xmd run` installs a host adapter that
|
|
7
|
+
* resolves those paths in the caller's own filesystem, and `xmd workflow`
|
|
8
|
+
* installs one that resolves them in a logical filesystem owned by a database
|
|
9
|
+
* transaction. Neither is named in the components that call this Api, which is
|
|
10
|
+
* what lets one document mean the same thing under both.
|
|
11
|
+
*
|
|
12
|
+
* The operations are **semantic**, not primitive. `writeTextFile` is a whole
|
|
13
|
+
* replacement — admission, resolution, target classification, parent creation,
|
|
14
|
+
* and commit — rather than a sequence a caller assembles, because assembling it
|
|
15
|
+
* from outside is what would let a path admitted by one provider be used by
|
|
16
|
+
* another. `API.Fs` remains the low-level host surface a host adapter is built
|
|
17
|
+
* on; it is not this boundary.
|
|
18
|
+
*
|
|
19
|
+
* `checkFilePath` is the one exception, and it is deliberately weak: pure path
|
|
20
|
+
* arithmetic, no filesystem access, and nothing usable comes back — no path, no
|
|
21
|
+
* handle, no authority token. `<File>`'s write form calls it to decide whether
|
|
22
|
+
* its children may expand at all, and the later `writeTextFile` repeats the
|
|
23
|
+
* same admission from the same authored path. A check that was skipped,
|
|
24
|
+
* replaced, or answered by another provider therefore authorizes nothing.
|
|
25
|
+
*
|
|
26
|
+
* ## Two kinds of failure
|
|
27
|
+
*
|
|
28
|
+
* An ordinary filesystem condition — missing, a directory, permission denied,
|
|
29
|
+
* no space — comes back as `Err(FilesError)` carrying frozen structural data.
|
|
30
|
+
* The consumer reads that data and selects a sentence from a fixed vocabulary;
|
|
31
|
+
* no message, errno code, resolved path, temporary path, or symlink target
|
|
32
|
+
* crosses this boundary. Cancellation is neither of these: it is not caught and
|
|
33
|
+
* never becomes a Result.
|
|
34
|
+
*
|
|
35
|
+
* A provider that is absent, that refuses an operation, or that breaks its own
|
|
36
|
+
* contract is not a filesystem condition. Those **throw**, with fixed
|
|
37
|
+
* diagnostics and no cause, and they end the execution rather than becoming
|
|
38
|
+
* something a document renders. A run whose Files provider is missing must not
|
|
39
|
+
* quietly reach the host instead.
|
|
40
|
+
*
|
|
41
|
+
* ## Why the data is structural
|
|
42
|
+
*
|
|
43
|
+
* Both the failures and the write outcome carry a plain frozen object under a
|
|
44
|
+
* stable `type` tag, and every consumer recognizes them by parsing that tag
|
|
45
|
+
* rather than with `instanceof`. Two copies of this package can be loaded at
|
|
46
|
+
* once — a repository component resolving its own runtime beside the engine's —
|
|
47
|
+
* and `instanceof` answers false across them, which would turn a provider
|
|
48
|
+
* failure into an unrecognized throw exactly when it matters most.
|
|
49
|
+
*/
|
|
50
|
+
import { type Api } from "@effectionx/context-api";
|
|
51
|
+
import type { Operation, Result } from "effection";
|
|
52
|
+
/** The stable discriminant on ordinary filesystem failure data. */
|
|
53
|
+
export declare const FILES_ERROR = "executablemd.runtime.files-error/v1";
|
|
54
|
+
/** The stable discriminant on infrastructure failure data. */
|
|
55
|
+
export declare const FILES_FATAL = "executablemd.runtime.files-fatal/v1";
|
|
56
|
+
/** The stable discriminant on a successful write's outcome data. */
|
|
57
|
+
export declare const FILES_WRITE_SUCCESS = "executablemd.runtime.files-write-success/v1";
|
|
58
|
+
/**
|
|
59
|
+
* The vocabulary an ordinary failure is reported in.
|
|
60
|
+
*
|
|
61
|
+
* A provider maps whatever its platform produced onto one of these before
|
|
62
|
+
* returning. An unmapped condition becomes `operation-failed`, which is a real
|
|
63
|
+
* answer rather than a placeholder: the consumer has a sentence for it, and the
|
|
64
|
+
* unmapped value itself never crosses the boundary.
|
|
65
|
+
*/
|
|
66
|
+
export type FilesReason = "empty-path" | "absolute-path" | "lexical-escape" | "resolved-escape" | "missing" | "directory" | "special-file" | "not-directory" | "permission-denied" | "read-only" | "too-many-symlinks" | "path-too-long" | "no-space" | "quota-exhausted" | "cross-device" | "busy" | "too-many-open-files" | "directory-not-empty" | "invalid-pattern" | "operation-failed";
|
|
67
|
+
/** The operations whose failure carries no commit outcome. */
|
|
68
|
+
export type FilesOperation = "check-file-path" | "read" | "glob" | "temporary-directory";
|
|
69
|
+
/** Where a non-write operation stopped. */
|
|
70
|
+
export type FilesPhase = "lexical" | "resolution" | "target" | "access" | "pattern" | "traversal" | "acquire";
|
|
71
|
+
/** Where a write stopped, which is what decides what may be said about the target. */
|
|
72
|
+
export type FileWritePhase = "lexical" | "resolution" | "target" | "parents" | "temporary" | "commit" | "cleanup" | "transaction";
|
|
73
|
+
/**
|
|
74
|
+
* What is known about the target afterwards.
|
|
75
|
+
*
|
|
76
|
+
* `commit-unknown` is an answer rather than a missing one: a commit that threw
|
|
77
|
+
* may have run or not, and no provider can tell which from where it stands.
|
|
78
|
+
* What still holds in that case is that the target is one complete version.
|
|
79
|
+
*/
|
|
80
|
+
export type FileWriteTarget = "unchanged" | "commit-unknown" | "committed" | "rolled-back";
|
|
81
|
+
export interface FilesFailureData {
|
|
82
|
+
readonly type: typeof FILES_ERROR;
|
|
83
|
+
readonly operation: FilesOperation;
|
|
84
|
+
readonly phase: FilesPhase;
|
|
85
|
+
readonly reason: FilesReason;
|
|
86
|
+
}
|
|
87
|
+
export interface FileWriteFailureData {
|
|
88
|
+
readonly type: typeof FILES_ERROR;
|
|
89
|
+
readonly operation: "write";
|
|
90
|
+
readonly phase: FileWritePhase;
|
|
91
|
+
readonly reason?: FilesReason;
|
|
92
|
+
readonly cleanup?: FilesReason;
|
|
93
|
+
readonly target: FileWriteTarget;
|
|
94
|
+
}
|
|
95
|
+
export type FilesErrorData = FilesFailureData | FileWriteFailureData;
|
|
96
|
+
/**
|
|
97
|
+
* The message every ordinary failure carries.
|
|
98
|
+
*
|
|
99
|
+
* Constant on purpose. A message is the part of an Error that gets printed by
|
|
100
|
+
* accident, and there is nothing safe to put in this one: the authored path
|
|
101
|
+
* belongs to the consumer that wrote it, and everything else belongs to the
|
|
102
|
+
* platform.
|
|
103
|
+
*/
|
|
104
|
+
export declare const FILES_ERROR_MESSAGE = "Files operation failed";
|
|
105
|
+
/** An ordinary filesystem failure. What it means is in `data`, never in the message. */
|
|
106
|
+
export declare class FilesError extends Error {
|
|
107
|
+
readonly data: FilesErrorData;
|
|
108
|
+
constructor(data: FilesErrorData);
|
|
109
|
+
}
|
|
110
|
+
export interface FileWriteSuccess {
|
|
111
|
+
readonly type: typeof FILES_WRITE_SUCCESS;
|
|
112
|
+
readonly publication: "host-committed" | "transaction-staged";
|
|
113
|
+
}
|
|
114
|
+
export interface FilePathInput {
|
|
115
|
+
readonly cwd: string;
|
|
116
|
+
readonly path: string;
|
|
117
|
+
}
|
|
118
|
+
export interface FileWriteInput extends FilePathInput {
|
|
119
|
+
readonly content: string;
|
|
120
|
+
}
|
|
121
|
+
export interface GlobInput {
|
|
122
|
+
readonly cwd: string;
|
|
123
|
+
readonly include: string[];
|
|
124
|
+
readonly exclude: string[];
|
|
125
|
+
}
|
|
126
|
+
export interface FilesHandler {
|
|
127
|
+
/**
|
|
128
|
+
* Whether this authored path is admissible at all, decided from the path and
|
|
129
|
+
* `cwd` alone. No filesystem access, and nothing usable comes back.
|
|
130
|
+
*/
|
|
131
|
+
checkFilePath(input: FilePathInput): Operation<Result<void>>;
|
|
132
|
+
readTextFile(input: FilePathInput): Operation<Result<string>>;
|
|
133
|
+
writeTextFile(input: FileWriteInput): Operation<Result<FileWriteSuccess>>;
|
|
134
|
+
/** Sorted, deduplicated, POSIX-separated paths of the regular files that match. */
|
|
135
|
+
globFiles(input: GlobInput): Operation<Result<string[]>>;
|
|
136
|
+
/**
|
|
137
|
+
* A directory that lives as long as the acquiring scope. A resource, so the
|
|
138
|
+
* caller holds it by acquisition rather than by remembering to remove it.
|
|
139
|
+
*/
|
|
140
|
+
temporaryDirectory(): Operation<Result<string>>;
|
|
141
|
+
}
|
|
142
|
+
/** The operations a provider may refuse outright rather than fail at. */
|
|
143
|
+
export type FilesDeniableOperation = "temporary-directory";
|
|
144
|
+
/**
|
|
145
|
+
* Which contract a provider broke.
|
|
146
|
+
*
|
|
147
|
+
* `authority` — the identity authorizing access is stale, foreign, or gone.
|
|
148
|
+
* `savepoint` — a nested transaction could not be rolled back or released.
|
|
149
|
+
* `protocol` — a handler threw, or returned data no consumer can trust.
|
|
150
|
+
* `teardown` — cleanup failed while the scope was already unwinding.
|
|
151
|
+
*/
|
|
152
|
+
export type FilesInvariantCategory = "authority" | "savepoint" | "protocol" | "teardown";
|
|
153
|
+
/**
|
|
154
|
+
* Infrastructure failure data.
|
|
155
|
+
*
|
|
156
|
+
* Three kinds, each with fixed fields and nothing derived from the condition
|
|
157
|
+
* that produced it. A category is control data for a consumer deciding what to
|
|
158
|
+
* fence, not text: no diagnostic interpolates it.
|
|
159
|
+
*/
|
|
160
|
+
export type FilesFatalData = {
|
|
161
|
+
readonly type: typeof FILES_FATAL;
|
|
162
|
+
readonly kind: "provider-unavailable";
|
|
163
|
+
} | {
|
|
164
|
+
readonly type: typeof FILES_FATAL;
|
|
165
|
+
readonly kind: "operation-denied";
|
|
166
|
+
readonly operation: FilesDeniableOperation;
|
|
167
|
+
} | {
|
|
168
|
+
readonly type: typeof FILES_FATAL;
|
|
169
|
+
readonly kind: "invariant";
|
|
170
|
+
readonly category: FilesInvariantCategory;
|
|
171
|
+
};
|
|
172
|
+
export declare const FILES_PROVIDER_UNAVAILABLE_MESSAGE = "Files provider is not installed";
|
|
173
|
+
export declare const FILES_OPERATION_DENIED_MESSAGE = "Files provider does not support temporary-directory";
|
|
174
|
+
export declare const FILES_INVARIANT_MESSAGE = "Files provider invariant failed";
|
|
175
|
+
/** No Files provider is installed, and there is no host to fall back to. */
|
|
176
|
+
export declare class FilesProviderUnavailableError extends Error {
|
|
177
|
+
readonly data: FilesFatalData;
|
|
178
|
+
constructor();
|
|
179
|
+
}
|
|
180
|
+
/** The installed provider does not implement this operation at all. */
|
|
181
|
+
export declare class FilesOperationDeniedError extends Error {
|
|
182
|
+
readonly data: FilesFatalData;
|
|
183
|
+
constructor(operation: FilesDeniableOperation);
|
|
184
|
+
}
|
|
185
|
+
/** A provider broke its own contract. */
|
|
186
|
+
export declare class FilesInvariantError extends Error {
|
|
187
|
+
readonly data: FilesFatalData;
|
|
188
|
+
constructor(category: FilesInvariantCategory);
|
|
189
|
+
}
|
|
190
|
+
/** An infrastructure failure, recognized structurally rather than by class. */
|
|
191
|
+
export interface FilesFatalFailure extends Error {
|
|
192
|
+
readonly data: FilesFatalData;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The vocabularies, for a provider that reads a failure back out of storage.
|
|
196
|
+
*
|
|
197
|
+
* A transaction-bound provider retains what it refused rather than a serialized
|
|
198
|
+
* error, so restoring one means turning stored text back into the vocabulary.
|
|
199
|
+
* Parsing it here is what keeps one list of reasons and phases: a provider that
|
|
200
|
+
* declared its own copy would be a second list to keep in agreement with this.
|
|
201
|
+
*/
|
|
202
|
+
export declare function parseFilesReason(value: unknown): FilesReason | undefined;
|
|
203
|
+
export declare function parseFilesPhase(value: unknown): FilesPhase | undefined;
|
|
204
|
+
export declare function parseFileWritePhase(value: unknown): FileWritePhase | undefined;
|
|
205
|
+
/**
|
|
206
|
+
* The infrastructure failure data this Error carries, if it carries valid data.
|
|
207
|
+
*
|
|
208
|
+
* Every field is checked, the member count with them, and that the object is
|
|
209
|
+
* frozen: extra keys are not the shape this contract describes, and a mutable
|
|
210
|
+
* one is not the shape a constructor here produces. Accepting either would let
|
|
211
|
+
* a provider smuggle a path or a message through under a recognized tag.
|
|
212
|
+
*/
|
|
213
|
+
export declare function parseFilesFatal(error: unknown): FilesFatalData | undefined;
|
|
214
|
+
/**
|
|
215
|
+
* Whether this failure satisfies the whole public infrastructure-failure
|
|
216
|
+
* contract, not merely the tag.
|
|
217
|
+
*
|
|
218
|
+
* Recognition decides two different things at once, and the second is why this
|
|
219
|
+
* is stricter than `parseFilesFatal`. A recognized failure is **rethrown by
|
|
220
|
+
* identity** — the object that was thrown is the object a fail-stop records —
|
|
221
|
+
* so recognizing one is a decision to let that exact object travel onward. An
|
|
222
|
+
* Error that carries the right data but also a raw platform message, or a cause
|
|
223
|
+
* chain holding an errno and a path, would then carry all of that past the
|
|
224
|
+
* boundary the reason vocabulary exists to hold.
|
|
225
|
+
*
|
|
226
|
+
* So the whole object has to match what a constructor here produces: the fixed
|
|
227
|
+
* name and diagnostic for its kind, frozen structural data with exactly the
|
|
228
|
+
* fields the kind describes, no cause, and no other enumerable member — string
|
|
229
|
+
* or symbol. Anything else is a candidate that fails the contract, and
|
|
230
|
+
* `invokeFiles` replaces it with a fresh invariant rather than preserving it.
|
|
231
|
+
*
|
|
232
|
+
* Structural throughout, so a failure constructed by a separately loaded copy
|
|
233
|
+
* of this package is recognized on exactly the same terms as one constructed
|
|
234
|
+
* here — `instanceof` answers false across two copies, which is the case this
|
|
235
|
+
* has to survive. That is also why the `name` is checked rather than the class:
|
|
236
|
+
* a second copy's constructor is a different function producing the same name.
|
|
237
|
+
*/
|
|
238
|
+
export declare function isFilesFatal(error: unknown): error is FilesFatalFailure;
|
|
239
|
+
/**
|
|
240
|
+
* The infrastructure failure this one is, by identity.
|
|
241
|
+
*
|
|
242
|
+
* The original object comes back rather than a replacement, because a fail-stop
|
|
243
|
+
* that records "the first error" has to record the one that was thrown.
|
|
244
|
+
*/
|
|
245
|
+
export declare function asFilesFatal(error: unknown): FilesFatalFailure | undefined;
|
|
246
|
+
/** Build an ordinary non-write failure. */
|
|
247
|
+
export declare function filesFailure(input: {
|
|
248
|
+
operation: FilesOperation;
|
|
249
|
+
phase: FilesPhase;
|
|
250
|
+
reason: FilesReason;
|
|
251
|
+
}): FilesError;
|
|
252
|
+
/**
|
|
253
|
+
* Build a write failure, refusing any combination a consumer could not read.
|
|
254
|
+
*
|
|
255
|
+
* A write's report is the only place a document learns what became of a file it
|
|
256
|
+
* asked to replace, so an invalid combination is a provider bug rather than a
|
|
257
|
+
* value to pass along and interpret later.
|
|
258
|
+
*/
|
|
259
|
+
export declare function fileWriteFailure(input: {
|
|
260
|
+
phase: FileWritePhase;
|
|
261
|
+
reason?: FilesReason;
|
|
262
|
+
cleanup?: FilesReason;
|
|
263
|
+
}): FilesError;
|
|
264
|
+
/**
|
|
265
|
+
* The non-write failure data this error carries, if it carries valid data.
|
|
266
|
+
*
|
|
267
|
+
* Malformed data is not fatal here — the consumer already has a sentence for
|
|
268
|
+
* "the operation failed" and nothing about a target is at stake — so this
|
|
269
|
+
* simply declines to recognize it.
|
|
270
|
+
*/
|
|
271
|
+
export declare function parseFilesFailure(error: unknown): FilesFailureData | undefined;
|
|
272
|
+
/**
|
|
273
|
+
* The write failure data this error carries, if it carries valid data.
|
|
274
|
+
*
|
|
275
|
+
* Unlike a non-write failure, malformed data here has no safe reading: every
|
|
276
|
+
* sentence a consumer could print makes a claim about whether the file was
|
|
277
|
+
* replaced. A caller treats `undefined` from a write as a protocol invariant
|
|
278
|
+
* rather than inventing a commit state.
|
|
279
|
+
*/
|
|
280
|
+
export declare function parseFileWriteFailure(error: unknown): FileWriteFailureData | undefined;
|
|
281
|
+
/** A successful write's outcome. */
|
|
282
|
+
export declare function fileWriteSuccess(publication: FileWriteSuccess["publication"]): FileWriteSuccess;
|
|
283
|
+
/**
|
|
284
|
+
* The write outcome this value is, if it is a valid one.
|
|
285
|
+
*
|
|
286
|
+
* A malformed success is as untrustworthy as a malformed failure: a provider
|
|
287
|
+
* that cannot describe what it did may not have done it, so a caller treats
|
|
288
|
+
* `undefined` here as a protocol invariant too.
|
|
289
|
+
*/
|
|
290
|
+
export declare function parseFileWriteSuccess(value: unknown): FileWriteSuccess | undefined;
|
|
291
|
+
/**
|
|
292
|
+
* The document filesystem Api.
|
|
293
|
+
*
|
|
294
|
+
* The terminal handler throws for every operation, including `checkFilePath`.
|
|
295
|
+
* A default that reached the host would make an uninstalled provider
|
|
296
|
+
* indistinguishable from an installed one, and the whole point of the boundary
|
|
297
|
+
* is that a workflow run cannot silently touch the caller's filesystem.
|
|
298
|
+
*/
|
|
299
|
+
export declare const Files: Api<FilesHandler>;
|