@phreshos/node 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/system.js CHANGED
@@ -1,39 +1,49 @@
1
- import { Client as CoreClient, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, Server as CoreServer, ServerService as CoreServerService, isServiceKey } from "@phreshos/core";
1
+ import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Endpoint as CoreEndpoint, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, isServiceKey } from "@phreshos/core";
2
2
  import { homedir } from "node:os";
3
3
  import { gatewayAddress } from "./address.js";
4
4
  import Events from "./events.js";
5
5
  import HandleRegistry from "./handle-registry.js";
6
6
  import { resolveHome } from "./home.js";
7
- import { filesystemStorage } from "./storage.js";
8
- import { programSql, programStore } from "./program-resources.js";
7
+ import { filesystemStorage, nativeStorage } from "./storage.js";
8
+ import { programPermissions, programSql, programStore } from "./program-resources.js";
9
9
  import { EndpointTrafficHandle, ServerTrafficHandle } from "./traffic.js";
10
- import { openConnection, request, streamProgram } from "./transport.js";
10
+ import { openConnection, request, stream } from "./transport.js";
11
11
  import Uploads from "./uploads.js";
12
+ import shell from "./shell.js";
12
13
  const systems = new WeakMap();
13
14
  const ProgramBase = CoreProgram;
14
15
  const ProcessBase = CoreProcess;
15
- const ServerBase = CoreServer;
16
- const ClientBase = CoreClient;
16
+ const ServerEndpointBase = CoreServerEndpoint;
17
+ const ClientEndpointBase = CoreClientEndpoint;
17
18
  /** One connected owner-local implementation of the shared System contract. */
18
19
  export class System {
19
- storage = filesystemStorage(homedir(), "the native home directory");
20
+ storage;
20
21
  appearance;
21
22
  program;
22
23
  process;
23
24
  uploads;
25
+ async fetch(input, init) {
26
+ const request = new Request(input, init);
27
+ return await fetch(request, { signal: connectedSignal(this, request.signal) });
28
+ }
29
+ async *shell(command, options = {}) {
30
+ yield* shell(command, { ...options, signal: connectedSignal(this, options.signal) });
31
+ }
24
32
  constructor(address, connection) {
25
33
  const lifetime = new AbortController();
26
34
  const handles = new HandleRegistry();
27
35
  const transport = {
28
36
  control: (value, signal) => request(address, "system", value, connectedSignal(this, signal)),
29
37
  api: (value, signal) => request(address, "api", value, connectedSignal(this, signal)),
30
- lifecycle: (value, signal) => streamProgram(address, value, connectedSignal(this, signal))
38
+ stream: (target, value, signal) => stream(address, target, value, connectedSignal(this, signal))
31
39
  };
32
40
  systems.set(this, { address, connection, handles, lifetime, transport, closed: false });
41
+ connection.once("close", () => closeSystem(this, new Error("This System connection is closed")));
42
+ this.storage = nativeStorage(homedir(), "the native filesystem", () => connectedSignal(this));
33
43
  this.appearance = new SystemAppearance(transport);
34
44
  this.program = new ProgramRegistry(this);
35
45
  this.process = new ProcessRegistry(this);
36
- this.uploads = new Uploads(value => transport.api(value));
46
+ this.uploads = new Uploads(value => transport.api(value), () => connectedSignal(this));
37
47
  }
38
48
  /** Connect to the System selected by argument, environment, or owner default. */
39
49
  static async connect(home) {
@@ -44,7 +54,7 @@ export class System {
44
54
  /** Atomically replace one runtime Program without touching its installed form. */
45
55
  async forceCreateProgram(source) {
46
56
  requireConnected(this);
47
- for await (const event of transport(this).lifecycle({ word: "force-create", program: source })) {
57
+ for await (const event of transport(this).stream("program", { word: "force-create", program: source })) {
48
58
  if (event.event === "created")
49
59
  return programHandle(this, required(event.program));
50
60
  }
@@ -52,13 +62,7 @@ export class System {
52
62
  }
53
63
  /** Close this owner connection and abort every attached operation it owns. */
54
64
  async disconnect() {
55
- const state = systemState(this);
56
- if (state.closed)
57
- return;
58
- state.closed = true;
59
- state.lifetime.abort(new Error("This System connection is closed"));
60
- state.connection.destroy();
61
- state.handles.clear();
65
+ closeSystem(this, new Error("This System connection is closed"));
62
66
  }
63
67
  service(key) {
64
68
  requireConnected(this);
@@ -81,6 +85,15 @@ function systemState(system) {
81
85
  throw new Error("Unknown System connection");
82
86
  return state;
83
87
  }
88
+ function closeSystem(system, reason) {
89
+ const state = systemState(system);
90
+ if (state.closed)
91
+ return;
92
+ state.closed = true;
93
+ state.lifetime.abort(reason);
94
+ state.connection.destroy();
95
+ state.handles.clear();
96
+ }
84
97
  function requireConnected(system) {
85
98
  if (systemState(system).closed)
86
99
  throw new Error("This System connection is closed");
@@ -149,7 +162,7 @@ class ProgramRegistry extends Events {
149
162
  }
150
163
  }
151
164
  async create(source) {
152
- for await (const event of transport(this.system).lifecycle({ word: "create", program: source })) {
165
+ for await (const event of transport(this.system).stream("program", { word: "create", program: source })) {
153
166
  if (event.event === "created")
154
167
  return programHandle(this.system, required(event.program));
155
168
  }
@@ -175,26 +188,30 @@ class ProgramHandle extends ProgramBase {
175
188
  database;
176
189
  process;
177
190
  startup;
191
+ permissions;
178
192
  snapshot;
179
193
  constructor(system, snapshot) {
180
194
  super();
181
195
  this.system = system;
182
196
  this.snapshot = snapshot;
183
- bindEvents(this, new Events(["forget", "uninstall"], (event, signal, timeout) => transport(system).control({
184
- capability: "program", operation: "wait", input: { program: snapshot.identity, event, timeout }
185
- }, signal).then(value => programEntityEvent(event, value))));
186
197
  this.reference = snapshot.reference;
187
198
  this.identity = snapshot.identity;
199
+ const address = this.address();
200
+ bindEvents(this, new Events(["forget", "uninstall"], (event, signal, timeout) => transport(system).api({
201
+ capability: "program", operation: "wait", handle: address, event, timeout
202
+ }, signal)));
188
203
  const request = (value) => transport(system).api(value);
189
- this.data = filesystemStorage(() => programStoragePath(system, this.identity, "data"), `Program "${this.identity}" data`);
190
- this.cache = filesystemStorage(() => programStoragePath(system, this.identity, "cache"), `Program "${this.identity}" cache`);
191
- this.store = programStore(request, this.identity);
192
- this.logs = programSql(request, this.identity, "logs");
193
- this.database = programSql(request, this.identity, "database");
204
+ this.data = filesystemStorage(() => programStoragePath(system, address, "data"), `Program "${this.identity}" data`, () => connectedSignal(system));
205
+ this.cache = filesystemStorage(() => programStoragePath(system, address, "cache"), `Program "${this.identity}" cache`, () => connectedSignal(system));
206
+ this.store = programStore(request, address);
207
+ this.logs = programSql(request, address, "logs");
208
+ this.database = programSql(request, address, "database");
194
209
  this.process = new ProgramProcesses(system, this);
195
210
  this.startup = new ProgramStartup(system, this);
211
+ this.permissions = programPermissions(request, address);
196
212
  }
197
213
  get name() { return this.snapshot.name; }
214
+ get assetId() { return this.snapshot.assetId; }
198
215
  get version() { return this.snapshot.version; }
199
216
  get description() { return this.snapshot.description; }
200
217
  get hasAgent() { return this.snapshot.hasAgent; }
@@ -212,7 +229,8 @@ class ProgramHandle extends ProgramBase {
212
229
  size: this.snapshot.client.size,
213
230
  position: this.snapshot.client.position,
214
231
  layer: this.snapshot.client.layer,
215
- minimize: this.snapshot.client.minimize
232
+ minimize: this.snapshot.client.minimize,
233
+ permissions: Object.freeze(Object.fromEntries(Object.entries(this.snapshot.client.permissions).map(([name, values]) => [name, Object.freeze([...values])])))
216
234
  }) : null;
217
235
  }
218
236
  update(snapshot) {
@@ -221,7 +239,7 @@ class ProgramHandle extends ProgramBase {
221
239
  this.snapshot = snapshot;
222
240
  }
223
241
  async icon(size = "medium") {
224
- const value = await transport(this.system).api({ capability: "program", operation: "icon", program: this.identity, size });
242
+ const value = await transport(this.system).api({ capability: "program", operation: "icon", handle: this.address(), size });
225
243
  if (!Array.isArray(value) || value.some(byte => typeof byte !== "number"))
226
244
  throw new Error("The System returned an invalid Program icon");
227
245
  return new Blob([Uint8Array.from(value)], { type: "image/png" });
@@ -229,11 +247,11 @@ class ProgramHandle extends ProgramBase {
229
247
  async agent() {
230
248
  if (!this.hasAgent)
231
249
  return null;
232
- const value = await transport(this.system).control({ capability: "program", operation: "agent", input: { program: this.identity } });
233
- return typeof value.content === "string" ? value.content : null;
250
+ const value = await transport(this.system).api({ capability: "program", operation: "agent", handle: this.address() });
251
+ return typeof value === "string" ? value : null;
234
252
  }
235
253
  async installed() {
236
- for await (const event of transport(this.system).lifecycle({ word: "installed", handle: this.address() })) {
254
+ for await (const event of transport(this.system).stream("program", { word: "installed", handle: this.address() })) {
237
255
  if (event.event === "installedState")
238
256
  return event.installed === true;
239
257
  }
@@ -241,8 +259,15 @@ class ProgramHandle extends ProgramBase {
241
259
  }
242
260
  install() { return command(this.system, { word: "install-existing", handle: this.address() }); }
243
261
  uninstall(everything = false) { return command(this.system, { word: "uninstall-existing", handle: this.address(), everything }); }
262
+ async fork(identity) {
263
+ for await (const event of transport(this.system).stream("program", { word: "fork", handle: this.address(), identity })) {
264
+ if (event.event === "created")
265
+ return programHandle(this.system, required(event.program));
266
+ }
267
+ throw new Error("The System did not confirm the forked Program");
268
+ }
244
269
  async forget() {
245
- for await (const _event of transport(this.system).lifecycle({ word: "forget", handle: this.address() })) { /* consume completion */ }
270
+ for await (const _event of transport(this.system).stream("program", { word: "forget", handle: this.address() })) { /* consume completion */ }
246
271
  }
247
272
  address() { return Object.freeze({ identity: this.identity, reference: this.reference }); }
248
273
  }
@@ -254,7 +279,7 @@ class ProgramStartup {
254
279
  this.program = program;
255
280
  }
256
281
  async get() {
257
- for await (const event of transport(this.system).lifecycle({
282
+ for await (const event of transport(this.system).stream("program", {
258
283
  word: "startup", handle: this.program.address(), operation: "get"
259
284
  })) {
260
285
  if (event.event === "startup")
@@ -269,7 +294,7 @@ class ProgramStartup {
269
294
  await this.change("disable");
270
295
  }
271
296
  async change(operation, launch) {
272
- for await (const event of transport(this.system).lifecycle({
297
+ for await (const event of transport(this.system).stream("program", {
273
298
  word: "startup", handle: this.program.address(), operation, launch
274
299
  })) {
275
300
  if (event.event === "startup")
@@ -282,13 +307,18 @@ class ProgramProcesses extends Events {
282
307
  system;
283
308
  program;
284
309
  constructor(system, program) {
285
- super(["create", "exit"], (event, signal, timeout) => transport(system).control({
286
- capability: "process", operation: "wait", input: { program: program.identity, event, timeout }
287
- }, signal).then(value => processEvent(system, value)));
310
+ super(["create", "exit"], (event, signal, timeout) => transport(system).api({
311
+ capability: "programProcess", operation: "wait", handle: program.address(), event, timeout
312
+ }, signal).then(value => programProcessEvent(system, event, value)));
288
313
  this.system = system;
289
314
  this.program = program;
290
315
  }
291
- async list() { return await listProcesses(this.system, this.program.identity); }
316
+ async list() {
317
+ const value = await transport(this.system).api({ capability: "programProcess", operation: "list", handle: this.program.address() });
318
+ if (!Array.isArray(value))
319
+ throw new Error("The System returned an invalid Program Process list");
320
+ return value.map(snapshot => processHandle(this.system, snapshot));
321
+ }
292
322
  async first() { return (await this.list()).sort(chronological)[0] ?? null; }
293
323
  async last() { return (await this.list()).sort(chronological).at(-1) ?? null; }
294
324
  async find(identityOrName) {
@@ -298,7 +328,7 @@ class ProgramProcesses extends Events {
298
328
  create(launch = {}) { return this.createExact("create-process", launch); }
299
329
  async *run(launch = {}, options = {}) {
300
330
  let process = null;
301
- for await (const event of transport(this.system).lifecycle({
331
+ for await (const event of transport(this.system).stream("program", {
302
332
  word: "run-process",
303
333
  handle: this.program.address(),
304
334
  launch
@@ -345,7 +375,7 @@ class ProgramProcesses extends Events {
345
375
  return processes.map(process => process.identity);
346
376
  }
347
377
  async createExact(word, launch) {
348
- for await (const event of transport(this.system).lifecycle({ word, handle: this.program.address(), launch })) {
378
+ for await (const event of transport(this.system).stream("program", { word, handle: this.program.address(), launch })) {
349
379
  if (event.event === "createdProcess")
350
380
  return processHandle(this.system, required(event.process));
351
381
  }
@@ -391,8 +421,8 @@ class ProcessHandle extends ProcessBase {
391
421
  this.identity = snapshot.identity;
392
422
  this.name = snapshot.name;
393
423
  this.startedAt = new Date(snapshot.startedAt);
394
- this.server = new ServerEndpoint(system, this);
395
- this.client = new ClientEndpoint(system, this);
424
+ this.server = new ServerEndpointHandle(system, this);
425
+ this.client = new ClientEndpointHandle(system, this);
396
426
  }
397
427
  program() { return programHandle(this.system, required(this.snapshot.programSnapshot, this.snapshot.program)); }
398
428
  async parent() {
@@ -437,6 +467,11 @@ class EndpointOperations extends Events {
437
467
  }
438
468
  async start(launch = {}) { await this.operation("start", launch); }
439
469
  async stop() { await this.operation("stop"); }
470
+ async waitReady(timeout) {
471
+ await transport(this.system).control({ capability: "endpoint", operation: "waitReady", input: {
472
+ process: this.owner.identity, endpoint: this.endpoint, timeout
473
+ } });
474
+ }
440
475
  async isService() {
441
476
  return await transport(this.system).api({
442
477
  capability: "endpoint", operation: "isService", process: this.owner.identity, endpoint: this.endpoint
@@ -458,7 +493,7 @@ class EndpointOperations extends Events {
458
493
  } });
459
494
  }
460
495
  }
461
- class ServerEndpoint extends ServerBase {
496
+ class ServerEndpointHandle extends ServerEndpointBase {
462
497
  system;
463
498
  owner;
464
499
  endpoint = "server";
@@ -476,6 +511,7 @@ class ServerEndpoint extends ServerBase {
476
511
  }
477
512
  process() { return this.base.process(); }
478
513
  exists() { return this.base.exists(); }
514
+ waitReady(timeout) { return this.base.waitReady(timeout); }
479
515
  isService() { return this.base.isService(); }
480
516
  start(launch) { return this.base.start(launch); }
481
517
  stop() { return this.base.stop(); }
@@ -490,11 +526,8 @@ class ServerEndpoint extends ServerBase {
490
526
  capability: "endpoint", operation: "ask", input: { process: this.owner.identity, endpoint: "server", event, payload, timeout: milliseconds }
491
527
  }) };
492
528
  }
493
- async waitReady(timeout) {
494
- await transport(this.system).control({ capability: "endpoint", operation: "waitReady", input: { process: this.owner.identity, endpoint: "server", timeout } });
495
- }
496
529
  }
497
- class ClientEndpoint extends ClientBase {
530
+ class ClientEndpointHandle extends ClientEndpointBase {
498
531
  endpoint = "client";
499
532
  traffic;
500
533
  lifecycle;
@@ -510,6 +543,7 @@ class ClientEndpoint extends ClientBase {
510
543
  }
511
544
  process() { return this.base.process(); }
512
545
  exists() { return this.base.exists(); }
546
+ waitReady(timeout) { return this.base.waitReady(timeout); }
513
547
  isService() { return this.base.isService(); }
514
548
  start(launch) { return this.base.start(launch); }
515
549
  stop() { return this.base.stop(); }
@@ -557,11 +591,14 @@ class ServiceBase {
557
591
  }, signal));
558
592
  }
559
593
  async exists() { return await transport(this.system).api({ capability: "service", operation: "exists", key: this.key }); }
594
+ async waitReady(timeout) {
595
+ await transport(this.system).api({ capability: "service", operation: "waitReady", key: this.key, timeout });
596
+ }
560
597
  publish(event, payload) {
561
598
  void transport(this.system).api({ capability: "service", operation: "publish", key: this.key, event, payload });
562
599
  }
563
600
  }
564
- /** Node-SDK handle for a Service provided by a Server Endpoint. */
601
+ /** Node SDK handle for a Service provided by a Server Endpoint. */
565
602
  export class ServerService extends CoreServerService {
566
603
  constructor() { super(); }
567
604
  }
@@ -581,9 +618,7 @@ class ServerServiceHandle extends ServerService {
581
618
  }, signal)));
582
619
  }
583
620
  exists() { return this.base.exists(); }
584
- async waitReady(timeout) {
585
- await transport(this.system).api({ capability: "service", operation: "waitReady", key: this.key, timeout });
586
- }
621
+ waitReady(timeout) { return this.base.waitReady(timeout); }
587
622
  publish = (event, payload) => this.base.publish(event, payload);
588
623
  async ask(event, payload) {
589
624
  return await transport(this.system).api({ capability: "service", operation: "ask", key: this.key, event, payload });
@@ -594,7 +629,7 @@ class ServerServiceHandle extends ServerService {
594
629
  }) };
595
630
  }
596
631
  }
597
- /** Node-SDK handle for a Service provided by a Client Endpoint. */
632
+ /** Node SDK handle for a Service provided by a Client Endpoint. */
598
633
  export class ClientService extends CoreClientService {
599
634
  constructor() { super(); }
600
635
  }
@@ -610,13 +645,14 @@ class ClientServiceHandle extends ClientService {
610
645
  }, signal)));
611
646
  }
612
647
  exists() { return this.base.exists(); }
648
+ waitReady(timeout) { return this.base.waitReady(timeout); }
613
649
  publish = (event, payload) => this.base.publish(event, payload);
614
650
  }
615
- async function listProcesses(system, program) {
651
+ async function listProcesses(system) {
616
652
  const processes = [];
617
653
  let offset = 0;
618
654
  while (true) {
619
- const page = await transport(system).control({ capability: "process", operation: "list", input: { program, limit: 100, offset } });
655
+ const page = await transport(system).control({ capability: "process", operation: "list", input: { limit: 100, offset } });
620
656
  processes.push(...page.data.map(snapshot => processHandle(system, snapshot)));
621
657
  offset += page.data.length;
622
658
  if (!page.truncated || !page.data.length)
@@ -624,7 +660,7 @@ async function listProcesses(system, program) {
624
660
  }
625
661
  }
626
662
  async function* command(system, request) {
627
- for await (const event of transport(system).lifecycle(request)) {
663
+ for await (const event of transport(system).stream("program", request)) {
628
664
  if (event.event === "output")
629
665
  yield {
630
666
  stream: event.stream === "stderr" ? "stderr" : "stdout",
@@ -641,10 +677,6 @@ async function waitEndpointLifecycle(system, owner, endpoint, event, signal, tim
641
677
  input: { process: owner.identity, endpoint, event, timeout }
642
678
  }, signal);
643
679
  }
644
- function programEntityEvent(event, value) {
645
- const payload = value.payload;
646
- return event === "uninstall" ? payload?.everything === true : undefined;
647
- }
648
680
  function processEvent(system, value) {
649
681
  const waited = value;
650
682
  const payload = waited.payload;
@@ -659,6 +691,17 @@ function processEvent(system, value) {
659
691
  return processHandle(system, payload);
660
692
  return payload;
661
693
  }
694
+ function programProcessEvent(system, event, value) {
695
+ if (event === "create")
696
+ return processHandle(system, value);
697
+ const exit = value;
698
+ return {
699
+ process: processHandle(system, required(exit.process)),
700
+ status: exit.status,
701
+ code: exit.code,
702
+ signal: exit.signal
703
+ };
704
+ }
662
705
  function exactProcessEvent(value) {
663
706
  const payload = value.payload;
664
707
  if (!payload)
@@ -680,8 +723,8 @@ function eventsOf(events) {
680
723
  };
681
724
  }
682
725
  function chronological(left, right) { return left.startedAt.getTime() - right.startedAt.getTime(); }
683
- async function programStoragePath(system, program, area) {
684
- const value = await transport(system).api({ capability: "program", operation: "storagePath", program, area });
726
+ async function programStoragePath(system, handle, area) {
727
+ const value = await transport(system).api({ capability: "program", operation: "storagePath", handle, area });
685
728
  if (typeof value !== "string")
686
729
  throw new Error("The System returned an invalid Program storage path");
687
730
  return value;
@@ -719,5 +762,5 @@ function required(value, identity = "") {
719
762
  export const Program = CoreProgram;
720
763
  export const Process = CoreProcess;
721
764
  export const Endpoint = CoreEndpoint;
722
- export const Server = CoreServer;
723
- export const Client = CoreClient;
765
+ export const ServerEndpoint = CoreServerEndpoint;
766
+ export const ClientEndpoint = CoreClientEndpoint;
package/dist/traffic.d.ts CHANGED
@@ -15,20 +15,20 @@ export declare class EndpointTrafficHandle<Definitions extends object = {}> exte
15
15
  event: string;
16
16
  questionId: string;
17
17
  message: Readonly<{
18
- to: import("@phreshos/core").Server<{}> | null;
18
+ to: import("@phreshos/core").ServerEndpoint<{}, unknown> | null;
19
19
  payload: Payload;
20
20
  }>;
21
21
  }>>;
22
22
  protected follow<Capture>(kind: Kind, convert: (value: unknown) => Capture, subscriber: (capture: Capture) => unknown, impossible?: (error: Error) => void): Cleanup;
23
23
  }
24
- /** Directed traffic originating from one canonical Server. */
24
+ /** Directed traffic originating from one canonical Server Endpoint. */
25
25
  export declare class ServerTrafficHandle<Definitions extends object = {}> extends EndpointTrafficHandle<Definitions> implements ServerTraffic<Definitions> {
26
26
  subscribeAnswers<Result = unknown>(subscriber: AnswerSubscriber<Result>): Cleanup;
27
27
  answers<Result = unknown>(options?: EventOptions): AsyncIterableIterator<Readonly<{
28
28
  event: string;
29
29
  questionId: string;
30
30
  message: Readonly<{
31
- to: Endpoint<{}> | null;
31
+ to: Endpoint<{}, unknown> | null;
32
32
  outcome: import("@phreshos/core").Outcome<Result>;
33
33
  }>;
34
34
  }>>;
package/dist/traffic.js CHANGED
@@ -52,7 +52,7 @@ export class EndpointTrafficHandle extends Events {
52
52
  return () => controller.abort();
53
53
  }
54
54
  }
55
- /** Directed traffic originating from one canonical Server. */
55
+ /** Directed traffic originating from one canonical Server Endpoint. */
56
56
  export class ServerTrafficHandle extends EndpointTrafficHandle {
57
57
  subscribeAnswers(subscriber) {
58
58
  return this.follow("answer", value => answer(value, this.resolveEndpoint), subscriber);
@@ -7,5 +7,5 @@ export interface TransportEvent {
7
7
  export declare function openConnection(path: string): Promise<Socket>;
8
8
  /** Execute one short authoritative System-control request. */
9
9
  export declare function request(path: string, target: "api" | "system", request: unknown, signal?: AbortSignal): Promise<unknown>;
10
- /** Stream one Program lifecycle operation until the System completes it. */
11
- export declare function streamProgram(path: string, request: unknown, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
10
+ /** Stream one long-running authoritative System operation. */
11
+ export declare function stream(path: string, target: "program", request: unknown, signal?: AbortSignal): AsyncGenerator<TransportEvent, void, unknown>;
package/dist/transport.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { connect } from "node:net";
2
+ const maximumStreamQueue = 256;
2
3
  /** Open and retain one owner-local System connection. */
3
4
  export function openConnection(path) {
4
5
  return new Promise((resolve, reject) => {
@@ -52,8 +53,8 @@ export function request(path, target, request, signal) {
52
53
  cancel();
53
54
  });
54
55
  }
55
- /** Stream one Program lifecycle operation until the System completes it. */
56
- export function streamProgram(path, request, signal) {
56
+ /** Stream one long-running authoritative System operation. */
57
+ export function stream(path, target, request, signal) {
57
58
  const events = [];
58
59
  let wake = null;
59
60
  let ended = false;
@@ -65,14 +66,27 @@ export function streamProgram(path, request, signal) {
65
66
  else
66
67
  signal?.addEventListener("abort", cancel, { once: true });
67
68
  let buffer = "";
68
- socket.on("connect", () => socket.write(`${JSON.stringify({ target: "program", request })}\n`));
69
+ socket.on("connect", () => socket.write(`${JSON.stringify({ target, request })}\n`));
69
70
  socket.on("data", chunk => {
70
71
  buffer += String(chunk);
71
72
  const lines = buffer.split("\n");
72
73
  buffer = lines.pop() ?? "";
73
74
  for (const line of lines)
74
75
  if (line.trim()) {
75
- const event = JSON.parse(line);
76
+ if (events.length >= maximumStreamQueue) {
77
+ failure = new Error(`System stream queue exceeded its capacity of ${maximumStreamQueue}`);
78
+ socket.destroy();
79
+ break;
80
+ }
81
+ let event;
82
+ try {
83
+ event = JSON.parse(line);
84
+ }
85
+ catch {
86
+ failure = new Error("The System returned an invalid stream event");
87
+ socket.destroy();
88
+ break;
89
+ }
76
90
  if (event.event === "error")
77
91
  failure = new Error(String(event.message));
78
92
  else
package/dist/uploads.d.ts CHANGED
@@ -3,8 +3,10 @@ type Ask = (request: object) => Promise<unknown>;
3
3
  /** Owner-local implementation of the System's opaque upload collection. */
4
4
  export default class Uploads implements SystemUploads {
5
5
  private readonly ask;
6
+ private readonly lifetime;
6
7
  private accessPromise;
7
- constructor(ask: Ask);
8
+ constructor(ask: Ask, lifetime: () => AbortSignal);
9
+ path(): Promise<string>;
8
10
  write(value: unknown): Promise<Upload>;
9
11
  stream(file: string): Promise<ReadableStream<Uint8Array<ArrayBufferLike>>>;
10
12
  bytes(file: string): Promise<Uint8Array<ArrayBuffer>>;
@@ -17,5 +19,6 @@ export default class Uploads implements SystemUploads {
17
19
  time: number;
18
20
  }> | null>;
19
21
  private access;
22
+ private active;
20
23
  }
21
24
  export {};
package/dist/uploads.js CHANGED
@@ -2,17 +2,23 @@ import { isUploadFile } from "@phreshos/core";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { createReadStream, createWriteStream, mkdirSync } from "node:fs";
4
4
  import { rename, rm } from "node:fs/promises";
5
- import { join } from "node:path";
5
+ import { isAbsolute, join } from "node:path";
6
6
  import { Readable } from "node:stream";
7
7
  import { pipeline } from "node:stream/promises";
8
8
  /** Owner-local implementation of the System's opaque upload collection. */
9
9
  export default class Uploads {
10
10
  ask;
11
+ lifetime;
11
12
  accessPromise = null;
12
- constructor(ask) {
13
+ constructor(ask, lifetime) {
13
14
  this.ask = ask;
15
+ this.lifetime = lifetime;
16
+ }
17
+ async path() {
18
+ return (await this.access()).path;
14
19
  }
15
20
  async write(value) {
21
+ const signal = this.active();
16
22
  const access = await this.access();
17
23
  const source = content(value);
18
24
  const identity = randomUUID();
@@ -29,7 +35,8 @@ export default class Uploads {
29
35
  throw new Error(`The upload exceeds ${access.limit / 1024 / 1024 / 1024} GB`);
30
36
  yield chunk;
31
37
  }
32
- }, createWriteStream(temporary, { flags: "wx" }));
38
+ }, createWriteStream(temporary, { flags: "wx" }), { signal });
39
+ signal.throwIfAborted();
33
40
  await rename(temporary, destination);
34
41
  }
35
42
  catch (error) {
@@ -42,22 +49,25 @@ export default class Uploads {
42
49
  return upload;
43
50
  }
44
51
  async stream(file) {
52
+ const signal = this.active();
45
53
  requireFile(file);
46
54
  const access = await this.access();
47
- return Readable.toWeb(createReadStream(join(access.path, file)));
55
+ return Readable.toWeb(createReadStream(join(access.path, file), { signal }));
48
56
  }
49
57
  async bytes(file) { return new Uint8Array(await new Response(await this.stream(file)).arrayBuffer()); }
50
58
  async text(file) { return new Response(await this.stream(file)).text(); }
51
59
  async json(file) { return JSON.parse(await this.text(file)); }
52
60
  async stat(file) {
61
+ this.active();
53
62
  requireFile(file);
54
63
  return await this.ask({ capability: "uploads", operation: "stat", file });
55
64
  }
56
65
  access() {
66
+ this.active();
57
67
  if (!this.accessPromise) {
58
68
  const resolving = this.ask({ capability: "uploads", operation: "access" }).then(value => {
59
69
  const access = value;
60
- if (!access || typeof access.path !== "string" || typeof access.limit !== "number")
70
+ if (!access || typeof access.path !== "string" || !isAbsolute(access.path) || typeof access.limit !== "number")
61
71
  throw new Error("The System returned invalid upload access");
62
72
  return { path: access.path, limit: access.limit };
63
73
  });
@@ -70,6 +80,11 @@ export default class Uploads {
70
80
  }
71
81
  return this.accessPromise;
72
82
  }
83
+ active() {
84
+ const signal = this.lifetime();
85
+ signal.throwIfAborted();
86
+ return signal;
87
+ }
73
88
  }
74
89
  function requireFile(file) {
75
90
  if (!isUploadFile(file))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phreshos/node",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Node.js access to PhreshOS and Program projects.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -29,14 +29,14 @@
29
29
  "packageManager": "bun@1.3.14",
30
30
  "scripts": {
31
31
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
32
- "check": "tsc --noEmit",
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
35
  "verify": "node --run check && node --run test",
36
36
  "prepack": "node --run build"
37
37
  },
38
38
  "dependencies": {
39
- "@phreshos/core": "^0.1.34",
39
+ "@phreshos/core": "^0.1.37",
40
40
  "adm-zip": "^0.6.0",
41
41
  "jiti": "^2.7.0"
42
42
  },