@phreshos/node 0.1.12 → 0.1.14
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/README.md +31 -36
- package/dist/development-client.d.ts +20 -0
- package/dist/development-client.js +259 -0
- package/dist/events.d.ts +4 -4
- package/dist/events.js +18 -29
- package/dist/main.d.ts +2 -2
- package/dist/main.js +2 -2
- package/dist/process-tree.d.ts +10 -0
- package/dist/process-tree.js +67 -0
- package/dist/program-resources.d.ts +6 -6
- package/dist/program-resources.js +12 -26
- package/dist/project.d.ts +5 -3
- package/dist/project.js +53 -13
- package/dist/representation.d.ts +100 -0
- package/dist/representation.js +331 -0
- package/dist/shell.d.ts +3 -0
- package/dist/shell.js +125 -0
- package/dist/storage.d.ts +6 -2
- package/dist/storage.js +27 -8
- package/dist/system.d.ts +26 -20
- package/dist/system.js +273 -300
- package/dist/traffic.d.ts +6 -6
- package/dist/traffic.js +15 -33
- package/dist/transport.d.ts +20 -10
- package/dist/transport.js +106 -110
- package/dist/uploads.d.ts +4 -1
- package/dist/uploads.js +20 -5
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +15 -0
- package/package.json +4 -2
package/dist/shell.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import ProcessTree from "./process-tree.js";
|
|
4
|
+
/** Execute one command locally while its iterator owns the complete process tree. */
|
|
5
|
+
export default function shell(command, options = {}) {
|
|
6
|
+
return (async function* () {
|
|
7
|
+
const input = validate(command, options);
|
|
8
|
+
const queue = [];
|
|
9
|
+
let wake = null;
|
|
10
|
+
let failure = null;
|
|
11
|
+
let closed = false;
|
|
12
|
+
let treeEnded = false;
|
|
13
|
+
let ending = null;
|
|
14
|
+
let settle;
|
|
15
|
+
const stopped = new Promise(resolve => { settle = resolve; });
|
|
16
|
+
const notify = () => {
|
|
17
|
+
wake?.();
|
|
18
|
+
wake = null;
|
|
19
|
+
};
|
|
20
|
+
const finish = () => {
|
|
21
|
+
if (!closed || !treeEnded || !ending)
|
|
22
|
+
return;
|
|
23
|
+
queue.push({
|
|
24
|
+
event: {
|
|
25
|
+
event: "exited",
|
|
26
|
+
exit: {
|
|
27
|
+
status: ending.signal ? "signaled" : "exited",
|
|
28
|
+
code: ending.code,
|
|
29
|
+
signal: ending.signal
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
settle();
|
|
34
|
+
notify();
|
|
35
|
+
};
|
|
36
|
+
const child = spawn(input.command, {
|
|
37
|
+
shell: true,
|
|
38
|
+
detached: true,
|
|
39
|
+
cwd: input.cwd,
|
|
40
|
+
env: { ...process.env, ...input.env },
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
42
|
+
});
|
|
43
|
+
const tree = new ProcessTree(child, (code, signal) => {
|
|
44
|
+
ending = { code, signal };
|
|
45
|
+
treeEnded = true;
|
|
46
|
+
finish();
|
|
47
|
+
});
|
|
48
|
+
const output = (stream, name) => {
|
|
49
|
+
stream.on("data", chunk => {
|
|
50
|
+
stream.pause();
|
|
51
|
+
queue.push({ event: { event: "output", stream: name, text: String(chunk) }, resume: () => stream.resume() });
|
|
52
|
+
notify();
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const abort = () => {
|
|
56
|
+
failure = options.signal?.reason instanceof Error ? options.signal.reason : new Error("The shell command was cancelled");
|
|
57
|
+
tree.stop();
|
|
58
|
+
notify();
|
|
59
|
+
};
|
|
60
|
+
child.once("spawn", () => {
|
|
61
|
+
if (typeof child.pid !== "number") {
|
|
62
|
+
failure = new Error("The shell command started without a process identity");
|
|
63
|
+
tree.stop();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
queue.push({ event: { event: "started", pid: child.pid } });
|
|
67
|
+
}
|
|
68
|
+
notify();
|
|
69
|
+
});
|
|
70
|
+
child.once("error", error => {
|
|
71
|
+
failure = error;
|
|
72
|
+
settle();
|
|
73
|
+
notify();
|
|
74
|
+
});
|
|
75
|
+
child.once("close", () => {
|
|
76
|
+
closed = true;
|
|
77
|
+
finish();
|
|
78
|
+
});
|
|
79
|
+
output(child.stdout, "stdout");
|
|
80
|
+
output(child.stderr, "stderr");
|
|
81
|
+
if (options.signal?.aborted)
|
|
82
|
+
abort();
|
|
83
|
+
else
|
|
84
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
85
|
+
try {
|
|
86
|
+
while (true) {
|
|
87
|
+
if (failure)
|
|
88
|
+
throw failure;
|
|
89
|
+
const next = queue.shift();
|
|
90
|
+
if (next) {
|
|
91
|
+
yield next.event;
|
|
92
|
+
next.resume?.();
|
|
93
|
+
if (next.event.event === "exited")
|
|
94
|
+
return;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
await new Promise(resolve => { wake = resolve; });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
options.signal?.removeEventListener("abort", abort);
|
|
102
|
+
if (!closed || !treeEnded)
|
|
103
|
+
tree.stop();
|
|
104
|
+
await stopped;
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
}
|
|
108
|
+
function validate(command, options) {
|
|
109
|
+
if (typeof command !== "string" || !command.trim())
|
|
110
|
+
throw new Error("A shell command must be non-empty text");
|
|
111
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
112
|
+
throw new Error("Shell options must be an object");
|
|
113
|
+
if (Object.keys(options).some(key => key !== "cwd" && key !== "env" && key !== "signal"))
|
|
114
|
+
throw new Error("Shell options contain an unknown field");
|
|
115
|
+
if (options.signal !== undefined && !(options.signal instanceof AbortSignal))
|
|
116
|
+
throw new Error("A shell signal must be an AbortSignal");
|
|
117
|
+
const cwd = options.cwd ?? homedir();
|
|
118
|
+
const env = options.env ?? {};
|
|
119
|
+
if (typeof cwd !== "string" || !cwd)
|
|
120
|
+
throw new Error("A shell working directory must be non-empty text");
|
|
121
|
+
if (!env || typeof env !== "object" || Array.isArray(env) || Object.values(env).some(value => typeof value !== "string")) {
|
|
122
|
+
throw new Error("Shell environment values must be text");
|
|
123
|
+
}
|
|
124
|
+
return { command, cwd, env };
|
|
125
|
+
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Storage } from "@phreshos/core";
|
|
2
2
|
/** Create one filesystem implementation bounded beneath a resolved absolute root. */
|
|
3
|
-
export declare function filesystemStorage(source: string | (() => Promise<string>), label: string):
|
|
3
|
+
export declare function filesystemStorage(source: string | (() => Promise<string>), label: string, lifetime?: Lifetime): Storage;
|
|
4
|
+
/** Create native filesystem access entered from one resolved absolute path. */
|
|
5
|
+
export declare function nativeStorage(source: string | (() => Promise<string>), label: string, lifetime?: Lifetime): Storage;
|
|
6
|
+
type Lifetime = () => AbortSignal;
|
|
7
|
+
export {};
|
package/dist/storage.js
CHANGED
|
@@ -1,39 +1,51 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createReadStream, createWriteStream, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
|
-
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
|
|
5
5
|
import { Readable } from "node:stream";
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
/** Create one filesystem implementation bounded beneath a resolved absolute root. */
|
|
8
|
-
export function filesystemStorage(source, label) {
|
|
8
|
+
export function filesystemStorage(source, label, lifetime) {
|
|
9
|
+
return createStorage(source, label, contained, lifetime);
|
|
10
|
+
}
|
|
11
|
+
/** Create native filesystem access entered from one resolved absolute path. */
|
|
12
|
+
export function nativeStorage(source, label, lifetime) {
|
|
13
|
+
return createStorage(source, label, (root, parts) => resolvePath(root, ...parts), lifetime);
|
|
14
|
+
}
|
|
15
|
+
function createStorage(source, label, locate, lifetime) {
|
|
9
16
|
let root = null;
|
|
10
17
|
const resolveRoot = () => {
|
|
18
|
+
active(lifetime);
|
|
11
19
|
if (!root)
|
|
12
20
|
root = Promise.resolve(typeof source === "string" ? source : source()).then(value => {
|
|
21
|
+
active(lifetime);
|
|
13
22
|
if (!isAbsolute(value))
|
|
14
23
|
throw new Error("A Storage root must be absolute");
|
|
15
24
|
return value;
|
|
16
25
|
});
|
|
17
26
|
return root;
|
|
18
27
|
};
|
|
19
|
-
const path = () => resolveRoot();
|
|
20
|
-
const resolve = async (...parts) =>
|
|
28
|
+
const path = async () => await resolveRoot();
|
|
29
|
+
const resolve = async (...parts) => locate(await path(), parts);
|
|
21
30
|
async function stream(...parts) {
|
|
31
|
+
const signal = active(lifetime);
|
|
22
32
|
const destination = await resolve(...parts);
|
|
23
33
|
const found = describe(destination);
|
|
24
34
|
if (!found)
|
|
25
35
|
throw new Error(`There is no ${parts.join("/")} in ${label}`);
|
|
26
36
|
if (found.kind !== "file")
|
|
27
37
|
throw new Error(`${parts.join("/")} is not a file`);
|
|
28
|
-
return Readable.toWeb(createReadStream(destination));
|
|
38
|
+
return Readable.toWeb(createReadStream(destination, { signal }));
|
|
29
39
|
}
|
|
30
40
|
async function write(...args) {
|
|
41
|
+
const signal = active(lifetime);
|
|
31
42
|
const parts = args.slice(0, -1);
|
|
32
43
|
const destination = await resolve(...parts);
|
|
33
44
|
const temporary = join(dirname(destination), `.${randomUUID()}.writing`);
|
|
34
45
|
mkdirSync(dirname(destination), { recursive: true });
|
|
35
46
|
try {
|
|
36
|
-
await pipeline(Readable.fromWeb(content(args.at(-1))), createWriteStream(temporary, { flags: "wx" }));
|
|
47
|
+
await pipeline(Readable.fromWeb(content(args.at(-1))), createWriteStream(temporary, { flags: "wx" }), { signal });
|
|
48
|
+
signal?.throwIfAborted();
|
|
37
49
|
renameSync(temporary, destination);
|
|
38
50
|
}
|
|
39
51
|
catch (error) {
|
|
@@ -49,14 +61,16 @@ export function filesystemStorage(source, label) {
|
|
|
49
61
|
async text(...parts) { return new Response(await stream(...parts)).text(); },
|
|
50
62
|
async json(...parts) { return JSON.parse(await new Response(await stream(...parts)).text()); },
|
|
51
63
|
write,
|
|
52
|
-
async stat(...parts) { return describe(await resolve(...parts)); },
|
|
53
|
-
async list(...parts) { return readdirSync(await resolve(...parts)).sort(); },
|
|
64
|
+
async stat(...parts) { active(lifetime); return describe(await resolve(...parts)); },
|
|
65
|
+
async list(...parts) { active(lifetime); return readdirSync(await resolve(...parts)).sort(); },
|
|
54
66
|
async delete(...parts) {
|
|
67
|
+
active(lifetime);
|
|
55
68
|
if (!parts.length)
|
|
56
69
|
throw new Error("Emptying a place is clear, not delete");
|
|
57
70
|
rmSync(await resolve(...parts), { recursive: true, force: true });
|
|
58
71
|
},
|
|
59
72
|
async clear(...parts) {
|
|
73
|
+
active(lifetime);
|
|
60
74
|
const destination = await resolve(...parts);
|
|
61
75
|
const found = describe(destination);
|
|
62
76
|
if (found && found.kind !== "directory")
|
|
@@ -66,6 +80,11 @@ export function filesystemStorage(source, label) {
|
|
|
66
80
|
}
|
|
67
81
|
};
|
|
68
82
|
}
|
|
83
|
+
function active(lifetime) {
|
|
84
|
+
const signal = lifetime?.();
|
|
85
|
+
signal?.throwIfAborted();
|
|
86
|
+
return signal;
|
|
87
|
+
}
|
|
69
88
|
function content(value) {
|
|
70
89
|
if (value instanceof ReadableStream)
|
|
71
90
|
return value;
|
package/dist/system.d.ts
CHANGED
|
@@ -1,42 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export type ProgramProcessRunOptions =
|
|
3
|
-
export type ProgramProcessRunEvent =
|
|
1
|
+
import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, type ProgramDefinition, type ProgramProcessRunEvent as CoreProgramProcessRunEvent, type ProgramProcessRunOptions as CoreProgramProcessRunOptions, type ServiceKey, type ShellOptions, type System as CoreSystem, type SystemProcess, type SystemProgram, type SystemUploads, type Storage, type WritableAppearance } from "@phreshos/core";
|
|
2
|
+
export type ProgramProcessRunOptions = CoreProgramProcessRunOptions;
|
|
3
|
+
export type ProgramProcessRunEvent = CoreProgramProcessRunEvent;
|
|
4
|
+
type ServiceEndpoint = ServiceKey["endpoint"];
|
|
5
|
+
type ServiceAddress<Endpoint extends ServiceEndpoint> = Omit<ServiceKey, "endpoint"> & Readonly<{
|
|
6
|
+
endpoint: Endpoint;
|
|
7
|
+
}>;
|
|
8
|
+
type ServiceHandle<Endpoint extends ServiceEndpoint, EventsMap extends object, Fallback = unknown> = Endpoint extends "server" ? ServerService<EventsMap, Fallback> : ClientService<EventsMap, Fallback>;
|
|
4
9
|
/** One connected owner-local implementation of the shared System contract. */
|
|
5
10
|
export declare class System implements CoreSystem {
|
|
6
|
-
readonly storage:
|
|
11
|
+
readonly storage: Storage;
|
|
7
12
|
readonly appearance: WritableAppearance;
|
|
8
13
|
readonly program: SystemProgram;
|
|
9
14
|
readonly process: SystemProcess;
|
|
10
15
|
readonly uploads: SystemUploads;
|
|
16
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
17
|
+
websocket(url: string | URL, protocols?: string | string[]): Promise<WebSocket>;
|
|
18
|
+
shell(command: string, options?: ShellOptions): AsyncGenerator<import("@phreshos/core").ShellEvent, void, void>;
|
|
11
19
|
private constructor();
|
|
12
20
|
/** Connect to the System selected by argument, environment, or owner default. */
|
|
13
21
|
static connect(home?: string): Promise<System>;
|
|
14
22
|
/** Atomically replace one runtime Program without touching its installed form. */
|
|
15
|
-
forceCreateProgram(source: ProgramDefinition | string): Promise<
|
|
23
|
+
forceCreateProgram(source: ProgramDefinition | string): Promise<Program>;
|
|
16
24
|
/** Close this owner connection and abort every attached operation it owns. */
|
|
17
25
|
disconnect(): Promise<void>;
|
|
18
|
-
service<
|
|
19
|
-
|
|
20
|
-
}):
|
|
21
|
-
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceKey & {
|
|
22
|
-
endpoint: "client";
|
|
23
|
-
}): ClientService<EventsMap, Fallback>;
|
|
26
|
+
service<Endpoint extends ServiceEndpoint>(key: ServiceAddress<Endpoint>): ServiceHandle<Endpoint, {}>;
|
|
27
|
+
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"server">): ServerService<EventsMap, Fallback>;
|
|
28
|
+
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"client">): ClientService<EventsMap, Fallback>;
|
|
24
29
|
}
|
|
25
|
-
/** Node
|
|
30
|
+
/** Node SDK handle for a Service provided by a Server Endpoint. */
|
|
26
31
|
export declare class ServerService<EventsMap extends object = {}, Fallback = unknown> extends CoreServerService<EventsMap, Fallback> {
|
|
27
32
|
protected constructor();
|
|
28
33
|
}
|
|
29
|
-
/** Node
|
|
34
|
+
/** Node SDK handle for a Service provided by a Client Endpoint. */
|
|
30
35
|
export declare class ClientService<EventsMap extends object = {}, Fallback = unknown> extends CoreClientService<EventsMap, Fallback> {
|
|
31
36
|
protected constructor();
|
|
32
37
|
}
|
|
33
|
-
export type Program =
|
|
38
|
+
export type Program = CoreProgram;
|
|
34
39
|
export declare const Program: typeof CoreProgram;
|
|
35
|
-
export type Process =
|
|
40
|
+
export type Process = CoreProcess;
|
|
36
41
|
export declare const Process: typeof CoreProcess;
|
|
37
|
-
export type Endpoint<EventsMap extends object = {}, Fallback = unknown> =
|
|
42
|
+
export type Endpoint<EventsMap extends object = {}, Fallback = unknown> = CoreEndpoint<EventsMap, Fallback>;
|
|
38
43
|
export declare const Endpoint: typeof CoreEndpoint;
|
|
39
|
-
export type
|
|
40
|
-
export declare const
|
|
41
|
-
export type
|
|
42
|
-
export declare const
|
|
44
|
+
export type ServerEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreServerEndpoint<EventsMap, Fallback>;
|
|
45
|
+
export declare const ServerEndpoint: typeof CoreServerEndpoint;
|
|
46
|
+
export type ClientEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreClientEndpoint<EventsMap, Fallback>;
|
|
47
|
+
export declare const ClientEndpoint: typeof CoreClientEndpoint;
|
|
48
|
+
export {};
|