@phreshos/client 0.1.15 → 0.1.17

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
@@ -141,13 +141,23 @@ identity is removed by the server host before the message reaches the desktop.
141
141
  The same rule applies to Client-visible traffic destinations.
142
142
 
143
143
  Client-visible Program handles can operate only within their own Program.
144
- They deliberately omit `install()` and `fork()`, and their storage areas never
144
+ They deliberately omit `install()` and `fork()`, and their `Storage` values never
145
145
  expose host filesystem paths. Every Client-side Window handle exposes the same
146
146
  authoritative, subscribable Window capability and its `window.local` physical
147
147
  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/main.d.ts CHANGED
@@ -5,4 +5,4 @@ export { current, type Current, type CurrentServer } from "./current.js";
5
5
  export { type Channel, type ChannelCapture, type ChannelEvents, type ChannelMessage } from "./channel.js";
6
6
  export { ClientServiceHandler, ServerServiceHandler, ServiceHandler, type ClientServiceChannel, type ServerServiceChannel, type ServiceChannel, type ServiceKey, type ServiceLifecycleEvents } from "@phreshos/core";
7
7
  export { Client, Endpoint, Process, Program, Server, type Window, type ProgramProcess, type AnswerCapture, type AnswerMessage, type AnswerObserver, type AskCapture, type AskMessage, type AskObserver, type ClientTraffic, type EndpointTraffic, type ServerTraffic, type TrafficCapture, type TrafficEvents, type TrafficMessage } from "./domain.js";
8
- export type { Askable, Capture, Captures, ClientDeclaration, Cleanup, DirectoryStat, EndpointDeclaration, EntryStat, EventMessage, EventName, EventObserver, EventOptions, EventSubscriber, Exit, FileStat, Launch, LaunchClient, Layer, LogKind, LogRecord, LogSource, Message, OtherStat, Outcome, PermissionDecision, Permission, Position, ProgramArea, ProgramEvents, ProgramProcessEvents, ProgramProcessExit, ProgramSql, ProgramStore, ProcessEvents, Publishable, ServedFile, Size, Subscribable, SubscribableEvents, SubscribableFallback, TimedAskable, TimedPermission, Timeoutable, Theme, ThemeEvents, ThemeProperties, Value, WritableTheme, WindowEvents, WindowGeometry, WindowLayer, WindowState, Easing, LocalWindow, LocalWindowSurface, SurfaceSettings, Transaction } from "@phreshos/core";
8
+ export type { Askable, Capture, Captures, ClientDeclaration, Cleanup, DirectoryStat, EndpointDeclaration, EntryStat, EventMessage, EventName, EventObserver, EventOptions, EventSubscriber, Exit, FileStat, Launch, LaunchClient, Layer, LogKind, LogRecord, LogSource, Message, OtherStat, Outcome, PermissionDecision, Permission, Position, Storage, ProgramEvents, ProgramProcessEvents, ProgramProcessExit, ProgramSql, ProgramStore, ProcessEvents, Publishable, ServedFile, Size, Subscribable, SubscribableEvents, SubscribableFallback, TimedAskable, TimedPermission, Timeoutable, Theme, ThemeEvents, ThemeProperties, Value, WritableTheme, WindowEvents, WindowGeometry, WindowLayer, WindowState, Easing, LocalWindow, LocalWindowSurface, SurfaceSettings, Transaction } from "@phreshos/core";
package/dist/storage.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { ProgramArea, ProgramSql, ProgramStore } from "@phreshos/core";
1
+ import type { ProgramSql, ProgramStore, Storage } from "@phreshos/core";
2
2
  /** Client-side Program storage, structurally scoped by the iframe boundary. */
3
- export declare function area(which: "data" | "cache"): ProgramArea;
3
+ export declare function area(which: "data" | "cache"): Storage;
4
4
  export declare function store(): ProgramStore;
5
5
  export declare function sql(kind: "database" | "logs"): ProgramSql;
package/dist/storage.js CHANGED
@@ -38,14 +38,12 @@ export function area(which) {
38
38
  async text(...path) { return new Response(await stream(...path)).text(); },
39
39
  async json(...path) { return JSON.parse(await new Response(await stream(...path)).text()); },
40
40
  async write(...args) {
41
- if (args.length < 2)
42
- throw new Error("Writing takes a file name and what to write");
43
41
  await transfer("write", args.slice(0, -1), content(args.at(-1)).body);
44
42
  },
45
43
  stat: (...path) => ask("stat", ...path),
46
44
  list: (...path) => ask("list", ...path),
47
45
  delete: (...path) => ask("delete", ...path),
48
- clear: () => ask("clear")
46
+ clear: (...path) => ask("clear", ...path)
49
47
  };
50
48
  }
51
49
  export function store() {
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.17",
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.14",
57
57
  "typescript": "^6.0.3"
58
58
  }
59
59
  }