@swifttui/web 0.0.13 → 0.0.15
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 +24 -10
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +2 -0
- package/dist/src/AccessibilityTree.js +156 -0
- package/dist/src/AccessibilityTree.js.map +1 -0
- package/dist/src/BoxDrawingRenderer.js +1106 -0
- package/dist/src/BoxDrawingRenderer.js.map +1 -0
- package/dist/src/WebHostApp.d.ts +41 -0
- package/dist/src/WebHostApp.js +135 -0
- package/dist/src/WebHostApp.js.map +1 -0
- package/dist/src/WebHostSceneManifest.d.ts +18 -0
- package/dist/src/WebHostSceneManifest.js +70 -0
- package/dist/src/WebHostSceneManifest.js.map +1 -0
- package/dist/src/WebHostSceneRuntime.d.ts +112 -0
- package/dist/src/WebHostSceneRuntime.js +651 -0
- package/dist/src/WebHostSceneRuntime.js.map +1 -0
- package/dist/src/WebHostSurfaceTransport.d.ts +166 -0
- package/dist/src/WebHostSurfaceTransport.js +252 -0
- package/dist/src/WebHostSurfaceTransport.js.map +1 -0
- package/dist/src/WebHostTerminalStyle.d.ts +92 -0
- package/dist/src/WebHostTerminalStyle.js +277 -0
- package/dist/src/WebHostTerminalStyle.js.map +1 -0
- package/dist/src/WebHostTestFixtures.d.ts +5 -0
- package/dist/src/WebHostTestFixtures.js +9 -0
- package/dist/src/WebHostTestFixtures.js.map +1 -0
- package/dist/src/WebSocketSceneBridge.d.ts +53 -0
- package/dist/src/WebSocketSceneBridge.js +124 -0
- package/dist/src/WebSocketSceneBridge.js.map +1 -0
- package/dist/src/wasi/BrowserWASIBridge.d.ts +33 -0
- package/dist/src/wasi/BrowserWASIBridge.js +97 -0
- package/dist/src/wasi/BrowserWASIBridge.js.map +1 -0
- package/dist/src/wasi/SharedInputQueue.d.ts +31 -0
- package/dist/src/wasi/SharedInputQueue.js +102 -0
- package/dist/src/wasi/SharedInputQueue.js.map +1 -0
- package/dist/src/wasi/StdIOPipe.d.ts +15 -0
- package/dist/src/wasi/StdIOPipe.js +56 -0
- package/dist/src/wasi/StdIOPipe.js.map +1 -0
- package/dist/src/wasi/WasiPollScheduler.js +114 -0
- package/dist/src/wasi/WasiPollScheduler.js.map +1 -0
- package/dist/src/wasi/WasmSceneRuntime.d.ts +23 -0
- package/dist/src/wasi/WasmSceneRuntime.js +119 -0
- package/dist/src/wasi/WasmSceneRuntime.js.map +1 -0
- package/dist/src/wasi/WasmSceneWorker.d.ts +27 -0
- package/dist/src/wasi/WasmSceneWorker.js +109 -0
- package/dist/src/wasi/WasmSceneWorker.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +2 -0
- package/dist/wasi-worker.d.ts +2 -0
- package/dist/wasi-worker.js +2 -0
- package/dist/wasi.d.ts +6 -0
- package/dist/wasi.js +6 -0
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +2 -0
- package/package.json +49 -18
- package/AGENTS.md +0 -52
- package/cli.ts +0 -168
- package/index.html +0 -50
- package/index.ts +0 -8
- package/manifest.ts +0 -1
- package/src/AccessibilityTree.ts +0 -262
- package/src/BoxDrawingRenderer.ts +0 -585
- package/src/PublicEntrypointBoundary.test.ts +0 -20
- package/src/WebHostApp.test.ts +0 -222
- package/src/WebHostApp.ts +0 -269
- package/src/WebHostSceneManifest.test.ts +0 -38
- package/src/WebHostSceneManifest.ts +0 -156
- package/src/WebHostSceneRuntime.test.ts +0 -1982
- package/src/WebHostSceneRuntime.ts +0 -1142
- package/src/WebHostSurfaceTransport.test.ts +0 -362
- package/src/WebHostSurfaceTransport.ts +0 -691
- package/src/WebHostTerminalStyle.test.ts +0 -123
- package/src/WebHostTerminalStyle.ts +0 -471
- package/src/WebHostTestFixtures.ts +0 -10
- package/src/WebSocketSceneBridge.test.ts +0 -198
- package/src/WebSocketSceneBridge.ts +0 -233
- package/src/browser.ts +0 -59
- package/src/wasi/BrowserWASIBridge.test.ts +0 -168
- package/src/wasi/BrowserWASIBridge.ts +0 -167
- package/src/wasi/SharedInputQueue.test.ts +0 -146
- package/src/wasi/SharedInputQueue.ts +0 -199
- package/src/wasi/StdIOPipe.ts +0 -72
- package/src/wasi/WasiPollScheduler.test.ts +0 -176
- package/src/wasi/WasiPollScheduler.ts +0 -305
- package/src/wasi/WasmSceneRuntime.ts +0 -205
- package/src/wasi/WasmSceneWorker.ts +0 -182
- package/testing.ts +0 -1
- package/tsconfig.json +0 -29
- package/wasi-worker.ts +0 -1
- package/wasi.ts +0 -4
- package/websocket.ts +0 -1
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import { Worker } from "node:worker_threads";
|
|
3
|
-
import { wasi } from "@bjorn3/browser_wasi_shim";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
SharedInputQueueReader,
|
|
7
|
-
type SharedInputQueueBuffers,
|
|
8
|
-
createSharedInputQueue,
|
|
9
|
-
} from "./SharedInputQueue.ts";
|
|
10
|
-
import {
|
|
11
|
-
WasiPollScheduler,
|
|
12
|
-
type WasiPollReadableSource,
|
|
13
|
-
readPollEventsForTesting,
|
|
14
|
-
writeClockSubscriptionForTesting,
|
|
15
|
-
writeFdReadSubscriptionForTesting,
|
|
16
|
-
} from "./WasiPollScheduler.ts";
|
|
17
|
-
|
|
18
|
-
test("scheduler completes a relative monotonic clock subscription", () => {
|
|
19
|
-
const memory = new WebAssembly.Memory({ initial: 1 });
|
|
20
|
-
const view = new DataView(memory.buffer);
|
|
21
|
-
writeClockSubscriptionForTesting(view, 0, {
|
|
22
|
-
userdata: 1n,
|
|
23
|
-
timeoutNanoseconds: 1_000_000n,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const scheduler = new WasiPollScheduler({
|
|
27
|
-
memory: () => memory,
|
|
28
|
-
stdin: closedSource(),
|
|
29
|
-
fallbackPoll: () => wasi.ERRNO_INVAL,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
expect(scheduler.pollOneOff(0, 128, 1, 256)).toBe(wasi.ERRNO_SUCCESS);
|
|
33
|
-
expect(readPollEventsForTesting(view, 128, 1)).toEqual([
|
|
34
|
-
{ userdata: 1n, errno: wasi.ERRNO_SUCCESS, eventtype: wasi.EVENTTYPE_CLOCK },
|
|
35
|
-
]);
|
|
36
|
-
expect(view.getUint32(256, true)).toBe(1);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("scheduler wakes stdin-only poll on stdin readability", async () => {
|
|
40
|
-
const memory = new WebAssembly.Memory({ initial: 1 });
|
|
41
|
-
const view = new DataView(memory.buffer);
|
|
42
|
-
const queue = createSharedInputQueue(8);
|
|
43
|
-
const reader = new SharedInputQueueReader(queue);
|
|
44
|
-
const worker = writeInputFromWorker(queue, "x", 10);
|
|
45
|
-
|
|
46
|
-
writeFdReadSubscriptionForTesting(view, 0, { userdata: 10n, fd: 0 });
|
|
47
|
-
|
|
48
|
-
const scheduler = new WasiPollScheduler({
|
|
49
|
-
memory: () => memory,
|
|
50
|
-
stdin: reader,
|
|
51
|
-
fallbackPoll: () => wasi.ERRNO_INVAL,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
expect(scheduler.pollOneOff(0, 128, 1, 256)).toBe(wasi.ERRNO_SUCCESS);
|
|
56
|
-
expect(readPollEventsForTesting(view, 128, 1)).toEqual([
|
|
57
|
-
{ userdata: 10n, errno: wasi.ERRNO_SUCCESS, eventtype: wasi.EVENTTYPE_FD_READ },
|
|
58
|
-
]);
|
|
59
|
-
expect(view.getBigUint64(128 + 16, true)).toBe(1n);
|
|
60
|
-
expect(view.getUint16(128 + 24, true)).toBe(0);
|
|
61
|
-
expect(view.getUint32(256, true)).toBe(1);
|
|
62
|
-
} finally {
|
|
63
|
-
await worker.terminate();
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("scheduler wakes mixed stdin and clock poll on stdin readability", async () => {
|
|
68
|
-
const memory = new WebAssembly.Memory({ initial: 1 });
|
|
69
|
-
const view = new DataView(memory.buffer);
|
|
70
|
-
const queue = createSharedInputQueue(8);
|
|
71
|
-
const reader = new SharedInputQueueReader(queue);
|
|
72
|
-
const worker = writeInputFromWorker(queue, "x", 10);
|
|
73
|
-
|
|
74
|
-
writeFdReadSubscriptionForTesting(view, 0, { userdata: 10n, fd: 0 });
|
|
75
|
-
writeClockSubscriptionForTesting(view, 48, {
|
|
76
|
-
userdata: 11n,
|
|
77
|
-
timeoutNanoseconds: 500_000_000n,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const scheduler = new WasiPollScheduler({
|
|
81
|
-
memory: () => memory,
|
|
82
|
-
stdin: reader,
|
|
83
|
-
fallbackPoll: () => wasi.ERRNO_INVAL,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
expect(scheduler.pollOneOff(0, 128, 2, 256)).toBe(wasi.ERRNO_SUCCESS);
|
|
88
|
-
expect(readPollEventsForTesting(view, 128, 1)).toEqual([
|
|
89
|
-
{ userdata: 10n, errno: wasi.ERRNO_SUCCESS, eventtype: wasi.EVENTTYPE_FD_READ },
|
|
90
|
-
]);
|
|
91
|
-
expect(view.getUint32(256, true)).toBe(1);
|
|
92
|
-
} finally {
|
|
93
|
-
await worker.terminate();
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("scheduler wakes mixed stdin and clock poll on timeout", () => {
|
|
98
|
-
const memory = new WebAssembly.Memory({ initial: 1 });
|
|
99
|
-
const view = new DataView(memory.buffer);
|
|
100
|
-
const queue = createSharedInputQueue(8);
|
|
101
|
-
const reader = new SharedInputQueueReader(queue);
|
|
102
|
-
|
|
103
|
-
writeFdReadSubscriptionForTesting(view, 0, { userdata: 10n, fd: 0 });
|
|
104
|
-
writeClockSubscriptionForTesting(view, 48, {
|
|
105
|
-
userdata: 11n,
|
|
106
|
-
timeoutNanoseconds: 1_000_000n,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const scheduler = new WasiPollScheduler({
|
|
110
|
-
memory: () => memory,
|
|
111
|
-
stdin: reader,
|
|
112
|
-
fallbackPoll: () => wasi.ERRNO_INVAL,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(scheduler.pollOneOff(0, 128, 2, 256)).toBe(wasi.ERRNO_SUCCESS);
|
|
116
|
-
expect(readPollEventsForTesting(view, 128, 1)).toEqual([
|
|
117
|
-
{ userdata: 11n, errno: wasi.ERRNO_SUCCESS, eventtype: wasi.EVENTTYPE_CLOCK },
|
|
118
|
-
]);
|
|
119
|
-
expect(view.getUint32(256, true)).toBe(1);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("scheduler reports closed stdin as readable hangup", () => {
|
|
123
|
-
const memory = new WebAssembly.Memory({ initial: 1 });
|
|
124
|
-
const view = new DataView(memory.buffer);
|
|
125
|
-
|
|
126
|
-
writeFdReadSubscriptionForTesting(view, 0, { userdata: 10n, fd: 0 });
|
|
127
|
-
|
|
128
|
-
const scheduler = new WasiPollScheduler({
|
|
129
|
-
memory: () => memory,
|
|
130
|
-
stdin: closedSource(),
|
|
131
|
-
fallbackPoll: () => wasi.ERRNO_INVAL,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(scheduler.pollOneOff(0, 128, 1, 256)).toBe(wasi.ERRNO_SUCCESS);
|
|
135
|
-
expect(readPollEventsForTesting(view, 128, 1)).toEqual([
|
|
136
|
-
{ userdata: 10n, errno: wasi.ERRNO_SUCCESS, eventtype: wasi.EVENTTYPE_FD_READ },
|
|
137
|
-
]);
|
|
138
|
-
expect(view.getBigUint64(128 + 16, true)).toBe(0n);
|
|
139
|
-
expect(view.getUint16(128 + 24, true)).toBe(wasi.EVENTRWFLAGS_FD_READWRITE_HANGUP);
|
|
140
|
-
expect(view.getUint32(256, true)).toBe(1);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
function closedSource(): WasiPollReadableSource {
|
|
144
|
-
return {
|
|
145
|
-
availableBytes: () => 0,
|
|
146
|
-
isClosed: () => true,
|
|
147
|
-
waitForReadable: () => "closed",
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function writeInputFromWorker(
|
|
152
|
-
queue: SharedInputQueueBuffers,
|
|
153
|
-
text: string,
|
|
154
|
-
delayMilliseconds: number
|
|
155
|
-
): Worker {
|
|
156
|
-
return new Worker(`
|
|
157
|
-
const { workerData } = require("node:worker_threads");
|
|
158
|
-
const control = new Int32Array(workerData.controlBuffer);
|
|
159
|
-
const data = new Uint8Array(workerData.dataBuffer);
|
|
160
|
-
const bytes = new TextEncoder().encode(workerData.text);
|
|
161
|
-
setTimeout(() => {
|
|
162
|
-
const writeIndex = Atomics.load(control, 1);
|
|
163
|
-
data.set(bytes, writeIndex % data.length);
|
|
164
|
-
Atomics.store(control, 1, writeIndex + bytes.length);
|
|
165
|
-
Atomics.notify(control, 1);
|
|
166
|
-
}, workerData.delayMilliseconds);
|
|
167
|
-
`, {
|
|
168
|
-
eval: true,
|
|
169
|
-
workerData: {
|
|
170
|
-
controlBuffer: queue.controlBuffer,
|
|
171
|
-
dataBuffer: queue.dataBuffer,
|
|
172
|
-
delayMilliseconds,
|
|
173
|
-
text,
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
}
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { wasi } from "@bjorn3/browser_wasi_shim";
|
|
2
|
-
|
|
3
|
-
import type { SharedInputReadiness } from "./SharedInputQueue.ts";
|
|
4
|
-
|
|
5
|
-
const subscriptionByteLength = 48;
|
|
6
|
-
const eventByteLength = 32;
|
|
7
|
-
const maximumAtomicsWaitMilliseconds = 2_147_483_647;
|
|
8
|
-
|
|
9
|
-
interface ClockSubscription {
|
|
10
|
-
readonly type: "clock";
|
|
11
|
-
readonly userdata: bigint;
|
|
12
|
-
readonly clockid: number;
|
|
13
|
-
readonly deadlineMilliseconds: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface FdReadSubscription {
|
|
17
|
-
readonly type: "fdRead";
|
|
18
|
-
readonly userdata: bigint;
|
|
19
|
-
readonly fd: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type SupportedSubscription = ClockSubscription | FdReadSubscription;
|
|
23
|
-
|
|
24
|
-
export interface WasiPollReadableSource {
|
|
25
|
-
availableBytes(): number;
|
|
26
|
-
isClosed(): boolean;
|
|
27
|
-
waitForReadable(timeoutMilliseconds?: number): SharedInputReadiness;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface WasiPollSchedulerOptions {
|
|
31
|
-
memory(): WebAssembly.Memory | undefined;
|
|
32
|
-
stdin: WasiPollReadableSource;
|
|
33
|
-
fallbackPoll(
|
|
34
|
-
inPtr: number,
|
|
35
|
-
outPtr: number,
|
|
36
|
-
nsubscriptions: number,
|
|
37
|
-
neventsPtr?: number
|
|
38
|
-
): number;
|
|
39
|
-
nowMilliseconds?(): number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class WasiPollScheduler {
|
|
43
|
-
private readonly memory: WasiPollSchedulerOptions["memory"];
|
|
44
|
-
private readonly stdin: WasiPollReadableSource;
|
|
45
|
-
private readonly fallbackPoll: WasiPollSchedulerOptions["fallbackPoll"];
|
|
46
|
-
private readonly nowMilliseconds: () => number;
|
|
47
|
-
private readonly waitBuffer = new Int32Array(
|
|
48
|
-
new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
constructor(options: WasiPollSchedulerOptions) {
|
|
52
|
-
this.memory = options.memory;
|
|
53
|
-
this.stdin = options.stdin;
|
|
54
|
-
this.fallbackPoll = options.fallbackPoll;
|
|
55
|
-
this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
pollOneOff(
|
|
59
|
-
inPtr: number,
|
|
60
|
-
outPtr: number,
|
|
61
|
-
nsubscriptions: number,
|
|
62
|
-
neventsPtr?: number
|
|
63
|
-
): number {
|
|
64
|
-
const memory = this.memory();
|
|
65
|
-
if (!memory || nsubscriptions <= 0) {
|
|
66
|
-
return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const view = new DataView(memory.buffer);
|
|
70
|
-
const subscriptions = readSubscriptions(
|
|
71
|
-
view,
|
|
72
|
-
inPtr,
|
|
73
|
-
nsubscriptions,
|
|
74
|
-
this.nowMilliseconds()
|
|
75
|
-
);
|
|
76
|
-
if (subscriptions === undefined) {
|
|
77
|
-
return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
while (true) {
|
|
81
|
-
const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());
|
|
82
|
-
if (ready.length > 0) {
|
|
83
|
-
writeEvents(view, outPtr, ready, this.stdin);
|
|
84
|
-
if (neventsPtr !== undefined) {
|
|
85
|
-
view.setUint32(neventsPtr, ready.length, true);
|
|
86
|
-
}
|
|
87
|
-
return wasi.ERRNO_SUCCESS;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const timeoutMilliseconds = shortestClockTimeoutMilliseconds(
|
|
91
|
-
subscriptions,
|
|
92
|
-
this.nowMilliseconds()
|
|
93
|
-
);
|
|
94
|
-
if (hasFdReadSubscription(subscriptions)) {
|
|
95
|
-
this.stdin.waitForReadable(timeoutMilliseconds);
|
|
96
|
-
} else if (timeoutMilliseconds !== undefined) {
|
|
97
|
-
Atomics.wait(
|
|
98
|
-
this.waitBuffer,
|
|
99
|
-
0,
|
|
100
|
-
0,
|
|
101
|
-
Math.min(timeoutMilliseconds, maximumAtomicsWaitMilliseconds)
|
|
102
|
-
);
|
|
103
|
-
} else {
|
|
104
|
-
return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function readSubscriptions(
|
|
111
|
-
view: DataView,
|
|
112
|
-
inPtr: number,
|
|
113
|
-
nsubscriptions: number,
|
|
114
|
-
nowMilliseconds: number
|
|
115
|
-
): SupportedSubscription[] | undefined {
|
|
116
|
-
const subscriptions: SupportedSubscription[] = [];
|
|
117
|
-
for (let index = 0; index < nsubscriptions; index += 1) {
|
|
118
|
-
const subscription = wasi.Subscription.read_bytes(
|
|
119
|
-
view,
|
|
120
|
-
inPtr + index * subscriptionByteLength
|
|
121
|
-
);
|
|
122
|
-
switch (subscription.eventtype) {
|
|
123
|
-
case wasi.EVENTTYPE_CLOCK:
|
|
124
|
-
if (!isSupportedClockId(subscription.clockid)) {
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
subscriptions.push({
|
|
128
|
-
type: "clock",
|
|
129
|
-
userdata: subscription.userdata,
|
|
130
|
-
clockid: subscription.clockid,
|
|
131
|
-
deadlineMilliseconds: clockDeadlineMilliseconds(subscription, nowMilliseconds),
|
|
132
|
-
});
|
|
133
|
-
break;
|
|
134
|
-
case wasi.EVENTTYPE_FD_READ:
|
|
135
|
-
if (subscription.clockid !== wasi.FD_STDIN) {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
subscriptions.push({
|
|
139
|
-
type: "fdRead",
|
|
140
|
-
userdata: subscription.userdata,
|
|
141
|
-
fd: subscription.clockid,
|
|
142
|
-
});
|
|
143
|
-
break;
|
|
144
|
-
default:
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return subscriptions;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function isSupportedClockId(
|
|
152
|
-
clockid: number
|
|
153
|
-
): boolean {
|
|
154
|
-
return clockid === wasi.CLOCKID_MONOTONIC || clockid === wasi.CLOCKID_REALTIME;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function shortestClockTimeoutMilliseconds(
|
|
158
|
-
subscriptions: readonly SupportedSubscription[],
|
|
159
|
-
nowMilliseconds: number
|
|
160
|
-
): number | undefined {
|
|
161
|
-
let timeoutMilliseconds: number | undefined;
|
|
162
|
-
for (const subscription of subscriptions) {
|
|
163
|
-
if (subscription.type !== "clock") {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
const remaining = clockRemainingMilliseconds(subscription, nowMilliseconds);
|
|
167
|
-
timeoutMilliseconds = timeoutMilliseconds === undefined
|
|
168
|
-
? remaining
|
|
169
|
-
: Math.min(timeoutMilliseconds, remaining);
|
|
170
|
-
}
|
|
171
|
-
return timeoutMilliseconds;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function readySubscriptions(
|
|
175
|
-
subscriptions: readonly SupportedSubscription[],
|
|
176
|
-
stdin: WasiPollReadableSource,
|
|
177
|
-
nowMilliseconds: number
|
|
178
|
-
): SupportedSubscription[] {
|
|
179
|
-
return subscriptions.filter((subscription) => {
|
|
180
|
-
switch (subscription.type) {
|
|
181
|
-
case "clock":
|
|
182
|
-
return clockRemainingMilliseconds(subscription, nowMilliseconds) <= 0;
|
|
183
|
-
case "fdRead":
|
|
184
|
-
return stdin.availableBytes() > 0 || stdin.isClosed();
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function hasFdReadSubscription(
|
|
190
|
-
subscriptions: readonly SupportedSubscription[]
|
|
191
|
-
): boolean {
|
|
192
|
-
return subscriptions.some((subscription) => subscription.type === "fdRead");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function clockRemainingMilliseconds(
|
|
196
|
-
subscription: ClockSubscription,
|
|
197
|
-
nowMilliseconds: number
|
|
198
|
-
): number {
|
|
199
|
-
return Math.max(0, subscription.deadlineMilliseconds - nowMillisecondsForClock(
|
|
200
|
-
subscription.clockid,
|
|
201
|
-
nowMilliseconds
|
|
202
|
-
));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function clockDeadlineMilliseconds(
|
|
206
|
-
subscription: wasi.Subscription,
|
|
207
|
-
nowMilliseconds: number
|
|
208
|
-
): number {
|
|
209
|
-
if ((subscription.flags & wasi.SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME) !== 0) {
|
|
210
|
-
return Number(subscription.timeout) / 1_000_000;
|
|
211
|
-
}
|
|
212
|
-
return nowMillisecondsForClock(subscription.clockid, nowMilliseconds)
|
|
213
|
-
+ Number(subscription.timeout) / 1_000_000;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function nowMillisecondsForClock(
|
|
217
|
-
clockid: number,
|
|
218
|
-
nowMilliseconds: number
|
|
219
|
-
): number {
|
|
220
|
-
if (clockid === wasi.CLOCKID_REALTIME) {
|
|
221
|
-
return Date.now();
|
|
222
|
-
}
|
|
223
|
-
return nowMilliseconds;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function writeEvents(
|
|
227
|
-
view: DataView,
|
|
228
|
-
outPtr: number,
|
|
229
|
-
subscriptions: readonly SupportedSubscription[],
|
|
230
|
-
stdin: WasiPollReadableSource
|
|
231
|
-
): void {
|
|
232
|
-
subscriptions.forEach((subscription, index) => {
|
|
233
|
-
const eventtype = subscription.type === "clock"
|
|
234
|
-
? wasi.EVENTTYPE_CLOCK
|
|
235
|
-
: wasi.EVENTTYPE_FD_READ;
|
|
236
|
-
const offset = outPtr + index * eventByteLength;
|
|
237
|
-
new wasi.Event(
|
|
238
|
-
subscription.userdata,
|
|
239
|
-
wasi.ERRNO_SUCCESS,
|
|
240
|
-
eventtype
|
|
241
|
-
).write_bytes(view, offset);
|
|
242
|
-
if (subscription.type === "fdRead") {
|
|
243
|
-
const availableBytes = Math.max(0, stdin.availableBytes());
|
|
244
|
-
view.setBigUint64(offset + 16, BigInt(availableBytes), true);
|
|
245
|
-
if (availableBytes === 0 && stdin.isClosed()) {
|
|
246
|
-
view.setUint16(offset + 24, wasi.EVENTRWFLAGS_FD_READWRITE_HANGUP, true);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function writeClockSubscriptionForTesting(
|
|
253
|
-
view: DataView,
|
|
254
|
-
offset: number,
|
|
255
|
-
subscription: {
|
|
256
|
-
userdata: bigint;
|
|
257
|
-
timeoutNanoseconds: bigint;
|
|
258
|
-
clockid?: number;
|
|
259
|
-
flags?: number;
|
|
260
|
-
}
|
|
261
|
-
): void {
|
|
262
|
-
clearRecord(view, offset, subscriptionByteLength);
|
|
263
|
-
view.setBigUint64(offset, subscription.userdata, true);
|
|
264
|
-
view.setUint8(offset + 8, wasi.EVENTTYPE_CLOCK);
|
|
265
|
-
view.setUint32(offset + 16, subscription.clockid ?? wasi.CLOCKID_MONOTONIC, true);
|
|
266
|
-
view.setBigUint64(offset + 24, subscription.timeoutNanoseconds, true);
|
|
267
|
-
view.setUint16(offset + 36, subscription.flags ?? 0, true);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function writeFdReadSubscriptionForTesting(
|
|
271
|
-
view: DataView,
|
|
272
|
-
offset: number,
|
|
273
|
-
subscription: {
|
|
274
|
-
userdata: bigint;
|
|
275
|
-
fd: number;
|
|
276
|
-
}
|
|
277
|
-
): void {
|
|
278
|
-
clearRecord(view, offset, subscriptionByteLength);
|
|
279
|
-
view.setBigUint64(offset, subscription.userdata, true);
|
|
280
|
-
view.setUint8(offset + 8, wasi.EVENTTYPE_FD_READ);
|
|
281
|
-
view.setUint32(offset + 16, subscription.fd, true);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export function readPollEventsForTesting(
|
|
285
|
-
view: DataView,
|
|
286
|
-
offset: number,
|
|
287
|
-
count: number
|
|
288
|
-
): Array<{ userdata: bigint; errno: number; eventtype: number }> {
|
|
289
|
-
return Array.from({ length: count }, (_, index) => {
|
|
290
|
-
const eventOffset = offset + index * eventByteLength;
|
|
291
|
-
return {
|
|
292
|
-
userdata: view.getBigUint64(eventOffset, true),
|
|
293
|
-
errno: view.getUint16(eventOffset + 8, true),
|
|
294
|
-
eventtype: view.getUint8(eventOffset + 10),
|
|
295
|
-
};
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function clearRecord(
|
|
300
|
-
view: DataView,
|
|
301
|
-
offset: number,
|
|
302
|
-
byteLength: number
|
|
303
|
-
): void {
|
|
304
|
-
new Uint8Array(view.buffer, offset, byteLength).fill(0);
|
|
305
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
WebHostSceneRuntime,
|
|
3
|
-
type WebHostSceneRuntimeOptions,
|
|
4
|
-
} from "../WebHostSceneRuntime.ts";
|
|
5
|
-
import {
|
|
6
|
-
encodeResizeControlMessage,
|
|
7
|
-
type BrowserWASIBridge,
|
|
8
|
-
} from "./BrowserWASIBridge.ts";
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
SharedInputQueueWriter,
|
|
12
|
-
createSharedInputQueue,
|
|
13
|
-
type SharedInputQueueBuffers,
|
|
14
|
-
} from "./SharedInputQueue.ts";
|
|
15
|
-
|
|
16
|
-
const workerModuleURL = new URL("./wasm-scene-worker.js", import.meta.url);
|
|
17
|
-
|
|
18
|
-
interface WorkerStartMessage {
|
|
19
|
-
type: "start";
|
|
20
|
-
wasmURL: string;
|
|
21
|
-
environment: Record<string, string>;
|
|
22
|
-
inputQueue: SharedInputQueueBuffers;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface WorkerOutputMessage {
|
|
26
|
-
type: "stdout" | "stderr";
|
|
27
|
-
chunk: Uint8Array;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface WorkerExitMessage {
|
|
31
|
-
type: "exit";
|
|
32
|
-
code: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface WorkerErrorMessage {
|
|
36
|
-
type: "error";
|
|
37
|
-
message: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
type WorkerMessage = WorkerOutputMessage | WorkerExitMessage | WorkerErrorMessage;
|
|
41
|
-
|
|
42
|
-
export interface WasmSceneResizeEvent {
|
|
43
|
-
sceneId: string;
|
|
44
|
-
columns: number;
|
|
45
|
-
rows: number;
|
|
46
|
-
cellWidth?: number;
|
|
47
|
-
cellHeight?: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface WasmSceneRuntimeHandle {
|
|
51
|
-
readonly descriptor: WebHostSceneRuntime["descriptor"];
|
|
52
|
-
sendInput(chunk: Uint8Array): void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface WasmSceneRuntimeFactoryOptions {
|
|
56
|
-
onSceneResize?(event: WasmSceneResizeEvent): void;
|
|
57
|
-
onRuntimeCreated?(runtime: WasmSceneRuntimeHandle): void;
|
|
58
|
-
workerModuleURL?: string | URL;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function createWasmSceneRuntimeFactory(
|
|
62
|
-
wasmURL: URL,
|
|
63
|
-
factoryOptions: WasmSceneRuntimeFactoryOptions = {}
|
|
64
|
-
): (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime {
|
|
65
|
-
return (options) => {
|
|
66
|
-
const runtime = new WasmSceneRuntime(options, wasmURL, factoryOptions);
|
|
67
|
-
factoryOptions.onRuntimeCreated?.(runtime);
|
|
68
|
-
return runtime;
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
class WasmSceneRuntime extends WebHostSceneRuntime {
|
|
73
|
-
private readonly bridge?: BrowserWASIBridge;
|
|
74
|
-
private readonly wasmURL: URL;
|
|
75
|
-
private readonly onSceneResize?: (event: WasmSceneResizeEvent) => void;
|
|
76
|
-
private readonly workerModuleURL: string | URL;
|
|
77
|
-
private readonly inputQueue?: SharedInputQueueBuffers;
|
|
78
|
-
private readonly inputWriter?: SharedInputQueueWriter;
|
|
79
|
-
|
|
80
|
-
private detachBridgeInputListener?: () => void;
|
|
81
|
-
private detachResizeListener?: () => void;
|
|
82
|
-
private worker?: Worker;
|
|
83
|
-
private didMount = false;
|
|
84
|
-
|
|
85
|
-
constructor(
|
|
86
|
-
options: WebHostSceneRuntimeOptions,
|
|
87
|
-
wasmURL: URL,
|
|
88
|
-
factoryOptions: WasmSceneRuntimeFactoryOptions
|
|
89
|
-
) {
|
|
90
|
-
let inputQueue: SharedInputQueueBuffers | undefined;
|
|
91
|
-
let inputWriter: SharedInputQueueWriter | undefined;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
inputQueue = createSharedInputQueue();
|
|
95
|
-
inputWriter = new SharedInputQueueWriter(inputQueue);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
console.error("[SwiftTUIWeb] failed to create shared stdin queue", error);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
super({
|
|
101
|
-
...options,
|
|
102
|
-
onInput: (chunk) => {
|
|
103
|
-
try {
|
|
104
|
-
inputWriter?.write(chunk);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
console.error("[SwiftTUIWeb] failed to enqueue terminal input", error);
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
this.bridge = options.bridge;
|
|
112
|
-
this.wasmURL = wasmURL;
|
|
113
|
-
this.onSceneResize = factoryOptions.onSceneResize;
|
|
114
|
-
this.workerModuleURL = factoryOptions.workerModuleURL ?? workerModuleURL;
|
|
115
|
-
this.inputQueue = inputQueue;
|
|
116
|
-
this.inputWriter = inputWriter;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
override async mount(): Promise<void> {
|
|
120
|
-
await super.mount();
|
|
121
|
-
if (this.didMount) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
this.didMount = true;
|
|
126
|
-
this.detachBridgeInputListener = this.bridge?.stdin.subscribe((chunk) => {
|
|
127
|
-
this.inputWriter?.write(chunk);
|
|
128
|
-
});
|
|
129
|
-
this.detachResizeListener = this.bridge?.subscribeResize((columns, rows, cellWidth, cellHeight) => {
|
|
130
|
-
this.onSceneResize?.({
|
|
131
|
-
sceneId: this.descriptor.id,
|
|
132
|
-
columns,
|
|
133
|
-
rows,
|
|
134
|
-
cellWidth,
|
|
135
|
-
cellHeight,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const initialColumns = Number(this.bridge?.environment.TUIGUI_COLUMNS ?? "0") || 0;
|
|
140
|
-
const initialRows = Number(this.bridge?.environment.TUIGUI_ROWS ?? "0") || 0;
|
|
141
|
-
if (!this.bridge && initialColumns > 0 && initialRows > 0) {
|
|
142
|
-
this.onSceneResize?.({
|
|
143
|
-
sceneId: this.descriptor.id,
|
|
144
|
-
columns: initialColumns,
|
|
145
|
-
rows: initialRows,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!this.inputQueue || !this.inputWriter || !this.bridge) {
|
|
150
|
-
this.writeOutput(
|
|
151
|
-
"\r\nSwiftTUI WASI browser runtime requires SharedArrayBuffer-backed stdin. Serve the app with COOP/COEP headers.\r\n"
|
|
152
|
-
);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
this.worker = new Worker(this.workerModuleURL, { type: "module" });
|
|
157
|
-
this.worker.addEventListener("message", (event: MessageEvent<WorkerMessage>) => {
|
|
158
|
-
this.handleWorkerMessage(event.data);
|
|
159
|
-
});
|
|
160
|
-
this.worker.addEventListener("error", (event) => {
|
|
161
|
-
this.bridge?.stderr.write(
|
|
162
|
-
`\nSwiftTUI WASI worker failed: ${event.message || "unknown worker error"}\n`
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const environment = { ...this.bridge.environment };
|
|
167
|
-
|
|
168
|
-
const message: WorkerStartMessage = {
|
|
169
|
-
type: "start",
|
|
170
|
-
wasmURL: this.wasmURL.href,
|
|
171
|
-
environment,
|
|
172
|
-
inputQueue: this.inputQueue,
|
|
173
|
-
};
|
|
174
|
-
this.worker.postMessage(message);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
override dispose(): void {
|
|
178
|
-
this.detachBridgeInputListener?.();
|
|
179
|
-
this.detachResizeListener?.();
|
|
180
|
-
this.inputWriter?.close();
|
|
181
|
-
this.worker?.terminate();
|
|
182
|
-
super.dispose();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private handleWorkerMessage(
|
|
186
|
-
message: WorkerMessage
|
|
187
|
-
): void {
|
|
188
|
-
switch (message.type) {
|
|
189
|
-
case "stdout":
|
|
190
|
-
this.bridge?.stdout.write(message.chunk);
|
|
191
|
-
break;
|
|
192
|
-
case "stderr":
|
|
193
|
-
this.bridge?.stderr.write(message.chunk);
|
|
194
|
-
break;
|
|
195
|
-
case "exit":
|
|
196
|
-
if (message.code !== 0) {
|
|
197
|
-
this.bridge?.stderr.write(`\nSwiftTUI WASI app exited with code ${message.code}.\n`);
|
|
198
|
-
}
|
|
199
|
-
break;
|
|
200
|
-
case "error":
|
|
201
|
-
this.bridge?.stderr.write(`\nFailed to start SwiftTUI WASI app: ${message.message}\n`);
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|