@phreshos/node 0.1.12 → 0.1.13

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/dist/shell.js ADDED
@@ -0,0 +1,125 @@
1
+ import { spawn } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import ProcessTree from "./process-tree.js";
4
+ /** Execute one command locally while its iterator owns the complete process tree. */
5
+ export default function shell(command, options = {}) {
6
+ return (async function* () {
7
+ const input = validate(command, options);
8
+ const queue = [];
9
+ let wake = null;
10
+ let failure = null;
11
+ let closed = false;
12
+ let treeEnded = false;
13
+ let ending = null;
14
+ let settle;
15
+ const stopped = new Promise(resolve => { settle = resolve; });
16
+ const notify = () => {
17
+ wake?.();
18
+ wake = null;
19
+ };
20
+ const finish = () => {
21
+ if (!closed || !treeEnded || !ending)
22
+ return;
23
+ queue.push({
24
+ event: {
25
+ event: "exited",
26
+ exit: {
27
+ status: ending.signal ? "signaled" : "exited",
28
+ code: ending.code,
29
+ signal: ending.signal
30
+ }
31
+ }
32
+ });
33
+ settle();
34
+ notify();
35
+ };
36
+ const child = spawn(input.command, {
37
+ shell: true,
38
+ detached: true,
39
+ cwd: input.cwd,
40
+ env: { ...process.env, ...input.env },
41
+ stdio: ["ignore", "pipe", "pipe"]
42
+ });
43
+ const tree = new ProcessTree(child, (code, signal) => {
44
+ ending = { code, signal };
45
+ treeEnded = true;
46
+ finish();
47
+ });
48
+ const output = (stream, name) => {
49
+ stream.on("data", chunk => {
50
+ stream.pause();
51
+ queue.push({ event: { event: "output", stream: name, text: String(chunk) }, resume: () => stream.resume() });
52
+ notify();
53
+ });
54
+ };
55
+ const abort = () => {
56
+ failure = options.signal?.reason instanceof Error ? options.signal.reason : new Error("The shell command was cancelled");
57
+ tree.stop();
58
+ notify();
59
+ };
60
+ child.once("spawn", () => {
61
+ if (typeof child.pid !== "number") {
62
+ failure = new Error("The shell command started without a process identity");
63
+ tree.stop();
64
+ }
65
+ else {
66
+ queue.push({ event: { event: "started", pid: child.pid } });
67
+ }
68
+ notify();
69
+ });
70
+ child.once("error", error => {
71
+ failure = error;
72
+ settle();
73
+ notify();
74
+ });
75
+ child.once("close", () => {
76
+ closed = true;
77
+ finish();
78
+ });
79
+ output(child.stdout, "stdout");
80
+ output(child.stderr, "stderr");
81
+ if (options.signal?.aborted)
82
+ abort();
83
+ else
84
+ options.signal?.addEventListener("abort", abort, { once: true });
85
+ try {
86
+ while (true) {
87
+ if (failure)
88
+ throw failure;
89
+ const next = queue.shift();
90
+ if (next) {
91
+ yield next.event;
92
+ next.resume?.();
93
+ if (next.event.event === "exited")
94
+ return;
95
+ continue;
96
+ }
97
+ await new Promise(resolve => { wake = resolve; });
98
+ }
99
+ }
100
+ finally {
101
+ options.signal?.removeEventListener("abort", abort);
102
+ if (!closed || !treeEnded)
103
+ tree.stop();
104
+ await stopped;
105
+ }
106
+ })();
107
+ }
108
+ function validate(command, options) {
109
+ if (typeof command !== "string" || !command.trim())
110
+ throw new Error("A shell command must be non-empty text");
111
+ if (!options || typeof options !== "object" || Array.isArray(options))
112
+ throw new Error("Shell options must be an object");
113
+ if (Object.keys(options).some(key => key !== "cwd" && key !== "env" && key !== "signal"))
114
+ throw new Error("Shell options contain an unknown field");
115
+ if (options.signal !== undefined && !(options.signal instanceof AbortSignal))
116
+ throw new Error("A shell signal must be an AbortSignal");
117
+ const cwd = options.cwd ?? homedir();
118
+ const env = options.env ?? {};
119
+ if (typeof cwd !== "string" || !cwd)
120
+ throw new Error("A shell working directory must be non-empty text");
121
+ if (!env || typeof env !== "object" || Array.isArray(env) || Object.values(env).some(value => typeof value !== "string")) {
122
+ throw new Error("Shell environment values must be text");
123
+ }
124
+ return { command, cwd, env };
125
+ }
package/dist/storage.d.ts CHANGED
@@ -1,3 +1,7 @@
1
- import type { SystemStorage } from "@phreshos/core";
1
+ import type { Storage } from "@phreshos/core";
2
2
  /** Create one filesystem implementation bounded beneath a resolved absolute root. */
3
- export declare function filesystemStorage(source: string | (() => Promise<string>), label: string): SystemStorage;
3
+ export declare function filesystemStorage(source: string | (() => Promise<string>), label: string, lifetime?: Lifetime): Storage;
4
+ /** Create native filesystem access entered from one resolved absolute path. */
5
+ export declare function nativeStorage(source: string | (() => Promise<string>), label: string, lifetime?: Lifetime): Storage;
6
+ type Lifetime = () => AbortSignal;
7
+ export {};
package/dist/storage.js CHANGED
@@ -1,39 +1,51 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createReadStream, createWriteStream, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
3
3
  import { rm } from "node:fs/promises";
4
- import { dirname, isAbsolute, join, relative, sep } from "node:path";
4
+ import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
5
5
  import { Readable } from "node:stream";
6
6
  import { pipeline } from "node:stream/promises";
7
7
  /** Create one filesystem implementation bounded beneath a resolved absolute root. */
8
- export function filesystemStorage(source, label) {
8
+ export function filesystemStorage(source, label, lifetime) {
9
+ return createStorage(source, label, contained, lifetime);
10
+ }
11
+ /** Create native filesystem access entered from one resolved absolute path. */
12
+ export function nativeStorage(source, label, lifetime) {
13
+ return createStorage(source, label, (root, parts) => resolvePath(root, ...parts), lifetime);
14
+ }
15
+ function createStorage(source, label, locate, lifetime) {
9
16
  let root = null;
10
17
  const resolveRoot = () => {
18
+ active(lifetime);
11
19
  if (!root)
12
20
  root = Promise.resolve(typeof source === "string" ? source : source()).then(value => {
21
+ active(lifetime);
13
22
  if (!isAbsolute(value))
14
23
  throw new Error("A Storage root must be absolute");
15
24
  return value;
16
25
  });
17
26
  return root;
18
27
  };
19
- const path = () => resolveRoot();
20
- const resolve = async (...parts) => contained(await path(), parts);
28
+ const path = async () => await resolveRoot();
29
+ const resolve = async (...parts) => locate(await path(), parts);
21
30
  async function stream(...parts) {
31
+ const signal = active(lifetime);
22
32
  const destination = await resolve(...parts);
23
33
  const found = describe(destination);
24
34
  if (!found)
25
35
  throw new Error(`There is no ${parts.join("/")} in ${label}`);
26
36
  if (found.kind !== "file")
27
37
  throw new Error(`${parts.join("/")} is not a file`);
28
- return Readable.toWeb(createReadStream(destination));
38
+ return Readable.toWeb(createReadStream(destination, { signal }));
29
39
  }
30
40
  async function write(...args) {
41
+ const signal = active(lifetime);
31
42
  const parts = args.slice(0, -1);
32
43
  const destination = await resolve(...parts);
33
44
  const temporary = join(dirname(destination), `.${randomUUID()}.writing`);
34
45
  mkdirSync(dirname(destination), { recursive: true });
35
46
  try {
36
- await pipeline(Readable.fromWeb(content(args.at(-1))), createWriteStream(temporary, { flags: "wx" }));
47
+ await pipeline(Readable.fromWeb(content(args.at(-1))), createWriteStream(temporary, { flags: "wx" }), { signal });
48
+ signal?.throwIfAborted();
37
49
  renameSync(temporary, destination);
38
50
  }
39
51
  catch (error) {
@@ -49,14 +61,16 @@ export function filesystemStorage(source, label) {
49
61
  async text(...parts) { return new Response(await stream(...parts)).text(); },
50
62
  async json(...parts) { return JSON.parse(await new Response(await stream(...parts)).text()); },
51
63
  write,
52
- async stat(...parts) { return describe(await resolve(...parts)); },
53
- async list(...parts) { return readdirSync(await resolve(...parts)).sort(); },
64
+ async stat(...parts) { active(lifetime); return describe(await resolve(...parts)); },
65
+ async list(...parts) { active(lifetime); return readdirSync(await resolve(...parts)).sort(); },
54
66
  async delete(...parts) {
67
+ active(lifetime);
55
68
  if (!parts.length)
56
69
  throw new Error("Emptying a place is clear, not delete");
57
70
  rmSync(await resolve(...parts), { recursive: true, force: true });
58
71
  },
59
72
  async clear(...parts) {
73
+ active(lifetime);
60
74
  const destination = await resolve(...parts);
61
75
  const found = describe(destination);
62
76
  if (found && found.kind !== "directory")
@@ -66,6 +80,11 @@ export function filesystemStorage(source, label) {
66
80
  }
67
81
  };
68
82
  }
83
+ function active(lifetime) {
84
+ const signal = lifetime?.();
85
+ signal?.throwIfAborted();
86
+ return signal;
87
+ }
69
88
  function content(value) {
70
89
  if (value instanceof ReadableStream)
71
90
  return value;
package/dist/system.d.ts CHANGED
@@ -1,42 +1,47 @@
1
- import { Client as CoreClient, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerService as CoreServerService, type ProgramDefinition, type ServiceKey, type System as CoreSystem, type SystemClientEntity, type SystemEndpointEntity, type SystemProcessEntity, type SystemProcess, type SystemProcessRunEvent, type SystemProcessRunOptions, type SystemProgram, type SystemProgramEntity, type SystemServerEntity, type SystemUploads, type SystemStorage, type WritableAppearance } from "@phreshos/core";
2
- export type ProgramProcessRunOptions = SystemProcessRunOptions;
3
- export type ProgramProcessRunEvent = SystemProcessRunEvent;
1
+ import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, type ProgramDefinition, type ProgramProcessRunEvent as CoreProgramProcessRunEvent, type ProgramProcessRunOptions as CoreProgramProcessRunOptions, type ServiceKey, type ShellOptions, type System as CoreSystem, type SystemProcess, type SystemProgram, type SystemUploads, type Storage, type WritableAppearance } from "@phreshos/core";
2
+ export type ProgramProcessRunOptions = CoreProgramProcessRunOptions;
3
+ export type ProgramProcessRunEvent = CoreProgramProcessRunEvent;
4
+ type ServiceEndpoint = ServiceKey["endpoint"];
5
+ type ServiceAddress<Endpoint extends ServiceEndpoint> = Omit<ServiceKey, "endpoint"> & Readonly<{
6
+ endpoint: Endpoint;
7
+ }>;
8
+ type ServiceHandle<Endpoint extends ServiceEndpoint, EventsMap extends object, Fallback = unknown> = Endpoint extends "server" ? ServerService<EventsMap, Fallback> : ClientService<EventsMap, Fallback>;
4
9
  /** One connected owner-local implementation of the shared System contract. */
5
10
  export declare class System implements CoreSystem {
6
- readonly storage: SystemStorage;
11
+ readonly storage: Storage;
7
12
  readonly appearance: WritableAppearance;
8
13
  readonly program: SystemProgram;
9
14
  readonly process: SystemProcess;
10
15
  readonly uploads: SystemUploads;
16
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
17
+ shell(command: string, options?: ShellOptions): AsyncGenerator<import("@phreshos/core").ShellEvent, void, void>;
11
18
  private constructor();
12
19
  /** Connect to the System selected by argument, environment, or owner default. */
13
20
  static connect(home?: string): Promise<System>;
14
21
  /** Atomically replace one runtime Program without touching its installed form. */
15
- forceCreateProgram(source: ProgramDefinition | string): Promise<SystemProgramEntity>;
22
+ forceCreateProgram(source: ProgramDefinition | string): Promise<Program>;
16
23
  /** Close this owner connection and abort every attached operation it owns. */
17
24
  disconnect(): Promise<void>;
18
- service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceKey & {
19
- endpoint: "server";
20
- }): ServerService<EventsMap, Fallback>;
21
- service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceKey & {
22
- endpoint: "client";
23
- }): ClientService<EventsMap, Fallback>;
25
+ service<Endpoint extends ServiceEndpoint>(key: ServiceAddress<Endpoint>): ServiceHandle<Endpoint, {}>;
26
+ service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"server">): ServerService<EventsMap, Fallback>;
27
+ service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"client">): ClientService<EventsMap, Fallback>;
24
28
  }
25
- /** Node-SDK handle for a Service provided by a Server Endpoint. */
29
+ /** Node SDK handle for a Service provided by a Server Endpoint. */
26
30
  export declare class ServerService<EventsMap extends object = {}, Fallback = unknown> extends CoreServerService<EventsMap, Fallback> {
27
31
  protected constructor();
28
32
  }
29
- /** Node-SDK handle for a Service provided by a Client Endpoint. */
33
+ /** Node SDK handle for a Service provided by a Client Endpoint. */
30
34
  export declare class ClientService<EventsMap extends object = {}, Fallback = unknown> extends CoreClientService<EventsMap, Fallback> {
31
35
  protected constructor();
32
36
  }
33
- export type Program = SystemProgramEntity;
37
+ export type Program = CoreProgram;
34
38
  export declare const Program: typeof CoreProgram;
35
- export type Process = SystemProcessEntity;
39
+ export type Process = CoreProcess;
36
40
  export declare const Process: typeof CoreProcess;
37
- export type Endpoint<EventsMap extends object = {}, Fallback = unknown> = SystemEndpointEntity<EventsMap, Fallback>;
41
+ export type Endpoint<EventsMap extends object = {}, Fallback = unknown> = CoreEndpoint<EventsMap, Fallback>;
38
42
  export declare const Endpoint: typeof CoreEndpoint;
39
- export type Server<EventsMap extends object = {}, Fallback = unknown> = SystemServerEntity<EventsMap, Fallback>;
40
- export declare const Server: typeof CoreServer;
41
- export type Client<EventsMap extends object = {}, Fallback = unknown> = SystemClientEntity<EventsMap, Fallback>;
42
- export declare const Client: typeof CoreClient;
43
+ export type ServerEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreServerEndpoint<EventsMap, Fallback>;
44
+ export declare const ServerEndpoint: typeof CoreServerEndpoint;
45
+ export type ClientEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreClientEndpoint<EventsMap, Fallback>;
46
+ export declare const ClientEndpoint: typeof CoreClientEndpoint;
47
+ export {};
package/dist/system.js CHANGED
@@ -1,39 +1,49 @@
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";
1
+ import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, 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
5
  import HandleRegistry from "./handle-registry.js";
6
6
  import { resolveHome } from "./home.js";
7
- import { filesystemStorage } from "./storage.js";
8
- import { programPermission, programSql, programStore } from "./program-resources.js";
7
+ import { filesystemStorage, nativeStorage } from "./storage.js";
8
+ import { programPermissions, programSql, programStore } from "./program-resources.js";
9
9
  import { EndpointTrafficHandle, ServerTrafficHandle } from "./traffic.js";
10
- import { openConnection, request, streamProgram } from "./transport.js";
10
+ import { openConnection, request, stream } from "./transport.js";
11
11
  import Uploads from "./uploads.js";
12
+ import shell from "./shell.js";
12
13
  const systems = new WeakMap();
13
14
  const ProgramBase = CoreProgram;
14
15
  const ProcessBase = CoreProcess;
15
- const ServerBase = CoreServer;
16
- const ClientBase = CoreClient;
16
+ const ServerEndpointBase = CoreServerEndpoint;
17
+ const ClientEndpointBase = CoreClientEndpoint;
17
18
  /** One connected owner-local implementation of the shared System contract. */
18
19
  export class System {
19
- storage = filesystemStorage(homedir(), "the native home directory");
20
+ storage;
20
21
  appearance;
21
22
  program;
22
23
  process;
23
24
  uploads;
25
+ async fetch(input, init) {
26
+ const request = new Request(input, init);
27
+ return await fetch(request, { signal: connectedSignal(this, request.signal) });
28
+ }
29
+ async *shell(command, options = {}) {
30
+ yield* shell(command, { ...options, signal: connectedSignal(this, options.signal) });
31
+ }
24
32
  constructor(address, connection) {
25
33
  const lifetime = new AbortController();
26
34
  const handles = new HandleRegistry();
27
35
  const transport = {
28
36
  control: (value, signal) => request(address, "system", value, connectedSignal(this, signal)),
29
37
  api: (value, signal) => request(address, "api", value, connectedSignal(this, signal)),
30
- lifecycle: (value, signal) => streamProgram(address, value, connectedSignal(this, signal))
38
+ stream: (target, value, signal) => stream(address, target, value, connectedSignal(this, signal))
31
39
  };
32
40
  systems.set(this, { address, connection, handles, lifetime, transport, closed: false });
41
+ connection.once("close", () => closeSystem(this, new Error("This System connection is closed")));
42
+ this.storage = nativeStorage(homedir(), "the native filesystem", () => connectedSignal(this));
33
43
  this.appearance = new SystemAppearance(transport);
34
44
  this.program = new ProgramRegistry(this);
35
45
  this.process = new ProcessRegistry(this);
36
- this.uploads = new Uploads(value => transport.api(value));
46
+ this.uploads = new Uploads(value => transport.api(value), () => connectedSignal(this));
37
47
  }
38
48
  /** Connect to the System selected by argument, environment, or owner default. */
39
49
  static async connect(home) {
@@ -44,7 +54,7 @@ export class System {
44
54
  /** Atomically replace one runtime Program without touching its installed form. */
45
55
  async forceCreateProgram(source) {
46
56
  requireConnected(this);
47
- for await (const event of transport(this).lifecycle({ word: "force-create", program: source })) {
57
+ for await (const event of transport(this).stream("program", { word: "force-create", program: source })) {
48
58
  if (event.event === "created")
49
59
  return programHandle(this, required(event.program));
50
60
  }
@@ -52,13 +62,7 @@ export class System {
52
62
  }
53
63
  /** Close this owner connection and abort every attached operation it owns. */
54
64
  async disconnect() {
55
- const state = systemState(this);
56
- if (state.closed)
57
- return;
58
- state.closed = true;
59
- state.lifetime.abort(new Error("This System connection is closed"));
60
- state.connection.destroy();
61
- state.handles.clear();
65
+ closeSystem(this, new Error("This System connection is closed"));
62
66
  }
63
67
  service(key) {
64
68
  requireConnected(this);
@@ -81,6 +85,15 @@ function systemState(system) {
81
85
  throw new Error("Unknown System connection");
82
86
  return state;
83
87
  }
88
+ function closeSystem(system, reason) {
89
+ const state = systemState(system);
90
+ if (state.closed)
91
+ return;
92
+ state.closed = true;
93
+ state.lifetime.abort(reason);
94
+ state.connection.destroy();
95
+ state.handles.clear();
96
+ }
84
97
  function requireConnected(system) {
85
98
  if (systemState(system).closed)
86
99
  throw new Error("This System connection is closed");
@@ -149,7 +162,7 @@ class ProgramRegistry extends Events {
149
162
  }
150
163
  }
151
164
  async create(source) {
152
- for await (const event of transport(this.system).lifecycle({ word: "create", program: source })) {
165
+ for await (const event of transport(this.system).stream("program", { word: "create", program: source })) {
153
166
  if (event.event === "created")
154
167
  return programHandle(this.system, required(event.program));
155
168
  }
@@ -173,9 +186,9 @@ class ProgramHandle extends ProgramBase {
173
186
  store;
174
187
  logs;
175
188
  database;
176
- permission;
177
189
  process;
178
190
  startup;
191
+ permissions;
179
192
  snapshot;
180
193
  constructor(system, snapshot) {
181
194
  super();
@@ -188,16 +201,17 @@ class ProgramHandle extends ProgramBase {
188
201
  capability: "program", operation: "wait", handle: address, event, timeout
189
202
  }, signal)));
190
203
  const request = (value) => transport(system).api(value);
191
- this.data = filesystemStorage(() => programStoragePath(system, address, "data"), `Program "${this.identity}" data`);
192
- this.cache = filesystemStorage(() => programStoragePath(system, address, "cache"), `Program "${this.identity}" cache`);
204
+ this.data = filesystemStorage(() => programStoragePath(system, address, "data"), `Program "${this.identity}" data`, () => connectedSignal(system));
205
+ this.cache = filesystemStorage(() => programStoragePath(system, address, "cache"), `Program "${this.identity}" cache`, () => connectedSignal(system));
193
206
  this.store = programStore(request, address);
194
207
  this.logs = programSql(request, address, "logs");
195
208
  this.database = programSql(request, address, "database");
196
- this.permission = programPermission(request, address);
197
209
  this.process = new ProgramProcesses(system, this);
198
210
  this.startup = new ProgramStartup(system, this);
211
+ this.permissions = programPermissions(request, address);
199
212
  }
200
213
  get name() { return this.snapshot.name; }
214
+ get assetId() { return this.snapshot.assetId; }
201
215
  get version() { return this.snapshot.version; }
202
216
  get description() { return this.snapshot.description; }
203
217
  get hasAgent() { return this.snapshot.hasAgent; }
@@ -215,7 +229,8 @@ class ProgramHandle extends ProgramBase {
215
229
  size: this.snapshot.client.size,
216
230
  position: this.snapshot.client.position,
217
231
  layer: this.snapshot.client.layer,
218
- minimize: this.snapshot.client.minimize
232
+ minimize: this.snapshot.client.minimize,
233
+ permissions: Object.freeze(Object.fromEntries(Object.entries(this.snapshot.client.permissions).map(([name, values]) => [name, Object.freeze([...values])])))
219
234
  }) : null;
220
235
  }
221
236
  update(snapshot) {
@@ -236,7 +251,7 @@ class ProgramHandle extends ProgramBase {
236
251
  return typeof value === "string" ? value : null;
237
252
  }
238
253
  async installed() {
239
- for await (const event of transport(this.system).lifecycle({ word: "installed", handle: this.address() })) {
254
+ for await (const event of transport(this.system).stream("program", { word: "installed", handle: this.address() })) {
240
255
  if (event.event === "installedState")
241
256
  return event.installed === true;
242
257
  }
@@ -245,14 +260,14 @@ class ProgramHandle extends ProgramBase {
245
260
  install() { return command(this.system, { word: "install-existing", handle: this.address() }); }
246
261
  uninstall(everything = false) { return command(this.system, { word: "uninstall-existing", handle: this.address(), everything }); }
247
262
  async fork(identity) {
248
- for await (const event of transport(this.system).lifecycle({ word: "fork", handle: this.address(), identity })) {
263
+ for await (const event of transport(this.system).stream("program", { word: "fork", handle: this.address(), identity })) {
249
264
  if (event.event === "created")
250
265
  return programHandle(this.system, required(event.program));
251
266
  }
252
267
  throw new Error("The System did not confirm the forked Program");
253
268
  }
254
269
  async forget() {
255
- for await (const _event of transport(this.system).lifecycle({ word: "forget", handle: this.address() })) { /* consume completion */ }
270
+ for await (const _event of transport(this.system).stream("program", { word: "forget", handle: this.address() })) { /* consume completion */ }
256
271
  }
257
272
  address() { return Object.freeze({ identity: this.identity, reference: this.reference }); }
258
273
  }
@@ -264,7 +279,7 @@ class ProgramStartup {
264
279
  this.program = program;
265
280
  }
266
281
  async get() {
267
- for await (const event of transport(this.system).lifecycle({
282
+ for await (const event of transport(this.system).stream("program", {
268
283
  word: "startup", handle: this.program.address(), operation: "get"
269
284
  })) {
270
285
  if (event.event === "startup")
@@ -279,7 +294,7 @@ class ProgramStartup {
279
294
  await this.change("disable");
280
295
  }
281
296
  async change(operation, launch) {
282
- for await (const event of transport(this.system).lifecycle({
297
+ for await (const event of transport(this.system).stream("program", {
283
298
  word: "startup", handle: this.program.address(), operation, launch
284
299
  })) {
285
300
  if (event.event === "startup")
@@ -313,7 +328,7 @@ class ProgramProcesses extends Events {
313
328
  create(launch = {}) { return this.createExact("create-process", launch); }
314
329
  async *run(launch = {}, options = {}) {
315
330
  let process = null;
316
- for await (const event of transport(this.system).lifecycle({
331
+ for await (const event of transport(this.system).stream("program", {
317
332
  word: "run-process",
318
333
  handle: this.program.address(),
319
334
  launch
@@ -360,7 +375,7 @@ class ProgramProcesses extends Events {
360
375
  return processes.map(process => process.identity);
361
376
  }
362
377
  async createExact(word, launch) {
363
- for await (const event of transport(this.system).lifecycle({ word, handle: this.program.address(), launch })) {
378
+ for await (const event of transport(this.system).stream("program", { word, handle: this.program.address(), launch })) {
364
379
  if (event.event === "createdProcess")
365
380
  return processHandle(this.system, required(event.process));
366
381
  }
@@ -406,8 +421,8 @@ class ProcessHandle extends ProcessBase {
406
421
  this.identity = snapshot.identity;
407
422
  this.name = snapshot.name;
408
423
  this.startedAt = new Date(snapshot.startedAt);
409
- this.server = new ServerEndpoint(system, this);
410
- this.client = new ClientEndpoint(system, this);
424
+ this.server = new ServerEndpointHandle(system, this);
425
+ this.client = new ClientEndpointHandle(system, this);
411
426
  }
412
427
  program() { return programHandle(this.system, required(this.snapshot.programSnapshot, this.snapshot.program)); }
413
428
  async parent() {
@@ -478,7 +493,7 @@ class EndpointOperations extends Events {
478
493
  } });
479
494
  }
480
495
  }
481
- class ServerEndpoint extends ServerBase {
496
+ class ServerEndpointHandle extends ServerEndpointBase {
482
497
  system;
483
498
  owner;
484
499
  endpoint = "server";
@@ -512,7 +527,7 @@ class ServerEndpoint extends ServerBase {
512
527
  }) };
513
528
  }
514
529
  }
515
- class ClientEndpoint extends ClientBase {
530
+ class ClientEndpointHandle extends ClientEndpointBase {
516
531
  endpoint = "client";
517
532
  traffic;
518
533
  lifecycle;
@@ -583,7 +598,7 @@ class ServiceBase {
583
598
  void transport(this.system).api({ capability: "service", operation: "publish", key: this.key, event, payload });
584
599
  }
585
600
  }
586
- /** Node-SDK handle for a Service provided by a Server Endpoint. */
601
+ /** Node SDK handle for a Service provided by a Server Endpoint. */
587
602
  export class ServerService extends CoreServerService {
588
603
  constructor() { super(); }
589
604
  }
@@ -614,7 +629,7 @@ class ServerServiceHandle extends ServerService {
614
629
  }) };
615
630
  }
616
631
  }
617
- /** Node-SDK handle for a Service provided by a Client Endpoint. */
632
+ /** Node SDK handle for a Service provided by a Client Endpoint. */
618
633
  export class ClientService extends CoreClientService {
619
634
  constructor() { super(); }
620
635
  }
@@ -645,7 +660,7 @@ async function listProcesses(system) {
645
660
  }
646
661
  }
647
662
  async function* command(system, request) {
648
- for await (const event of transport(system).lifecycle(request)) {
663
+ for await (const event of transport(system).stream("program", request)) {
649
664
  if (event.event === "output")
650
665
  yield {
651
666
  stream: event.stream === "stderr" ? "stderr" : "stdout",
@@ -747,5 +762,5 @@ function required(value, identity = "") {
747
762
  export const Program = CoreProgram;
748
763
  export const Process = CoreProcess;
749
764
  export const Endpoint = CoreEndpoint;
750
- export const Server = CoreServer;
751
- export const Client = CoreClient;
765
+ export const ServerEndpoint = CoreServerEndpoint;
766
+ export const ClientEndpoint = CoreClientEndpoint;
package/dist/traffic.d.ts CHANGED
@@ -15,13 +15,13 @@ export declare class EndpointTrafficHandle<Definitions extends object = {}> exte
15
15
  event: string;
16
16
  questionId: string;
17
17
  message: Readonly<{
18
- to: import("@phreshos/core").Server<{}, unknown> | null;
18
+ to: import("@phreshos/core").ServerEndpoint<{}, unknown> | null;
19
19
  payload: Payload;
20
20
  }>;
21
21
  }>>;
22
22
  protected follow<Capture>(kind: Kind, convert: (value: unknown) => Capture, subscriber: (capture: Capture) => unknown, impossible?: (error: Error) => void): Cleanup;
23
23
  }
24
- /** Directed traffic originating from one canonical Server. */
24
+ /** Directed traffic originating from one canonical Server Endpoint. */
25
25
  export declare class ServerTrafficHandle<Definitions extends object = {}> extends EndpointTrafficHandle<Definitions> implements ServerTraffic<Definitions> {
26
26
  subscribeAnswers<Result = unknown>(subscriber: AnswerSubscriber<Result>): Cleanup;
27
27
  answers<Result = unknown>(options?: EventOptions): AsyncIterableIterator<Readonly<{
package/dist/traffic.js CHANGED
@@ -52,7 +52,7 @@ export class EndpointTrafficHandle extends Events {
52
52
  return () => controller.abort();
53
53
  }
54
54
  }
55
- /** Directed traffic originating from one canonical Server. */
55
+ /** Directed traffic originating from one canonical Server Endpoint. */
56
56
  export class ServerTrafficHandle extends EndpointTrafficHandle {
57
57
  subscribeAnswers(subscriber) {
58
58
  return this.follow("answer", value => answer(value, this.resolveEndpoint), subscriber);
@@ -7,5 +7,5 @@ export interface TransportEvent {
7
7
  export declare function openConnection(path: string): Promise<Socket>;
8
8
  /** Execute one short authoritative System-control request. */
9
9
  export declare function request(path: string, target: "api" | "system", request: unknown, signal?: AbortSignal): Promise<unknown>;
10
- /** Stream one Program lifecycle operation until the System completes it. */
11
- export declare function streamProgram(path: string, request: unknown, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
10
+ /** Stream one long-running authoritative System operation. */
11
+ export declare function stream(path: string, target: "program", request: unknown, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
package/dist/transport.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { connect } from "node:net";
2
+ const maximumStreamQueue = 256;
2
3
  /** Open and retain one owner-local System connection. */
3
4
  export function openConnection(path) {
4
5
  return new Promise((resolve, reject) => {
@@ -52,8 +53,8 @@ export function request(path, target, request, signal) {
52
53
  cancel();
53
54
  });
54
55
  }
55
- /** Stream one Program lifecycle operation until the System completes it. */
56
- export function streamProgram(path, request, signal) {
56
+ /** Stream one long-running authoritative System operation. */
57
+ export function stream(path, target, request, signal) {
57
58
  const events = [];
58
59
  let wake = null;
59
60
  let ended = false;
@@ -65,14 +66,27 @@ export function streamProgram(path, request, signal) {
65
66
  else
66
67
  signal?.addEventListener("abort", cancel, { once: true });
67
68
  let buffer = "";
68
- socket.on("connect", () => socket.write(`${JSON.stringify({ target: "program", request })}\n`));
69
+ socket.on("connect", () => socket.write(`${JSON.stringify({ target, request })}\n`));
69
70
  socket.on("data", chunk => {
70
71
  buffer += String(chunk);
71
72
  const lines = buffer.split("\n");
72
73
  buffer = lines.pop() ?? "";
73
74
  for (const line of lines)
74
75
  if (line.trim()) {
75
- const event = JSON.parse(line);
76
+ if (events.length >= maximumStreamQueue) {
77
+ failure = new Error(`System stream queue exceeded its capacity of ${maximumStreamQueue}`);
78
+ socket.destroy();
79
+ break;
80
+ }
81
+ let event;
82
+ try {
83
+ event = JSON.parse(line);
84
+ }
85
+ catch {
86
+ failure = new Error("The System returned an invalid stream event");
87
+ socket.destroy();
88
+ break;
89
+ }
76
90
  if (event.event === "error")
77
91
  failure = new Error(String(event.message));
78
92
  else