Overview
The hook builder provides a fluent interface for defining event listeners. Hooks execute when their target event(s) are emitted, enabling reactive and decoupled architecture.
import { r, globals } from "@bluelibs/runner";
const sendWelcomeEmail = r.hook("app.hooks.sendWelcome")
.on(userRegisteredEvent)
.order(10)
.dependencies(() => ({ emailService }))
.run(async ({ payload, deps, logger }) => {
await logger.info(`Sending welcome email to ${payload.email}`);
await deps.emailService.send(payload.email, "Welcome!");
})
.build();
Methods
on()
Specifies which event(s) this hook listens to.
event
IEvent | IEvent[] | '*'
required
The event(s) to listen to. Can be:
- A single event definition
- An array of event definitions
"*" to listen to all events
// Single event
.on(userRegisteredEvent)
// Multiple events
.on([orderCreated, orderUpdated, orderDeleted])
// All events (wildcard)
.on("*")
Returns: New builder with event target set (required before build())
run()
Defines the hook’s execution logic.
.run(
fn: (context: HookContext<TPayload, TDeps>) => Promise<void>
)
The hook execution function. Receives a context object with:
payload - The event payload (typed based on the event)
event - The event definition that was emitted
deps - Resolved dependencies
runtime - Runtime instance
logger - Scoped logger
.run(async ({ payload, event, deps, runtime, logger }) => {
await logger.info(`Handling ${event.id}`);
await deps.notificationService.notify(payload);
await runtime.runTask(updateMetricsTask, { event: event.id });
})
Returns: New builder with run function set (required before build())
order()
Sets the execution order for this hook.
Execution priority. Lower numbers run first. Hooks with the same order run concurrently if the event has parallel(true).// Early hook (runs first)
.order(-10)
// Default priority
.order(0)
// Late hook (runs last)
.order(100)
Returns: New builder with order set
dependencies()
Defines dependencies to inject into the hook.
.dependencies<TDeps>(
deps: TDeps | (() => TDeps),
options?: { override?: boolean }
)
deps
DependencyMap | () => DependencyMap
required
Object mapping dependency keys to resources. Can be static or a function..dependencies(() => ({
emailService: emailServiceResource,
logger: globals.resources.logger,
database: databaseResource,
}))
When true, replaces existing dependencies. When false (default), merges with existing.
Returns: New builder with updated dependencies type
Attaches tags for grouping and filtering.
.tags<TTags>(
tags: TagType[],
options?: { override?: boolean }
)
Array of tag definitions..tags([criticalTag, asyncTag])
When true, replaces existing tags. When false (default), appends to existing.
Returns: New builder with tags attached
Attaches metadata for documentation and tooling.
.meta<TMeta>(metadata: ITaskMeta)
Metadata object with optional title and description..meta({
title: "Send Welcome Email",
description: "Sends a welcome email when user registers",
})
Returns: New builder with metadata attached
throws()
Documents which errors the hook may throw.
.throws(errorList: ThrowsList)
Array of error classes. This is declarative only (for documentation)..throws([EmailError, NetworkError])
Returns: New builder with error documentation
build()
Builds and returns the final hook definition.
.build(): IHook<TDeps, TOn, TMeta>
Returns: Immutable hook definition ready to register
Throws: Error if on() or run() was not called
Type Signature
interface HookFluentBuilder<
TDeps extends DependencyMapType = {},
TOn extends ValidOnTarget | undefined = undefined,
TMeta extends ITaskMeta = ITaskMeta
> {
id: string;
on<TNewOn extends ValidOnTarget>(on: TNewOn): HookFluentBuilder<TDeps, TNewOn, TMeta>;
order(order: number): HookFluentBuilder<TDeps, TOn, TMeta>;
dependencies<TNewDeps extends DependencyMapType>(
deps: TNewDeps | (() => TNewDeps),
options?: { override?: boolean }
): HookFluentBuilder<TDeps & TNewDeps, TOn, TMeta>;
tags<TNewTags extends TagType[]>(
t: TNewTags,
options?: { override?: boolean }
): HookFluentBuilder<TDeps, TOn, TMeta>;
meta<TNewMeta extends ITaskMeta>(m: TNewMeta): HookFluentBuilder<TDeps, TOn, TNewMeta>;
throws(list: ThrowsList): HookFluentBuilder<TDeps, TOn, TMeta>;
run(fn: IHookDefinition<TDeps, ResolvedOn<TOn>, TMeta>["run"]): HookFluentBuilder<TDeps, TOn, TMeta>;
build(): IHook<TDeps, ResolvedOn<TOn>, TMeta>;
}
Examples
Basic Hook
const logEvent = r.hook("app.hooks.logEvent")
.on(userRegisteredEvent)
.run(async ({ payload, logger }) => {
await logger.info(`User registered: ${payload.email}`);
})
.build();
Hook with Dependencies
const sendWelcomeEmail = r.hook("app.hooks.sendWelcome")
.on(userRegisteredEvent)
.dependencies(() => ({
emailService: emailServiceResource,
templateEngine: templateResource,
}))
.run(async ({ payload, deps, logger }) => {
await logger.info(`Sending welcome email to ${payload.email}`);
const content = await deps.templateEngine.render('welcome', payload);
await deps.emailService.send(payload.email, content);
})
.build();
Hook with Order (Priority)
// High priority - runs first
const validateData = r.hook("app.hooks.validate")
.on(dataReceivedEvent)
.order(-100)
.run(async ({ payload }) => {
if (!isValid(payload)) throw new ValidationError();
})
.build();
// Normal priority
const processData = r.hook("app.hooks.process")
.on(dataReceivedEvent)
.order(0)
.run(async ({ payload }) => {
await processData(payload);
})
.build();
// Low priority - runs last
const logCompletion = r.hook("app.hooks.logComplete")
.on(dataReceivedEvent)
.order(100)
.run(async ({ logger }) => {
await logger.info('Data processing complete');
})
.build();
Hook Listening to Multiple Events
const trackUserActivity = r.hook("app.hooks.trackActivity")
.on([userLoggedIn, userLoggedOut, userUpdatedProfile])
.dependencies(() => ({ analytics: analyticsResource }))
.run(async ({ payload, event, deps }) => {
await deps.analytics.track(event.id, payload);
})
.build();
Wildcard Hook (All Events)
const debugLogger = r.hook("app.hooks.debugAll")
.on("*")
.run(async ({ event, payload, logger }) => {
await logger.debug(`Event emitted: ${event.id}`, { payload });
})
.build();
Hook with Task Execution
const processOrderHook = r.hook("app.hooks.processOrder")
.on(orderCreatedEvent)
.dependencies(() => ({
processOrderTask,
}))
.run(async ({ payload, runtime }) => {
// Execute a task in response to event
await runtime.runTask(processOrderTask, { orderId: payload.orderId });
})
.build();
Hook with Error Handling
const notifyAdmin = r.hook("app.hooks.notifyAdmin")
.on(criticalErrorEvent)
.dependencies(() => ({
alertService: alertServiceResource,
}))
.throws([AlertError, NetworkError])
.meta({
title: "Notify Admin on Critical Error",
description: "Sends alert to admin when critical errors occur",
})
.run(async ({ payload, deps, logger }) => {
try {
await deps.alertService.send(payload);
} catch (error) {
await logger.error('Failed to notify admin', { error });
throw error; // Re-throw to mark hook as failed
}
})
.build();
Conditional Hook Logic
const sendNotification = r.hook("app.hooks.notify")
.on(orderStatusChanged)
.dependencies(() => ({
notificationService: notificationResource,
userPreferences: preferencesResource,
}))
.run(async ({ payload, deps, logger }) => {
const prefs = await deps.userPreferences.get(payload.userId);
if (!prefs.emailNotifications) {
await logger.debug('Skipping notification - user disabled emails');
return;
}
await deps.notificationService.send(payload.userId, payload.status);
})
.build();
Hook Execution Model
Sequential Execution
Hooks execute in order (lowest to highest):
// Order: -10
const first = r.hook("first").on(event).order(-10).run(async () => {}).build();
// Order: 0 (default)
const second = r.hook("second").on(event).run(async () => {}).build();
// Order: 10
const third = r.hook("third").on(event).order(10).run(async () => {}).build();
// Execution: first → second → third
Parallel Execution
Hooks with same order run concurrently if event has parallel(true):
const event = r.event("myEvent").parallel(true).build();
// All have order 0 - run in parallel
const hook1 = r.hook("h1").on(event).run(async () => {}).build();
const hook2 = r.hook("h2").on(event).run(async () => {}).build();
const hook3 = r.hook("h3").on(event).run(async () => {}).build();
Error Handling
By default, hook errors are logged but don’t stop other hooks:
await runtime.emitEvent(myEvent, payload, {
failureMode: 'continue', // Continue even if hooks fail (default)
throwOnError: false, // Don't throw on hook failure (default)
});
// Or get detailed report:
const report = await runtime.emitEvent(myEvent, payload, { report: true });
if (report.failedListeners.length > 0) {
console.error('Failed hooks:', report.failedListeners);
}