| name | eventa |
| description | Guide for using @moeru/eventa — a transport-aware event library powering ergonomic RPC and streaming flows. Use this skill whenever the user imports from '@moeru/eventa', mentions eventa, needs cross-process/cross-thread event communication (Electron IPC, Web Workers, WebSocket, BroadcastChannel, EventEmitter, EventTarget, Worker Threads), wants to define type-safe events with RPC invoke patterns, needs streaming RPC (server-streaming, client-streaming, or bidirectional), or asks about transport-agnostic event abstractions. Also use when the user discusses alternatives to birpc or async-call-rpc. |
| license | MIT |
| metadata | {"author":"moeru-ai","version":"1.0.0"} |
@moeru/eventa
Transport-aware events powering ergonomic RPC and streaming flows.
Core Concepts
Eventa is built around three ideas:
- Events are first-class — define typed events once, use them everywhere
- Transports are swappable — the same event definitions work across Electron IPC, WebSocket, Web Workers, BroadcastChannel, EventEmitter, EventTarget, and Worker Threads
- RPC is just events — invoke/stream patterns are composed from the same event primitives
API Quick Reference
Event Definition & Context
import { createContext, defineEventa } from '@moeru/eventa'
const move = defineEventa<{ x: number, y: number }>()
const ctx = createContext()
ctx.emit(move, { x: 100, y: 200 })
ctx.on(move, ({ body }) => console.log(body.x, body.y))
Unary RPC (Invoke)
import { createContext, defineInvoke, defineInvokeEventa, defineInvokeHandler } from '@moeru/eventa'
const ctx = createContext()
const echo = defineInvokeEventa<{ output: string }, { input: string }>('rpc:echo')
defineInvokeHandler(ctx, echo, ({ input }) => ({ output: input.toUpperCase() }))
const invokeEcho = defineInvoke(ctx, echo)
const result = await invokeEcho({ input: 'hello' })
Streaming RPC (Server-Streaming)
import { createContext, defineInvokeEventa, defineStreamInvoke, defineStreamInvokeHandler, toStreamHandler } from '@moeru/eventa'
const ctx = createContext()
const sync = defineInvokeEventa<
{ type: 'progress' | 'result', value: number },
{ jobId: string }
>('rpc:sync')
defineStreamInvokeHandler(ctx, sync, async function* ({ jobId }) {
for (let i = 1; i <= 5; i++) {
yield { type: 'progress' as const, value: i * 20 }
}
yield { type: 'result' as const, value: 100 }
})
defineStreamInvokeHandler(ctx, sync, toStreamHandler(async ({ payload, emit }) => {
emit({ type: 'progress', value: 0 })
emit({ type: 'result', value: 100 })
}))
stream = (ctx, sync)
( update ({ : })) {
.(update., update.)
}
Client-Streaming (Stream Input, Unary Output)
const recordRoute = defineInvokeEventa<
{ distance: number, points: number },
ReadableStream<{ lat: number, lng: number }>
>('rpc:record-route')
defineInvokeHandler(ctx, recordRoute, async (stream) => {
let points = 0
for await (const _ of stream) points += 1
return { distance: points * 10, points }
})
const invoke = defineInvoke(ctx, recordRoute)
const input = new ReadableStream({
start(c) { c.enqueue({ lat: 0, lng: 0 }); c.enqueue({ lat: 1, lng: 1 }); c.close() },
})
await invoke(input)
Bidirectional Streaming
const routeChat = defineInvokeEventa<
{ message: string },
ReadableStream<{ message: string }>
>('rpc:route-chat')
defineStreamInvokeHandler(ctx, routeChat, async function* (incoming) {
for await (const note of incoming) {
yield { message: `echo: ${note.message}` }
}
})
const stream = defineStreamInvoke(ctx, routeChat)
for await (const note of stream(outgoing)) {
console.log(note.message)
}
Abort/Cancel
const controller = new AbortController()
const promise = invokeMethod({ input: 'work' }, { signal: controller.signal })
controller.abort('user cancelled')
defineInvokeHandler(ctx, event, async ({ input }, options) => {
const signal = options?.abortController?.signal
if (signal?.aborted) return { output: 'aborted' }
signal?.addEventListener('abort', () => { }, { once: true })
return { output: `done: ${input}` }
})
Multi-hop Channels
Channels form ordered routing chains. They carry events, unary invokes, every
stream frame, and invocation cancellation through intermediate contexts.
import { linkChannel, pipeChannel } from '@moeru/eventa'
pipeChannel(a, b, c)
linkChannel(a, b, c)
There is no direct a to c edge. Use multiple explicit pipes for fan-out.
Disposing a channel removes its edges only; context abort never cascades across
a link. One connected graph must have one effective handler for each invoke
definition.
Each local emit creates an EventaInner whose deliveryId survives channel
hops and transport serialization. Contexts suppress recently seen delivery IDs
and stop forwarding when hopsRemaining reaches zero. Plugins may inspect the
read-only inner value and transform or drop its Eventa, but may not replace routing
identity or hop state.
For iframe-to-server routing, connect the EventTarget-side context to the
plugin's BroadcastChannel context, then connect the gateway's BroadcastChannel
context to its WebSocket context. The adapters carry the inner value across the
runtime boundaries; no directional forwarding markers are needed.
Contexts do not serialize concurrent emit() calls. Request and response
stream pumps await each frame only to preserve per-invocation stream order;
cancellation is routed independently and may arrive before request frames.
Bulk Registration (Shorthands)
const events = {
double: defineInvokeEventa<number, number>(),
append: defineInvokeEventa<string, string>(),
}
defineInvokeHandlers(ctx, events, {
double: input => input * 2,
append: input => `${input}!`,
})
const { double, append } = defineInvokes(ctx, events)
Adapters
Each adapter wraps a specific transport into an eventa context. The pattern is always:
import { createContext } from '@moeru/eventa/adapters/<adapter-name>'
const { context } = createContext(transportInstance)
Available Adapters
| Adapter | Import Path | Transport |
|---|
| Electron Main | @moeru/eventa/adapters/electron/main | ipcMain + webContents |
| Electron Renderer | @moeru/eventa/adapters/electron/renderer | ipcRenderer |
| Web Worker (main) | @moeru/eventa/adapters/webworkers | Worker instance |
| Web Worker (worker) | @moeru/eventa/adapters/webworkers/worker | self (worker global) |
| Worker Threads (main) | @moeru/eventa/adapters/worker-threads | Node.js Worker |
| Worker Threads (worker) | @moeru/eventa/adapters/worker-threads/worker | parentPort |
| WebSocket Client | @moeru/eventa/adapters/websocket/native | WebSocket |
| WebSocket Server (H3) | @moeru/eventa/adapters/websocket/h3 | H3 WebSocket hooks |
| BroadcastChannel | @moeru/eventa/adapters/broadcast-channel | BroadcastChannel |
| EventTarget | @moeru/eventa/adapters/event-target | EventTarget |
| EventEmitter | @moeru/eventa/adapters/event-emitter | Node.js EventEmitter |
Adapter Usage Pattern (Electron Example)
import { defineInvokeEventa } from '@moeru/eventa'
export const readdir = defineInvokeEventa<{ dirs: string[] }, { path: string }>('fs:readdir')
import { createContext } from '@moeru/eventa/adapters/electron/main'
const { context } = createContext(ipcMain, mainWindow.webContents)
defineInvokeHandler(context, readdir, async ({ path }) => ({ dirs: await fs.readdir(path) }))
import { createContext } from '@moeru/eventa/adapters/electron/renderer'
const { context } = createContext(ipcRenderer)
const invokeReaddir = defineInvoke(context, readdir)
const result = await invokeReaddir({ path: '/usr' })
Advanced Features
- Delivery routing:
EventaInner<T> preserves delivery identity and hop budget across channels and adapters
- Match expressions:
matchBy(glob), matchBy(regex), and(...), or(...) for event filtering
- WebSocket lifecycle:
wsConnectedEvent and wsDisconnectedEvent from the native adapter
Key Rules
- Always define events in a shared module — both sides import the same event definition for type safety
defineInvokeEventa<Res, Req>() — Response type comes first, Request type second
- Handlers can throw errors safely — eventa propagates them to the caller
- Validate data at the edges — eventa forwards whatever payload you emit
- Install only the peer dependencies you need (electron, h3, web-worker are all optional)
Documentation
For the latest API reference, use context7 to query @moeru/eventa documentation.