@phreshos/client 0.1.32 → 0.1.34

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/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 = unknown> = 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 = unknown>(key: ServiceAddress<"server">): ServerService<ServiceEvents, Fallback>;
22
- /** Returns a typed stable handle for one exact Client service identity. */
23
- service<ServiceEvents extends object, Fallback = unknown>(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
@@ -1,7 +1,7 @@
1
1
  import Deadline from "./deadline.js";
2
2
  import { defaultTimeout } from "./events.js";
3
3
  import captureClientOutput from "./log.js";
4
- import { deserialize, serialize } from "./messagepack.js";
4
+ import { deserialize, serialize } from "@the-link/messagepack";
5
5
  /** The client endpoint's sole postMessage adapter. */
6
6
  class Wire {
7
7
  parent = window.parent === window ? null : window.parent;
@@ -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.32",
3
+ "version": "0.1.34",
4
4
  "description": "The SDK used by a Program's client endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -47,13 +47,13 @@
47
47
  "prepack": "node --run build"
48
48
  },
49
49
  "peerDependencies": {
50
- "@phreshos/core": "^0.1.36"
50
+ "@phreshos/core": "^0.1.38"
51
51
  },
52
52
  "dependencies": {
53
- "@msgpack/msgpack": "^3.1.3"
53
+ "@the-link/messagepack": "^0.1.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@phreshos/core": "^0.1.36",
56
+ "@phreshos/core": "^0.1.38",
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
- }
@@ -1,3 +0,0 @@
1
- export type Bytes = Uint8Array<ArrayBuffer>;
2
- export declare const serialize: (value: unknown, attachments?: readonly object[]) => Bytes;
3
- export declare const deserialize: (bytes: Uint8Array, attachments?: readonly unknown[]) => unknown;
@@ -1,130 +0,0 @@
1
- import { decode, encode, ExtensionCodec } from "@msgpack/msgpack";
2
- class Attachment {
3
- index;
4
- constructor(index) {
5
- this.index = index;
6
- }
7
- }
8
- class BigInteger {
9
- value;
10
- constructor(value) {
11
- this.value = value;
12
- }
13
- }
14
- class Undefined {
15
- }
16
- const textEncoder = new TextEncoder();
17
- const textDecoder = new TextDecoder("utf-8", { fatal: true });
18
- const extensions = new ExtensionCodec();
19
- const serializePrepared = (value, context) => encode(value, { context, extensionCodec: extensions });
20
- const deserializePrepared = (bytes, context) => decode(bytes, { context, extensionCodec: extensions });
21
- extensions.register({
22
- type: 0,
23
- encode: value => value instanceof Attachment ? uint32(value.index) : null,
24
- decode: (bytes, _type, context) => {
25
- const attachment = context.incoming?.[readUint32(bytes)];
26
- if (attachment === undefined)
27
- throw new Error("The MessagePack attachment is absent");
28
- return attachment;
29
- }
30
- });
31
- extensions.register({
32
- type: 1,
33
- encode: (value, context) => value instanceof Map ? serializePrepared([...value], context) : null,
34
- decode: (bytes, _type, context) => new Map(deserializePrepared(bytes, context))
35
- });
36
- extensions.register({
37
- type: 2,
38
- encode: (value, context) => value instanceof Set ? serializePrepared([...value], context) : null,
39
- decode: (bytes, _type, context) => new Set(deserializePrepared(bytes, context))
40
- });
41
- extensions.register({
42
- type: 3,
43
- encode: (value, context) => value instanceof RegExp ? serializePrepared([value.source, value.flags, value.lastIndex], context) : null,
44
- decode: (bytes, _type, context) => {
45
- const [source, flags, lastIndex] = deserializePrepared(bytes, context);
46
- const expression = new RegExp(source, flags);
47
- expression.lastIndex = lastIndex;
48
- return expression;
49
- }
50
- });
51
- extensions.register({
52
- type: 4,
53
- encode: value => value instanceof URL ? textEncoder.encode(value.href) : null,
54
- decode: bytes => new URL(textDecoder.decode(bytes))
55
- });
56
- extensions.register({
57
- type: 5,
58
- encode: (value, context) => value instanceof Error ? serializePrepared(prepare({
59
- cause: value.cause,
60
- message: value.message,
61
- name: value.name,
62
- stack: value.stack
63
- }, context), context) : null,
64
- decode: (bytes, _type, context) => {
65
- const value = deserializePrepared(bytes, context);
66
- const error = new Error(value.message, { cause: value.cause });
67
- error.name = value.name;
68
- if (value.stack !== undefined)
69
- error.stack = value.stack;
70
- return error;
71
- }
72
- });
73
- extensions.register({
74
- type: 6,
75
- encode: value => value instanceof BigInteger ? textEncoder.encode(value.value.toString()) : null,
76
- decode: bytes => BigInt(textDecoder.decode(bytes))
77
- });
78
- extensions.register({
79
- type: 7,
80
- encode: value => value instanceof Undefined ? new Uint8Array() : null,
81
- decode: () => undefined
82
- });
83
- export const serialize = (value, attachments = []) => {
84
- const outgoing = new Map(attachments.map((attachment, index) => [attachment, index]));
85
- const context = { outgoing };
86
- return serializePrepared(prepare(value, context), context);
87
- };
88
- export const deserialize = (bytes, attachments = []) => {
89
- return deserializePrepared(bytes, { incoming: attachments });
90
- };
91
- const prepare = (value, context) => {
92
- if (value === undefined)
93
- return new Undefined();
94
- if (typeof value === "bigint")
95
- return new BigInteger(value);
96
- if (typeof value === "function" || typeof value === "symbol")
97
- return null;
98
- if (value === null || typeof value !== "object")
99
- return value;
100
- const attachment = context.outgoing?.get(value);
101
- if (attachment !== undefined)
102
- return new Attachment(attachment);
103
- if (value instanceof Date || value instanceof RegExp || value instanceof URL || value instanceof Error)
104
- return value;
105
- if (value instanceof ArrayBuffer)
106
- return new Uint8Array(value);
107
- if (ArrayBuffer.isView(value))
108
- return Uint8Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
109
- if (value instanceof Map)
110
- return new Map([...value].map(([key, entry]) => [prepare(key, context), prepare(entry, context)]));
111
- if (value instanceof Set)
112
- return new Set([...value].map(entry => prepare(entry, context)));
113
- if (Array.isArray(value))
114
- return value.map(entry => prepare(entry, context));
115
- if ("toJSON" in value && typeof value.toJSON === "function")
116
- return prepare(value.toJSON(), context);
117
- return Object.fromEntries(Object.entries(value)
118
- .filter(([, entry]) => typeof entry !== "function")
119
- .map(([key, entry]) => [key, prepare(entry, context)]));
120
- };
121
- const uint32 = (value) => {
122
- const bytes = new Uint8Array(4);
123
- new DataView(bytes.buffer).setUint32(0, value);
124
- return bytes;
125
- };
126
- const readUint32 = (bytes) => {
127
- if (bytes.byteLength !== 4)
128
- throw new Error("The MessagePack attachment reference is invalid");
129
- return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
130
- };