comlink

A TypeScript library for reliable one-to-many postMessage communication between parent and child window contexts.

Library
npm
v4.0.3
7stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
60/100Good
Development Activity92
Maintenance100
Community16
Maturity32
Momentum0

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
67/100Good
Architecture78
Code Quality55
Innovation65
Learning Curve70

@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 / createChannel API for the parent side that manages one-to-many connections to child windows
  • A createNode API for the child side that connects back to a named parent Channel
  • Type-safe post() (fire-and-forget) and fetch() (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 postMessage strings

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.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search