@swifttui/web 0.4.1 → 0.4.3

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 (35) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.js +2 -2
  3. package/dist/src/CanvasSurfacePainter.d.ts +16 -0
  4. package/dist/src/CanvasSurfacePainter.js +160 -21
  5. package/dist/src/CanvasSurfacePainter.js.map +1 -1
  6. package/dist/src/DomSurfacePainter.d.ts +17 -3
  7. package/dist/src/DomSurfacePainter.js +50 -5
  8. package/dist/src/DomSurfacePainter.js.map +1 -1
  9. package/dist/src/SurfacePainterConformanceControl.js +13 -0
  10. package/dist/src/SurfacePainterConformanceControl.js.map +1 -0
  11. package/dist/src/SurfaceRenderer.d.ts +3 -2
  12. package/dist/src/SurfaceRenderer.js.map +1 -1
  13. package/dist/src/WebHostSceneRuntime.d.ts +3 -1
  14. package/dist/src/WebHostSceneRuntime.js +10 -7
  15. package/dist/src/WebHostSceneRuntime.js.map +1 -1
  16. package/dist/src/WebHostSurfaceTransport.d.ts +61 -4
  17. package/dist/src/WebHostSurfaceTransport.js +150 -12
  18. package/dist/src/WebHostSurfaceTransport.js.map +1 -1
  19. package/dist/src/WebSocketSceneBridge.d.ts +2 -1
  20. package/dist/src/WebSocketSceneBridge.js +26 -11
  21. package/dist/src/WebSocketSceneBridge.js.map +1 -1
  22. package/dist/src/normalizeWireTokens.d.ts +5 -0
  23. package/dist/src/wasi/BrowserWASIBridge.d.ts +8 -0
  24. package/dist/src/wasi/BrowserWASIBridge.js +29 -7
  25. package/dist/src/wasi/BrowserWASIBridge.js.map +1 -1
  26. package/dist/src/wasi/SharedInputQueue.d.ts +61 -1
  27. package/dist/src/wasi/SharedInputQueue.js +108 -0
  28. package/dist/src/wasi/SharedInputQueue.js.map +1 -1
  29. package/dist/src/wasi/WasmSceneRuntime.js +38 -8
  30. package/dist/src/wasi/WasmSceneRuntime.js.map +1 -1
  31. package/dist/src/wasi/WasmSceneWorker.js +1 -1
  32. package/dist/testing.d.ts +2 -1
  33. package/dist/wasi.d.ts +2 -2
  34. package/dist/wasi.js +1 -1
  35. package/package.json +2 -1
@@ -25,6 +25,7 @@ declare class BrowserWASIBridge {
25
25
  readonly environment: Record<string, string>;
26
26
  private detachStdout?;
27
27
  private detachStderr?;
28
+ private decoder;
28
29
  private readonly resizeListeners;
29
30
  private latestResize;
30
31
  constructor(options: BrowserWASIBridgeOptions);
@@ -32,7 +33,14 @@ declare class BrowserWASIBridge {
32
33
  resize(columns: number, rows: number, cellWidth?: number, cellHeight?: number): void;
33
34
  updateRenderStyle(style: WebHostTerminalStyle): void;
34
35
  sendInput(chunk: Uint8Array): void;
36
+ requestImagePayloads(ids: readonly string[]): readonly string[];
37
+ /**
38
+ * Called by the WASI runtime when its shared stdin queue has enough capacity
39
+ * for the delivery that previously failed.
40
+ */
41
+ notifyInputCapacityAvailable(): void;
35
42
  subscribeResize(listener: (columns: number, rows: number, cellWidth?: number, cellHeight?: number) => void): () => void;
43
+ private sendPendingResyncRequests;
36
44
  dispose(): void;
37
45
  }
38
46
  //#endregion
@@ -2,7 +2,9 @@ import { StdIOPipe } from "./StdIOPipe.js";
2
2
  import { resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./WasmEngineCapabilities.js";
3
3
  import { encodeWebHostTerminalRenderStyleBase64 } from "../WebHostTerminalStyle.js";
4
4
  import { WebHostOutputDecoder, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeResyncControlMessage } from "../WebHostSurfaceTransport.js";
5
+ import { sharedInputQueueDefaultCapacity } from "./SharedInputQueue.js";
5
6
  //#region src/wasi/BrowserWASIBridge.ts
7
+ const maximumWASIResyncControlBytes = sharedInputQueueDefaultCapacity - 1;
6
8
  var BrowserWASIBridge = class {
7
9
  stdin = new StdIOPipe();
8
10
  stdout = new StdIOPipe();
@@ -10,6 +12,7 @@ var BrowserWASIBridge = class {
10
12
  environment;
11
13
  detachStdout;
12
14
  detachStderr;
15
+ decoder = new WebHostOutputDecoder();
13
16
  resizeListeners = /* @__PURE__ */ new Set();
14
17
  latestResize;
15
18
  constructor(options) {
@@ -33,11 +36,11 @@ var BrowserWASIBridge = class {
33
36
  bindOutput(sink) {
34
37
  this.detachStdout?.();
35
38
  this.detachStderr?.();
36
- const decoder = new WebHostOutputDecoder();
39
+ this.decoder = new WebHostOutputDecoder();
37
40
  this.detachStdout = this.stdout.subscribe((chunk) => {
38
- for (const record of decoder.feed(chunk)) switch (record.type) {
41
+ for (const record of this.decoder.feed(chunk)) switch (record.type) {
39
42
  case "surface":
40
- sink.presentSurface(record.frame);
43
+ sink.presentSurface(record.frame, this.decoder.prepareToPresentSurface(record.frame));
41
44
  break;
42
45
  case "clipboard":
43
46
  sink.writeClipboard?.(record.text);
@@ -53,10 +56,7 @@ var BrowserWASIBridge = class {
53
56
  sink.writeOutput?.(record.text);
54
57
  break;
55
58
  }
56
- const request = decoder.takeResyncRequest();
57
- if (request) {
58
- if (!this.stdin.write(encodeResyncControlMessage(request))) decoder.resyncRequestDeliveryFailed(request);
59
- }
59
+ this.sendPendingResyncRequests();
60
60
  });
61
61
  this.detachStderr = this.stderr.subscribe((chunk) => {
62
62
  sink.writeError?.(new TextDecoder().decode(chunk));
@@ -83,6 +83,18 @@ var BrowserWASIBridge = class {
83
83
  sendInput(chunk) {
84
84
  this.stdin.write(chunk);
85
85
  }
86
+ requestImagePayloads(ids) {
87
+ const acceptedIds = this.decoder.requestImagePayloads(ids);
88
+ this.sendPendingResyncRequests();
89
+ return acceptedIds;
90
+ }
91
+ /**
92
+ * Called by the WASI runtime when its shared stdin queue has enough capacity
93
+ * for the delivery that previously failed.
94
+ */
95
+ notifyInputCapacityAvailable() {
96
+ this.sendPendingResyncRequests();
97
+ }
86
98
  subscribeResize(listener) {
87
99
  this.resizeListeners.add(listener);
88
100
  listener(this.latestResize.columns, this.latestResize.rows, this.latestResize.cellWidth, this.latestResize.cellHeight);
@@ -90,6 +102,16 @@ var BrowserWASIBridge = class {
90
102
  this.resizeListeners.delete(listener);
91
103
  };
92
104
  }
105
+ sendPendingResyncRequests() {
106
+ while (true) {
107
+ const request = this.decoder.takeResyncRequest(maximumWASIResyncControlBytes);
108
+ if (!request) return;
109
+ if (!this.stdin.write(encodeResyncControlMessage(request))) {
110
+ this.decoder.resyncRequestDeliveryFailed(request);
111
+ return;
112
+ }
113
+ }
114
+ }
93
115
  dispose() {
94
116
  this.detachStdout?.();
95
117
  this.detachStderr?.();
@@ -1 +1 @@
1
- {"version":3,"file":"BrowserWASIBridge.js","names":[],"sources":["../../../src/wasi/BrowserWASIBridge.ts"],"sourcesContent":["import { StdIOPipe } from \"./StdIOPipe.ts\";\nimport {\n resolveWasmEngineCapabilities,\n stackProfileEnvironmentDefaults,\n type WasmEngineCapabilities,\n} from \"./WasmEngineCapabilities.ts\";\nimport {\n encodeWebHostTerminalRenderStyleBase64,\n type WebHostTerminalStyle,\n} from \"../WebHostTerminalStyle.ts\";\nimport {\n WebHostOutputDecoder,\n encodeResyncControlMessage,\n encodeRenderStyleControlMessage,\n encodeResizeControlMessage,\n type WebHostOutputSink,\n} from \"../WebHostSurfaceTransport.ts\";\n\nexport interface BrowserWASIBridgeOptions {\n sceneId: string;\n columns: number;\n rows: number;\n environment?: Record<string, string>;\n renderStyle?: WebHostTerminalStyle;\n /**\n * Detected engine capabilities driving engine-conditional environment\n * defaults (e.g. disabling the stack-lean resolve profile on V8). Defaults\n * to probing the current engine; injectable for tests and embedders that\n * want to force a profile.\n */\n engineCapabilities?: WasmEngineCapabilities;\n}\n\nexport type BrowserWASIOutputSink = WebHostOutputSink;\n\nexport class BrowserWASIBridge {\n readonly stdin = new StdIOPipe();\n readonly stdout = new StdIOPipe();\n readonly stderr = new StdIOPipe();\n readonly environment: Record<string, string>;\n\n private detachStdout?: () => void;\n private detachStderr?: () => void;\n private readonly resizeListeners = new Set<(\n columns: number,\n rows: number,\n cellWidth?: number,\n cellHeight?: number\n ) => void>();\n private latestResize: {\n columns: number;\n rows: number;\n cellWidth?: number;\n cellHeight?: number;\n };\n\n constructor(options: BrowserWASIBridgeOptions) {\n this.environment = {\n SWIFTTUI_MODE: \"browser\",\n SWIFTTUI_TRANSPORT: \"surface\",\n SWIFTTUI_SURFACE_DELTA: \"1\",\n SWIFTTUI_SCENE: options.sceneId,\n SWIFTTUI_COLUMNS: String(Math.max(1, options.columns)),\n SWIFTTUI_ROWS: String(Math.max(1, options.rows)),\n // Browser default (2026-07, engine-blind): the single-threaded WASI\n // drive surfaces tick invalidations exactly where the async driver's\n // supersession predicate samples, so the default `async` disposal\n // (completed-frame visual-only drops + pre-start cancels) coalesces\n // steady scenes to ~1 wire frame per 3+ generations — the 0.1.9 live\n // regression. `async-no-cancel` keeps scheduler intent-merging but\n // commits every completed frame; measured 0.22 → 0.86 distinct-\n // generation coverage on the deployed Life scene, both execution\n // modes, with per-frame cost unchanged. Live lean sessions measure\n // zero drops/cancels, so the default is safe engine-blind. Callers\n // (and the `?renderMode=` page seam) override via\n // `options.environment`; rollback is this one line.\n SWIFTTUI_RENDER_MODE: \"async-no-cancel\",\n ...stackProfileEnvironmentDefaults(\n options.engineCapabilities ?? resolveWasmEngineCapabilities()\n ),\n ...options.environment,\n ...(options.renderStyle\n ? {\n SWIFTTUI_RENDER_STYLE: encodeWebHostTerminalRenderStyleBase64(\n options.renderStyle\n ),\n }\n : {}),\n };\n this.latestResize = {\n columns: Math.max(1, options.columns),\n rows: Math.max(1, options.rows),\n };\n }\n\n bindOutput(\n sink: BrowserWASIOutputSink\n ): void {\n this.detachStdout?.();\n this.detachStderr?.();\n const decoder = new WebHostOutputDecoder();\n this.detachStdout = this.stdout.subscribe((chunk) => {\n for (const record of decoder.feed(chunk)) {\n switch (record.type) {\n case \"surface\":\n sink.presentSurface(record.frame);\n break;\n case \"clipboard\":\n void sink.writeClipboard?.(record.text);\n break;\n case \"runtimeIssue\":\n sink.notifyRuntimeIssue?.(record.issue);\n break;\n case \"frameDiagnostic\":\n sink.recordFrameDiagnostic?.(record.diagnostic);\n break;\n case \"surfaceDropped\":\n break;\n case \"text\":\n sink.writeOutput?.(record.text);\n break;\n }\n }\n const request = decoder.takeResyncRequest();\n if (request) {\n const accepted = this.stdin.write(encodeResyncControlMessage(request));\n if (!accepted) {\n decoder.resyncRequestDeliveryFailed(request);\n }\n }\n });\n this.detachStderr = this.stderr.subscribe((chunk) => {\n sink.writeError?.(new TextDecoder().decode(chunk));\n });\n }\n\n resize(\n columns: number,\n rows: number,\n cellWidth?: number,\n cellHeight?: number\n ): void {\n const normalizedColumns = Math.max(1, columns);\n const normalizedRows = Math.max(1, rows);\n this.environment.SWIFTTUI_COLUMNS = String(normalizedColumns);\n this.environment.SWIFTTUI_ROWS = String(normalizedRows);\n this.latestResize = {\n columns: normalizedColumns,\n rows: normalizedRows,\n cellWidth,\n cellHeight,\n };\n this.stdin.write(encodeResizeControlMessage(columns, rows, cellWidth, cellHeight));\n for (const listener of this.resizeListeners) {\n listener(normalizedColumns, normalizedRows, cellWidth, cellHeight);\n }\n }\n\n updateRenderStyle(\n style: WebHostTerminalStyle\n ): void {\n this.environment.SWIFTTUI_RENDER_STYLE = encodeWebHostTerminalRenderStyleBase64(style);\n this.stdin.write(encodeRenderStyleControlMessage(style));\n }\n\n sendInput(\n chunk: Uint8Array\n ): void {\n this.stdin.write(chunk);\n }\n\n subscribeResize(\n listener: (\n columns: number,\n rows: number,\n cellWidth?: number,\n cellHeight?: number\n ) => void\n ): () => void {\n this.resizeListeners.add(listener);\n listener(\n this.latestResize.columns,\n this.latestResize.rows,\n this.latestResize.cellWidth,\n this.latestResize.cellHeight\n );\n return () => {\n this.resizeListeners.delete(listener);\n };\n }\n\n dispose(): void {\n this.detachStdout?.();\n this.detachStderr?.();\n this.resizeListeners.clear();\n this.stdin.close();\n this.stdout.close();\n this.stderr.close();\n }\n}\n\nexport {\n encodeRenderStyleControlMessage,\n encodeResizeControlMessage,\n};\n"],"mappings":";;;;;AAmCA,IAAa,oBAAb,MAA+B;CAC7B,QAAiB,IAAI,UAAU;CAC/B,SAAkB,IAAI,UAAU;CAChC,SAAkB,IAAI,UAAU;CAChC;CAEA;CACA;CACA,kCAAmC,IAAI,IAK5B;CACX;CAOA,YAAY,SAAmC;EAC7C,KAAK,cAAc;GACjB,eAAe;GACf,oBAAoB;GACpB,wBAAwB;GACxB,gBAAgB,QAAQ;GACxB,kBAAkB,OAAO,KAAK,IAAI,GAAG,QAAQ,OAAO,CAAC;GACrD,eAAe,OAAO,KAAK,IAAI,GAAG,QAAQ,IAAI,CAAC;GAa/C,sBAAsB;GACtB,GAAG,gCACD,QAAQ,sBAAsB,8BAA8B,CAC9D;GACA,GAAG,QAAQ;GACX,GAAI,QAAQ,cACR,EACE,uBAAuB,uCACrB,QAAQ,WACV,EACF,IACA,CAAC;EACP;EACA,KAAK,eAAe;GAClB,SAAS,KAAK,IAAI,GAAG,QAAQ,OAAO;GACpC,MAAM,KAAK,IAAI,GAAG,QAAQ,IAAI;EAChC;CACF;CAEA,WACE,MACM;EACN,KAAK,eAAe;EACpB,KAAK,eAAe;EACpB,MAAM,UAAU,IAAI,qBAAqB;EACzC,KAAK,eAAe,KAAK,OAAO,WAAW,UAAU;GACnD,KAAK,MAAM,UAAU,QAAQ,KAAK,KAAK,GACrC,QAAQ,OAAO,MAAf;IACA,KAAK;KACH,KAAK,eAAe,OAAO,KAAK;KAChC;IACF,KAAK;KACH,KAAU,iBAAiB,OAAO,IAAI;KACtC;IACF,KAAK;KACH,KAAK,qBAAqB,OAAO,KAAK;KACtC;IACF,KAAK;KACH,KAAK,wBAAwB,OAAO,UAAU;KAC9C;IACF,KAAK,kBACH;IACF,KAAK;KACH,KAAK,cAAc,OAAO,IAAI;KAC9B;GACF;GAEF,MAAM,UAAU,QAAQ,kBAAkB;GAC1C,IAAI,SAEE;QAAA,CADa,KAAK,MAAM,MAAM,2BAA2B,OAAO,CACxD,GACV,QAAQ,4BAA4B,OAAO;GAAA;EAGjD,CAAC;EACD,KAAK,eAAe,KAAK,OAAO,WAAW,UAAU;GACnD,KAAK,aAAa,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,CAAC;EACnD,CAAC;CACH;CAEA,OACE,SACA,MACA,WACA,YACM;EACN,MAAM,oBAAoB,KAAK,IAAI,GAAG,OAAO;EAC7C,MAAM,iBAAiB,KAAK,IAAI,GAAG,IAAI;EACvC,KAAK,YAAY,mBAAmB,OAAO,iBAAiB;EAC5D,KAAK,YAAY,gBAAgB,OAAO,cAAc;EACtD,KAAK,eAAe;GAClB,SAAS;GACT,MAAM;GACN;GACA;EACF;EACA,KAAK,MAAM,MAAM,2BAA2B,SAAS,MAAM,WAAW,UAAU,CAAC;EACjF,KAAK,MAAM,YAAY,KAAK,iBAC1B,SAAS,mBAAmB,gBAAgB,WAAW,UAAU;CAErE;CAEA,kBACE,OACM;EACN,KAAK,YAAY,wBAAwB,uCAAuC,KAAK;EACrF,KAAK,MAAM,MAAM,gCAAgC,KAAK,CAAC;CACzD;CAEA,UACE,OACM;EACN,KAAK,MAAM,MAAM,KAAK;CACxB;CAEA,gBACE,UAMY;EACZ,KAAK,gBAAgB,IAAI,QAAQ;EACjC,SACE,KAAK,aAAa,SAClB,KAAK,aAAa,MAClB,KAAK,aAAa,WAClB,KAAK,aAAa,UACpB;EACA,aAAa;GACX,KAAK,gBAAgB,OAAO,QAAQ;EACtC;CACF;CAEA,UAAgB;EACd,KAAK,eAAe;EACpB,KAAK,eAAe;EACpB,KAAK,gBAAgB,MAAM;EAC3B,KAAK,MAAM,MAAM;EACjB,KAAK,OAAO,MAAM;EAClB,KAAK,OAAO,MAAM;CACpB;AACF"}
1
+ {"version":3,"file":"BrowserWASIBridge.js","names":[],"sources":["../../../src/wasi/BrowserWASIBridge.ts"],"sourcesContent":["import { StdIOPipe } from \"./StdIOPipe.ts\";\nimport {\n resolveWasmEngineCapabilities,\n stackProfileEnvironmentDefaults,\n type WasmEngineCapabilities,\n} from \"./WasmEngineCapabilities.ts\";\nimport {\n encodeWebHostTerminalRenderStyleBase64,\n type WebHostTerminalStyle,\n} from \"../WebHostTerminalStyle.ts\";\nimport {\n WebHostOutputDecoder,\n encodeResyncControlMessage,\n encodeRenderStyleControlMessage,\n encodeResizeControlMessage,\n type WebHostOutputSink,\n} from \"../WebHostSurfaceTransport.ts\";\nimport { sharedInputQueueDefaultCapacity } from \"./SharedInputQueue.ts\";\n\nconst maximumWASIResyncControlBytes = sharedInputQueueDefaultCapacity - 1;\n\nexport interface BrowserWASIBridgeOptions {\n sceneId: string;\n columns: number;\n rows: number;\n environment?: Record<string, string>;\n renderStyle?: WebHostTerminalStyle;\n /**\n * Detected engine capabilities driving engine-conditional environment\n * defaults (e.g. disabling the stack-lean resolve profile on V8). Defaults\n * to probing the current engine; injectable for tests and embedders that\n * want to force a profile.\n */\n engineCapabilities?: WasmEngineCapabilities;\n}\n\nexport type BrowserWASIOutputSink = WebHostOutputSink;\n\nexport class BrowserWASIBridge {\n readonly stdin = new StdIOPipe();\n readonly stdout = new StdIOPipe();\n readonly stderr = new StdIOPipe();\n readonly environment: Record<string, string>;\n\n private detachStdout?: () => void;\n private detachStderr?: () => void;\n private decoder = new WebHostOutputDecoder();\n private readonly resizeListeners = new Set<(\n columns: number,\n rows: number,\n cellWidth?: number,\n cellHeight?: number\n ) => void>();\n private latestResize: {\n columns: number;\n rows: number;\n cellWidth?: number;\n cellHeight?: number;\n };\n\n constructor(options: BrowserWASIBridgeOptions) {\n this.environment = {\n SWIFTTUI_MODE: \"browser\",\n SWIFTTUI_TRANSPORT: \"surface\",\n SWIFTTUI_SURFACE_DELTA: \"1\",\n SWIFTTUI_SCENE: options.sceneId,\n SWIFTTUI_COLUMNS: String(Math.max(1, options.columns)),\n SWIFTTUI_ROWS: String(Math.max(1, options.rows)),\n // Browser default (2026-07, engine-blind): the single-threaded WASI\n // drive surfaces tick invalidations exactly where the async driver's\n // supersession predicate samples, so the default `async` disposal\n // (completed-frame visual-only drops + pre-start cancels) coalesces\n // steady scenes to ~1 wire frame per 3+ generations — the 0.1.9 live\n // regression. `async-no-cancel` keeps scheduler intent-merging but\n // commits every completed frame; measured 0.22 → 0.86 distinct-\n // generation coverage on the deployed Life scene, both execution\n // modes, with per-frame cost unchanged. Live lean sessions measure\n // zero drops/cancels, so the default is safe engine-blind. Callers\n // (and the `?renderMode=` page seam) override via\n // `options.environment`; rollback is this one line.\n SWIFTTUI_RENDER_MODE: \"async-no-cancel\",\n ...stackProfileEnvironmentDefaults(\n options.engineCapabilities ?? resolveWasmEngineCapabilities()\n ),\n ...options.environment,\n ...(options.renderStyle\n ? {\n SWIFTTUI_RENDER_STYLE: encodeWebHostTerminalRenderStyleBase64(\n options.renderStyle\n ),\n }\n : {}),\n };\n this.latestResize = {\n columns: Math.max(1, options.columns),\n rows: Math.max(1, options.rows),\n };\n }\n\n bindOutput(\n sink: BrowserWASIOutputSink\n ): void {\n this.detachStdout?.();\n this.detachStderr?.();\n this.decoder = new WebHostOutputDecoder();\n this.detachStdout = this.stdout.subscribe((chunk) => {\n for (const record of this.decoder.feed(chunk)) {\n switch (record.type) {\n case \"surface\":\n sink.presentSurface(\n record.frame,\n this.decoder.prepareToPresentSurface(record.frame)\n );\n break;\n case \"clipboard\":\n void sink.writeClipboard?.(record.text);\n break;\n case \"runtimeIssue\":\n sink.notifyRuntimeIssue?.(record.issue);\n break;\n case \"frameDiagnostic\":\n sink.recordFrameDiagnostic?.(record.diagnostic);\n break;\n case \"surfaceDropped\":\n break;\n case \"text\":\n sink.writeOutput?.(record.text);\n break;\n }\n }\n this.sendPendingResyncRequests();\n });\n this.detachStderr = this.stderr.subscribe((chunk) => {\n sink.writeError?.(new TextDecoder().decode(chunk));\n });\n }\n\n resize(\n columns: number,\n rows: number,\n cellWidth?: number,\n cellHeight?: number\n ): void {\n const normalizedColumns = Math.max(1, columns);\n const normalizedRows = Math.max(1, rows);\n this.environment.SWIFTTUI_COLUMNS = String(normalizedColumns);\n this.environment.SWIFTTUI_ROWS = String(normalizedRows);\n this.latestResize = {\n columns: normalizedColumns,\n rows: normalizedRows,\n cellWidth,\n cellHeight,\n };\n this.stdin.write(encodeResizeControlMessage(columns, rows, cellWidth, cellHeight));\n for (const listener of this.resizeListeners) {\n listener(normalizedColumns, normalizedRows, cellWidth, cellHeight);\n }\n }\n\n updateRenderStyle(\n style: WebHostTerminalStyle\n ): void {\n this.environment.SWIFTTUI_RENDER_STYLE = encodeWebHostTerminalRenderStyleBase64(style);\n this.stdin.write(encodeRenderStyleControlMessage(style));\n }\n\n sendInput(\n chunk: Uint8Array\n ): void {\n this.stdin.write(chunk);\n }\n\n requestImagePayloads(\n ids: readonly string[]\n ): readonly string[] {\n const acceptedIds = this.decoder.requestImagePayloads(ids);\n this.sendPendingResyncRequests();\n return acceptedIds;\n }\n\n /**\n * Called by the WASI runtime when its shared stdin queue has enough capacity\n * for the delivery that previously failed.\n */\n notifyInputCapacityAvailable(): void {\n this.sendPendingResyncRequests();\n }\n\n subscribeResize(\n listener: (\n columns: number,\n rows: number,\n cellWidth?: number,\n cellHeight?: number\n ) => void\n ): () => void {\n this.resizeListeners.add(listener);\n listener(\n this.latestResize.columns,\n this.latestResize.rows,\n this.latestResize.cellWidth,\n this.latestResize.cellHeight\n );\n return () => {\n this.resizeListeners.delete(listener);\n };\n }\n\n private sendPendingResyncRequests(): void {\n while (true) {\n const request = this.decoder.takeResyncRequest(\n maximumWASIResyncControlBytes\n );\n if (!request) {\n return;\n }\n const accepted = this.stdin.write(encodeResyncControlMessage(request));\n if (!accepted) {\n this.decoder.resyncRequestDeliveryFailed(request);\n return;\n }\n }\n }\n\n dispose(): void {\n this.detachStdout?.();\n this.detachStderr?.();\n this.resizeListeners.clear();\n this.stdin.close();\n this.stdout.close();\n this.stderr.close();\n }\n}\n\nexport {\n encodeRenderStyleControlMessage,\n encodeResizeControlMessage,\n};\n"],"mappings":"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"}
@@ -5,6 +5,29 @@ interface SharedInputQueueBuffers {
5
5
  readonly dataBuffer: SharedArrayBuffer;
6
6
  }
7
7
  type SharedInputReadiness = "readable" | "closed" | "timedOut";
8
+ /**
9
+ * The outcome of one logical `writeAsync`.
10
+ *
11
+ * `partial` carries how many bytes reached the ring before the deadline. It is
12
+ * distinct from `timedOut`-with-nothing-written because a caller reporting a
13
+ * dropped paste wants to say whether the app saw part of it.
14
+ */
15
+ type SharedInputWriteOutcome = {
16
+ readonly status: "written";
17
+ } | {
18
+ readonly status: "closed";
19
+ readonly bytesWritten: number;
20
+ } | {
21
+ readonly status: "partial";
22
+ readonly bytesWritten: number;
23
+ readonly bytesRemaining: number;
24
+ };
25
+ interface SharedInputWriteOptions {
26
+ /** Total budget for the whole logical write. Defaults to 500 ms. */
27
+ readonly deadlineMilliseconds?: number;
28
+ /** Injectable clock, so deadline behavior is testable without wall time. */
29
+ readonly now?: () => number;
30
+ }
8
31
  interface SharedInputQueueState {
9
32
  readonly control: Int32Array;
10
33
  readonly data: Uint8Array;
@@ -13,8 +36,45 @@ declare function createSharedInputQueue(capacity?: number): SharedInputQueueBuff
13
36
  declare function hydrateSharedInputQueue(buffers: SharedInputQueueBuffers): SharedInputQueueState;
14
37
  declare class SharedInputQueueWriter {
15
38
  private readonly queue;
39
+ /**
40
+ * Serializes `writeAsync` calls. A chunked write suspends while the reader
41
+ * drains, so two concurrent logical writes would otherwise interleave their
42
+ * segments in the ring and corrupt both records.
43
+ */
44
+ private writeChain;
45
+ /**
46
+ * How many logical writes are queued or in flight. A write must join the
47
+ * chain whenever one is already pending, or a small later chunk could
48
+ * overtake an earlier chunked one and land out of order.
49
+ */
50
+ private pendingWrites;
16
51
  constructor(buffers: SharedInputQueueBuffers);
52
+ /**
53
+ * Streams one logical write into the ring, in as many segments as the reader's
54
+ * drain rate requires.
55
+ *
56
+ * A single `write` can only ever enqueue what currently fits, so a paste
57
+ * larger than the free space failed outright and the whole clipboard was lost.
58
+ * Here the write takes `min(free, remaining)` bytes at a time and awaits
59
+ * capacity in between, so a paste larger than the ring streams through it
60
+ * while the worker drains. No record shape changes: the bytes arrive in
61
+ * order, so a bracketed paste is still one paste.
62
+ *
63
+ * Never blocks: this runs on the main thread, where `Atomics.wait` is
64
+ * forbidden, so it awaits the reader's notification instead. Each wait is
65
+ * capped at 50 ms (or whatever is left of the deadline) so a missed
66
+ * notification costs one bounded recheck rather than a hang, and the whole
67
+ * write is bounded by a 500 ms deadline.
68
+ */
69
+ writeAsync(chunk: Uint8Array | string, options?: SharedInputWriteOptions): Promise<SharedInputWriteOutcome>;
70
+ private performChunkedWrite;
71
+ private writeSegment;
17
72
  write(chunk: Uint8Array | string): void;
73
+ availableCapacity(): number;
74
+ waitForCapacity(minimumBytes: number, options?: {
75
+ readonly timeoutMilliseconds?: number;
76
+ readonly singleWait?: boolean;
77
+ }): Promise<boolean>;
18
78
  close(): void;
19
79
  }
20
80
  declare class SharedInputQueueReader {
@@ -27,5 +87,5 @@ declare class SharedInputQueueReader {
27
87
  isClosed(): boolean;
28
88
  }
29
89
  //#endregion
30
- export { SharedInputQueueBuffers, SharedInputQueueReader, SharedInputQueueWriter, SharedInputReadiness, createSharedInputQueue, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity };
90
+ export { SharedInputQueueBuffers, SharedInputQueueReader, SharedInputQueueWriter, SharedInputReadiness, SharedInputWriteOptions, SharedInputWriteOutcome, createSharedInputQueue, hydrateSharedInputQueue, sharedInputQueueDefaultCapacity };
31
91
  //# sourceMappingURL=SharedInputQueue.d.ts.map
@@ -1,5 +1,7 @@
1
1
  //#region src/wasi/SharedInputQueue.ts
2
2
  const controlSlots = 3;
3
+ const capacityWaitTimeoutMilliseconds = 50;
4
+ const writeDeadlineMilliseconds = 500;
3
5
  const sharedInputQueueDefaultCapacity = 64 * 1024;
4
6
  function createSharedInputQueue(capacity = sharedInputQueueDefaultCapacity) {
5
7
  if (typeof SharedArrayBuffer === "undefined") throw new Error("SharedArrayBuffer is unavailable. Serve the app with COOP/COEP headers so browser WASI stdin can stay live.");
@@ -17,9 +19,92 @@ function hydrateSharedInputQueue(buffers) {
17
19
  }
18
20
  var SharedInputQueueWriter = class {
19
21
  queue;
22
+ /**
23
+ * Serializes `writeAsync` calls. A chunked write suspends while the reader
24
+ * drains, so two concurrent logical writes would otherwise interleave their
25
+ * segments in the ring and corrupt both records.
26
+ */
27
+ writeChain = Promise.resolve();
28
+ /**
29
+ * How many logical writes are queued or in flight. A write must join the
30
+ * chain whenever one is already pending, or a small later chunk could
31
+ * overtake an earlier chunked one and land out of order.
32
+ */
33
+ pendingWrites = 0;
20
34
  constructor(buffers) {
21
35
  this.queue = hydrateSharedInputQueue(buffers);
22
36
  }
37
+ /**
38
+ * Streams one logical write into the ring, in as many segments as the reader's
39
+ * drain rate requires.
40
+ *
41
+ * A single `write` can only ever enqueue what currently fits, so a paste
42
+ * larger than the free space failed outright and the whole clipboard was lost.
43
+ * Here the write takes `min(free, remaining)` bytes at a time and awaits
44
+ * capacity in between, so a paste larger than the ring streams through it
45
+ * while the worker drains. No record shape changes: the bytes arrive in
46
+ * order, so a bracketed paste is still one paste.
47
+ *
48
+ * Never blocks: this runs on the main thread, where `Atomics.wait` is
49
+ * forbidden, so it awaits the reader's notification instead. Each wait is
50
+ * capped at 50 ms (or whatever is left of the deadline) so a missed
51
+ * notification costs one bounded recheck rather than a hang, and the whole
52
+ * write is bounded by a 500 ms deadline.
53
+ */
54
+ writeAsync(chunk, options = {}) {
55
+ const bytes = normalizeChunk(chunk);
56
+ if (bytes.length == 0) return Promise.resolve({ status: "written" });
57
+ if (Atomics.load(this.queue.control, 2) !== 0) return Promise.resolve({
58
+ status: "closed",
59
+ bytesWritten: 0
60
+ });
61
+ if (this.pendingWrites === 0 && bytes.length <= this.availableCapacity()) {
62
+ this.writeSegment(bytes);
63
+ return Promise.resolve({ status: "written" });
64
+ }
65
+ this.pendingWrites += 1;
66
+ const attempt = this.writeChain.then(() => this.performChunkedWrite(bytes, options), () => this.performChunkedWrite(bytes, options));
67
+ this.writeChain = attempt;
68
+ return attempt.finally(() => {
69
+ this.pendingWrites -= 1;
70
+ });
71
+ }
72
+ async performChunkedWrite(bytes, options) {
73
+ const now = options.now ?? (() => Date.now());
74
+ const deadline = now() + Math.max(0, options.deadlineMilliseconds ?? writeDeadlineMilliseconds);
75
+ let written = 0;
76
+ while (written < bytes.length) {
77
+ if (Atomics.load(this.queue.control, 2) !== 0) return {
78
+ status: "closed",
79
+ bytesWritten: written
80
+ };
81
+ const free = this.availableCapacity();
82
+ if (free > 0) {
83
+ const segment = Math.min(free, bytes.length - written);
84
+ this.writeSegment(bytes.subarray(written, written + segment));
85
+ written += segment;
86
+ continue;
87
+ }
88
+ const remainingBudget = deadline - now();
89
+ if (remainingBudget <= 0) return {
90
+ status: "partial",
91
+ bytesWritten: written,
92
+ bytesRemaining: bytes.length - written
93
+ };
94
+ await this.waitForCapacity(1, {
95
+ timeoutMilliseconds: Math.min(capacityWaitTimeoutMilliseconds, remainingBudget),
96
+ singleWait: true
97
+ });
98
+ }
99
+ return { status: "written" };
100
+ }
101
+ writeSegment(segment) {
102
+ const length = this.queue.data.length;
103
+ const writeIndex = Atomics.load(this.queue.control, 1);
104
+ writeToRingBuffer(this.queue.data, segment, writeIndex);
105
+ Atomics.store(this.queue.control, 1, ringAdvance(writeIndex, segment.length, length));
106
+ Atomics.notify(this.queue.control, 1);
107
+ }
23
108
  write(chunk) {
24
109
  if (Atomics.load(this.queue.control, 2) !== 0) return;
25
110
  const bytes = normalizeChunk(chunk);
@@ -33,9 +118,31 @@ var SharedInputQueueWriter = class {
33
118
  Atomics.store(this.queue.control, 1, ringAdvance(writeIndex, bytes.length, length));
34
119
  Atomics.notify(this.queue.control, 1);
35
120
  }
121
+ availableCapacity() {
122
+ const length = this.queue.data.length;
123
+ return length - ringUsed(Atomics.load(this.queue.control, 0), Atomics.load(this.queue.control, 1), length);
124
+ }
125
+ async waitForCapacity(minimumBytes, options = {}) {
126
+ const required = Math.max(0, Math.ceil(minimumBytes));
127
+ if (required > this.queue.data.length) return false;
128
+ const timeout = options.timeoutMilliseconds ?? capacityWaitTimeoutMilliseconds;
129
+ while (true) {
130
+ const readIndex = Atomics.load(this.queue.control, 0);
131
+ if (Atomics.load(this.queue.control, 2) !== 0) return false;
132
+ if (this.availableCapacity() >= required) return true;
133
+ if (typeof Atomics.waitAsync === "function") {
134
+ const waiting = Atomics.waitAsync(this.queue.control, 0, readIndex, timeout);
135
+ if (waiting.async) await waiting.value;
136
+ } else await new Promise((resolve) => {
137
+ setTimeout(resolve, 1);
138
+ });
139
+ if (options.singleWait) return this.availableCapacity() >= required;
140
+ }
141
+ }
36
142
  close() {
37
143
  Atomics.store(this.queue.control, 2, 1);
38
144
  Atomics.notify(this.queue.control, 1);
145
+ Atomics.notify(this.queue.control, 0);
39
146
  }
40
147
  };
41
148
  var SharedInputQueueReader = class {
@@ -61,6 +168,7 @@ var SharedInputQueueReader = class {
61
168
  const byteCount = Math.min(maxBytes, availableBytes);
62
169
  const chunk = readFromRingBuffer(this.queue.data, readIndex, byteCount);
63
170
  Atomics.store(this.queue.control, 0, ringAdvance(readIndex, byteCount, length));
171
+ Atomics.notify(this.queue.control, 0);
64
172
  return chunk;
65
173
  }
66
174
  availableBytes() {
@@ -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;\nconst writeDeadlineMilliseconds = 500;\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\n/**\n * The outcome of one logical `writeAsync`.\n *\n * `partial` carries how many bytes reached the ring before the deadline. It is\n * distinct from `timedOut`-with-nothing-written because a caller reporting a\n * dropped paste wants to say whether the app saw part of it.\n */\nexport type SharedInputWriteOutcome =\n | { readonly status: \"written\" }\n | { readonly status: \"closed\"; readonly bytesWritten: number }\n | { readonly status: \"partial\"; readonly bytesWritten: number; readonly bytesRemaining: number };\n\nexport interface SharedInputWriteOptions {\n /** Total budget for the whole logical write. Defaults to 500 ms. */\n readonly deadlineMilliseconds?: number;\n /** Injectable clock, so deadline behavior is testable without wall time. */\n readonly now?: () => number;\n}\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 * Serializes `writeAsync` calls. A chunked write suspends while the reader\n * drains, so two concurrent logical writes would otherwise interleave their\n * segments in the ring and corrupt both records.\n */\n private writeChain: Promise<unknown> = Promise.resolve();\n /**\n * How many logical writes are queued or in flight. A write must join the\n * chain whenever one is already pending, or a small later chunk could\n * overtake an earlier chunked one and land out of order.\n */\n private pendingWrites = 0;\n\n constructor(buffers: SharedInputQueueBuffers) {\n this.queue = hydrateSharedInputQueue(buffers);\n }\n\n /**\n * Streams one logical write into the ring, in as many segments as the reader's\n * drain rate requires.\n *\n * A single `write` can only ever enqueue what currently fits, so a paste\n * larger than the free space failed outright and the whole clipboard was lost.\n * Here the write takes `min(free, remaining)` bytes at a time and awaits\n * capacity in between, so a paste larger than the ring streams through it\n * while the worker drains. No record shape changes: the bytes arrive in\n * order, so a bracketed paste is still one paste.\n *\n * Never blocks: this runs on the main thread, where `Atomics.wait` is\n * forbidden, so it awaits the reader's notification instead. Each wait is\n * capped at 50 ms (or whatever is left of the deadline) so a missed\n * notification costs one bounded recheck rather than a hang, and the whole\n * write is bounded by a 500 ms deadline.\n */\n writeAsync(\n chunk: Uint8Array | string,\n options: SharedInputWriteOptions = {}\n ): Promise<SharedInputWriteOutcome> {\n const bytes = normalizeChunk(chunk);\n if (bytes.length == 0) {\n return Promise.resolve({ status: \"written\" });\n }\n if (Atomics.load(this.queue.control, ControlSlot.closed) !== 0) {\n return Promise.resolve({ status: \"closed\", bytesWritten: 0 });\n }\n\n // Fast path: with nothing queued ahead of it and room for the whole chunk,\n // the write lands synchronously. That keeps an ordinary keystroke exactly as\n // immediate as it was before chunking existed — only a write that cannot fit\n // pays for suspension.\n if (this.pendingWrites === 0 && bytes.length <= this.availableCapacity()) {\n this.writeSegment(bytes);\n return Promise.resolve({ status: \"written\" });\n }\n\n this.pendingWrites += 1;\n const attempt = this.writeChain.then(\n () => this.performChunkedWrite(bytes, options),\n () => this.performChunkedWrite(bytes, options)\n );\n this.writeChain = attempt;\n return attempt.finally(() => {\n this.pendingWrites -= 1;\n });\n }\n\n private async performChunkedWrite(\n bytes: Uint8Array,\n options: SharedInputWriteOptions\n ): Promise<SharedInputWriteOutcome> {\n const now = options.now ?? (() => Date.now());\n const deadline = now()\n + Math.max(0, options.deadlineMilliseconds ?? writeDeadlineMilliseconds);\n let written = 0;\n\n while (written < bytes.length) {\n if (Atomics.load(this.queue.control, ControlSlot.closed) !== 0) {\n return { status: \"closed\", bytesWritten: written };\n }\n\n const free = this.availableCapacity();\n if (free > 0) {\n const segment = Math.min(free, bytes.length - written);\n this.writeSegment(bytes.subarray(written, written + segment));\n written += segment;\n continue;\n }\n\n const remainingBudget = deadline - now();\n if (remainingBudget <= 0) {\n return {\n status: \"partial\",\n bytesWritten: written,\n bytesRemaining: bytes.length - written,\n };\n }\n // `singleWait` keeps the deadline in this loop: without it the helper\n // would spin internally until capacity arrived, ignoring the budget.\n await this.waitForCapacity(1, {\n timeoutMilliseconds: Math.min(capacityWaitTimeoutMilliseconds, remainingBudget),\n singleWait: true,\n });\n }\n\n return { status: \"written\" };\n }\n\n private writeSegment(\n segment: Uint8Array\n ): void {\n const length = this.queue.data.length;\n const writeIndex = Atomics.load(this.queue.control, ControlSlot.writeIndex);\n writeToRingBuffer(this.queue.data, segment, writeIndex);\n Atomics.store(\n this.queue.control,\n ControlSlot.writeIndex,\n ringAdvance(writeIndex, segment.length, length)\n );\n Atomics.notify(this.queue.control, ControlSlot.writeIndex);\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 options: { readonly timeoutMilliseconds?: number; readonly singleWait?: boolean } = {}\n ): Promise<boolean> {\n const required = Math.max(0, Math.ceil(minimumBytes));\n if (required > this.queue.data.length) {\n return false;\n }\n const timeout = options.timeoutMilliseconds ?? capacityWaitTimeoutMilliseconds;\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 timeout\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 // One bounded recheck and return, for callers that own the retry loop\n // themselves: a missed notification then costs a single capped wait\n // rather than spinning inside here.\n if (options.singleWait) {\n return this.availableCapacity() >= required;\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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"}
@@ -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);
@@ -47,20 +52,33 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
47
52
  } catch (error) {
48
53
  sharedQueueError = error;
49
54
  }
55
+ const overflowReporter = {};
56
+ const enqueueInput = (writer, chunk) => {
57
+ writer.writeAsync(chunk).then((outcome) => {
58
+ if (inputCapacityNotifier.disposed || outcome.status === "written") return;
59
+ if (outcome.status === "closed") return;
60
+ overflowReporter.report?.(outcome.bytesWritten, outcome.bytesRemaining);
61
+ });
62
+ if (!inputCapacityNotifier.pending) {
63
+ inputCapacityNotifier.pending = true;
64
+ writer.waitForCapacity(1).then((available) => {
65
+ inputCapacityNotifier.pending = false;
66
+ if (available && !inputCapacityNotifier.disposed) options.bridge?.notifyInputCapacityAvailable();
67
+ });
68
+ }
69
+ };
50
70
  const inputRouter = { route: (chunk) => {
51
71
  if (!inputWriter) return false;
52
- try {
53
- inputWriter.write(chunk);
54
- return true;
55
- } catch (error) {
56
- console.error("[SwiftTUIWeb] failed to enqueue terminal input", error);
57
- return false;
58
- }
72
+ enqueueInput(inputWriter, chunk);
73
+ return true;
59
74
  } };
60
75
  super({
61
76
  ...options,
62
77
  onInput: (chunk) => inputRouter.route(chunk)
63
78
  });
79
+ overflowReporter.report = (bytesWritten, bytesRemaining) => {
80
+ this.notifyInputOverflow(bytesWritten, bytesRemaining);
81
+ };
64
82
  this.bridge = options.bridge;
65
83
  this.wasmURL = wasmURL;
66
84
  this.onSceneResize = factoryOptions.onSceneResize;
@@ -69,9 +87,20 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
69
87
  this.inputQueue = inputQueue;
70
88
  this.inputWriter = inputWriter;
71
89
  this.inputRouter = inputRouter;
90
+ this.inputCapacityNotifier = inputCapacityNotifier;
72
91
  this.sharedQueueError = sharedQueueError;
73
92
  this.pauseCell = pauseCell;
74
93
  }
94
+ notifyInputOverflow(bytesWritten, bytesRemaining) {
95
+ const message = bytesWritten === 0 ? `Dropped ${bytesRemaining} byte(s) of terminal input: the app did not read from its input queue within 500 ms.` : `Delivered ${bytesWritten} byte(s) of terminal input and dropped ${bytesRemaining}: the app did not drain its input queue within 500 ms.`;
96
+ this.notifyRuntimeIssue({
97
+ severity: "warning",
98
+ code: "web.input.queueDeadlineExceeded",
99
+ message,
100
+ description: `SwiftTUI runtime warning [web.input.queueDeadlineExceeded] ${message}`,
101
+ source: "web-host"
102
+ });
103
+ }
75
104
  onRuntimeSuspensionChange(suspended) {
76
105
  this.suspended = suspended;
77
106
  if (this.pauseCell) setWasmPauseCellPaused(this.pauseCell, suspended);
@@ -131,6 +160,7 @@ var WasmSceneRuntime = class extends WebHostSceneRuntime {
131
160
  this.worker.postMessage(message);
132
161
  }
133
162
  dispose() {
163
+ this.inputCapacityNotifier.disposed = true;
134
164
  this.detachBridgeInputListener?.();
135
165
  this.detachResizeListener?.();
136
166
  this.inputWriter?.close();
@@ -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): boolean };\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): 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 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.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.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
+ {"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 // Input is streamed rather than all-or-nothing. A single ring write can\n // only enqueue what currently fits, so a paste larger than the free space\n // used to fail outright and drop the whole clipboard; `writeAsync` takes\n // `min(free, remaining)` bytes at a time and awaits the reader in between,\n // bounded by a 500 ms deadline. It never blocks — this is the main thread.\n // Assigned right after `super()`: `this` is unavailable until then, and the\n // reporter is only ever invoked from a settled promise afterwards.\n const overflowReporter: {\n report?: (bytesWritten: number, bytesRemaining: number) => void;\n } = {};\n\n const enqueueInput = (\n writer: SharedInputQueueWriter,\n chunk: Uint8Array\n ): void => {\n void writer.writeAsync(chunk).then((outcome) => {\n if (inputCapacityNotifier.disposed || outcome.status === \"written\") {\n return;\n }\n if (outcome.status === \"closed\") {\n return;\n }\n // Only a write that ran out of budget is reportable, and it is\n // reportable *into the app's mount*: silently losing the tail of a\n // paste is exactly the failure this stage exists to remove, so it must\n // not be console-only.\n overflowReporter.report?.(outcome.bytesWritten, outcome.bytesRemaining);\n });\n if (!inputCapacityNotifier.pending) {\n inputCapacityNotifier.pending = true;\n void writer.waitForCapacity(1).then((available) => {\n inputCapacityNotifier.pending = false;\n if (available && !inputCapacityNotifier.disposed) {\n (options.bridge as BrowserWASIBridge | undefined)\n ?.notifyInputCapacityAvailable();\n }\n });\n }\n };\n\n const inputRouter = {\n route: (chunk: Uint8Array): boolean => {\n if (!inputWriter) {\n return false;\n }\n enqueueInput(inputWriter, chunk);\n return true;\n },\n };\n\n super({\n ...options,\n onInput: (chunk) => inputRouter.route(chunk),\n });\n overflowReporter.report = (bytesWritten, bytesRemaining) => {\n this.notifyInputOverflow(bytesWritten, bytesRemaining);\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 /// Reports a logical input write that ran out of its deadline.\n ///\n /// Surfaced as a runtime issue rather than a console message: the tail of a\n /// paste going missing is a user-visible data loss, and the whole point of\n /// the chunked writer is that it should not happen silently.\n private notifyInputOverflow(\n bytesWritten: number,\n bytesRemaining: number\n ): void {\n const message = bytesWritten === 0\n ? `Dropped ${bytesRemaining} byte(s) of terminal input: the app did not read from its input queue within 500 ms.`\n : `Delivered ${bytesWritten} byte(s) of terminal input and dropped ${bytesRemaining}: the app did not drain its input queue within 500 ms.`;\n this.notifyRuntimeIssue({\n severity: \"warning\",\n code: \"web.input.queueDeadlineExceeded\",\n message,\n description: `SwiftTUI runtime warning [web.input.queueDeadlineExceeded] ${message}`,\n source: \"web-host\",\n });\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":";;;;;;AAsBA,MAAM,kBAAkB,IAAI,IAAI,0BAA0B,OAAO,KAAK,GAAG;AA2DzE,SAAgB,yBACd,YACA,cACA,2BACmB;CACnB,IAAI,eAAe,QACjB,OAAO;CAET,IAAI,CAAC,aAAa,cAChB,OAAO;CAOT,IAAI,CAAC,2BACH,OAAO;CAET,OAAO;AACT;AAEA,SAAgB,8BACd,SACA,iBAAiD,CAAC,GACY;CAC9D,QAAQ,YAAY;EAClB,MAAM,UAAU,IAAI,iBAAiB,SAAS,SAAS,cAAc;EACrE,eAAe,mBAAmB,OAAO;EACzC,OAAO;CACT;AACF;AAEA,IAAM,mBAAN,cAA+B,oBAAoB;CACjD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAIA;CACA;CAEA;CACA;CACA;CACA;CACA,WAAmB;CACnB,YAAoB;CAEpB,YACE,SACA,SACA,gBACA;EACA,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,MAAM,wBAAwB;GAC5B,UAAU;GACV,SAAS;EACX;EAEA,IAAI;GACF,aAAa,uBAAuB;GACpC,cAAc,IAAI,uBAAuB,UAAU;GACnD,YAAY,oBAAoB;EAClC,SAAS,OAAO;GAGd,mBAAmB;EACrB;EASA,MAAM,mBAEF,CAAC;EAEL,MAAM,gBACJ,QACA,UACS;GACT,OAAY,WAAW,KAAK,CAAC,CAAC,MAAM,YAAY;IAC9C,IAAI,sBAAsB,YAAY,QAAQ,WAAW,WACvD;IAEF,IAAI,QAAQ,WAAW,UACrB;IAMF,iBAAiB,SAAS,QAAQ,cAAc,QAAQ,cAAc;GACxE,CAAC;GACD,IAAI,CAAC,sBAAsB,SAAS;IAClC,sBAAsB,UAAU;IAChC,OAAY,gBAAgB,CAAC,CAAC,CAAC,MAAM,cAAc;KACjD,sBAAsB,UAAU;KAChC,IAAI,aAAa,CAAC,sBAAsB,UACtC,QAAS,QACL,6BAA6B;IAErC,CAAC;GACH;EACF;EAEA,MAAM,cAAc,EAClB,QAAQ,UAA+B;GACrC,IAAI,CAAC,aACH,OAAO;GAET,aAAa,aAAa,KAAK;GAC/B,OAAO;EACT,EACF;EAEA,MAAM;GACJ,GAAG;GACH,UAAU,UAAU,YAAY,MAAM,KAAK;EAC7C,CAAC;EACD,iBAAiB,UAAU,cAAc,mBAAmB;GAC1D,KAAK,oBAAoB,cAAc,cAAc;EACvD;EAEA,KAAK,SAAS,QAAQ;EACtB,KAAK,UAAU;EACf,KAAK,gBAAgB,eAAe;EACpC,KAAK,kBAAkB,eAAe,mBAAmB;EACzD,KAAK,0BAA0B,eAAe,iBAAiB;EAC/D,KAAK,aAAa;EAClB,KAAK,cAAc;EACnB,KAAK,cAAc;EACnB,KAAK,wBAAwB;EAC7B,KAAK,mBAAmB;EACxB,KAAK,YAAY;CACnB;CAOA,oBACE,cACA,gBACM;EACN,MAAM,UAAU,iBAAiB,IAC7B,WAAW,eAAe,wFAC1B,aAAa,aAAa,yCAAyC,eAAe;EACtF,KAAK,mBAAmB;GACtB,UAAU;GACV,MAAM;GACN;GACA,aAAa,8DAA8D;GAC3E,QAAQ;EACV,CAAC;CACH;CAEA,0BACE,WACM;EACN,KAAK,YAAY;EACjB,IAAI,KAAK,WACP,uBAAuB,KAAK,WAAW,SAAS;EAElD,KAAK,UAAU,aAAa,SAAS;CACvC;CAEA,MAAe,QAAuB;EACpC,MAAM,MAAM,MAAM;EAClB,IAAI,KAAK,UACP;EAGF,KAAK,WAAW;EAChB,KAAK,4BAA4B,KAAK,QAAQ,MAAM,WAAW,UAAU;GACvE,OAAO,KAAK,YAAY,MAAM,KAAK;EACrC,CAAC;EACD,KAAK,uBAAuB,KAAK,QAAQ,iBAAiB,SAAS,MAAM,WAAW,eAAe;GACjG,KAAK,gBAAgB;IACnB,SAAS,KAAK,WAAW;IACzB;IACA;IACA;IACA;GACF,CAAC;EACH,CAAC;EAED,MAAM,iBAAiB,OAAO,KAAK,QAAQ,YAAY,oBAAoB,GAAG,KAAK;EACnF,MAAM,cAAc,OAAO,KAAK,QAAQ,YAAY,iBAAiB,GAAG,KAAK;EAC7E,IAAI,CAAC,KAAK,UAAU,iBAAiB,KAAK,cAAc,GACtD,KAAK,gBAAgB;GACnB,SAAS,KAAK,WAAW;GACzB,SAAS;GACT,MAAM;EACR,CAAC;EAGH,IAAI,CAAC,KAAK,QAAQ;GAChB,KAAK,YACH,+DACF;GACA;EACF;EAOA,IALa,yBACX,KAAK,yBACL,8BAA8B,GAC9B,KAAK,eAAe,KAAA,KAAa,KAAK,gBAAgB,KAAA,CAEjD,MAAM,eAAe;GAC1B,KAAK,wBAAwB;GAC7B;EACF;EAEA,IAAI,CAAC,KAAK,cAAc,CAAC,KAAK,aAAa;GACzC,IAAI,KAAK,qBAAqB,KAAA,GAC5B,QAAQ,MACN,qDACA,KAAK,gBACP;GAEF,KAAK,YACH,sHACF;GACA;EACF;EAEA,KAAK,SAAS,IAAI,OAAO,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;EACjE,KAAK,OAAO,iBAAiB,YAAY,UAAuC;GAC9E,KAAK,oBAAoB,MAAM,IAAI;EACrC,CAAC;EACD,KAAK,OAAO,iBAAiB,UAAU,UAAU;GAC/C,KAAK,QAAQ,OAAO,MAClB,kCAAkC,MAAM,WAAW,uBAAuB,GAC5E;EACF,CAAC;EAED,MAAM,cAAc,EAAE,GAAG,KAAK,OAAO,YAAY;EAEjD,MAAM,UAA8B;GAClC,MAAM;GACN,SAAS,KAAK,QAAQ;GACtB;GACA,YAAY,KAAK;GACjB,WAAW,KAAK;EAClB;EACA,KAAK,OAAO,YAAY,OAAO;CACjC;CAEA,UAAyB;EACvB,KAAK,sBAAsB,WAAW;EACtC,KAAK,4BAA4B;EACjC,KAAK,uBAAuB;EAC5B,KAAK,aAAa,MAAM;EACxB,KAAK,QAAQ,UAAU;EACvB,KAAK,UAAU,QAAQ;EACvB,MAAM,QAAQ;CAChB;CAEA,0BAAwC;EACtC,MAAM,SAAS,KAAK;EACpB,IAAI,CAAC,QACH;EAEF,MAAM,WAAW,IAAI,uBAAuB;GAC1C,SAAS,KAAK,QAAQ;GACtB,aAAa;IACX,GAAG,0CAA0C,8BAA8B,CAAC;IAC5E,GAAG,OAAO;GACZ;GACA,WAAW,UAAU,OAAO,OAAO,MAAM,KAAK;GAC9C,WAAW,UAAU,OAAO,OAAO,MAAM,KAAK;GAC9C,SAAS,SAAS;IAChB,IAAI,SAAS,GACX,OAAO,OAAO,MAAM,wCAAwC,KAAK,IAAI;GAEzE;GACA,UAAU,YAAY;IACpB,OAAO,OAAO,MAAM,wCAAwC,QAAQ,GAAG;GACzE;EACF,CAAC;EACD,KAAK,WAAW;EAChB,KAAK,YAAY,SAAS,UAAU;GAClC,SAAS,UAAU,KAAK;GACxB,OAAO;EACT;EACA,SAAS,aAAa,KAAK,SAAS;EACpC,SAAS,MAAM;CACjB;CAEA,oBACE,SACM;EACN,QAAQ,QAAQ,MAAhB;GACA,KAAK;IACH,KAAK,QAAQ,OAAO,MAAM,QAAQ,KAAK;IACvC;GACF,KAAK;IACH,KAAK,QAAQ,OAAO,MAAM,QAAQ,KAAK;IACvC;GACF,KAAK;IACH,IAAI,QAAQ,SAAS,GACnB,KAAK,QAAQ,OAAO,MAAM,wCAAwC,QAAQ,KAAK,IAAI;IAErF;GACF,KAAK;IACH,KAAK,QAAQ,OAAO,MAAM,wCAAwC,QAAQ,QAAQ,GAAG;IACrF;EACF;CACF;AACF"}
@@ -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 };