@swifttui/web 0.0.14 → 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.
Files changed (92) hide show
  1. package/README.md +24 -10
  2. package/dist/index.d.ts +9 -0
  3. package/dist/index.js +9 -0
  4. package/dist/manifest.d.ts +2 -0
  5. package/dist/manifest.js +2 -0
  6. package/dist/src/AccessibilityTree.js +156 -0
  7. package/dist/src/AccessibilityTree.js.map +1 -0
  8. package/dist/src/BoxDrawingRenderer.js +1106 -0
  9. package/dist/src/BoxDrawingRenderer.js.map +1 -0
  10. package/dist/src/WebHostApp.d.ts +41 -0
  11. package/dist/src/WebHostApp.js +135 -0
  12. package/dist/src/WebHostApp.js.map +1 -0
  13. package/dist/src/WebHostSceneManifest.d.ts +18 -0
  14. package/dist/src/WebHostSceneManifest.js +70 -0
  15. package/dist/src/WebHostSceneManifest.js.map +1 -0
  16. package/dist/src/WebHostSceneRuntime.d.ts +112 -0
  17. package/dist/src/WebHostSceneRuntime.js +651 -0
  18. package/dist/src/WebHostSceneRuntime.js.map +1 -0
  19. package/dist/src/WebHostSurfaceTransport.d.ts +166 -0
  20. package/dist/src/WebHostSurfaceTransport.js +252 -0
  21. package/dist/src/WebHostSurfaceTransport.js.map +1 -0
  22. package/dist/src/WebHostTerminalStyle.d.ts +92 -0
  23. package/dist/src/WebHostTerminalStyle.js +277 -0
  24. package/dist/src/WebHostTerminalStyle.js.map +1 -0
  25. package/dist/src/WebHostTestFixtures.d.ts +5 -0
  26. package/dist/src/WebHostTestFixtures.js +9 -0
  27. package/dist/src/WebHostTestFixtures.js.map +1 -0
  28. package/dist/src/WebSocketSceneBridge.d.ts +53 -0
  29. package/dist/src/WebSocketSceneBridge.js +124 -0
  30. package/dist/src/WebSocketSceneBridge.js.map +1 -0
  31. package/dist/src/wasi/BrowserWASIBridge.d.ts +33 -0
  32. package/dist/src/wasi/BrowserWASIBridge.js +97 -0
  33. package/dist/src/wasi/BrowserWASIBridge.js.map +1 -0
  34. package/dist/src/wasi/SharedInputQueue.d.ts +31 -0
  35. package/dist/src/wasi/SharedInputQueue.js +102 -0
  36. package/dist/src/wasi/SharedInputQueue.js.map +1 -0
  37. package/dist/src/wasi/StdIOPipe.d.ts +15 -0
  38. package/dist/src/wasi/StdIOPipe.js +56 -0
  39. package/dist/src/wasi/StdIOPipe.js.map +1 -0
  40. package/dist/src/wasi/WasiPollScheduler.js +114 -0
  41. package/dist/src/wasi/WasiPollScheduler.js.map +1 -0
  42. package/dist/src/wasi/WasmSceneRuntime.d.ts +23 -0
  43. package/dist/src/wasi/WasmSceneRuntime.js +119 -0
  44. package/dist/src/wasi/WasmSceneRuntime.js.map +1 -0
  45. package/dist/src/wasi/WasmSceneWorker.d.ts +27 -0
  46. package/dist/src/wasi/WasmSceneWorker.js +109 -0
  47. package/dist/src/wasi/WasmSceneWorker.js.map +1 -0
  48. package/dist/testing.d.ts +2 -0
  49. package/dist/testing.js +2 -0
  50. package/dist/wasi-worker.d.ts +2 -0
  51. package/dist/wasi-worker.js +2 -0
  52. package/dist/wasi.d.ts +6 -0
  53. package/dist/wasi.js +6 -0
  54. package/dist/websocket.d.ts +2 -0
  55. package/dist/websocket.js +2 -0
  56. package/package.json +49 -18
  57. package/AGENTS.md +0 -52
  58. package/cli.ts +0 -168
  59. package/index.html +0 -50
  60. package/index.ts +0 -8
  61. package/manifest.ts +0 -1
  62. package/src/AccessibilityTree.ts +0 -262
  63. package/src/BoxDrawingRenderer.ts +0 -585
  64. package/src/PublicEntrypointBoundary.test.ts +0 -20
  65. package/src/WebHostApp.test.ts +0 -222
  66. package/src/WebHostApp.ts +0 -269
  67. package/src/WebHostSceneManifest.test.ts +0 -38
  68. package/src/WebHostSceneManifest.ts +0 -156
  69. package/src/WebHostSceneRuntime.test.ts +0 -1982
  70. package/src/WebHostSceneRuntime.ts +0 -1142
  71. package/src/WebHostSurfaceTransport.test.ts +0 -362
  72. package/src/WebHostSurfaceTransport.ts +0 -691
  73. package/src/WebHostTerminalStyle.test.ts +0 -123
  74. package/src/WebHostTerminalStyle.ts +0 -471
  75. package/src/WebHostTestFixtures.ts +0 -10
  76. package/src/WebSocketSceneBridge.test.ts +0 -198
  77. package/src/WebSocketSceneBridge.ts +0 -233
  78. package/src/browser.ts +0 -59
  79. package/src/wasi/BrowserWASIBridge.test.ts +0 -168
  80. package/src/wasi/BrowserWASIBridge.ts +0 -167
  81. package/src/wasi/SharedInputQueue.test.ts +0 -146
  82. package/src/wasi/SharedInputQueue.ts +0 -199
  83. package/src/wasi/StdIOPipe.ts +0 -72
  84. package/src/wasi/WasiPollScheduler.test.ts +0 -176
  85. package/src/wasi/WasiPollScheduler.ts +0 -305
  86. package/src/wasi/WasmSceneRuntime.ts +0 -205
  87. package/src/wasi/WasmSceneWorker.ts +0 -182
  88. package/testing.ts +0 -1
  89. package/tsconfig.json +0 -29
  90. package/wasi-worker.ts +0 -1
  91. package/wasi.ts +0 -4
  92. 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
- }