@phreshos/client 0.1.31 → 0.1.33

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/storage.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { content } from "./content.js";
2
2
  import controlledStream from "./controlled-stream.js";
3
3
  import wire from "./wire.js";
4
- /** Client-side Program storage, structurally scoped by the iframe boundary. */
5
- export function area(which) {
4
+ /** Client-side access to one exact Program storage area. */
5
+ export function area(program, which) {
6
6
  async function ask(operation, ...values) {
7
- const answer = await wire.request([which, undefined, operation, ...values]);
7
+ const answer = await wire.request([which, program, operation, ...values]);
8
8
  return answer[0];
9
9
  }
10
10
  async function transfer(operation, path, body = null) {
@@ -12,7 +12,7 @@ export function area(which) {
12
12
  const abort = () => channel.port1.postMessage("abort");
13
13
  const close = () => channel.port1.close();
14
14
  try {
15
- const answer = await wire.request([which, undefined, operation, path, body, channel.port2], undefined, body instanceof ReadableStream ? [body, channel.port2] : [channel.port2]);
15
+ const answer = await wire.request([which, program, operation, path, body, channel.port2], undefined, body instanceof ReadableStream ? [body, channel.port2] : [channel.port2]);
16
16
  if (operation === "write") {
17
17
  close();
18
18
  return null;
@@ -34,6 +34,8 @@ export function area(which) {
34
34
  return body;
35
35
  }
36
36
  return {
37
+ path: () => ask("path"),
38
+ resolve: (...path) => ask("resolve", ...path),
37
39
  stream,
38
40
  async bytes(...path) { return new Uint8Array(await new Response(await stream(...path)).arrayBuffer()); },
39
41
  async text(...path) { return new Response(await stream(...path)).text(); },
@@ -47,9 +49,9 @@ export function area(which) {
47
49
  clear: (...path) => ask("clear", ...path)
48
50
  };
49
51
  }
50
- export function store() {
52
+ export function store(program) {
51
53
  async function ask(operation, ...values) {
52
- const answer = await wire.request(["store", undefined, operation, ...values]);
54
+ const answer = await wire.request(["store", program, operation, ...values]);
53
55
  return answer[0];
54
56
  }
55
57
  return {
@@ -60,13 +62,62 @@ export function store() {
60
62
  clear: () => ask("clear")
61
63
  };
62
64
  }
63
- export function sql(kind) {
65
+ export function sql(kind, program) {
64
66
  return {
65
67
  async query(statement, ...rest) {
66
68
  const text = typeof statement === "string" ? statement : statement.raw.join("?");
67
69
  const values = typeof statement === "string" ? (Array.isArray(rest[0]) ? rest[0] : []) : rest;
68
- const answer = await wire.request([kind, undefined, text, values]);
70
+ const answer = await wire.request([kind, program, text, values]);
69
71
  return answer[0];
70
72
  }
71
73
  };
72
74
  }
75
+ /** Access to the System's native storage through the Client boundary. */
76
+ export function systemStorage() {
77
+ async function ask(operation, ...values) {
78
+ const answer = await wire.request(["host-storage", operation, ...values]);
79
+ return answer[0];
80
+ }
81
+ async function transfer(operation, path, body = null) {
82
+ const channel = new MessageChannel();
83
+ const abort = () => channel.port1.postMessage("abort");
84
+ const close = () => channel.port1.close();
85
+ try {
86
+ const answer = await wire.request([`host-storage-${operation}`, path, body, channel.port2], undefined, body instanceof ReadableStream ? [body, channel.port2] : [channel.port2]);
87
+ if (operation === "write") {
88
+ close();
89
+ return null;
90
+ }
91
+ if (!(answer[0] instanceof ReadableStream))
92
+ throw new Error("The storage response has no byte stream");
93
+ return controlledStream(answer[0], abort, close);
94
+ }
95
+ catch (error) {
96
+ abort();
97
+ close();
98
+ throw error;
99
+ }
100
+ }
101
+ async function stream(...path) {
102
+ const body = await transfer("stream", path);
103
+ if (!body)
104
+ throw new Error("The storage response has no body");
105
+ return body;
106
+ }
107
+ return {
108
+ path: () => ask("path"),
109
+ resolve: (...path) => ask("resolve", ...path),
110
+ stream,
111
+ async bytes(...path) { return new Uint8Array(await new Response(await stream(...path)).arrayBuffer()); },
112
+ async text(...path) { return new Response(await stream(...path)).text(); },
113
+ async json(...path) { return JSON.parse(await new Response(await stream(...path)).text()); },
114
+ async write(...args) {
115
+ const path = args.slice(0, -1);
116
+ await transfer("write", path, content(args.at(-1)).body);
117
+ },
118
+ stat: (...path) => ask("stat", ...path),
119
+ list: (...path) => ask("list", ...path),
120
+ delete: (...path) => ask("delete", ...path),
121
+ clear: (...path) => ask("clear", ...path)
122
+ };
123
+ }
package/dist/system.d.ts CHANGED
@@ -1,27 +1,3 @@
1
- import type { AppearanceSource, ClientService, ServerService, ServiceKey, SystemUploads } from "@phreshos/core";
2
- import { type SystemDesktop } from "./desktop/desktop.js";
3
- type ServiceEndpoint = ServiceKey["endpoint"];
4
- type ServiceAddress<Endpoint extends ServiceEndpoint> = Omit<ServiceKey, "endpoint"> & Readonly<{
5
- endpoint: Endpoint;
6
- }>;
7
- type ServiceHandle<Endpoint extends ServiceEndpoint, Events extends object, Fallback = never> = Endpoint extends "server" ? ServerService<Events, Fallback> : ClientService<Events, Fallback>;
8
- /** Desktop capabilities structurally available to a Client endpoint. */
9
- export interface System {
10
- /** Complete unresolved Appearance read from the System authority. */
11
- readonly appearance: AppearanceSource;
12
- /** Capabilities owned by the Desktop containing this Client. */
13
- readonly desktop: SystemDesktop;
14
- /** Flat System-owned public uploads capability. */
15
- readonly uploads: SystemUploads;
16
- /** Performs an unrestricted server-side fetch on behalf of this Client. */
17
- fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
18
- /** Returns a precisely typed stable handle for either service endpoint. */
19
- service<Endpoint extends ServiceEndpoint>(key: ServiceAddress<Endpoint>): ServiceHandle<Endpoint, {}>;
20
- /** Returns a typed stable handle for one exact Server service identity. */
21
- service<ServiceEvents extends object, Fallback = never>(key: ServiceAddress<"server">): ServerService<ServiceEvents, Fallback>;
22
- /** Returns a typed stable handle for one exact Client service identity. */
23
- service<ServiceEvents extends object, Fallback = never>(key: ServiceAddress<"client">): ClientService<ServiceEvents, Fallback>;
24
- }
25
- /** Desktop capabilities available to this Client. */
26
- export declare const system: System;
27
- export {};
1
+ import { type System as CoreSystem } from "@phreshos/core";
2
+ /** The global System represented through this Client runtime. */
3
+ export declare const system: CoreSystem;
package/dist/system.js CHANGED
@@ -1,13 +1,22 @@
1
+ import { parseShellEvent } from "@phreshos/core";
1
2
  import ClientAppearance from "./appearance.js";
2
3
  import wire from "./wire.js";
3
- import ClientDesktop, {} from "./desktop/desktop.js";
4
4
  import { prepareService } from "./service.js";
5
5
  import { uploads } from "./uploads.js";
6
6
  import controlledStream from "./controlled-stream.js";
7
+ import Events from "./events.js";
8
+ import { exit, process, program } from "./domain.js";
9
+ import { systemStorage } from "./storage.js";
7
10
  class ClientSystem {
11
+ storage = systemStorage();
8
12
  appearance = new ClientAppearance();
9
- desktop = new ClientDesktop();
13
+ program = new SystemProgramHandle();
14
+ process = new SystemProcessHandle();
10
15
  uploads = uploads;
16
+ async forceCreateProgram(source) {
17
+ const answer = await wire.request(["host-program-force-create", source]);
18
+ return program(answer[0]);
19
+ }
11
20
  service(key) { return prepareService(key); }
12
21
  async fetch(input, init) {
13
22
  const request = new Request(input, {
@@ -64,6 +73,61 @@ class ClientSystem {
64
73
  });
65
74
  return response;
66
75
  }
76
+ async *shell(command, options = {}) {
77
+ const { signal, ...settings } = options;
78
+ for await (const event of wire.stream(["shell", command, settings], undefined, signal))
79
+ yield parseShellEvent(event);
80
+ }
81
+ }
82
+ class SystemProgramHandle extends Events {
83
+ constructor() {
84
+ super((event, listener, impossible) => wire.on("host-program", event, (...values) => listener(systemProgramEvent(event, values)), null, impossible), observer => wire.onAll("host-program", (event, ...values) => {
85
+ if (typeof event === "string")
86
+ observer(event, systemProgramEvent(event, values));
87
+ }));
88
+ }
89
+ async list(onlyInstalled = false) {
90
+ const answer = await wire.request(["host-program-list", onlyInstalled]);
91
+ return answer[0].map(program);
92
+ }
93
+ async find(identity) {
94
+ const answer = await wire.request(["host-program-find", identity]);
95
+ return answer[0] ? program(answer[0]) : null;
96
+ }
97
+ async create(source) {
98
+ const answer = await wire.request(["host-program-create", source]);
99
+ return program(answer[0]);
100
+ }
101
+ }
102
+ class SystemProcessHandle extends Events {
103
+ constructor() {
104
+ super((event, listener, impossible) => wire.on("host-process", event, (...values) => listener(systemProcessEvent(event, values)), null, impossible), observer => wire.onAll("host-process", (event, ...values) => {
105
+ if (typeof event === "string")
106
+ observer(event, systemProcessEvent(event, values));
107
+ }));
108
+ }
109
+ async list() {
110
+ const answer = await wire.request(["host-process-list"]);
111
+ return answer[0].map(record => process(record));
112
+ }
113
+ async find(identity) {
114
+ const answer = await wire.request(["host-process-find", identity]);
115
+ return answer[0] ? process(answer[0]) : null;
116
+ }
117
+ }
118
+ function systemProcessEvent(event, values) {
119
+ if (event === "create")
120
+ return process(values[1]);
121
+ if (event === "exit")
122
+ return { process: process(values[1]), ...exit(values[2], values[3]) };
123
+ return values[0];
124
+ }
125
+ function systemProgramEvent(event, values) {
126
+ if (event === "create" || event === "forget" || event === "install")
127
+ return program(values[1]);
128
+ if (event === "uninstall")
129
+ return { program: program(values[1]), everything: values[2] === true };
130
+ return values[0];
67
131
  }
68
132
  function requestHeaders(normalized, supplied) {
69
133
  if (!supplied)
@@ -80,5 +144,5 @@ function closeControl(signal, port, abort) {
80
144
  signal.removeEventListener("abort", abort);
81
145
  port.close();
82
146
  }
83
- /** Desktop capabilities available to this Client. */
147
+ /** The global System represented through this Client runtime. */
84
148
  export const system = new ClientSystem();
package/dist/uploads.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type SystemUploads, type Upload } from "@phreshos/core";
2
2
  /** Flat upload access transported through the Client's Desktop boundary. */
3
3
  declare class ClientUploads implements SystemUploads {
4
+ path(): Promise<string>;
4
5
  write(value: unknown): Promise<Upload>;
5
6
  stream(file: string): Promise<ReadableStream<Uint8Array>>;
6
7
  bytes(file: string): Promise<Uint8Array<ArrayBuffer>>;
package/dist/uploads.js CHANGED
@@ -4,6 +4,12 @@ import controlledStream from "./controlled-stream.js";
4
4
  import wire from "./wire.js";
5
5
  /** Flat upload access transported through the Client's Desktop boundary. */
6
6
  class ClientUploads {
7
+ async path() {
8
+ const answer = await wire.request(["uploads", "path"]);
9
+ if (typeof answer[0] !== "string" || !answer[0])
10
+ throw new Error("The System returned an invalid uploads path");
11
+ return answer[0];
12
+ }
7
13
  async write(value) {
8
14
  const source = content(value);
9
15
  const channel = new MessageChannel();
package/dist/wire.d.ts CHANGED
@@ -17,7 +17,7 @@ declare class Wire {
17
17
  send(route: string, ...values: unknown[]): void;
18
18
  request(values: unknown[], timeout?: number, transfer?: Transferable[]): Promise<unknown>;
19
19
  /** Opens one long-running desktop operation and yields its ordered values. */
20
- stream(values: unknown[], timeout?: number): AsyncIterableIterator<unknown>;
20
+ stream(values: unknown[], timeout?: number, signal?: AbortSignal): AsyncIterableIterator<unknown>;
21
21
  /** Requests a value while treating the caller's deadline as an absent answer. */
22
22
  requestOrNull(values: unknown[], timeout: number): Promise<unknown | null>;
23
23
  requestWithin(values: unknown[], deadline: Deadline, transfer?: Transferable[]): Promise<unknown>;
@@ -37,8 +37,7 @@ declare class Wire {
37
37
  /** Follow destinationless events emitted by one Endpoint. */
38
38
  follow(target: HandleAddress | null, half: "server" | "client", event: string | null, handler: Handler, impossible?: Failure): Cleanup;
39
39
  /** Follow one exact service lifecycle or application event route. */
40
- followService(key: ServiceKey, scope: "lifecycle" | "events", event: string | null, handler: Handler): Cleanup;
41
- samplePointer(movementX: number, movementY: number): void;
40
+ followService(key: ServiceKey, scope: "lifecycle" | "events", event: string | null, handler: Handler, impossible?: Failure): Cleanup;
42
41
  private register;
43
42
  private unregister;
44
43
  private deliver;
package/dist/wire.js CHANGED
@@ -54,7 +54,7 @@ class Wire {
54
54
  return this.question("end-host", values, timeout, transfer);
55
55
  }
56
56
  /** Opens one long-running desktop operation and yields its ordered values. */
57
- stream(values, timeout = defaultTimeout) {
57
+ stream(values, timeout = defaultTimeout, signal) {
58
58
  const wire = this;
59
59
  return (async function* () {
60
60
  const question = `client:${crypto.randomUUID()}:${crypto.randomUUID()}`;
@@ -70,6 +70,15 @@ class Wire {
70
70
  state.wake = null;
71
71
  }, timeout)
72
72
  };
73
+ const abort = () => {
74
+ state.failure = signal?.reason instanceof Error ? signal.reason : new Error("The operation was cancelled");
75
+ state.wake?.();
76
+ state.wake = null;
77
+ };
78
+ if (signal?.aborted)
79
+ abort();
80
+ else
81
+ signal?.addEventListener("abort", abort, { once: true });
73
82
  wire.send("boundary", "expect", question);
74
83
  wire.streams.set(question, state);
75
84
  wire.send("end-host", "stream", question, ...values);
@@ -87,6 +96,7 @@ class Wire {
87
96
  }
88
97
  }
89
98
  finally {
99
+ signal?.removeEventListener("abort", abort);
90
100
  clearTimeout(state.timer);
91
101
  wire.streams.delete(question);
92
102
  wire.send("boundary", "forget", question);
@@ -259,18 +269,18 @@ class Wire {
259
269
  });
260
270
  }
261
271
  /** Follow one exact service lifecycle or application event route. */
262
- followService(key, scope, event, handler) {
272
+ followService(key, scope, event, handler, impossible) {
263
273
  const subscription = crypto.randomUUID();
264
274
  const stop = this.on("service-event", subscription, handler);
265
- this.send("end-host", "service-follow", subscription, key, scope, event);
275
+ if (impossible)
276
+ this.impossible.set(subscription, impossible);
277
+ this.send("end-host", "service-follow", subscription, key, scope, event, impossible !== undefined);
266
278
  return once(() => {
267
279
  stop();
280
+ this.impossible.delete(subscription);
268
281
  this.send("end-host", "service-unfollow", subscription);
269
282
  });
270
283
  }
271
- samplePointer(movementX, movementY) {
272
- this.send("end-host", "pointerSample", movementX, movementY);
273
- }
274
284
  register(kind, route, event, subject, impossible) {
275
285
  const subscription = crypto.randomUUID();
276
286
  if (impossible)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/client",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "The SDK used by a Program's client endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -39,7 +39,7 @@
39
39
  "packageManager": "bun@1.3.14",
40
40
  "scripts": {
41
41
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
42
- "check": "tsc --noEmit",
42
+ "check": "tsc --noEmit && tsc -p tsconfig.test.json",
43
43
  "build": "node --run clean && tsc --noEmit false --outDir dist --rootDir source",
44
44
  "verify:completions": "node scripts/verify-completions.mjs",
45
45
  "verify:package": "node scripts/verify-package.mjs",
@@ -47,13 +47,13 @@
47
47
  "prepack": "node --run build"
48
48
  },
49
49
  "peerDependencies": {
50
- "@phreshos/core": "^0.1.35"
50
+ "@phreshos/core": "^0.1.37"
51
51
  },
52
52
  "dependencies": {
53
53
  "@msgpack/msgpack": "^3.1.3"
54
54
  },
55
55
  "devDependencies": {
56
- "@phreshos/core": "^0.1.35",
56
+ "@phreshos/core": "^0.1.37",
57
57
  "typescript": "^6.0.3"
58
58
  }
59
59
  }
@@ -1,11 +0,0 @@
1
- import Events from "../events.js";
2
- /** Permission-guarded access to the Desktop pointer visible to this Client. */
3
- export default class ClientPointer extends Events {
4
- private listeners;
5
- private readonly sample;
6
- constructor();
7
- snapshot(): Promise<Readonly<{
8
- position: import("@phreshos/core").DesktopPointerPosition | null;
9
- }>>;
10
- private withSampling;
11
- }
@@ -1,59 +0,0 @@
1
- import Events from "../events.js";
2
- import wire from "../wire.js";
3
- /** Permission-guarded access to the Desktop pointer visible to this Client. */
4
- export default class ClientPointer extends Events {
5
- listeners = 0;
6
- sample = (event) => wire.samplePointer(event.movementX, event.movementY);
7
- constructor() {
8
- super((event, listener, impossible) => this.withSampling(event === "move", wire.on("host-desktop-pointer", event, value => {
9
- const snapshot = createSnapshot(value);
10
- if (snapshot)
11
- listener(snapshot);
12
- }, null, impossible)), observer => this.withSampling(true, wire.onAll("host-desktop-pointer", (event, value) => {
13
- const snapshot = createSnapshot(value);
14
- if (event === "move" && snapshot)
15
- observer(event, snapshot);
16
- })));
17
- }
18
- async snapshot() {
19
- const answer = await wire.request(["desktopPointer"]);
20
- const snapshot = createSnapshot(answer[0]);
21
- if (!snapshot)
22
- throw new Error("The Desktop returned an invalid pointer");
23
- return snapshot;
24
- }
25
- withSampling(sample, stop) {
26
- if (!sample)
27
- return stop;
28
- if (this.listeners++ === 0)
29
- window.addEventListener("pointermove", this.sample);
30
- let active = true;
31
- return () => {
32
- if (!active)
33
- return;
34
- active = false;
35
- stop();
36
- this.listeners--;
37
- if (this.listeners === 0)
38
- window.removeEventListener("pointermove", this.sample);
39
- };
40
- }
41
- }
42
- function createSnapshot(value) {
43
- if (!record(value))
44
- return null;
45
- if (value.position === null)
46
- return Object.freeze({ position: null });
47
- if (!record(value.position))
48
- return null;
49
- const { x, y } = value.position;
50
- if (!finite(x) || !finite(y))
51
- return null;
52
- return Object.freeze({ position: Object.freeze({ x, y }) });
53
- }
54
- function record(value) {
55
- return typeof value === "object" && value !== null && !Array.isArray(value);
56
- }
57
- function finite(value) {
58
- return typeof value === "number" && Number.isFinite(value);
59
- }