@phreshos/node 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/traffic.d.ts CHANGED
@@ -1,15 +1,15 @@
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;
@@ -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,29 +25,13 @@ 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
37
  /** Directed traffic originating from one canonical Server Endpoint. */
@@ -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 long-running authoritative System operation. */
11
- export declare function stream(path: string, target: "program", 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,133 +1,115 @@
1
- import { connect } from "node:net";
2
- const maximumStreamQueue = 256;
3
- /** Open and retain one owner-local System connection. */
4
- export function openConnection(path) {
5
- return new Promise((resolve, reject) => {
6
- const socket = connect(path);
7
- const failed = () => reject(unavailable(path));
8
- socket.once("connect", () => {
9
- socket.off("error", failed);
10
- socket.on("error", () => undefined);
11
- resolve(socket);
12
- });
13
- socket.once("error", failed);
14
- });
15
- }
16
- /** Execute one short authoritative System-control request. */
17
- export function request(path, target, request, signal) {
18
- return new Promise((resolve, reject) => {
19
- const socket = connect(path);
20
- let buffer = "";
21
- let settled = false;
22
- const finish = (work) => {
23
- 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)
24
17
  return;
25
- settled = true;
26
- signal?.removeEventListener("abort", cancel);
27
- socket.destroy();
28
- work();
29
- };
30
- const cancel = () => finish(() => reject(signal?.reason instanceof Error ? signal.reason : new Error("The request was cancelled")));
31
- signal?.addEventListener("abort", cancel, { once: true });
32
- socket.on("connect", () => socket.write(`${JSON.stringify({ target, request })}\n`));
33
- socket.on("data", chunk => {
34
- buffer += String(chunk);
35
- const boundary = buffer.indexOf("\n");
36
- if (boundary < 0)
37
- return;
38
- let outcome;
39
- try {
40
- outcome = JSON.parse(buffer.slice(0, boundary));
41
- }
42
- catch {
43
- return finish(() => reject(new Error("The System returned an invalid response")));
44
- }
45
- if (outcome.success)
46
- finish(() => resolve(outcome.result));
18
+ if (!this.active)
19
+ this.queued.push({ event, values });
47
20
  else
48
- finish(() => reject(new Error(outcome.error)));
21
+ this.deliver(event, values);
49
22
  });
50
- socket.on("error", () => finish(() => reject(unavailable(path))));
51
- socket.on("close", () => finish(() => reject(new Error("The System closed the request without an answer"))));
52
- if (signal?.aborted)
53
- cancel();
54
- });
55
- }
56
- /** Stream one long-running authoritative System operation. */
57
- export function stream(path, target, request, signal) {
58
- const events = [];
59
- let wake = null;
60
- let ended = false;
61
- let failure = null;
62
- const socket = connect(path);
63
- const cancel = () => socket.destroy(signal?.reason instanceof Error ? signal.reason : undefined);
64
- if (signal?.aborted)
65
- cancel();
66
- else
67
- signal?.addEventListener("abort", cancel, { once: true });
68
- let buffer = "";
69
- socket.on("connect", () => socket.write(`${JSON.stringify({ target, request })}\n`));
70
- socket.on("data", chunk => {
71
- buffer += String(chunk);
72
- const lines = buffer.split("\n");
73
- buffer = lines.pop() ?? "";
74
- for (const line of lines)
75
- if (line.trim()) {
76
- if (events.length >= maximumStreamQueue) {
77
- failure = new Error(`System stream queue exceeded its capacity of ${maximumStreamQueue}`);
78
- socket.destroy();
79
- break;
80
- }
81
- let event;
82
- try {
83
- event = JSON.parse(line);
84
- }
85
- catch {
86
- failure = new Error("The System returned an invalid stream event");
87
- socket.destroy();
88
- break;
89
- }
90
- if (event.event === "error")
91
- failure = new Error(String(event.message));
92
- else
93
- events.push(event);
94
- }
95
- wake?.();
96
- wake = null;
97
- });
98
- socket.on("error", error => {
99
- failure = signal?.aborted
100
- ? signal.reason instanceof Error ? signal.reason : new Error("The operation was cancelled")
101
- : error;
102
- wake?.();
103
- wake = null;
104
- });
105
- socket.on("close", () => {
106
- ended = true;
107
- signal?.removeEventListener("abort", cancel);
108
- wake?.();
109
- wake = null;
110
- });
111
- 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);
112
30
  try {
113
- while (true) {
114
- if (events.length) {
115
- yield events.shift();
116
- continue;
117
- }
118
- if (failure)
119
- throw failure;
120
- if (ended)
121
- return;
122
- 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);
123
80
  }
81
+ catch { /* One local observer cannot break boundary delivery. */ }
124
82
  }
125
- finally {
126
- signal?.removeEventListener("abort", cancel);
127
- 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();
128
103
  }
129
- })();
104
+ };
130
105
  }
131
- function unavailable(path) {
132
- 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);
133
115
  }
@@ -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.13",
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.37",
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
  },