| 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 })
}))
const stream = defineStreamInvoke(ctx, sync)
for await (const update of stream({ jobId: 'import' })) {
console.log(update.type, update.value)
}
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}` }
})
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'
import { createContext } from '@moeru/eventa/adapters/electron/main'
import { createContext } from '@moeru/eventa/adapters/electron/renderer'
export const readdir = defineInvokeEventa<{ dirs: string[] }, { path: string }>('fs:readdir')
const { context } = createContext(ipcMain, mainWindow.webContents)
defineInvokeHandler(context, readdir, async ({ path }) => ({ dirs: await fs.readdir(path) }))
const { context } = createContext(ipcRenderer)
const invokeReaddir = defineInvoke(context, readdir)
const result = await invokeReaddir({ path: '/usr' })
Advanced Features
- Directional events:
defineInboundEventa<T>() and defineOutboundEventa<T>() for flow control
- 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.