@phreshos/server 0.1.14 → 0.1.15

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
@@ -110,9 +110,12 @@ incarnation is ready. The Server SDK owns one deadline across readiness and the
110
110
  answer; absence or incarnation loss rejects without turning a boundary into a
111
111
  waiter.
112
112
 
113
- Server Program handles add `install()` and `fork()`. Their filesystem areas
114
- also expose `path()` and traversal-safe `resolve()`, because filesystem work is
115
- performed locally in the Server SDK after the Host supplies only the area root.
113
+ Server Program handles add `install()` and `fork()`. `install()` is an async
114
+ generator that opens immediately, yields ordered `stdout` and `stderr` command
115
+ chunks, completes only after installation succeeds, and throws the owning
116
+ System error when installation fails. Their filesystem areas also expose
117
+ `path()` and traversal-safe `resolve()`, because filesystem work is performed
118
+ locally in the Server SDK after the Host supplies only the area root.
116
119
  Object descriptions passed to `host.program.create()` therefore require an
117
120
  explicit absolute storage root as well as at least one declared Endpoint.
118
121
 
@@ -190,3 +193,17 @@ The system validates this through the same launch contract as
190
193
  `program.process.create()`. Setting startup does not create a Process immediately.
191
194
  `uninstall(false)` preserves the configuration but makes it inactive until the
192
195
  Program is installed again; removing everything deletes it.
196
+
197
+ Installation and uninstallation expose any declared Server command output as
198
+ ordered streams. Consuming the generator waits for the authoritative operation
199
+ to finish:
200
+
201
+ ```ts
202
+ for await (const chunk of program.install()) {
203
+ (chunk.stream === "stderr" ? process.stderr : process.stdout).write(chunk.text)
204
+ }
205
+
206
+ for await (const chunk of program.uninstall(true)) {
207
+ (chunk.stream === "stderr" ? process.stderr : process.stdout).write(chunk.text)
208
+ }
209
+ ```
package/dist/domain.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Client as CoreClient, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, type AnswerCapture, type AskCapture, type Cleanup, type ClientDeclaration, type Exit, type Launch, type Position, type ProgramPermission, type ProgramProcess as CoreProgramProcess, type ProgramArea as CoreProgramArea, type Size, type TrafficMessage, type Window as CoreWindow, type WindowState } from "@phreshos/core";
1
+ import { Client as CoreClient, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, type AnswerCapture, type AskCapture, type Cleanup, type ClientDeclaration, type Exit, type Launch, type Position, type ProgramCommandChunk, type ProgramPermission, type ProgramProcess as CoreProgramProcess, type ProgramArea as CoreProgramArea, type Size, type TrafficMessage, type Window as CoreWindow, type WindowState } from "@phreshos/core";
2
2
  import Events from "./events.js";
3
3
  import { type ProgramStartup } from "./startup.js";
4
4
  import { type ClientServiceHandler, type ServerServiceHandler } from "./service.js";
@@ -63,8 +63,8 @@ export interface Program<Events extends object = {}> extends Omit<CoreProgram<Ev
63
63
  readonly permission: ProgramPermission;
64
64
  /** Operations and lifecycle observation for this Program's Processes. */
65
65
  readonly process: ProgramProcess;
66
- /** Installs this Program and returns the same handle. */
67
- install(): Promise<this>;
66
+ /** Installs this Program while yielding its command output in order. */
67
+ install(): AsyncGenerator<ProgramCommandChunk, void, void>;
68
68
  /** Creates a new runtime Program with the supplied stable identity. */
69
69
  fork(identity: string): Promise<Program>;
70
70
  }
package/dist/domain.js CHANGED
@@ -70,21 +70,31 @@ class ProgramHandle extends ProgramBase {
70
70
  const answer = await wire.request(["installed", this.address]);
71
71
  return answer[0];
72
72
  }
73
- async install() {
74
- await wire.request(["install", this.address]);
75
- return this;
73
+ async *install() {
74
+ for await (const value of wire.stream(["install", this.address])) {
75
+ yield programCommandChunk(value);
76
+ }
76
77
  }
77
78
  async fork(identity) {
78
79
  const answer = await wire.request(["fork", this.address, identity]);
79
80
  return program(answer[0]);
80
81
  }
81
- async uninstall(everything = false) {
82
- await wire.request(["uninstall", this.address, everything]);
82
+ async *uninstall(everything = false) {
83
+ for await (const value of wire.stream(["uninstall", this.address, everything])) {
84
+ yield programCommandChunk(value);
85
+ }
83
86
  }
84
87
  async forget() {
85
88
  await wire.request(["forget", this.address]);
86
89
  }
87
90
  }
91
+ function programCommandChunk(value) {
92
+ const chunk = value;
93
+ if (!chunk || (chunk.stream !== "stdout" && chunk.stream !== "stderr") || typeof chunk.text !== "string") {
94
+ throw new Error("The host returned an invalid Program command chunk");
95
+ }
96
+ return Object.freeze({ stream: chunk.stream, text: chunk.text });
97
+ }
88
98
  function declaration(record) {
89
99
  return Object.freeze({
90
100
  start: record.start
package/dist/wire.d.ts CHANGED
@@ -7,6 +7,7 @@ type TrafficKind = "publish" | "ask" | "answer";
7
7
  /** The server endpoint's sole IPC adapter. */
8
8
  declare class Wire {
9
9
  private readonly pending;
10
+ private readonly streams;
10
11
  private readonly subscribers;
11
12
  private readonly every;
12
13
  private readonly answerers;
@@ -17,6 +18,8 @@ declare class Wire {
17
18
  send(route: string, ...values: unknown[]): void;
18
19
  request(values: unknown[], timeout?: number): Promise<unknown>;
19
20
  requestWithin(values: unknown[], deadline: Deadline): Promise<unknown>;
21
+ /** Opens one long-running host operation and yields its ordered values. */
22
+ stream(values: unknown[], timeout?: number): AsyncIterableIterator<unknown>;
20
23
  /** Resolves this endpoint's Process address only for operations that need it. */
21
24
  identity(): Promise<{
22
25
  process: string;
@@ -40,6 +43,7 @@ declare class Wire {
40
43
  private releaseWaiting;
41
44
  private forgetIncoming;
42
45
  private settle;
46
+ private receiveStream;
43
47
  }
44
48
  declare const _default: Wire;
45
49
  export default _default;
package/dist/wire.js CHANGED
@@ -5,6 +5,7 @@ import { deserialize, serialize } from "./messagepack.js";
5
5
  /** The server endpoint's sole IPC adapter. */
6
6
  class Wire {
7
7
  pending = new Map();
8
+ streams = new Map();
8
9
  subscribers = new Map();
9
10
  every = new Map();
10
11
  answerers = new Map();
@@ -35,6 +36,10 @@ class Wire {
35
36
  }
36
37
  return;
37
38
  }
39
+ if (values[0] === "stream" && typeof values[1] === "string" && typeof values[2] === "string") {
40
+ this.receiveStream(values[1], values[2], values[3]);
41
+ return;
42
+ }
38
43
  if (values[0] === "answer" && typeof values[1] === "string") {
39
44
  this.settle(values[1], values.at(-1));
40
45
  return;
@@ -66,6 +71,46 @@ class Wire {
66
71
  this.send("end-host", "wait", question, ...values);
67
72
  });
68
73
  }
74
+ /** Opens one long-running host operation and yields its ordered values. */
75
+ stream(values, timeout = defaultTimeout) {
76
+ const wire = this;
77
+ return (async function* () {
78
+ const question = randomUUID();
79
+ const state = {
80
+ queue: [],
81
+ opened: false,
82
+ ended: false,
83
+ failure: null,
84
+ wake: null,
85
+ timer: setTimeout(() => {
86
+ state.failure = new Error(`Answer timeout ${timeout}ms`);
87
+ state.wake?.();
88
+ state.wake = null;
89
+ }, timeout)
90
+ };
91
+ wire.send("boundary", "expect", question);
92
+ wire.streams.set(question, state);
93
+ wire.send("end-host", "stream", question, ...values);
94
+ try {
95
+ while (true) {
96
+ if (state.queue.length) {
97
+ yield state.queue.shift();
98
+ continue;
99
+ }
100
+ if (state.failure)
101
+ throw state.failure;
102
+ if (state.ended)
103
+ return;
104
+ await new Promise(resolve => { state.wake = resolve; });
105
+ }
106
+ }
107
+ finally {
108
+ clearTimeout(state.timer);
109
+ wire.streams.delete(question);
110
+ wire.send("boundary", "forget", question);
111
+ }
112
+ })();
113
+ }
69
114
  /** Resolves this endpoint's Process address only for operations that need it. */
70
115
  identity() {
71
116
  if (!this.identityPromise) {
@@ -255,7 +300,44 @@ class Wire {
255
300
  else
256
301
  pending.reject(new Error("The boundary returned an invalid outcome"));
257
302
  }
303
+ receiveStream(question, operation, value) {
304
+ const stream = this.streams.get(question);
305
+ if (!stream || stream.ended || stream.failure)
306
+ return;
307
+ if (operation === "open") {
308
+ if (stream.opened)
309
+ return;
310
+ stream.opened = true;
311
+ clearTimeout(stream.timer);
312
+ }
313
+ else if (!stream.opened) {
314
+ stream.failure = new Error("The boundary produced a stream value before opening the stream");
315
+ }
316
+ else if (operation === "data") {
317
+ if (stream.queue.length >= maximumStreamQueue) {
318
+ stream.failure = new Error(`Host stream queue exceeded its capacity of ${maximumStreamQueue}`);
319
+ }
320
+ else {
321
+ stream.queue.push(value);
322
+ }
323
+ }
324
+ else if (operation === "answer") {
325
+ const outcome = value;
326
+ if (outcome?.success === true)
327
+ stream.ended = true;
328
+ else if (outcome?.success === false && typeof outcome.error === "string")
329
+ stream.failure = new Error(outcome.error);
330
+ else
331
+ stream.failure = new Error("The boundary returned an invalid stream outcome");
332
+ }
333
+ else {
334
+ stream.failure = new Error(`The boundary returned an invalid stream operation "${operation}"`);
335
+ }
336
+ stream.wake?.();
337
+ stream.wake = null;
338
+ }
258
339
  }
340
+ const maximumStreamQueue = 256;
259
341
  function failed(error) {
260
342
  return {
261
343
  success: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/server",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "The SDK used by a Program's server endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -52,7 +52,7 @@
52
52
  "@msgpack/msgpack": "^3.1.3"
53
53
  },
54
54
  "devDependencies": {
55
- "@phreshos/core": "^0.1.12",
55
+ "@phreshos/core": "^0.1.13",
56
56
  "@types/node": "^26.2.0",
57
57
  "typescript": "^6.0.3"
58
58
  }