@phreshos/node 0.1.1 → 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 +33 -8
- package/dist/handle-registry.d.ts +6 -0
- package/dist/handle-registry.js +13 -0
- package/dist/main.d.ts +2 -2
- package/dist/main.js +1 -1
- package/dist/project.d.ts +16 -20
- package/dist/project.js +26 -108
- package/dist/system.d.ts +187 -2
- package/dist/system.js +104 -51
- package/package.json +2 -2
- package/dist/client-development.d.ts +0 -29
- package/dist/client-development.js +0 -199
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@ import { Project, System } from "@phreshos/node"
|
|
|
8
8
|
const project = await Project.open()
|
|
9
9
|
const system = await System.connect()
|
|
10
10
|
|
|
11
|
-
await project.install(system)
|
|
11
|
+
for await (const chunk of await project.install(system)) {
|
|
12
|
+
process[chunk.stream].write(chunk.text)
|
|
13
|
+
}
|
|
12
14
|
|
|
13
15
|
// This is the same transport-neutral System contract used by Server Programs.
|
|
14
16
|
const programs = await system.program.list()
|
|
@@ -27,15 +29,38 @@ const project = await Project.open() // process.cwd()
|
|
|
27
29
|
const system = await System.connect()
|
|
28
30
|
|
|
29
31
|
await project.pack()
|
|
30
|
-
await project.start(system, { signal })
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
for await (const event of await project.start(system, { signal })) {
|
|
33
|
+
// started, output, exited
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for await (const event of await project.dev(system, { signal })) {
|
|
37
|
+
// started, output, exited
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for await (const chunk of await project.install(system)) {
|
|
41
|
+
// stdout or stderr
|
|
42
|
+
}
|
|
33
43
|
|
|
34
44
|
await system.disconnect()
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
These shortcuts return the original `program.install()` or
|
|
48
|
+
`program.process.run()` generator; they do not consume or mirror its events.
|
|
49
|
+
The CLI owns presentation and its additional development Client server.
|
|
50
|
+
|
|
51
|
+
For custom composition, resolve either Program definition explicitly:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const definition = project.productionDefinition()
|
|
55
|
+
// const definition = project.developmentDefinition()
|
|
56
|
+
|
|
57
|
+
const program = await system.forceCreateProgram(definition)
|
|
58
|
+
const lifecycle = program.process.run({}, { signal })
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The lower-level System API stays composable: use
|
|
62
|
+
`system.forceCreateProgram(definition)` to replace one runtime Program, and
|
|
40
63
|
`program.process.run(launch, { signal })` when one Process should live exactly
|
|
41
|
-
as long as its asynchronous iterator.
|
|
64
|
+
as long as its asynchronous iterator. Node also exports the runtime `Program`,
|
|
65
|
+
`Process`, `Endpoint`, `Server`, and `Client` constructors; handles are
|
|
66
|
+
canonical within one connected `System` and support `instanceof`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Canonical domain handles owned for exactly one connected System context. */
|
|
2
|
+
export default class HandleRegistry {
|
|
3
|
+
handles = new Map();
|
|
4
|
+
obtain(key, create) {
|
|
5
|
+
const existing = this.handles.get(key);
|
|
6
|
+
if (existing)
|
|
7
|
+
return existing;
|
|
8
|
+
const value = create();
|
|
9
|
+
this.handles.set(key, value);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
clear() { this.handles.clear(); }
|
|
13
|
+
}
|
package/dist/main.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { gatewayAddress } from "./address.js";
|
|
2
|
-
export { System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
|
|
2
|
+
export { Client, Endpoint, Process, Program, Server, System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
|
|
3
3
|
export { resolveHome } from "./home.js";
|
|
4
|
-
export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions
|
|
4
|
+
export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions } from "./project.js";
|
package/dist/main.js
CHANGED
package/dist/project.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Config, type
|
|
1
|
+
import { type Config, type ProgramDefinition, type System as SystemContract } from "@phreshos/core";
|
|
2
2
|
/** One loaded Program authoring project rooted at an absolute directory. */
|
|
3
3
|
export declare class Project {
|
|
4
4
|
readonly directory: string;
|
|
@@ -6,26 +6,26 @@ export declare class Project {
|
|
|
6
6
|
private constructor();
|
|
7
7
|
/** Discover a project from cwd, a directory, or a phresh.config.ts path. */
|
|
8
8
|
static open(source?: string): Promise<Project>;
|
|
9
|
-
/** Create a
|
|
9
|
+
/** Create a Project from an already loaded authoring configuration. */
|
|
10
10
|
static define(config: Config, options?: ProjectOptions): Project;
|
|
11
11
|
/** Read this project's package manifest. */
|
|
12
12
|
manifest(): Promise<Manifest>;
|
|
13
|
-
/** Resolve this authoring
|
|
14
|
-
|
|
13
|
+
/** Resolve this authoring configuration into its production Program definition. */
|
|
14
|
+
productionDefinition(): ProgramDefinition;
|
|
15
|
+
/** Resolve this authoring configuration into its development Program definition. */
|
|
16
|
+
developmentDefinition(): ProgramDefinition;
|
|
17
|
+
private definition;
|
|
15
18
|
/** Run the optional author-owned production build command. */
|
|
16
19
|
build(): Promise<void>;
|
|
17
|
-
/** Build
|
|
18
|
-
start(system: SystemContract, options?: ProjectRunOptions): Promise<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}>>;
|
|
27
|
-
/** Build and install this project's production Program. */
|
|
28
|
-
install(system: SystemContract): Promise<SystemProgramEntity>;
|
|
20
|
+
/** Build this Project and return its production Process lifecycle generator. */
|
|
21
|
+
start(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").SystemProcessRunEvent, void, void>>;
|
|
22
|
+
/** Return this Project's development Process lifecycle generator. */
|
|
23
|
+
dev(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").SystemProcessRunEvent, void, void>>;
|
|
24
|
+
/** Build this Project and return its Program installation generator. */
|
|
25
|
+
install(system: SystemContract): Promise<AsyncGenerator<Readonly<{
|
|
26
|
+
stream: "stdout" | "stderr";
|
|
27
|
+
text: string;
|
|
28
|
+
}>, void, void>>;
|
|
29
29
|
/** Build and package this Program into its canonical release shape. */
|
|
30
30
|
pack(): Promise<PackedProject>;
|
|
31
31
|
private run;
|
|
@@ -38,10 +38,6 @@ export interface ProjectRunOptions {
|
|
|
38
38
|
options?: Record<string, string>;
|
|
39
39
|
signal?: AbortSignal;
|
|
40
40
|
}
|
|
41
|
-
export type ProjectRunResult = Readonly<{
|
|
42
|
-
process: SystemProcessEntity;
|
|
43
|
-
exit: Exit;
|
|
44
|
-
}>;
|
|
45
41
|
export type PackedProject = Readonly<{
|
|
46
42
|
archive: string;
|
|
47
43
|
archivePath: string;
|
package/dist/project.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRelativeValue, layers } from "@phreshos/core";
|
|
1
|
+
import { isRelativeValue, layers, } from "@phreshos/core";
|
|
2
2
|
import AdmZip from "adm-zip";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
@@ -6,7 +6,6 @@ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
7
7
|
import { delimiter, isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
8
8
|
import { createJiti } from "jiti";
|
|
9
|
-
import { assertAvailable, commandFailure, DevelopmentClient, waitForDevelopmentClient } from "./client-development.js";
|
|
10
9
|
const configFile = "phresh.config.ts";
|
|
11
10
|
/** One loaded Program authoring project rooted at an absolute directory. */
|
|
12
11
|
export class Project {
|
|
@@ -30,7 +29,7 @@ export class Project {
|
|
|
30
29
|
throw new Error(`${configFile} must export its config as the default export`);
|
|
31
30
|
return new Project(config, resolve(path, ".."));
|
|
32
31
|
}
|
|
33
|
-
/** Create a
|
|
32
|
+
/** Create a Project from an already loaded authoring configuration. */
|
|
34
33
|
static define(config, options = {}) {
|
|
35
34
|
return new Project(config, options.directory ?? process.cwd());
|
|
36
35
|
}
|
|
@@ -45,8 +44,15 @@ export class Project {
|
|
|
45
44
|
}
|
|
46
45
|
return manifest;
|
|
47
46
|
}
|
|
48
|
-
/** Resolve this authoring
|
|
49
|
-
|
|
47
|
+
/** Resolve this authoring configuration into its production Program definition. */
|
|
48
|
+
productionDefinition() {
|
|
49
|
+
return this.definition("production");
|
|
50
|
+
}
|
|
51
|
+
/** Resolve this authoring configuration into its development Program definition. */
|
|
52
|
+
developmentDefinition() {
|
|
53
|
+
return this.definition("development");
|
|
54
|
+
}
|
|
55
|
+
definition(mode) {
|
|
50
56
|
const config = this.config;
|
|
51
57
|
const server = serverHalf(config.server, mode);
|
|
52
58
|
const client = clientHalf(config.client, mode);
|
|
@@ -102,30 +108,20 @@ export class Project {
|
|
|
102
108
|
});
|
|
103
109
|
});
|
|
104
110
|
}
|
|
105
|
-
/** Build
|
|
111
|
+
/** Build this Project and return its production Process lifecycle generator. */
|
|
106
112
|
async start(system, options = {}) {
|
|
107
113
|
await this.build();
|
|
108
|
-
return await this.run(system,
|
|
114
|
+
return await this.run(system, this.productionDefinition(), options);
|
|
109
115
|
}
|
|
110
|
-
/**
|
|
116
|
+
/** Return this Project's development Process lifecycle generator. */
|
|
111
117
|
async dev(system, options = {}) {
|
|
112
|
-
return await this.run(system,
|
|
118
|
+
return await this.run(system, this.developmentDefinition(), options);
|
|
113
119
|
}
|
|
114
|
-
/** Build and
|
|
120
|
+
/** Build this Project and return its Program installation generator. */
|
|
115
121
|
async install(system) {
|
|
116
122
|
await this.build();
|
|
117
|
-
const program = await system.forceCreateProgram(this.
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
for await (const chunk of program.install())
|
|
121
|
-
write(chunk.stream, chunk.text);
|
|
122
|
-
installed = true;
|
|
123
|
-
return program;
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
if (!installed)
|
|
127
|
-
await forgetCurrent(program);
|
|
128
|
-
}
|
|
123
|
+
const program = await system.forceCreateProgram(this.productionDefinition());
|
|
124
|
+
return program.install();
|
|
129
125
|
}
|
|
130
126
|
/** Build and package this Program into its canonical release shape. */
|
|
131
127
|
async pack() {
|
|
@@ -144,7 +140,7 @@ export class Project {
|
|
|
144
140
|
file(zip, this.directory, this.config.icon, "icon.png", "Program icon");
|
|
145
141
|
if (this.config.agent)
|
|
146
142
|
file(zip, this.directory, this.config.agent, "agent.md", "Program agent documentation");
|
|
147
|
-
const declaration = Buffer.from(JSON.stringify(
|
|
143
|
+
const declaration = Buffer.from(JSON.stringify(packageDefinition(this.config, version), null, 4) + "\n");
|
|
148
144
|
zip.addFile("program.json", declaration);
|
|
149
145
|
const archive = `${this.config.identity}@${version}.zip`;
|
|
150
146
|
const bytes = zip.toBuffer();
|
|
@@ -157,96 +153,18 @@ export class Project {
|
|
|
157
153
|
writeFileSync(checksumPath, `${digest} ${archive}\n`);
|
|
158
154
|
return Object.freeze({ archive, archivePath, checksumPath, declarationPath, digest });
|
|
159
155
|
}
|
|
160
|
-
async run(system,
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
? this.config.client?.development
|
|
164
|
-
: undefined;
|
|
165
|
-
const command = development?.startCommand;
|
|
166
|
-
if (command)
|
|
167
|
-
await assertAvailable(development.url);
|
|
168
|
-
const client = command ? new DevelopmentClient(command, this.directory) : undefined;
|
|
169
|
-
const controller = new AbortController();
|
|
170
|
-
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
171
|
-
let program = null;
|
|
172
|
-
try {
|
|
173
|
-
if (development) {
|
|
174
|
-
for await (const event of waitForDevelopmentClient(development, client, signal))
|
|
175
|
-
presentDevelopment(event);
|
|
176
|
-
}
|
|
177
|
-
program = await system.forceCreateProgram(description);
|
|
178
|
-
const run = program.process.run({ options: options.options ?? {} }, { signal });
|
|
179
|
-
const iterator = run[Symbol.asyncIterator]();
|
|
180
|
-
let lifecycle = iterator.next();
|
|
181
|
-
let developmentExit = client?.exited();
|
|
182
|
-
let developmentOutput = client?.outputAvailable();
|
|
183
|
-
let process = null;
|
|
184
|
-
let ending = null;
|
|
185
|
-
while (true) {
|
|
186
|
-
for (const event of client?.drain() ?? [])
|
|
187
|
-
presentDevelopment(event);
|
|
188
|
-
const outcome = await Promise.race([
|
|
189
|
-
lifecycle.then(result => ({ source: "system", result })),
|
|
190
|
-
...(developmentExit ? [developmentExit.then(result => ({ source: "client", result }))] : []),
|
|
191
|
-
...(developmentOutput ? [developmentOutput.then(() => ({ source: "output" }))] : [])
|
|
192
|
-
]);
|
|
193
|
-
if (outcome.source === "output") {
|
|
194
|
-
developmentOutput = client?.outputAvailable();
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (outcome.source === "client") {
|
|
198
|
-
developmentExit = undefined;
|
|
199
|
-
if (!client?.endingWasRequested())
|
|
200
|
-
throw commandFailure(outcome.result);
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (outcome.result.done)
|
|
204
|
-
break;
|
|
205
|
-
const event = outcome.result.value;
|
|
206
|
-
if (event.event === "started")
|
|
207
|
-
process = event.process;
|
|
208
|
-
else if (event.event === "output")
|
|
209
|
-
write(event.stream, event.text);
|
|
210
|
-
else
|
|
211
|
-
ending = event.exit;
|
|
212
|
-
lifecycle = iterator.next();
|
|
213
|
-
}
|
|
214
|
-
if (!process || !ending)
|
|
215
|
-
throw new Error("The System ended the Program run without a complete Process lifecycle");
|
|
216
|
-
return Object.freeze({ process, exit: ending });
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
controller.abort(new Error("The Project run ended"));
|
|
220
|
-
await client?.stop();
|
|
221
|
-
if (program)
|
|
222
|
-
await forgetCurrent(program);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
function presentDevelopment(event) {
|
|
227
|
-
if (event.event === "output")
|
|
228
|
-
write(event.stream === "err" ? "stderr" : "stdout", String(event.text ?? ""));
|
|
229
|
-
}
|
|
230
|
-
function write(stream, text) {
|
|
231
|
-
(stream === "stderr" ? process.stderr : process.stdout).write(text);
|
|
232
|
-
}
|
|
233
|
-
async function forgetCurrent(program) {
|
|
234
|
-
try {
|
|
235
|
-
await program.forget();
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
if (error instanceof Error && error.message === "The Program represented by this handle does not exist")
|
|
239
|
-
return;
|
|
240
|
-
throw error;
|
|
156
|
+
async run(system, definition, options) {
|
|
157
|
+
const program = await system.forceCreateProgram(definition);
|
|
158
|
+
return program.process.run({ options: options.options ?? {} }, { signal: options.signal });
|
|
241
159
|
}
|
|
242
160
|
}
|
|
243
161
|
function serverHalf(half, mode) {
|
|
244
162
|
if (!half)
|
|
245
163
|
return null;
|
|
246
|
-
const { development, startCommand, entryFile, ...
|
|
164
|
+
const { development, startCommand, entryFile, ...declared } = half;
|
|
247
165
|
if (mode === "production" || !development)
|
|
248
|
-
return { ...
|
|
249
|
-
return { ...
|
|
166
|
+
return { ...declared, ...serverExecution({ startCommand, entryFile }) };
|
|
167
|
+
return { ...declared, location: ".", ...serverExecution(development) };
|
|
250
168
|
}
|
|
251
169
|
function serverExecution(server) {
|
|
252
170
|
return server.startCommand !== undefined ? { startCommand: server.startCommand } : { entryFile: server.entryFile };
|
|
@@ -337,7 +255,7 @@ function commandEnvironment(directory) {
|
|
|
337
255
|
const inherited = process.env[key];
|
|
338
256
|
return { ...process.env, [key]: [join(directory, "node_modules", ".bin"), inherited].filter(Boolean).join(delimiter) };
|
|
339
257
|
}
|
|
340
|
-
function
|
|
258
|
+
function packageDefinition(config, version) {
|
|
341
259
|
return {
|
|
342
260
|
identity: config.identity,
|
|
343
261
|
name: config.name,
|
package/dist/system.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ClientServiceHandler as CoreClientServiceHandler, ServerServiceHandler as CoreServerServiceHandler, type ClientServiceChannel, type
|
|
1
|
+
import { Client as CoreClient, ClientServiceHandler as CoreClientServiceHandler, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerServiceHandler as CoreServerServiceHandler, type ClientDeclaration, type ClientServiceChannel, type EndpointDeclaration, type Launch, type LaunchClient, type Position, type ProgramDefinition, type ProgramCommandChunk, type ServerServiceChannel, type ServiceKey, type Size, type System as CoreSystem, type SystemClientEntity, type SystemEndpointEntity, type SystemProcessEntity, type SystemProcess, type SystemProgram, type SystemProgramEntity, type SystemProgramProcessEvents, type SystemServerEntity, type SystemUploads, type WritableAppearance, type Window, type WindowEvents, type WindowGeometry } from "@phreshos/core";
|
|
2
|
+
import Events from "./events.js";
|
|
2
3
|
import { type TransportEvent } from "./transport.js";
|
|
3
4
|
export type ProgramProcessRunOptions = Readonly<{
|
|
4
5
|
signal?: AbortSignal;
|
|
@@ -18,6 +19,10 @@ interface SystemTransport {
|
|
|
18
19
|
api(request: object, signal?: AbortSignal): Promise<unknown>;
|
|
19
20
|
lifecycle(request: object, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, void>;
|
|
20
21
|
}
|
|
22
|
+
declare const ProgramBase: new () => object;
|
|
23
|
+
declare const ProcessBase: new () => object;
|
|
24
|
+
declare const ServerBase: new () => object;
|
|
25
|
+
declare const ClientBase: new () => object;
|
|
21
26
|
/** One connected owner-local implementation of the shared System contract. */
|
|
22
27
|
export declare class System implements CoreSystem {
|
|
23
28
|
private readonly connection;
|
|
@@ -31,11 +36,12 @@ export declare class System implements CoreSystem {
|
|
|
31
36
|
readonly transport: SystemTransport;
|
|
32
37
|
private closed;
|
|
33
38
|
private readonly lifetime;
|
|
39
|
+
private readonly handles;
|
|
34
40
|
private constructor();
|
|
35
41
|
/** Connect to the System selected by argument, environment, or owner default. */
|
|
36
42
|
static connect(home?: string): Promise<System>;
|
|
37
43
|
/** Atomically replace one runtime Program without touching its installed form. */
|
|
38
|
-
forceCreateProgram(source:
|
|
44
|
+
forceCreateProgram(source: ProgramDefinition | string): Promise<SystemProgramEntity>;
|
|
39
45
|
/** Close this owner connection and abort every attached operation it owns. */
|
|
40
46
|
disconnect(): Promise<void>;
|
|
41
47
|
service<EventsMap extends object = {}>(key: ServiceKey & {
|
|
@@ -44,9 +50,149 @@ export declare class System implements CoreSystem {
|
|
|
44
50
|
service<EventsMap extends object = {}>(key: ServiceKey & {
|
|
45
51
|
endpoint: "client";
|
|
46
52
|
}): ClientService<EventsMap>;
|
|
53
|
+
programHandle(snapshot: ProgramSnapshot): ProgramHandle;
|
|
54
|
+
processHandle(snapshot: ProcessSnapshot): ProcessHandle;
|
|
47
55
|
private signal;
|
|
48
56
|
private requireConnected;
|
|
49
57
|
}
|
|
58
|
+
interface ProgramHandle extends SystemProgramEntity {
|
|
59
|
+
}
|
|
60
|
+
declare class ProgramHandle extends ProgramBase {
|
|
61
|
+
private readonly system;
|
|
62
|
+
private readonly reference;
|
|
63
|
+
readonly identity: string;
|
|
64
|
+
readonly process: ProgramProcesses;
|
|
65
|
+
readonly startup: ProgramStartup;
|
|
66
|
+
private snapshot;
|
|
67
|
+
constructor(system: System, snapshot: ProgramSnapshot);
|
|
68
|
+
get name(): string;
|
|
69
|
+
get version(): string | null;
|
|
70
|
+
get description(): string | null;
|
|
71
|
+
get hasAgent(): boolean;
|
|
72
|
+
get server(): EndpointDeclaration | null;
|
|
73
|
+
get client(): ClientDeclaration | null;
|
|
74
|
+
update(snapshot: ProgramSnapshot): void;
|
|
75
|
+
agent(): Promise<string | null>;
|
|
76
|
+
installed(): Promise<boolean>;
|
|
77
|
+
install(): AsyncGenerator<Readonly<{
|
|
78
|
+
stream: "stdout" | "stderr";
|
|
79
|
+
text: string;
|
|
80
|
+
}>, void, void>;
|
|
81
|
+
uninstall(everything?: boolean): AsyncGenerator<Readonly<{
|
|
82
|
+
stream: "stdout" | "stderr";
|
|
83
|
+
text: string;
|
|
84
|
+
}>, void, void>;
|
|
85
|
+
forget(): Promise<void>;
|
|
86
|
+
address(): Readonly<{
|
|
87
|
+
identity: string;
|
|
88
|
+
reference: string;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
declare class ProgramStartup {
|
|
92
|
+
private readonly system;
|
|
93
|
+
private readonly program;
|
|
94
|
+
constructor(system: System, program: ProgramHandle);
|
|
95
|
+
get(): Promise<Readonly<{
|
|
96
|
+
name?: string;
|
|
97
|
+
server?: boolean;
|
|
98
|
+
client?: boolean | LaunchClient;
|
|
99
|
+
options?: Readonly<Record<string, string>>;
|
|
100
|
+
}> | null>;
|
|
101
|
+
enable(launch?: Launch): Promise<void>;
|
|
102
|
+
disable(): Promise<void>;
|
|
103
|
+
private change;
|
|
104
|
+
}
|
|
105
|
+
declare class ProgramProcesses extends Events<SystemProgramProcessEvents> {
|
|
106
|
+
private readonly system;
|
|
107
|
+
private readonly program;
|
|
108
|
+
constructor(system: System, program: ProgramHandle);
|
|
109
|
+
list(): Promise<ProcessHandle[]>;
|
|
110
|
+
first(): Promise<ProcessHandle | null>;
|
|
111
|
+
last(): Promise<ProcessHandle | null>;
|
|
112
|
+
find(identityOrName: string): Promise<ProcessHandle | null>;
|
|
113
|
+
create(launch?: Launch): Promise<ProcessHandle>;
|
|
114
|
+
run(launch?: Launch, options?: ProgramProcessRunOptions): AsyncGenerator<ProgramProcessRunEvent, void, void>;
|
|
115
|
+
findOrCreate(launch: Launch & {
|
|
116
|
+
name: string;
|
|
117
|
+
}): Promise<ProcessHandle>;
|
|
118
|
+
exitAll(): Promise<string[]>;
|
|
119
|
+
private createExact;
|
|
120
|
+
}
|
|
121
|
+
interface ProcessHandle extends SystemProcessEntity {
|
|
122
|
+
}
|
|
123
|
+
declare class ProcessHandle extends ProcessBase {
|
|
124
|
+
private readonly system;
|
|
125
|
+
private readonly snapshot;
|
|
126
|
+
readonly identity: string;
|
|
127
|
+
readonly name: string | null;
|
|
128
|
+
readonly startedAt: Date;
|
|
129
|
+
readonly server: ServerEndpoint;
|
|
130
|
+
readonly client: ClientEndpoint;
|
|
131
|
+
constructor(system: System, snapshot: ProcessSnapshot);
|
|
132
|
+
program(): ProgramHandle;
|
|
133
|
+
exit(): Promise<void>;
|
|
134
|
+
exited(): Promise<boolean>;
|
|
135
|
+
}
|
|
136
|
+
interface ServerEndpoint extends SystemServerEntity {
|
|
137
|
+
}
|
|
138
|
+
declare class ServerEndpoint extends ServerBase {
|
|
139
|
+
private readonly system;
|
|
140
|
+
private readonly owner;
|
|
141
|
+
readonly endpoint: "server";
|
|
142
|
+
private readonly base;
|
|
143
|
+
constructor(system: System, owner: ProcessHandle);
|
|
144
|
+
process(): Promise<ProcessHandle>;
|
|
145
|
+
exists(): Promise<boolean>;
|
|
146
|
+
start(): Promise<void>;
|
|
147
|
+
stop(): Promise<void>;
|
|
148
|
+
publish(event: string, payload?: unknown): void;
|
|
149
|
+
ask<Answer = unknown>(event: string, payload?: unknown): Promise<Answer>;
|
|
150
|
+
timeout(milliseconds: number): {
|
|
151
|
+
ask: <Answer = unknown>(event: string, payload?: unknown) => Promise<Answer>;
|
|
152
|
+
};
|
|
153
|
+
waitReady(timeout?: number): Promise<void>;
|
|
154
|
+
service<EventsMap extends object = {}>(): Promise<ServerService<EventsMap> | null>;
|
|
155
|
+
}
|
|
156
|
+
interface ClientEndpoint extends SystemClientEntity {
|
|
157
|
+
}
|
|
158
|
+
declare class ClientEndpoint extends ClientBase {
|
|
159
|
+
readonly endpoint: "client";
|
|
160
|
+
readonly window: SystemWindow;
|
|
161
|
+
private readonly base;
|
|
162
|
+
constructor(system: System, owner: ProcessHandle);
|
|
163
|
+
process(): Promise<ProcessHandle>;
|
|
164
|
+
exists(): Promise<boolean>;
|
|
165
|
+
start(overrides?: LaunchClient): Promise<void>;
|
|
166
|
+
stop(): Promise<void>;
|
|
167
|
+
publish(event: string, payload?: unknown): void;
|
|
168
|
+
service<EventsMap extends object = {}>(): Promise<ClientService<EventsMap> | null>;
|
|
169
|
+
}
|
|
170
|
+
declare class SystemWindow extends Events<WindowEvents> implements Window {
|
|
171
|
+
private readonly system;
|
|
172
|
+
private readonly process;
|
|
173
|
+
constructor(system: System, process: ProcessHandle);
|
|
174
|
+
title(): Promise<string>;
|
|
175
|
+
position(): Promise<Readonly<{
|
|
176
|
+
x: import("@phreshos/core").Value;
|
|
177
|
+
y: import("@phreshos/core").Value;
|
|
178
|
+
}>>;
|
|
179
|
+
size(): Promise<Readonly<{
|
|
180
|
+
width: import("@phreshos/core").Value;
|
|
181
|
+
height: import("@phreshos/core").Value;
|
|
182
|
+
}>>;
|
|
183
|
+
minimized(): Promise<boolean>;
|
|
184
|
+
front(): Promise<boolean>;
|
|
185
|
+
layer(): Promise<import("@phreshos/core").Layer>;
|
|
186
|
+
location(): Promise<string>;
|
|
187
|
+
move(position: Position): Promise<void>;
|
|
188
|
+
resize(size: Size): Promise<void>;
|
|
189
|
+
setGeometry(geometry: WindowGeometry): Promise<void>;
|
|
190
|
+
minimize(minimized?: boolean): Promise<void>;
|
|
191
|
+
changeTitle(title: string): Promise<void>;
|
|
192
|
+
raise(): Promise<void>;
|
|
193
|
+
private snapshot;
|
|
194
|
+
private change;
|
|
195
|
+
}
|
|
50
196
|
declare class ServerService<EventsMap extends object = {}> extends CoreServerServiceHandler<EventsMap> {
|
|
51
197
|
readonly name: string;
|
|
52
198
|
readonly channel: ServerServiceChannel<EventsMap>;
|
|
@@ -67,4 +213,43 @@ declare class ClientService<EventsMap extends object = {}> extends CoreClientSer
|
|
|
67
213
|
enabled(): Promise<boolean>;
|
|
68
214
|
waitReady(timeout?: number): Promise<void>;
|
|
69
215
|
}
|
|
216
|
+
interface ProgramSnapshot {
|
|
217
|
+
reference: string;
|
|
218
|
+
identity: string;
|
|
219
|
+
name: string;
|
|
220
|
+
version: string | null;
|
|
221
|
+
description: string | null;
|
|
222
|
+
installed?: boolean;
|
|
223
|
+
hasAgent: boolean;
|
|
224
|
+
server: {
|
|
225
|
+
start: boolean;
|
|
226
|
+
} | null;
|
|
227
|
+
client: ClientDeclaration | null;
|
|
228
|
+
}
|
|
229
|
+
interface ProcessSnapshot {
|
|
230
|
+
reference: string;
|
|
231
|
+
identity: string;
|
|
232
|
+
name: string | null;
|
|
233
|
+
program: string;
|
|
234
|
+
programSnapshot?: ProgramSnapshot;
|
|
235
|
+
startedAt: string;
|
|
236
|
+
server: {
|
|
237
|
+
declared: boolean;
|
|
238
|
+
running: boolean;
|
|
239
|
+
};
|
|
240
|
+
client: {
|
|
241
|
+
declared: boolean;
|
|
242
|
+
running: boolean;
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
export type Program = SystemProgramEntity;
|
|
246
|
+
export declare const Program: typeof CoreProgram;
|
|
247
|
+
export type Process = SystemProcessEntity;
|
|
248
|
+
export declare const Process: typeof CoreProcess;
|
|
249
|
+
export type Endpoint<EventsMap extends object = {}> = SystemEndpointEntity<EventsMap>;
|
|
250
|
+
export declare const Endpoint: typeof CoreEndpoint;
|
|
251
|
+
export type Server<EventsMap extends object = {}> = SystemServerEntity<EventsMap>;
|
|
252
|
+
export declare const Server: typeof CoreServer;
|
|
253
|
+
export type Client<EventsMap extends object = {}> = SystemClientEntity<EventsMap>;
|
|
254
|
+
export declare const Client: typeof CoreClient;
|
|
70
255
|
export {};
|
package/dist/system.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { ClientServiceHandler as CoreClientServiceHandler, ServerServiceHandler as CoreServerServiceHandler } from "@phreshos/core";
|
|
1
|
+
import { Client as CoreClient, ClientServiceHandler as CoreClientServiceHandler, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerServiceHandler as CoreServerServiceHandler } 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
|
+
import HandleRegistry from "./handle-registry.js";
|
|
5
6
|
import { resolveHome } from "./home.js";
|
|
6
7
|
import { filesystemStorage } from "./storage.js";
|
|
7
8
|
import { openConnection, request, streamProgram } from "./transport.js";
|
|
8
9
|
import Uploads from "./uploads.js";
|
|
10
|
+
const ProgramBase = CoreProgram;
|
|
11
|
+
const ProcessBase = CoreProcess;
|
|
12
|
+
const ServerBase = CoreServer;
|
|
13
|
+
const ClientBase = CoreClient;
|
|
9
14
|
/** One connected owner-local implementation of the shared System contract. */
|
|
10
15
|
export class System {
|
|
11
16
|
connection;
|
|
@@ -19,6 +24,7 @@ export class System {
|
|
|
19
24
|
transport;
|
|
20
25
|
closed = false;
|
|
21
26
|
lifetime = new AbortController();
|
|
27
|
+
handles = new HandleRegistry();
|
|
22
28
|
constructor(home, address, connection) {
|
|
23
29
|
this.connection = connection;
|
|
24
30
|
this.home = home;
|
|
@@ -44,7 +50,7 @@ export class System {
|
|
|
44
50
|
this.requireConnected();
|
|
45
51
|
for await (const event of this.transport.lifecycle({ word: "force-create", program: source })) {
|
|
46
52
|
if (event.event === "created")
|
|
47
|
-
return
|
|
53
|
+
return this.programHandle(required(event.program));
|
|
48
54
|
}
|
|
49
55
|
throw new Error("The System did not confirm the created Program");
|
|
50
56
|
}
|
|
@@ -55,6 +61,7 @@ export class System {
|
|
|
55
61
|
this.closed = true;
|
|
56
62
|
this.lifetime.abort(new Error("This System connection is closed"));
|
|
57
63
|
this.connection.destroy();
|
|
64
|
+
this.handles.clear();
|
|
58
65
|
}
|
|
59
66
|
service(key) {
|
|
60
67
|
this.requireConnected();
|
|
@@ -62,6 +69,14 @@ export class System {
|
|
|
62
69
|
? new ServerService(this, key)
|
|
63
70
|
: new ClientService(this, key);
|
|
64
71
|
}
|
|
72
|
+
programHandle(snapshot) {
|
|
73
|
+
const handle = this.handles.obtain(`program:${snapshot.reference}`, () => new ProgramHandle(this, snapshot));
|
|
74
|
+
handle.update(snapshot);
|
|
75
|
+
return handle;
|
|
76
|
+
}
|
|
77
|
+
processHandle(snapshot) {
|
|
78
|
+
return this.handles.obtain(`process:${snapshot.reference}`, () => new ProcessHandle(this, snapshot));
|
|
79
|
+
}
|
|
65
80
|
signal(signal) {
|
|
66
81
|
this.requireConnected();
|
|
67
82
|
return signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
|
|
@@ -100,7 +115,7 @@ class ProgramRegistry extends Events {
|
|
|
100
115
|
operation: "list",
|
|
101
116
|
input: { installedOnly: onlyInstalled, limit: 100, offset }
|
|
102
117
|
});
|
|
103
|
-
programs.push(...page.data.map(snapshot =>
|
|
118
|
+
programs.push(...page.data.map(snapshot => this.system.programHandle(snapshot)));
|
|
104
119
|
offset += page.data.length;
|
|
105
120
|
if (!page.truncated || !page.data.length)
|
|
106
121
|
return programs;
|
|
@@ -109,7 +124,7 @@ class ProgramRegistry extends Events {
|
|
|
109
124
|
async find(identity) {
|
|
110
125
|
try {
|
|
111
126
|
const snapshot = await this.system.transport.control({ capability: "program", operation: "inspect", input: { program: identity } });
|
|
112
|
-
return
|
|
127
|
+
return this.system.programHandle(snapshot);
|
|
113
128
|
}
|
|
114
129
|
catch (error) {
|
|
115
130
|
if (unknown(error, "Program"))
|
|
@@ -120,7 +135,7 @@ class ProgramRegistry extends Events {
|
|
|
120
135
|
async create(source) {
|
|
121
136
|
for await (const event of this.system.transport.lifecycle({ word: "create", program: source })) {
|
|
122
137
|
if (event.event === "created")
|
|
123
|
-
return
|
|
138
|
+
return this.system.programHandle(required(event.program));
|
|
124
139
|
}
|
|
125
140
|
throw new Error("The System did not confirm the created Program");
|
|
126
141
|
}
|
|
@@ -128,46 +143,52 @@ class ProgramRegistry extends Events {
|
|
|
128
143
|
const waited = value;
|
|
129
144
|
if (waited.event === "uninstall") {
|
|
130
145
|
const payload = waited.payload;
|
|
131
|
-
return { program:
|
|
146
|
+
return { program: this.system.programHandle(required(payload.program)), everythingRemoved: payload.everythingRemoved === true };
|
|
132
147
|
}
|
|
133
|
-
return
|
|
148
|
+
return this.system.programHandle(required(waited.payload));
|
|
134
149
|
}
|
|
135
150
|
}
|
|
136
|
-
class ProgramHandle extends
|
|
151
|
+
class ProgramHandle extends ProgramBase {
|
|
137
152
|
system;
|
|
138
153
|
reference;
|
|
139
154
|
identity;
|
|
140
|
-
name;
|
|
141
|
-
version;
|
|
142
|
-
description;
|
|
143
|
-
hasAgent;
|
|
144
|
-
server;
|
|
145
|
-
client;
|
|
146
155
|
process;
|
|
147
156
|
startup;
|
|
157
|
+
snapshot;
|
|
148
158
|
constructor(system, snapshot) {
|
|
149
|
-
super(
|
|
150
|
-
capability: "program", operation: "wait", input: { program: snapshot.identity, event, timeout }
|
|
151
|
-
}, signal).then(value => value.payload));
|
|
159
|
+
super();
|
|
152
160
|
this.system = system;
|
|
161
|
+
this.snapshot = snapshot;
|
|
162
|
+
bindEvents(this, new Events(["forget", "uninstall"], (event, signal, timeout) => system.transport.control({
|
|
163
|
+
capability: "program", operation: "wait", input: { program: snapshot.identity, event, timeout }
|
|
164
|
+
}, signal).then(value => value.payload)));
|
|
153
165
|
this.reference = snapshot.reference;
|
|
154
166
|
this.identity = snapshot.identity;
|
|
155
|
-
this.name = snapshot.name;
|
|
156
|
-
this.version = snapshot.version;
|
|
157
|
-
this.description = snapshot.description;
|
|
158
|
-
this.hasAgent = snapshot.hasAgent;
|
|
159
|
-
this.server = snapshot.server ? Object.freeze({ start: snapshot.server.start }) : null;
|
|
160
|
-
this.client = snapshot.client ? Object.freeze({
|
|
161
|
-
start: snapshot.client.start,
|
|
162
|
-
title: snapshot.client.title,
|
|
163
|
-
size: snapshot.client.size,
|
|
164
|
-
position: snapshot.client.position,
|
|
165
|
-
layer: snapshot.client.layer,
|
|
166
|
-
minimize: snapshot.client.minimize
|
|
167
|
-
}) : null;
|
|
168
167
|
this.process = new ProgramProcesses(system, this);
|
|
169
168
|
this.startup = new ProgramStartup(system, this);
|
|
170
169
|
}
|
|
170
|
+
get name() { return this.snapshot.name; }
|
|
171
|
+
get version() { return this.snapshot.version; }
|
|
172
|
+
get description() { return this.snapshot.description; }
|
|
173
|
+
get hasAgent() { return this.snapshot.hasAgent; }
|
|
174
|
+
get server() {
|
|
175
|
+
return this.snapshot.server ? Object.freeze({ start: this.snapshot.server.start }) : null;
|
|
176
|
+
}
|
|
177
|
+
get client() {
|
|
178
|
+
return this.snapshot.client ? Object.freeze({
|
|
179
|
+
start: this.snapshot.client.start,
|
|
180
|
+
title: this.snapshot.client.title,
|
|
181
|
+
size: this.snapshot.client.size,
|
|
182
|
+
position: this.snapshot.client.position,
|
|
183
|
+
layer: this.snapshot.client.layer,
|
|
184
|
+
minimize: this.snapshot.client.minimize
|
|
185
|
+
}) : null;
|
|
186
|
+
}
|
|
187
|
+
update(snapshot) {
|
|
188
|
+
if (snapshot.reference !== this.reference)
|
|
189
|
+
throw new Error("A Program handle cannot become another Program");
|
|
190
|
+
this.snapshot = snapshot;
|
|
191
|
+
}
|
|
171
192
|
async agent() {
|
|
172
193
|
if (!this.hasAgent)
|
|
173
194
|
return null;
|
|
@@ -246,7 +267,7 @@ class ProgramProcesses extends Events {
|
|
|
246
267
|
launch
|
|
247
268
|
}, options.signal)) {
|
|
248
269
|
if (event.event === "started") {
|
|
249
|
-
process =
|
|
270
|
+
process = this.system.processHandle(required(event.process));
|
|
250
271
|
yield Object.freeze({ event: "started", process });
|
|
251
272
|
}
|
|
252
273
|
else if (event.event === "output") {
|
|
@@ -289,7 +310,7 @@ class ProgramProcesses extends Events {
|
|
|
289
310
|
async createExact(word, launch) {
|
|
290
311
|
for await (const event of this.system.transport.lifecycle({ word, handle: this.program.address(), launch })) {
|
|
291
312
|
if (event.event === "createdProcess")
|
|
292
|
-
return
|
|
313
|
+
return this.system.processHandle(required(event.process));
|
|
293
314
|
}
|
|
294
315
|
throw new Error("The System did not confirm the created Process");
|
|
295
316
|
}
|
|
@@ -306,7 +327,7 @@ class ProcessRegistry extends Events {
|
|
|
306
327
|
async find(identity) {
|
|
307
328
|
try {
|
|
308
329
|
const snapshot = await this.system.transport.control({ capability: "process", operation: "inspect", input: { process: identity } });
|
|
309
|
-
return
|
|
330
|
+
return this.system.processHandle(snapshot);
|
|
310
331
|
}
|
|
311
332
|
catch (error) {
|
|
312
333
|
if (unknown(error, "Process"))
|
|
@@ -315,7 +336,7 @@ class ProcessRegistry extends Events {
|
|
|
315
336
|
}
|
|
316
337
|
}
|
|
317
338
|
}
|
|
318
|
-
class ProcessHandle extends
|
|
339
|
+
class ProcessHandle extends ProcessBase {
|
|
319
340
|
system;
|
|
320
341
|
snapshot;
|
|
321
342
|
identity;
|
|
@@ -324,26 +345,28 @@ class ProcessHandle extends Events {
|
|
|
324
345
|
server;
|
|
325
346
|
client;
|
|
326
347
|
constructor(system, snapshot) {
|
|
327
|
-
super(
|
|
328
|
-
capability: "process", operation: "wait", input: { process: snapshot.identity, event, timeout }
|
|
329
|
-
}, signal).then(value => processEvent(system, value)));
|
|
348
|
+
super();
|
|
330
349
|
this.system = system;
|
|
331
350
|
this.snapshot = snapshot;
|
|
351
|
+
bindEvents(this, new Events(["endpointStart", "endpointStop", "exit"], (event, signal, timeout) => system.transport.control({
|
|
352
|
+
capability: "process", operation: "wait", input: { process: snapshot.identity, event, timeout }
|
|
353
|
+
}, signal).then(value => processEvent(system, value))));
|
|
332
354
|
this.identity = snapshot.identity;
|
|
333
355
|
this.name = snapshot.name;
|
|
334
356
|
this.startedAt = new Date(snapshot.startedAt);
|
|
335
357
|
this.server = new ServerEndpoint(system, this);
|
|
336
358
|
this.client = new ClientEndpoint(system, this);
|
|
337
359
|
}
|
|
338
|
-
program() { return
|
|
360
|
+
program() { return this.system.programHandle(required(this.snapshot.programSnapshot, this.snapshot.program)); }
|
|
339
361
|
async exit() {
|
|
340
362
|
await this.system.transport.control({ capability: "process", operation: "exit", input: { process: this.identity } });
|
|
341
363
|
}
|
|
342
364
|
async exited() { return await this.system.process.find(this.identity) === null; }
|
|
343
365
|
}
|
|
344
|
-
class
|
|
366
|
+
class EndpointOperations extends Events {
|
|
345
367
|
system;
|
|
346
368
|
owner;
|
|
369
|
+
endpoint;
|
|
347
370
|
constructor(system, owner, endpoint) {
|
|
348
371
|
super([], (event, signal, timeout) => event === null
|
|
349
372
|
? system.transport.api({ capability: "endpoint", operation: "wait", process: owner.identity, endpoint, event, timeout }, signal)
|
|
@@ -352,13 +375,14 @@ class EndpointHandle extends Events {
|
|
|
352
375
|
}, signal).then(value => value.payload));
|
|
353
376
|
this.system = system;
|
|
354
377
|
this.owner = owner;
|
|
378
|
+
this.endpoint = endpoint;
|
|
355
379
|
}
|
|
356
380
|
process() { return Promise.resolve(this.owner); }
|
|
357
381
|
async exists() {
|
|
358
382
|
const value = await this.inspect();
|
|
359
383
|
return value.running;
|
|
360
384
|
}
|
|
361
|
-
async start() { await this.operation("start"); }
|
|
385
|
+
async start(client) { await this.operation("start", client); }
|
|
362
386
|
async stop() { await this.operation("stop"); }
|
|
363
387
|
async service() {
|
|
364
388
|
const key = await this.system.transport.api({ capability: "endpoint", operation: "service", process: this.owner.identity, endpoint: this.endpoint });
|
|
@@ -380,9 +404,23 @@ class EndpointHandle extends Events {
|
|
|
380
404
|
} });
|
|
381
405
|
}
|
|
382
406
|
}
|
|
383
|
-
class ServerEndpoint extends
|
|
407
|
+
class ServerEndpoint extends ServerBase {
|
|
408
|
+
system;
|
|
409
|
+
owner;
|
|
384
410
|
endpoint = "server";
|
|
385
|
-
|
|
411
|
+
base;
|
|
412
|
+
constructor(system, owner) {
|
|
413
|
+
super();
|
|
414
|
+
this.system = system;
|
|
415
|
+
this.owner = owner;
|
|
416
|
+
this.base = new EndpointOperations(system, owner, "server");
|
|
417
|
+
bindEvents(this, this.base);
|
|
418
|
+
}
|
|
419
|
+
process() { return this.base.process(); }
|
|
420
|
+
exists() { return this.base.exists(); }
|
|
421
|
+
start() { return this.base.start(); }
|
|
422
|
+
stop() { return this.base.stop(); }
|
|
423
|
+
publish(event, payload) { return this.base.publish(event, payload); }
|
|
386
424
|
async ask(event, payload) {
|
|
387
425
|
return await this.system.transport.control({ capability: "endpoint", operation: "ask", input: {
|
|
388
426
|
process: this.owner.identity, endpoint: "server", event, payload
|
|
@@ -397,19 +435,26 @@ class ServerEndpoint extends EndpointHandle {
|
|
|
397
435
|
await this.system.transport.control({ capability: "endpoint", operation: "waitReady", input: { process: this.owner.identity, endpoint: "server", timeout } });
|
|
398
436
|
}
|
|
399
437
|
async service() {
|
|
400
|
-
return await
|
|
438
|
+
return await this.base.service();
|
|
401
439
|
}
|
|
402
440
|
}
|
|
403
|
-
class ClientEndpoint extends
|
|
441
|
+
class ClientEndpoint extends ClientBase {
|
|
404
442
|
endpoint = "client";
|
|
405
443
|
window;
|
|
444
|
+
base;
|
|
406
445
|
constructor(system, owner) {
|
|
407
|
-
super(
|
|
446
|
+
super();
|
|
447
|
+
this.base = new EndpointOperations(system, owner, "client");
|
|
448
|
+
bindEvents(this, this.base);
|
|
408
449
|
this.window = new SystemWindow(system, owner);
|
|
409
450
|
}
|
|
410
|
-
|
|
451
|
+
process() { return this.base.process(); }
|
|
452
|
+
exists() { return this.base.exists(); }
|
|
453
|
+
start(overrides) { return this.base.start(overrides); }
|
|
454
|
+
stop() { return this.base.stop(); }
|
|
455
|
+
publish(event, payload) { return this.base.publish(event, payload); }
|
|
411
456
|
async service() {
|
|
412
|
-
return await
|
|
457
|
+
return await this.base.service();
|
|
413
458
|
}
|
|
414
459
|
}
|
|
415
460
|
class SystemWindow extends Events {
|
|
@@ -512,7 +557,7 @@ async function listProcesses(system, program) {
|
|
|
512
557
|
let offset = 0;
|
|
513
558
|
while (true) {
|
|
514
559
|
const page = await system.transport.control({ capability: "process", operation: "list", input: { program, limit: 100, offset } });
|
|
515
|
-
processes.push(...page.data.map(snapshot =>
|
|
560
|
+
processes.push(...page.data.map(snapshot => system.processHandle(snapshot)));
|
|
516
561
|
offset += page.data.length;
|
|
517
562
|
if (!page.truncated || !page.data.length)
|
|
518
563
|
return processes;
|
|
@@ -532,19 +577,22 @@ function processEvent(system, value) {
|
|
|
532
577
|
const payload = waited.payload;
|
|
533
578
|
if (waited.event === "exit" && payload)
|
|
534
579
|
return {
|
|
535
|
-
process:
|
|
580
|
+
process: system.processHandle(required(payload.processSnapshot, String(payload.process ?? ""))),
|
|
536
581
|
status: payload.status,
|
|
537
582
|
code: payload.code,
|
|
538
583
|
signal: payload.signal
|
|
539
584
|
};
|
|
540
585
|
if ((waited.event === "endpointStart" || waited.event === "endpointStop") && payload?.processSnapshot) {
|
|
541
|
-
const process =
|
|
586
|
+
const process = system.processHandle(payload.processSnapshot);
|
|
542
587
|
return payload.endpoint === "client" ? process.client : process.server;
|
|
543
588
|
}
|
|
544
589
|
if (payload && typeof payload.identity === "string")
|
|
545
|
-
return
|
|
590
|
+
return system.processHandle(payload);
|
|
546
591
|
return payload;
|
|
547
592
|
}
|
|
593
|
+
function bindEvents(target, events) {
|
|
594
|
+
Object.assign(target, eventsOf(events));
|
|
595
|
+
}
|
|
548
596
|
function eventsOf(events) {
|
|
549
597
|
return {
|
|
550
598
|
subscribe: events.subscribe,
|
|
@@ -560,3 +608,8 @@ function required(value, identity = "") {
|
|
|
560
608
|
return value;
|
|
561
609
|
throw new Error(`The System returned no ${identity ? `${identity} ` : ""}snapshot`);
|
|
562
610
|
}
|
|
611
|
+
export const Program = CoreProgram;
|
|
612
|
+
export const Process = CoreProcess;
|
|
613
|
+
export const Endpoint = CoreEndpoint;
|
|
614
|
+
export const Server = CoreServer;
|
|
615
|
+
export const Client = CoreClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Node.js access to PhreshOS and Program projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"prepack": "node --run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@phreshos/core": "^0.1.
|
|
35
|
+
"@phreshos/core": "^0.1.23",
|
|
36
36
|
"adm-zip": "^0.6.0",
|
|
37
37
|
"jiti": "^2.7.0"
|
|
38
38
|
},
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { ClientDevelopment } from "@phreshos/core";
|
|
2
|
-
import type { TransportEvent } from "./transport.js";
|
|
3
|
-
/** One Client development server owned by a Project development run. */
|
|
4
|
-
export declare class DevelopmentClient {
|
|
5
|
-
private readonly child;
|
|
6
|
-
private readonly output;
|
|
7
|
-
private stopped;
|
|
8
|
-
private result;
|
|
9
|
-
private outputWaiter;
|
|
10
|
-
private readonly completion;
|
|
11
|
-
constructor(command: string, directory: string);
|
|
12
|
-
drain(): TransportEvent[];
|
|
13
|
-
exited(): Promise<CommandExit>;
|
|
14
|
-
exitResult(): CommandExit | null;
|
|
15
|
-
outputAvailable(): Promise<void>;
|
|
16
|
-
stop(): Promise<void>;
|
|
17
|
-
endingWasRequested(): boolean;
|
|
18
|
-
private push;
|
|
19
|
-
}
|
|
20
|
-
/** Refuse to claim a URL already served by an unrelated process. */
|
|
21
|
-
export declare function assertAvailable(url: string): Promise<void>;
|
|
22
|
-
/** Wait until a development Client can be loaded by a sandboxed Program iframe. */
|
|
23
|
-
export declare function waitForDevelopmentClient(config: ClientDevelopment, client?: DevelopmentClient, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
|
|
24
|
-
export declare function commandFailure(exit: CommandExit): Error;
|
|
25
|
-
export interface CommandExit {
|
|
26
|
-
code: number | null;
|
|
27
|
-
signal: NodeJS.Signals | null;
|
|
28
|
-
error: Error | null;
|
|
29
|
-
}
|
|
@@ -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 Project 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
|
-
}
|