FrameworkEvents
Event System
Type-safe event definitions with effects, transforms, and subscriptions
The event system provides a type-safe way to define domain events that trigger side effects and can be persisted, replayed, and subscribed to.
Core Concepts
Event Types
There are two types of events:
- Aggregate Events - Events tied to an entity (e.g.,
task.created,user.updated). These have an aggregate ID and type for history queries. - Ephemeral Events - Fire-and-forget events not tied to an entity. Useful for notifications or cross-cutting concerns.
Event Flow
Defining Events
Use the ev() factory to create events:
import { ev } from "@lawhive/framework"
import { z } from "zod"
const { event, ephemeral, router } = ev()
// Aggregate event with effect
const taskCreated = event({
schema: z.object({ name: z.string() }),
effect: async (input) => {
return await prisma.task.create({ data: input })
},
selectId: ({ output }) => output.id,
})
// With transform (payload differs from input)
const taskRenamed = event({
schema: z.object({ id: z.string(), name: z.string() }),
effect: async (input) => {
return await prisma.task.update({
where: { id: input.id },
data: { name: input.name },
})
},
transform: (input, output) => ({
id: output.id,
oldName: output.name, // from before update
newName: input.name,
}),
selectId: ({ payload }) => payload.id,
})
// Ephemeral event (no effect, no aggregate)
const notificationSent = ephemeral({
schema: z.object({ userId: z.string(), message: z.string() }),
})Event Routers
Group related events into routers:
const taskEvents = router({
created: taskCreated,
renamed: taskRenamed,
deleted: taskDeleted,
})
const userEvents = router({
registered: userRegistered,
updated: userUpdated,
})
// Combine into root router
const events = router({
tasks: taskEvents,
users: userEvents,
})Creating an Event Client
The event client provides the publishing API:
import { createEventClient } from "@lawhive/framework"
import { createEventStoreAdapter } from "@lawhive/framework/adapters"
const eventClient = createEventClient(events, {
adapters: [
createEventStoreAdapter({ storage: myEventStorage }),
],
createContext: async (fn) => {
return prisma.$transaction(async (tx) => {
return fn({ transaction: tx })
})
},
})Publishing Events
// Single event - returns the effect result
const task = await eventClient.tasks.created.publish({ name: "My Task" })
// Build event DTOs without publishing (for batch)
const dto1 = eventClient.tasks.created.build({ id: "1", name: "Task 1" })
const dto2 = eventClient.tasks.created.build({ id: "2", name: "Task 2" })
// Publish multiple events atomically
await eventClient.publishMany([dto1, dto2], { atomic: true })Subscriptions
Add subscribers to react to events:
const taskCreated = event({
schema: z.object({ name: z.string() }),
effect: async (input) => prisma.task.create({ data: input }),
selectId: ({ output }) => output.id,
subscribers: [
async (payload) => {
await sendNotification(`Task ${payload.name} created`)
},
async (payload) => {
await updateSearchIndex("tasks", payload)
},
],
})Event Handler
For consuming events (e.g., from a queue):
import { createEventHandler } from "@lawhive/framework"
const handler = createEventHandler(events)
// Process incoming event
const result = await handler({
id: "evt_123",
type: "aggregate",
key: "tasks.created",
aggregateId: "task_456",
aggregateType: "tasks",
payload: { id: "task_456", name: "My Task" },
})