@swifttui/web 0.0.14 → 0.0.16

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,167 +0,0 @@
1
- import { StdIOPipe } from "./StdIOPipe.ts";
2
- import {
3
- encodeWebHostTerminalRenderStyleBase64,
4
- type WebHostTerminalStyle,
5
- } from "../WebHostTerminalStyle.ts";
6
- import {
7
- WebHostOutputDecoder,
8
- encodeRenderStyleControlMessage,
9
- encodeResizeControlMessage,
10
- type WebHostOutputSink,
11
- } from "../WebHostSurfaceTransport.ts";
12
-
13
- export interface BrowserWASIBridgeOptions {
14
- sceneId: string;
15
- columns: number;
16
- rows: number;
17
- environment?: Record<string, string>;
18
- renderStyle?: WebHostTerminalStyle;
19
- }
20
-
21
- export type BrowserWASIOutputSink = WebHostOutputSink;
22
-
23
- export class BrowserWASIBridge {
24
- readonly stdin = new StdIOPipe();
25
- readonly stdout = new StdIOPipe();
26
- readonly stderr = new StdIOPipe();
27
- readonly environment: Record<string, string>;
28
-
29
- private detachStdout?: () => void;
30
- private detachStderr?: () => void;
31
- private readonly resizeListeners = new Set<(
32
- columns: number,
33
- rows: number,
34
- cellWidth?: number,
35
- cellHeight?: number
36
- ) => void>();
37
- private latestResize: {
38
- columns: number;
39
- rows: number;
40
- cellWidth?: number;
41
- cellHeight?: number;
42
- };
43
-
44
- constructor(options: BrowserWASIBridgeOptions) {
45
- this.environment = {
46
- TUIGUI_MODE: "browser",
47
- TUIGUI_TRANSPORT: "surface",
48
- TUIGUI_SURFACE_DELTA: "1",
49
- TUIGUI_SCENE: options.sceneId,
50
- TUIGUI_COLUMNS: String(Math.max(1, options.columns)),
51
- TUIGUI_ROWS: String(Math.max(1, options.rows)),
52
- ...options.environment,
53
- ...(options.renderStyle
54
- ? {
55
- TUIGUI_RENDER_STYLE: encodeWebHostTerminalRenderStyleBase64(
56
- options.renderStyle
57
- ),
58
- }
59
- : {}),
60
- };
61
- this.latestResize = {
62
- columns: Math.max(1, options.columns),
63
- rows: Math.max(1, options.rows),
64
- };
65
- }
66
-
67
- bindOutput(
68
- sink: BrowserWASIOutputSink
69
- ): void {
70
- this.detachStdout?.();
71
- this.detachStderr?.();
72
- const decoder = new WebHostOutputDecoder();
73
- this.detachStdout = this.stdout.subscribe((chunk) => {
74
- for (const record of decoder.feed(chunk)) {
75
- switch (record.type) {
76
- case "surface":
77
- sink.presentSurface(record.frame);
78
- break;
79
- case "clipboard":
80
- void sink.writeClipboard?.(record.text);
81
- break;
82
- case "runtimeIssue":
83
- sink.notifyRuntimeIssue?.(record.issue);
84
- break;
85
- case "frameDiagnostic":
86
- sink.recordFrameDiagnostic?.(record.diagnostic);
87
- break;
88
- case "text":
89
- sink.writeOutput?.(record.text);
90
- break;
91
- }
92
- }
93
- });
94
- this.detachStderr = this.stderr.subscribe((chunk) => {
95
- sink.writeError?.(new TextDecoder().decode(chunk));
96
- });
97
- }
98
-
99
- resize(
100
- columns: number,
101
- rows: number,
102
- cellWidth?: number,
103
- cellHeight?: number
104
- ): void {
105
- const normalizedColumns = Math.max(1, columns);
106
- const normalizedRows = Math.max(1, rows);
107
- this.environment.TUIGUI_COLUMNS = String(normalizedColumns);
108
- this.environment.TUIGUI_ROWS = String(normalizedRows);
109
- this.latestResize = {
110
- columns: normalizedColumns,
111
- rows: normalizedRows,
112
- cellWidth,
113
- cellHeight,
114
- };
115
- this.stdin.write(encodeResizeControlMessage(columns, rows, cellWidth, cellHeight));
116
- for (const listener of this.resizeListeners) {
117
- listener(normalizedColumns, normalizedRows, cellWidth, cellHeight);
118
- }
119
- }
120
-
121
- updateRenderStyle(
122
- style: WebHostTerminalStyle
123
- ): void {
124
- this.environment.TUIGUI_RENDER_STYLE = encodeWebHostTerminalRenderStyleBase64(style);
125
- this.stdin.write(encodeRenderStyleControlMessage(style));
126
- }
127
-
128
- sendInput(
129
- chunk: Uint8Array
130
- ): void {
131
- this.stdin.write(chunk);
132
- }
133
-
134
- subscribeResize(
135
- listener: (
136
- columns: number,
137
- rows: number,
138
- cellWidth?: number,
139
- cellHeight?: number
140
- ) => void
141
- ): () => void {
142
- this.resizeListeners.add(listener);
143
- listener(
144
- this.latestResize.columns,
145
- this.latestResize.rows,
146
- this.latestResize.cellWidth,
147
- this.latestResize.cellHeight
148
- );
149
- return () => {
150
- this.resizeListeners.delete(listener);
151
- };
152
- }
153
-
154
- dispose(): void {
155
- this.detachStdout?.();
156
- this.detachStderr?.();
157
- this.resizeListeners.clear();
158
- this.stdin.close();
159
- this.stdout.close();
160
- this.stderr.close();
161
- }
162
- }
163
-
164
- export {
165
- encodeRenderStyleControlMessage,
166
- encodeResizeControlMessage,
167
- };
@@ -1,146 +0,0 @@
1
- import { expect, test } from "bun:test";
2
- import { Worker } from "node:worker_threads";
3
-
4
- import {
5
- SharedInputQueueReader,
6
- SharedInputQueueWriter,
7
- type SharedInputQueueBuffers,
8
- createSharedInputQueue,
9
- } from "./SharedInputQueue.ts";
10
-
11
- test("shared input queue preserves write order across partial reads", () => {
12
- const queue = createSharedInputQueue(8);
13
- const writer = new SharedInputQueueWriter(queue);
14
- const reader = new SharedInputQueueReader(queue);
15
-
16
- writer.write("abcdef");
17
-
18
- expect(decode(reader.readAvailable(2))).toBe("ab");
19
- expect(decode(reader.readAvailable(4))).toBe("cdef");
20
- expect(reader.readAvailable(4)).toBeUndefined();
21
- });
22
-
23
- test("shared input queue wraps around the ring buffer", () => {
24
- const queue = createSharedInputQueue(8);
25
- const writer = new SharedInputQueueWriter(queue);
26
- const reader = new SharedInputQueueReader(queue);
27
-
28
- writer.write("abcdef");
29
- expect(decode(reader.readAvailable(4))).toBe("abcd");
30
-
31
- writer.write("gh");
32
- expect(decode(reader.readAvailable(4))).toBe("efgh");
33
- expect(reader.readAvailable(4)).toBeUndefined();
34
- });
35
-
36
- test("shared input queue reports EOF after close once buffered data is drained", () => {
37
- const queue = createSharedInputQueue(8);
38
- const writer = new SharedInputQueueWriter(queue);
39
- const reader = new SharedInputQueueReader(queue);
40
-
41
- writer.write("ok");
42
- writer.close();
43
-
44
- expect(decode(reader.readAvailable(8))).toBe("ok");
45
- expect(reader.readAvailable(8)).toBeUndefined();
46
- expect(reader.read(8)).toBeUndefined();
47
- });
48
-
49
- test("shared input queue reports readable bytes without consuming them", () => {
50
- const queue = createSharedInputQueue(8);
51
- const writer = new SharedInputQueueWriter(queue);
52
- const reader = new SharedInputQueueReader(queue);
53
-
54
- expect(reader.availableBytes()).toBe(0);
55
-
56
- writer.write("abc");
57
-
58
- expect(reader.availableBytes()).toBe(3);
59
- expect(decode(reader.readAvailable(2))).toBe("ab");
60
- expect(reader.availableBytes()).toBe(1);
61
- });
62
-
63
- test("shared input queue timed readiness wait wakes on write", async () => {
64
- const queue = createSharedInputQueue(8);
65
- const reader = new SharedInputQueueReader(queue);
66
- const worker = writeInputFromWorker(queue, "x", 10);
67
-
68
- try {
69
- expect(reader.waitForReadable(250)).toBe("readable");
70
- expect(decode(reader.readAvailable(1))).toBe("x");
71
- } finally {
72
- await worker.terminate();
73
- }
74
- });
75
-
76
- test("shared input queue timed readiness wait returns timedOut", () => {
77
- const queue = createSharedInputQueue(8);
78
- const reader = new SharedInputQueueReader(queue);
79
-
80
- expect(reader.waitForReadable(1)).toBe("timedOut");
81
- });
82
-
83
- test("shared input queue readiness wait wakes on close", async () => {
84
- const queue = createSharedInputQueue(8);
85
- const reader = new SharedInputQueueReader(queue);
86
- const worker = closeInputFromWorker(queue, 10);
87
-
88
- try {
89
- expect(reader.waitForReadable(250)).toBe("closed");
90
- } finally {
91
- await worker.terminate();
92
- }
93
- });
94
-
95
- function decode(
96
- chunk: Uint8Array | undefined
97
- ): string | undefined {
98
- return chunk ? new TextDecoder().decode(chunk) : undefined;
99
- }
100
-
101
- function writeInputFromWorker(
102
- queue: SharedInputQueueBuffers,
103
- text: string,
104
- delayMilliseconds: number
105
- ): Worker {
106
- return new Worker(`
107
- const { workerData } = require("node:worker_threads");
108
- const control = new Int32Array(workerData.controlBuffer);
109
- const data = new Uint8Array(workerData.dataBuffer);
110
- const bytes = new TextEncoder().encode(workerData.text);
111
- setTimeout(() => {
112
- const writeIndex = Atomics.load(control, 1);
113
- data.set(bytes, writeIndex % data.length);
114
- Atomics.store(control, 1, writeIndex + bytes.length);
115
- Atomics.notify(control, 1);
116
- }, workerData.delayMilliseconds);
117
- `, {
118
- eval: true,
119
- workerData: {
120
- controlBuffer: queue.controlBuffer,
121
- dataBuffer: queue.dataBuffer,
122
- delayMilliseconds,
123
- text,
124
- },
125
- });
126
- }
127
-
128
- function closeInputFromWorker(
129
- queue: SharedInputQueueBuffers,
130
- delayMilliseconds: number
131
- ): Worker {
132
- return new Worker(`
133
- const { workerData } = require("node:worker_threads");
134
- const control = new Int32Array(workerData.controlBuffer);
135
- setTimeout(() => {
136
- Atomics.store(control, 2, 1);
137
- Atomics.notify(control, 1);
138
- }, workerData.delayMilliseconds);
139
- `, {
140
- eval: true,
141
- workerData: {
142
- controlBuffer: queue.controlBuffer,
143
- delayMilliseconds,
144
- },
145
- });
146
- }
@@ -1,199 +0,0 @@
1
- const controlSlots = 3;
2
-
3
- const enum ControlSlot {
4
- readIndex = 0,
5
- writeIndex = 1,
6
- closed = 2,
7
- }
8
-
9
- export const sharedInputQueueDefaultCapacity = 64 * 1024;
10
-
11
- export interface SharedInputQueueBuffers {
12
- readonly controlBuffer: SharedArrayBuffer;
13
- readonly dataBuffer: SharedArrayBuffer;
14
- }
15
-
16
- export type SharedInputReadiness = "readable" | "closed" | "timedOut";
17
-
18
- interface SharedInputQueueState {
19
- readonly control: Int32Array;
20
- readonly data: Uint8Array;
21
- }
22
-
23
- export function createSharedInputQueue(
24
- capacity: number = sharedInputQueueDefaultCapacity
25
- ): SharedInputQueueBuffers {
26
- if (typeof SharedArrayBuffer === "undefined") {
27
- throw new Error(
28
- "SharedArrayBuffer is unavailable. Serve the app with COOP/COEP headers so browser WASI stdin can stay live."
29
- );
30
- }
31
-
32
- if (!Number.isInteger(capacity) || capacity <= 0) {
33
- throw new Error(`Shared input queue capacity must be a positive integer, received ${capacity}.`);
34
- }
35
-
36
- return {
37
- controlBuffer: new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * controlSlots),
38
- dataBuffer: new SharedArrayBuffer(capacity),
39
- };
40
- }
41
-
42
- export function hydrateSharedInputQueue(
43
- buffers: SharedInputQueueBuffers
44
- ): SharedInputQueueState {
45
- return {
46
- control: new Int32Array(buffers.controlBuffer),
47
- data: new Uint8Array(buffers.dataBuffer),
48
- };
49
- }
50
-
51
- export class SharedInputQueueWriter {
52
- private readonly queue: SharedInputQueueState;
53
-
54
- constructor(buffers: SharedInputQueueBuffers) {
55
- this.queue = hydrateSharedInputQueue(buffers);
56
- }
57
-
58
- write(chunk: Uint8Array | string): void {
59
- if (Atomics.load(this.queue.control, ControlSlot.closed) !== 0) {
60
- return;
61
- }
62
-
63
- const bytes = normalizeChunk(chunk);
64
- if (bytes.length == 0) {
65
- return;
66
- }
67
-
68
- const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);
69
- const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);
70
- const usedCapacity = writeIndex - readIndex;
71
- const availableCapacity = this.queue.data.length - usedCapacity;
72
-
73
- if (bytes.length > availableCapacity) {
74
- throw new Error(
75
- `Shared input queue overflow: cannot enqueue ${bytes.length} byte(s) into ${availableCapacity} byte(s) of free space.`
76
- );
77
- }
78
-
79
- writeToRingBuffer(this.queue.data, bytes, writeIndex);
80
- Atomics.store(this.queue.control, ControlSlot.writeIndex, writeIndex + bytes.length);
81
- Atomics.notify(this.queue.control, ControlSlot.writeIndex);
82
- }
83
-
84
- close(): void {
85
- Atomics.store(this.queue.control, ControlSlot.closed, 1);
86
- Atomics.notify(this.queue.control, ControlSlot.writeIndex);
87
- }
88
- }
89
-
90
- export class SharedInputQueueReader {
91
- private readonly queue: SharedInputQueueState;
92
-
93
- constructor(buffers: SharedInputQueueBuffers) {
94
- this.queue = hydrateSharedInputQueue(buffers);
95
- }
96
-
97
- read(maxBytes: number): Uint8Array | undefined {
98
- while (true) {
99
- const next = this.readAvailable(maxBytes);
100
- if (next) {
101
- return next;
102
- }
103
-
104
- if (this.isClosed()) {
105
- return undefined;
106
- }
107
-
108
- const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);
109
- Atomics.wait(this.queue.control, ControlSlot.writeIndex, writeIndex);
110
- }
111
- }
112
-
113
- readAvailable(maxBytes: number): Uint8Array | undefined {
114
- if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
115
- return new Uint8Array();
116
- }
117
-
118
- const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);
119
- const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);
120
- const availableBytes = writeIndex - readIndex;
121
-
122
- if (availableBytes <= 0) {
123
- return undefined;
124
- }
125
-
126
- const byteCount = Math.min(maxBytes, availableBytes);
127
- const chunk = readFromRingBuffer(this.queue.data, readIndex, byteCount);
128
- Atomics.store(this.queue.control, ControlSlot.readIndex, readIndex + byteCount);
129
- return chunk;
130
- }
131
-
132
- availableBytes(): number {
133
- const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);
134
- const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);
135
- return Math.max(0, writeIndex - readIndex);
136
- }
137
-
138
- waitForReadable(
139
- timeoutMilliseconds?: number
140
- ): SharedInputReadiness {
141
- while (true) {
142
- if (this.availableBytes() > 0) {
143
- return "readable";
144
- }
145
- if (this.isClosed()) {
146
- return "closed";
147
- }
148
-
149
- const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);
150
- const result = Atomics.wait(
151
- this.queue.control,
152
- ControlSlot.writeIndex,
153
- writeIndex,
154
- timeoutMilliseconds
155
- );
156
- if (result === "timed-out") {
157
- return "timedOut";
158
- }
159
- }
160
- }
161
-
162
- isClosed(): boolean {
163
- return Atomics.load(this.queue.control, ControlSlot.closed) !== 0;
164
- }
165
- }
166
-
167
- function normalizeChunk(
168
- chunk: Uint8Array | string
169
- ): Uint8Array {
170
- return typeof chunk == "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);
171
- }
172
-
173
- function writeToRingBuffer(
174
- buffer: Uint8Array,
175
- chunk: Uint8Array,
176
- startIndex: number
177
- ): void {
178
- const offset = startIndex % buffer.length;
179
- const firstSegmentLength = Math.min(chunk.length, buffer.length - offset);
180
- buffer.set(chunk.subarray(0, firstSegmentLength), offset);
181
- if (firstSegmentLength < chunk.length) {
182
- buffer.set(chunk.subarray(firstSegmentLength), 0);
183
- }
184
- }
185
-
186
- function readFromRingBuffer(
187
- buffer: Uint8Array,
188
- startIndex: number,
189
- byteCount: number
190
- ): Uint8Array {
191
- const chunk = new Uint8Array(byteCount);
192
- const offset = startIndex % buffer.length;
193
- const firstSegmentLength = Math.min(byteCount, buffer.length - offset);
194
- chunk.set(buffer.subarray(offset, offset + firstSegmentLength), 0);
195
- if (firstSegmentLength < byteCount) {
196
- chunk.set(buffer.subarray(0, byteCount - firstSegmentLength), firstSegmentLength);
197
- }
198
- return chunk;
199
- }
@@ -1,72 +0,0 @@
1
- export class StdIOPipe implements AsyncIterable<Uint8Array> {
2
- private readonly chunks: Uint8Array[] = [];
3
- private readonly waiters: Array<(value: IteratorResult<Uint8Array>) => void> = [];
4
- private readonly listeners = new Set<(chunk: Uint8Array) => void>();
5
- private closed = false;
6
-
7
- write(chunk: Uint8Array | string): void {
8
- if (this.closed) {
9
- return;
10
- }
11
-
12
- const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);
13
- const waiter = this.waiters.shift();
14
- if (waiter) {
15
- waiter({ done: false, value: bytes });
16
- return;
17
- }
18
-
19
- this.chunks.push(bytes);
20
- for (const listener of this.listeners) {
21
- listener(bytes);
22
- }
23
- }
24
-
25
- close(): void {
26
- if (this.closed) {
27
- return;
28
- }
29
-
30
- this.closed = true;
31
- while (this.waiters.length > 0) {
32
- const waiter = this.waiters.shift();
33
- waiter?.({ done: true, value: undefined as never });
34
- }
35
- }
36
-
37
- async read(): Promise<Uint8Array | undefined> {
38
- const next = this.chunks.shift();
39
- if (next) {
40
- return next;
41
- }
42
-
43
- if (this.closed) {
44
- return undefined;
45
- }
46
-
47
- return await new Promise<Uint8Array | undefined>((resolve) => {
48
- this.waiters.push((result) => {
49
- resolve(result.done ? undefined : result.value);
50
- });
51
- });
52
- }
53
-
54
- subscribe(
55
- listener: (chunk: Uint8Array) => void
56
- ): () => void {
57
- this.listeners.add(listener);
58
- return () => {
59
- this.listeners.delete(listener);
60
- };
61
- }
62
-
63
- async *[Symbol.asyncIterator](): AsyncIterator<Uint8Array> {
64
- while (true) {
65
- const next = await this.read();
66
- if (!next) {
67
- return;
68
- }
69
- yield next;
70
- }
71
- }
72
- }