From 02bd60383d66275ef9c64eef9bab24208aebc8f8 Mon Sep 17 00:00:00 2001 From: Martyn Janes Date: Mon, 26 Oct 2026 15:25:22 +0800 Subject: [PATCH] fix: run engineReady before the web server listens --- .../engine-models/src/models/IEngineServer.ts | 4 +- packages/engine-server/docs/examples.md | 9 ++++ packages/engine-server/src/engineServer.ts | 15 ++++-- .../engine-server/tests/engineServer.spec.ts | 52 +++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/packages/engine-models/src/models/IEngineServer.ts b/packages/engine-models/src/models/IEngineServer.ts index a6a38c75..7a1c8978 100644 --- a/packages/engine-models/src/models/IEngineServer.ts +++ b/packages/engine-models/src/models/IEngineServer.ts @@ -1,4 +1,5 @@ // Copyright 2024 IOTA Stiftung. +// Copyright 2026 3Sixty Trade Ltd. // SPDX-License-Identifier: Apache-2.0. import type { IRestRoute, ISocketRoute } from "@3sixty/api-models"; @@ -36,9 +37,10 @@ export interface IEngineServer { /** * Start the engine server. + * @param engineReady Called once the engine has started and the routes are built, before the web server accepts requests. * @returns A promise that resolves when the server has started and is ready to accept requests. */ - start(): Promise; + start(engineReady?: () => Promise): Promise; /** * Stop the engine server. diff --git a/packages/engine-server/docs/examples.md b/packages/engine-server/docs/examples.md index 2caa654e..14fb20b6 100644 --- a/packages/engine-server/docs/examples.md +++ b/packages/engine-server/docs/examples.md @@ -72,6 +72,15 @@ await server.start(); await server.stop(); ``` +Pass `engineReady` to `start` for work which must complete before the first request is served. It runs once the engine has started and the routes are built, before the web server listens: + +```typescript +await server.start(async () => { + const routes = server.getRestRoutes(); + // e.g. seed data the route processors depend on +}); +``` + ## addDefaultRestPaths and addDefaultSocketPaths ```typescript diff --git a/packages/engine-server/src/engineServer.ts b/packages/engine-server/src/engineServer.ts index bbe38723..3732bd53 100644 --- a/packages/engine-server/src/engineServer.ts +++ b/packages/engine-server/src/engineServer.ts @@ -216,14 +216,15 @@ export class EngineServer< /** * Start the engine server. + * @param engineReady Called once the engine has started and the routes are built, before the web server accepts requests. * @returns A promise that resolves when the server has started and is ready to accept requests. */ - public async start(): Promise { + public async start(engineReady?: () => Promise): Promise { await this._engineCore.start(); try { await ContextIdStore.run(this._engineCore.getContextIds() ?? {}, async () => { - await this.startWebServer(); + await this.startWebServer(engineReady); }); } catch (err) { // The engine components are already running, so they must be stopped when the @@ -256,10 +257,11 @@ export class EngineServer< /** * Starts the web server. + * @param engineReady Called after the web server is built and before it starts listening. * @returns A promise that resolves when the web server is built and listening. * @internal */ - private async startWebServer(): Promise { + private async startWebServer(engineReady?: () => Promise): Promise { this._restRoutes = await this.buildRestRoutes(); this._socketRoutes = await this.buildSocketRoutes(); const restRouteProcessors = RestRouteProcessorFactory.names().map(n => @@ -328,6 +330,13 @@ export class EngineServer< this._socketRoutes, coreConfig.web ); + + // Work which must complete before any request is served, such as seeding the + // authorization model the route processors check requests against. + if (Is.function(engineReady)) { + await engineReady(); + } + await this._webServer.start(); } diff --git a/packages/engine-server/tests/engineServer.spec.ts b/packages/engine-server/tests/engineServer.spec.ts index b6213c86..271fd3da 100644 --- a/packages/engine-server/tests/engineServer.spec.ts +++ b/packages/engine-server/tests/engineServer.spec.ts @@ -212,6 +212,58 @@ describe("engine-server", () => { await engineServer.stop(); }); + test("Calls engineReady once the engine and routes are ready, before listening", async () => { + const engine = new Engine({ + config: { + silent: true, + types: { + informationComponent: [ + { + type: InformationComponentType.Service, + options: { config: { serverInfo: { name: "foo", version: "1" } } }, + restPath: "/info" + } + ] + }, + web: { port } + } + }); + const engineServer = new EngineServer({ engineCore: engine }); + + let engineStarted = false; + let routeCount = 0; + let listening = true; + await engineServer.start(async () => { + engineStarted = engine.isStarted(); + routeCount = engineServer.getRestRoutes().length; + listening = await fetch(`http://localhost:${port}/info`).then( + () => true, + () => false + ); + }); + + expect(engineStarted).toEqual(true); + expect(routeCount).toBeGreaterThan(0); + expect(listening).toEqual(false); + + const response = await fetch(`http://localhost:${port}/info`); + expect(response.status).toEqual(200); + + await engineServer.stop(); + }); + + test("Stops the engine when engineReady fails", async () => { + const engine = new Engine({ config: { silent: true, types: {}, web: { port } } }); + const engineServer = new EngineServer({ engineCore: engine }); + + await expect( + engineServer.start(async () => { + throw new Error("not ready"); + }) + ).rejects.toThrow("not ready"); + expect(engine.isStarted()).toEqual(false); + }); + test("Fails to start engine server with unsupported web server type", async () => { const engine = new Engine({ config: {