@phreshos/node 0.1.7 → 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");
@@ -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/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();
@@ -166,6 +168,11 @@ class ProgramHandle extends ProgramBase {
166
168
  system;
167
169
  reference;
168
170
  identity;
171
+ data;
172
+ cache;
173
+ store;
174
+ logs;
175
+ database;
169
176
  process;
170
177
  startup;
171
178
  snapshot;
@@ -178,6 +185,12 @@ class ProgramHandle extends ProgramBase {
178
185
  }, signal).then(value => programEntityEvent(event, value))));
179
186
  this.reference = snapshot.reference;
180
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");
181
194
  this.process = new ProgramProcesses(system, this);
182
195
  this.startup = new ProgramStartup(system, this);
183
196
  }
@@ -207,6 +220,12 @@ class ProgramHandle extends ProgramBase {
207
220
  throw new Error("A Program handle cannot become another Program");
208
221
  this.snapshot = snapshot;
209
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
+ }
210
229
  async agent() {
211
230
  if (!this.hasAgent)
212
231
  return null;
@@ -376,10 +395,22 @@ class ProcessHandle extends ProcessBase {
376
395
  this.client = new ClientEndpoint(system, this);
377
396
  }
378
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]; }
379
409
  async exit() {
380
410
  await transport(this.system).control({ capability: "process", operation: "exit", input: { process: this.identity } });
381
411
  }
382
412
  async exited() { return await this.system.process.find(this.identity) === null; }
413
+ async exists() { return !await this.exited(); }
383
414
  }
384
415
  class EndpointOperations extends Events {
385
416
  system;
@@ -431,6 +462,7 @@ class ServerEndpoint extends ServerBase {
431
462
  system;
432
463
  owner;
433
464
  endpoint = "server";
465
+ traffic;
434
466
  lifecycle;
435
467
  base;
436
468
  constructor(system, owner) {
@@ -438,6 +470,7 @@ class ServerEndpoint extends ServerBase {
438
470
  this.system = system;
439
471
  this.owner = owner;
440
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));
441
474
  this.lifecycle = this.base.lifecycle;
442
475
  bindEvents(this, this.base);
443
476
  }
@@ -463,12 +496,14 @@ class ServerEndpoint extends ServerBase {
463
496
  }
464
497
  class ClientEndpoint extends ClientBase {
465
498
  endpoint = "client";
499
+ traffic;
466
500
  lifecycle;
467
501
  window;
468
502
  base;
469
503
  constructor(system, owner) {
470
504
  super();
471
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));
472
507
  this.lifecycle = this.base.lifecycle;
473
508
  bindEvents(this, this.base);
474
509
  this.window = new SystemWindow(system, owner);
@@ -645,6 +680,36 @@ function eventsOf(events) {
645
680
  };
646
681
  }
647
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
+ }
648
713
  function unknown(error, entity) { return error instanceof Error && error.message.startsWith(`Unknown ${entity}`); }
649
714
  function required(value, identity = "") {
650
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.7",
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.29",
39
+ "@phreshos/core": "^0.1.30",
40
40
  "adm-zip": "^0.6.0",
41
41
  "jiti": "^2.7.0"
42
42
  },