Creating a Custom Wooks Adapter
Wooks handles various event types beyond just HTTP. You can create custom adapters that provide a familiar developer experience for any event-driven scenario — such as workflows, jobs, or specialized protocols.
Overview
To create a custom adapter:
Define an Event Kind: Declare the event's typed slots using
defineEventKindandslot. (See Custom Event Context for patterns and examples.)Build Wooks: Implement wooks using
defineWook,key, andcachedto provide access to event-scoped data. (See Custom Event Context for examples.)Create a Context Factory: Build a function that creates an
EventContext, seeds it with event-specific data, and returns a runner function.Extend
WooksAdapterBase: Build a class that registers event handlers via the router and triggers events using the context factory.
Step by Step
1. Define Event Kind and Wooks
import {
defineEventKind,
slot,
key,
defineWook,
} from '@wooksjs/event-core'
// Define the event kind with typed seed slots
const jobKind = defineEventKind('JOB', {
jobId: slot<string>(),
payload: slot<unknown>(),
})
// Build wooks for this event type
const jobStatusKey = key<'pending' | 'running' | 'done'>('job.status')
export const useJob = defineWook((ctx) => ({
getJobId: () => ctx.get(jobKind.keys.jobId),
getPayload: <T = unknown>() => ctx.get(jobKind.keys.payload) as T,
getStatus: () => ctx.has(jobStatusKey) ? ctx.get(jobStatusKey) : 'pending',
setStatus: (s: 'pending' | 'running' | 'done') => ctx.set(jobStatusKey, s),
}))2. Create a Context Factory
Every built-in adapter exports a context factory that hardcodes the event kind and delegates to createEventContext. The factory signature matches createEventContext(options, kind, seeds, fn) but omits the kind parameter:
import { createEventContext } from '@wooksjs/event-core'
import type { EventContextOptions, EventKindSeeds } from '@wooksjs/event-core'
export function createJobContext<R>(
options: EventContextOptions,
seeds: EventKindSeeds<typeof jobKind>,
fn: () => R,
): R {
return createEventContext(options, jobKind, seeds, fn)
}This is the pattern used by all built-in adapters: createHttpContext, createCliContext, createWsConnectionContext, createWsMessageContext, createWfContext, and resumeWfContext. The context factory:
- Accepts
EventContextOptions(with optionalparentfor nested contexts) - Accepts typed
seedsmatching the event kind schema - Runs
fninside the seededAsyncLocalStoragecontext - Returns
fn's return value (sync or async) for span tracking
3. Extend WooksAdapterBase
import { WooksAdapterBase, Wooks } from 'wooks'
import type { TWooksHandler } from 'wooks'
import type { TConsoleBase } from '@prostojs/logger'
interface TJobAdapterOptions {
logger?: TConsoleBase
onNotFound?: TWooksHandler
}
class WooksJob extends WooksAdapterBase {
protected logger: TConsoleBase
protected eventContextOptions: EventContextOptions
constructor(opts?: TJobAdapterOptions, wooks?: Wooks | WooksAdapterBase) {
super(wooks, opts?.logger)
this.logger = opts?.logger || this.getLogger('[wooks-job]')
this.eventContextOptions = this.getEventContextOptions()
}
/** Register a handler for a job route. */
job<ResType = unknown>(path: string, handler: TWooksHandler<ResType>) {
return this.on<ResType>('JOB', path, handler)
}
/** Trigger a job event. */
async trigger(path: string, payload: unknown) {
return createJobContext(
this.eventContextOptions,
{ jobId: path, payload },
async () => {
const { handlers } = this.wooks.lookup('JOB', `/${path}`)
if (!handlers) {
throw new Error(`No handler for job: ${path}`)
}
for (const handler of handlers) {
return await handler()
}
},
)
}
}Key points:
- Call
super(wooks, opts?.logger)— accepts an optional sharedWooksinstance or another adapter - Cache
this.getEventContextOptions()once in the constructor - Register handlers with
this.on('JOB', path, handler)— the first argument is the event method - In the trigger method, use the context factory and look up handlers via
this.wooks.lookup()
Usage
const app = new WooksJob()
app.job('/process', async () => {
const { getJobId, getPayload, setStatus } = useJob()
setStatus('running')
const data = getPayload<{ items: string[] }>()
console.log(`Job ${getJobId()}: processing ${data.items.length} items`)
setStatus('done')
return { processed: data.items.length }
})
await app.trigger('process', { items: ['a', 'b', 'c'] })Sharing Context Across Adapters
Adapters can share a single Wooks router by passing one adapter into another's constructor:
import { createHttpApp } from '@wooksjs/event-http'
const http = createHttpApp()
const jobs = new WooksJob({}, http) // shares the same Wooks instance
// HTTP handler that triggers a job
http.post('/submit', async () => {
return await jobs.trigger('process', { items: ['a', 'b'] })
})The Wooks Router Instance
All adapters delegate routing to a shared Wooks instance — a thin wrapper around @prostojs/router.
Global Singleton
When the wooks constructor argument is omitted, WooksAdapterBase calls getGlobalWooks() — a process-global singleton created on first use. Every adapter created without an explicit instance shares this router. The optional logger and routerOpts arguments only take effect on the call that creates the singleton; later calls return the existing instance unchanged. Use clearGlobalWooks() to reset it (useful in tests or dev-mode hot reload):
import { getGlobalWooks, clearGlobalWooks } from 'wooks'
const wooks = getGlobalWooks() // creates on first call, reuses afterwards
clearGlobalWooks() // next getGlobalWooks() creates a fresh instanceRouter Options
To control routing behavior, construct a Wooks instance explicitly with TWooksOptions and pass it to your adapters:
import { Wooks } from 'wooks'
const wooks = new Wooks({
router: {
ignoreTrailingSlash: true, // `/path` and `/path/` match the same route
ignoreCase: true, // case-insensitive route matching
cacheLimit: 1000, // max parsed routes to cache
},
})
const jobs = new WooksJob({}, wooks)lookup() and lookupHandlers()
Both resolve handlers for (method, path) against an event context (defaults to current()), and as a side effect:
- write the matched route params to the context (the
routeParamsKeyslot, read byuseRouteParams()) - fire the
ContextInjectorhooks'Handler:routed'/'Handler:not_found'(see below)
lookup() returns { handlers, segments, firstStatic, path } — all fields are null on a miss. lookupHandlers() is the fast variant: it returns just TWooksHandler[] | null without allocating a result object.
getRouter() exposes the underlying ProstoRouter for direct access.
For HTTP upgrade integration (httpApp.ws(handler)), the WooksUpgradeHandler type defines the contract a WebSocket-style adapter implements.
Observability (ContextInjector)
@wooksjs/event-core ships a no-op ContextInjector base class. Subclass it and install your instance globally to add tracing, metrics, or logging around event lifecycle points — e.g. OpenTelemetry spans:
import { ContextInjector, replaceContextInjector } from '@wooksjs/event-core'
class OtelInjector extends ContextInjector<string> {
with<T>(name: string, attrs: Record<string, string | number | boolean>, cb: () => T): T {
return tracer.startActiveSpan(name, (span) => {
span.setAttributes(attrs)
try { return cb() } finally { span.end() }
})
}
hook(method: string, name: 'Handler:routed' | 'Handler:not_found', route?: string) {
// record routing metrics
}
}
replaceContextInjector(new OtelInjector())with(name, attributes, cb)wraps a callback. The framework wraps every kinded event in'Event:start'with{ eventType }attributes — which is why adapter callbacks should return their results (sync or async) so the span covers the full handler execution.hook(method, name, route?)fires on every route lookup:'Handler:routed'with the matched route path, or'Handler:not_found'.getContextInjector()returns the installed injector, ornulluntil one is installed.resetContextInjector()removes the installed injector, restoring the no-op default (useful in tests).
Summary
- Define an event kind:
defineEventKindwithslot<T>()markers declares your event's typed shape. - Build wooks: Use
defineWook,key,cachedto provide clean APIs for accessing event-scoped data. - Create a context factory: Export a function
(options, seeds, fn)that callscreateEventContext(options, kind, seeds, fn)with the kind hardcoded — the standard pattern across all adapters. - Extend
WooksAdapterBase: Usethis.on()to register handlers andthis.wooks.lookup()to find them. Use the context factory to run handlers inside the event context. Return results from callbacks to enable span tracking viaContextInjector.