@phreshos/client 0.1.0
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 +65 -0
- package/dist/channel.d.ts +8 -0
- package/dist/channel.js +16 -0
- package/dist/content.d.ts +7 -0
- package/dist/content.js +51 -0
- package/dist/current.d.ts +20 -0
- package/dist/current.js +58 -0
- package/dist/deadline.d.ts +7 -0
- package/dist/deadline.js +13 -0
- package/dist/domain.d.ts +93 -0
- package/dist/domain.js +285 -0
- package/dist/events.d.ts +16 -0
- package/dist/events.js +83 -0
- package/dist/host.d.ts +25 -0
- package/dist/host.js +149 -0
- package/dist/log.d.ts +6 -0
- package/dist/log.js +64 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +4 -0
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +98 -0
- package/dist/wire.d.ts +34 -0
- package/dist/wire.js +226 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# `@phreshos/client`
|
|
2
|
+
|
|
3
|
+
The Client SDK defines the contextual capabilities available inside a
|
|
4
|
+
Program's client endpoint.
|
|
5
|
+
|
|
6
|
+
## Package status
|
|
7
|
+
|
|
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.
|
|
11
|
+
|
|
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.
|
|
15
|
+
|
|
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 host and transport implementations.
|
|
19
|
+
|
|
20
|
+
Its `Host` contract exposes only desktop-session capabilities: surface and
|
|
21
|
+
pointer state, publicly served values, and unrestricted server-side Fetch. It
|
|
22
|
+
does not expose system-wide Program or Process discovery.
|
|
23
|
+
|
|
24
|
+
A Client may traverse `Process.parent()` through any number of ancestors in
|
|
25
|
+
its own Program. The first parent outside that Program is structurally hidden
|
|
26
|
+
and returned as `null`; no cross-Program Process handle enters the client. If
|
|
27
|
+
client code fabricates or otherwise obtains an unauthorized handle, the system
|
|
28
|
+
responds exactly as it does for a nonexistent Process.
|
|
29
|
+
|
|
30
|
+
Its JavaScript entry point adapts the iframe boundary to these contracts. The
|
|
31
|
+
SDK owns callbacks, waits, queues, and their cleanup; the boundary owns only
|
|
32
|
+
the forwarding registrations requested by the SDK.
|
|
33
|
+
|
|
34
|
+
`Current` combines navigation into the executing Client's Process with its
|
|
35
|
+
inbound subscription Channel. The paired Server is explicitly named as
|
|
36
|
+
`current.server`; its publishing, asking, existence, readiness, start, and stop
|
|
37
|
+
operations never masquerade as properties of `current`. `current.stop()` stops
|
|
38
|
+
the executing Client, while complete Process exit remains available only
|
|
39
|
+
through `current.process()`.
|
|
40
|
+
|
|
41
|
+
`server.ask()` does not route a question before the current Server incarnation
|
|
42
|
+
is ready. The Client SDK owns one deadline across readiness and the answer;
|
|
43
|
+
absence or incarnation loss rejects without turning the boundary into a waiter.
|
|
44
|
+
|
|
45
|
+
The package provides two contextual values rather than constructors:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { host, current } from "@phreshos/client"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The current Client's inbound Channel is composed directly into `current`. It
|
|
52
|
+
exposes `subscribe()`, `waitFor()`, `events()`, and `observe()` for events
|
|
53
|
+
addressed to this Client. It has no outbound, existence, readiness, or
|
|
54
|
+
lifecycle operations; those belong to `current.server`, `current.stop()`, or an
|
|
55
|
+
explicit Endpoint handle.
|
|
56
|
+
|
|
57
|
+
Messages sent by an Endpoint in the same Program contain that real Endpoint in
|
|
58
|
+
`from`. If the sender belongs to another Program, `from` is `null`; the foreign
|
|
59
|
+
identity is removed by the server host before the message reaches the desktop.
|
|
60
|
+
The same rule applies to Client-visible traffic destinations.
|
|
61
|
+
|
|
62
|
+
Client-visible Program handles can operate only within their own Program.
|
|
63
|
+
They deliberately omit `install()` and `fork()`, and their storage areas never
|
|
64
|
+
expose host filesystem paths. Client Window handles add synchronous
|
|
65
|
+
`localMove()` and `localResize()` for representation-local gestures.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Channel as CoreChannel, ChannelCapture as CoreChannelCapture, ChannelEvents as CoreChannelEvents, ChannelMessage as CoreChannelMessage } from "@phreshos/core";
|
|
2
|
+
import { type Endpoint } from "./domain.js";
|
|
3
|
+
export type ChannelMessage<Payload = unknown> = CoreChannelMessage<Payload, Endpoint | null>;
|
|
4
|
+
export type ChannelEvents<Events extends object> = CoreChannelEvents<Events, Endpoint | null>;
|
|
5
|
+
export type ChannelCapture<Events extends object = {}> = CoreChannelCapture<Events, Endpoint | null>;
|
|
6
|
+
export interface Channel<Events extends object = {}> extends CoreChannel<Events, Endpoint | null> {
|
|
7
|
+
}
|
|
8
|
+
export declare const channel: Channel;
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { visibleEndpoint } from "./domain.js";
|
|
2
|
+
import Events from "./events.js";
|
|
3
|
+
import wire from "./wire.js";
|
|
4
|
+
class ClientChannel extends Events {
|
|
5
|
+
constructor() {
|
|
6
|
+
super((event, listener, impossible) => wire.on("end-end", event, value => listener(message(value)), null, impossible), observer => wire.onAll("end-end", (event, value) => {
|
|
7
|
+
if (typeof event === "string")
|
|
8
|
+
observer(event, message(value));
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function message(value) {
|
|
13
|
+
const raw = value;
|
|
14
|
+
return { from: visibleEndpoint(raw.from), payload: raw.payload };
|
|
15
|
+
}
|
|
16
|
+
export const channel = new ClientChannel();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ContentBody = Blob | ReadableStream<Uint8Array>;
|
|
2
|
+
/** Converts one authored value into the browser body and metadata used by storage and serving. */
|
|
3
|
+
export declare function content(value: unknown): {
|
|
4
|
+
body: ContentBody;
|
|
5
|
+
extension: string;
|
|
6
|
+
type: string;
|
|
7
|
+
};
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Converts one authored value into the browser body and metadata used by storage and serving. */
|
|
2
|
+
export function content(value) {
|
|
3
|
+
const binary = "application/octet-stream";
|
|
4
|
+
if (typeof File !== "undefined" && value instanceof File) {
|
|
5
|
+
const type = value.type || binary;
|
|
6
|
+
return { body: value, extension: extension(value.name, type), type };
|
|
7
|
+
}
|
|
8
|
+
if (value instanceof Blob) {
|
|
9
|
+
const type = value.type || binary;
|
|
10
|
+
return { body: value, extension: extension("", type), type };
|
|
11
|
+
}
|
|
12
|
+
if (value instanceof ReadableStream)
|
|
13
|
+
return { body: value, extension: "bin", type: binary };
|
|
14
|
+
if (typeof value === "string")
|
|
15
|
+
return { body: new Blob([value]), extension: "txt", type: "text/plain" };
|
|
16
|
+
if (value instanceof ArrayBuffer)
|
|
17
|
+
return { body: new Blob([value]), extension: "bin", type: binary };
|
|
18
|
+
if (ArrayBuffer.isView(value)) {
|
|
19
|
+
const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
20
|
+
return { body: new Blob([bytes.slice()]), extension: "bin", type: binary };
|
|
21
|
+
}
|
|
22
|
+
const json = JSON.stringify(value);
|
|
23
|
+
if (json === undefined)
|
|
24
|
+
throw new Error("This value cannot be written as JSON");
|
|
25
|
+
return { body: new Blob([json]), extension: "json", type: "application/json" };
|
|
26
|
+
}
|
|
27
|
+
const extensions = {
|
|
28
|
+
"application/gzip": "gz",
|
|
29
|
+
"application/javascript": "js",
|
|
30
|
+
"application/json": "json",
|
|
31
|
+
"application/pdf": "pdf",
|
|
32
|
+
"application/wasm": "wasm",
|
|
33
|
+
"application/zip": "zip",
|
|
34
|
+
"audio/mpeg": "mp3",
|
|
35
|
+
"image/gif": "gif",
|
|
36
|
+
"image/jpeg": "jpg",
|
|
37
|
+
"image/png": "png",
|
|
38
|
+
"image/svg+xml": "svg",
|
|
39
|
+
"image/webp": "webp",
|
|
40
|
+
"text/css": "css",
|
|
41
|
+
"text/csv": "csv",
|
|
42
|
+
"text/html": "html",
|
|
43
|
+
"text/javascript": "js",
|
|
44
|
+
"text/plain": "txt",
|
|
45
|
+
"video/mp4": "mp4"
|
|
46
|
+
};
|
|
47
|
+
function extension(name, type) {
|
|
48
|
+
return name.match(/\.([A-Za-z0-9]+)$/)?.[1]?.toLowerCase()
|
|
49
|
+
?? extensions[type.split(";", 1)[0].toLowerCase()]
|
|
50
|
+
?? "bin";
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Askable, Channel as CoreChannel, Publishable } from "@phreshos/core";
|
|
2
|
+
import { type Process, type Program, type Endpoint, type Window } from "./domain.js";
|
|
3
|
+
/** The paired Server as addressed from the currently executing Client. */
|
|
4
|
+
export interface CurrentServer extends Publishable, Askable {
|
|
5
|
+
exists(): Promise<boolean>;
|
|
6
|
+
start(): Promise<void>;
|
|
7
|
+
stop(): Promise<void>;
|
|
8
|
+
waitReady(timeout?: number): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** Current Client context: its inbound Channel, owner hierarchy, and paired Server. */
|
|
11
|
+
export interface Current<Events extends object = {}> extends CoreChannel<Events, Endpoint | null> {
|
|
12
|
+
readonly server: CurrentServer;
|
|
13
|
+
process(): Promise<Process>;
|
|
14
|
+
parent(): Promise<Process | null>;
|
|
15
|
+
program(): Promise<Program>;
|
|
16
|
+
window(): Promise<Window>;
|
|
17
|
+
option(name: string): Promise<string | undefined>;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare const current: Current;
|
package/dist/current.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { channel } from "./channel.js";
|
|
2
|
+
import Deadline from "./deadline.js";
|
|
3
|
+
import { process, program } from "./domain.js";
|
|
4
|
+
import wire from "./wire.js";
|
|
5
|
+
class PairedServer {
|
|
6
|
+
publish(event, payload) { wire.send("end-end", event, payload); }
|
|
7
|
+
async ask(event, payload) {
|
|
8
|
+
return this.askWithin(undefined, event, payload);
|
|
9
|
+
}
|
|
10
|
+
timeout(milliseconds) {
|
|
11
|
+
return { ask: (event, payload) => this.askWithin(milliseconds, event, payload) };
|
|
12
|
+
}
|
|
13
|
+
async exists() {
|
|
14
|
+
const answer = await wire.request(["exists", "server"]);
|
|
15
|
+
return answer[0];
|
|
16
|
+
}
|
|
17
|
+
async start() { await wire.request(["start-endpoint", undefined, "server"]); }
|
|
18
|
+
async stop() { await wire.request(["stop-endpoint", undefined, "server"]); }
|
|
19
|
+
async waitReady(timeout) { await wire.request(["wait-ready"], timeout); }
|
|
20
|
+
async askWithin(timeout, event, payload) {
|
|
21
|
+
const deadline = new Deadline(timeout);
|
|
22
|
+
await wire.requestWithin(["wait-ready", null, true], deadline);
|
|
23
|
+
return await wire.askServerWithin(event, payload, deadline);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class ClientCurrent {
|
|
27
|
+
server = new PairedServer();
|
|
28
|
+
constructor() {
|
|
29
|
+
bindChannel(this, channel);
|
|
30
|
+
}
|
|
31
|
+
async process() {
|
|
32
|
+
const answer = await wire.request(["process"]);
|
|
33
|
+
return process(answer[0]);
|
|
34
|
+
}
|
|
35
|
+
async parent() {
|
|
36
|
+
const answer = await wire.request(["parent"]);
|
|
37
|
+
return answer[0] ? process(answer[0]) : null;
|
|
38
|
+
}
|
|
39
|
+
async program() {
|
|
40
|
+
const answer = await wire.request(["program"]);
|
|
41
|
+
return program(answer[0]);
|
|
42
|
+
}
|
|
43
|
+
async window() { return (await this.process()).client.window(); }
|
|
44
|
+
async option(name) {
|
|
45
|
+
const answer = await wire.request(["option", undefined, name]);
|
|
46
|
+
return answer[0];
|
|
47
|
+
}
|
|
48
|
+
async stop() { await wire.request(["stop-current"]); }
|
|
49
|
+
}
|
|
50
|
+
function bindChannel(target, source) {
|
|
51
|
+
Object.assign(target, {
|
|
52
|
+
subscribe: source.subscribe.bind(source),
|
|
53
|
+
waitFor: source.waitFor.bind(source),
|
|
54
|
+
events: source.events.bind(source),
|
|
55
|
+
observe: source.observe.bind(source)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export const current = new ClientCurrent();
|
package/dist/deadline.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defaultTimeout } from "./events.js";
|
|
2
|
+
/** One SDK-owned deadline shared by every stage of an asynchronous operation. */
|
|
3
|
+
export default class Deadline {
|
|
4
|
+
milliseconds;
|
|
5
|
+
expiresAt;
|
|
6
|
+
constructor(milliseconds = defaultTimeout) {
|
|
7
|
+
this.milliseconds = milliseconds;
|
|
8
|
+
this.expiresAt = Date.now() + milliseconds;
|
|
9
|
+
}
|
|
10
|
+
remaining() {
|
|
11
|
+
return Math.max(0, this.expiresAt - Date.now());
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/domain.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Client as CoreClient, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, Window as CoreWindow, type AnswerCapture as CoreAnswerCapture, type AnswerMessage as CoreAnswerMessage, type AnswerObserver as CoreAnswerObserver, type AskCapture as CoreAskCapture, type AskMessage as CoreAskMessage, type AskObserver as CoreAskObserver, type ClientTraffic as CoreClientTraffic, type ClientDeclaration, type EndpointTraffic as CoreEndpointTraffic, type EndpointDeclaration, type Exit, type Launch, type Position, type ServerTraffic as CoreServerTraffic, type Size, type TrafficMessage as CoreTrafficMessage, type TrafficCapture as CoreTrafficCapture, type TrafficEvents as CoreTrafficEvents } from "@phreshos/core";
|
|
2
|
+
import Events from "./events.js";
|
|
3
|
+
export interface ProgramRecord {
|
|
4
|
+
identity: string;
|
|
5
|
+
installed?: boolean;
|
|
6
|
+
name: string;
|
|
7
|
+
version: string | null;
|
|
8
|
+
description: string | null;
|
|
9
|
+
icons?: boolean;
|
|
10
|
+
server: EndpointDeclaration | null;
|
|
11
|
+
client: ClientDeclaration | null;
|
|
12
|
+
}
|
|
13
|
+
export interface ProcessRecord {
|
|
14
|
+
identity: string;
|
|
15
|
+
name: string | null;
|
|
16
|
+
program: ProgramRecord;
|
|
17
|
+
options: Record<string, string>;
|
|
18
|
+
startedAt: string | Date;
|
|
19
|
+
server: Record<string, never> | null;
|
|
20
|
+
client: Record<string, never> | null;
|
|
21
|
+
}
|
|
22
|
+
export interface EndpointReference {
|
|
23
|
+
kind: "server" | "client";
|
|
24
|
+
process: ProcessRecord;
|
|
25
|
+
}
|
|
26
|
+
export type Program<Events extends object = {}> = Omit<CoreProgram<Events>, "processes" | "getProcess" | "createProcess"> & {
|
|
27
|
+
processes(): Promise<Process[]>;
|
|
28
|
+
getProcess(identityOrName: string): Promise<Process | null>;
|
|
29
|
+
createProcess(launch?: Launch): Promise<Process>;
|
|
30
|
+
};
|
|
31
|
+
export type Process<Events extends object = {}> = Omit<CoreProcess<Events>, "server" | "client" | "program" | "parent"> & {
|
|
32
|
+
readonly server: Server;
|
|
33
|
+
readonly client: Client;
|
|
34
|
+
program(): Program;
|
|
35
|
+
parent(): Promise<Process | null>;
|
|
36
|
+
};
|
|
37
|
+
export type TrafficMessage<Payload = unknown> = CoreTrafficMessage<Payload, Endpoint | null>;
|
|
38
|
+
export type TrafficEvents<Events extends object> = CoreTrafficEvents<Events, Endpoint | null>;
|
|
39
|
+
export type TrafficCapture<Events extends object = {}> = CoreTrafficCapture<Events, Endpoint | null>;
|
|
40
|
+
export type AskMessage<Payload = unknown> = CoreAskMessage<Payload, Server | null>;
|
|
41
|
+
export type AskCapture<Payload = unknown> = CoreAskCapture<Payload, Server | null>;
|
|
42
|
+
export type AskObserver<Payload = unknown> = CoreAskObserver<Payload, Server | null>;
|
|
43
|
+
export type AnswerMessage<Result = unknown> = CoreAnswerMessage<Result, Endpoint | null>;
|
|
44
|
+
export type AnswerCapture<Result = unknown> = CoreAnswerCapture<Result, Endpoint | null>;
|
|
45
|
+
export type AnswerObserver<Result = unknown> = CoreAnswerObserver<Result, Endpoint | null>;
|
|
46
|
+
export type EndpointTraffic<Events extends object = {}> = CoreEndpointTraffic<Events, Endpoint | null, Server | null>;
|
|
47
|
+
export type ServerTraffic<Events extends object = {}> = CoreServerTraffic<Events, Endpoint | null, Server | null>;
|
|
48
|
+
export type ClientTraffic<Events extends object = {}> = CoreClientTraffic<Events, Endpoint | null, Server | null>;
|
|
49
|
+
export type Endpoint<Events extends object = {}> = Omit<CoreEndpoint<Events>, "process" | "traffic"> & {
|
|
50
|
+
readonly traffic: EndpointTraffic<Events>;
|
|
51
|
+
process(): Process;
|
|
52
|
+
};
|
|
53
|
+
export type Server<Events extends object = {}> = Omit<CoreServer<Events>, "process" | "traffic"> & Endpoint<Events> & {
|
|
54
|
+
readonly traffic: ServerTraffic<Events>;
|
|
55
|
+
process(): Process;
|
|
56
|
+
};
|
|
57
|
+
export type Client<Events extends object = {}> = Omit<CoreClient<Events>, "process" | "traffic" | "window"> & Endpoint<Events> & {
|
|
58
|
+
readonly traffic: ClientTraffic<Events>;
|
|
59
|
+
process(): Process;
|
|
60
|
+
window(): Promise<Window>;
|
|
61
|
+
};
|
|
62
|
+
export type Window<Events extends object = {}> = Omit<CoreWindow<Events>, "client"> & {
|
|
63
|
+
client(): Client;
|
|
64
|
+
localMove(position: Position): void;
|
|
65
|
+
localResize(size: Size): void;
|
|
66
|
+
};
|
|
67
|
+
export declare function scoped(route: string, subject: string | null, convert: (event: string, values: unknown[]) => unknown): Events;
|
|
68
|
+
export declare function exit(code: unknown, signal: unknown): Exit;
|
|
69
|
+
export declare function trafficMessage(value: unknown): TrafficMessage;
|
|
70
|
+
export declare function bindEvents(target: object, events: Events): void;
|
|
71
|
+
export declare function program(record: ProgramRecord): Program;
|
|
72
|
+
export declare function process(record: ProcessRecord): Process;
|
|
73
|
+
export declare function endpoint(reference: EndpointReference | undefined): Endpoint;
|
|
74
|
+
/** Resolves only endpoint identities intentionally visible to this Client. */
|
|
75
|
+
export declare function visibleEndpoint(reference: EndpointReference | null | undefined): Endpoint | null;
|
|
76
|
+
export declare const Program: {
|
|
77
|
+
new <Events extends object = {}>(): Program<Events>;
|
|
78
|
+
};
|
|
79
|
+
export declare const Process: {
|
|
80
|
+
new <Events extends object = {}>(): Process<Events>;
|
|
81
|
+
};
|
|
82
|
+
export declare const Endpoint: {
|
|
83
|
+
new <Events extends object = {}>(): Endpoint<Events>;
|
|
84
|
+
};
|
|
85
|
+
export declare const Server: {
|
|
86
|
+
new <Events extends object = {}>(): Server<Events>;
|
|
87
|
+
};
|
|
88
|
+
export declare const Client: {
|
|
89
|
+
new <Events extends object = {}>(): Client<Events>;
|
|
90
|
+
};
|
|
91
|
+
export declare const Window: {
|
|
92
|
+
new <Events extends object = {}>(): Window<Events>;
|
|
93
|
+
};
|
package/dist/domain.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Client as CoreClient, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, Window as CoreWindow } from "@phreshos/core";
|
|
2
|
+
import Events from "./events.js";
|
|
3
|
+
import Deadline from "./deadline.js";
|
|
4
|
+
import { area, sql, store } from "./storage.js";
|
|
5
|
+
import wire from "./wire.js";
|
|
6
|
+
const ProgramBase = CoreProgram;
|
|
7
|
+
const ProcessBase = CoreProcess;
|
|
8
|
+
const ServerBase = CoreServer;
|
|
9
|
+
const ClientBase = CoreClient;
|
|
10
|
+
const WindowBase = CoreWindow;
|
|
11
|
+
class ProgramHandle extends ProgramBase {
|
|
12
|
+
identity;
|
|
13
|
+
name;
|
|
14
|
+
version;
|
|
15
|
+
description;
|
|
16
|
+
server;
|
|
17
|
+
client;
|
|
18
|
+
data = area("data");
|
|
19
|
+
cache = area("cache");
|
|
20
|
+
store = store();
|
|
21
|
+
logs = sql("logs");
|
|
22
|
+
database = sql("database");
|
|
23
|
+
constructor(record) {
|
|
24
|
+
super();
|
|
25
|
+
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));
|
|
32
|
+
}
|
|
33
|
+
async processes() {
|
|
34
|
+
const answer = await wire.request(["processes"]);
|
|
35
|
+
return answer[0].map(process);
|
|
36
|
+
}
|
|
37
|
+
async getProcess(identityOrName) {
|
|
38
|
+
const answer = await wire.request(["program-process", undefined, identityOrName]);
|
|
39
|
+
return answer[0] ? process(answer[0]) : null;
|
|
40
|
+
}
|
|
41
|
+
async createProcess(launch = {}) {
|
|
42
|
+
const answer = await wire.request(["create-process", undefined, launch]);
|
|
43
|
+
return process(answer[0]);
|
|
44
|
+
}
|
|
45
|
+
async apiDocs() {
|
|
46
|
+
const answer = await wire.request(["api-docs"]);
|
|
47
|
+
return answer[0];
|
|
48
|
+
}
|
|
49
|
+
async installed() {
|
|
50
|
+
const answer = await wire.request(["installed"]);
|
|
51
|
+
return answer[0];
|
|
52
|
+
}
|
|
53
|
+
async uninstall(everything = false) { await wire.request(["uninstall", undefined, everything]); }
|
|
54
|
+
async forget() { await wire.request(["forget"]); }
|
|
55
|
+
async exitAll() {
|
|
56
|
+
const answer = await wire.request(["exit-all"]);
|
|
57
|
+
return answer[0];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
class ProcessHandle extends ProcessBase {
|
|
61
|
+
identity;
|
|
62
|
+
name;
|
|
63
|
+
startedAt;
|
|
64
|
+
server;
|
|
65
|
+
client;
|
|
66
|
+
ownerProgram;
|
|
67
|
+
options;
|
|
68
|
+
constructor(record) {
|
|
69
|
+
super();
|
|
70
|
+
this.identity = record.identity;
|
|
71
|
+
this.name = record.name;
|
|
72
|
+
this.startedAt = new Date(record.startedAt);
|
|
73
|
+
this.ownerProgram = program(record.program);
|
|
74
|
+
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));
|
|
78
|
+
}
|
|
79
|
+
program() { return this.ownerProgram; }
|
|
80
|
+
async parent() {
|
|
81
|
+
const answer = await wire.request(["parent", this.identity]);
|
|
82
|
+
return answer[0] ? process(answer[0]) : null;
|
|
83
|
+
}
|
|
84
|
+
async option(name) {
|
|
85
|
+
if (name in this.options)
|
|
86
|
+
return this.options[name];
|
|
87
|
+
const answer = await wire.request(["option", this.identity, name]);
|
|
88
|
+
return answer[0];
|
|
89
|
+
}
|
|
90
|
+
async exit() { await wire.request(["exit", this.identity]); }
|
|
91
|
+
async exited() {
|
|
92
|
+
const answer = await wire.request(["exited", this.identity]);
|
|
93
|
+
return answer[0];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
class TrafficHandle extends Events {
|
|
97
|
+
owner;
|
|
98
|
+
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) => {
|
|
101
|
+
if (typeof event === "string")
|
|
102
|
+
observer(event, trafficMessage(value));
|
|
103
|
+
}));
|
|
104
|
+
this.owner = owner;
|
|
105
|
+
this.kind = kind;
|
|
106
|
+
}
|
|
107
|
+
observeAsks(observer) {
|
|
108
|
+
return wire.observe(this.owner.identity, this.kind, "ask", null, (event, questionId, value) => {
|
|
109
|
+
if (typeof event !== "string" || typeof questionId !== "string")
|
|
110
|
+
return;
|
|
111
|
+
observer({ event, questionId, message: trafficMessage(value) });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
class ServerTrafficHandle extends TrafficHandle {
|
|
116
|
+
observeAnswers(observer) {
|
|
117
|
+
return wire.observe(this.owner.identity, "server", "answer", null, (event, questionId, value) => {
|
|
118
|
+
if (typeof event !== "string" || typeof questionId !== "string")
|
|
119
|
+
return;
|
|
120
|
+
const raw = value;
|
|
121
|
+
observer({ event, questionId, message: { to: visibleEndpoint(raw.to), outcome: raw.outcome } });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
class ServerHandle extends ServerBase {
|
|
126
|
+
owner;
|
|
127
|
+
traffic;
|
|
128
|
+
constructor(owner) {
|
|
129
|
+
super();
|
|
130
|
+
this.owner = owner;
|
|
131
|
+
this.traffic = new ServerTrafficHandle(owner, "server");
|
|
132
|
+
}
|
|
133
|
+
process() { return this.owner; }
|
|
134
|
+
publish(event, payload) { wire.send("end-host", "send", this.owner.identity, "server", event, payload); }
|
|
135
|
+
async exists() {
|
|
136
|
+
const answer = await wire.request(["exists", "server", this.owner.identity]);
|
|
137
|
+
return answer[0];
|
|
138
|
+
}
|
|
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) {
|
|
143
|
+
return this.askWithin(undefined, event, payload);
|
|
144
|
+
}
|
|
145
|
+
timeout(milliseconds) {
|
|
146
|
+
return { ask: (event, payload) => this.askWithin(milliseconds, event, payload) };
|
|
147
|
+
}
|
|
148
|
+
async askWithin(timeout, event, payload) {
|
|
149
|
+
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()}`;
|
|
153
|
+
const questionId = crypto.randomUUID();
|
|
154
|
+
const waiting = wire.expectWithin(address, deadline);
|
|
155
|
+
wire.send("end-host", "ask", this.owner.identity, "server", address, questionId, event, payload);
|
|
156
|
+
try {
|
|
157
|
+
return await waiting;
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
wire.forget(address);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
class ClientHandle extends ClientBase {
|
|
165
|
+
owner;
|
|
166
|
+
traffic;
|
|
167
|
+
constructor(owner) {
|
|
168
|
+
super();
|
|
169
|
+
this.owner = owner;
|
|
170
|
+
this.traffic = new TrafficHandle(owner, "client");
|
|
171
|
+
}
|
|
172
|
+
process() { return this.owner; }
|
|
173
|
+
publish(event, payload) { wire.send("end-host", "send", this.owner.identity, "client", event, payload); }
|
|
174
|
+
async exists() {
|
|
175
|
+
const answer = await wire.request(["exists", "client", this.owner.identity]);
|
|
176
|
+
return answer[0];
|
|
177
|
+
}
|
|
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
|
+
}
|
|
184
|
+
}
|
|
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]));
|
|
191
|
+
}
|
|
192
|
+
client() { return this.owner; }
|
|
193
|
+
async state() {
|
|
194
|
+
const answer = await wire.request(["window", this.owner.process().identity]);
|
|
195
|
+
return answer[0];
|
|
196
|
+
}
|
|
197
|
+
async title() { return (await this.state()).title; }
|
|
198
|
+
async position() { return (await this.state()).position; }
|
|
199
|
+
async size() { return (await this.state()).size; }
|
|
200
|
+
async minimized() { return (await this.state()).minimized; }
|
|
201
|
+
async front() { return (await this.state()).front; }
|
|
202
|
+
async layer() { return (await this.state()).layer; }
|
|
203
|
+
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]); }
|
|
211
|
+
}
|
|
212
|
+
export function scoped(route, subject, convert) {
|
|
213
|
+
return new Events((event, listener, impossible) => wire.on(route, event, (...values) => {
|
|
214
|
+
const message = unscoped(subject, values);
|
|
215
|
+
if (message)
|
|
216
|
+
listener(convert(event, message));
|
|
217
|
+
}, subject, impossible), observer => wire.onAll(route, (event, ...values) => {
|
|
218
|
+
if (typeof event !== "string")
|
|
219
|
+
return;
|
|
220
|
+
const message = unscoped(subject, values);
|
|
221
|
+
if (message)
|
|
222
|
+
observer(event, convert(event, message));
|
|
223
|
+
}, subject));
|
|
224
|
+
}
|
|
225
|
+
function unscoped(subject, values) {
|
|
226
|
+
if (subject === null)
|
|
227
|
+
return values;
|
|
228
|
+
return values[0] === subject ? values.slice(1) : null;
|
|
229
|
+
}
|
|
230
|
+
function programEvent(event, values) {
|
|
231
|
+
if (event === "serverStart" || event === "clientStart" || event === "clientStop" || event === "processCreate")
|
|
232
|
+
return process(values[0]);
|
|
233
|
+
if (event === "serverStop")
|
|
234
|
+
return { process: process(values[0]), code: numberOrNull(values[1]), signal: stringOrNull(values[2]) };
|
|
235
|
+
if (event === "processExit")
|
|
236
|
+
return { process: process(values[0]), ...exit(values[1], values[2]) };
|
|
237
|
+
if (event === "uninstall")
|
|
238
|
+
return { everythingRemoved: values[0] === true };
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
function processEvent(event, values) {
|
|
242
|
+
if (event === "serverStop")
|
|
243
|
+
return { code: numberOrNull(values[0]), signal: stringOrNull(values[1]) };
|
|
244
|
+
if (event === "exit")
|
|
245
|
+
return exit(values[0], values[1]);
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
export function exit(code, signal) {
|
|
249
|
+
const namedSignal = stringOrNull(signal);
|
|
250
|
+
return { status: namedSignal === null ? "exited" : "signaled", code: numberOrNull(code), signal: namedSignal };
|
|
251
|
+
}
|
|
252
|
+
function numberOrNull(value) { return typeof value === "number" ? value : null; }
|
|
253
|
+
function stringOrNull(value) { return typeof value === "string" ? value : null; }
|
|
254
|
+
export function trafficMessage(value) {
|
|
255
|
+
const raw = value;
|
|
256
|
+
return { to: visibleEndpoint(raw.to), payload: raw.payload };
|
|
257
|
+
}
|
|
258
|
+
export function bindEvents(target, events) {
|
|
259
|
+
Object.assign(target, {
|
|
260
|
+
subscribe: events.subscribe.bind(events),
|
|
261
|
+
waitFor: events.waitFor.bind(events),
|
|
262
|
+
events: events.events.bind(events),
|
|
263
|
+
observe: events.observe.bind(events)
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
export function program(record) { return new ProgramHandle(record); }
|
|
267
|
+
export function process(record) { return new ProcessHandle(record); }
|
|
268
|
+
export function endpoint(reference) {
|
|
269
|
+
if (!reference)
|
|
270
|
+
throw new Error("The boundary returned an invalid Endpoint reference");
|
|
271
|
+
const owner = new ProcessHandle(reference.process);
|
|
272
|
+
return reference.kind === "server" ? owner.server : owner.client;
|
|
273
|
+
}
|
|
274
|
+
/** Resolves only endpoint identities intentionally visible to this Client. */
|
|
275
|
+
export function visibleEndpoint(reference) {
|
|
276
|
+
if (reference === null)
|
|
277
|
+
return null;
|
|
278
|
+
return endpoint(reference);
|
|
279
|
+
}
|
|
280
|
+
export const Program = CoreProgram;
|
|
281
|
+
export const Process = CoreProcess;
|
|
282
|
+
export const Endpoint = CoreEndpoint;
|
|
283
|
+
export const Server = CoreServer;
|
|
284
|
+
export const Client = CoreClient;
|
|
285
|
+
export const Window = CoreWindow;
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Capture, Cleanup, EventOptions } from "@phreshos/core";
|
|
2
|
+
export declare const defaultTimeout = 10000;
|
|
3
|
+
type Failure = (error: Error) => void;
|
|
4
|
+
type Listener = (message: unknown) => unknown;
|
|
5
|
+
type Observer = (event: string, message: unknown) => unknown;
|
|
6
|
+
/** SDK-owned waits, queues and callbacks over boundary forwarding state. */
|
|
7
|
+
export default class Events {
|
|
8
|
+
private readonly listen;
|
|
9
|
+
private readonly listenAll;
|
|
10
|
+
constructor(listen: (event: string, listener: Listener, impossible?: Failure) => Cleanup, listenAll: (observer: Observer) => Cleanup);
|
|
11
|
+
subscribe(event: string, subscriber: Listener): Cleanup;
|
|
12
|
+
waitFor(event: string, timeout?: number): Promise<unknown>;
|
|
13
|
+
events(event: string, options?: EventOptions): AsyncIterableIterator<unknown>;
|
|
14
|
+
observe(observer: (capture: Capture<string, unknown>) => unknown): Cleanup;
|
|
15
|
+
}
|
|
16
|
+
export {};
|