@phreshos/node 0.1.13 → 0.1.14
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 +31 -41
- package/dist/events.d.ts +4 -4
- package/dist/events.js +18 -29
- package/dist/main.d.ts +1 -1
- package/dist/main.js +1 -1
- package/dist/program-resources.d.ts +4 -4
- package/dist/program-resources.js +9 -24
- package/dist/representation.d.ts +100 -0
- package/dist/representation.js +331 -0
- package/dist/system.d.ts +1 -0
- package/dist/system.js +239 -281
- package/dist/traffic.d.ts +4 -4
- package/dist/traffic.js +14 -32
- package/dist/transport.d.ts +20 -10
- package/dist/transport.js +106 -124
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +15 -0
- package/package.json +4 -2
|
@@ -0,0 +1,331 @@
|
|
|
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
|
+
if (!record(value) || typeof value.reference !== "string" || typeof value.identity !== "string" || typeof value.program !== "string" || !record(value.options)) {
|
|
274
|
+
throw new Error("The System returned an invalid Process");
|
|
275
|
+
}
|
|
276
|
+
const startedAt = value.startedAt instanceof Date ? value.startedAt : new Date(String(value.startedAt));
|
|
277
|
+
if (Number.isNaN(startedAt.getTime()))
|
|
278
|
+
throw new Error("The System returned an invalid Process start time");
|
|
279
|
+
return {
|
|
280
|
+
reference: value.reference,
|
|
281
|
+
identity: value.identity,
|
|
282
|
+
name: typeof value.name === "string" ? value.name : null,
|
|
283
|
+
program: value.program,
|
|
284
|
+
parent: address(value.parent),
|
|
285
|
+
options: value.options,
|
|
286
|
+
startedAt,
|
|
287
|
+
server: record(value.server) ? { ready: value.server.ready === true, service: value.server.service === true } : null,
|
|
288
|
+
client: record(value.client) ? {
|
|
289
|
+
service: value.client.service === true,
|
|
290
|
+
sameOrigin: value.client.sameOrigin === true,
|
|
291
|
+
window: windowState(value.client.window)
|
|
292
|
+
} : null
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function windowState(value) {
|
|
296
|
+
if (!record(value) || typeof value.title !== "string" || typeof value.location !== "string" || typeof value.depth !== "number" || typeof value.minimized !== "boolean") {
|
|
297
|
+
throw new Error("The System returned an invalid Window");
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
title: value.title,
|
|
301
|
+
position: value.position,
|
|
302
|
+
size: value.size,
|
|
303
|
+
depth: value.depth,
|
|
304
|
+
minimized: value.minimized,
|
|
305
|
+
layer: value.layer,
|
|
306
|
+
location: value.location
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function address(value) {
|
|
310
|
+
if (!record(value) || typeof value.reference !== "string" || typeof value.identity !== "string")
|
|
311
|
+
return null;
|
|
312
|
+
return { reference: value.reference, identity: value.identity };
|
|
313
|
+
}
|
|
314
|
+
function windowMessage(event, process) {
|
|
315
|
+
const window = process.client.window;
|
|
316
|
+
if (event === "move")
|
|
317
|
+
return window.position;
|
|
318
|
+
if (event === "resize")
|
|
319
|
+
return window.size;
|
|
320
|
+
if (event === "geometry")
|
|
321
|
+
return { position: window.position, size: window.size };
|
|
322
|
+
if (event === "change-title")
|
|
323
|
+
return window.title;
|
|
324
|
+
if (event === "minimize")
|
|
325
|
+
return window.minimized;
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
function camel(value) { return value === "change-title" ? "changeTitle" : value; }
|
|
329
|
+
function record(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
330
|
+
function exception(error) { return error instanceof Error ? error : new Error(String(error)); }
|
|
331
|
+
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. */
|