@phreshos/client 0.1.27 → 0.1.29

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
@@ -1,217 +1,84 @@
1
1
  # `@phreshos/client`
2
2
 
3
- The Client SDK defines the contextual capabilities available inside a
4
- Program's client endpoint.
3
+ The SDK for a PhreshOS Program's Client Endpoint.
5
4
 
6
- ## Package status
5
+ The Client SDK adapts the sandboxed desktop boundary to the shared Core domain
6
+ model. It exposes the restricted `system` and `context` available inside a
7
+ Client without redefining Program, Process, Endpoint, Server, or Client.
7
8
 
8
- This package is one component of a larger architecture that is still under
9
- active testing. The architecture's components will be released in stages as
10
- their contracts and integrations are verified.
9
+ ## Installation
11
10
 
12
- `@phreshos/client` is not intended to be used on its own. It requires the
13
- shared contracts from `@phreshos/core` and a compatible desktop boundary to
14
- provide its runtime environment.
11
+ | Package manager | Command |
12
+ | --- | --- |
13
+ | npm | `npm install @phreshos/client` |
14
+ | pnpm | `pnpm add @phreshos/client` |
15
+ | Bun | `bun add @phreshos/client` |
16
+ | Yarn | `yarn add @phreshos/client` |
15
17
 
16
- It uses the domain objects and shared contracts from `@phreshos/core` through
17
- a peer dependency. It does not redefine those objects, own server-side
18
- capabilities, or contain System and transport implementations.
18
+ `@phreshos/core` is a peer dependency.
19
19
 
20
- Its `System` contract exposes only desktop-session capabilities: read-only
21
- System Appearance, mutable preferences local to this Desktop, desktop and
22
- pointer state, flat public uploads, and unrestricted server-side Fetch.
23
- `system.appearance.snapshot()` reads complete unresolved authoritative state.
24
- `system.desktopPreferences.snapshot()` reads the complete effective
25
- `{ theme, animations }` state. Its updates may set either preference explicitly
26
- or use `"default"` to resume following the native environment. Both capabilities
27
- expose ordinary live-only `change` subscriptions with no initial replay. A
28
- Client cannot write authoritative Appearance or discover system-wide Programs
29
- and Processes.
30
-
31
- `system.uploads` has the same flat contract as the Server SDK. It writes a
32
- value, then reads or describes it using exactly one opaque generated file key;
33
- it exposes no filesystem path, path segments, listing, deletion, or clearing.
34
-
35
- Desktop and pointer access are independent objects rather than events merged
36
- into System:
20
+ ## Context
37
21
 
38
22
  ```ts
39
- const desktop = await system.desktop.size()
40
- const stopDesktop = system.desktop.subscribe("resize", next => undefined)
23
+ import { context } from "@phreshos/client"
41
24
 
42
- const position = await system.pointer.position()
43
- const stopPointer = system.pointer.subscribe("move", next => undefined)
44
- ```
45
-
46
- The desktop size is the complete desktop area containing this Client,
47
- independent of the Client Window's layer. Client code cannot select a layer,
48
- and the desktop's gutter never enters an endpoint. Pointer position and
49
- movement both require the `pointer` permission. Reads are asynchronous
50
- requests; subscriptions receive only future publications and never replay a
51
- retained value.
25
+ context.subscribe("changed", message => {
26
+ console.log(message)
27
+ })
52
28
 
53
- Permission decisions belong to the current Program, not to the desktop System.
54
- They are available from a Program handle and flattened through `context` as the
55
- same canonical capability:
29
+ context.publish("changed", { value: 1 })
56
30
 
57
- ```ts
58
31
  const program = await context.program()
59
-
60
- await program.permission.granted("pointer")
61
- await context.permission.request("pointer")
62
- await context.permission.timeout(5_000).request("pointer")
63
-
64
- context.permission === program.permission
32
+ const process = await context.process()
33
+ const server = context.server
34
+ const window = context.window
65
35
  ```
66
36
 
67
- `granted()` reads the effective decision without prompting. `request()` asks
68
- only when no known decision can answer immediately and returns `null` if its
69
- deadline expires. The desktop remains the internal enforcement boundary, but
70
- permission ownership does not alter the public shape of `system`.
37
+ `context` belongs to the executing Client. It provides communication,
38
+ navigation to its Program and Process, its paired Server, its Window, and
39
+ Client-owned capabilities.
71
40
 
72
- The Client System exposes only direct, exact Service handles. Creating a handle
73
- does not read or start the Service and exposes no Service registry:
41
+ ## System
74
42
 
75
43
  ```ts
76
- const service = system.service({
77
- program: "counter",
78
- endpoint: "server",
79
- name: "state"
80
- })
81
-
82
- await service.waitReady()
83
- if (await service.enabled()) await service.ask("value")
44
+ import { system } from "@phreshos/client"
84
45
 
85
- service.subscribe("changed", message => console.log(message))
86
- service.lifecycle.subscribe("disable", () => console.log("unavailable"))
46
+ const appearance = await system.appearance.snapshot()
47
+ const surface = await system.desktop.surface.snapshot()
48
+ const pointer = await system.desktop.pointer.snapshot()
49
+ const preferences = await system.desktop.preferences.snapshot()
87
50
  ```
88
51
 
89
- A Client may traverse `Process.parent()` through any number of ancestors in
90
- its own Program. The first parent outside that Program is structurally hidden
91
- and returned as `null`; no cross-Program Process handle enters the client. If
92
- client code fabricates or otherwise obtains an unauthorized handle, the system
93
- responds exactly as it does for a nonexistent Process.
94
-
95
- Its JavaScript entry point adapts the iframe boundary to these contracts. The
96
- SDK owns callbacks, waits, queues, and their cleanup; the boundary owns only
97
- the forwarding registrations requested by the SDK.
98
-
99
- Importing the SDK and establishing the iframe's host lease inject no message
100
- into the endpoint. Identity, Theme, Process, Window, readiness, lifecycle, and
101
- application values enter only in response to an explicit request or a live
102
- registration made by Program code.
103
-
104
- `Context` combines communication with navigation into the executing Client's
105
- Process. The paired Server is explicitly named as
106
- `context.server`; its publishing, asking, existence, readiness, start, and stop
107
- operations never masquerade as properties of `context`. `context.stop()` stops
108
- the executing Client, while complete Process exit remains available only
109
- through `context.process()`. It is the canonical Process-owned handle, so
110
- `context.server === (await context.process()).server`.
111
- Endpoint `process()` navigation is asynchronous; contextual ownership is
112
- requested only when navigation needs it and then retained by the SDK.
113
-
114
- All domain handles are canonical within this iframe's JavaScript realm. Lookup,
115
- navigation, event payloads, and message metadata reuse the same weakly retained
116
- handle. A Client and its synchronous `window` capability remain stable for the
117
- Process lifetime; Window operations always address that Client's current live
118
- presentation state.
119
-
120
- `server.ask()` does not route a question before the current Server incarnation
121
- is ready. The Client SDK owns one deadline across readiness and the answer;
122
- absence or incarnation loss rejects without turning the boundary into a waiter.
123
-
124
- The package provides two contextual runtime entry points:
52
+ The Client System exposes only capabilities allowed by the desktop boundary:
53
+ read-only Appearance, desktop surface and pointer state, writable desktop
54
+ preferences, uploads, Fetch, and exact Service handles. It does not expose
55
+ system-wide Program or Process registries.
125
56
 
126
- ```ts
127
- import { system, context } from "@phreshos/client"
128
- ```
57
+ Requests read current state. Subscriptions observe future publications and do
58
+ not replay an initial value. Importing the SDK performs neither operation.
129
59
 
130
- It also re-exports the shared Core runtime classes—`Program`, `Process`,
131
- `Endpoint`, `Server`, and `Client`—and refines the handles returned through
132
- them. These are the same domain classes used by the Server SDK, so
133
- `instanceof Server` and `instanceof Client` retain one meaning. `Window`, like
134
- `ClientTraffic`, is a type-only capability owned by Client and has no
135
- independent `instanceof` identity.
136
-
137
- The executing Client's communication belongs directly to `context`. Its
138
- subscription tools receive events addressed to this Client, while `publish()`
139
- emits outward from this Client without choosing a destination. Existence and
140
- readiness belong to `context.server`; the current Client stops through
141
- `context.stop()`, and explicit Endpoint handles expose their own
142
- `endpoint.lifecycle` subscriptions.
143
-
144
- An Endpoint handle is also a selective source: `endpoint.subscribe()` follows
145
- destinationless events emitted by that Endpoint. Its `traffic` property remains
146
- reserved for directed publications, questions, and answers.
147
-
148
- Messages sent by an Endpoint in the same Program contain that real Endpoint in
149
- `from`. If the sender belongs to another Program, `from` is `null`; the foreign
150
- identity is removed by the authoritative System before the message reaches the desktop.
151
- The same rule applies to Client-visible traffic destinations.
152
-
153
- Client-visible Program handles can operate only within their own Program.
154
- They deliberately omit `install()` and `fork()`, and their `Storage` values never
155
- expose native filesystem paths. Every Client-side Window handle exposes the same
156
- authoritative, subscribable Window capability and its `window.local` physical
157
- representation on this desktop. Local reads and updates have no events and do
158
- not change Server state. `context.window` is only convenient access to the
159
- executing Client's canonical Window; it has no additional authority.
160
-
161
- A Client may uninstall only its own Program. The operation is an async
162
- generator so a declared Server cleanup command remains observable without a
163
- fixed answer timeout:
60
+ ## Development
164
61
 
165
- ```ts
166
- for await (const chunk of program.uninstall()) {
167
- console.log(chunk.stream, chunk.text)
168
- }
62
+ ```sh
63
+ bun install --frozen-lockfile
64
+ bun run verify
169
65
  ```
170
66
 
171
- A Program may converge its Clients on one named Process without a manual
172
- `find()`/`create()` race:
67
+ `verify` checks the source, completion surface, build, and published package
68
+ shape.
173
69
 
174
- ```ts
175
- const shared = await program.process.findOrCreate({
176
- name: "shared-server",
177
- server: true,
178
- client: false
179
- })
180
- ```
181
-
182
- The authoritative Core returns the existing Process only when its normalized
183
- launch is equivalent; a conflicting launch rejects without changing it.
70
+ See the [Client and Server documentation](https://github.com/PhreshOS/docs/blob/main/content/docs/sdks/client-and-server.mdx)
71
+ for the shared model and authority boundary.
184
72
 
185
- When both dimensions must change, `setGeometry({ position, size })` commits
186
- them through one authoritative request and produces one `geometry` event.
187
- Calling `move()` and `resize()` sequentially or through `Promise.all()` remains
188
- two independent operations and can expose an intermediate state remotely.
73
+ ## Repository boundary
189
74
 
190
- An `under` or `over` representation may request one local system-rendered Surface:
75
+ This repository owns the Client runtime adapter. Core owns the domain model, the
76
+ System owns enforcement and forwarding, React owns framework adaptation, and
77
+ React UI owns visual interpretation.
191
78
 
192
- ```ts
193
- await context.window.local.surface.set({ duration: 240, easing: "ease-out", wait: true })
79
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the repository workflow and
80
+ [SECURITY.md](SECURITY.md) for private vulnerability reporting.
194
81
 
195
- await context.window.local.surface.remove({ duration: 180, easing: "ease-in" })
196
- ```
82
+ ## License
197
83
 
198
- The desktop holds local state only for the lifetime of that iframe
199
- representation. Reloading or destroying it resets the representation from
200
- authoritative truth, while other desktops remain unaffected. Program code may
201
- synchronize desired presence through its Server and explicitly apply it
202
- again. `set()` and `remove()` accept only a required `VisibilityTransition`;
203
- Programs cannot configure the System Surface's material, opacity, or radius.
204
- The transition uses milliseconds and a stable named or cubic Bézier easing;
205
- the desktop performs the motion, honors reduced motion, and removes the Surface
206
- only after its exit transition completes. `wait: true` makes the request settle
207
- with that transition. A new iframe representation restores nothing. The
208
- container follows the iframe geometry while the independently rounded Surface
209
- neither clips nor masks Client content. The ordinary `window` layer rejects
210
- the capability.
211
-
212
- `program.icon()` requests the current Program's guaranteed PNG `Blob` on
213
- demand. The desktop derives the Program from the calling frame; no identity,
214
- private asset address, or filesystem path crosses from Client code.
215
-
216
- Persistent startup is deliberately absent. Only a Server endpoint may change
217
- whether an installed Program creates a Process when the system starts.
84
+ Licensed under the [MIT License](LICENSE). Copyright © 2026 Zohayr SLILEH.
package/dist/context.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Context as CoreContext, ContextCapture as CoreContextCapture, ContextEvents as CoreContextEvents, ContextMessage as CoreContextMessage } from "@phreshos/core";
2
- import { Client, Server, type Endpoint, type Process, type Program, type Window } from "./domain.js";
2
+ import { Server, type Endpoint, type Process, type Program, type Window } from "./domain.js";
3
3
  /** The executing Process's canonical Server handle. */
4
4
  export type ContextServer<Events extends object = {}> = Server<Events>;
5
5
  /** One value addressed to the current Client, with a client-visible sender. */
@@ -9,7 +9,7 @@ export type ContextEvents<Events extends object> = CoreContextEvents<Events, End
9
9
  /** Every event observable through the current Client Context. */
10
10
  export type ContextCapture<Events extends object = {}> = CoreContextCapture<Events, Endpoint | null>;
11
11
  /** Client runtime context: inbound communication, owner hierarchy, and paired Server. */
12
- export interface Context<Events extends object = {}> extends CoreContext<Events, Endpoint | null>, Pick<Client, "service"> {
12
+ export interface Context<Events extends object = {}> extends CoreContext<Events, Endpoint | null> {
13
13
  /** The same Server handle exposed by the executing Process. */
14
14
  readonly server: ContextServer;
15
15
  /** Presentation capability of the executing Client. */
@@ -18,6 +18,8 @@ export interface Context<Events extends object = {}> extends CoreContext<Events,
18
18
  readonly permission: Program["permission"];
19
19
  /** Returns the Process represented by this Client. */
20
20
  process(): Promise<Process>;
21
+ /** Returns the executing Process's Program-local name, or `null` when unnamed. */
22
+ name(): Promise<string | null>;
21
23
  /** Returns the accessible parent Process, or `null` when none exists. */
22
24
  parent(): Promise<Process | null>;
23
25
  /** Returns the Program that owns this Client. */
package/dist/context.js CHANGED
@@ -2,7 +2,6 @@ import Deadline from "./deadline.js";
2
2
  import { Client, Server, ServerTrafficHandle, TrafficHandle, bindEvents, endpointLifecycle, endpointEvents, process, program, visibleEndpoint, window as windowHandle } from "./domain.js";
3
3
  import Events from "./events.js";
4
4
  import wire from "./wire.js";
5
- import { disableCurrentService, enableCurrentService, endpointService } from "./service.js";
6
5
  import { currentProgramPermission } from "./permissions.js";
7
6
  const ServerBase = Server;
8
7
  const ClientBase = Client;
@@ -21,9 +20,9 @@ class ContextServerHandle extends ServerBase {
21
20
  const answer = await wire.request(["exists", "server"]);
22
21
  return answer[0];
23
22
  }
24
- async start() { await wire.request(["start-endpoint", undefined, "server"]); }
23
+ async start(launch = {}) { await wire.request(["start-endpoint", undefined, "server", launch]); }
25
24
  async stop() { await wire.request(["stop-endpoint", undefined, "server"]); }
26
- service() { return endpointService(null, "server"); }
25
+ async isService() { return (await wire.request(["is-service", "server"]))[0]; }
27
26
  async waitReady(timeout) { await wire.request(["wait-ready"], timeout); }
28
27
  async ask(event, payload = undefined) {
29
28
  return this.askWithin(undefined, event, payload);
@@ -75,9 +74,9 @@ class ContextClientHandle extends ClientBase {
75
74
  const answer = await wire.request(["exists", "client"]);
76
75
  return answer[0];
77
76
  }
78
- async start(overrides = {}) { await wire.request(["start-endpoint", undefined, "client", overrides]); }
77
+ async start(launch = {}) { await wire.request(["start-endpoint", undefined, "client", launch]); }
79
78
  async stop() { await wire.request(["stop-endpoint", undefined, "client"]); }
80
- service() { return endpointService(null, "client"); }
79
+ async isService() { return (await wire.request(["is-service", "client"]))[0]; }
81
80
  }
82
81
  contextClient = new ContextClientHandle(owner);
83
82
  class ClientContext extends Events {
@@ -91,6 +90,7 @@ class ClientContext extends Events {
91
90
  }, null, impossible));
92
91
  }
93
92
  process() { return owner(); }
93
+ async name() { return (await owner()).name; }
94
94
  async parent() {
95
95
  const answer = await wire.request(["parent"]);
96
96
  return answer[0] ? process(answer[0]) : null;
@@ -104,10 +104,8 @@ class ClientContext extends Events {
104
104
  return answer[0];
105
105
  }
106
106
  async stop() { await wire.request(["stop-current"]); }
107
- service() { return endpointService(null, "client"); }
107
+ async isService() { return contextClient.isService(); }
108
108
  publish(event, payload = undefined) { wire.send("end-host", "emit", event, payload); }
109
- async enableService(name) { await enableCurrentService(name); }
110
- async disableService() { await disableCurrentService(); }
111
109
  }
112
110
  async function currentAddress() {
113
111
  const identity = await wire.identity();
@@ -0,0 +1,13 @@
1
+ import type { DesktopPointerSource, DesktopSurfaceSource, WritableDesktopPreferencesSource } from "@phreshos/core";
2
+ /** Every capability owned by the Desktop containing this Client. */
3
+ export interface SystemDesktop {
4
+ readonly surface: DesktopSurfaceSource;
5
+ readonly pointer: DesktopPointerSource;
6
+ readonly preferences: WritableDesktopPreferencesSource;
7
+ }
8
+ /** Desktop access bound to the current Client Process boundary. */
9
+ export default class ClientDesktop implements SystemDesktop {
10
+ readonly surface: DesktopSurfaceSource;
11
+ readonly pointer: DesktopPointerSource;
12
+ readonly preferences: WritableDesktopPreferencesSource;
13
+ }
@@ -0,0 +1,9 @@
1
+ import ClientPointer from "./pointer.js";
2
+ import ClientPreferences from "./preferences.js";
3
+ import ClientSurface from "./surface.js";
4
+ /** Desktop access bound to the current Client Process boundary. */
5
+ export default class ClientDesktop {
6
+ surface = new ClientSurface();
7
+ pointer = new ClientPointer();
8
+ preferences = new ClientPreferences();
9
+ }
@@ -0,0 +1,11 @@
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
+ }
@@ -0,0 +1,59 @@
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
+ }
@@ -0,0 +1,11 @@
1
+ import type { DesktopPreferencesUpdate } from "@phreshos/core";
2
+ import Events from "../events.js";
3
+ /** Effective preferences owned by the Desktop containing this Client. */
4
+ export default class ClientPreferences extends Events {
5
+ constructor();
6
+ snapshot(): Promise<Readonly<{
7
+ theme: import("@phreshos/core").Theme;
8
+ animations: boolean;
9
+ }>>;
10
+ update(preferences: DesktopPreferencesUpdate): Promise<void>;
11
+ }
@@ -1,8 +1,7 @@
1
- import {} from "@phreshos/core";
2
- import Events from "./events.js";
3
- import wire from "./wire.js";
4
- /** Mutable effective preferences local to the current Desktop. */
5
- export default class ClientDesktopPreferences extends Events {
1
+ import Events from "../events.js";
2
+ import wire from "../wire.js";
3
+ /** Effective preferences owned by the Desktop containing this Client. */
4
+ export default class ClientPreferences extends Events {
6
5
  constructor() {
7
6
  super((event, listener, impossible) => wire.on("host-desktop-preferences", event, value => {
8
7
  listener(value);
@@ -0,0 +1,8 @@
1
+ import Events from "../events.js";
2
+ /** Read-only access to the Desktop surface containing this Client. */
3
+ export default class ClientSurface extends Events {
4
+ constructor();
5
+ snapshot(): Promise<Readonly<{
6
+ size: import("@phreshos/core").DesktopSize;
7
+ }>>;
8
+ }
@@ -0,0 +1,37 @@
1
+ import Events from "../events.js";
2
+ import wire from "../wire.js";
3
+ /** Read-only access to the Desktop surface containing this Client. */
4
+ export default class ClientSurface extends Events {
5
+ constructor() {
6
+ super((event, listener, impossible) => wire.on("host-desktop-surface", event, value => {
7
+ const snapshot = createSnapshot(value);
8
+ if (snapshot)
9
+ listener(snapshot);
10
+ }, null, impossible), observer => wire.onAll("host-desktop-surface", (event, value) => {
11
+ const snapshot = createSnapshot(value);
12
+ if (typeof event === "string" && snapshot)
13
+ observer(event, snapshot);
14
+ }));
15
+ }
16
+ async snapshot() {
17
+ const answer = await wire.request(["desktopSurface"]);
18
+ const snapshot = createSnapshot(answer[0]);
19
+ if (!snapshot)
20
+ throw new Error("The System returned an invalid Desktop surface");
21
+ return snapshot;
22
+ }
23
+ }
24
+ function createSnapshot(value) {
25
+ if (!record(value) || !record(value.size))
26
+ return null;
27
+ const { width, height } = value.size;
28
+ if (!finite(width) || !finite(height))
29
+ return null;
30
+ return Object.freeze({ size: Object.freeze({ width, height }) });
31
+ }
32
+ function record(value) {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+ function finite(value) {
36
+ return typeof value === "number" && Number.isFinite(value);
37
+ }
package/dist/domain.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface HandleAddress {
6
6
  }
7
7
  export interface EndpointDeclarationRecord {
8
8
  start: boolean;
9
+ service: boolean;
9
10
  }
10
11
  export interface ClientDeclarationRecord extends EndpointDeclarationRecord {
11
12
  title: string | null;
@@ -32,8 +33,11 @@ export interface ProcessRecord {
32
33
  program: ProgramRecord;
33
34
  options: Record<string, string>;
34
35
  startedAt: string | Date;
35
- server: Record<string, never> | null;
36
- client: Record<string, never> | null;
36
+ server: EndpointRecord | null;
37
+ client: EndpointRecord | null;
38
+ }
39
+ export interface EndpointRecord {
40
+ service: boolean;
37
41
  }
38
42
  export interface EndpointReference {
39
43
  kind: "server" | "client";
package/dist/domain.js CHANGED
@@ -4,7 +4,6 @@ import Deadline from "./deadline.js";
4
4
  import HandleRegistry from "./handle-registry.js";
5
5
  import { area, sql, store } from "./storage.js";
6
6
  import wire from "./wire.js";
7
- import { endpointService } from "./service.js";
8
7
  import { currentProgramPermission } from "./permissions.js";
9
8
  const handles = new HandleRegistry();
10
9
  const ProgramBase = CoreProgram;
@@ -75,7 +74,8 @@ function programCommandChunk(value) {
75
74
  }
76
75
  function declaration(record) {
77
76
  return Object.freeze({
78
- start: record.start
77
+ start: record.start,
78
+ service: record.service
79
79
  });
80
80
  }
81
81
  function clientDeclaration(record) {
@@ -219,9 +219,9 @@ class ServerHandle extends ServerBase {
219
219
  const answer = await wire.request(["exists", "server", this.owner.address]);
220
220
  return answer[0];
221
221
  }
222
- async start() { await wire.request(["start-endpoint", this.owner.address, "server"]); }
222
+ async start(launch = {}) { await wire.request(["start-endpoint", this.owner.address, "server", launch]); }
223
223
  async stop() { await wire.request(["stop-endpoint", this.owner.address, "server"]); }
224
- service() { return endpointService(this.owner.address, "server"); }
224
+ async isService() { return (await wire.request(["is-service", "server", this.owner.address]))[0]; }
225
225
  async waitReady(timeout) { await wire.request(["wait-ready", this.owner.address], timeout); }
226
226
  async ask(event, payload = undefined) {
227
227
  return this.askWithin(undefined, event, payload);
@@ -264,9 +264,9 @@ class ClientHandle extends ClientBase {
264
264
  const answer = await wire.request(["exists", "client", this.owner.address]);
265
265
  return answer[0];
266
266
  }
267
- async start(overrides = {}) { await wire.request(["start-endpoint", this.owner.address, "client", overrides]); }
267
+ async start(launch = {}) { await wire.request(["start-endpoint", this.owner.address, "client", launch]); }
268
268
  async stop() { await wire.request(["stop-endpoint", this.owner.address, "client"]); }
269
- service() { return endpointService(this.owner.address, "client"); }
269
+ async isService() { return (await wire.request(["is-service", "client", this.owner.address]))[0]; }
270
270
  }
271
271
  class WindowHandle extends Events {
272
272
  target;
package/dist/main.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  export { system, type System } from "./system.js";
2
- export { type SystemPointer, type PointerEvents, type PointerPosition } from "./pointer.js";
3
- export { type DesktopEvents, type DesktopSize, type SystemDesktop } from "./desktop.js";
2
+ export { type SystemDesktop } from "./desktop/desktop.js";
4
3
  export { context, type Context, type ContextCapture, type ContextEvents, type ContextMessage, type ContextServer } from "./context.js";
5
- export { ClientService, ServerService, Service, type ServiceKey, type ServiceLifecycle, type ServiceLifecycleEvents } from "@phreshos/core";
4
+ export { ClientService, ServerService, Service, type ServiceKey, } from "@phreshos/core";
6
5
  export { Client, Endpoint, Process, Program, Server, type Window, type ProgramProcess, type AnswerCapture, type AnswerMessage, type AnswerSubscriber, type AskCapture, type AskMessage, type AskSubscriber, type ClientTraffic, type EndpointTraffic, type ServerTraffic, type TrafficCapture, type TrafficEvents, type TrafficMessage } from "./domain.js";
7
- export type { Askable, Capture, Captures, CaptureSubscriber, ClientDeclaration, Cleanup, DirectoryStat, EndpointDeclaration, EndpointLifecycle, EndpointLifecycleEvents, EntryStat, EventMessage, EventName, EventOptions, EventSubscriber, Exit, FileStat, Launch, LaunchClient, Layer, LogKind, LogRecord, LogSource, Message, OtherStat, Outcome, PermissionDecision, Permission, Position, Storage, ProgramEvents, ProgramProcessEvents, ProgramProcessExit, ProgramSql, ProgramStore, ProcessEvents, Publishable, SystemUploads, Upload, Size, Subscribable, SubscribableEvents, SubscribableFallback, TimedAskable, TimedPermission, Timeoutable, Appearance, AppearanceEvents, AppearanceSource, AppearanceSurface, AnimationsPreference, DesktopPreferences, DesktopPreferencesEvents, DesktopPreferencesUpdate, SystemDesktopPreferences, ThemedValue, Theme, ThemePreference, Value, WindowEvents, WindowGeometry, WindowLayer, WindowState, Easing, LocalWindow, LocalWindowSurface, VisibilityTransition, Transaction } from "@phreshos/core";
6
+ export type { Askable, Capture, Captures, CaptureSubscriber, ClientLaunch, ClientDeclaration, Cleanup, DirectoryStat, EndpointDeclaration, EndpointLifecycle, EndpointLifecycleEvents, EntryStat, EventMessage, EventName, EventOptions, EventSubscriber, Exit, FileStat, Launch, ServerLaunch, Layer, LogKind, LogRecord, LogSource, Message, OtherStat, Outcome, PermissionDecision, Permission, Position, Storage, ProgramEvents, ProgramProcessEvents, ProgramProcessExit, ProgramSql, ProgramStore, ProcessEvents, Publishable, SystemUploads, Upload, Size, Subscribable, SubscribableEvents, SubscribableFallback, TimedAskable, TimedPermission, Timeoutable, Appearance, AppearanceEvents, AppearanceSource, AppearanceSurface, AnimationsPreference, DesktopPreferences, DesktopPreferencesEvents, DesktopPreferencesSource, DesktopPreferencesUpdate, DesktopPointerEvents, DesktopPointerPosition, DesktopPointerSnapshot, DesktopPointerSource, DesktopSize, DesktopSurfaceEvents, DesktopSurfaceSnapshot, DesktopSurfaceSource, WritableDesktopPreferencesSource, ThemedValue, Theme, ThemePreference, Value, WindowEvents, WindowGeometry, WindowLayer, WindowState, Easing, LocalWindow, LocalWindowSurface, VisibilityTransition, Transaction } from "@phreshos/core";
package/dist/main.js CHANGED
@@ -1,6 +1,5 @@
1
1
  export { system } from "./system.js";
2
- export {} from "./pointer.js";
3
- export {} from "./desktop.js";
2
+ export {} from "./desktop/desktop.js";
4
3
  export { context } from "./context.js";
5
- export { ClientService, ServerService, Service } from "@phreshos/core";
4
+ export { ClientService, ServerService, Service, } from "@phreshos/core";
6
5
  export { Client, Endpoint, Process, Program, Server } from "./domain.js";
package/dist/service.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { type ClientService, type ServerService, type Service, type ServiceKey } from "@phreshos/core";
2
- import type { HandleAddress } from "./domain.js";
3
2
  export declare function prepareService<EventsMap extends object = {}>(key: ServiceKey & {
4
3
  endpoint: "server";
5
4
  }): ServerService<EventsMap>;
@@ -7,8 +6,3 @@ export declare function prepareService<EventsMap extends object = {}>(key: Servi
7
6
  endpoint: "client";
8
7
  }): ClientService<EventsMap>;
9
8
  export declare function prepareService(key: ServiceKey): Service;
10
- export declare function enableCurrentService(name: string): Promise<void>;
11
- export declare function disableCurrentService(): Promise<void>;
12
- export declare function endpointService<EventsMap extends object = {}>(target: HandleAddress | null, endpoint: "server"): Promise<ServerService<EventsMap> | null>;
13
- export declare function endpointService<EventsMap extends object = {}>(target: HandleAddress | null, endpoint: "client"): Promise<ClientService<EventsMap> | null>;
14
- export declare function endpointService(target: HandleAddress | null, endpoint: "server" | "client"): Promise<Service | null>;
package/dist/service.js CHANGED
@@ -6,37 +6,43 @@ import wire from "./wire.js";
6
6
  const handles = new HandleRegistry();
7
7
  const ServerServiceBase = CoreServerService;
8
8
  const ClientServiceBase = CoreClientService;
9
- class ServerHandler extends ServerServiceBase {
9
+ class ServiceHandle {
10
10
  key;
11
- name;
12
11
  lifecycle;
13
12
  constructor(key) {
14
- super();
15
13
  this.key = key;
16
- this.name = key.name;
17
14
  this.lifecycle = new Events(...serviceEvents(key, "lifecycle"));
18
- bindEvents(this, new Events(...serviceEvents(key, "events")));
19
15
  }
20
16
  publish(event, payload = undefined) {
21
17
  wire.send("end-host", "service-send", this.key, event, payload);
22
18
  }
23
- async ask(event, payload = undefined) {
24
- return await this.askWithin(new Deadline(), event, payload);
25
- }
26
- timeout(milliseconds) {
27
- return {
28
- ask: (event, payload = undefined) => {
29
- return this.askWithin(new Deadline(milliseconds), event, payload);
30
- }
31
- };
32
- }
33
- async enabled() {
34
- const answer = await wire.request(["service-enabled", this.key]);
19
+ async exists() {
20
+ const answer = await wire.request(["service-exists", this.key]);
35
21
  return answer[0];
36
22
  }
23
+ }
24
+ class ServerHandler extends ServerServiceBase {
25
+ key;
26
+ lifecycle;
27
+ service;
28
+ constructor(key) {
29
+ super();
30
+ this.key = key;
31
+ this.service = new ServiceHandle(key);
32
+ this.lifecycle = this.service.lifecycle;
33
+ bindEvents(this, new Events(...serviceEvents(key, "events")));
34
+ }
35
+ publish(event, payload = undefined) { this.service.publish(event, payload); }
36
+ exists() { return this.service.exists(); }
37
37
  async waitReady(timeout) {
38
38
  await wire.request(["service-wait-ready", this.key, timeout], timeout);
39
39
  }
40
+ async ask(event, payload = undefined) {
41
+ return await this.askWithin(new Deadline(), event, payload);
42
+ }
43
+ timeout(milliseconds) {
44
+ return { ask: (event, payload = undefined) => (this.askWithin(new Deadline(milliseconds), event, payload)) };
45
+ }
40
46
  async askWithin(deadline, event, payload) {
41
47
  const identity = await wire.identity();
42
48
  const address = `client:${identity.process}:${crypto.randomUUID()}`;
@@ -52,44 +58,29 @@ class ServerHandler extends ServerServiceBase {
52
58
  }
53
59
  }
54
60
  class ClientHandler extends ClientServiceBase {
55
- key;
56
- name;
57
61
  lifecycle;
62
+ service;
58
63
  constructor(key) {
59
64
  super();
60
- this.key = key;
61
- this.name = key.name;
62
- this.lifecycle = new Events(...serviceEvents(key, "lifecycle"));
65
+ this.service = new ServiceHandle(key);
66
+ this.lifecycle = this.service.lifecycle;
63
67
  bindEvents(this, new Events(...serviceEvents(key, "events")));
64
68
  }
65
- async enabled() {
66
- const answer = await wire.request(["service-enabled", this.key]);
67
- return answer[0];
68
- }
69
- async waitReady(timeout) {
70
- await wire.request(["service-wait-ready", this.key, timeout], timeout);
71
- }
69
+ publish(event, payload = undefined) { this.service.publish(event, payload); }
70
+ exists() { return this.service.exists(); }
72
71
  }
73
72
  export function prepareService(key) {
74
73
  if (!isServiceKey(key))
75
74
  throw new Error("A complete service key is required");
76
- const normalized = Object.freeze({ program: key.program, endpoint: key.endpoint, name: key.name });
77
- const identity = JSON.stringify([normalized.program, normalized.endpoint, normalized.name]);
78
- return handles.obtain(`service:${identity}`, () => {
79
- return normalized.endpoint === "server"
80
- ? new ServerHandler(normalized)
81
- : new ClientHandler(normalized);
75
+ const normalized = Object.freeze({
76
+ ...(key.program === undefined ? {} : { program: key.program }),
77
+ process: key.process,
78
+ endpoint: key.endpoint
82
79
  });
83
- }
84
- export async function enableCurrentService(name) {
85
- await wire.request(["enable-service", name]);
86
- }
87
- export async function disableCurrentService() {
88
- await wire.request(["disable-service"]);
89
- }
90
- export async function endpointService(target, endpoint) {
91
- const answer = await wire.request(["endpoint-service", target, endpoint]);
92
- return answer[0] ? prepareService(answer[0]) : null;
80
+ const identity = JSON.stringify([key.program ?? null, key.process, key.endpoint]);
81
+ return handles.obtain(`service:${identity}`, () => normalized.endpoint === "server"
82
+ ? new ServerHandler(normalized)
83
+ : new ClientHandler(normalized));
93
84
  }
94
85
  function serviceEvents(key, scope) {
95
86
  return [
package/dist/system.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import type { AppearanceSource, ClientService, SystemDesktopPreferences, ServerService, ServiceKey, SystemUploads } from "@phreshos/core";
2
- import { type SystemPointer } from "./pointer.js";
3
- import { type SystemDesktop } from "./desktop.js";
1
+ import type { AppearanceSource, ClientService, ServerService, ServiceKey, SystemUploads } from "@phreshos/core";
2
+ import { type SystemDesktop } from "./desktop/desktop.js";
4
3
  type ServiceEndpoint = ServiceKey["endpoint"];
5
4
  type ServiceAddress<Endpoint extends ServiceEndpoint> = Omit<ServiceKey, "endpoint"> & Readonly<{
6
5
  endpoint: Endpoint;
@@ -10,12 +9,8 @@ type ServiceHandle<Endpoint extends ServiceEndpoint, Events extends object> = En
10
9
  export interface System {
11
10
  /** Complete unresolved Appearance read from the System authority. */
12
11
  readonly appearance: AppearanceSource;
13
- /** Mutable effective preferences local to this Desktop. */
14
- readonly desktopPreferences: SystemDesktopPreferences;
15
- /** Layer-independent desktop size reads and live updates. */
12
+ /** Capabilities owned by the Desktop containing this Client. */
16
13
  readonly desktop: SystemDesktop;
17
- /** Permission-guarded desktop pointer reads and live movement. */
18
- readonly pointer: SystemPointer;
19
14
  /** Flat System-owned public uploads capability. */
20
15
  readonly uploads: SystemUploads;
21
16
  /** Performs an unrestricted server-side fetch on behalf of this Client. */
package/dist/system.js CHANGED
@@ -1,16 +1,12 @@
1
1
  import ClientAppearance from "./appearance.js";
2
- import ClientDesktopPreferences from "./desktop-preferences.js";
3
2
  import wire from "./wire.js";
4
- import ClientPointer, {} from "./pointer.js";
5
- import ClientDesktop, {} from "./desktop.js";
3
+ import ClientDesktop, {} from "./desktop/desktop.js";
6
4
  import { prepareService } from "./service.js";
7
5
  import { uploads } from "./uploads.js";
8
6
  import controlledStream from "./controlled-stream.js";
9
7
  class ClientSystem {
10
8
  appearance = new ClientAppearance();
11
- desktopPreferences = new ClientDesktopPreferences();
12
9
  desktop = new ClientDesktop();
13
- pointer = new ClientPointer();
14
10
  uploads = uploads;
15
11
  service(key) { return prepareService(key); }
16
12
  async fetch(input, init) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/client",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
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.27"
50
+ "@phreshos/core": "^0.1.31"
51
51
  },
52
52
  "dependencies": {
53
53
  "@msgpack/msgpack": "^3.1.3"
54
54
  },
55
55
  "devDependencies": {
56
- "@phreshos/core": "^0.1.27",
56
+ "@phreshos/core": "^0.1.31",
57
57
  "typescript": "^6.0.3"
58
58
  }
59
59
  }
@@ -1,11 +0,0 @@
1
- import { type DesktopPreferencesUpdate } from "@phreshos/core";
2
- import Events from "./events.js";
3
- /** Mutable effective preferences local to the current Desktop. */
4
- export default class ClientDesktopPreferences extends Events {
5
- constructor();
6
- snapshot(): Promise<Readonly<{
7
- theme: import("@phreshos/core").Theme;
8
- animations: boolean;
9
- }>>;
10
- update(preferences: DesktopPreferencesUpdate): Promise<void>;
11
- }
package/dist/desktop.d.ts DELETED
@@ -1,25 +0,0 @@
1
- import type { Subscribable } from "@phreshos/core";
2
- import Events from "./events.js";
3
- /** The complete measured desktop area in CSS pixels. */
4
- export type DesktopSize = Readonly<{
5
- width: number;
6
- height: number;
7
- }>;
8
- /** Live changes to the desktop area containing this Client. */
9
- export type DesktopEvents = {
10
- /** The desktop area resized. */
11
- resize: DesktopSize;
12
- };
13
- /** Explicit desktop size reads and future resizes. */
14
- export interface SystemDesktop extends Subscribable<DesktopEvents, never> {
15
- /** Reads the complete current desktop area in CSS pixels. */
16
- size(): Promise<DesktopSize>;
17
- }
18
- /** Desktop access bound to the current Client Process boundary. */
19
- export default class ClientDesktop extends Events {
20
- constructor();
21
- size(): Promise<Readonly<{
22
- width: number;
23
- height: number;
24
- }>>;
25
- }
package/dist/desktop.js DELETED
@@ -1,34 +0,0 @@
1
- import Events from "./events.js";
2
- import wire from "./wire.js";
3
- /** Desktop access bound to the current Client Process boundary. */
4
- export default class ClientDesktop extends Events {
5
- constructor() {
6
- super((event, listener, impossible) => wire.on("host-desktop", event, value => {
7
- const size = createSize(value);
8
- if (size)
9
- listener(size);
10
- }, null, impossible), observer => wire.onAll("host-desktop", (event, value) => {
11
- const size = createSize(value);
12
- if (typeof event === "string" && size)
13
- observer(event, size);
14
- }));
15
- }
16
- async size() {
17
- const answer = await wire.request(["desktop"]);
18
- const size = createSize(answer[0]);
19
- if (!size)
20
- throw new Error("The system returned an invalid desktop size");
21
- return size;
22
- }
23
- }
24
- function createSize(value) {
25
- if (typeof value !== "object" || value === null || Array.isArray(value))
26
- return null;
27
- const size = value;
28
- if (!finite(size.width) || !finite(size.height))
29
- return null;
30
- return Object.freeze({ width: size.width, height: size.height });
31
- }
32
- function finite(value) {
33
- return typeof value === "number" && Number.isFinite(value);
34
- }
package/dist/pointer.d.ts DELETED
@@ -1,35 +0,0 @@
1
- import type { Subscribable } from "@phreshos/core";
2
- import Events from "./events.js";
3
- /** Pointer coordinates relative to the desktop display core. */
4
- export type PointerPosition = Readonly<{
5
- /** Horizontal coordinate in CSS pixels. */
6
- x: number;
7
- /** Vertical coordinate in CSS pixels. */
8
- y: number;
9
- }>;
10
- /** Live pointer events visible to this Client. */
11
- export type PointerEvents = {
12
- /** The pointer moved over the desktop display core. */
13
- move: PointerPosition;
14
- };
15
- /** Permission-guarded pointer positions and future movement. */
16
- export interface SystemPointer extends Subscribable<PointerEvents, never> {
17
- /**
18
- * Reads the current desktop pointer position, or `null` before one is known.
19
- * Rejects unless the `pointer` permission is currently granted.
20
- */
21
- position(): Promise<PointerPosition | null>;
22
- }
23
- /** Client pointer access bound to the current Process boundary. */
24
- export default class ClientPointer extends Events {
25
- private listeners;
26
- private readonly sample;
27
- constructor();
28
- position(): Promise<Readonly<{
29
- /** Horizontal coordinate in CSS pixels. */
30
- x: number;
31
- /** Vertical coordinate in CSS pixels. */
32
- y: number;
33
- }> | null>;
34
- private withSampling;
35
- }
package/dist/pointer.js DELETED
@@ -1,54 +0,0 @@
1
- import Events from "./events.js";
2
- import wire from "./wire.js";
3
- /** Client pointer access bound to the current Process boundary. */
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-pointer", event, value => {
9
- const position = createPosition(value);
10
- if (position)
11
- listener(position);
12
- }, null, impossible)), observer => this.withSampling(true, wire.onAll("host-pointer", (event, value) => {
13
- const position = createPosition(value);
14
- if (event === "move" && position)
15
- observer(event, position);
16
- })));
17
- }
18
- async position() {
19
- const answer = await wire.request(["pointer"]);
20
- if (answer[0] === null)
21
- return null;
22
- const position = createPosition(answer[0]);
23
- if (!position)
24
- throw new Error("The desktop returned an invalid Pointer position");
25
- return position;
26
- }
27
- withSampling(sample, stop) {
28
- if (!sample)
29
- return stop;
30
- if (this.listeners++ === 0)
31
- window.addEventListener("pointermove", this.sample);
32
- let active = true;
33
- return () => {
34
- if (!active)
35
- return;
36
- active = false;
37
- stop();
38
- this.listeners--;
39
- if (this.listeners === 0)
40
- window.removeEventListener("pointermove", this.sample);
41
- };
42
- }
43
- }
44
- function createPosition(value) {
45
- if (typeof value !== "object" || value === null || Array.isArray(value))
46
- return null;
47
- const position = value;
48
- if (!finite(position.x) || !finite(position.y))
49
- return null;
50
- return Object.freeze({ x: position.x, y: position.y });
51
- }
52
- function finite(value) {
53
- return typeof value === "number" && Number.isFinite(value);
54
- }