comlink
A TypeScript library for reliable one-to-many postMessage communication between parent and child window contexts.
Repository Health
Technical Analysis
@sanity/comlink solves the recurring problem of talking reliably across Window boundaries — iframes, popups, and other cross-origin contexts — where the raw postMessage API offers no connection lifecycle, no request/response correlation, and no recovery story. It layers a Controller/Channel/Node model on top: a parent-side Controller manages one or more Channels, each of which can maintain connections to multiple child Nodes, with automatic handshake negotiation, heartbeat-based health monitoring, and reconnection built in.
Messages are fully typed end-to-end through TypeScript generics, so both fire-and-forget post() calls and awaited fetch() request/response exchanges are checked at compile time against the message contracts you define. Internally, each connection is modeled as an XState actor moving through explicit idle -> handshaking -> connected -> disconnected states, which gives the library predictable, inspectable behavior instead of ad hoc event listeners. It originated inside Sanity Studio to power communication between the Studio and embedded visual-editing surfaces, and is published standalone for any application that needs the same guarantees.
What You Get
- A
createController/createChannelAPI for the parent side that manages one-to-many connections to child windows - A
createNodeAPI for the child side that connects back to a named parent Channel - Type-safe
post()(fire-and-forget) andfetch()(request/response) message passing driven by TypeScript generics - Automatic handshake negotiation and heartbeat monitoring that detects and recovers from dropped connections
- Explicit connection status events (
idle,handshaking,connected,disconnected) you can subscribe to - Origin validation on every connection to guard against untrusted senders
Common Use Cases
- Communicating between a host application and an embedded iframe-based tool or widget
- Building visual/live editing experiences where a preview iframe needs to stay in sync with an editor
- Coordinating a popup window (e.g. an OAuth or picker flow) back to its opener
- Any embedded-widget architecture that needs typed request/response calls across a window boundary instead of raw
postMessagestrings
Under The Hood
Architecture
The package separates concerns cleanly across controller.ts (parent-side Channel/target orchestration), node.ts (child-side counterpart), connection.ts (the shared XState machine that every connection is built from), request.ts (request/response correlation on top of the connection), and common.ts (listen-logic shared between Node and Connection). A Controller owns any number of Channels; each Channel owns one Connection per target Window; each Connection is backed by an XState actor that drives the handshake -> heartbeat -> message -> disconnect lifecycle. Because Controller and Node both build directly on createConnectionMachine, a change to that core actor propagates to every consumer — the layering is disciplined but concentrates real risk in one file.
Tech Stack
Written in TypeScript on top of XState 5 (actors, assign, enqueueActions, fromCallback) for connection state and RxJS 7.8 for reactive plumbing, with uuid for connection identifiers. The package is built with @sanity/pkg-utils, tested with Vitest, linted with the type-aware oxlint, and formatted with oxfmt; the surrounding monorepo is managed with pnpm workspaces and Turborepo, and releases go out through Changesets. A Vite-based playground app, deployed to Vercel, exercises the library manually in a real iframe/popup setup.
Code Quality
No dedicated unit tests exist inside packages/comlink/src — the package’s own test script runs Vitest with --pass-with-no-tests, even though the monorepo’s CI still executes pnpm test across all packages. Quality is instead enforced through strict, fully-generic TypeScript types for every message/response shape, type-aware linting via oxlint, and consistent Prettier/oxfmt formatting in CI. A handful of @ts-expect-error @todo @help comments around internal event subscriptions in controller.ts mark known rough edges in the type system rather than swallowed runtime errors.
What Makes It Unique
Rather than wrapping postMessage with a thin pub/sub layer, comlink models the entire connection as an explicit, inspectable state machine with a real lifecycle (handshake, heartbeat, disconnect, automatic recovery) and layers a one-to-many Controller/Channel/Node topology on top, so one parent can manage typed connections to many children through a single API surface. Combining that with generic-driven, awaitable request/response calls over what is natively a fire-and-forget transport is the library’s clearest technical differentiator versus a hand-rolled postMessage listener.
Used by 2 apps in this directory
Sanity
CMS
Open-source headless CMS with a fully customizable React Studio, real-time collaborative editing, structured content modeling, and GROQ query language
Sanity
CMS
Open-source headless CMS with a fully customizable React Studio, real-time collaborative editing, structured content modeling, and GROQ query language