@phreshos/node 0.1.16 → 0.1.18

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/README.md CHANGED
@@ -27,6 +27,9 @@ cross the connection boundary.
27
27
  | Bun | `bun add @phreshos/node` |
28
28
  | Yarn | `yarn add @phreshos/node` |
29
29
 
30
+ `@phreshos/core` is a peer dependency and the single import path for shared
31
+ System and runtime contracts.
32
+
30
33
  ```ts
31
34
  import { Project, System } from "@phreshos/node"
32
35
 
@@ -50,8 +53,14 @@ bun install --frozen-lockfile
50
53
  bun run verify
51
54
  ```
52
55
 
53
- `verify` checks the types, builds the package, and runs the connection and
54
- Project tests.
56
+ `verify` checks the types, builds the package, runs the connection and Project
57
+ tests, and validates the published package shape independently.
58
+
59
+ `check` performs static checks, `build` creates distributable output, and `test`
60
+ runs Vitest assertions from `tests/`. Run `build` before testing built artifacts.
61
+ `verify` runs `check`, `build`, and `test` in order. Operational tooling belongs
62
+ in `scripts/`; tests and their fixtures belong in `tests/`. Verification uses
63
+ the committed dependency graph without local package substitutions.
55
64
 
56
65
  ## Related repositories
57
66
 
@@ -0,0 +1,8 @@
1
+ import type { WritableContent } from "@phreshos/core";
2
+ /** Encodes one writable value without assigning it to Storage or Uploads. */
3
+ export declare function content(value: WritableContent): EncodedContent;
4
+ export interface EncodedContent {
5
+ stream: ReadableStream<Uint8Array>;
6
+ extension: string;
7
+ type: string;
8
+ }
@@ -0,0 +1,36 @@
1
+ /** Encodes one writable value without assigning it to Storage or Uploads. */
2
+ export function content(value) {
3
+ const binary = "application/octet-stream";
4
+ if (typeof File !== "undefined" && value instanceof File) {
5
+ const type = value.type || binary;
6
+ return { stream: value.stream(), extension: extension(value.name, type), type };
7
+ }
8
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
9
+ const type = value.type || binary;
10
+ return { stream: value.stream(), extension: extension("", type), type };
11
+ }
12
+ if (value instanceof ReadableStream)
13
+ return { stream: value, extension: "bin", type: binary };
14
+ if (typeof value === "string")
15
+ return { stream: new Blob([value]).stream(), extension: "txt", type: "text/plain" };
16
+ if (value instanceof ArrayBuffer)
17
+ return { stream: new Blob([value]).stream(), extension: "bin", type: binary };
18
+ if (ArrayBuffer.isView(value)) {
19
+ const bytes = new Uint8Array(value.byteLength);
20
+ bytes.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
21
+ return { stream: new Blob([bytes]).stream(), extension: "bin", type: binary };
22
+ }
23
+ const json = JSON.stringify(value);
24
+ if (json === undefined)
25
+ throw new Error("Writable content must have a JSON representation");
26
+ return { stream: new Blob([json]).stream(), extension: "json", type: "application/json" };
27
+ }
28
+ const extensions = {
29
+ "application/gzip": "gz", "application/javascript": "js", "application/json": "json", "application/pdf": "pdf",
30
+ "application/wasm": "wasm", "application/zip": "zip", "audio/mpeg": "mp3", "image/gif": "gif", "image/jpeg": "jpg",
31
+ "image/png": "png", "image/svg+xml": "svg", "image/webp": "webp", "text/css": "css", "text/csv": "csv",
32
+ "text/html": "html", "text/javascript": "js", "text/plain": "txt", "video/mp4": "mp4"
33
+ };
34
+ function extension(name, type) {
35
+ return name.match(/\.([A-Za-z0-9]+)$/)?.[1]?.toLowerCase() ?? extensions[type.split(";", 1)[0].toLowerCase()] ?? "bin";
36
+ }
package/dist/events.d.ts CHANGED
@@ -1,14 +1,15 @@
1
- import type { Cleanup, EventOptions, Subscribable } from "@phreshos/core";
1
+ import { subscribableDefinition, type Cleanup, type EventOptions, type Subscribable, type SubscribableDefinition } from "@phreshos/core";
2
2
  type Failure = (error: Error) => void;
3
3
  type Register<Message> = (subscriber: (message: Message) => unknown, impossible?: Failure) => Cleanup;
4
4
  type Subscribe = (event: string | null, subscriber: (message: unknown) => unknown, impossible?: Failure) => Cleanup;
5
5
  /** Adapts one live representation source into the shared Subscribable contract. */
6
- export default class Events<Definitions extends object, Fallback = never> {
6
+ export default class Events<Definitions extends object, Fallback = never> implements Subscribable<Definitions, Fallback> {
7
7
  private readonly names;
8
8
  private readonly register;
9
+ readonly [subscribableDefinition]?: SubscribableDefinition<Definitions, Fallback>;
9
10
  constructor(names: readonly string[], register: Subscribe);
10
11
  readonly subscribe: Subscribable<Definitions, Fallback>["subscribe"];
11
- readonly waitFor: Subscribable<Definitions, Fallback>["waitFor"];
12
+ readonly wait: Subscribable<Definitions, Fallback>["wait"];
12
13
  readonly events: Subscribable<Definitions, Fallback>["events"];
13
14
  private listen;
14
15
  }
package/dist/events.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { subscribableDefinition } from "@phreshos/core";
1
2
  /** Adapts one live representation source into the shared Subscribable contract. */
2
3
  export default class Events {
3
4
  names;
@@ -19,7 +20,7 @@ export default class Events {
19
20
  eventOrSubscriber({ event: capture.event, message: capture.payload });
20
21
  });
21
22
  });
22
- waitFor = ((event, timeout = 10_000) => new Promise((resolve, reject) => {
23
+ wait = ((event, timeout = 10_000) => new Promise((resolve, reject) => {
23
24
  let stop = () => undefined;
24
25
  const timer = setTimeout(() => {
25
26
  stop();
package/dist/main.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { gatewayAddress } from "./address.js";
2
- export { ClientEndpoint, ClientService, Endpoint, Process, Program, ServerEndpoint, ServerService, System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
3
- export { Service, clientPermissionCatalog, isPermissionName, type ClientLaunch, type Launch, type Permission, type PermissionDefinition, type PermissionDefinitions, type PermissionInput, type PermissionName, type PermissionRequest, type PermissionValue, type PermissionValueDomain, type Permissions, type ProgramPermissions, type ProgramDefinition, type ServerLaunch, type ServiceKey, type ShellEvent, type ShellOptions, type ProgramStartup, type Storage, } from "@phreshos/core";
2
+ export { System } from "./system.js";
4
3
  export { resolveHome } from "./home.js";
5
4
  export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions } from "./project.js";
package/dist/main.js CHANGED
@@ -1,5 +1,4 @@
1
1
  export { gatewayAddress } from "./address.js";
2
- export { ClientEndpoint, ClientService, Endpoint, Process, Program, ServerEndpoint, ServerService, System } from "./system.js";
3
- export { Service, clientPermissionCatalog, isPermissionName, } from "@phreshos/core";
2
+ export { System } from "./system.js";
4
3
  export { resolveHome } from "./home.js";
5
4
  export { Project } from "./project.js";
@@ -0,0 +1,3 @@
1
+ import type { Network } from "@phreshos/core";
2
+ /** Networking shares the lifetime of its owning System connection. */
3
+ export default function network(signal: () => AbortSignal): Network;
@@ -0,0 +1,13 @@
1
+ import websocket from "./websocket.js";
2
+ /** Networking shares the lifetime of its owning System connection. */
3
+ export default function network(signal) {
4
+ return {
5
+ async fetch(input, init) {
6
+ const request = new Request(input, init);
7
+ return fetch(request, { signal: AbortSignal.any([request.signal, signal()]) });
8
+ },
9
+ websocket(url, protocols) {
10
+ return websocket(url, protocols, signal());
11
+ }
12
+ };
13
+ }
package/dist/project.js CHANGED
@@ -61,34 +61,45 @@ export class Project {
61
61
  if (mode === "development" && !config.server?.development && !config.client?.development) {
62
62
  throw new Error("Nothing here says how this Program is developed");
63
63
  }
64
- return {
64
+ const definition = {
65
65
  identity: config.identity,
66
66
  name: config.name,
67
67
  version: config.version,
68
68
  description: config.description,
69
+ categories: config.categories,
70
+ keywords: config.keywords,
71
+ website: config.website,
69
72
  icon: config.icon && resolve(this.directory, config.icon),
70
73
  agent: config.agent && resolve(this.directory, config.agent),
71
- storage: resolve(this.directory, "storage"),
72
- ...server && { server: {
73
- location: resolve(this.directory, server.location),
74
- start: server.start,
75
- service: server.service,
76
- installCommand: config.server?.installCommand,
77
- uninstallCommand: config.server?.uninstallCommand,
78
- ...serverExecution(server)
79
- } },
80
- ...client && { client: {
81
- location: /^https?:\/\//i.test(client.location) ? client.location : resolve(this.directory, client.location),
82
- start: client.start,
83
- service: client.service,
84
- title: config.client?.title,
85
- size: config.client?.size,
86
- position: config.client?.position,
87
- layer: config.client?.layer,
88
- minimize: config.client?.minimize,
89
- permissions: config.client?.permissions
90
- } }
74
+ storage: resolve(this.directory, "storage")
91
75
  };
76
+ const serverDefinition = server ? {
77
+ location: resolve(this.directory, server.location),
78
+ start: server.start,
79
+ service: server.service,
80
+ installCommand: config.server?.installCommand,
81
+ uninstallCommand: config.server?.uninstallCommand,
82
+ ...serverExecution(server)
83
+ } : null;
84
+ const clientDefinition = client ? {
85
+ location: /^https?:\/\//i.test(client.location) ? client.location : resolve(this.directory, client.location),
86
+ start: client.start,
87
+ service: client.service,
88
+ title: config.client?.title,
89
+ size: config.client?.size,
90
+ position: config.client?.position,
91
+ layer: config.client?.layer,
92
+ minimize: config.client?.minimize,
93
+ maximize: config.client?.maximize,
94
+ permissions: config.client?.permissions
95
+ } : null;
96
+ if (serverDefinition && clientDefinition)
97
+ return { ...definition, server: serverDefinition, client: clientDefinition };
98
+ if (serverDefinition)
99
+ return { ...definition, server: serverDefinition };
100
+ if (clientDefinition)
101
+ return { ...definition, client: clientDefinition };
102
+ throw new Error("A Program must define a Server, a Client, or both");
92
103
  }
93
104
  /** Run the optional author-owned production build command. */
94
105
  async build() {
@@ -135,7 +146,7 @@ export class Project {
135
146
  }
136
147
  async prepareDevelopment(system, development, options) {
137
148
  const client = await DevelopmentClient.prepare(development, this.directory);
138
- const program = await system.forceCreateProgram(this.definition("development", client.url));
149
+ const program = await system.program.forceCreate(this.definition("development", client.url));
139
150
  try {
140
151
  await client.start(program.assetId, options.signal);
141
152
  const lifecycle = program.process.run({ options: options.options ?? {} }, { signal: client.processSignal(options.signal) });
@@ -150,7 +161,7 @@ export class Project {
150
161
  /** Build this Project and return its Program installation generator. */
151
162
  async install(system) {
152
163
  await this.build();
153
- const program = await system.forceCreateProgram(this.productionDefinition());
164
+ const program = await system.program.forceCreate(this.productionDefinition());
154
165
  return program.install();
155
166
  }
156
167
  /** Build and package this Program into its canonical release shape. */
@@ -184,7 +195,7 @@ export class Project {
184
195
  return Object.freeze({ archive, archivePath, checksumPath, declarationPath, digest });
185
196
  }
186
197
  async run(system, definition, options) {
187
- const program = await system.forceCreateProgram(definition);
198
+ const program = await system.program.forceCreate(definition);
188
199
  return program.process.run({ options: options.options ?? {} }, { signal: options.signal });
189
200
  }
190
201
  }
@@ -326,6 +337,7 @@ function packageDefinition(config, version) {
326
337
  position: config.client.position,
327
338
  layer: config.client.layer,
328
339
  minimize: config.client.minimize,
340
+ maximize: config.client.maximize,
329
341
  permissions: config.client.permissions
330
342
  } }
331
343
  };
@@ -1,32 +1,17 @@
1
- import type { Appearance, ClientDeclaration, EndpointDeclaration, ServiceKey, WindowGeometry, WindowLayer } from "@phreshos/core";
1
+ import { type Appearance, type ProcessSnapshot, type ProgramSnapshot, type ServiceKey, type WindowState as CoreWindowState } from "@phreshos/core";
2
2
  import type { GatewayConnection } from "./transport.js";
3
- export interface ProgramState {
4
- reference: string;
5
- identity: string;
6
- assetId: string;
3
+ export type ProgramState = ProgramSnapshot & Readonly<{
7
4
  installed: boolean;
8
- name: string;
9
- version: string | null;
10
- description: string | null;
11
- hasAgent: boolean;
12
- server: EndpointDeclaration | null;
13
- client: ClientDeclaration | null;
14
- }
15
- export interface WindowState {
16
- title: string;
17
- position: WindowGeometry["position"];
18
- size: WindowGeometry["size"];
5
+ }>;
6
+ export type WindowState = Omit<CoreWindowState, "front"> & Readonly<{
19
7
  depth: number;
20
- minimized: boolean;
21
- layer: WindowLayer;
22
- location: string;
23
- }
8
+ }>;
24
9
  export interface ProcessIdentityState {
25
10
  reference: string;
26
11
  identity: string;
27
12
  name: string | null;
28
13
  program: string;
29
- options: Record<string, string>;
14
+ options: ProcessSnapshot["options"];
30
15
  startedAt: Date;
31
16
  }
32
17
  export interface ProcessState extends ProcessIdentityState {
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { parseProgramSnapshot, parseAppearance } from "@phreshos/core";
2
3
  const maximumStreamQueue = 256;
3
4
  /** A connection-owned, live representation of the authoritative System model. */
4
5
  export default class SystemRepresentation {
@@ -13,7 +14,7 @@ export default class SystemRepresentation {
13
14
  this.connection = connection;
14
15
  const session = ownerSession(connection.session);
15
16
  this.authorization = session.authorization;
16
- this.appearance = session.linkManager.appearance.value;
17
+ this.appearance = parseAppearance(session.linkManager.appearance.value);
17
18
  for (const [, value] of session.authManager.programManager.programs) {
18
19
  const program = programState(value);
19
20
  this.programs.set(program.identity, program);
@@ -133,7 +134,7 @@ export default class SystemRepresentation {
133
134
  followModel(appearance) {
134
135
  const subscribe = (event, listener) => this.release.push(this.connection.subscribe(event, listener));
135
136
  subscribe(`property-update:${appearance}`, value => {
136
- this.appearance = value;
137
+ this.appearance = parseAppearance(value);
137
138
  this.emit("appearance", this.appearance);
138
139
  });
139
140
  subscribe("/auth/program/create", value => this.arriveProgram("create", value));
@@ -148,7 +149,7 @@ export default class SystemRepresentation {
148
149
  subscribe("/auth/process/client-stop", (identity, value) => this.changeEndpoint(identity, "client", value, false));
149
150
  subscribe("/auth/process/client-access", (identity, value) => this.changeEndpoint(identity, "client", value));
150
151
  subscribe("/auth/process/exited", (value, code, signal) => this.exitProcess(value, code, signal));
151
- for (const event of ["move", "resize", "geometry", "change-title", "raise", "minimize"]) {
152
+ for (const event of ["move", "resize", "geometry", "change-title", "raise", "minimize", "maximize"]) {
152
153
  subscribe(`/auth/process/${event}`, value => this.changeWindow(event, value));
153
154
  }
154
155
  }
@@ -253,21 +254,10 @@ function ownerSession(value) {
253
254
  };
254
255
  }
255
256
  function programState(value) {
256
- if (!record(value) || typeof value.reference !== "string" || typeof value.identity !== "string" || typeof value.assetId !== "string" || typeof value.name !== "string") {
257
- throw new Error("The System returned an invalid Program");
258
- }
259
- return {
260
- reference: value.reference,
261
- identity: value.identity,
262
- assetId: value.assetId,
263
- installed: value.installed === true,
264
- name: value.name,
265
- version: typeof value.version === "string" ? value.version : null,
266
- description: typeof value.description === "string" ? value.description : null,
267
- hasAgent: value.hasAgent === true,
268
- server: value.server,
269
- client: value.client
270
- };
257
+ const parsed = parseProgramSnapshot(value);
258
+ if (parsed.installed === undefined)
259
+ throw new Error("The System returned a Program without installation state");
260
+ return { ...parsed, installed: parsed.installed };
271
261
  }
272
262
  function processState(value) {
273
263
  const identity = processIdentityState(value);
@@ -307,7 +297,7 @@ export function processIdentityState(value) {
307
297
  };
308
298
  }
309
299
  function windowState(value) {
310
- if (!record(value) || typeof value.title !== "string" || typeof value.location !== "string" || typeof value.depth !== "number" || typeof value.minimized !== "boolean") {
300
+ if (!record(value) || typeof value.title !== "string" || typeof value.depth !== "number" || typeof value.minimized !== "boolean" || typeof value.maximized !== "boolean") {
311
301
  throw new Error("The System returned an invalid Window");
312
302
  }
313
303
  return {
@@ -316,8 +306,8 @@ function windowState(value) {
316
306
  size: value.size,
317
307
  depth: value.depth,
318
308
  minimized: value.minimized,
319
- layer: value.layer,
320
- location: value.location
309
+ maximized: value.maximized,
310
+ layer: value.layer
321
311
  };
322
312
  }
323
313
  function windowMessage(event, process) {
@@ -332,6 +322,8 @@ function windowMessage(event, process) {
332
322
  return window.title;
333
323
  if (event === "minimize")
334
324
  return window.minimized;
325
+ if (event === "maximize")
326
+ return window.maximized;
335
327
  return true;
336
328
  }
337
329
  function camel(value) { return value === "change-title" ? "changeTitle" : value; }
package/dist/storage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Storage } from "@phreshos/core";
1
+ import { Storage } from "@phreshos/core";
2
2
  /** Create one filesystem implementation bounded beneath a resolved absolute root. */
3
3
  export declare function filesystemStorage(source: string | (() => Promise<string>), label: string, lifetime?: Lifetime): Storage;
4
4
  /** Create native filesystem access entered from one resolved absolute path. */
package/dist/storage.js CHANGED
@@ -1,111 +1,244 @@
1
+ import { Storage, StorageFile } from "@phreshos/core";
1
2
  import { randomUUID } from "node:crypto";
2
- import { createReadStream, createWriteStream, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
3
- import { rm } from "node:fs/promises";
4
- import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
3
+ import { createReadStream, createWriteStream, linkSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, statfsSync, statSync } from "node:fs";
4
+ import { rm, watch as watchPath } from "node:fs/promises";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
5
6
  import { Readable } from "node:stream";
6
7
  import { pipeline } from "node:stream/promises";
8
+ import { content } from "./content.js";
7
9
  /** Create one filesystem implementation bounded beneath a resolved absolute root. */
8
10
  export function filesystemStorage(source, label, lifetime) {
9
- return createStorage(source, label, contained, lifetime);
11
+ return new NodeStorage(new StorageBoundary(source, label, contained, lifetime), []);
10
12
  }
11
13
  /** Create native filesystem access entered from one resolved absolute path. */
12
14
  export function nativeStorage(source, label, lifetime) {
13
- return createStorage(source, label, (root, parts) => resolvePath(root, ...parts), lifetime);
15
+ return new NodeStorage(new StorageBoundary(source, label, native, lifetime), []);
14
16
  }
15
- function createStorage(source, label, locate, lifetime) {
16
- let root = null;
17
- const resolveRoot = () => {
18
- active(lifetime);
19
- if (!root)
20
- root = Promise.resolve(typeof source === "string" ? source : source()).then(value => {
21
- active(lifetime);
17
+ class StorageBoundary {
18
+ source;
19
+ label;
20
+ locate;
21
+ lifetime;
22
+ root = null;
23
+ constructor(source, label, locate, lifetime) {
24
+ this.source = source;
25
+ this.label = label;
26
+ this.locate = locate;
27
+ this.lifetime = lifetime;
28
+ }
29
+ active() {
30
+ const signal = this.lifetime?.();
31
+ signal?.throwIfAborted();
32
+ return signal;
33
+ }
34
+ async path(parts) {
35
+ return this.locate(await this.rootPath(), parts);
36
+ }
37
+ rootPath() {
38
+ this.active();
39
+ if (!this.root) {
40
+ const resolving = Promise.resolve(typeof this.source === "string" ? this.source : this.source()).then(value => {
41
+ this.active();
22
42
  if (!isAbsolute(value))
23
43
  throw new Error("A Storage root must be absolute");
24
44
  return value;
25
45
  });
26
- return root;
27
- };
28
- const path = async () => await resolveRoot();
29
- const resolve = async (...parts) => locate(await path(), parts);
30
- async function stream(...parts) {
31
- const signal = active(lifetime);
32
- const destination = await resolve(...parts);
46
+ const retained = resolving.catch(error => {
47
+ if (this.root === retained)
48
+ this.root = null;
49
+ throw error;
50
+ });
51
+ this.root = retained;
52
+ }
53
+ return this.root;
54
+ }
55
+ }
56
+ class NodeStorage extends Storage {
57
+ boundary;
58
+ parts;
59
+ constructor(boundary, parts) {
60
+ super();
61
+ this.boundary = boundary;
62
+ this.parts = parts;
63
+ }
64
+ async name() { const path = await this.path(); return basename(path) || path; }
65
+ path() { return this.boundary.path(this.parts); }
66
+ navigate(...parts) {
67
+ return new NodeStorage(this.boundary, [...this.parts, ...parts]);
68
+ }
69
+ file(...parts) {
70
+ return new NodeStorageFile(this.boundary, [...this.parts, ...parts]);
71
+ }
72
+ async create() {
73
+ this.boundary.active();
74
+ mkdirSync(await this.path(), { recursive: true });
75
+ }
76
+ async stat() {
77
+ this.boundary.active();
78
+ const value = describe(await this.path());
79
+ if (!value)
80
+ return null;
81
+ if (value.kind !== "storage")
82
+ throw new Error(`${await this.path()} is not a Storage directory`);
83
+ return value.stat;
84
+ }
85
+ async list(options = {}) {
86
+ this.boundary.active();
87
+ const depth = listDepth(options);
88
+ const entries = [];
89
+ await this.collect(entries, [], depth);
90
+ return entries;
91
+ }
92
+ async collect(entries, relativeParts, depth) {
93
+ if (depth === 0)
94
+ return;
95
+ const location = this.navigate(...relativeParts);
96
+ const found = await location.stat();
97
+ if (!found)
98
+ throw new Error(`There is no ${await location.path()} in ${this.boundary.label}`);
99
+ for (const name of readdirSync(await location.path()).sort()) {
100
+ const childParts = [...relativeParts, name];
101
+ const child = describe(await this.boundary.path([...this.parts, ...childParts]));
102
+ if (!child)
103
+ continue;
104
+ if (child.kind === "file")
105
+ entries.push(this.file(...childParts));
106
+ else {
107
+ entries.push(this.navigate(...childParts));
108
+ if (depth > 1)
109
+ await this.collect(entries, childParts, depth - 1);
110
+ }
111
+ }
112
+ }
113
+ async copy(destination, options = {}) {
114
+ await copyStorage(this, destination, options);
115
+ }
116
+ async move(destination, options = {}) {
117
+ const [sourcePath, destinationPath] = await Promise.all([this.path(), destination.path()]);
118
+ if (samePath(sourcePath, destinationPath))
119
+ return;
120
+ await copyStorage(this, destination, options);
121
+ await this.delete();
122
+ }
123
+ async delete() {
124
+ this.boundary.active();
125
+ rmSync(await this.path(), { recursive: true, force: true });
126
+ }
127
+ async clear() {
128
+ this.boundary.active();
129
+ const destination = await this.path();
33
130
  const found = describe(destination);
131
+ if (found?.kind === "file")
132
+ throw new Error("Only a Storage directory can be cleared");
133
+ rmSync(destination, { recursive: true, force: true });
134
+ mkdirSync(destination, { recursive: true });
135
+ }
136
+ async space() {
137
+ this.boundary.active();
138
+ const value = statfsSync(await this.path());
139
+ const capacity = value.blocks * value.bsize;
140
+ const available = value.bavail * value.bsize;
141
+ return { capacity, available, used: capacity - value.bfree * value.bsize };
142
+ }
143
+ async *watch(options = {}) {
144
+ const lifetime = this.boundary.active();
145
+ const signal = combinedSignal(lifetime, options.signal);
146
+ for await (const change of watchPath(await this.path(), { recursive: options.recursive, signal })) {
147
+ yield { event: change.eventType, path: change.filename === null ? null : String(change.filename) };
148
+ }
149
+ }
150
+ }
151
+ class NodeStorageFile extends StorageFile {
152
+ boundary;
153
+ parts;
154
+ constructor(boundary, parts) {
155
+ super();
156
+ this.boundary = boundary;
157
+ this.parts = parts;
158
+ }
159
+ async name() { const path = await this.path(); return basename(path) || path; }
160
+ path() { return this.boundary.path(this.parts); }
161
+ async stat() {
162
+ this.boundary.active();
163
+ const value = describe(await this.path());
164
+ if (!value)
165
+ return null;
166
+ if (value.kind !== "file")
167
+ throw new Error(`${await this.path()} is not a file`);
168
+ return value.stat;
169
+ }
170
+ async stream(options = {}) {
171
+ const signal = this.boundary.active();
172
+ const destination = await this.path();
173
+ const found = await this.stat();
34
174
  if (!found)
35
- throw new Error(`There is no ${parts.join("/")} in ${label}`);
36
- if (found.kind !== "file")
37
- throw new Error(`${parts.join("/")} is not a file`);
38
- return Readable.toWeb(createReadStream(destination, { signal }));
39
- }
40
- async function write(...args) {
41
- const signal = active(lifetime);
42
- const parts = args.slice(0, -1);
43
- const destination = await resolve(...parts);
175
+ throw new Error(`There is no ${this.parts.join("/")} in ${this.boundary.label}`);
176
+ if (options.length === 0)
177
+ return new ReadableStream({ start(controller) { controller.close(); } });
178
+ const range = readRange(options);
179
+ return Readable.toWeb(createReadStream(destination, { ...range, signal }));
180
+ }
181
+ async bytes(options) {
182
+ return new Uint8Array(await new Response(await this.stream(options)).arrayBuffer());
183
+ }
184
+ async text(options) {
185
+ return new Response(await this.stream(options)).text();
186
+ }
187
+ async json() {
188
+ return JSON.parse(await this.text());
189
+ }
190
+ async write(value, options = {}) {
191
+ const signal = this.boundary.active();
192
+ const destination = await this.path();
44
193
  const temporary = join(dirname(destination), `.${randomUUID()}.writing`);
45
194
  mkdirSync(dirname(destination), { recursive: true });
46
195
  try {
47
- await pipeline(Readable.fromWeb(content(args.at(-1))), createWriteStream(temporary, { flags: "wx" }), { signal });
196
+ await pipeline(Readable.fromWeb(content(value).stream), createWriteStream(temporary, { flags: "wx" }), { signal });
48
197
  signal?.throwIfAborted();
49
- renameSync(temporary, destination);
198
+ if (options.overwrite === false) {
199
+ linkSync(temporary, destination);
200
+ rmSync(temporary, { force: true });
201
+ }
202
+ else
203
+ renameSync(temporary, destination);
50
204
  }
51
205
  catch (error) {
52
206
  await rm(temporary, { force: true }).catch(() => undefined);
53
207
  throw error;
54
208
  }
55
209
  }
56
- return {
57
- path,
58
- resolve,
59
- stream,
60
- async bytes(...parts) { return new Uint8Array(await new Response(await stream(...parts)).arrayBuffer()); },
61
- async text(...parts) { return new Response(await stream(...parts)).text(); },
62
- async json(...parts) { return JSON.parse(await new Response(await stream(...parts)).text()); },
63
- write,
64
- async stat(...parts) { active(lifetime); return describe(await resolve(...parts)); },
65
- async list(...parts) { active(lifetime); return readdirSync(await resolve(...parts)).sort(); },
66
- async delete(...parts) {
67
- active(lifetime);
68
- if (!parts.length)
69
- throw new Error("Emptying a place is clear, not delete");
70
- rmSync(await resolve(...parts), { recursive: true, force: true });
71
- },
72
- async clear(...parts) {
73
- active(lifetime);
74
- const destination = await resolve(...parts);
75
- const found = describe(destination);
76
- if (found && found.kind !== "directory")
77
- throw new Error("Only a Storage directory can be cleared");
78
- rmSync(destination, { recursive: true, force: true });
79
- mkdirSync(destination, { recursive: true });
80
- }
81
- };
82
- }
83
- function active(lifetime) {
84
- const signal = lifetime?.();
85
- signal?.throwIfAborted();
86
- return signal;
87
- }
88
- function content(value) {
89
- if (value instanceof ReadableStream)
90
- return value;
91
- if (value instanceof Uint8Array)
92
- return new Blob([bytes(value)]).stream();
93
- if (value instanceof ArrayBuffer)
94
- return new Blob([value]).stream();
95
- if (typeof Blob !== "undefined" && value instanceof Blob)
96
- return value.stream();
97
- if (typeof value === "string")
98
- return new Blob([value]).stream();
99
- return new Blob([JSON.stringify(value)]).stream();
210
+ async append(value) {
211
+ const signal = this.boundary.active();
212
+ const destination = await this.path();
213
+ mkdirSync(dirname(destination), { recursive: true });
214
+ await pipeline(Readable.fromWeb(content(value).stream), createWriteStream(destination, { flags: "a" }), { signal });
215
+ }
216
+ async copy(destination, options = {}) {
217
+ const [sourcePath, destinationPath] = await Promise.all([this.path(), destination.path()]);
218
+ if (samePath(sourcePath, destinationPath))
219
+ return;
220
+ await destination.write(await this.stream(), { overwrite: options.overwrite ?? false });
221
+ }
222
+ async move(destination, options = {}) {
223
+ const [sourcePath, destinationPath] = await Promise.all([this.path(), destination.path()]);
224
+ if (samePath(sourcePath, destinationPath))
225
+ return;
226
+ await this.copy(destination, options);
227
+ await this.delete();
228
+ }
229
+ async delete() {
230
+ this.boundary.active();
231
+ rmSync(await this.path(), { force: true });
232
+ }
100
233
  }
101
- function bytes(value) {
102
- return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
234
+ function native(root, parts) {
235
+ return resolvePath(root, ...parts);
103
236
  }
104
237
  function contained(root, parts) {
105
238
  const destination = join(root, ...parts);
106
239
  const step = relative(root, destination);
107
240
  if (step === ".." || step.startsWith(`..${sep}`) || isAbsolute(step))
108
- throw new Error("A Storage path may not leave its configured directory");
241
+ throw new Error("A Storage path may not leave its configured boundary");
109
242
  let current = root;
110
243
  for (const part of step.split(sep).filter(Boolean)) {
111
244
  current = join(current, part);
@@ -131,9 +264,66 @@ function describe(path) {
131
264
  return null;
132
265
  throw error;
133
266
  }
267
+ const modifiedAt = Math.round(value.mtimeMs);
134
268
  if (value.isFile())
135
- return { kind: "file", size: value.size, modifiedAt: value.mtimeMs };
269
+ return { kind: "file", stat: { size: value.size, modifiedAt } };
136
270
  if (value.isDirectory())
137
- return { kind: "directory", modifiedAt: value.mtimeMs };
138
- return { kind: "other", modifiedAt: value.mtimeMs };
271
+ return { kind: "storage", stat: { modifiedAt } };
272
+ throw new Error(`${path} is neither a file nor a Storage directory`);
273
+ }
274
+ function readRange(options) {
275
+ const offset = options.offset ?? 0;
276
+ if (!Number.isSafeInteger(offset) || offset < 0)
277
+ throw new Error("A Storage read offset must be a non-negative safe integer");
278
+ if (options.length === undefined)
279
+ return { start: offset };
280
+ if (!Number.isSafeInteger(options.length) || options.length < 0)
281
+ throw new Error("A Storage read length must be a non-negative safe integer");
282
+ if (!Number.isSafeInteger(offset + options.length))
283
+ throw new Error("A Storage byte range must use safe integers");
284
+ return { start: offset, end: offset + options.length - 1 };
285
+ }
286
+ function listDepth(options) {
287
+ if (options.depth !== undefined && (!Number.isSafeInteger(options.depth) || options.depth < 0)) {
288
+ throw new Error("A Storage list depth must be a non-negative safe integer");
289
+ }
290
+ if (options.depth !== undefined && !options.recursive)
291
+ throw new Error("A Storage list depth requires recursive listing");
292
+ if (!options.recursive)
293
+ return 1;
294
+ return options.depth ?? Number.POSITIVE_INFINITY;
295
+ }
296
+ async function copyStorage(source, destination, options) {
297
+ const [sourcePath, destinationPath] = await Promise.all([source.path(), destination.path()]);
298
+ if (samePath(sourcePath, destinationPath))
299
+ return;
300
+ if (descendsFrom(destinationPath, sourcePath))
301
+ throw new Error("A Storage directory cannot be copied inside itself");
302
+ if (!await source.stat())
303
+ throw new Error(`There is no Storage directory at ${sourcePath}`);
304
+ if (await destination.stat()) {
305
+ if (!options.overwrite)
306
+ throw new Error(`A Storage directory already exists at ${destinationPath}`);
307
+ await destination.delete();
308
+ }
309
+ await destination.create();
310
+ for (const entry of await source.list()) {
311
+ const name = await entry.name();
312
+ if (entry instanceof StorageFile)
313
+ await entry.copy(destination.file(name), options);
314
+ else
315
+ await entry.copy(destination.navigate(name), options);
316
+ }
317
+ }
318
+ function samePath(left, right) {
319
+ return resolvePath(left) === resolvePath(right);
320
+ }
321
+ function descendsFrom(path, parent) {
322
+ const step = relative(parent, path);
323
+ return step !== "" && step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
324
+ }
325
+ function combinedSignal(left, right) {
326
+ if (left && right)
327
+ return AbortSignal.any([left, right]);
328
+ return left ?? right;
139
329
  }
package/dist/system.d.ts CHANGED
@@ -1,11 +1,9 @@
1
- import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, type ProgramDefinition, type ProgramProcessRunEvent as CoreProgramProcessRunEvent, type ProgramProcessRunOptions as CoreProgramProcessRunOptions, type ServiceKey, type ShellOptions, type System as CoreSystem, type SystemProcess, type SystemProgram, type SystemUploads, type Storage, type WritableAppearance } from "@phreshos/core";
2
- export type ProgramProcessRunOptions = CoreProgramProcessRunOptions;
3
- export type ProgramProcessRunEvent = CoreProgramProcessRunEvent;
1
+ import { ClientService as CoreClientService, ServerService as CoreServerService, type ServiceKey, type ShellOptions, type System as CoreSystem, type SystemProcess, type SystemProgram, type SystemUploads, type Storage, type WritableAppearance } from "@phreshos/core";
4
2
  type ServiceEndpoint = ServiceKey["endpoint"];
5
3
  type ServiceAddress<Endpoint extends ServiceEndpoint> = Omit<ServiceKey, "endpoint"> & Readonly<{
6
4
  endpoint: Endpoint;
7
5
  }>;
8
- type ServiceHandle<Endpoint extends ServiceEndpoint, EventsMap extends object, Fallback = unknown> = Endpoint extends "server" ? ServerService<EventsMap, Fallback> : ClientService<EventsMap, Fallback>;
6
+ type ServiceHandle<Endpoint extends ServiceEndpoint, EventsMap extends object, Fallback = unknown> = Endpoint extends "server" ? CoreServerService<EventsMap, Fallback> : CoreClientService<EventsMap, Fallback>;
9
7
  /** One connected owner-local implementation of the shared System contract. */
10
8
  export declare class System implements CoreSystem {
11
9
  readonly storage: Storage;
@@ -13,36 +11,15 @@ export declare class System implements CoreSystem {
13
11
  readonly program: SystemProgram;
14
12
  readonly process: SystemProcess;
15
13
  readonly uploads: SystemUploads;
16
- fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
17
- websocket(url: string | URL, protocols?: string | string[]): Promise<WebSocket>;
14
+ readonly network: import("@phreshos/core").Network;
18
15
  shell(command: string, options?: ShellOptions): AsyncGenerator<import("@phreshos/core").ShellEvent, void, void>;
19
16
  private constructor();
20
17
  /** Connect to the System selected by argument, environment, or owner default. */
21
18
  static connect(home?: string): Promise<System>;
22
- /** Atomically replace one runtime Program without touching its installed form. */
23
- forceCreateProgram(source: ProgramDefinition | string): Promise<Program>;
24
19
  /** Close this owner connection and abort every attached operation it owns. */
25
20
  disconnect(): Promise<void>;
26
21
  service<Endpoint extends ServiceEndpoint>(key: ServiceAddress<Endpoint>): ServiceHandle<Endpoint, {}>;
27
- service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"server">): ServerService<EventsMap, Fallback>;
28
- service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"client">): ClientService<EventsMap, Fallback>;
22
+ service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"server">): CoreServerService<EventsMap, Fallback>;
23
+ service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"client">): CoreClientService<EventsMap, Fallback>;
29
24
  }
30
- /** Node SDK handle for a Service provided by a Server Endpoint. */
31
- export declare class ServerService<EventsMap extends object = {}, Fallback = unknown> extends CoreServerService<EventsMap, Fallback> {
32
- protected constructor();
33
- }
34
- /** Node SDK handle for a Service provided by a Client Endpoint. */
35
- export declare class ClientService<EventsMap extends object = {}, Fallback = unknown> extends CoreClientService<EventsMap, Fallback> {
36
- protected constructor();
37
- }
38
- export type Program = CoreProgram;
39
- export declare const Program: typeof CoreProgram;
40
- export type Process = CoreProcess;
41
- export declare const Process: typeof CoreProcess;
42
- export type Endpoint<EventsMap extends object = {}, Fallback = unknown> = CoreEndpoint<EventsMap, Fallback>;
43
- export declare const Endpoint: typeof CoreEndpoint;
44
- export type ServerEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreServerEndpoint<EventsMap, Fallback>;
45
- export declare const ServerEndpoint: typeof CoreServerEndpoint;
46
- export type ClientEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreClientEndpoint<EventsMap, Fallback>;
47
- export declare const ClientEndpoint: typeof CoreClientEndpoint;
48
25
  export {};
package/dist/system.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, isServiceKey, parseClientPermissions } from "@phreshos/core";
1
+ import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, isServiceKey, parseEndpointReference } from "@phreshos/core";
2
2
  import { homedir } from "node:os";
3
3
  import { gatewayAddress } from "./address.js";
4
4
  import Events from "./events.js";
@@ -11,13 +11,9 @@ import SystemRepresentation, { processIdentityState } from "./representation.js"
11
11
  import { GatewayConnection, openConnection } from "./transport.js";
12
12
  import Uploads from "./uploads.js";
13
13
  import shell from "./shell.js";
14
- import websocket from "./websocket.js";
14
+ import network from "./network.js";
15
15
  const systems = new WeakMap();
16
16
  const processSnapshots = new WeakMap();
17
- const ProgramBase = CoreProgram;
18
- const ProcessBase = CoreProcess;
19
- const ServerEndpointBase = CoreServerEndpoint;
20
- const ClientEndpointBase = CoreClientEndpoint;
21
17
  /** One connected owner-local implementation of the shared System contract. */
22
18
  export class System {
23
19
  storage;
@@ -25,13 +21,7 @@ export class System {
25
21
  program;
26
22
  process;
27
23
  uploads;
28
- async fetch(input, init) {
29
- const request = new Request(input, init);
30
- return await fetch(request, { signal: connectedSignal(this, request.signal) });
31
- }
32
- websocket(url, protocols) {
33
- return websocket(url, protocols, connectedSignal(this));
34
- }
24
+ network = network(() => connectedSignal(this));
35
25
  async *shell(command, options = {}) {
36
26
  yield* shell(command, { ...options, signal: connectedSignal(this, options.signal) });
37
27
  }
@@ -54,12 +44,6 @@ export class System {
54
44
  const address = gatewayAddress(resolved);
55
45
  return new System(await openConnection(address));
56
46
  }
57
- /** Atomically replace one runtime Program without touching its installed form. */
58
- async forceCreateProgram(source) {
59
- requireConnected(this);
60
- const identity = await representation(this).call("/program/force-create-program", source, "");
61
- return programHandle(this, required(representation(this).programs.get(identity), identity));
62
- }
63
47
  /** Close this owner connection and abort every attached operation it owns. */
64
48
  async disconnect() {
65
49
  await closeSystem(this, new Error("This System connection is closed"));
@@ -153,13 +137,21 @@ class ProgramRegistry extends Events {
153
137
  const identity = await representation(this.system).call("/program/create-program", source);
154
138
  return programHandle(this.system, required(representation(this.system).programs.get(identity), identity));
155
139
  }
140
+ async forceCreate(source) {
141
+ requireConnected(this.system);
142
+ const identity = await representation(this.system).call("/program/force-create-program", source, "");
143
+ return programHandle(this.system, required(representation(this.system).programs.get(identity), identity));
144
+ }
156
145
  event(event, values) {
157
146
  const program = programHandle(this.system, required(values[0]));
158
147
  return event === "uninstall" ? { program, everything: values[1] === true } : program;
159
148
  }
160
149
  }
161
- class ProgramHandle extends ProgramBase {
150
+ class ProgramHandle extends CoreProgram {
162
151
  system;
152
+ subscribe;
153
+ wait;
154
+ events;
163
155
  reference;
164
156
  identity;
165
157
  data;
@@ -178,11 +170,14 @@ class ProgramHandle extends ProgramBase {
178
170
  this.reference = snapshot.reference;
179
171
  this.identity = snapshot.identity;
180
172
  const address = this.address();
181
- bindEvents(this, new Events(["forget", "uninstall"], (event, subscriber) => {
173
+ const events = new Events(["forget", "uninstall"], (event, subscriber) => {
182
174
  if (event === null)
183
175
  throw new Error("Program events are named");
184
176
  return representation(system).on(`program:${this.reference}:${event}`, (...values) => subscriber(values[0]));
185
- }));
177
+ });
178
+ this.subscribe = events.subscribe;
179
+ this.wait = events.wait;
180
+ this.events = events.events;
186
181
  representation(system).on(`program:${this.reference}:change`, value => this.update(value));
187
182
  const call = (event, ...values) => representation(system).call(event, ...values);
188
183
  this.data = filesystemStorage(() => programStoragePath(system, address, "data"), `Program "${this.identity}" data`, () => connectedSignal(system));
@@ -205,18 +200,7 @@ class ProgramHandle extends ProgramBase {
205
200
  service: this.snapshot.server.service
206
201
  }) : null;
207
202
  }
208
- get client() {
209
- return this.snapshot.client ? Object.freeze({
210
- start: this.snapshot.client.start,
211
- service: this.snapshot.client.service,
212
- title: this.snapshot.client.title,
213
- size: this.snapshot.client.size,
214
- position: this.snapshot.client.position,
215
- layer: this.snapshot.client.layer,
216
- minimize: this.snapshot.client.minimize,
217
- permissions: parseClientPermissions(this.snapshot.client.permissions)
218
- }) : null;
219
- }
203
+ get client() { return this.snapshot.client; }
220
204
  update(snapshot) {
221
205
  if (snapshot.reference !== this.reference)
222
206
  throw new Error("A Program handle cannot become another Program");
@@ -359,8 +343,11 @@ class ProcessRegistry extends Events {
359
343
  return process ? processHandle(this.system, process) : null;
360
344
  }
361
345
  }
362
- class ProcessHandle extends ProcessBase {
346
+ class ProcessHandle extends CoreProcess {
363
347
  system;
348
+ subscribe;
349
+ wait;
350
+ events;
364
351
  identity;
365
352
  name;
366
353
  startedAt;
@@ -370,7 +357,10 @@ class ProcessHandle extends ProcessBase {
370
357
  super();
371
358
  this.system = system;
372
359
  processSnapshots.set(this, snapshot);
373
- bindEvents(this, new Events(["exit"], (_event, subscriber) => (representation(system).on(`process:${snapshot.reference}:exit`, value => subscriber(value)))));
360
+ const events = new Events(["exit"], (_event, subscriber) => (representation(system).on(`process:${snapshot.reference}:exit`, value => subscriber(value))));
361
+ this.subscribe = events.subscribe;
362
+ this.wait = events.wait;
363
+ this.events = events.events;
374
364
  this.identity = snapshot.identity;
375
365
  this.name = snapshot.name;
376
366
  this.startedAt = new Date(snapshot.startedAt);
@@ -390,7 +380,10 @@ class ProcessHandle extends ProcessBase {
390
380
  });
391
381
  return value === null ? null : processHandle(this.system, processIdentityState(value));
392
382
  }
393
- async option(name) { return processState(this.system, this).options[name]; }
383
+ async options(name) {
384
+ const options = processState(this.system, this).options;
385
+ return name === undefined ? Object.freeze({ ...options }) : options[name];
386
+ }
394
387
  async exit() {
395
388
  await representation(this.system).call("/process/exit", this.identity);
396
389
  }
@@ -433,9 +426,12 @@ class EndpointOperations extends Events {
433
426
  await representation(this.system).call(`/process/endpoint/${operation}`, this.owner.identity, this.endpoint, launch);
434
427
  }
435
428
  }
436
- class ServerEndpointHandle extends ServerEndpointBase {
429
+ class ServerEndpointHandle extends CoreServerEndpoint {
437
430
  system;
438
431
  owner;
432
+ subscribe;
433
+ wait;
434
+ events;
439
435
  endpoint = "server";
440
436
  traffic;
441
437
  lifecycle;
@@ -447,7 +443,9 @@ class ServerEndpointHandle extends ServerEndpointBase {
447
443
  this.base = new EndpointOperations(system, owner, "server");
448
444
  this.traffic = new ServerTrafficHandle(representation(system), owner.identity, "server", value => endpointFromReference(system, value));
449
445
  this.lifecycle = this.base.lifecycle;
450
- bindEvents(this, this.base);
446
+ this.subscribe = this.base.subscribe;
447
+ this.wait = this.base.wait;
448
+ this.events = this.base.events;
451
449
  }
452
450
  process() { return this.base.process(); }
453
451
  exists() { return this.base.exists(); }
@@ -455,7 +453,7 @@ class ServerEndpointHandle extends ServerEndpointBase {
455
453
  isService() { return this.base.isService(); }
456
454
  start(launch) { return this.base.start(launch); }
457
455
  stop() { return this.base.stop(); }
458
- publish(event, payload) { return this.base.publish(event, payload); }
456
+ publish = (event, payload) => this.base.publish(event, payload);
459
457
  async ask(event, payload) {
460
458
  return await this.askWithin(event, payload, 10_000);
461
459
  }
@@ -466,7 +464,10 @@ class ServerEndpointHandle extends ServerEndpointBase {
466
464
  return representation(this.system).call("/process/endpoint/ask", this.owner.identity, event, payload, timeout);
467
465
  }
468
466
  }
469
- class ClientEndpointHandle extends ClientEndpointBase {
467
+ class ClientEndpointHandle extends CoreClientEndpoint {
468
+ subscribe;
469
+ wait;
470
+ events;
470
471
  endpoint = "client";
471
472
  traffic;
472
473
  lifecycle;
@@ -477,7 +478,9 @@ class ClientEndpointHandle extends ClientEndpointBase {
477
478
  this.base = new EndpointOperations(system, owner, "client");
478
479
  this.traffic = new EndpointTrafficHandle(representation(system), owner.identity, "client", value => endpointFromReference(system, value));
479
480
  this.lifecycle = this.base.lifecycle;
480
- bindEvents(this, this.base);
481
+ this.subscribe = this.base.subscribe;
482
+ this.wait = this.base.wait;
483
+ this.events = this.base.events;
481
484
  this.window = new SystemWindow(system, owner);
482
485
  }
483
486
  process() { return this.base.process(); }
@@ -486,13 +489,13 @@ class ClientEndpointHandle extends ClientEndpointBase {
486
489
  isService() { return this.base.isService(); }
487
490
  start(launch) { return this.base.start(launch); }
488
491
  stop() { return this.base.stop(); }
489
- publish(event, payload) { return this.base.publish(event, payload); }
492
+ publish = (event, payload) => this.base.publish(event, payload);
490
493
  }
491
494
  class SystemWindow extends Events {
492
495
  system;
493
496
  process;
494
497
  constructor(system, process) {
495
- super(["move", "resize", "geometry", "minimize", "changeTitle", "front"], (event, subscriber) => {
498
+ super(["move", "resize", "geometry", "minimize", "maximize", "changeTitle", "front"], (event, subscriber) => {
496
499
  if (event === null)
497
500
  throw new Error("Window events are named");
498
501
  return representation(system).on(`window:${process.identity}:${event}`, subscriber);
@@ -504,13 +507,14 @@ class SystemWindow extends Events {
504
507
  async position() { return (await this.snapshot()).position; }
505
508
  async size() { return (await this.snapshot()).size; }
506
509
  async minimized() { return (await this.snapshot()).minimized; }
510
+ async maximized() { return (await this.snapshot()).maximized; }
507
511
  async front() { return frontWindow(this.system, this.process); }
508
512
  async layer() { return (await this.snapshot()).layer; }
509
- async location() { return (await this.snapshot()).location; }
510
513
  async move(position) { await this.change("move", position); }
511
514
  async resize(size) { await this.change("resize", size); }
512
515
  async setGeometry(geometry) { await this.change("geometry", geometry); }
513
516
  async minimize(minimized = true) { await this.change("minimize", minimized); }
517
+ async maximize(maximized = true) { await this.change("maximize", maximized); }
514
518
  async changeTitle(title) { await this.change("change-title", title); }
515
519
  async raise() { await this.change("raise"); }
516
520
  snapshot() {
@@ -542,14 +546,13 @@ class ServiceBase {
542
546
  void representation(this.system).call("/process/service/publish", this.key, event, payload);
543
547
  }
544
548
  }
545
- /** Node SDK handle for a Service provided by a Server Endpoint. */
546
- export class ServerService extends CoreServerService {
547
- constructor() { super(); }
548
- }
549
- class ServerServiceHandle extends ServerService {
549
+ class ServerServiceHandle extends CoreServerService {
550
550
  system;
551
551
  key;
552
552
  lifecycle;
553
+ subscribe;
554
+ wait;
555
+ events;
553
556
  base;
554
557
  constructor(system, key) {
555
558
  super();
@@ -557,9 +560,12 @@ class ServerServiceHandle extends ServerService {
557
560
  this.key = key;
558
561
  this.base = new ServiceBase(system, key);
559
562
  this.lifecycle = this.base.lifecycle;
560
- bindEvents(this, new Events([], (event, subscriber, impossible) => representation(system).follow({
563
+ const events = new Events([], (event, subscriber, impossible) => representation(system).follow({
561
564
  scope: "service", key, kind: "events", event
562
- }, (_received, payload) => subscriber(payload), impossible)));
565
+ }, (_received, payload) => subscriber(payload), impossible));
566
+ this.subscribe = events.subscribe;
567
+ this.wait = events.wait;
568
+ this.events = events.events;
563
569
  }
564
570
  exists() { return this.base.exists(); }
565
571
  waitReady(timeout) { return this.base.waitReady(timeout); }
@@ -571,20 +577,22 @@ class ServerServiceHandle extends ServerService {
571
577
  return { ask: (event, payload) => representation(this.system).call("/process/service/ask", this.key, event, payload, milliseconds) };
572
578
  }
573
579
  }
574
- /** Node SDK handle for a Service provided by a Client Endpoint. */
575
- export class ClientService extends CoreClientService {
576
- constructor() { super(); }
577
- }
578
- class ClientServiceHandle extends ClientService {
580
+ class ClientServiceHandle extends CoreClientService {
579
581
  lifecycle;
582
+ subscribe;
583
+ wait;
584
+ events;
580
585
  base;
581
586
  constructor(system, key) {
582
587
  super();
583
588
  this.base = new ServiceBase(system, key);
584
589
  this.lifecycle = this.base.lifecycle;
585
- bindEvents(this, new Events([], (event, subscriber, impossible) => representation(system).follow({
590
+ const events = new Events([], (event, subscriber, impossible) => representation(system).follow({
586
591
  scope: "service", key, kind: "events", event
587
- }, (_received, payload) => subscriber(payload), impossible)));
592
+ }, (_received, payload) => subscriber(payload), impossible));
593
+ this.subscribe = events.subscribe;
594
+ this.wait = events.wait;
595
+ this.events = events.events;
588
596
  }
589
597
  exists() { return this.base.exists(); }
590
598
  waitReady(timeout) { return this.base.waitReady(timeout); }
@@ -620,16 +628,6 @@ function programProcessEvent(system, event, values) {
620
628
  const process = processHandle(system, required(values[0]));
621
629
  return event === "exit" ? { process, ...values[1] } : process;
622
630
  }
623
- function bindEvents(target, events) {
624
- Object.assign(target, eventsOf(events));
625
- }
626
- function eventsOf(events) {
627
- return {
628
- subscribe: events.subscribe,
629
- waitFor: events.waitFor,
630
- events: events.events
631
- };
632
- }
633
631
  function chronological(left, right) { return left.startedAt.getTime() - right.startedAt.getTime(); }
634
632
  async function programStoragePath(system, handle, area) {
635
633
  const value = await representation(system).call("/program/area", handle, area, "path", []);
@@ -638,10 +636,9 @@ async function programStoragePath(system, handle, area) {
638
636
  return value;
639
637
  }
640
638
  function endpointFromReference(system, value) {
641
- const reference = value;
642
- if (!reference || (reference.kind !== "server" && reference.kind !== "client") || typeof reference.process?.identity !== "string") {
643
- throw new Error("The System returned an invalid Endpoint reference");
644
- }
639
+ if (value === null)
640
+ return null;
641
+ const reference = parseEndpointReference(value);
645
642
  const owner = processHandle(system, required(representation(system).processes.get(reference.process.identity), reference.process.identity));
646
643
  return reference.kind === "server" ? owner.server : owner.client;
647
644
  }
@@ -715,8 +712,3 @@ function required(value, identity = "") {
715
712
  return value;
716
713
  throw new Error(`The System returned no ${identity ? `${identity} ` : ""}snapshot`);
717
714
  }
718
- export const Program = CoreProgram;
719
- export const Process = CoreProcess;
720
- export const Endpoint = CoreEndpoint;
721
- export const ServerEndpoint = CoreServerEndpoint;
722
- export const ClientEndpoint = CoreClientEndpoint;
package/dist/traffic.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { AnswerSubscriber, AskSubscriber, Cleanup, Endpoint, EventOptions,
2
2
  import Events from "./events.js";
3
3
  import type SystemRepresentation from "./representation.js";
4
4
  type Kind = "publish" | "ask" | "answer";
5
- type ResolveEndpoint = (value: unknown) => Endpoint;
5
+ type ResolveEndpoint = (value: unknown) => Endpoint | null;
6
6
  /** Directed traffic originating from one canonical Endpoint. */
7
7
  export declare class EndpointTrafficHandle<Definitions extends object = {}> extends Events<TrafficEvents<Definitions>, never> {
8
8
  private readonly representation;
package/dist/uploads.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type SystemUploads, type Upload } from "@phreshos/core";
1
+ import { type SystemUploads, type Upload, type WritableContent } from "@phreshos/core";
2
2
  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 {
@@ -7,16 +7,14 @@ export default class Uploads implements SystemUploads {
7
7
  private accessPromise;
8
8
  constructor(ask: Ask, lifetime: () => AbortSignal);
9
9
  path(): Promise<string>;
10
- write(value: unknown): Promise<Upload>;
10
+ write(value: WritableContent): Promise<Upload>;
11
11
  stream(file: string): Promise<ReadableStream<Uint8Array<ArrayBufferLike>>>;
12
12
  bytes(file: string): Promise<Uint8Array<ArrayBuffer>>;
13
13
  text(file: string): Promise<string>;
14
14
  json<Value>(file: string): Promise<Value>;
15
15
  stat(file: string): Promise<Readonly<{
16
- file: string;
17
- type: string | null;
18
16
  size: number;
19
- time: number;
17
+ modifiedAt: number;
20
18
  }> | null>;
21
19
  private access;
22
20
  private active;
package/dist/uploads.js CHANGED
@@ -5,6 +5,7 @@ import { rename, rm } from "node:fs/promises";
5
5
  import { isAbsolute, join } from "node:path";
6
6
  import { Readable } from "node:stream";
7
7
  import { pipeline } from "node:stream/promises";
8
+ import { content } from "./content.js";
8
9
  /** Owner-local implementation of the System's opaque upload collection. */
9
10
  export default class Uploads {
10
11
  ask;
@@ -46,7 +47,7 @@ export default class Uploads {
46
47
  const upload = await this.stat(file);
47
48
  if (!upload)
48
49
  throw new Error("The completed upload could not be described");
49
- return upload;
50
+ return { file, ...upload };
50
51
  }
51
52
  async stream(file) {
52
53
  const signal = this.active();
@@ -90,36 +91,3 @@ function requireFile(file) {
90
91
  if (!isUploadFile(file))
91
92
  throw new Error("That is not an upload file");
92
93
  }
93
- function content(value) {
94
- if (typeof File !== "undefined" && value instanceof File) {
95
- const type = value.type || "application/octet-stream";
96
- return { stream: value.stream(), extension: extension(value.name, type) };
97
- }
98
- if (typeof Blob !== "undefined" && value instanceof Blob)
99
- return { stream: value.stream(), extension: extension("", value.type) };
100
- if (value instanceof ReadableStream)
101
- return { stream: value, extension: "bin" };
102
- if (value instanceof Uint8Array)
103
- return { stream: new Blob([bytes(value)]).stream(), extension: "bin" };
104
- if (value instanceof ArrayBuffer)
105
- return { stream: new Blob([value]).stream(), extension: "bin" };
106
- if (typeof value === "string")
107
- return { stream: new Blob([value]).stream(), extension: "txt" };
108
- return { stream: new Blob([JSON.stringify(value)]).stream(), extension: "json" };
109
- }
110
- function bytes(value) {
111
- return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
112
- }
113
- function extension(name, type) {
114
- const named = /\.([a-z0-9]+)$/i.exec(name)?.[1]?.toLowerCase();
115
- if (named && /^[a-z0-9]+$/.test(named))
116
- return named;
117
- return extensions[type] ?? "bin";
118
- }
119
- const extensions = {
120
- "application/json": "json",
121
- "image/jpeg": "jpg",
122
- "image/png": "png",
123
- "image/svg+xml": "svg",
124
- "text/plain": "txt"
125
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/node",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Node.js access to PhreshOS and Program projects.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -31,20 +31,24 @@
31
31
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
32
32
  "check": "tsc --noEmit && tsc -p tsconfig.test.json",
33
33
  "build": "node --run clean && tsc --noEmit false --outDir dist --rootDir source",
34
- "test": "node --run build && node --test tests/*.test.mjs",
35
- "verify": "node --run check && node --run test",
34
+ "test": "vitest run --project default",
35
+ "verify": "node --run check && node --run build && node --run test",
36
36
  "prepack": "node --run build"
37
37
  },
38
38
  "dependencies": {
39
- "@phreshos/core": "^0.1.41",
40
39
  "@the-link/ipc": "^0.2.1",
41
40
  "@the-link/messagepack": "^0.1.0",
42
41
  "adm-zip": "^0.6.0",
43
42
  "jiti": "^2.7.0"
44
43
  },
44
+ "peerDependencies": {
45
+ "@phreshos/core": "^0.1.43"
46
+ },
45
47
  "devDependencies": {
48
+ "@phreshos/core": "^0.1.43",
46
49
  "@types/adm-zip": "^0.5.8",
47
50
  "@types/node": "^26.2.0",
48
- "typescript": "^6.0.3"
51
+ "typescript": "^6.0.3",
52
+ "vitest": "^4.1.10"
49
53
  }
50
54
  }