@phreshos/node 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,4 @@
1
1
  export { gatewayAddress } from "./address.js";
2
- export { System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
2
+ export { Client, Endpoint, Process, Program, Server, System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
3
3
  export { resolveHome } from "./home.js";
4
- export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions, type ProjectRunResult } from "./project.js";
4
+ export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions } from "./project.js";
package/dist/main.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { gatewayAddress } from "./address.js";
2
- export { System } from "./system.js";
2
+ export { Client, Endpoint, Process, Program, Server, System } from "./system.js";
3
3
  export { resolveHome } from "./home.js";
4
4
  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, ClientServiceHandler as CoreClientServiceHandler, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerServiceHandler as CoreServerServiceHandler, type ClientDeclaration, type ClientServiceChannel, type EndpointDeclaration, type Launch, type LaunchClient, type Position, type ProgramDefinition, type ProgramCommandChunk, type ServerServiceChannel, type ServiceKey, type Size, type System as CoreSystem, type SystemClientEntity, type SystemEndpointEntity, type SystemProcessEntity, type SystemProcess, type SystemProgram, type SystemProgramEntity, type SystemProgramProcessEvents, type SystemServerEntity, type SystemUploads, type WritableAppearance, type Window, type WindowEvents, type WindowGeometry } from "@phreshos/core";
2
+ import Events from "./events.js";
2
3
  import { type TransportEvent } from "./transport.js";
3
4
  export type ProgramProcessRunOptions = Readonly<{
4
5
  signal?: AbortSignal;
@@ -18,6 +19,10 @@ interface SystemTransport {
18
19
  api(request: object, signal?: AbortSignal): Promise<unknown>;
19
20
  lifecycle(request: object, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, void>;
20
21
  }
22
+ declare const ProgramBase: new () => object;
23
+ declare const ProcessBase: new () => object;
24
+ declare const ServerBase: new () => object;
25
+ declare const ClientBase: new () => object;
21
26
  /** One connected owner-local implementation of the shared System contract. */
22
27
  export declare class System implements CoreSystem {
23
28
  private readonly connection;
@@ -31,11 +36,12 @@ export declare class System implements CoreSystem {
31
36
  readonly transport: SystemTransport;
32
37
  private closed;
33
38
  private readonly lifetime;
39
+ private readonly handles;
34
40
  private constructor();
35
41
  /** Connect to the System selected by argument, environment, or owner default. */
36
42
  static connect(home?: string): Promise<System>;
37
43
  /** Atomically replace one runtime Program without touching its installed form. */
38
- forceCreateProgram(source: 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,9 +50,149 @@ export declare class System implements CoreSystem {
44
50
  service<EventsMap extends object = {}>(key: ServiceKey & {
45
51
  endpoint: "client";
46
52
  }): ClientService<EventsMap>;
53
+ programHandle(snapshot: ProgramSnapshot): ProgramHandle;
54
+ processHandle(snapshot: ProcessSnapshot): ProcessHandle;
47
55
  private signal;
48
56
  private requireConnected;
49
57
  }
58
+ interface ProgramHandle extends SystemProgramEntity {
59
+ }
60
+ declare class ProgramHandle extends ProgramBase {
61
+ private readonly system;
62
+ private readonly reference;
63
+ readonly identity: string;
64
+ readonly process: ProgramProcesses;
65
+ readonly startup: ProgramStartup;
66
+ private snapshot;
67
+ constructor(system: System, snapshot: ProgramSnapshot);
68
+ get name(): string;
69
+ get version(): string | null;
70
+ get description(): string | null;
71
+ get hasAgent(): boolean;
72
+ get server(): EndpointDeclaration | null;
73
+ get client(): ClientDeclaration | null;
74
+ update(snapshot: ProgramSnapshot): void;
75
+ agent(): Promise<string | null>;
76
+ installed(): Promise<boolean>;
77
+ install(): AsyncGenerator<Readonly<{
78
+ stream: "stdout" | "stderr";
79
+ text: string;
80
+ }>, void, void>;
81
+ uninstall(everything?: boolean): AsyncGenerator<Readonly<{
82
+ stream: "stdout" | "stderr";
83
+ text: string;
84
+ }>, void, void>;
85
+ forget(): Promise<void>;
86
+ address(): Readonly<{
87
+ identity: string;
88
+ reference: string;
89
+ }>;
90
+ }
91
+ declare class ProgramStartup {
92
+ private readonly system;
93
+ private readonly program;
94
+ constructor(system: System, program: ProgramHandle);
95
+ get(): Promise<Readonly<{
96
+ name?: string;
97
+ server?: boolean;
98
+ client?: boolean | LaunchClient;
99
+ options?: Readonly<Record<string, string>>;
100
+ }> | null>;
101
+ enable(launch?: Launch): Promise<void>;
102
+ disable(): Promise<void>;
103
+ private change;
104
+ }
105
+ declare class ProgramProcesses extends Events<SystemProgramProcessEvents> {
106
+ private readonly system;
107
+ private readonly program;
108
+ constructor(system: System, program: ProgramHandle);
109
+ list(): Promise<ProcessHandle[]>;
110
+ first(): Promise<ProcessHandle | null>;
111
+ last(): Promise<ProcessHandle | null>;
112
+ find(identityOrName: string): Promise<ProcessHandle | null>;
113
+ create(launch?: Launch): Promise<ProcessHandle>;
114
+ run(launch?: Launch, options?: ProgramProcessRunOptions): AsyncGenerator<ProgramProcessRunEvent, void, void>;
115
+ findOrCreate(launch: Launch & {
116
+ name: string;
117
+ }): Promise<ProcessHandle>;
118
+ exitAll(): Promise<string[]>;
119
+ private createExact;
120
+ }
121
+ interface ProcessHandle extends SystemProcessEntity {
122
+ }
123
+ declare class ProcessHandle extends ProcessBase {
124
+ private readonly system;
125
+ private readonly snapshot;
126
+ readonly identity: string;
127
+ readonly name: string | null;
128
+ readonly startedAt: Date;
129
+ readonly server: ServerEndpoint;
130
+ readonly client: ClientEndpoint;
131
+ constructor(system: System, snapshot: ProcessSnapshot);
132
+ program(): ProgramHandle;
133
+ exit(): Promise<void>;
134
+ exited(): Promise<boolean>;
135
+ }
136
+ interface ServerEndpoint extends SystemServerEntity {
137
+ }
138
+ declare class ServerEndpoint extends ServerBase {
139
+ private readonly system;
140
+ private readonly owner;
141
+ readonly endpoint: "server";
142
+ private readonly base;
143
+ constructor(system: System, owner: ProcessHandle);
144
+ process(): Promise<ProcessHandle>;
145
+ exists(): Promise<boolean>;
146
+ start(): Promise<void>;
147
+ stop(): Promise<void>;
148
+ publish(event: string, payload?: unknown): void;
149
+ ask<Answer = unknown>(event: string, payload?: unknown): Promise<Answer>;
150
+ timeout(milliseconds: number): {
151
+ ask: <Answer = unknown>(event: string, payload?: unknown) => Promise<Answer>;
152
+ };
153
+ waitReady(timeout?: number): Promise<void>;
154
+ service<EventsMap extends object = {}>(): Promise<ServerService<EventsMap> | null>;
155
+ }
156
+ interface ClientEndpoint extends SystemClientEntity {
157
+ }
158
+ declare class ClientEndpoint extends ClientBase {
159
+ readonly endpoint: "client";
160
+ readonly window: SystemWindow;
161
+ private readonly base;
162
+ constructor(system: System, owner: ProcessHandle);
163
+ process(): Promise<ProcessHandle>;
164
+ exists(): Promise<boolean>;
165
+ start(overrides?: LaunchClient): Promise<void>;
166
+ stop(): Promise<void>;
167
+ publish(event: string, payload?: unknown): void;
168
+ service<EventsMap extends object = {}>(): Promise<ClientService<EventsMap> | null>;
169
+ }
170
+ declare class SystemWindow extends Events<WindowEvents> implements Window {
171
+ private readonly system;
172
+ private readonly process;
173
+ constructor(system: System, process: ProcessHandle);
174
+ title(): Promise<string>;
175
+ position(): Promise<Readonly<{
176
+ x: import("@phreshos/core").Value;
177
+ y: import("@phreshos/core").Value;
178
+ }>>;
179
+ size(): Promise<Readonly<{
180
+ width: import("@phreshos/core").Value;
181
+ height: import("@phreshos/core").Value;
182
+ }>>;
183
+ minimized(): Promise<boolean>;
184
+ front(): Promise<boolean>;
185
+ layer(): Promise<import("@phreshos/core").Layer>;
186
+ location(): Promise<string>;
187
+ move(position: Position): Promise<void>;
188
+ resize(size: Size): Promise<void>;
189
+ setGeometry(geometry: WindowGeometry): Promise<void>;
190
+ minimize(minimized?: boolean): Promise<void>;
191
+ changeTitle(title: string): Promise<void>;
192
+ raise(): Promise<void>;
193
+ private snapshot;
194
+ private change;
195
+ }
50
196
  declare class ServerService<EventsMap extends object = {}> extends CoreServerServiceHandler<EventsMap> {
51
197
  readonly name: string;
52
198
  readonly channel: ServerServiceChannel<EventsMap>;
@@ -67,4 +213,43 @@ declare class ClientService<EventsMap extends object = {}> extends CoreClientSer
67
213
  enabled(): Promise<boolean>;
68
214
  waitReady(timeout?: number): Promise<void>;
69
215
  }
216
+ interface ProgramSnapshot {
217
+ reference: string;
218
+ identity: string;
219
+ name: string;
220
+ version: string | null;
221
+ description: string | null;
222
+ installed?: boolean;
223
+ hasAgent: boolean;
224
+ server: {
225
+ start: boolean;
226
+ } | null;
227
+ client: ClientDeclaration | null;
228
+ }
229
+ interface ProcessSnapshot {
230
+ reference: string;
231
+ identity: string;
232
+ name: string | null;
233
+ program: string;
234
+ programSnapshot?: ProgramSnapshot;
235
+ startedAt: string;
236
+ server: {
237
+ declared: boolean;
238
+ running: boolean;
239
+ };
240
+ client: {
241
+ declared: boolean;
242
+ running: boolean;
243
+ };
244
+ }
245
+ export type Program = SystemProgramEntity;
246
+ export declare const Program: typeof CoreProgram;
247
+ export type Process = SystemProcessEntity;
248
+ export declare const Process: typeof CoreProcess;
249
+ export type Endpoint<EventsMap extends object = {}> = SystemEndpointEntity<EventsMap>;
250
+ export declare const Endpoint: typeof CoreEndpoint;
251
+ export type Server<EventsMap extends object = {}> = SystemServerEntity<EventsMap>;
252
+ export declare const Server: typeof CoreServer;
253
+ export type Client<EventsMap extends object = {}> = SystemClientEntity<EventsMap>;
254
+ export declare const Client: typeof CoreClient;
70
255
  export {};
package/dist/system.js CHANGED
@@ -1,11 +1,16 @@
1
- import { ClientServiceHandler as CoreClientServiceHandler, ServerServiceHandler as CoreServerServiceHandler } from "@phreshos/core";
1
+ import { Client as CoreClient, ClientServiceHandler as CoreClientServiceHandler, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerServiceHandler as CoreServerServiceHandler } from "@phreshos/core";
2
2
  import { homedir } from "node:os";
3
3
  import { gatewayAddress } from "./address.js";
4
4
  import Events from "./events.js";
5
+ import HandleRegistry from "./handle-registry.js";
5
6
  import { resolveHome } from "./home.js";
6
7
  import { filesystemStorage } from "./storage.js";
7
8
  import { openConnection, request, streamProgram } from "./transport.js";
8
9
  import Uploads from "./uploads.js";
10
+ const ProgramBase = CoreProgram;
11
+ const ProcessBase = CoreProcess;
12
+ const ServerBase = CoreServer;
13
+ const ClientBase = CoreClient;
9
14
  /** One connected owner-local implementation of the shared System contract. */
10
15
  export class System {
11
16
  connection;
@@ -19,6 +24,7 @@ export class System {
19
24
  transport;
20
25
  closed = false;
21
26
  lifetime = new AbortController();
27
+ handles = new HandleRegistry();
22
28
  constructor(home, address, connection) {
23
29
  this.connection = connection;
24
30
  this.home = home;
@@ -44,7 +50,7 @@ export class System {
44
50
  this.requireConnected();
45
51
  for await (const event of this.transport.lifecycle({ word: "force-create", program: source })) {
46
52
  if (event.event === "created")
47
- return 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,6 +61,7 @@ export class System {
55
61
  this.closed = true;
56
62
  this.lifetime.abort(new Error("This System connection is closed"));
57
63
  this.connection.destroy();
64
+ this.handles.clear();
58
65
  }
59
66
  service(key) {
60
67
  this.requireConnected();
@@ -62,6 +69,14 @@ export class System {
62
69
  ? new ServerService(this, key)
63
70
  : new ClientService(this, key);
64
71
  }
72
+ programHandle(snapshot) {
73
+ const handle = this.handles.obtain(`program:${snapshot.reference}`, () => new ProgramHandle(this, snapshot));
74
+ handle.update(snapshot);
75
+ return handle;
76
+ }
77
+ processHandle(snapshot) {
78
+ return this.handles.obtain(`process:${snapshot.reference}`, () => new ProcessHandle(this, snapshot));
79
+ }
65
80
  signal(signal) {
66
81
  this.requireConnected();
67
82
  return signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
@@ -100,7 +115,7 @@ class ProgramRegistry extends Events {
100
115
  operation: "list",
101
116
  input: { installedOnly: onlyInstalled, limit: 100, offset }
102
117
  });
103
- programs.push(...page.data.map(snapshot => new ProgramHandle(this.system, snapshot)));
118
+ programs.push(...page.data.map(snapshot => this.system.programHandle(snapshot)));
104
119
  offset += page.data.length;
105
120
  if (!page.truncated || !page.data.length)
106
121
  return programs;
@@ -109,7 +124,7 @@ class ProgramRegistry extends Events {
109
124
  async find(identity) {
110
125
  try {
111
126
  const snapshot = await this.system.transport.control({ capability: "program", operation: "inspect", input: { program: identity } });
112
- return new ProgramHandle(this.system, snapshot);
127
+ return this.system.programHandle(snapshot);
113
128
  }
114
129
  catch (error) {
115
130
  if (unknown(error, "Program"))
@@ -120,7 +135,7 @@ class ProgramRegistry extends Events {
120
135
  async create(source) {
121
136
  for await (const event of this.system.transport.lifecycle({ word: "create", program: source })) {
122
137
  if (event.event === "created")
123
- return new ProgramHandle(this.system, required(event.program));
138
+ return this.system.programHandle(required(event.program));
124
139
  }
125
140
  throw new Error("The System did not confirm the created Program");
126
141
  }
@@ -128,46 +143,52 @@ class ProgramRegistry extends Events {
128
143
  const waited = value;
129
144
  if (waited.event === "uninstall") {
130
145
  const payload = waited.payload;
131
- return { program: new ProgramHandle(this.system, required(payload.program)), everythingRemoved: payload.everythingRemoved === true };
146
+ return { program: this.system.programHandle(required(payload.program)), everythingRemoved: payload.everythingRemoved === true };
132
147
  }
133
- return new ProgramHandle(this.system, required(waited.payload));
148
+ return this.system.programHandle(required(waited.payload));
134
149
  }
135
150
  }
136
- class ProgramHandle extends Events {
151
+ class ProgramHandle extends ProgramBase {
137
152
  system;
138
153
  reference;
139
154
  identity;
140
- name;
141
- version;
142
- description;
143
- hasAgent;
144
- server;
145
- client;
146
155
  process;
147
156
  startup;
157
+ snapshot;
148
158
  constructor(system, snapshot) {
149
- super(["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));
159
+ super();
152
160
  this.system = system;
161
+ this.snapshot = snapshot;
162
+ bindEvents(this, new Events(["forget", "uninstall"], (event, signal, timeout) => system.transport.control({
163
+ capability: "program", operation: "wait", input: { program: snapshot.identity, event, timeout }
164
+ }, signal).then(value => value.payload)));
153
165
  this.reference = snapshot.reference;
154
166
  this.identity = snapshot.identity;
155
- this.name = snapshot.name;
156
- this.version = snapshot.version;
157
- this.description = snapshot.description;
158
- this.hasAgent = snapshot.hasAgent;
159
- this.server = snapshot.server ? Object.freeze({ start: snapshot.server.start }) : null;
160
- this.client = snapshot.client ? Object.freeze({
161
- start: snapshot.client.start,
162
- title: snapshot.client.title,
163
- size: snapshot.client.size,
164
- position: snapshot.client.position,
165
- layer: snapshot.client.layer,
166
- minimize: snapshot.client.minimize
167
- }) : null;
168
167
  this.process = new ProgramProcesses(system, this);
169
168
  this.startup = new ProgramStartup(system, this);
170
169
  }
170
+ get name() { return this.snapshot.name; }
171
+ get version() { return this.snapshot.version; }
172
+ get description() { return this.snapshot.description; }
173
+ get hasAgent() { return this.snapshot.hasAgent; }
174
+ get server() {
175
+ return this.snapshot.server ? Object.freeze({ start: this.snapshot.server.start }) : null;
176
+ }
177
+ get client() {
178
+ return this.snapshot.client ? Object.freeze({
179
+ start: this.snapshot.client.start,
180
+ title: this.snapshot.client.title,
181
+ size: this.snapshot.client.size,
182
+ position: this.snapshot.client.position,
183
+ layer: this.snapshot.client.layer,
184
+ minimize: this.snapshot.client.minimize
185
+ }) : null;
186
+ }
187
+ update(snapshot) {
188
+ if (snapshot.reference !== this.reference)
189
+ throw new Error("A Program handle cannot become another Program");
190
+ this.snapshot = snapshot;
191
+ }
171
192
  async agent() {
172
193
  if (!this.hasAgent)
173
194
  return null;
@@ -246,7 +267,7 @@ class ProgramProcesses extends Events {
246
267
  launch
247
268
  }, options.signal)) {
248
269
  if (event.event === "started") {
249
- process = new ProcessHandle(this.system, required(event.process));
270
+ process = this.system.processHandle(required(event.process));
250
271
  yield Object.freeze({ event: "started", process });
251
272
  }
252
273
  else if (event.event === "output") {
@@ -289,7 +310,7 @@ class ProgramProcesses extends Events {
289
310
  async createExact(word, launch) {
290
311
  for await (const event of this.system.transport.lifecycle({ word, handle: this.program.address(), launch })) {
291
312
  if (event.event === "createdProcess")
292
- return new ProcessHandle(this.system, required(event.process));
313
+ return this.system.processHandle(required(event.process));
293
314
  }
294
315
  throw new Error("The System did not confirm the created Process");
295
316
  }
@@ -306,7 +327,7 @@ class ProcessRegistry extends Events {
306
327
  async find(identity) {
307
328
  try {
308
329
  const snapshot = await this.system.transport.control({ capability: "process", operation: "inspect", input: { process: identity } });
309
- return new ProcessHandle(this.system, snapshot);
330
+ return this.system.processHandle(snapshot);
310
331
  }
311
332
  catch (error) {
312
333
  if (unknown(error, "Process"))
@@ -315,7 +336,7 @@ class ProcessRegistry extends Events {
315
336
  }
316
337
  }
317
338
  }
318
- class ProcessHandle extends Events {
339
+ class ProcessHandle extends ProcessBase {
319
340
  system;
320
341
  snapshot;
321
342
  identity;
@@ -324,26 +345,28 @@ class ProcessHandle extends Events {
324
345
  server;
325
346
  client;
326
347
  constructor(system, snapshot) {
327
- super(["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)));
348
+ super();
330
349
  this.system = system;
331
350
  this.snapshot = snapshot;
351
+ bindEvents(this, new Events(["endpointStart", "endpointStop", "exit"], (event, signal, timeout) => system.transport.control({
352
+ capability: "process", operation: "wait", input: { process: snapshot.identity, event, timeout }
353
+ }, signal).then(value => processEvent(system, value))));
332
354
  this.identity = snapshot.identity;
333
355
  this.name = snapshot.name;
334
356
  this.startedAt = new Date(snapshot.startedAt);
335
357
  this.server = new ServerEndpoint(system, this);
336
358
  this.client = new ClientEndpoint(system, this);
337
359
  }
338
- program() { return new ProgramHandle(this.system, required(this.snapshot.programSnapshot, this.snapshot.program)); }
360
+ program() { return this.system.programHandle(required(this.snapshot.programSnapshot, this.snapshot.program)); }
339
361
  async exit() {
340
362
  await this.system.transport.control({ capability: "process", operation: "exit", input: { process: this.identity } });
341
363
  }
342
364
  async exited() { return await this.system.process.find(this.identity) === null; }
343
365
  }
344
- class EndpointHandle extends Events {
366
+ class EndpointOperations extends Events {
345
367
  system;
346
368
  owner;
369
+ endpoint;
347
370
  constructor(system, owner, endpoint) {
348
371
  super([], (event, signal, timeout) => event === null
349
372
  ? system.transport.api({ capability: "endpoint", operation: "wait", process: owner.identity, endpoint, event, timeout }, signal)
@@ -352,13 +375,14 @@ class EndpointHandle extends Events {
352
375
  }, signal).then(value => value.payload));
353
376
  this.system = system;
354
377
  this.owner = owner;
378
+ this.endpoint = endpoint;
355
379
  }
356
380
  process() { return Promise.resolve(this.owner); }
357
381
  async exists() {
358
382
  const value = await this.inspect();
359
383
  return value.running;
360
384
  }
361
- async start() { await this.operation("start"); }
385
+ async start(client) { await this.operation("start", client); }
362
386
  async stop() { await this.operation("stop"); }
363
387
  async service() {
364
388
  const key = await this.system.transport.api({ capability: "endpoint", operation: "service", process: this.owner.identity, endpoint: this.endpoint });
@@ -380,9 +404,23 @@ class EndpointHandle extends Events {
380
404
  } });
381
405
  }
382
406
  }
383
- class ServerEndpoint extends EndpointHandle {
407
+ class ServerEndpoint extends ServerBase {
408
+ system;
409
+ owner;
384
410
  endpoint = "server";
385
- constructor(system, owner) { super(system, owner, "server"); }
411
+ base;
412
+ constructor(system, owner) {
413
+ super();
414
+ this.system = system;
415
+ this.owner = owner;
416
+ this.base = new EndpointOperations(system, owner, "server");
417
+ bindEvents(this, this.base);
418
+ }
419
+ process() { return this.base.process(); }
420
+ exists() { return this.base.exists(); }
421
+ start() { return this.base.start(); }
422
+ stop() { return this.base.stop(); }
423
+ publish(event, payload) { return this.base.publish(event, payload); }
386
424
  async ask(event, payload) {
387
425
  return await this.system.transport.control({ capability: "endpoint", operation: "ask", input: {
388
426
  process: this.owner.identity, endpoint: "server", event, payload
@@ -397,19 +435,26 @@ class ServerEndpoint extends EndpointHandle {
397
435
  await this.system.transport.control({ capability: "endpoint", operation: "waitReady", input: { process: this.owner.identity, endpoint: "server", timeout } });
398
436
  }
399
437
  async service() {
400
- return await super.service();
438
+ return await this.base.service();
401
439
  }
402
440
  }
403
- class ClientEndpoint extends EndpointHandle {
441
+ class ClientEndpoint extends ClientBase {
404
442
  endpoint = "client";
405
443
  window;
444
+ base;
406
445
  constructor(system, owner) {
407
- super(system, owner, "client");
446
+ super();
447
+ this.base = new EndpointOperations(system, owner, "client");
448
+ bindEvents(this, this.base);
408
449
  this.window = new SystemWindow(system, owner);
409
450
  }
410
- async start(overrides) { await this.operation("start", overrides); }
451
+ process() { return this.base.process(); }
452
+ exists() { return this.base.exists(); }
453
+ start(overrides) { return this.base.start(overrides); }
454
+ stop() { return this.base.stop(); }
455
+ publish(event, payload) { return this.base.publish(event, payload); }
411
456
  async service() {
412
- return await super.service();
457
+ return await this.base.service();
413
458
  }
414
459
  }
415
460
  class SystemWindow extends Events {
@@ -512,7 +557,7 @@ async function listProcesses(system, program) {
512
557
  let offset = 0;
513
558
  while (true) {
514
559
  const page = await system.transport.control({ capability: "process", operation: "list", input: { program, limit: 100, offset } });
515
- processes.push(...page.data.map(snapshot => new ProcessHandle(system, snapshot)));
560
+ processes.push(...page.data.map(snapshot => system.processHandle(snapshot)));
516
561
  offset += page.data.length;
517
562
  if (!page.truncated || !page.data.length)
518
563
  return processes;
@@ -532,19 +577,22 @@ function processEvent(system, value) {
532
577
  const payload = waited.payload;
533
578
  if (waited.event === "exit" && payload)
534
579
  return {
535
- process: new ProcessHandle(system, required(payload.processSnapshot, String(payload.process ?? ""))),
580
+ process: system.processHandle(required(payload.processSnapshot, String(payload.process ?? ""))),
536
581
  status: payload.status,
537
582
  code: payload.code,
538
583
  signal: payload.signal
539
584
  };
540
585
  if ((waited.event === "endpointStart" || waited.event === "endpointStop") && payload?.processSnapshot) {
541
- const process = new ProcessHandle(system, payload.processSnapshot);
586
+ const process = system.processHandle(payload.processSnapshot);
542
587
  return payload.endpoint === "client" ? process.client : process.server;
543
588
  }
544
589
  if (payload && typeof payload.identity === "string")
545
- return new ProcessHandle(system, payload);
590
+ return system.processHandle(payload);
546
591
  return payload;
547
592
  }
593
+ function bindEvents(target, events) {
594
+ Object.assign(target, eventsOf(events));
595
+ }
548
596
  function eventsOf(events) {
549
597
  return {
550
598
  subscribe: events.subscribe,
@@ -560,3 +608,8 @@ function required(value, identity = "") {
560
608
  return value;
561
609
  throw new Error(`The System returned no ${identity ? `${identity} ` : ""}snapshot`);
562
610
  }
611
+ export const Program = CoreProgram;
612
+ export const Process = CoreProcess;
613
+ export const Endpoint = CoreEndpoint;
614
+ export const Server = CoreServer;
615
+ export const Client = CoreClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/node",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Node.js access to PhreshOS and Program projects.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -32,7 +32,7 @@
32
32
  "prepack": "node --run build"
33
33
  },
34
34
  "dependencies": {
35
- "@phreshos/core": "^0.1.22",
35
+ "@phreshos/core": "^0.1.23",
36
36
  "adm-zip": "^0.6.0",
37
37
  "jiti": "^2.7.0"
38
38
  },
@@ -1,29 +0,0 @@
1
- import type { ClientDevelopment } from "@phreshos/core";
2
- import type { TransportEvent } from "./transport.js";
3
- /** One Client development server owned by a Project development run. */
4
- export declare class DevelopmentClient {
5
- private readonly child;
6
- private readonly output;
7
- private stopped;
8
- private result;
9
- private outputWaiter;
10
- private readonly completion;
11
- constructor(command: string, directory: string);
12
- drain(): TransportEvent[];
13
- exited(): Promise<CommandExit>;
14
- exitResult(): CommandExit | null;
15
- outputAvailable(): Promise<void>;
16
- stop(): Promise<void>;
17
- endingWasRequested(): boolean;
18
- private push;
19
- }
20
- /** Refuse to claim a URL already served by an unrelated process. */
21
- export declare function assertAvailable(url: string): Promise<void>;
22
- /** Wait until a development Client can be loaded by a sandboxed Program iframe. */
23
- export declare function waitForDevelopmentClient(config: ClientDevelopment, client?: DevelopmentClient, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
24
- export declare function commandFailure(exit: CommandExit): Error;
25
- export interface CommandExit {
26
- code: number | null;
27
- signal: NodeJS.Signals | null;
28
- error: Error | null;
29
- }
@@ -1,199 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { connect } from "node:net";
3
- import { delimiter, join } from "node:path";
4
- const readinessTimeout = 15_000;
5
- const pollingInterval = 200;
6
- const reportingInterval = 2_000;
7
- const sandboxedClientOrigin = "null";
8
- /** One Client development server owned by a Project development run. */
9
- export class DevelopmentClient {
10
- child;
11
- output = [];
12
- stopped = false;
13
- result = null;
14
- outputWaiter = null;
15
- completion;
16
- constructor(command, directory) {
17
- this.child = spawn(command, {
18
- cwd: directory,
19
- env: commandEnvironment(directory),
20
- shell: true,
21
- stdio: ["ignore", "pipe", "pipe"],
22
- detached: true
23
- });
24
- this.child.stdout?.on("data", chunk => this.push(outputEvent("out", chunk)));
25
- this.child.stderr?.on("data", chunk => this.push(outputEvent("err", chunk)));
26
- this.completion = new Promise(resolve => {
27
- let settled = false;
28
- const finish = (exit) => {
29
- if (settled)
30
- return;
31
- settled = true;
32
- this.result = exit;
33
- this.outputWaiter?.();
34
- this.outputWaiter = null;
35
- resolve(exit);
36
- };
37
- this.child.once("error", error => finish({ code: null, signal: null, error }));
38
- this.child.once("exit", (code, signal) => finish({ code, signal, error: null }));
39
- });
40
- }
41
- drain() { return this.output.splice(0); }
42
- exited() { return this.completion; }
43
- exitResult() { return this.result; }
44
- outputAvailable() {
45
- if (this.output.length || this.result)
46
- return Promise.resolve();
47
- return new Promise(resolve => { this.outputWaiter = resolve; });
48
- }
49
- async stop() {
50
- if (this.stopped)
51
- return;
52
- this.stopped = true;
53
- if (!running(this.child))
54
- return;
55
- terminate(this.child, "SIGTERM");
56
- await waitUntilStopped(this.child, 1_000);
57
- if (running(this.child))
58
- terminate(this.child, "SIGKILL");
59
- await waitUntilStopped(this.child, 1_000);
60
- }
61
- endingWasRequested() { return this.stopped; }
62
- push(event) {
63
- this.output.push(event);
64
- this.outputWaiter?.();
65
- this.outputWaiter = null;
66
- }
67
- }
68
- /** Refuse to claim a URL already served by an unrelated process. */
69
- export async function assertAvailable(url) {
70
- if (!await occupied(url))
71
- return;
72
- throw new Error(`Client development URL is already in use: ${url}`);
73
- }
74
- /** Wait until a development Client can be loaded by a sandboxed Program iframe. */
75
- export async function* waitForDevelopmentClient(config, client, signal) {
76
- const began = Date.now();
77
- let nextReport = began + reportingInterval;
78
- while (Date.now() - began < readinessTimeout) {
79
- throwIfAborted(signal);
80
- for (const event of client?.drain() ?? [])
81
- yield event;
82
- const exit = client?.exitResult();
83
- if (exit && !client?.endingWasRequested())
84
- throw commandFailure(exit);
85
- const availability = await inspect(config.url, readinessTimeout - (Date.now() - began));
86
- if (availability === "ready")
87
- return;
88
- if (availability === "cors-blocked") {
89
- throw new Error([
90
- `Client development URL responded, but does not allow the sandboxed Client origin: ${config.url}`,
91
- "Enable CORS so the response includes Access-Control-Allow-Origin: *."
92
- ].join("\n"));
93
- }
94
- const now = Date.now();
95
- if (now >= nextReport) {
96
- yield { event: "waiting", subject: "client", url: config.url };
97
- while (nextReport <= now)
98
- nextReport += reportingInterval;
99
- }
100
- await pause(Math.min(pollingInterval, readinessTimeout - (now - began)), signal);
101
- }
102
- throw new Error(`Client development URL did not respond within 15 seconds: ${config.url}`);
103
- }
104
- export function commandFailure(exit) {
105
- if (exit.error)
106
- return new Error(`Client development command failed: ${exit.error.message}`);
107
- if (exit.signal)
108
- return new Error(`Client development command ended on ${exit.signal}`);
109
- return new Error(`Client development command exited with ${exit.code ?? 0}`);
110
- }
111
- function outputEvent(stream, chunk) {
112
- return { event: "output", source: "client-development", stream, text: String(chunk) };
113
- }
114
- function commandEnvironment(directory) {
115
- const key = Object.keys(process.env).find(name => name.toLowerCase() === "path") ?? "PATH";
116
- const inherited = process.env[key];
117
- return { ...process.env, [key]: [join(directory, "node_modules", ".bin"), inherited].filter(Boolean).join(delimiter) };
118
- }
119
- async function occupied(url) {
120
- const location = new URL(url);
121
- const port = Number(location.port || (location.protocol === "https:" ? 443 : 80));
122
- return await new Promise(resolve => {
123
- const socket = connect({ host: location.hostname, port });
124
- let done = false;
125
- const finish = (value) => {
126
- if (done)
127
- return;
128
- done = true;
129
- socket.destroy();
130
- resolve(value);
131
- };
132
- socket.setTimeout(500);
133
- socket.once("connect", () => finish(true));
134
- socket.once("error", () => finish(false));
135
- socket.once("timeout", () => finish(false));
136
- });
137
- }
138
- async function inspect(url, remaining) {
139
- try {
140
- const response = await fetch(url, {
141
- headers: { origin: sandboxedClientOrigin },
142
- signal: AbortSignal.timeout(Math.max(1, Math.min(500, remaining)))
143
- });
144
- const allowedOrigin = response.headers.get("access-control-allow-origin")?.trim();
145
- await response.body?.cancel();
146
- return allowedOrigin === "*" || allowedOrigin === sandboxedClientOrigin ? "ready" : "cors-blocked";
147
- }
148
- catch {
149
- return "unavailable";
150
- }
151
- }
152
- function terminate(child, signal) {
153
- if (!child.pid)
154
- return;
155
- try {
156
- process.kill(-child.pid, signal);
157
- }
158
- catch {
159
- child.kill(signal);
160
- }
161
- }
162
- function running(child) {
163
- if (!child.pid)
164
- return false;
165
- try {
166
- process.kill(-child.pid, 0);
167
- return true;
168
- }
169
- catch {
170
- return child.exitCode === null && child.signalCode === null;
171
- }
172
- }
173
- async function waitUntilStopped(child, milliseconds) {
174
- const deadline = Date.now() + milliseconds;
175
- while (running(child) && Date.now() < deadline)
176
- await new Promise(resolve => setTimeout(resolve, 20));
177
- }
178
- function pause(milliseconds, signal) {
179
- return new Promise((resolve, reject) => {
180
- const timer = setTimeout(finish, Math.max(0, milliseconds));
181
- const cancel = () => {
182
- cleanup();
183
- reject(signal?.reason instanceof Error ? signal.reason : new Error("The operation was cancelled"));
184
- };
185
- const cleanup = () => {
186
- clearTimeout(timer);
187
- signal?.removeEventListener("abort", cancel);
188
- };
189
- function finish() { cleanup(); resolve(); }
190
- if (signal?.aborted)
191
- cancel();
192
- else
193
- signal?.addEventListener("abort", cancel, { once: true });
194
- });
195
- }
196
- function throwIfAborted(signal) {
197
- if (signal?.aborted)
198
- throw signal.reason instanceof Error ? signal.reason : new Error("The operation was cancelled");
199
- }