@phreshos/node 0.1.12 → 0.1.13
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 +5 -0
- package/dist/development-client.d.ts +20 -0
- package/dist/development-client.js +259 -0
- package/dist/main.d.ts +2 -2
- package/dist/main.js +1 -1
- package/dist/process-tree.d.ts +10 -0
- package/dist/process-tree.js +67 -0
- package/dist/program-resources.d.ts +3 -3
- package/dist/program-resources.js +10 -9
- package/dist/project.d.ts +5 -3
- package/dist/project.js +53 -13
- 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 +25 -20
- package/dist/system.js +54 -39
- package/dist/traffic.d.ts +2 -2
- package/dist/traffic.js +1 -1
- package/dist/transport.d.ts +2 -2
- package/dist/transport.js +18 -4
- package/dist/uploads.d.ts +4 -1
- package/dist/uploads.js +20 -5
- package/package.json +2 -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,47 @@
|
|
|
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
|
+
shell(command: string, options?: ShellOptions): AsyncGenerator<import("@phreshos/core").ShellEvent, void, void>;
|
|
11
18
|
private constructor();
|
|
12
19
|
/** Connect to the System selected by argument, environment, or owner default. */
|
|
13
20
|
static connect(home?: string): Promise<System>;
|
|
14
21
|
/** Atomically replace one runtime Program without touching its installed form. */
|
|
15
|
-
forceCreateProgram(source: ProgramDefinition | string): Promise<
|
|
22
|
+
forceCreateProgram(source: ProgramDefinition | string): Promise<Program>;
|
|
16
23
|
/** Close this owner connection and abort every attached operation it owns. */
|
|
17
24
|
disconnect(): Promise<void>;
|
|
18
|
-
service<
|
|
19
|
-
|
|
20
|
-
}):
|
|
21
|
-
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceKey & {
|
|
22
|
-
endpoint: "client";
|
|
23
|
-
}): ClientService<EventsMap, Fallback>;
|
|
25
|
+
service<Endpoint extends ServiceEndpoint>(key: ServiceAddress<Endpoint>): ServiceHandle<Endpoint, {}>;
|
|
26
|
+
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"server">): ServerService<EventsMap, Fallback>;
|
|
27
|
+
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"client">): ClientService<EventsMap, Fallback>;
|
|
24
28
|
}
|
|
25
|
-
/** Node
|
|
29
|
+
/** Node SDK handle for a Service provided by a Server Endpoint. */
|
|
26
30
|
export declare class ServerService<EventsMap extends object = {}, Fallback = unknown> extends CoreServerService<EventsMap, Fallback> {
|
|
27
31
|
protected constructor();
|
|
28
32
|
}
|
|
29
|
-
/** Node
|
|
33
|
+
/** Node SDK handle for a Service provided by a Client Endpoint. */
|
|
30
34
|
export declare class ClientService<EventsMap extends object = {}, Fallback = unknown> extends CoreClientService<EventsMap, Fallback> {
|
|
31
35
|
protected constructor();
|
|
32
36
|
}
|
|
33
|
-
export type Program =
|
|
37
|
+
export type Program = CoreProgram;
|
|
34
38
|
export declare const Program: typeof CoreProgram;
|
|
35
|
-
export type Process =
|
|
39
|
+
export type Process = CoreProcess;
|
|
36
40
|
export declare const Process: typeof CoreProcess;
|
|
37
|
-
export type Endpoint<EventsMap extends object = {}, Fallback = unknown> =
|
|
41
|
+
export type Endpoint<EventsMap extends object = {}, Fallback = unknown> = CoreEndpoint<EventsMap, Fallback>;
|
|
38
42
|
export declare const Endpoint: typeof CoreEndpoint;
|
|
39
|
-
export type
|
|
40
|
-
export declare const
|
|
41
|
-
export type
|
|
42
|
-
export declare const
|
|
43
|
+
export type ServerEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreServerEndpoint<EventsMap, Fallback>;
|
|
44
|
+
export declare const ServerEndpoint: typeof CoreServerEndpoint;
|
|
45
|
+
export type ClientEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreClientEndpoint<EventsMap, Fallback>;
|
|
46
|
+
export declare const ClientEndpoint: typeof CoreClientEndpoint;
|
|
47
|
+
export {};
|
package/dist/system.js
CHANGED
|
@@ -1,39 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, isServiceKey } from "@phreshos/core";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { gatewayAddress } from "./address.js";
|
|
4
4
|
import Events from "./events.js";
|
|
5
5
|
import HandleRegistry from "./handle-registry.js";
|
|
6
6
|
import { resolveHome } from "./home.js";
|
|
7
|
-
import { filesystemStorage } from "./storage.js";
|
|
8
|
-
import {
|
|
7
|
+
import { filesystemStorage, nativeStorage } from "./storage.js";
|
|
8
|
+
import { programPermissions, programSql, programStore } from "./program-resources.js";
|
|
9
9
|
import { EndpointTrafficHandle, ServerTrafficHandle } from "./traffic.js";
|
|
10
|
-
import { openConnection, request,
|
|
10
|
+
import { openConnection, request, stream } from "./transport.js";
|
|
11
11
|
import Uploads from "./uploads.js";
|
|
12
|
+
import shell from "./shell.js";
|
|
12
13
|
const systems = new WeakMap();
|
|
13
14
|
const ProgramBase = CoreProgram;
|
|
14
15
|
const ProcessBase = CoreProcess;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
16
|
+
const ServerEndpointBase = CoreServerEndpoint;
|
|
17
|
+
const ClientEndpointBase = CoreClientEndpoint;
|
|
17
18
|
/** One connected owner-local implementation of the shared System contract. */
|
|
18
19
|
export class System {
|
|
19
|
-
storage
|
|
20
|
+
storage;
|
|
20
21
|
appearance;
|
|
21
22
|
program;
|
|
22
23
|
process;
|
|
23
24
|
uploads;
|
|
25
|
+
async fetch(input, init) {
|
|
26
|
+
const request = new Request(input, init);
|
|
27
|
+
return await fetch(request, { signal: connectedSignal(this, request.signal) });
|
|
28
|
+
}
|
|
29
|
+
async *shell(command, options = {}) {
|
|
30
|
+
yield* shell(command, { ...options, signal: connectedSignal(this, options.signal) });
|
|
31
|
+
}
|
|
24
32
|
constructor(address, connection) {
|
|
25
33
|
const lifetime = new AbortController();
|
|
26
34
|
const handles = new HandleRegistry();
|
|
27
35
|
const transport = {
|
|
28
36
|
control: (value, signal) => request(address, "system", value, connectedSignal(this, signal)),
|
|
29
37
|
api: (value, signal) => request(address, "api", value, connectedSignal(this, signal)),
|
|
30
|
-
|
|
38
|
+
stream: (target, value, signal) => stream(address, target, value, connectedSignal(this, signal))
|
|
31
39
|
};
|
|
32
40
|
systems.set(this, { address, connection, handles, lifetime, transport, closed: false });
|
|
41
|
+
connection.once("close", () => closeSystem(this, new Error("This System connection is closed")));
|
|
42
|
+
this.storage = nativeStorage(homedir(), "the native filesystem", () => connectedSignal(this));
|
|
33
43
|
this.appearance = new SystemAppearance(transport);
|
|
34
44
|
this.program = new ProgramRegistry(this);
|
|
35
45
|
this.process = new ProcessRegistry(this);
|
|
36
|
-
this.uploads = new Uploads(value => transport.api(value));
|
|
46
|
+
this.uploads = new Uploads(value => transport.api(value), () => connectedSignal(this));
|
|
37
47
|
}
|
|
38
48
|
/** Connect to the System selected by argument, environment, or owner default. */
|
|
39
49
|
static async connect(home) {
|
|
@@ -44,7 +54,7 @@ export class System {
|
|
|
44
54
|
/** Atomically replace one runtime Program without touching its installed form. */
|
|
45
55
|
async forceCreateProgram(source) {
|
|
46
56
|
requireConnected(this);
|
|
47
|
-
for await (const event of transport(this).
|
|
57
|
+
for await (const event of transport(this).stream("program", { word: "force-create", program: source })) {
|
|
48
58
|
if (event.event === "created")
|
|
49
59
|
return programHandle(this, required(event.program));
|
|
50
60
|
}
|
|
@@ -52,13 +62,7 @@ export class System {
|
|
|
52
62
|
}
|
|
53
63
|
/** Close this owner connection and abort every attached operation it owns. */
|
|
54
64
|
async disconnect() {
|
|
55
|
-
|
|
56
|
-
if (state.closed)
|
|
57
|
-
return;
|
|
58
|
-
state.closed = true;
|
|
59
|
-
state.lifetime.abort(new Error("This System connection is closed"));
|
|
60
|
-
state.connection.destroy();
|
|
61
|
-
state.handles.clear();
|
|
65
|
+
closeSystem(this, new Error("This System connection is closed"));
|
|
62
66
|
}
|
|
63
67
|
service(key) {
|
|
64
68
|
requireConnected(this);
|
|
@@ -81,6 +85,15 @@ function systemState(system) {
|
|
|
81
85
|
throw new Error("Unknown System connection");
|
|
82
86
|
return state;
|
|
83
87
|
}
|
|
88
|
+
function closeSystem(system, reason) {
|
|
89
|
+
const state = systemState(system);
|
|
90
|
+
if (state.closed)
|
|
91
|
+
return;
|
|
92
|
+
state.closed = true;
|
|
93
|
+
state.lifetime.abort(reason);
|
|
94
|
+
state.connection.destroy();
|
|
95
|
+
state.handles.clear();
|
|
96
|
+
}
|
|
84
97
|
function requireConnected(system) {
|
|
85
98
|
if (systemState(system).closed)
|
|
86
99
|
throw new Error("This System connection is closed");
|
|
@@ -149,7 +162,7 @@ class ProgramRegistry extends Events {
|
|
|
149
162
|
}
|
|
150
163
|
}
|
|
151
164
|
async create(source) {
|
|
152
|
-
for await (const event of transport(this.system).
|
|
165
|
+
for await (const event of transport(this.system).stream("program", { word: "create", program: source })) {
|
|
153
166
|
if (event.event === "created")
|
|
154
167
|
return programHandle(this.system, required(event.program));
|
|
155
168
|
}
|
|
@@ -173,9 +186,9 @@ class ProgramHandle extends ProgramBase {
|
|
|
173
186
|
store;
|
|
174
187
|
logs;
|
|
175
188
|
database;
|
|
176
|
-
permission;
|
|
177
189
|
process;
|
|
178
190
|
startup;
|
|
191
|
+
permissions;
|
|
179
192
|
snapshot;
|
|
180
193
|
constructor(system, snapshot) {
|
|
181
194
|
super();
|
|
@@ -188,16 +201,17 @@ class ProgramHandle extends ProgramBase {
|
|
|
188
201
|
capability: "program", operation: "wait", handle: address, event, timeout
|
|
189
202
|
}, signal)));
|
|
190
203
|
const request = (value) => transport(system).api(value);
|
|
191
|
-
this.data = filesystemStorage(() => programStoragePath(system, address, "data"), `Program "${this.identity}" data
|
|
192
|
-
this.cache = filesystemStorage(() => programStoragePath(system, address, "cache"), `Program "${this.identity}" cache
|
|
204
|
+
this.data = filesystemStorage(() => programStoragePath(system, address, "data"), `Program "${this.identity}" data`, () => connectedSignal(system));
|
|
205
|
+
this.cache = filesystemStorage(() => programStoragePath(system, address, "cache"), `Program "${this.identity}" cache`, () => connectedSignal(system));
|
|
193
206
|
this.store = programStore(request, address);
|
|
194
207
|
this.logs = programSql(request, address, "logs");
|
|
195
208
|
this.database = programSql(request, address, "database");
|
|
196
|
-
this.permission = programPermission(request, address);
|
|
197
209
|
this.process = new ProgramProcesses(system, this);
|
|
198
210
|
this.startup = new ProgramStartup(system, this);
|
|
211
|
+
this.permissions = programPermissions(request, address);
|
|
199
212
|
}
|
|
200
213
|
get name() { return this.snapshot.name; }
|
|
214
|
+
get assetId() { return this.snapshot.assetId; }
|
|
201
215
|
get version() { return this.snapshot.version; }
|
|
202
216
|
get description() { return this.snapshot.description; }
|
|
203
217
|
get hasAgent() { return this.snapshot.hasAgent; }
|
|
@@ -215,7 +229,8 @@ class ProgramHandle extends ProgramBase {
|
|
|
215
229
|
size: this.snapshot.client.size,
|
|
216
230
|
position: this.snapshot.client.position,
|
|
217
231
|
layer: this.snapshot.client.layer,
|
|
218
|
-
minimize: this.snapshot.client.minimize
|
|
232
|
+
minimize: this.snapshot.client.minimize,
|
|
233
|
+
permissions: Object.freeze(Object.fromEntries(Object.entries(this.snapshot.client.permissions).map(([name, values]) => [name, Object.freeze([...values])])))
|
|
219
234
|
}) : null;
|
|
220
235
|
}
|
|
221
236
|
update(snapshot) {
|
|
@@ -236,7 +251,7 @@ class ProgramHandle extends ProgramBase {
|
|
|
236
251
|
return typeof value === "string" ? value : null;
|
|
237
252
|
}
|
|
238
253
|
async installed() {
|
|
239
|
-
for await (const event of transport(this.system).
|
|
254
|
+
for await (const event of transport(this.system).stream("program", { word: "installed", handle: this.address() })) {
|
|
240
255
|
if (event.event === "installedState")
|
|
241
256
|
return event.installed === true;
|
|
242
257
|
}
|
|
@@ -245,14 +260,14 @@ class ProgramHandle extends ProgramBase {
|
|
|
245
260
|
install() { return command(this.system, { word: "install-existing", handle: this.address() }); }
|
|
246
261
|
uninstall(everything = false) { return command(this.system, { word: "uninstall-existing", handle: this.address(), everything }); }
|
|
247
262
|
async fork(identity) {
|
|
248
|
-
for await (const event of transport(this.system).
|
|
263
|
+
for await (const event of transport(this.system).stream("program", { word: "fork", handle: this.address(), identity })) {
|
|
249
264
|
if (event.event === "created")
|
|
250
265
|
return programHandle(this.system, required(event.program));
|
|
251
266
|
}
|
|
252
267
|
throw new Error("The System did not confirm the forked Program");
|
|
253
268
|
}
|
|
254
269
|
async forget() {
|
|
255
|
-
for await (const _event of transport(this.system).
|
|
270
|
+
for await (const _event of transport(this.system).stream("program", { word: "forget", handle: this.address() })) { /* consume completion */ }
|
|
256
271
|
}
|
|
257
272
|
address() { return Object.freeze({ identity: this.identity, reference: this.reference }); }
|
|
258
273
|
}
|
|
@@ -264,7 +279,7 @@ class ProgramStartup {
|
|
|
264
279
|
this.program = program;
|
|
265
280
|
}
|
|
266
281
|
async get() {
|
|
267
|
-
for await (const event of transport(this.system).
|
|
282
|
+
for await (const event of transport(this.system).stream("program", {
|
|
268
283
|
word: "startup", handle: this.program.address(), operation: "get"
|
|
269
284
|
})) {
|
|
270
285
|
if (event.event === "startup")
|
|
@@ -279,7 +294,7 @@ class ProgramStartup {
|
|
|
279
294
|
await this.change("disable");
|
|
280
295
|
}
|
|
281
296
|
async change(operation, launch) {
|
|
282
|
-
for await (const event of transport(this.system).
|
|
297
|
+
for await (const event of transport(this.system).stream("program", {
|
|
283
298
|
word: "startup", handle: this.program.address(), operation, launch
|
|
284
299
|
})) {
|
|
285
300
|
if (event.event === "startup")
|
|
@@ -313,7 +328,7 @@ class ProgramProcesses extends Events {
|
|
|
313
328
|
create(launch = {}) { return this.createExact("create-process", launch); }
|
|
314
329
|
async *run(launch = {}, options = {}) {
|
|
315
330
|
let process = null;
|
|
316
|
-
for await (const event of transport(this.system).
|
|
331
|
+
for await (const event of transport(this.system).stream("program", {
|
|
317
332
|
word: "run-process",
|
|
318
333
|
handle: this.program.address(),
|
|
319
334
|
launch
|
|
@@ -360,7 +375,7 @@ class ProgramProcesses extends Events {
|
|
|
360
375
|
return processes.map(process => process.identity);
|
|
361
376
|
}
|
|
362
377
|
async createExact(word, launch) {
|
|
363
|
-
for await (const event of transport(this.system).
|
|
378
|
+
for await (const event of transport(this.system).stream("program", { word, handle: this.program.address(), launch })) {
|
|
364
379
|
if (event.event === "createdProcess")
|
|
365
380
|
return processHandle(this.system, required(event.process));
|
|
366
381
|
}
|
|
@@ -406,8 +421,8 @@ class ProcessHandle extends ProcessBase {
|
|
|
406
421
|
this.identity = snapshot.identity;
|
|
407
422
|
this.name = snapshot.name;
|
|
408
423
|
this.startedAt = new Date(snapshot.startedAt);
|
|
409
|
-
this.server = new
|
|
410
|
-
this.client = new
|
|
424
|
+
this.server = new ServerEndpointHandle(system, this);
|
|
425
|
+
this.client = new ClientEndpointHandle(system, this);
|
|
411
426
|
}
|
|
412
427
|
program() { return programHandle(this.system, required(this.snapshot.programSnapshot, this.snapshot.program)); }
|
|
413
428
|
async parent() {
|
|
@@ -478,7 +493,7 @@ class EndpointOperations extends Events {
|
|
|
478
493
|
} });
|
|
479
494
|
}
|
|
480
495
|
}
|
|
481
|
-
class
|
|
496
|
+
class ServerEndpointHandle extends ServerEndpointBase {
|
|
482
497
|
system;
|
|
483
498
|
owner;
|
|
484
499
|
endpoint = "server";
|
|
@@ -512,7 +527,7 @@ class ServerEndpoint extends ServerBase {
|
|
|
512
527
|
}) };
|
|
513
528
|
}
|
|
514
529
|
}
|
|
515
|
-
class
|
|
530
|
+
class ClientEndpointHandle extends ClientEndpointBase {
|
|
516
531
|
endpoint = "client";
|
|
517
532
|
traffic;
|
|
518
533
|
lifecycle;
|
|
@@ -583,7 +598,7 @@ class ServiceBase {
|
|
|
583
598
|
void transport(this.system).api({ capability: "service", operation: "publish", key: this.key, event, payload });
|
|
584
599
|
}
|
|
585
600
|
}
|
|
586
|
-
/** Node
|
|
601
|
+
/** Node SDK handle for a Service provided by a Server Endpoint. */
|
|
587
602
|
export class ServerService extends CoreServerService {
|
|
588
603
|
constructor() { super(); }
|
|
589
604
|
}
|
|
@@ -614,7 +629,7 @@ class ServerServiceHandle extends ServerService {
|
|
|
614
629
|
}) };
|
|
615
630
|
}
|
|
616
631
|
}
|
|
617
|
-
/** Node
|
|
632
|
+
/** Node SDK handle for a Service provided by a Client Endpoint. */
|
|
618
633
|
export class ClientService extends CoreClientService {
|
|
619
634
|
constructor() { super(); }
|
|
620
635
|
}
|
|
@@ -645,7 +660,7 @@ async function listProcesses(system) {
|
|
|
645
660
|
}
|
|
646
661
|
}
|
|
647
662
|
async function* command(system, request) {
|
|
648
|
-
for await (const event of transport(system).
|
|
663
|
+
for await (const event of transport(system).stream("program", request)) {
|
|
649
664
|
if (event.event === "output")
|
|
650
665
|
yield {
|
|
651
666
|
stream: event.stream === "stderr" ? "stderr" : "stdout",
|
|
@@ -747,5 +762,5 @@ function required(value, identity = "") {
|
|
|
747
762
|
export const Program = CoreProgram;
|
|
748
763
|
export const Process = CoreProcess;
|
|
749
764
|
export const Endpoint = CoreEndpoint;
|
|
750
|
-
export const
|
|
751
|
-
export const
|
|
765
|
+
export const ServerEndpoint = CoreServerEndpoint;
|
|
766
|
+
export const ClientEndpoint = CoreClientEndpoint;
|
package/dist/traffic.d.ts
CHANGED
|
@@ -15,13 +15,13 @@ export declare class EndpointTrafficHandle<Definitions extends object = {}> exte
|
|
|
15
15
|
event: string;
|
|
16
16
|
questionId: string;
|
|
17
17
|
message: Readonly<{
|
|
18
|
-
to: import("@phreshos/core").
|
|
18
|
+
to: import("@phreshos/core").ServerEndpoint<{}, unknown> | null;
|
|
19
19
|
payload: Payload;
|
|
20
20
|
}>;
|
|
21
21
|
}>>;
|
|
22
22
|
protected follow<Capture>(kind: Kind, convert: (value: unknown) => Capture, subscriber: (capture: Capture) => unknown, impossible?: (error: Error) => void): Cleanup;
|
|
23
23
|
}
|
|
24
|
-
/** Directed traffic originating from one canonical Server. */
|
|
24
|
+
/** Directed traffic originating from one canonical Server Endpoint. */
|
|
25
25
|
export declare class ServerTrafficHandle<Definitions extends object = {}> extends EndpointTrafficHandle<Definitions> implements ServerTraffic<Definitions> {
|
|
26
26
|
subscribeAnswers<Result = unknown>(subscriber: AnswerSubscriber<Result>): Cleanup;
|
|
27
27
|
answers<Result = unknown>(options?: EventOptions): AsyncIterableIterator<Readonly<{
|
package/dist/traffic.js
CHANGED
|
@@ -52,7 +52,7 @@ export class EndpointTrafficHandle extends Events {
|
|
|
52
52
|
return () => controller.abort();
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
/** Directed traffic originating from one canonical Server. */
|
|
55
|
+
/** Directed traffic originating from one canonical Server Endpoint. */
|
|
56
56
|
export class ServerTrafficHandle extends EndpointTrafficHandle {
|
|
57
57
|
subscribeAnswers(subscriber) {
|
|
58
58
|
return this.follow("answer", value => answer(value, this.resolveEndpoint), subscriber);
|
package/dist/transport.d.ts
CHANGED
|
@@ -7,5 +7,5 @@ export interface TransportEvent {
|
|
|
7
7
|
export declare function openConnection(path: string): Promise<Socket>;
|
|
8
8
|
/** Execute one short authoritative System-control request. */
|
|
9
9
|
export declare function request(path: string, target: "api" | "system", request: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
10
|
-
/** Stream one
|
|
11
|
-
export declare function
|
|
10
|
+
/** Stream one long-running authoritative System operation. */
|
|
11
|
+
export declare function stream(path: string, target: "program", request: unknown, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
|
package/dist/transport.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connect } from "node:net";
|
|
2
|
+
const maximumStreamQueue = 256;
|
|
2
3
|
/** Open and retain one owner-local System connection. */
|
|
3
4
|
export function openConnection(path) {
|
|
4
5
|
return new Promise((resolve, reject) => {
|
|
@@ -52,8 +53,8 @@ export function request(path, target, request, signal) {
|
|
|
52
53
|
cancel();
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
|
-
/** Stream one
|
|
56
|
-
export function
|
|
56
|
+
/** Stream one long-running authoritative System operation. */
|
|
57
|
+
export function stream(path, target, request, signal) {
|
|
57
58
|
const events = [];
|
|
58
59
|
let wake = null;
|
|
59
60
|
let ended = false;
|
|
@@ -65,14 +66,27 @@ export function streamProgram(path, request, signal) {
|
|
|
65
66
|
else
|
|
66
67
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
67
68
|
let buffer = "";
|
|
68
|
-
socket.on("connect", () => socket.write(`${JSON.stringify({ target
|
|
69
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ target, request })}\n`));
|
|
69
70
|
socket.on("data", chunk => {
|
|
70
71
|
buffer += String(chunk);
|
|
71
72
|
const lines = buffer.split("\n");
|
|
72
73
|
buffer = lines.pop() ?? "";
|
|
73
74
|
for (const line of lines)
|
|
74
75
|
if (line.trim()) {
|
|
75
|
-
|
|
76
|
+
if (events.length >= maximumStreamQueue) {
|
|
77
|
+
failure = new Error(`System stream queue exceeded its capacity of ${maximumStreamQueue}`);
|
|
78
|
+
socket.destroy();
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
let event;
|
|
82
|
+
try {
|
|
83
|
+
event = JSON.parse(line);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
failure = new Error("The System returned an invalid stream event");
|
|
87
|
+
socket.destroy();
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
76
90
|
if (event.event === "error")
|
|
77
91
|
failure = new Error(String(event.message));
|
|
78
92
|
else
|