Messaging
epos.bus is the messaging layer used to communicate between extension contexts. You can use the same API in the popup, background, side panel, web page, or iframes.
The most basic methods are:
epos.bus.on()to listen for messages.epos.bus.send()to send a message to other contexts.epos.bus.off()to remove a listener.
Why “bus”?
In computing, a bus is not a vehicle, but a communication system that transfers data between different components. It acts as a centralized hub for exchanging messages.
Why Use epos.bus?
Browser extensions often need different messaging APIs, depending on where a message starts and where it needs to go.
epos.bus gives you one API for all of those cases. Instead of switching between chrome.runtime.sendMessage, chrome.tabs.sendMessage, and window.postMessage, you use the same methods everywhere and let Epos handle the routing.
Basics
The most common pattern is one context listening and another context sending.
In the example below, the background listens for analytics:event, and the popup sends it:
epos.bus.on('analytics:event', eventName => {
console.log('Analytics event received:', eventName)
})const App = () => {
return (
<button onClick={() => epos.bus.send('analytics:event', 'save')}>
Save
</button>
)
}
epos.render(<App />){
"name": "My Extension",
"targets": [
{
"matches": "<popup>",
"load": ["dist/popup.js"]
},
{
"matches": "<background>",
"load": ["dist/background.js"]
}
]
}That is the most common pattern. You choose any event name, attach a listener with on(), and send data with send().
Returning Data
epos.bus.send() is not only for fire-and-forget events. If a listener returns a value, the sender receives it back.
This works well for request-response flows:
// background.ts
epos.bus.on('user:get', async (userId: string) => {
const response = await fetch(`https://api.example.com/users/${userId}`)
return await response.json()
})
// popup.tsx
const user = await epos.bus.send<User>('user:get', '42')
console.log(user)If there are no listeners, send() resolves to undefined.
Local Events with emit()
epos.bus.send() does not call listeners in the current context. It is meant for sending messages to other contexts.
To trigger local listeners, use epos.bus.emit() instead:
epos.bus.on('modal:close', () => {
console.log('Close modal')
})
epos.bus.emit('modal:close')This can be useful for local coordination when you do not need cross-context delivery.
Removing Listeners
Use epos.bus.off() when a listener should stop receiving messages.
const handleUpdate = (value: number) => {
console.log('Updated:', value)
}
epos.bus.on('counter:update', handleUpdate)
// Later
epos.bus.off('counter:update', handleUpdate)If you call off() without a callback, Epos removes all listeners for that event name in the current context.
One-Time Listeners
Sometimes an event should be handled only once. In that case, use epos.bus.once():
epos.bus.once('auth:ready', () => {
console.log('Auth is ready')
})After the first call, the listener is removed automatically.
Waiting for Readiness
For startup flows, it is common to wait until another context finishes some work. epos.bus.setSignal() and epos.bus.waitSignal() are made for that.
For example, your popup may need to wait until the background loads configuration:
// background.ts
const config = await loadConfig()
epos.bus.setSignal('config:ready', config)
// popup.tsx
const config = await epos.bus.waitSignal<Config>('config:ready')
console.log(config)By default, waitSignal() waits indefinitely. You can also pass a timeout in milliseconds. If the signal is not set within that time, it resolves to undefined:
// Resolves to `undefined` if config is not ready within 5 seconds
const config = await epos.bus.waitSignal<Config>('config:ready', 5000)Type Safety
If you are using TypeScript, send() can be typed in two ways.
- For a simple return value, pass the expected result type:
const total = await epos.bus.send<number>('math:sum', 5, 10)- If you also want argument checking, pass a function type:
import type { sum } from './background'
const total = await epos.bus.send<typeof sum>('math:sum', 5, 10)export const sum = (a: number, b: number) => a + b
epos.bus.on('math:sum', sum)This keeps the event name string-based, while still giving you solid TypeScript support.
Providing this
To set this for a listener, pass the context as the third argument to on():
const api = {
value: 42,
getValue () {
return this.value
}
}
epos.bus.on('api:getValue', api.getValue, api)Exposing APIs
If you have many related methods, exposing them all through epos.bus.on() can get tedious:
export const userApi = {
getUser () { ... },
updateUser () { ... },
removeUser () { ... },
// ... 10 more methods
}
epos.bus.on('user:get', userApi.getUser, userApi)
epos.bus.on('user:update', userApi.updateUser, userApi)
epos.bus.on('user:remove', userApi.removeUser, userApi)
// ... 10 more listenersTo expose all methods at once, you can register() the API object and make it available to other contexts:
export const userApi = { ... }
epos.bus.register('user', userApi)Then, in another context, you can use() that API by its name:
import type { userApi } from './background'
const userApi = epos.bus.use<typeof userApi>('user')
const user = await userApi.getUser('42')Notice that epos.bus.use() returns the API object immediately. You do not await it.
You also get full type safety for the API methods.
If the API should no longer be available, you can unregister it later with epos.bus.unregister('user').
Namespaces with for()
On larger projects, event names can start to collide. epos.bus.for() solves that by creating a namespaced version of the bus:
const chatBus = epos.bus.for('chat')
chatBus.on('message', text => {
console.log('Chat message:', text)
})
// In another context
await chatBus.send('message', 'Hello')The namespaced bus has all the methods of the normal epos.bus API. It also has a dispose() method that removes all listeners registered through it.
Sending Blobs
epos.bus is not limited to JSON data. You can safely transfer Blob objects between contexts. This is useful when you work with images, files, or other binary data.
// popup.ts
await epos.bus.send('file:save', imageBlob)
// background.ts
epos.bus.on('file:save', async (blob: Blob) => {
console.log('Save file:', blob.type, blob.size)
})Epos does not serialize blobs as base64 strings. Instead, it uses a more efficient transfer techniques. You can safely send large files between contexts without freezing your app.
When to Use What
- Use
on()andsend()for normal cross-context messaging. - Use
emit()for local-only events. - Use
once()when an event should be handled a single time. - Use
setSignal()andwaitSignal()for readiness. - Use
register()anduse()when you want to expose an API to another context. - Use
for()to create a namespaced bus and avoid event name collisions.
If you want the exact signatures, continue to the Bus API Reference.