@phreshos/client 0.1.0 → 0.1.2

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/domain.js CHANGED
@@ -1,38 +1,49 @@
1
- import { Client as CoreClient, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, Window as CoreWindow } from "@phreshos/core";
1
+ import { Client as CoreClient, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer } from "@phreshos/core";
2
2
  import Events from "./events.js";
3
3
  import Deadline from "./deadline.js";
4
+ import HandleRegistry from "./handle-registry.js";
4
5
  import { area, sql, store } from "./storage.js";
5
6
  import wire from "./wire.js";
7
+ const handles = new HandleRegistry();
6
8
  const ProgramBase = CoreProgram;
7
9
  const ProcessBase = CoreProcess;
8
10
  const ServerBase = CoreServer;
9
11
  const ClientBase = CoreClient;
10
- const WindowBase = CoreWindow;
11
12
  class ProgramHandle extends ProgramBase {
12
13
  identity;
13
- name;
14
- version;
15
- description;
16
- server;
17
- client;
14
+ reference;
18
15
  data = area("data");
19
16
  cache = area("cache");
20
17
  store = store();
21
18
  logs = sql("logs");
22
19
  database = sql("database");
20
+ record;
23
21
  constructor(record) {
24
22
  super();
25
23
  this.identity = record.identity;
26
- this.name = record.name;
27
- this.version = record.version;
28
- this.description = record.description;
29
- this.server = record.server;
30
- this.client = record.client;
31
- bindEvents(this, scoped("host-end", record.identity, programEvent));
24
+ this.reference = record.reference;
25
+ this.record = record;
26
+ bindEvents(this, scoped("host-end", record.reference, programEvent));
27
+ }
28
+ get name() { return this.record.name; }
29
+ get version() { return this.record.version; }
30
+ get description() { return this.record.description; }
31
+ get server() { return this.record.server; }
32
+ get client() { return this.record.client; }
33
+ update(record) {
34
+ if (record.reference !== this.reference)
35
+ throw new Error("A Program handle cannot become another Program");
36
+ this.record = record;
32
37
  }
33
38
  async processes() {
34
39
  const answer = await wire.request(["processes"]);
35
- return answer[0].map(process);
40
+ return answer[0].map(record => process(record));
41
+ }
42
+ async firstProcess() {
43
+ return chronological(await this.processes())[0] ?? null;
44
+ }
45
+ async lastProcess() {
46
+ return chronological(await this.processes()).at(-1) ?? null;
36
47
  }
37
48
  async getProcess(identityOrName) {
38
49
  const answer = await wire.request(["program-process", undefined, identityOrName]);
@@ -46,6 +57,10 @@ class ProgramHandle extends ProgramBase {
46
57
  const answer = await wire.request(["api-docs"]);
47
58
  return answer[0];
48
59
  }
60
+ async icon(size = "medium") {
61
+ const answer = await wire.request(["icon", size]);
62
+ return new Blob([Uint8Array.from(answer[0])], { type: "image/png" });
63
+ }
49
64
  async installed() {
50
65
  const answer = await wire.request(["installed"]);
51
66
  return answer[0];
@@ -57,64 +72,70 @@ class ProgramHandle extends ProgramBase {
57
72
  return answer[0];
58
73
  }
59
74
  }
75
+ function chronological(processes) {
76
+ return processes.sort((left, right) => left.startedAt.getTime() - right.startedAt.getTime());
77
+ }
60
78
  class ProcessHandle extends ProcessBase {
61
79
  identity;
80
+ reference;
62
81
  name;
63
82
  startedAt;
64
83
  server;
65
84
  client;
66
85
  ownerProgram;
67
86
  options;
68
- constructor(record) {
87
+ constructor(record, endpoints = {}) {
69
88
  super();
70
89
  this.identity = record.identity;
90
+ this.reference = record.reference;
71
91
  this.name = record.name;
72
92
  this.startedAt = new Date(record.startedAt);
73
93
  this.ownerProgram = program(record.program);
74
94
  this.options = record.options;
75
- this.server = new ServerHandle(this);
76
- this.client = new ClientHandle(this);
77
- bindEvents(this, scoped("process-host", record.identity, processEvent));
95
+ this.server = endpointHandle(this, "server", endpoints.server);
96
+ this.client = endpointHandle(this, "client", endpoints.client);
97
+ bindEvents(this, scoped("process-host", record.reference, processEvent));
78
98
  }
79
99
  program() { return this.ownerProgram; }
100
+ get address() { return { identity: this.identity, reference: this.reference }; }
80
101
  async parent() {
81
- const answer = await wire.request(["parent", this.identity]);
102
+ const answer = await wire.request(["parent", this.address]);
82
103
  return answer[0] ? process(answer[0]) : null;
83
104
  }
84
105
  async option(name) {
85
106
  if (name in this.options)
86
107
  return this.options[name];
87
- const answer = await wire.request(["option", this.identity, name]);
108
+ const answer = await wire.request(["option", this.address, name]);
88
109
  return answer[0];
89
110
  }
90
- async exit() { await wire.request(["exit", this.identity]); }
111
+ async exit() { await wire.request(["exit", this.address]); }
91
112
  async exited() {
92
- const answer = await wire.request(["exited", this.identity]);
113
+ const answer = await wire.request(["exited", this.address]);
93
114
  return answer[0];
94
115
  }
95
116
  }
96
- class TrafficHandle extends Events {
97
- owner;
117
+ export class TrafficHandle extends Events {
118
+ target;
98
119
  kind;
99
- constructor(owner, kind) {
100
- super((event, listener, impossible) => wire.observe(owner.identity, kind, "publish", event, value => listener(trafficMessage(value)), impossible), observer => wire.observe(owner.identity, kind, "publish", null, (event, value) => {
120
+ constructor(target, kind) {
121
+ super((event, listener, impossible) => wire.observe(target, kind, "publish", event, value => listener(trafficMessage(value)), impossible), observer => wire.observe(target, kind, "publish", null, (event, value) => {
101
122
  if (typeof event === "string")
102
123
  observer(event, trafficMessage(value));
103
124
  }));
104
- this.owner = owner;
125
+ this.target = target;
105
126
  this.kind = kind;
106
127
  }
107
128
  observeAsks(observer) {
108
- return wire.observe(this.owner.identity, this.kind, "ask", null, (event, questionId, value) => {
129
+ return wire.observe(this.target, this.kind, "ask", null, (event, questionId, value) => {
109
130
  if (typeof event !== "string" || typeof questionId !== "string")
110
131
  return;
111
132
  observer({ event, questionId, message: trafficMessage(value) });
112
133
  });
113
134
  }
114
135
  }
115
- class ServerTrafficHandle extends TrafficHandle {
136
+ export class ServerTrafficHandle extends TrafficHandle {
116
137
  observeAnswers(observer) {
117
- return wire.observe(this.owner.identity, "server", "answer", null, (event, questionId, value) => {
138
+ return wire.observe(this.target, "server", "answer", null, (event, questionId, value) => {
118
139
  if (typeof event !== "string" || typeof questionId !== "string")
119
140
  return;
120
141
  const raw = value;
@@ -128,31 +149,32 @@ class ServerHandle extends ServerBase {
128
149
  constructor(owner) {
129
150
  super();
130
151
  this.owner = owner;
131
- this.traffic = new ServerTrafficHandle(owner, "server");
152
+ this.traffic = new ServerTrafficHandle(owner.address, "server");
153
+ bindEvents(this, endpointEvents(owner.address, "server"));
132
154
  }
133
- process() { return this.owner; }
134
- publish(event, payload) { wire.send("end-host", "send", this.owner.identity, "server", event, payload); }
155
+ async process() { return this.owner; }
156
+ publish(event, payload = undefined) { wire.send("end-host", "send", this.owner.address, "server", event, payload); }
135
157
  async exists() {
136
- const answer = await wire.request(["exists", "server", this.owner.identity]);
158
+ const answer = await wire.request(["exists", "server", this.owner.address]);
137
159
  return answer[0];
138
160
  }
139
- async start() { await wire.request(["start-endpoint", this.owner.identity, "server"]); }
140
- async stop() { await wire.request(["stop-endpoint", this.owner.identity, "server"]); }
141
- async waitReady(timeout) { await wire.request(["wait-ready", this.owner.identity], timeout); }
142
- async ask(event, payload) {
161
+ async start() { await wire.request(["start-endpoint", this.owner.address, "server"]); }
162
+ async stop() { await wire.request(["stop-endpoint", this.owner.address, "server"]); }
163
+ async waitReady(timeout) { await wire.request(["wait-ready", this.owner.address], timeout); }
164
+ async ask(event, payload = undefined) {
143
165
  return this.askWithin(undefined, event, payload);
144
166
  }
145
167
  timeout(milliseconds) {
146
- return { ask: (event, payload) => this.askWithin(milliseconds, event, payload) };
168
+ return { ask: (event, payload = undefined) => this.askWithin(milliseconds, event, payload) };
147
169
  }
148
170
  async askWithin(timeout, event, payload) {
149
171
  const deadline = new Deadline(timeout);
150
- await wire.requestWithin(["wait-ready", this.owner.identity, true], deadline);
151
- const source = await wire.identity;
152
- const address = `client:${source}:${crypto.randomUUID()}`;
172
+ await wire.requestWithin(["wait-ready", this.owner.address, true], deadline);
173
+ const source = await wire.identity();
174
+ const address = `client:${source.process}:${crypto.randomUUID()}`;
153
175
  const questionId = crypto.randomUUID();
154
176
  const waiting = wire.expectWithin(address, deadline);
155
- wire.send("end-host", "ask", this.owner.identity, "server", address, questionId, event, payload);
177
+ wire.send("end-host", "ask", this.owner.address, "server", address, questionId, event, payload);
156
178
  try {
157
179
  return await waiting;
158
180
  }
@@ -164,34 +186,33 @@ class ServerHandle extends ServerBase {
164
186
  class ClientHandle extends ClientBase {
165
187
  owner;
166
188
  traffic;
189
+ window;
167
190
  constructor(owner) {
168
191
  super();
169
192
  this.owner = owner;
170
- this.traffic = new TrafficHandle(owner, "client");
193
+ this.traffic = new TrafficHandle(owner.address, "client");
194
+ this.window = window(async () => owner.address);
195
+ bindEvents(this, endpointEvents(owner.address, "client"));
171
196
  }
172
- process() { return this.owner; }
173
- publish(event, payload) { wire.send("end-host", "send", this.owner.identity, "client", event, payload); }
197
+ async process() { return this.owner; }
198
+ publish(event, payload = undefined) { wire.send("end-host", "send", this.owner.address, "client", event, payload); }
174
199
  async exists() {
175
- const answer = await wire.request(["exists", "client", this.owner.identity]);
200
+ const answer = await wire.request(["exists", "client", this.owner.address]);
176
201
  return answer[0];
177
202
  }
178
- async start(overrides = {}) { await wire.request(["start-endpoint", this.owner.identity, "client", overrides]); }
179
- async stop() { await wire.request(["stop-endpoint", this.owner.identity, "client"]); }
180
- async window() {
181
- await wire.request(["window", this.owner.identity]);
182
- return new WindowHandle(this);
183
- }
203
+ async start(overrides = {}) { await wire.request(["start-endpoint", this.owner.address, "client", overrides]); }
204
+ async stop() { await wire.request(["stop-endpoint", this.owner.address, "client"]); }
184
205
  }
185
- class WindowHandle extends WindowBase {
186
- owner;
187
- constructor(owner) {
188
- super();
189
- this.owner = owner;
190
- bindEvents(this, scoped("host-end", owner.process().identity, (_event, values) => values[0]));
206
+ class WindowHandle extends Events {
207
+ target;
208
+ surface;
209
+ constructor(target) {
210
+ super(...deferredScoped("host-end", target, (_event, values) => values[0]));
211
+ this.target = target;
212
+ this.surface = new WindowSurfaceHandle(target);
191
213
  }
192
- client() { return this.owner; }
193
214
  async state() {
194
- const answer = await wire.request(["window", this.owner.process().identity]);
215
+ const answer = await wire.request(["window", await this.target()]);
195
216
  return answer[0];
196
217
  }
197
218
  async title() { return (await this.state()).title; }
@@ -201,13 +222,64 @@ class WindowHandle extends WindowBase {
201
222
  async front() { return (await this.state()).front; }
202
223
  async layer() { return (await this.state()).layer; }
203
224
  async location() { return (await this.state()).location; }
204
- async move(position) { await wire.request(["move", this.owner.process().identity, position]); }
205
- localMove(position) { wire.send("end-host", "localMove", this.owner.process().identity, position); }
206
- async resize(size) { await wire.request(["resize", this.owner.process().identity, size]); }
207
- localResize(size) { wire.send("end-host", "localResize", this.owner.process().identity, size); }
208
- async minimize(minimized = true) { await wire.request(["minimize", this.owner.process().identity, minimized]); }
209
- async changeTitle(title) { await wire.request(["changeTitle", this.owner.process().identity, title]); }
210
- async raise() { await wire.request(["raise", this.owner.process().identity]); }
225
+ async move(position) { await wire.request(["move", await this.target(), position]); }
226
+ async localMove(position) { await wire.request(["localMove", await this.target(), position]); }
227
+ async resize(size) { await wire.request(["resize", await this.target(), size]); }
228
+ async localResize(size) { await wire.request(["localResize", await this.target(), size]); }
229
+ async minimize(minimized = true) { await wire.request(["minimize", await this.target(), minimized]); }
230
+ async changeTitle(title) { await wire.request(["changeTitle", await this.target(), title]); }
231
+ async raise() { await wire.request(["raise", await this.target()]); }
232
+ }
233
+ class WindowSurfaceHandle {
234
+ target;
235
+ constructor(target) {
236
+ this.target = target;
237
+ }
238
+ async set(settings = {}) { await wire.request(["surfaceSet", await this.target(), settings]); }
239
+ async remove() { await wire.request(["surfaceRemove", await this.target()]); }
240
+ }
241
+ function deferredScoped(route, target, convert) {
242
+ return [
243
+ (event, listener, impossible) => {
244
+ if (!windowEvent(event)) {
245
+ impossible?.(new Error(`A Window has no "${event}" event`));
246
+ return () => undefined;
247
+ }
248
+ return deferred(target, subject => wire.on(route, event, (...values) => {
249
+ const message = unscoped(subject, values);
250
+ if (message)
251
+ listener(convert(event, message));
252
+ }, subject, impossible), impossible);
253
+ },
254
+ observer => deferred(target, subject => wire.onAll(route, (event, ...values) => {
255
+ if (typeof event !== "string" || !windowEvent(event))
256
+ return;
257
+ const message = unscoped(subject, values);
258
+ if (message)
259
+ observer(event, convert(event, message));
260
+ }, subject))
261
+ ];
262
+ }
263
+ function windowEvent(event) {
264
+ return event === "move" || event === "resize" || event === "minimize" || event === "changeTitle" || event === "front";
265
+ }
266
+ function deferred(target, register, impossible) {
267
+ let active = true;
268
+ let stop = () => undefined;
269
+ void target().then(address => {
270
+ if (active)
271
+ stop = register(address.reference);
272
+ }, error => {
273
+ const failure = error instanceof Error ? error : new Error(String(error));
274
+ if (active && impossible)
275
+ impossible(failure);
276
+ else if (active)
277
+ queueMicrotask(() => { throw failure; });
278
+ });
279
+ return () => {
280
+ active = false;
281
+ stop();
282
+ };
211
283
  }
212
284
  export function scoped(route, subject, convert) {
213
285
  return new Events((event, listener, impossible) => wire.on(route, event, (...values) => {
@@ -222,16 +294,23 @@ export function scoped(route, subject, convert) {
222
294
  observer(event, convert(event, message));
223
295
  }, subject));
224
296
  }
297
+ /** Destinationless events originating from one Endpoint handle. */
298
+ export function endpointEvents(target, half) {
299
+ return new Events((event, listener, impossible) => wire.follow(target, half, event, listener, impossible), observer => wire.follow(target, half, null, (event, payload) => {
300
+ if (typeof event === "string")
301
+ observer(event, payload);
302
+ }));
303
+ }
225
304
  function unscoped(subject, values) {
226
305
  if (subject === null)
227
306
  return values;
228
307
  return values[0] === subject ? values.slice(1) : null;
229
308
  }
230
309
  function programEvent(event, values) {
231
- if (event === "serverStart" || event === "clientStart" || event === "clientStop" || event === "processCreate")
310
+ if (event === "endpointStart" || event === "endpointStop")
311
+ return lifecycleEndpoint(values[0], values[1]);
312
+ if (event === "processCreate")
232
313
  return process(values[0]);
233
- if (event === "serverStop")
234
- return { process: process(values[0]), code: numberOrNull(values[1]), signal: stringOrNull(values[2]) };
235
314
  if (event === "processExit")
236
315
  return { process: process(values[0]), ...exit(values[1], values[2]) };
237
316
  if (event === "uninstall")
@@ -239,12 +318,20 @@ function programEvent(event, values) {
239
318
  return undefined;
240
319
  }
241
320
  function processEvent(event, values) {
242
- if (event === "serverStop")
243
- return { code: numberOrNull(values[0]), signal: stringOrNull(values[1]) };
321
+ if (event === "endpointStart" || event === "endpointStop")
322
+ return lifecycleEndpoint(values[0], values[1]);
244
323
  if (event === "exit")
245
324
  return exit(values[0], values[1]);
246
325
  return undefined;
247
326
  }
327
+ function lifecycleEndpoint(record, kind) {
328
+ const owner = process(record);
329
+ if (kind === "server")
330
+ return owner.server;
331
+ if (kind === "client")
332
+ return owner.client;
333
+ throw new Error("The host returned an invalid Endpoint lifecycle event");
334
+ }
248
335
  export function exit(code, signal) {
249
336
  const namedSignal = stringOrNull(signal);
250
337
  return { status: namedSignal === null ? "exited" : "signaled", code: numberOrNull(code), signal: namedSignal };
@@ -263,23 +350,42 @@ export function bindEvents(target, events) {
263
350
  observe: events.observe.bind(events)
264
351
  });
265
352
  }
266
- export function program(record) { return new ProgramHandle(record); }
267
- export function process(record) { return new ProcessHandle(record); }
353
+ export function program(record) {
354
+ const handle = handles.obtain(`program:${record.reference}`, () => new ProgramHandle(record));
355
+ handle.update(record);
356
+ return handle;
357
+ }
358
+ export function process(record, endpoints = {}) {
359
+ return handles.obtain(`process:${record.reference}`, () => new ProcessHandle(record, endpoints));
360
+ }
268
361
  export function endpoint(reference) {
269
362
  if (!reference)
270
363
  throw new Error("The boundary returned an invalid Endpoint reference");
271
- const owner = new ProcessHandle(reference.process);
364
+ const owner = process(reference.process);
272
365
  return reference.kind === "server" ? owner.server : owner.client;
273
366
  }
367
+ export function claimEndpoint(reference, kind, endpoint) {
368
+ return handles.adopt(`endpoint:${reference}:${kind}`, endpoint);
369
+ }
370
+ function endpointHandle(owner, kind, preferred) {
371
+ return handles.obtain(`endpoint:${owner.reference}:${kind}`, () => preferred ?? (kind === "server" ? new ServerHandle(owner) : new ClientHandle(owner)));
372
+ }
373
+ export function window(target) {
374
+ return new WindowHandle(target);
375
+ }
274
376
  /** Resolves only endpoint identities intentionally visible to this Client. */
275
377
  export function visibleEndpoint(reference) {
276
378
  if (reference === null)
277
379
  return null;
278
380
  return endpoint(reference);
279
381
  }
382
+ /** Runtime constructor shared by all client-visible Program handles. */
280
383
  export const Program = CoreProgram;
384
+ /** Runtime constructor shared by all client-visible Process handles. */
281
385
  export const Process = CoreProcess;
386
+ /** Runtime constructor shared by all client-visible Endpoint handles. */
282
387
  export const Endpoint = CoreEndpoint;
388
+ /** Runtime constructor shared by all client-visible Server handles. */
283
389
  export const Server = CoreServer;
390
+ /** Runtime constructor shared by all client-visible Client handles. */
284
391
  export const Client = CoreClient;
285
- export const Window = CoreWindow;
@@ -0,0 +1,7 @@
1
+ /** Weak canonical references for system-backed domain handles in this SDK realm. */
2
+ export default class HandleRegistry {
3
+ private readonly handles;
4
+ private readonly released;
5
+ obtain<Value extends object>(key: string, create: () => Value): Value;
6
+ adopt<Value extends object>(key: string, value: Value): Value;
7
+ }
@@ -0,0 +1,24 @@
1
+ /** Weak canonical references for system-backed domain handles in this SDK realm. */
2
+ export default class HandleRegistry {
3
+ handles = new Map();
4
+ released = new FinalizationRegistry(({ key, reference }) => {
5
+ if (this.handles.get(key) === reference)
6
+ this.handles.delete(key);
7
+ });
8
+ obtain(key, create) {
9
+ const existing = this.handles.get(key)?.deref();
10
+ if (existing)
11
+ return existing;
12
+ const value = create();
13
+ const reference = new WeakRef(value);
14
+ this.handles.set(key, reference);
15
+ this.released.register(value, { key, reference });
16
+ return value;
17
+ }
18
+ adopt(key, value) {
19
+ const existing = this.handles.get(key)?.deref();
20
+ if (existing && existing !== value)
21
+ throw new Error(`The canonical handle for "${key}" already exists`);
22
+ return existing ?? this.obtain(key, () => value);
23
+ }
24
+ }
package/dist/host.d.ts CHANGED
@@ -1,25 +1,20 @@
1
- import type { Layer, ServedFile, Subscribable } from "@phreshos/core";
2
- /** Pointer coordinates relative to the desktop display core. */
3
- export type PointerPosition = Readonly<{
4
- x: number;
5
- y: number;
6
- }>;
7
- /** One desktop layer's available workspace. */
8
- export type Surface = Readonly<{
9
- width: number;
10
- height: number;
11
- gutter: number;
12
- }>;
13
- /** Session-local desktop events available to a Client endpoint. */
14
- export type HostEvents = {
15
- surface: Surface;
16
- pointerMove: PointerPosition;
17
- };
1
+ import type { Permissions, ServedFile, Theme, ThemeProperties } from "@phreshos/core";
2
+ import { type HostPointer } from "./pointer.js";
3
+ import { type HostSurface } from "./surface.js";
18
4
  /** Desktop capabilities structurally available to a Client endpoint. */
19
- export interface Host<Events extends object = {}> extends Subscribable<HostEvents & Events, never> {
20
- pointerPosition(): Promise<PointerPosition | null>;
21
- surface(layer?: Layer): Promise<Surface>;
5
+ export interface Host {
6
+ /** Read-only system Theme explicitly read from and observed through the desktop host. */
7
+ readonly theme: Theme<ThemeProperties>;
8
+ /** This Client Window's asynchronous surface read and live updates. */
9
+ readonly surface: HostSurface;
10
+ /** Permission-guarded desktop pointer reads and live movement. */
11
+ readonly pointer: HostPointer;
12
+ /** Permission decisions for capabilities guarded by the desktop. */
13
+ readonly permissions: Permissions;
14
+ /** Stores one value as a publicly reachable file. */
22
15
  serve(value: unknown): Promise<ServedFile>;
16
+ /** Performs an unrestricted server-side fetch on behalf of this Client. */
23
17
  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
24
18
  }
19
+ /** Desktop capabilities available to this Client. */
25
20
  export declare const host: Host;
package/dist/host.js CHANGED
@@ -1,20 +1,14 @@
1
1
  import { content } from "./content.js";
2
- import Events from "./events.js";
2
+ import ClientTheme from "./theme.js";
3
3
  import wire from "./wire.js";
4
- class ClientHost extends Events {
5
- pointerListeners = 0;
6
- samplePointer = (event) => wire.samplePointer(event.movementX, event.movementY);
7
- constructor() {
8
- super((event, listener, impossible) => this.withPointerSampling(event === "pointerMove", wire.on("host-end", event, listener, null, impossible)), observer => this.withPointerSampling(true, combine(wire.on("host-end", "surface", value => observer("surface", value)), wire.on("host-end", "pointerMove", value => observer("pointerMove", value)))));
9
- }
10
- async pointerPosition() {
11
- const answer = await wire.request(["pointerPosition"]);
12
- return answer[0];
13
- }
14
- async surface(layer) {
15
- const answer = await wire.request(["surface", undefined, layer]);
16
- return answer[0];
17
- }
4
+ import ClientPermissions from "./permissions.js";
5
+ import ClientPointer, {} from "./pointer.js";
6
+ import ClientSurface, {} from "./surface.js";
7
+ class ClientHost {
8
+ theme = new ClientTheme();
9
+ surface = new ClientSurface();
10
+ pointer = new ClientPointer();
11
+ permissions = new ClientPermissions();
18
12
  async serve(value) {
19
13
  const source = content(value);
20
14
  const channel = new MessageChannel();
@@ -85,22 +79,6 @@ class ClientHost extends Events {
85
79
  });
86
80
  return response;
87
81
  }
88
- withPointerSampling(sample, stop) {
89
- if (!sample)
90
- return stop;
91
- if (this.pointerListeners++ === 0)
92
- window.addEventListener("pointermove", this.samplePointer);
93
- let active = true;
94
- return () => {
95
- if (!active)
96
- return;
97
- active = false;
98
- stop();
99
- this.pointerListeners--;
100
- if (this.pointerListeners === 0)
101
- window.removeEventListener("pointermove", this.samplePointer);
102
- };
103
- }
104
82
  }
105
83
  function requestHeaders(normalized, supplied) {
106
84
  if (!supplied)
@@ -142,8 +120,5 @@ function closeControl(signal, port, abort) {
142
120
  signal.removeEventListener("abort", abort);
143
121
  port.close();
144
122
  }
145
- function combine(...cleanups) {
146
- return () => { for (const cleanup of cleanups)
147
- cleanup(); };
148
- }
123
+ /** Desktop capabilities available to this Client. */
149
124
  export const host = new ClientHost();
package/dist/main.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- export { host, type Host, type HostEvents, type PointerPosition, type Surface } from "./host.js";
1
+ export { host, type Host } from "./host.js";
2
+ export { type HostPointer, type PointerEvents, type PointerPosition } from "./pointer.js";
3
+ export { type HostSurface, type Surface, type SurfaceEvents } from "./surface.js";
2
4
  export { current, type Current, type CurrentServer } from "./current.js";
3
5
  export { type Channel, type ChannelCapture, type ChannelEvents, type ChannelMessage } from "./channel.js";
4
- export { Client, Endpoint, Process, Program, Server, Window, 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";
5
- 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, Position, ProgramArea, ProgramEvents, ProgramProcessExit, ProgramServerStop, ProgramSql, ProgramStore, ProcessEvents, Publishable, ServedFile, Size, Subscribable, TimedAskable, Value, WindowEvents, WindowState } from "@phreshos/core";
6
+ export { Client, Endpoint, Process, Program, Server, type Window, 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";
7
+ 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, Permissions, Position, ProgramArea, ProgramEvents, ProgramProcessExit, ProgramSql, ProgramStore, ProcessEvents, Publishable, ServedFile, Size, Subscribable, SubscribableEvents, SubscribableFallback, TimedAskable, TimedPermissions, Timeoutable, Theme, ThemeEvents, ThemeProperties, Value, WritableTheme, WindowEvents, WindowLayer, WindowState, WindowSurface, WindowSurfaceEasing, WindowSurfaceSettings, WindowSurfaceTransaction } from "@phreshos/core";
package/dist/main.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export { host } from "./host.js";
2
+ export {} from "./pointer.js";
3
+ export {} from "./surface.js";
2
4
  export { current } from "./current.js";
3
5
  export {} from "./channel.js";
4
- export { Client, Endpoint, Process, Program, Server, Window } from "./domain.js";
6
+ export { Client, Endpoint, Process, Program, Server } from "./domain.js";
@@ -0,0 +1,8 @@
1
+ import type { PermissionDecision, PermissionName, Permissions, TimedPermissions } from "@phreshos/core";
2
+ /** Client permission access bound to the current Process boundary. */
3
+ export default class ClientPermissions implements Permissions {
4
+ granted(name: PermissionName): Promise<PermissionDecision>;
5
+ request(name: PermissionName): Promise<PermissionDecision>;
6
+ timeout(milliseconds: number): TimedPermissions;
7
+ private requestWithin;
8
+ }
@@ -0,0 +1,21 @@
1
+ import wire from "./wire.js";
2
+ const defaultPermissionTimeout = 30_000;
3
+ /** Client permission access bound to the current Process boundary. */
4
+ export default class ClientPermissions {
5
+ async granted(name) {
6
+ const answer = await wire.request(["permission-granted", name]);
7
+ return answer[0];
8
+ }
9
+ request(name) {
10
+ return this.requestWithin(name, defaultPermissionTimeout);
11
+ }
12
+ timeout(milliseconds) {
13
+ if (!Number.isFinite(milliseconds) || milliseconds < 0)
14
+ throw new Error("A permission timeout must be a non-negative finite number");
15
+ return Object.freeze({ request: (name) => this.requestWithin(name, milliseconds) });
16
+ }
17
+ async requestWithin(name, milliseconds) {
18
+ const answer = await wire.requestOrNull(["permission-request", name], milliseconds);
19
+ return answer?.[0] ?? null;
20
+ }
21
+ }
@@ -0,0 +1,35 @@
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 HostPointer 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
+ }