@phreshos/node 0.1.16 → 0.1.17

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,8 @@ 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.
55
58
 
56
59
  ## Related repositories
57
60
 
@@ -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/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";
package/dist/project.js CHANGED
@@ -135,7 +135,7 @@ export class Project {
135
135
  }
136
136
  async prepareDevelopment(system, development, options) {
137
137
  const client = await DevelopmentClient.prepare(development, this.directory);
138
- const program = await system.forceCreateProgram(this.definition("development", client.url));
138
+ const program = await system.program.forceCreate(this.definition("development", client.url));
139
139
  try {
140
140
  await client.start(program.assetId, options.signal);
141
141
  const lifecycle = program.process.run({ options: options.options ?? {} }, { signal: client.processSignal(options.signal) });
@@ -150,7 +150,7 @@ export class Project {
150
150
  /** Build this Project and return its Program installation generator. */
151
151
  async install(system) {
152
152
  await this.build();
153
- const program = await system.forceCreateProgram(this.productionDefinition());
153
+ const program = await system.program.forceCreate(this.productionDefinition());
154
154
  return program.install();
155
155
  }
156
156
  /** Build and package this Program into its canonical release shape. */
@@ -184,7 +184,7 @@ export class Project {
184
184
  return Object.freeze({ archive, archivePath, checksumPath, declarationPath, digest });
185
185
  }
186
186
  async run(system, definition, options) {
187
- const program = await system.forceCreateProgram(definition);
187
+ const program = await system.program.forceCreate(definition);
188
188
  return program.process.run({ options: options.options ?? {} }, { signal: options.signal });
189
189
  }
190
190
  }
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;
@@ -19,30 +17,10 @@ export declare class System implements CoreSystem {
19
17
  private constructor();
20
18
  /** Connect to the System selected by argument, environment, or owner default. */
21
19
  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
20
  /** Close this owner connection and abort every attached operation it owns. */
25
21
  disconnect(): Promise<void>;
26
22
  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>;
23
+ service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"server">): CoreServerService<EventsMap, Fallback>;
24
+ service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"client">): CoreClientService<EventsMap, Fallback>;
29
25
  }
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
26
  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, parseClientPermissions } from "@phreshos/core";
2
2
  import { homedir } from "node:os";
3
3
  import { gatewayAddress } from "./address.js";
4
4
  import Events from "./events.js";
@@ -54,12 +54,6 @@ export class System {
54
54
  const address = gatewayAddress(resolved);
55
55
  return new System(await openConnection(address));
56
56
  }
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
57
  /** Close this owner connection and abort every attached operation it owns. */
64
58
  async disconnect() {
65
59
  await closeSystem(this, new Error("This System connection is closed"));
@@ -153,6 +147,11 @@ class ProgramRegistry extends Events {
153
147
  const identity = await representation(this.system).call("/program/create-program", source);
154
148
  return programHandle(this.system, required(representation(this.system).programs.get(identity), identity));
155
149
  }
150
+ async forceCreate(source) {
151
+ requireConnected(this.system);
152
+ const identity = await representation(this.system).call("/program/force-create-program", source, "");
153
+ return programHandle(this.system, required(representation(this.system).programs.get(identity), identity));
154
+ }
156
155
  event(event, values) {
157
156
  const program = programHandle(this.system, required(values[0]));
158
157
  return event === "uninstall" ? { program, everything: values[1] === true } : program;
@@ -542,11 +541,7 @@ class ServiceBase {
542
541
  void representation(this.system).call("/process/service/publish", this.key, event, payload);
543
542
  }
544
543
  }
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 {
544
+ class ServerServiceHandle extends CoreServerService {
550
545
  system;
551
546
  key;
552
547
  lifecycle;
@@ -571,11 +566,7 @@ class ServerServiceHandle extends ServerService {
571
566
  return { ask: (event, payload) => representation(this.system).call("/process/service/ask", this.key, event, payload, milliseconds) };
572
567
  }
573
568
  }
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 {
569
+ class ClientServiceHandle extends CoreClientService {
579
570
  lifecycle;
580
571
  base;
581
572
  constructor(system, key) {
@@ -715,8 +706,3 @@ function required(value, identity = "") {
715
706
  return value;
716
707
  throw new Error(`The System returned no ${identity ? `${identity} ` : ""}snapshot`);
717
708
  }
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/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.17",
4
4
  "description": "Node.js access to PhreshOS and Program projects.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -32,17 +32,21 @@
32
32
  "check": "tsc --noEmit && tsc -p tsconfig.test.json",
33
33
  "build": "node --run clean && tsc --noEmit false --outDir dist --rootDir source",
34
34
  "test": "node --run build && node --test tests/*.test.mjs",
35
- "verify": "node --run check && node --run test",
35
+ "verify:package": "node scripts/verify-package.mjs",
36
+ "verify": "node --run check && node --run test && node --run verify:package",
36
37
  "prepack": "node --run build"
37
38
  },
38
39
  "dependencies": {
39
- "@phreshos/core": "^0.1.41",
40
40
  "@the-link/ipc": "^0.2.1",
41
41
  "@the-link/messagepack": "^0.1.0",
42
42
  "adm-zip": "^0.6.0",
43
43
  "jiti": "^2.7.0"
44
44
  },
45
+ "peerDependencies": {
46
+ "@phreshos/core": "^0.1.42"
47
+ },
45
48
  "devDependencies": {
49
+ "@phreshos/core": "^0.1.42",
46
50
  "@types/adm-zip": "^0.5.8",
47
51
  "@types/node": "^26.2.0",
48
52
  "typescript": "^6.0.3"