@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/README.md
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
# `@phreshos/node`
|
|
2
2
|
|
|
3
|
-
Node.js access to a
|
|
3
|
+
Node.js access to a PhreshOS System and local Program projects.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
[Documentation](https://docs.phreshos.com/sdks/node) ·
|
|
6
|
+
[System](https://docs.phreshos.com/system) ·
|
|
7
|
+
[First Program](https://docs.phreshos.com/first-program) ·
|
|
8
|
+
[Source](https://github.com/PhreshOS/node)
|
|
9
|
+
|
|
10
|
+
## Role
|
|
11
|
+
|
|
12
|
+
The Node SDK exposes the same complete System contract available inside Server
|
|
13
|
+
Endpoints. It adds only the lifecycle of an externally owned connection and the
|
|
14
|
+
`Project` authoring API for building, running, installing, and packaging local
|
|
15
|
+
Program projects.
|
|
16
|
+
|
|
17
|
+
The connection transport is not public API. Core owns the domain model, and the
|
|
18
|
+
System remains authoritative whether an operation can execute locally or must
|
|
19
|
+
cross the connection boundary.
|
|
7
20
|
|
|
8
21
|
## Installation
|
|
9
22
|
|
|
@@ -14,44 +27,21 @@ only connection lifecycle and project tooling.
|
|
|
14
27
|
| Bun | `bun add @phreshos/node` |
|
|
15
28
|
| Yarn | `yarn add @phreshos/node` |
|
|
16
29
|
|
|
17
|
-
## System
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
import { System } from "@phreshos/node"
|
|
21
|
-
|
|
22
|
-
const system = await System.connect()
|
|
23
|
-
const programs = await system.program.list()
|
|
24
|
-
|
|
25
|
-
await system.disconnect()
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The transport is not public API. Code receives the same System, Program,
|
|
29
|
-
Process, Endpoint, Service, Window, storage, and upload handles used by the
|
|
30
|
-
Server SDK. `disconnect()` exists because external Node code owns the
|
|
31
|
-
connection.
|
|
32
|
-
|
|
33
|
-
## Project
|
|
34
|
-
|
|
35
30
|
```ts
|
|
36
31
|
import { Project, System } from "@phreshos/node"
|
|
37
32
|
|
|
38
|
-
const project = await Project.open()
|
|
39
33
|
const system = await System.connect()
|
|
34
|
+
const project = await Project.open()
|
|
40
35
|
|
|
41
|
-
for await (const event of await project.start(system
|
|
36
|
+
for await (const event of await project.start(system)) {
|
|
42
37
|
console.log(event)
|
|
43
38
|
}
|
|
44
39
|
|
|
45
40
|
await system.disconnect()
|
|
46
41
|
```
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
receives only the resulting Program definition.
|
|
51
|
-
|
|
52
|
-
`Project.open()` starts from the current working directory by default.
|
|
53
|
-
`System.connect()` resolves an explicit System home, then `PHRESHOS_HOME`,
|
|
54
|
-
then the current owner's default System home.
|
|
43
|
+
See [Node SDK](https://docs.phreshos.com/sdks/node) for System connection and
|
|
44
|
+
Project lifecycle details.
|
|
55
45
|
|
|
56
46
|
## Development
|
|
57
47
|
|
|
@@ -60,14 +50,19 @@ bun install --frozen-lockfile
|
|
|
60
50
|
bun run verify
|
|
61
51
|
```
|
|
62
52
|
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
`verify` checks the types, builds the package, and runs the connection and
|
|
54
|
+
Project tests.
|
|
65
55
|
|
|
66
|
-
##
|
|
56
|
+
## Related repositories
|
|
67
57
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
- [`@phreshos/core`](https://github.com/PhreshOS/core) owns the shared System
|
|
59
|
+
and runtime contracts.
|
|
60
|
+
- [`@phreshos/server`](https://github.com/PhreshOS/server) exposes the same
|
|
61
|
+
System contract inside Server Endpoints.
|
|
62
|
+
- [`@phreshos/cli`](https://github.com/PhreshOS/cli) presents Project and System
|
|
63
|
+
operations as terminal commands.
|
|
64
|
+
- [PhreshOS System](https://github.com/PhreshOS/system) owns the connected
|
|
65
|
+
runtime.
|
|
71
66
|
|
|
72
67
|
## License
|
|
73
68
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ClientDevelopment, ProgramProcessRunEvent } from "@phreshos/core";
|
|
2
|
+
/** One prepared Client development source owned by a Project development run. */
|
|
3
|
+
export default class DevelopmentClient {
|
|
4
|
+
readonly url: string;
|
|
5
|
+
private readonly startCommand;
|
|
6
|
+
private readonly directory;
|
|
7
|
+
private command;
|
|
8
|
+
private controller;
|
|
9
|
+
private releaseSignal;
|
|
10
|
+
private constructor();
|
|
11
|
+
/** Select the development address without starting the authored command. */
|
|
12
|
+
static prepare(development: ClientDevelopment, directory: string): Promise<DevelopmentClient>;
|
|
13
|
+
/** Start and verify the Client beneath its System-created asset address. */
|
|
14
|
+
start(assetId: string, signal?: AbortSignal): Promise<void>;
|
|
15
|
+
processSignal(fallback?: AbortSignal): AbortSignal | undefined;
|
|
16
|
+
supervise(lifecycle: AsyncGenerator<ProgramProcessRunEvent, void, void>): AsyncGenerator<ProgramProcessRunEvent, void, void>;
|
|
17
|
+
dispose(reason: unknown): Promise<void>;
|
|
18
|
+
private waitUntilReady;
|
|
19
|
+
private supervisedLifecycle;
|
|
20
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { connect, createServer } from "node:net";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
const readinessTimeout = 15_000;
|
|
5
|
+
const pollingInterval = 200;
|
|
6
|
+
/** One prepared Client development source owned by a Project development run. */
|
|
7
|
+
export default class DevelopmentClient {
|
|
8
|
+
url;
|
|
9
|
+
startCommand;
|
|
10
|
+
directory;
|
|
11
|
+
command = null;
|
|
12
|
+
controller = null;
|
|
13
|
+
releaseSignal = () => undefined;
|
|
14
|
+
constructor(url, startCommand, directory) {
|
|
15
|
+
this.url = url;
|
|
16
|
+
this.startCommand = startCommand;
|
|
17
|
+
this.directory = directory;
|
|
18
|
+
}
|
|
19
|
+
/** Select the development address without starting the authored command. */
|
|
20
|
+
static async prepare(development, directory) {
|
|
21
|
+
const url = development.url ?? `http://localhost:${await availablePort()}/`;
|
|
22
|
+
if (development.startCommand)
|
|
23
|
+
await assertAvailable(url);
|
|
24
|
+
return new DevelopmentClient(url, development.startCommand ?? null, directory);
|
|
25
|
+
}
|
|
26
|
+
/** Start and verify the Client beneath its System-created asset address. */
|
|
27
|
+
async start(assetId, signal) {
|
|
28
|
+
if (this.controller)
|
|
29
|
+
throw new Error("The Client development source has already started");
|
|
30
|
+
const base = `/program/${assetId}/assets/`;
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
this.controller = controller;
|
|
33
|
+
this.releaseSignal = forwardAbort(signal, controller);
|
|
34
|
+
try {
|
|
35
|
+
if (this.startCommand) {
|
|
36
|
+
this.command = new OwnedCommand(this.startCommand, this.directory, {
|
|
37
|
+
PHRESHOS_CLIENT_BASE: base,
|
|
38
|
+
PHRESHOS_CLIENT_PORT: String(portOf(this.url))
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
await this.waitUntilReady(new URL(base, this.url).href);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
await this.dispose(error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
processSignal(fallback) {
|
|
49
|
+
return this.command ? this.controller?.signal : fallback;
|
|
50
|
+
}
|
|
51
|
+
supervise(lifecycle) {
|
|
52
|
+
if (!this.command) {
|
|
53
|
+
this.releaseSignal();
|
|
54
|
+
return lifecycle;
|
|
55
|
+
}
|
|
56
|
+
return this.supervisedLifecycle(lifecycle);
|
|
57
|
+
}
|
|
58
|
+
async dispose(reason) {
|
|
59
|
+
this.releaseSignal();
|
|
60
|
+
this.controller?.abort(reason);
|
|
61
|
+
await this.command?.stop();
|
|
62
|
+
}
|
|
63
|
+
async waitUntilReady(url) {
|
|
64
|
+
const deadline = Date.now() + readinessTimeout;
|
|
65
|
+
while (Date.now() < deadline) {
|
|
66
|
+
this.controller.signal.throwIfAborted();
|
|
67
|
+
const exit = this.command?.exitResult();
|
|
68
|
+
if (exit)
|
|
69
|
+
throw commandFailure(exit);
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(500, deadline - Date.now())))
|
|
73
|
+
});
|
|
74
|
+
await response.body?.cancel();
|
|
75
|
+
if (response.ok)
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
catch { /* The development server is still starting. */ }
|
|
79
|
+
await pause(Math.min(pollingInterval, Math.max(0, deadline - Date.now())), this.controller.signal);
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`Client development URL did not respond within 15 seconds: ${url}`);
|
|
82
|
+
}
|
|
83
|
+
async *supervisedLifecycle(lifecycle) {
|
|
84
|
+
const iterator = lifecycle[Symbol.asyncIterator]();
|
|
85
|
+
const command = this.command;
|
|
86
|
+
try {
|
|
87
|
+
while (true) {
|
|
88
|
+
const next = iterator.next();
|
|
89
|
+
const outcome = await Promise.race([
|
|
90
|
+
next.then(result => ({ source: "system", result }), error => ({ source: "system-error", error })),
|
|
91
|
+
command.exited().then(result => ({ source: "client", result }))
|
|
92
|
+
]);
|
|
93
|
+
if (outcome.source === "client") {
|
|
94
|
+
const error = commandFailure(outcome.result);
|
|
95
|
+
this.controller?.abort(error);
|
|
96
|
+
await next.catch(() => undefined);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
if (outcome.source === "system-error")
|
|
100
|
+
throw outcome.error;
|
|
101
|
+
if (outcome.result.done)
|
|
102
|
+
return;
|
|
103
|
+
yield outcome.result.value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await this.dispose(new Error("The development lifecycle ended"));
|
|
108
|
+
await iterator.return?.();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** One operating-system command supervised as a complete process tree. */
|
|
113
|
+
class OwnedCommand {
|
|
114
|
+
child;
|
|
115
|
+
completion;
|
|
116
|
+
result = null;
|
|
117
|
+
stopped = false;
|
|
118
|
+
constructor(command, directory, environment) {
|
|
119
|
+
this.child = spawn(command, {
|
|
120
|
+
cwd: directory,
|
|
121
|
+
env: commandEnvironment(directory, environment),
|
|
122
|
+
shell: true,
|
|
123
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
124
|
+
detached: true
|
|
125
|
+
});
|
|
126
|
+
this.child.stdout?.pipe(process.stdout, { end: false });
|
|
127
|
+
this.child.stderr?.pipe(process.stderr, { end: false });
|
|
128
|
+
this.completion = new Promise(resolve => {
|
|
129
|
+
const finish = (exit) => {
|
|
130
|
+
if (this.result)
|
|
131
|
+
return;
|
|
132
|
+
this.result = exit;
|
|
133
|
+
resolve(exit);
|
|
134
|
+
};
|
|
135
|
+
this.child.once("error", error => finish({ code: null, signal: null, error }));
|
|
136
|
+
this.child.once("exit", (code, signal) => finish({ code, signal, error: null }));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
exited() { return this.completion; }
|
|
140
|
+
exitResult() { return this.result; }
|
|
141
|
+
async stop() {
|
|
142
|
+
if (this.stopped)
|
|
143
|
+
return;
|
|
144
|
+
this.stopped = true;
|
|
145
|
+
if (!running(this.child))
|
|
146
|
+
return;
|
|
147
|
+
terminate(this.child, "SIGTERM");
|
|
148
|
+
await waitUntilStopped(this.child, 1_000);
|
|
149
|
+
if (running(this.child))
|
|
150
|
+
terminate(this.child, "SIGKILL");
|
|
151
|
+
await waitUntilStopped(this.child, 1_000);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function availablePort() {
|
|
155
|
+
const server = createServer();
|
|
156
|
+
return await new Promise((done, fail) => {
|
|
157
|
+
server.once("error", fail);
|
|
158
|
+
server.listen(0, "localhost", () => {
|
|
159
|
+
const address = server.address();
|
|
160
|
+
if (!address || typeof address === "string") {
|
|
161
|
+
server.close();
|
|
162
|
+
fail(new Error("An available Client development port could not be selected"));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
server.close(error => error ? fail(error) : done(address.port));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function assertAvailable(url) {
|
|
170
|
+
const location = new URL(url);
|
|
171
|
+
const occupied = await new Promise(done => {
|
|
172
|
+
const socket = connect({ host: location.hostname, port: portOf(url) });
|
|
173
|
+
let finished = false;
|
|
174
|
+
const finish = (value) => {
|
|
175
|
+
if (finished)
|
|
176
|
+
return;
|
|
177
|
+
finished = true;
|
|
178
|
+
socket.destroy();
|
|
179
|
+
done(value);
|
|
180
|
+
};
|
|
181
|
+
socket.setTimeout(500);
|
|
182
|
+
socket.once("connect", () => finish(true));
|
|
183
|
+
socket.once("error", () => finish(false));
|
|
184
|
+
socket.once("timeout", () => finish(false));
|
|
185
|
+
});
|
|
186
|
+
if (occupied)
|
|
187
|
+
throw new Error(`Client development URL is already in use: ${url}`);
|
|
188
|
+
}
|
|
189
|
+
function portOf(url) {
|
|
190
|
+
const location = new URL(url);
|
|
191
|
+
return Number(location.port || (location.protocol === "https:" ? 443 : 80));
|
|
192
|
+
}
|
|
193
|
+
function commandEnvironment(directory, additions) {
|
|
194
|
+
const key = Object.keys(process.env).find(name => name.toLowerCase() === "path") ?? "PATH";
|
|
195
|
+
const inherited = process.env[key];
|
|
196
|
+
return { ...process.env, ...additions, [key]: [join(directory, "node_modules", ".bin"), inherited].filter(Boolean).join(delimiter) };
|
|
197
|
+
}
|
|
198
|
+
function forwardAbort(source, target) {
|
|
199
|
+
if (!source)
|
|
200
|
+
return () => undefined;
|
|
201
|
+
const abort = () => target.abort(source.reason);
|
|
202
|
+
if (source.aborted)
|
|
203
|
+
abort();
|
|
204
|
+
else
|
|
205
|
+
source.addEventListener("abort", abort, { once: true });
|
|
206
|
+
return () => source.removeEventListener("abort", abort);
|
|
207
|
+
}
|
|
208
|
+
function commandFailure(exit) {
|
|
209
|
+
if (exit.error)
|
|
210
|
+
return new Error(`Client development command failed: ${exit.error.message}`);
|
|
211
|
+
if (exit.signal)
|
|
212
|
+
return new Error(`Client development command ended on ${exit.signal}`);
|
|
213
|
+
return new Error(`Client development command exited with ${exit.code ?? 0}`);
|
|
214
|
+
}
|
|
215
|
+
function terminate(child, signal) {
|
|
216
|
+
if (!child.pid)
|
|
217
|
+
return;
|
|
218
|
+
try {
|
|
219
|
+
process.kill(-child.pid, signal);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
child.kill(signal);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function running(child) {
|
|
226
|
+
if (!child.pid)
|
|
227
|
+
return false;
|
|
228
|
+
try {
|
|
229
|
+
process.kill(-child.pid, 0);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return child.exitCode === null && child.signalCode === null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function waitUntilStopped(child, milliseconds) {
|
|
237
|
+
const deadline = Date.now() + milliseconds;
|
|
238
|
+
while (running(child) && Date.now() < deadline)
|
|
239
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
240
|
+
}
|
|
241
|
+
function pause(milliseconds, signal) {
|
|
242
|
+
return new Promise((done, fail) => {
|
|
243
|
+
if (signal.aborted) {
|
|
244
|
+
fail(signal.reason);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const timeout = setTimeout(finish, milliseconds);
|
|
248
|
+
const abort = () => finish(signal.reason);
|
|
249
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
250
|
+
function finish(error) {
|
|
251
|
+
clearTimeout(timeout);
|
|
252
|
+
signal.removeEventListener("abort", abort);
|
|
253
|
+
if (error !== undefined)
|
|
254
|
+
fail(error);
|
|
255
|
+
else
|
|
256
|
+
done();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
package/dist/events.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Cleanup, EventOptions, Subscribable } from "@phreshos/core";
|
|
2
|
-
type Wait = (event: string | null, signal: AbortSignal, timeout?: number) => Promise<unknown>;
|
|
3
2
|
type Failure = (error: Error) => void;
|
|
4
3
|
type Register<Message> = (subscriber: (message: Message) => unknown, impossible?: Failure) => Cleanup;
|
|
5
|
-
|
|
4
|
+
type Subscribe = (event: string | null, subscriber: (message: unknown) => unknown, impossible?: Failure) => Cleanup;
|
|
5
|
+
/** Adapts one live representation source into the shared Subscribable contract. */
|
|
6
6
|
export default class Events<Definitions extends object, Fallback = never> {
|
|
7
7
|
private readonly names;
|
|
8
|
-
private readonly
|
|
9
|
-
constructor(names: readonly string[],
|
|
8
|
+
private readonly register;
|
|
9
|
+
constructor(names: readonly string[], register: Subscribe);
|
|
10
10
|
readonly subscribe: Subscribable<Definitions, Fallback>["subscribe"];
|
|
11
11
|
readonly waitFor: Subscribable<Definitions, Fallback>["waitFor"];
|
|
12
12
|
readonly events: Subscribable<Definitions, Fallback>["events"];
|
package/dist/events.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/** Adapts
|
|
1
|
+
/** Adapts one live representation source into the shared Subscribable contract. */
|
|
2
2
|
export default class Events {
|
|
3
3
|
names;
|
|
4
|
-
|
|
5
|
-
constructor(names,
|
|
4
|
+
register;
|
|
5
|
+
constructor(names, register) {
|
|
6
6
|
this.names = names;
|
|
7
|
-
this.
|
|
7
|
+
this.register = register;
|
|
8
8
|
}
|
|
9
9
|
subscribe = ((eventOrSubscriber, subscriber) => {
|
|
10
10
|
if (typeof eventOrSubscriber === "string")
|
|
@@ -19,9 +19,19 @@ export default class Events {
|
|
|
19
19
|
eventOrSubscriber({ event: capture.event, message: capture.payload });
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
|
-
waitFor = ((event, timeout) => {
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
waitFor = ((event, timeout = 10_000) => new Promise((resolve, reject) => {
|
|
23
|
+
let stop = () => undefined;
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
stop();
|
|
26
|
+
reject(new Error(`The "${event}" event did not occur before the timeout`));
|
|
27
|
+
}, timeout);
|
|
28
|
+
const finish = (work) => {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
stop();
|
|
31
|
+
work();
|
|
32
|
+
};
|
|
33
|
+
stop = this.listen(event, message => finish(() => resolve(message)), error => finish(() => reject(error)));
|
|
34
|
+
}));
|
|
25
35
|
events = ((eventOrOptions = {}, namedOptions = {}) => {
|
|
26
36
|
if (typeof eventOrOptions === "string") {
|
|
27
37
|
return stream((subscriber, impossible) => this.listen(eventOrOptions, subscriber, impossible), namedOptions);
|
|
@@ -39,22 +49,7 @@ export default class Events {
|
|
|
39
49
|
}, eventOrOptions);
|
|
40
50
|
});
|
|
41
51
|
listen(event, subscriber, impossible) {
|
|
42
|
-
|
|
43
|
-
void (async () => {
|
|
44
|
-
while (!controller.signal.aborted) {
|
|
45
|
-
try {
|
|
46
|
-
subscriber(await this.wait(event, controller.signal, 86_400_000));
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
if (!controller.signal.aborted && timeout(error))
|
|
50
|
-
continue;
|
|
51
|
-
if (!controller.signal.aborted)
|
|
52
|
-
impossible?.(failure(error));
|
|
53
|
-
controller.abort();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
})();
|
|
57
|
-
return () => controller.abort();
|
|
52
|
+
return this.register(event, subscriber, impossible);
|
|
58
53
|
}
|
|
59
54
|
}
|
|
60
55
|
export function stream(register, options = {}) {
|
|
@@ -109,9 +104,3 @@ export function stream(register, options = {}) {
|
|
|
109
104
|
}
|
|
110
105
|
})();
|
|
111
106
|
}
|
|
112
|
-
function timeout(error) {
|
|
113
|
-
return error instanceof Error && /timeout|timed out/i.test(error.message);
|
|
114
|
-
}
|
|
115
|
-
function failure(error) {
|
|
116
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
117
|
-
}
|
package/dist/main.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { gatewayAddress } from "./address.js";
|
|
2
|
-
export {
|
|
3
|
-
export { Service, type ClientLaunch, type Launch, type
|
|
2
|
+
export { ClientEndpoint, ClientService, Endpoint, Process, Program, ServerEndpoint, ServerService, System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
|
|
3
|
+
export { Service, clientPermissionCatalog, isPermissionName, type ClientLaunch, type Launch, type Permission, type PermissionChange, type PermissionDefinition, type PermissionDefinitions, type PermissionInput, type PermissionName, type PermissionValue, type PermissionValueDomain, type Permissions, type ProgramPermissions, type ProgramDefinition, type ServerLaunch, type ServiceKey, type ShellEvent, type ShellOptions, type ProgramStartup, type Storage, } from "@phreshos/core";
|
|
4
4
|
export { resolveHome } from "./home.js";
|
|
5
5
|
export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions } from "./project.js";
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { gatewayAddress } from "./address.js";
|
|
2
|
-
export {
|
|
3
|
-
export { Service, } from "@phreshos/core";
|
|
2
|
+
export { ClientEndpoint, ClientService, Endpoint, Process, Program, ServerEndpoint, ServerService, System } from "./system.js";
|
|
3
|
+
export { Service, clientPermissionCatalog, isPermissionName, } from "@phreshos/core";
|
|
4
4
|
export { resolveHome } from "./home.js";
|
|
5
5
|
export { Project } from "./project.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
/** The complete operating-system process tree beneath one shell command. */
|
|
3
|
+
export default class ProcessTree {
|
|
4
|
+
private readonly child;
|
|
5
|
+
private readonly ended;
|
|
6
|
+
private forcing;
|
|
7
|
+
constructor(child: ChildProcess, ended: (code: number | null, signal: NodeJS.Signals | null) => void | Promise<void>);
|
|
8
|
+
stop(): void;
|
|
9
|
+
private finish;
|
|
10
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const terminationGrace = 1_000;
|
|
2
|
+
/** The complete operating-system process tree beneath one shell command. */
|
|
3
|
+
export default class ProcessTree {
|
|
4
|
+
child;
|
|
5
|
+
ended;
|
|
6
|
+
forcing = null;
|
|
7
|
+
constructor(child, ended) {
|
|
8
|
+
this.child = child;
|
|
9
|
+
this.ended = ended;
|
|
10
|
+
child.on("exit", (code, signal) => { this.finish(code, signal).catch(() => undefined); });
|
|
11
|
+
}
|
|
12
|
+
stop() {
|
|
13
|
+
signalTree(this.child, "SIGTERM");
|
|
14
|
+
if (this.forcing)
|
|
15
|
+
return;
|
|
16
|
+
this.forcing = setTimeout(() => signalTree(this.child, "SIGKILL"), terminationGrace);
|
|
17
|
+
this.forcing.unref();
|
|
18
|
+
}
|
|
19
|
+
async finish(code, signal) {
|
|
20
|
+
if (this.forcing)
|
|
21
|
+
clearTimeout(this.forcing);
|
|
22
|
+
this.forcing = null;
|
|
23
|
+
await finishTree(this.child);
|
|
24
|
+
await this.ended(code, signal);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function signalTree(child, signal) {
|
|
28
|
+
if (!child.pid)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
process.kill(-child.pid, signal);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === "ESRCH")
|
|
35
|
+
return;
|
|
36
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
37
|
+
child.kill(signal);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function finishTree(child) {
|
|
41
|
+
const pid = child.pid;
|
|
42
|
+
if (!pid || !treeExists(pid))
|
|
43
|
+
return;
|
|
44
|
+
signalTree(child, "SIGTERM");
|
|
45
|
+
if (await waitForTree(pid))
|
|
46
|
+
return;
|
|
47
|
+
signalTree(child, "SIGKILL");
|
|
48
|
+
await waitForTree(pid);
|
|
49
|
+
}
|
|
50
|
+
async function waitForTree(pid) {
|
|
51
|
+
const began = Date.now();
|
|
52
|
+
while (treeExists(pid)) {
|
|
53
|
+
if (Date.now() - began >= terminationGrace)
|
|
54
|
+
return false;
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function treeExists(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(-pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return error.code !== "ESRCH";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type
|
|
2
|
-
type
|
|
1
|
+
import { type ProgramPermissions, type ProgramSql, type ProgramStore } from "@phreshos/core";
|
|
2
|
+
type Call = <Result = unknown>(event: string, ...values: unknown[]) => Promise<Result>;
|
|
3
3
|
type ProgramAddress = Readonly<{
|
|
4
4
|
identity: string;
|
|
5
5
|
reference: string;
|
|
6
6
|
}>;
|
|
7
7
|
/** Program-owned key-value storage carried through the owner-local Gateway. */
|
|
8
|
-
export declare function programStore(
|
|
8
|
+
export declare function programStore(call: Call, handle: ProgramAddress): ProgramStore;
|
|
9
9
|
/** Program-owned SQL capability carried through the owner-local Gateway. */
|
|
10
|
-
export declare function programSql(
|
|
11
|
-
/**
|
|
12
|
-
export declare function
|
|
10
|
+
export declare function programSql(call: Call, handle: ProgramAddress, database: "database" | "logs"): ProgramSql;
|
|
11
|
+
/** Program permission management carried through the owner-local Gateway. */
|
|
12
|
+
export declare function programPermissions(call: Call, handle: ProgramAddress): ProgramPermissions;
|
|
13
13
|
export {};
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
+
import { parsePermission, parsePermissionChange, parsePermissions } from "@phreshos/core";
|
|
1
2
|
/** Program-owned key-value storage carried through the owner-local Gateway. */
|
|
2
|
-
export function programStore(
|
|
3
|
-
const operate = (storeOperation, key, value, ttl) =>
|
|
4
|
-
capability: "program",
|
|
5
|
-
operation: "store",
|
|
6
|
-
handle,
|
|
7
|
-
storeOperation,
|
|
8
|
-
key,
|
|
9
|
-
value,
|
|
10
|
-
ttl
|
|
11
|
-
});
|
|
3
|
+
export function programStore(call, handle) {
|
|
4
|
+
const operate = (storeOperation, key, value, ttl) => (call("/program/store", handle, storeOperation, key, value, ttl));
|
|
12
5
|
return {
|
|
13
6
|
get: (key) => operate("get", key),
|
|
14
7
|
set: (key, value, ttl) => operate("set", key, value, ttl),
|
|
@@ -18,29 +11,22 @@ export function programStore(request, handle) {
|
|
|
18
11
|
};
|
|
19
12
|
}
|
|
20
13
|
/** Program-owned SQL capability carried through the owner-local Gateway. */
|
|
21
|
-
export function programSql(
|
|
14
|
+
export function programSql(call, handle, database) {
|
|
22
15
|
return {
|
|
23
16
|
query(statement, ...rest) {
|
|
24
17
|
const [text, values] = written(statement, rest);
|
|
25
|
-
return
|
|
18
|
+
return call(`/program/${database}`, handle, text, values);
|
|
26
19
|
}
|
|
27
20
|
};
|
|
28
21
|
}
|
|
29
|
-
/**
|
|
30
|
-
export function
|
|
31
|
-
const operate = (permissionOperation, name,
|
|
32
|
-
capability: "program",
|
|
33
|
-
operation: "permission",
|
|
34
|
-
handle,
|
|
35
|
-
permissionOperation,
|
|
36
|
-
name,
|
|
37
|
-
value
|
|
38
|
-
});
|
|
22
|
+
/** Program permission management carried through the owner-local Gateway. */
|
|
23
|
+
export function programPermissions(call, handle) {
|
|
24
|
+
const operate = (permissionOperation, name, permission) => (call("/program/permissions", handle, permissionOperation, name, permission));
|
|
39
25
|
return {
|
|
40
|
-
get
|
|
41
|
-
|
|
42
|
-
set
|
|
43
|
-
delete
|
|
26
|
+
async get(name) { return parsePermission(name, await operate("get", name)); },
|
|
27
|
+
async all() { return parsePermissions(await operate("all")); },
|
|
28
|
+
async set(name, permission) { return parsePermissionChange(name, await operate("set", name, permission)); },
|
|
29
|
+
async delete(name) { return parsePermissionChange(name, await operate("delete", name)); }
|
|
44
30
|
};
|
|
45
31
|
}
|
|
46
32
|
function written(statement, rest) {
|
package/dist/project.d.ts
CHANGED
|
@@ -18,9 +18,11 @@ export declare class Project {
|
|
|
18
18
|
/** Run the optional author-owned production build command. */
|
|
19
19
|
build(): Promise<void>;
|
|
20
20
|
/** Build this Project and return its production Process lifecycle generator. */
|
|
21
|
-
start(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").
|
|
22
|
-
/**
|
|
23
|
-
dev(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").
|
|
21
|
+
start(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").ProgramProcessRunEvent, void, void>>;
|
|
22
|
+
/** Prepare this Project's Client development source and return its Process lifecycle. */
|
|
23
|
+
dev(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").ProgramProcessRunEvent, void, void>>;
|
|
24
|
+
private developmentRun;
|
|
25
|
+
private prepareDevelopment;
|
|
24
26
|
/** Build this Project and return its Program installation generator. */
|
|
25
27
|
install(system: SystemContract): Promise<AsyncGenerator<Readonly<{
|
|
26
28
|
stream: "stdout" | "stderr";
|