Getting Started
Installation
npm install @yellatech/engine
Hello World
import { createEngine } from '@yellatech/engine'
const engine = createEngine()
engine.register('greet', 'user:signup', async (ctx) => {
const payload = ctx.payload as { name: string }
console.log(`Welcome, ${payload.name}!`)
return { success: true }
})
// emitAndWait emits the event, then waits for the engine to go idle.
// In a real service, you'd just call engine.emit(), the dispatcher processes
// runs in the background. emitAndWait is useful for scripts and tests.
await engine.emitAndWait('user:signup', { name: 'Alice' })
await engine.stop()
With SQLite Persistence
const engine = createEngine({
store: { type: 'sqlite', path: './my-app.db' },
})
Or set STATE_DB_PATH environment variable:
STATE_DB_PATH=./my-app.db node app.js
Using process() (recommended)
The process() API provides a cleaner syntax with named parameters and built-in helpers:
import { createEngine } from '@yellatech/engine'
const engine = createEngine()
engine.process({
name: 'greet',
on: 'user:signup',
run: async (ctx) => {
const payload = ctx.payload as { name: string }
console.log(`Welcome, ${payload.name}!`)
return ctx.ok()
},
})
// Same as above, emitAndWait is a convenience for scripts.
// In a server, just call engine.emit() and let the dispatcher handle it.
await engine.emitAndWait('user:signup', { name: 'Alice' })
await engine.stop()
ctx.ok() and ctx.fail() replace manual { success: true/false } construction. Both register() and process() coexist, use whichever you prefer.
emit() vs emitAndWait() vs drain()
In a running service, use emit(), it enqueues work and returns immediately:
engine.emit('user:signup', { name: 'Alice' })
// returns immediately, dispatcher processes in the background
drain() waits for all in-flight runs to finish. Useful as a sync point or for graceful shutdown:
engine.emit('order:placed', { item: 'widget' })
engine.emit('order:placed', { item: 'gadget' })
// wait for everything to settle
await engine.drain()
console.log(`Done: ${engine.countByState('completed')} runs completed`)
emitAndWait() is shorthand for emitting an event and then waiting for the engine to go idle, handy in scripts and tests.
Dev Dashboard
Add server to see a live dashboard of your processes, runs, and traces:
const engine = createEngine({
server: { port: 3000 },
})
Open http://localhost:3000 in your browser. The dashboard is intended for development and operator visibility; see Dashboard for the full UI walkthrough and route surface, and Configuration for the server option itself.
Declaring Event Flow
When processes chain events via triggerEvent or ctx.ok(..., { emit }), you can declare what each process emits. This builds a static event graph visible in the dashboard’s Graph tab and via engine.getGraph():
engine.process({
name: 'validate',
on: 'order:new',
emits: ['order:validated'],
run: async (ctx) => {
// ... validation logic
return ctx.ok(ctx.payload, { emit: 'order:validated' })
},
})
engine.process({
name: 'charge',
on: 'order:validated',
emits: ['order:charged'],
run: async (ctx) => {
const payload = ctx.payload as { total: number }
const charge = await ctx.effect({
key: 'stripe-charge',
run: () => stripe.charges.create({ amount: payload.total }),
})
return ctx.ok({ chargeId: charge.id }, { emit: 'order:charged' })
},
})
// Inspect the topology
const graph = engine.getGraph()
// graph.edges: [{ from: 'validate', event: 'order:validated', to: 'charge' }, ...]
The emits field is optional, processes work without it. But declaring it enables the Graph tab to show how your processes connect.
What Happens
register(orprocess) maps the handlergreetto the eventuser:signup.emitAndWaitcreates a run inidlestate, then waits for the engine to go idle.- The dispatcher claims the run, transitions it to
running, and calls your handler. - Your handler returns
{ success: true }(orctx.ok()), and the run transitions tocompleted.