@phreshos/client 0.1.15 → 0.1.16

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
@@ -148,6 +148,16 @@ representation on this desktop. Local reads and updates have no events and do
148
148
  not change Server state. `current.window` is only convenient access to the
149
149
  executing Client's canonical Window; it has no additional authority.
150
150
 
151
+ A Client may uninstall only its own Program. The operation is an async
152
+ generator so a declared Server cleanup command remains observable without a
153
+ fixed answer timeout:
154
+
155
+ ```ts
156
+ for await (const chunk of program.uninstall()) {
157
+ console.log(chunk.stream, chunk.text)
158
+ }
159
+ ```
160
+
151
161
  A Program may converge its Clients on one named Process without a manual
152
162
  `find()`/`create()` race:
153
163
 
package/dist/domain.js CHANGED
@@ -59,9 +59,20 @@ class ProgramHandle extends ProgramBase {
59
59
  const answer = await wire.request(["installed"]);
60
60
  return answer[0];
61
61
  }
62
- async uninstall(everything = false) { await wire.request(["uninstall", undefined, everything]); }
62
+ async *uninstall(everything = false) {
63
+ for await (const value of wire.stream(["uninstall", undefined, everything])) {
64
+ yield programCommandChunk(value);
65
+ }
66
+ }
63
67
  async forget() { await wire.request(["forget"]); }
64
68
  }
69
+ function programCommandChunk(value) {
70
+ const chunk = value;
71
+ if (!chunk || (chunk.stream !== "stdout" && chunk.stream !== "stderr") || typeof chunk.text !== "string") {
72
+ throw new Error("The desktop returned an invalid Program command chunk");
73
+ }
74
+ return Object.freeze({ stream: chunk.stream, text: chunk.text });
75
+ }
65
76
  function declaration(record) {
66
77
  return Object.freeze({
67
78
  start: record.start
package/dist/wire.d.ts CHANGED
@@ -8,6 +8,7 @@ type TrafficKind = "publish" | "ask" | "answer";
8
8
  declare class Wire {
9
9
  private readonly parent;
10
10
  private readonly pending;
11
+ private readonly streams;
11
12
  private readonly subscribers;
12
13
  private readonly every;
13
14
  private readonly impossible;
@@ -15,6 +16,8 @@ declare class Wire {
15
16
  constructor();
16
17
  send(route: string, ...values: unknown[]): void;
17
18
  request(values: unknown[], timeout?: number, transfer?: Transferable[]): Promise<unknown>;
19
+ /** Opens one long-running desktop operation and yields its ordered values. */
20
+ stream(values: unknown[], timeout?: number): AsyncIterableIterator<unknown>;
18
21
  /** Requests a value while treating the caller's deadline as an absent answer. */
19
22
  requestOrNull(values: unknown[], timeout: number): Promise<unknown | null>;
20
23
  requestWithin(values: unknown[], deadline: Deadline, transfer?: Transferable[]): Promise<unknown>;
@@ -40,6 +43,7 @@ declare class Wire {
40
43
  private unregister;
41
44
  private deliver;
42
45
  private settle;
46
+ private receiveStream;
43
47
  }
44
48
  declare const wire: Wire;
45
49
  export default wire;
package/dist/wire.js CHANGED
@@ -6,6 +6,7 @@ import { deserialize, serialize } from "./messagepack.js";
6
6
  class Wire {
7
7
  parent = window.parent === window ? null : window.parent;
8
8
  pending = new Map();
9
+ streams = new Map();
9
10
  subscribers = new Map();
10
11
  every = new Map();
11
12
  impossible = new Map();
@@ -40,6 +41,10 @@ class Wire {
40
41
  this.settle(values[1], values.at(-1));
41
42
  return;
42
43
  }
44
+ if (values[0] === "stream" && typeof values[1] === "string" && typeof values[2] === "string") {
45
+ this.receiveStream(values[1], values[2], values[3]);
46
+ return;
47
+ }
43
48
  this.deliver(route, values);
44
49
  });
45
50
  }
@@ -49,6 +54,46 @@ class Wire {
49
54
  request(values, timeout = defaultTimeout, transfer = []) {
50
55
  return this.question("end-host", values, timeout, transfer);
51
56
  }
57
+ /** Opens one long-running desktop operation and yields its ordered values. */
58
+ stream(values, timeout = defaultTimeout) {
59
+ const wire = this;
60
+ return (async function* () {
61
+ const question = `client:${crypto.randomUUID()}:${crypto.randomUUID()}`;
62
+ const state = {
63
+ queue: [],
64
+ opened: false,
65
+ ended: false,
66
+ failure: null,
67
+ wake: null,
68
+ timer: setTimeout(() => {
69
+ state.failure = new Error(`Answer timeout ${timeout}ms`);
70
+ state.wake?.();
71
+ state.wake = null;
72
+ }, timeout)
73
+ };
74
+ wire.send("boundary", "expect", question);
75
+ wire.streams.set(question, state);
76
+ wire.send("end-host", "stream", question, ...values);
77
+ try {
78
+ while (true) {
79
+ if (state.queue.length) {
80
+ yield state.queue.shift();
81
+ continue;
82
+ }
83
+ if (state.failure)
84
+ throw state.failure;
85
+ if (state.ended)
86
+ return;
87
+ await new Promise(resolve => { state.wake = resolve; });
88
+ }
89
+ }
90
+ finally {
91
+ clearTimeout(state.timer);
92
+ wire.streams.delete(question);
93
+ wire.send("boundary", "forget", question);
94
+ }
95
+ })();
96
+ }
52
97
  /** Requests a value while treating the caller's deadline as an absent answer. */
53
98
  requestOrNull(values, timeout) {
54
99
  return this.question("end-host", values, timeout, [], timeout, true);
@@ -261,6 +306,38 @@ class Wire {
261
306
  else
262
307
  pending.reject(new Error("The boundary returned an invalid outcome"));
263
308
  }
309
+ receiveStream(question, operation, value) {
310
+ const stream = this.streams.get(question);
311
+ if (!stream || stream.ended || stream.failure)
312
+ return;
313
+ if (operation === "open") {
314
+ if (stream.opened)
315
+ return;
316
+ stream.opened = true;
317
+ clearTimeout(stream.timer);
318
+ }
319
+ else if (!stream.opened)
320
+ stream.failure = new Error("The boundary produced a stream value before opening the stream");
321
+ else if (operation === "data") {
322
+ if (stream.queue.length >= maximumStreamQueue)
323
+ stream.failure = new Error(`Host stream queue exceeded its capacity of ${maximumStreamQueue}`);
324
+ else
325
+ stream.queue.push(value);
326
+ }
327
+ else if (operation === "answer") {
328
+ const outcome = value;
329
+ if (outcome?.success === true)
330
+ stream.ended = true;
331
+ else if (outcome?.success === false && typeof outcome.error === "string")
332
+ stream.failure = new Error(outcome.error);
333
+ else
334
+ stream.failure = new Error("The boundary returned an invalid stream outcome");
335
+ }
336
+ else
337
+ stream.failure = new Error(`The boundary returned an invalid stream operation "${operation}"`);
338
+ stream.wake?.();
339
+ stream.wake = null;
340
+ }
264
341
  }
265
342
  function invoke(handler, values) {
266
343
  Promise.resolve().then(() => handler(...values)).catch(error => queueMicrotask(() => { throw error; }));
@@ -305,6 +382,7 @@ function nativeAttachments(value, transfer) {
305
382
  visit(value);
306
383
  return attachments;
307
384
  }
385
+ const maximumStreamQueue = 256;
308
386
  const wire = new Wire();
309
387
  captureClientOutput(wire);
310
388
  export default wire;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/client",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "The SDK used by a Program's client endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -53,7 +53,7 @@
53
53
  "@msgpack/msgpack": "^3.1.3"
54
54
  },
55
55
  "devDependencies": {
56
- "@phreshos/core": "^0.1.12",
56
+ "@phreshos/core": "^0.1.13",
57
57
  "typescript": "^6.0.3"
58
58
  }
59
59
  }