@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 +11 -0
- package/dist/browser/filesystem.d.ts +27 -0
- package/dist/browser/filesystem.js +32 -0
- package/dist/browser/in-memory-filesystem.d.ts +16 -0
- package/dist/browser/in-memory-filesystem.js +93 -3
- package/dist/browser/opfs-filesystem.d.ts +51 -0
- package/dist/browser/opfs-filesystem.js +245 -0
- package/dist/io/calls.d.ts +1 -0
- package/dist/io/calls.js +2 -0
- package/dist/io/extension.d.ts +15 -0
- package/dist/io/extension.js +1 -0
- package/dist/io/worker-sockets.d.ts +0 -3
- package/dist/io/worker-sockets.js +4 -6
- package/dist/io/worker-thread.bundle.js +76 -56
- package/dist/io/worker-thread.js +27 -4
- package/dist/nodejs/io-worker.d.ts +28 -0
- package/dist/nodejs/io-worker.js +32 -0
- package/dist/nodejs/sockets.d.ts +11 -2
- package/dist/nodejs/sockets.js +18 -0
- package/package.json +6 -2
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
|
+
}
|
package/dist/io/calls.d.ts
CHANGED
|
@@ -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
|
-
|
|
57
|
-
|
|
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;
|
package/dist/io/worker-thread.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/nodejs/sockets.d.ts
CHANGED
|
@@ -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;
|
package/dist/nodejs/sockets.js
CHANGED
|
@@ -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.
|
|
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.
|
|
74
|
+
"vitest": "^4.1.11"
|
|
71
75
|
},
|
|
72
76
|
"scripts": {
|
|
73
77
|
"build": "tsc --project tsconfig.build.json && node scripts/build-worker.mjs",
|