@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 +11 -2
- package/dist/content.d.ts +8 -0
- package/dist/content.js +36 -0
- package/dist/events.d.ts +4 -3
- package/dist/events.js +2 -1
- package/dist/main.d.ts +1 -2
- package/dist/main.js +1 -2
- package/dist/network.d.ts +3 -0
- package/dist/network.js +13 -0
- package/dist/project.js +36 -24
- package/dist/representation.d.ts +6 -21
- package/dist/representation.js +13 -21
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +270 -80
- package/dist/system.d.ts +5 -28
- package/dist/system.js +70 -78
- package/dist/traffic.d.ts +1 -1
- package/dist/uploads.d.ts +3 -5
- package/dist/uploads.js +2 -34
- package/package.json +9 -5
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,
|
|
54
|
-
|
|
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
|
+
}
|
package/dist/content.js
ADDED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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/network.js
ADDED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
};
|
package/dist/representation.d.ts
CHANGED
|
@@ -1,32 +1,17 @@
|
|
|
1
|
-
import type
|
|
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
|
|
4
|
-
reference: string;
|
|
5
|
-
identity: string;
|
|
6
|
-
assetId: string;
|
|
3
|
+
export type ProgramState = ProgramSnapshot & Readonly<{
|
|
7
4
|
installed: boolean;
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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:
|
|
14
|
+
options: ProcessSnapshot["options"];
|
|
30
15
|
startedAt: Date;
|
|
31
16
|
}
|
|
32
17
|
export interface ProcessState extends ProcessIdentityState {
|
package/dist/representation.js
CHANGED
|
@@ -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
|
-
|
|
257
|
-
|
|
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.
|
|
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
|
-
|
|
320
|
-
|
|
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
|
|
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
|
|
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
|
|
15
|
+
return new NodeStorage(new StorageBoundary(source, label, native, lifetime), []);
|
|
14
16
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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(
|
|
196
|
+
await pipeline(Readable.fromWeb(content(value).stream), createWriteStream(temporary, { flags: "wx" }), { signal });
|
|
48
197
|
signal?.throwIfAborted();
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
102
|
-
return
|
|
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
|
|
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
|
|
269
|
+
return { kind: "file", stat: { size: value.size, modifiedAt } };
|
|
136
270
|
if (value.isDirectory())
|
|
137
|
-
return { kind: "
|
|
138
|
-
|
|
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 {
|
|
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" ?
|
|
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
|
-
|
|
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">):
|
|
28
|
-
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"client">):
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
}
|