@phreshos/node 0.1.1 → 0.1.3

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
@@ -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
- await project.dev(system, { signal })
32
- await project.install(system)
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
- `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
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,6 @@
1
+ /** Canonical domain handles owned for exactly one connected System context. */
2
+ export default class HandleRegistry {
3
+ private readonly handles;
4
+ obtain<Value extends object>(key: string, create: () => Value): Value;
5
+ clear(): void;
6
+ }
@@ -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,5 @@
1
1
  export { gatewayAddress } from "./address.js";
2
- export { System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
2
+ export { Client, ClientService, Endpoint, Process, Program, Server, ServerService, System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
3
+ export { Service, type ClientServiceChannel, type ServerServiceChannel, type ServiceChannel, type ServiceKey, type ServiceLifecycleEvents } from "@phreshos/core";
3
4
  export { resolveHome } from "./home.js";
4
- export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions, type ProjectRunResult } from "./project.js";
5
+ export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions } from "./project.js";
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { gatewayAddress } from "./address.js";
2
- export { System } from "./system.js";
2
+ export { Client, ClientService, Endpoint, Process, Program, Server, ServerService, System } from "./system.js";
3
+ export { Service } from "@phreshos/core";
3
4
  export { resolveHome } from "./home.js";
4
5
  export { Project } from "./project.js";
package/dist/project.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Config, type Exit, type ProgramDescription, type System as SystemContract, type SystemProcessEntity, type SystemProgramEntity } from "@phreshos/core";
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 project from an already loaded definition. */
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 definition into one runnable Program description. */
14
- description(mode: ProjectMode): ProgramDescription;
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 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>;
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 project from an already loaded definition. */
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 definition into one runnable Program description. */
49
- description(mode) {
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 and run this project's production Program until its Process exits. */
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, "production", options);
114
+ return await this.run(system, this.productionDefinition(), options);
109
115
  }
110
- /** Run this project's development Program and its optional Client server. */
116
+ /** Return this Project's development Process lifecycle generator. */
111
117
  async dev(system, options = {}) {
112
- return await this.run(system, "development", options);
118
+ return await this.run(system, this.developmentDefinition(), options);
113
119
  }
114
- /** Build and install this project's production Program. */
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.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
- }
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(packageDescription(this.config, version), null, 4) + "\n");
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, 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;
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, ...description } = half;
164
+ const { development, startCommand, entryFile, ...declared } = half;
247
165
  if (mode === "production" || !development)
248
- return { ...description, ...serverExecution({ startCommand, entryFile }) };
249
- return { ...description, location: ".", ...serverExecution(development) };
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 packageDescription(config, version) {
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 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";
1
+ import { Client as CoreClient, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerService as CoreServerService, type ClientDeclaration, type EndpointDeclaration, type Launch, type LaunchClient, type Position, type ProgramDefinition, type ProgramCommandChunk, 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: ProgramDescription | string): Promise<SystemProgramEntity>;
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,27 +50,194 @@ 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
  }
50
- declare class ServerService<EventsMap extends object = {}> extends CoreServerServiceHandler<EventsMap> {
51
- readonly name: string;
52
- readonly channel: ServerServiceChannel<EventsMap>;
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";
53
142
  private readonly base;
54
- constructor(system: System, key: ServiceKey & {
55
- endpoint: "server";
56
- });
57
- enabled(): Promise<boolean>;
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
+ };
58
153
  waitReady(timeout?: number): Promise<void>;
154
+ service<EventsMap extends object = {}>(): Promise<ServerService<EventsMap> | null>;
59
155
  }
60
- declare class ClientService<EventsMap extends object = {}> extends CoreClientServiceHandler<EventsMap> {
61
- readonly name: string;
62
- readonly channel: ClientServiceChannel<EventsMap>;
156
+ interface ClientEndpoint extends SystemClientEntity {
157
+ }
158
+ declare class ClientEndpoint extends ClientBase {
159
+ readonly endpoint: "client";
160
+ readonly window: SystemWindow;
63
161
  private readonly base;
64
- constructor(system: System, key: ServiceKey & {
65
- endpoint: "client";
66
- });
67
- enabled(): Promise<boolean>;
68
- waitReady(timeout?: number): Promise<void>;
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
+ }
196
+ /** Node-SDK handle for a Service provided by a Server Endpoint. */
197
+ export declare class ServerService<EventsMap extends object = {}> extends CoreServerService<EventsMap> {
198
+ protected constructor();
199
+ }
200
+ /** Node-SDK handle for a Service provided by a Client Endpoint. */
201
+ export declare class ClientService<EventsMap extends object = {}> extends CoreClientService<EventsMap> {
202
+ protected constructor();
203
+ }
204
+ interface ProgramSnapshot {
205
+ reference: string;
206
+ identity: string;
207
+ name: string;
208
+ version: string | null;
209
+ description: string | null;
210
+ installed?: boolean;
211
+ hasAgent: boolean;
212
+ server: {
213
+ start: boolean;
214
+ } | null;
215
+ client: ClientDeclaration | null;
216
+ }
217
+ interface ProcessSnapshot {
218
+ reference: string;
219
+ identity: string;
220
+ name: string | null;
221
+ program: string;
222
+ programSnapshot?: ProgramSnapshot;
223
+ startedAt: string;
224
+ server: {
225
+ declared: boolean;
226
+ running: boolean;
227
+ };
228
+ client: {
229
+ declared: boolean;
230
+ running: boolean;
231
+ };
69
232
  }
233
+ export type Program = SystemProgramEntity;
234
+ export declare const Program: typeof CoreProgram;
235
+ export type Process = SystemProcessEntity;
236
+ export declare const Process: typeof CoreProcess;
237
+ export type Endpoint<EventsMap extends object = {}> = SystemEndpointEntity<EventsMap>;
238
+ export declare const Endpoint: typeof CoreEndpoint;
239
+ export type Server<EventsMap extends object = {}> = SystemServerEntity<EventsMap>;
240
+ export declare const Server: typeof CoreServer;
241
+ export type Client<EventsMap extends object = {}> = SystemClientEntity<EventsMap>;
242
+ export declare const Client: typeof CoreClient;
70
243
  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, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerService as CoreServerService, isServiceKey } from "@phreshos/core";
2
2
  import { homedir } from "node:os";
3
3
  import { gatewayAddress } from "./address.js";
4
4
  import Events from "./events.js";
5
+ 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 new ProgramHandle(this, required(event.program));
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,12 +61,25 @@ 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();
61
- return key.endpoint === "server"
62
- ? new ServerService(this, key)
63
- : new ClientService(this, key);
68
+ if (!isServiceKey(key))
69
+ throw new Error("A complete service key is required");
70
+ const normalized = Object.freeze({ program: key.program, endpoint: key.endpoint, name: key.name });
71
+ const identity = JSON.stringify([normalized.program, normalized.endpoint, normalized.name]);
72
+ return this.handles.obtain(`service:${identity}`, () => normalized.endpoint === "server"
73
+ ? new ServerServiceHandle(this, normalized)
74
+ : new ClientServiceHandle(this, normalized));
75
+ }
76
+ programHandle(snapshot) {
77
+ const handle = this.handles.obtain(`program:${snapshot.reference}`, () => new ProgramHandle(this, snapshot));
78
+ handle.update(snapshot);
79
+ return handle;
80
+ }
81
+ processHandle(snapshot) {
82
+ return this.handles.obtain(`process:${snapshot.reference}`, () => new ProcessHandle(this, snapshot));
64
83
  }
65
84
  signal(signal) {
66
85
  this.requireConnected();
@@ -100,7 +119,7 @@ class ProgramRegistry extends Events {
100
119
  operation: "list",
101
120
  input: { installedOnly: onlyInstalled, limit: 100, offset }
102
121
  });
103
- programs.push(...page.data.map(snapshot => new ProgramHandle(this.system, snapshot)));
122
+ programs.push(...page.data.map(snapshot => this.system.programHandle(snapshot)));
104
123
  offset += page.data.length;
105
124
  if (!page.truncated || !page.data.length)
106
125
  return programs;
@@ -109,7 +128,7 @@ class ProgramRegistry extends Events {
109
128
  async find(identity) {
110
129
  try {
111
130
  const snapshot = await this.system.transport.control({ capability: "program", operation: "inspect", input: { program: identity } });
112
- return new ProgramHandle(this.system, snapshot);
131
+ return this.system.programHandle(snapshot);
113
132
  }
114
133
  catch (error) {
115
134
  if (unknown(error, "Program"))
@@ -120,7 +139,7 @@ class ProgramRegistry extends Events {
120
139
  async create(source) {
121
140
  for await (const event of this.system.transport.lifecycle({ word: "create", program: source })) {
122
141
  if (event.event === "created")
123
- return new ProgramHandle(this.system, required(event.program));
142
+ return this.system.programHandle(required(event.program));
124
143
  }
125
144
  throw new Error("The System did not confirm the created Program");
126
145
  }
@@ -128,46 +147,52 @@ class ProgramRegistry extends Events {
128
147
  const waited = value;
129
148
  if (waited.event === "uninstall") {
130
149
  const payload = waited.payload;
131
- return { program: new ProgramHandle(this.system, required(payload.program)), everythingRemoved: payload.everythingRemoved === true };
150
+ return { program: this.system.programHandle(required(payload.program)), everythingRemoved: payload.everythingRemoved === true };
132
151
  }
133
- return new ProgramHandle(this.system, required(waited.payload));
152
+ return this.system.programHandle(required(waited.payload));
134
153
  }
135
154
  }
136
- class ProgramHandle extends Events {
155
+ class ProgramHandle extends ProgramBase {
137
156
  system;
138
157
  reference;
139
158
  identity;
140
- name;
141
- version;
142
- description;
143
- hasAgent;
144
- server;
145
- client;
146
159
  process;
147
160
  startup;
161
+ snapshot;
148
162
  constructor(system, snapshot) {
149
- super(["forget", "uninstall"], (event, signal, timeout) => system.transport.control({
150
- capability: "program", operation: "wait", input: { program: snapshot.identity, event, timeout }
151
- }, signal).then(value => value.payload));
163
+ super();
152
164
  this.system = system;
165
+ this.snapshot = snapshot;
166
+ bindEvents(this, new Events(["forget", "uninstall"], (event, signal, timeout) => system.transport.control({
167
+ capability: "program", operation: "wait", input: { program: snapshot.identity, event, timeout }
168
+ }, signal).then(value => value.payload)));
153
169
  this.reference = snapshot.reference;
154
170
  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
171
  this.process = new ProgramProcesses(system, this);
169
172
  this.startup = new ProgramStartup(system, this);
170
173
  }
174
+ get name() { return this.snapshot.name; }
175
+ get version() { return this.snapshot.version; }
176
+ get description() { return this.snapshot.description; }
177
+ get hasAgent() { return this.snapshot.hasAgent; }
178
+ get server() {
179
+ return this.snapshot.server ? Object.freeze({ start: this.snapshot.server.start }) : null;
180
+ }
181
+ get client() {
182
+ return this.snapshot.client ? Object.freeze({
183
+ start: this.snapshot.client.start,
184
+ title: this.snapshot.client.title,
185
+ size: this.snapshot.client.size,
186
+ position: this.snapshot.client.position,
187
+ layer: this.snapshot.client.layer,
188
+ minimize: this.snapshot.client.minimize
189
+ }) : null;
190
+ }
191
+ update(snapshot) {
192
+ if (snapshot.reference !== this.reference)
193
+ throw new Error("A Program handle cannot become another Program");
194
+ this.snapshot = snapshot;
195
+ }
171
196
  async agent() {
172
197
  if (!this.hasAgent)
173
198
  return null;
@@ -246,7 +271,7 @@ class ProgramProcesses extends Events {
246
271
  launch
247
272
  }, options.signal)) {
248
273
  if (event.event === "started") {
249
- process = new ProcessHandle(this.system, required(event.process));
274
+ process = this.system.processHandle(required(event.process));
250
275
  yield Object.freeze({ event: "started", process });
251
276
  }
252
277
  else if (event.event === "output") {
@@ -289,7 +314,7 @@ class ProgramProcesses extends Events {
289
314
  async createExact(word, launch) {
290
315
  for await (const event of this.system.transport.lifecycle({ word, handle: this.program.address(), launch })) {
291
316
  if (event.event === "createdProcess")
292
- return new ProcessHandle(this.system, required(event.process));
317
+ return this.system.processHandle(required(event.process));
293
318
  }
294
319
  throw new Error("The System did not confirm the created Process");
295
320
  }
@@ -306,7 +331,7 @@ class ProcessRegistry extends Events {
306
331
  async find(identity) {
307
332
  try {
308
333
  const snapshot = await this.system.transport.control({ capability: "process", operation: "inspect", input: { process: identity } });
309
- return new ProcessHandle(this.system, snapshot);
334
+ return this.system.processHandle(snapshot);
310
335
  }
311
336
  catch (error) {
312
337
  if (unknown(error, "Process"))
@@ -315,7 +340,7 @@ class ProcessRegistry extends Events {
315
340
  }
316
341
  }
317
342
  }
318
- class ProcessHandle extends Events {
343
+ class ProcessHandle extends ProcessBase {
319
344
  system;
320
345
  snapshot;
321
346
  identity;
@@ -324,26 +349,28 @@ class ProcessHandle extends Events {
324
349
  server;
325
350
  client;
326
351
  constructor(system, snapshot) {
327
- super(["endpointStart", "endpointStop", "exit"], (event, signal, timeout) => system.transport.control({
328
- capability: "process", operation: "wait", input: { process: snapshot.identity, event, timeout }
329
- }, signal).then(value => processEvent(system, value)));
352
+ super();
330
353
  this.system = system;
331
354
  this.snapshot = snapshot;
355
+ bindEvents(this, new Events(["endpointStart", "endpointStop", "exit"], (event, signal, timeout) => system.transport.control({
356
+ capability: "process", operation: "wait", input: { process: snapshot.identity, event, timeout }
357
+ }, signal).then(value => processEvent(system, value))));
332
358
  this.identity = snapshot.identity;
333
359
  this.name = snapshot.name;
334
360
  this.startedAt = new Date(snapshot.startedAt);
335
361
  this.server = new ServerEndpoint(system, this);
336
362
  this.client = new ClientEndpoint(system, this);
337
363
  }
338
- program() { return new ProgramHandle(this.system, required(this.snapshot.programSnapshot, this.snapshot.program)); }
364
+ program() { return this.system.programHandle(required(this.snapshot.programSnapshot, this.snapshot.program)); }
339
365
  async exit() {
340
366
  await this.system.transport.control({ capability: "process", operation: "exit", input: { process: this.identity } });
341
367
  }
342
368
  async exited() { return await this.system.process.find(this.identity) === null; }
343
369
  }
344
- class EndpointHandle extends Events {
370
+ class EndpointOperations extends Events {
345
371
  system;
346
372
  owner;
373
+ endpoint;
347
374
  constructor(system, owner, endpoint) {
348
375
  super([], (event, signal, timeout) => event === null
349
376
  ? system.transport.api({ capability: "endpoint", operation: "wait", process: owner.identity, endpoint, event, timeout }, signal)
@@ -352,13 +379,14 @@ class EndpointHandle extends Events {
352
379
  }, signal).then(value => value.payload));
353
380
  this.system = system;
354
381
  this.owner = owner;
382
+ this.endpoint = endpoint;
355
383
  }
356
384
  process() { return Promise.resolve(this.owner); }
357
385
  async exists() {
358
386
  const value = await this.inspect();
359
387
  return value.running;
360
388
  }
361
- async start() { await this.operation("start"); }
389
+ async start(client) { await this.operation("start", client); }
362
390
  async stop() { await this.operation("stop"); }
363
391
  async service() {
364
392
  const key = await this.system.transport.api({ capability: "endpoint", operation: "service", process: this.owner.identity, endpoint: this.endpoint });
@@ -380,9 +408,23 @@ class EndpointHandle extends Events {
380
408
  } });
381
409
  }
382
410
  }
383
- class ServerEndpoint extends EndpointHandle {
411
+ class ServerEndpoint extends ServerBase {
412
+ system;
413
+ owner;
384
414
  endpoint = "server";
385
- constructor(system, owner) { super(system, owner, "server"); }
415
+ base;
416
+ constructor(system, owner) {
417
+ super();
418
+ this.system = system;
419
+ this.owner = owner;
420
+ this.base = new EndpointOperations(system, owner, "server");
421
+ bindEvents(this, this.base);
422
+ }
423
+ process() { return this.base.process(); }
424
+ exists() { return this.base.exists(); }
425
+ start() { return this.base.start(); }
426
+ stop() { return this.base.stop(); }
427
+ publish(event, payload) { return this.base.publish(event, payload); }
386
428
  async ask(event, payload) {
387
429
  return await this.system.transport.control({ capability: "endpoint", operation: "ask", input: {
388
430
  process: this.owner.identity, endpoint: "server", event, payload
@@ -397,19 +439,26 @@ class ServerEndpoint extends EndpointHandle {
397
439
  await this.system.transport.control({ capability: "endpoint", operation: "waitReady", input: { process: this.owner.identity, endpoint: "server", timeout } });
398
440
  }
399
441
  async service() {
400
- return await super.service();
442
+ return await this.base.service();
401
443
  }
402
444
  }
403
- class ClientEndpoint extends EndpointHandle {
445
+ class ClientEndpoint extends ClientBase {
404
446
  endpoint = "client";
405
447
  window;
448
+ base;
406
449
  constructor(system, owner) {
407
- super(system, owner, "client");
450
+ super();
451
+ this.base = new EndpointOperations(system, owner, "client");
452
+ bindEvents(this, this.base);
408
453
  this.window = new SystemWindow(system, owner);
409
454
  }
410
- async start(overrides) { await this.operation("start", overrides); }
455
+ process() { return this.base.process(); }
456
+ exists() { return this.base.exists(); }
457
+ start(overrides) { return this.base.start(overrides); }
458
+ stop() { return this.base.stop(); }
459
+ publish(event, payload) { return this.base.publish(event, payload); }
411
460
  async service() {
412
- return await super.service();
461
+ return await this.base.service();
413
462
  }
414
463
  }
415
464
  class SystemWindow extends Events {
@@ -457,7 +506,11 @@ class ServiceBase extends Events {
457
506
  async enabled() { return await this.system.transport.api({ capability: "service", operation: "enabled", key: this.key }); }
458
507
  async waitReady(timeout) { await this.system.transport.api({ capability: "service", operation: "waitReady", key: this.key, timeout }); }
459
508
  }
460
- class ServerService extends CoreServerServiceHandler {
509
+ /** Node-SDK handle for a Service provided by a Server Endpoint. */
510
+ export class ServerService extends CoreServerService {
511
+ constructor() { super(); }
512
+ }
513
+ class ServerServiceHandle extends ServerService {
461
514
  name;
462
515
  channel;
463
516
  base;
@@ -471,7 +524,11 @@ class ServerService extends CoreServerServiceHandler {
471
524
  enabled() { return this.base.enabled(); }
472
525
  waitReady(timeout) { return this.base.waitReady(timeout); }
473
526
  }
474
- class ClientService extends CoreClientServiceHandler {
527
+ /** Node-SDK handle for a Service provided by a Client Endpoint. */
528
+ export class ClientService extends CoreClientService {
529
+ constructor() { super(); }
530
+ }
531
+ class ClientServiceHandle extends ClientService {
475
532
  name;
476
533
  channel;
477
534
  base;
@@ -512,7 +569,7 @@ async function listProcesses(system, program) {
512
569
  let offset = 0;
513
570
  while (true) {
514
571
  const page = await system.transport.control({ capability: "process", operation: "list", input: { program, limit: 100, offset } });
515
- processes.push(...page.data.map(snapshot => new ProcessHandle(system, snapshot)));
572
+ processes.push(...page.data.map(snapshot => system.processHandle(snapshot)));
516
573
  offset += page.data.length;
517
574
  if (!page.truncated || !page.data.length)
518
575
  return processes;
@@ -532,19 +589,22 @@ function processEvent(system, value) {
532
589
  const payload = waited.payload;
533
590
  if (waited.event === "exit" && payload)
534
591
  return {
535
- process: new ProcessHandle(system, required(payload.processSnapshot, String(payload.process ?? ""))),
592
+ process: system.processHandle(required(payload.processSnapshot, String(payload.process ?? ""))),
536
593
  status: payload.status,
537
594
  code: payload.code,
538
595
  signal: payload.signal
539
596
  };
540
597
  if ((waited.event === "endpointStart" || waited.event === "endpointStop") && payload?.processSnapshot) {
541
- const process = new ProcessHandle(system, payload.processSnapshot);
598
+ const process = system.processHandle(payload.processSnapshot);
542
599
  return payload.endpoint === "client" ? process.client : process.server;
543
600
  }
544
601
  if (payload && typeof payload.identity === "string")
545
- return new ProcessHandle(system, payload);
602
+ return system.processHandle(payload);
546
603
  return payload;
547
604
  }
605
+ function bindEvents(target, events) {
606
+ Object.assign(target, eventsOf(events));
607
+ }
548
608
  function eventsOf(events) {
549
609
  return {
550
610
  subscribe: events.subscribe,
@@ -560,3 +620,8 @@ function required(value, identity = "") {
560
620
  return value;
561
621
  throw new Error(`The System returned no ${identity ? `${identity} ` : ""}snapshot`);
562
622
  }
623
+ export const Program = CoreProgram;
624
+ export const Process = CoreProcess;
625
+ export const Endpoint = CoreEndpoint;
626
+ export const Server = CoreServer;
627
+ export const Client = CoreClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/node",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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.22",
35
+ "@phreshos/core": "^0.1.24",
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
- }