#Internal Events
Alepha uses a hook-based event system for lifecycle management and cross-service communication.
#Confusion Warning
$hookis not a React Hook (e.g.useState). It is an event listener.$hookis not a pub/sub system. Use$topicfor pub/sub.
#Lifecycle
When alepha.start() is called, the framework emits configure, start and ready, in that order; alepha.stop() emits stop. Each one waits for every hook of the previous one to finish. The Alepha Instance draws the whole sequence, failure path included.
| Hook | When | Typical use |
|---|---|---|
configure |
Before start, after container is locked | Register providers, resolve configuration |
start |
After configure | Connect to databases, start listeners |
ready |
After start | Application is fully operational |
stop |
On shutdown (SIGINT/SIGTERM or manual) | Close connections, flush buffers |
All four receive the Alepha instance as payload.
Hooks of one event run one after the other, each awaited before the next, never in parallel. A hook that throws during configure, start or ready aborts the boot. During stop, every hook runs even when one throws, and stop() reports the failures together once all of them have run.
#Using $hook
Register hooks with the $hook primitive. It must be a class property, on a singleton service: a $hook instantiated after the app has started (a scoped or transient service built during a request) throws, since a hook is never unregistered and would leak once per instantiation.
1import { $hook } from "alepha"; 2import { $logger } from "alepha/logger"; 3 4class DatabaseService { 5 log = $logger(); 6 7 onStart = $hook({ 8 on: "start", 9 handler: async () => {10 await this.connectToDatabase();11 this.log.info("Database connected");12 },13 });14 15 onStop = $hook({16 on: "stop",17 handler: async () => {18 await this.disconnectFromDatabase();19 this.log.info("Database disconnected");20 },21 });22}
#Hook options
1$hook({2 on: "start", // required: event name3 handler: async () => {4 /* ... */5 }, // required: callback6 priority: "first", // optional: "first" | "last" (default: insertion order)7});
priority: "first" places the hook at the front of the execution queue. priority: "last" places it at the end. Without a priority, hooks execute in registration order (which follows dependency order). For finer ordering relative to specific services, before and after accept a service class or instance (or an array of them) whose hooks this one must run before or after:
1class CacheWarmer {2 onStart = $hook({3 on: "start",4 after: DatabaseService, // the database is connected first5 handler: async () => {6 /* ... */7 },8 });9}
priority sorts first; before and after then order hooks within the same priority tier.
#Hook call tracking
Each $hook instance tracks how many times it has been called:
1const alepha = Alepha.create().with(App);2await alepha.start();3 4const app = alepha.inject(App);5console.log(app.onStart.called); // 1
This is useful for testing to ensure hooks are called the expected number of times.
#Built-in Hooks
The core Hooks interface defines:
1interface Hooks { 2 configure: Alepha; // configuration phase 3 start: Alepha; // start phase 4 ready: Alepha; // ready phase 5 stop: Alepha; // shutdown phase 6 "state:mutated": { 7 // state change notification 8 key: keyof State; 9 value: any;10 prevValue: any;11 };12 "state:registered": { atom: Atom }; // an atom was registered13}
Other modules extend this interface. For example, alepha/logger adds log:entry, alepha/server adds server-related hooks, and so on.
#Custom Hooks
Define custom hooks using TypeScript module augmentation:
1declare module "alepha" {2 interface Hooks {3 "billing:invoice:created": {4 invoiceId: string;5 amount: number;6 };7 }8}
#Listening to custom hooks
As a class property with $hook:
1class NotificationService {2 onInvoice = $hook({3 on: "billing:invoice:created",4 handler: async ({ invoiceId, amount }) => {5 await this.sendReceipt(invoiceId, amount);6 },7 });8}
Or directly on the event manager:
1alepha.events.on("billing:invoice:created", ({ invoiceId, amount }) => {2 console.log(`Invoice ${invoiceId} created for ${amount}`);3});
alepha.events.on() returns an unsubscribe function:
1const unsubscribe = alepha.events.on("billing:invoice:created", handler);2// later...3unsubscribe();
#Emitting custom hooks
1await alepha.events.emit("billing:invoice:created", {2 invoiceId: "inv_123",3 amount: 99.99,4});
The emit method accepts options:
1await alepha.events.emit("billing:invoice:created", payload, {2 log: true, // log execution timing of each hook3 catch: true, // catch errors and log them instead of throwing4});
With log: true, a report object ({ completed: [], failed: [] }) you pass in the options is filled with the callers of the hooks that finished and, under catch: true, of those that threw (without catch, the first throw ends the emit). alepha.events.callers(event) lists who listens to an event, which is what to print when a hook is missing.
#Compiled Events (advanced)
emit() already compiles and caches an optimized executor per event, so there is no overhead
to avoid in normal code. compile() exists to hoist that one cache lookup out of a hot loop:
1// After all hooks are registered (e.g. after start)2const onRequest = alepha.events.compile("server:request:before", {3 catch: true,4});5 6// In the request handler - returns void if sync, Promise if async7const result = onRequest({ request, route });8if (result) await result;
#Naming hooks
Hook names follow namespace[:subject]:verb[:phase], with lower camel case
inside each segment. Interceptable operations end in before and their later
observers in after. Observed operations use begin, success, error and
end. Facts use a past participle without a phase, for example
payments:captured or cache:hit. No onX or beforeX segment is allowed.
The only bare names are configure, start, ready and stop; log entries use
log:entry. The explicitly preserved job lifecycle/control family remains
job:begin, job:success, job:error, job:end and job:cancel.
check:conventions checks every Hooks declaration, tests and apps included.
Retired framework names fail immediately and name their replacement.
The complete migration table is in the changelog. New application facts can be
named myApp:task:completed; an interceptable operation can use
myApp:task:run:before. Register listeners before startup. A retired name is an
error even when TypeScript has been bypassed, so stale listeners fail at boot.