@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.
package/dist/system.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, isServiceKey } from "@phreshos/core";
1
+ import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, isServiceKey, parseClientPermissions } from "@phreshos/core";
2
2
  import { homedir } from "node:os";
3
3
  import { gatewayAddress } from "./address.js";
4
4
  import Events from "./events.js";
@@ -7,10 +7,13 @@ import { resolveHome } from "./home.js";
7
7
  import { filesystemStorage, nativeStorage } from "./storage.js";
8
8
  import { programPermissions, programSql, programStore } from "./program-resources.js";
9
9
  import { EndpointTrafficHandle, ServerTrafficHandle } from "./traffic.js";
10
- import { openConnection, request, stream } from "./transport.js";
10
+ import SystemRepresentation, { processIdentityState } from "./representation.js";
11
+ import { GatewayConnection, openConnection } from "./transport.js";
11
12
  import Uploads from "./uploads.js";
12
13
  import shell from "./shell.js";
14
+ import websocket from "./websocket.js";
13
15
  const systems = new WeakMap();
16
+ const processSnapshots = new WeakMap();
14
17
  const ProgramBase = CoreProgram;
15
18
  const ProcessBase = CoreProcess;
16
19
  const ServerEndpointBase = CoreServerEndpoint;
@@ -26,43 +29,40 @@ export class System {
26
29
  const request = new Request(input, init);
27
30
  return await fetch(request, { signal: connectedSignal(this, request.signal) });
28
31
  }
32
+ websocket(url, protocols) {
33
+ return websocket(url, protocols, connectedSignal(this));
34
+ }
29
35
  async *shell(command, options = {}) {
30
36
  yield* shell(command, { ...options, signal: connectedSignal(this, options.signal) });
31
37
  }
32
- constructor(address, connection) {
38
+ constructor(connection) {
33
39
  const lifetime = new AbortController();
34
40
  const handles = new HandleRegistry();
35
- const transport = {
36
- control: (value, signal) => request(address, "system", value, connectedSignal(this, signal)),
37
- api: (value, signal) => request(address, "api", value, connectedSignal(this, signal)),
38
- stream: (target, value, signal) => stream(address, target, value, connectedSignal(this, signal))
39
- };
40
- systems.set(this, { address, connection, handles, lifetime, transport, closed: false });
41
- connection.once("close", () => closeSystem(this, new Error("This System connection is closed")));
41
+ const representation = new SystemRepresentation(connection);
42
+ systems.set(this, { connection, handles, lifetime, representation, closed: false });
43
+ connection.onDisconnect(() => void closeSystem(this, new Error("This System connection is closed")));
42
44
  this.storage = nativeStorage(homedir(), "the native filesystem", () => connectedSignal(this));
43
- this.appearance = new SystemAppearance(transport);
45
+ this.appearance = new SystemAppearance(this);
44
46
  this.program = new ProgramRegistry(this);
45
47
  this.process = new ProcessRegistry(this);
46
- this.uploads = new Uploads(value => transport.api(value), () => connectedSignal(this));
48
+ this.uploads = new Uploads(value => uploadRequest(this, value), () => connectedSignal(this));
49
+ representation.activate();
47
50
  }
48
51
  /** Connect to the System selected by argument, environment, or owner default. */
49
52
  static async connect(home) {
50
53
  const resolved = resolveHome(home);
51
54
  const address = gatewayAddress(resolved);
52
- return new System(address, await openConnection(address));
55
+ return new System(await openConnection(address));
53
56
  }
54
57
  /** Atomically replace one runtime Program without touching its installed form. */
55
58
  async forceCreateProgram(source) {
56
59
  requireConnected(this);
57
- for await (const event of transport(this).stream("program", { word: "force-create", program: source })) {
58
- if (event.event === "created")
59
- return programHandle(this, required(event.program));
60
- }
61
- throw new Error("The System did not confirm the created Program");
60
+ const identity = await representation(this).call("/program/force-create-program", source, "");
61
+ return programHandle(this, required(representation(this).programs.get(identity), identity));
62
62
  }
63
63
  /** Close this owner connection and abort every attached operation it owns. */
64
64
  async disconnect() {
65
- closeSystem(this, new Error("This System connection is closed"));
65
+ await closeSystem(this, new Error("This System connection is closed"));
66
66
  }
67
67
  service(key) {
68
68
  requireConnected(this);
@@ -88,11 +88,12 @@ function systemState(system) {
88
88
  function closeSystem(system, reason) {
89
89
  const state = systemState(system);
90
90
  if (state.closed)
91
- return;
91
+ return Promise.resolve();
92
92
  state.closed = true;
93
93
  state.lifetime.abort(reason);
94
- state.connection.destroy();
94
+ state.representation.close();
95
95
  state.handles.clear();
96
+ return state.connection.disconnect();
96
97
  }
97
98
  function requireConnected(system) {
98
99
  if (systemState(system).closed)
@@ -103,9 +104,9 @@ function connectedSignal(system, signal) {
103
104
  requireConnected(system);
104
105
  return signal ? AbortSignal.any([signal, lifetime]) : lifetime;
105
106
  }
106
- function transport(system) {
107
+ function representation(system) {
107
108
  requireConnected(system);
108
- return systemState(system).transport;
109
+ return systemState(system).representation;
109
110
  }
110
111
  function programHandle(system, snapshot) {
111
112
  const handle = systemState(system).handles.obtain(`program:${snapshot.reference}`, () => new ProgramHandle(system, snapshot));
@@ -116,65 +117,45 @@ function processHandle(system, snapshot) {
116
117
  return systemState(system).handles.obtain(`process:${snapshot.reference}`, () => new ProcessHandle(system, snapshot));
117
118
  }
118
119
  class SystemAppearance extends Events {
119
- transport;
120
- constructor(transport) {
121
- super(["change"], (_event, signal) => transport.api({ capability: "appearance", operation: "wait" }, signal));
122
- this.transport = transport;
120
+ system;
121
+ constructor(system) {
122
+ super(["change"], (_event, subscriber) => representation(system).on("appearance", subscriber));
123
+ this.system = system;
123
124
  }
124
125
  async snapshot() {
125
- return await this.transport.api({ capability: "appearance", operation: "snapshot" });
126
+ return representation(this.system).appearance;
126
127
  }
127
128
  async update(appearance) {
128
- await this.transport.api({ capability: "appearance", operation: "update", value: appearance });
129
+ await representation(this.system).call("/appearance/update", appearance);
129
130
  }
130
131
  }
131
132
  class ProgramRegistry extends Events {
132
133
  system;
133
134
  constructor(system) {
134
- super(["create", "forget", "install", "uninstall"], (event, signal, timeout) => (transport(system).control({ capability: "program", operation: "wait", input: { event, timeout } }, signal)
135
- .then(value => this.event(value))));
135
+ super(["create", "forget", "install", "uninstall"], (event, subscriber) => {
136
+ if (event === null)
137
+ throw new Error("System Program events are named");
138
+ return representation(system).on(`program:${event}`, (...values) => subscriber(this.event(event, values)));
139
+ });
136
140
  this.system = system;
137
141
  }
138
142
  async list(onlyInstalled = false) {
139
- const programs = [];
140
- let offset = 0;
141
- while (true) {
142
- const page = await transport(this.system).control({
143
- capability: "program",
144
- operation: "list",
145
- input: { installedOnly: onlyInstalled, limit: 100, offset }
146
- });
147
- programs.push(...page.data.map(snapshot => programHandle(this.system, snapshot)));
148
- offset += page.data.length;
149
- if (!page.truncated || !page.data.length)
150
- return programs;
151
- }
143
+ return [...representation(this.system).programs.values()]
144
+ .filter(program => !onlyInstalled || program.installed)
145
+ .sort((left, right) => left.identity.localeCompare(right.identity))
146
+ .map(program => programHandle(this.system, program));
152
147
  }
153
148
  async find(identity) {
154
- try {
155
- const snapshot = await transport(this.system).control({ capability: "program", operation: "inspect", input: { program: identity } });
156
- return programHandle(this.system, snapshot);
157
- }
158
- catch (error) {
159
- if (unknown(error, "Program"))
160
- return null;
161
- throw error;
162
- }
149
+ const program = representation(this.system).programs.get(identity);
150
+ return program ? programHandle(this.system, program) : null;
163
151
  }
164
152
  async create(source) {
165
- for await (const event of transport(this.system).stream("program", { word: "create", program: source })) {
166
- if (event.event === "created")
167
- return programHandle(this.system, required(event.program));
168
- }
169
- throw new Error("The System did not confirm the created Program");
153
+ const identity = await representation(this.system).call("/program/create-program", source);
154
+ return programHandle(this.system, required(representation(this.system).programs.get(identity), identity));
170
155
  }
171
- async event(value) {
172
- const waited = value;
173
- if (waited.event === "uninstall") {
174
- const payload = waited.payload;
175
- return { program: programHandle(this.system, required(payload.program)), everything: payload.everything === true };
176
- }
177
- return programHandle(this.system, required(waited.payload));
156
+ event(event, values) {
157
+ const program = programHandle(this.system, required(values[0]));
158
+ return event === "uninstall" ? { program, everything: values[1] === true } : program;
178
159
  }
179
160
  }
180
161
  class ProgramHandle extends ProgramBase {
@@ -197,18 +178,21 @@ class ProgramHandle extends ProgramBase {
197
178
  this.reference = snapshot.reference;
198
179
  this.identity = snapshot.identity;
199
180
  const address = this.address();
200
- bindEvents(this, new Events(["forget", "uninstall"], (event, signal, timeout) => transport(system).api({
201
- capability: "program", operation: "wait", handle: address, event, timeout
202
- }, signal)));
203
- const request = (value) => transport(system).api(value);
181
+ bindEvents(this, new Events(["forget", "uninstall"], (event, subscriber) => {
182
+ if (event === null)
183
+ throw new Error("Program events are named");
184
+ return representation(system).on(`program:${this.reference}:${event}`, (...values) => subscriber(values[0]));
185
+ }));
186
+ representation(system).on(`program:${this.reference}:change`, value => this.update(value));
187
+ const call = (event, ...values) => representation(system).call(event, ...values);
204
188
  this.data = filesystemStorage(() => programStoragePath(system, address, "data"), `Program "${this.identity}" data`, () => connectedSignal(system));
205
189
  this.cache = filesystemStorage(() => programStoragePath(system, address, "cache"), `Program "${this.identity}" cache`, () => connectedSignal(system));
206
- this.store = programStore(request, address);
207
- this.logs = programSql(request, address, "logs");
208
- this.database = programSql(request, address, "database");
190
+ this.store = programStore(call, address);
191
+ this.logs = programSql(call, address, "logs");
192
+ this.database = programSql(call, address, "database");
209
193
  this.process = new ProgramProcesses(system, this);
210
194
  this.startup = new ProgramStartup(system, this);
211
- this.permissions = programPermissions(request, address);
195
+ this.permissions = programPermissions(call, address);
212
196
  }
213
197
  get name() { return this.snapshot.name; }
214
198
  get assetId() { return this.snapshot.assetId; }
@@ -230,7 +214,7 @@ class ProgramHandle extends ProgramBase {
230
214
  position: this.snapshot.client.position,
231
215
  layer: this.snapshot.client.layer,
232
216
  minimize: this.snapshot.client.minimize,
233
- permissions: Object.freeze(Object.fromEntries(Object.entries(this.snapshot.client.permissions).map(([name, values]) => [name, Object.freeze([...values])])))
217
+ permissions: parseClientPermissions(this.snapshot.client.permissions)
234
218
  }) : null;
235
219
  }
236
220
  update(snapshot) {
@@ -239,7 +223,7 @@ class ProgramHandle extends ProgramBase {
239
223
  this.snapshot = snapshot;
240
224
  }
241
225
  async icon(size = "medium") {
242
- const value = await transport(this.system).api({ capability: "program", operation: "icon", handle: this.address(), size });
226
+ const value = await representation(this.system).call("/program/icon", this.address(), size);
243
227
  if (!Array.isArray(value) || value.some(byte => typeof byte !== "number"))
244
228
  throw new Error("The System returned an invalid Program icon");
245
229
  return new Blob([Uint8Array.from(value)], { type: "image/png" });
@@ -247,27 +231,20 @@ class ProgramHandle extends ProgramBase {
247
231
  async agent() {
248
232
  if (!this.hasAgent)
249
233
  return null;
250
- const value = await transport(this.system).api({ capability: "program", operation: "agent", handle: this.address() });
234
+ const value = await representation(this.system).call("/program/agent", this.address());
251
235
  return typeof value === "string" ? value : null;
252
236
  }
253
237
  async installed() {
254
- for await (const event of transport(this.system).stream("program", { word: "installed", handle: this.address() })) {
255
- if (event.event === "installedState")
256
- return event.installed === true;
257
- }
258
- throw new Error("The System returned no Program installation state");
238
+ return this.snapshot.installed;
259
239
  }
260
- install() { return command(this.system, { word: "install-existing", handle: this.address() }); }
261
- uninstall(everything = false) { return command(this.system, { word: "uninstall-existing", handle: this.address(), everything }); }
240
+ install() { return command(this.system, "install", this.address()); }
241
+ uninstall(everything = false) { return command(this.system, "uninstall", this.address(), everything); }
262
242
  async fork(identity) {
263
- for await (const event of transport(this.system).stream("program", { word: "fork", handle: this.address(), identity })) {
264
- if (event.event === "created")
265
- return programHandle(this.system, required(event.program));
266
- }
267
- throw new Error("The System did not confirm the forked Program");
243
+ const created = await representation(this.system).call("/program/fork-program", this.address(), identity);
244
+ return programHandle(this.system, required(representation(this.system).programs.get(created), created));
268
245
  }
269
246
  async forget() {
270
- for await (const _event of transport(this.system).stream("program", { word: "forget", handle: this.address() })) { /* consume completion */ }
247
+ await representation(this.system).call("/program/forget-program", this.address(), "");
271
248
  }
272
249
  address() { return Object.freeze({ identity: this.identity, reference: this.reference }); }
273
250
  }
@@ -279,13 +256,7 @@ class ProgramStartup {
279
256
  this.program = program;
280
257
  }
281
258
  async get() {
282
- for await (const event of transport(this.system).stream("program", {
283
- word: "startup", handle: this.program.address(), operation: "get"
284
- })) {
285
- if (event.event === "startup")
286
- return event.launch;
287
- }
288
- throw new Error("The System returned no Program startup state");
259
+ return await representation(this.system).call("/program/startup", this.program.address(), "get");
289
260
  }
290
261
  async enable(launch = {}) {
291
262
  await this.change("enable", launch);
@@ -294,30 +265,25 @@ class ProgramStartup {
294
265
  await this.change("disable");
295
266
  }
296
267
  async change(operation, launch) {
297
- for await (const event of transport(this.system).stream("program", {
298
- word: "startup", handle: this.program.address(), operation, launch
299
- })) {
300
- if (event.event === "startup")
301
- return;
302
- }
303
- throw new Error("The System did not confirm the Program startup change");
268
+ await representation(this.system).call("/program/startup", this.program.address(), operation, launch);
304
269
  }
305
270
  }
306
271
  class ProgramProcesses extends Events {
307
272
  system;
308
273
  program;
309
274
  constructor(system, program) {
310
- super(["create", "exit"], (event, signal, timeout) => transport(system).api({
311
- capability: "programProcess", operation: "wait", handle: program.address(), event, timeout
312
- }, signal).then(value => programProcessEvent(system, event, value)));
275
+ super(["create", "exit"], (event, subscriber) => {
276
+ if (event === null)
277
+ throw new Error("Program Process events are named");
278
+ return representation(system).on(`program:${program.identity}:process:${event}`, (...values) => (subscriber(programProcessEvent(system, event, values))));
279
+ });
313
280
  this.system = system;
314
281
  this.program = program;
315
282
  }
316
283
  async list() {
317
- const value = await transport(this.system).api({ capability: "programProcess", operation: "list", handle: this.program.address() });
318
- if (!Array.isArray(value))
319
- throw new Error("The System returned an invalid Program Process list");
320
- return value.map(snapshot => processHandle(this.system, snapshot));
284
+ return [...representation(this.system).processes.values()]
285
+ .filter(process => process.program === this.program.identity)
286
+ .map(process => processHandle(this.system, process));
321
287
  }
322
288
  async first() { return (await this.list()).sort(chronological)[0] ?? null; }
323
289
  async last() { return (await this.list()).sort(chronological).at(-1) ?? null; }
@@ -328,13 +294,12 @@ class ProgramProcesses extends Events {
328
294
  create(launch = {}) { return this.createExact("create-process", launch); }
329
295
  async *run(launch = {}, options = {}) {
330
296
  let process = null;
331
- for await (const event of transport(this.system).stream("program", {
332
- word: "run-process",
333
- handle: this.program.address(),
334
- launch
335
- }, options.signal)) {
297
+ for await (const event of representation(this.system).command("run", this.program.address(), launch, options.signal)) {
336
298
  if (event.event === "started") {
337
- process = processHandle(this.system, required(event.process));
299
+ const identity = event.process?.identity;
300
+ if (typeof identity !== "string")
301
+ throw new Error("The System returned an invalid started Process");
302
+ process = processHandle(this.system, required(representation(this.system).processes.get(identity), identity));
338
303
  yield Object.freeze({ event: "started", process });
339
304
  }
340
305
  else if (event.event === "output") {
@@ -370,42 +335,32 @@ class ProgramProcesses extends Events {
370
335
  }
371
336
  findOrCreate(launch) { return this.createExact("find-or-create-process", launch); }
372
337
  async exitAll() {
373
- const processes = await this.list();
374
- await Promise.all(processes.map(process => process.exit()));
375
- return processes.map(process => process.identity);
338
+ return await representation(this.system).call("/process/exit-all", this.program.identity, "");
376
339
  }
377
340
  async createExact(word, launch) {
378
- for await (const event of transport(this.system).stream("program", { word, handle: this.program.address(), launch })) {
379
- if (event.event === "createdProcess")
380
- return processHandle(this.system, required(event.process));
381
- }
382
- throw new Error("The System did not confirm the created Process");
341
+ const route = word === "create-process" ? "/program/create-process" : "/program/find-or-create-process";
342
+ const identity = await representation(this.system).call(route, this.program.address(), launch, null);
343
+ return processHandle(this.system, required(representation(this.system).processes.get(identity), identity));
383
344
  }
384
345
  }
385
346
  class ProcessRegistry extends Events {
386
347
  system;
387
348
  constructor(system) {
388
- super(["create", "exit"], (event, signal, timeout) => transport(system).control({
389
- capability: "process", operation: "wait", input: { event, timeout }
390
- }, signal).then(value => processEvent(system, value)));
349
+ super(["create", "exit"], (event, subscriber) => {
350
+ if (event === null)
351
+ throw new Error("System Process events are named");
352
+ return representation(system).on(`process:${event}`, (...values) => subscriber(processEvent(system, event, values)));
353
+ });
391
354
  this.system = system;
392
355
  }
393
356
  list() { return listProcesses(this.system); }
394
357
  async find(identity) {
395
- try {
396
- const snapshot = await transport(this.system).control({ capability: "process", operation: "inspect", input: { process: identity } });
397
- return processHandle(this.system, snapshot);
398
- }
399
- catch (error) {
400
- if (unknown(error, "Process"))
401
- return null;
402
- throw error;
403
- }
358
+ const process = representation(this.system).processes.get(identity);
359
+ return process ? processHandle(this.system, process) : null;
404
360
  }
405
361
  }
406
362
  class ProcessHandle extends ProcessBase {
407
363
  system;
408
- snapshot;
409
364
  identity;
410
365
  name;
411
366
  startedAt;
@@ -414,30 +369,30 @@ class ProcessHandle extends ProcessBase {
414
369
  constructor(system, snapshot) {
415
370
  super();
416
371
  this.system = system;
417
- this.snapshot = snapshot;
418
- bindEvents(this, new Events(["exit"], (event, signal, timeout) => transport(system).control({
419
- capability: "process", operation: "wait", input: { process: snapshot.identity, event, timeout }
420
- }, signal).then(exactProcessEvent)));
372
+ processSnapshots.set(this, snapshot);
373
+ bindEvents(this, new Events(["exit"], (_event, subscriber) => (representation(system).on(`process:${snapshot.reference}:exit`, value => subscriber(value)))));
421
374
  this.identity = snapshot.identity;
422
375
  this.name = snapshot.name;
423
376
  this.startedAt = new Date(snapshot.startedAt);
424
377
  this.server = new ServerEndpointHandle(system, this);
425
378
  this.client = new ClientEndpointHandle(system, this);
426
379
  }
427
- program() { return programHandle(this.system, required(this.snapshot.programSnapshot, this.snapshot.program)); }
380
+ program() {
381
+ const snapshot = processState(this.system, this);
382
+ return programHandle(this.system, required(representation(this.system).programs.get(snapshot.program), snapshot.program));
383
+ }
428
384
  async parent() {
429
385
  if (!await this.exists())
430
386
  throw new Error(`Process "${this.identity}" no longer exists`);
431
- if (this.snapshot.parent === null)
432
- return null;
433
- const parent = await this.system.process.find(this.snapshot.parent);
434
- if (!parent)
435
- throw new Error("The parent Process no longer exists");
436
- return parent;
387
+ const value = await representation(this.system).call("/process/parent", {
388
+ identity: this.identity,
389
+ reference: processReference(this)
390
+ });
391
+ return value === null ? null : processHandle(this.system, processIdentityState(value));
437
392
  }
438
- async option(name) { return this.snapshot.options[name]; }
393
+ async option(name) { return processState(this.system, this).options[name]; }
439
394
  async exit() {
440
- await transport(this.system).control({ capability: "process", operation: "exit", input: { process: this.identity } });
395
+ await representation(this.system).call("/process/exit", this.identity);
441
396
  }
442
397
  async exited() { return await this.system.process.find(this.identity) === null; }
443
398
  async exists() { return !await this.exited(); }
@@ -448,49 +403,34 @@ class EndpointOperations extends Events {
448
403
  endpoint;
449
404
  lifecycle;
450
405
  constructor(system, owner, endpoint) {
451
- super([], (event, signal, timeout) => event === null
452
- ? transport(system).api({ capability: "endpoint", operation: "wait", process: owner.identity, endpoint, event, timeout }, signal)
453
- : transport(system).control({
454
- capability: "endpoint", operation: "wait", input: { process: owner.identity, endpoint, event, timeout }
455
- }, signal).then(value => value.payload));
406
+ super([], (event, subscriber, impossible) => representation(system).follow({
407
+ scope: "endpoint",
408
+ process: owner.identity,
409
+ endpoint,
410
+ event
411
+ }, (_received, payload) => subscriber(payload), impossible));
456
412
  this.system = system;
457
413
  this.owner = owner;
458
414
  this.endpoint = endpoint;
459
- this.lifecycle = new Events(["start", "stop"], (event, signal, timeout) => {
460
- return waitEndpointLifecycle(system, owner, endpoint, event, signal, timeout);
461
- });
415
+ this.lifecycle = new Events(["start", "stop"], (event, subscriber, impossible) => (endpointLifecycle(system, owner, endpoint, event, subscriber, impossible)));
462
416
  }
463
417
  process() { return Promise.resolve(this.owner); }
464
418
  async exists() {
465
- const value = await this.inspect();
466
- return value.running;
419
+ return endpointState(this.system, this.owner, this.endpoint) !== null;
467
420
  }
468
421
  async start(launch = {}) { await this.operation("start", launch); }
469
422
  async stop() { await this.operation("stop"); }
470
423
  async waitReady(timeout) {
471
- await transport(this.system).control({ capability: "endpoint", operation: "waitReady", input: {
472
- process: this.owner.identity, endpoint: this.endpoint, timeout
473
- } });
424
+ await waitEndpointReady(this.system, this.owner, this.endpoint, timeout);
474
425
  }
475
426
  async isService() {
476
- return await transport(this.system).api({
477
- capability: "endpoint", operation: "isService", process: this.owner.identity, endpoint: this.endpoint
478
- });
427
+ return endpointState(this.system, this.owner, this.endpoint)?.service === true;
479
428
  }
480
429
  publish(event, payload) {
481
- void transport(this.system).control({ capability: "endpoint", operation: "publish", input: {
482
- process: this.owner.identity, endpoint: this.endpoint, event, payload
483
- } });
484
- }
485
- inspect() {
486
- return transport(this.system).control({ capability: "endpoint", operation: "inspect", input: {
487
- process: this.owner.identity, endpoint: this.endpoint
488
- } });
430
+ void representation(this.system).call("/process/endpoint/publish", this.owner.identity, this.endpoint, event, payload);
489
431
  }
490
432
  async operation(operation, launch) {
491
- await transport(this.system).control({ capability: "endpoint", operation, input: {
492
- process: this.owner.identity, endpoint: this.endpoint, ...(launch ? { launch } : {})
493
- } });
433
+ await representation(this.system).call(`/process/endpoint/${operation}`, this.owner.identity, this.endpoint, launch);
494
434
  }
495
435
  }
496
436
  class ServerEndpointHandle extends ServerEndpointBase {
@@ -505,7 +445,7 @@ class ServerEndpointHandle extends ServerEndpointBase {
505
445
  this.system = system;
506
446
  this.owner = owner;
507
447
  this.base = new EndpointOperations(system, owner, "server");
508
- this.traffic = new ServerTrafficHandle((value, signal) => transport(system).api(value, signal), owner.identity, "server", value => endpointFromReference(system, value));
448
+ this.traffic = new ServerTrafficHandle(representation(system), owner.identity, "server", value => endpointFromReference(system, value));
509
449
  this.lifecycle = this.base.lifecycle;
510
450
  bindEvents(this, this.base);
511
451
  }
@@ -517,14 +457,13 @@ class ServerEndpointHandle extends ServerEndpointBase {
517
457
  stop() { return this.base.stop(); }
518
458
  publish(event, payload) { return this.base.publish(event, payload); }
519
459
  async ask(event, payload) {
520
- return await transport(this.system).control({ capability: "endpoint", operation: "ask", input: {
521
- process: this.owner.identity, endpoint: "server", event, payload
522
- } });
460
+ return await this.askWithin(event, payload, 10_000);
523
461
  }
524
462
  timeout(milliseconds) {
525
- return { ask: (event, payload) => transport(this.system).control({
526
- capability: "endpoint", operation: "ask", input: { process: this.owner.identity, endpoint: "server", event, payload, timeout: milliseconds }
527
- }) };
463
+ return { ask: (event, payload) => this.askWithin(event, payload, milliseconds) };
464
+ }
465
+ askWithin(event, payload, timeout) {
466
+ return representation(this.system).call("/process/endpoint/ask", this.owner.identity, event, payload, timeout);
528
467
  }
529
468
  }
530
469
  class ClientEndpointHandle extends ClientEndpointBase {
@@ -536,7 +475,7 @@ class ClientEndpointHandle extends ClientEndpointBase {
536
475
  constructor(system, owner) {
537
476
  super();
538
477
  this.base = new EndpointOperations(system, owner, "client");
539
- this.traffic = new EndpointTrafficHandle((value, signal) => transport(system).api(value, signal), owner.identity, "client", value => endpointFromReference(system, value));
478
+ this.traffic = new EndpointTrafficHandle(representation(system), owner.identity, "client", value => endpointFromReference(system, value));
540
479
  this.lifecycle = this.base.lifecycle;
541
480
  bindEvents(this, this.base);
542
481
  this.window = new SystemWindow(system, owner);
@@ -553,9 +492,11 @@ class SystemWindow extends Events {
553
492
  system;
554
493
  process;
555
494
  constructor(system, process) {
556
- super(["move", "resize", "geometry", "minimize", "changeTitle", "front"], (event, signal, timeout) => transport(system).control({
557
- capability: "window", operation: "wait", input: { process: process.identity, event, timeout }
558
- }, signal).then(value => value.payload));
495
+ super(["move", "resize", "geometry", "minimize", "changeTitle", "front"], (event, subscriber) => {
496
+ if (event === null)
497
+ throw new Error("Window events are named");
498
+ return representation(system).on(`window:${process.identity}:${event}`, subscriber);
499
+ });
559
500
  this.system = system;
560
501
  this.process = process;
561
502
  }
@@ -563,20 +504,23 @@ class SystemWindow extends Events {
563
504
  async position() { return (await this.snapshot()).position; }
564
505
  async size() { return (await this.snapshot()).size; }
565
506
  async minimized() { return (await this.snapshot()).minimized; }
566
- async front() { return (await this.snapshot()).front; }
507
+ async front() { return frontWindow(this.system, this.process); }
567
508
  async layer() { return (await this.snapshot()).layer; }
568
509
  async location() { return (await this.snapshot()).location; }
569
- async move(position) { await this.change("move", { position }); }
570
- async resize(size) { await this.change("resize", { size }); }
571
- async setGeometry(geometry) { await this.change("setGeometry", geometry); }
572
- async minimize(minimized = true) { await this.change("minimize", { minimized }); }
573
- async changeTitle(title) { await this.change("changeTitle", { title }); }
574
- async raise() { await this.change("raise", {}); }
510
+ async move(position) { await this.change("move", position); }
511
+ async resize(size) { await this.change("resize", size); }
512
+ async setGeometry(geometry) { await this.change("geometry", geometry); }
513
+ async minimize(minimized = true) { await this.change("minimize", minimized); }
514
+ async changeTitle(title) { await this.change("change-title", title); }
515
+ async raise() { await this.change("raise"); }
575
516
  snapshot() {
576
- return transport(this.system).control({ capability: "window", operation: "inspect", input: { process: this.process.identity } });
517
+ const window = processState(this.system, this.process).client?.window;
518
+ if (!window)
519
+ throw new Error(`Process "${this.process.identity}" has no live Client Endpoint`);
520
+ return Promise.resolve(window);
577
521
  }
578
522
  async change(operation, input) {
579
- await transport(this.system).control({ capability: "window", operation, input: { process: this.process.identity, ...input } });
523
+ await representation(this.system).call(`/process/${operation}`, this.process.identity, input);
580
524
  }
581
525
  }
582
526
  class ServiceBase {
@@ -586,16 +530,16 @@ class ServiceBase {
586
530
  constructor(system, key) {
587
531
  this.system = system;
588
532
  this.key = key;
589
- this.lifecycle = new Events(["start", "stop"], (event, signal, timeout) => transport(system).api({
590
- capability: "service", operation: "wait", scope: "lifecycle", key, event, timeout
591
- }, signal));
533
+ this.lifecycle = new Events(["start", "stop"], (event, subscriber, impossible) => representation(system).follow({
534
+ scope: "service", key, kind: "lifecycle", event
535
+ }, (_received, payload) => subscriber(payload), impossible));
592
536
  }
593
- async exists() { return await transport(this.system).api({ capability: "service", operation: "exists", key: this.key }); }
537
+ async exists() { return serviceState(this.system, this.key) !== null; }
594
538
  async waitReady(timeout) {
595
- await transport(this.system).api({ capability: "service", operation: "waitReady", key: this.key, timeout });
539
+ await representation(this.system).call("/process/service/wait-ready", this.key, timeout);
596
540
  }
597
541
  publish(event, payload) {
598
- void transport(this.system).api({ capability: "service", operation: "publish", key: this.key, event, payload });
542
+ void representation(this.system).call("/process/service/publish", this.key, event, payload);
599
543
  }
600
544
  }
601
545
  /** Node SDK handle for a Service provided by a Server Endpoint. */
@@ -613,20 +557,18 @@ class ServerServiceHandle extends ServerService {
613
557
  this.key = key;
614
558
  this.base = new ServiceBase(system, key);
615
559
  this.lifecycle = this.base.lifecycle;
616
- bindEvents(this, new Events([], (event, signal, timeout) => transport(system).api({
617
- capability: "service", operation: "wait", scope: "events", key, event, timeout
618
- }, signal)));
560
+ bindEvents(this, new Events([], (event, subscriber, impossible) => representation(system).follow({
561
+ scope: "service", key, kind: "events", event
562
+ }, (_received, payload) => subscriber(payload), impossible)));
619
563
  }
620
564
  exists() { return this.base.exists(); }
621
565
  waitReady(timeout) { return this.base.waitReady(timeout); }
622
566
  publish = (event, payload) => this.base.publish(event, payload);
623
567
  async ask(event, payload) {
624
- return await transport(this.system).api({ capability: "service", operation: "ask", key: this.key, event, payload });
568
+ return await representation(this.system).call("/process/service/ask", this.key, event, payload, 10_000);
625
569
  }
626
570
  timeout(milliseconds) {
627
- return { ask: (event, payload) => transport(this.system).api({
628
- capability: "service", operation: "ask", key: this.key, event, payload, timeout: milliseconds
629
- }) };
571
+ return { ask: (event, payload) => representation(this.system).call("/process/service/ask", this.key, event, payload, milliseconds) };
630
572
  }
631
573
  }
632
574
  /** Node SDK handle for a Service provided by a Client Endpoint. */
@@ -640,27 +582,21 @@ class ClientServiceHandle extends ClientService {
640
582
  super();
641
583
  this.base = new ServiceBase(system, key);
642
584
  this.lifecycle = this.base.lifecycle;
643
- bindEvents(this, new Events([], (event, signal, timeout) => transport(system).api({
644
- capability: "service", operation: "wait", scope: "events", key, event, timeout
645
- }, signal)));
585
+ bindEvents(this, new Events([], (event, subscriber, impossible) => representation(system).follow({
586
+ scope: "service", key, kind: "events", event
587
+ }, (_received, payload) => subscriber(payload), impossible)));
646
588
  }
647
589
  exists() { return this.base.exists(); }
648
590
  waitReady(timeout) { return this.base.waitReady(timeout); }
649
591
  publish = (event, payload) => this.base.publish(event, payload);
650
592
  }
651
593
  async function listProcesses(system) {
652
- const processes = [];
653
- let offset = 0;
654
- while (true) {
655
- const page = await transport(system).control({ capability: "process", operation: "list", input: { limit: 100, offset } });
656
- processes.push(...page.data.map(snapshot => processHandle(system, snapshot)));
657
- offset += page.data.length;
658
- if (!page.truncated || !page.data.length)
659
- return processes;
660
- }
661
- }
662
- async function* command(system, request) {
663
- for await (const event of transport(system).stream("program", request)) {
594
+ return [...representation(system).processes.values()]
595
+ .sort((left, right) => right.startedAt.getTime() - left.startedAt.getTime())
596
+ .map(process => processHandle(system, process));
597
+ }
598
+ async function* command(system, operation, subject, value) {
599
+ for await (const event of representation(system).command(operation, subject, value)) {
664
600
  if (event.event === "output")
665
601
  yield {
666
602
  stream: event.stream === "stderr" ? "stderr" : "stdout",
@@ -668,49 +604,21 @@ async function* command(system, request) {
668
604
  };
669
605
  }
670
606
  }
671
- async function waitEndpointLifecycle(system, owner, endpoint, event, signal, timeout = 10_000) {
607
+ function endpointLifecycle(system, owner, endpoint, event, subscriber, impossible) {
672
608
  if (event !== "start" && event !== "stop")
673
609
  throw new Error(`An Endpoint lifecycle has no "${event}" event`);
674
- await transport(system).control({
675
- capability: "endpoint",
676
- operation: "waitLifecycle",
677
- input: { process: owner.identity, endpoint, event, timeout }
678
- }, signal);
679
- }
680
- function processEvent(system, value) {
681
- const waited = value;
682
- const payload = waited.payload;
683
- if (waited.event === "exit" && payload)
684
- return {
685
- process: processHandle(system, required(payload.processSnapshot, String(payload.process ?? ""))),
686
- status: payload.status,
687
- code: payload.code,
688
- signal: payload.signal
689
- };
690
- if (payload && typeof payload.identity === "string")
691
- return processHandle(system, payload);
692
- return payload;
693
- }
694
- function programProcessEvent(system, event, value) {
695
- if (event === "create")
696
- return processHandle(system, value);
697
- const exit = value;
698
- return {
699
- process: processHandle(system, required(exit.process)),
700
- status: exit.status,
701
- code: exit.code,
702
- signal: exit.signal
703
- };
610
+ const model = representation(system);
611
+ const stopEvent = model.on(`endpoint:${processReference(owner)}:${endpoint}:${event}`, () => subscriber(undefined));
612
+ const stopExit = model.on(`process:${processReference(owner)}:exit`, () => impossible?.(new Error(`Process "${owner.identity}" exited`)));
613
+ return () => { stopEvent(); stopExit(); };
704
614
  }
705
- function exactProcessEvent(value) {
706
- const payload = value.payload;
707
- if (!payload)
708
- return payload;
709
- return {
710
- status: payload.status,
711
- code: payload.code,
712
- signal: payload.signal
713
- };
615
+ function processEvent(system, event, values) {
616
+ const process = processHandle(system, required(values[0]));
617
+ return event === "exit" ? { process, ...values[1] } : process;
618
+ }
619
+ function programProcessEvent(system, event, values) {
620
+ const process = processHandle(system, required(values[0]));
621
+ return event === "exit" ? { process, ...values[1] } : process;
714
622
  }
715
623
  function bindEvents(target, events) {
716
624
  Object.assign(target, eventsOf(events));
@@ -724,36 +632,84 @@ function eventsOf(events) {
724
632
  }
725
633
  function chronological(left, right) { return left.startedAt.getTime() - right.startedAt.getTime(); }
726
634
  async function programStoragePath(system, handle, area) {
727
- const value = await transport(system).api({ capability: "program", operation: "storagePath", handle, area });
635
+ const value = await representation(system).call("/program/area", handle, area, "path", []);
728
636
  if (typeof value !== "string")
729
637
  throw new Error("The System returned an invalid Program storage path");
730
638
  return value;
731
639
  }
732
640
  function endpointFromReference(system, value) {
733
641
  const reference = value;
734
- if (!reference || (reference.kind !== "server" && reference.kind !== "client"))
642
+ if (!reference || (reference.kind !== "server" && reference.kind !== "client") || typeof reference.process?.identity !== "string") {
735
643
  throw new Error("The System returned an invalid Endpoint reference");
736
- const owner = processHandle(system, snapshotFromReference(reference.process));
644
+ }
645
+ const owner = processHandle(system, required(representation(system).processes.get(reference.process.identity), reference.process.identity));
737
646
  return reference.kind === "server" ? owner.server : owner.client;
738
647
  }
739
- function snapshotFromReference(reference) {
740
- const owner = reference.program;
741
- if (!owner || typeof owner.reference !== "string" || typeof owner.identity !== "string")
742
- throw new Error("The System returned an invalid Process reference");
743
- return {
744
- reference: reference.reference,
745
- identity: reference.identity,
746
- name: reference.name,
747
- program: owner.identity,
748
- programSnapshot: owner,
749
- parent: null,
750
- options: reference.options,
751
- startedAt: reference.startedAt,
752
- server: { declared: owner.server !== null, running: reference.server !== null, service: reference.server?.service === true },
753
- client: { declared: owner.client !== null, running: reference.client !== null, service: reference.client?.service === true }
754
- };
648
+ function processState(system, process) {
649
+ const original = required(processSnapshots.get(process));
650
+ const current = representation(system).processes.get(process.identity);
651
+ if (!current || current.reference !== original.reference)
652
+ throw new Error(`Process "${process.identity}" no longer exists`);
653
+ return current;
654
+ }
655
+ function processReference(process) { return required(processSnapshots.get(process)).reference; }
656
+ function endpointState(system, process, endpoint) {
657
+ const current = representation(system).processes.get(process.identity);
658
+ const original = required(processSnapshots.get(process));
659
+ return current?.reference === original.reference ? current[endpoint] : null;
660
+ }
661
+ function waitEndpointReady(system, process, endpoint, timeout = 10_000) {
662
+ if (endpointReady(endpointState(system, process, endpoint), endpoint))
663
+ return Promise.resolve();
664
+ return new Promise((resolve, reject) => {
665
+ const model = representation(system);
666
+ const reference = processReference(process);
667
+ const finish = (work) => {
668
+ clearTimeout(timer);
669
+ stopStart();
670
+ stopReady();
671
+ stopExit();
672
+ work();
673
+ };
674
+ const inspect = () => {
675
+ if (endpointReady(endpointState(system, process, endpoint), endpoint))
676
+ finish(resolve);
677
+ };
678
+ const stopStart = model.on(`endpoint:${reference}:${endpoint}:start`, inspect);
679
+ const stopReady = model.on(`endpoint:${reference}:${endpoint}:ready`, inspect);
680
+ const stopExit = model.on(`process:${reference}:exit`, () => finish(() => reject(new Error(`Process "${process.identity}" exited`))));
681
+ const timer = setTimeout(() => finish(() => reject(new Error("The Endpoint did not become ready before the timeout"))), timeout);
682
+ inspect();
683
+ });
684
+ }
685
+ function endpointReady(state, endpoint) {
686
+ return state !== null && (endpoint === "client" || "ready" in state && state.ready);
687
+ }
688
+ function serviceState(system, key) {
689
+ const model = representation(system);
690
+ const process = key.program === undefined
691
+ ? model.processes.get(key.process)
692
+ : [...model.processes.values()].find(candidate => candidate.program === key.program && (candidate.identity === key.process || candidate.name === key.process));
693
+ const endpoint = process?.[key.endpoint];
694
+ return endpoint?.service === true ? endpoint : null;
695
+ }
696
+ function frontWindow(system, process) {
697
+ const window = processState(system, process).client?.window;
698
+ if (!window || window.minimized)
699
+ return false;
700
+ return ![...representation(system).processes.values()].some(candidate => {
701
+ const other = candidate.client?.window;
702
+ return other && !other.minimized && other.layer === window.layer && other.depth > window.depth;
703
+ });
704
+ }
705
+ async function uploadRequest(system, value) {
706
+ const request = value;
707
+ if (request.operation === "access")
708
+ return representation(system).call("/uploads/access");
709
+ if (request.operation === "stat")
710
+ return representation(system).call("/uploads/stat", request.file);
711
+ throw new Error(`The Uploads API does not know "${String(request.operation)}"`);
755
712
  }
756
- function unknown(error, entity) { return error instanceof Error && error.message.startsWith(`Unknown ${entity}`); }
757
713
  function required(value, identity = "") {
758
714
  if (value !== undefined)
759
715
  return value;