@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host `API.Files` provider — document filesystem access in the caller's
|
|
3
|
+
* own filesystem.
|
|
4
|
+
*
|
|
5
|
+
* This is what `xmd run` installs. A document's relative path is resolved
|
|
6
|
+
* against the contextual working directory and used as an ordinary host path,
|
|
7
|
+
* so a document can hand a file to a tool the caller already has. Everything
|
|
8
|
+
* below is built on the low-level `API.Fs`, which is deliberate: a host that
|
|
9
|
+
* already wraps `API.Fs` to observe or sandbox the engine's own file access
|
|
10
|
+
* keeps seeing a document's access on the same terms.
|
|
11
|
+
*
|
|
12
|
+
* ## What containment means here
|
|
13
|
+
*
|
|
14
|
+
* Access is confined to the contextual directory, judged against the filesystem
|
|
15
|
+
* as this adapter observes it. An empty path, an absolute path, and a lexical
|
|
16
|
+
* `..` escape are refused without touching the filesystem at all; a symlink
|
|
17
|
+
* leading out is refused once resolution can see it.
|
|
18
|
+
*
|
|
19
|
+
* That is sound **while the host pathname namespace is stable**, and every
|
|
20
|
+
* guarantee here is stated on that basis. It is not a sandbox. Another process
|
|
21
|
+
* can replace a directory, symlink, junction, or reparse point between the
|
|
22
|
+
* moment this adapter observes a path and the moment it uses one, and nothing
|
|
23
|
+
* available on the shipped runtimes closes that window without a native
|
|
24
|
+
* dependency. What is contained is the document's own children — the case a
|
|
25
|
+
* document controls — because resolution is deferred until after they run.
|
|
26
|
+
*
|
|
27
|
+
* ## Writes
|
|
28
|
+
*
|
|
29
|
+
* A write goes through a sibling temporary file and a rename. The rename is the
|
|
30
|
+
* commit point: everything before it can fail or be cancelled with the previous
|
|
31
|
+
* file untouched, and once it begins the target holds the complete old file or
|
|
32
|
+
* the complete new one, never a partial write. It is a commit rather than a
|
|
33
|
+
* transaction — a rename that returned is not undone by a later cancellation.
|
|
34
|
+
* The temporary also closes the one hole resolution cannot: a dangling symlink
|
|
35
|
+
* has nothing to resolve, and `rename` replaces the link rather than following
|
|
36
|
+
* it wherever it points.
|
|
37
|
+
*
|
|
38
|
+
* ## What crosses the boundary
|
|
39
|
+
*
|
|
40
|
+
* Nothing from a caught platform error. An errno code *selects* a
|
|
41
|
+
* `FilesReason`, and the reason is all the consumer receives — no message, no
|
|
42
|
+
* code, no resolved path, no temporary path, and no symlink target. A platform
|
|
43
|
+
* error names the path it failed on, and for a write that path can be a
|
|
44
|
+
* temporary the document never chose.
|
|
45
|
+
*/
|
|
46
|
+
import type { Operation } from "effection";
|
|
47
|
+
import type { FilesHandler } from "./files.js";
|
|
48
|
+
/**
|
|
49
|
+
* A private step a host operation is about to take.
|
|
50
|
+
*
|
|
51
|
+
* Test-only. Production entrypoints install the adapter without one, so this is
|
|
52
|
+
* neither global state nor a capability: an observer can watch, and the point of
|
|
53
|
+
* watching is to replace part of the tree between an observation and the call
|
|
54
|
+
* that follows it, which is how the stable-namespace limitation is made
|
|
55
|
+
* observable rather than merely stated.
|
|
56
|
+
*/
|
|
57
|
+
export interface HostFilesEvent {
|
|
58
|
+
readonly operation: "read" | "write" | "glob";
|
|
59
|
+
readonly phase: "target" | "access" | "parents" | "temporary" | "commit" | "cleanup" | "read-dir";
|
|
60
|
+
}
|
|
61
|
+
/** Synchronous, so nothing can run between the observation and the call it precedes. */
|
|
62
|
+
export type HostFilesObserver = (event: HostFilesEvent) => void;
|
|
63
|
+
export interface HostFilesOptions {
|
|
64
|
+
readonly observe?: HostFilesObserver;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build a host provider.
|
|
68
|
+
*
|
|
69
|
+
* Exported so a test can drive one operation directly; entrypoints install it
|
|
70
|
+
* with {@link useHostFiles}.
|
|
71
|
+
*/
|
|
72
|
+
export declare function hostFilesHandler(options?: HostFilesOptions): FilesHandler;
|
|
73
|
+
/**
|
|
74
|
+
* Install the host provider beneath ordinary middleware.
|
|
75
|
+
*
|
|
76
|
+
* `at: "min"` is what lets a host wrap document filesystem access without
|
|
77
|
+
* replacing it — middleware installed later sees these operations and can
|
|
78
|
+
* delegate to them.
|
|
79
|
+
*/
|
|
80
|
+
export declare function useHostFiles(options?: HostFilesOptions): Operation<void>;
|
package/types/mod.d.ts
CHANGED
|
@@ -5,22 +5,32 @@
|
|
|
5
5
|
* `API` is available for middleware (`.around()`).
|
|
6
6
|
* For normal calls, import operations directly.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Seven domain APIs:
|
|
9
9
|
* - `API.Process` — subprocess execution (`exec`)
|
|
10
|
-
* - `API.Fs` — filesystem (`readTextFile`, `writeTextFile`,
|
|
11
|
-
* `realpath`, `ensureDir`, `rename`, `remove`)
|
|
10
|
+
* - `API.Fs` — the low-level host filesystem (`readTextFile`, `writeTextFile`,
|
|
11
|
+
* `stat`, `glob`, `realpath`, `ensureDir`, `rename`, `remove`)
|
|
12
|
+
* - `API.Files` — document filesystem access as whole semantic operations,
|
|
13
|
+
* with no host default. `useHostFiles()` installs the host provider.
|
|
12
14
|
* - `API.Fetch` — HTTP requests (`fetch`)
|
|
13
15
|
* - `API.Env` — the host: variables, platform info, the command that invokes
|
|
14
16
|
* this xmd, and eval-block compilation
|
|
15
17
|
* (`cwd`, `env`, `platform`, `command`, `compile`)
|
|
16
|
-
* - `
|
|
18
|
+
* - `API.Service` — scoped attached service startup (`startService`)
|
|
19
|
+
* - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`)
|
|
17
20
|
*
|
|
18
21
|
* See `apis.ts` for architecture rationale.
|
|
19
22
|
* See `@executablemd/runtime/test` for composable test stubs.
|
|
20
23
|
*/
|
|
21
24
|
export { API } from "./apis.js";
|
|
22
|
-
export { exec, readTextFile, writeTextFile, stat, glob, realpath, ensureDir, rename, remove, fetch, cwd, env, platform, command, compile, } from "./apis.js";
|
|
25
|
+
export { exec, readTextFile, writeTextFile, stat, glob, realpath, ensureDir, rename, remove, fetch, cwd, env, platform, command, compile, useQuietProcessOutput, } from "./apis.js";
|
|
23
26
|
export type { EvalBlock, ResponseHeaders, RuntimeFetchResponse, StatResult } from "./apis.js";
|
|
24
|
-
export {
|
|
25
|
-
export {
|
|
27
|
+
export { Service, SERVICE_HOSTNAME, SERVICE_READY_PREFIX, ServiceProcessExitBeforeReadyError, ServiceProtocolDuplicateError, ServiceProtocolHostnameMismatchError, ServiceProtocolIncompatibleError, ServiceProtocolMalformedError, ServiceProtocolTokenMismatchError, ServiceProviderError, ServiceStartupTimeoutError, ServiceTeardownError, ServiceUnexpectedExitError, parseServiceReadyRecord, startService, } from "./service.js";
|
|
28
|
+
export type { ServiceEndpoint, ServiceHandler, ServiceAttachment, ServiceStartOptions, } from "./service.js";
|
|
29
|
+
export { Config, timeout, timeoutExec, timeoutFetch } from "./config.js";
|
|
26
30
|
export type { ConfigApi } from "./config.js";
|
|
31
|
+
export { asDuration, durationError, parseDuration } from "./duration.js";
|
|
32
|
+
export type { ProcessExecOptions, ProcessOutcome } from "./apis.js";
|
|
33
|
+
export { asFilesFatal, FILES_ERROR, FILES_ERROR_MESSAGE, FILES_FATAL, FILES_INVARIANT_MESSAGE, FILES_OPERATION_DENIED_MESSAGE, FILES_PROVIDER_UNAVAILABLE_MESSAGE, FILES_WRITE_SUCCESS, Files, FilesError, FilesInvariantError, FilesOperationDeniedError, FilesProviderUnavailableError, fileWriteFailure, fileWriteSuccess, filesFailure, isFilesFatal, parseFilesPhase, parseFilesReason, parseFileWriteFailure, parseFileWritePhase, parseFileWriteSuccess, parseFilesFailure, parseFilesFatal, } from "./files.js";
|
|
34
|
+
export type { FilePathInput, FilesDeniableOperation, FilesErrorData, FilesFailureData, FilesFatalData, FilesFatalFailure, FilesHandler, FilesInvariantCategory, FilesOperation, FilesPhase, FilesReason, FileWriteFailureData, FileWriteInput, FileWritePhase, FileWriteSuccess, FileWriteTarget, GlobInput, } from "./files.js";
|
|
35
|
+
export { hostFilesHandler, useHostFiles } from "./host-files.js";
|
|
36
|
+
export type { HostFilesEvent, HostFilesObserver, HostFilesOptions } from "./host-files.js";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-neutral attached-service lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* The shared runtime owns the XMD service handshake shape and validation. A
|
|
5
|
+
* runtime-named host adapter supplies process startup through `API.Service`
|
|
6
|
+
* middleware.
|
|
7
|
+
*/
|
|
8
|
+
import { type Api, type Operations } from "@effectionx/context-api";
|
|
9
|
+
import type { Operation } from "effection";
|
|
10
|
+
export declare const SERVICE_READY_PREFIX = "XMD_SERVICE_READY:";
|
|
11
|
+
export declare const SERVICE_HOSTNAME = "127.0.0.1";
|
|
12
|
+
export interface ServiceEndpoint {
|
|
13
|
+
readonly hostname: string;
|
|
14
|
+
readonly port: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ServiceStartOptions {
|
|
17
|
+
readonly command: string;
|
|
18
|
+
readonly cwd?: string;
|
|
19
|
+
readonly startupTimeout?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ServiceAttachment {
|
|
22
|
+
readonly endpoint: Readonly<ServiceEndpoint>;
|
|
23
|
+
}
|
|
24
|
+
export interface ServiceHandler {
|
|
25
|
+
start(options: ServiceStartOptions): Operation<ServiceAttachment>;
|
|
26
|
+
}
|
|
27
|
+
export declare class ServiceProviderError extends Error {
|
|
28
|
+
name: string;
|
|
29
|
+
constructor();
|
|
30
|
+
}
|
|
31
|
+
export declare class ServiceProtocolMalformedError extends Error {
|
|
32
|
+
name: string;
|
|
33
|
+
constructor();
|
|
34
|
+
}
|
|
35
|
+
export declare class ServiceProtocolIncompatibleError extends Error {
|
|
36
|
+
name: string;
|
|
37
|
+
constructor();
|
|
38
|
+
}
|
|
39
|
+
export declare class ServiceProtocolTokenMismatchError extends Error {
|
|
40
|
+
name: string;
|
|
41
|
+
constructor();
|
|
42
|
+
}
|
|
43
|
+
export declare class ServiceProtocolHostnameMismatchError extends Error {
|
|
44
|
+
name: string;
|
|
45
|
+
constructor();
|
|
46
|
+
}
|
|
47
|
+
export declare class ServiceProtocolDuplicateError extends Error {
|
|
48
|
+
name: string;
|
|
49
|
+
constructor();
|
|
50
|
+
}
|
|
51
|
+
export declare class ServiceStartupTimeoutError extends Error {
|
|
52
|
+
name: string;
|
|
53
|
+
constructor(timeout: number);
|
|
54
|
+
}
|
|
55
|
+
interface ServiceExitStatus {
|
|
56
|
+
readonly code?: number;
|
|
57
|
+
readonly signal?: string;
|
|
58
|
+
}
|
|
59
|
+
export declare class ServiceProcessExitBeforeReadyError extends Error {
|
|
60
|
+
name: string;
|
|
61
|
+
constructor(status: ServiceExitStatus);
|
|
62
|
+
}
|
|
63
|
+
export declare class ServiceUnexpectedExitError extends Error {
|
|
64
|
+
name: string;
|
|
65
|
+
constructor(status: ServiceExitStatus);
|
|
66
|
+
}
|
|
67
|
+
export declare class ServiceTeardownError extends Error {
|
|
68
|
+
name: string;
|
|
69
|
+
constructor(options?: {
|
|
70
|
+
cause?: unknown;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Parse and authenticate one prefix-stripped v1 handshake payload. */
|
|
74
|
+
export declare function parseServiceReadyRecord(payload: string, expectedToken: string): ServiceEndpoint;
|
|
75
|
+
export declare const Service: Api<ServiceHandler>;
|
|
76
|
+
export declare const startService: Operations<ServiceHandler>["start"];
|
|
77
|
+
export {};
|
package/types/test/mod.d.ts
CHANGED
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
* - `useStubFs(files)` — in-memory filesystem
|
|
7
7
|
* - `useEchoExec()` — simple echo-based exec
|
|
8
8
|
* - `useFailingExec(exitCode, stderr)` — always-failing exec
|
|
9
|
+
* - `useStubService(endpoint)` — scoped provider-neutral service attachment
|
|
9
10
|
*/
|
|
10
|
-
export { useStubFs, useEchoExec, useFailingExec } from "./stubs.js";
|
|
11
|
+
export { useStubFs, useEchoExec, useFailingExec, useStubService } from "./stubs.js";
|
package/types/test/stubs.d.ts
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
import type { Operation } from "effection";
|
|
23
|
+
import type { ServiceEndpoint } from "../service.js";
|
|
24
|
+
/** Install a provider-neutral scoped service attachment stub. */
|
|
25
|
+
export declare function useStubService(endpoint: ServiceEndpoint): Operation<void>;
|
|
23
26
|
/**
|
|
24
27
|
* Install an in-memory filesystem stub.
|
|
25
28
|
*
|
|
@@ -40,6 +43,12 @@ export declare function useStubFs(files: Record<string, string>): Operation<void
|
|
|
40
43
|
*
|
|
41
44
|
* Recognizes `bash -c "echo ..."` and returns the echo'd text as stdout.
|
|
42
45
|
* All other commands return the script text as stdout with exit code 0.
|
|
46
|
+
*
|
|
47
|
+
* It answers like a real child: the text is written to the stdio chain as it
|
|
48
|
+
* "arrives", so whatever encloses the call sees it the way it would see a
|
|
49
|
+
* child's, and it is retained in the outcome only when the caller asked
|
|
50
|
+
* for retention. A stub that always returned strings would let a document
|
|
51
|
+
* render output the contract says was already displayed.
|
|
43
52
|
*/
|
|
44
53
|
export declare function useEchoExec(): Operation<void>;
|
|
45
54
|
/**
|
package/esm/find-free-port.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* findFreePort — find an available TCP port using the OS.
|
|
3
|
-
*/
|
|
4
|
-
import { race } from "effection";
|
|
5
|
-
import { once } from "@effectionx/node";
|
|
6
|
-
import { createServer } from "node:net";
|
|
7
|
-
/**
|
|
8
|
-
* Find an available TCP port by binding to port 0 and reading the
|
|
9
|
-
* OS-assigned port number.
|
|
10
|
-
*/
|
|
11
|
-
export function* findFreePort() {
|
|
12
|
-
const server = createServer();
|
|
13
|
-
const listening = once(server, "listening");
|
|
14
|
-
const error = once(server, "error");
|
|
15
|
-
server.listen(0);
|
|
16
|
-
try {
|
|
17
|
-
const rethrowError = {
|
|
18
|
-
*[Symbol.iterator]() {
|
|
19
|
-
const [err] = yield* error;
|
|
20
|
-
throw err;
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
yield* race([listening, rethrowError]);
|
|
24
|
-
const addr = server.address();
|
|
25
|
-
if (!addr || typeof addr !== "object") {
|
|
26
|
-
throw new Error("findFreePort: unexpected address format");
|
|
27
|
-
}
|
|
28
|
-
return addr.port;
|
|
29
|
-
}
|
|
30
|
-
finally {
|
|
31
|
-
server.close();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* findFreePort — find an available TCP port using the OS.
|
|
3
|
-
*/
|
|
4
|
-
import type { Operation } from "effection";
|
|
5
|
-
/**
|
|
6
|
-
* Find an available TCP port by binding to port 0 and reading the
|
|
7
|
-
* OS-assigned port number.
|
|
8
|
-
*/
|
|
9
|
-
export declare function findFreePort(): Operation<number>;
|