quickbus
v1.0.1
Published
Promise-based RPC wrapper for postMessage transports.
Readme
quickbus
A small promise-based RPC layer for postMessage transports.
quickbus lets you expose a handler object on one side of a messaging boundary and call it from the other side as if it were a local async API. It is intended for:
- parent page <-> iframe communication
- window <-> popup communication
- page <-> service worker communication
MessagePort/MessageChannelcommunication
Installation
npm install quickbusImporting
import { Client, Server } from 'quickbus';const { Client, Server } = require('quickbus');Quick Start
Parent Page To Iframe
Page:
import { Client } from 'quickbus';
const iframe = document.querySelector('iframe');
const frameOrigin = 'https://child.example.com';
const bus = Client.forIframe(iframe, frameOrigin);
const greeting = await bus.sayHello('World');
console.log(greeting);Each client call returns an awaitable request handle. That means this still works:
const greeting = await bus.sayHello('World');But you can also keep the handle and abort it locally if the caller decides to stop waiting:
const request = bus.sayHello('World');
setTimeout(() => request.abort(), 5000);
const greeting = await request;Iframe:
import { Server } from 'quickbus';
const server = new Server({
sayHello(to) {
return `Hello, ${to}!`;
}
}, 'https://parent.example.com');
window.addEventListener('message', event => {
server.handleMessageEvent(event);
});Child Iframe To Parent Window
Parent:
import { Server } from 'quickbus';
const server = new Server({
sayHello(name) {
return `Hello from ${name}!`;
}
}, window.location.origin);
window.addEventListener('message', event => {
server.handleMessageEvent(event);
});Child iframe:
import { Client } from 'quickbus';
const bus = Client.forWindow(window.parent, window.location.origin);
const message = await bus.sayHello('Parent');
console.log(message);Page To Service Worker
Page:
import { Client } from 'quickbus';
await navigator.serviceWorker.register('/sw.mjs', { type: 'module' });
await navigator.serviceWorker.ready;
const bus = Client.forServiceWorker(navigator.serviceWorker);
const greeting = await bus.sayHello('Worker');
console.log(greeting);Service worker:
import { Server } from 'quickbus';
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', event => event.waitUntil(self.clients.claim()));
const server = new Server({
sayHello(to) {
return `Hello, ${to}!`;
}
});
self.addEventListener('message', event => {
server.handleMessageEvent(event);
});Service Worker To Page
Page:
import { Server } from 'quickbus';
const server = new Server({
getOpenTabs() {
return Array.from(document.querySelectorAll('[data-tab]'))
.map(tab => tab.getAttribute('data-tab'));
}
});
window.addEventListener('message', event => {
server.handleMessageEvent(event);
});Service worker:
import { Client } from 'quickbus';
self.addEventListener('message', async event => {
if(event.data?.action !== 'inspect-tabs')
{
return;
}
const client = Client.forWindow(event.source);
const tabs = await client.getOpenTabs();
console.log(tabs);
});MessagePort
import { Client, Server } from 'quickbus';
const channel = new MessageChannel();
const client = Client.forMessagePort(channel.port1);
const server = new Server({
add(a, b) {
return a + b;
}
});
channel.port2.addEventListener('message', event => {
server.handleMessageEvent(event);
});
channel.port2.start?.();
channel.port1.start?.();
console.log(await client.add(2, 3));Client API
new Client({ to, from?, origin? })
Creates a proxy-backed RPC client.
Parameters:
to: requiredpostMessagetargetfrom: optional event target that receives replymessageeventsorigin: optionaltargetOriginfor outboundpostMessage(...)
Example:
const bus = new Client({
to: iframe.contentWindow,
from: window,
origin: 'https://child.example.com'
});Notes:
- If
fromis omitted,Clientfirst triesglobalThis. - If
globalThiscannot receivemessageevents in the current runtime, it falls back totowhen possible. - The constructor accepts a named options object only.
- Each RPC method returns a promise-like request handle with
.abort(). - Aborting a request clears the local pending token, but does not send a cancellation message to the remote transport.
Client.forIframe(iframe, origin?, from?)
Convenience wrapper for parent-page-to-iframe messaging.
Equivalent to:
new Client({
to: iframe.contentWindow,
from,
origin
});When from is omitted, the normal Client fallback logic applies, which usually resolves to the parent window.
Client.forWindow(targetWindow, origin?, from?)
Convenience wrapper for window or popup targets such as:
window.parentwindow.opener- a handle returned by
window.open(...)
Client.forServiceWorker(serviceWorkerOrContainer, from?)
Convenience wrapper for service worker messaging from a page.
Accepted inputs:
navigator.serviceWorkernavigator.serviceWorker.controller- a
ServiceWorker-like target
If you pass a ServiceWorkerContainer such as navigator.serviceWorker, quickbus uses:
to = navigator.serviceWorker.controllerfrom = navigator.serviceWorkerby default
That default matters because service worker replies are delivered on the container, not the page window.
Client.forMessagePort(port, origin?)
Convenience wrapper for MessagePort.
This uses the same port for both directions:
to = portfrom = port
Server API
new Server(handler, ...origins)
Creates an RPC server that dispatches inbound actions to handler.
Parameters:
handler: object whose function properties implement your RPC methods...origins: optional allowlist of acceptableevent.originvalues
Example:
const server = new Server(handler, 'https://client.example.com');Behavior:
- If no origins are provided, replies are allowed by default.
- If one or more origins are provided, replies are only sent when
event.originmatches one of them. - Responses are posted back through
event.source.
server.handleMessageEvent(event)
Handles one inbound message event.
Typical usage:
window.addEventListener('message', event => {
server.handleMessageEvent(event);
});The handler return value may be synchronous or async. Errors are caught, serialized with JSON.stringify, and returned as the error field in the reply payload.
Protocol
Outgoing request shape:
{
action: 'methodName',
params: [arg1, arg2],
token: 'uuid'
}Reply shape:
{
re: 'uuid',
result: value,
error: serializedError
}Security Notes
- Always pass explicit origins for cross-origin iframe or window messaging.
Serverorigin filtering is opt-in. If you want origin enforcement, provide one or more origins to the constructor.Clientdoes not currently validate reply origin before resolving a pending token. If you need stronger guarantees, pair it with explicit server origin controls and a trusted channel topology.
Mental Model
quickbus has two pieces:
Serverlistens for inboundmessageevents, dispatches the requested action to a handler object, and posts the result back to the sender.Clientsends{ action, params, token }messages and resolves a promise when the matching{ re: token, result, error }reply arrives.
The important design detail is that the place you send to is not always the place you listen on:
- parent page -> iframe:
to = iframe.contentWindow,from = window - child iframe -> parent:
to = window.parent,from = window MessagePort:to = port,from = port- service worker from a page:
to = navigator.serviceWorker.controller,from = navigator.serviceWorker
That is why Client uses named transport options and wrapper helpers instead of a single positional constructor.
Development
Available scripts:
npm run build
npm test
npm run test:e2e
npm run lint
npm run tscWhat they do:
npm run build: rebuilds the published root artifacts fromsource/npm test: runs the Node unit tests intest/*.test.mjsnpm run test:e2e: runs the Playwright browser transport testsnpm run lint: runs ESLint onsource/npm run tsc: runs TypeScript checking against the JSDoc-typed source
Current Browser Coverage
The Playwright suite currently verifies:
- parent page -> iframe RPC
- child iframe -> parent window RPC
- page -> service worker RPC
License
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Copyright 2025-2026 Sean Morris
