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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -2
  3. package/assets/wasi/LICENSE +220 -0
  4. package/assets/wasi/cli.d.ts +43 -0
  5. package/assets/wasi/cli.js +147 -0
  6. package/assets/wasi/clocks.d.ts +3 -0
  7. package/assets/wasi/clocks.js +55 -0
  8. package/assets/wasi/common.d.ts +5 -0
  9. package/assets/wasi/common.js +16 -0
  10. package/assets/wasi/config.d.ts +2 -0
  11. package/assets/wasi/config.js +7 -0
  12. package/assets/wasi/environment.d.ts +5 -0
  13. package/assets/wasi/environment.js +24 -0
  14. package/assets/wasi/filesystem.d.ts +117 -0
  15. package/assets/wasi/filesystem.js +341 -0
  16. package/assets/wasi/helpers/cli.d.ts +7 -0
  17. package/assets/wasi/helpers/cli.js +25 -0
  18. package/assets/wasi/helpers/filesystem.d.ts +6 -0
  19. package/assets/wasi/helpers/filesystem.js +10 -0
  20. package/assets/wasi/http.d.ts +13 -0
  21. package/assets/wasi/http.js +953 -0
  22. package/assets/wasi/in-memory-filesystem.d.ts +38 -0
  23. package/assets/wasi/in-memory-filesystem.js +620 -0
  24. package/assets/wasi/in-memory-http.d.ts +8 -0
  25. package/assets/wasi/in-memory-http.js +11 -0
  26. package/assets/wasi/in-memory-sockets.d.ts +34 -0
  27. package/assets/wasi/in-memory-sockets.js +294 -0
  28. package/assets/wasi/index.d.ts +7 -0
  29. package/assets/wasi/index.js +7 -0
  30. package/assets/wasi/io.d.ts +67 -0
  31. package/assets/wasi/io.js +366 -0
  32. package/assets/wasi/opfs-filesystem.d.ts +52 -0
  33. package/assets/wasi/opfs-filesystem.js +262 -0
  34. package/assets/wasi/random.d.ts +4 -0
  35. package/assets/wasi/random.js +51 -0
  36. package/assets/wasi/sockets.d.ts +9 -0
  37. package/assets/wasi/sockets.js +85 -0
  38. package/browser.mjs +209 -0
  39. package/client.mjs +138 -0
  40. package/index.d.ts +199 -0
  41. package/install.d.ts +22 -0
  42. package/install.mjs +176 -0
  43. package/native-registration.mjs +70 -0
  44. package/node.mjs +332 -0
  45. package/package.json +66 -4
  46. package/policy.mjs +433 -0
  47. 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;