Skip to content

feat: add a warmup phase to the application lifecycle - #48

Merged
thetutlage merged 4 commits into
9.xfrom
feat/warmup-lifecycle
Aug 19, 2026
Merged

thetutlage merged 4 commits into
9.xfrom
feat/warmup-lifecycle

Conversation

@thetutlage

@thetutlage thetutlage commented Aug 19, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a warmup phase to the application lifecycle, so an application can be fully assembled without ever being made ready.

This is groundwork for adonisjs/assembler#100 and the node ace codegen command in core, which need a booted application to read the routes from, but must not start an HTTP server, queue workers or a Vite dev server along the way.

What changed

app.warmUp() starts the providers, runs the starting hooks and imports the preload files. That is everything app.start used to do before invoking its callback, so app.start now simply delegates to it. The order is unchanged, which matters because preload files rely on the state established by providers.start() — @adonisjs/queue initialises its manager there and the start/scheduler.ts preload schedules jobs against it.

init → boot → [ providers.start → starting hooks → preloads ] → callback → ready
                                  ▲
                          warmUp() stops here

Application modes. An app is created with a mode, defaulting to run.

  • run — behaves exactly as before.
  • warmup — the app is only being assembled to be inspected. app.start throws, and app.terminate skips the providers shutdown hooks.

Providers use the mode to skip their long running side-effects:

async start() {
  if (this.app.getMode() !== 'run') {
    return
  }
  await startQueueWorkers()
}

The mode must never be used to register different bindings. The app being inspected has to match the app that runs, otherwise the generated code describes something that does not exist.

Skipping shutdown in warmup mode. Every shutdown hook begins by resolving its manager from the container (container.make('lucid.db'), container.make('redis') and so on). Since register() only binds lazy factories, running them for a warmed up app would construct the very resources that were never used, just to tear them down. Terminating already skips provider shutdown for shallower states, because #providersWithShutdownListeners is populated during register().

Is this breaking?

No, it is a minor.

Additive: warmUp(), getMode(), setMode(), isWarmedUp, ApplicationModes and the optional mode constructor option. Mode defaults to run, so the new start throw and the shutdown skip are unreachable for existing code.

Two things are technically observable, neither in the documented surface:

  1. ApplicationStates gains 'warming' | 'warmed', so an exhaustive switch over getState() loses exhaustiveness under strict.
  2. During start() the state now passes through 'warming'/'warmed' instead of staying 'booted', which is visible to code calling getState() from inside a starting hook or a preload.

Tests

11 new tests in tests/application/warm_up.spec.ts covering warm-then-stop, providers.start() running while ready() does not, preload import during warmup, idempotency, the start rejection, terminate from 'warmed', hook ordering for a normal run, starting an already warmed app, and the mode accessors.

Adds "app.warmUp" to assemble the application without ever making it ready.
The method starts the providers, runs the "starting" hooks and imports the
preload files, which is everything "app.start" used to do before invoking its
callback. Now "app.start" delegates to it, so nothing moves for an app that
runs.

The application is also given a mode. The default "run" mode behaves exactly
like before, whereas the "warmup" mode tells the providers that the app is
only being assembled to be inspected, hence they must skip their long running
side-effects. Calling "app.start" on such an app throws and terminating it
skips the providers shutdown hooks, since nothing was ever opened.

The phase exists for the tooling that has to inspect an application instead
of using it. For example, generating the route types without starting the
HTTP server.
@thetutlage thetutlage self-assigned this Aug 19, 2026
@thetutlage
thetutlage merged commit 8474819 into 9.x Aug 19, 2026
11 checks passed
@thetutlage
thetutlage deleted the feat/warmup-lifecycle branch August 19, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant