Skip to content

Instantly share code, notes, and snippets.

@zicklag
Last active September 9, 2025 16:58
Show Gist options
  • Save zicklag/2d7e139ee8eeeed78eb5697980a5da54 to your computer and use it in GitHub Desktop.
Save zicklag/2d7e139ee8eeeed78eb5697980a5da54 to your computer and use it in GitHub Desktop.
Simple helper to make it easy to communicate with typed interfaces over browser MessagePorts
import MySharedWorker from './mySharedWorker.ts?sharedworker'; // using Vite shared worker import
import { messagePortInterface } from './messagePortInterface';
//
// We need to define the interfaces that we use on both sides of the message port.
//
// This is the interface we use for the frontend, i.e. when the worker wants to call
// a function remotely against the UI thread.
export type MainThreadInterface = {
getServiceAuth: (aud: string) => Promise<string>;
};
// This is the interface we use for the "backend"
export type SharedWorkerInterface = {
sayHello: (name: string, name2: string) => Promise<string>;
logout: () => Promise<void>;
};
// Initialize shared worker
const worker = new MySharedWorker();
// Now we can create our message port interface. We need to specify the local interface
// first, and then the remote interface as type parameters.
export const workerInterface = messagePortInterface<MainThreadInterface, SharedWorkerInterface>(
worker.port, // Provide the port to communicate on
// And now we have to implement the MainThreadInterface
{
async getServiceAuth(aud) {
const resp = await atproto.agent?.com.atproto.server.getServiceAuth({
aud
});
if (!resp) throw 'Could not get service auth';
return resp?.data.token;
}
}
);
// We can now call the worker interface
const response = await workerInterface.sayHello("John");
type HalfInterface = { [key: string]: (...args: any[]) => Promise<unknown> };
type IncomingMessage<In extends HalfInterface, Out extends HalfInterface> =
| {
[K in keyof In]: ['call', K, string, ...Parameters<In[K]>];
}[keyof In]
| { [K in keyof Out]: ['response', string, 'resolve' | 'reject', ReturnType<Out[K]>] }[keyof Out];
export function messagePortInterface<Local extends HalfInterface, Remote extends HalfInterface>(
messagePort: MessagePort,
handlers: Local
): Remote {
const pendingResponseResolers: {
[key: string]: {
resolve: (resp: ReturnType<Remote[keyof Remote]>) => void;
reject: (error: any) => void;
};
} = {};
messagePort.onmessage = async (ev: MessageEvent<IncomingMessage<Local, Remote>>) => {
const type = ev.data[0];
if (type == 'call') {
const [_, name, requestId, ...parameters] = ev.data;
for (const [event, handler] of Object.entries(handlers)) {
if (event == name) {
try {
const resp = await handler(...parameters);
messagePort.postMessage(['response', requestId, 'resolve', resp]);
} catch (e) {
messagePort.postMessage(['response', requestId, 'reject', e]);
}
}
}
} else if (type == 'response') {
const [_, requestId, action, data] = ev.data;
pendingResponseResolers[requestId][action](data);
delete pendingResponseResolers[requestId];
}
};
return new Proxy(
{
messagePort
},
{
get({ messagePort }, name) {
const n = name as keyof Remote;
return (...args: Parameters<Remote[typeof n]>): ReturnType<Remote[typeof n]> => {
let reqId = crypto.randomUUID();
const respPromise = new Promise(
(resolve, reject) => (pendingResponseResolers[reqId] = { resolve, reject })
);
messagePort.postMessage(['call', n, reqId, ...args]);
return respPromise as any;
};
}
}
) as unknown as Remote;
}
import type { MainThreadInterface, SharedWorkerInterface } from './exampleBrowserScript';
import { messagePortInterface } from './messagePortInterface';
// In the shared worker we wait for a connection
(globalThis as any).onconnect = ({ ports: [port] }: { ports: [MessagePort] }) => {
// And then we can create a message port interface on this side of the port.
// Again we specify the local interface, in this case we specify the shared worker
// interface first, and the main thread interface second.
const mainInterface = messagePortInterface<SharedWorkerInterface, MainThreadInterface>(port, {
// Then we need to implement the shared worker interface
async sayHello(name, name2) {
return `hello, ${name}, ${name2}`;
},
async logout() {
// Implement logout
}
});
// We can call functions on the main thread interface, too
const auth = await mainInterface.getServiceAuth('did:web:example.org')
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment