From a174c355d2040c26c17ebe4fdc1111e6f231d2de Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:19:36 +0000 Subject: [PATCH 1/5] Answer the disconnect prepare in the close old connections test The test's first connector had no Link, so the controller's disconnect prepare went unanswered and it waited the full 10 s timeout. Co-Authored-By: Claude Opus 5.5 --- test/integration/link.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integration/link.js b/test/integration/link.js index 01beb471..57ecbeaa 100644 --- a/test/integration/link.js +++ b/test/integration/link.js @@ -147,7 +147,9 @@ describe("Integration of lib/link", function() { it("should close old connections from the same address", async function() { slowTest(this); - const hostConnectorFirst = new TestHostConnector(url, 0.2); + // Without a link the disconnect prepare goes unanswered and the controller waits 10 s for it + const hostLinkFirst = new lib.Link(new TestHostConnector(url, 0.2)); + const hostConnectorFirst = hostLinkFirst.connector; hostConnectorFirst.token = hostConnector.token; hostConnectorFirst.hostId = hostConnector.hostId; const onceClose = events.once(hostConnectorFirst, "close"); From 0312a3b099787dbce46979362a3d0e17dcc5f700 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:29:42 +0000 Subject: [PATCH 2/5] Start the controller and host CLIs faster Like ctl, the controller and host now compile schema validators on first use instead of at import, which cost about 120 ms per invocation. The Controller and Host classes are only imported by the commands that start them, so config, bootstrap and the other commands no longer load express, ws and the rest of the servers. clusteriocontroller config set goes from 413 ms to 231 ms and clusteriohost config set from 412 ms to 225 ms. The integration tests run the bins, like npx does, instead of the package directories. Co-Authored-By: Claude Opus 5.5 --- packages/controller/controller.ts | 7 ++++++- packages/controller/src/set_lazy_schema_compilation.ts | 3 +++ packages/host/host.ts | 6 +++++- packages/host/src/set_lazy_schema_compilation.ts | 3 +++ test/integration/index.js | 6 +++--- 5 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 packages/controller/src/set_lazy_schema_compilation.ts create mode 100644 packages/host/src/set_lazy_schema_compilation.ts diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 46b04cb0..9e154c47 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -26,10 +26,12 @@ import DailyRotateFile from "winston-daily-rotate-file"; import jwt from "jsonwebtoken"; // homebrew modules +// Reduce startup time by lazy compiling schemas. +import "./src/set_lazy_schema_compilation.js"; import * as lib from "@clusterio/lib"; import { ConsoleTransport, levels, logger } from "@clusterio/lib"; -import Controller from "./src/Controller.js"; +import type Controller from "./src/Controller.js"; import UserManager from "./src/UserManager.js"; import UserRecord from "./src/UserRecord.js"; import packageConfig from "@clusterio/controller/package.json" with { type: "json" }; @@ -203,6 +205,7 @@ async function handleBootstrapCommand( } let ctlConfig = new lib.CtlConfig("control"); + const { default: Controller } = await import("./src/Controller.js"); ctlConfig.set("ctl.controller_url", Controller.calculateControllerUrl(controllerConfig)); ctlConfig.set( "ctl.controller_token", @@ -475,6 +478,8 @@ async function startup() { return; } + // Loaded here as the server is not needed by the other commands + const { default: Controller } = await import("./src/Controller.js"); controller = await Controller.fromConfig( controllerConfig, pluginInfos, diff --git a/packages/controller/src/set_lazy_schema_compilation.ts b/packages/controller/src/set_lazy_schema_compilation.ts new file mode 100644 index 00000000..f5ff3418 --- /dev/null +++ b/packages/controller/src/set_lazy_schema_compilation.ts @@ -0,0 +1,3 @@ +// This needs to be a separate file due to ECMAScript Modules evaluating code in +// imported modules before any code in the file that imports it. +(global as any).lazySchemaCompilation = true; diff --git a/packages/host/host.ts b/packages/host/host.ts index 11ea2ba8..e9af8f0b 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -20,10 +20,12 @@ import winston from "winston"; import "winston-daily-rotate-file"; // internal libraries +// Reduce startup time by lazy compiling schemas. +import "./src/set_lazy_schema_compilation.js"; import * as lib from "@clusterio/lib"; import { ConsoleTransport, levels, logger } from "@clusterio/lib"; -import Host from "./src/Host.js"; +import type Host from "./src/Host.js"; import { createScenarioCommand, handleCreateScenarioCommand } from "./src/create_scenario.js"; let host: Host | undefined; @@ -233,6 +235,8 @@ async function startHost() { return; } + // Loaded here as the server is not needed by the other commands + const { default: Host } = await import("./src/Host.js"); let hostConnector = new HostConnector(hostConfig, pluginInfos); host = await Host.fromConfig( hostConfig, diff --git a/packages/host/src/set_lazy_schema_compilation.ts b/packages/host/src/set_lazy_schema_compilation.ts new file mode 100644 index 00000000..f5ff3418 --- /dev/null +++ b/packages/host/src/set_lazy_schema_compilation.ts @@ -0,0 +1,3 @@ +// This needs to be a separate file due to ECMAScript Modules evaluating code in +// imported modules before any code in the file that imports it. +(global as any).lazySchemaCompilation = true; diff --git a/test/integration/index.js b/test/integration/index.js index ccb977e7..158bbd2a 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -205,17 +205,17 @@ export function exec(command, options = {}) { } export function execController(...args) { - args[0] = `node --enable-source-maps ../../packages/controller ${args[0]}`; + args[0] = `node --enable-source-maps ../../packages/controller/dist/node/controller.js ${args[0]}`; return exec(...args); } export function execHost(...args) { - args[0] = `node --enable-source-maps ../../packages/host ${args[0]}`; + args[0] = `node --enable-source-maps ../../packages/host/dist/node/host.js ${args[0]}`; return exec(...args); } export function execCtlProcess(...args) { - args[0] = `node --enable-source-maps ../../packages/ctl ${args[0]}`; + args[0] = `node --enable-source-maps ../../packages/ctl/dist/node/ctl.js ${args[0]}`; return exec(...args); } From 75cdfbad8d90dba7d4740ac193c1d11ba260138d Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:16:28 +0000 Subject: [PATCH 3/5] Split the test jobs in two halves The clusterioctl, plugin system and instance integration tests take about half of the test time, so they run on one runner and the rest of the tests on another, each half with its own controller and host. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/node.js.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 9a0508f6..32f9a0e5 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -34,11 +34,20 @@ jobs: # Supported factorio versions are latest stable and one minor version before stable (even if from a previous major) factorio-version: [1.1.110, 2.0.77, 2.1.17] - + + # The tests run in two halves of similar length on separate runners, + # the clusterioctl, plugin system and instance integration tests and the rest. + tests: [ctl, other] + # Coverage is performed on the older of the supported node versions include: - node-version: 22.x factorio-version: 2.1.17 + tests: ctl + coverage: true + - node-version: 22.x + factorio-version: 2.1.17 + tests: other coverage: true steps: @@ -51,10 +60,10 @@ jobs: run: wget -q -O factorio.tar.gz https://www.factorio.com/get-download/${{ matrix.factorio-version }}/headless/linux64 && tar -xf factorio.tar.gz && rm factorio.tar.gz - name: Run tests if: ${{ !matrix.coverage }} - run: pnpm test + run: pnpm test --grep "^(Integration of Clusterio (clusterioctl|plugin system)|Clusterio Instance) " ${{ matrix.tests == 'other' && '--invert' || '' }} - name: Run coverage if: ${{ matrix.coverage }} - run: pnpm run ci-cover + run: pnpm run ci-cover --grep "^(Integration of Clusterio (clusterioctl|plugin system)|Clusterio Instance) " ${{ matrix.tests == 'other' && '--invert' || '' }} - name: Upload coverage to Codecov if: ${{ matrix.coverage }} uses: codecov/codecov-action@v7 @@ -67,7 +76,7 @@ jobs: if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: - name: logs-${{ matrix.node-version }}-${{ matrix.factorio-version }} + name: logs-${{ matrix.node-version }}-${{ matrix.factorio-version }}-${{ matrix.tests }} path: temp/test/logs version-compat: From b9d7033724054e9bd60cc2a37058cf677b38d2a1 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:31:57 +0000 Subject: [PATCH 4/5] Test old Node.js versions only with the latest Factorio Splitting the test jobs doubled them to 14, over GitHub's concurrent job limit together with the other jobs. Every Factorio version now runs on the latest Node.js only, and the other Node.js versions only with the latest Factorio, which is 10 test jobs. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/node.js.yml | 17 ++++++++++++----- docs/contributing.md | 3 ++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 32f9a0e5..cb033ec7 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -28,19 +28,26 @@ jobs: strategy: fail-fast: false matrix: - # Test Node.js 24 and 26 across the Factorio matrix; Node.js 22 runs coverage below. + # To stay within the concurrent job limit, every Factorio version is + # tested on the latest Node.js, and every other supported Node.js + # version only with the latest Factorio. Coverage is performed on the + # oldest supported Node.js. # Node.js release schedule: https://nodejs.org/en/about/releases/ - node-version: [24.x, 26.x] - - # Supported factorio versions are latest stable and one minor version before stable (even if from a previous major) + # Supported factorio versions are latest stable and one minor version before stable (even if from a previous major) + node-version: [26.x] factorio-version: [1.1.110, 2.0.77, 2.1.17] # The tests run in two halves of similar length on separate runners, # the clusterioctl, plugin system and instance integration tests and the rest. tests: [ctl, other] - # Coverage is performed on the older of the supported node versions include: + - node-version: 24.x + factorio-version: 2.1.17 + tests: ctl + - node-version: 24.x + factorio-version: 2.1.17 + tests: other - node-version: 22.x factorio-version: 2.1.17 tests: ctl diff --git a/docs/contributing.md b/docs/contributing.md index 5a9adfdb..a206de81 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -339,7 +339,8 @@ The format of this log should be self explanatory, please follow it carefully. P For development, use the latest Node.js LTS release, following the [installation instructions](../README.md#installation). Make sure the Node.js built-in APIs and npm packages you use remain compatible with the supported versions. -The GitHub Actions tests currently run on Node.js 24 and 26 across the Factorio matrix, with Node.js 22 used for coverage; see the [CI workflow](../.github/workflows/node.js.yml) for the version matrix. +The GitHub Actions tests run every Factorio version on the latest Node.js, and every other supported Node.js version with the latest Factorio only, which keeps the number of jobs within GitHub's concurrent job limit. +Coverage is collected on the oldest supported Node.js; see the [CI workflow](../.github/workflows/node.js.yml) for the version matrix. For Factorio, active support covers the latest stable version and the previous major version, using version lines such as 2.1, 2.0 and 1.1 as described in the [Factorio support policy](decisions.md#factorio). Older versions from 0.17 onwards receive passive support where it does not restrict actively supported versions. From e7c21fb90749878b224314bfb5c2de235ac79577 Mon Sep 17 00:00:00 2001 From: Bastiaan <17990055+bbassie@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:41:37 +0100 Subject: [PATCH 5/5] Merge pull request #1150 from bbassie/fix/node-engine-range Require Node.js 22.18 or 24.2 for import.meta.main --- package.json | 2 +- packages/controller/package.json | 2 +- packages/create/package.json | 2 +- packages/ctl/package.json | 2 +- packages/host/package.json | 2 +- packages/lib/package.json | 2 +- packages/web_ui/package.json | 2 +- plugins/global_chat/package.json | 2 +- plugins/player_auth/package.json | 2 +- plugins/player_sync/package.json | 2 +- plugins/research_sync/package.json | 2 +- plugins/statistics_exporter/package.json | 2 +- plugins/subspace_storage/package.json | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 8ae0c1d4..47f86fa3 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ ] }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "workspaces": [ "packages/*", diff --git a/packages/controller/package.json b/packages/controller/package.json index e968acbd..0d144cc1 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -32,7 +32,7 @@ "clusteriocontroller": "./dist/node/controller.js" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "dependencies": { diff --git a/packages/create/package.json b/packages/create/package.json index 00b6e754..ba34a393 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -28,7 +28,7 @@ "clusteriocreate": "./create.js" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "dependencies": { diff --git a/packages/ctl/package.json b/packages/ctl/package.json index 48930727..914839ca 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -32,7 +32,7 @@ "clusterioctl": "./dist/node/ctl.js" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "dependencies": { diff --git a/packages/host/package.json b/packages/host/package.json index b552445f..26d1f9fe 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -33,7 +33,7 @@ "clusteriohost": "./dist/node/host.js" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "dependencies": { diff --git a/packages/lib/package.json b/packages/lib/package.json index b38ab998..88d3a177 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -46,7 +46,7 @@ "compile_validator": "node scripts/compile_validator.js" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "dependencies": { diff --git a/packages/web_ui/package.json b/packages/web_ui/package.json index 13c6551d..3aa99c7a 100644 --- a/packages/web_ui/package.json +++ b/packages/web_ui/package.json @@ -29,7 +29,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "dependencies": { diff --git a/plugins/global_chat/package.json b/plugins/global_chat/package.json index eda028c1..fb58ed99 100644 --- a/plugins/global_chat/package.json +++ b/plugins/global_chat/package.json @@ -30,7 +30,7 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "peerDependencies": { diff --git a/plugins/player_auth/package.json b/plugins/player_auth/package.json index e3bde80e..24028e8f 100644 --- a/plugins/player_auth/package.json +++ b/plugins/player_auth/package.json @@ -30,7 +30,7 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "peerDependencies": { diff --git a/plugins/player_sync/package.json b/plugins/player_sync/package.json index 6836b0ce..68bcdbc4 100644 --- a/plugins/player_sync/package.json +++ b/plugins/player_sync/package.json @@ -30,7 +30,7 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "peerDependencies": { diff --git a/plugins/research_sync/package.json b/plugins/research_sync/package.json index 19df29e0..c409a5f5 100644 --- a/plugins/research_sync/package.json +++ b/plugins/research_sync/package.json @@ -30,7 +30,7 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "peerDependencies": { diff --git a/plugins/statistics_exporter/package.json b/plugins/statistics_exporter/package.json index b46f9958..0f9d78ad 100644 --- a/plugins/statistics_exporter/package.json +++ b/plugins/statistics_exporter/package.json @@ -30,7 +30,7 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "peerDependencies": { diff --git a/plugins/subspace_storage/package.json b/plugins/subspace_storage/package.json index ee5f47d5..2b3e5de6 100644 --- a/plugins/subspace_storage/package.json +++ b/plugins/subspace_storage/package.json @@ -30,7 +30,7 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "engines": { - "node": ">=22" + "node": "^22.18.0 || >=24.2.0" }, "peerDependencies": {