@phreshos/node 0.1.0 → 0.1.2
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 +42 -15
- package/dist/handle-registry.d.ts +6 -0
- package/dist/handle-registry.js +13 -0
- package/dist/home.d.ts +1 -1
- package/dist/home.js +1 -1
- package/dist/main.d.ts +2 -3
- package/dist/main.js +1 -2
- package/dist/project.d.ts +21 -4
- package/dist/project.js +35 -9
- package/dist/system.d.ts +252 -6
- package/dist/system.js +256 -82
- package/dist/transport.d.ts +3 -3
- package/dist/transport.js +19 -13
- package/package.json +2 -2
- package/dist/client-development.d.ts +0 -29
- package/dist/client-development.js +0 -199
- package/dist/gateway.d.ts +0 -49
- package/dist/gateway.js +0 -151
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { connect } from "node:net";
|
|
3
|
-
import { delimiter, join } from "node:path";
|
|
4
|
-
const readinessTimeout = 15_000;
|
|
5
|
-
const pollingInterval = 200;
|
|
6
|
-
const reportingInterval = 2_000;
|
|
7
|
-
const sandboxedClientOrigin = "null";
|
|
8
|
-
/** One client development server owned by a Gateway development run. */
|
|
9
|
-
export class DevelopmentClient {
|
|
10
|
-
child;
|
|
11
|
-
output = [];
|
|
12
|
-
stopped = false;
|
|
13
|
-
result = null;
|
|
14
|
-
outputWaiter = null;
|
|
15
|
-
completion;
|
|
16
|
-
constructor(command, directory) {
|
|
17
|
-
this.child = spawn(command, {
|
|
18
|
-
cwd: directory,
|
|
19
|
-
env: commandEnvironment(directory),
|
|
20
|
-
shell: true,
|
|
21
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
22
|
-
detached: true
|
|
23
|
-
});
|
|
24
|
-
this.child.stdout?.on("data", chunk => this.push(outputEvent("out", chunk)));
|
|
25
|
-
this.child.stderr?.on("data", chunk => this.push(outputEvent("err", chunk)));
|
|
26
|
-
this.completion = new Promise(resolve => {
|
|
27
|
-
let settled = false;
|
|
28
|
-
const finish = (exit) => {
|
|
29
|
-
if (settled)
|
|
30
|
-
return;
|
|
31
|
-
settled = true;
|
|
32
|
-
this.result = exit;
|
|
33
|
-
this.outputWaiter?.();
|
|
34
|
-
this.outputWaiter = null;
|
|
35
|
-
resolve(exit);
|
|
36
|
-
};
|
|
37
|
-
this.child.once("error", error => finish({ code: null, signal: null, error }));
|
|
38
|
-
this.child.once("exit", (code, signal) => finish({ code, signal, error: null }));
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
drain() { return this.output.splice(0); }
|
|
42
|
-
exited() { return this.completion; }
|
|
43
|
-
exitResult() { return this.result; }
|
|
44
|
-
outputAvailable() {
|
|
45
|
-
if (this.output.length || this.result)
|
|
46
|
-
return Promise.resolve();
|
|
47
|
-
return new Promise(resolve => { this.outputWaiter = resolve; });
|
|
48
|
-
}
|
|
49
|
-
async stop() {
|
|
50
|
-
if (this.stopped)
|
|
51
|
-
return;
|
|
52
|
-
this.stopped = true;
|
|
53
|
-
if (!running(this.child))
|
|
54
|
-
return;
|
|
55
|
-
terminate(this.child, "SIGTERM");
|
|
56
|
-
await waitUntilStopped(this.child, 1_000);
|
|
57
|
-
if (running(this.child))
|
|
58
|
-
terminate(this.child, "SIGKILL");
|
|
59
|
-
await waitUntilStopped(this.child, 1_000);
|
|
60
|
-
}
|
|
61
|
-
endingWasRequested() { return this.stopped; }
|
|
62
|
-
push(event) {
|
|
63
|
-
this.output.push(event);
|
|
64
|
-
this.outputWaiter?.();
|
|
65
|
-
this.outputWaiter = null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/** Refuse to claim a URL already served by an unrelated process. */
|
|
69
|
-
export async function assertAvailable(url) {
|
|
70
|
-
if (!await occupied(url))
|
|
71
|
-
return;
|
|
72
|
-
throw new Error(`Client development URL is already in use: ${url}`);
|
|
73
|
-
}
|
|
74
|
-
/** Wait until a development Client can be loaded by a sandboxed Program iframe. */
|
|
75
|
-
export async function* waitForDevelopmentClient(config, client, signal) {
|
|
76
|
-
const began = Date.now();
|
|
77
|
-
let nextReport = began + reportingInterval;
|
|
78
|
-
while (Date.now() - began < readinessTimeout) {
|
|
79
|
-
throwIfAborted(signal);
|
|
80
|
-
for (const event of client?.drain() ?? [])
|
|
81
|
-
yield event;
|
|
82
|
-
const exit = client?.exitResult();
|
|
83
|
-
if (exit && !client?.endingWasRequested())
|
|
84
|
-
throw commandFailure(exit);
|
|
85
|
-
const availability = await inspect(config.url, readinessTimeout - (Date.now() - began));
|
|
86
|
-
if (availability === "ready")
|
|
87
|
-
return;
|
|
88
|
-
if (availability === "cors-blocked") {
|
|
89
|
-
throw new Error([
|
|
90
|
-
`Client development URL responded, but does not allow the sandboxed Client origin: ${config.url}`,
|
|
91
|
-
"Enable CORS so the response includes Access-Control-Allow-Origin: *."
|
|
92
|
-
].join("\n"));
|
|
93
|
-
}
|
|
94
|
-
const now = Date.now();
|
|
95
|
-
if (now >= nextReport) {
|
|
96
|
-
yield { event: "waiting", subject: "client", url: config.url };
|
|
97
|
-
while (nextReport <= now)
|
|
98
|
-
nextReport += reportingInterval;
|
|
99
|
-
}
|
|
100
|
-
await pause(Math.min(pollingInterval, readinessTimeout - (now - began)), signal);
|
|
101
|
-
}
|
|
102
|
-
throw new Error(`Client development URL did not respond within 15 seconds: ${config.url}`);
|
|
103
|
-
}
|
|
104
|
-
export function commandFailure(exit) {
|
|
105
|
-
if (exit.error)
|
|
106
|
-
return new Error(`Client development command failed: ${exit.error.message}`);
|
|
107
|
-
if (exit.signal)
|
|
108
|
-
return new Error(`Client development command ended on ${exit.signal}`);
|
|
109
|
-
return new Error(`Client development command exited with ${exit.code ?? 0}`);
|
|
110
|
-
}
|
|
111
|
-
function outputEvent(stream, chunk) {
|
|
112
|
-
return { event: "output", source: "client-development", stream, text: String(chunk) };
|
|
113
|
-
}
|
|
114
|
-
function commandEnvironment(directory) {
|
|
115
|
-
const key = Object.keys(process.env).find(name => name.toLowerCase() === "path") ?? "PATH";
|
|
116
|
-
const inherited = process.env[key];
|
|
117
|
-
return { ...process.env, [key]: [join(directory, "node_modules", ".bin"), inherited].filter(Boolean).join(delimiter) };
|
|
118
|
-
}
|
|
119
|
-
async function occupied(url) {
|
|
120
|
-
const location = new URL(url);
|
|
121
|
-
const port = Number(location.port || (location.protocol === "https:" ? 443 : 80));
|
|
122
|
-
return await new Promise(resolve => {
|
|
123
|
-
const socket = connect({ host: location.hostname, port });
|
|
124
|
-
let done = false;
|
|
125
|
-
const finish = (value) => {
|
|
126
|
-
if (done)
|
|
127
|
-
return;
|
|
128
|
-
done = true;
|
|
129
|
-
socket.destroy();
|
|
130
|
-
resolve(value);
|
|
131
|
-
};
|
|
132
|
-
socket.setTimeout(500);
|
|
133
|
-
socket.once("connect", () => finish(true));
|
|
134
|
-
socket.once("error", () => finish(false));
|
|
135
|
-
socket.once("timeout", () => finish(false));
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
async function inspect(url, remaining) {
|
|
139
|
-
try {
|
|
140
|
-
const response = await fetch(url, {
|
|
141
|
-
headers: { origin: sandboxedClientOrigin },
|
|
142
|
-
signal: AbortSignal.timeout(Math.max(1, Math.min(500, remaining)))
|
|
143
|
-
});
|
|
144
|
-
const allowedOrigin = response.headers.get("access-control-allow-origin")?.trim();
|
|
145
|
-
await response.body?.cancel();
|
|
146
|
-
return allowedOrigin === "*" || allowedOrigin === sandboxedClientOrigin ? "ready" : "cors-blocked";
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return "unavailable";
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
function terminate(child, signal) {
|
|
153
|
-
if (!child.pid)
|
|
154
|
-
return;
|
|
155
|
-
try {
|
|
156
|
-
process.kill(-child.pid, signal);
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
child.kill(signal);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
function running(child) {
|
|
163
|
-
if (!child.pid)
|
|
164
|
-
return false;
|
|
165
|
-
try {
|
|
166
|
-
process.kill(-child.pid, 0);
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
return child.exitCode === null && child.signalCode === null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
async function waitUntilStopped(child, milliseconds) {
|
|
174
|
-
const deadline = Date.now() + milliseconds;
|
|
175
|
-
while (running(child) && Date.now() < deadline)
|
|
176
|
-
await new Promise(resolve => setTimeout(resolve, 20));
|
|
177
|
-
}
|
|
178
|
-
function pause(milliseconds, signal) {
|
|
179
|
-
return new Promise((resolve, reject) => {
|
|
180
|
-
const timer = setTimeout(finish, Math.max(0, milliseconds));
|
|
181
|
-
const cancel = () => {
|
|
182
|
-
cleanup();
|
|
183
|
-
reject(signal?.reason instanceof Error ? signal.reason : new Error("The operation was cancelled"));
|
|
184
|
-
};
|
|
185
|
-
const cleanup = () => {
|
|
186
|
-
clearTimeout(timer);
|
|
187
|
-
signal?.removeEventListener("abort", cancel);
|
|
188
|
-
};
|
|
189
|
-
function finish() { cleanup(); resolve(); }
|
|
190
|
-
if (signal?.aborted)
|
|
191
|
-
cancel();
|
|
192
|
-
else
|
|
193
|
-
signal?.addEventListener("abort", cancel, { once: true });
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
function throwIfAborted(signal) {
|
|
197
|
-
if (signal?.aborted)
|
|
198
|
-
throw signal.reason instanceof Error ? signal.reason : new Error("The operation was cancelled");
|
|
199
|
-
}
|
package/dist/gateway.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { ProgramDescription, System, SystemControlClient, SystemControlRequest } from "@phreshos/core";
|
|
2
|
-
import { Project, type PackedProject } from "./project.js";
|
|
3
|
-
import { type GatewayEvent } from "./transport.js";
|
|
4
|
-
/** One explicit owner-local connection to a running PhreshOS System. */
|
|
5
|
-
export declare class Gateway implements SystemControlClient {
|
|
6
|
-
private readonly connection;
|
|
7
|
-
readonly home: string;
|
|
8
|
-
readonly address: string;
|
|
9
|
-
readonly system: System;
|
|
10
|
-
private closed;
|
|
11
|
-
private readonly lifetime;
|
|
12
|
-
private constructor();
|
|
13
|
-
/** Connect to an already running System selected by argument, environment, or owner default. */
|
|
14
|
-
static open(home?: string): Promise<Gateway>;
|
|
15
|
-
/** Execute one operation from the transport-neutral System-control vocabulary. */
|
|
16
|
-
execute(request: SystemControlRequest, signal?: AbortSignal): Promise<unknown>;
|
|
17
|
-
/** Build and package one local Program project. */
|
|
18
|
-
pack(project: Project): Promise<PackedProject>;
|
|
19
|
-
/** Install one local Program project in this Gateway's System. */
|
|
20
|
-
install(source: Project | ProgramDescription, options?: InstallOptions): AsyncGenerator<GatewayEvent, void, void>;
|
|
21
|
-
/** Build and start one local production Program, attached to this Gateway. */
|
|
22
|
-
start(project: Project, options?: RunOptions): AsyncGenerator<GatewayEvent, void, unknown>;
|
|
23
|
-
/** Start one local Program in development, including its declared Client development server. */
|
|
24
|
-
dev(project: Project, options?: RunOptions): AsyncGenerator<GatewayEvent, void, unknown>;
|
|
25
|
-
/** Uninstall one Program by identity or local Project. */
|
|
26
|
-
uninstall(program: string | Project, options?: UninstallOptions): AsyncGenerator<GatewayEvent, void, void>;
|
|
27
|
-
/** Close this Gateway without stopping the System. */
|
|
28
|
-
close(): Promise<void>;
|
|
29
|
-
private runProject;
|
|
30
|
-
private program;
|
|
31
|
-
private control;
|
|
32
|
-
private api;
|
|
33
|
-
private lifecycle;
|
|
34
|
-
private signal;
|
|
35
|
-
private requireOpen;
|
|
36
|
-
}
|
|
37
|
-
export interface InstallOptions {
|
|
38
|
-
run?: boolean;
|
|
39
|
-
startup?: boolean;
|
|
40
|
-
signal?: AbortSignal;
|
|
41
|
-
}
|
|
42
|
-
export interface RunOptions {
|
|
43
|
-
options?: Record<string, string>;
|
|
44
|
-
signal?: AbortSignal;
|
|
45
|
-
}
|
|
46
|
-
export interface UninstallOptions {
|
|
47
|
-
everything?: boolean;
|
|
48
|
-
signal?: AbortSignal;
|
|
49
|
-
}
|
package/dist/gateway.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { gatewayAddress } from "./address.js";
|
|
2
|
-
import { resolveHome } from "./home.js";
|
|
3
|
-
import { Project } from "./project.js";
|
|
4
|
-
import { openConnection, request as gatewayRequest, streamProgram } from "./transport.js";
|
|
5
|
-
import { gatewaySystem } from "./system.js";
|
|
6
|
-
import { assertAvailable, commandFailure, DevelopmentClient, waitForDevelopmentClient } from "./client-development.js";
|
|
7
|
-
/** One explicit owner-local connection to a running PhreshOS System. */
|
|
8
|
-
export class Gateway {
|
|
9
|
-
connection;
|
|
10
|
-
home;
|
|
11
|
-
address;
|
|
12
|
-
system;
|
|
13
|
-
closed = false;
|
|
14
|
-
lifetime = new AbortController();
|
|
15
|
-
constructor(home, address, connection) {
|
|
16
|
-
this.connection = connection;
|
|
17
|
-
this.home = home;
|
|
18
|
-
this.address = address;
|
|
19
|
-
this.system = gatewaySystem({
|
|
20
|
-
control: (request, signal) => this.control(request, signal),
|
|
21
|
-
api: (request, signal) => this.api(request, signal),
|
|
22
|
-
lifecycle: (request, signal) => this.lifecycle(request, signal)
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
/** Connect to an already running System selected by argument, environment, or owner default. */
|
|
26
|
-
static async open(home) {
|
|
27
|
-
const resolvedHome = resolveHome(home);
|
|
28
|
-
const address = gatewayAddress(resolvedHome);
|
|
29
|
-
const connection = await openConnection(address);
|
|
30
|
-
return new Gateway(resolvedHome, address, connection);
|
|
31
|
-
}
|
|
32
|
-
/** Execute one operation from the transport-neutral System-control vocabulary. */
|
|
33
|
-
execute(request, signal) {
|
|
34
|
-
this.requireOpen();
|
|
35
|
-
return this.control(request, signal);
|
|
36
|
-
}
|
|
37
|
-
/** Build and package one local Program project. */
|
|
38
|
-
pack(project) {
|
|
39
|
-
this.requireOpen();
|
|
40
|
-
return project.pack();
|
|
41
|
-
}
|
|
42
|
-
/** Install one local Program project in this Gateway's System. */
|
|
43
|
-
async *install(source, options = {}) {
|
|
44
|
-
this.requireOpen();
|
|
45
|
-
if (source instanceof Project)
|
|
46
|
-
await source.build();
|
|
47
|
-
const program = source instanceof Project ? source.description("production") : source;
|
|
48
|
-
yield* this.program({
|
|
49
|
-
word: "install",
|
|
50
|
-
program,
|
|
51
|
-
run: options.run === true,
|
|
52
|
-
startup: options.startup === true
|
|
53
|
-
}, options.signal);
|
|
54
|
-
}
|
|
55
|
-
/** Build and start one local production Program, attached to this Gateway. */
|
|
56
|
-
start(project, options = {}) {
|
|
57
|
-
this.requireOpen();
|
|
58
|
-
return this.runProject(project, "production", options);
|
|
59
|
-
}
|
|
60
|
-
/** Start one local Program in development, including its declared Client development server. */
|
|
61
|
-
dev(project, options = {}) {
|
|
62
|
-
this.requireOpen();
|
|
63
|
-
return this.runProject(project, "development", options);
|
|
64
|
-
}
|
|
65
|
-
/** Uninstall one Program by identity or local Project. */
|
|
66
|
-
uninstall(program, options = {}) {
|
|
67
|
-
this.requireOpen();
|
|
68
|
-
const identity = typeof program === "string" ? program : program.config.identity;
|
|
69
|
-
return this.program({ word: "uninstall", identity, everything: options.everything === true }, options.signal);
|
|
70
|
-
}
|
|
71
|
-
/** Close this Gateway without stopping the System. */
|
|
72
|
-
async close() {
|
|
73
|
-
if (this.closed)
|
|
74
|
-
return;
|
|
75
|
-
this.closed = true;
|
|
76
|
-
this.lifetime.abort(new Error("This Gateway is closed"));
|
|
77
|
-
this.connection.destroy();
|
|
78
|
-
}
|
|
79
|
-
async *runProject(project, mode, options) {
|
|
80
|
-
if (mode === "production")
|
|
81
|
-
await project.build();
|
|
82
|
-
const program = project.description(mode);
|
|
83
|
-
const development = mode === "development" && program.client && (program.client.start ?? true)
|
|
84
|
-
? project.config.client?.development
|
|
85
|
-
: undefined;
|
|
86
|
-
const command = development?.startCommand;
|
|
87
|
-
if (command)
|
|
88
|
-
await assertAvailable(development.url);
|
|
89
|
-
const client = command ? new DevelopmentClient(command, project.directory) : undefined;
|
|
90
|
-
const controller = new AbortController();
|
|
91
|
-
const signal = this.signal(options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal);
|
|
92
|
-
try {
|
|
93
|
-
if (development)
|
|
94
|
-
yield* waitForDevelopmentClient(development, client, signal);
|
|
95
|
-
const lifecycle = this.program({ word: "run", program, options: options.options ?? {} }, signal);
|
|
96
|
-
const iterator = lifecycle[Symbol.asyncIterator]();
|
|
97
|
-
let lifecycleNext = iterator.next();
|
|
98
|
-
let exit = client?.exited();
|
|
99
|
-
let output = client?.outputAvailable();
|
|
100
|
-
while (true) {
|
|
101
|
-
for (const event of client?.drain() ?? [])
|
|
102
|
-
yield event;
|
|
103
|
-
const outcome = await Promise.race([
|
|
104
|
-
lifecycleNext.then(result => ({ source: "system", result })),
|
|
105
|
-
...(exit ? [exit.then(result => ({ source: "client", result }))] : []),
|
|
106
|
-
...(output ? [output.then(() => ({ source: "output" }))] : [])
|
|
107
|
-
]);
|
|
108
|
-
if (outcome.source === "output") {
|
|
109
|
-
output = client?.outputAvailable();
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (outcome.source === "client") {
|
|
113
|
-
exit = undefined;
|
|
114
|
-
if (!client?.endingWasRequested())
|
|
115
|
-
throw commandFailure(outcome.result);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (outcome.result.done)
|
|
119
|
-
return;
|
|
120
|
-
yield outcome.result.value;
|
|
121
|
-
lifecycleNext = iterator.next();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
finally {
|
|
125
|
-
controller.abort(new Error("The local Program run ended"));
|
|
126
|
-
await client?.stop();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
program(request, signal) {
|
|
130
|
-
return this.lifecycle(request, signal);
|
|
131
|
-
}
|
|
132
|
-
control(request, signal) {
|
|
133
|
-
this.requireOpen();
|
|
134
|
-
return gatewayRequest(this.address, "system", request, this.signal(signal));
|
|
135
|
-
}
|
|
136
|
-
api(request, signal) {
|
|
137
|
-
this.requireOpen();
|
|
138
|
-
return gatewayRequest(this.address, "api", request, this.signal(signal));
|
|
139
|
-
}
|
|
140
|
-
lifecycle(request, signal) {
|
|
141
|
-
this.requireOpen();
|
|
142
|
-
return streamProgram(this.address, request, this.signal(signal));
|
|
143
|
-
}
|
|
144
|
-
signal(signal) {
|
|
145
|
-
return signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
|
|
146
|
-
}
|
|
147
|
-
requireOpen() {
|
|
148
|
-
if (this.closed)
|
|
149
|
-
throw new Error("This Gateway is closed");
|
|
150
|
-
}
|
|
151
|
-
}
|