@bytecodealliance/preview2-shim 0.24.1 → 0.25.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/README.md CHANGED
@@ -256,3 +256,14 @@ See [LICENSE](LICENSE) for more details.
256
256
  Unless you explicitly state otherwise, any contribution intentionally submitted
257
257
  for inclusion in this project by you, as defined in the Apache-2.0 license,
258
258
  shall be licensed as above, without any additional terms or conditions.
259
+
260
+ ### Host IO extensions
261
+
262
+ Opt-in providers can use the Node-only `@bytecodealliance/preview2-shim/io-worker`
263
+ entry point to operate on existing streams in the shim's IO worker. Host-selected
264
+ modules load lazily and share the worker's stream, future, and poll ownership rules.
265
+ Guest code cannot select extension modules.
266
+
267
+ The Node `wasi:tls` provider and its TLS policy live in
268
+ [`jco-std`](../jco-std/README.md#http-1), which wraps the supplied TCP streams
269
+ without opening a replacement connection.
@@ -3,6 +3,8 @@ import type { FileData } from "./in-memory-filesystem.js";
3
3
  export { _setCwd } from "./config.js";
4
4
  export { InMemoryFilesystemAdapter } from "./in-memory-filesystem.js";
5
5
  export type { FileData, FileDataEntry } from "./in-memory-filesystem.js";
6
+ export { OpfsFilesystemAdapter, loadOpfsCapability } from "./opfs-filesystem.js";
7
+ export type { OpfsCapability } from "./opfs-filesystem.js";
6
8
  type Filesize = TypesNamespace.Filesize;
7
9
  type OpenFlags = TypesNamespace.OpenFlags;
8
10
  type PathFlags = TypesNamespace.PathFlags;
@@ -15,6 +17,17 @@ export interface BrowserFilesystemDescriptor extends Omit<TypesNamespace.Descrip
15
17
  openAt(pathFlags: PathFlags, path: string, openFlags: OpenFlags, flags: TypesNamespace.DescriptorFlags): BrowserFilesystemDescriptor;
16
18
  renameAt(oldPath: string, newDescriptor: BrowserFilesystemDescriptor, newPath: string): void;
17
19
  isSameObject(other: BrowserFilesystemDescriptor): boolean;
20
+ /**
21
+ * Advisory locking, for host application code holding a descriptor directly
22
+ * (not part of the WASI guest-facing surface). Adapters that support it should
23
+ * implement all five; `tryLock*` return whether the lock was acquired instead
24
+ * of throwing.
25
+ */
26
+ lockShared?(): void;
27
+ lockExclusive?(): void;
28
+ tryLockShared?(): boolean;
29
+ tryLockExclusive?(): boolean;
30
+ unlock?(): void;
18
31
  }
19
32
  export interface BrowserFilesystemAdapter<Capability = unknown> {
20
33
  getRoot(capability: Capability): BrowserFilesystemDescriptor;
@@ -60,6 +73,11 @@ declare class Descriptor implements TypesNamespace.Descriptor {
60
73
  isSameObject(other: TypesNamespace.Descriptor): boolean;
61
74
  metadataHash(): TypesNamespace.MetadataHashValue;
62
75
  metadataHashAt(pathFlags: PathFlags, path: string): TypesNamespace.MetadataHashValue;
76
+ lockShared(): void | undefined;
77
+ lockExclusive(): void | undefined;
78
+ tryLockShared(): boolean;
79
+ tryLockExclusive(): boolean;
80
+ unlock(): void | undefined;
63
81
  }
64
82
  export declare const preopens: typeof PreopensNamespace;
65
83
  /** Create isolated filesystem namespaces backed by an application-selected adapter. */
@@ -81,6 +99,15 @@ export declare function _setPreopens(preopensConfig: Record<string, FileData>):
81
99
  * @param fileData - The file data object representing the directory
82
100
  */
83
101
  export declare function _addPreopen(virtualPath: string, fileData: FileData): void;
102
+ /**
103
+ * Add a single preopen backed by a custom adapter (e.g. `OpfsFilesystemAdapter`) instead of the
104
+ * default in-memory one. Lets a host wire an alternative `BrowserFilesystemAdapter` into the
105
+ * top-level `wasi:filesystem/preopens` singleton that transpiled components import statically.
106
+ * @param virtualPath - The virtual path visible to the guest
107
+ * @param adapter - The adapter that will back this preopen
108
+ * @param capability - The adapter-specific capability to load as the preopen's root
109
+ */
110
+ export declare function _addPreopenWithAdapter<Capability>(virtualPath: string, adapter: BrowserFilesystemAdapter<Capability>, capability: Capability): void;
84
111
  /** Clear all preopens, giving the guest no filesystem access. */
85
112
  export declare function _clearPreopens(): void;
86
113
  /** Get current preopens configuration. */
@@ -3,6 +3,7 @@ import { InMemoryFilesystemAdapter } from "./in-memory-filesystem.js";
3
3
  import { _setCwd } from "./config.js";
4
4
  export { _setCwd } from "./config.js";
5
5
  export { InMemoryFilesystemAdapter } from "./in-memory-filesystem.js";
6
+ export { OpfsFilesystemAdapter, loadOpfsCapability } from "./opfs-filesystem.js";
6
7
  class DirectoryEntryStream {
7
8
  #implementation;
8
9
  static _create(implementation) {
@@ -108,6 +109,21 @@ class Descriptor {
108
109
  metadataHashAt(pathFlags, path) {
109
110
  return this.#implementation.metadataHashAt(pathFlags, path);
110
111
  }
112
+ lockShared() {
113
+ return this.#implementation.lockShared?.();
114
+ }
115
+ lockExclusive() {
116
+ return this.#implementation.lockExclusive?.();
117
+ }
118
+ tryLockShared() {
119
+ return this.#implementation.tryLockShared?.() ?? false;
120
+ }
121
+ tryLockExclusive() {
122
+ return this.#implementation.tryLockExclusive?.() ?? false;
123
+ }
124
+ unlock() {
125
+ return this.#implementation.unlock?.();
126
+ }
111
127
  }
112
128
  const descriptorGetImplementation = Descriptor.prototype._getImplementation;
113
129
  // @ts-expect-error - Deleting prototype method
@@ -185,6 +201,22 @@ export function _addPreopen(virtualPath, fileData) {
185
201
  _rootPreopen = entry;
186
202
  }
187
203
  }
204
+ /**
205
+ * Add a single preopen backed by a custom adapter (e.g. `OpfsFilesystemAdapter`) instead of the
206
+ * default in-memory one. Lets a host wire an alternative `BrowserFilesystemAdapter` into the
207
+ * top-level `wasi:filesystem/preopens` singleton that transpiled components import statically.
208
+ * @param virtualPath - The virtual path visible to the guest
209
+ * @param adapter - The adapter that will back this preopen
210
+ * @param capability - The adapter-specific capability to load as the preopen's root
211
+ */
212
+ export function _addPreopenWithAdapter(virtualPath, adapter, capability) {
213
+ const descriptor = descriptorCreate(adapter.getRoot(capability));
214
+ const entry = [descriptor, virtualPath];
215
+ _preopens.push(entry);
216
+ if (virtualPath === "/") {
217
+ _rootPreopen = entry;
218
+ }
219
+ }
188
220
  /** Clear all preopens, giving the guest no filesystem access. */
189
221
  export function _clearPreopens() {
190
222
  _preopens = [];
@@ -16,6 +16,22 @@ export interface FileDataEntry {
16
16
  * };
17
17
  */
18
18
  export type FileData = FileDataEntry;
19
+ /**
20
+ * Subscribe to every mutation across every in-memory tree (regardless of which
21
+ * adapter loaded it). Lets a persistence layer (e.g. `OpfsFilesystemAdapter`)
22
+ * react to writes without wrapping every mutating method individually. Returns
23
+ * an unsubscribe function.
24
+ */
25
+ export declare function _onTouch(listener: (entry: FileDataEntry) => void): () => void;
26
+ /**
27
+ * Well-known symbol a `BrowserFilesystemDescriptor` wrapper (e.g. the cross-tab-locking
28
+ * `Proxy` from `OpfsFilesystemAdapter`) can implement to hand back the real descriptor it
29
+ * wraps. `renameAt`/`linkAt`/`isSameObject` reach into a *second* descriptor argument's
30
+ * private `#entry` field directly - private-field access bypasses `Proxy` traps and fails
31
+ * its brand check against a wrapper, so a wrapper must be unwrapped via ordinary property
32
+ * access (which a `Proxy` handles correctly) before that field access happens.
33
+ */
34
+ export declare const UNWRAP_DESCRIPTOR: unique symbol;
19
35
  /** Explicit ephemeral storage adapter for browser applications and tests. */
20
36
  export declare class InMemoryFilesystemAdapter implements BrowserFilesystemAdapter<FileData> {
21
37
  getRoot(capability: FileData): BrowserFilesystemDescriptor;
@@ -138,8 +138,31 @@ function metadata(entry) {
138
138
  }
139
139
  return value;
140
140
  }
141
+ const fileLocks = new WeakMap();
142
+ function lockState(entry) {
143
+ let state = fileLocks.get(entry);
144
+ if (!state) {
145
+ state = { exclusiveHolder: null, sharedHolders: new Set() };
146
+ fileLocks.set(entry, state);
147
+ }
148
+ return state;
149
+ }
150
+ const touchListeners = new Set();
151
+ /**
152
+ * Subscribe to every mutation across every in-memory tree (regardless of which
153
+ * adapter loaded it). Lets a persistence layer (e.g. `OpfsFilesystemAdapter`)
154
+ * react to writes without wrapping every mutating method individually. Returns
155
+ * an unsubscribe function.
156
+ */
157
+ export function _onTouch(listener) {
158
+ touchListeners.add(listener);
159
+ return () => touchListeners.delete(listener);
160
+ }
141
161
  function touch(entry) {
142
162
  metadata(entry).version++;
163
+ for (const listener of touchListeners) {
164
+ listener(entry);
165
+ }
143
166
  }
144
167
  function getFileWriteBuffer(entry, source, requiredLength) {
145
168
  let buffer = fileWriteBuffers.get(entry);
@@ -374,7 +397,7 @@ class Descriptor {
374
397
  if (entry.dir) {
375
398
  throw "not-permitted";
376
399
  }
377
- const [newParent, newName] = getParentEntry(descriptorGetEntry(newDescriptor), newPath);
400
+ const [newParent, newName] = getParentEntry(descriptorGetEntry(unwrapDescriptor(newDescriptor)), newPath);
378
401
  if (newParent.dir[newName]) {
379
402
  throw "exist";
380
403
  }
@@ -447,7 +470,7 @@ class Descriptor {
447
470
  if (!entry) {
448
471
  throw "no-entry";
449
472
  }
450
- const [newParent, newName] = getParentEntry(descriptorGetEntry(newDescriptor), newPath);
473
+ const [newParent, newName] = getParentEntry(descriptorGetEntry(unwrapDescriptor(newDescriptor)), newPath);
451
474
  const replaced = newParent.dir[newName];
452
475
  if ((oldParent === newParent && oldName === newName) || replaced === entry) {
453
476
  return;
@@ -501,7 +524,7 @@ class Descriptor {
501
524
  touch(parent);
502
525
  }
503
526
  isSameObject(other) {
504
- return descriptorGetEntry(other) === this.#entry;
527
+ return descriptorGetEntry(unwrapDescriptor(other)) === this.#entry;
505
528
  }
506
529
  metadataHash() {
507
530
  const value = metadata(this.#entry);
@@ -511,6 +534,54 @@ class Descriptor {
511
534
  const value = metadata(getChildEntry(this.#entry, path, pathFlags.symlinkFollow));
512
535
  return { upper: value.id, lower: value.version };
513
536
  }
537
+ /**
538
+ * Default advisory-locking implementation: a same-process reader/writer lock
539
+ * keyed on the underlying entry. There's no real contention to wait out in a
540
+ * single-threaded environment, so `lockShared`/`lockExclusive` don't block -
541
+ * they throw `would-block` immediately when the lock isn't free, same as the
542
+ * `tryLock*` variants report `false`.
543
+ */
544
+ tryLockShared() {
545
+ const state = lockState(this.#entry);
546
+ if (state.exclusiveHolder && state.exclusiveHolder !== this) {
547
+ return false;
548
+ }
549
+ state.sharedHolders.add(this);
550
+ return true;
551
+ }
552
+ tryLockExclusive() {
553
+ const state = lockState(this.#entry);
554
+ if (state.exclusiveHolder && state.exclusiveHolder !== this) {
555
+ return false;
556
+ }
557
+ const otherReaders = state.sharedHolders.size - (state.sharedHolders.has(this) ? 1 : 0);
558
+ if (otherReaders > 0) {
559
+ return false;
560
+ }
561
+ state.sharedHolders.delete(this);
562
+ state.exclusiveHolder = this;
563
+ return true;
564
+ }
565
+ lockShared() {
566
+ if (!this.tryLockShared()) {
567
+ throw "would-block";
568
+ }
569
+ }
570
+ lockExclusive() {
571
+ if (!this.tryLockExclusive()) {
572
+ throw "would-block";
573
+ }
574
+ }
575
+ unlock() {
576
+ const state = fileLocks.get(this.#entry);
577
+ if (!state) {
578
+ return;
579
+ }
580
+ state.sharedHolders.delete(this);
581
+ if (state.exclusiveHolder === this) {
582
+ state.exclusiveHolder = null;
583
+ }
584
+ }
514
585
  }
515
586
  const descriptorGetEntry = Descriptor.prototype._getEntry;
516
587
  // @ts-expect-error - Deleting prototype method
@@ -518,6 +589,25 @@ delete Descriptor.prototype._getEntry;
518
589
  const descriptorCreate = Descriptor._create;
519
590
  // @ts-expect-error - Deleting static method
520
591
  delete Descriptor._create;
592
+ /**
593
+ * Well-known symbol a `BrowserFilesystemDescriptor` wrapper (e.g. the cross-tab-locking
594
+ * `Proxy` from `OpfsFilesystemAdapter`) can implement to hand back the real descriptor it
595
+ * wraps. `renameAt`/`linkAt`/`isSameObject` reach into a *second* descriptor argument's
596
+ * private `#entry` field directly - private-field access bypasses `Proxy` traps and fails
597
+ * its brand check against a wrapper, so a wrapper must be unwrapped via ordinary property
598
+ * access (which a `Proxy` handles correctly) before that field access happens.
599
+ */
600
+ export const UNWRAP_DESCRIPTOR = Symbol("browserFilesystemDescriptor.unwrap");
601
+ function unwrapDescriptor(descriptor) {
602
+ let current = descriptor;
603
+ for (;;) {
604
+ const inner = current[UNWRAP_DESCRIPTOR];
605
+ if (!inner || inner === current) {
606
+ return current;
607
+ }
608
+ current = inner;
609
+ }
610
+ }
521
611
  /** Explicit ephemeral storage adapter for browser applications and tests. */
522
612
  export class InMemoryFilesystemAdapter {
523
613
  getRoot(capability) {
@@ -0,0 +1,51 @@
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's current in-memory state back to OPFS. */
49
+ flush(): Promise<void>;
50
+ dispose(): void;
51
+ }
@@ -0,0 +1,245 @@
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
+ #unsubscribeTouch;
210
+ constructor(options = {}) {
211
+ this.#lockManager = options.lockManager;
212
+ this.#unsubscribeTouch = _onTouch(() => this.#scheduleFlush());
213
+ }
214
+ getRoot(capability) {
215
+ this.#roots.push(capability);
216
+ const descriptor = this.#inMemory.getRoot(capability.data);
217
+ return this.#lockManager
218
+ ? withCrossTabLocking(descriptor, capability.handle.name, this.#lockManager)
219
+ : descriptor;
220
+ }
221
+ #scheduleFlush() {
222
+ if (this.#flushScheduled) {
223
+ return;
224
+ }
225
+ this.#flushScheduled = true;
226
+ queueMicrotask(() => {
227
+ this.#flushScheduled = false;
228
+ void this.flush();
229
+ });
230
+ }
231
+ /** Persist every loaded root's current in-memory state back to OPFS. */
232
+ async flush() {
233
+ await Promise.all(this.#roots.map(async (root) => {
234
+ const dir = root.data.dir ?? {};
235
+ await writeEntries(root.handle, dir);
236
+ const symlinks = {};
237
+ collectSymlinks(dir, "", symlinks);
238
+ await writeSymlinksFile(root.handle, symlinks);
239
+ }));
240
+ }
241
+ dispose() {
242
+ this.#unsubscribeTouch();
243
+ void this.flush();
244
+ }
245
+ }
@@ -100,4 +100,5 @@ export declare const SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST: number;
100
100
  export declare const SOCKET_RESOLVE_ADDRESS_TAKE_REQUEST: number;
101
101
  export declare const SOCKET_RESOLVE_ADDRESS_SUBSCRIBE_REQUEST: number;
102
102
  export declare const SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST: number;
103
+ export declare const WORKER_EXTENSION_CALL: number;
103
104
  export declare const reverseMap: {};
package/dist/io/calls.js CHANGED
@@ -116,6 +116,8 @@ export const SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST = ++call_id << CALL_SHIFT;
116
116
  export const SOCKET_RESOLVE_ADDRESS_TAKE_REQUEST = ++call_id << CALL_SHIFT;
117
117
  export const SOCKET_RESOLVE_ADDRESS_SUBSCRIBE_REQUEST = ++call_id << CALL_SHIFT;
118
118
  export const SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST = ++call_id << CALL_SHIFT;
119
+ // Host extensions execute alongside the streams they operate on.
120
+ export const WORKER_EXTENSION_CALL = ++call_id << CALL_SHIFT;
119
121
  export const reverseMap = {};
120
122
  import * as calls from "./calls.js";
121
123
  for (const name of Object.getOwnPropertyNames(calls)) {
@@ -0,0 +1,15 @@
1
+ /** IO-worker integration for opt-in host providers. No capability is installed by importing this module. */
2
+ import type { Readable, Writable } from "node:stream";
3
+ export interface WorkerExtensionContext {
4
+ createFuture(promise: Promise<void>): number;
5
+ createReadableStream(stream: Readable): number;
6
+ createWritableStream(stream: Writable): number;
7
+ getStream(id: number): unknown;
8
+ resourceCounts(): {
9
+ streams: number;
10
+ futures: number;
11
+ polls: number;
12
+ sockets: number;
13
+ };
14
+ }
15
+ export type WorkerExtension = (operation: string, args: unknown[]) => unknown | Promise<unknown>;
@@ -0,0 +1 @@
1
+ export {};
@@ -11,9 +11,6 @@ export declare function noLookup(ip: any, _opts: any, cb: any): void;
11
11
  export declare function socketResolveAddress(name: string): Promise<{
12
12
  tag: string;
13
13
  val: Ipv4Address | Ipv6Address;
14
- }[][]> | Promise<{
15
- tag: string;
16
- val: Ipv4Address | Ipv6Address;
17
14
  }[]>;
18
15
  export declare function convertSocketError(err: any): "access-denied" | "address-in-use" | "address-not-bindable" | "concurrency-conflict" | "connection-aborted" | "connection-refused" | "connection-reset" | "datagram-too-large" | "invalid-argument" | "invalid-state" | "new-socket-limit" | "not-supported" | "out-of-memory" | "remote-unreachable" | "unknown" | "would-block";
19
16
  export declare function convertSocketErrorCode(code: any): "access-denied" | "address-in-use" | "address-not-bindable" | "concurrency-conflict" | "connection-aborted" | "connection-refused" | "connection-reset" | "datagram-too-large" | "invalid-argument" | "invalid-state" | "new-socket-limit" | "not-supported" | "out-of-memory" | "remote-unreachable" | "unknown" | "would-block";
@@ -51,12 +51,10 @@ export function socketResolveAddress(name) {
51
51
  }
52
52
  return lookup(name, dnsLookupOptions).then((addresses) => {
53
53
  return (Array.isArray(addresses) ? addresses : [addresses]).map(({ address, family }) => {
54
- return [
55
- {
56
- tag: "ipv" + family,
57
- val: (family === 4 ? ipv4ToTuple : ipv6ToTuple)(address),
58
- },
59
- ];
54
+ return {
55
+ tag: "ipv" + family,
56
+ val: (family === 4 ? ipv4ToTuple : ipv6ToTuple)(address),
57
+ };
60
58
  });
61
59
  }, (err) => {
62
60
  switch (err.code) {
@@ -23,54 +23,6 @@ var __exportAll = (all, no_symbols) => {
23
23
  return target;
24
24
  };
25
25
  //#endregion
26
- //#region dist/synckit/index.js
27
- function extractProperties(object) {
28
- if (object && typeof object === "object") {
29
- const properties = {};
30
- for (const key in object) properties[key] = object[key];
31
- return properties;
32
- }
33
- }
34
- function runAsWorker(fn) {
35
- if (!workerData) return;
36
- const { workerPort, debug } = workerData;
37
- try {
38
- parentPort?.on("message", ({ sharedBuffer, cid, args }) => {
39
- (async () => {
40
- const sharedBufferView = new Int32Array(sharedBuffer);
41
- let msg;
42
- try {
43
- msg = {
44
- cid,
45
- result: await fn(...args)
46
- };
47
- } catch (error) {
48
- msg = {
49
- cid,
50
- error,
51
- properties: extractProperties(error)
52
- };
53
- }
54
- workerPort.postMessage(msg);
55
- Atomics.add(sharedBufferView, 0, 1);
56
- Atomics.notify(sharedBufferView, 0);
57
- })();
58
- });
59
- } catch (error) {
60
- parentPort?.on("message", ({ sharedBuffer, cid }) => {
61
- const sharedBufferView = new Int32Array(sharedBuffer);
62
- workerPort.postMessage({
63
- cid,
64
- error,
65
- properties: extractProperties(error)
66
- });
67
- Atomics.add(sharedBufferView, 0, 1);
68
- Atomics.notify(sharedBufferView, 0);
69
- });
70
- }
71
- return debug;
72
- }
73
- //#endregion
74
26
  //#region dist/io/calls.js
75
27
  var calls_exports = /* @__PURE__ */ __exportAll({
76
28
  CALL_MASK: () => CALL_MASK,
@@ -175,6 +127,7 @@ var calls_exports = /* @__PURE__ */ __exportAll({
175
127
  STDERR: () => STDERR,
176
128
  STDIN: () => STDIN,
177
129
  STDOUT: () => STDOUT,
130
+ WORKER_EXTENSION_CALL: () => WORKER_EXTENSION_CALL,
178
131
  reverseMap: () => reverseMap
179
132
  });
180
133
  let call_id = 0;
@@ -280,12 +233,61 @@ const SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST = ++call_id << 24;
280
233
  const SOCKET_RESOLVE_ADDRESS_TAKE_REQUEST = ++call_id << 24;
281
234
  const SOCKET_RESOLVE_ADDRESS_SUBSCRIBE_REQUEST = ++call_id << 24;
282
235
  const SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST = ++call_id << 24;
236
+ const WORKER_EXTENSION_CALL = ++call_id << 24;
283
237
  const reverseMap = {};
284
238
  for (const name of Object.getOwnPropertyNames(calls_exports)) {
285
239
  if (name === "reverseMap") continue;
286
240
  reverseMap[calls_exports[name]] = name;
287
241
  }
288
242
  //#endregion
243
+ //#region dist/synckit/index.js
244
+ function extractProperties(object) {
245
+ if (object && typeof object === "object") {
246
+ const properties = {};
247
+ for (const key in object) properties[key] = object[key];
248
+ return properties;
249
+ }
250
+ }
251
+ function runAsWorker(fn) {
252
+ if (!workerData) return;
253
+ const { workerPort, debug } = workerData;
254
+ try {
255
+ parentPort?.on("message", ({ sharedBuffer, cid, args }) => {
256
+ (async () => {
257
+ const sharedBufferView = new Int32Array(sharedBuffer);
258
+ let msg;
259
+ try {
260
+ msg = {
261
+ cid,
262
+ result: await fn(...args)
263
+ };
264
+ } catch (error) {
265
+ msg = {
266
+ cid,
267
+ error,
268
+ properties: extractProperties(error)
269
+ };
270
+ }
271
+ workerPort.postMessage(msg);
272
+ Atomics.add(sharedBufferView, 0, 1);
273
+ Atomics.notify(sharedBufferView, 0);
274
+ })();
275
+ });
276
+ } catch (error) {
277
+ parentPort?.on("message", ({ sharedBuffer, cid }) => {
278
+ const sharedBufferView = new Int32Array(sharedBuffer);
279
+ workerPort.postMessage({
280
+ cid,
281
+ error,
282
+ properties: extractProperties(error)
283
+ });
284
+ Atomics.add(sharedBufferView, 0, 1);
285
+ Atomics.notify(sharedBufferView, 0);
286
+ });
287
+ }
288
+ return debug;
289
+ }
290
+ //#endregion
289
291
  //#region dist/io/worker-http.js
290
292
  const agentOptions = { keepAlive: true };
291
293
  const httpAgent = new Agent(agentOptions);
@@ -577,10 +579,10 @@ function socketResolveAddress(name) {
577
579
  if (!parsedUrl) throw "invalid-argument";
578
580
  return lookup(name, dnsLookupOptions).then((addresses) => {
579
581
  return (Array.isArray(addresses) ? addresses : [addresses]).map(({ address, family }) => {
580
- return [{
582
+ return {
581
583
  tag: "ipv" + family,
582
584
  val: (family === 4 ? ipv4ToTuple : ipv6ToTuple)(address)
583
- }];
585
+ };
584
586
  });
585
587
  }, (err) => {
586
588
  switch (err.code) {
@@ -1438,6 +1440,28 @@ function getStreamOrThrow(streamId) {
1438
1440
  if (stream.stream.closed) throw { tag: "closed" };
1439
1441
  return stream;
1440
1442
  }
1443
+ const extensions = /* @__PURE__ */ new Map();
1444
+ async function callExtension(module, operation, args) {
1445
+ let extension = extensions.get(module);
1446
+ if (!extension) {
1447
+ extension = import(module).then(({ default: create }) => {
1448
+ return create({
1449
+ createFuture: (promise) => createFuture(promise, void 0),
1450
+ createReadableStream,
1451
+ createWritableStream,
1452
+ getStream: (id) => getStreamOrThrow(id).stream,
1453
+ resourceCounts: () => ({
1454
+ streams: streams.size,
1455
+ futures: futures.size,
1456
+ polls: polls.size,
1457
+ sockets: tcpSockets.size
1458
+ })
1459
+ });
1460
+ });
1461
+ extensions.set(module, extension);
1462
+ }
1463
+ return (await extension)(operation, args);
1464
+ }
1441
1465
  /**
1442
1466
  * @param {number} call
1443
1467
  * @param {number | null} id
@@ -1447,6 +1471,7 @@ function getStreamOrThrow(streamId) {
1447
1471
  function handle(call, id, payload) {
1448
1472
  if (uncaughtException) throw uncaughtException;
1449
1473
  switch (call) {
1474
+ case WORKER_EXTENSION_CALL: return callExtension(payload.module, payload.operation, payload.args);
1450
1475
  case HTTP_CREATE_REQUEST: {
1451
1476
  const { method, scheme, authority, pathWithQuery, headers, body, connectTimeout, betweenBytesTimeout, firstByteTimeout } = payload;
1452
1477
  return createFuture(createHttpRequest(method, scheme, authority, pathWithQuery, headers, body, connectTimeout, betweenBytesTimeout, firstByteTimeout), void 0);
@@ -1806,12 +1831,7 @@ function handle(call, id, payload) {
1806
1831
  if (!polls.delete(id)) throw new Error(`wasi-io trap: Disposed a poll ${id} that does not exist`);
1807
1832
  return;
1808
1833
  case FUTURE_TAKE_VALUE: return futureTakeValue(id);
1809
- case FUTURE_SUBSCRIBE: {
1810
- const { pollState } = futures.get(id);
1811
- const pollId = ++pollCnt;
1812
- polls.set(pollId, pollState);
1813
- return pollId;
1814
- }
1834
+ case FUTURE_SUBSCRIBE: return createPoll(futures.get(id).pollState);
1815
1835
  case FUTURE_DISPOSE:
1816
1836
  futureDispose(id, true);
1817
1837
  return;
@@ -1,3 +1,4 @@
1
+ import { WORKER_EXTENSION_CALL } from "./calls.js";
1
2
  import { createReadStream, createWriteStream } from "node:fs";
2
3
  import { hrtime, stderr, stdout } from "node:process";
3
4
  import { PassThrough } from "node:stream";
@@ -118,6 +119,29 @@ export function getStreamOrThrow(streamId) {
118
119
  }
119
120
  return stream;
120
121
  }
122
+ const extensions = new Map();
123
+ async function callExtension(module, operation, args) {
124
+ let extension = extensions.get(module);
125
+ if (!extension) {
126
+ extension = import(module).then(({ default: create }) => {
127
+ const context = {
128
+ createFuture: (promise) => createFuture(promise, undefined),
129
+ createReadableStream,
130
+ createWritableStream,
131
+ getStream: (id) => getStreamOrThrow(id).stream,
132
+ resourceCounts: () => ({
133
+ streams: streams.size,
134
+ futures: futures.size,
135
+ polls: polls.size,
136
+ sockets: tcpSockets.size,
137
+ }),
138
+ };
139
+ return create(context);
140
+ });
141
+ extensions.set(module, extension);
142
+ }
143
+ return (await extension)(operation, args);
144
+ }
121
145
  /**
122
146
  * @param {number} call
123
147
  * @param {number | null} id
@@ -129,6 +153,8 @@ function handle(call, id, payload) {
129
153
  throw uncaughtException;
130
154
  }
131
155
  switch (call) {
156
+ case WORKER_EXTENSION_CALL:
157
+ return callExtension(payload.module, payload.operation, payload.args);
132
158
  // Http
133
159
  case HTTP_CREATE_REQUEST: {
134
160
  const { method, scheme, authority, pathWithQuery, headers, body, connectTimeout, betweenBytesTimeout, firstByteTimeout, } = payload;
@@ -685,10 +711,7 @@ function handle(call, id, payload) {
685
711
  case FUTURE_TAKE_VALUE:
686
712
  return futureTakeValue(id);
687
713
  case FUTURE_SUBSCRIBE: {
688
- const { pollState } = futures.get(id);
689
- const pollId = ++pollCnt;
690
- polls.set(pollId, pollState);
691
- return pollId;
714
+ return createPoll(futures.get(id).pollState);
692
715
  }
693
716
  case FUTURE_DISPOSE:
694
717
  return void futureDispose(id, true);
@@ -0,0 +1,28 @@
1
+ import type { InputStream, OutputStream } from "../../types/interfaces/wasi-io-streams.js";
2
+ import type { Pollable } from "../../types/interfaces/wasi-io-poll.js";
3
+ import type { Error as IoError } from "../../types/interfaces/wasi-io-error.js";
4
+ export type { WorkerExtension, WorkerExtensionContext } from "../io/extension.js";
5
+ export type FutureResult<T, E> = {
6
+ tag: "err";
7
+ val?: undefined;
8
+ } | {
9
+ tag: "ok";
10
+ val: {
11
+ tag: "ok";
12
+ val: T;
13
+ } | {
14
+ tag: "err";
15
+ val: E;
16
+ };
17
+ } | undefined;
18
+ /** Loads a host-selected module once in the existing worker; never accepts a guest module path. */
19
+ export declare function callExtension(module: URL, operation: string, args: unknown[]): unknown;
20
+ export declare function inputStreamId(stream: InputStream): number;
21
+ export declare function outputStreamId(stream: OutputStream): number;
22
+ export declare function inputStreamCreate(id: number): InputStream;
23
+ export declare function outputStreamCreate(id: number): OutputStream;
24
+ export declare function futureSubscribe(id: number, parent: object): Pollable;
25
+ /** T and E are the promise's value and rejection types chosen by the host extension. */
26
+ export declare function futureTakeValue<T, E>(id: number): FutureResult<T, E>;
27
+ export declare function futureDispose(id: number): void;
28
+ export declare function createIoError(message: string): IoError;
@@ -0,0 +1,32 @@
1
+ /** Low-level integration with the shared Node IO worker for opt-in host providers. */
2
+ import * as io from "../io/worker-io.js";
3
+ import { WORKER_EXTENSION_CALL, SOCKET_TCP, FUTURE_TAKE_VALUE, FUTURE_SUBSCRIBE, FUTURE_DISPOSE, } from "../io/calls.js";
4
+ /** Loads a host-selected module once in the existing worker; never accepts a guest module path. */
5
+ export function callExtension(module, operation, args) {
6
+ return io.ioCall(WORKER_EXTENSION_CALL, null, { module: module.href, operation, args });
7
+ }
8
+ export function inputStreamId(stream) {
9
+ return io.inputStreamId(stream);
10
+ }
11
+ export function outputStreamId(stream) {
12
+ return io.outputStreamId(stream);
13
+ }
14
+ export function inputStreamCreate(id) {
15
+ return io.inputStreamCreate(SOCKET_TCP, id);
16
+ }
17
+ export function outputStreamCreate(id) {
18
+ return io.outputStreamCreate(SOCKET_TCP, id);
19
+ }
20
+ export function futureSubscribe(id, parent) {
21
+ return io.pollableCreate(io.ioCall(FUTURE_SUBSCRIBE, id, undefined), parent);
22
+ }
23
+ /** T and E are the promise's value and rejection types chosen by the host extension. */
24
+ export function futureTakeValue(id) {
25
+ return io.ioCall(FUTURE_TAKE_VALUE, id, undefined);
26
+ }
27
+ export function futureDispose(id) {
28
+ io.ioCall(FUTURE_DISPOSE, id, undefined);
29
+ }
30
+ export function createIoError(message) {
31
+ return new io.error.Error(message);
32
+ }
@@ -1,6 +1,7 @@
1
1
  import type { instanceNetwork as InstanceNetworkNamespace, ipNameLookup as IpNameLookupNamespace, network as NetworkNamespace, tcpCreateSocket as TcpCreateSocketNamespace, tcp as TcpNamespace, udpCreateSocket as UdpCreateSocketNamespace, udp as UdpNamespace } from "../../types/sockets.js";
2
2
  declare class Network implements NetworkNamespace.Network {
3
3
  #private;
4
+ noop(): void;
4
5
  static _denyDnsLookup(network?: Network): void;
5
6
  static _denyTcp(network?: Network): void;
6
7
  static _denyUdp(network?: Network): void;
@@ -12,7 +13,11 @@ export declare const _denyDnsLookup: typeof Network._denyDnsLookup;
12
13
  export declare const _denyTcp: typeof Network._denyTcp;
13
14
  export declare const _denyUdp: typeof Network._denyUdp;
14
15
  export declare const instanceNetwork: typeof InstanceNetworkNamespace;
15
- export declare const network: typeof NetworkNamespace;
16
+ export declare const network: typeof NetworkNamespace & {
17
+ networkErrorCode(error: {
18
+ toDebugString(): string;
19
+ }): NetworkNamespace.ErrorCode | undefined;
20
+ };
16
21
  export declare const ipNameLookup: typeof IpNameLookupNamespace;
17
22
  export declare const tcpCreateSocket: typeof TcpCreateSocketNamespace;
18
23
  export declare const tcp: typeof TcpNamespace;
@@ -24,7 +29,11 @@ export interface SocketsConfig {
24
29
  /** Create socket namespaces with an instance-local network capability. */
25
30
  export declare function createSockets(config?: SocketsConfig): {
26
31
  instanceNetwork: typeof InstanceNetworkNamespace;
27
- network: typeof NetworkNamespace;
32
+ network: typeof NetworkNamespace & {
33
+ networkErrorCode(error: {
34
+ toDebugString(): string;
35
+ }): NetworkNamespace.ErrorCode | undefined;
36
+ };
28
37
  ipNameLookup: typeof IpNameLookupNamespace;
29
38
  tcpCreateSocket: typeof TcpCreateSocketNamespace;
30
39
  tcp: typeof TcpNamespace;
@@ -3,6 +3,8 @@ import { earlyDispose, inputStreamCreate, ioCall, outputStreamCreate, pollableCr
3
3
  const symbolDispose = Symbol.dispose || Symbol.for("dispose");
4
4
  // Network class privately stores capabilities
5
5
  class Network {
6
+ // Compatibility with the resource placeholder in the bundled WASI 0.2.10 WIT.
7
+ noop() { }
6
8
  #allowDnsLookup = true;
7
9
  #allowTcp = true;
8
10
  #allowUdp = true;
@@ -51,6 +53,22 @@ export const instanceNetwork = {
51
53
  };
52
54
  export const network = {
53
55
  Network,
56
+ networkErrorCode(error) {
57
+ const payload = "payload" in error ? error.payload : undefined;
58
+ if (typeof payload !== "object" || payload === null || !("code" in payload)) {
59
+ return undefined;
60
+ }
61
+ const codes = {
62
+ ECONNRESET: "connection-reset",
63
+ ECONNREFUSED: "connection-refused",
64
+ ECONNABORTED: "connection-aborted",
65
+ ETIMEDOUT: "timeout",
66
+ EACCES: "access-denied",
67
+ EPERM: "access-denied",
68
+ ENETUNREACH: "remote-unreachable",
69
+ };
70
+ return typeof payload.code === "string" ? codes[payload.code] : undefined;
71
+ },
54
72
  };
55
73
  class ResolveAddressStream {
56
74
  #id = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytecodealliance/preview2-shim",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "WASI Preview2 shim for JS environments",
5
5
  "homepage": "https://github.com/bytecodealliance/jco#readme",
6
6
  "bugs": {
@@ -54,6 +54,10 @@
54
54
  },
55
55
  "./interfaces/*": {
56
56
  "types": "./types/interfaces/*.d.ts"
57
+ },
58
+ "./io-worker": {
59
+ "types": "./dist/nodejs/io-worker.d.ts",
60
+ "node": "./dist/nodejs/io-worker.js"
57
61
  }
58
62
  },
59
63
  "dependencies": {
@@ -67,7 +71,7 @@
67
71
  "puppeteer": "^25.1.0",
68
72
  "rolldown": "^1.2.4",
69
73
  "typescript": "7.0.2",
70
- "vitest": "^4.1.10"
74
+ "vitest": "^4.1.11"
71
75
  },
72
76
  "scripts": {
73
77
  "build": "tsc --project tsconfig.build.json && node scripts/build-worker.mjs",