@phreshos/node 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/traffic.d.ts CHANGED
@@ -1,27 +1,27 @@
1
1
  import type { AnswerSubscriber, AskSubscriber, Cleanup, Endpoint, EventOptions, ServerTraffic, TrafficEvents } from "@phreshos/core";
2
2
  import Events from "./events.js";
3
+ import type SystemRepresentation from "./representation.js";
3
4
  type Kind = "publish" | "ask" | "answer";
4
- type Request = (value: object, signal?: AbortSignal) => Promise<unknown>;
5
5
  type ResolveEndpoint = (value: unknown) => Endpoint;
6
6
  /** Directed traffic originating from one canonical Endpoint. */
7
7
  export declare class EndpointTrafficHandle<Definitions extends object = {}> extends Events<TrafficEvents<Definitions>, never> {
8
- private readonly request;
8
+ private readonly representation;
9
9
  private readonly process;
10
10
  private readonly endpoint;
11
11
  protected readonly resolveEndpoint: ResolveEndpoint;
12
- constructor(request: Request, process: string, endpoint: "server" | "client", resolveEndpoint: ResolveEndpoint);
12
+ constructor(representation: SystemRepresentation, process: string, endpoint: "server" | "client", resolveEndpoint: ResolveEndpoint);
13
13
  subscribeAsks<Payload = unknown>(subscriber: AskSubscriber<Payload>): Cleanup;
14
14
  asks<Payload = unknown>(options?: EventOptions): AsyncIterableIterator<Readonly<{
15
15
  event: string;
16
16
  questionId: string;
17
17
  message: Readonly<{
18
- to: import("@phreshos/core").Server<{}, unknown> | null;
18
+ to: import("@phreshos/core").ServerEndpoint<{}, unknown> | null;
19
19
  payload: Payload;
20
20
  }>;
21
21
  }>>;
22
22
  protected follow<Capture>(kind: Kind, convert: (value: unknown) => Capture, subscriber: (capture: Capture) => unknown, impossible?: (error: Error) => void): Cleanup;
23
23
  }
24
- /** Directed traffic originating from one canonical Server. */
24
+ /** Directed traffic originating from one canonical Server Endpoint. */
25
25
  export declare class ServerTrafficHandle<Definitions extends object = {}> extends EndpointTrafficHandle<Definitions> implements ServerTraffic<Definitions> {
26
26
  subscribeAnswers<Result = unknown>(subscriber: AnswerSubscriber<Result>): Cleanup;
27
27
  answers<Result = unknown>(options?: EventOptions): AsyncIterableIterator<Readonly<{
@@ -29,7 +29,7 @@ export declare class ServerTrafficHandle<Definitions extends object = {}> extend
29
29
  questionId: string;
30
30
  message: Readonly<{
31
31
  to: Endpoint<{}, unknown> | null;
32
- outcome: import("@phreshos/core").Outcome<Result>;
32
+ outcome: import("@phreshos/core").AnswerOutcome<Result>;
33
33
  }>;
34
34
  }>>;
35
35
  }
package/dist/traffic.js CHANGED
@@ -1,21 +1,19 @@
1
1
  import Events, { stream } from "./events.js";
2
2
  /** Directed traffic originating from one canonical Endpoint. */
3
3
  export class EndpointTrafficHandle extends Events {
4
- request;
4
+ representation;
5
5
  process;
6
6
  endpoint;
7
7
  resolveEndpoint;
8
- constructor(request, process, endpoint, resolveEndpoint) {
9
- super([], (event, signal, timeout) => request({
10
- capability: "traffic",
11
- operation: "wait",
8
+ constructor(representation, process, endpoint, resolveEndpoint) {
9
+ super([], (event, subscriber, impossible) => representation.follow({
10
+ scope: "traffic",
12
11
  process,
13
12
  endpoint,
14
13
  kind: "publish",
15
- event,
16
- timeout
17
- }, signal).then(value => publication(value, resolveEndpoint, event === null)));
18
- this.request = request;
14
+ event
15
+ }, (received, values) => subscriber(publication({ event: received, values }, resolveEndpoint, event === null)), impossible));
16
+ this.representation = representation;
19
17
  this.process = process;
20
18
  this.endpoint = endpoint;
21
19
  this.resolveEndpoint = resolveEndpoint;
@@ -27,32 +25,16 @@ export class EndpointTrafficHandle extends Events {
27
25
  return stream((subscriber, impossible) => this.follow("ask", value => question(value, this.resolveEndpoint), subscriber, impossible), options);
28
26
  }
29
27
  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();
28
+ return this.representation.follow({
29
+ scope: "traffic",
30
+ process: this.process,
31
+ endpoint: this.endpoint,
32
+ kind,
33
+ event: null
34
+ }, (event, values) => subscriber(convert({ event, values })), impossible);
53
35
  }
54
36
  }
55
- /** Directed traffic originating from one canonical Server. */
37
+ /** Directed traffic originating from one canonical Server Endpoint. */
56
38
  export class ServerTrafficHandle extends EndpointTrafficHandle {
57
39
  subscribeAnswers(subscriber) {
58
40
  return this.follow("answer", value => answer(value, this.resolveEndpoint), subscriber);
@@ -1,11 +1,21 @@
1
- import { type Socket } from "node:net";
2
- export interface TransportEvent {
3
- event?: string;
4
- [key: string]: unknown;
1
+ /** One persistent owner-local IPC connection to the System Link Manager. */
2
+ export declare class GatewayConnection {
3
+ private readonly link;
4
+ session: unknown;
5
+ private active;
6
+ private readonly queued;
7
+ private readonly subscribers;
8
+ private constructor();
9
+ static open(path: string): Promise<GatewayConnection>;
10
+ onDisconnect(subscriber: (error: Error) => void): () => void;
11
+ disconnect(): Promise<void>;
12
+ /** Invoke one operation exposed by the connected System Link Manager. */
13
+ call<Result = unknown>(event: string, ...values: unknown[]): Promise<Result>;
14
+ /** Observe one live event emitted by the connected System Link Manager. */
15
+ subscribe(event: string, subscriber: (...values: unknown[]) => unknown): () => void;
16
+ /** Begin delivery after the connected representation has installed its listeners. */
17
+ activate(): void;
18
+ private deliver;
5
19
  }
6
- /** Open and retain one owner-local System connection. */
7
- export declare function openConnection(path: string): Promise<Socket>;
8
- /** Execute one short authoritative System-control request. */
9
- export declare function request(path: string, target: "api" | "system", request: unknown, signal?: AbortSignal): Promise<unknown>;
10
- /** Stream one Program lifecycle operation until the System completes it. */
11
- export declare function streamProgram(path: string, request: unknown, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
20
+ /** Open and authenticate one owner-local System connection. */
21
+ export declare function openConnection(path: string): Promise<GatewayConnection>;
package/dist/transport.js CHANGED
@@ -1,119 +1,115 @@
1
- import { connect } from "node:net";
2
- /** Open and retain one owner-local System connection. */
3
- export function openConnection(path) {
4
- return new Promise((resolve, reject) => {
5
- const socket = connect(path);
6
- const failed = () => reject(unavailable(path));
7
- socket.once("connect", () => {
8
- socket.off("error", failed);
9
- socket.on("error", () => undefined);
10
- resolve(socket);
11
- });
12
- socket.once("error", failed);
13
- });
14
- }
15
- /** Execute one short authoritative System-control request. */
16
- export function request(path, target, request, signal) {
17
- return new Promise((resolve, reject) => {
18
- const socket = connect(path);
19
- let buffer = "";
20
- let settled = false;
21
- const finish = (work) => {
22
- if (settled)
1
+ import { SocketClient } from "@the-link/ipc/socket-client";
2
+ import messagepack from "@the-link/messagepack";
3
+ const events = {
4
+ ready: "/gateway/ready"
5
+ };
6
+ /** One persistent owner-local IPC connection to the System Link Manager. */
7
+ export class GatewayConnection {
8
+ link;
9
+ session = null;
10
+ active = false;
11
+ queued = [];
12
+ subscribers = new Map();
13
+ constructor(link) {
14
+ this.link = link;
15
+ link.$inbound.forwardTo((event, ...values) => {
16
+ if (event === events.ready)
23
17
  return;
24
- settled = true;
25
- signal?.removeEventListener("abort", cancel);
26
- socket.destroy();
27
- work();
28
- };
29
- const cancel = () => finish(() => reject(signal?.reason instanceof Error ? signal.reason : new Error("The request was cancelled")));
30
- signal?.addEventListener("abort", cancel, { once: true });
31
- socket.on("connect", () => socket.write(`${JSON.stringify({ target, request })}\n`));
32
- socket.on("data", chunk => {
33
- buffer += String(chunk);
34
- const boundary = buffer.indexOf("\n");
35
- if (boundary < 0)
36
- return;
37
- let outcome;
38
- try {
39
- outcome = JSON.parse(buffer.slice(0, boundary));
40
- }
41
- catch {
42
- return finish(() => reject(new Error("The System returned an invalid response")));
43
- }
44
- if (outcome.success)
45
- finish(() => resolve(outcome.result));
18
+ if (!this.active)
19
+ this.queued.push({ event, values });
46
20
  else
47
- finish(() => reject(new Error(outcome.error)));
21
+ this.deliver(event, values);
48
22
  });
49
- socket.on("error", () => finish(() => reject(unavailable(path))));
50
- socket.on("close", () => finish(() => reject(new Error("The System closed the request without an answer"))));
51
- if (signal?.aborted)
52
- cancel();
53
- });
54
- }
55
- /** Stream one Program lifecycle operation until the System completes it. */
56
- export function streamProgram(path, request, signal) {
57
- const events = [];
58
- let wake = null;
59
- let ended = false;
60
- let failure = null;
61
- const socket = connect(path);
62
- const cancel = () => socket.destroy(signal?.reason instanceof Error ? signal.reason : undefined);
63
- if (signal?.aborted)
64
- cancel();
65
- else
66
- signal?.addEventListener("abort", cancel, { once: true });
67
- let buffer = "";
68
- socket.on("connect", () => socket.write(`${JSON.stringify({ target: "program", request })}\n`));
69
- socket.on("data", chunk => {
70
- buffer += String(chunk);
71
- const lines = buffer.split("\n");
72
- buffer = lines.pop() ?? "";
73
- for (const line of lines)
74
- if (line.trim()) {
75
- const event = JSON.parse(line);
76
- if (event.event === "error")
77
- failure = new Error(String(event.message));
78
- else
79
- events.push(event);
80
- }
81
- wake?.();
82
- wake = null;
83
- });
84
- socket.on("error", error => {
85
- failure = signal?.aborted
86
- ? signal.reason instanceof Error ? signal.reason : new Error("The operation was cancelled")
87
- : error;
88
- wake?.();
89
- wake = null;
90
- });
91
- socket.on("close", () => {
92
- ended = true;
93
- signal?.removeEventListener("abort", cancel);
94
- wake?.();
95
- wake = null;
96
- });
97
- return (async function* () {
23
+ }
24
+ static async open(path) {
25
+ const link = new SocketClient(path);
26
+ const connection = new GatewayConnection(link);
27
+ link.setSerialize(messagepack.serialize);
28
+ link.setDeserialize(messagepack.deserialize);
29
+ const readiness = ready(link);
98
30
  try {
99
- while (true) {
100
- if (events.length) {
101
- yield events.shift();
102
- continue;
103
- }
104
- if (failure)
105
- throw failure;
106
- if (ended)
107
- return;
108
- await new Promise(resolve => { wake = resolve; });
31
+ await link.connect();
32
+ }
33
+ catch (error) {
34
+ readiness.cancel();
35
+ await link.disconnect();
36
+ throw unavailable(path, error);
37
+ }
38
+ try {
39
+ connection.session = await readiness.promise;
40
+ return connection;
41
+ }
42
+ catch (error) {
43
+ await link.disconnect();
44
+ throw new Error("The System Gateway did not establish an owner session", { cause: error });
45
+ }
46
+ }
47
+ onDisconnect(subscriber) {
48
+ return this.link.$internal.subscribe("disconnect", subscriber);
49
+ }
50
+ disconnect() {
51
+ return this.link.disconnect();
52
+ }
53
+ /** Invoke one operation exposed by the connected System Link Manager. */
54
+ call(event, ...values) {
55
+ return this.link.$outbound.publishFirst(event, ...values);
56
+ }
57
+ /** Observe one live event emitted by the connected System Link Manager. */
58
+ subscribe(event, subscriber) {
59
+ const subscribers = this.subscribers.get(event) ?? new Set();
60
+ subscribers.add(subscriber);
61
+ this.subscribers.set(event, subscribers);
62
+ return () => {
63
+ subscribers.delete(subscriber);
64
+ if (!subscribers.size)
65
+ this.subscribers.delete(event);
66
+ };
67
+ }
68
+ /** Begin delivery after the connected representation has installed its listeners. */
69
+ activate() {
70
+ if (this.active)
71
+ return;
72
+ this.active = true;
73
+ for (const { event, values } of this.queued.splice(0))
74
+ this.deliver(event, values);
75
+ }
76
+ deliver(event, values) {
77
+ for (const subscriber of this.subscribers.get(event) ?? []) {
78
+ try {
79
+ subscriber(...values);
109
80
  }
81
+ catch { /* One local observer cannot break boundary delivery. */ }
110
82
  }
111
- finally {
112
- signal?.removeEventListener("abort", cancel);
113
- socket.destroy();
83
+ }
84
+ }
85
+ function ready(link) {
86
+ let received = () => undefined;
87
+ let disconnected = () => undefined;
88
+ const promise = new Promise((resolve, reject) => {
89
+ received = link.$inbound.subscribeOnce(events.ready, value => {
90
+ disconnected();
91
+ resolve(value);
92
+ });
93
+ disconnected = link.$internal.subscribeOnce("disconnect", error => {
94
+ received();
95
+ reject(exception(error));
96
+ });
97
+ });
98
+ return {
99
+ promise,
100
+ cancel() {
101
+ received();
102
+ disconnected();
114
103
  }
115
- })();
104
+ };
116
105
  }
117
- function unavailable(path) {
118
- return new Error(`No System is listening at ${path} — start PhreshOS first`);
106
+ function exception(error) {
107
+ return error instanceof Error ? error : new Error(String(error));
108
+ }
109
+ function unavailable(path, cause) {
110
+ return new Error(`No System is listening at ${path} — start PhreshOS first`, { cause });
111
+ }
112
+ /** Open and authenticate one owner-local System connection. */
113
+ export function openConnection(path) {
114
+ return GatewayConnection.open(path);
119
115
  }
package/dist/uploads.d.ts CHANGED
@@ -3,8 +3,10 @@ type Ask = (request: object) => Promise<unknown>;
3
3
  /** Owner-local implementation of the System's opaque upload collection. */
4
4
  export default class Uploads implements SystemUploads {
5
5
  private readonly ask;
6
+ private readonly lifetime;
6
7
  private accessPromise;
7
- constructor(ask: Ask);
8
+ constructor(ask: Ask, lifetime: () => AbortSignal);
9
+ path(): Promise<string>;
8
10
  write(value: unknown): Promise<Upload>;
9
11
  stream(file: string): Promise<ReadableStream<Uint8Array<ArrayBufferLike>>>;
10
12
  bytes(file: string): Promise<Uint8Array<ArrayBuffer>>;
@@ -17,5 +19,6 @@ export default class Uploads implements SystemUploads {
17
19
  time: number;
18
20
  }> | null>;
19
21
  private access;
22
+ private active;
20
23
  }
21
24
  export {};
package/dist/uploads.js CHANGED
@@ -2,17 +2,23 @@ import { isUploadFile } from "@phreshos/core";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { createReadStream, createWriteStream, mkdirSync } from "node:fs";
4
4
  import { rename, rm } from "node:fs/promises";
5
- import { join } from "node:path";
5
+ import { isAbsolute, join } from "node:path";
6
6
  import { Readable } from "node:stream";
7
7
  import { pipeline } from "node:stream/promises";
8
8
  /** Owner-local implementation of the System's opaque upload collection. */
9
9
  export default class Uploads {
10
10
  ask;
11
+ lifetime;
11
12
  accessPromise = null;
12
- constructor(ask) {
13
+ constructor(ask, lifetime) {
13
14
  this.ask = ask;
15
+ this.lifetime = lifetime;
16
+ }
17
+ async path() {
18
+ return (await this.access()).path;
14
19
  }
15
20
  async write(value) {
21
+ const signal = this.active();
16
22
  const access = await this.access();
17
23
  const source = content(value);
18
24
  const identity = randomUUID();
@@ -29,7 +35,8 @@ export default class Uploads {
29
35
  throw new Error(`The upload exceeds ${access.limit / 1024 / 1024 / 1024} GB`);
30
36
  yield chunk;
31
37
  }
32
- }, createWriteStream(temporary, { flags: "wx" }));
38
+ }, createWriteStream(temporary, { flags: "wx" }), { signal });
39
+ signal.throwIfAborted();
33
40
  await rename(temporary, destination);
34
41
  }
35
42
  catch (error) {
@@ -42,22 +49,25 @@ export default class Uploads {
42
49
  return upload;
43
50
  }
44
51
  async stream(file) {
52
+ const signal = this.active();
45
53
  requireFile(file);
46
54
  const access = await this.access();
47
- return Readable.toWeb(createReadStream(join(access.path, file)));
55
+ return Readable.toWeb(createReadStream(join(access.path, file), { signal }));
48
56
  }
49
57
  async bytes(file) { return new Uint8Array(await new Response(await this.stream(file)).arrayBuffer()); }
50
58
  async text(file) { return new Response(await this.stream(file)).text(); }
51
59
  async json(file) { return JSON.parse(await this.text(file)); }
52
60
  async stat(file) {
61
+ this.active();
53
62
  requireFile(file);
54
63
  return await this.ask({ capability: "uploads", operation: "stat", file });
55
64
  }
56
65
  access() {
66
+ this.active();
57
67
  if (!this.accessPromise) {
58
68
  const resolving = this.ask({ capability: "uploads", operation: "access" }).then(value => {
59
69
  const access = value;
60
- if (!access || typeof access.path !== "string" || typeof access.limit !== "number")
70
+ if (!access || typeof access.path !== "string" || !isAbsolute(access.path) || typeof access.limit !== "number")
61
71
  throw new Error("The System returned invalid upload access");
62
72
  return { path: access.path, limit: access.limit };
63
73
  });
@@ -70,6 +80,11 @@ export default class Uploads {
70
80
  }
71
81
  return this.accessPromise;
72
82
  }
83
+ active() {
84
+ const signal = this.lifetime();
85
+ signal.throwIfAborted();
86
+ return signal;
87
+ }
73
88
  }
74
89
  function requireFile(file) {
75
90
  if (!isUploadFile(file))
@@ -0,0 +1,2 @@
1
+ /** Open a native WebSocket owned by one connected System lifetime. */
2
+ export default function websocket(url: string | URL, protocols: string | string[] | undefined, signal: AbortSignal): Promise<WebSocket>;
@@ -0,0 +1,15 @@
1
+ /** Open a native WebSocket owned by one connected System lifetime. */
2
+ export default async function websocket(url, protocols, signal) {
3
+ if (signal.aborted)
4
+ throw signal.reason;
5
+ const socket = protocols === undefined
6
+ ? new WebSocket(url)
7
+ : new WebSocket(url, protocols);
8
+ const close = () => socket.close();
9
+ const release = () => signal.removeEventListener("abort", close);
10
+ signal.addEventListener("abort", close, { once: true });
11
+ socket.addEventListener("close", release, { once: true });
12
+ if (signal.aborted)
13
+ close();
14
+ return socket;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/node",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Node.js access to PhreshOS and Program projects.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -36,7 +36,9 @@
36
36
  "prepack": "node --run build"
37
37
  },
38
38
  "dependencies": {
39
- "@phreshos/core": "^0.1.36",
39
+ "@phreshos/core": "^0.1.39",
40
+ "@the-link/ipc": "^0.2.1",
41
+ "@the-link/messagepack": "^0.1.0",
40
42
  "adm-zip": "^0.6.0",
41
43
  "jiti": "^2.7.0"
42
44
  },