Event System

Server uses @hornjs/evt for typed lifecycle events.

Overview

Server extends EventDispatcher<ServerEventMap>, so event names and payload types stay aligned:

import type { ServerEventMap } from "@hornjs/fest";

The current lifecycle events are:

  • serve
  • close
  • error

Lifecycle Event Types

ServerServeEvent

Dispatched after the runtime adapter reports that the server is listening.

ServerCloseEvent

Dispatched after shutdown completes and all waitUntil() tasks have settled.

ServerErrorEvent

Dispatched when asynchronous runtime adapter initialization fails.

The event exposes the original error through event.error.

Custom Events

ServerEventMap is built from the built-in lifecycle events plus ServerEventMapCustom, so you can extend it with module augmentation.

import { Server } from "@hornjs/fest";

declare module "@hornjs/fest" {
  interface ServerEventMapCustom {
    reload: CustomEvent<{ full: boolean }>;
  }
}

const server = new Server({
  routes: {
    "/": () => new Response("ok"),
  },
  fetch: () => new Response("Not Found", { status: 404 }),
});

server.addEventListener("reload", (event) => {
  console.log(event.detail.full);
});

server.dispatchEvent(
  new CustomEvent("reload", {
    detail: { full: true },
  }),
);

This works because ServerEventMap is defined as the built-in lifecycle map intersected with the augmentable ServerEventMapCustom interface.

Listening for Events

Use standard DOM-style event listeners:

import { Server, ServerCloseEvent, ServerErrorEvent, ServerServeEvent } from "@hornjs/fest";

const server = new Server({
  fetch() {
    return new Response("hello");
  },
});

server.addEventListener("serve", (event: ServerServeEvent) => {
  console.log("ready", server.url);
});

server.addEventListener("error", (event: ServerErrorEvent) => {
  console.error("failed", event.error);
});

server.addEventListener("close", (_event: ServerCloseEvent) => {
  console.log("closed");
});

await server.ready();

Listening Model

Server integrates @hornjs/evt through the EventDispatcher base class, so it uses the familiar DOM-style listener model.

Listening with addEventListener()

Register a listener for one lifecycle event.

  • type must be a key from ServerEventMap
  • listener receives the correctly typed event object
  • once and signal behave like the standard DOM API
  • listener identity follows the DOM model: listener + capture
server.addEventListener(
  "serve",
  () => {
    console.log("server is ready");
  },
  { once: true },
);

Removing listeners

Remove a previously registered listener.

function onClose() {
  console.log("closed");
}

server.addEventListener("close", onClose);
server.removeEventListener("close", onClose);

Dispatch semantics

Dispatch an event instance to all listeners registered for event.type.

In fest, lifecycle events are dispatched internally by Server, but the underlying semantics come from @hornjs/evt:

  • dispatch runs at-target only
  • there is no bubbling tree
  • event.target, event.currentTarget, and event.eventPhase are normalized to match DOM-style dispatch behavior
server.dispatchEvent(new ServerCloseEvent());

In normal application code, you typically listen for lifecycle events rather than dispatching them yourself.

Event Timing

serve

Triggered from server.serve() after the adapter resolves the final listener URL.

close

Triggered from server.close() after:

  • the runtime adapter has closed
  • pending waitUntil() work has completed

error

Triggered only for non-abort failures during deferred adapter initialization.

If startup fails after serve() has already begun, you should still handle the rejected promise from ready() or serve().

Programmatic Coordination

Events are useful when you want to react to lifecycle changes without tightly coupling everything to await server.ready():

const server = new Server({
  manual: true,
  routes: {
    "/": () => new Response("ok"),
  },
  fetch: () => new Response("Not Found", { status: 404 }),
});

server.addEventListener("serve", () => {
  console.log("server is live");
});

await server.serve();

For one-off startup flows, await server.ready() is usually simpler. Events are more useful when integrating logging, health reporting, or host-level process coordination.

Behavior Notes

The event behavior inherited from @hornjs/evt is intentionally predictable:

  • event names are typed string keys from ServerEventMap
  • listener exceptions do not change the current dispatch loop by default
  • ServerErrorEvent is part of fest lifecycle semantics, not evt's optional cancelable dispatcher "error" event feature