@phreshos/node 0.1.13 → 0.1.15

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.
@@ -0,0 +1,340 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const maximumStreamQueue = 256;
3
+ /** A connection-owned, live representation of the authoritative System model. */
4
+ export default class SystemRepresentation {
5
+ connection;
6
+ authorization;
7
+ programs = new Map();
8
+ processes = new Map();
9
+ appearance;
10
+ listeners = new Map();
11
+ release = [];
12
+ constructor(connection) {
13
+ this.connection = connection;
14
+ const session = ownerSession(connection.session);
15
+ this.authorization = session.authorization;
16
+ this.appearance = session.linkManager.appearance.value;
17
+ for (const [, value] of session.authManager.programManager.programs) {
18
+ const program = programState(value);
19
+ this.programs.set(program.identity, program);
20
+ }
21
+ for (const [, value] of session.authManager.processManager.processes) {
22
+ const process = processState(value);
23
+ this.processes.set(process.identity, process);
24
+ }
25
+ this.followModel(session.linkManager.appearance.key);
26
+ }
27
+ activate() { this.connection.activate(); }
28
+ close() {
29
+ for (const stop of this.release.splice(0))
30
+ stop();
31
+ this.listeners.clear();
32
+ }
33
+ call(event, ...values) {
34
+ return this.connection.call(`/auth${event}`, this.authorization, ...values);
35
+ }
36
+ on(event, listener) {
37
+ const listeners = this.listeners.get(event) ?? new Set();
38
+ listeners.add(listener);
39
+ this.listeners.set(event, listeners);
40
+ return () => {
41
+ listeners.delete(listener);
42
+ if (!listeners.size)
43
+ this.listeners.delete(event);
44
+ };
45
+ }
46
+ follow(observation, listener, impossible) {
47
+ const subscription = randomUUID();
48
+ let active = true;
49
+ const stopEvent = this.connection.subscribe("/auth/process/followed", (received, event, value) => {
50
+ if (active && received === subscription && typeof event === "string")
51
+ listener(event, value);
52
+ });
53
+ const stopImpossible = this.connection.subscribe("/auth/process/impossible", (received, reason) => {
54
+ if (!active || received !== subscription)
55
+ return;
56
+ stop();
57
+ impossible?.(new Error(String(reason)));
58
+ });
59
+ const stop = () => {
60
+ if (!active)
61
+ return;
62
+ active = false;
63
+ stopEvent();
64
+ stopImpossible();
65
+ void this.call("/process/unfollow", subscription).catch(() => undefined);
66
+ };
67
+ void this.call("/process/follow", subscription, observation).catch(error => {
68
+ if (!active)
69
+ return;
70
+ stop();
71
+ impossible?.(exception(error));
72
+ });
73
+ return stop;
74
+ }
75
+ /** Stream one Program command through the live LinkManager connection. */
76
+ command(operation, subject, value, signal) {
77
+ const stream = randomUUID();
78
+ const queue = [];
79
+ let wake = null;
80
+ let ended = false;
81
+ let failure = null;
82
+ const stopOutput = this.connection.subscribe("/auth/program/command-output", (received, output) => {
83
+ if (received !== stream || ended)
84
+ return;
85
+ if (!record(output))
86
+ failure = new Error("The System returned an invalid Program command event");
87
+ else if (queue.length >= maximumStreamQueue)
88
+ failure = new Error(`System stream queue exceeded its capacity of ${maximumStreamQueue}`);
89
+ else
90
+ queue.push(output);
91
+ wake?.();
92
+ wake = null;
93
+ });
94
+ const cancel = () => { void this.call("/program/command-cancel", stream).catch(() => undefined); };
95
+ const abort = () => {
96
+ failure = abortReason(signal);
97
+ cancel();
98
+ wake?.();
99
+ wake = null;
100
+ };
101
+ if (signal?.aborted) {
102
+ failure = abortReason(signal);
103
+ ended = true;
104
+ }
105
+ else
106
+ signal?.addEventListener("abort", abort, { once: true });
107
+ const running = ended
108
+ ? Promise.resolve()
109
+ : this.call("/program/command", stream, operation, subject, value, "").then(() => { ended = true; wake?.(); wake = null; }, error => { failure = signal?.aborted ? abortReason(signal) : exception(error); ended = true; wake?.(); wake = null; });
110
+ return (async function* () {
111
+ try {
112
+ while (true) {
113
+ if (queue.length) {
114
+ yield queue.shift();
115
+ continue;
116
+ }
117
+ if (failure)
118
+ throw failure;
119
+ if (ended)
120
+ return;
121
+ await new Promise(resolve => { wake = resolve; });
122
+ }
123
+ }
124
+ finally {
125
+ stopOutput();
126
+ signal?.removeEventListener("abort", abort);
127
+ if (!ended)
128
+ cancel();
129
+ await running.catch(() => undefined);
130
+ }
131
+ })();
132
+ }
133
+ followModel(appearance) {
134
+ const subscribe = (event, listener) => this.release.push(this.connection.subscribe(event, listener));
135
+ subscribe(`property-update:${appearance}`, value => {
136
+ this.appearance = value;
137
+ this.emit("appearance", this.appearance);
138
+ });
139
+ subscribe("/auth/program/create", value => this.arriveProgram("create", value));
140
+ subscribe("/auth/program/install", value => this.arriveProgram("install", value));
141
+ subscribe("/auth/program/uninstall", (value, everything) => this.uninstallProgram(value, everything === true));
142
+ subscribe("/auth/program/forget", value => this.forgetProgram(value));
143
+ subscribe("/auth/process/created", value => this.createProcess(value));
144
+ subscribe("/auth/process/server-ready", identity => this.serverReady(identity));
145
+ subscribe("/auth/process/server-start", (identity, value) => this.changeEndpoint(identity, "server", value));
146
+ subscribe("/auth/process/server-stop", (identity, value) => this.changeEndpoint(identity, "server", value, false));
147
+ subscribe("/auth/process/client-start", (identity, value) => this.changeEndpoint(identity, "client", value));
148
+ subscribe("/auth/process/client-stop", (identity, value) => this.changeEndpoint(identity, "client", value, false));
149
+ subscribe("/auth/process/client-access", (identity, value) => this.changeEndpoint(identity, "client", value));
150
+ subscribe("/auth/process/exited", (value, code, signal) => this.exitProcess(value, code, signal));
151
+ for (const event of ["move", "resize", "geometry", "change-title", "raise", "minimize"]) {
152
+ subscribe(`/auth/process/${event}`, value => this.changeWindow(event, value));
153
+ }
154
+ }
155
+ arriveProgram(event, value) {
156
+ const program = programState(value);
157
+ this.programs.set(program.identity, program);
158
+ this.emit(`program:${program.reference}:change`, program);
159
+ this.emit(`program:${event}`, program);
160
+ }
161
+ uninstallProgram(value, everything) {
162
+ const program = programState(value);
163
+ this.programs.set(program.identity, program);
164
+ this.emit(`program:${program.reference}:change`, program);
165
+ this.emit(`program:${program.reference}:uninstall`, everything);
166
+ this.emit("program:uninstall", program, everything);
167
+ }
168
+ forgetProgram(value) {
169
+ if (typeof value !== "string")
170
+ return;
171
+ const program = this.programs.get(value);
172
+ if (!program)
173
+ return;
174
+ this.programs.delete(value);
175
+ this.emit(`program:${program.reference}:forget`);
176
+ this.emit("program:forget", program);
177
+ }
178
+ createProcess(value) {
179
+ const process = processState(value);
180
+ this.processes.set(process.identity, process);
181
+ this.emit("process:create", process);
182
+ this.emit(`program:${process.program}:process:create`, process);
183
+ }
184
+ serverReady(value) {
185
+ if (typeof value !== "string")
186
+ return;
187
+ const process = this.processes.get(value);
188
+ if (!process?.server)
189
+ return;
190
+ process.server.ready = true;
191
+ this.emit(`endpoint:${process.reference}:server:ready`);
192
+ }
193
+ changeEndpoint(identity, endpoint, value, running = true) {
194
+ if (typeof identity !== "string")
195
+ return;
196
+ const current = this.processes.get(identity);
197
+ if (!current)
198
+ return;
199
+ const incoming = processState(value);
200
+ current.server = incoming.server;
201
+ current.client = incoming.client;
202
+ this.emit(`endpoint:${current.reference}:${endpoint}:${running ? "start" : "stop"}`);
203
+ this.emit(`process:${current.reference}:change`, current);
204
+ }
205
+ exitProcess(value, code, signal) {
206
+ const received = processState(value);
207
+ const process = this.processes.get(received.identity) ?? received;
208
+ this.processes.delete(process.identity);
209
+ const exit = {
210
+ status: typeof signal === "string" ? "signaled" : "exited",
211
+ code: typeof code === "number" ? code : null,
212
+ signal: typeof signal === "string" ? signal : null
213
+ };
214
+ this.emit(`process:${process.reference}:exit`, exit);
215
+ this.emit("process:exit", process, exit);
216
+ this.emit(`program:${process.program}:process:exit`, process, exit);
217
+ }
218
+ changeWindow(event, value) {
219
+ if (!record(value) || typeof value.identity !== "string" || !record(value.window))
220
+ return;
221
+ const process = this.processes.get(value.identity);
222
+ if (!process?.client)
223
+ return;
224
+ process.client.window = windowState(value.window);
225
+ this.emit(`window:${process.identity}:${camel(event)}`, windowMessage(event, process));
226
+ this.emit(`process:${process.reference}:change`, process);
227
+ }
228
+ emit(event, ...values) {
229
+ for (const listener of this.listeners.get(event) ?? [])
230
+ listener(...values);
231
+ }
232
+ }
233
+ function ownerSession(value) {
234
+ if (!record(value) || typeof value.authorization !== "string")
235
+ throw new Error("The System Gateway returned an invalid owner session");
236
+ const linkManager = value.linkManager;
237
+ const authManager = value.authManager;
238
+ if (!record(linkManager) || !record(linkManager.appearance) || typeof linkManager.appearance.key !== "string")
239
+ throw new Error("The System Gateway returned invalid Appearance state");
240
+ if (!record(authManager) || !record(authManager.programManager) || !record(authManager.processManager))
241
+ throw new Error("The System Gateway returned an invalid System state");
242
+ const programs = authManager.programManager.programs;
243
+ const processes = authManager.processManager.processes;
244
+ if (!Array.isArray(programs) || !Array.isArray(processes))
245
+ throw new Error("The System Gateway returned invalid domain collections");
246
+ return {
247
+ authorization: value.authorization,
248
+ linkManager: { appearance: { key: linkManager.appearance.key, value: linkManager.appearance.value } },
249
+ authManager: {
250
+ programManager: { programs: programs },
251
+ processManager: { processes: processes }
252
+ }
253
+ };
254
+ }
255
+ function programState(value) {
256
+ if (!record(value) || typeof value.reference !== "string" || typeof value.identity !== "string" || typeof value.assetId !== "string" || typeof value.name !== "string") {
257
+ throw new Error("The System returned an invalid Program");
258
+ }
259
+ return {
260
+ reference: value.reference,
261
+ identity: value.identity,
262
+ assetId: value.assetId,
263
+ installed: value.installed === true,
264
+ name: value.name,
265
+ version: typeof value.version === "string" ? value.version : null,
266
+ description: typeof value.description === "string" ? value.description : null,
267
+ hasAgent: value.hasAgent === true,
268
+ server: value.server,
269
+ client: value.client
270
+ };
271
+ }
272
+ function processState(value) {
273
+ const identity = processIdentityState(value);
274
+ const source = value;
275
+ return {
276
+ ...identity,
277
+ server: record(source.server) ? { ready: source.server.ready === true, service: source.server.service === true } : null,
278
+ client: record(source.client) ? {
279
+ service: source.client.service === true,
280
+ sameOrigin: source.client.sameOrigin === true,
281
+ window: windowState(source.client.window)
282
+ } : null
283
+ };
284
+ }
285
+ /** Read the immutable facts needed to retain one Process handle. */
286
+ export function processIdentityState(value) {
287
+ if (!record(value) || typeof value.reference !== "string" || typeof value.identity !== "string" || !record(value.options)) {
288
+ throw new Error("The System returned an invalid Process");
289
+ }
290
+ const program = typeof value.program === "string"
291
+ ? value.program
292
+ : record(value.program) && typeof value.program.identity === "string"
293
+ ? value.program.identity
294
+ : null;
295
+ const startedAt = value.startedAt instanceof Date ? value.startedAt : new Date(String(value.startedAt));
296
+ if (program === null)
297
+ throw new Error("The System returned an invalid Process owner");
298
+ if (Number.isNaN(startedAt.getTime()))
299
+ throw new Error("The System returned an invalid Process start time");
300
+ return {
301
+ reference: value.reference,
302
+ identity: value.identity,
303
+ name: typeof value.name === "string" ? value.name : null,
304
+ program,
305
+ options: value.options,
306
+ startedAt
307
+ };
308
+ }
309
+ function windowState(value) {
310
+ if (!record(value) || typeof value.title !== "string" || typeof value.location !== "string" || typeof value.depth !== "number" || typeof value.minimized !== "boolean") {
311
+ throw new Error("The System returned an invalid Window");
312
+ }
313
+ return {
314
+ title: value.title,
315
+ position: value.position,
316
+ size: value.size,
317
+ depth: value.depth,
318
+ minimized: value.minimized,
319
+ layer: value.layer,
320
+ location: value.location
321
+ };
322
+ }
323
+ function windowMessage(event, process) {
324
+ const window = process.client.window;
325
+ if (event === "move")
326
+ return window.position;
327
+ if (event === "resize")
328
+ return window.size;
329
+ if (event === "geometry")
330
+ return { position: window.position, size: window.size };
331
+ if (event === "change-title")
332
+ return window.title;
333
+ if (event === "minimize")
334
+ return window.minimized;
335
+ return true;
336
+ }
337
+ function camel(value) { return value === "change-title" ? "changeTitle" : value; }
338
+ function record(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
339
+ function exception(error) { return error instanceof Error ? error : new Error(String(error)); }
340
+ function abortReason(signal) { return signal.reason instanceof Error ? signal.reason : new Error("The operation was cancelled"); }
package/dist/system.d.ts CHANGED
@@ -14,6 +14,7 @@ export declare class System implements CoreSystem {
14
14
  readonly process: SystemProcess;
15
15
  readonly uploads: SystemUploads;
16
16
  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
17
+ websocket(url: string | URL, protocols?: string | string[]): Promise<WebSocket>;
17
18
  shell(command: string, options?: ShellOptions): AsyncGenerator<import("@phreshos/core").ShellEvent, void, void>;
18
19
  private constructor();
19
20
  /** Connect to the System selected by argument, environment, or owner default. */