import type { Terminal } from "@blit-sh/browser"; import type { ConnectionStatus, TerminalPalette } from "./types"; import { type GlRenderer } from "./gl-renderer"; /** * Infer refresh from rAF intervals without assuming a table of display modes. * * Browser timestamps are quantized: a 240 Hz clock can alternate between * 4.1 and 4.2 ms. Taking the median selects one bucket (4.2 -> 238 Hz), * while averaging recovers the underlying 4.166... ms period. Reject Tukey * outliers first so isolated missed callbacks and compensating intervals do * not skew the result without changing the ratio of the 4.1/4.2 ms buckets. */ export declare function estimateDisplayFps(intervals: readonly number[]): number; export type BlitWasmModule = typeof import("@blit-sh/browser"); export type TerminalDirtyListener = (ptyId: number) => void; export interface TerminalStoreDelegate { send(data: Uint8Array): void; getStatus(): ConnectionStatus; log?(msg: string): void; } export declare class TerminalStore { /** Hidden documents retain low-rate liveness without spending a display * refresh worth of terminal and compositor work. */ private static readonly HIDDEN_DISPLAY_FPS; private mod; private terminals; private staleTerminals; private retainCount; private pendingFree; private subscribed; private desired; private readonly delegate; private dirtyListeners; private leadPtyId; private fontFamily; private fontSize; private cellPw; private cellPh; private palette; private disposed; private ready; private readyListeners; /** Incremented every time any terminal's cell metrics are set, so renderers can detect stale state. */ metricsGeneration: number; private sharedRenderer; private sharedCanvas; private webgpuProbe; private webgpuRenderer; private webgpuCanvas; private displayFps; /** Small display-rate changes are commonly measurement noise, not a mode * change. Require the same nearby result repeatedly so a steady clock does * not alternate between adjacent integer rates every ten seconds. */ private pendingDisplayFps; private pendingDisplayFpsCount; private static readonly DISPLAY_FPS_CONFIRMATIONS; private static readonly IMMEDIATE_DISPLAY_FPS_DROP_RATIO; private static readonly RAF_PROBE_MIN_SAMPLES; private static readonly RAF_PROBE_DURATION_MS; private rafHandle; private rafProbeTimer; private visibilityHandler; private rafPrev; private rafProbeStartedAt; private rafSamples; private pendingAppliedFrames; private ackAheadFrames; private applyMsX10; private metricsFlushQueued; private metricsHeartbeat; private pendingAcks; /** Queued compressed payloads per PTY, drained in the rAF callback. */ private pendingFrames; constructor(delegate: TerminalStoreDelegate, wasm: BlitWasmModule | Promise); /** Fire-and-forget WebGPU probe. If it succeeds, the next getSharedRenderer * call will pick it up. If it fails, we silently fall through to WebGL2. */ private probeWebGpu; private nowMs; private resetClientMetrics; private queueClientMetricsFlush; private startMetricsHeartbeat; private stopMetricsHeartbeat; private flushClientMetrics; private noteAppliedFrame; isReady(): boolean; private _wasmMem; /** Get the WASM linear memory for zero-copy typed array views. */ wasmMemory(): WebAssembly.Memory | null; onReady(listener: () => void): () => void; private createTerminal; handleUpdate(ptyId: number, payload: Uint8Array): void; handleStatusChange(status: ConnectionStatus): void; getTerminal(ptyId: number): Terminal | null; setLead(ptyId: number | null): void; setFontFamily(fontFamily: string): void; setFontSize(fontSize: number): void; /** Resolve the canvas a caller should composite FROM via drawImage. Every * backend renders into its own canvas which the caller drawImages * synchronously right after render(), so we hand back the canvas directly. */ private compositeCanvas; /** * Throw away the shared renderer after a GPU context or device loss, so the * next {@link getSharedRenderer} builds a replacement. * * The canvas goes with it, and that is the point: a canvas keeps the context * it was first given for life. A canvas whose WebGL2 context was lost hands * back that same dead context from `getContext("webgl2")`, and a canvas * configured for WebGPU refuses `getContext("webgl2")` altogether — which * would have taken the WebGL2 *and* Canvas2D fallbacks down with it and left * `getSharedRenderer` returning null forever. Only a fresh element rebinds. * * Repainting is part of the recovery: rendering is event-driven, so without * this an idle pane would sit blank until its next output. */ private discardSharedRenderer; /** * The WebGPU device died. Drop it as a candidate so * {@link getSharedRenderer} can't promote it back, and rebuild only if it was * the renderer actually in use — a device lost before the probe promoted it * should cost a healthy WebGL2 fallback nothing. */ private handleWebGpuLost; /** Get a shared renderer for all surfaces. Prefers WebGPU (async probe), * falls back to WebGL2, then Canvas 2D. */ getSharedRenderer(): { renderer: GlRenderer; canvas: HTMLCanvasElement; } | null; /** Upper bound on frames held per PTY while the WASM module loads. Well * above what a sub-second module fetch can accumulate; the cap only exists * so a module that never resolves can't grow the queue without limit. */ private static readonly MAX_QUEUED_FRAMES; /** * Hold a frame that can't be decoded yet. * * On overflow the queue is dropped and the PTY re-subscribed rather than * replayed: a gap in a delta stream is unrecoverable, but a fresh subscribe * makes the server encode against an empty basis and send a full frame. */ private queueFrame; /** * Apply everything queued by {@link queueFrame}, in arrival order, creating * the terminals the frames belong to. Callers repaint afterwards. */ private drainQueuedFrames; /** * Force every surface to re-prepare and repaint. * * The dirty listeners are the existing "this terminal's content changed" * path, and a surface's callback already sets `contentDirty` and schedules a * frame. That is exactly what a late WASM load or a renderer swap needs even * though no cell changed — and reusing it beats a second notification * mechanism that would have to stay in step with the first. */ private notifyAllDirty; /** Mark the latest applied terminal state as painted to the screen. */ noteFrameRendered(): void; getDebugStats(leadPtyId?: number | null): { displayFps: number; rendererBackend: string; pendingApplied: number; ackAhead: number; applyMs: number; mouseMode: number; mouseEncoding: number; terminals: number; staleTerminals: number; subscribed: number; pendingFrameQueues: number; totalPendingFrames: number; }; invalidateAtlas(): void; setPalette(palette: TerminalPalette): void; setCellSize(pw: number, ph: number): void; getCellSize(): { pw: number; ph: number; }; setDesiredSubscriptions(ptyIds: Set): void; /** * Get the current retain count for a PTY. */ getRetainCount(ptyId: number): number; retain(ptyId: number): void; release(ptyId: number): void; freeTerminal(ptyId: number): void; private doFree; addDirtyListener(listener: TerminalDirtyListener): () => void; private syncSubscriptions; private sendDisplayFps; /** Apply one completed rAF probe. Mode-sized changes take effect * immediately; adjacent integer results need confirmation because they are * the expected rounding noise around a stable physical refresh rate. */ private acceptDisplayFps; private startRafProbe; /** * Re-measure the display rate when it may have changed: returning to a * visible tab, which is also when a window has plausibly been dragged to * a monitor with a different refresh rate, and every ten seconds while * visible. The periodic sample lets a startup probe taken during a busy * burst recover; each probe stops after a 500 ms measurement window. */ private armRafProbe; private stopRafProbe; private resync; /** Permanently destroy the store — free all WASM terminals and GL resources. */ destroy(): void; } //# sourceMappingURL=TerminalStore.d.ts.map