alepha@docs:~/framework/docs/guides/core$
cat 5-internal-events.md | pretty
4 min read
Last commit:

#Internal Events

Alepha uses a hook-based event system for lifecycle management and cross-service communication.

#Confusion Warning

  • $hook is not a React Hook (e.g. useState). It is an event listener.
  • $hook is not a pub/sub system. Use $topic for 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.

typescript
 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

typescript
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:

typescript
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:

typescript
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:

typescript
 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:

typescript
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:

typescript
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:

typescript
1alepha.events.on("billing:invoice:created", ({ invoiceId, amount }) => {2  console.log(`Invoice ${invoiceId} created for ${amount}`);3});

alepha.events.on() returns an unsubscribe function:

typescript
1const unsubscribe = alepha.events.on("billing:invoice:created", handler);2// later...3unsubscribe();

#Emitting custom hooks

typescript
1await alepha.events.emit("billing:invoice:created", {2  invoiceId: "inv_123",3  amount: 99.99,4});

The emit method accepts options:

typescript
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:

typescript
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.