@polyengine/wasi 0.1.0-pre.g633468a
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/LICENSE +202 -0
- package/README.md +23 -0
- package/esm/cli.js +160 -0
- package/esm/cli_stdio.js +203 -0
- package/esm/clocks.js +84 -0
- package/esm/filesystem.js +83 -0
- package/esm/filesystem_node.js +463 -0
- package/esm/filesystem_web.js +315 -0
- package/esm/http.js +663 -0
- package/esm/internal/cli_shared.js +26 -0
- package/esm/internal/fs_provider.js +795 -0
- package/esm/internal/sockets_02.js +992 -0
- package/esm/internal/sockets_03.js +969 -0
- package/esm/internal/sockets_platform.js +530 -0
- package/esm/internal/sockets_shared.js +268 -0
- package/esm/io.js +643 -0
- package/esm/mod.js +80 -0
- package/esm/package.json +3 -0
- package/esm/random.js +96 -0
- package/esm/sockets.js +172 -0
- package/package.json +98 -0
- package/types/cli.d.ts +44 -0
- package/types/cli_stdio.d.ts +31 -0
- package/types/clocks.d.ts +8 -0
- package/types/filesystem.d.ts +30 -0
- package/types/filesystem_node.d.ts +16 -0
- package/types/filesystem_web.d.ts +61 -0
- package/types/http.d.ts +169 -0
- package/types/internal/cli_shared.d.ts +26 -0
- package/types/internal/fs_provider.d.ts +174 -0
- package/types/internal/sockets_02.d.ts +37 -0
- package/types/internal/sockets_03.d.ts +12 -0
- package/types/internal/sockets_platform.d.ts +112 -0
- package/types/internal/sockets_shared.d.ts +256 -0
- package/types/io.d.ts +177 -0
- package/types/mod.d.ts +31 -0
- package/types/random.d.ts +17 -0
- package/types/sockets.d.ts +22 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// `@polyengine/wasi/filesystem-web` — `wasi:filesystem@0.2` + `@0.3` over the
|
|
2
|
+
// Origin Private File System (the browser's `navigator.storage` file API;
|
|
3
|
+
// any object implementing the structural handle interfaces below works,
|
|
4
|
+
// including in-memory fakes for tests). Preopens are explicit handle
|
|
5
|
+
// grants:
|
|
6
|
+
//
|
|
7
|
+
// const root = await navigator.storage.getDirectory();
|
|
8
|
+
// instantiate(a, { ...wasi(), ...filesystemWeb({ preopens: { "/": root } }).imports })
|
|
9
|
+
//
|
|
10
|
+
// READ-ONLY BY DEFAULT: the grant above is read-only. Writes need the
|
|
11
|
+
// package-level `writable: true` — one flag for the whole
|
|
12
|
+
// implementation, never per-preopen (fs_provider.ts header for why, and
|
|
13
|
+
// for the enforcement site: it is the provider, not this backend).
|
|
14
|
+
//
|
|
15
|
+
// ASYNC BY CONSTRUCTION: every OPFS op returns a promise, so the 0.2
|
|
16
|
+
// track's sync WIT descriptor methods are marked park-capable (A14, on
|
|
17
|
+
// the per-call class prototypes — fs_provider.ts): a p2 guest that
|
|
18
|
+
// touches the filesystem parks through the suspending kernel and needs
|
|
19
|
+
// JSPI; on engines without it a genuine wait raises `NeedsJspi` at the
|
|
20
|
+
// park site. The 0.3 track is async in WIT and needs no parking. (The
|
|
21
|
+
// synchronous OPFS access handles — `createSyncAccessHandle` — exist
|
|
22
|
+
// only in dedicated workers; this impl deliberately targets the portable
|
|
23
|
+
// async API.)
|
|
24
|
+
//
|
|
25
|
+
// Fidelity notes, honest and deliberate:
|
|
26
|
+
// * every positional write is open-writable → write → close (OPFS
|
|
27
|
+
// writables COMMIT on close; keeping one open would make reads stale
|
|
28
|
+
// and crashes lossy). Durable and simple, not fast.
|
|
29
|
+
// * OPFS has no symlinks, hard links, or settable timestamps:
|
|
30
|
+
// `link-at`/`symlink-at`/`readlink-at`/`set-times*` fail
|
|
31
|
+
// `unsupported`. Stat reports mtime only (files; from
|
|
32
|
+
// `File.lastModified`, ms resolution), link-count 1.
|
|
33
|
+
// * `exclusive` create and `rename-at` are emulated (probe-then-act /
|
|
34
|
+
// `move()` where the engine ships it, copy+delete for files
|
|
35
|
+
// otherwise; directory renames without `move()` are `unsupported`).
|
|
36
|
+
// * object identity (metadata-hash, is-same-object) derives from the
|
|
37
|
+
// guest path (FNV via the provider) and `isSameEntry`.
|
|
38
|
+
import { makeFilesystem, } from "./internal/fs_provider.js";
|
|
39
|
+
// --- error mapping -----------------------------------------------------------------
|
|
40
|
+
const DOM_ERROR_MAP = {
|
|
41
|
+
NotFoundError: "no-entry",
|
|
42
|
+
TypeMismatchError: "not-directory",
|
|
43
|
+
NotAllowedError: "access",
|
|
44
|
+
SecurityError: "access",
|
|
45
|
+
QuotaExceededError: "quota",
|
|
46
|
+
InvalidModificationError: "not-empty",
|
|
47
|
+
AbortError: "interrupted",
|
|
48
|
+
NoModificationAllowedError: "busy",
|
|
49
|
+
};
|
|
50
|
+
function domError(code, message) {
|
|
51
|
+
return Object.assign(new Error(message), { fsCode: code });
|
|
52
|
+
}
|
|
53
|
+
const NS_PER_MS = 1000000n;
|
|
54
|
+
function makeWebBackend() {
|
|
55
|
+
const childPath = (base, segments) => segments.length === 0 ? base.path : `${base.path}/${segments.join("/")}`;
|
|
56
|
+
const requireDir = (h) => {
|
|
57
|
+
if (h.handle.kind !== "directory") {
|
|
58
|
+
throw domError("not-directory", `${h.path}: not a directory`);
|
|
59
|
+
}
|
|
60
|
+
return h.handle;
|
|
61
|
+
};
|
|
62
|
+
const requireFile = (h) => {
|
|
63
|
+
if (h.handle.kind !== "file") {
|
|
64
|
+
throw domError("is-directory", `${h.path}: is a directory`);
|
|
65
|
+
}
|
|
66
|
+
return h.handle;
|
|
67
|
+
};
|
|
68
|
+
/** Walk intermediate segments (never creating); returns the parent dir
|
|
69
|
+
* for the final component, or the base itself for []. */
|
|
70
|
+
const walk = async (base, segments) => {
|
|
71
|
+
let dir = requireDir(base);
|
|
72
|
+
for (const seg of segments)
|
|
73
|
+
dir = await dir.getDirectoryHandle(seg);
|
|
74
|
+
return dir;
|
|
75
|
+
};
|
|
76
|
+
const parentOf = (base, segments) => walk(base, segments.slice(0, -1));
|
|
77
|
+
/** Resolve segments to a handle (file or directory), never creating. */
|
|
78
|
+
const resolve = async (base, segments) => {
|
|
79
|
+
if (segments.length === 0)
|
|
80
|
+
return base.handle;
|
|
81
|
+
const parent = await parentOf(base, segments);
|
|
82
|
+
const name = segments[segments.length - 1];
|
|
83
|
+
try {
|
|
84
|
+
return await parent.getFileHandle(name);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
if (e?.name === "TypeMismatchError") {
|
|
88
|
+
return await parent.getDirectoryHandle(name);
|
|
89
|
+
}
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const statOfFile = async (file) => {
|
|
94
|
+
const f = await file.getFile();
|
|
95
|
+
return {
|
|
96
|
+
type: "regular-file",
|
|
97
|
+
linkCount: 1n,
|
|
98
|
+
size: BigInt(f.size),
|
|
99
|
+
mtimeNs: BigInt(f.lastModified) * NS_PER_MS,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
const statOfHandle = (h) => h.kind === "file" ? statOfFile(h) : Promise.resolve({
|
|
103
|
+
type: "directory",
|
|
104
|
+
linkCount: 1n,
|
|
105
|
+
size: 0n,
|
|
106
|
+
});
|
|
107
|
+
/** FNV-1a over the guest path: OPFS has no dev/ino (module header). */
|
|
108
|
+
const pathIdentity = (path) => {
|
|
109
|
+
let h = 0xcbf29ce484222325n;
|
|
110
|
+
for (const byte of new TextEncoder().encode(path)) {
|
|
111
|
+
h = ((h ^ BigInt(byte)) * 0x100000001b3n) & 0xffffffffffffffffn;
|
|
112
|
+
}
|
|
113
|
+
return { a: h, b: BigInt(path.length) };
|
|
114
|
+
};
|
|
115
|
+
const writeAt = async (file, data, position) => {
|
|
116
|
+
const w = await file.createWritable({ keepExistingData: true });
|
|
117
|
+
try {
|
|
118
|
+
await w.write({ type: "write", position, data });
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await w.close(); // commit (or persist what was written before a failure)
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
isSync: false,
|
|
126
|
+
mapError(e) {
|
|
127
|
+
const tagged = e?.fsCode;
|
|
128
|
+
if (tagged !== undefined)
|
|
129
|
+
return tagged;
|
|
130
|
+
const name = e?.name;
|
|
131
|
+
return (typeof name === "string" ? DOM_ERROR_MAP[name] : undefined) ?? "io";
|
|
132
|
+
},
|
|
133
|
+
async openAt(base, segments, opts) {
|
|
134
|
+
const path = childPath(base, segments);
|
|
135
|
+
if (segments.length === 0) {
|
|
136
|
+
// Opening "." — the base itself.
|
|
137
|
+
if (opts.exclusive && opts.create)
|
|
138
|
+
throw domError("exist", `${path}: exists`);
|
|
139
|
+
return { handle: { handle: base.handle, path }, type: base.handle.kind === "file" ? "regular-file" : "directory" };
|
|
140
|
+
}
|
|
141
|
+
const parent = await parentOf(base, segments);
|
|
142
|
+
const name = segments[segments.length - 1];
|
|
143
|
+
if (opts.directory) {
|
|
144
|
+
const dir = await parent.getDirectoryHandle(name, { create: opts.create });
|
|
145
|
+
return { handle: { handle: dir, path }, type: "directory" };
|
|
146
|
+
}
|
|
147
|
+
// Does it already exist, and as what?
|
|
148
|
+
let existing;
|
|
149
|
+
try {
|
|
150
|
+
existing = await parent.getFileHandle(name);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
const en = e?.name;
|
|
154
|
+
if (en === "TypeMismatchError")
|
|
155
|
+
existing = await parent.getDirectoryHandle(name);
|
|
156
|
+
else if (en !== "NotFoundError")
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
if (existing?.kind === "directory") {
|
|
160
|
+
if (opts.create && opts.exclusive)
|
|
161
|
+
throw domError("exist", `${path}: exists`);
|
|
162
|
+
return { handle: { handle: existing, path }, type: "directory" };
|
|
163
|
+
}
|
|
164
|
+
if (existing !== undefined && opts.create && opts.exclusive) {
|
|
165
|
+
throw domError("exist", `${path}: exists`);
|
|
166
|
+
}
|
|
167
|
+
const file = existing ??
|
|
168
|
+
await (opts.create
|
|
169
|
+
? parent.getFileHandle(name, { create: true })
|
|
170
|
+
: parent.getFileHandle(name)); // throws NotFoundError
|
|
171
|
+
if (opts.truncate) {
|
|
172
|
+
const w = await file.createWritable({ keepExistingData: false });
|
|
173
|
+
await w.close();
|
|
174
|
+
}
|
|
175
|
+
return { handle: { handle: file, path }, type: "regular-file" };
|
|
176
|
+
},
|
|
177
|
+
close(_h) {
|
|
178
|
+
// OPFS handles hold no OS resources between operations.
|
|
179
|
+
},
|
|
180
|
+
stat: (h) => statOfHandle(h.handle),
|
|
181
|
+
async statAt(base, segments, _follow) {
|
|
182
|
+
return await statOfHandle(await resolve(base, segments));
|
|
183
|
+
},
|
|
184
|
+
async read(h, length, offset) {
|
|
185
|
+
const f = await requireFile(h).getFile();
|
|
186
|
+
if (offset >= f.size || length === 0)
|
|
187
|
+
return new Uint8Array(0);
|
|
188
|
+
const end = Math.min(offset + length, f.size);
|
|
189
|
+
return new Uint8Array(await f.slice(offset, end).arrayBuffer());
|
|
190
|
+
},
|
|
191
|
+
async write(h, buffer, offset) {
|
|
192
|
+
await writeAt(requireFile(h), buffer, offset);
|
|
193
|
+
return buffer.length;
|
|
194
|
+
},
|
|
195
|
+
async append(h, buffer) {
|
|
196
|
+
const file = requireFile(h);
|
|
197
|
+
const size = (await file.getFile()).size;
|
|
198
|
+
await writeAt(file, buffer, size);
|
|
199
|
+
return buffer.length;
|
|
200
|
+
},
|
|
201
|
+
async setSize(h, size) {
|
|
202
|
+
const w = await requireFile(h).createWritable({ keepExistingData: true });
|
|
203
|
+
try {
|
|
204
|
+
await w.truncate(size);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await w.close();
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
setTimes() {
|
|
211
|
+
throw domError("unsupported", "OPFS: timestamps are not settable");
|
|
212
|
+
},
|
|
213
|
+
setTimesAt() {
|
|
214
|
+
throw domError("unsupported", "OPFS: timestamps are not settable");
|
|
215
|
+
},
|
|
216
|
+
syncAll() {
|
|
217
|
+
// Per-operation commits (module header): nothing buffered to sync.
|
|
218
|
+
},
|
|
219
|
+
syncData() { },
|
|
220
|
+
async readDirectory(h) {
|
|
221
|
+
const out = [];
|
|
222
|
+
for await (const [name, handle] of requireDir(h).entries()) {
|
|
223
|
+
out.push({ name, type: handle.kind === "directory" ? "directory" : "regular-file" });
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
},
|
|
227
|
+
async createDirectoryAt(base, segments) {
|
|
228
|
+
const parent = await parentOf(base, segments);
|
|
229
|
+
const name = segments[segments.length - 1];
|
|
230
|
+
// POSIX mkdir fails on ANY existing entry; getDirectoryHandle
|
|
231
|
+
// ({create}) would silently accept an existing directory.
|
|
232
|
+
let exists = true;
|
|
233
|
+
try {
|
|
234
|
+
await resolve({ handle: parent, path: "" }, [name]);
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
if (e?.name !== "NotFoundError")
|
|
238
|
+
throw e;
|
|
239
|
+
exists = false;
|
|
240
|
+
}
|
|
241
|
+
if (exists)
|
|
242
|
+
throw domError("exist", `${childPath(base, segments)}: exists`);
|
|
243
|
+
await parent.getDirectoryHandle(name, { create: true });
|
|
244
|
+
},
|
|
245
|
+
async removeDirectoryAt(base, segments) {
|
|
246
|
+
const parent = await parentOf(base, segments);
|
|
247
|
+
const name = segments[segments.length - 1];
|
|
248
|
+
await parent.getDirectoryHandle(name); // TypeMismatchError → not-directory
|
|
249
|
+
await parent.removeEntry(name); // InvalidModificationError → not-empty
|
|
250
|
+
},
|
|
251
|
+
async unlinkFileAt(base, segments) {
|
|
252
|
+
const parent = await parentOf(base, segments);
|
|
253
|
+
const name = segments[segments.length - 1];
|
|
254
|
+
try {
|
|
255
|
+
await parent.getFileHandle(name);
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
if (e?.name === "TypeMismatchError") {
|
|
259
|
+
throw domError("is-directory", `${childPath(base, segments)}: is a directory`);
|
|
260
|
+
}
|
|
261
|
+
throw e;
|
|
262
|
+
}
|
|
263
|
+
await parent.removeEntry(name);
|
|
264
|
+
},
|
|
265
|
+
async renameAt(oldBase, oldSegments, newBase, newSegments) {
|
|
266
|
+
const target = await resolve(oldBase, oldSegments);
|
|
267
|
+
const newParent = await parentOf(newBase, newSegments);
|
|
268
|
+
const newName = newSegments[newSegments.length - 1];
|
|
269
|
+
if (target.move !== undefined) {
|
|
270
|
+
await target.move(newParent, newName);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (target.kind === "directory") {
|
|
274
|
+
throw domError("unsupported", "OPFS: directory rename requires FileSystemHandle.move");
|
|
275
|
+
}
|
|
276
|
+
// Fallback: copy + delete (non-atomic, module header).
|
|
277
|
+
const bytes = new Uint8Array(await (await target.getFile()).arrayBuffer());
|
|
278
|
+
const dest = await newParent.getFileHandle(newName, { create: true });
|
|
279
|
+
const w = await dest.createWritable({ keepExistingData: false });
|
|
280
|
+
try {
|
|
281
|
+
await w.write({ type: "write", position: 0, data: bytes });
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
await w.close();
|
|
285
|
+
}
|
|
286
|
+
const oldParent = await parentOf(oldBase, oldSegments);
|
|
287
|
+
await oldParent.removeEntry(oldSegments[oldSegments.length - 1]);
|
|
288
|
+
},
|
|
289
|
+
// linkAt / symlinkAt / readlinkAt: absent → the provider answers
|
|
290
|
+
// `unsupported` (OPFS has no links).
|
|
291
|
+
identity: (h) => pathIdentity(h.path),
|
|
292
|
+
async identityAt(base, segments, _follow) {
|
|
293
|
+
await resolve(base, segments); // existence check (NotFound → no-entry)
|
|
294
|
+
return pathIdentity(childPath(base, segments));
|
|
295
|
+
},
|
|
296
|
+
isSame(a, b) {
|
|
297
|
+
return a.handle.isSameEntry(b.handle);
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* `wasi:filesystem` over the Origin Private File System (module header).
|
|
303
|
+
* Serves both the `@0.2` (parking, JSPI) and `@0.3` tracks.
|
|
304
|
+
*/
|
|
305
|
+
export function filesystemWeb(options) {
|
|
306
|
+
const preopens = Object.entries(options.preopens).map(([guestName, handle]) => {
|
|
307
|
+
if (handle.kind !== "directory") {
|
|
308
|
+
throw new TypeError(`filesystemWeb: preopen ${guestName} is not a directory handle`);
|
|
309
|
+
}
|
|
310
|
+
return [{ handle, path: guestName }, guestName];
|
|
311
|
+
});
|
|
312
|
+
return makeFilesystem(makeWebBackend(), preopens, {
|
|
313
|
+
writable: options.writable === true,
|
|
314
|
+
});
|
|
315
|
+
}
|