@phreshos/server 0.1.0

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.js ADDED
@@ -0,0 +1,92 @@
1
+ export const defaultTimeout = 10_000;
2
+ /**
3
+ * SDK-owned subscription behavior over one boundary registration source.
4
+ *
5
+ * The boundary owns forwarding state only. Promises, deadlines, queues and
6
+ * callbacks remain here, inside the endpoint that requested the data.
7
+ */
8
+ export default class Events {
9
+ listen;
10
+ listenAll;
11
+ constructor(listen, listenAll) {
12
+ this.listen = listen;
13
+ this.listenAll = listenAll;
14
+ }
15
+ subscribe(event, subscriber) {
16
+ return this.listen(event, subscriber);
17
+ }
18
+ waitFor(event, timeout = defaultTimeout) {
19
+ return new Promise((resolve, reject) => {
20
+ let settled = false;
21
+ let stop = () => undefined;
22
+ const finish = (settle) => {
23
+ if (settled)
24
+ return;
25
+ settled = true;
26
+ clearTimeout(timer);
27
+ stop();
28
+ settle();
29
+ };
30
+ const timer = setTimeout(() => {
31
+ finish(() => reject(new Error(`Event promise timeout ${timeout}ms`)));
32
+ }, timeout);
33
+ stop = this.listen(event, message => finish(() => resolve(message)), error => finish(() => reject(error)));
34
+ });
35
+ }
36
+ events(event, options = {}) {
37
+ const capacity = options.capacity ?? 64;
38
+ if (capacity !== Infinity && (!Number.isInteger(capacity) || capacity < 0)) {
39
+ throw new Error("An event queue capacity must be a non-negative integer or Infinity");
40
+ }
41
+ const listen = this.listen;
42
+ return (async function* () {
43
+ const queue = [];
44
+ let ended = false;
45
+ let failure = null;
46
+ let wake = null;
47
+ const stop = listen(event, message => {
48
+ if (ended || failure)
49
+ return;
50
+ if (queue.length >= capacity) {
51
+ failure = new Error(`Event queue exceeded its capacity of ${capacity}`);
52
+ }
53
+ else {
54
+ queue.push(message);
55
+ }
56
+ wake?.();
57
+ wake = null;
58
+ }, error => {
59
+ if (ended || failure)
60
+ return;
61
+ failure = error;
62
+ wake?.();
63
+ wake = null;
64
+ });
65
+ const abort = () => {
66
+ ended = true;
67
+ wake?.();
68
+ wake = null;
69
+ };
70
+ options.signal?.addEventListener("abort", abort, { once: true });
71
+ try {
72
+ while (!ended) {
73
+ if (queue.length) {
74
+ yield queue.shift();
75
+ continue;
76
+ }
77
+ if (failure)
78
+ throw failure;
79
+ await new Promise(resolve => { wake = resolve; });
80
+ }
81
+ }
82
+ finally {
83
+ ended = true;
84
+ stop();
85
+ options.signal?.removeEventListener("abort", abort);
86
+ }
87
+ })();
88
+ }
89
+ observe(observer) {
90
+ return this.listenAll((event, message) => observer({ event, message }));
91
+ }
92
+ }
package/dist/host.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { Exit, Layer, Position, ServedFile, Size, Subscribable } from "@phreshos/core";
2
+ import { type Process, type Program } from "./domain.js";
3
+ export type ServerDescription = Readonly<{
4
+ location: string;
5
+ start?: boolean;
6
+ installCommand?: string;
7
+ startCommand: string;
8
+ }>;
9
+ export type ClientDescription = Readonly<{
10
+ location: string;
11
+ start?: boolean;
12
+ title?: string;
13
+ size?: Size;
14
+ position?: Position;
15
+ layer?: Layer;
16
+ minimize?: boolean;
17
+ }>;
18
+ type Description = Readonly<{
19
+ identity: string;
20
+ name?: string;
21
+ version?: string;
22
+ description?: string;
23
+ apiDocs?: string;
24
+ icons?: string;
25
+ storage: string;
26
+ }>;
27
+ export type ProgramDescription = Description & (Readonly<{
28
+ server: ServerDescription;
29
+ client?: ClientDescription;
30
+ }> | Readonly<{
31
+ server?: ServerDescription;
32
+ client: ClientDescription;
33
+ }>);
34
+ export type HostServerStop = Omit<Exit, "status"> & Readonly<{
35
+ process: Process;
36
+ }>;
37
+ export type HostProgramUninstall = Readonly<{
38
+ program: Program;
39
+ everythingRemoved: boolean;
40
+ }>;
41
+ export type HostProcessExit = Exit & Readonly<{
42
+ process: Process;
43
+ }>;
44
+ export type HostEvents = {
45
+ serverStart: Process;
46
+ serverStop: HostServerStop;
47
+ clientStart: Process;
48
+ clientStop: Process;
49
+ programCreate: Program;
50
+ programForget: Program;
51
+ programInstall: Program;
52
+ programUninstall: HostProgramUninstall;
53
+ processCreate: Process;
54
+ processExit: HostProcessExit;
55
+ };
56
+ export interface Host<Events extends object = {}> extends Subscribable<HostEvents & Events, never> {
57
+ serve(value: unknown): Promise<ServedFile>;
58
+ programs(onlyInstalled?: boolean): Promise<Program[]>;
59
+ getProgram(identity: string): Promise<Program>;
60
+ createProgram(source: ProgramDescription | string): Promise<Program>;
61
+ processes(): Promise<Process[]>;
62
+ getProcess(identity: string): Promise<Process>;
63
+ }
64
+ export declare const host: Host;
65
+ export {};
package/dist/host.js ADDED
@@ -0,0 +1,54 @@
1
+ import Events from "./events.js";
2
+ import { exit, process, program } from "./domain.js";
3
+ import serve from "./served.js";
4
+ import wire from "./wire.js";
5
+ class ServerHost extends Events {
6
+ constructor() {
7
+ super((event, listener, impossible) => wire.on("host-end", event, (...values) => listener(hostEvent(event, values)), null, impossible), observer => wire.onAll("host-end", (event, ...values) => {
8
+ if (typeof event === "string")
9
+ observer(event, hostEvent(event, values));
10
+ }));
11
+ }
12
+ serve(value) { return serve(value); }
13
+ async programs(onlyInstalled = false) {
14
+ const answer = await wire.request(["programs", onlyInstalled]);
15
+ return answer[0].map(program);
16
+ }
17
+ async getProgram(identity) {
18
+ const answer = await wire.request(["program", identity]);
19
+ return program(answer[0]);
20
+ }
21
+ async createProgram(source) {
22
+ const answer = await wire.request(["create-program", source]);
23
+ return program(answer[0]);
24
+ }
25
+ async processes() {
26
+ const answer = await wire.request(["processes"]);
27
+ return answer[0].map(process);
28
+ }
29
+ async getProcess(identity) {
30
+ const answer = await wire.request(["process", identity]);
31
+ return process(answer[0]);
32
+ }
33
+ }
34
+ function hostEvent(event, values) {
35
+ if (event === "serverStart" || event === "clientStart" || event === "clientStop" || event === "processCreate") {
36
+ return process(values[1]);
37
+ }
38
+ if (event === "serverStop") {
39
+ return { process: process(values[1]), code: numberOrNull(values[2]), signal: stringOrNull(values[3]) };
40
+ }
41
+ if (event === "processExit") {
42
+ return { process: process(values[1]), ...exit(values[2], values[3]) };
43
+ }
44
+ if (event === "programCreate" || event === "programForget" || event === "programInstall") {
45
+ return program(values[1]);
46
+ }
47
+ if (event === "programUninstall") {
48
+ return { program: program(values[1]), everythingRemoved: values[2] === true };
49
+ }
50
+ return values[0];
51
+ }
52
+ function numberOrNull(value) { return typeof value === "number" ? value : null; }
53
+ function stringOrNull(value) { return typeof value === "string" ? value : null; }
54
+ export const host = new ServerHost();
package/dist/main.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { host, type Host, type ClientDescription, type HostEvents, type HostProcessExit, type HostProgramUninstall, type HostServerStop, type ProgramDescription, type ServerDescription } from "./host.js";
2
+ export { current, type Current, type CurrentClient } from "./current.js";
3
+ export { type Answerer, type Channel } from "./channel.js";
4
+ export { Client, Endpoint, Process, Program, Server, Window, type ProgramArea } from "./domain.js";
5
+ export type { AnswerCapture, AnswerMessage, AnswerObserver, Askable, AskCapture, AskMessage, AskObserver, Capture, Captures, ChannelCapture, ChannelEvents, ChannelMessage, ClientTraffic, ClientDeclaration, Cleanup, DirectoryStat, EndpointDeclaration, EndpointTraffic, EntryStat, EventMessage, EventName, EventObserver, EventOptions, EventSubscriber, Exit, FileStat, Launch, LaunchClient, Layer, LogKind, LogRecord, LogSource, Message, OtherStat, Outcome, Position, ProgramEvents, ProgramProcessExit, ProgramServerStop, ProgramSql, ProgramStore, ProcessEvents, Publishable, ServedFile, ServerTraffic, Size, Subscribable, TimedAskable, TrafficCapture, TrafficEvents, TrafficMessage, Value, WindowEvents, WindowState } from "@phreshos/core";
package/dist/main.js ADDED
@@ -0,0 +1,4 @@
1
+ export { host } from "./host.js";
2
+ export { current } from "./current.js";
3
+ export {} from "./channel.js";
4
+ export { Client, Endpoint, Process, Program, Server, Window } from "./domain.js";
@@ -0,0 +1,3 @@
1
+ import type { ServedFile } from "@phreshos/core";
2
+ /** Writes one public value directly into the host-managed served-file area. */
3
+ export default function serve(value: unknown): Promise<ServedFile>;
package/dist/served.js ADDED
@@ -0,0 +1,41 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createWriteStream, mkdirSync, statSync } from "node:fs";
3
+ import { rename, rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { content } from "./storage.js";
8
+ import wire from "./wire.js";
9
+ /** Writes one public value directly into the host-managed served-file area. */
10
+ export default async function serve(value) {
11
+ const answer = await wire.request(["serve"]);
12
+ const [root, limit] = answer;
13
+ const source = content(value);
14
+ const identity = randomUUID();
15
+ const file = `${identity}.${source.extension}`;
16
+ const temporary = join(root, `.${identity}.serving`);
17
+ const destination = join(root, file);
18
+ let size = 0;
19
+ mkdirSync(root, { recursive: true });
20
+ try {
21
+ await pipeline(Readable.fromWeb(source.stream), async function* (chunks) {
22
+ for await (const chunk of chunks) {
23
+ size += chunk.byteLength;
24
+ if (size > limit)
25
+ throw new Error(`The served value exceeds ${limit / 1024 / 1024 / 1024} GB`);
26
+ yield chunk;
27
+ }
28
+ }, createWriteStream(temporary, { flags: "wx" }));
29
+ await rename(temporary, destination);
30
+ }
31
+ catch (error) {
32
+ await rm(temporary, { force: true }).catch(() => undefined);
33
+ throw error;
34
+ }
35
+ return {
36
+ file,
37
+ type: source.type,
38
+ size,
39
+ time: Math.round(statSync(destination).mtimeMs)
40
+ };
41
+ }
@@ -0,0 +1,14 @@
1
+ import type { ProgramArea, ProgramSql, ProgramStore } from "@phreshos/core";
2
+ export interface ServerArea extends ProgramArea {
3
+ path(): Promise<string>;
4
+ resolve(...path: string[]): Promise<string>;
5
+ }
6
+ /** Server-local implementation of one Program-owned filesystem area. */
7
+ export declare function area(program: string, which: "data" | "cache"): ServerArea;
8
+ export declare function store(program: string): ProgramStore;
9
+ export declare function sql(kind: "database" | "logs", program: string): ProgramSql;
10
+ export declare function content(value: unknown): {
11
+ stream: ReadableStream<Uint8Array>;
12
+ extension: string;
13
+ type: string;
14
+ };
@@ -0,0 +1,172 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createReadStream, createWriteStream, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
3
+ import { rm } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ import wire from "./wire.js";
8
+ /** Server-local implementation of one Program-owned filesystem area. */
9
+ export function area(program, which) {
10
+ async function path() {
11
+ const answer = await wire.request([which, program, "path"]);
12
+ return answer[0];
13
+ }
14
+ async function resolve(...parts) {
15
+ const root = await path();
16
+ const destination = join(root, ...parts);
17
+ const step = relative(root, destination);
18
+ if (step === ".." || step.startsWith(`..${sep}`) || isAbsolute(step)) {
19
+ throw new Error("A storage path may not leave its area");
20
+ }
21
+ return destination;
22
+ }
23
+ async function stream(...parts) {
24
+ const destination = await resolve(...parts);
25
+ const found = describe(destination);
26
+ if (!found)
27
+ throw new Error(`There is no ${parts.join("/")} in this Program's ${which}`);
28
+ if (found.kind !== "file")
29
+ throw new Error(`${parts.join("/")} is not a file`);
30
+ return Readable.toWeb(createReadStream(destination));
31
+ }
32
+ async function write(...args) {
33
+ if (args.length < 2)
34
+ throw new Error("Writing takes a file name and what to write");
35
+ const parts = args.slice(0, -1);
36
+ const destination = await resolve(...parts);
37
+ const temporary = join(dirname(destination), `.${randomUUID()}.writing`);
38
+ mkdirSync(dirname(destination), { recursive: true });
39
+ try {
40
+ await pipeline(Readable.fromWeb(content(args.at(-1)).stream), createWriteStream(temporary, { flags: "wx" }));
41
+ renameSync(temporary, destination);
42
+ }
43
+ catch (error) {
44
+ await rm(temporary, { force: true }).catch(() => undefined);
45
+ throw error;
46
+ }
47
+ }
48
+ return {
49
+ path,
50
+ resolve,
51
+ stream,
52
+ async bytes(...parts) {
53
+ return new Uint8Array(await new Response(await stream(...parts)).arrayBuffer());
54
+ },
55
+ async text(...parts) {
56
+ return new Response(await stream(...parts)).text();
57
+ },
58
+ async json(...parts) {
59
+ return JSON.parse(await new Response(await stream(...parts)).text());
60
+ },
61
+ write,
62
+ async stat(...parts) {
63
+ return describe(await resolve(...parts));
64
+ },
65
+ async list(...parts) {
66
+ return readdirSync(await resolve(...parts)).sort();
67
+ },
68
+ async delete(...parts) {
69
+ if (!parts.length)
70
+ throw new Error("Emptying a place is clear, not delete");
71
+ rmSync(await resolve(...parts), { recursive: true, force: true });
72
+ },
73
+ async clear() {
74
+ const root = await path();
75
+ rmSync(root, { recursive: true, force: true });
76
+ mkdirSync(root, { recursive: true });
77
+ }
78
+ };
79
+ }
80
+ export function store(program) {
81
+ async function ask(operation, ...values) {
82
+ const answer = await wire.request(["store", program, operation, ...values]);
83
+ return answer[0];
84
+ }
85
+ return {
86
+ get: (key) => ask("get", key),
87
+ set: (key, value, ttl) => ask("set", key, value, ttl),
88
+ delete: (key) => ask("delete", key),
89
+ has: (key) => ask("has", key),
90
+ clear: () => ask("clear")
91
+ };
92
+ }
93
+ export function sql(kind, program) {
94
+ return {
95
+ async query(statement, ...rest) {
96
+ const [text, values] = written(statement, rest);
97
+ const answer = await wire.request([kind, program, text, values]);
98
+ return answer[0];
99
+ }
100
+ };
101
+ }
102
+ export function content(value) {
103
+ const binary = "application/octet-stream";
104
+ if (typeof File !== "undefined" && value instanceof File) {
105
+ const type = value.type || binary;
106
+ return { stream: value.stream(), extension: extension(value.name, type), type };
107
+ }
108
+ if (value instanceof Blob) {
109
+ const type = value.type || binary;
110
+ return { stream: value.stream(), extension: extension("", type), type };
111
+ }
112
+ if (value instanceof ReadableStream)
113
+ return { stream: value, extension: "bin", type: binary };
114
+ if (typeof value === "string")
115
+ return { stream: new Blob([value]).stream(), extension: "txt", type: "text/plain" };
116
+ if (value instanceof ArrayBuffer)
117
+ return { stream: new Blob([value]).stream(), extension: "bin", type: binary };
118
+ if (ArrayBuffer.isView(value)) {
119
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
120
+ return { stream: new Blob([bytes.slice()]).stream(), extension: "bin", type: binary };
121
+ }
122
+ const json = JSON.stringify(value);
123
+ if (json === undefined)
124
+ throw new Error("This value cannot be written as JSON");
125
+ return { stream: new Blob([json]).stream(), extension: "json", type: "application/json" };
126
+ }
127
+ function describe(path) {
128
+ let found;
129
+ try {
130
+ found = statSync(path);
131
+ }
132
+ catch (error) {
133
+ if (error.code === "ENOENT")
134
+ return null;
135
+ throw error;
136
+ }
137
+ const modifiedAt = Math.round(found.mtimeMs);
138
+ if (found.isFile())
139
+ return { kind: "file", size: found.size, modifiedAt };
140
+ if (found.isDirectory())
141
+ return { kind: "directory", modifiedAt };
142
+ return { kind: "other", modifiedAt };
143
+ }
144
+ function written(statement, rest) {
145
+ if (typeof statement === "string")
146
+ return [statement, Array.isArray(rest[0]) ? rest[0] : []];
147
+ return [statement.raw.join("?"), rest];
148
+ }
149
+ const extensions = {
150
+ "application/gzip": "gz",
151
+ "application/javascript": "js",
152
+ "application/json": "json",
153
+ "application/pdf": "pdf",
154
+ "application/wasm": "wasm",
155
+ "application/zip": "zip",
156
+ "audio/mpeg": "mp3",
157
+ "image/gif": "gif",
158
+ "image/jpeg": "jpg",
159
+ "image/png": "png",
160
+ "image/svg+xml": "svg",
161
+ "image/webp": "webp",
162
+ "text/css": "css",
163
+ "text/csv": "csv",
164
+ "text/html": "html",
165
+ "text/javascript": "js",
166
+ "text/plain": "txt",
167
+ "video/mp4": "mp4"
168
+ };
169
+ function extension(name, type) {
170
+ const named = name.match(/\.([A-Za-z0-9]+)$/)?.[1];
171
+ return named?.toLowerCase() ?? extensions[type.split(";", 1)[0].toLowerCase()] ?? "bin";
172
+ }
package/dist/wire.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ import type { Cleanup } from "@phreshos/core";
2
+ import Deadline from "./deadline.js";
3
+ type Handler = (...values: unknown[]) => unknown;
4
+ type Failure = (error: Error) => void;
5
+ type TrafficKind = "publish" | "ask" | "answer";
6
+ /** The server endpoint's sole IPC adapter. */
7
+ declare class Wire {
8
+ private readonly pending;
9
+ private readonly subscribers;
10
+ private readonly every;
11
+ private readonly answerers;
12
+ private readonly waiting;
13
+ private readonly impossible;
14
+ readonly identity: Promise<{
15
+ process: string;
16
+ program: string;
17
+ }>;
18
+ constructor();
19
+ send(route: string, ...values: unknown[]): void;
20
+ request(values: unknown[], timeout?: number): Promise<unknown>;
21
+ requestWithin(values: unknown[], deadline: Deadline): Promise<unknown>;
22
+ expectWithin(question: string, deadline: Deadline): Promise<unknown>;
23
+ forget(question: string): void;
24
+ on(route: string, event: string, handler: Handler, subject?: string | null, impossible?: Failure): Cleanup;
25
+ onAll(route: string, handler: Handler, subject?: string | null): Cleanup;
26
+ answer(route: string, event: string, handler: Handler): Cleanup;
27
+ observe(target: string, half: "server" | "client", kind: TrafficKind, event: string | null, handler: Handler, impossible?: Failure): Cleanup;
28
+ private register;
29
+ private unregister;
30
+ private deliver;
31
+ private receiveQuestion;
32
+ private sendAnswer;
33
+ private releaseWaiting;
34
+ private forgetIncoming;
35
+ private settle;
36
+ }
37
+ declare const _default: Wire;
38
+ export default _default;