@swifttui/web 0.4.0 → 0.4.2

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 (41) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.js +2 -2
  3. package/dist/src/AccessibilityTree.js +10 -2
  4. package/dist/src/AccessibilityTree.js.map +1 -1
  5. package/dist/src/CanvasSurfacePainter.d.ts +16 -0
  6. package/dist/src/CanvasSurfacePainter.js +162 -16
  7. package/dist/src/CanvasSurfacePainter.js.map +1 -1
  8. package/dist/src/DomSurfacePainter.d.ts +17 -3
  9. package/dist/src/DomSurfacePainter.js +63 -10
  10. package/dist/src/DomSurfacePainter.js.map +1 -1
  11. package/dist/src/SurfacePainterConformanceControl.js +13 -0
  12. package/dist/src/SurfacePainterConformanceControl.js.map +1 -0
  13. package/dist/src/SurfaceRenderer.d.ts +3 -2
  14. package/dist/src/SurfaceRenderer.js.map +1 -1
  15. package/dist/src/WebHostSceneRuntime.d.ts +3 -1
  16. package/dist/src/WebHostSceneRuntime.js +16 -7
  17. package/dist/src/WebHostSceneRuntime.js.map +1 -1
  18. package/dist/src/WebHostSurfaceTransport.d.ts +63 -6
  19. package/dist/src/WebHostSurfaceTransport.js +186 -9
  20. package/dist/src/WebHostSurfaceTransport.js.map +1 -1
  21. package/dist/src/WebSocketSceneBridge.d.ts +2 -0
  22. package/dist/src/WebSocketSceneBridge.js +30 -5
  23. package/dist/src/WebSocketSceneBridge.js.map +1 -1
  24. package/dist/src/normalizeWireTokens.d.ts +5 -0
  25. package/dist/src/normalizeWireTokens.js +41 -0
  26. package/dist/src/normalizeWireTokens.js.map +1 -0
  27. package/dist/src/wasi/BrowserWASIBridge.d.ts +8 -0
  28. package/dist/src/wasi/BrowserWASIBridge.js +31 -4
  29. package/dist/src/wasi/BrowserWASIBridge.js.map +1 -1
  30. package/dist/src/wasi/SharedInputQueue.d.ts +2 -0
  31. package/dist/src/wasi/SharedInputQueue.js +22 -0
  32. package/dist/src/wasi/SharedInputQueue.js.map +1 -1
  33. package/dist/src/wasi/StdIOPipe.d.ts +2 -2
  34. package/dist/src/wasi/StdIOPipe.js +5 -3
  35. package/dist/src/wasi/StdIOPipe.js.map +1 -1
  36. package/dist/src/wasi/WasmSceneRuntime.js +24 -4
  37. package/dist/src/wasi/WasmSceneRuntime.js.map +1 -1
  38. package/dist/src/wasi/WasmSceneWorker.js +1 -1
  39. package/dist/testing.d.ts +2 -1
  40. package/dist/wasi.js +1 -1
  41. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SharedInputQueue.js","names":[],"sources":["../../../src/wasi/SharedInputQueue.ts"],"sourcesContent":["const controlSlots = 3;\n\nconst enum ControlSlot {\n readIndex = 0,\n writeIndex = 1,\n closed = 2,\n}\n\nexport const sharedInputQueueDefaultCapacity = 64 * 1024;\n\nexport interface SharedInputQueueBuffers {\n readonly controlBuffer: SharedArrayBuffer;\n readonly dataBuffer: SharedArrayBuffer;\n}\n\nexport type SharedInputReadiness = \"readable\" | \"closed\" | \"timedOut\";\n\ninterface SharedInputQueueState {\n readonly control: Int32Array;\n readonly data: Uint8Array;\n}\n\nexport function createSharedInputQueue(\n capacity: number = sharedInputQueueDefaultCapacity\n): SharedInputQueueBuffers {\n if (typeof SharedArrayBuffer === \"undefined\") {\n throw new Error(\n \"SharedArrayBuffer is unavailable. Serve the app with COOP/COEP headers so browser WASI stdin can stay live.\"\n );\n }\n\n if (!Number.isInteger(capacity) || capacity <= 0) {\n throw new Error(`Shared input queue capacity must be a positive integer, received ${capacity}.`);\n }\n\n return {\n controlBuffer: new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * controlSlots),\n dataBuffer: new SharedArrayBuffer(capacity),\n };\n}\n\nexport function hydrateSharedInputQueue(\n buffers: SharedInputQueueBuffers\n): SharedInputQueueState {\n return {\n control: new Int32Array(buffers.controlBuffer),\n data: new Uint8Array(buffers.dataBuffer),\n };\n}\n\nexport class SharedInputQueueWriter {\n private readonly queue: SharedInputQueueState;\n\n constructor(buffers: SharedInputQueueBuffers) {\n this.queue = hydrateSharedInputQueue(buffers);\n }\n\n write(chunk: Uint8Array | string): void {\n if (Atomics.load(this.queue.control, ControlSlot.closed) !== 0) {\n return;\n }\n\n const bytes = normalizeChunk(chunk);\n if (bytes.length == 0) {\n return;\n }\n\n const length = this.queue.data.length;\n const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n const usedCapacity = ringUsed(readIndex, writeIndex, length);\n const availableCapacity = length - usedCapacity;\n\n if (bytes.length > availableCapacity) {\n throw new Error(\n `Shared input queue overflow: cannot enqueue ${bytes.length} byte(s) into ${availableCapacity} byte(s) of free space.`\n );\n }\n\n writeToRingBuffer(this.queue.data, bytes, writeIndex);\n Atomics.store(\n this.queue.control,\n ControlSlot.writeIndex,\n ringAdvance(writeIndex, bytes.length, length)\n );\n Atomics.notify(this.queue.control, ControlSlot.writeIndex);\n }\n\n close(): void {\n Atomics.store(this.queue.control, ControlSlot.closed, 1);\n Atomics.notify(this.queue.control, ControlSlot.writeIndex);\n }\n}\n\nexport class SharedInputQueueReader {\n private readonly queue: SharedInputQueueState;\n\n constructor(buffers: SharedInputQueueBuffers) {\n this.queue = hydrateSharedInputQueue(buffers);\n }\n\n read(maxBytes: number): Uint8Array | undefined {\n while (true) {\n const next = this.readAvailable(maxBytes);\n if (next) {\n return next;\n }\n\n if (this.isClosed()) {\n return undefined;\n }\n\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n Atomics.wait(this.queue.control, ControlSlot.writeIndex, writeIndex);\n }\n }\n\n readAvailable(maxBytes: number): Uint8Array | undefined {\n if (!Number.isInteger(maxBytes) || maxBytes <= 0) {\n return new Uint8Array();\n }\n\n const length = this.queue.data.length;\n const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n const availableBytes = ringUsed(readIndex, writeIndex, length);\n\n if (availableBytes <= 0) {\n return undefined;\n }\n\n const byteCount = Math.min(maxBytes, availableBytes);\n const chunk = readFromRingBuffer(this.queue.data, readIndex, byteCount);\n Atomics.store(\n this.queue.control,\n ControlSlot.readIndex,\n ringAdvance(readIndex, byteCount, length)\n );\n return chunk;\n }\n\n availableBytes(): number {\n const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n return ringUsed(readIndex, writeIndex, this.queue.data.length);\n }\n\n waitForReadable(\n timeoutMilliseconds?: number\n ): SharedInputReadiness {\n while (true) {\n if (this.availableBytes() > 0) {\n return \"readable\";\n }\n if (this.isClosed()) {\n return \"closed\";\n }\n\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n const result = Atomics.wait(\n this.queue.control,\n ControlSlot.writeIndex,\n writeIndex,\n timeoutMilliseconds\n );\n if (result === \"timed-out\") {\n return \"timedOut\";\n }\n }\n }\n\n isClosed(): boolean {\n return Atomics.load(this.queue.control, ControlSlot.closed) !== 0;\n }\n}\n\nfunction normalizeChunk(\n chunk: Uint8Array | string\n): Uint8Array {\n return typeof chunk == \"string\" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);\n}\n\n// The read/write cursors are kept in the half-open range [0, 2 * length) — the\n// classic \"two indices mod 2N\" ring buffer. Bounding both cursors keeps them\n// from growing without limit and overflowing Int32 across long sessions, while\n// still distinguishing a full queue (used == length) from an empty one\n// (used == 0). The data-buffer offset for either cursor is cursor % length.\nfunction ringUsed(\n readIndex: number,\n writeIndex: number,\n length: number\n): number {\n const span = 2 * length;\n return ((writeIndex - readIndex) % span + span) % span;\n}\n\nfunction ringAdvance(\n index: number,\n delta: number,\n length: number\n): number {\n return (index + delta) % (2 * length);\n}\n\nfunction writeToRingBuffer(\n buffer: Uint8Array,\n chunk: Uint8Array,\n startIndex: number\n): void {\n const offset = startIndex % buffer.length;\n const firstSegmentLength = Math.min(chunk.length, buffer.length - offset);\n buffer.set(chunk.subarray(0, firstSegmentLength), offset);\n if (firstSegmentLength < chunk.length) {\n buffer.set(chunk.subarray(firstSegmentLength), 0);\n }\n}\n\nfunction readFromRingBuffer(\n buffer: Uint8Array,\n startIndex: number,\n byteCount: number\n): Uint8Array {\n const chunk = new Uint8Array(byteCount);\n const offset = startIndex % buffer.length;\n const firstSegmentLength = Math.min(byteCount, buffer.length - offset);\n chunk.set(buffer.subarray(offset, offset + firstSegmentLength), 0);\n if (firstSegmentLength < byteCount) {\n chunk.set(buffer.subarray(0, byteCount - firstSegmentLength), firstSegmentLength);\n }\n return chunk;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SharedInputQueue.js","names":[],"sources":["../../../src/wasi/SharedInputQueue.ts"],"sourcesContent":["const controlSlots = 3;\nconst capacityWaitTimeoutMilliseconds = 50;\n\nconst enum ControlSlot {\n readIndex = 0,\n writeIndex = 1,\n closed = 2,\n}\n\nexport const sharedInputQueueDefaultCapacity = 64 * 1024;\n\nexport interface SharedInputQueueBuffers {\n readonly controlBuffer: SharedArrayBuffer;\n readonly dataBuffer: SharedArrayBuffer;\n}\n\nexport type SharedInputReadiness = \"readable\" | \"closed\" | \"timedOut\";\n\ninterface SharedInputQueueState {\n readonly control: Int32Array;\n readonly data: Uint8Array;\n}\n\nexport function createSharedInputQueue(\n capacity: number = sharedInputQueueDefaultCapacity\n): SharedInputQueueBuffers {\n if (typeof SharedArrayBuffer === \"undefined\") {\n throw new Error(\n \"SharedArrayBuffer is unavailable. Serve the app with COOP/COEP headers so browser WASI stdin can stay live.\"\n );\n }\n\n if (!Number.isInteger(capacity) || capacity <= 0) {\n throw new Error(`Shared input queue capacity must be a positive integer, received ${capacity}.`);\n }\n\n return {\n controlBuffer: new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * controlSlots),\n dataBuffer: new SharedArrayBuffer(capacity),\n };\n}\n\nexport function hydrateSharedInputQueue(\n buffers: SharedInputQueueBuffers\n): SharedInputQueueState {\n return {\n control: new Int32Array(buffers.controlBuffer),\n data: new Uint8Array(buffers.dataBuffer),\n };\n}\n\nexport class SharedInputQueueWriter {\n private readonly queue: SharedInputQueueState;\n\n constructor(buffers: SharedInputQueueBuffers) {\n this.queue = hydrateSharedInputQueue(buffers);\n }\n\n write(chunk: Uint8Array | string): void {\n if (Atomics.load(this.queue.control, ControlSlot.closed) !== 0) {\n return;\n }\n\n const bytes = normalizeChunk(chunk);\n if (bytes.length == 0) {\n return;\n }\n\n const length = this.queue.data.length;\n const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n const usedCapacity = ringUsed(readIndex, writeIndex, length);\n const availableCapacity = length - usedCapacity;\n\n if (bytes.length > availableCapacity) {\n throw new Error(\n `Shared input queue overflow: cannot enqueue ${bytes.length} byte(s) into ${availableCapacity} byte(s) of free space.`\n );\n }\n\n writeToRingBuffer(this.queue.data, bytes, writeIndex);\n Atomics.store(\n this.queue.control,\n ControlSlot.writeIndex,\n ringAdvance(writeIndex, bytes.length, length)\n );\n Atomics.notify(this.queue.control, ControlSlot.writeIndex);\n }\n\n availableCapacity(): number {\n const length = this.queue.data.length;\n const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n return length - ringUsed(readIndex, writeIndex, length);\n }\n\n async waitForCapacity(\n minimumBytes: number\n ): Promise<boolean> {\n const required = Math.max(0, Math.ceil(minimumBytes));\n if (required > this.queue.data.length) {\n return false;\n }\n\n while (true) {\n const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);\n if (Atomics.load(this.queue.control, ControlSlot.closed) !== 0) {\n return false;\n }\n if (this.availableCapacity() >= required) {\n return true;\n }\n\n if (typeof Atomics.waitAsync === \"function\") {\n const waiting = Atomics.waitAsync(\n this.queue.control,\n ControlSlot.readIndex,\n readIndex,\n capacityWaitTimeoutMilliseconds\n );\n if (waiting.async) {\n await waiting.value;\n }\n } else {\n await new Promise<void>((resolve) => {\n setTimeout(resolve, 1);\n });\n }\n }\n }\n\n close(): void {\n Atomics.store(this.queue.control, ControlSlot.closed, 1);\n Atomics.notify(this.queue.control, ControlSlot.writeIndex);\n Atomics.notify(this.queue.control, ControlSlot.readIndex);\n }\n}\n\nexport class SharedInputQueueReader {\n private readonly queue: SharedInputQueueState;\n\n constructor(buffers: SharedInputQueueBuffers) {\n this.queue = hydrateSharedInputQueue(buffers);\n }\n\n read(maxBytes: number): Uint8Array | undefined {\n while (true) {\n const next = this.readAvailable(maxBytes);\n if (next) {\n return next;\n }\n\n if (this.isClosed()) {\n return undefined;\n }\n\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n Atomics.wait(this.queue.control, ControlSlot.writeIndex, writeIndex);\n }\n }\n\n readAvailable(maxBytes: number): Uint8Array | undefined {\n if (!Number.isInteger(maxBytes) || maxBytes <= 0) {\n return new Uint8Array();\n }\n\n const length = this.queue.data.length;\n const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n const availableBytes = ringUsed(readIndex, writeIndex, length);\n\n if (availableBytes <= 0) {\n return undefined;\n }\n\n const byteCount = Math.min(maxBytes, availableBytes);\n const chunk = readFromRingBuffer(this.queue.data, readIndex, byteCount);\n Atomics.store(\n this.queue.control,\n ControlSlot.readIndex,\n ringAdvance(readIndex, byteCount, length)\n );\n Atomics.notify(this.queue.control, ControlSlot.readIndex);\n return chunk;\n }\n\n availableBytes(): number {\n const readIndex = Atomics.load(this.queue.control, ControlSlot.readIndex);\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n return ringUsed(readIndex, writeIndex, this.queue.data.length);\n }\n\n waitForReadable(\n timeoutMilliseconds?: number\n ): SharedInputReadiness {\n while (true) {\n if (this.availableBytes() > 0) {\n return \"readable\";\n }\n if (this.isClosed()) {\n return \"closed\";\n }\n\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n const result = Atomics.wait(\n this.queue.control,\n ControlSlot.writeIndex,\n writeIndex,\n timeoutMilliseconds\n );\n if (result === \"timed-out\") {\n return \"timedOut\";\n }\n }\n }\n\n isClosed(): boolean {\n return Atomics.load(this.queue.control, ControlSlot.closed) !== 0;\n }\n}\n\nfunction normalizeChunk(\n chunk: Uint8Array | string\n): Uint8Array {\n return typeof chunk == \"string\" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);\n}\n\n// The read/write cursors are kept in the half-open range [0, 2 * length) — the\n// classic \"two indices mod 2N\" ring buffer. Bounding both cursors keeps them\n// from growing without limit and overflowing Int32 across long sessions, while\n// still distinguishing a full queue (used == length) from an empty one\n// (used == 0). The data-buffer offset for either cursor is cursor % length.\nfunction ringUsed(\n readIndex: number,\n writeIndex: number,\n length: number\n): number {\n const span = 2 * length;\n return ((writeIndex - readIndex) % span + span) % span;\n}\n\nfunction ringAdvance(\n index: number,\n delta: number,\n length: number\n): number {\n return (index + delta) % (2 * length);\n}\n\nfunction writeToRingBuffer(\n buffer: Uint8Array,\n chunk: Uint8Array,\n startIndex: number\n): void {\n const offset = startIndex % buffer.length;\n const firstSegmentLength = Math.min(chunk.length, buffer.length - offset);\n buffer.set(chunk.subarray(0, firstSegmentLength), offset);\n if (firstSegmentLength < chunk.length) {\n buffer.set(chunk.subarray(firstSegmentLength), 0);\n }\n}\n\nfunction readFromRingBuffer(\n buffer: Uint8Array,\n startIndex: number,\n byteCount: number\n): Uint8Array {\n const chunk = new Uint8Array(byteCount);\n const offset = startIndex % buffer.length;\n const firstSegmentLength = Math.min(byteCount, buffer.length - offset);\n chunk.set(buffer.subarray(offset, offset + firstSegmentLength), 0);\n if (firstSegmentLength < byteCount) {\n chunk.set(buffer.subarray(0, byteCount - firstSegmentLength), firstSegmentLength);\n }\n return chunk;\n}\n"],"mappings":"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"}
@@ -4,10 +4,10 @@ declare class StdIOPipe implements AsyncIterable<Uint8Array> {
4
4
  private readonly waiters;
5
5
  private readonly listeners;
6
6
  private closed;
7
- write(chunk: Uint8Array | string): void;
7
+ write(chunk: Uint8Array | string): boolean;
8
8
  close(): void;
9
9
  read(): Promise<Uint8Array | undefined>;
10
- subscribe(listener: (chunk: Uint8Array) => void): () => void;
10
+ subscribe(listener: (chunk: Uint8Array) => boolean | void): () => void;
11
11
  [Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
12
12
  }
13
13
  //#endregion
@@ -5,7 +5,7 @@ var StdIOPipe = class {
5
5
  listeners = /* @__PURE__ */ new Set();
6
6
  closed = false;
7
7
  write(chunk) {
8
- if (this.closed) return;
8
+ if (this.closed) return false;
9
9
  const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);
10
10
  const waiter = this.waiters.shift();
11
11
  if (waiter) {
@@ -13,10 +13,12 @@ var StdIOPipe = class {
13
13
  done: false,
14
14
  value: bytes
15
15
  });
16
- return;
16
+ return true;
17
17
  }
18
18
  this.chunks.push(bytes);
19
- for (const listener of this.listeners) listener(bytes);
19
+ let accepted = true;
20
+ for (const listener of this.listeners) if (listener(bytes) === false) accepted = false;
21
+ return accepted;
20
22
  }
21
23
  close() {
22
24
  if (this.closed) return;
@@ -1 +1 @@
1
- {"version":3,"file":"StdIOPipe.js","names":[],"sources":["../../../src/wasi/StdIOPipe.ts"],"sourcesContent":["export class StdIOPipe implements AsyncIterable<Uint8Array> {\n private readonly chunks: Uint8Array[] = [];\n private readonly waiters: Array<(value: IteratorResult<Uint8Array>) => void> = [];\n private readonly listeners = new Set<(chunk: Uint8Array) => void>();\n private closed = false;\n\n write(chunk: Uint8Array | string): void {\n if (this.closed) {\n return;\n }\n\n const bytes = typeof chunk === \"string\" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);\n const waiter = this.waiters.shift();\n if (waiter) {\n waiter({ done: false, value: bytes });\n return;\n }\n\n this.chunks.push(bytes);\n for (const listener of this.listeners) {\n listener(bytes);\n }\n }\n\n close(): void {\n if (this.closed) {\n return;\n }\n\n this.closed = true;\n while (this.waiters.length > 0) {\n const waiter = this.waiters.shift();\n waiter?.({ done: true, value: undefined as never });\n }\n }\n\n async read(): Promise<Uint8Array | undefined> {\n const next = this.chunks.shift();\n if (next) {\n return next;\n }\n\n if (this.closed) {\n return undefined;\n }\n\n return await new Promise<Uint8Array | undefined>((resolve) => {\n this.waiters.push((result) => {\n resolve(result.done ? undefined : result.value);\n });\n });\n }\n\n subscribe(\n listener: (chunk: Uint8Array) => void\n ): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n async *[Symbol.asyncIterator](): AsyncIterator<Uint8Array> {\n while (true) {\n const next = await this.read();\n if (!next) {\n return;\n }\n yield next;\n }\n }\n}\n"],"mappings":";AAAA,IAAa,YAAb,MAA4D;CAC1D,SAAwC,CAAC;CACzC,UAA+E,CAAC;CAChF,4BAA6B,IAAI,IAAiC;CAClE,SAAiB;CAEjB,MAAM,OAAkC;EACtC,IAAI,KAAK,QACP;EAGF,MAAM,QAAQ,OAAO,UAAU,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,IAAI,IAAI,WAAW,KAAK;EAChG,MAAM,SAAS,KAAK,QAAQ,MAAM;EAClC,IAAI,QAAQ;GACV,OAAO;IAAE,MAAM;IAAO,OAAO;GAAM,CAAC;GACpC;EACF;EAEA,KAAK,OAAO,KAAK,KAAK;EACtB,KAAK,MAAM,YAAY,KAAK,WAC1B,SAAS,KAAK;CAElB;CAEA,QAAc;EACZ,IAAI,KAAK,QACP;EAGF,KAAK,SAAS;EACd,OAAO,KAAK,QAAQ,SAAS,GAE3B,KADoB,QAAQ,MACvB,CAAC,GAAG;GAAE,MAAM;GAAM,OAAO,KAAA;EAAmB,CAAC;CAEtD;CAEA,MAAM,OAAwC;EAC5C,MAAM,OAAO,KAAK,OAAO,MAAM;EAC/B,IAAI,MACF,OAAO;EAGT,IAAI,KAAK,QACP;EAGF,OAAO,MAAM,IAAI,SAAiC,YAAY;GAC5D,KAAK,QAAQ,MAAM,WAAW;IAC5B,QAAQ,OAAO,OAAO,KAAA,IAAY,OAAO,KAAK;GAChD,CAAC;EACH,CAAC;CACH;CAEA,UACE,UACY;EACZ,KAAK,UAAU,IAAI,QAAQ;EAC3B,aAAa;GACX,KAAK,UAAU,OAAO,QAAQ;EAChC;CACF;CAEA,QAAQ,OAAO,iBAA4C;EACzD,OAAO,MAAM;GACX,MAAM,OAAO,MAAM,KAAK,KAAK;GAC7B,IAAI,CAAC,MACH;GAEF,MAAM;EACR;CACF;AACF"}
1
+ {"version":3,"file":"StdIOPipe.js","names":[],"sources":["../../../src/wasi/StdIOPipe.ts"],"sourcesContent":["export class StdIOPipe implements AsyncIterable<Uint8Array> {\n private readonly chunks: Uint8Array[] = [];\n private readonly waiters: Array<(value: IteratorResult<Uint8Array>) => void> = [];\n private readonly listeners = new Set<(chunk: Uint8Array) => boolean | void>();\n private closed = false;\n\n write(chunk: Uint8Array | string): boolean {\n if (this.closed) {\n return false;\n }\n\n const bytes = typeof chunk === \"string\" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);\n const waiter = this.waiters.shift();\n if (waiter) {\n waiter({ done: false, value: bytes });\n return true;\n }\n\n this.chunks.push(bytes);\n let accepted = true;\n for (const listener of this.listeners) {\n if (listener(bytes) === false) {\n accepted = false;\n }\n }\n return accepted;\n }\n\n close(): void {\n if (this.closed) {\n return;\n }\n\n this.closed = true;\n while (this.waiters.length > 0) {\n const waiter = this.waiters.shift();\n waiter?.({ done: true, value: undefined as never });\n }\n }\n\n async read(): Promise<Uint8Array | undefined> {\n const next = this.chunks.shift();\n if (next) {\n return next;\n }\n\n if (this.closed) {\n return undefined;\n }\n\n return await new Promise<Uint8Array | undefined>((resolve) => {\n this.waiters.push((result) => {\n resolve(result.done ? undefined : result.value);\n });\n });\n }\n\n subscribe(\n listener: (chunk: Uint8Array) => boolean | void\n ): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n async *[Symbol.asyncIterator](): AsyncIterator<Uint8Array> {\n while (true) {\n const next = await this.read();\n if (!next) {\n return;\n }\n yield next;\n }\n }\n}\n"],"mappings":";AAAA,IAAa,YAAb,MAA4D;CAC1D,SAAwC,CAAC;CACzC,UAA+E,CAAC;CAChF,4BAA6B,IAAI,IAA2C;CAC5E,SAAiB;CAEjB,MAAM,OAAqC;EACzC,IAAI,KAAK,QACP,OAAO;EAGT,MAAM,QAAQ,OAAO,UAAU,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,IAAI,IAAI,WAAW,KAAK;EAChG,MAAM,SAAS,KAAK,QAAQ,MAAM;EAClC,IAAI,QAAQ;GACV,OAAO;IAAE,MAAM;IAAO,OAAO;GAAM,CAAC;GACpC,OAAO;EACT;EAEA,KAAK,OAAO,KAAK,KAAK;EACtB,IAAI,WAAW;EACf,KAAK,MAAM,YAAY,KAAK,WAC1B,IAAI,SAAS,KAAK,MAAM,OACtB,WAAW;EAGf,OAAO;CACT;CAEA,QAAc;EACZ,IAAI,KAAK,QACP;EAGF,KAAK,SAAS;EACd,OAAO,KAAK,QAAQ,SAAS,GAE3B,KADoB,QAAQ,MACvB,CAAC,GAAG;GAAE,MAAM;GAAM,OAAO,KAAA;EAAmB,CAAC;CAEtD;CAEA,MAAM,OAAwC;EAC5C,MAAM,OAAO,KAAK,OAAO,MAAM;EAC/B,IAAI,MACF,OAAO;EAGT,IAAI,KAAK,QACP;EAGF,OAAO,MAAM,IAAI,SAAiC,YAAY;GAC5D,KAAK,QAAQ,MAAM,WAAW;IAC5B,QAAQ,OAAO,OAAO,KAAA,IAAY,OAAO,KAAK;GAChD,CAAC;EACH,CAAC;CACH;CAEA,UACE,UACY;EACZ,KAAK,UAAU,IAAI,QAAQ;EAC3B,aAAa;GACX,KAAK,UAAU,OAAO,QAAQ;EAChC;CACF;CAEA,QAAQ,OAAO,iBAA4C;EACzD,OAAO,MAAM;GACX,MAAM,OAAO,MAAM,KAAK,KAAK;GAC7B,IAAI,CAAC,MACH;GAEF,MAAM;EACR;CACF;AACF"}
@@ -1,8 +1,8 @@
1
1
  import { mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities } from "./WasmEngineCapabilities.js";
2
+ import { SharedInputQueueWriter, createSharedInputQueue } from "./SharedInputQueue.js";
2
3
  import { WebHostSceneRuntime } from "../WebHostSceneRuntime.js";
3
4
  import { createWasmPauseCell, setWasmPauseCellPaused } from "./WasmRuntimePause.js";
4
5
  import { MainThreadWasmExecutor } from "./MainThreadWasmExecutor.js";
5
- import { SharedInputQueueWriter, createSharedInputQueue } from "./SharedInputQueue.js";
6
6
  //#region src/wasi/WasmSceneRuntime.ts
7
7
  const workerModuleURL = new URL("./wasm-scene-worker.js", import.meta.url);
8
8
  function resolveWasmExecutionMode(preference, capabilities, sharedInputQueueAvailable) {
@@ -27,6 +27,7 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
27
27
  inputQueue;
28
28
  inputWriter;
29
29
  inputRouter;
30
+ inputCapacityNotifier;
30
31
  sharedQueueError;
31
32
  pauseCell;
32
33
  detachBridgeInputListener;
@@ -40,6 +41,10 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
40
41
  let inputWriter;
41
42
  let sharedQueueError;
42
43
  let pauseCell;
44
+ const inputCapacityNotifier = {
45
+ disposed: false,
46
+ pending: false
47
+ };
43
48
  try {
44
49
  inputQueue = createSharedInputQueue();
45
50
  inputWriter = new SharedInputQueueWriter(inputQueue);
@@ -48,10 +53,20 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
48
53
  sharedQueueError = error;
49
54
  }
50
55
  const inputRouter = { route: (chunk) => {
56
+ if (!inputWriter) return false;
51
57
  try {
52
- inputWriter?.write(chunk);
58
+ inputWriter.write(chunk);
59
+ return true;
53
60
  } catch (error) {
54
61
  console.error("[SwiftTUIWeb] failed to enqueue terminal input", error);
62
+ if (!inputCapacityNotifier.pending) {
63
+ inputCapacityNotifier.pending = true;
64
+ inputWriter.waitForCapacity(chunk.byteLength).then((available) => {
65
+ inputCapacityNotifier.pending = false;
66
+ if (available && !inputCapacityNotifier.disposed) options.bridge?.notifyInputCapacityAvailable();
67
+ });
68
+ }
69
+ return false;
55
70
  }
56
71
  } };
57
72
  super({
@@ -66,6 +81,7 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
66
81
  this.inputQueue = inputQueue;
67
82
  this.inputWriter = inputWriter;
68
83
  this.inputRouter = inputRouter;
84
+ this.inputCapacityNotifier = inputCapacityNotifier;
69
85
  this.sharedQueueError = sharedQueueError;
70
86
  this.pauseCell = pauseCell;
71
87
  }
@@ -79,7 +95,7 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
79
95
  if (this.didMount) return;
80
96
  this.didMount = true;
81
97
  this.detachBridgeInputListener = this.bridge?.stdin.subscribe((chunk) => {
82
- this.inputRouter.route(chunk);
98
+ return this.inputRouter.route(chunk);
83
99
  });
84
100
  this.detachResizeListener = this.bridge?.subscribeResize((columns, rows, cellWidth, cellHeight) => {
85
101
  this.onSceneResize?.({
@@ -128,6 +144,7 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
128
144
  this.worker.postMessage(message);
129
145
  }
130
146
  dispose() {
147
+ this.inputCapacityNotifier.disposed = true;
131
148
  this.detachBridgeInputListener?.();
132
149
  this.detachResizeListener?.();
133
150
  this.inputWriter?.close();
@@ -154,7 +171,10 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
154
171
  }
155
172
  });
156
173
  this.executor = executor;
157
- this.inputRouter.route = (chunk) => executor.sendInput(chunk);
174
+ this.inputRouter.route = (chunk) => {
175
+ executor.sendInput(chunk);
176
+ return true;
177
+ };
158
178
  executor.setSuspended(this.suspended);
159
179
  executor.start();
160
180
  }
@@ -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 { 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\";\nimport { createWasmPauseCell, setWasmPauseCellPaused } from \"./WasmRuntimePause.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 pauseCell?: SharedArrayBuffer;\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 private readonly pauseCell?: SharedArrayBuffer;\n\n private detachBridgeInputListener?: () => void;\n private detachResizeListener?: () => void;\n private worker?: Worker;\n private executor?: MainThreadWasmExecutor;\n private didMount = false;\n private suspended = 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 let pauseCell: SharedArrayBuffer | undefined;\n\n try {\n inputQueue = createSharedInputQueue();\n inputWriter = new SharedInputQueueWriter(inputQueue);\n pauseCell = createWasmPauseCell();\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 this.pauseCell = pauseCell;\n }\n\n protected override onRuntimeSuspensionChange(\n suspended: boolean\n ): void {\n this.suspended = suspended;\n if (this.pauseCell) {\n setWasmPauseCellPaused(this.pauseCell, suspended);\n }\n this.executor?.setSuspended(suspended);\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.SWIFTTUI_COLUMNS ?? \"0\") || 0;\n const initialRows = Number(this.bridge?.environment.SWIFTTUI_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 pauseCell: this.pauseCell,\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.setSuspended(this.suspended);\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
+ {"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\";\nimport { createWasmPauseCell, setWasmPauseCellPaused } from \"./WasmRuntimePause.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 pauseCell?: SharedArrayBuffer;\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): boolean };\n private readonly inputCapacityNotifier: {\n disposed: boolean;\n pending: boolean;\n };\n private readonly sharedQueueError?: unknown;\n private readonly pauseCell?: SharedArrayBuffer;\n\n private detachBridgeInputListener?: () => void;\n private detachResizeListener?: () => void;\n private worker?: Worker;\n private executor?: MainThreadWasmExecutor;\n private didMount = false;\n private suspended = 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 let pauseCell: SharedArrayBuffer | undefined;\n const inputCapacityNotifier = {\n disposed: false,\n pending: false,\n };\n\n try {\n inputQueue = createSharedInputQueue();\n inputWriter = new SharedInputQueueWriter(inputQueue);\n pauseCell = createWasmPauseCell();\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): boolean => {\n if (!inputWriter) {\n return false;\n }\n try {\n inputWriter.write(chunk);\n return true;\n } catch (error) {\n console.error(\"[SwiftTUIWeb] failed to enqueue terminal input\", error);\n if (!inputCapacityNotifier.pending) {\n inputCapacityNotifier.pending = true;\n void inputWriter.waitForCapacity(chunk.byteLength).then((available) => {\n inputCapacityNotifier.pending = false;\n if (available && !inputCapacityNotifier.disposed) {\n (options.bridge as BrowserWASIBridge | undefined)\n ?.notifyInputCapacityAvailable();\n }\n });\n }\n return false;\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.inputCapacityNotifier = inputCapacityNotifier;\n this.sharedQueueError = sharedQueueError;\n this.pauseCell = pauseCell;\n }\n\n protected override onRuntimeSuspensionChange(\n suspended: boolean\n ): void {\n this.suspended = suspended;\n if (this.pauseCell) {\n setWasmPauseCellPaused(this.pauseCell, suspended);\n }\n this.executor?.setSuspended(suspended);\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 return 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.SWIFTTUI_COLUMNS ?? \"0\") || 0;\n const initialRows = Number(this.bridge?.environment.SWIFTTUI_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 pauseCell: this.pauseCell,\n };\n this.worker.postMessage(message);\n }\n\n override dispose(): void {\n this.inputCapacityNotifier.disposed = true;\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) => {\n executor.sendInput(chunk);\n return true;\n };\n executor.setSuspended(this.suspended);\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,6 +1,6 @@
1
+ import { SharedInputQueueReader } from "./SharedInputQueue.js";
1
2
  import { WasiPollScheduler } from "./WasiPollScheduler.js";
2
3
  import { PausableMonotonicClock, WorkerWasmPauseGate, installPausableClockTimeGet } from "./WasmRuntimePause.js";
3
- import { SharedInputQueueReader } from "./SharedInputQueue.js";
4
4
  import { ConsoleStdout, Fd, WASI, wasi } from "@bjorn3/browser_wasi_shim";
5
5
  //#region src/wasi/WasmSceneWorker.ts
6
6
  function startWasmSceneWorker() {
package/dist/testing.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import { transportFixture } from "./src/WebHostTestFixtures.js";
2
- export { transportFixture };
2
+ import { CanvasSurfacePainterOptions } from "./src/CanvasSurfacePainter.js";
3
+ export { type CanvasSurfacePainterOptions, transportFixture };
package/dist/wasi.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
2
2
  import { classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
3
3
  import { encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
4
+ import { SharedInputQueueReader, SharedInputQueueWriter, createSharedInputQueue, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity } from "./src/wasi/SharedInputQueue.js";
4
5
  import { BrowserWASIBridge } from "./src/wasi/BrowserWASIBridge.js";
5
6
  import { MainThreadWasmPauseGate, PausableMonotonicClock, WorkerWasmPauseGate, createWasmPauseCell, installPausableClockTimeGet, isWasmPauseCellPaused, setWasmPauseCellPaused } from "./src/wasi/WasmRuntimePause.js";
6
7
  import { MainThreadWasmExecutor } from "./src/wasi/MainThreadWasmExecutor.js";
7
- import { SharedInputQueueReader, SharedInputQueueWriter, createSharedInputQueue, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity } from "./src/wasi/SharedInputQueue.js";
8
8
  import { createWasmSceneRuntimeFactory, resolveWasmExecutionMode } from "./src/wasi/WasmSceneRuntime.js";
9
9
  export { BrowserWASIBridge, MainThreadWasmExecutor, MainThreadWasmPauseGate, PausableMonotonicClock, SharedInputQueueReader, SharedInputQueueWriter, StdIOPipe, WorkerWasmPauseGate, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createSharedInputQueue, createWasmPauseCell, createWasmSceneRuntimeFactory, encodeRenderStyleControlMessage, encodeResizeControlMessage, hydrateSharedInputQueue, installPausableClockTimeGet, isWasmPauseCellPaused, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, resolveWasmExecutionMode, setWasmPauseCellPaused, sharedInputQueueDefaultCapacity, stackProfileEnvironmentDefaults };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swifttui/web",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,6 +55,7 @@
55
55
  "build:app": "bun run cli.ts build --dist dist-demo",
56
56
  "dev": "bun run cli.ts dev --dist dist-demo",
57
57
  "test": "bun test",
58
+ "typecheck:image-recovery": "tsc -p tsconfig.image-payload-recovery.json",
58
59
  "prepublishOnly": "tsdown"
59
60
  },
60
61
  "publishConfig": {