// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import type { FacetConfig, PluginConfig } from "../charts/chart"; import type { PerspectiveClickDetail } from "../event-detail"; import type { ThemeSnapshot } from "../theme/theme"; import type { ViewConfig } from "@perspective-dev/client"; export type { ThemeSnapshot }; /** * Worker-mode control-channel messages. Distinct from the perspective * `ProxySession` channel that the worker's `Client` uses to talk to the * host's real `Client` — that's pure protobuf bytes; this one is the * chart's own renderer control plane. */ export type ControlMsg = | InitMsg | SetViewByNameMsg | SetColumnsConfigMsg | SetPluginConfigMsg | SetBufferMaxCapacityMsg | LoadAndRenderMsg | RedrawMsg | ResizeMsg | ClearMsg | InvalidateThemeMsg | RestoreZoomMsg | ResetAllZoomsMsg | ResetExpandedDomainMsg | SaveZoomReqMsg | SnapshotPngReqMsg | InteractionMsg | DestroyMsg; export type WorkerMsg = | ReadyMsg | ZoomChangedMsg | SaveZoomReplyMsg | SnapshotPngReplyMsg | PinTooltipMsg | DismissTooltipMsg | SetCursorMsg | UserClickMsg | UserSelectMsg | LoadAndRenderAckMsg | FrameBitmapMsg | ErrorMsg; /** * Session-tagged envelopes for the shared-worker transport. Every * message between the host and the shared `Worker` carries a numeric * `sessionId` that addresses a specific `WorkerRenderer` slot in the * worker's `RENDERERS` map. * * In-process mode bypasses these — its `MessageChannel` is per- * transport, so messages are already private and routing isn't * needed. Only worker-mode `RendererTransport` and the worker-scope * message handler wrap / unwrap envelopes. */ export interface ControlEnvelope { sessionId: number; msg: ControlMsg; } export interface WorkerEnvelope { sessionId: number; msg: WorkerMsg; } export interface InitMsg { kind: "init"; /** * GL canvas display strategy. `"direct"` means `glCanvas` below is * the host's `.webgl-canvas` transferred via * `transferControlToOffscreen` and the renderer paints straight * into it. `"blit"` means `glCanvas` is omitted; the renderer * allocates its own internal `OffscreenCanvas`, renders into it, * and posts each completed frame back as a `FrameBitmapMsg` for * the host to draw into a 2D-context display canvas. */ renderMode: "direct" | "blit"; /** * Transferred via `transferControlToOffscreen` on the host. Present * iff `renderMode === "direct"`. In blit mode the renderer * constructs its own offscreen surface from `cssWidth`/`cssHeight`/ * `dpr` and there is no host-side GL drawing buffer. */ glCanvas?: OffscreenCanvas; gridlinesCanvas: OffscreenCanvas; chromeCanvas: OffscreenCanvas; /** * `MessagePort` to the host's `ProxySession`. Worker mode only — * the worker bootstraps a fresh `Client` and bridges it through * this port. In-process mode uses the host's `Client` directly * (handed in via `bootstrapInProcess`'s `client` option), so no * proxy bridge is needed. */ proxyPort?: MessagePort; /** * Compiled perspective-js client wasm forwarded from the host. * Worker mode only — passed to `module.initSync(...)` after the * worker dynamic-imports `clientWorkerURL`. In-process mode * inherits the host's already-bound wasm via the supplied * `Client` instance. */ clientWasm?: WebAssembly.Module; /** * URL the worker uses to dynamic-import the perspective-viewer * wasm-bindgen JS module. Worker mode only — required because * the worker scope can't share module instances with the host. * In-process mode uses the host's already-loaded module via * the supplied `Client` instance. */ clientWorkerURL?: URL; /** * `ChartTypeConfig.tag` — selects which `ChartImplementation` to * construct in the worker. The worker has its own copy of the * chart class registry. */ chartTag: string; /** * Server-assigned `View` name for `client.__unsafe_open_view(name)`. */ viewName: string; /** * `Table` name for the worker to resolve via `client.open_table(...)` * once at bootstrap. Used for source-schema lookups (group-by level * types) on the render path so the host doesn't have to await * `table.schema()` on every draw. May be omitted if the host viewer * has no table loaded yet — `loadAndRender` falls back to an empty * source schema in that case. */ tableName?: string; facetConfig: FacetConfig; /** * Initial plugin-scoped global config. Seeds the chart impl's * `_pluginConfig` before the first `loadAndRender` so the build * pipeline (`auto_alt_y_axis`, `band_inner_frac`, `bar_inner_pad`) * and render-path uniforms see correct values on the very first * frame. The host's later `restore({ plugin_config })` arrives as * a `setPluginConfig` control msg. */ pluginConfig: PluginConfig; defaultChartType?: string; /** * Pre-resolved CSS-variable theme snapshot from the host. */ themeVars: ThemeSnapshot; /** * `@font-face` rules captured from the host document, to be * re-loaded into the worker's `self.fonts` set before first * paint. Worker mode only — workers don't share `FontFaceSet` * with the document, so any font referenced via `font-family` * must be reloaded there. In-process mode shares * `document.fonts` with the host so the array is empty / unused. * See `snapshotFontFaces()` for CORS / scope caveats. */ fontFaces: FontFaceDescriptor[]; /** * Initial CSS size + DPR; subsequent resizes arrive as `resize`. */ cssWidth: number; cssHeight: number; dpr: number; /** * `ChartTypeConfig.max_cells` for the buffer pool. */ bufferMaxCapacity: number; /** * If `true`, the `WebGLContextManager` constructed on the * renderer side compiles + links every shader in * `SHADER_MANIFEST` during construction. Trades a known init-time * cost for elimination of inline compile latency on first frame. * Default behavior (when undefined) is lazy compilation as a * side effect of each glyph's first `getOrCreate` call. */ precompileShaders?: boolean; } /** * Plain-object form of an `@font-face` rule, structured-cloneable for * `postMessage`. The worker reconstitutes a `FontFace` via * `new FontFace(family, src, descriptors)`, awaits its load, and * registers it in `self.fonts`. * * `src` is the raw CSS `src:` value (e.g. * `url(https://…/foo.woff2) format("woff2")`), with every `url(...)` * already absolutized on the host against the parent stylesheet's * `href` — the worker's script URL is a Blob URL, so relative URLs * would otherwise fail to resolve. */ export interface FontFaceDescriptor { family: string; src: string; style?: string; weight?: string; stretch?: string; unicodeRange?: string; variant?: string; featureSettings?: string; display?: string; } export interface SetViewByNameMsg { kind: "setViewByName"; name: string; } export interface SetColumnsConfigMsg { kind: "setColumnsConfig"; cfg: Record; } /** * Host → worker: replace the chart impl's `_pluginConfig` with a new * snapshot. Sent on every `plugin.restore({ plugin_config })`. The * chart re-syncs derived state (`_facetConfig.facet_mode`, * `_facetConfig.zoom_mode`, `_autoFitValue`) in `setPluginConfig` and * the host posts a `redraw` so render-path uniform changes (line * widths, point size) take effect on the next frame. Build-time * fields (`auto_alt_y_axis`, `band_inner_frac`, `bar_inner_pad`) take * effect on the next `loadAndRender`. */ export interface SetPluginConfigMsg { kind: "setPluginConfig"; cfg: PluginConfig; } export interface SetBufferMaxCapacityMsg { kind: "setBufferMaxCapacity"; n: number; } /** * Host → worker: trigger a full data-fetch + render cycle. The worker * resolves all schema / row-count metadata against its own `View` and * `Table` (no host-side `Client`/`Table`/`View` await on the render * path), runs `view.with_typed_arrays`, and uploads the resulting * column data straight into the chart impl on the same thread — * eliminating the prior cross-boundary `postMessage` of column buffers. * * `viewerConfig` ships the bits the worker can't read for itself * (`` element APIs, not `Client`/`Table`/`View`). * * Mid-flight cancellation: each call increments a worker-side * generation counter. A newer `loadAndRender` arriving while one is in * flight causes the older call's `with_typed_arrays` callback to throw * a sentinel before any chart mutation, unwinding the wasm Arrow * buffer release before the new call proceeds. Both calls reply with * `loadAndRenderAck` so the host promise resolves either way. */ export interface LoadAndRenderMsg { kind: "loadAndRender"; msgId: number; viewerConfig: { group_by: string[]; split_by: string[]; columns: (string | null)[]; }; options: { float32: boolean; }; } /** * Worker → host reply to a `LoadAndRenderMsg`. Always sent — including * on stale-generation drop — so the host's awaited promise resolves * deterministically. */ export interface LoadAndRenderAckMsg { kind: "loadAndRenderAck"; msgId: number; } export interface RedrawMsg { kind: "redraw"; } export interface ResizeMsg { kind: "resize"; cssWidth: number; cssHeight: number; dpr: number; } export interface ClearMsg { kind: "clear"; } export interface InvalidateThemeMsg { kind: "invalidateTheme"; /** * Fresh CSS-variable snapshot — the worker can't read DOM. */ themeVars: ThemeSnapshot; } export interface RestoreZoomMsg { kind: "restoreZoom"; state: any; } export interface ResetAllZoomsMsg { kind: "resetAllZooms"; } /** * Host → worker: clear the chart's `domain_mode: "expand"` accumulator * so the next data load starts from a fresh extent. Sent at the head * of every `plugin.draw()` (which always indicates a view-level * change). `plugin.update()` does not send this — same view, more * data, the accumulator should keep growing. */ export interface ResetExpandedDomainMsg { kind: "resetExpandedDomain"; } export interface SaveZoomReqMsg { kind: "saveZoom"; requestId: number; } /** * Host → worker: ask the renderer to flush a frame and return a PNG of * the composited canvas stack. The reply ships a `Blob` correlated to * the request via `requestId` (allocated by the host's * `RendererTransport.snapshotPng()`). */ export interface SnapshotPngReqMsg { kind: "snapshotPng"; requestId: number; } /** * Worker → host reply to a `SnapshotPngReqMsg`. Resolves the * corresponding host-side promise with the encoded `Blob`. */ export interface SnapshotPngReplyMsg { kind: "snapshotPngReply"; requestId: number; blob: Blob; } export interface DestroyMsg { kind: "destroy"; } /** * Raw pointer / wheel event forwarded from the host's * `RawEventForwarder` to the renderer. Coordinates are canvas-relative * CSS pixels (host already subtracted `getBoundingClientRect`). * * `pointerdown` carries `pointerId` so the host can drive * `setPointerCapture` while the corresponding `pointermove` / * `pointerup` events fire even when the cursor leaves the canvas. * * `pointermove` drives both pan (when a drag is active) and tooltip * hover (when not). `pointerleave` drives tooltip leave. `click` / * `dblclick` drive tooltip click handling. One channel per cursor * stream — no parallel `mouse*` mirror. */ export type InteractionEvent = | { type: "wheel"; mx: number; my: number; deltaY: number } | { type: "pointerdown"; mx: number; my: number; pointerId: number } | { type: "pointermove"; mx: number; my: number } | { type: "pointerup" } | { type: "pointerleave" } | { type: "click"; mx: number; my: number } | { type: "dblclick"; mx: number; my: number }; export interface InteractionMsg { kind: "interaction"; event: InteractionEvent; } export interface ReadyMsg { kind: "ready"; } export interface ZoomChangedMsg { kind: "zoomChanged"; isDefault: boolean; } export interface SaveZoomReplyMsg { kind: "saveZoomReply"; requestId: number; state: any; } export interface ErrorMsg { kind: "error"; message: string; } /** * Worker-side request to render a pinned tooltip on the host. The * worker has no DOM, so the persistent tooltip `
` is materialized * by `RendererTransport` (via a `DomHostSink`) on receipt. `bounds` ships * the chart's CSS size so the host can clamp the tooltip without * reading the canvas geometry itself. */ export interface PinTooltipMsg { kind: "pinTooltip"; lines: string[]; pos: { px: number; py: number }; bounds: { cssWidth: number; cssHeight: number }; } export interface DismissTooltipMsg { kind: "dismissTooltip"; } /** * Renderer → host: set the GL canvas's `style.cursor`. The renderer * has no DOM (worker mode) — `cursor` is a CSS cursor value * (`"pointer"`, `"default"`, etc.) applied directly by the host on * receipt. */ export interface SetCursorMsg { kind: "setCursor"; cursor: string; } /** * Renderer → host: a user click landed on a chart glyph. Host * re-dispatches as `CustomEvent` on the * `` ancestor. Payload is a plain object so it * survives `postMessage` without losing the class prototype. */ export interface UserClickMsg { kind: "userClick"; detail: PerspectiveClickDetail; } /** * Renderer → host: a user click pinned or unpinned a chart target. * Host materializes a `PerspectiveSelectDetail` from this payload plus * its own cached previous-insert config and dispatches as * `CustomEvent` (`perspective-global-filter`). * `removeConfigs` is computed host-side — not sent. */ export interface UserSelectMsg { kind: "userSelect"; selected: boolean; row: Record; column_names: string[]; insertConfig: Partial; } /** * Worker → host: a completed GL frame, materialized as an * `ImageBitmap` from the renderer's internal offscreen canvas via * `transferToImageBitmap()`. Sent only in `renderMode === "blit"`, * after each `_fullRender` completes. The host draws the bitmap into * its 2D-context display canvas and calls `bitmap.close()` to release * the GPU-backed surface. * * The bitmap MUST appear in the postMessage transfer list — the * underlying surface is moved, not copied, so failing to transfer * renders the host's drawImage a no-op (or worse, a Safari crash on * older WebKits). */ export interface FrameBitmapMsg { kind: "frameBitmap"; bitmap: ImageBitmap; }