Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/engine-models/src/models/IEngineServer.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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<void>;
start(engineReady?: () => Promise<void>): Promise<void>;

/**
* Stop the engine server.
Expand Down
9 changes: 9 additions & 0 deletions packages/engine-server/docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions packages/engine-server/src/engineServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
public async start(engineReady?: () => Promise<void>): Promise<void> {
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
Expand Down Expand Up @@ -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<void> {
private async startWebServer(engineReady?: () => Promise<void>): Promise<void> {
this._restRoutes = await this.buildRestRoutes();
this._socketRoutes = await this.buildSocketRoutes();
const restRouteProcessors = RestRouteProcessorFactory.names().map(n =>
Expand Down Expand Up @@ -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();
}

Expand Down
52 changes: 52 additions & 0 deletions packages/engine-server/tests/engineServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading