Type-safe RPC for browser windows

Inter-Window
Procedure Call

Talk to popups, child tabs, and detached windows like they were local functions. Register on one side, invoke from the other — with timeouts, abort signals, and a typed error hierarchy.

$pnpm add @silurus/iwpc
Try the demos
Quick start

Register on one side. Invoke from the other.

example.ts
import { useIwpcWindow } from '@silurus/iwpc';

// Parent
const iwpc = useIwpcWindow();
const child = await iwpc?.open('./child');
const name = await child.invoke<void, string>('ASK_NAME');

// Child
iwpc?.register('ASK_NAME', () => prompt('What is your name?') ?? '');
Demos

Two patterns, two transports

Each row is the same demo over two transports. The call shape is identical — the difference is what happens to your event loop.
Open the parent and child side by side and watch DevTools for verbose logs.

postMessage
Counter sync
Fire-and-forget RPC

Each side increments the other. The simplest pattern: register a handler, invoke it from the other window. No return value.

Open demo
BroadcastChannel
Counter sync
Fire-and-forget RPC

Same demo. Child opens with noopener and runs on its own event loop — a busy parent never blocks the child or vice versa.

Open demo
postMessage
Async return values
await invoke<…, T>()

Open a child window as a remote dialog and await a typed result — pick a color, confirm, or enter text. Shared event loop with the parent.

Open demo
BroadcastChannel
Async return values
await invoke<…, T>()

Same dialog flow, but with thread isolation: the popup runs on its own event loop so its UI stays responsive even when the parent is doing heavy work.

Open demo
Why BroadcastChannel?

The BroadcastChannel transport opens the child with noopener, so the two windows live on independent event loops. Heavy work in one window — a long parse, a layout thrash, a CPU spike — does not stall the other. The API surface is identical to the postMessage transport; reach for it when the popup needs to stay responsive regardless of what the parent is doing.