@swifttui/web 0.1.9 → 0.1.10
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/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/src/wasi/MainThreadInputQueue.js +61 -0
- package/dist/src/wasi/MainThreadInputQueue.js.map +1 -0
- package/dist/src/wasi/MainThreadWasmExecutor.d.ts +32 -0
- package/dist/src/wasi/MainThreadWasmExecutor.js +116 -0
- package/dist/src/wasi/MainThreadWasmExecutor.js.map +1 -0
- package/dist/src/wasi/WasiPollScheduler.js +38 -1
- package/dist/src/wasi/WasiPollScheduler.js.map +1 -1
- package/dist/src/wasi/WasmEngineCapabilities.d.ts +16 -3
- package/dist/src/wasi/WasmEngineCapabilities.js +15 -3
- package/dist/src/wasi/WasmEngineCapabilities.js.map +1 -1
- package/dist/src/wasi/WasmSceneRuntime.d.ts +15 -1
- package/dist/src/wasi/WasmSceneRuntime.js +60 -11
- package/dist/src/wasi/WasmSceneRuntime.js.map +1 -1
- package/dist/src/wasi/WasmSceneWorker.js +1 -1
- package/dist/wasi.d.ts +4 -3
- package/dist/wasi.js +4 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ import { WebHostSceneBridge, WebHostSceneRuntime, WebHostSceneRuntimeOptions, Wh
|
|
|
5
5
|
import { WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
|
|
6
6
|
import { WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, createWebHostApp } from "./src/WebHostApp.js";
|
|
7
7
|
import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
|
|
8
|
-
import { WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, classifyWasmEngineFamily, collectWasmEngineProbeSignals, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
8
|
+
import { JSPIConstructors, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
9
9
|
import { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink } from "./src/wasi/BrowserWASIBridge.js";
|
|
10
|
-
export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, SUPPORTED_SURFACE_VERSION, StdIOPipe, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, WebHostANSIColors, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostSceneBridge, WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, WheelMode, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, loadWebHostSceneManifest, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
10
|
+
export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, JSPIConstructors, ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, SUPPORTED_SURFACE_VERSION, StdIOPipe, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, WebHostANSIColors, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostSceneBridge, WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, WheelMode, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
|
|
2
|
-
import { classifyWasmEngineFamily, collectWasmEngineProbeSignals, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
2
|
+
import { classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
3
3
|
import { applyWebHostTerminalStyle, decodeWebHostTerminalRenderStyleBase64, encodeWebHostTerminalRenderStyleBase64, mergeWebHostTerminalStyle, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webTUITerminalBackgroundColor } from "./src/WebHostTerminalStyle.js";
|
|
4
4
|
import { SUPPORTED_SURFACE_VERSION, WebHostOutputDecoder, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
|
|
5
5
|
import { BrowserWASIBridge } from "./src/wasi/BrowserWASIBridge.js";
|
|
@@ -7,4 +7,4 @@ import { WebSocketSceneBridge, webSocketSceneURL } from "./src/WebSocketSceneBri
|
|
|
7
7
|
import { loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
|
|
8
8
|
import { WebHostSceneRuntime } from "./src/WebHostSceneRuntime.js";
|
|
9
9
|
import { createWebHostApp } from "./src/WebHostApp.js";
|
|
10
|
-
export { BrowserWASIBridge, SUPPORTED_SURFACE_VERSION, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, loadWebHostSceneManifest, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
10
|
+
export { BrowserWASIBridge, SUPPORTED_SURFACE_VERSION, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/wasi/MainThreadInputQueue.ts
|
|
2
|
+
/**
|
|
3
|
+
* Plain single-thread stdin queue for the main-thread (JSPI) execution mode.
|
|
4
|
+
* Fills the role `SharedInputQueue` plays for the worker mode, but readiness
|
|
5
|
+
* is a promise instead of an `Atomics.wait` — no SharedArrayBuffer and
|
|
6
|
+
* therefore no COOP/COEP requirement.
|
|
7
|
+
*/
|
|
8
|
+
var MainThreadInputQueue = class {
|
|
9
|
+
chunks = [];
|
|
10
|
+
closed = false;
|
|
11
|
+
waiters = [];
|
|
12
|
+
write(chunk) {
|
|
13
|
+
if (this.closed || chunk.byteLength === 0) return;
|
|
14
|
+
this.chunks.push(chunk);
|
|
15
|
+
this.resolveWaiters("readable");
|
|
16
|
+
}
|
|
17
|
+
close() {
|
|
18
|
+
if (this.closed) return;
|
|
19
|
+
this.closed = true;
|
|
20
|
+
this.resolveWaiters("closed");
|
|
21
|
+
}
|
|
22
|
+
isClosed() {
|
|
23
|
+
return this.closed && this.chunks.length === 0;
|
|
24
|
+
}
|
|
25
|
+
availableBytes() {
|
|
26
|
+
return this.chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
27
|
+
}
|
|
28
|
+
readAvailable(size) {
|
|
29
|
+
const first = this.chunks[0];
|
|
30
|
+
if (!first) return;
|
|
31
|
+
if (first.byteLength <= size) {
|
|
32
|
+
this.chunks = this.chunks.slice(1);
|
|
33
|
+
return first;
|
|
34
|
+
}
|
|
35
|
+
this.chunks = [first.subarray(size), ...this.chunks.slice(1)];
|
|
36
|
+
return first.subarray(0, size);
|
|
37
|
+
}
|
|
38
|
+
waitForReadableAsync(timeoutMilliseconds) {
|
|
39
|
+
if (this.chunks.length > 0) return Promise.resolve("readable");
|
|
40
|
+
if (this.closed) return Promise.resolve("closed");
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const waiter = (readiness) => resolve(readiness);
|
|
43
|
+
this.waiters = [...this.waiters, waiter];
|
|
44
|
+
if (timeoutMilliseconds !== void 0 && Number.isFinite(timeoutMilliseconds)) setTimeout(() => {
|
|
45
|
+
if (this.waiters.includes(waiter)) {
|
|
46
|
+
this.waiters = this.waiters.filter((pending) => pending !== waiter);
|
|
47
|
+
resolve("timedOut");
|
|
48
|
+
}
|
|
49
|
+
}, Math.max(0, timeoutMilliseconds));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
resolveWaiters(readiness) {
|
|
53
|
+
const waiters = this.waiters;
|
|
54
|
+
this.waiters = [];
|
|
55
|
+
for (const waiter of waiters) waiter(readiness);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { MainThreadInputQueue };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=MainThreadInputQueue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MainThreadInputQueue.js","names":[],"sources":["../../../src/wasi/MainThreadInputQueue.ts"],"sourcesContent":["import type { SharedInputReadiness } from \"./SharedInputQueue.ts\";\nimport type { SuspendingWasiPollReadableSource } from \"./WasiPollScheduler.ts\";\n\n/**\n * Plain single-thread stdin queue for the main-thread (JSPI) execution mode.\n * Fills the role `SharedInputQueue` plays for the worker mode, but readiness\n * is a promise instead of an `Atomics.wait` — no SharedArrayBuffer and\n * therefore no COOP/COEP requirement.\n */\nexport class MainThreadInputQueue implements SuspendingWasiPollReadableSource {\n private chunks: Uint8Array[] = [];\n private closed = false;\n private waiters: Array<(readiness: SharedInputReadiness) => void> = [];\n\n write(chunk: Uint8Array): void {\n if (this.closed || chunk.byteLength === 0) {\n return;\n }\n this.chunks.push(chunk);\n this.resolveWaiters(\"readable\");\n }\n\n close(): void {\n if (this.closed) {\n return;\n }\n this.closed = true;\n this.resolveWaiters(\"closed\");\n }\n\n isClosed(): boolean {\n return this.closed && this.chunks.length === 0;\n }\n\n availableBytes(): number {\n return this.chunks.reduce((total, chunk) => total + chunk.byteLength, 0);\n }\n\n readAvailable(size: number): Uint8Array | undefined {\n const first = this.chunks[0];\n if (!first) {\n return undefined;\n }\n if (first.byteLength <= size) {\n this.chunks = this.chunks.slice(1);\n return first;\n }\n this.chunks = [first.subarray(size), ...this.chunks.slice(1)];\n return first.subarray(0, size);\n }\n\n waitForReadableAsync(\n timeoutMilliseconds?: number\n ): Promise<SharedInputReadiness> {\n if (this.chunks.length > 0) {\n return Promise.resolve(\"readable\");\n }\n if (this.closed) {\n return Promise.resolve(\"closed\");\n }\n return new Promise((resolve) => {\n const waiter = (readiness: SharedInputReadiness): void => resolve(readiness);\n this.waiters = [...this.waiters, waiter];\n if (timeoutMilliseconds !== undefined && Number.isFinite(timeoutMilliseconds)) {\n setTimeout(() => {\n if (this.waiters.includes(waiter)) {\n this.waiters = this.waiters.filter((pending) => pending !== waiter);\n resolve(\"timedOut\");\n }\n }, Math.max(0, timeoutMilliseconds));\n }\n });\n }\n\n private resolveWaiters(readiness: SharedInputReadiness): void {\n const waiters = this.waiters;\n this.waiters = [];\n for (const waiter of waiters) {\n waiter(readiness);\n }\n }\n}\n"],"mappings":";;;;;;;AASA,IAAa,uBAAb,MAA8E;CAC5E,SAA+B,CAAC;CAChC,SAAiB;CACjB,UAAoE,CAAC;CAErE,MAAM,OAAyB;EAC7B,IAAI,KAAK,UAAU,MAAM,eAAe,GACtC;EAEF,KAAK,OAAO,KAAK,KAAK;EACtB,KAAK,eAAe,UAAU;CAChC;CAEA,QAAc;EACZ,IAAI,KAAK,QACP;EAEF,KAAK,SAAS;EACd,KAAK,eAAe,QAAQ;CAC9B;CAEA,WAAoB;EAClB,OAAO,KAAK,UAAU,KAAK,OAAO,WAAW;CAC/C;CAEA,iBAAyB;EACvB,OAAO,KAAK,OAAO,QAAQ,OAAO,UAAU,QAAQ,MAAM,YAAY,CAAC;CACzE;CAEA,cAAc,MAAsC;EAClD,MAAM,QAAQ,KAAK,OAAO;EAC1B,IAAI,CAAC,OACH;EAEF,IAAI,MAAM,cAAc,MAAM;GAC5B,KAAK,SAAS,KAAK,OAAO,MAAM,CAAC;GACjC,OAAO;EACT;EACA,KAAK,SAAS,CAAC,MAAM,SAAS,IAAI,GAAG,GAAG,KAAK,OAAO,MAAM,CAAC,CAAC;EAC5D,OAAO,MAAM,SAAS,GAAG,IAAI;CAC/B;CAEA,qBACE,qBAC+B;EAC/B,IAAI,KAAK,OAAO,SAAS,GACvB,OAAO,QAAQ,QAAQ,UAAU;EAEnC,IAAI,KAAK,QACP,OAAO,QAAQ,QAAQ,QAAQ;EAEjC,OAAO,IAAI,SAAS,YAAY;GAC9B,MAAM,UAAU,cAA0C,QAAQ,SAAS;GAC3E,KAAK,UAAU,CAAC,GAAG,KAAK,SAAS,MAAM;GACvC,IAAI,wBAAwB,KAAA,KAAa,OAAO,SAAS,mBAAmB,GAC1E,iBAAiB;IACf,IAAI,KAAK,QAAQ,SAAS,MAAM,GAAG;KACjC,KAAK,UAAU,KAAK,QAAQ,QAAQ,YAAY,YAAY,MAAM;KAClE,QAAQ,UAAU;IACpB;GACF,GAAG,KAAK,IAAI,GAAG,mBAAmB,CAAC;EAEvC,CAAC;CACH;CAEA,eAAuB,WAAuC;EAC5D,MAAM,UAAU,KAAK;EACrB,KAAK,UAAU,CAAC;EAChB,KAAK,MAAM,UAAU,SACnB,OAAO,SAAS;CAEpB;AACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/wasi/MainThreadWasmExecutor.d.ts
|
|
2
|
+
interface MainThreadWasmExecutorOptions {
|
|
3
|
+
wasmURL: string | URL;
|
|
4
|
+
environment: Record<string, string>;
|
|
5
|
+
onStdout(chunk: Uint8Array): void;
|
|
6
|
+
onStderr(chunk: Uint8Array): void;
|
|
7
|
+
onExit?(code: number): void;
|
|
8
|
+
onError?(message: string): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Runs a SwiftTUI WASI app on the main thread using WebAssembly JSPI: the
|
|
12
|
+
* `poll_oneoff` import is wrapped in `WebAssembly.Suspending`, so the app's
|
|
13
|
+
* run loop suspends to the browser event loop instead of blocking a worker
|
|
14
|
+
* on `Atomics.wait`. Requires JSPI; needs neither a worker nor
|
|
15
|
+
* SharedArrayBuffer (and therefore no COOP/COEP headers).
|
|
16
|
+
*
|
|
17
|
+
* Disposal closes stdin; the SwiftTUI runner exits cooperatively on stdin
|
|
18
|
+
* hangup, which resolves the promised `_start` call.
|
|
19
|
+
*/
|
|
20
|
+
declare class MainThreadWasmExecutor {
|
|
21
|
+
private readonly options;
|
|
22
|
+
private readonly stdin;
|
|
23
|
+
private didStart;
|
|
24
|
+
constructor(options: MainThreadWasmExecutorOptions);
|
|
25
|
+
start(): void;
|
|
26
|
+
sendInput(chunk: Uint8Array): void;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
private run;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { MainThreadWasmExecutor, MainThreadWasmExecutorOptions };
|
|
32
|
+
//# sourceMappingURL=MainThreadWasmExecutor.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jspiConstructors } from "./WasmEngineCapabilities.js";
|
|
2
|
+
import { MainThreadInputQueue } from "./MainThreadInputQueue.js";
|
|
3
|
+
import { SuspendingWasiPollScheduler } from "./WasiPollScheduler.js";
|
|
4
|
+
import { ConsoleStdout, Fd, WASI, WASIProcExit, wasi } from "@bjorn3/browser_wasi_shim";
|
|
5
|
+
//#region src/wasi/MainThreadWasmExecutor.ts
|
|
6
|
+
/**
|
|
7
|
+
* Runs a SwiftTUI WASI app on the main thread using WebAssembly JSPI: the
|
|
8
|
+
* `poll_oneoff` import is wrapped in `WebAssembly.Suspending`, so the app's
|
|
9
|
+
* run loop suspends to the browser event loop instead of blocking a worker
|
|
10
|
+
* on `Atomics.wait`. Requires JSPI; needs neither a worker nor
|
|
11
|
+
* SharedArrayBuffer (and therefore no COOP/COEP headers).
|
|
12
|
+
*
|
|
13
|
+
* Disposal closes stdin; the SwiftTUI runner exits cooperatively on stdin
|
|
14
|
+
* hangup, which resolves the promised `_start` call.
|
|
15
|
+
*/
|
|
16
|
+
var MainThreadWasmExecutor = class {
|
|
17
|
+
options;
|
|
18
|
+
stdin = new MainThreadInputQueue();
|
|
19
|
+
didStart = false;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
start() {
|
|
24
|
+
if (this.didStart) return;
|
|
25
|
+
this.didStart = true;
|
|
26
|
+
this.run();
|
|
27
|
+
}
|
|
28
|
+
sendInput(chunk) {
|
|
29
|
+
this.stdin.write(chunk);
|
|
30
|
+
}
|
|
31
|
+
dispose() {
|
|
32
|
+
this.stdin.close();
|
|
33
|
+
}
|
|
34
|
+
async run() {
|
|
35
|
+
try {
|
|
36
|
+
const jspi = jspiConstructors();
|
|
37
|
+
if (!jspi) throw new Error("WebAssembly JSPI (Suspending/promising) is unavailable in this engine");
|
|
38
|
+
const shim = new WASI(["app.wasm"], Object.entries(this.options.environment).map(([key, value]) => `${key}=${value}`), [
|
|
39
|
+
new MainThreadInputFileDescriptor(this.stdin),
|
|
40
|
+
new ConsoleStdout((chunk) => this.options.onStdout(chunk)),
|
|
41
|
+
new ConsoleStdout((chunk) => this.options.onStderr(chunk))
|
|
42
|
+
]);
|
|
43
|
+
const instanceExports = () => shim.inst?.exports;
|
|
44
|
+
const originalPoll = shim.wasiImport.poll_oneoff;
|
|
45
|
+
const scheduler = new SuspendingWasiPollScheduler({
|
|
46
|
+
memory: () => instanceExports()?.memory,
|
|
47
|
+
stdin: this.stdin,
|
|
48
|
+
fallbackPoll: (inPtr, outPtr, nsubscriptions, neventsPtr) => originalPoll(inPtr, outPtr, nsubscriptions, neventsPtr)
|
|
49
|
+
});
|
|
50
|
+
const response = await fetch(this.options.wasmURL);
|
|
51
|
+
if (!response.ok) throw new Error(`failed to load ${String(this.options.wasmURL)}: ${response.status} ${response.statusText}`);
|
|
52
|
+
const module = await WebAssembly.compile(await response.arrayBuffer());
|
|
53
|
+
const instance = await WebAssembly.instantiate(module, { wasi_snapshot_preview1: {
|
|
54
|
+
...shim.wasiImport,
|
|
55
|
+
poll_oneoff: new jspi.Suspending((inPtr, outPtr, nsubscriptions, neventsPtr) => scheduler.pollOneOff(inPtr, outPtr, nsubscriptions, neventsPtr))
|
|
56
|
+
} });
|
|
57
|
+
shim.inst = instance;
|
|
58
|
+
const start = jspi.promising(instance.exports._start);
|
|
59
|
+
try {
|
|
60
|
+
await start();
|
|
61
|
+
this.options.onExit?.(0);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof WASIProcExit) {
|
|
64
|
+
this.options.onExit?.(error.code);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.options.onError?.(error instanceof Error ? error.message : String(error));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var MainThreadInputFileDescriptor = class extends Fd {
|
|
75
|
+
queue;
|
|
76
|
+
fdstat = (() => {
|
|
77
|
+
const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0);
|
|
78
|
+
fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_READ);
|
|
79
|
+
return fdstat;
|
|
80
|
+
})();
|
|
81
|
+
constructor(queue) {
|
|
82
|
+
super();
|
|
83
|
+
this.queue = queue;
|
|
84
|
+
}
|
|
85
|
+
fd_fdstat_get() {
|
|
86
|
+
return {
|
|
87
|
+
ret: wasi.ERRNO_SUCCESS,
|
|
88
|
+
fdstat: this.fdstat
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
fd_filestat_get() {
|
|
92
|
+
return {
|
|
93
|
+
ret: wasi.ERRNO_SUCCESS,
|
|
94
|
+
filestat: new wasi.Filestat(0n, wasi.FILETYPE_CHARACTER_DEVICE, 0n)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
fd_read(size) {
|
|
98
|
+
const chunk = this.queue.readAvailable(size);
|
|
99
|
+
if (chunk) return {
|
|
100
|
+
ret: wasi.ERRNO_SUCCESS,
|
|
101
|
+
data: chunk
|
|
102
|
+
};
|
|
103
|
+
if (this.queue.isClosed()) return {
|
|
104
|
+
ret: wasi.ERRNO_SUCCESS,
|
|
105
|
+
data: /* @__PURE__ */ new Uint8Array()
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
ret: wasi.ERRNO_AGAIN,
|
|
109
|
+
data: /* @__PURE__ */ new Uint8Array()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
//#endregion
|
|
114
|
+
export { MainThreadWasmExecutor };
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=MainThreadWasmExecutor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MainThreadWasmExecutor.js","names":[],"sources":["../../../src/wasi/MainThreadWasmExecutor.ts"],"sourcesContent":["import { ConsoleStdout, Fd, WASI, WASIProcExit, wasi } from \"@bjorn3/browser_wasi_shim\";\n\nimport { MainThreadInputQueue } from \"./MainThreadInputQueue.ts\";\nimport { SuspendingWasiPollScheduler } from \"./WasiPollScheduler.ts\";\nimport { jspiConstructors } from \"./WasmEngineCapabilities.ts\";\n\nexport interface MainThreadWasmExecutorOptions {\n wasmURL: string | URL;\n environment: Record<string, string>;\n onStdout(chunk: Uint8Array): void;\n onStderr(chunk: Uint8Array): void;\n onExit?(code: number): void;\n onError?(message: string): void;\n}\n\n/**\n * Runs a SwiftTUI WASI app on the main thread using WebAssembly JSPI: the\n * `poll_oneoff` import is wrapped in `WebAssembly.Suspending`, so the app's\n * run loop suspends to the browser event loop instead of blocking a worker\n * on `Atomics.wait`. Requires JSPI; needs neither a worker nor\n * SharedArrayBuffer (and therefore no COOP/COEP headers).\n *\n * Disposal closes stdin; the SwiftTUI runner exits cooperatively on stdin\n * hangup, which resolves the promised `_start` call.\n */\nexport class MainThreadWasmExecutor {\n private readonly options: MainThreadWasmExecutorOptions;\n private readonly stdin = new MainThreadInputQueue();\n private didStart = false;\n\n constructor(options: MainThreadWasmExecutorOptions) {\n this.options = options;\n }\n\n start(): void {\n if (this.didStart) {\n return;\n }\n this.didStart = true;\n void this.run();\n }\n\n sendInput(chunk: Uint8Array): void {\n this.stdin.write(chunk);\n }\n\n dispose(): void {\n this.stdin.close();\n }\n\n private async run(): Promise<void> {\n try {\n const jspi = jspiConstructors();\n if (!jspi) {\n throw new Error(\n \"WebAssembly JSPI (Suspending/promising) is unavailable in this engine\"\n );\n }\n\n const shim = new WASI(\n [\"app.wasm\"],\n Object.entries(this.options.environment).map(([key, value]) => `${key}=${value}`),\n [\n new MainThreadInputFileDescriptor(this.stdin),\n new ConsoleStdout((chunk) => this.options.onStdout(chunk)),\n new ConsoleStdout((chunk) => this.options.onStderr(chunk)),\n ]\n );\n\n const instanceExports = (): { memory?: WebAssembly.Memory } | undefined =>\n (shim as unknown as { inst?: { exports: { memory?: WebAssembly.Memory } } }).inst\n ?.exports;\n const originalPoll = shim.wasiImport.poll_oneoff as (\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ) => number;\n const scheduler = new SuspendingWasiPollScheduler({\n memory: () => instanceExports()?.memory,\n stdin: this.stdin,\n fallbackPoll: (inPtr, outPtr, nsubscriptions, neventsPtr) =>\n originalPoll(inPtr, outPtr, nsubscriptions, neventsPtr),\n });\n\n const response = await fetch(this.options.wasmURL);\n if (!response.ok) {\n throw new Error(\n `failed to load ${String(this.options.wasmURL)}: ${response.status} ${response.statusText}`\n );\n }\n const module = await WebAssembly.compile(await response.arrayBuffer());\n const instance = await WebAssembly.instantiate(module, {\n wasi_snapshot_preview1: {\n ...shim.wasiImport,\n poll_oneoff: new jspi.Suspending(\n (inPtr: number, outPtr: number, nsubscriptions: number, neventsPtr: number) =>\n scheduler.pollOneOff(inPtr, outPtr, nsubscriptions, neventsPtr)\n ),\n },\n });\n (shim as unknown as { inst: WebAssembly.Instance }).inst =\n instance as WebAssembly.Instance;\n\n const start = jspi.promising(\n (instance as WebAssembly.Instance).exports._start\n );\n try {\n await start();\n this.options.onExit?.(0);\n } catch (error) {\n if (error instanceof WASIProcExit) {\n this.options.onExit?.(error.code);\n return;\n }\n throw error;\n }\n } catch (error) {\n this.options.onError?.(error instanceof Error ? error.message : String(error));\n }\n }\n}\n\nclass MainThreadInputFileDescriptor extends Fd {\n private readonly queue: MainThreadInputQueue;\n private readonly fdstat = (() => {\n const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0);\n fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_READ);\n return fdstat;\n })();\n\n constructor(queue: MainThreadInputQueue) {\n super();\n this.queue = queue;\n }\n\n override fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat } {\n return { ret: wasi.ERRNO_SUCCESS, fdstat: this.fdstat };\n }\n\n override fd_filestat_get(): { ret: number; filestat: wasi.Filestat } {\n return {\n ret: wasi.ERRNO_SUCCESS,\n filestat: new wasi.Filestat(0n, wasi.FILETYPE_CHARACTER_DEVICE, 0n),\n };\n }\n\n override fd_read(size: number): { ret: number; data: Uint8Array } {\n const chunk = this.queue.readAvailable(size);\n if (chunk) {\n return { ret: wasi.ERRNO_SUCCESS, data: chunk };\n }\n if (this.queue.isClosed()) {\n return { ret: wasi.ERRNO_SUCCESS, data: new Uint8Array() };\n }\n return { ret: wasi.ERRNO_AGAIN, data: new Uint8Array() };\n }\n}\n"],"mappings":"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"}
|
|
@@ -35,6 +35,43 @@ var WasiPollScheduler = class {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* The JSPI (main-thread) counterpart of `WasiPollScheduler`: identical
|
|
40
|
+
* subscription semantics, but the blocking waits become awaited promises so
|
|
41
|
+
* the surrounding `poll_oneoff` import can be wrapped in
|
|
42
|
+
* `WebAssembly.Suspending` and run without a worker or SharedArrayBuffer.
|
|
43
|
+
*/
|
|
44
|
+
var SuspendingWasiPollScheduler = class {
|
|
45
|
+
memory;
|
|
46
|
+
stdin;
|
|
47
|
+
fallbackPoll;
|
|
48
|
+
nowMilliseconds;
|
|
49
|
+
constructor(options) {
|
|
50
|
+
this.memory = options.memory;
|
|
51
|
+
this.stdin = options.stdin;
|
|
52
|
+
this.fallbackPoll = options.fallbackPoll;
|
|
53
|
+
this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());
|
|
54
|
+
}
|
|
55
|
+
async pollOneOff(inPtr, outPtr, nsubscriptions, neventsPtr) {
|
|
56
|
+
const memory = this.memory();
|
|
57
|
+
if (!memory || nsubscriptions <= 0) return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);
|
|
58
|
+
const subscriptions = readSubscriptions(new DataView(memory.buffer), inPtr, nsubscriptions, this.nowMilliseconds());
|
|
59
|
+
if (subscriptions === void 0) return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);
|
|
60
|
+
while (true) {
|
|
61
|
+
const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());
|
|
62
|
+
if (ready.length > 0) {
|
|
63
|
+
const view = new DataView(memory.buffer);
|
|
64
|
+
writeEvents(view, outPtr, ready, this.stdin);
|
|
65
|
+
if (neventsPtr !== void 0) view.setUint32(neventsPtr, ready.length, true);
|
|
66
|
+
return wasi.ERRNO_SUCCESS;
|
|
67
|
+
}
|
|
68
|
+
const timeoutMilliseconds = shortestClockTimeoutMilliseconds(subscriptions, this.nowMilliseconds());
|
|
69
|
+
if (hasFdReadSubscription(subscriptions)) await this.stdin.waitForReadableAsync(timeoutMilliseconds);
|
|
70
|
+
else if (timeoutMilliseconds !== void 0) await new Promise((resolve) => setTimeout(resolve, Math.min(timeoutMilliseconds, maximumAtomicsWaitMilliseconds)));
|
|
71
|
+
else return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
38
75
|
function readSubscriptions(view, inPtr, nsubscriptions, nowMilliseconds) {
|
|
39
76
|
const subscriptions = [];
|
|
40
77
|
for (let index = 0; index < nsubscriptions; index += 1) {
|
|
@@ -109,6 +146,6 @@ function writeEvents(view, outPtr, subscriptions, stdin) {
|
|
|
109
146
|
});
|
|
110
147
|
}
|
|
111
148
|
//#endregion
|
|
112
|
-
export { WasiPollScheduler };
|
|
149
|
+
export { SuspendingWasiPollScheduler, WasiPollScheduler };
|
|
113
150
|
|
|
114
151
|
//# sourceMappingURL=WasiPollScheduler.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WasiPollScheduler.js","names":[],"sources":["../../../src/wasi/WasiPollScheduler.ts"],"sourcesContent":["import { wasi } from \"@bjorn3/browser_wasi_shim\";\n\nimport type { SharedInputReadiness } from \"./SharedInputQueue.ts\";\n\nconst subscriptionByteLength = 48;\nconst eventByteLength = 32;\nconst maximumAtomicsWaitMilliseconds = 2_147_483_647;\n\ninterface ClockSubscription {\n readonly type: \"clock\";\n readonly userdata: bigint;\n readonly clockid: number;\n readonly deadlineMilliseconds: number;\n}\n\ninterface FdReadSubscription {\n readonly type: \"fdRead\";\n readonly userdata: bigint;\n readonly fd: number;\n}\n\ntype SupportedSubscription = ClockSubscription | FdReadSubscription;\n\nexport interface WasiPollReadableSource {\n availableBytes(): number;\n isClosed(): boolean;\n waitForReadable(timeoutMilliseconds?: number): SharedInputReadiness;\n}\n\nexport interface WasiPollSchedulerOptions {\n memory(): WebAssembly.Memory | undefined;\n stdin: WasiPollReadableSource;\n fallbackPoll(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number;\n nowMilliseconds?(): number;\n}\n\nexport class WasiPollScheduler {\n private readonly memory: WasiPollSchedulerOptions[\"memory\"];\n private readonly stdin: WasiPollReadableSource;\n private readonly fallbackPoll: WasiPollSchedulerOptions[\"fallbackPoll\"];\n private readonly nowMilliseconds: () => number;\n private readonly waitBuffer = new Int32Array(\n new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)\n );\n\n constructor(options: WasiPollSchedulerOptions) {\n this.memory = options.memory;\n this.stdin = options.stdin;\n this.fallbackPoll = options.fallbackPoll;\n this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());\n }\n\n pollOneOff(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number {\n const memory = this.memory();\n if (!memory || nsubscriptions <= 0) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n const view = new DataView(memory.buffer);\n const subscriptions = readSubscriptions(\n view,\n inPtr,\n nsubscriptions,\n this.nowMilliseconds()\n );\n if (subscriptions === undefined) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n while (true) {\n const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());\n if (ready.length > 0) {\n writeEvents(view, outPtr, ready, this.stdin);\n if (neventsPtr !== undefined) {\n view.setUint32(neventsPtr, ready.length, true);\n }\n return wasi.ERRNO_SUCCESS;\n }\n\n const timeoutMilliseconds = shortestClockTimeoutMilliseconds(\n subscriptions,\n this.nowMilliseconds()\n );\n if (hasFdReadSubscription(subscriptions)) {\n this.stdin.waitForReadable(timeoutMilliseconds);\n } else if (timeoutMilliseconds !== undefined) {\n Atomics.wait(\n this.waitBuffer,\n 0,\n 0,\n Math.min(timeoutMilliseconds, maximumAtomicsWaitMilliseconds)\n );\n } else {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n }\n }\n}\n\nfunction readSubscriptions(\n view: DataView,\n inPtr: number,\n nsubscriptions: number,\n nowMilliseconds: number\n): SupportedSubscription[] | undefined {\n const subscriptions: SupportedSubscription[] = [];\n for (let index = 0; index < nsubscriptions; index += 1) {\n const subscription = wasi.Subscription.read_bytes(\n view,\n inPtr + index * subscriptionByteLength\n );\n switch (subscription.eventtype) {\n case wasi.EVENTTYPE_CLOCK:\n if (!isSupportedClockId(subscription.clockid)) {\n return undefined;\n }\n subscriptions.push({\n type: \"clock\",\n userdata: subscription.userdata,\n clockid: subscription.clockid,\n deadlineMilliseconds: clockDeadlineMilliseconds(subscription, nowMilliseconds),\n });\n break;\n case wasi.EVENTTYPE_FD_READ:\n if (subscription.clockid !== wasi.FD_STDIN) {\n return undefined;\n }\n subscriptions.push({\n type: \"fdRead\",\n userdata: subscription.userdata,\n fd: subscription.clockid,\n });\n break;\n default:\n return undefined;\n }\n }\n return subscriptions;\n}\n\nfunction isSupportedClockId(\n clockid: number\n): boolean {\n return clockid === wasi.CLOCKID_MONOTONIC || clockid === wasi.CLOCKID_REALTIME;\n}\n\nfunction shortestClockTimeoutMilliseconds(\n subscriptions: readonly SupportedSubscription[],\n nowMilliseconds: number\n): number | undefined {\n let timeoutMilliseconds: number | undefined;\n for (const subscription of subscriptions) {\n if (subscription.type !== \"clock\") {\n continue;\n }\n const remaining = clockRemainingMilliseconds(subscription, nowMilliseconds);\n timeoutMilliseconds = timeoutMilliseconds === undefined\n ? remaining\n : Math.min(timeoutMilliseconds, remaining);\n }\n return timeoutMilliseconds;\n}\n\nfunction readySubscriptions(\n subscriptions: readonly SupportedSubscription[],\n stdin: WasiPollReadableSource,\n nowMilliseconds: number\n): SupportedSubscription[] {\n return subscriptions.filter((subscription) => {\n switch (subscription.type) {\n case \"clock\":\n return clockRemainingMilliseconds(subscription, nowMilliseconds) <= 0;\n case \"fdRead\":\n return stdin.availableBytes() > 0 || stdin.isClosed();\n }\n });\n}\n\nfunction hasFdReadSubscription(\n subscriptions: readonly SupportedSubscription[]\n): boolean {\n return subscriptions.some((subscription) => subscription.type === \"fdRead\");\n}\n\nfunction clockRemainingMilliseconds(\n subscription: ClockSubscription,\n nowMilliseconds: number\n): number {\n return Math.max(0, subscription.deadlineMilliseconds - nowMillisecondsForClock(\n subscription.clockid,\n nowMilliseconds\n ));\n}\n\nfunction clockDeadlineMilliseconds(\n subscription: wasi.Subscription,\n nowMilliseconds: number\n): number {\n if ((subscription.flags & wasi.SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME) !== 0) {\n return Number(subscription.timeout) / 1_000_000;\n }\n return nowMillisecondsForClock(subscription.clockid, nowMilliseconds)\n + Number(subscription.timeout) / 1_000_000;\n}\n\nfunction nowMillisecondsForClock(\n clockid: number,\n nowMilliseconds: number\n): number {\n if (clockid === wasi.CLOCKID_REALTIME) {\n return Date.now();\n }\n return nowMilliseconds;\n}\n\nfunction writeEvents(\n view: DataView,\n outPtr: number,\n subscriptions: readonly SupportedSubscription[],\n stdin: WasiPollReadableSource\n): void {\n subscriptions.forEach((subscription, index) => {\n const eventtype = subscription.type === \"clock\"\n ? wasi.EVENTTYPE_CLOCK\n : wasi.EVENTTYPE_FD_READ;\n const offset = outPtr + index * eventByteLength;\n new wasi.Event(\n subscription.userdata,\n wasi.ERRNO_SUCCESS,\n eventtype\n ).write_bytes(view, offset);\n if (subscription.type === \"fdRead\") {\n const availableBytes = Math.max(0, stdin.availableBytes());\n view.setBigUint64(offset + 16, BigInt(availableBytes), true);\n if (availableBytes === 0 && stdin.isClosed()) {\n view.setUint16(offset + 24, wasi.EVENTRWFLAGS_FD_READWRITE_HANGUP, true);\n }\n }\n });\n}\n\nexport function writeClockSubscriptionForTesting(\n view: DataView,\n offset: number,\n subscription: {\n userdata: bigint;\n timeoutNanoseconds: bigint;\n clockid?: number;\n flags?: number;\n }\n): void {\n clearRecord(view, offset, subscriptionByteLength);\n view.setBigUint64(offset, subscription.userdata, true);\n view.setUint8(offset + 8, wasi.EVENTTYPE_CLOCK);\n view.setUint32(offset + 16, subscription.clockid ?? wasi.CLOCKID_MONOTONIC, true);\n view.setBigUint64(offset + 24, subscription.timeoutNanoseconds, true);\n view.setUint16(offset + 36, subscription.flags ?? 0, true);\n}\n\nexport function writeFdReadSubscriptionForTesting(\n view: DataView,\n offset: number,\n subscription: {\n userdata: bigint;\n fd: number;\n }\n): void {\n clearRecord(view, offset, subscriptionByteLength);\n view.setBigUint64(offset, subscription.userdata, true);\n view.setUint8(offset + 8, wasi.EVENTTYPE_FD_READ);\n view.setUint32(offset + 16, subscription.fd, true);\n}\n\nexport function readPollEventsForTesting(\n view: DataView,\n offset: number,\n count: number\n): Array<{ userdata: bigint; errno: number; eventtype: number }> {\n return Array.from({ length: count }, (_, index) => {\n const eventOffset = offset + index * eventByteLength;\n return {\n userdata: view.getBigUint64(eventOffset, true),\n errno: view.getUint16(eventOffset + 8, true),\n eventtype: view.getUint8(eventOffset + 10),\n };\n });\n}\n\nfunction clearRecord(\n view: DataView,\n offset: number,\n byteLength: number\n): void {\n new Uint8Array(view.buffer, offset, byteLength).fill(0);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WasiPollScheduler.js","names":[],"sources":["../../../src/wasi/WasiPollScheduler.ts"],"sourcesContent":["import { wasi } from \"@bjorn3/browser_wasi_shim\";\n\nimport type { SharedInputReadiness } from \"./SharedInputQueue.ts\";\n\nconst subscriptionByteLength = 48;\nconst eventByteLength = 32;\nconst maximumAtomicsWaitMilliseconds = 2_147_483_647;\n\ninterface ClockSubscription {\n readonly type: \"clock\";\n readonly userdata: bigint;\n readonly clockid: number;\n readonly deadlineMilliseconds: number;\n}\n\ninterface FdReadSubscription {\n readonly type: \"fdRead\";\n readonly userdata: bigint;\n readonly fd: number;\n}\n\ntype SupportedSubscription = ClockSubscription | FdReadSubscription;\n\nexport interface WasiPollReadableState {\n availableBytes(): number;\n isClosed(): boolean;\n}\n\nexport interface WasiPollReadableSource extends WasiPollReadableState {\n waitForReadable(timeoutMilliseconds?: number): SharedInputReadiness;\n}\n\nexport interface SuspendingWasiPollReadableSource extends WasiPollReadableState {\n waitForReadableAsync(timeoutMilliseconds?: number): Promise<SharedInputReadiness>;\n}\n\nexport interface WasiPollSchedulerOptions {\n memory(): WebAssembly.Memory | undefined;\n stdin: WasiPollReadableSource;\n fallbackPoll(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number;\n nowMilliseconds?(): number;\n}\n\nexport class WasiPollScheduler {\n private readonly memory: WasiPollSchedulerOptions[\"memory\"];\n private readonly stdin: WasiPollReadableSource;\n private readonly fallbackPoll: WasiPollSchedulerOptions[\"fallbackPoll\"];\n private readonly nowMilliseconds: () => number;\n private readonly waitBuffer = new Int32Array(\n new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)\n );\n\n constructor(options: WasiPollSchedulerOptions) {\n this.memory = options.memory;\n this.stdin = options.stdin;\n this.fallbackPoll = options.fallbackPoll;\n this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());\n }\n\n pollOneOff(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number {\n const memory = this.memory();\n if (!memory || nsubscriptions <= 0) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n const view = new DataView(memory.buffer);\n const subscriptions = readSubscriptions(\n view,\n inPtr,\n nsubscriptions,\n this.nowMilliseconds()\n );\n if (subscriptions === undefined) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n while (true) {\n const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());\n if (ready.length > 0) {\n writeEvents(view, outPtr, ready, this.stdin);\n if (neventsPtr !== undefined) {\n view.setUint32(neventsPtr, ready.length, true);\n }\n return wasi.ERRNO_SUCCESS;\n }\n\n const timeoutMilliseconds = shortestClockTimeoutMilliseconds(\n subscriptions,\n this.nowMilliseconds()\n );\n if (hasFdReadSubscription(subscriptions)) {\n this.stdin.waitForReadable(timeoutMilliseconds);\n } else if (timeoutMilliseconds !== undefined) {\n Atomics.wait(\n this.waitBuffer,\n 0,\n 0,\n Math.min(timeoutMilliseconds, maximumAtomicsWaitMilliseconds)\n );\n } else {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n }\n }\n}\n\nexport interface SuspendingWasiPollSchedulerOptions {\n memory(): WebAssembly.Memory | undefined;\n stdin: SuspendingWasiPollReadableSource;\n fallbackPoll(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number;\n nowMilliseconds?(): number;\n}\n\n/**\n * The JSPI (main-thread) counterpart of `WasiPollScheduler`: identical\n * subscription semantics, but the blocking waits become awaited promises so\n * the surrounding `poll_oneoff` import can be wrapped in\n * `WebAssembly.Suspending` and run without a worker or SharedArrayBuffer.\n */\nexport class SuspendingWasiPollScheduler {\n private readonly memory: SuspendingWasiPollSchedulerOptions[\"memory\"];\n private readonly stdin: SuspendingWasiPollReadableSource;\n private readonly fallbackPoll: SuspendingWasiPollSchedulerOptions[\"fallbackPoll\"];\n private readonly nowMilliseconds: () => number;\n\n constructor(options: SuspendingWasiPollSchedulerOptions) {\n this.memory = options.memory;\n this.stdin = options.stdin;\n this.fallbackPoll = options.fallbackPoll;\n this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());\n }\n\n async pollOneOff(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): Promise<number> {\n const memory = this.memory();\n if (!memory || nsubscriptions <= 0) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n const subscriptions = readSubscriptions(\n new DataView(memory.buffer),\n inPtr,\n nsubscriptions,\n this.nowMilliseconds()\n );\n if (subscriptions === undefined) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n while (true) {\n const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());\n if (ready.length > 0) {\n // Re-derive the view each pass: memory.buffer detaches when the wasm\n // grows memory while we were suspended.\n const view = new DataView(memory.buffer);\n writeEvents(view, outPtr, ready, this.stdin);\n if (neventsPtr !== undefined) {\n view.setUint32(neventsPtr, ready.length, true);\n }\n return wasi.ERRNO_SUCCESS;\n }\n\n const timeoutMilliseconds = shortestClockTimeoutMilliseconds(\n subscriptions,\n this.nowMilliseconds()\n );\n if (hasFdReadSubscription(subscriptions)) {\n await this.stdin.waitForReadableAsync(timeoutMilliseconds);\n } else if (timeoutMilliseconds !== undefined) {\n await new Promise((resolve) =>\n setTimeout(resolve, Math.min(timeoutMilliseconds, maximumAtomicsWaitMilliseconds))\n );\n } else {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n }\n }\n}\n\nfunction readSubscriptions(\n view: DataView,\n inPtr: number,\n nsubscriptions: number,\n nowMilliseconds: number\n): SupportedSubscription[] | undefined {\n const subscriptions: SupportedSubscription[] = [];\n for (let index = 0; index < nsubscriptions; index += 1) {\n const subscription = wasi.Subscription.read_bytes(\n view,\n inPtr + index * subscriptionByteLength\n );\n switch (subscription.eventtype) {\n case wasi.EVENTTYPE_CLOCK:\n if (!isSupportedClockId(subscription.clockid)) {\n return undefined;\n }\n subscriptions.push({\n type: \"clock\",\n userdata: subscription.userdata,\n clockid: subscription.clockid,\n deadlineMilliseconds: clockDeadlineMilliseconds(subscription, nowMilliseconds),\n });\n break;\n case wasi.EVENTTYPE_FD_READ:\n if (subscription.clockid !== wasi.FD_STDIN) {\n return undefined;\n }\n subscriptions.push({\n type: \"fdRead\",\n userdata: subscription.userdata,\n fd: subscription.clockid,\n });\n break;\n default:\n return undefined;\n }\n }\n return subscriptions;\n}\n\nfunction isSupportedClockId(\n clockid: number\n): boolean {\n return clockid === wasi.CLOCKID_MONOTONIC || clockid === wasi.CLOCKID_REALTIME;\n}\n\nfunction shortestClockTimeoutMilliseconds(\n subscriptions: readonly SupportedSubscription[],\n nowMilliseconds: number\n): number | undefined {\n let timeoutMilliseconds: number | undefined;\n for (const subscription of subscriptions) {\n if (subscription.type !== \"clock\") {\n continue;\n }\n const remaining = clockRemainingMilliseconds(subscription, nowMilliseconds);\n timeoutMilliseconds = timeoutMilliseconds === undefined\n ? remaining\n : Math.min(timeoutMilliseconds, remaining);\n }\n return timeoutMilliseconds;\n}\n\nfunction readySubscriptions(\n subscriptions: readonly SupportedSubscription[],\n stdin: WasiPollReadableState,\n nowMilliseconds: number\n): SupportedSubscription[] {\n return subscriptions.filter((subscription) => {\n switch (subscription.type) {\n case \"clock\":\n return clockRemainingMilliseconds(subscription, nowMilliseconds) <= 0;\n case \"fdRead\":\n return stdin.availableBytes() > 0 || stdin.isClosed();\n }\n });\n}\n\nfunction hasFdReadSubscription(\n subscriptions: readonly SupportedSubscription[]\n): boolean {\n return subscriptions.some((subscription) => subscription.type === \"fdRead\");\n}\n\nfunction clockRemainingMilliseconds(\n subscription: ClockSubscription,\n nowMilliseconds: number\n): number {\n return Math.max(0, subscription.deadlineMilliseconds - nowMillisecondsForClock(\n subscription.clockid,\n nowMilliseconds\n ));\n}\n\nfunction clockDeadlineMilliseconds(\n subscription: wasi.Subscription,\n nowMilliseconds: number\n): number {\n if ((subscription.flags & wasi.SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME) !== 0) {\n return Number(subscription.timeout) / 1_000_000;\n }\n return nowMillisecondsForClock(subscription.clockid, nowMilliseconds)\n + Number(subscription.timeout) / 1_000_000;\n}\n\nfunction nowMillisecondsForClock(\n clockid: number,\n nowMilliseconds: number\n): number {\n if (clockid === wasi.CLOCKID_REALTIME) {\n return Date.now();\n }\n return nowMilliseconds;\n}\n\nfunction writeEvents(\n view: DataView,\n outPtr: number,\n subscriptions: readonly SupportedSubscription[],\n stdin: WasiPollReadableSource\n): void {\n subscriptions.forEach((subscription, index) => {\n const eventtype = subscription.type === \"clock\"\n ? wasi.EVENTTYPE_CLOCK\n : wasi.EVENTTYPE_FD_READ;\n const offset = outPtr + index * eventByteLength;\n new wasi.Event(\n subscription.userdata,\n wasi.ERRNO_SUCCESS,\n eventtype\n ).write_bytes(view, offset);\n if (subscription.type === \"fdRead\") {\n const availableBytes = Math.max(0, stdin.availableBytes());\n view.setBigUint64(offset + 16, BigInt(availableBytes), true);\n if (availableBytes === 0 && stdin.isClosed()) {\n view.setUint16(offset + 24, wasi.EVENTRWFLAGS_FD_READWRITE_HANGUP, true);\n }\n }\n });\n}\n\nexport function writeClockSubscriptionForTesting(\n view: DataView,\n offset: number,\n subscription: {\n userdata: bigint;\n timeoutNanoseconds: bigint;\n clockid?: number;\n flags?: number;\n }\n): void {\n clearRecord(view, offset, subscriptionByteLength);\n view.setBigUint64(offset, subscription.userdata, true);\n view.setUint8(offset + 8, wasi.EVENTTYPE_CLOCK);\n view.setUint32(offset + 16, subscription.clockid ?? wasi.CLOCKID_MONOTONIC, true);\n view.setBigUint64(offset + 24, subscription.timeoutNanoseconds, true);\n view.setUint16(offset + 36, subscription.flags ?? 0, true);\n}\n\nexport function writeFdReadSubscriptionForTesting(\n view: DataView,\n offset: number,\n subscription: {\n userdata: bigint;\n fd: number;\n }\n): void {\n clearRecord(view, offset, subscriptionByteLength);\n view.setBigUint64(offset, subscription.userdata, true);\n view.setUint8(offset + 8, wasi.EVENTTYPE_FD_READ);\n view.setUint32(offset + 16, subscription.fd, true);\n}\n\nexport function readPollEventsForTesting(\n view: DataView,\n offset: number,\n count: number\n): Array<{ userdata: bigint; errno: number; eventtype: number }> {\n return Array.from({ length: count }, (_, index) => {\n const eventOffset = offset + index * eventByteLength;\n return {\n userdata: view.getBigUint64(eventOffset, true),\n errno: view.getUint16(eventOffset + 8, true),\n eventtype: view.getUint8(eventOffset + 10),\n };\n });\n}\n\nfunction clearRecord(\n view: DataView,\n offset: number,\n byteLength: number\n): void {\n new Uint8Array(view.buffer, offset, byteLength).fill(0);\n}\n"],"mappings":"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"}
|
|
@@ -11,8 +11,9 @@ interface WasmEngineCapabilities {
|
|
|
11
11
|
engine: WasmEngineFamily;
|
|
12
12
|
/**
|
|
13
13
|
* WebAssembly JavaScript Promise Integration (`WebAssembly.Suspending` +
|
|
14
|
-
* `WebAssembly.promising`). When true,
|
|
15
|
-
* can suspend on stdin instead of
|
|
14
|
+
* `WebAssembly.promising`). When true, the main-thread execution mode
|
|
15
|
+
* (`MainThreadWasmExecutor`) can suspend on stdin/timers instead of
|
|
16
|
+
* blocking a worker on `Atomics.wait`.
|
|
16
17
|
*/
|
|
17
18
|
supportsJSPI: boolean;
|
|
18
19
|
/**
|
|
@@ -30,6 +31,18 @@ declare function resolveWasmEngineCapabilities(signals?: WasmEngineProbeSignals)
|
|
|
30
31
|
* `SWIFTTUI_STACK_LEAN_PROFILE` (or a tuning override) always wins.
|
|
31
32
|
*/
|
|
32
33
|
declare function stackProfileEnvironmentDefaults(capabilities: WasmEngineCapabilities): Record<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* Environment defaults for the main-thread (JSPI) execution mode, where the
|
|
36
|
+
* wasm runs on the page's thread and gets its far larger stack budget
|
|
37
|
+
* (measured ~12.7× the worker's on trunk WebKit).
|
|
38
|
+
*/
|
|
39
|
+
declare function mainThreadStackProfileEnvironmentDefaults(capabilities: WasmEngineCapabilities): Record<string, string>;
|
|
40
|
+
interface JSPIConstructors {
|
|
41
|
+
Suspending: new (fn: (...args: never[]) => unknown) => unknown;
|
|
42
|
+
promising: (fn: unknown) => (...args: unknown[]) => Promise<unknown>;
|
|
43
|
+
}
|
|
44
|
+
/** Typed access to the JSPI surface, or undefined where unsupported. */
|
|
45
|
+
declare function jspiConstructors(): JSPIConstructors | undefined;
|
|
33
46
|
//#endregion
|
|
34
|
-
export { WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, classifyWasmEngineFamily, collectWasmEngineProbeSignals, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults };
|
|
47
|
+
export { JSPIConstructors, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults };
|
|
35
48
|
//# sourceMappingURL=WasmEngineCapabilities.d.ts.map
|
|
@@ -30,10 +30,22 @@ function resolveWasmEngineCapabilities(signals = collectWasmEngineProbeSignals()
|
|
|
30
30
|
* `SWIFTTUI_STACK_LEAN_PROFILE` (or a tuning override) always wins.
|
|
31
31
|
*/
|
|
32
32
|
function stackProfileEnvironmentDefaults(capabilities) {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Environment defaults for the main-thread (JSPI) execution mode, where the
|
|
37
|
+
* wasm runs on the page's thread and gets its far larger stack budget
|
|
38
|
+
* (measured ~12.7× the worker's on trunk WebKit).
|
|
39
|
+
*/
|
|
40
|
+
function mainThreadStackProfileEnvironmentDefaults(capabilities) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
/** Typed access to the JSPI surface, or undefined where unsupported. */
|
|
44
|
+
function jspiConstructors() {
|
|
45
|
+
const wasm = globalThis.WebAssembly;
|
|
46
|
+
if (typeof wasm?.Suspending === "function" && typeof wasm?.promising === "function") return wasm;
|
|
35
47
|
}
|
|
36
48
|
//#endregion
|
|
37
|
-
export { classifyWasmEngineFamily, collectWasmEngineProbeSignals, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults };
|
|
49
|
+
export { classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults };
|
|
38
50
|
|
|
39
51
|
//# sourceMappingURL=WasmEngineCapabilities.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WasmEngineCapabilities.js","names":[],"sources":["../../../src/wasi/WasmEngineCapabilities.ts"],"sourcesContent":["// Runtime detection of the browser's JS/wasm engine family and the wasm\n// capabilities that decide how the SwiftTUI WASI runtime should execute.\n//\n// JavaScriptCore runs wasm calls on the host thread's native stack, and\n// Darwin worker threads get ~1/16 of the main-thread stack budget, so\n// SwiftTUI's WASI build defaults to its stack-lean resolve profile\n// (`SWIFTTUI_STACK_LEAN_PROFILE`, depth-capped chunked resolve). That profile\n// costs steady-state pipeline time, and engines with roomy worker stacks\n// don't need it. Detection is deliberately asymmetric: a wrongly applied lean\n// profile only costs speed, while wrongly disabling it on
|
|
1
|
+
{"version":3,"file":"WasmEngineCapabilities.js","names":[],"sources":["../../../src/wasi/WasmEngineCapabilities.ts"],"sourcesContent":["// Runtime detection of the browser's JS/wasm engine family and the wasm\n// capabilities that decide how the SwiftTUI WASI runtime should execute.\n//\n// JavaScriptCore runs wasm calls on the host thread's native stack, and\n// Darwin worker threads get ~1/16 of the main-thread stack budget, so\n// SwiftTUI's WASI build defaults to its stack-lean resolve profile\n// (`SWIFTTUI_STACK_LEAN_PROFILE`, depth-capped chunked resolve). That profile\n// costs steady-state pipeline time, and engines with roomy worker stacks\n// don't need it. Detection is deliberately asymmetric: a wrongly applied lean\n// profile only costs speed, while wrongly disabling it on a small-stack\n// engine overflows the wasm stack and kills the app — so lean stays\n// recommended unless the engine is confidently V8, the only family with a\n// measured, comfortable worker budget. Gecko is measured (Firefox, live,\n// 2026-07) to NOT fit the non-lean shape in its worker: it must keep the\n// lean profile in worker mode.\n//\n// Engine classification reads Error mechanics rather than user-agent\n// strings: V8 formats stack frames as ` at fn (url)`, JSC and Gecko as\n// `fn@url`, and the two are split by engine-specific Error instance\n// properties (Gecko `fileName`, JSC `sourceURL`). Trunk WebKit (STP ≥ 238)\n// no longer exposes `sourceURL` on constructed Errors, so JSC\n// classification there rides the `fn@url` stack-shape fallback. Non-browser\n// JSC hosts that emulate V8 stack frames for Node compatibility (e.g. Bun)\n// classify as \"v8\"; the probe targets browser engines, where the formats\n// don't cross.\n\nexport type WasmEngineFamily = \"v8\" | \"jsc\" | \"gecko\" | \"unknown\";\n\nexport interface WasmEngineProbeSignals {\n errorStack: string;\n errorHasGeckoFileName: boolean;\n errorHasJSCSourceURL: boolean;\n wasmSuspendingType: string;\n wasmPromisingType: string;\n}\n\nexport interface WasmEngineCapabilities {\n engine: WasmEngineFamily;\n /**\n * WebAssembly JavaScript Promise Integration (`WebAssembly.Suspending` +\n * `WebAssembly.promising`). When true, the main-thread execution mode\n * (`MainThreadWasmExecutor`) can suspend on stdin/timers instead of\n * blocking a worker on `Atomics.wait`.\n */\n supportsJSPI: boolean;\n /**\n * Whether the SwiftTUI WASI build should keep its stack-lean resolve\n * profile on this engine.\n */\n stackLeanRecommended: boolean;\n}\n\nexport function collectWasmEngineProbeSignals(): WasmEngineProbeSignals {\n const probe = new Error(\"wasm-engine-probe\");\n const wasm = (\n globalThis as {\n WebAssembly?: { Suspending?: unknown; promising?: unknown };\n }\n ).WebAssembly;\n return {\n errorStack: typeof probe.stack === \"string\" ? probe.stack : \"\",\n errorHasGeckoFileName: \"fileName\" in probe,\n errorHasJSCSourceURL: \"sourceURL\" in probe,\n wasmSuspendingType: typeof wasm?.Suspending,\n wasmPromisingType: typeof wasm?.promising,\n };\n}\n\nexport function classifyWasmEngineFamily(\n signals: WasmEngineProbeSignals\n): WasmEngineFamily {\n if (/^\\s*at /m.test(signals.errorStack)) {\n return \"v8\";\n }\n if (signals.errorHasGeckoFileName) {\n return \"gecko\";\n }\n if (signals.errorHasJSCSourceURL || /^[^\\n]*@/m.test(signals.errorStack)) {\n return \"jsc\";\n }\n return \"unknown\";\n}\n\nexport function resolveWasmEngineCapabilities(\n signals: WasmEngineProbeSignals = collectWasmEngineProbeSignals()\n): WasmEngineCapabilities {\n const engine = classifyWasmEngineFamily(signals);\n return {\n engine,\n supportsJSPI:\n signals.wasmSuspendingType === \"function\" &&\n signals.wasmPromisingType === \"function\",\n stackLeanRecommended: engine !== \"v8\",\n };\n}\n\n/**\n * WASI environment defaults implied by the engine capabilities. Spread these\n * *before* caller-provided environment entries so an explicit\n * `SWIFTTUI_STACK_LEAN_PROFILE` (or a tuning override) always wins.\n */\nexport function stackProfileEnvironmentDefaults(\n capabilities: WasmEngineCapabilities\n): Record<string, string> {\n // HOLD (2026-07-20): no engine currently gets a non-lean default, even\n // where the stack budget fits (V8 workers, JSC/V8 main threads). The\n // browser surface transport has only ever shipped lean; under the\n // non-lean profile the reuse gates suppress per-generation publications\n // and steady scenes coalesce to ~1 wire frame per several ticks — a\n // worse regression than lean's resolve cost. Revisit when the framework\n // emits per-tick deltas under reuse; `stackLeanRecommended` still\n // reports which engines could flip.\n void capabilities;\n return {};\n}\n\n/**\n * Environment defaults for the main-thread (JSPI) execution mode, where the\n * wasm runs on the page's thread and gets its far larger stack budget\n * (measured ~12.7× the worker's on trunk WebKit).\n */\nexport function mainThreadStackProfileEnvironmentDefaults(\n capabilities: WasmEngineCapabilities\n): Record<string, string> {\n // Same hold as `stackProfileEnvironmentDefaults`: the main-thread stack\n // budget fits non-lean on JSC and V8 (measured), but non-lean frame\n // emission is not production-ready. Callers can still force the profile\n // via `SWIFTTUI_STACK_LEAN_PROFILE`.\n void capabilities;\n return {};\n}\n\nexport interface JSPIConstructors {\n Suspending: new (fn: (...args: never[]) => unknown) => unknown;\n promising: (fn: unknown) => (...args: unknown[]) => Promise<unknown>;\n}\n\n/** Typed access to the JSPI surface, or undefined where unsupported. */\nexport function jspiConstructors(): JSPIConstructors | undefined {\n const wasm = globalThis.WebAssembly as unknown as Partial<JSPIConstructors> | undefined;\n if (\n typeof wasm?.Suspending === \"function\" &&\n typeof wasm?.promising === \"function\"\n ) {\n return wasm as JSPIConstructors;\n }\n return undefined;\n}\n"],"mappings":";AAoDA,SAAgB,gCAAwD;CACtE,MAAM,wBAAQ,IAAI,MAAM,mBAAmB;CAC3C,MAAM,OACJ,WAGA;CACF,OAAO;EACL,YAAY,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;EAC5D,uBAAuB,cAAc;EACrC,sBAAsB,eAAe;EACrC,oBAAoB,OAAO,MAAM;EACjC,mBAAmB,OAAO,MAAM;CAClC;AACF;AAEA,SAAgB,yBACd,SACkB;CAClB,IAAI,WAAW,KAAK,QAAQ,UAAU,GACpC,OAAO;CAET,IAAI,QAAQ,uBACV,OAAO;CAET,IAAI,QAAQ,wBAAwB,YAAY,KAAK,QAAQ,UAAU,GACrE,OAAO;CAET,OAAO;AACT;AAEA,SAAgB,8BACd,UAAkC,8BAA8B,GACxC;CACxB,MAAM,SAAS,yBAAyB,OAAO;CAC/C,OAAO;EACL;EACA,cACE,QAAQ,uBAAuB,cAC/B,QAAQ,sBAAsB;EAChC,sBAAsB,WAAW;CACnC;AACF;;;;;;AAOA,SAAgB,gCACd,cACwB;CAUxB,OAAO,CAAC;AACV;;;;;;AAOA,SAAgB,0CACd,cACwB;CAMxB,OAAO,CAAC;AACV;;AAQA,SAAgB,mBAAiD;CAC/D,MAAM,OAAO,WAAW;CACxB,IACE,OAAO,MAAM,eAAe,cAC5B,OAAO,MAAM,cAAc,YAE3B,OAAO;AAGX"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WebHostSceneRuntime, WebHostSceneRuntimeOptions } from "../WebHostSceneRuntime.js";
|
|
2
|
+
import { WasmEngineCapabilities } from "./WasmEngineCapabilities.js";
|
|
2
3
|
//#region src/wasi/WasmSceneRuntime.d.ts
|
|
3
4
|
interface WasmSceneResizeEvent {
|
|
4
5
|
sceneId: string;
|
|
@@ -11,12 +12,25 @@ interface WasmSceneRuntimeHandle {
|
|
|
11
12
|
readonly descriptor: WebHostSceneRuntime["descriptor"];
|
|
12
13
|
sendInput(chunk: Uint8Array): void;
|
|
13
14
|
}
|
|
15
|
+
type WasmExecutionMode = "worker" | "main-thread";
|
|
16
|
+
type WasmExecutionModePreference = WasmExecutionMode | "auto";
|
|
14
17
|
interface WasmSceneRuntimeFactoryOptions {
|
|
15
18
|
onSceneResize?(event: WasmSceneResizeEvent): void;
|
|
16
19
|
onRuntimeCreated?(runtime: WasmSceneRuntimeHandle): void;
|
|
17
20
|
workerModuleURL?: string | URL;
|
|
21
|
+
/**
|
|
22
|
+
* How to execute the wasm app. "worker" is the classic path
|
|
23
|
+
* (`Atomics.wait` stdin, needs SharedArrayBuffer/COOP/COEP). "main-thread"
|
|
24
|
+
* runs on the page's thread via WebAssembly JSPI — larger stack budget (no
|
|
25
|
+
* stack-lean profile on measured engines), no COOP/COEP requirement, at
|
|
26
|
+
* the cost of sharing the main thread. "auto" (default) picks main-thread
|
|
27
|
+
* only where workers cannot run (SharedArrayBuffer unavailable and JSPI
|
|
28
|
+
* present); workers everywhere else.
|
|
29
|
+
*/
|
|
30
|
+
executionMode?: WasmExecutionModePreference;
|
|
18
31
|
}
|
|
32
|
+
declare function resolveWasmExecutionMode(preference: WasmExecutionModePreference, capabilities: WasmEngineCapabilities, sharedInputQueueAvailable: boolean): WasmExecutionMode;
|
|
19
33
|
declare function createWasmSceneRuntimeFactory(wasmURL: URL, factoryOptions?: WasmSceneRuntimeFactoryOptions): (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;
|
|
20
34
|
//#endregion
|
|
21
|
-
export { WasmSceneResizeEvent, WasmSceneRuntimeFactoryOptions, WasmSceneRuntimeHandle, createWasmSceneRuntimeFactory };
|
|
35
|
+
export { WasmExecutionMode, WasmExecutionModePreference, WasmSceneResizeEvent, WasmSceneRuntimeFactoryOptions, WasmSceneRuntimeHandle, createWasmSceneRuntimeFactory, resolveWasmExecutionMode };
|
|
22
36
|
//# sourceMappingURL=WasmSceneRuntime.d.ts.map
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities } from "./WasmEngineCapabilities.js";
|
|
1
2
|
import { WebHostSceneRuntime } from "../WebHostSceneRuntime.js";
|
|
3
|
+
import { MainThreadWasmExecutor } from "./MainThreadWasmExecutor.js";
|
|
2
4
|
import { SharedInputQueueWriter, createSharedInputQueue } from "./SharedInputQueue.js";
|
|
3
5
|
//#region src/wasi/WasmSceneRuntime.ts
|
|
4
6
|
const workerModuleURL = new URL("./wasm-scene-worker.js", import.meta.url);
|
|
7
|
+
function resolveWasmExecutionMode(preference, capabilities, sharedInputQueueAvailable) {
|
|
8
|
+
if (preference !== "auto") return preference;
|
|
9
|
+
if (!capabilities.supportsJSPI) return "worker";
|
|
10
|
+
if (!sharedInputQueueAvailable) return "main-thread";
|
|
11
|
+
return "worker";
|
|
12
|
+
}
|
|
5
13
|
function createWasmSceneRuntimeFactory(wasmURL, factoryOptions = {}) {
|
|
6
14
|
return (options) => {
|
|
7
15
|
const runtime = new WasmSceneRuntime(options, wasmURL, factoryOptions);
|
|
@@ -14,44 +22,53 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
|
|
|
14
22
|
wasmURL;
|
|
15
23
|
onSceneResize;
|
|
16
24
|
workerModuleURL;
|
|
25
|
+
executionModePreference;
|
|
17
26
|
inputQueue;
|
|
18
27
|
inputWriter;
|
|
28
|
+
inputRouter;
|
|
29
|
+
sharedQueueError;
|
|
19
30
|
detachBridgeInputListener;
|
|
20
31
|
detachResizeListener;
|
|
21
32
|
worker;
|
|
33
|
+
executor;
|
|
22
34
|
didMount = false;
|
|
23
35
|
constructor(options, wasmURL, factoryOptions) {
|
|
24
36
|
let inputQueue;
|
|
25
37
|
let inputWriter;
|
|
38
|
+
let sharedQueueError;
|
|
26
39
|
try {
|
|
27
40
|
inputQueue = createSharedInputQueue();
|
|
28
41
|
inputWriter = new SharedInputQueueWriter(inputQueue);
|
|
29
42
|
} catch (error) {
|
|
30
|
-
|
|
43
|
+
sharedQueueError = error;
|
|
31
44
|
}
|
|
45
|
+
const inputRouter = { route: (chunk) => {
|
|
46
|
+
try {
|
|
47
|
+
inputWriter?.write(chunk);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error("[SwiftTUIWeb] failed to enqueue terminal input", error);
|
|
50
|
+
}
|
|
51
|
+
} };
|
|
32
52
|
super({
|
|
33
53
|
...options,
|
|
34
|
-
onInput: (chunk) =>
|
|
35
|
-
try {
|
|
36
|
-
inputWriter?.write(chunk);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error("[SwiftTUIWeb] failed to enqueue terminal input", error);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
54
|
+
onInput: (chunk) => inputRouter.route(chunk)
|
|
41
55
|
});
|
|
42
56
|
this.bridge = options.bridge;
|
|
43
57
|
this.wasmURL = wasmURL;
|
|
44
58
|
this.onSceneResize = factoryOptions.onSceneResize;
|
|
45
59
|
this.workerModuleURL = factoryOptions.workerModuleURL ?? workerModuleURL;
|
|
60
|
+
this.executionModePreference = factoryOptions.executionMode ?? "auto";
|
|
46
61
|
this.inputQueue = inputQueue;
|
|
47
62
|
this.inputWriter = inputWriter;
|
|
63
|
+
this.inputRouter = inputRouter;
|
|
64
|
+
this.sharedQueueError = sharedQueueError;
|
|
48
65
|
}
|
|
49
66
|
async mount() {
|
|
50
67
|
await super.mount();
|
|
51
68
|
if (this.didMount) return;
|
|
52
69
|
this.didMount = true;
|
|
53
70
|
this.detachBridgeInputListener = this.bridge?.stdin.subscribe((chunk) => {
|
|
54
|
-
this.
|
|
71
|
+
this.inputRouter.route(chunk);
|
|
55
72
|
});
|
|
56
73
|
this.detachResizeListener = this.bridge?.subscribeResize((columns, rows, cellWidth, cellHeight) => {
|
|
57
74
|
this.onSceneResize?.({
|
|
@@ -69,7 +86,16 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
|
|
|
69
86
|
columns: initialColumns,
|
|
70
87
|
rows: initialRows
|
|
71
88
|
});
|
|
72
|
-
if (!this.
|
|
89
|
+
if (!this.bridge) {
|
|
90
|
+
this.writeOutput("\r\nSwiftTUI WASI browser runtime requires a WASI bridge.\r\n");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (resolveWasmExecutionMode(this.executionModePreference, resolveWasmEngineCapabilities(), this.inputQueue !== void 0 && this.inputWriter !== void 0) === "main-thread") {
|
|
94
|
+
this.startMainThreadExecutor();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!this.inputQueue || !this.inputWriter) {
|
|
98
|
+
if (this.sharedQueueError !== void 0) console.error("[SwiftTUIWeb] failed to create shared stdin queue", this.sharedQueueError);
|
|
73
99
|
this.writeOutput("\r\nSwiftTUI WASI browser runtime requires SharedArrayBuffer-backed stdin. Serve the app with COOP/COEP headers.\r\n");
|
|
74
100
|
return;
|
|
75
101
|
}
|
|
@@ -94,8 +120,31 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
|
|
|
94
120
|
this.detachResizeListener?.();
|
|
95
121
|
this.inputWriter?.close();
|
|
96
122
|
this.worker?.terminate();
|
|
123
|
+
this.executor?.dispose();
|
|
97
124
|
super.dispose();
|
|
98
125
|
}
|
|
126
|
+
startMainThreadExecutor() {
|
|
127
|
+
const bridge = this.bridge;
|
|
128
|
+
if (!bridge) return;
|
|
129
|
+
const executor = new MainThreadWasmExecutor({
|
|
130
|
+
wasmURL: this.wasmURL.href,
|
|
131
|
+
environment: {
|
|
132
|
+
...mainThreadStackProfileEnvironmentDefaults(resolveWasmEngineCapabilities()),
|
|
133
|
+
...bridge.environment
|
|
134
|
+
},
|
|
135
|
+
onStdout: (chunk) => bridge.stdout.write(chunk),
|
|
136
|
+
onStderr: (chunk) => bridge.stderr.write(chunk),
|
|
137
|
+
onExit: (code) => {
|
|
138
|
+
if (code !== 0) bridge.stderr.write(`\nSwiftTUI WASI app exited with code ${code}.\n`);
|
|
139
|
+
},
|
|
140
|
+
onError: (message) => {
|
|
141
|
+
bridge.stderr.write(`\nFailed to start SwiftTUI WASI app: ${message}\n`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
this.executor = executor;
|
|
145
|
+
this.inputRouter.route = (chunk) => executor.sendInput(chunk);
|
|
146
|
+
executor.start();
|
|
147
|
+
}
|
|
99
148
|
handleWorkerMessage(message) {
|
|
100
149
|
switch (message.type) {
|
|
101
150
|
case "stdout":
|
|
@@ -114,6 +163,6 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
|
|
|
114
163
|
}
|
|
115
164
|
};
|
|
116
165
|
//#endregion
|
|
117
|
-
export { createWasmSceneRuntimeFactory };
|
|
166
|
+
export { createWasmSceneRuntimeFactory, resolveWasmExecutionMode };
|
|
118
167
|
|
|
119
168
|
//# sourceMappingURL=WasmSceneRuntime.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WasmSceneRuntime.js","names":[],"sources":["../../../src/wasi/WasmSceneRuntime.ts"],"sourcesContent":["import {\n WebHostSceneRuntime,\n type WebHostSceneRuntimeOptions,\n} from \"../WebHostSceneRuntime.ts\";\nimport {\n encodeResizeControlMessage,\n type BrowserWASIBridge,\n} from \"./BrowserWASIBridge.ts\";\n\nimport {\n SharedInputQueueWriter,\n createSharedInputQueue,\n type SharedInputQueueBuffers,\n} from \"./SharedInputQueue.ts\";\n\nconst workerModuleURL = new URL(\"./wasm-scene-worker.js\", import.meta.url);\n\ninterface WorkerStartMessage {\n type: \"start\";\n wasmURL: string;\n environment: Record<string, string>;\n inputQueue: SharedInputQueueBuffers;\n}\n\ninterface WorkerOutputMessage {\n type: \"stdout\" | \"stderr\";\n chunk: Uint8Array;\n}\n\ninterface WorkerExitMessage {\n type: \"exit\";\n code: number;\n}\n\ninterface WorkerErrorMessage {\n type: \"error\";\n message: string;\n}\n\ntype WorkerMessage = WorkerOutputMessage | WorkerExitMessage | WorkerErrorMessage;\n\nexport interface WasmSceneResizeEvent {\n sceneId: string;\n columns: number;\n rows: number;\n cellWidth?: number;\n cellHeight?: number;\n}\n\nexport interface WasmSceneRuntimeHandle {\n readonly descriptor: WebHostSceneRuntime[\"descriptor\"];\n sendInput(chunk: Uint8Array): void;\n}\n\nexport interface WasmSceneRuntimeFactoryOptions {\n onSceneResize?(event: WasmSceneResizeEvent): void;\n onRuntimeCreated?(runtime: WasmSceneRuntimeHandle): void;\n workerModuleURL?: string | URL;\n}\n\nexport function createWasmSceneRuntimeFactory(\n wasmURL: URL,\n factoryOptions: WasmSceneRuntimeFactoryOptions = {}\n): (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime {\n return (options) => {\n const runtime = new WasmSceneRuntime(options, wasmURL, factoryOptions);\n factoryOptions.onRuntimeCreated?.(runtime);\n return runtime;\n };\n}\n\nclass WasmSceneRuntime extends WebHostSceneRuntime {\n private readonly bridge?: BrowserWASIBridge;\n private readonly wasmURL: URL;\n private readonly onSceneResize?: (event: WasmSceneResizeEvent) => void;\n private readonly workerModuleURL: string | URL;\n private readonly inputQueue?: SharedInputQueueBuffers;\n private readonly inputWriter?: SharedInputQueueWriter;\n\n private detachBridgeInputListener?: () => void;\n private detachResizeListener?: () => void;\n private worker?: Worker;\n private didMount = false;\n\n constructor(\n options: WebHostSceneRuntimeOptions,\n wasmURL: URL,\n factoryOptions: WasmSceneRuntimeFactoryOptions\n ) {\n let inputQueue: SharedInputQueueBuffers | undefined;\n let inputWriter: SharedInputQueueWriter | undefined;\n\n try {\n inputQueue = createSharedInputQueue();\n inputWriter = new SharedInputQueueWriter(inputQueue);\n } catch (error) {\n console.error(\"[SwiftTUIWeb] failed to create shared stdin queue\", error);\n }\n\n super({\n ...options,\n onInput: (chunk) => {\n try {\n inputWriter?.write(chunk);\n } catch (error) {\n console.error(\"[SwiftTUIWeb] failed to enqueue terminal input\", error);\n }\n },\n });\n\n this.bridge = options.bridge;\n this.wasmURL = wasmURL;\n this.onSceneResize = factoryOptions.onSceneResize;\n this.workerModuleURL = factoryOptions.workerModuleURL ?? workerModuleURL;\n this.inputQueue = inputQueue;\n this.inputWriter = inputWriter;\n }\n\n override async mount(): Promise<void> {\n await super.mount();\n if (this.didMount) {\n return;\n }\n\n this.didMount = true;\n this.detachBridgeInputListener = this.bridge?.stdin.subscribe((chunk) => {\n this.inputWriter?.write(chunk);\n });\n this.detachResizeListener = this.bridge?.subscribeResize((columns, rows, cellWidth, cellHeight) => {\n this.onSceneResize?.({\n sceneId: this.descriptor.id,\n columns,\n rows,\n cellWidth,\n cellHeight,\n });\n });\n\n const initialColumns = Number(this.bridge?.environment.TUIGUI_COLUMNS ?? \"0\") || 0;\n const initialRows = Number(this.bridge?.environment.TUIGUI_ROWS ?? \"0\") || 0;\n if (!this.bridge && initialColumns > 0 && initialRows > 0) {\n this.onSceneResize?.({\n sceneId: this.descriptor.id,\n columns: initialColumns,\n rows: initialRows,\n });\n }\n\n if (!this.inputQueue || !this.inputWriter || !this.bridge) {\n this.writeOutput(\n \"\\r\\nSwiftTUI WASI browser runtime requires SharedArrayBuffer-backed stdin. Serve the app with COOP/COEP headers.\\r\\n\"\n );\n return;\n }\n\n this.worker = new Worker(this.workerModuleURL, { type: \"module\" });\n this.worker.addEventListener(\"message\", (event: MessageEvent<WorkerMessage>) => {\n this.handleWorkerMessage(event.data);\n });\n this.worker.addEventListener(\"error\", (event) => {\n this.bridge?.stderr.write(\n `\\nSwiftTUI WASI worker failed: ${event.message || \"unknown worker error\"}\\n`\n );\n });\n\n const environment = { ...this.bridge.environment };\n\n const message: WorkerStartMessage = {\n type: \"start\",\n wasmURL: this.wasmURL.href,\n environment,\n inputQueue: this.inputQueue,\n };\n this.worker.postMessage(message);\n }\n\n override dispose(): void {\n this.detachBridgeInputListener?.();\n this.detachResizeListener?.();\n this.inputWriter?.close();\n this.worker?.terminate();\n super.dispose();\n }\n\n private handleWorkerMessage(\n message: WorkerMessage\n ): void {\n switch (message.type) {\n case \"stdout\":\n this.bridge?.stdout.write(message.chunk);\n break;\n case \"stderr\":\n this.bridge?.stderr.write(message.chunk);\n break;\n case \"exit\":\n if (message.code !== 0) {\n this.bridge?.stderr.write(`\\nSwiftTUI WASI app exited with code ${message.code}.\\n`);\n }\n break;\n case \"error\":\n this.bridge?.stderr.write(`\\nFailed to start SwiftTUI WASI app: ${message.message}\\n`);\n break;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WasmSceneRuntime.js","names":[],"sources":["../../../src/wasi/WasmSceneRuntime.ts"],"sourcesContent":["import {\n WebHostSceneRuntime,\n type WebHostSceneRuntimeOptions,\n} from \"../WebHostSceneRuntime.ts\";\nimport {\n encodeResizeControlMessage,\n type BrowserWASIBridge,\n} from \"./BrowserWASIBridge.ts\";\n\nimport { MainThreadWasmExecutor } from \"./MainThreadWasmExecutor.ts\";\nimport {\n SharedInputQueueWriter,\n createSharedInputQueue,\n type SharedInputQueueBuffers,\n} from \"./SharedInputQueue.ts\";\nimport {\n mainThreadStackProfileEnvironmentDefaults,\n resolveWasmEngineCapabilities,\n type WasmEngineCapabilities,\n} from \"./WasmEngineCapabilities.ts\";\n\nconst workerModuleURL = new URL(\"./wasm-scene-worker.js\", import.meta.url);\n\ninterface WorkerStartMessage {\n type: \"start\";\n wasmURL: string;\n environment: Record<string, string>;\n inputQueue: SharedInputQueueBuffers;\n}\n\ninterface WorkerOutputMessage {\n type: \"stdout\" | \"stderr\";\n chunk: Uint8Array;\n}\n\ninterface WorkerExitMessage {\n type: \"exit\";\n code: number;\n}\n\ninterface WorkerErrorMessage {\n type: \"error\";\n message: string;\n}\n\ntype WorkerMessage = WorkerOutputMessage | WorkerExitMessage | WorkerErrorMessage;\n\nexport interface WasmSceneResizeEvent {\n sceneId: string;\n columns: number;\n rows: number;\n cellWidth?: number;\n cellHeight?: number;\n}\n\nexport interface WasmSceneRuntimeHandle {\n readonly descriptor: WebHostSceneRuntime[\"descriptor\"];\n sendInput(chunk: Uint8Array): void;\n}\n\nexport type WasmExecutionMode = \"worker\" | \"main-thread\";\nexport type WasmExecutionModePreference = WasmExecutionMode | \"auto\";\n\nexport interface WasmSceneRuntimeFactoryOptions {\n onSceneResize?(event: WasmSceneResizeEvent): void;\n onRuntimeCreated?(runtime: WasmSceneRuntimeHandle): void;\n workerModuleURL?: string | URL;\n /**\n * How to execute the wasm app. \"worker\" is the classic path\n * (`Atomics.wait` stdin, needs SharedArrayBuffer/COOP/COEP). \"main-thread\"\n * runs on the page's thread via WebAssembly JSPI — larger stack budget (no\n * stack-lean profile on measured engines), no COOP/COEP requirement, at\n * the cost of sharing the main thread. \"auto\" (default) picks main-thread\n * only where workers cannot run (SharedArrayBuffer unavailable and JSPI\n * present); workers everywhere else.\n */\n executionMode?: WasmExecutionModePreference;\n}\n\nexport function resolveWasmExecutionMode(\n preference: WasmExecutionModePreference,\n capabilities: WasmEngineCapabilities,\n sharedInputQueueAvailable: boolean\n): WasmExecutionMode {\n if (preference !== \"auto\") {\n return preference;\n }\n if (!capabilities.supportsJSPI) {\n return \"worker\";\n }\n // Workers stay the auto default even on JSPI-capable engines: main-thread\n // execution shares the page's thread, and its stack-budget advantage only\n // pays off once the non-lean profile is production-ready (see\n // `stackProfileEnvironmentDefaults`). JSPI's auto role today is running\n // where workers cannot — pages without cross-origin isolation.\n if (!sharedInputQueueAvailable) {\n return \"main-thread\";\n }\n return \"worker\";\n}\n\nexport function createWasmSceneRuntimeFactory(\n wasmURL: URL,\n factoryOptions: WasmSceneRuntimeFactoryOptions = {}\n): (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime {\n return (options) => {\n const runtime = new WasmSceneRuntime(options, wasmURL, factoryOptions);\n factoryOptions.onRuntimeCreated?.(runtime);\n return runtime;\n };\n}\n\nclass WasmSceneRuntime extends WebHostSceneRuntime {\n private readonly bridge?: BrowserWASIBridge;\n private readonly wasmURL: URL;\n private readonly onSceneResize?: (event: WasmSceneResizeEvent) => void;\n private readonly workerModuleURL: string | URL;\n private readonly executionModePreference: WasmExecutionModePreference;\n private readonly inputQueue?: SharedInputQueueBuffers;\n private readonly inputWriter?: SharedInputQueueWriter;\n private readonly inputRouter: { route(chunk: Uint8Array): void };\n private readonly sharedQueueError?: unknown;\n\n private detachBridgeInputListener?: () => void;\n private detachResizeListener?: () => void;\n private worker?: Worker;\n private executor?: MainThreadWasmExecutor;\n private didMount = false;\n\n constructor(\n options: WebHostSceneRuntimeOptions,\n wasmURL: URL,\n factoryOptions: WasmSceneRuntimeFactoryOptions\n ) {\n let inputQueue: SharedInputQueueBuffers | undefined;\n let inputWriter: SharedInputQueueWriter | undefined;\n let sharedQueueError: unknown;\n\n try {\n inputQueue = createSharedInputQueue();\n inputWriter = new SharedInputQueueWriter(inputQueue);\n } catch (error) {\n // Not fatal here: the main-thread (JSPI) mode runs without\n // SharedArrayBuffer. Surfaced at mount if the worker mode needs it.\n sharedQueueError = error;\n }\n\n const inputRouter = {\n route: (chunk: Uint8Array): void => {\n try {\n inputWriter?.write(chunk);\n } catch (error) {\n console.error(\"[SwiftTUIWeb] failed to enqueue terminal input\", error);\n }\n },\n };\n\n super({\n ...options,\n onInput: (chunk) => inputRouter.route(chunk),\n });\n\n this.bridge = options.bridge;\n this.wasmURL = wasmURL;\n this.onSceneResize = factoryOptions.onSceneResize;\n this.workerModuleURL = factoryOptions.workerModuleURL ?? workerModuleURL;\n this.executionModePreference = factoryOptions.executionMode ?? \"auto\";\n this.inputQueue = inputQueue;\n this.inputWriter = inputWriter;\n this.inputRouter = inputRouter;\n this.sharedQueueError = sharedQueueError;\n }\n\n override async mount(): Promise<void> {\n await super.mount();\n if (this.didMount) {\n return;\n }\n\n this.didMount = true;\n this.detachBridgeInputListener = this.bridge?.stdin.subscribe((chunk) => {\n this.inputRouter.route(chunk);\n });\n this.detachResizeListener = this.bridge?.subscribeResize((columns, rows, cellWidth, cellHeight) => {\n this.onSceneResize?.({\n sceneId: this.descriptor.id,\n columns,\n rows,\n cellWidth,\n cellHeight,\n });\n });\n\n const initialColumns = Number(this.bridge?.environment.TUIGUI_COLUMNS ?? \"0\") || 0;\n const initialRows = Number(this.bridge?.environment.TUIGUI_ROWS ?? \"0\") || 0;\n if (!this.bridge && initialColumns > 0 && initialRows > 0) {\n this.onSceneResize?.({\n sceneId: this.descriptor.id,\n columns: initialColumns,\n rows: initialRows,\n });\n }\n\n if (!this.bridge) {\n this.writeOutput(\n \"\\r\\nSwiftTUI WASI browser runtime requires a WASI bridge.\\r\\n\"\n );\n return;\n }\n\n const mode = resolveWasmExecutionMode(\n this.executionModePreference,\n resolveWasmEngineCapabilities(),\n this.inputQueue !== undefined && this.inputWriter !== undefined\n );\n if (mode === \"main-thread\") {\n this.startMainThreadExecutor();\n return;\n }\n\n if (!this.inputQueue || !this.inputWriter) {\n if (this.sharedQueueError !== undefined) {\n console.error(\n \"[SwiftTUIWeb] failed to create shared stdin queue\",\n this.sharedQueueError\n );\n }\n this.writeOutput(\n \"\\r\\nSwiftTUI WASI browser runtime requires SharedArrayBuffer-backed stdin. Serve the app with COOP/COEP headers.\\r\\n\"\n );\n return;\n }\n\n this.worker = new Worker(this.workerModuleURL, { type: \"module\" });\n this.worker.addEventListener(\"message\", (event: MessageEvent<WorkerMessage>) => {\n this.handleWorkerMessage(event.data);\n });\n this.worker.addEventListener(\"error\", (event) => {\n this.bridge?.stderr.write(\n `\\nSwiftTUI WASI worker failed: ${event.message || \"unknown worker error\"}\\n`\n );\n });\n\n const environment = { ...this.bridge.environment };\n\n const message: WorkerStartMessage = {\n type: \"start\",\n wasmURL: this.wasmURL.href,\n environment,\n inputQueue: this.inputQueue,\n };\n this.worker.postMessage(message);\n }\n\n override dispose(): void {\n this.detachBridgeInputListener?.();\n this.detachResizeListener?.();\n this.inputWriter?.close();\n this.worker?.terminate();\n this.executor?.dispose();\n super.dispose();\n }\n\n private startMainThreadExecutor(): void {\n const bridge = this.bridge;\n if (!bridge) {\n return;\n }\n const executor = new MainThreadWasmExecutor({\n wasmURL: this.wasmURL.href,\n environment: {\n ...mainThreadStackProfileEnvironmentDefaults(resolveWasmEngineCapabilities()),\n ...bridge.environment,\n },\n onStdout: (chunk) => bridge.stdout.write(chunk),\n onStderr: (chunk) => bridge.stderr.write(chunk),\n onExit: (code) => {\n if (code !== 0) {\n bridge.stderr.write(`\\nSwiftTUI WASI app exited with code ${code}.\\n`);\n }\n },\n onError: (message) => {\n bridge.stderr.write(`\\nFailed to start SwiftTUI WASI app: ${message}\\n`);\n },\n });\n this.executor = executor;\n this.inputRouter.route = (chunk) => executor.sendInput(chunk);\n executor.start();\n }\n\n private handleWorkerMessage(\n message: WorkerMessage\n ): void {\n switch (message.type) {\n case \"stdout\":\n this.bridge?.stdout.write(message.chunk);\n break;\n case \"stderr\":\n this.bridge?.stderr.write(message.chunk);\n break;\n case \"exit\":\n if (message.code !== 0) {\n this.bridge?.stderr.write(`\\nSwiftTUI WASI app exited with code ${message.code}.\\n`);\n }\n break;\n case \"error\":\n this.bridge?.stderr.write(`\\nFailed to start SwiftTUI WASI app: ${message.message}\\n`);\n break;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SharedInputQueueReader } from "./SharedInputQueue.js";
|
|
2
1
|
import { WasiPollScheduler } from "./WasiPollScheduler.js";
|
|
2
|
+
import { SharedInputQueueReader } from "./SharedInputQueue.js";
|
|
3
3
|
import { ConsoleStdout, Fd, WASI, wasi } from "@bjorn3/browser_wasi_shim";
|
|
4
4
|
//#region src/wasi/WasmSceneWorker.ts
|
|
5
5
|
function startWasmSceneWorker() {
|
package/dist/wasi.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
|
|
2
2
|
import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
|
|
3
|
-
import { WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, classifyWasmEngineFamily, collectWasmEngineProbeSignals, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
3
|
+
import { JSPIConstructors, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
4
4
|
import { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink } from "./src/wasi/BrowserWASIBridge.js";
|
|
5
5
|
import { SharedInputQueueBuffers, SharedInputQueueReader, SharedInputQueueWriter, SharedInputReadiness, createSharedInputQueue, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity } from "./src/wasi/SharedInputQueue.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { MainThreadWasmExecutor, MainThreadWasmExecutorOptions } from "./src/wasi/MainThreadWasmExecutor.js";
|
|
7
|
+
import { WasmExecutionMode, WasmExecutionModePreference, WasmSceneResizeEvent, WasmSceneRuntimeFactoryOptions, WasmSceneRuntimeHandle, createWasmSceneRuntimeFactory, resolveWasmExecutionMode } from "./src/wasi/WasmSceneRuntime.js";
|
|
8
|
+
export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, JSPIConstructors, MainThreadWasmExecutor, MainThreadWasmExecutorOptions, SharedInputQueueBuffers, SharedInputQueueReader, SharedInputQueueWriter, SharedInputReadiness, StdIOPipe, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, WasmExecutionMode, WasmExecutionModePreference, WasmSceneResizeEvent, WasmSceneRuntimeFactoryOptions, WasmSceneRuntimeHandle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createSharedInputQueue, createWasmSceneRuntimeFactory, encodeRenderStyleControlMessage, encodeResizeControlMessage, hydrateSharedInputQueue, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, resolveWasmExecutionMode, sharedInputQueueDefaultCapacity, stackProfileEnvironmentDefaults };
|
package/dist/wasi.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
|
|
2
|
-
import { classifyWasmEngineFamily, collectWasmEngineProbeSignals, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
2
|
+
import { classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
3
3
|
import { encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
|
|
4
4
|
import { BrowserWASIBridge } from "./src/wasi/BrowserWASIBridge.js";
|
|
5
|
+
import { MainThreadWasmExecutor } from "./src/wasi/MainThreadWasmExecutor.js";
|
|
5
6
|
import { SharedInputQueueReader, SharedInputQueueWriter, createSharedInputQueue, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity } from "./src/wasi/SharedInputQueue.js";
|
|
6
|
-
import { createWasmSceneRuntimeFactory } from "./src/wasi/WasmSceneRuntime.js";
|
|
7
|
-
export { BrowserWASIBridge, SharedInputQueueReader, SharedInputQueueWriter, StdIOPipe, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createSharedInputQueue, createWasmSceneRuntimeFactory, encodeRenderStyleControlMessage, encodeResizeControlMessage, hydrateSharedInputQueue, resolveWasmEngineCapabilities, sharedInputQueueDefaultCapacity, stackProfileEnvironmentDefaults };
|
|
7
|
+
import { createWasmSceneRuntimeFactory, resolveWasmExecutionMode } from "./src/wasi/WasmSceneRuntime.js";
|
|
8
|
+
export { BrowserWASIBridge, MainThreadWasmExecutor, SharedInputQueueReader, SharedInputQueueWriter, StdIOPipe, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createSharedInputQueue, createWasmSceneRuntimeFactory, encodeRenderStyleControlMessage, encodeResizeControlMessage, hydrateSharedInputQueue, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, resolveWasmExecutionMode, sharedInputQueueDefaultCapacity, stackProfileEnvironmentDefaults };
|