@phreshos/node 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/content.d.ts +8 -0
- package/dist/content.js +36 -0
- package/dist/main.d.ts +1 -2
- package/dist/main.js +1 -2
- package/dist/project.js +3 -3
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +270 -80
- package/dist/system.d.ts +4 -26
- package/dist/system.js +8 -22
- package/dist/uploads.d.ts +3 -5
- package/dist/uploads.js +2 -34
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -27,6 +27,9 @@ cross the connection boundary.
|
|
|
27
27
|
| Bun | `bun add @phreshos/node` |
|
|
28
28
|
| Yarn | `yarn add @phreshos/node` |
|
|
29
29
|
|
|
30
|
+
`@phreshos/core` is a peer dependency and the single import path for shared
|
|
31
|
+
System and runtime contracts.
|
|
32
|
+
|
|
30
33
|
```ts
|
|
31
34
|
import { Project, System } from "@phreshos/node"
|
|
32
35
|
|
|
@@ -50,8 +53,8 @@ bun install --frozen-lockfile
|
|
|
50
53
|
bun run verify
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
`verify` checks the types, builds the package,
|
|
54
|
-
|
|
56
|
+
`verify` checks the types, builds the package, runs the connection and Project
|
|
57
|
+
tests, and validates the published package shape independently.
|
|
55
58
|
|
|
56
59
|
## Related repositories
|
|
57
60
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WritableContent } from "@phreshos/core";
|
|
2
|
+
/** Encodes one writable value without assigning it to Storage or Uploads. */
|
|
3
|
+
export declare function content(value: WritableContent): EncodedContent;
|
|
4
|
+
export interface EncodedContent {
|
|
5
|
+
stream: ReadableStream<Uint8Array>;
|
|
6
|
+
extension: string;
|
|
7
|
+
type: string;
|
|
8
|
+
}
|
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/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/project.js
CHANGED
|
@@ -135,7 +135,7 @@ export class Project {
|
|
|
135
135
|
}
|
|
136
136
|
async prepareDevelopment(system, development, options) {
|
|
137
137
|
const client = await DevelopmentClient.prepare(development, this.directory);
|
|
138
|
-
const program = await system.
|
|
138
|
+
const program = await system.program.forceCreate(this.definition("development", client.url));
|
|
139
139
|
try {
|
|
140
140
|
await client.start(program.assetId, options.signal);
|
|
141
141
|
const lifecycle = program.process.run({ options: options.options ?? {} }, { signal: client.processSignal(options.signal) });
|
|
@@ -150,7 +150,7 @@ export class Project {
|
|
|
150
150
|
/** Build this Project and return its Program installation generator. */
|
|
151
151
|
async install(system) {
|
|
152
152
|
await this.build();
|
|
153
|
-
const program = await system.
|
|
153
|
+
const program = await system.program.forceCreate(this.productionDefinition());
|
|
154
154
|
return program.install();
|
|
155
155
|
}
|
|
156
156
|
/** Build and package this Program into its canonical release shape. */
|
|
@@ -184,7 +184,7 @@ export class Project {
|
|
|
184
184
|
return Object.freeze({ archive, archivePath, checksumPath, declarationPath, digest });
|
|
185
185
|
}
|
|
186
186
|
async run(system, definition, options) {
|
|
187
|
-
const program = await system.
|
|
187
|
+
const program = await system.program.forceCreate(definition);
|
|
188
188
|
return program.process.run({ options: options.options ?? {} }, { signal: options.signal });
|
|
189
189
|
}
|
|
190
190
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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;
|
|
@@ -19,30 +17,10 @@ export declare class System implements CoreSystem {
|
|
|
19
17
|
private constructor();
|
|
20
18
|
/** Connect to the System selected by argument, environment, or owner default. */
|
|
21
19
|
static connect(home?: string): Promise<System>;
|
|
22
|
-
/** Atomically replace one runtime Program without touching its installed form. */
|
|
23
|
-
forceCreateProgram(source: ProgramDefinition | string): Promise<Program>;
|
|
24
20
|
/** Close this owner connection and abort every attached operation it owns. */
|
|
25
21
|
disconnect(): Promise<void>;
|
|
26
22
|
service<Endpoint extends ServiceEndpoint>(key: ServiceAddress<Endpoint>): ServiceHandle<Endpoint, {}>;
|
|
27
|
-
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"server">):
|
|
28
|
-
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"client">):
|
|
23
|
+
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"server">): CoreServerService<EventsMap, Fallback>;
|
|
24
|
+
service<EventsMap extends object = {}, Fallback = unknown>(key: ServiceAddress<"client">): CoreClientService<EventsMap, Fallback>;
|
|
29
25
|
}
|
|
30
|
-
/** Node SDK handle for a Service provided by a Server Endpoint. */
|
|
31
|
-
export declare class ServerService<EventsMap extends object = {}, Fallback = unknown> extends CoreServerService<EventsMap, Fallback> {
|
|
32
|
-
protected constructor();
|
|
33
|
-
}
|
|
34
|
-
/** Node SDK handle for a Service provided by a Client Endpoint. */
|
|
35
|
-
export declare class ClientService<EventsMap extends object = {}, Fallback = unknown> extends CoreClientService<EventsMap, Fallback> {
|
|
36
|
-
protected constructor();
|
|
37
|
-
}
|
|
38
|
-
export type Program = CoreProgram;
|
|
39
|
-
export declare const Program: typeof CoreProgram;
|
|
40
|
-
export type Process = CoreProcess;
|
|
41
|
-
export declare const Process: typeof CoreProcess;
|
|
42
|
-
export type Endpoint<EventsMap extends object = {}, Fallback = unknown> = CoreEndpoint<EventsMap, Fallback>;
|
|
43
|
-
export declare const Endpoint: typeof CoreEndpoint;
|
|
44
|
-
export type ServerEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreServerEndpoint<EventsMap, Fallback>;
|
|
45
|
-
export declare const ServerEndpoint: typeof CoreServerEndpoint;
|
|
46
|
-
export type ClientEndpoint<EventsMap extends object = {}, Fallback = unknown> = CoreClientEndpoint<EventsMap, Fallback>;
|
|
47
|
-
export declare const ClientEndpoint: typeof CoreClientEndpoint;
|
|
48
26
|
export {};
|
package/dist/system.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService,
|
|
1
|
+
import { ClientEndpoint as CoreClientEndpoint, ClientService as CoreClientService, Process as CoreProcess, Program as CoreProgram, ServerEndpoint as CoreServerEndpoint, ServerService as CoreServerService, isServiceKey, parseClientPermissions } from "@phreshos/core";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { gatewayAddress } from "./address.js";
|
|
4
4
|
import Events from "./events.js";
|
|
@@ -54,12 +54,6 @@ export class System {
|
|
|
54
54
|
const address = gatewayAddress(resolved);
|
|
55
55
|
return new System(await openConnection(address));
|
|
56
56
|
}
|
|
57
|
-
/** Atomically replace one runtime Program without touching its installed form. */
|
|
58
|
-
async forceCreateProgram(source) {
|
|
59
|
-
requireConnected(this);
|
|
60
|
-
const identity = await representation(this).call("/program/force-create-program", source, "");
|
|
61
|
-
return programHandle(this, required(representation(this).programs.get(identity), identity));
|
|
62
|
-
}
|
|
63
57
|
/** Close this owner connection and abort every attached operation it owns. */
|
|
64
58
|
async disconnect() {
|
|
65
59
|
await closeSystem(this, new Error("This System connection is closed"));
|
|
@@ -153,6 +147,11 @@ class ProgramRegistry extends Events {
|
|
|
153
147
|
const identity = await representation(this.system).call("/program/create-program", source);
|
|
154
148
|
return programHandle(this.system, required(representation(this.system).programs.get(identity), identity));
|
|
155
149
|
}
|
|
150
|
+
async forceCreate(source) {
|
|
151
|
+
requireConnected(this.system);
|
|
152
|
+
const identity = await representation(this.system).call("/program/force-create-program", source, "");
|
|
153
|
+
return programHandle(this.system, required(representation(this.system).programs.get(identity), identity));
|
|
154
|
+
}
|
|
156
155
|
event(event, values) {
|
|
157
156
|
const program = programHandle(this.system, required(values[0]));
|
|
158
157
|
return event === "uninstall" ? { program, everything: values[1] === true } : program;
|
|
@@ -542,11 +541,7 @@ class ServiceBase {
|
|
|
542
541
|
void representation(this.system).call("/process/service/publish", this.key, event, payload);
|
|
543
542
|
}
|
|
544
543
|
}
|
|
545
|
-
|
|
546
|
-
export class ServerService extends CoreServerService {
|
|
547
|
-
constructor() { super(); }
|
|
548
|
-
}
|
|
549
|
-
class ServerServiceHandle extends ServerService {
|
|
544
|
+
class ServerServiceHandle extends CoreServerService {
|
|
550
545
|
system;
|
|
551
546
|
key;
|
|
552
547
|
lifecycle;
|
|
@@ -571,11 +566,7 @@ class ServerServiceHandle extends ServerService {
|
|
|
571
566
|
return { ask: (event, payload) => representation(this.system).call("/process/service/ask", this.key, event, payload, milliseconds) };
|
|
572
567
|
}
|
|
573
568
|
}
|
|
574
|
-
|
|
575
|
-
export class ClientService extends CoreClientService {
|
|
576
|
-
constructor() { super(); }
|
|
577
|
-
}
|
|
578
|
-
class ClientServiceHandle extends ClientService {
|
|
569
|
+
class ClientServiceHandle extends CoreClientService {
|
|
579
570
|
lifecycle;
|
|
580
571
|
base;
|
|
581
572
|
constructor(system, key) {
|
|
@@ -715,8 +706,3 @@ function required(value, identity = "") {
|
|
|
715
706
|
return value;
|
|
716
707
|
throw new Error(`The System returned no ${identity ? `${identity} ` : ""}snapshot`);
|
|
717
708
|
}
|
|
718
|
-
export const Program = CoreProgram;
|
|
719
|
-
export const Process = CoreProcess;
|
|
720
|
-
export const Endpoint = CoreEndpoint;
|
|
721
|
-
export const ServerEndpoint = CoreServerEndpoint;
|
|
722
|
-
export const ClientEndpoint = CoreClientEndpoint;
|
package/dist/uploads.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SystemUploads, type Upload } from "@phreshos/core";
|
|
1
|
+
import { type SystemUploads, type Upload, type WritableContent } from "@phreshos/core";
|
|
2
2
|
type Ask = (request: object) => Promise<unknown>;
|
|
3
3
|
/** Owner-local implementation of the System's opaque upload collection. */
|
|
4
4
|
export default class Uploads implements SystemUploads {
|
|
@@ -7,16 +7,14 @@ export default class Uploads implements SystemUploads {
|
|
|
7
7
|
private accessPromise;
|
|
8
8
|
constructor(ask: Ask, lifetime: () => AbortSignal);
|
|
9
9
|
path(): Promise<string>;
|
|
10
|
-
write(value:
|
|
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.17",
|
|
4
4
|
"description": "Node.js access to PhreshOS and Program projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -32,17 +32,21 @@
|
|
|
32
32
|
"check": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
33
33
|
"build": "node --run clean && tsc --noEmit false --outDir dist --rootDir source",
|
|
34
34
|
"test": "node --run build && node --test tests/*.test.mjs",
|
|
35
|
-
"verify": "node
|
|
35
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
36
|
+
"verify": "node --run check && node --run test && node --run verify:package",
|
|
36
37
|
"prepack": "node --run build"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"@phreshos/core": "^0.1.40",
|
|
40
40
|
"@the-link/ipc": "^0.2.1",
|
|
41
41
|
"@the-link/messagepack": "^0.1.0",
|
|
42
42
|
"adm-zip": "^0.6.0",
|
|
43
43
|
"jiti": "^2.7.0"
|
|
44
44
|
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@phreshos/core": "^0.1.42"
|
|
47
|
+
},
|
|
45
48
|
"devDependencies": {
|
|
49
|
+
"@phreshos/core": "^0.1.42",
|
|
46
50
|
"@types/adm-zip": "^0.5.8",
|
|
47
51
|
"@types/node": "^26.2.0",
|
|
48
52
|
"typescript": "^6.0.3"
|