@phreshos/node 0.1.6 → 0.1.8

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/events.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import type { Subscribable } from "@phreshos/core";
1
+ import type { Cleanup, EventOptions, Subscribable } from "@phreshos/core";
2
2
  type Wait = (event: string | null, signal: AbortSignal, timeout?: number) => Promise<unknown>;
3
+ type Failure = (error: Error) => void;
4
+ type Register<Message> = (subscriber: (message: Message) => unknown, impossible?: Failure) => Cleanup;
3
5
  /** Adapts authoritative one-event waits into the shared Subscribable contract. */
4
6
  export default class Events<Definitions extends object, Fallback = never> {
5
7
  private readonly names;
@@ -10,4 +12,5 @@ export default class Events<Definitions extends object, Fallback = never> {
10
12
  readonly events: Subscribable<Definitions, Fallback>["events"];
11
13
  private listen;
12
14
  }
15
+ export declare function stream<Message>(register: Register<Message>, options?: EventOptions): AsyncIterableIterator<Message>;
13
16
  export {};
package/dist/events.js CHANGED
@@ -57,7 +57,7 @@ export default class Events {
57
57
  return () => controller.abort();
58
58
  }
59
59
  }
60
- function stream(register, options = {}) {
60
+ export function stream(register, options = {}) {
61
61
  const capacity = options.capacity ?? 64;
62
62
  if (capacity !== Infinity && (!Number.isInteger(capacity) || capacity < 0)) {
63
63
  throw new Error("An event queue capacity must be a non-negative integer or Infinity");
package/dist/main.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { gatewayAddress } from "./address.js";
2
2
  export { Client, ClientService, Endpoint, Process, Program, Server, ServerService, System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
3
- export { Service, type ServiceKey, type ServiceLifecycle, type ServiceLifecycleEvents } from "@phreshos/core";
3
+ export { Service, type ClientLaunch, type Launch, type ProgramDefinition, type ServerLaunch, type ServiceKey, } from "@phreshos/core";
4
4
  export { resolveHome } from "./home.js";
5
5
  export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions } from "./project.js";
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { gatewayAddress } from "./address.js";
2
2
  export { Client, ClientService, Endpoint, Process, Program, Server, ServerService, System } from "./system.js";
3
- export { Service } from "@phreshos/core";
3
+ export { Service, } from "@phreshos/core";
4
4
  export { resolveHome } from "./home.js";
5
5
  export { Project } from "./project.js";
@@ -0,0 +1,7 @@
1
+ import type { ProgramSql, ProgramStore } from "@phreshos/core";
2
+ type Request = (value: object) => Promise<unknown>;
3
+ /** Program-owned key-value storage carried through the owner-local Gateway. */
4
+ export declare function programStore(request: Request, program: string): ProgramStore;
5
+ /** Program-owned SQL capability carried through the owner-local Gateway. */
6
+ export declare function programSql(request: Request, program: string, database: "database" | "logs"): ProgramSql;
7
+ export {};
@@ -0,0 +1,33 @@
1
+ /** Program-owned key-value storage carried through the owner-local Gateway. */
2
+ export function programStore(request, program) {
3
+ const operate = (storeOperation, key, value, ttl) => request({
4
+ capability: "program",
5
+ operation: "store",
6
+ program,
7
+ storeOperation,
8
+ key,
9
+ value,
10
+ ttl
11
+ });
12
+ return {
13
+ get: (key) => operate("get", key),
14
+ set: (key, value, ttl) => operate("set", key, value, ttl),
15
+ delete: (key) => operate("delete", key),
16
+ has: (key) => operate("has", key),
17
+ clear: () => operate("clear")
18
+ };
19
+ }
20
+ /** Program-owned SQL capability carried through the owner-local Gateway. */
21
+ export function programSql(request, program, database) {
22
+ return {
23
+ query(statement, ...rest) {
24
+ const [text, values] = written(statement, rest);
25
+ return request({ capability: "program", operation: "query", program, database, statement: text, values });
26
+ }
27
+ };
28
+ }
29
+ function written(statement, rest) {
30
+ if (typeof statement === "string")
31
+ return [statement, Array.isArray(rest[0]) ? rest[0] : []];
32
+ return [statement.raw.join("?"), rest];
33
+ }
package/dist/project.js CHANGED
@@ -70,6 +70,7 @@ export class Project {
70
70
  ...server && { server: {
71
71
  location: resolve(this.directory, server.location),
72
72
  start: server.start,
73
+ service: server.service,
73
74
  installCommand: config.server?.installCommand,
74
75
  uninstallCommand: config.server?.uninstallCommand,
75
76
  ...serverExecution(server)
@@ -77,6 +78,7 @@ export class Project {
77
78
  ...client && { client: {
78
79
  location: /^https?:\/\//i.test(client.location) ? client.location : resolve(this.directory, client.location),
79
80
  start: client.start,
81
+ service: client.service,
80
82
  title: config.client?.title,
81
83
  size: config.client?.size,
82
84
  position: config.client?.position,
@@ -203,6 +205,8 @@ function validateConfig(config) {
203
205
  throw new Error(`A declared ${half} half must have a location`);
204
206
  if (declared.start !== undefined && typeof declared.start !== "boolean")
205
207
  throw new Error(`A declared ${half} Endpoint's start default must be true or false`);
208
+ if (declared.service !== undefined && typeof declared.service !== "boolean")
209
+ throw new Error(`A declared ${half} Endpoint's service default must be true or false`);
206
210
  }
207
211
  if (!(config.server && (config.server.start ?? true)) && !(config.client && (config.client.start ?? true))) {
208
212
  throw new Error("A Program's default Process must start a Server Endpoint, a Client Endpoint, or both");
@@ -269,6 +273,7 @@ function packageDefinition(config, version) {
269
273
  ...config.server && { server: {
270
274
  location: "server",
271
275
  start: config.server.start,
276
+ service: config.server.service,
272
277
  installCommand: config.server.installCommand,
273
278
  uninstallCommand: config.server.uninstallCommand,
274
279
  ...serverExecution(config.server)
@@ -276,6 +281,7 @@ function packageDefinition(config, version) {
276
281
  ...config.client && { client: {
277
282
  location: "client",
278
283
  start: config.client.start,
284
+ service: config.client.service,
279
285
  title: config.client.title,
280
286
  size: config.client.size,
281
287
  position: config.client.position,
package/dist/storage.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { Storage } from "@phreshos/core";
2
- /** Create one filesystem implementation bounded beneath an absolute root. */
3
- export declare function filesystemStorage(root: string, label: string): Storage;
2
+ /** Create one filesystem implementation bounded beneath a resolved absolute root. */
3
+ export declare function filesystemStorage(source: string | (() => Promise<string>), label: string): Storage;
package/dist/storage.js CHANGED
@@ -4,13 +4,21 @@ import { rm } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, sep } from "node:path";
5
5
  import { Readable } from "node:stream";
6
6
  import { pipeline } from "node:stream/promises";
7
- /** Create one filesystem implementation bounded beneath an absolute root. */
8
- export function filesystemStorage(root, label) {
9
- if (!isAbsolute(root))
10
- throw new Error("A Storage root must be absolute");
11
- const resolve = (...parts) => contained(root, parts);
7
+ /** Create one filesystem implementation bounded beneath a resolved absolute root. */
8
+ export function filesystemStorage(source, label) {
9
+ let root = null;
10
+ const resolveRoot = () => {
11
+ if (!root)
12
+ root = Promise.resolve(typeof source === "string" ? source : source()).then(value => {
13
+ if (!isAbsolute(value))
14
+ throw new Error("A Storage root must be absolute");
15
+ return value;
16
+ });
17
+ return root;
18
+ };
19
+ const resolve = async (...parts) => contained(await resolveRoot(), parts);
12
20
  async function stream(...parts) {
13
- const destination = resolve(...parts);
21
+ const destination = await resolve(...parts);
14
22
  const found = describe(destination);
15
23
  if (!found)
16
24
  throw new Error(`There is no ${parts.join("/")} in ${label}`);
@@ -20,7 +28,7 @@ export function filesystemStorage(root, label) {
20
28
  }
21
29
  async function write(...args) {
22
30
  const parts = args.slice(0, -1);
23
- const destination = resolve(...parts);
31
+ const destination = await resolve(...parts);
24
32
  const temporary = join(dirname(destination), `.${randomUUID()}.writing`);
25
33
  mkdirSync(dirname(destination), { recursive: true });
26
34
  try {
@@ -38,15 +46,15 @@ export function filesystemStorage(root, label) {
38
46
  async text(...parts) { return new Response(await stream(...parts)).text(); },
39
47
  async json(...parts) { return JSON.parse(await new Response(await stream(...parts)).text()); },
40
48
  write,
41
- async stat(...parts) { return describe(resolve(...parts)); },
42
- async list(...parts) { return readdirSync(resolve(...parts)).sort(); },
49
+ async stat(...parts) { return describe(await resolve(...parts)); },
50
+ async list(...parts) { return readdirSync(await resolve(...parts)).sort(); },
43
51
  async delete(...parts) {
44
52
  if (!parts.length)
45
53
  throw new Error("Emptying a place is clear, not delete");
46
- rmSync(resolve(...parts), { recursive: true, force: true });
54
+ rmSync(await resolve(...parts), { recursive: true, force: true });
47
55
  },
48
56
  async clear(...parts) {
49
- const destination = resolve(...parts);
57
+ const destination = await resolve(...parts);
50
58
  const found = describe(destination);
51
59
  if (found && found.kind !== "directory")
52
60
  throw new Error("Only a Storage directory can be cleared");
package/dist/system.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Client as CoreClient, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerService as CoreServerService, type ProgramDefinition, type ProgramCommandChunk, type ServiceKey, type System as CoreSystem, type SystemClientEntity, type SystemEndpointEntity, type SystemProcessEntity, type SystemProcess, type SystemProgram, type SystemProgramEntity, type SystemServerEntity, type SystemUploads, type WritableAppearance } from "@phreshos/core";
1
+ import { Client as CoreClient, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerService as CoreServerService, type ProgramDefinition, type ProgramCommandChunk, type ServiceKey, type System as CoreSystem, type SystemClientEntity, type SystemEndpointEntity, type SystemProcessEntity, type SystemProcess, type SystemProgram, type SystemProgramEntity, type SystemServerEntity, type SystemUploads, type Storage, type WritableAppearance } from "@phreshos/core";
2
2
  export type ProgramProcessRunOptions = Readonly<{
3
3
  signal?: AbortSignal;
4
4
  }>;
@@ -14,7 +14,7 @@ export type ProgramProcessRunEvent = Readonly<{
14
14
  }>;
15
15
  /** One connected owner-local implementation of the shared System contract. */
16
16
  export declare class System implements CoreSystem {
17
- readonly storage: import("@phreshos/core").Storage;
17
+ readonly storage: Storage;
18
18
  readonly appearance: WritableAppearance;
19
19
  readonly program: SystemProgram;
20
20
  readonly process: SystemProcess;
package/dist/system.js CHANGED
@@ -5,6 +5,8 @@ import Events from "./events.js";
5
5
  import HandleRegistry from "./handle-registry.js";
6
6
  import { resolveHome } from "./home.js";
7
7
  import { filesystemStorage } from "./storage.js";
8
+ import { programSql, programStore } from "./program-resources.js";
9
+ import { EndpointTrafficHandle, ServerTrafficHandle } from "./traffic.js";
8
10
  import { openConnection, request, streamProgram } from "./transport.js";
9
11
  import Uploads from "./uploads.js";
10
12
  const systems = new WeakMap();
@@ -62,8 +64,12 @@ export class System {
62
64
  requireConnected(this);
63
65
  if (!isServiceKey(key))
64
66
  throw new Error("A complete service key is required");
65
- const normalized = Object.freeze({ program: key.program, endpoint: key.endpoint, name: key.name });
66
- const identity = JSON.stringify([normalized.program, normalized.endpoint, normalized.name]);
67
+ const normalized = Object.freeze({
68
+ ...(key.program === undefined ? {} : { program: key.program }),
69
+ process: key.process,
70
+ endpoint: key.endpoint
71
+ });
72
+ const identity = JSON.stringify([key.program ?? null, key.process, key.endpoint]);
67
73
  return systemState(this).handles.obtain(`service:${identity}`, () => normalized.endpoint === "server"
68
74
  ? new ServerServiceHandle(this, normalized)
69
75
  : new ClientServiceHandle(this, normalized));
@@ -162,6 +168,11 @@ class ProgramHandle extends ProgramBase {
162
168
  system;
163
169
  reference;
164
170
  identity;
171
+ data;
172
+ cache;
173
+ store;
174
+ logs;
175
+ database;
165
176
  process;
166
177
  startup;
167
178
  snapshot;
@@ -174,6 +185,12 @@ class ProgramHandle extends ProgramBase {
174
185
  }, signal).then(value => programEntityEvent(event, value))));
175
186
  this.reference = snapshot.reference;
176
187
  this.identity = snapshot.identity;
188
+ const request = (value) => transport(system).api(value);
189
+ this.data = filesystemStorage(() => programStoragePath(system, this.identity, "data"), `Program "${this.identity}" data`);
190
+ this.cache = filesystemStorage(() => programStoragePath(system, this.identity, "cache"), `Program "${this.identity}" cache`);
191
+ this.store = programStore(request, this.identity);
192
+ this.logs = programSql(request, this.identity, "logs");
193
+ this.database = programSql(request, this.identity, "database");
177
194
  this.process = new ProgramProcesses(system, this);
178
195
  this.startup = new ProgramStartup(system, this);
179
196
  }
@@ -182,11 +199,15 @@ class ProgramHandle extends ProgramBase {
182
199
  get description() { return this.snapshot.description; }
183
200
  get hasAgent() { return this.snapshot.hasAgent; }
184
201
  get server() {
185
- return this.snapshot.server ? Object.freeze({ start: this.snapshot.server.start }) : null;
202
+ return this.snapshot.server ? Object.freeze({
203
+ start: this.snapshot.server.start,
204
+ service: this.snapshot.server.service
205
+ }) : null;
186
206
  }
187
207
  get client() {
188
208
  return this.snapshot.client ? Object.freeze({
189
209
  start: this.snapshot.client.start,
210
+ service: this.snapshot.client.service,
190
211
  title: this.snapshot.client.title,
191
212
  size: this.snapshot.client.size,
192
213
  position: this.snapshot.client.position,
@@ -199,6 +220,12 @@ class ProgramHandle extends ProgramBase {
199
220
  throw new Error("A Program handle cannot become another Program");
200
221
  this.snapshot = snapshot;
201
222
  }
223
+ async icon(size = "medium") {
224
+ const value = await transport(this.system).api({ capability: "program", operation: "icon", program: this.identity, size });
225
+ if (!Array.isArray(value) || value.some(byte => typeof byte !== "number"))
226
+ throw new Error("The System returned an invalid Program icon");
227
+ return new Blob([Uint8Array.from(value)], { type: "image/png" });
228
+ }
202
229
  async agent() {
203
230
  if (!this.hasAgent)
204
231
  return null;
@@ -368,10 +395,22 @@ class ProcessHandle extends ProcessBase {
368
395
  this.client = new ClientEndpoint(system, this);
369
396
  }
370
397
  program() { return programHandle(this.system, required(this.snapshot.programSnapshot, this.snapshot.program)); }
398
+ async parent() {
399
+ if (!await this.exists())
400
+ throw new Error(`Process "${this.identity}" no longer exists`);
401
+ if (this.snapshot.parent === null)
402
+ return null;
403
+ const parent = await this.system.process.find(this.snapshot.parent);
404
+ if (!parent)
405
+ throw new Error("The parent Process no longer exists");
406
+ return parent;
407
+ }
408
+ async option(name) { return this.snapshot.options[name]; }
371
409
  async exit() {
372
410
  await transport(this.system).control({ capability: "process", operation: "exit", input: { process: this.identity } });
373
411
  }
374
412
  async exited() { return await this.system.process.find(this.identity) === null; }
413
+ async exists() { return !await this.exited(); }
375
414
  }
376
415
  class EndpointOperations extends Events {
377
416
  system;
@@ -396,11 +435,12 @@ class EndpointOperations extends Events {
396
435
  const value = await this.inspect();
397
436
  return value.running;
398
437
  }
399
- async start(client) { await this.operation("start", client); }
438
+ async start(launch = {}) { await this.operation("start", launch); }
400
439
  async stop() { await this.operation("stop"); }
401
- async service() {
402
- const key = await transport(this.system).api({ capability: "endpoint", operation: "service", process: this.owner.identity, endpoint: this.endpoint });
403
- return key ? this.system.service(key) : null;
440
+ async isService() {
441
+ return await transport(this.system).api({
442
+ capability: "endpoint", operation: "isService", process: this.owner.identity, endpoint: this.endpoint
443
+ });
404
444
  }
405
445
  publish(event, payload) {
406
446
  void transport(this.system).control({ capability: "endpoint", operation: "publish", input: {
@@ -412,9 +452,9 @@ class EndpointOperations extends Events {
412
452
  process: this.owner.identity, endpoint: this.endpoint
413
453
  } });
414
454
  }
415
- async operation(operation, client) {
455
+ async operation(operation, launch) {
416
456
  await transport(this.system).control({ capability: "endpoint", operation, input: {
417
- process: this.owner.identity, endpoint: this.endpoint, ...(client ? { client } : {})
457
+ process: this.owner.identity, endpoint: this.endpoint, ...(launch ? { launch } : {})
418
458
  } });
419
459
  }
420
460
  }
@@ -422,6 +462,7 @@ class ServerEndpoint extends ServerBase {
422
462
  system;
423
463
  owner;
424
464
  endpoint = "server";
465
+ traffic;
425
466
  lifecycle;
426
467
  base;
427
468
  constructor(system, owner) {
@@ -429,12 +470,14 @@ class ServerEndpoint extends ServerBase {
429
470
  this.system = system;
430
471
  this.owner = owner;
431
472
  this.base = new EndpointOperations(system, owner, "server");
473
+ this.traffic = new ServerTrafficHandle((value, signal) => transport(system).api(value, signal), owner.identity, "server", value => endpointFromReference(system, value));
432
474
  this.lifecycle = this.base.lifecycle;
433
475
  bindEvents(this, this.base);
434
476
  }
435
477
  process() { return this.base.process(); }
436
478
  exists() { return this.base.exists(); }
437
- start() { return this.base.start(); }
479
+ isService() { return this.base.isService(); }
480
+ start(launch) { return this.base.start(launch); }
438
481
  stop() { return this.base.stop(); }
439
482
  publish(event, payload) { return this.base.publish(event, payload); }
440
483
  async ask(event, payload) {
@@ -450,30 +493,27 @@ class ServerEndpoint extends ServerBase {
450
493
  async waitReady(timeout) {
451
494
  await transport(this.system).control({ capability: "endpoint", operation: "waitReady", input: { process: this.owner.identity, endpoint: "server", timeout } });
452
495
  }
453
- async service() {
454
- return await this.base.service();
455
- }
456
496
  }
457
497
  class ClientEndpoint extends ClientBase {
458
498
  endpoint = "client";
499
+ traffic;
459
500
  lifecycle;
460
501
  window;
461
502
  base;
462
503
  constructor(system, owner) {
463
504
  super();
464
505
  this.base = new EndpointOperations(system, owner, "client");
506
+ this.traffic = new EndpointTrafficHandle((value, signal) => transport(system).api(value, signal), owner.identity, "client", value => endpointFromReference(system, value));
465
507
  this.lifecycle = this.base.lifecycle;
466
508
  bindEvents(this, this.base);
467
509
  this.window = new SystemWindow(system, owner);
468
510
  }
469
511
  process() { return this.base.process(); }
470
512
  exists() { return this.base.exists(); }
471
- start(overrides) { return this.base.start(overrides); }
513
+ isService() { return this.base.isService(); }
514
+ start(launch) { return this.base.start(launch); }
472
515
  stop() { return this.base.stop(); }
473
516
  publish(event, payload) { return this.base.publish(event, payload); }
474
- async service() {
475
- return await this.base.service();
476
- }
477
517
  }
478
518
  class SystemWindow extends Events {
479
519
  system;
@@ -508,18 +548,18 @@ class SystemWindow extends Events {
508
548
  class ServiceBase {
509
549
  system;
510
550
  key;
511
- name;
512
551
  lifecycle;
513
552
  constructor(system, key) {
514
553
  this.system = system;
515
554
  this.key = key;
516
- this.lifecycle = new Events(["enable", "disable"], (event, signal, timeout) => transport(system).api({
555
+ this.lifecycle = new Events(["start", "stop"], (event, signal, timeout) => transport(system).api({
517
556
  capability: "service", operation: "wait", scope: "lifecycle", key, event, timeout
518
557
  }, signal));
519
- this.name = key.name;
520
558
  }
521
- async enabled() { return await transport(this.system).api({ capability: "service", operation: "enabled", key: this.key }); }
522
- async waitReady(timeout) { await transport(this.system).api({ capability: "service", operation: "waitReady", key: this.key, timeout }); }
559
+ async exists() { return await transport(this.system).api({ capability: "service", operation: "exists", key: this.key }); }
560
+ publish(event, payload) {
561
+ void transport(this.system).api({ capability: "service", operation: "publish", key: this.key, event, payload });
562
+ }
523
563
  }
524
564
  /** Node-SDK handle for a Service provided by a Server Endpoint. */
525
565
  export class ServerService extends CoreServerService {
@@ -528,7 +568,6 @@ export class ServerService extends CoreServerService {
528
568
  class ServerServiceHandle extends ServerService {
529
569
  system;
530
570
  key;
531
- name;
532
571
  lifecycle;
533
572
  base;
534
573
  constructor(system, key) {
@@ -536,17 +575,16 @@ class ServerServiceHandle extends ServerService {
536
575
  this.system = system;
537
576
  this.key = key;
538
577
  this.base = new ServiceBase(system, key);
539
- this.name = key.name;
540
578
  this.lifecycle = this.base.lifecycle;
541
579
  bindEvents(this, new Events([], (event, signal, timeout) => transport(system).api({
542
580
  capability: "service", operation: "wait", scope: "events", key, event, timeout
543
581
  }, signal)));
544
582
  }
545
- enabled() { return this.base.enabled(); }
546
- waitReady(timeout) { return this.base.waitReady(timeout); }
547
- publish = (event, payload) => {
548
- void transport(this.system).api({ capability: "service", operation: "publish", key: this.key, event, payload });
549
- };
583
+ exists() { return this.base.exists(); }
584
+ async waitReady(timeout) {
585
+ await transport(this.system).api({ capability: "service", operation: "waitReady", key: this.key, timeout });
586
+ }
587
+ publish = (event, payload) => this.base.publish(event, payload);
550
588
  async ask(event, payload) {
551
589
  return await transport(this.system).api({ capability: "service", operation: "ask", key: this.key, event, payload });
552
590
  }
@@ -561,20 +599,18 @@ export class ClientService extends CoreClientService {
561
599
  constructor() { super(); }
562
600
  }
563
601
  class ClientServiceHandle extends ClientService {
564
- name;
565
602
  lifecycle;
566
603
  base;
567
604
  constructor(system, key) {
568
605
  super();
569
606
  this.base = new ServiceBase(system, key);
570
- this.name = key.name;
571
607
  this.lifecycle = this.base.lifecycle;
572
608
  bindEvents(this, new Events([], (event, signal, timeout) => transport(system).api({
573
609
  capability: "service", operation: "wait", scope: "events", key, event, timeout
574
610
  }, signal)));
575
611
  }
576
- enabled() { return this.base.enabled(); }
577
- waitReady(timeout) { return this.base.waitReady(timeout); }
612
+ exists() { return this.base.exists(); }
613
+ publish = (event, payload) => this.base.publish(event, payload);
578
614
  }
579
615
  async function listProcesses(system, program) {
580
616
  const processes = [];
@@ -644,6 +680,36 @@ function eventsOf(events) {
644
680
  };
645
681
  }
646
682
  function chronological(left, right) { return left.startedAt.getTime() - right.startedAt.getTime(); }
683
+ async function programStoragePath(system, program, area) {
684
+ const value = await transport(system).api({ capability: "program", operation: "storagePath", program, area });
685
+ if (typeof value !== "string")
686
+ throw new Error("The System returned an invalid Program storage path");
687
+ return value;
688
+ }
689
+ function endpointFromReference(system, value) {
690
+ const reference = value;
691
+ if (!reference || (reference.kind !== "server" && reference.kind !== "client"))
692
+ throw new Error("The System returned an invalid Endpoint reference");
693
+ const owner = processHandle(system, snapshotFromReference(reference.process));
694
+ return reference.kind === "server" ? owner.server : owner.client;
695
+ }
696
+ function snapshotFromReference(reference) {
697
+ const owner = reference.program;
698
+ if (!owner || typeof owner.reference !== "string" || typeof owner.identity !== "string")
699
+ throw new Error("The System returned an invalid Process reference");
700
+ return {
701
+ reference: reference.reference,
702
+ identity: reference.identity,
703
+ name: reference.name,
704
+ program: owner.identity,
705
+ programSnapshot: owner,
706
+ parent: null,
707
+ options: reference.options,
708
+ startedAt: reference.startedAt,
709
+ server: { declared: owner.server !== null, running: reference.server !== null, service: reference.server?.service === true },
710
+ client: { declared: owner.client !== null, running: reference.client !== null, service: reference.client?.service === true }
711
+ };
712
+ }
647
713
  function unknown(error, entity) { return error instanceof Error && error.message.startsWith(`Unknown ${entity}`); }
648
714
  function required(value, identity = "") {
649
715
  if (value !== undefined)
@@ -0,0 +1,36 @@
1
+ import type { AnswerSubscriber, AskSubscriber, Cleanup, Endpoint, EventOptions, ServerTraffic, TrafficEvents, TrafficMessage } from "@phreshos/core";
2
+ import Events from "./events.js";
3
+ type Kind = "publish" | "ask" | "answer";
4
+ type Request = (value: object, signal?: AbortSignal) => Promise<unknown>;
5
+ type ResolveEndpoint = (value: unknown) => Endpoint;
6
+ /** Directed traffic originating from one canonical Endpoint. */
7
+ export declare class EndpointTrafficHandle<Definitions extends object = {}> extends Events<TrafficEvents<Definitions>, keyof Definitions extends never ? TrafficMessage : never> {
8
+ private readonly request;
9
+ private readonly process;
10
+ private readonly endpoint;
11
+ protected readonly resolveEndpoint: ResolveEndpoint;
12
+ constructor(request: Request, process: string, endpoint: "server" | "client", resolveEndpoint: ResolveEndpoint);
13
+ subscribeAsks<Payload = unknown>(subscriber: AskSubscriber<Payload>): Cleanup;
14
+ asks<Payload = unknown>(options?: EventOptions): AsyncIterableIterator<Readonly<{
15
+ event: string;
16
+ questionId: string;
17
+ message: Readonly<{
18
+ to: import("@phreshos/core").Server<{}>;
19
+ payload: Payload;
20
+ }>;
21
+ }>>;
22
+ protected follow<Capture>(kind: Kind, convert: (value: unknown) => Capture, subscriber: (capture: Capture) => unknown, impossible?: (error: Error) => void): Cleanup;
23
+ }
24
+ /** Directed traffic originating from one canonical Server. */
25
+ export declare class ServerTrafficHandle<Definitions extends object = {}> extends EndpointTrafficHandle<Definitions> implements ServerTraffic<Definitions> {
26
+ subscribeAnswers<Result = unknown>(subscriber: AnswerSubscriber<Result>): Cleanup;
27
+ answers<Result = unknown>(options?: EventOptions): AsyncIterableIterator<Readonly<{
28
+ event: string;
29
+ questionId: string;
30
+ message: Readonly<{
31
+ to: Endpoint<{}>;
32
+ outcome: import("@phreshos/core").Outcome<Result>;
33
+ }>;
34
+ }>>;
35
+ }
36
+ export {};
@@ -0,0 +1,101 @@
1
+ import Events, { stream } from "./events.js";
2
+ /** Directed traffic originating from one canonical Endpoint. */
3
+ export class EndpointTrafficHandle extends Events {
4
+ request;
5
+ process;
6
+ endpoint;
7
+ resolveEndpoint;
8
+ constructor(request, process, endpoint, resolveEndpoint) {
9
+ super([], (event, signal, timeout) => request({
10
+ capability: "traffic",
11
+ operation: "wait",
12
+ process,
13
+ endpoint,
14
+ kind: "publish",
15
+ event,
16
+ timeout
17
+ }, signal).then(value => publication(value, resolveEndpoint, event === null)));
18
+ this.request = request;
19
+ this.process = process;
20
+ this.endpoint = endpoint;
21
+ this.resolveEndpoint = resolveEndpoint;
22
+ }
23
+ subscribeAsks(subscriber) {
24
+ return this.follow("ask", value => question(value, this.resolveEndpoint), subscriber);
25
+ }
26
+ asks(options) {
27
+ return stream((subscriber, impossible) => this.follow("ask", value => question(value, this.resolveEndpoint), subscriber, impossible), options);
28
+ }
29
+ follow(kind, convert, subscriber, impossible) {
30
+ const controller = new AbortController();
31
+ void (async () => {
32
+ while (!controller.signal.aborted) {
33
+ try {
34
+ const value = await this.request({
35
+ capability: "traffic",
36
+ operation: "wait",
37
+ process: this.process,
38
+ endpoint: this.endpoint,
39
+ kind,
40
+ event: null,
41
+ timeout: 86_400_000
42
+ }, controller.signal);
43
+ subscriber(convert(value));
44
+ }
45
+ catch (error) {
46
+ if (!controller.signal.aborted)
47
+ impossible?.(error instanceof Error ? error : new Error(String(error)));
48
+ controller.abort();
49
+ }
50
+ }
51
+ })();
52
+ return () => controller.abort();
53
+ }
54
+ }
55
+ /** Directed traffic originating from one canonical Server. */
56
+ export class ServerTrafficHandle extends EndpointTrafficHandle {
57
+ subscribeAnswers(subscriber) {
58
+ return this.follow("answer", value => answer(value, this.resolveEndpoint), subscriber);
59
+ }
60
+ answers(options) {
61
+ return stream((subscriber, impossible) => this.follow("answer", value => answer(value, this.resolveEndpoint), subscriber, impossible), options);
62
+ }
63
+ }
64
+ function publication(value, resolve, captured) {
65
+ const received = traffic(value);
66
+ const message = directed(received.values[0], resolve);
67
+ return captured ? { event: received.event, payload: message } : message;
68
+ }
69
+ function question(value, resolve) {
70
+ const received = traffic(value);
71
+ if (typeof received.values[0] !== "string")
72
+ throw new Error("The System returned invalid question traffic");
73
+ return {
74
+ event: received.event,
75
+ questionId: received.values[0],
76
+ message: directed(received.values[1], resolve)
77
+ };
78
+ }
79
+ function answer(value, resolve) {
80
+ const received = traffic(value);
81
+ const raw = received.values[1];
82
+ if (typeof received.values[0] !== "string" || !raw || typeof raw !== "object")
83
+ throw new Error("The System returned invalid answer traffic");
84
+ return {
85
+ event: received.event,
86
+ questionId: received.values[0],
87
+ message: { to: resolve(raw.to), outcome: raw.outcome }
88
+ };
89
+ }
90
+ function directed(value, resolve) {
91
+ const raw = value;
92
+ if (!raw || typeof raw !== "object")
93
+ throw new Error("The System returned invalid Endpoint traffic");
94
+ return { to: resolve(raw.to), payload: raw.payload };
95
+ }
96
+ function traffic(value) {
97
+ const received = value;
98
+ if (!received || typeof received.event !== "string" || !Array.isArray(received.values))
99
+ throw new Error("The System returned invalid traffic");
100
+ return { event: received.event, values: received.values };
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/node",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Node.js access to PhreshOS and Program projects.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -36,7 +36,7 @@
36
36
  "prepack": "node --run build"
37
37
  },
38
38
  "dependencies": {
39
- "@phreshos/core": "^0.1.27",
39
+ "@phreshos/core": "^0.1.30",
40
40
  "adm-zip": "^0.6.0",
41
41
  "jiti": "^2.7.0"
42
42
  },