@swifttui/web 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/src/CanvasSurfacePainter.js +3 -10
- package/dist/src/CanvasSurfacePainter.js.map +1 -1
- package/dist/src/DomSurfacePainter.d.ts +51 -0
- package/dist/src/DomSurfacePainter.js +267 -0
- package/dist/src/DomSurfacePainter.js.map +1 -0
- package/dist/src/SurfaceRenderer.d.ts +50 -0
- package/dist/src/SurfaceRenderer.js +21 -0
- package/dist/src/SurfaceRenderer.js.map +1 -0
- package/dist/src/WebHostApp.d.ts +28 -1
- package/dist/src/WebHostApp.js +34 -2
- package/dist/src/WebHostApp.js.map +1 -1
- package/dist/src/WebHostSceneRuntime.d.ts +48 -1
- package/dist/src/WebHostSceneRuntime.js +86 -14
- package/dist/src/WebHostSceneRuntime.js.map +1 -1
- package/dist/src/wasi/MainThreadWasmExecutor.d.ts +8 -0
- package/dist/src/wasi/MainThreadWasmExecutor.js +16 -1
- package/dist/src/wasi/MainThreadWasmExecutor.js.map +1 -1
- package/dist/src/wasi/WasiPollScheduler.js +6 -0
- package/dist/src/wasi/WasiPollScheduler.js.map +1 -1
- package/dist/src/wasi/WasmRuntimePause.d.ts +63 -0
- package/dist/src/wasi/WasmRuntimePause.js +130 -0
- package/dist/src/wasi/WasmRuntimePause.js.map +1 -0
- package/dist/src/wasi/WasmSceneRuntime.js +14 -1
- package/dist/src/wasi/WasmSceneRuntime.js.map +1 -1
- package/dist/src/wasi/WasmSceneWorker.d.ts +7 -0
- package/dist/src/wasi/WasmSceneWorker.js +11 -4
- package/dist/src/wasi/WasmSceneWorker.js.map +1 -1
- package/dist/wasi.d.ts +2 -1
- package/dist/wasi.js +2 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebHostSceneRuntime.js","names":[],"sources":["../../src/WebHostSceneRuntime.ts"],"sourcesContent":["import {\n applyWebHostTerminalStyle,\n normalizeWebHostTerminalStyle,\n type ResolvedWebHostTerminalStyle,\n type WebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport {\n CanvasSurfacePainter,\n fontForStyle,\n type CanvasSurfaceMetrics,\n} from \"./CanvasSurfacePainter.ts\";\nimport {\n InputEventEncoder,\n type CellLocation,\n type PointerButton,\n} from \"./InputEventEncoder.ts\";\nimport {\n cellLocationForEvent,\n linkTargetAt,\n rawCellLocationForEvent,\n wheelTargetCanScroll,\n type PointerGeometryMetrics,\n} from \"./PointerGeometry.ts\";\nimport { AccessibilityTreeMounter } from \"./AccessibilityTree.ts\";\nimport {\n type WebHostFocusPresentation,\n type WebHostFrameDiagnosticRecord,\n type WebHostOutputSink,\n type WebHostRuntimeIssue,\n type WebHostSurfaceDamage,\n type WebHostSurfaceFrame,\n} from \"./WebHostSurfaceTransport.ts\";\nimport type { WebHostSceneDescriptor } from \"./WebHostSceneManifest.ts\";\n\nexport interface WebHostSceneBridge {\n bindOutput(sink: WebHostOutputSink): void;\n resize(columns: number, rows: number, cellWidth?: number, cellHeight?: number): void;\n updateRenderStyle(style: WebHostTerminalStyle): void;\n sendInput(chunk: Uint8Array): void;\n dispose(): void;\n}\n\nexport interface WebHostSceneRuntimeOptions {\n mount: HTMLElement;\n descriptor: WebHostSceneDescriptor;\n style: WebHostTerminalStyle;\n bridge?: WebHostSceneBridge;\n onInput(chunk: Uint8Array): void;\n onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;\n synchronizeAccessibilityFocus?: boolean;\n /**\n * How the embedded view treats mouse-wheel input.\n * - `\"chain\"` (default): forward the wheel only while a scrollable region\n * under the pointer can still scroll in that direction; otherwise let it\n * fall through so the page (or parent iframe) scrolls — iframe-like nested\n * scrolling. A scene with no `ScrollView` never traps the wheel. Uses the\n * `scrollRegions` the app publishes in its frames.\n * - `\"capture\"`: always forward the wheel to the app while the pointer is over\n * the surface (and `preventDefault` page scroll). Best for full-screen apps\n * where there is no page to scroll past.\n * - `\"passive\"`: never capture; the page always scrolls.\n *\n * Takes precedence over the legacy `captureWheelInput` flag.\n */\n wheelMode?: WheelMode;\n /**\n * Legacy boolean wheel gate. `true` → `\"capture\"`, `false` → `\"passive\"`.\n * Prefer `wheelMode`. Ignored when `wheelMode` is set. When neither is set the\n * mode defaults to `\"chain\"`.\n */\n captureWheelInput?: boolean;\n /**\n * Called when the user clicks a hyperlink cell (a click is a pointer-down\n * and pointer-up over the same link target). When unset, `http(s)` targets\n * open in a new tab via `window.open(url, \"_blank\", \"noopener,noreferrer\")`\n * and other schemes are ignored. Mirrors the Android host's tap-to-open.\n */\n onOpenHyperlink?: (url: string) => void;\n}\n\nexport type WheelMode = \"capture\" | \"chain\" | \"passive\";\n\n/**\n * Resolves the legacy `captureWheelInput` flag to a {@link WheelMode}. When the\n * flag is unset the mode defaults to `\"chain\"`, so embeds never trap a visitor\n * who is merely scrolling past the view; `true` maps to `\"capture\"` and `false`\n * to `\"passive\"` to preserve the old boolean behavior.\n */\nfunction legacyWheelMode(captureWheelInput: boolean | undefined): WheelMode {\n if (captureWheelInput === undefined) {\n return \"chain\";\n }\n return captureWheelInput ? \"capture\" : \"passive\";\n}\n\n/**\n * Coordinates a single SwiftTUI scene's browser presentation: it owns the DOM\n * mount, canvas, accessibility tree, and bridge wiring, and delegates the heavy\n * responsibilities to focused collaborators — {@link CanvasSurfacePainter} for\n * canvas drawing, {@link InputEventEncoder} for wire-message encoding, and the\n * {@link PointerGeometry} helpers for pixel→cell hit-testing and wheel chaining.\n */\nexport class WebHostSceneRuntime {\n readonly descriptor: WebHostSceneDescriptor;\n readonly element: HTMLElement;\n readonly terminalMount: HTMLElement;\n\n private readonly bridge?: WebHostSceneBridge;\n private readonly onInput: (chunk: Uint8Array) => void;\n private readonly onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;\n private readonly synchronizeAccessibilityFocus: boolean;\n private readonly wheelMode: WheelMode;\n private readonly painter = new CanvasSurfacePainter();\n private readonly inputEncoder = new InputEventEncoder();\n private currentStyle: ResolvedWebHostTerminalStyle;\n private canvas?: HTMLCanvasElement;\n private accessibilityTree?: AccessibilityTreeMounter;\n private diagnosticText?: HTMLElement;\n private resizeObserver?: ResizeObserver;\n private detachInputHandlers?: () => void;\n private currentFrame?: WebHostSurfaceFrame;\n private columns = 80;\n private rows = 24;\n private cellWidth = 8;\n private cellHeight = 18;\n private activePointerButton: PointerButton = \"primary\";\n private hasCapturedPointer = false;\n private readonly onOpenHyperlink?: (url: string) => void;\n private pointerDownLinkTarget?: string;\n private lastSentResize?: {\n columns: number;\n rows: number;\n cellWidth: number;\n cellHeight: number;\n };\n private isVisible = false;\n\n constructor(options: WebHostSceneRuntimeOptions) {\n this.descriptor = options.descriptor;\n this.currentStyle = normalizeWebHostTerminalStyle(options.style);\n this.bridge = options.bridge;\n this.onInput = options.onInput;\n this.onFrameDiagnostic = options.onFrameDiagnostic;\n this.synchronizeAccessibilityFocus = options.synchronizeAccessibilityFocus ?? true;\n this.wheelMode = options.wheelMode ?? legacyWheelMode(options.captureWheelInput);\n this.onOpenHyperlink = options.onOpenHyperlink;\n this.element = document.createElement(\"section\");\n this.element.className = \"webhost-scene\";\n this.element.dataset.sceneId = options.descriptor.id;\n this.element.hidden = true;\n\n const header = document.createElement(\"div\");\n header.className = \"webhost-scene__header\";\n header.textContent = options.descriptor.title ?? options.descriptor.id;\n\n this.terminalMount = document.createElement(\"div\");\n this.terminalMount.className = \"webhost-scene__terminal\";\n this.terminalMount.tabIndex = 0;\n\n this.element.append(header, this.terminalMount);\n options.mount.appendChild(this.element);\n this.applyVisibility();\n }\n\n async mount(): Promise<void> {\n if (this.canvas) {\n return;\n }\n\n const canvas = document.createElement(\"canvas\");\n canvas.className = \"webhost-scene__surface\";\n canvas.setAttribute(\"aria-hidden\", \"true\");\n this.canvas = canvas;\n this.painter.attach(canvas, () => this.draw());\n this.accessibilityTree = new AccessibilityTreeMounter();\n this.terminalMount.replaceChildren(\n canvas,\n this.accessibilityTree.element,\n this.accessibilityTree.announcerElement\n );\n this.installInputHandlers();\n this.installResizeObserver();\n\n this.bridge?.bindOutput({\n presentSurface: (frame) => this.presentSurface(frame),\n writeClipboard: (text) => this.writeClipboard(text),\n notifyRuntimeIssue: (issue) => this.notifyRuntimeIssue(issue),\n recordFrameDiagnostic: (diagnostic) => this.recordFrameDiagnostic(diagnostic),\n writeOutput: (text) => this.writeOutput(text),\n writeError: (text) => this.writeOutput(text),\n });\n\n this.applyStyle(this.currentStyle);\n this.measureCells();\n this.resizeToMount();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n setVisible(\n visible: boolean\n ): void {\n this.isVisible = visible;\n this.applyVisibility();\n if (visible) {\n this.resizeToMount();\n if (this.synchronizeAccessibilityFocus) {\n this.terminalMount.focus?.({ preventScroll: true });\n }\n }\n }\n\n setStyle(\n style: WebHostTerminalStyle\n ): void {\n this.currentStyle = normalizeWebHostTerminalStyle(style);\n this.applyStyle(this.currentStyle);\n this.bridge?.updateRenderStyle(this.currentStyle);\n this.measureCells();\n this.resizeToMount();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n resize(\n columns: number,\n rows: number\n ): void {\n this.columns = Math.max(1, Math.round(columns));\n this.rows = Math.max(1, Math.round(rows));\n this.resizeCanvas();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n writeOutput(\n text: string\n ): void {\n if (!this.diagnosticText) {\n const diagnosticText = document.createElement(\"pre\");\n diagnosticText.className = \"webhost-scene__diagnostic\";\n this.diagnosticText = diagnosticText;\n this.terminalMount.appendChild(diagnosticText);\n }\n this.diagnosticText.textContent = `${this.diagnosticText.textContent ?? \"\"}${text}`;\n }\n\n notifyRuntimeIssue(\n issue: WebHostRuntimeIssue\n ): void {\n console.log(issue.description);\n }\n\n private recordFrameDiagnostic(\n diagnostic: WebHostFrameDiagnosticRecord\n ): void {\n this.onFrameDiagnostic?.(diagnostic);\n }\n\n async writeClipboard(\n text: string\n ): Promise<void> {\n const clipboard = globalThis.navigator?.clipboard;\n if (!clipboard?.writeText) {\n return;\n }\n\n try {\n await clipboard.writeText(text);\n } catch {\n // Clipboard permissions are browser/user-gesture dependent; hosts treat\n // rejection as a best-effort no-op rather than surfacing diagnostics.\n }\n }\n\n sendInput(\n chunk: Uint8Array\n ): void {\n this.onInput(chunk);\n }\n\n dispose(): void {\n this.detachInputHandlers?.();\n this.resizeObserver?.disconnect();\n this.element.remove();\n }\n\n private presentSurface(\n frame: WebHostSurfaceFrame\n ): void {\n const previousFrame = this.currentFrame;\n this.currentFrame = frame;\n this.columns = Math.max(1, Math.round(frame.width));\n this.rows = Math.max(1, Math.round(frame.height));\n const resized = this.resizeCanvas();\n this.draw(previousFrame && !resized ? frame.damage : undefined);\n this.syncAccessibilityTree();\n }\n\n /**\n * The current frame's preferred grid size in cells, when the app published\n * one — the measured pre-minimum content size, for embedders negotiating\n * with an outer layout system (the Android host's preferred columns/rows).\n */\n get preferredGridSize(): { width: number; height: number } | undefined {\n const frame = this.currentFrame;\n if (frame?.preferredGridWidth === undefined || frame.preferredGridHeight === undefined) {\n return undefined;\n }\n return { width: frame.preferredGridWidth, height: frame.preferredGridHeight };\n }\n\n /**\n * The current frame's settled focus presentation, when the app published\n * one. `prefersTextInput` is what the Android host uses to gate its IME;\n * embedders can drive virtual-keyboard or focus affordances from it.\n */\n get focusPresentation(): WebHostFocusPresentation | undefined {\n return this.currentFrame?.focusPresentation;\n }\n\n private linkTarget(\n location: CellLocation\n ): string | undefined {\n return linkTargetAt(\n this.currentFrame?.links,\n this.currentFrame?.linkTargets,\n location\n );\n }\n\n private openHyperlink(\n url: string\n ): void {\n if (this.onOpenHyperlink) {\n this.onOpenHyperlink(url);\n return;\n }\n // Defense in depth on top of the app-side OSC-8 destination sanitization:\n // the default handler only opens web schemes.\n if (!/^https?:/i.test(url)) {\n return;\n }\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n }\n\n private applyStyle(\n style: WebHostTerminalStyle\n ): void {\n applyWebHostTerminalStyle(this.element, style);\n this.element.style.padding = \"0.75rem\";\n this.element.style.borderRadius = \"16px\";\n this.element.style.boxShadow = \"0 20px 50px rgba(0, 0, 0, 0.28)\";\n this.element.style.overflow = \"hidden\";\n this.element.style.gap = \"0.5rem\";\n this.element.style.gridTemplateRows = \"auto 1fr\";\n\n this.terminalMount.style.position = \"relative\";\n this.terminalMount.style.overflow = \"hidden\";\n // Keep a captured wheel from rubber-banding/chaining the page; the wheel\n // capture vs. fall-through decision lives in handleWheel.\n this.terminalMount.style.overscrollBehavior = \"contain\";\n this.terminalMount.style.outline = \"none\";\n this.terminalMount.style.background = webTUITerminalBackgroundColor(this.currentStyle);\n this.terminalMount.style.minHeight = `${this.cellHeight * 8}px`;\n\n if (this.canvas) {\n this.canvas.style.display = \"block\";\n this.canvas.style.width = \"100%\";\n this.canvas.style.height = \"100%\";\n }\n }\n\n private applyVisibility(): void {\n this.element.hidden = !this.isVisible;\n this.element.style.setProperty(\n \"display\",\n this.isVisible ? \"grid\" : \"none\",\n \"important\"\n );\n }\n\n private installResizeObserver(): void {\n if (typeof ResizeObserver === \"undefined\") {\n return;\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n this.resizeToMount();\n });\n this.resizeObserver.observe(this.terminalMount);\n }\n\n private installInputHandlers(): void {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.metaKey || event.isComposing) {\n return;\n }\n const message = this.inputEncoder.encodeKey(event);\n if (!message) {\n return;\n }\n\n this.onInput(message);\n event.preventDefault();\n };\n\n const handlePaste = (event: ClipboardEvent) => {\n const text = event.clipboardData?.getData(\"text/plain\") ?? \"\";\n if (!text) {\n return;\n }\n this.onInput(this.inputEncoder.encodePaste(text));\n event.preventDefault();\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const location = this.cellLocation(event);\n if (!location) {\n return;\n }\n\n const button = this.inputEncoder.pointerButton(event.button);\n this.activePointerButton = button;\n this.hasCapturedPointer = true;\n this.pointerDownLinkTarget = button === \"primary\"\n ? this.linkTarget(location)\n : undefined;\n this.terminalMount.focus?.({ preventScroll: true });\n this.terminalMount.setPointerCapture?.(event.pointerId);\n this.onInput(this.inputEncoder.encodePointerDown(location, button, event));\n event.preventDefault();\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n const location = this.hasCapturedPointer\n ? this.rawCellLocation(event)\n : this.cellLocation(event);\n this.terminalMount.releasePointerCapture?.(event.pointerId);\n this.hasCapturedPointer = false;\n const downLinkTarget = this.pointerDownLinkTarget;\n this.pointerDownLinkTarget = undefined;\n if (!location) {\n return;\n }\n\n const button = this.inputEncoder.pointerButton(event.button) ?? this.activePointerButton;\n this.onInput(this.inputEncoder.encodePointerUp(location, button, event));\n // A click — down and up over the same link target — opens the link,\n // mirroring the Android host's tap-to-open. The app still receives the\n // pointer messages above.\n if (downLinkTarget !== undefined && this.linkTarget(location) === downLinkTarget) {\n this.openHyperlink(downLinkTarget);\n }\n event.preventDefault();\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const location = event.buttons && this.hasCapturedPointer\n ? this.rawCellLocation(event)\n : this.cellLocation(event);\n if (!location) {\n return;\n }\n\n if (!this.hasCapturedPointer) {\n this.terminalMount.style.cursor =\n this.linkTarget(location) !== undefined ? \"pointer\" : \"\";\n }\n this.onInput(this.inputEncoder.encodePointerMove(location, this.activePointerButton, event));\n };\n\n const handleWheel = (event: WheelEvent) => {\n if (this.wheelMode === \"passive\") {\n return;\n }\n\n const location = this.cellLocation(event);\n if (!location) {\n // Pointer is outside the cell grid (sub-cell margin / gutter). Don't\n // capture — let the wheel fall through to the page.\n return;\n }\n\n // In \"chain\" mode, capture only while a scrollable region under the\n // pointer can still move in this direction; otherwise let the wheel fall\n // through so the page (or parent iframe) scrolls — iframe-like behavior.\n // \"capture\" mode always forwards while over the surface (legacy).\n if (this.wheelMode === \"chain\"\n && !wheelTargetCanScroll(this.currentFrame?.scrollRegions, location, event.deltaX, event.deltaY)) {\n return;\n }\n\n this.onInput(this.inputEncoder.encodeWheel(location, event));\n event.preventDefault();\n };\n\n this.terminalMount.addEventListener(\"keydown\", handleKeyDown);\n this.terminalMount.addEventListener(\"paste\", handlePaste);\n this.terminalMount.addEventListener(\"pointerdown\", handlePointerDown);\n this.terminalMount.addEventListener(\"pointerup\", handlePointerUp);\n this.terminalMount.addEventListener(\"pointermove\", handlePointerMove);\n this.terminalMount.addEventListener(\"wheel\", handleWheel, { passive: false });\n\n this.detachInputHandlers = () => {\n this.terminalMount.removeEventListener(\"keydown\", handleKeyDown);\n this.terminalMount.removeEventListener(\"paste\", handlePaste);\n this.terminalMount.removeEventListener(\"pointerdown\", handlePointerDown);\n this.terminalMount.removeEventListener(\"pointerup\", handlePointerUp);\n this.terminalMount.removeEventListener(\"pointermove\", handlePointerMove);\n this.terminalMount.removeEventListener(\"wheel\", handleWheel);\n };\n }\n\n private resizeToMount(): void {\n this.measureCells();\n const rect = this.terminalMount.getBoundingClientRect?.();\n const width = rect?.width && rect.width > 0 ? rect.width : this.columns * this.cellWidth;\n const height = rect?.height && rect.height > 0 ? rect.height : this.rows * this.cellHeight;\n const nextColumns = Math.max(1, Math.floor(width / this.cellWidth));\n const nextRows = Math.max(1, Math.floor(height / this.cellHeight));\n\n this.columns = nextColumns;\n this.rows = nextRows;\n this.sendResizeIfNeeded();\n this.resizeCanvas();\n }\n\n private sendResizeIfNeeded(): void {\n const current = {\n columns: this.columns,\n rows: this.rows,\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n };\n if (this.lastSentResize\n && this.lastSentResize.columns === current.columns\n && this.lastSentResize.rows === current.rows\n && this.lastSentResize.cellWidth === current.cellWidth\n && this.lastSentResize.cellHeight === current.cellHeight\n ) {\n return;\n }\n\n this.lastSentResize = current;\n this.bridge?.resize(current.columns, current.rows, current.cellWidth, current.cellHeight);\n }\n\n private resizeCanvas(): boolean {\n if (!this.canvas) {\n return false;\n }\n\n const cssWidth = Math.max(1, this.columns * this.cellWidth);\n const cssHeight = Math.max(1, this.rows * this.cellHeight);\n const scale = globalThis.window?.devicePixelRatio || 1;\n const width = Math.ceil(cssWidth * scale);\n const height = Math.ceil(cssHeight * scale);\n const styleWidth = `${cssWidth}px`;\n const styleHeight = `${cssHeight}px`;\n if (this.canvas.width === width\n && this.canvas.height === height\n && this.canvas.style.width === styleWidth\n && this.canvas.style.height === styleHeight\n ) {\n return false;\n }\n\n this.canvas.width = width;\n this.canvas.height = height;\n this.canvas.style.width = styleWidth;\n this.canvas.style.height = styleHeight;\n return true;\n }\n\n private measureCells(): void {\n const canvas = this.canvas ?? document.createElement(\"canvas\");\n const context = canvas.getContext?.(\"2d\");\n if (!context) {\n this.cellWidth = Math.max(1, Math.round(this.currentStyle.fontSize * 0.62));\n this.cellHeight = Math.max(1, Math.round(this.currentStyle.fontSize * 1.35));\n return;\n }\n\n context.font = fontForStyle(this.currentStyle);\n this.cellWidth = Math.max(1, Math.ceil(context.measureText(\"W\").width));\n this.cellHeight = Math.max(1, Math.ceil(this.currentStyle.fontSize * 1.35));\n }\n\n private draw(\n damage?: WebHostSurfaceDamage\n ): void {\n this.painter.paint(this.surfaceMetrics(), this.currentFrame, damage);\n }\n\n private syncAccessibilityTree(): void {\n const tree = this.accessibilityTree;\n if (!tree || !this.currentFrame) {\n return;\n }\n\n tree.present(this.currentFrame.accessibilityTree ?? [], {\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n }, this.currentFrame.accessibilityAnnouncements ?? [], {\n synchronizeFocus: this.synchronizeAccessibilityFocus,\n });\n }\n\n private surfaceMetrics(): CanvasSurfaceMetrics {\n return {\n columns: this.columns,\n rows: this.rows,\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n style: this.currentStyle,\n };\n }\n\n private pointerMetrics(): PointerGeometryMetrics {\n return {\n rect: this.canvas?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.(),\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n columns: this.columns,\n rows: this.rows,\n };\n }\n\n private cellLocation(\n event: MouseEvent\n ): CellLocation | undefined {\n return cellLocationForEvent(event, this.pointerMetrics());\n }\n\n private rawCellLocation(\n event: MouseEvent\n ): CellLocation | undefined {\n return rawCellLocationForEvent(event, this.pointerMetrics());\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WebHostSceneRuntime.js","names":[],"sources":["../../src/WebHostSceneRuntime.ts"],"sourcesContent":["import {\n applyWebHostTerminalStyle,\n normalizeWebHostTerminalStyle,\n type ResolvedWebHostTerminalStyle,\n type WebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport {\n CanvasSurfacePainter,\n fontForStyle,\n type CanvasSurfaceMetrics,\n} from \"./CanvasSurfacePainter.ts\";\nimport { DomSurfacePainter } from \"./DomSurfacePainter.ts\";\nimport type { WebHostSurfaceRendererKind } from \"./SurfaceRenderer.ts\";\nimport {\n InputEventEncoder,\n type CellLocation,\n type PointerButton,\n} from \"./InputEventEncoder.ts\";\nimport {\n cellLocationForEvent,\n linkTargetAt,\n rawCellLocationForEvent,\n wheelTargetCanScroll,\n type PointerGeometryMetrics,\n} from \"./PointerGeometry.ts\";\nimport { AccessibilityTreeMounter } from \"./AccessibilityTree.ts\";\nimport {\n type WebHostFocusPresentation,\n type WebHostFrameDiagnosticRecord,\n type WebHostOutputSink,\n type WebHostRuntimeIssue,\n type WebHostSurfaceDamage,\n type WebHostSurfaceFrame,\n} from \"./WebHostSurfaceTransport.ts\";\nimport type { WebHostSceneDescriptor } from \"./WebHostSceneManifest.ts\";\n\nexport interface WebHostSceneBridge {\n bindOutput(sink: WebHostOutputSink): void;\n resize(columns: number, rows: number, cellWidth?: number, cellHeight?: number): void;\n updateRenderStyle(style: WebHostTerminalStyle): void;\n sendInput(chunk: Uint8Array): void;\n dispose(): void;\n}\n\nexport interface WebHostSceneRuntimeOptions {\n mount: HTMLElement;\n descriptor: WebHostSceneDescriptor;\n style: WebHostTerminalStyle;\n bridge?: WebHostSceneBridge;\n onInput(chunk: Uint8Array): void;\n onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;\n synchronizeAccessibilityFocus?: boolean;\n /**\n * How the embedded view treats mouse-wheel input.\n * - `\"chain\"` (default): forward the wheel only while a scrollable region\n * under the pointer can still scroll in that direction; otherwise let it\n * fall through so the page (or parent iframe) scrolls — iframe-like nested\n * scrolling. A scene with no `ScrollView` never traps the wheel. Uses the\n * `scrollRegions` the app publishes in its frames.\n * - `\"capture\"`: always forward the wheel to the app while the pointer is over\n * the surface (and `preventDefault` page scroll). Best for full-screen apps\n * where there is no page to scroll past.\n * - `\"passive\"`: never capture; the page always scrolls.\n *\n * Takes precedence over the legacy `captureWheelInput` flag.\n */\n wheelMode?: WheelMode;\n /**\n * Legacy boolean wheel gate. `true` → `\"capture\"`, `false` → `\"passive\"`.\n * Prefer `wheelMode`. Ignored when `wheelMode` is set. When neither is set the\n * mode defaults to `\"chain\"`.\n */\n captureWheelInput?: boolean;\n /**\n * Called when the user clicks a hyperlink cell (a click is a pointer-down\n * and pointer-up over the same link target). When unset, `http(s)` targets\n * open in a new tab via `window.open(url, \"_blank\", \"noopener,noreferrer\")`\n * and other schemes are ignored. Mirrors the Android host's tap-to-open.\n */\n onOpenHyperlink?: (url: string) => void;\n /**\n * Whether to suspend the scene's app while it cannot be seen — when the\n * scene is switched to the background (`setVisible(false)`) or the whole\n * document is hidden (`setDocumentVisible(false)`). Suspension parks the\n * app's run loop and freezes its monotonic clock, so a hidden scene costs\n * no CPU and resumes exactly where it left off. Defaults to `true`; set\n * `false` to let background scenes keep running (pre-suspension behavior).\n */\n suspendWhenHidden?: boolean;\n /**\n * Which surface presenter draws the scene's frames. `\"canvas\"` (default)\n * paints onto a 2D `<canvas>`; `\"dom\"` renders cells as absolutely\n * positioned text elements — native font rendering and, uniquely, real\n * text selection: hold Alt/Option and drag to select instead of sending\n * pointer input to the app. See {@link WebHostSurfaceRendererKind}.\n */\n renderer?: WebHostSurfaceRendererKind;\n}\n\nexport type WheelMode = \"capture\" | \"chain\" | \"passive\";\n\n/**\n * Resolves the legacy `captureWheelInput` flag to a {@link WheelMode}. When the\n * flag is unset the mode defaults to `\"chain\"`, so embeds never trap a visitor\n * who is merely scrolling past the view; `true` maps to `\"capture\"` and `false`\n * to `\"passive\"` to preserve the old boolean behavior.\n */\nfunction legacyWheelMode(captureWheelInput: boolean | undefined): WheelMode {\n if (captureWheelInput === undefined) {\n return \"chain\";\n }\n return captureWheelInput ? \"capture\" : \"passive\";\n}\n\n/**\n * Coordinates a single SwiftTUI scene's browser presentation: it owns the DOM\n * mount, canvas, accessibility tree, and bridge wiring, and delegates the heavy\n * responsibilities to focused collaborators — {@link CanvasSurfacePainter} for\n * canvas drawing, {@link InputEventEncoder} for wire-message encoding, and the\n * {@link PointerGeometry} helpers for pixel→cell hit-testing and wheel chaining.\n */\nexport class WebHostSceneRuntime {\n readonly descriptor: WebHostSceneDescriptor;\n readonly element: HTMLElement;\n readonly terminalMount: HTMLElement;\n\n private readonly bridge?: WebHostSceneBridge;\n private readonly onInput: (chunk: Uint8Array) => void;\n private readonly onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;\n private readonly synchronizeAccessibilityFocus: boolean;\n private readonly wheelMode: WheelMode;\n private readonly rendererKind: WebHostSurfaceRendererKind;\n private readonly painter: CanvasSurfacePainter | DomSurfacePainter;\n private readonly inputEncoder = new InputEventEncoder();\n private currentStyle: ResolvedWebHostTerminalStyle;\n private canvas?: HTMLCanvasElement;\n private domSurfaceRoot?: HTMLElement;\n private lastDomSurfaceSize?: { width: number; height: number };\n private accessibilityTree?: AccessibilityTreeMounter;\n private diagnosticText?: HTMLElement;\n private resizeObserver?: ResizeObserver;\n private detachInputHandlers?: () => void;\n private currentFrame?: WebHostSurfaceFrame;\n private columns = 80;\n private rows = 24;\n private cellWidth = 8;\n private cellHeight = 18;\n private activePointerButton: PointerButton = \"primary\";\n private hasCapturedPointer = false;\n private readonly onOpenHyperlink?: (url: string) => void;\n private pointerDownLinkTarget?: string;\n private lastSentResize?: {\n columns: number;\n rows: number;\n cellWidth: number;\n cellHeight: number;\n };\n private isVisible = false;\n private documentVisible = true;\n private runtimeSuspended = false;\n private readonly suspendWhenHidden: boolean;\n\n constructor(options: WebHostSceneRuntimeOptions) {\n this.descriptor = options.descriptor;\n this.currentStyle = normalizeWebHostTerminalStyle(options.style);\n this.bridge = options.bridge;\n this.onInput = options.onInput;\n this.onFrameDiagnostic = options.onFrameDiagnostic;\n this.synchronizeAccessibilityFocus = options.synchronizeAccessibilityFocus ?? true;\n this.wheelMode = options.wheelMode ?? legacyWheelMode(options.captureWheelInput);\n this.rendererKind = options.renderer ?? \"canvas\";\n this.painter = this.rendererKind === \"dom\"\n ? new DomSurfacePainter()\n : new CanvasSurfacePainter();\n this.onOpenHyperlink = options.onOpenHyperlink;\n this.suspendWhenHidden = options.suspendWhenHidden ?? true;\n this.element = document.createElement(\"section\");\n this.element.className = \"webhost-scene\";\n this.element.dataset.sceneId = options.descriptor.id;\n this.element.hidden = true;\n\n const header = document.createElement(\"div\");\n header.className = \"webhost-scene__header\";\n header.textContent = options.descriptor.title ?? options.descriptor.id;\n\n this.terminalMount = document.createElement(\"div\");\n this.terminalMount.className = \"webhost-scene__terminal\";\n this.terminalMount.tabIndex = 0;\n\n this.element.append(header, this.terminalMount);\n options.mount.appendChild(this.element);\n this.applyVisibility();\n }\n\n async mount(): Promise<void> {\n if (this.surfaceElement) {\n return;\n }\n\n if (this.painter instanceof DomSurfacePainter) {\n const surfaceRoot = document.createElement(\"div\");\n surfaceRoot.className = \"webhost-scene__surface webhost-scene__surface--dom\";\n surfaceRoot.setAttribute(\"aria-hidden\", \"true\");\n this.domSurfaceRoot = surfaceRoot;\n this.painter.attach(surfaceRoot);\n } else {\n const canvas = document.createElement(\"canvas\");\n canvas.className = \"webhost-scene__surface\";\n canvas.setAttribute(\"aria-hidden\", \"true\");\n this.canvas = canvas;\n this.painter.attach(canvas, () => this.draw());\n }\n this.accessibilityTree = new AccessibilityTreeMounter();\n this.terminalMount.replaceChildren(\n this.surfaceElement as HTMLElement,\n this.accessibilityTree.element,\n this.accessibilityTree.announcerElement\n );\n this.installInputHandlers();\n this.installResizeObserver();\n\n this.bridge?.bindOutput({\n presentSurface: (frame) => this.presentSurface(frame),\n writeClipboard: (text) => this.writeClipboard(text),\n notifyRuntimeIssue: (issue) => this.notifyRuntimeIssue(issue),\n recordFrameDiagnostic: (diagnostic) => this.recordFrameDiagnostic(diagnostic),\n writeOutput: (text) => this.writeOutput(text),\n writeError: (text) => this.writeOutput(text),\n });\n\n this.applyStyle(this.currentStyle);\n this.measureCells();\n this.resizeToMount();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n setVisible(\n visible: boolean\n ): void {\n this.isVisible = visible;\n this.applyVisibility();\n if (visible) {\n this.resizeToMount();\n if (this.synchronizeAccessibilityFocus) {\n this.terminalMount.focus?.({ preventScroll: true });\n }\n }\n this.updateRuntimeSuspension();\n }\n\n /**\n * Reports whether the surrounding document can be seen at all (browser tab\n * visible, iframe on-screen, …). Combined with the scene-level\n * `setVisible`: the app is suspended while either says hidden, unless\n * `suspendWhenHidden` is `false`.\n */\n setDocumentVisible(\n visible: boolean\n ): void {\n this.documentVisible = visible;\n this.updateRuntimeSuspension();\n }\n\n private updateRuntimeSuspension(): void {\n const suspended = this.suspendWhenHidden && (!this.isVisible || !this.documentVisible);\n if (suspended === this.runtimeSuspended) {\n return;\n }\n this.runtimeSuspended = suspended;\n this.onRuntimeSuspensionChange(suspended);\n }\n\n /**\n * Suspension hook for subclasses that own an app execution vehicle (the\n * WASI worker / JSPI executor). The base runtime only presents frames, so\n * it has nothing to suspend.\n */\n protected onRuntimeSuspensionChange(\n _suspended: boolean\n ): void {}\n\n setStyle(\n style: WebHostTerminalStyle\n ): void {\n this.currentStyle = normalizeWebHostTerminalStyle(style);\n this.applyStyle(this.currentStyle);\n this.bridge?.updateRenderStyle(this.currentStyle);\n this.measureCells();\n this.resizeToMount();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n resize(\n columns: number,\n rows: number\n ): void {\n this.columns = Math.max(1, Math.round(columns));\n this.rows = Math.max(1, Math.round(rows));\n this.resizeSurface();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n writeOutput(\n text: string\n ): void {\n if (!this.diagnosticText) {\n const diagnosticText = document.createElement(\"pre\");\n diagnosticText.className = \"webhost-scene__diagnostic\";\n this.diagnosticText = diagnosticText;\n this.terminalMount.appendChild(diagnosticText);\n }\n this.diagnosticText.textContent = `${this.diagnosticText.textContent ?? \"\"}${text}`;\n }\n\n notifyRuntimeIssue(\n issue: WebHostRuntimeIssue\n ): void {\n console.log(issue.description);\n }\n\n private recordFrameDiagnostic(\n diagnostic: WebHostFrameDiagnosticRecord\n ): void {\n this.onFrameDiagnostic?.(diagnostic);\n }\n\n async writeClipboard(\n text: string\n ): Promise<void> {\n const clipboard = globalThis.navigator?.clipboard;\n if (!clipboard?.writeText) {\n return;\n }\n\n try {\n await clipboard.writeText(text);\n } catch {\n // Clipboard permissions are browser/user-gesture dependent; hosts treat\n // rejection as a best-effort no-op rather than surfacing diagnostics.\n }\n }\n\n sendInput(\n chunk: Uint8Array\n ): void {\n this.onInput(chunk);\n }\n\n dispose(): void {\n this.detachInputHandlers?.();\n this.resizeObserver?.disconnect();\n this.element.remove();\n }\n\n private presentSurface(\n frame: WebHostSurfaceFrame\n ): void {\n const previousFrame = this.currentFrame;\n this.currentFrame = frame;\n this.columns = Math.max(1, Math.round(frame.width));\n this.rows = Math.max(1, Math.round(frame.height));\n const resized = this.resizeSurface();\n this.draw(previousFrame && !resized ? frame.damage : undefined);\n this.syncAccessibilityTree();\n }\n\n /**\n * The current frame's preferred grid size in cells, when the app published\n * one — the measured pre-minimum content size, for embedders negotiating\n * with an outer layout system (the Android host's preferred columns/rows).\n */\n get preferredGridSize(): { width: number; height: number } | undefined {\n const frame = this.currentFrame;\n if (frame?.preferredGridWidth === undefined || frame.preferredGridHeight === undefined) {\n return undefined;\n }\n return { width: frame.preferredGridWidth, height: frame.preferredGridHeight };\n }\n\n /**\n * The current frame's settled focus presentation, when the app published\n * one. `prefersTextInput` is what the Android host uses to gate its IME;\n * embedders can drive virtual-keyboard or focus affordances from it.\n */\n get focusPresentation(): WebHostFocusPresentation | undefined {\n return this.currentFrame?.focusPresentation;\n }\n\n private linkTarget(\n location: CellLocation\n ): string | undefined {\n return linkTargetAt(\n this.currentFrame?.links,\n this.currentFrame?.linkTargets,\n location\n );\n }\n\n private openHyperlink(\n url: string\n ): void {\n if (this.onOpenHyperlink) {\n this.onOpenHyperlink(url);\n return;\n }\n // Defense in depth on top of the app-side OSC-8 destination sanitization:\n // the default handler only opens web schemes.\n if (!/^https?:/i.test(url)) {\n return;\n }\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n }\n\n private applyStyle(\n style: WebHostTerminalStyle\n ): void {\n applyWebHostTerminalStyle(this.element, style);\n this.element.style.padding = \"0.75rem\";\n this.element.style.borderRadius = \"16px\";\n this.element.style.boxShadow = \"0 20px 50px rgba(0, 0, 0, 0.28)\";\n this.element.style.overflow = \"hidden\";\n this.element.style.gap = \"0.5rem\";\n this.element.style.gridTemplateRows = \"auto 1fr\";\n\n this.terminalMount.style.position = \"relative\";\n this.terminalMount.style.overflow = \"hidden\";\n // Keep a captured wheel from rubber-banding/chaining the page; the wheel\n // capture vs. fall-through decision lives in handleWheel.\n this.terminalMount.style.overscrollBehavior = \"contain\";\n this.terminalMount.style.outline = \"none\";\n this.terminalMount.style.background = webTUITerminalBackgroundColor(this.currentStyle);\n this.terminalMount.style.minHeight = `${this.cellHeight * 8}px`;\n\n if (this.canvas) {\n this.canvas.style.display = \"block\";\n this.canvas.style.width = \"100%\";\n this.canvas.style.height = \"100%\";\n }\n if (this.domSurfaceRoot) {\n this.domSurfaceRoot.style.display = \"block\";\n this.domSurfaceRoot.style.position = \"relative\";\n }\n }\n\n /** The element the active painter presents frames into. */\n private get surfaceElement(): HTMLElement | undefined {\n return this.canvas ?? this.domSurfaceRoot;\n }\n\n private applyVisibility(): void {\n this.element.hidden = !this.isVisible;\n this.element.style.setProperty(\n \"display\",\n this.isVisible ? \"grid\" : \"none\",\n \"important\"\n );\n }\n\n private installResizeObserver(): void {\n if (typeof ResizeObserver === \"undefined\") {\n return;\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n this.resizeToMount();\n });\n this.resizeObserver.observe(this.terminalMount);\n }\n\n private installInputHandlers(): void {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.metaKey || event.isComposing) {\n return;\n }\n const message = this.inputEncoder.encodeKey(event);\n if (!message) {\n return;\n }\n\n this.onInput(message);\n event.preventDefault();\n };\n\n const handlePaste = (event: ClipboardEvent) => {\n const text = event.clipboardData?.getData(\"text/plain\") ?? \"\";\n if (!text) {\n return;\n }\n this.onInput(this.inputEncoder.encodePaste(text));\n event.preventDefault();\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (this.allowsNativeTextSelection(event)) {\n // DOM renderer + Alt/Option: leave the event to the browser so the\n // drag becomes a native text selection instead of app pointer input.\n return;\n }\n const location = this.cellLocation(event);\n if (!location) {\n return;\n }\n\n const button = this.inputEncoder.pointerButton(event.button);\n this.activePointerButton = button;\n this.hasCapturedPointer = true;\n this.pointerDownLinkTarget = button === \"primary\"\n ? this.linkTarget(location)\n : undefined;\n this.terminalMount.focus?.({ preventScroll: true });\n this.terminalMount.setPointerCapture?.(event.pointerId);\n this.onInput(this.inputEncoder.encodePointerDown(location, button, event));\n event.preventDefault();\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n if (!this.hasCapturedPointer && this.allowsNativeTextSelection(event)) {\n return;\n }\n const location = this.hasCapturedPointer\n ? this.rawCellLocation(event)\n : this.cellLocation(event);\n this.terminalMount.releasePointerCapture?.(event.pointerId);\n this.hasCapturedPointer = false;\n const downLinkTarget = this.pointerDownLinkTarget;\n this.pointerDownLinkTarget = undefined;\n if (!location) {\n return;\n }\n\n const button = this.inputEncoder.pointerButton(event.button) ?? this.activePointerButton;\n this.onInput(this.inputEncoder.encodePointerUp(location, button, event));\n // A click — down and up over the same link target — opens the link,\n // mirroring the Android host's tap-to-open. The app still receives the\n // pointer messages above.\n if (downLinkTarget !== undefined && this.linkTarget(location) === downLinkTarget) {\n this.openHyperlink(downLinkTarget);\n }\n event.preventDefault();\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n if (!this.hasCapturedPointer && this.allowsNativeTextSelection(event)) {\n return;\n }\n const location = event.buttons && this.hasCapturedPointer\n ? this.rawCellLocation(event)\n : this.cellLocation(event);\n if (!location) {\n return;\n }\n\n if (!this.hasCapturedPointer) {\n this.terminalMount.style.cursor =\n this.linkTarget(location) !== undefined ? \"pointer\" : \"\";\n }\n this.onInput(this.inputEncoder.encodePointerMove(location, this.activePointerButton, event));\n };\n\n const handleWheel = (event: WheelEvent) => {\n if (this.wheelMode === \"passive\") {\n return;\n }\n\n const location = this.cellLocation(event);\n if (!location) {\n // Pointer is outside the cell grid (sub-cell margin / gutter). Don't\n // capture — let the wheel fall through to the page.\n return;\n }\n\n // In \"chain\" mode, capture only while a scrollable region under the\n // pointer can still move in this direction; otherwise let the wheel fall\n // through so the page (or parent iframe) scrolls — iframe-like behavior.\n // \"capture\" mode always forwards while over the surface (legacy).\n if (this.wheelMode === \"chain\"\n && !wheelTargetCanScroll(this.currentFrame?.scrollRegions, location, event.deltaX, event.deltaY)) {\n return;\n }\n\n this.onInput(this.inputEncoder.encodeWheel(location, event));\n event.preventDefault();\n };\n\n this.terminalMount.addEventListener(\"keydown\", handleKeyDown);\n this.terminalMount.addEventListener(\"paste\", handlePaste);\n this.terminalMount.addEventListener(\"pointerdown\", handlePointerDown);\n this.terminalMount.addEventListener(\"pointerup\", handlePointerUp);\n this.terminalMount.addEventListener(\"pointermove\", handlePointerMove);\n this.terminalMount.addEventListener(\"wheel\", handleWheel, { passive: false });\n\n this.detachInputHandlers = () => {\n this.terminalMount.removeEventListener(\"keydown\", handleKeyDown);\n this.terminalMount.removeEventListener(\"paste\", handlePaste);\n this.terminalMount.removeEventListener(\"pointerdown\", handlePointerDown);\n this.terminalMount.removeEventListener(\"pointerup\", handlePointerUp);\n this.terminalMount.removeEventListener(\"pointermove\", handlePointerMove);\n this.terminalMount.removeEventListener(\"wheel\", handleWheel);\n };\n }\n\n private resizeToMount(): void {\n this.measureCells();\n const rect = this.terminalMount.getBoundingClientRect?.();\n const width = rect?.width && rect.width > 0 ? rect.width : this.columns * this.cellWidth;\n const height = rect?.height && rect.height > 0 ? rect.height : this.rows * this.cellHeight;\n const nextColumns = Math.max(1, Math.floor(width / this.cellWidth));\n const nextRows = Math.max(1, Math.floor(height / this.cellHeight));\n\n this.columns = nextColumns;\n this.rows = nextRows;\n this.sendResizeIfNeeded();\n this.resizeSurface();\n }\n\n private sendResizeIfNeeded(): void {\n const current = {\n columns: this.columns,\n rows: this.rows,\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n };\n if (this.lastSentResize\n && this.lastSentResize.columns === current.columns\n && this.lastSentResize.rows === current.rows\n && this.lastSentResize.cellWidth === current.cellWidth\n && this.lastSentResize.cellHeight === current.cellHeight\n ) {\n return;\n }\n\n this.lastSentResize = current;\n this.bridge?.resize(current.columns, current.rows, current.cellWidth, current.cellHeight);\n }\n\n private resizeSurface(): boolean {\n const cssWidth = Math.max(1, this.columns * this.cellWidth);\n const cssHeight = Math.max(1, this.rows * this.cellHeight);\n\n if (this.domSurfaceRoot) {\n const last = this.lastDomSurfaceSize;\n if (last && last.width === cssWidth && last.height === cssHeight) {\n return false;\n }\n this.lastDomSurfaceSize = { width: cssWidth, height: cssHeight };\n this.domSurfaceRoot.style.width = `${cssWidth}px`;\n this.domSurfaceRoot.style.height = `${cssHeight}px`;\n return true;\n }\n\n if (!this.canvas) {\n return false;\n }\n\n const scale = globalThis.window?.devicePixelRatio || 1;\n const width = Math.ceil(cssWidth * scale);\n const height = Math.ceil(cssHeight * scale);\n const styleWidth = `${cssWidth}px`;\n const styleHeight = `${cssHeight}px`;\n if (this.canvas.width === width\n && this.canvas.height === height\n && this.canvas.style.width === styleWidth\n && this.canvas.style.height === styleHeight\n ) {\n return false;\n }\n\n this.canvas.width = width;\n this.canvas.height = height;\n this.canvas.style.width = styleWidth;\n this.canvas.style.height = styleHeight;\n return true;\n }\n\n private measureCells(): void {\n const canvas = this.canvas ?? document.createElement(\"canvas\");\n const context = canvas.getContext?.(\"2d\");\n if (!context) {\n this.cellWidth = Math.max(1, Math.round(this.currentStyle.fontSize * 0.62));\n this.cellHeight = Math.max(1, Math.round(this.currentStyle.fontSize * 1.35));\n return;\n }\n\n context.font = fontForStyle(this.currentStyle);\n this.cellWidth = Math.max(1, Math.ceil(context.measureText(\"W\").width));\n this.cellHeight = Math.max(1, Math.ceil(this.currentStyle.fontSize * 1.35));\n }\n\n private draw(\n damage?: WebHostSurfaceDamage\n ): void {\n this.painter.paint(this.surfaceMetrics(), this.currentFrame, damage);\n }\n\n private syncAccessibilityTree(): void {\n const tree = this.accessibilityTree;\n if (!tree || !this.currentFrame) {\n return;\n }\n\n tree.present(this.currentFrame.accessibilityTree ?? [], {\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n }, this.currentFrame.accessibilityAnnouncements ?? [], {\n synchronizeFocus: this.synchronizeAccessibilityFocus,\n });\n }\n\n private surfaceMetrics(): CanvasSurfaceMetrics {\n return {\n columns: this.columns,\n rows: this.rows,\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n style: this.currentStyle,\n };\n }\n\n private pointerMetrics(): PointerGeometryMetrics {\n return {\n rect: this.surfaceElement?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.(),\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n columns: this.columns,\n rows: this.rows,\n };\n }\n\n /**\n * Whether this pointer event should be left to the browser for native text\n * selection instead of being forwarded to the app. Only the DOM renderer\n * has real text nodes to select, and only while Alt/Option is held — plain\n * pointer input still belongs to the app.\n */\n private allowsNativeTextSelection(\n event: MouseEvent\n ): boolean {\n return this.rendererKind === \"dom\" && event.altKey;\n }\n\n private cellLocation(\n event: MouseEvent\n ): CellLocation | undefined {\n return cellLocationForEvent(event, this.pointerMetrics());\n }\n\n private rawCellLocation(\n event: MouseEvent\n ): CellLocation | undefined {\n return rawCellLocationForEvent(event, this.pointerMetrics());\n }\n}\n"],"mappings":"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"}
|
|
@@ -20,10 +20,18 @@ interface MainThreadWasmExecutorOptions {
|
|
|
20
20
|
declare class MainThreadWasmExecutor {
|
|
21
21
|
private readonly options;
|
|
22
22
|
private readonly stdin;
|
|
23
|
+
private readonly pauseClock;
|
|
24
|
+
private readonly pauseGate;
|
|
23
25
|
private didStart;
|
|
24
26
|
constructor(options: MainThreadWasmExecutorOptions);
|
|
25
27
|
start(): void;
|
|
26
28
|
sendInput(chunk: Uint8Array): void;
|
|
29
|
+
/**
|
|
30
|
+
* Suspends or resumes the app: while suspended, the run loop parks between
|
|
31
|
+
* `poll_oneoff` waits and the app's monotonic clock freezes, so a hidden
|
|
32
|
+
* scene costs no CPU and resumes without a deadline catch-up burst.
|
|
33
|
+
*/
|
|
34
|
+
setSuspended(suspended: boolean): void;
|
|
27
35
|
dispose(): void;
|
|
28
36
|
private run;
|
|
29
37
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jspiConstructors } from "./WasmEngineCapabilities.js";
|
|
2
2
|
import { MainThreadInputQueue } from "./MainThreadInputQueue.js";
|
|
3
3
|
import { SuspendingWasiPollScheduler } from "./WasiPollScheduler.js";
|
|
4
|
+
import { MainThreadWasmPauseGate, PausableMonotonicClock, installPausableClockTimeGet } from "./WasmRuntimePause.js";
|
|
4
5
|
import { ConsoleStdout, Fd, WASI, WASIProcExit, wasi } from "@bjorn3/browser_wasi_shim";
|
|
5
6
|
//#region src/wasi/MainThreadWasmExecutor.ts
|
|
6
7
|
/**
|
|
@@ -16,6 +17,8 @@ import { ConsoleStdout, Fd, WASI, WASIProcExit, wasi } from "@bjorn3/browser_was
|
|
|
16
17
|
var MainThreadWasmExecutor = class {
|
|
17
18
|
options;
|
|
18
19
|
stdin = new MainThreadInputQueue();
|
|
20
|
+
pauseClock = new PausableMonotonicClock();
|
|
21
|
+
pauseGate = new MainThreadWasmPauseGate(this.pauseClock);
|
|
19
22
|
didStart = false;
|
|
20
23
|
constructor(options) {
|
|
21
24
|
this.options = options;
|
|
@@ -28,8 +31,17 @@ var MainThreadWasmExecutor = class {
|
|
|
28
31
|
sendInput(chunk) {
|
|
29
32
|
this.stdin.write(chunk);
|
|
30
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Suspends or resumes the app: while suspended, the run loop parks between
|
|
36
|
+
* `poll_oneoff` waits and the app's monotonic clock freezes, so a hidden
|
|
37
|
+
* scene costs no CPU and resumes without a deadline catch-up burst.
|
|
38
|
+
*/
|
|
39
|
+
setSuspended(suspended) {
|
|
40
|
+
this.pauseGate.setPaused(suspended);
|
|
41
|
+
}
|
|
31
42
|
dispose() {
|
|
32
43
|
this.stdin.close();
|
|
44
|
+
this.pauseGate.setPaused(false);
|
|
33
45
|
}
|
|
34
46
|
async run() {
|
|
35
47
|
try {
|
|
@@ -45,8 +57,11 @@ var MainThreadWasmExecutor = class {
|
|
|
45
57
|
const scheduler = new SuspendingWasiPollScheduler({
|
|
46
58
|
memory: () => instanceExports()?.memory,
|
|
47
59
|
stdin: this.stdin,
|
|
48
|
-
fallbackPoll: (inPtr, outPtr, nsubscriptions, neventsPtr) => originalPoll(inPtr, outPtr, nsubscriptions, neventsPtr)
|
|
60
|
+
fallbackPoll: (inPtr, outPtr, nsubscriptions, neventsPtr) => originalPoll(inPtr, outPtr, nsubscriptions, neventsPtr),
|
|
61
|
+
nowMilliseconds: () => this.pauseClock.nowMilliseconds(),
|
|
62
|
+
pauseGate: this.pauseGate
|
|
49
63
|
});
|
|
64
|
+
installPausableClockTimeGet(shim.wasiImport, () => instanceExports()?.memory, this.pauseClock);
|
|
50
65
|
const response = await fetch(this.options.wasmURL);
|
|
51
66
|
if (!response.ok) throw new Error(`failed to load ${String(this.options.wasmURL)}: ${response.status} ${response.statusText}`);
|
|
52
67
|
const module = await WebAssembly.compile(await response.arrayBuffer());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MainThreadWasmExecutor.js","names":[],"sources":["../../../src/wasi/MainThreadWasmExecutor.ts"],"sourcesContent":["import { ConsoleStdout, Fd, WASI, WASIProcExit, wasi } from \"@bjorn3/browser_wasi_shim\";\n\nimport { MainThreadInputQueue } from \"./MainThreadInputQueue.ts\";\nimport { SuspendingWasiPollScheduler } from \"./WasiPollScheduler.ts\";\nimport { jspiConstructors } from \"./WasmEngineCapabilities.ts\";\n\nexport interface MainThreadWasmExecutorOptions {\n wasmURL: string | URL;\n environment: Record<string, string>;\n onStdout(chunk: Uint8Array): void;\n onStderr(chunk: Uint8Array): void;\n onExit?(code: number): void;\n onError?(message: string): void;\n}\n\n/**\n * Runs a SwiftTUI WASI app on the main thread using WebAssembly JSPI: the\n * `poll_oneoff` import is wrapped in `WebAssembly.Suspending`, so the app's\n * run loop suspends to the browser event loop instead of blocking a worker\n * on `Atomics.wait`. Requires JSPI; needs neither a worker nor\n * SharedArrayBuffer (and therefore no COOP/COEP headers).\n *\n * Disposal closes stdin; the SwiftTUI runner exits cooperatively on stdin\n * hangup, which resolves the promised `_start` call.\n */\nexport class MainThreadWasmExecutor {\n private readonly options: MainThreadWasmExecutorOptions;\n private readonly stdin = new MainThreadInputQueue();\n private didStart = false;\n\n constructor(options: MainThreadWasmExecutorOptions) {\n this.options = options;\n }\n\n start(): void {\n if (this.didStart) {\n return;\n }\n this.didStart = true;\n void this.run();\n }\n\n sendInput(chunk: Uint8Array): void {\n this.stdin.write(chunk);\n }\n\n dispose(): void {\n this.stdin.close();\n }\n\n private async run(): Promise<void> {\n try {\n const jspi = jspiConstructors();\n if (!jspi) {\n throw new Error(\n \"WebAssembly JSPI (Suspending/promising) is unavailable in this engine\"\n );\n }\n\n const shim = new WASI(\n [\"app.wasm\"],\n Object.entries(this.options.environment).map(([key, value]) => `${key}=${value}`),\n [\n new MainThreadInputFileDescriptor(this.stdin),\n new ConsoleStdout((chunk) => this.options.onStdout(chunk)),\n new ConsoleStdout((chunk) => this.options.onStderr(chunk)),\n ]\n );\n\n const instanceExports = (): { memory?: WebAssembly.Memory } | undefined =>\n (shim as unknown as { inst?: { exports: { memory?: WebAssembly.Memory } } }).inst\n ?.exports;\n const originalPoll = shim.wasiImport.poll_oneoff as (\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ) => number;\n const scheduler = new SuspendingWasiPollScheduler({\n memory: () => instanceExports()?.memory,\n stdin: this.stdin,\n fallbackPoll: (inPtr, outPtr, nsubscriptions, neventsPtr) =>\n originalPoll(inPtr, outPtr, nsubscriptions, neventsPtr),\n });\n\n const response = await fetch(this.options.wasmURL);\n if (!response.ok) {\n throw new Error(\n `failed to load ${String(this.options.wasmURL)}: ${response.status} ${response.statusText}`\n );\n }\n const module = await WebAssembly.compile(await response.arrayBuffer());\n const instance = await WebAssembly.instantiate(module, {\n wasi_snapshot_preview1: {\n ...shim.wasiImport,\n poll_oneoff: new jspi.Suspending(\n (inPtr: number, outPtr: number, nsubscriptions: number, neventsPtr: number) =>\n scheduler.pollOneOff(inPtr, outPtr, nsubscriptions, neventsPtr)\n ),\n },\n });\n (shim as unknown as { inst: WebAssembly.Instance }).inst =\n instance as WebAssembly.Instance;\n\n const start = jspi.promising(\n (instance as WebAssembly.Instance).exports._start\n );\n try {\n await start();\n this.options.onExit?.(0);\n } catch (error) {\n if (error instanceof WASIProcExit) {\n this.options.onExit?.(error.code);\n return;\n }\n throw error;\n }\n } catch (error) {\n this.options.onError?.(error instanceof Error ? error.message : String(error));\n }\n }\n}\n\nclass MainThreadInputFileDescriptor extends Fd {\n private readonly queue: MainThreadInputQueue;\n private readonly fdstat = (() => {\n const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0);\n fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_READ);\n return fdstat;\n })();\n\n constructor(queue: MainThreadInputQueue) {\n super();\n this.queue = queue;\n }\n\n override fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat } {\n return { ret: wasi.ERRNO_SUCCESS, fdstat: this.fdstat };\n }\n\n override fd_filestat_get(): { ret: number; filestat: wasi.Filestat } {\n return {\n ret: wasi.ERRNO_SUCCESS,\n filestat: new wasi.Filestat(0n, wasi.FILETYPE_CHARACTER_DEVICE, 0n),\n };\n }\n\n override fd_read(size: number): { ret: number; data: Uint8Array } {\n const chunk = this.queue.readAvailable(size);\n if (chunk) {\n return { ret: wasi.ERRNO_SUCCESS, data: chunk };\n }\n if (this.queue.isClosed()) {\n return { ret: wasi.ERRNO_SUCCESS, data: new Uint8Array() };\n }\n return { ret: wasi.ERRNO_AGAIN, data: new Uint8Array() };\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"MainThreadWasmExecutor.js","names":[],"sources":["../../../src/wasi/MainThreadWasmExecutor.ts"],"sourcesContent":["import { ConsoleStdout, Fd, WASI, WASIProcExit, wasi } from \"@bjorn3/browser_wasi_shim\";\n\nimport { MainThreadInputQueue } from \"./MainThreadInputQueue.ts\";\nimport { SuspendingWasiPollScheduler } from \"./WasiPollScheduler.ts\";\nimport { jspiConstructors } from \"./WasmEngineCapabilities.ts\";\nimport {\n MainThreadWasmPauseGate,\n PausableMonotonicClock,\n installPausableClockTimeGet,\n} from \"./WasmRuntimePause.ts\";\n\nexport interface MainThreadWasmExecutorOptions {\n wasmURL: string | URL;\n environment: Record<string, string>;\n onStdout(chunk: Uint8Array): void;\n onStderr(chunk: Uint8Array): void;\n onExit?(code: number): void;\n onError?(message: string): void;\n}\n\n/**\n * Runs a SwiftTUI WASI app on the main thread using WebAssembly JSPI: the\n * `poll_oneoff` import is wrapped in `WebAssembly.Suspending`, so the app's\n * run loop suspends to the browser event loop instead of blocking a worker\n * on `Atomics.wait`. Requires JSPI; needs neither a worker nor\n * SharedArrayBuffer (and therefore no COOP/COEP headers).\n *\n * Disposal closes stdin; the SwiftTUI runner exits cooperatively on stdin\n * hangup, which resolves the promised `_start` call.\n */\nexport class MainThreadWasmExecutor {\n private readonly options: MainThreadWasmExecutorOptions;\n private readonly stdin = new MainThreadInputQueue();\n private readonly pauseClock = new PausableMonotonicClock();\n private readonly pauseGate = new MainThreadWasmPauseGate(this.pauseClock);\n private didStart = false;\n\n constructor(options: MainThreadWasmExecutorOptions) {\n this.options = options;\n }\n\n start(): void {\n if (this.didStart) {\n return;\n }\n this.didStart = true;\n void this.run();\n }\n\n sendInput(chunk: Uint8Array): void {\n this.stdin.write(chunk);\n }\n\n /**\n * Suspends or resumes the app: while suspended, the run loop parks between\n * `poll_oneoff` waits and the app's monotonic clock freezes, so a hidden\n * scene costs no CPU and resumes without a deadline catch-up burst.\n */\n setSuspended(\n suspended: boolean\n ): void {\n this.pauseGate.setPaused(suspended);\n }\n\n dispose(): void {\n this.stdin.close();\n // Release a suspended run loop so it can observe the stdin hangup and\n // exit cooperatively instead of staying parked forever.\n this.pauseGate.setPaused(false);\n }\n\n private async run(): Promise<void> {\n try {\n const jspi = jspiConstructors();\n if (!jspi) {\n throw new Error(\n \"WebAssembly JSPI (Suspending/promising) is unavailable in this engine\"\n );\n }\n\n const shim = new WASI(\n [\"app.wasm\"],\n Object.entries(this.options.environment).map(([key, value]) => `${key}=${value}`),\n [\n new MainThreadInputFileDescriptor(this.stdin),\n new ConsoleStdout((chunk) => this.options.onStdout(chunk)),\n new ConsoleStdout((chunk) => this.options.onStderr(chunk)),\n ]\n );\n\n const instanceExports = (): { memory?: WebAssembly.Memory } | undefined =>\n (shim as unknown as { inst?: { exports: { memory?: WebAssembly.Memory } } }).inst\n ?.exports;\n const originalPoll = shim.wasiImport.poll_oneoff as (\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ) => number;\n const scheduler = new SuspendingWasiPollScheduler({\n memory: () => instanceExports()?.memory,\n stdin: this.stdin,\n fallbackPoll: (inPtr, outPtr, nsubscriptions, neventsPtr) =>\n originalPoll(inPtr, outPtr, nsubscriptions, neventsPtr),\n nowMilliseconds: () => this.pauseClock.nowMilliseconds(),\n pauseGate: this.pauseGate,\n });\n // Must run before `shim.wasiImport` is spread into the instantiate\n // imports below, or the wasm captures the unpausable clock.\n installPausableClockTimeGet(\n shim.wasiImport,\n () => instanceExports()?.memory,\n this.pauseClock\n );\n\n const response = await fetch(this.options.wasmURL);\n if (!response.ok) {\n throw new Error(\n `failed to load ${String(this.options.wasmURL)}: ${response.status} ${response.statusText}`\n );\n }\n const module = await WebAssembly.compile(await response.arrayBuffer());\n const instance = await WebAssembly.instantiate(module, {\n wasi_snapshot_preview1: {\n ...shim.wasiImport,\n poll_oneoff: new jspi.Suspending(\n (inPtr: number, outPtr: number, nsubscriptions: number, neventsPtr: number) =>\n scheduler.pollOneOff(inPtr, outPtr, nsubscriptions, neventsPtr)\n ),\n },\n });\n (shim as unknown as { inst: WebAssembly.Instance }).inst =\n instance as WebAssembly.Instance;\n\n const start = jspi.promising(\n (instance as WebAssembly.Instance).exports._start\n );\n try {\n await start();\n this.options.onExit?.(0);\n } catch (error) {\n if (error instanceof WASIProcExit) {\n this.options.onExit?.(error.code);\n return;\n }\n throw error;\n }\n } catch (error) {\n this.options.onError?.(error instanceof Error ? error.message : String(error));\n }\n }\n}\n\nclass MainThreadInputFileDescriptor extends Fd {\n private readonly queue: MainThreadInputQueue;\n private readonly fdstat = (() => {\n const fdstat = new wasi.Fdstat(wasi.FILETYPE_CHARACTER_DEVICE, 0);\n fdstat.fs_rights_base = BigInt(wasi.RIGHTS_FD_READ);\n return fdstat;\n })();\n\n constructor(queue: MainThreadInputQueue) {\n super();\n this.queue = queue;\n }\n\n override fd_fdstat_get(): { ret: number; fdstat: wasi.Fdstat } {\n return { ret: wasi.ERRNO_SUCCESS, fdstat: this.fdstat };\n }\n\n override fd_filestat_get(): { ret: number; filestat: wasi.Filestat } {\n return {\n ret: wasi.ERRNO_SUCCESS,\n filestat: new wasi.Filestat(0n, wasi.FILETYPE_CHARACTER_DEVICE, 0n),\n };\n }\n\n override fd_read(size: number): { ret: number; data: Uint8Array } {\n const chunk = this.queue.readAvailable(size);\n if (chunk) {\n return { ret: wasi.ERRNO_SUCCESS, data: chunk };\n }\n if (this.queue.isClosed()) {\n return { ret: wasi.ERRNO_SUCCESS, data: new Uint8Array() };\n }\n return { ret: wasi.ERRNO_AGAIN, data: new Uint8Array() };\n }\n}\n"],"mappings":"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"}
|
|
@@ -8,12 +8,14 @@ var WasiPollScheduler = class {
|
|
|
8
8
|
stdin;
|
|
9
9
|
fallbackPoll;
|
|
10
10
|
nowMilliseconds;
|
|
11
|
+
pauseGate;
|
|
11
12
|
waitBuffer = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
12
13
|
constructor(options) {
|
|
13
14
|
this.memory = options.memory;
|
|
14
15
|
this.stdin = options.stdin;
|
|
15
16
|
this.fallbackPoll = options.fallbackPoll;
|
|
16
17
|
this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());
|
|
18
|
+
this.pauseGate = options.pauseGate;
|
|
17
19
|
}
|
|
18
20
|
pollOneOff(inPtr, outPtr, nsubscriptions, neventsPtr) {
|
|
19
21
|
const memory = this.memory();
|
|
@@ -22,6 +24,7 @@ var WasiPollScheduler = class {
|
|
|
22
24
|
const subscriptions = readSubscriptions(view, inPtr, nsubscriptions, this.nowMilliseconds());
|
|
23
25
|
if (subscriptions === void 0) return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);
|
|
24
26
|
while (true) {
|
|
27
|
+
this.pauseGate?.blockWhilePaused();
|
|
25
28
|
const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());
|
|
26
29
|
if (ready.length > 0) {
|
|
27
30
|
writeEvents(view, outPtr, ready, this.stdin);
|
|
@@ -46,11 +49,13 @@ var SuspendingWasiPollScheduler = class {
|
|
|
46
49
|
stdin;
|
|
47
50
|
fallbackPoll;
|
|
48
51
|
nowMilliseconds;
|
|
52
|
+
pauseGate;
|
|
49
53
|
constructor(options) {
|
|
50
54
|
this.memory = options.memory;
|
|
51
55
|
this.stdin = options.stdin;
|
|
52
56
|
this.fallbackPoll = options.fallbackPoll;
|
|
53
57
|
this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());
|
|
58
|
+
this.pauseGate = options.pauseGate;
|
|
54
59
|
}
|
|
55
60
|
async pollOneOff(inPtr, outPtr, nsubscriptions, neventsPtr) {
|
|
56
61
|
const memory = this.memory();
|
|
@@ -58,6 +63,7 @@ var SuspendingWasiPollScheduler = class {
|
|
|
58
63
|
const subscriptions = readSubscriptions(new DataView(memory.buffer), inPtr, nsubscriptions, this.nowMilliseconds());
|
|
59
64
|
if (subscriptions === void 0) return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);
|
|
60
65
|
while (true) {
|
|
66
|
+
await this.pauseGate?.waitWhilePaused();
|
|
61
67
|
const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());
|
|
62
68
|
if (ready.length > 0) {
|
|
63
69
|
const view = new DataView(memory.buffer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WasiPollScheduler.js","names":[],"sources":["../../../src/wasi/WasiPollScheduler.ts"],"sourcesContent":["import { wasi } from \"@bjorn3/browser_wasi_shim\";\n\nimport type { SharedInputReadiness } from \"./SharedInputQueue.ts\";\n\nconst subscriptionByteLength = 48;\nconst eventByteLength = 32;\nconst maximumAtomicsWaitMilliseconds = 2_147_483_647;\n\ninterface ClockSubscription {\n readonly type: \"clock\";\n readonly userdata: bigint;\n readonly clockid: number;\n readonly deadlineMilliseconds: number;\n}\n\ninterface FdReadSubscription {\n readonly type: \"fdRead\";\n readonly userdata: bigint;\n readonly fd: number;\n}\n\ntype SupportedSubscription = ClockSubscription | FdReadSubscription;\n\nexport interface WasiPollReadableState {\n availableBytes(): number;\n isClosed(): boolean;\n}\n\nexport interface WasiPollReadableSource extends WasiPollReadableState {\n waitForReadable(timeoutMilliseconds?: number): SharedInputReadiness;\n}\n\nexport interface SuspendingWasiPollReadableSource extends WasiPollReadableState {\n waitForReadableAsync(timeoutMilliseconds?: number): Promise<SharedInputReadiness>;\n}\n\nexport interface WasiPollSchedulerOptions {\n memory(): WebAssembly.Memory | undefined;\n stdin: WasiPollReadableSource;\n fallbackPoll(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number;\n nowMilliseconds?(): number;\n}\n\nexport class WasiPollScheduler {\n private readonly memory: WasiPollSchedulerOptions[\"memory\"];\n private readonly stdin: WasiPollReadableSource;\n private readonly fallbackPoll: WasiPollSchedulerOptions[\"fallbackPoll\"];\n private readonly nowMilliseconds: () => number;\n private readonly waitBuffer = new Int32Array(\n new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)\n );\n\n constructor(options: WasiPollSchedulerOptions) {\n this.memory = options.memory;\n this.stdin = options.stdin;\n this.fallbackPoll = options.fallbackPoll;\n this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());\n }\n\n pollOneOff(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number {\n const memory = this.memory();\n if (!memory || nsubscriptions <= 0) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n const view = new DataView(memory.buffer);\n const subscriptions = readSubscriptions(\n view,\n inPtr,\n nsubscriptions,\n this.nowMilliseconds()\n );\n if (subscriptions === undefined) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n while (true) {\n const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());\n if (ready.length > 0) {\n writeEvents(view, outPtr, ready, this.stdin);\n if (neventsPtr !== undefined) {\n view.setUint32(neventsPtr, ready.length, true);\n }\n return wasi.ERRNO_SUCCESS;\n }\n\n const timeoutMilliseconds = shortestClockTimeoutMilliseconds(\n subscriptions,\n this.nowMilliseconds()\n );\n if (hasFdReadSubscription(subscriptions)) {\n this.stdin.waitForReadable(timeoutMilliseconds);\n } else if (timeoutMilliseconds !== undefined) {\n Atomics.wait(\n this.waitBuffer,\n 0,\n 0,\n Math.min(timeoutMilliseconds, maximumAtomicsWaitMilliseconds)\n );\n } else {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n }\n }\n}\n\nexport interface SuspendingWasiPollSchedulerOptions {\n memory(): WebAssembly.Memory | undefined;\n stdin: SuspendingWasiPollReadableSource;\n fallbackPoll(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number;\n nowMilliseconds?(): number;\n}\n\n/**\n * The JSPI (main-thread) counterpart of `WasiPollScheduler`: identical\n * subscription semantics, but the blocking waits become awaited promises so\n * the surrounding `poll_oneoff` import can be wrapped in\n * `WebAssembly.Suspending` and run without a worker or SharedArrayBuffer.\n */\nexport class SuspendingWasiPollScheduler {\n private readonly memory: SuspendingWasiPollSchedulerOptions[\"memory\"];\n private readonly stdin: SuspendingWasiPollReadableSource;\n private readonly fallbackPoll: SuspendingWasiPollSchedulerOptions[\"fallbackPoll\"];\n private readonly nowMilliseconds: () => number;\n\n constructor(options: SuspendingWasiPollSchedulerOptions) {\n this.memory = options.memory;\n this.stdin = options.stdin;\n this.fallbackPoll = options.fallbackPoll;\n this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());\n }\n\n async pollOneOff(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): Promise<number> {\n const memory = this.memory();\n if (!memory || nsubscriptions <= 0) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n const subscriptions = readSubscriptions(\n new DataView(memory.buffer),\n inPtr,\n nsubscriptions,\n this.nowMilliseconds()\n );\n if (subscriptions === undefined) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n while (true) {\n const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());\n if (ready.length > 0) {\n // Re-derive the view each pass: memory.buffer detaches when the wasm\n // grows memory while we were suspended.\n const view = new DataView(memory.buffer);\n writeEvents(view, outPtr, ready, this.stdin);\n if (neventsPtr !== undefined) {\n view.setUint32(neventsPtr, ready.length, true);\n }\n return wasi.ERRNO_SUCCESS;\n }\n\n const timeoutMilliseconds = shortestClockTimeoutMilliseconds(\n subscriptions,\n this.nowMilliseconds()\n );\n if (hasFdReadSubscription(subscriptions)) {\n await this.stdin.waitForReadableAsync(timeoutMilliseconds);\n } else if (timeoutMilliseconds !== undefined) {\n await new Promise((resolve) =>\n setTimeout(resolve, Math.min(timeoutMilliseconds, maximumAtomicsWaitMilliseconds))\n );\n } else {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n }\n }\n}\n\nfunction readSubscriptions(\n view: DataView,\n inPtr: number,\n nsubscriptions: number,\n nowMilliseconds: number\n): SupportedSubscription[] | undefined {\n const subscriptions: SupportedSubscription[] = [];\n for (let index = 0; index < nsubscriptions; index += 1) {\n const subscription = wasi.Subscription.read_bytes(\n view,\n inPtr + index * subscriptionByteLength\n );\n switch (subscription.eventtype) {\n case wasi.EVENTTYPE_CLOCK:\n if (!isSupportedClockId(subscription.clockid)) {\n return undefined;\n }\n subscriptions.push({\n type: \"clock\",\n userdata: subscription.userdata,\n clockid: subscription.clockid,\n deadlineMilliseconds: clockDeadlineMilliseconds(subscription, nowMilliseconds),\n });\n break;\n case wasi.EVENTTYPE_FD_READ:\n if (subscription.clockid !== wasi.FD_STDIN) {\n return undefined;\n }\n subscriptions.push({\n type: \"fdRead\",\n userdata: subscription.userdata,\n fd: subscription.clockid,\n });\n break;\n default:\n return undefined;\n }\n }\n return subscriptions;\n}\n\nfunction isSupportedClockId(\n clockid: number\n): boolean {\n return clockid === wasi.CLOCKID_MONOTONIC || clockid === wasi.CLOCKID_REALTIME;\n}\n\nfunction shortestClockTimeoutMilliseconds(\n subscriptions: readonly SupportedSubscription[],\n nowMilliseconds: number\n): number | undefined {\n let timeoutMilliseconds: number | undefined;\n for (const subscription of subscriptions) {\n if (subscription.type !== \"clock\") {\n continue;\n }\n const remaining = clockRemainingMilliseconds(subscription, nowMilliseconds);\n timeoutMilliseconds = timeoutMilliseconds === undefined\n ? remaining\n : Math.min(timeoutMilliseconds, remaining);\n }\n return timeoutMilliseconds;\n}\n\nfunction readySubscriptions(\n subscriptions: readonly SupportedSubscription[],\n stdin: WasiPollReadableState,\n nowMilliseconds: number\n): SupportedSubscription[] {\n return subscriptions.filter((subscription) => {\n switch (subscription.type) {\n case \"clock\":\n return clockRemainingMilliseconds(subscription, nowMilliseconds) <= 0;\n case \"fdRead\":\n return stdin.availableBytes() > 0 || stdin.isClosed();\n }\n });\n}\n\nfunction hasFdReadSubscription(\n subscriptions: readonly SupportedSubscription[]\n): boolean {\n return subscriptions.some((subscription) => subscription.type === \"fdRead\");\n}\n\nfunction clockRemainingMilliseconds(\n subscription: ClockSubscription,\n nowMilliseconds: number\n): number {\n return Math.max(0, subscription.deadlineMilliseconds - nowMillisecondsForClock(\n subscription.clockid,\n nowMilliseconds\n ));\n}\n\nfunction clockDeadlineMilliseconds(\n subscription: wasi.Subscription,\n nowMilliseconds: number\n): number {\n if ((subscription.flags & wasi.SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME) !== 0) {\n return Number(subscription.timeout) / 1_000_000;\n }\n return nowMillisecondsForClock(subscription.clockid, nowMilliseconds)\n + Number(subscription.timeout) / 1_000_000;\n}\n\nfunction nowMillisecondsForClock(\n clockid: number,\n nowMilliseconds: number\n): number {\n if (clockid === wasi.CLOCKID_REALTIME) {\n return Date.now();\n }\n return nowMilliseconds;\n}\n\nfunction writeEvents(\n view: DataView,\n outPtr: number,\n subscriptions: readonly SupportedSubscription[],\n stdin: WasiPollReadableSource\n): void {\n subscriptions.forEach((subscription, index) => {\n const eventtype = subscription.type === \"clock\"\n ? wasi.EVENTTYPE_CLOCK\n : wasi.EVENTTYPE_FD_READ;\n const offset = outPtr + index * eventByteLength;\n new wasi.Event(\n subscription.userdata,\n wasi.ERRNO_SUCCESS,\n eventtype\n ).write_bytes(view, offset);\n if (subscription.type === \"fdRead\") {\n const availableBytes = Math.max(0, stdin.availableBytes());\n view.setBigUint64(offset + 16, BigInt(availableBytes), true);\n if (availableBytes === 0 && stdin.isClosed()) {\n view.setUint16(offset + 24, wasi.EVENTRWFLAGS_FD_READWRITE_HANGUP, true);\n }\n }\n });\n}\n\nexport function writeClockSubscriptionForTesting(\n view: DataView,\n offset: number,\n subscription: {\n userdata: bigint;\n timeoutNanoseconds: bigint;\n clockid?: number;\n flags?: number;\n }\n): void {\n clearRecord(view, offset, subscriptionByteLength);\n view.setBigUint64(offset, subscription.userdata, true);\n view.setUint8(offset + 8, wasi.EVENTTYPE_CLOCK);\n view.setUint32(offset + 16, subscription.clockid ?? wasi.CLOCKID_MONOTONIC, true);\n view.setBigUint64(offset + 24, subscription.timeoutNanoseconds, true);\n view.setUint16(offset + 36, subscription.flags ?? 0, true);\n}\n\nexport function writeFdReadSubscriptionForTesting(\n view: DataView,\n offset: number,\n subscription: {\n userdata: bigint;\n fd: number;\n }\n): void {\n clearRecord(view, offset, subscriptionByteLength);\n view.setBigUint64(offset, subscription.userdata, true);\n view.setUint8(offset + 8, wasi.EVENTTYPE_FD_READ);\n view.setUint32(offset + 16, subscription.fd, true);\n}\n\nexport function readPollEventsForTesting(\n view: DataView,\n offset: number,\n count: number\n): Array<{ userdata: bigint; errno: number; eventtype: number }> {\n return Array.from({ length: count }, (_, index) => {\n const eventOffset = offset + index * eventByteLength;\n return {\n userdata: view.getBigUint64(eventOffset, true),\n errno: view.getUint16(eventOffset + 8, true),\n eventtype: view.getUint8(eventOffset + 10),\n };\n });\n}\n\nfunction clearRecord(\n view: DataView,\n offset: number,\n byteLength: number\n): void {\n new Uint8Array(view.buffer, offset, byteLength).fill(0);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WasiPollScheduler.js","names":[],"sources":["../../../src/wasi/WasiPollScheduler.ts"],"sourcesContent":["import { wasi } from \"@bjorn3/browser_wasi_shim\";\n\nimport type { SharedInputReadiness } from \"./SharedInputQueue.ts\";\n\nconst subscriptionByteLength = 48;\nconst eventByteLength = 32;\nconst maximumAtomicsWaitMilliseconds = 2_147_483_647;\n\ninterface ClockSubscription {\n readonly type: \"clock\";\n readonly userdata: bigint;\n readonly clockid: number;\n readonly deadlineMilliseconds: number;\n}\n\ninterface FdReadSubscription {\n readonly type: \"fdRead\";\n readonly userdata: bigint;\n readonly fd: number;\n}\n\ntype SupportedSubscription = ClockSubscription | FdReadSubscription;\n\nexport interface WasiPollReadableState {\n availableBytes(): number;\n isClosed(): boolean;\n}\n\nexport interface WasiPollReadableSource extends WasiPollReadableState {\n waitForReadable(timeoutMilliseconds?: number): SharedInputReadiness;\n}\n\nexport interface SuspendingWasiPollReadableSource extends WasiPollReadableState {\n waitForReadableAsync(timeoutMilliseconds?: number): Promise<SharedInputReadiness>;\n}\n\n/**\n * Host-controlled suspension seam for the blocking (worker) scheduler. The\n * gate is consulted between waits: while the host holds the runtime paused,\n * `blockWhilePaused` parks the worker thread and — via the shared pausable\n * clock — excludes the parked time from every clock deadline, so pending\n * timeouts resume with their remaining time intact.\n */\nexport interface WasiPollPauseGate {\n blockWhilePaused(): void;\n}\n\n/** The awaited counterpart of {@link WasiPollPauseGate} for the JSPI path. */\nexport interface SuspendingWasiPollPauseGate {\n waitWhilePaused(): Promise<void>;\n}\n\nexport interface WasiPollSchedulerOptions {\n memory(): WebAssembly.Memory | undefined;\n stdin: WasiPollReadableSource;\n fallbackPoll(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number;\n nowMilliseconds?(): number;\n pauseGate?: WasiPollPauseGate;\n}\n\nexport class WasiPollScheduler {\n private readonly memory: WasiPollSchedulerOptions[\"memory\"];\n private readonly stdin: WasiPollReadableSource;\n private readonly fallbackPoll: WasiPollSchedulerOptions[\"fallbackPoll\"];\n private readonly nowMilliseconds: () => number;\n private readonly pauseGate?: WasiPollPauseGate;\n private readonly waitBuffer = new Int32Array(\n new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)\n );\n\n constructor(options: WasiPollSchedulerOptions) {\n this.memory = options.memory;\n this.stdin = options.stdin;\n this.fallbackPoll = options.fallbackPoll;\n this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());\n this.pauseGate = options.pauseGate;\n }\n\n pollOneOff(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number {\n const memory = this.memory();\n if (!memory || nsubscriptions <= 0) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n const view = new DataView(memory.buffer);\n const subscriptions = readSubscriptions(\n view,\n inPtr,\n nsubscriptions,\n this.nowMilliseconds()\n );\n if (subscriptions === undefined) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n while (true) {\n this.pauseGate?.blockWhilePaused();\n const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());\n if (ready.length > 0) {\n writeEvents(view, outPtr, ready, this.stdin);\n if (neventsPtr !== undefined) {\n view.setUint32(neventsPtr, ready.length, true);\n }\n return wasi.ERRNO_SUCCESS;\n }\n\n const timeoutMilliseconds = shortestClockTimeoutMilliseconds(\n subscriptions,\n this.nowMilliseconds()\n );\n if (hasFdReadSubscription(subscriptions)) {\n this.stdin.waitForReadable(timeoutMilliseconds);\n } else if (timeoutMilliseconds !== undefined) {\n Atomics.wait(\n this.waitBuffer,\n 0,\n 0,\n Math.min(timeoutMilliseconds, maximumAtomicsWaitMilliseconds)\n );\n } else {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n }\n }\n}\n\nexport interface SuspendingWasiPollSchedulerOptions {\n memory(): WebAssembly.Memory | undefined;\n stdin: SuspendingWasiPollReadableSource;\n fallbackPoll(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): number;\n nowMilliseconds?(): number;\n pauseGate?: SuspendingWasiPollPauseGate;\n}\n\n/**\n * The JSPI (main-thread) counterpart of `WasiPollScheduler`: identical\n * subscription semantics, but the blocking waits become awaited promises so\n * the surrounding `poll_oneoff` import can be wrapped in\n * `WebAssembly.Suspending` and run without a worker or SharedArrayBuffer.\n */\nexport class SuspendingWasiPollScheduler {\n private readonly memory: SuspendingWasiPollSchedulerOptions[\"memory\"];\n private readonly stdin: SuspendingWasiPollReadableSource;\n private readonly fallbackPoll: SuspendingWasiPollSchedulerOptions[\"fallbackPoll\"];\n private readonly nowMilliseconds: () => number;\n private readonly pauseGate?: SuspendingWasiPollPauseGate;\n\n constructor(options: SuspendingWasiPollSchedulerOptions) {\n this.memory = options.memory;\n this.stdin = options.stdin;\n this.fallbackPoll = options.fallbackPoll;\n this.nowMilliseconds = options.nowMilliseconds ?? (() => performance.now());\n this.pauseGate = options.pauseGate;\n }\n\n async pollOneOff(\n inPtr: number,\n outPtr: number,\n nsubscriptions: number,\n neventsPtr?: number\n ): Promise<number> {\n const memory = this.memory();\n if (!memory || nsubscriptions <= 0) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n const subscriptions = readSubscriptions(\n new DataView(memory.buffer),\n inPtr,\n nsubscriptions,\n this.nowMilliseconds()\n );\n if (subscriptions === undefined) {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n\n while (true) {\n await this.pauseGate?.waitWhilePaused();\n const ready = readySubscriptions(subscriptions, this.stdin, this.nowMilliseconds());\n if (ready.length > 0) {\n // Re-derive the view each pass: memory.buffer detaches when the wasm\n // grows memory while we were suspended.\n const view = new DataView(memory.buffer);\n writeEvents(view, outPtr, ready, this.stdin);\n if (neventsPtr !== undefined) {\n view.setUint32(neventsPtr, ready.length, true);\n }\n return wasi.ERRNO_SUCCESS;\n }\n\n const timeoutMilliseconds = shortestClockTimeoutMilliseconds(\n subscriptions,\n this.nowMilliseconds()\n );\n if (hasFdReadSubscription(subscriptions)) {\n await this.stdin.waitForReadableAsync(timeoutMilliseconds);\n } else if (timeoutMilliseconds !== undefined) {\n await new Promise((resolve) =>\n setTimeout(resolve, Math.min(timeoutMilliseconds, maximumAtomicsWaitMilliseconds))\n );\n } else {\n return this.fallbackPoll(inPtr, outPtr, nsubscriptions, neventsPtr);\n }\n }\n }\n}\n\nfunction readSubscriptions(\n view: DataView,\n inPtr: number,\n nsubscriptions: number,\n nowMilliseconds: number\n): SupportedSubscription[] | undefined {\n const subscriptions: SupportedSubscription[] = [];\n for (let index = 0; index < nsubscriptions; index += 1) {\n const subscription = wasi.Subscription.read_bytes(\n view,\n inPtr + index * subscriptionByteLength\n );\n switch (subscription.eventtype) {\n case wasi.EVENTTYPE_CLOCK:\n if (!isSupportedClockId(subscription.clockid)) {\n return undefined;\n }\n subscriptions.push({\n type: \"clock\",\n userdata: subscription.userdata,\n clockid: subscription.clockid,\n deadlineMilliseconds: clockDeadlineMilliseconds(subscription, nowMilliseconds),\n });\n break;\n case wasi.EVENTTYPE_FD_READ:\n if (subscription.clockid !== wasi.FD_STDIN) {\n return undefined;\n }\n subscriptions.push({\n type: \"fdRead\",\n userdata: subscription.userdata,\n fd: subscription.clockid,\n });\n break;\n default:\n return undefined;\n }\n }\n return subscriptions;\n}\n\nfunction isSupportedClockId(\n clockid: number\n): boolean {\n return clockid === wasi.CLOCKID_MONOTONIC || clockid === wasi.CLOCKID_REALTIME;\n}\n\nfunction shortestClockTimeoutMilliseconds(\n subscriptions: readonly SupportedSubscription[],\n nowMilliseconds: number\n): number | undefined {\n let timeoutMilliseconds: number | undefined;\n for (const subscription of subscriptions) {\n if (subscription.type !== \"clock\") {\n continue;\n }\n const remaining = clockRemainingMilliseconds(subscription, nowMilliseconds);\n timeoutMilliseconds = timeoutMilliseconds === undefined\n ? remaining\n : Math.min(timeoutMilliseconds, remaining);\n }\n return timeoutMilliseconds;\n}\n\nfunction readySubscriptions(\n subscriptions: readonly SupportedSubscription[],\n stdin: WasiPollReadableState,\n nowMilliseconds: number\n): SupportedSubscription[] {\n return subscriptions.filter((subscription) => {\n switch (subscription.type) {\n case \"clock\":\n return clockRemainingMilliseconds(subscription, nowMilliseconds) <= 0;\n case \"fdRead\":\n return stdin.availableBytes() > 0 || stdin.isClosed();\n }\n });\n}\n\nfunction hasFdReadSubscription(\n subscriptions: readonly SupportedSubscription[]\n): boolean {\n return subscriptions.some((subscription) => subscription.type === \"fdRead\");\n}\n\nfunction clockRemainingMilliseconds(\n subscription: ClockSubscription,\n nowMilliseconds: number\n): number {\n return Math.max(0, subscription.deadlineMilliseconds - nowMillisecondsForClock(\n subscription.clockid,\n nowMilliseconds\n ));\n}\n\nfunction clockDeadlineMilliseconds(\n subscription: wasi.Subscription,\n nowMilliseconds: number\n): number {\n if ((subscription.flags & wasi.SUBCLOCKFLAGS_SUBSCRIPTION_CLOCK_ABSTIME) !== 0) {\n return Number(subscription.timeout) / 1_000_000;\n }\n return nowMillisecondsForClock(subscription.clockid, nowMilliseconds)\n + Number(subscription.timeout) / 1_000_000;\n}\n\nfunction nowMillisecondsForClock(\n clockid: number,\n nowMilliseconds: number\n): number {\n if (clockid === wasi.CLOCKID_REALTIME) {\n return Date.now();\n }\n return nowMilliseconds;\n}\n\nfunction writeEvents(\n view: DataView,\n outPtr: number,\n subscriptions: readonly SupportedSubscription[],\n stdin: WasiPollReadableSource\n): void {\n subscriptions.forEach((subscription, index) => {\n const eventtype = subscription.type === \"clock\"\n ? wasi.EVENTTYPE_CLOCK\n : wasi.EVENTTYPE_FD_READ;\n const offset = outPtr + index * eventByteLength;\n new wasi.Event(\n subscription.userdata,\n wasi.ERRNO_SUCCESS,\n eventtype\n ).write_bytes(view, offset);\n if (subscription.type === \"fdRead\") {\n const availableBytes = Math.max(0, stdin.availableBytes());\n view.setBigUint64(offset + 16, BigInt(availableBytes), true);\n if (availableBytes === 0 && stdin.isClosed()) {\n view.setUint16(offset + 24, wasi.EVENTRWFLAGS_FD_READWRITE_HANGUP, true);\n }\n }\n });\n}\n\nexport function writeClockSubscriptionForTesting(\n view: DataView,\n offset: number,\n subscription: {\n userdata: bigint;\n timeoutNanoseconds: bigint;\n clockid?: number;\n flags?: number;\n }\n): void {\n clearRecord(view, offset, subscriptionByteLength);\n view.setBigUint64(offset, subscription.userdata, true);\n view.setUint8(offset + 8, wasi.EVENTTYPE_CLOCK);\n view.setUint32(offset + 16, subscription.clockid ?? wasi.CLOCKID_MONOTONIC, true);\n view.setBigUint64(offset + 24, subscription.timeoutNanoseconds, true);\n view.setUint16(offset + 36, subscription.flags ?? 0, true);\n}\n\nexport function writeFdReadSubscriptionForTesting(\n view: DataView,\n offset: number,\n subscription: {\n userdata: bigint;\n fd: number;\n }\n): void {\n clearRecord(view, offset, subscriptionByteLength);\n view.setBigUint64(offset, subscription.userdata, true);\n view.setUint8(offset + 8, wasi.EVENTTYPE_FD_READ);\n view.setUint32(offset + 16, subscription.fd, true);\n}\n\nexport function readPollEventsForTesting(\n view: DataView,\n offset: number,\n count: number\n): Array<{ userdata: bigint; errno: number; eventtype: number }> {\n return Array.from({ length: count }, (_, index) => {\n const eventOffset = offset + index * eventByteLength;\n return {\n userdata: view.getBigUint64(eventOffset, true),\n errno: view.getUint16(eventOffset + 8, true),\n eventtype: view.getUint8(eventOffset + 10),\n };\n });\n}\n\nfunction clearRecord(\n view: DataView,\n offset: number,\n byteLength: number\n): void {\n new Uint8Array(view.buffer, offset, byteLength).fill(0);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/wasi/WasmRuntimePause.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A monotonic clock that excludes time spent paused. `nowMilliseconds()`
|
|
4
|
+
* mirrors `performance.now()` minus every accumulated pause, so WASI clock
|
|
5
|
+
* reads (`clock_time_get`) and `poll_oneoff` deadline math both observe a
|
|
6
|
+
* frozen clock across a pause: pending timeouts keep their remaining time,
|
|
7
|
+
* no expired-deadline catch-up burst fires on resume, and app-side animation
|
|
8
|
+
* clocks do not jump.
|
|
9
|
+
*/
|
|
10
|
+
declare class PausableMonotonicClock {
|
|
11
|
+
private readonly rawNowMilliseconds;
|
|
12
|
+
private accumulatedPauseMilliseconds;
|
|
13
|
+
constructor(rawNowMilliseconds?: () => number);
|
|
14
|
+
nowMilliseconds(): number;
|
|
15
|
+
nowNanoseconds(): bigint;
|
|
16
|
+
addPausedMilliseconds(milliseconds: number): void;
|
|
17
|
+
get pausedMilliseconds(): number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates the shared pause cell the main thread uses to suspend a wasm scene
|
|
21
|
+
* worker. One Int32 slot: 0 = running, 1 = paused. The worker parks on the
|
|
22
|
+
* cell between `poll_oneoff` waits, so a paused scene costs zero CPU.
|
|
23
|
+
*/
|
|
24
|
+
declare function createWasmPauseCell(): SharedArrayBuffer;
|
|
25
|
+
declare function setWasmPauseCellPaused(cell: SharedArrayBuffer, paused: boolean): void;
|
|
26
|
+
declare function isWasmPauseCellPaused(cell: SharedArrayBuffer): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Worker-side pause gate: blocks the calling (worker) thread while the shared
|
|
29
|
+
* pause cell is set, then credits the parked wall time to the pausable clock.
|
|
30
|
+
* Must never run on a browser main thread — `Atomics.wait` is worker-only.
|
|
31
|
+
*/
|
|
32
|
+
declare class WorkerWasmPauseGate {
|
|
33
|
+
private readonly flags;
|
|
34
|
+
private readonly clock;
|
|
35
|
+
private readonly rawNowMilliseconds;
|
|
36
|
+
constructor(cell: SharedArrayBuffer, clock: PausableMonotonicClock, rawNowMilliseconds?: () => number);
|
|
37
|
+
blockWhilePaused(): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Main-thread (JSPI) pause gate: the awaited counterpart of
|
|
41
|
+
* `WorkerWasmPauseGate`. While paused, `waitWhilePaused` suspends on a promise
|
|
42
|
+
* that `setPaused(false)` resolves, then credits the parked time to the clock.
|
|
43
|
+
*/
|
|
44
|
+
declare class MainThreadWasmPauseGate {
|
|
45
|
+
private readonly clock;
|
|
46
|
+
private readonly rawNowMilliseconds;
|
|
47
|
+
private paused;
|
|
48
|
+
private resumeWaiters;
|
|
49
|
+
constructor(clock: PausableMonotonicClock, rawNowMilliseconds?: () => number);
|
|
50
|
+
get isPaused(): boolean;
|
|
51
|
+
setPaused(paused: boolean): void;
|
|
52
|
+
waitWhilePaused(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Redirects the WASI `clock_time_get` import's MONOTONIC reads through the
|
|
56
|
+
* pausable clock so app-side time agrees with the pause-aware `poll_oneoff`
|
|
57
|
+
* deadline math. Non-monotonic clocks (REALTIME) keep the shim's behavior —
|
|
58
|
+
* wall-clock time genuinely advances across a pause.
|
|
59
|
+
*/
|
|
60
|
+
declare function installPausableClockTimeGet(wasiImport: Record<string, unknown>, memory: () => WebAssembly.Memory | undefined, clock: PausableMonotonicClock): void;
|
|
61
|
+
//#endregion
|
|
62
|
+
export { MainThreadWasmPauseGate, PausableMonotonicClock, WorkerWasmPauseGate, createWasmPauseCell, installPausableClockTimeGet, isWasmPauseCellPaused, setWasmPauseCellPaused };
|
|
63
|
+
//# sourceMappingURL=WasmRuntimePause.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { wasi } from "@bjorn3/browser_wasi_shim";
|
|
2
|
+
//#region src/wasi/WasmRuntimePause.ts
|
|
3
|
+
const pausedFlagValue = 1;
|
|
4
|
+
/**
|
|
5
|
+
* A monotonic clock that excludes time spent paused. `nowMilliseconds()`
|
|
6
|
+
* mirrors `performance.now()` minus every accumulated pause, so WASI clock
|
|
7
|
+
* reads (`clock_time_get`) and `poll_oneoff` deadline math both observe a
|
|
8
|
+
* frozen clock across a pause: pending timeouts keep their remaining time,
|
|
9
|
+
* no expired-deadline catch-up burst fires on resume, and app-side animation
|
|
10
|
+
* clocks do not jump.
|
|
11
|
+
*/
|
|
12
|
+
var PausableMonotonicClock = class {
|
|
13
|
+
rawNowMilliseconds;
|
|
14
|
+
accumulatedPauseMilliseconds = 0;
|
|
15
|
+
constructor(rawNowMilliseconds = () => performance.now()) {
|
|
16
|
+
this.rawNowMilliseconds = rawNowMilliseconds;
|
|
17
|
+
}
|
|
18
|
+
nowMilliseconds() {
|
|
19
|
+
return this.rawNowMilliseconds() - this.accumulatedPauseMilliseconds;
|
|
20
|
+
}
|
|
21
|
+
nowNanoseconds() {
|
|
22
|
+
return BigInt(Math.round(this.nowMilliseconds() * 1e6));
|
|
23
|
+
}
|
|
24
|
+
addPausedMilliseconds(milliseconds) {
|
|
25
|
+
if (milliseconds > 0) this.accumulatedPauseMilliseconds += milliseconds;
|
|
26
|
+
}
|
|
27
|
+
get pausedMilliseconds() {
|
|
28
|
+
return this.accumulatedPauseMilliseconds;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Creates the shared pause cell the main thread uses to suspend a wasm scene
|
|
33
|
+
* worker. One Int32 slot: 0 = running, 1 = paused. The worker parks on the
|
|
34
|
+
* cell between `poll_oneoff` waits, so a paused scene costs zero CPU.
|
|
35
|
+
*/
|
|
36
|
+
function createWasmPauseCell() {
|
|
37
|
+
if (typeof SharedArrayBuffer === "undefined") throw new Error("SharedArrayBuffer is unavailable. Serve the app with COOP/COEP headers so worker-mode pause can work.");
|
|
38
|
+
return new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
|
|
39
|
+
}
|
|
40
|
+
function setWasmPauseCellPaused(cell, paused) {
|
|
41
|
+
const flags = new Int32Array(cell);
|
|
42
|
+
Atomics.store(flags, 0, paused ? pausedFlagValue : 0);
|
|
43
|
+
if (!paused) Atomics.notify(flags, 0);
|
|
44
|
+
}
|
|
45
|
+
function isWasmPauseCellPaused(cell) {
|
|
46
|
+
return Atomics.load(new Int32Array(cell), 0) === pausedFlagValue;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Worker-side pause gate: blocks the calling (worker) thread while the shared
|
|
50
|
+
* pause cell is set, then credits the parked wall time to the pausable clock.
|
|
51
|
+
* Must never run on a browser main thread — `Atomics.wait` is worker-only.
|
|
52
|
+
*/
|
|
53
|
+
var WorkerWasmPauseGate = class {
|
|
54
|
+
flags;
|
|
55
|
+
clock;
|
|
56
|
+
rawNowMilliseconds;
|
|
57
|
+
constructor(cell, clock, rawNowMilliseconds = () => performance.now()) {
|
|
58
|
+
this.flags = new Int32Array(cell);
|
|
59
|
+
this.clock = clock;
|
|
60
|
+
this.rawNowMilliseconds = rawNowMilliseconds;
|
|
61
|
+
}
|
|
62
|
+
blockWhilePaused() {
|
|
63
|
+
let pausedMilliseconds = 0;
|
|
64
|
+
while (Atomics.load(this.flags, 0) === pausedFlagValue) {
|
|
65
|
+
const parkStart = this.rawNowMilliseconds();
|
|
66
|
+
Atomics.wait(this.flags, 0, pausedFlagValue);
|
|
67
|
+
pausedMilliseconds += this.rawNowMilliseconds() - parkStart;
|
|
68
|
+
}
|
|
69
|
+
this.clock.addPausedMilliseconds(pausedMilliseconds);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Main-thread (JSPI) pause gate: the awaited counterpart of
|
|
74
|
+
* `WorkerWasmPauseGate`. While paused, `waitWhilePaused` suspends on a promise
|
|
75
|
+
* that `setPaused(false)` resolves, then credits the parked time to the clock.
|
|
76
|
+
*/
|
|
77
|
+
var MainThreadWasmPauseGate = class {
|
|
78
|
+
clock;
|
|
79
|
+
rawNowMilliseconds;
|
|
80
|
+
paused = false;
|
|
81
|
+
resumeWaiters = [];
|
|
82
|
+
constructor(clock, rawNowMilliseconds = () => performance.now()) {
|
|
83
|
+
this.clock = clock;
|
|
84
|
+
this.rawNowMilliseconds = rawNowMilliseconds;
|
|
85
|
+
}
|
|
86
|
+
get isPaused() {
|
|
87
|
+
return this.paused;
|
|
88
|
+
}
|
|
89
|
+
setPaused(paused) {
|
|
90
|
+
if (this.paused === paused) return;
|
|
91
|
+
this.paused = paused;
|
|
92
|
+
if (!paused) {
|
|
93
|
+
const waiters = this.resumeWaiters;
|
|
94
|
+
this.resumeWaiters = [];
|
|
95
|
+
for (const resume of waiters) resume();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async waitWhilePaused() {
|
|
99
|
+
let pausedMilliseconds = 0;
|
|
100
|
+
while (this.paused) {
|
|
101
|
+
const parkStart = this.rawNowMilliseconds();
|
|
102
|
+
await new Promise((resolve) => {
|
|
103
|
+
this.resumeWaiters.push(resolve);
|
|
104
|
+
});
|
|
105
|
+
pausedMilliseconds += this.rawNowMilliseconds() - parkStart;
|
|
106
|
+
}
|
|
107
|
+
this.clock.addPausedMilliseconds(pausedMilliseconds);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Redirects the WASI `clock_time_get` import's MONOTONIC reads through the
|
|
112
|
+
* pausable clock so app-side time agrees with the pause-aware `poll_oneoff`
|
|
113
|
+
* deadline math. Non-monotonic clocks (REALTIME) keep the shim's behavior —
|
|
114
|
+
* wall-clock time genuinely advances across a pause.
|
|
115
|
+
*/
|
|
116
|
+
function installPausableClockTimeGet(wasiImport, memory, clock) {
|
|
117
|
+
const original = wasiImport.clock_time_get;
|
|
118
|
+
if (typeof original !== "function") return;
|
|
119
|
+
wasiImport.clock_time_get = (clockid, precision, timePtr) => {
|
|
120
|
+
if (clockid !== wasi.CLOCKID_MONOTONIC) return original(clockid, precision, timePtr);
|
|
121
|
+
const currentMemory = memory();
|
|
122
|
+
if (!currentMemory) return original(clockid, precision, timePtr);
|
|
123
|
+
new DataView(currentMemory.buffer).setBigUint64(timePtr, clock.nowNanoseconds(), true);
|
|
124
|
+
return wasi.ERRNO_SUCCESS;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { MainThreadWasmPauseGate, PausableMonotonicClock, WorkerWasmPauseGate, createWasmPauseCell, installPausableClockTimeGet, isWasmPauseCellPaused, setWasmPauseCellPaused };
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=WasmRuntimePause.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WasmRuntimePause.js","names":[],"sources":["../../../src/wasi/WasmRuntimePause.ts"],"sourcesContent":["import { wasi } from \"@bjorn3/browser_wasi_shim\";\n\nconst pausedFlagValue = 1;\n\n/**\n * A monotonic clock that excludes time spent paused. `nowMilliseconds()`\n * mirrors `performance.now()` minus every accumulated pause, so WASI clock\n * reads (`clock_time_get`) and `poll_oneoff` deadline math both observe a\n * frozen clock across a pause: pending timeouts keep their remaining time,\n * no expired-deadline catch-up burst fires on resume, and app-side animation\n * clocks do not jump.\n */\nexport class PausableMonotonicClock {\n private readonly rawNowMilliseconds: () => number;\n private accumulatedPauseMilliseconds = 0;\n\n constructor(rawNowMilliseconds: () => number = () => performance.now()) {\n this.rawNowMilliseconds = rawNowMilliseconds;\n }\n\n nowMilliseconds(): number {\n return this.rawNowMilliseconds() - this.accumulatedPauseMilliseconds;\n }\n\n nowNanoseconds(): bigint {\n return BigInt(Math.round(this.nowMilliseconds() * 1e6));\n }\n\n addPausedMilliseconds(\n milliseconds: number\n ): void {\n if (milliseconds > 0) {\n this.accumulatedPauseMilliseconds += milliseconds;\n }\n }\n\n get pausedMilliseconds(): number {\n return this.accumulatedPauseMilliseconds;\n }\n}\n\n/**\n * Creates the shared pause cell the main thread uses to suspend a wasm scene\n * worker. One Int32 slot: 0 = running, 1 = paused. The worker parks on the\n * cell between `poll_oneoff` waits, so a paused scene costs zero CPU.\n */\nexport function createWasmPauseCell(): SharedArrayBuffer {\n if (typeof SharedArrayBuffer === \"undefined\") {\n throw new Error(\n \"SharedArrayBuffer is unavailable. Serve the app with COOP/COEP headers so worker-mode pause can work.\"\n );\n }\n return new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);\n}\n\nexport function setWasmPauseCellPaused(\n cell: SharedArrayBuffer,\n paused: boolean\n): void {\n const flags = new Int32Array(cell);\n Atomics.store(flags, 0, paused ? pausedFlagValue : 0);\n if (!paused) {\n Atomics.notify(flags, 0);\n }\n}\n\nexport function isWasmPauseCellPaused(\n cell: SharedArrayBuffer\n): boolean {\n return Atomics.load(new Int32Array(cell), 0) === pausedFlagValue;\n}\n\n/**\n * Worker-side pause gate: blocks the calling (worker) thread while the shared\n * pause cell is set, then credits the parked wall time to the pausable clock.\n * Must never run on a browser main thread — `Atomics.wait` is worker-only.\n */\nexport class WorkerWasmPauseGate {\n private readonly flags: Int32Array;\n private readonly clock: PausableMonotonicClock;\n private readonly rawNowMilliseconds: () => number;\n\n constructor(\n cell: SharedArrayBuffer,\n clock: PausableMonotonicClock,\n rawNowMilliseconds: () => number = () => performance.now()\n ) {\n this.flags = new Int32Array(cell);\n this.clock = clock;\n this.rawNowMilliseconds = rawNowMilliseconds;\n }\n\n blockWhilePaused(): void {\n let pausedMilliseconds = 0;\n while (Atomics.load(this.flags, 0) === pausedFlagValue) {\n const parkStart = this.rawNowMilliseconds();\n Atomics.wait(this.flags, 0, pausedFlagValue);\n pausedMilliseconds += this.rawNowMilliseconds() - parkStart;\n }\n this.clock.addPausedMilliseconds(pausedMilliseconds);\n }\n}\n\n/**\n * Main-thread (JSPI) pause gate: the awaited counterpart of\n * `WorkerWasmPauseGate`. While paused, `waitWhilePaused` suspends on a promise\n * that `setPaused(false)` resolves, then credits the parked time to the clock.\n */\nexport class MainThreadWasmPauseGate {\n private readonly clock: PausableMonotonicClock;\n private readonly rawNowMilliseconds: () => number;\n private paused = false;\n private resumeWaiters: Array<() => void> = [];\n\n constructor(\n clock: PausableMonotonicClock,\n rawNowMilliseconds: () => number = () => performance.now()\n ) {\n this.clock = clock;\n this.rawNowMilliseconds = rawNowMilliseconds;\n }\n\n get isPaused(): boolean {\n return this.paused;\n }\n\n setPaused(\n paused: boolean\n ): void {\n if (this.paused === paused) {\n return;\n }\n this.paused = paused;\n if (!paused) {\n const waiters = this.resumeWaiters;\n this.resumeWaiters = [];\n for (const resume of waiters) {\n resume();\n }\n }\n }\n\n async waitWhilePaused(): Promise<void> {\n let pausedMilliseconds = 0;\n while (this.paused) {\n const parkStart = this.rawNowMilliseconds();\n await new Promise<void>((resolve) => {\n this.resumeWaiters.push(resolve);\n });\n pausedMilliseconds += this.rawNowMilliseconds() - parkStart;\n }\n this.clock.addPausedMilliseconds(pausedMilliseconds);\n }\n}\n\ntype ClockTimeGet = (clockid: number, precision: bigint, timePtr: number) => number;\n\n/**\n * Redirects the WASI `clock_time_get` import's MONOTONIC reads through the\n * pausable clock so app-side time agrees with the pause-aware `poll_oneoff`\n * deadline math. Non-monotonic clocks (REALTIME) keep the shim's behavior —\n * wall-clock time genuinely advances across a pause.\n */\nexport function installPausableClockTimeGet(\n wasiImport: Record<string, unknown>,\n memory: () => WebAssembly.Memory | undefined,\n clock: PausableMonotonicClock\n): void {\n const original = wasiImport.clock_time_get as ClockTimeGet | undefined;\n if (typeof original !== \"function\") {\n return;\n }\n wasiImport.clock_time_get = (\n clockid: number,\n precision: bigint,\n timePtr: number\n ): number => {\n if (clockid !== wasi.CLOCKID_MONOTONIC) {\n return original(clockid, precision, timePtr);\n }\n const currentMemory = memory();\n if (!currentMemory) {\n return original(clockid, precision, timePtr);\n }\n new DataView(currentMemory.buffer).setBigUint64(timePtr, clock.nowNanoseconds(), true);\n return wasi.ERRNO_SUCCESS;\n };\n}\n"],"mappings":";;AAEA,MAAM,kBAAkB;;;;;;;;;AAUxB,IAAa,yBAAb,MAAoC;CAClC;CACA,+BAAuC;CAEvC,YAAY,2BAAyC,YAAY,IAAI,GAAG;EACtE,KAAK,qBAAqB;CAC5B;CAEA,kBAA0B;EACxB,OAAO,KAAK,mBAAmB,IAAI,KAAK;CAC1C;CAEA,iBAAyB;EACvB,OAAO,OAAO,KAAK,MAAM,KAAK,gBAAgB,IAAI,GAAG,CAAC;CACxD;CAEA,sBACE,cACM;EACN,IAAI,eAAe,GACjB,KAAK,gCAAgC;CAEzC;CAEA,IAAI,qBAA6B;EAC/B,OAAO,KAAK;CACd;AACF;;;;;;AAOA,SAAgB,sBAAyC;CACvD,IAAI,OAAO,sBAAsB,aAC/B,MAAM,IAAI,MACR,uGACF;CAEF,OAAO,IAAI,kBAAkB,WAAW,iBAAiB;AAC3D;AAEA,SAAgB,uBACd,MACA,QACM;CACN,MAAM,QAAQ,IAAI,WAAW,IAAI;CACjC,QAAQ,MAAM,OAAO,GAAG,SAAS,kBAAkB,CAAC;CACpD,IAAI,CAAC,QACH,QAAQ,OAAO,OAAO,CAAC;AAE3B;AAEA,SAAgB,sBACd,MACS;CACT,OAAO,QAAQ,KAAK,IAAI,WAAW,IAAI,GAAG,CAAC,MAAM;AACnD;;;;;;AAOA,IAAa,sBAAb,MAAiC;CAC/B;CACA;CACA;CAEA,YACE,MACA,OACA,2BAAyC,YAAY,IAAI,GACzD;EACA,KAAK,QAAQ,IAAI,WAAW,IAAI;EAChC,KAAK,QAAQ;EACb,KAAK,qBAAqB;CAC5B;CAEA,mBAAyB;EACvB,IAAI,qBAAqB;EACzB,OAAO,QAAQ,KAAK,KAAK,OAAO,CAAC,MAAM,iBAAiB;GACtD,MAAM,YAAY,KAAK,mBAAmB;GAC1C,QAAQ,KAAK,KAAK,OAAO,GAAG,eAAe;GAC3C,sBAAsB,KAAK,mBAAmB,IAAI;EACpD;EACA,KAAK,MAAM,sBAAsB,kBAAkB;CACrD;AACF;;;;;;AAOA,IAAa,0BAAb,MAAqC;CACnC;CACA;CACA,SAAiB;CACjB,gBAA2C,CAAC;CAE5C,YACE,OACA,2BAAyC,YAAY,IAAI,GACzD;EACA,KAAK,QAAQ;EACb,KAAK,qBAAqB;CAC5B;CAEA,IAAI,WAAoB;EACtB,OAAO,KAAK;CACd;CAEA,UACE,QACM;EACN,IAAI,KAAK,WAAW,QAClB;EAEF,KAAK,SAAS;EACd,IAAI,CAAC,QAAQ;GACX,MAAM,UAAU,KAAK;GACrB,KAAK,gBAAgB,CAAC;GACtB,KAAK,MAAM,UAAU,SACnB,OAAO;EAEX;CACF;CAEA,MAAM,kBAAiC;EACrC,IAAI,qBAAqB;EACzB,OAAO,KAAK,QAAQ;GAClB,MAAM,YAAY,KAAK,mBAAmB;GAC1C,MAAM,IAAI,SAAe,YAAY;IACnC,KAAK,cAAc,KAAK,OAAO;GACjC,CAAC;GACD,sBAAsB,KAAK,mBAAmB,IAAI;EACpD;EACA,KAAK,MAAM,sBAAsB,kBAAkB;CACrD;AACF;;;;;;;AAUA,SAAgB,4BACd,YACA,QACA,OACM;CACN,MAAM,WAAW,WAAW;CAC5B,IAAI,OAAO,aAAa,YACtB;CAEF,WAAW,kBACT,SACA,WACA,YACW;EACX,IAAI,YAAY,KAAK,mBACnB,OAAO,SAAS,SAAS,WAAW,OAAO;EAE7C,MAAM,gBAAgB,OAAO;EAC7B,IAAI,CAAC,eACH,OAAO,SAAS,SAAS,WAAW,OAAO;EAE7C,IAAI,SAAS,cAAc,MAAM,CAAC,CAAC,aAAa,SAAS,MAAM,eAAe,GAAG,IAAI;EACrF,OAAO,KAAK;CACd;AACF"}
|