wasm-platform-kit 0.0.0-stage → 0.2.0
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 +21 -0
- package/README.md +224 -2
- package/assets/wasi/LICENSE +220 -0
- package/assets/wasi/cli.d.ts +43 -0
- package/assets/wasi/cli.js +147 -0
- package/assets/wasi/clocks.d.ts +3 -0
- package/assets/wasi/clocks.js +55 -0
- package/assets/wasi/common.d.ts +5 -0
- package/assets/wasi/common.js +16 -0
- package/assets/wasi/config.d.ts +2 -0
- package/assets/wasi/config.js +7 -0
- package/assets/wasi/environment.d.ts +5 -0
- package/assets/wasi/environment.js +24 -0
- package/assets/wasi/filesystem.d.ts +117 -0
- package/assets/wasi/filesystem.js +341 -0
- package/assets/wasi/helpers/cli.d.ts +7 -0
- package/assets/wasi/helpers/cli.js +25 -0
- package/assets/wasi/helpers/filesystem.d.ts +6 -0
- package/assets/wasi/helpers/filesystem.js +10 -0
- package/assets/wasi/http.d.ts +13 -0
- package/assets/wasi/http.js +953 -0
- package/assets/wasi/in-memory-filesystem.d.ts +38 -0
- package/assets/wasi/in-memory-filesystem.js +620 -0
- package/assets/wasi/in-memory-http.d.ts +8 -0
- package/assets/wasi/in-memory-http.js +11 -0
- package/assets/wasi/in-memory-sockets.d.ts +34 -0
- package/assets/wasi/in-memory-sockets.js +294 -0
- package/assets/wasi/index.d.ts +7 -0
- package/assets/wasi/index.js +7 -0
- package/assets/wasi/io.d.ts +67 -0
- package/assets/wasi/io.js +366 -0
- package/assets/wasi/opfs-filesystem.d.ts +52 -0
- package/assets/wasi/opfs-filesystem.js +262 -0
- package/assets/wasi/random.d.ts +4 -0
- package/assets/wasi/random.js +51 -0
- package/assets/wasi/sockets.d.ts +9 -0
- package/assets/wasi/sockets.js +85 -0
- package/browser.mjs +209 -0
- package/client.mjs +138 -0
- package/index.d.ts +199 -0
- package/install.d.ts +22 -0
- package/install.mjs +176 -0
- package/native-registration.mjs +70 -0
- package/node.mjs +332 -0
- package/package.json +66 -4
- package/policy.mjs +433 -0
- package/worker.mjs +389 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { checkedU64AsNumber } from "./common.js";
|
|
2
|
+
let id = 0;
|
|
3
|
+
const symbolDispose = Symbol.dispose || Symbol.for("dispose");
|
|
4
|
+
const checkedLength = (len, name = "length") => checkedU64AsNumber(len, name);
|
|
5
|
+
function closed() {
|
|
6
|
+
throw { tag: "closed" };
|
|
7
|
+
}
|
|
8
|
+
class IoError extends Error {
|
|
9
|
+
toDebugString() {
|
|
10
|
+
return this.message;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export const ioErrorCreate = (message) => new IoError(message);
|
|
14
|
+
class InputStream {
|
|
15
|
+
id;
|
|
16
|
+
handler;
|
|
17
|
+
#open = true;
|
|
18
|
+
#children = new Set();
|
|
19
|
+
static _create(handler) {
|
|
20
|
+
const stream = new InputStream();
|
|
21
|
+
if (!handler) {
|
|
22
|
+
console.trace("no handler");
|
|
23
|
+
}
|
|
24
|
+
stream.id = ++id;
|
|
25
|
+
stream.handler = handler;
|
|
26
|
+
return stream;
|
|
27
|
+
}
|
|
28
|
+
read(len) {
|
|
29
|
+
checkedLength(len);
|
|
30
|
+
if (!this.#open) {
|
|
31
|
+
closed();
|
|
32
|
+
}
|
|
33
|
+
if (this.handler.read) {
|
|
34
|
+
return this.handler.read.call(this, len);
|
|
35
|
+
}
|
|
36
|
+
return this.handler.blockingRead.call(this, len);
|
|
37
|
+
}
|
|
38
|
+
blockingRead(len) {
|
|
39
|
+
checkedLength(len);
|
|
40
|
+
if (!this.#open) {
|
|
41
|
+
closed();
|
|
42
|
+
}
|
|
43
|
+
return this.handler.blockingRead.call(this, len);
|
|
44
|
+
}
|
|
45
|
+
skip(len) {
|
|
46
|
+
checkedLength(len);
|
|
47
|
+
if (!this.#open) {
|
|
48
|
+
closed();
|
|
49
|
+
}
|
|
50
|
+
if (this.handler.skip) {
|
|
51
|
+
return this.handler.skip.call(this, len);
|
|
52
|
+
}
|
|
53
|
+
if (this.handler.read) {
|
|
54
|
+
const bytes = this.handler.read.call(this, len);
|
|
55
|
+
return BigInt(bytes.byteLength);
|
|
56
|
+
}
|
|
57
|
+
return this.blockingSkip.call(this, len);
|
|
58
|
+
}
|
|
59
|
+
blockingSkip(len) {
|
|
60
|
+
checkedLength(len);
|
|
61
|
+
if (!this.#open) {
|
|
62
|
+
closed();
|
|
63
|
+
}
|
|
64
|
+
if (this.handler.blockingSkip) {
|
|
65
|
+
return this.handler.blockingSkip.call(this, len);
|
|
66
|
+
}
|
|
67
|
+
const bytes = this.handler.blockingRead.call(this, len);
|
|
68
|
+
return BigInt(bytes.byteLength);
|
|
69
|
+
}
|
|
70
|
+
subscribe() {
|
|
71
|
+
if (!this.#open) {
|
|
72
|
+
return pollableCreate();
|
|
73
|
+
}
|
|
74
|
+
const pollable = this.handler.subscribe
|
|
75
|
+
? this.handler.subscribe.call(this)
|
|
76
|
+
: pollableCreate();
|
|
77
|
+
if (pollable instanceof Pollable) {
|
|
78
|
+
this.#children.add(pollable);
|
|
79
|
+
pollable._onDispose(() => this.#children.delete(pollable));
|
|
80
|
+
}
|
|
81
|
+
return pollable;
|
|
82
|
+
}
|
|
83
|
+
[symbolDispose]() {
|
|
84
|
+
if (!this.#open) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.#open = false;
|
|
88
|
+
for (const child of this.#children) {
|
|
89
|
+
child._invalidate();
|
|
90
|
+
}
|
|
91
|
+
this.#children.clear();
|
|
92
|
+
if (this.handler.drop) {
|
|
93
|
+
this.handler.drop.call(this);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export const inputStreamCreate = InputStream._create;
|
|
98
|
+
// @ts-expect-error - Deleting static method
|
|
99
|
+
delete InputStream._create;
|
|
100
|
+
class OutputStream {
|
|
101
|
+
id;
|
|
102
|
+
open;
|
|
103
|
+
handler;
|
|
104
|
+
#permit = 0n;
|
|
105
|
+
#children = new Set();
|
|
106
|
+
static _create(handler) {
|
|
107
|
+
const stream = new OutputStream();
|
|
108
|
+
if (!handler) {
|
|
109
|
+
console.trace("no handler");
|
|
110
|
+
}
|
|
111
|
+
stream.id = ++id;
|
|
112
|
+
stream.open = true;
|
|
113
|
+
stream.handler = handler;
|
|
114
|
+
return stream;
|
|
115
|
+
}
|
|
116
|
+
checkWrite() {
|
|
117
|
+
if (!this.open) {
|
|
118
|
+
closed();
|
|
119
|
+
}
|
|
120
|
+
if (this.handler.checkWrite) {
|
|
121
|
+
const permit = this.handler.checkWrite.call(this);
|
|
122
|
+
checkedLength(permit, "write permit");
|
|
123
|
+
this.#permit = permit;
|
|
124
|
+
return permit;
|
|
125
|
+
}
|
|
126
|
+
this.#permit = 1000000n;
|
|
127
|
+
return this.#permit;
|
|
128
|
+
}
|
|
129
|
+
write(buf) {
|
|
130
|
+
if (!this.open) {
|
|
131
|
+
closed();
|
|
132
|
+
}
|
|
133
|
+
if (BigInt(buf.byteLength) > this.#permit) {
|
|
134
|
+
throw new Error("write exceeds the permit returned by checkWrite");
|
|
135
|
+
}
|
|
136
|
+
this.#permit -= BigInt(buf.byteLength);
|
|
137
|
+
this.handler.write.call(this, buf);
|
|
138
|
+
}
|
|
139
|
+
blockingWriteAndFlush(buf) {
|
|
140
|
+
if (!this.open) {
|
|
141
|
+
closed();
|
|
142
|
+
}
|
|
143
|
+
if (buf.byteLength > 4096) {
|
|
144
|
+
throw new RangeError("blockingWriteAndFlush accepts at most 4096 bytes");
|
|
145
|
+
}
|
|
146
|
+
if (this.handler.blockingWriteAndFlush) {
|
|
147
|
+
return this.handler.blockingWriteAndFlush.call(this, buf);
|
|
148
|
+
}
|
|
149
|
+
this.handler.write.call(this, buf);
|
|
150
|
+
if (this.handler.blockingFlush) {
|
|
151
|
+
this.handler.blockingFlush.call(this);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
this.handler.flush?.call(this);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
flush() {
|
|
158
|
+
if (!this.open) {
|
|
159
|
+
closed();
|
|
160
|
+
}
|
|
161
|
+
this.#permit = 0n;
|
|
162
|
+
if (this.handler.flush) {
|
|
163
|
+
this.handler.flush.call(this);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
blockingFlush() {
|
|
167
|
+
if (!this.open) {
|
|
168
|
+
closed();
|
|
169
|
+
}
|
|
170
|
+
if (this.handler.blockingFlush) {
|
|
171
|
+
this.handler.blockingFlush.call(this);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
this.handler.flush?.call(this);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
writeZeroes(len) {
|
|
178
|
+
const length = checkedLength(len);
|
|
179
|
+
if (len > this.#permit) {
|
|
180
|
+
throw new Error("write exceeds the permit returned by checkWrite");
|
|
181
|
+
}
|
|
182
|
+
this.write.call(this, new Uint8Array(length));
|
|
183
|
+
}
|
|
184
|
+
blockingWriteZeroesAndFlush(len) {
|
|
185
|
+
const length = checkedLength(len);
|
|
186
|
+
if (length > 4096) {
|
|
187
|
+
throw new RangeError("blockingWriteZeroesAndFlush accepts at most 4096 bytes");
|
|
188
|
+
}
|
|
189
|
+
this.blockingWriteAndFlush.call(this, new Uint8Array(length));
|
|
190
|
+
}
|
|
191
|
+
splice(src, len) {
|
|
192
|
+
const spliceLen = Math.min(checkedLength(len), Number(this.checkWrite.call(this)));
|
|
193
|
+
const bytes = src.read(BigInt(spliceLen));
|
|
194
|
+
this.write.call(this, bytes);
|
|
195
|
+
return BigInt(bytes.byteLength);
|
|
196
|
+
}
|
|
197
|
+
blockingSplice(src, len) {
|
|
198
|
+
const spliceLen = Math.min(checkedLength(len), Number(this.checkWrite.call(this)));
|
|
199
|
+
const bytes = src.blockingRead(BigInt(spliceLen));
|
|
200
|
+
this.write.call(this, bytes);
|
|
201
|
+
return BigInt(bytes.byteLength);
|
|
202
|
+
}
|
|
203
|
+
subscribe() {
|
|
204
|
+
if (!this.open) {
|
|
205
|
+
return pollableCreate();
|
|
206
|
+
}
|
|
207
|
+
const pollable = this.handler.subscribe
|
|
208
|
+
? this.handler.subscribe.call(this)
|
|
209
|
+
: pollableCreate();
|
|
210
|
+
if (pollable instanceof Pollable) {
|
|
211
|
+
this.#children.add(pollable);
|
|
212
|
+
pollable._onDispose(() => this.#children.delete(pollable));
|
|
213
|
+
}
|
|
214
|
+
return pollable;
|
|
215
|
+
}
|
|
216
|
+
[symbolDispose]() {
|
|
217
|
+
if (!this.open) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.open = false;
|
|
221
|
+
this.#permit = 0n;
|
|
222
|
+
for (const child of this.#children) {
|
|
223
|
+
child._invalidate();
|
|
224
|
+
}
|
|
225
|
+
this.#children.clear();
|
|
226
|
+
this.handler.drop?.call(this);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
export const outputStreamCreate = OutputStream._create;
|
|
230
|
+
// @ts-expect-error - Deleting static method
|
|
231
|
+
delete OutputStream._create;
|
|
232
|
+
export const error = {
|
|
233
|
+
Error: IoError,
|
|
234
|
+
};
|
|
235
|
+
export const streams = { InputStream, OutputStream };
|
|
236
|
+
class Pollable {
|
|
237
|
+
#source = { ready: () => true, wait: () => Promise.resolve() };
|
|
238
|
+
#invalid = false;
|
|
239
|
+
#disposed = false;
|
|
240
|
+
#wait = null;
|
|
241
|
+
#disposeCallbacks = [];
|
|
242
|
+
#wakeUnusable;
|
|
243
|
+
#unusable = new Promise((resolve) => (this.#wakeUnusable = resolve));
|
|
244
|
+
static _create(source) {
|
|
245
|
+
const pollable = new Pollable();
|
|
246
|
+
if (source instanceof Promise) {
|
|
247
|
+
let ready = false;
|
|
248
|
+
const wait = source.then(() => {
|
|
249
|
+
ready = true;
|
|
250
|
+
}, () => {
|
|
251
|
+
ready = true;
|
|
252
|
+
});
|
|
253
|
+
pollable.#source = { ready: () => ready, wait: () => wait };
|
|
254
|
+
}
|
|
255
|
+
else if (source) {
|
|
256
|
+
pollable.#source = source;
|
|
257
|
+
}
|
|
258
|
+
return pollable;
|
|
259
|
+
}
|
|
260
|
+
ready() {
|
|
261
|
+
this.#assertUsable();
|
|
262
|
+
return this.#source.ready();
|
|
263
|
+
}
|
|
264
|
+
block() {
|
|
265
|
+
this.#assertUsable();
|
|
266
|
+
if (this.#source.ready()) {
|
|
267
|
+
return Promise.resolve();
|
|
268
|
+
}
|
|
269
|
+
// Deduplicate simultaneous waiters, but discard a completed wait so a
|
|
270
|
+
// level-triggered source can be polled again after its event is consumed.
|
|
271
|
+
if (!this.#wait) {
|
|
272
|
+
this.#wait = Promise.race([
|
|
273
|
+
Promise.resolve(this.#source.wait()),
|
|
274
|
+
this.#unusable.then(() => this.#assertUsable()),
|
|
275
|
+
]).finally(() => {
|
|
276
|
+
this.#wait = null;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return this.#wait;
|
|
280
|
+
}
|
|
281
|
+
_onDispose(callback) {
|
|
282
|
+
if (this.#disposed) {
|
|
283
|
+
callback();
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
this.#disposeCallbacks.push(callback);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
_invalidate() {
|
|
290
|
+
if (this.#invalid || this.#disposed) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.#invalid = true;
|
|
294
|
+
this.#wakeUnusable();
|
|
295
|
+
}
|
|
296
|
+
#assertUsable() {
|
|
297
|
+
if (this.#disposed) {
|
|
298
|
+
throw new Error("pollable has been disposed");
|
|
299
|
+
}
|
|
300
|
+
if (this.#invalid) {
|
|
301
|
+
throw new Error("pollable's parent resource has been disposed");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
[symbolDispose]() {
|
|
305
|
+
if (this.#disposed) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this.#disposed = true;
|
|
309
|
+
this.#wakeUnusable();
|
|
310
|
+
for (const callback of this.#disposeCallbacks.splice(0)) {
|
|
311
|
+
callback();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export const pollableCreate = Pollable._create;
|
|
316
|
+
// @ts-expect-error - Deleting static method
|
|
317
|
+
delete Pollable._create;
|
|
318
|
+
function pollList(list) {
|
|
319
|
+
if (list.length === 0) {
|
|
320
|
+
throw new Error("poll list must not be empty");
|
|
321
|
+
}
|
|
322
|
+
if (list.length > 0xffffffff) {
|
|
323
|
+
throw new Error("poll list length exceeds u32 index range");
|
|
324
|
+
}
|
|
325
|
+
const ready = [];
|
|
326
|
+
for (let i = 0; i < list.length; i++) {
|
|
327
|
+
if (list[i].ready()) {
|
|
328
|
+
ready.push(i);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (ready.length > 0) {
|
|
332
|
+
// Browser guests commonly use an immediately-ready timer alongside an
|
|
333
|
+
// asynchronous Web API pollable. Yield a host task so Fetch, timers, and
|
|
334
|
+
// other event sources can progress instead of starving in a sync loop.
|
|
335
|
+
return new Promise((resolve) => setTimeout(() => {
|
|
336
|
+
const result = [];
|
|
337
|
+
for (let i = 0; i < list.length; i++) {
|
|
338
|
+
if (list[i].ready()) {
|
|
339
|
+
result.push(i);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
resolve(new Uint32Array(result));
|
|
343
|
+
}, 0));
|
|
344
|
+
}
|
|
345
|
+
// None ready synchronously. Wait for the first to resolve via Promise.race,
|
|
346
|
+
// then sweep for any others that became ready concurrently.
|
|
347
|
+
return Promise.race(list.map((pollable) => pollable.block())).then(() => {
|
|
348
|
+
const result = [];
|
|
349
|
+
for (let i = 0; i < list.length; i++) {
|
|
350
|
+
if (list[i].ready()) {
|
|
351
|
+
result.push(i);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return new Uint32Array(result);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function pollOne(poll) {
|
|
358
|
+
return poll.block();
|
|
359
|
+
}
|
|
360
|
+
export const poll = {
|
|
361
|
+
Pollable,
|
|
362
|
+
pollList,
|
|
363
|
+
pollOne,
|
|
364
|
+
// @ts-expect-error Not matching signature from WIT
|
|
365
|
+
poll: pollList,
|
|
366
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { BrowserFilesystemAdapter, BrowserFilesystemDescriptor } from "./filesystem.js";
|
|
2
|
+
import type { FileData } from "./in-memory-filesystem.js";
|
|
3
|
+
/**
|
|
4
|
+
* A directory handle paired with the in-memory tree it was loaded into.
|
|
5
|
+
* `getRoot` hands the `data` half straight to `InMemoryFilesystemAdapter`, so every
|
|
6
|
+
* guest-visible filesystem operation stays fully synchronous; only the initial load
|
|
7
|
+
* and the explicit `flush`/`dispose` round-trip to OPFS are async.
|
|
8
|
+
*/
|
|
9
|
+
export interface OpfsCapability {
|
|
10
|
+
handle: FileSystemDirectoryHandle;
|
|
11
|
+
data: FileData;
|
|
12
|
+
}
|
|
13
|
+
/** Load an OPFS directory into an in-memory tree, ready to hand to `OpfsFilesystemAdapter.getRoot`. */
|
|
14
|
+
export declare function loadOpfsCapability(handle: FileSystemDirectoryHandle): Promise<OpfsCapability>;
|
|
15
|
+
/**
|
|
16
|
+
* The slice of `LockManager` this adapter needs - narrow enough to fake in tests or other hosts.
|
|
17
|
+
*
|
|
18
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/LockManager
|
|
19
|
+
*/
|
|
20
|
+
export interface BrowserLockManager extends Omit<LockManager, "query"> {
|
|
21
|
+
}
|
|
22
|
+
export interface OpfsFilesystemAdapterOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Also make advisory locks (`lockShared`/`lockExclusive`/`tryLock*`/`unlock`) request a
|
|
25
|
+
* matching lock from the given `BrowserLockManager` (usually the global `navigator.locks`,
|
|
26
|
+
* or a fake in tests), so tabs sharing the same OPFS root get best-effort cross-tab
|
|
27
|
+
* coordination on top of the default same-process reader/writer lock. Omit to keep locking
|
|
28
|
+
* same-process only - off by default.
|
|
29
|
+
*/
|
|
30
|
+
lockManager?: BrowserLockManager;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Browser filesystem adapter backed by the Origin Private File System.
|
|
34
|
+
*
|
|
35
|
+
* Every guest-facing `Descriptor` operation - including advisory locking, which
|
|
36
|
+
* comes for free as `InMemoryFilesystemAdapter`'s same-process reader/writer lock -
|
|
37
|
+
* is delegated to `InMemoryFilesystemAdapter` and stays synchronous. Persistence to
|
|
38
|
+
* OPFS happens automatically shortly after any mutation (debounced to a microtask,
|
|
39
|
+
* so a burst of writes only triggers one round-trip); `flush()`/`dispose()` remain
|
|
40
|
+
* available to force it explicitly, e.g. before navigating away.
|
|
41
|
+
*
|
|
42
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system
|
|
43
|
+
*/
|
|
44
|
+
export declare class OpfsFilesystemAdapter implements BrowserFilesystemAdapter<OpfsCapability> {
|
|
45
|
+
#private;
|
|
46
|
+
constructor(options?: OpfsFilesystemAdapterOptions);
|
|
47
|
+
getRoot(capability: OpfsCapability): BrowserFilesystemDescriptor;
|
|
48
|
+
/** Persist every loaded root, waiting for earlier automatic and explicit writes to close. */
|
|
49
|
+
flush(): Promise<void>;
|
|
50
|
+
/** Stop automatic persistence and wait for the final flush before releasing the backing store. */
|
|
51
|
+
dispose(): Promise<void>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { InMemoryFilesystemAdapter, UNWRAP_DESCRIPTOR, _onTouch } from "./in-memory-filesystem.js";
|
|
2
|
+
// OPFS has no native symlink concept. All symlinks under a root are recorded in a
|
|
3
|
+
// single hidden sidecar file at that root, keyed by their path relative to it, so
|
|
4
|
+
// they survive a flush + reload instead of only living for the lifetime of one
|
|
5
|
+
// capability.
|
|
6
|
+
const SYMLINKS_FILE = ".__wasi_symlinks__.json";
|
|
7
|
+
async function readSymlinksFile(handle) {
|
|
8
|
+
try {
|
|
9
|
+
const fileHandle = await handle.getFileHandle(SYMLINKS_FILE);
|
|
10
|
+
const file = await fileHandle.getFile();
|
|
11
|
+
return JSON.parse(await file.text());
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function writeSymlinksFile(handle, symlinks) {
|
|
18
|
+
if (Object.keys(symlinks).length === 0) {
|
|
19
|
+
try {
|
|
20
|
+
await handle.removeEntry(SYMLINKS_FILE);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// no sidecar to remove
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const fileHandle = await handle.getFileHandle(SYMLINKS_FILE, { create: true });
|
|
28
|
+
const writable = await fileHandle.createWritable();
|
|
29
|
+
await writable.write(JSON.stringify(symlinks));
|
|
30
|
+
await writable.close();
|
|
31
|
+
}
|
|
32
|
+
/** Collect every symlink under `dir` into a flat path -> target map, relative to `dir` itself. */
|
|
33
|
+
function collectSymlinks(dir, prefix, out) {
|
|
34
|
+
for (const [name, entry] of Object.entries(dir)) {
|
|
35
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
36
|
+
if (entry.symlink !== undefined) {
|
|
37
|
+
out[path] = entry.symlink;
|
|
38
|
+
}
|
|
39
|
+
else if (entry.dir) {
|
|
40
|
+
collectSymlinks(entry.dir, path, out);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Place each recorded symlink back into the tree at its path, skipping any path whose parent is missing. */
|
|
45
|
+
function injectSymlinks(root, symlinks) {
|
|
46
|
+
for (const [path, target] of Object.entries(symlinks)) {
|
|
47
|
+
const parts = path.split("/").filter(Boolean);
|
|
48
|
+
const name = parts.pop();
|
|
49
|
+
if (!name) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let dir = root;
|
|
53
|
+
for (const part of parts) {
|
|
54
|
+
dir = dir[part]?.dir;
|
|
55
|
+
if (!dir) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (dir && !(name in dir)) {
|
|
60
|
+
dir[name] = { symlink: target };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function readEntries(handle) {
|
|
65
|
+
const dir = {};
|
|
66
|
+
for await (const [name, child] of handle.entries()) {
|
|
67
|
+
if (name === SYMLINKS_FILE) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (child.kind === "directory") {
|
|
71
|
+
dir[name] = { dir: await readEntries(child) };
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const file = await child.getFile();
|
|
75
|
+
dir[name] = { source: new Uint8Array(await file.arrayBuffer()) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return dir;
|
|
79
|
+
}
|
|
80
|
+
async function writeEntries(handle, dir) {
|
|
81
|
+
const seen = new Set(Object.keys(dir));
|
|
82
|
+
seen.add(SYMLINKS_FILE);
|
|
83
|
+
for await (const [name] of handle.entries()) {
|
|
84
|
+
if (!seen.has(name)) {
|
|
85
|
+
await handle.removeEntry(name, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const [name, entry] of Object.entries(dir)) {
|
|
89
|
+
if (entry.symlink !== undefined) {
|
|
90
|
+
// Recorded separately in the root's symlink sidecar file.
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (entry.dir) {
|
|
94
|
+
await writeEntries(await handle.getDirectoryHandle(name, { create: true }), entry.dir);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const fileHandle = await handle.getFileHandle(name, { create: true });
|
|
98
|
+
const writable = await fileHandle.createWritable();
|
|
99
|
+
const source = entry.source ?? new Uint8Array();
|
|
100
|
+
const bytes = typeof source === "string" ? new TextEncoder().encode(source) : source;
|
|
101
|
+
await writable.write(bytes.slice());
|
|
102
|
+
await writable.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Load an OPFS directory into an in-memory tree, ready to hand to `OpfsFilesystemAdapter.getRoot`. */
|
|
106
|
+
export async function loadOpfsCapability(handle) {
|
|
107
|
+
const dir = await readEntries(handle);
|
|
108
|
+
injectSymlinks(dir, await readSymlinksFile(handle));
|
|
109
|
+
return { handle, data: { dir } };
|
|
110
|
+
}
|
|
111
|
+
const LOCK_METHODS = new Set([
|
|
112
|
+
"lockShared",
|
|
113
|
+
"lockExclusive",
|
|
114
|
+
"tryLockShared",
|
|
115
|
+
"tryLockExclusive",
|
|
116
|
+
"unlock",
|
|
117
|
+
]);
|
|
118
|
+
function lockModeOf(prop) {
|
|
119
|
+
return prop.endsWith("Exclusive") ? "exclusive" : "shared";
|
|
120
|
+
}
|
|
121
|
+
/** Join an OPFS-relative path onto an existing one, for naming a cross-tab lock. */
|
|
122
|
+
function joinLockPath(base, segment) {
|
|
123
|
+
let path = base;
|
|
124
|
+
for (const part of segment.split("/")) {
|
|
125
|
+
if (part === "" || part === ".") {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (part === "..") {
|
|
129
|
+
path = path.slice(0, path.lastIndexOf("/"));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
path = path ? `${path}/${part}` : part;
|
|
133
|
+
}
|
|
134
|
+
return path;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Wrap a descriptor so its advisory locking also makes a best-effort request against
|
|
138
|
+
* `BrowserLockManager`, in addition to the default same-process reader/writer lock.
|
|
139
|
+
* The Web Locks request is fire-and-forget - there's no way to block a synchronous
|
|
140
|
+
* `lockExclusive()` on a cross-tab grant without JSPI, so this only coordinates
|
|
141
|
+
* tabs that are already idle/cooperating.
|
|
142
|
+
*/
|
|
143
|
+
function withCrossTabLocking(descriptor, lockName, lockManager) {
|
|
144
|
+
let release = null;
|
|
145
|
+
function requestCrossTabLock(mode) {
|
|
146
|
+
if (!lockManager) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
lockManager
|
|
150
|
+
.request(lockName, { mode }, () => new Promise((resolve) => (release = resolve)))
|
|
151
|
+
.catch(() => {
|
|
152
|
+
// best-effort only; local same-process locking already enforced correctness
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function releaseCrossTabLock() {
|
|
156
|
+
release?.();
|
|
157
|
+
release = null;
|
|
158
|
+
}
|
|
159
|
+
return new Proxy(descriptor, {
|
|
160
|
+
get(target, prop, receiver) {
|
|
161
|
+
if (prop === UNWRAP_DESCRIPTOR) {
|
|
162
|
+
return target;
|
|
163
|
+
}
|
|
164
|
+
const value = Reflect.get(target, prop, receiver);
|
|
165
|
+
if (prop === "openAt") {
|
|
166
|
+
return (...args) => {
|
|
167
|
+
const child = Reflect.apply(value, target, args);
|
|
168
|
+
return withCrossTabLocking(child, joinLockPath(lockName, args[1]), lockManager);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (typeof prop === "string" && LOCK_METHODS.has(prop) && typeof value === "function") {
|
|
172
|
+
return (...args) => {
|
|
173
|
+
const result = Reflect.apply(value, target, args);
|
|
174
|
+
if (prop === "unlock") {
|
|
175
|
+
releaseCrossTabLock();
|
|
176
|
+
}
|
|
177
|
+
else if (prop.startsWith("tryLock")) {
|
|
178
|
+
if (result) {
|
|
179
|
+
requestCrossTabLock(lockModeOf(prop));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
requestCrossTabLock(lockModeOf(prop));
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Browser filesystem adapter backed by the Origin Private File System.
|
|
194
|
+
*
|
|
195
|
+
* Every guest-facing `Descriptor` operation - including advisory locking, which
|
|
196
|
+
* comes for free as `InMemoryFilesystemAdapter`'s same-process reader/writer lock -
|
|
197
|
+
* is delegated to `InMemoryFilesystemAdapter` and stays synchronous. Persistence to
|
|
198
|
+
* OPFS happens automatically shortly after any mutation (debounced to a microtask,
|
|
199
|
+
* so a burst of writes only triggers one round-trip); `flush()`/`dispose()` remain
|
|
200
|
+
* available to force it explicitly, e.g. before navigating away.
|
|
201
|
+
*
|
|
202
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system
|
|
203
|
+
*/
|
|
204
|
+
export class OpfsFilesystemAdapter {
|
|
205
|
+
#inMemory = new InMemoryFilesystemAdapter();
|
|
206
|
+
#roots = [];
|
|
207
|
+
#lockManager;
|
|
208
|
+
#flushScheduled = false;
|
|
209
|
+
#flushPromise = Promise.resolve();
|
|
210
|
+
#disposal;
|
|
211
|
+
#unsubscribeTouch;
|
|
212
|
+
constructor(options = {}) {
|
|
213
|
+
this.#lockManager = options.lockManager;
|
|
214
|
+
this.#unsubscribeTouch = _onTouch(() => this.#scheduleFlush());
|
|
215
|
+
}
|
|
216
|
+
getRoot(capability) {
|
|
217
|
+
this.#roots.push(capability);
|
|
218
|
+
const descriptor = this.#inMemory.getRoot(capability.data);
|
|
219
|
+
return this.#lockManager
|
|
220
|
+
? withCrossTabLocking(descriptor, capability.handle.name, this.#lockManager)
|
|
221
|
+
: descriptor;
|
|
222
|
+
}
|
|
223
|
+
#scheduleFlush() {
|
|
224
|
+
if (this.#flushScheduled) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.#flushScheduled = true;
|
|
228
|
+
queueMicrotask(() => {
|
|
229
|
+
if (!this.#flushScheduled) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.#flushScheduled = false;
|
|
233
|
+
void this.flush();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** Persist every loaded root, waiting for earlier automatic and explicit writes to close. */
|
|
237
|
+
flush() {
|
|
238
|
+
// This flush includes queued mutations; don't start another automatic pass for them.
|
|
239
|
+
this.#flushScheduled = false;
|
|
240
|
+
const flushed = this.#flushPromise
|
|
241
|
+
.catch(() => { })
|
|
242
|
+
.then(async () => {
|
|
243
|
+
await Promise.all(this.#roots.map(async (root) => {
|
|
244
|
+
const dir = root.data.dir ?? {};
|
|
245
|
+
await writeEntries(root.handle, dir);
|
|
246
|
+
const symlinks = {};
|
|
247
|
+
collectSymlinks(dir, "", symlinks);
|
|
248
|
+
await writeSymlinksFile(root.handle, symlinks);
|
|
249
|
+
}));
|
|
250
|
+
});
|
|
251
|
+
this.#flushPromise = flushed;
|
|
252
|
+
return flushed;
|
|
253
|
+
}
|
|
254
|
+
/** Stop automatic persistence and wait for the final flush before releasing the backing store. */
|
|
255
|
+
dispose() {
|
|
256
|
+
if (!this.#disposal) {
|
|
257
|
+
this.#unsubscribeTouch();
|
|
258
|
+
this.#disposal = this.flush();
|
|
259
|
+
}
|
|
260
|
+
return this.#disposal;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { insecure as InsecureNamespace, insecureSeed as InsecureSeedNamespace, random as RandomNamespace } from "../../types/random.js";
|
|
2
|
+
export declare const insecure: typeof InsecureNamespace;
|
|
3
|
+
export declare const insecureSeed: typeof InsecureSeedNamespace;
|
|
4
|
+
export declare const random: typeof RandomNamespace;
|