@phreshos/node 0.1.0 → 0.1.1

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 CHANGED
@@ -3,37 +3,39 @@
3
3
  The Node.js interface for a running PhreshOS System and local Program projects.
4
4
 
5
5
  ```ts
6
- import { Gateway, Project } from "@phreshos/node"
6
+ import { Project, System } from "@phreshos/node"
7
7
 
8
8
  const project = await Project.open()
9
- const gateway = await Gateway.open()
9
+ const system = await System.connect()
10
10
 
11
- for await (const event of gateway.install(project)) {
12
- // installation progress
13
- }
11
+ await project.install(system)
14
12
 
15
- // The complete transport-neutral System contract used by Server Programs.
16
- const programs = await gateway.system.program.list()
13
+ // This is the same transport-neutral System contract used by Server Programs.
14
+ const programs = await system.program.list()
17
15
 
18
- await gateway.close()
16
+ await system.disconnect()
19
17
  ```
20
18
 
21
19
  `Project.open()` discovers `phresh.config.ts` from the current working
22
- directory by default. `Gateway.open()` resolves its home from an explicit
20
+ directory by default. `System.connect()` resolves its home from an explicit
23
21
  argument, then `PHRESHOS_HOME`, then the current user's `.phreshos` directory.
24
22
 
25
23
  Project operations remain available without duplicating CLI logic:
26
24
 
27
25
  ```ts
28
26
  const project = await Project.open() // process.cwd()
29
- const gateway = await Gateway.open()
27
+ const system = await System.connect()
30
28
 
31
29
  await project.pack()
32
- for await (const event of gateway.start(project)) { /* production run */ }
33
- for await (const event of gateway.dev(project)) { /* development run */ }
34
- for await (const event of gateway.install(project)) { /* installation */ }
30
+ await project.start(system, { signal })
31
+ await project.dev(system, { signal })
32
+ await project.install(system)
33
+
34
+ await system.disconnect()
35
35
  ```
36
36
 
37
- `start`, `dev`, and `install` expose ordered asynchronous event streams. The
38
- CLI only interprets arguments and presents those events; it does not implement
39
- a second Project or Gateway lifecycle.
37
+ `Project` owns authoring concepts such as production builds and development
38
+ Client servers. The lower-level System API stays composable: use
39
+ `system.forceCreateProgram(description)` to replace one runtime Program, and
40
+ `program.process.run(launch, { signal })` when one Process should live exactly
41
+ as long as its asynchronous iterator.
@@ -1,6 +1,6 @@
1
1
  import type { ClientDevelopment } from "@phreshos/core";
2
- import type { GatewayEvent } from "./transport.js";
3
- /** One client development server owned by a Gateway development run. */
2
+ import type { TransportEvent } from "./transport.js";
3
+ /** One Client development server owned by a Project development run. */
4
4
  export declare class DevelopmentClient {
5
5
  private readonly child;
6
6
  private readonly output;
@@ -9,7 +9,7 @@ export declare class DevelopmentClient {
9
9
  private outputWaiter;
10
10
  private readonly completion;
11
11
  constructor(command: string, directory: string);
12
- drain(): GatewayEvent[];
12
+ drain(): TransportEvent[];
13
13
  exited(): Promise<CommandExit>;
14
14
  exitResult(): CommandExit | null;
15
15
  outputAvailable(): Promise<void>;
@@ -20,7 +20,7 @@ export declare class DevelopmentClient {
20
20
  /** Refuse to claim a URL already served by an unrelated process. */
21
21
  export declare function assertAvailable(url: string): Promise<void>;
22
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<GatewayEvent, void, unknown>;
23
+ export declare function waitForDevelopmentClient(config: ClientDevelopment, client?: DevelopmentClient, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
24
24
  export declare function commandFailure(exit: CommandExit): Error;
25
25
  export interface CommandExit {
26
26
  code: number | null;
@@ -5,7 +5,7 @@ const readinessTimeout = 15_000;
5
5
  const pollingInterval = 200;
6
6
  const reportingInterval = 2_000;
7
7
  const sandboxedClientOrigin = "null";
8
- /** One client development server owned by a Gateway development run. */
8
+ /** One Client development server owned by a Project development run. */
9
9
  export class DevelopmentClient {
10
10
  child;
11
11
  output = [];
package/dist/home.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- /** Resolve the absolute PhreshOS home selected for one Gateway. */
1
+ /** Resolve the absolute PhreshOS home selected for one System connection. */
2
2
  export declare function resolveHome(home?: string, environment?: NodeJS.ProcessEnv, userHome?: string): string;
package/dist/home.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, realpathSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { isAbsolute, join, normalize } from "node:path";
4
- /** Resolve the absolute PhreshOS home selected for one Gateway. */
4
+ /** Resolve the absolute PhreshOS home selected for one System connection. */
5
5
  export function resolveHome(home, environment = process.env, userHome = homedir()) {
6
6
  const selected = home ?? environment.PHRESHOS_HOME;
7
7
  if (selected === undefined)
package/dist/main.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { gatewayAddress } from "./address.js";
2
- export { Gateway, type InstallOptions, type RunOptions, type UninstallOptions } from "./gateway.js";
2
+ export { 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 } from "./project.js";
5
- export { type GatewayEvent } from "./transport.js";
4
+ export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions, type ProjectRunResult } from "./project.js";
package/dist/main.js CHANGED
@@ -1,5 +1,4 @@
1
1
  export { gatewayAddress } from "./address.js";
2
- export { Gateway } from "./gateway.js";
2
+ export { System } from "./system.js";
3
3
  export { resolveHome } from "./home.js";
4
4
  export { Project } from "./project.js";
5
- export {} from "./transport.js";
package/dist/project.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Config, type ProgramDescription } from "@phreshos/core";
1
+ import { type Config, type Exit, type ProgramDescription, type System as SystemContract, type SystemProcessEntity, type SystemProgramEntity } 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;
@@ -14,13 +14,34 @@ export declare class Project {
14
14
  description(mode: ProjectMode): ProgramDescription;
15
15
  /** Run the optional author-owned production build command. */
16
16
  build(): Promise<void>;
17
+ /** Build and run this project's production Program until its Process exits. */
18
+ start(system: SystemContract, options?: ProjectRunOptions): Promise<Readonly<{
19
+ process: SystemProcessEntity;
20
+ exit: Exit;
21
+ }>>;
22
+ /** Run this project's development Program and its optional Client server. */
23
+ dev(system: SystemContract, options?: ProjectRunOptions): Promise<Readonly<{
24
+ process: SystemProcessEntity;
25
+ exit: Exit;
26
+ }>>;
27
+ /** Build and install this project's production Program. */
28
+ install(system: SystemContract): Promise<SystemProgramEntity>;
17
29
  /** Build and package this Program into its canonical release shape. */
18
30
  pack(): Promise<PackedProject>;
31
+ private run;
19
32
  }
20
33
  export type ProjectMode = "production" | "development";
21
34
  export interface ProjectOptions {
22
35
  directory?: string;
23
36
  }
37
+ export interface ProjectRunOptions {
38
+ options?: Record<string, string>;
39
+ signal?: AbortSignal;
40
+ }
41
+ export type ProjectRunResult = Readonly<{
42
+ process: SystemProcessEntity;
43
+ exit: Exit;
44
+ }>;
24
45
  export type PackedProject = Readonly<{
25
46
  archive: string;
26
47
  archivePath: string;
package/dist/project.js CHANGED
@@ -6,6 +6,7 @@ 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";
9
10
  const configFile = "phresh.config.ts";
10
11
  /** One loaded Program authoring project rooted at an absolute directory. */
11
12
  export class Project {
@@ -101,6 +102,31 @@ export class Project {
101
102
  });
102
103
  });
103
104
  }
105
+ /** Build and run this project's production Program until its Process exits. */
106
+ async start(system, options = {}) {
107
+ await this.build();
108
+ return await this.run(system, "production", options);
109
+ }
110
+ /** Run this project's development Program and its optional Client server. */
111
+ async dev(system, options = {}) {
112
+ return await this.run(system, "development", options);
113
+ }
114
+ /** Build and install this project's production Program. */
115
+ async install(system) {
116
+ await this.build();
117
+ const program = await system.forceCreateProgram(this.description("production"));
118
+ let installed = false;
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
+ }
129
+ }
104
130
  /** Build and package this Program into its canonical release shape. */
105
131
  async pack() {
106
132
  await this.build();
@@ -131,6 +157,88 @@ export class Project {
131
157
  writeFileSync(checksumPath, `${digest} ${archive}\n`);
132
158
  return Object.freeze({ archive, archivePath, checksumPath, declarationPath, digest });
133
159
  }
160
+ async run(system, mode, options) {
161
+ const description = this.description(mode);
162
+ const development = mode === "development" && description.client && (description.client.start ?? true)
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;
241
+ }
134
242
  }
135
243
  function serverHalf(half, mode) {
136
244
  if (!half)
package/dist/system.d.ts CHANGED
@@ -1,9 +1,70 @@
1
- import { type System } from "@phreshos/core";
2
- import type { GatewayEvent } from "./transport.js";
3
- export interface SystemTransport {
1
+ import { ClientServiceHandler as CoreClientServiceHandler, ServerServiceHandler as CoreServerServiceHandler, type ClientServiceChannel, type ProgramDescription, type ProgramCommandChunk, type ServerServiceChannel, type ServiceKey, type System as CoreSystem, type SystemProcessEntity, type SystemProcess, type SystemProgram, type SystemProgramEntity, type SystemUploads, type WritableAppearance } from "@phreshos/core";
2
+ import { type TransportEvent } from "./transport.js";
3
+ export type ProgramProcessRunOptions = Readonly<{
4
+ signal?: AbortSignal;
5
+ }>;
6
+ export type ProgramProcessRunEvent = Readonly<{
7
+ event: "started";
8
+ process: SystemProcessEntity;
9
+ }> | (Readonly<{
10
+ event: "output";
11
+ }> & ProgramCommandChunk) | Readonly<{
12
+ event: "exited";
13
+ process: SystemProcessEntity;
14
+ exit: import("@phreshos/core").Exit;
15
+ }>;
16
+ interface SystemTransport {
4
17
  control(request: object, signal?: AbortSignal): Promise<unknown>;
5
18
  api(request: object, signal?: AbortSignal): Promise<unknown>;
6
- lifecycle(request: object, signal?: AbortSignal): AsyncGenerator<GatewayEvent, void, void>;
19
+ lifecycle(request: object, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, void>;
7
20
  }
8
- /** Build the exact shared System contract over an owner-local Gateway transport. */
9
- export declare function gatewaySystem(transport: SystemTransport): System;
21
+ /** One connected owner-local implementation of the shared System contract. */
22
+ export declare class System implements CoreSystem {
23
+ private readonly connection;
24
+ readonly home: string;
25
+ readonly address: string;
26
+ readonly storage: import("@phreshos/core").Storage;
27
+ readonly appearance: WritableAppearance;
28
+ readonly program: SystemProgram;
29
+ readonly process: SystemProcess;
30
+ readonly uploads: SystemUploads;
31
+ readonly transport: SystemTransport;
32
+ private closed;
33
+ private readonly lifetime;
34
+ private constructor();
35
+ /** Connect to the System selected by argument, environment, or owner default. */
36
+ static connect(home?: string): Promise<System>;
37
+ /** Atomically replace one runtime Program without touching its installed form. */
38
+ forceCreateProgram(source: ProgramDescription | string): Promise<SystemProgramEntity>;
39
+ /** Close this owner connection and abort every attached operation it owns. */
40
+ disconnect(): Promise<void>;
41
+ service<EventsMap extends object = {}>(key: ServiceKey & {
42
+ endpoint: "server";
43
+ }): ServerService<EventsMap>;
44
+ service<EventsMap extends object = {}>(key: ServiceKey & {
45
+ endpoint: "client";
46
+ }): ClientService<EventsMap>;
47
+ private signal;
48
+ private requireConnected;
49
+ }
50
+ declare class ServerService<EventsMap extends object = {}> extends CoreServerServiceHandler<EventsMap> {
51
+ readonly name: string;
52
+ readonly channel: ServerServiceChannel<EventsMap>;
53
+ private readonly base;
54
+ constructor(system: System, key: ServiceKey & {
55
+ endpoint: "server";
56
+ });
57
+ enabled(): Promise<boolean>;
58
+ waitReady(timeout?: number): Promise<void>;
59
+ }
60
+ declare class ClientService<EventsMap extends object = {}> extends CoreClientServiceHandler<EventsMap> {
61
+ readonly name: string;
62
+ readonly channel: ClientServiceChannel<EventsMap>;
63
+ private readonly base;
64
+ constructor(system: System, key: ServiceKey & {
65
+ endpoint: "client";
66
+ });
67
+ enabled(): Promise<boolean>;
68
+ waitReady(timeout?: number): Promise<void>;
69
+ }
70
+ export {};
package/dist/system.js CHANGED
@@ -1,33 +1,77 @@
1
1
  import { ClientServiceHandler as CoreClientServiceHandler, ServerServiceHandler as CoreServerServiceHandler } from "@phreshos/core";
2
2
  import { homedir } from "node:os";
3
+ import { gatewayAddress } from "./address.js";
3
4
  import Events from "./events.js";
5
+ import { resolveHome } from "./home.js";
4
6
  import { filesystemStorage } from "./storage.js";
7
+ import { openConnection, request, streamProgram } from "./transport.js";
5
8
  import Uploads from "./uploads.js";
6
- /** Build the exact shared System contract over an owner-local Gateway transport. */
7
- export function gatewaySystem(transport) {
8
- return new GatewaySystem(transport);
9
- }
10
- class GatewaySystem {
11
- transport;
9
+ /** One connected owner-local implementation of the shared System contract. */
10
+ export class System {
11
+ connection;
12
+ home;
13
+ address;
12
14
  storage = filesystemStorage(homedir(), "the native home directory");
13
15
  appearance;
14
16
  program;
15
17
  process;
16
18
  uploads;
17
- constructor(transport) {
18
- this.transport = transport;
19
- this.appearance = new GatewayAppearance(transport);
19
+ transport;
20
+ closed = false;
21
+ lifetime = new AbortController();
22
+ constructor(home, address, connection) {
23
+ this.connection = connection;
24
+ this.home = home;
25
+ this.address = address;
26
+ this.transport = {
27
+ control: (value, signal) => request(address, "system", value, this.signal(signal)),
28
+ api: (value, signal) => request(address, "api", value, this.signal(signal)),
29
+ lifecycle: (value, signal) => streamProgram(address, value, this.signal(signal))
30
+ };
31
+ this.appearance = new SystemAppearance(this.transport);
20
32
  this.program = new ProgramRegistry(this);
21
33
  this.process = new ProcessRegistry(this);
22
- this.uploads = new Uploads(request => transport.api(request));
34
+ this.uploads = new Uploads(value => this.transport.api(value));
35
+ }
36
+ /** Connect to the System selected by argument, environment, or owner default. */
37
+ static async connect(home) {
38
+ const resolved = resolveHome(home);
39
+ const address = gatewayAddress(resolved);
40
+ return new System(resolved, address, await openConnection(address));
41
+ }
42
+ /** Atomically replace one runtime Program without touching its installed form. */
43
+ async forceCreateProgram(source) {
44
+ this.requireConnected();
45
+ for await (const event of this.transport.lifecycle({ word: "force-create", program: source })) {
46
+ if (event.event === "created")
47
+ return new ProgramHandle(this, required(event.program));
48
+ }
49
+ throw new Error("The System did not confirm the created Program");
50
+ }
51
+ /** Close this owner connection and abort every attached operation it owns. */
52
+ async disconnect() {
53
+ if (this.closed)
54
+ return;
55
+ this.closed = true;
56
+ this.lifetime.abort(new Error("This System connection is closed"));
57
+ this.connection.destroy();
23
58
  }
24
59
  service(key) {
60
+ this.requireConnected();
25
61
  return key.endpoint === "server"
26
62
  ? new ServerService(this, key)
27
63
  : new ClientService(this, key);
28
64
  }
65
+ signal(signal) {
66
+ this.requireConnected();
67
+ return signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
68
+ }
69
+ requireConnected() {
70
+ if (this.closed)
71
+ throw new Error("This System connection is closed");
72
+ }
29
73
  }
30
- class GatewayAppearance extends Events {
74
+ class SystemAppearance extends Events {
31
75
  transport;
32
76
  constructor(transport) {
33
77
  super(["change"], (_event, signal) => transport.api({ capability: "appearance", operation: "wait" }, signal));
@@ -74,17 +118,11 @@ class ProgramRegistry extends Events {
74
118
  }
75
119
  }
76
120
  async create(source) {
77
- let identity = null;
78
121
  for await (const event of this.system.transport.lifecycle({ word: "create", program: source })) {
79
122
  if (event.event === "created")
80
- identity = String(event.identity);
123
+ return new ProgramHandle(this.system, required(event.program));
81
124
  }
82
- if (!identity)
83
- throw new Error("The System did not confirm the created Program");
84
- const program = await this.find(identity);
85
- if (!program)
86
- throw new Error("The created Program cannot be found");
87
- return program;
125
+ throw new Error("The System did not confirm the created Program");
88
126
  }
89
127
  async event(value) {
90
128
  const waited = value;
@@ -97,6 +135,7 @@ class ProgramRegistry extends Events {
97
135
  }
98
136
  class ProgramHandle extends Events {
99
137
  system;
138
+ reference;
100
139
  identity;
101
140
  name;
102
141
  version;
@@ -105,11 +144,13 @@ class ProgramHandle extends Events {
105
144
  server;
106
145
  client;
107
146
  process;
147
+ startup;
108
148
  constructor(system, snapshot) {
109
149
  super(["forget", "uninstall"], (event, signal, timeout) => system.transport.control({
110
150
  capability: "program", operation: "wait", input: { program: snapshot.identity, event, timeout }
111
151
  }, signal).then(value => value.payload));
112
152
  this.system = system;
153
+ this.reference = snapshot.reference;
113
154
  this.identity = snapshot.identity;
114
155
  this.name = snapshot.name;
115
156
  this.version = snapshot.version;
@@ -125,6 +166,7 @@ class ProgramHandle extends Events {
125
166
  minimize: snapshot.client.minimize
126
167
  }) : null;
127
168
  this.process = new ProgramProcesses(system, this);
169
+ this.startup = new ProgramStartup(system, this);
128
170
  }
129
171
  async agent() {
130
172
  if (!this.hasAgent)
@@ -133,15 +175,49 @@ class ProgramHandle extends Events {
133
175
  return typeof value.content === "string" ? value.content : null;
134
176
  }
135
177
  async installed() {
136
- const value = await this.system.transport.control({ capability: "program", operation: "inspect", input: { program: this.identity } });
137
- if (typeof value.installed !== "boolean")
138
- throw new Error("The System returned no Program installation state");
139
- return value.installed;
178
+ for await (const event of this.system.transport.lifecycle({ word: "installed", handle: this.address() })) {
179
+ if (event.event === "installedState")
180
+ return event.installed === true;
181
+ }
182
+ throw new Error("The System returned no Program installation state");
140
183
  }
141
- install() { return command(this.system, { word: "install-existing", identity: this.identity }); }
142
- uninstall(everything = false) { return command(this.system, { word: "uninstall-existing", identity: this.identity, everything }); }
184
+ install() { return command(this.system, { word: "install-existing", handle: this.address() }); }
185
+ uninstall(everything = false) { return command(this.system, { word: "uninstall-existing", handle: this.address(), everything }); }
143
186
  async forget() {
144
- for await (const _event of this.system.transport.lifecycle({ word: "forget", identity: this.identity })) { /* consume completion */ }
187
+ for await (const _event of this.system.transport.lifecycle({ word: "forget", handle: this.address() })) { /* consume completion */ }
188
+ }
189
+ address() { return Object.freeze({ identity: this.identity, reference: this.reference }); }
190
+ }
191
+ class ProgramStartup {
192
+ system;
193
+ program;
194
+ constructor(system, program) {
195
+ this.system = system;
196
+ this.program = program;
197
+ }
198
+ async get() {
199
+ for await (const event of this.system.transport.lifecycle({
200
+ word: "startup", handle: this.program.address(), operation: "get"
201
+ })) {
202
+ if (event.event === "startup")
203
+ return event.launch;
204
+ }
205
+ throw new Error("The System returned no Program startup state");
206
+ }
207
+ async enable(launch = {}) {
208
+ await this.change("enable", launch);
209
+ }
210
+ async disable() {
211
+ await this.change("disable");
212
+ }
213
+ async change(operation, launch) {
214
+ for await (const event of this.system.transport.lifecycle({
215
+ word: "startup", handle: this.program.address(), operation, launch
216
+ })) {
217
+ if (event.event === "startup")
218
+ return;
219
+ }
220
+ throw new Error("The System did not confirm the Program startup change");
145
221
  }
146
222
  }
147
223
  class ProgramProcesses extends Events {
@@ -161,13 +237,62 @@ class ProgramProcesses extends Events {
161
237
  const found = (await this.list()).find(process => process.identity === identityOrName || process.name === identityOrName);
162
238
  return found ?? null;
163
239
  }
164
- create(launch = {}) { return createProcess(this.system, "create", this.program.identity, launch); }
165
- findOrCreate(launch) { return createProcess(this.system, "findOrCreate", this.program.identity, launch); }
240
+ create(launch = {}) { return this.createExact("create-process", launch); }
241
+ async *run(launch = {}, options = {}) {
242
+ let process = null;
243
+ for await (const event of this.system.transport.lifecycle({
244
+ word: "run-process",
245
+ handle: this.program.address(),
246
+ launch
247
+ }, options.signal)) {
248
+ if (event.event === "started") {
249
+ process = new ProcessHandle(this.system, required(event.process));
250
+ yield Object.freeze({ event: "started", process });
251
+ }
252
+ else if (event.event === "output") {
253
+ if ((event.stream !== "stdout" && event.stream !== "stderr") || typeof event.text !== "string") {
254
+ throw new Error("The System returned an invalid Process output event");
255
+ }
256
+ yield Object.freeze({
257
+ event: "output",
258
+ stream: event.stream,
259
+ text: event.text
260
+ });
261
+ }
262
+ else if (event.event === "exited") {
263
+ if (!process)
264
+ throw new Error("The System ended a Process before confirming its start");
265
+ const value = event.exit;
266
+ if (!value
267
+ || (value.status !== "exited" && value.status !== "signaled")
268
+ || (value.code !== null && typeof value.code !== "number")
269
+ || (value.signal !== null && typeof value.signal !== "string")) {
270
+ throw new Error("The System returned an invalid Process exit event");
271
+ }
272
+ const exit = Object.freeze({
273
+ status: value.status,
274
+ code: value.code,
275
+ signal: value.signal
276
+ });
277
+ yield Object.freeze({ event: "exited", process, exit });
278
+ }
279
+ else
280
+ throw new Error("The System returned an unknown Process run event");
281
+ }
282
+ }
283
+ findOrCreate(launch) { return this.createExact("find-or-create-process", launch); }
166
284
  async exitAll() {
167
285
  const processes = await this.list();
168
286
  await Promise.all(processes.map(process => process.exit()));
169
287
  return processes.map(process => process.identity);
170
288
  }
289
+ async createExact(word, launch) {
290
+ for await (const event of this.system.transport.lifecycle({ word, handle: this.program.address(), launch })) {
291
+ if (event.event === "createdProcess")
292
+ return new ProcessHandle(this.system, required(event.process));
293
+ }
294
+ throw new Error("The System did not confirm the created Process");
295
+ }
171
296
  }
172
297
  class ProcessRegistry extends Events {
173
298
  system;
@@ -280,14 +405,14 @@ class ClientEndpoint extends EndpointHandle {
280
405
  window;
281
406
  constructor(system, owner) {
282
407
  super(system, owner, "client");
283
- this.window = new GatewayWindow(system, owner);
408
+ this.window = new SystemWindow(system, owner);
284
409
  }
285
410
  async start(overrides) { await this.operation("start", overrides); }
286
411
  async service() {
287
412
  return await super.service();
288
413
  }
289
414
  }
290
- class GatewayWindow extends Events {
415
+ class SystemWindow extends Events {
291
416
  system;
292
417
  process;
293
418
  constructor(system, process) {
@@ -393,10 +518,6 @@ async function listProcesses(system, program) {
393
518
  return processes;
394
519
  }
395
520
  }
396
- async function createProcess(system, operation, program, launch) {
397
- const snapshot = await system.transport.control({ capability: "process", operation, input: { program, launch } });
398
- return new ProcessHandle(system, snapshot);
399
- }
400
521
  async function* command(system, request) {
401
522
  for await (const event of system.transport.lifecycle(request)) {
402
523
  if (event.event === "output")
@@ -1,11 +1,11 @@
1
1
  import { type Socket } from "node:net";
2
- export interface GatewayEvent {
2
+ export interface TransportEvent {
3
3
  event?: string;
4
4
  [key: string]: unknown;
5
5
  }
6
- /** Open and retain one owner-local Gateway connection. */
6
+ /** Open and retain one owner-local System connection. */
7
7
  export declare function openConnection(path: string): Promise<Socket>;
8
8
  /** Execute one short authoritative System-control request. */
9
9
  export declare function request(path: string, target: "api" | "system", request: unknown, signal?: AbortSignal): Promise<unknown>;
10
10
  /** Stream one Program lifecycle operation until the System completes it. */
11
- export declare function streamProgram(path: string, request: unknown, signal?: AbortSignal): AsyncGenerator<GatewayEvent, void, unknown>;
11
+ export declare function streamProgram(path: string, request: unknown, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
package/dist/transport.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { connect } from "node:net";
2
- /** Open and retain one owner-local Gateway connection. */
2
+ /** Open and retain one owner-local System connection. */
3
3
  export function openConnection(path) {
4
4
  return new Promise((resolve, reject) => {
5
5
  const socket = connect(path);
@@ -39,7 +39,7 @@ export function request(path, target, request, signal) {
39
39
  outcome = JSON.parse(buffer.slice(0, boundary));
40
40
  }
41
41
  catch {
42
- return finish(() => reject(new Error("The System returned an invalid Gateway response")));
42
+ return finish(() => reject(new Error("The System returned an invalid response")));
43
43
  }
44
44
  if (outcome.success)
45
45
  finish(() => resolve(outcome.result));
@@ -47,7 +47,7 @@ export function request(path, target, request, signal) {
47
47
  finish(() => reject(new Error(outcome.error)));
48
48
  });
49
49
  socket.on("error", () => finish(() => reject(unavailable(path))));
50
- socket.on("close", () => finish(() => reject(new Error("The System closed the Gateway request without an answer"))));
50
+ socket.on("close", () => finish(() => reject(new Error("The System closed the request without an answer"))));
51
51
  if (signal?.aborted)
52
52
  cancel();
53
53
  });
@@ -95,19 +95,25 @@ export function streamProgram(path, request, signal) {
95
95
  wake = null;
96
96
  });
97
97
  return (async function* () {
98
- while (true) {
99
- if (events.length) {
100
- yield events.shift();
101
- continue;
98
+ try {
99
+ while (true) {
100
+ if (events.length) {
101
+ yield events.shift();
102
+ continue;
103
+ }
104
+ if (failure)
105
+ throw failure;
106
+ if (ended)
107
+ return;
108
+ await new Promise(resolve => { wake = resolve; });
102
109
  }
103
- if (failure)
104
- throw failure;
105
- if (ended)
106
- return;
107
- await new Promise(resolve => { wake = resolve; });
110
+ }
111
+ finally {
112
+ signal?.removeEventListener("abort", cancel);
113
+ socket.destroy();
108
114
  }
109
115
  })();
110
116
  }
111
117
  function unavailable(path) {
112
- return new Error(`No System Gateway is listening at ${path} — start PhreshOS first`);
118
+ return new Error(`No System is listening at ${path} — start PhreshOS first`);
113
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/node",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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.21",
35
+ "@phreshos/core": "^0.1.22",
36
36
  "adm-zip": "^0.6.0",
37
37
  "jiti": "^2.7.0"
38
38
  },
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
- }