From 49a634c65f1b0fbec4ba5d484e119b15218c4858 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sun, 28 Jun 2026 17:56:51 +0200 Subject: [PATCH 001/125] feat(mqtt): enhance publish and subscribe methods with QoS options --- .../coordinator-service/src/infrastructure/mqtt/consumer.ts | 4 ++-- microservices/coordinator-service/src/main.ts | 4 ++-- microservices/machine-simulator-service/src/main.ts | 5 +++-- microservices/machine-simulator-service/src/mqtt/client.ts | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/microservices/coordinator-service/src/infrastructure/mqtt/consumer.ts b/microservices/coordinator-service/src/infrastructure/mqtt/consumer.ts index caa82e96..6641824b 100644 --- a/microservices/coordinator-service/src/infrastructure/mqtt/consumer.ts +++ b/microservices/coordinator-service/src/infrastructure/mqtt/consumer.ts @@ -5,8 +5,8 @@ export function createMqttConsumer(url: string, logger: Logger) { const base = createMqttClient(url, logger) return { - subscribe(topic: string) { - base.client.subscribe(topic, (err) => { + subscribe(topic: string, options?: { qos: 0 | 1 | 2 }) { + base.client.subscribe(topic, options, (err) => { if (err) { logger.error('subscribe failed', { topic, err }) return diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index a4cfdd6a..6f301769 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -71,8 +71,8 @@ function bootstrap() { logger ) - mqtt.subscribe(`${config.mqtt.topic}/+`) - mqtt.subscribe(`${config.mqtt.heartbeatTopic}/+`) + mqtt.subscribe(`${config.mqtt.topic}/+`, { qos: 1 }) + mqtt.subscribe(`${config.mqtt.heartbeatTopic}/+`, { qos: 0 }) mqtt.onMessage((topic, message) => { consumer.handle(topic, message) diff --git a/microservices/machine-simulator-service/src/main.ts b/microservices/machine-simulator-service/src/main.ts index 428b366c..1f2ca387 100644 --- a/microservices/machine-simulator-service/src/main.ts +++ b/microservices/machine-simulator-service/src/main.ts @@ -16,7 +16,7 @@ const telemetryInterval = setInterval(() => { const event = generateTelemetry(config.telemetry, clock) const topic = `${config.mqtt.topic}/${config.telemetry.machineId}` - mqtt.publish(topic, JSON.stringify(event)) + mqtt.publish(topic, JSON.stringify(event), { qos: 1 }) logger.info('published', { event }) }, config.intervalMs) @@ -25,7 +25,8 @@ const heartbeatInterval = setInterval(() => { const heartbeatTopic = `${config.mqtt.heartbeatTopic}/${config.telemetry.machineId}` mqtt.publish( heartbeatTopic, - JSON.stringify({ timestamp: clock.now().toISOString() }) + JSON.stringify({ timestamp: clock.now().toISOString() }), + { qos: 0 } ) }, config.heartbeatIntervalMs) diff --git a/microservices/machine-simulator-service/src/mqtt/client.ts b/microservices/machine-simulator-service/src/mqtt/client.ts index dbb37879..0a77f5e9 100644 --- a/microservices/machine-simulator-service/src/mqtt/client.ts +++ b/microservices/machine-simulator-service/src/mqtt/client.ts @@ -9,7 +9,7 @@ export function createMqttClient(url: string, logger: PinoLogger) { }) client.on('reconnect', () => { - logger.info(`simulator reconnecting...`) + logger.info(`reconnecting...`) }) client.on('error', (err) => { @@ -17,9 +17,9 @@ export function createMqttClient(url: string, logger: PinoLogger) { }) return { - publish(topic: string, message: string) { + publish(topic: string, message: string, options?: { qos: 0 | 1 | 2 }) { logger.info(`published message to topic ${topic}`, { message }) - client.publish(topic, message) + client.publish(topic, message, options) }, close(): Promise { From 74b8c09c108d66f21857a70cfcb45fa25509aa9a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 22:06:05 +0200 Subject: [PATCH 002/125] feat(deps): add better-sqlite3 and its types to dependencies --- .../package-lock.json | 321 +++++++++++++++++- .../machine-simulator-service/package.json | 9 +- 2 files changed, 327 insertions(+), 3 deletions(-) diff --git a/microservices/machine-simulator-service/package-lock.json b/microservices/machine-simulator-service/package-lock.json index 28509191..f2993a59 100644 --- a/microservices/machine-simulator-service/package-lock.json +++ b/microservices/machine-simulator-service/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "mqtt": "^5.15.2", "pino": "^10.3.1", @@ -16,6 +17,7 @@ "tsx": "^4.23.0" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "^26.1.0", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.6.0", @@ -1062,6 +1064,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1615,6 +1627,29 @@ ], "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", @@ -1781,6 +1816,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1792,7 +1851,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2063,6 +2121,15 @@ "node": ">=0.8.x" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2150,6 +2217,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2188,6 +2261,12 @@ "dev": true, "license": "ISC" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2202,6 +2281,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2284,6 +2369,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -2754,6 +2845,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2779,6 +2882,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mqtt": { "version": "5.15.2", "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.2.tgz", @@ -2847,6 +2956,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2854,6 +2969,18 @@ "dev": true, "license": "MIT" }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/number-allocator": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", @@ -3083,6 +3210,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3166,6 +3320,30 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -3280,7 +3458,6 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3319,6 +3496,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -3419,6 +3641,89 @@ "node": ">=8" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -3518,6 +3823,18 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/microservices/machine-simulator-service/package.json b/microservices/machine-simulator-service/package.json index ef8b8de6..aedb920d 100644 --- a/microservices/machine-simulator-service/package.json +++ b/microservices/machine-simulator-service/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "description": "Machine sensor data simulator.", "scripts": { + "start": "tsx src/main.ts", "lint": "eslint \"src/**/*.ts\"", "lint:fix": "eslint \"src/**/*.ts\" --fix", "format:check": "prettier --check \"{src,test}/**/*.ts\"", @@ -27,6 +28,7 @@ }, "homepage": "https://github.com/Factory-Manager/factory-manager#readme", "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "^26.1.0", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.6.0", @@ -36,10 +38,15 @@ "vitest": "^4.1.10" }, "dependencies": { + "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "mqtt": "^5.15.2", "pino": "^10.3.1", "pino-pretty": "^13.1.3", "tsx": "^4.23.0" + }, + "allowScripts": { + "esbuild@0.28.1": true, + "better-sqlite3@12.11.1": true } -} \ No newline at end of file +} From e40596c0826e696962cbd247f69b024fbf46e84b Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 22:07:26 +0200 Subject: [PATCH 003/125] feat(outbox): add OutboxStatus enum and OutboxMessage type --- .../src/messaging/outbox/outbox-message.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 microservices/machine-simulator-service/src/messaging/outbox/outbox-message.ts diff --git a/microservices/machine-simulator-service/src/messaging/outbox/outbox-message.ts b/microservices/machine-simulator-service/src/messaging/outbox/outbox-message.ts new file mode 100644 index 00000000..bd9d5a18 --- /dev/null +++ b/microservices/machine-simulator-service/src/messaging/outbox/outbox-message.ts @@ -0,0 +1,14 @@ +export enum OutboxStatus { + PENDING = 'PENDING', + SENT = 'SENT', + FAILED = 'FAILED' +} + +export type OutboxMessage = { + eventId: string + topic: string + payload: string + status: OutboxStatus + createdAt: Date + attempts: number +} From 4559b86682967b43e1a2dfe23caae586623c956a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 22:08:09 +0200 Subject: [PATCH 004/125] feat(outbox): create OutboxRepository interface for saving messages --- .../src/messaging/outbox/outbox.repository.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 microservices/machine-simulator-service/src/messaging/outbox/outbox.repository.ts diff --git a/microservices/machine-simulator-service/src/messaging/outbox/outbox.repository.ts b/microservices/machine-simulator-service/src/messaging/outbox/outbox.repository.ts new file mode 100644 index 00000000..d2795238 --- /dev/null +++ b/microservices/machine-simulator-service/src/messaging/outbox/outbox.repository.ts @@ -0,0 +1,5 @@ +import { OutboxMessage } from './outbox-message' + +export interface OutboxRepository { + save(message: OutboxMessage): void +} From beaa4f9d0bbac2fa09a77ff7581a05ac3bec8b35 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 23:04:32 +0200 Subject: [PATCH 005/125] feat(outbox): implement createSqliteDatabase function for outbox persistence --- .../sqlite/create-sqlite-database.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts diff --git a/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts b/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts new file mode 100644 index 00000000..abf0de5c --- /dev/null +++ b/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts @@ -0,0 +1,20 @@ +import fs from 'node:fs' +import Database from 'better-sqlite3' +import path from 'path/win32' + +export function createSqliteDatabase(outboxDbPath: string): Database.Database { + const dataDir = path.resolve('data') + fs.mkdirSync(dataDir, { recursive: true }) + const dbPath = path.resolve(process.cwd(), outboxDbPath) + const db = new Database(dbPath) + db.exec(` + CREATE TABLE IF NOT EXISTS machine_simulator_outbox ( + event_id TEXT PRIMARY KEY, + topic TEXT NOT NULL, + payload TEXT NOT NULL, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + attempts INTEGER NOT NULL + )`) + return db +} From 9b02c2e83f5c1d73f9ef426e8e2c5db05bc52309 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 23:07:27 +0200 Subject: [PATCH 006/125] feat(outbox): introduce SqliteOutboxRepository for message persistence --- .../sqlite/sqlite-outbox.repository.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/sqlite-outbox.repository.ts diff --git a/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/sqlite-outbox.repository.ts b/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/sqlite-outbox.repository.ts new file mode 100644 index 00000000..12fb9d52 --- /dev/null +++ b/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/sqlite-outbox.repository.ts @@ -0,0 +1,30 @@ +import { Database } from 'better-sqlite3' +import { OutboxRepository } from '../../../messaging/outbox/outbox.repository' +import { OutboxMessage } from '../../../messaging/outbox/outbox-message' + +export class SqliteOutboxRepository implements OutboxRepository { + constructor(private readonly db: Database) {} + + save(message: OutboxMessage): void { + const query = ` + INSERT INTO machine_simulator_outbox ( + event_id, + topic, + payload, + status, + created_at, + attempts + ) + VALUES (?, ?, ?, ?, ?, ?) + ` + const params = [ + message.eventId, + message.topic, + message.payload, + message.status.toString(), + message.createdAt.toISOString(), + message.attempts + ] + this.db.prepare(query).run(params) + } +} From 639aa78816a55338ab4591aebb38346350048b40 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 23:10:16 +0200 Subject: [PATCH 007/125] feat(config): add outbox database path to AppConfig for configuration --- microservices/machine-simulator-service/src/config/env.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/microservices/machine-simulator-service/src/config/env.ts b/microservices/machine-simulator-service/src/config/env.ts index f7726a64..47a0c4ad 100644 --- a/microservices/machine-simulator-service/src/config/env.ts +++ b/microservices/machine-simulator-service/src/config/env.ts @@ -11,6 +11,7 @@ export type AppConfig = { } intervalMs: number heartbeatIntervalMs: number + outboxDbPath: string telemetry: TelemetryConfig } @@ -23,7 +24,7 @@ export function getConfig(): AppConfig { }, intervalMs: Number(process.env.INTERVAL_MS!), heartbeatIntervalMs: Number(process.env.HEARTBEAT_INTERVAL_MS!), - + outboxDbPath: process.env.OUTBOX_DB_PATH!, telemetry: { machineId: process.env.MACHINE_ID!, From e53393e4b284a62f5055e6efd866130032e839aa Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 23:11:55 +0200 Subject: [PATCH 008/125] feat(machine-simulator-service): integrate outbox message persistence for telemetry events --- .../machine-simulator-service/src/main.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/microservices/machine-simulator-service/src/main.ts b/microservices/machine-simulator-service/src/main.ts index 1f2ca387..2cc140dd 100644 --- a/microservices/machine-simulator-service/src/main.ts +++ b/microservices/machine-simulator-service/src/main.ts @@ -1,22 +1,40 @@ +import { randomUUID } from 'crypto' import { getConfig } from './config/env' import { generateTelemetry } from './generator/telemetry-generator' import { PinoLogger } from './infrastructure/logger/pino-logger' +import { createSqliteDatabase } from './infrastructure/persistence/sqlite/create-sqlite-database' +import { SqliteOutboxRepository } from './infrastructure/persistence/sqlite/sqlite-outbox.repository' import { SystemClock } from './infrastructure/time/system-clock' import { createMqttClient } from './mqtt/client' +import { OutboxMessage, OutboxStatus } from './messaging/outbox/outbox-message' +import { TelemetryEvent } from './types/telemetry-event' const config = getConfig() const logger = new PinoLogger() const clock = new SystemClock() const mqtt = createMqttClient(config.mqtt.url, logger) +const db = createSqliteDatabase(config.outboxDbPath) +const outbox = new SqliteOutboxRepository(db) logger.info('simulator started') const telemetryInterval = setInterval(() => { - const event = generateTelemetry(config.telemetry, clock) + const event: TelemetryEvent = generateTelemetry(config.telemetry, clock) const topic = `${config.mqtt.topic}/${config.telemetry.machineId}` - mqtt.publish(topic, JSON.stringify(event), { qos: 1 }) + const outboxMessage: OutboxMessage = { + eventId: randomUUID(), + topic, + payload: JSON.stringify(event), + status: OutboxStatus.PENDING, + createdAt: clock.now(), + attempts: 0 + } + + outbox.save(outboxMessage) + + mqtt.publish(topic, outboxMessage.payload, { qos: 1 }) logger.info('published', { event }) }, config.intervalMs) @@ -35,6 +53,7 @@ async function shutdown(signal: string) { clearInterval(telemetryInterval) clearInterval(heartbeatInterval) await mqtt.close() + db.close() process.exit(0) } From 40ee829ed98ad31f1565c5787c4e5bad01627fda Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 23:16:12 +0200 Subject: [PATCH 009/125] feat(docker): replace npm install with npm ci --omit=dev for consistent dependency installation --- microservices/coordinator-service/Dockerfile | 2 +- microservices/machine-simulator-service/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/microservices/coordinator-service/Dockerfile b/microservices/coordinator-service/Dockerfile index c7c18c3b..c07b11f0 100644 --- a/microservices/coordinator-service/Dockerfile +++ b/microservices/coordinator-service/Dockerfile @@ -3,7 +3,7 @@ FROM node:24.18.0-alpine WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm ci --omit=dev COPY . . diff --git a/microservices/machine-simulator-service/Dockerfile b/microservices/machine-simulator-service/Dockerfile index c7c18c3b..c07b11f0 100644 --- a/microservices/machine-simulator-service/Dockerfile +++ b/microservices/machine-simulator-service/Dockerfile @@ -3,7 +3,7 @@ FROM node:24.18.0-alpine WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm ci --omit=dev COPY . . From d188f1aea93c5e931b1ab4d648ed0a135ce643d5 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 23:17:49 +0200 Subject: [PATCH 010/125] chore(gitignore): add database files to .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 49f37235..bb57d64a 100644 --- a/.gitignore +++ b/.gitignore @@ -181,4 +181,6 @@ dist **/docs/ **/html/ -**/coverage/ \ No newline at end of file +**/coverage/ + +**/data/*.db \ No newline at end of file From 385784671be0b1fa11738ebcd97a4c189421798d Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 30 Jun 2026 23:18:08 +0200 Subject: [PATCH 011/125] feat(dockerignore): add node_modules and data directories to .dockerignore --- microservices/machine-simulator-service/.dockerignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 microservices/machine-simulator-service/.dockerignore diff --git a/microservices/machine-simulator-service/.dockerignore b/microservices/machine-simulator-service/.dockerignore new file mode 100644 index 00000000..ff227fe9 --- /dev/null +++ b/microservices/machine-simulator-service/.dockerignore @@ -0,0 +1,2 @@ +node_modules/ +data/ \ No newline at end of file From 4b22a747b2ce44ecc1a8b92f4a063df1291b70f2 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 1 Jul 2026 23:22:48 +0200 Subject: [PATCH 012/125] feat(config): add node environment to AppConfig --- microservices/coordinator-service/src/config/env.ts | 9 +++++++++ .../machine-simulator-service/src/config/env.ts | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/microservices/coordinator-service/src/config/env.ts b/microservices/coordinator-service/src/config/env.ts index 85b674da..3db719db 100644 --- a/microservices/coordinator-service/src/config/env.ts +++ b/microservices/coordinator-service/src/config/env.ts @@ -2,7 +2,15 @@ import dotenv from 'dotenv' dotenv.config() +export type NodeEnv = 'development' | 'production' + +function toNodeEnv(value?: string): NodeEnv { + if (value === 'production') return 'production' + return 'development' +} + export type AppConfig = { + nodeEnv: NodeEnv mqtt: { url: string topic: string @@ -14,6 +22,7 @@ export type AppConfig = { export function getConfig(): AppConfig { return { + nodeEnv: toNodeEnv(process.env.NODE_ENV), mqtt: { url: process.env.MQTT_URL!, topic: process.env.MQTT_TOPIC!, diff --git a/microservices/machine-simulator-service/src/config/env.ts b/microservices/machine-simulator-service/src/config/env.ts index 47a0c4ad..82f92276 100644 --- a/microservices/machine-simulator-service/src/config/env.ts +++ b/microservices/machine-simulator-service/src/config/env.ts @@ -3,7 +3,15 @@ import type { TelemetryConfig } from '../types/telemetry-config' dotenv.config() +export type NodeEnv = 'development' | 'production' + +function toNodeEnv(value?: string): NodeEnv { + if (value === 'production') return 'production' + return 'development' +} + export type AppConfig = { + nodeEnv: NodeEnv mqtt: { url: string topic: string @@ -17,6 +25,7 @@ export type AppConfig = { export function getConfig(): AppConfig { return { + nodeEnv: toNodeEnv(process.env.NODE_ENV), mqtt: { url: process.env.MQTT_URL!, topic: process.env.MQTT_TOPIC!, From 43a17c44c6ed3405193e31d40aeebe7f3a1219f8 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 1 Jul 2026 23:25:14 +0200 Subject: [PATCH 013/125] chore(deps): move pino-pretty to devDependencies for better dependency management --- .../machine-simulator-service/package-lock.json | 10 +++++++++- microservices/machine-simulator-service/package.json | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/microservices/machine-simulator-service/package-lock.json b/microservices/machine-simulator-service/package-lock.json index f2993a59..cde1a2fc 100644 --- a/microservices/machine-simulator-service/package-lock.json +++ b/microservices/machine-simulator-service/package-lock.json @@ -13,7 +13,6 @@ "dotenv": "^17.4.2", "mqtt": "^5.15.2", "pino": "^10.3.1", - "pino-pretty": "^13.1.3", "tsx": "^4.23.0" }, "devDependencies": { @@ -21,6 +20,7 @@ "@types/node": "^26.1.0", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.6.0", + "pino-pretty": "^13.1.3", "prettier": "^3.9.4", "typescript": "^6.0.3", "typescript-eslint": "^8.62.1", @@ -1731,6 +1731,7 @@ "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, "license": "MIT" }, "node_modules/commist": { @@ -1794,6 +1795,7 @@ "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -2144,6 +2146,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz", "integrity": "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==", + "dev": true, "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -2171,6 +2174,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, "license": "MIT" }, "node_modules/fast-unique-numbers": { @@ -2457,6 +2461,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -3155,6 +3160,7 @@ "version": "13.1.3", "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.7", @@ -3442,6 +3448,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, "funding": [ { "type": "github", @@ -3620,6 +3627,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14.16" diff --git a/microservices/machine-simulator-service/package.json b/microservices/machine-simulator-service/package.json index aedb920d..a52b207e 100644 --- a/microservices/machine-simulator-service/package.json +++ b/microservices/machine-simulator-service/package.json @@ -32,6 +32,7 @@ "@types/node": "^26.1.0", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.6.0", + "pino-pretty": "^13.1.3", "prettier": "^3.9.4", "typescript": "^6.0.3", "typescript-eslint": "^8.62.1", @@ -42,7 +43,6 @@ "dotenv": "^17.4.2", "mqtt": "^5.15.2", "pino": "^10.3.1", - "pino-pretty": "^13.1.3", "tsx": "^4.23.0" }, "allowScripts": { From bf549419561610ef54710748c04fd6abedc4b014 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 1 Jul 2026 23:28:06 +0200 Subject: [PATCH 014/125] refactor(logger): update PinoLogger with check on NodeEnv modality --- .../src/infrastructure/logger/pino-logger.ts | 34 +++++++++++-------- .../src/infrastructure/logger/pino-logger.ts | 30 +++++++++------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts b/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts index 168df271..4bab20a0 100644 --- a/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts +++ b/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts @@ -1,22 +1,28 @@ import pino from 'pino' import { Logger } from '@/application/ports/logger' +import { NodeEnv } from '@/config/env' export class PinoLogger implements Logger { - constructor( - private logger = pino({ - transport: { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: 'SYS:standard', - ignore: 'pid,hostname' - } - } - }) - ) {} + private readonly logger: pino.Logger - child(context: object): Logger { - return new PinoLogger(this.logger.child(context)) + constructor(nodeEnv: NodeEnv) { + this.logger = + nodeEnv === 'development' + ? pino({ + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname' + } + } + }) + : pino() + } + + child(context: object): pino.Logger { + return this.logger.child(context) } info(message: string, metadata?: object): void { this.logger.info(metadata ?? {}, message) diff --git a/microservices/machine-simulator-service/src/infrastructure/logger/pino-logger.ts b/microservices/machine-simulator-service/src/infrastructure/logger/pino-logger.ts index 05092e11..6059f1f8 100644 --- a/microservices/machine-simulator-service/src/infrastructure/logger/pino-logger.ts +++ b/microservices/machine-simulator-service/src/infrastructure/logger/pino-logger.ts @@ -1,18 +1,24 @@ import pino from 'pino' +import { NodeEnv } from '../../config/env' export class PinoLogger { - constructor( - private logger = pino({ - transport: { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: 'SYS:standard', - ignore: 'pid,hostname' - } - } - }) - ) {} + private readonly logger: pino.Logger + + constructor(nodeEnv: NodeEnv) { + this.logger = + nodeEnv === 'development' + ? pino({ + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname' + } + } + }) + : pino() + } info(message: string, metadata?: object): void { this.logger.info(metadata ?? {}, message) From 008bcf06c901d29f3da8c80adef2541d606df87a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 1 Jul 2026 23:30:11 +0200 Subject: [PATCH 015/125] feat(logger): initialize PinoLogger with node environment configuration --- microservices/coordinator-service/src/main.ts | 4 ++-- microservices/machine-simulator-service/src/main.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 6f301769..83d31d1c 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -33,7 +33,8 @@ function buildMachineConfig(): MachineConfig { } function bootstrap() { - const baseLogger = new PinoLogger() + const config = getConfig() + const baseLogger = new PinoLogger(config.nodeEnv) const logger = baseLogger.child({ service: 'coordinator-service' }) @@ -41,7 +42,6 @@ function bootstrap() { service: 'mqtt' }) - const config = getConfig() const mqtt = createMqttConsumer(config.mqtt.url, mqttLogger) const policies = [new TemperaturePolicy()] diff --git a/microservices/machine-simulator-service/src/main.ts b/microservices/machine-simulator-service/src/main.ts index 2cc140dd..f95dee0d 100644 --- a/microservices/machine-simulator-service/src/main.ts +++ b/microservices/machine-simulator-service/src/main.ts @@ -10,7 +10,7 @@ import { OutboxMessage, OutboxStatus } from './messaging/outbox/outbox-message' import { TelemetryEvent } from './types/telemetry-event' const config = getConfig() -const logger = new PinoLogger() +const logger = new PinoLogger(config.nodeEnv) const clock = new SystemClock() const mqtt = createMqttClient(config.mqtt.url, logger) From c830d66ba363ee58ea58ed99c373925be345d52e Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 1 Jul 2026 23:31:50 +0200 Subject: [PATCH 016/125] chore(docker): remove --omit=dev from npm ci to ensure all dependencies are installed --- microservices/coordinator-service/Dockerfile | 2 +- microservices/machine-simulator-service/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/microservices/coordinator-service/Dockerfile b/microservices/coordinator-service/Dockerfile index c07b11f0..253ee344 100644 --- a/microservices/coordinator-service/Dockerfile +++ b/microservices/coordinator-service/Dockerfile @@ -3,7 +3,7 @@ FROM node:24.18.0-alpine WORKDIR /app COPY package*.json ./ -RUN npm ci --omit=dev +RUN npm ci COPY . . diff --git a/microservices/machine-simulator-service/Dockerfile b/microservices/machine-simulator-service/Dockerfile index c07b11f0..253ee344 100644 --- a/microservices/machine-simulator-service/Dockerfile +++ b/microservices/machine-simulator-service/Dockerfile @@ -3,7 +3,7 @@ FROM node:24.18.0-alpine WORKDIR /app COPY package*.json ./ -RUN npm ci --omit=dev +RUN npm ci COPY . . From a9769cd4c94f00fd4ea8fee9d02e9f38dee18e66 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 1 Jul 2026 23:33:46 +0200 Subject: [PATCH 017/125] feat(docker): add NODE_ENV environment variable to docker compose simulators --- docker/docker-compose.simulators.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/docker-compose.simulators.yml b/docker/docker-compose.simulators.yml index 5b0f2b75..6659cd29 100644 --- a/docker/docker-compose.simulators.yml +++ b/docker/docker-compose.simulators.yml @@ -12,6 +12,7 @@ services: depends_on: - mqtt-broker environment: + NODE_ENV: development MQTT_URL: mqtt://mqtt-broker:1883 MQTT_TOPIC: factory/telemetry INTERVAL_MS: 2000 @@ -24,6 +25,7 @@ services: depends_on: - mqtt-broker environment: + NODE_ENV: development MACHINE_ID: machine-1 MQTT_URL: mqtt://mqtt-broker:1883 MQTT_TOPIC: factory/telemetry From 4f3873103aa59a92a94591d93a71bd90e986c294 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 1 Jul 2026 23:34:49 +0200 Subject: [PATCH 018/125] feat(docker): add .dockerignore to exclude node_modules and data directories --- microservices/coordinator-service/.dockerignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 microservices/coordinator-service/.dockerignore diff --git a/microservices/coordinator-service/.dockerignore b/microservices/coordinator-service/.dockerignore new file mode 100644 index 00000000..ff227fe9 --- /dev/null +++ b/microservices/coordinator-service/.dockerignore @@ -0,0 +1,2 @@ +node_modules/ +data/ \ No newline at end of file From 515f5ab9926d0d6bbb07a38e51ece3ceef8d3974 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 1 Jul 2026 23:37:47 +0200 Subject: [PATCH 019/125] feat(mqtt): refactor publish method to return a promise for improve error handling --- .../src/mqtt/client.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/microservices/machine-simulator-service/src/mqtt/client.ts b/microservices/machine-simulator-service/src/mqtt/client.ts index 0a77f5e9..564cd434 100644 --- a/microservices/machine-simulator-service/src/mqtt/client.ts +++ b/microservices/machine-simulator-service/src/mqtt/client.ts @@ -17,9 +17,24 @@ export function createMqttClient(url: string, logger: PinoLogger) { }) return { - publish(topic: string, message: string, options?: { qos: 0 | 1 | 2 }) { - logger.info(`published message to topic ${topic}`, { message }) - client.publish(topic, message, options) + publish( + topic: string, + message: string, + options?: { qos: 0 | 1 | 2 } + ): Promise { + logger.info(`publishing message to topic ${topic}`, { message }) + + return new Promise((resolve, reject) => { + client.publish(topic, message, options ?? {}, (err) => { + if (err) { + logger.error('publish failed', { err }) + return reject(err) + } + + logger.info(`published message to topic ${topic}`) + resolve() + }) + }) }, close(): Promise { From 27dd448b3c56c2dc3fe1e16b510e6b0a9bfe7c56 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 1 Jul 2026 23:38:49 +0200 Subject: [PATCH 020/125] feat(mqtt): update telemetry setInterval to handle publish errors with async/await --- microservices/machine-simulator-service/src/main.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/microservices/machine-simulator-service/src/main.ts b/microservices/machine-simulator-service/src/main.ts index f95dee0d..16c4123d 100644 --- a/microservices/machine-simulator-service/src/main.ts +++ b/microservices/machine-simulator-service/src/main.ts @@ -19,7 +19,7 @@ const outbox = new SqliteOutboxRepository(db) logger.info('simulator started') -const telemetryInterval = setInterval(() => { +const telemetryInterval = setInterval(async () => { const event: TelemetryEvent = generateTelemetry(config.telemetry, clock) const topic = `${config.mqtt.topic}/${config.telemetry.machineId}` @@ -34,8 +34,12 @@ const telemetryInterval = setInterval(() => { outbox.save(outboxMessage) - mqtt.publish(topic, outboxMessage.payload, { qos: 1 }) - logger.info('published', { event }) + try { + await mqtt.publish(topic, outboxMessage.payload, { qos: 1 }) + logger.info('published', { event }) + } catch (err) { + logger.error('failed to publish', { err, event }) + } }, config.intervalMs) const heartbeatInterval = setInterval(() => { From ba7776ee49277808450d264edcc37f45a3ac4d24 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 3 Jul 2026 17:32:05 +0200 Subject: [PATCH 021/125] feat(outbox): add updateStatus to OutboxRepository interface --- .../src/messaging/outbox/outbox.repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/microservices/machine-simulator-service/src/messaging/outbox/outbox.repository.ts b/microservices/machine-simulator-service/src/messaging/outbox/outbox.repository.ts index d2795238..9d68db85 100644 --- a/microservices/machine-simulator-service/src/messaging/outbox/outbox.repository.ts +++ b/microservices/machine-simulator-service/src/messaging/outbox/outbox.repository.ts @@ -2,4 +2,5 @@ import { OutboxMessage } from './outbox-message' export interface OutboxRepository { save(message: OutboxMessage): void + updateStatus(eventId: string, status: string, attempts: number): void } From 828b6f4723a501e2d019f64498bcab8316952d25 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 3 Jul 2026 17:32:47 +0200 Subject: [PATCH 022/125] feat(outbox): implement updateStatus method in SqliteOutboxRepository --- .../persistence/sqlite/sqlite-outbox.repository.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/sqlite-outbox.repository.ts b/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/sqlite-outbox.repository.ts index 12fb9d52..bbc55dea 100644 --- a/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/sqlite-outbox.repository.ts +++ b/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/sqlite-outbox.repository.ts @@ -11,7 +11,7 @@ export class SqliteOutboxRepository implements OutboxRepository { event_id, topic, payload, - status, + status, created_at, attempts ) @@ -27,4 +27,14 @@ export class SqliteOutboxRepository implements OutboxRepository { ] this.db.prepare(query).run(params) } + + updateStatus(eventId: string, status: string, attempts: number): void { + const query = ` + UPDATE machine_simulator_outbox + SET status = ?, attempts = ? + WHERE event_id = ? + ` + const params = [status, attempts, eventId] + this.db.prepare(query).run(params) + } } From 842fb74fc9045add223f488141e1f82635373dc6 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 3 Jul 2026 17:35:50 +0200 Subject: [PATCH 023/125] feat(mqtt): enhance error handling by updating outbox status on publish success and failure and incrementing the number of attempts --- microservices/machine-simulator-service/src/main.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/microservices/machine-simulator-service/src/main.ts b/microservices/machine-simulator-service/src/main.ts index 16c4123d..f37aa053 100644 --- a/microservices/machine-simulator-service/src/main.ts +++ b/microservices/machine-simulator-service/src/main.ts @@ -36,8 +36,18 @@ const telemetryInterval = setInterval(async () => { try { await mqtt.publish(topic, outboxMessage.payload, { qos: 1 }) + outbox.updateStatus( + outboxMessage.eventId, + OutboxStatus.SENT, + outboxMessage.attempts + 1 + ) logger.info('published', { event }) } catch (err) { + outbox.updateStatus( + outboxMessage.eventId, + OutboxStatus.FAILED, + outboxMessage.attempts + 1 + ) logger.error('failed to publish', { err, event }) } }, config.intervalMs) From aed6eabef6c0e89d904e183a529c5ce30e7b7ac7 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 4 Jul 2026 13:13:41 +0200 Subject: [PATCH 024/125] feat(deps): add better-sqlite3 and its types to package.json --- .../coordinator-service/package-lock.json | 325 +++++++++++++++++- .../coordinator-service/package.json | 5 +- 2 files changed, 323 insertions(+), 7 deletions(-) diff --git a/microservices/coordinator-service/package-lock.json b/microservices/coordinator-service/package-lock.json index 01435d8e..c7a70c9d 100644 --- a/microservices/coordinator-service/package-lock.json +++ b/microservices/coordinator-service/package-lock.json @@ -9,11 +9,13 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "mqtt": "^5.15.2", "pino": "^10.3.1" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "^25.9.4", "@vitest/coverage-v8": "^4.1.10", "@vitest/ui": "^4.1.10", @@ -1170,6 +1172,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1774,6 +1786,29 @@ ], "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", @@ -1851,6 +1886,12 @@ "node": ">=18" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -1943,6 +1984,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1955,7 +2020,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1977,7 +2041,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -2249,6 +2312,15 @@ "node": ">=0.8.x" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2349,6 +2421,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2389,6 +2467,12 @@ "dev": true, "license": "ISC" }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2404,6 +2488,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -2507,6 +2597,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -3053,6 +3149,18 @@ "dev": true, "license": "MIT" }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -3088,6 +3196,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mqtt": { "version": "5.15.2", "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.2.tgz", @@ -3172,6 +3286,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3179,6 +3299,18 @@ "dev": true, "license": "MIT" }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/number-allocator": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", @@ -3216,7 +3348,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3439,6 +3570,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3501,7 +3659,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -3535,6 +3692,30 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -3670,7 +3851,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3711,6 +3891,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -3827,6 +4052,83 @@ "node": ">=8" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -3937,6 +4239,18 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4313,7 +4627,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { diff --git a/microservices/coordinator-service/package.json b/microservices/coordinator-service/package.json index 6f416106..e1b4c20e 100644 --- a/microservices/coordinator-service/package.json +++ b/microservices/coordinator-service/package.json @@ -36,6 +36,7 @@ }, "homepage": "https://github.com/Factory-Manager/factory-manager#readme", "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/node": "^25.9.4", "@vitest/coverage-v8": "^4.1.10", "@vitest/ui": "^4.1.10", @@ -49,11 +50,13 @@ "vitest": "^4.1.10" }, "dependencies": { + "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "mqtt": "^5.15.2", "pino": "^10.3.1" }, "allowScripts": { - "esbuild@0.28.1": true + "esbuild@0.28.1": true, + "better-sqlite3@12.11.1": true } } From c622d9b8f64a870ea935079e8c5e90863d2e8d3f Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 4 Jul 2026 13:14:39 +0200 Subject: [PATCH 025/125] feat(inbox): add InboxStatus enum and InboxMessage type for message handling --- .../persistence/sqlite/models/inbox-message.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts new file mode 100644 index 00000000..22e8b8c9 --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts @@ -0,0 +1,14 @@ +export enum InboxStatus { + PENDING = 'PENDING', + PROCESSED = 'PROCESSED', + FAILED = 'FAILED' +} + +export type InboxMessage = { + eventId: string + topic: string + payload: string + status: InboxStatus + receivedAt: Date + processedAt: Date | null +} From 9a4d245f69ab5028340ac5061afcff07e4e81b05 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 4 Jul 2026 13:15:16 +0200 Subject: [PATCH 026/125] feat(inbox): create InboxRepository interface for saving messages --- .../src/application/ports/inbox.repository.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 microservices/coordinator-service/src/application/ports/inbox.repository.ts diff --git a/microservices/coordinator-service/src/application/ports/inbox.repository.ts b/microservices/coordinator-service/src/application/ports/inbox.repository.ts new file mode 100644 index 00000000..9635b234 --- /dev/null +++ b/microservices/coordinator-service/src/application/ports/inbox.repository.ts @@ -0,0 +1,5 @@ +import { InboxMessage } from '../../infrastructure/persistence/sqlite/models/inbox-message' + +export interface InboxRepository { + save(message: InboxMessage): void +} From be18501da9567d659388ebf35a8fe057246dcbac Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 4 Jul 2026 13:20:08 +0200 Subject: [PATCH 027/125] feat(outbox): implement createSqliteDatabase to initialize SQLite database and inbox table --- .../sqlite/create-sqlite-database.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts new file mode 100644 index 00000000..5e415b9d --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts @@ -0,0 +1,20 @@ +import fs from 'node:fs' +import Database from 'better-sqlite3' +import path from 'path/win32' + +export function createSqliteDatabase(inboxDbPath: string): Database.Database { + const dataDir = path.resolve('data') + fs.mkdirSync(dataDir, { recursive: true }) + const dbPath = path.resolve(process.cwd(), inboxDbPath) + const db = new Database(dbPath) + db.exec(` + CREATE TABLE IF NOT EXISTS coordinator_inbox ( + event_id TEXT PRIMARY KEY, + topic TEXT NOT NULL, + payload TEXT NOT NULL, + status TEXT NOT NULL, + received_at TEXT NOT NULL, + processed_at TEXT + )`) + return db +} From 99baf3f5d10111af1751c7d315e0582318f6922b Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 4 Jul 2026 13:20:35 +0200 Subject: [PATCH 028/125] feat(inbox): implement SqliteInboxRepository --- .../sqlite/sqlite-inbox.repository.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts new file mode 100644 index 00000000..597a0ce4 --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts @@ -0,0 +1,29 @@ +import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { InboxRepository } from '@/application/ports/inbox.repository' +import { Database } from 'better-sqlite3' + +export class SqliteInboxRepository implements InboxRepository { + constructor(private readonly db: Database) {} + save(message: InboxMessage): void { + const query = ` + INSERT OR IGNORE INTO coordinator_inbox ( + event_id, + topic, + payload, + status, + received_at, + processed_at + ) + VALUES (?, ?, ?, ?, ?, ?) + ` + const params = [ + message.eventId, + message.topic, + message.payload, + message.status, + message.receivedAt.toISOString(), + message.processedAt ? message.processedAt.toISOString() : null + ] + this.db.prepare(query).run(params) + } +} From f9bed9e3788e189d81d3d164d26be77dc8cf9a40 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 4 Jul 2026 13:21:49 +0200 Subject: [PATCH 029/125] feat(config): add inboxDbPath to env configuration --- microservices/coordinator-service/src/config/env.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/config/env.ts b/microservices/coordinator-service/src/config/env.ts index 3db719db..812464c5 100644 --- a/microservices/coordinator-service/src/config/env.ts +++ b/microservices/coordinator-service/src/config/env.ts @@ -18,6 +18,7 @@ export type AppConfig = { } intervalMs: number heartbeatIntervalMs: number + inboxDbPath: string } export function getConfig(): AppConfig { @@ -29,6 +30,7 @@ export function getConfig(): AppConfig { heartbeatTopic: process.env.MQTT_HEARTBEAT_TOPIC! }, intervalMs: Number(process.env.INTERVAL_MS), - heartbeatIntervalMs: Number(process.env.HEARTBEAT_INTERVAL_MS) + heartbeatIntervalMs: Number(process.env.HEARTBEAT_INTERVAL_MS), + inboxDbPath: process.env.INBOX_DB_PATH! } } From c917f3fca6abc49c3c831fe29e66284ed823e635 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 4 Jul 2026 13:33:37 +0200 Subject: [PATCH 030/125] feat(coordinator): refactor MQTT dispatcher with inbox persistence --- .../messaging/coordinator-message-consumer.ts | 21 ----------- .../messaging/mqtt-message-dispatcher.ts | 36 +++++++++++++++++++ microservices/coordinator-service/src/main.ts | 29 ++++++++------- 3 files changed, 52 insertions(+), 34 deletions(-) delete mode 100644 microservices/coordinator-service/src/application/messaging/coordinator-message-consumer.ts create mode 100644 microservices/coordinator-service/src/application/messaging/mqtt-message-dispatcher.ts diff --git a/microservices/coordinator-service/src/application/messaging/coordinator-message-consumer.ts b/microservices/coordinator-service/src/application/messaging/coordinator-message-consumer.ts deleted file mode 100644 index f7359b22..00000000 --- a/microservices/coordinator-service/src/application/messaging/coordinator-message-consumer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MessageProcessor } from './message-processor' -import { Logger } from '../ports/logger' - -export class CoordinatorMessageConsumer { - constructor( - private readonly processors: MessageProcessor[], - private readonly logger: Logger - ) {} - - handle(topic: string, message: Buffer): void { - try { - const processor = this.processors.find((p) => p.canHandle(topic)) - if (!processor) { - return - } - processor.process(topic, message) - } catch (err) { - this.logger.error('message processing failed', { err, topic }) - } - } -} diff --git a/microservices/coordinator-service/src/application/messaging/mqtt-message-dispatcher.ts b/microservices/coordinator-service/src/application/messaging/mqtt-message-dispatcher.ts new file mode 100644 index 00000000..66516897 --- /dev/null +++ b/microservices/coordinator-service/src/application/messaging/mqtt-message-dispatcher.ts @@ -0,0 +1,36 @@ +import { MessageProcessor } from './message-processor' +import { Logger } from '../ports/logger' +import { InboxRepository } from '../ports/inbox.repository' +import { Clock } from '../ports/clock' +import { randomUUID } from 'crypto' +import { InboxStatus } from '@/infrastructure/persistence/sqlite/models/inbox-message' + +export class MqttMessageDispatcher { + constructor( + private readonly processors: MessageProcessor[], + private readonly inboxRepository: InboxRepository, + private readonly clock: Clock, + private readonly logger: Logger + ) {} + + handle(topic: string, message: Buffer): void { + this.inboxRepository.save({ + eventId: randomUUID(), + topic, + payload: message.toString(), + status: InboxStatus.PENDING, + receivedAt: this.clock.now(), + processedAt: null + }) + + try { + const processor = this.processors.find((p) => p.canHandle(topic)) + if (!processor) { + return + } + processor.process(topic, message) + } catch (err) { + this.logger.error('message processing failed', { err, topic }) + } + } +} diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 83d31d1c..15c28a9e 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -14,10 +14,12 @@ import { createMqttConsumer } from './infrastructure/mqtt/consumer' import { getConfig } from './config/env' import { ProcessHeartbeat } from './application/heartbeat/process-heartbeat' import { TelemetryProcessor } from './application/messaging/processor/telemetry-processor' -import { CoordinatorMessageConsumer } from './application/messaging/coordinator-message-consumer' +import { MqttMessageDispatcher } from './application/messaging/mqtt-message-dispatcher' import { HeartbeatProcessor } from './application/messaging/processor/heartbeat-processor' import { TelemetryMessageMapper } from './application/telemetry/mapper/map-telemetry-message' import { HeartbeatMessageMapper } from './application/heartbeat/mapper/map-heartbeat-message' +import { SqliteInboxRepository } from './infrastructure/persistence/sqlite/sqlite-inbox.repository' +import { createSqliteDatabase } from './infrastructure/persistence/sqlite/create-sqlite-database' function buildMachineConfig(): MachineConfig { return new MachineConfig( @@ -34,27 +36,26 @@ function buildMachineConfig(): MachineConfig { function bootstrap() { const config = getConfig() + const clock = new SystemClock() const baseLogger = new PinoLogger(config.nodeEnv) const logger = baseLogger.child({ service: 'coordinator-service' }) - const mqttLogger = baseLogger.child({ - service: 'mqtt' - }) - const mqtt = createMqttConsumer(config.mqtt.url, mqttLogger) + const mqtt = createMqttConsumer( + config.mqtt.url, + baseLogger.child({ service: 'mqtt' }) + ) + const db = createSqliteDatabase(config.inboxDbPath) + const inboxRepository = new SqliteInboxRepository(db) const policies = [new TemperaturePolicy()] const anomalyDetector = new AnomalyDetector(policies) - const processTelemetry = new ProcessTelemetry( - anomalyDetector, - new SystemClock(), - logger - ) - const processHeartbeat = new ProcessHeartbeat(new SystemClock(), logger) + const processTelemetry = new ProcessTelemetry(anomalyDetector, clock, logger) + const processHeartbeat = new ProcessHeartbeat(clock, logger) const machineConfig = buildMachineConfig() - const consumer = new CoordinatorMessageConsumer( + const dispatcher = new MqttMessageDispatcher( [ new TelemetryProcessor( processTelemetry, @@ -68,6 +69,8 @@ function bootstrap() { config.mqtt.heartbeatTopic ) ], + inboxRepository, + clock, logger ) @@ -75,7 +78,7 @@ function bootstrap() { mqtt.subscribe(`${config.mqtt.heartbeatTopic}/+`, { qos: 0 }) mqtt.onMessage((topic, message) => { - consumer.handle(topic, message) + dispatcher.handle(topic, message) }) } From 42e720daa8dbeeb8704ff4d89b9a3577466e4193 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 6 Jul 2026 23:18:56 +0200 Subject: [PATCH 031/125] feat(inbox): add IncomingMessage type for message handling --- .../src/application/messaging/incoming-message.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 microservices/coordinator-service/src/application/messaging/incoming-message.ts diff --git a/microservices/coordinator-service/src/application/messaging/incoming-message.ts b/microservices/coordinator-service/src/application/messaging/incoming-message.ts new file mode 100644 index 00000000..103c8808 --- /dev/null +++ b/microservices/coordinator-service/src/application/messaging/incoming-message.ts @@ -0,0 +1,6 @@ +export type IncomingMessage = { + id: string + topic: string + payload: Buffer + receivedAt: Date +} From 3e8ada22278076a063ff7f10fbd8379041d78d50 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 6 Jul 2026 23:25:51 +0200 Subject: [PATCH 032/125] refactor(coordinator): update MessageProcessor interface to use IncomingMessage type --- .../src/application/messaging/message-processor.ts | 9 +++++---- .../messaging/processor/heartbeat-processor.ts | 7 ++++--- .../messaging/processor/telemetry-processor.ts | 7 ++++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/microservices/coordinator-service/src/application/messaging/message-processor.ts b/microservices/coordinator-service/src/application/messaging/message-processor.ts index 7459fe24..4181bf20 100644 --- a/microservices/coordinator-service/src/application/messaging/message-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/message-processor.ts @@ -1,3 +1,5 @@ +import { IncomingMessage } from './incoming-message' + /** * Interface for processing messages based on their topic. */ @@ -10,9 +12,8 @@ export interface MessageProcessor { canHandle(topic: string): boolean /** - * Processes the message for the given topic. - * @param topic The topic of the message. - * @param message The message payload as a Buffer. + * Processes the incoming message. + * @param incomingMessage The incoming message to be processed. */ - process(topic: string, message: Buffer): void + process(incomingMessage: IncomingMessage): void } diff --git a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts index 7479a9c2..aed8cf19 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts @@ -2,6 +2,7 @@ import { MessageProcessor } from '@/application/messaging/message-processor' import { ProcessHeartbeat } from '../../heartbeat/process-heartbeat' import { HeartbeatInput } from '../../heartbeat/dto/heartbeat-input' import { HeartbeatMessageMapper } from '../../heartbeat/mapper/map-heartbeat-message' +import { IncomingMessage } from '../incoming-message' export class HeartbeatProcessor implements MessageProcessor { constructor( @@ -17,10 +18,10 @@ export class HeartbeatProcessor implements MessageProcessor { return topic.startsWith(prefix) } - process(topic: string, message: Buffer): void { + process(incomingMessage: IncomingMessage): void { const input: HeartbeatInput = this.heartbeatMessageMapper.map( - topic, - message + incomingMessage.topic, + incomingMessage.payload ) this.processHeartbeat.execute(input) } diff --git a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts index 89b8a56d..0e6c8f06 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts @@ -3,6 +3,7 @@ import { MachineConfig } from '@/domain/machine/machine-config' import { ProcessTelemetry } from '../../telemetry/process-telemetry' import { TelemetryInput } from '../../telemetry/dto/telemetry-input' import { TelemetryMessageMapper } from '../../telemetry/mapper/map-telemetry-message' +import { IncomingMessage } from '../incoming-message' export class TelemetryProcessor implements MessageProcessor { constructor( @@ -19,10 +20,10 @@ export class TelemetryProcessor implements MessageProcessor { return topic.startsWith(prefix) } - process(topic: string, message: Buffer): void { + process(incomingMessage: IncomingMessage): void { const input: TelemetryInput = this.telemetryMessageMapper.map( - topic, - message + incomingMessage.topic, + incomingMessage.payload ) this.processTelemetry.execute(input, this.machineConfig) } From 4d1a064f8ce76302b7087ee9c11cda44326c906f Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 6 Jul 2026 23:58:13 +0200 Subject: [PATCH 033/125] refactor(coordinator): remove message mappers and inline mapping logic for heartbeat and telemetry --- .../heartbeat/mapper/map-heartbeat-message.ts | 12 ------------ .../src/application/messaging/message-mapper.ts | 12 ------------ .../messaging/processor/heartbeat-processor.ts | 10 ++++------ .../messaging/processor/telemetry-processor.ts | 10 ++++------ .../telemetry/mapper/map-telemetry-message.ts | 12 ------------ 5 files changed, 8 insertions(+), 48 deletions(-) delete mode 100644 microservices/coordinator-service/src/application/heartbeat/mapper/map-heartbeat-message.ts delete mode 100644 microservices/coordinator-service/src/application/messaging/message-mapper.ts delete mode 100644 microservices/coordinator-service/src/application/telemetry/mapper/map-telemetry-message.ts diff --git a/microservices/coordinator-service/src/application/heartbeat/mapper/map-heartbeat-message.ts b/microservices/coordinator-service/src/application/heartbeat/mapper/map-heartbeat-message.ts deleted file mode 100644 index 8b26807a..00000000 --- a/microservices/coordinator-service/src/application/heartbeat/mapper/map-heartbeat-message.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { MessageMapper } from '@/application/messaging/message-mapper' -import { HeartbeatInput } from '../dto/heartbeat-input' - -export class HeartbeatMessageMapper implements MessageMapper { - map(topic: string, message: Buffer): HeartbeatInput { - const payload = JSON.parse(message.toString()) - return { - machineId: topic.split('/').pop(), - ...payload - } - } -} diff --git a/microservices/coordinator-service/src/application/messaging/message-mapper.ts b/microservices/coordinator-service/src/application/messaging/message-mapper.ts deleted file mode 100644 index 5817a70e..00000000 --- a/microservices/coordinator-service/src/application/messaging/message-mapper.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Interface for mapping messages from a specific topic to a desired type. - * @template T The type to which the message will be mapped. - */ -export interface MessageMapper { - /** - * Transforms a message from a specific topic into the desired type. - * @param topic The topic from which the message was received. - * @param message The message to be transformed, represented as a Buffer. - */ - map(topic: string, message: Buffer): T -} diff --git a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts index aed8cf19..e8c9e38a 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts @@ -1,13 +1,11 @@ import { MessageProcessor } from '@/application/messaging/message-processor' import { ProcessHeartbeat } from '../../heartbeat/process-heartbeat' import { HeartbeatInput } from '../../heartbeat/dto/heartbeat-input' -import { HeartbeatMessageMapper } from '../../heartbeat/mapper/map-heartbeat-message' import { IncomingMessage } from '../incoming-message' export class HeartbeatProcessor implements MessageProcessor { constructor( private readonly processHeartbeat: ProcessHeartbeat, - private readonly heartbeatMessageMapper: HeartbeatMessageMapper, private readonly heartbeatTopicPrefix: string ) {} @@ -19,10 +17,10 @@ export class HeartbeatProcessor implements MessageProcessor { } process(incomingMessage: IncomingMessage): void { - const input: HeartbeatInput = this.heartbeatMessageMapper.map( - incomingMessage.topic, - incomingMessage.payload - ) + const input: HeartbeatInput = { + machineId: incomingMessage.topic.split('/').pop(), + ...JSON.parse(incomingMessage.payload.toString()) + } this.processHeartbeat.execute(input) } } diff --git a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts index 0e6c8f06..7d337f6e 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts @@ -2,14 +2,12 @@ import { MessageProcessor } from '@/application/messaging/message-processor' import { MachineConfig } from '@/domain/machine/machine-config' import { ProcessTelemetry } from '../../telemetry/process-telemetry' import { TelemetryInput } from '../../telemetry/dto/telemetry-input' -import { TelemetryMessageMapper } from '../../telemetry/mapper/map-telemetry-message' import { IncomingMessage } from '../incoming-message' export class TelemetryProcessor implements MessageProcessor { constructor( private readonly processTelemetry: ProcessTelemetry, private readonly machineConfig: MachineConfig, - private readonly telemetryMessageMapper: TelemetryMessageMapper, private readonly telemetryTopicPrefix: string ) {} @@ -21,10 +19,10 @@ export class TelemetryProcessor implements MessageProcessor { } process(incomingMessage: IncomingMessage): void { - const input: TelemetryInput = this.telemetryMessageMapper.map( - incomingMessage.topic, - incomingMessage.payload - ) + const input: TelemetryInput = { + machineId: incomingMessage.topic.split('/').pop(), + ...JSON.parse(incomingMessage.payload.toString()) + } this.processTelemetry.execute(input, this.machineConfig) } } diff --git a/microservices/coordinator-service/src/application/telemetry/mapper/map-telemetry-message.ts b/microservices/coordinator-service/src/application/telemetry/mapper/map-telemetry-message.ts deleted file mode 100644 index 6a940300..00000000 --- a/microservices/coordinator-service/src/application/telemetry/mapper/map-telemetry-message.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { MessageMapper } from '@/application/messaging/message-mapper' -import { TelemetryInput } from '../dto/telemetry-input' - -export class TelemetryMessageMapper implements MessageMapper { - map(topic: string, message: Buffer): TelemetryInput { - const payload = JSON.parse(message.toString()) - return { - machineId: topic.split('/').pop(), - ...payload - } - } -} From ca82ea4bcfe766c1dc6bafd21903e35e23ea8225 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 6 Jul 2026 23:59:58 +0200 Subject: [PATCH 034/125] refactor(inbox): rename class into InboxReceiver for handling incoming messages --- .../application/messaging/inbox-receiver.ts | 24 +++++++++++++ .../messaging/mqtt-message-dispatcher.ts | 36 ------------------- 2 files changed, 24 insertions(+), 36 deletions(-) create mode 100644 microservices/coordinator-service/src/application/messaging/inbox-receiver.ts delete mode 100644 microservices/coordinator-service/src/application/messaging/mqtt-message-dispatcher.ts diff --git a/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts b/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts new file mode 100644 index 00000000..ffa554b2 --- /dev/null +++ b/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts @@ -0,0 +1,24 @@ +import { Logger } from '../ports/logger' +import { InboxRepository } from '../ports/inbox.repository' +import { InboxStatus } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { IncomingMessage } from './incoming-message' + +export class InboxReceiver { + constructor( + private readonly inboxRepository: InboxRepository, + private readonly logger: Logger + ) {} + + receive(incomingMessage: IncomingMessage): void { + const inboxMessage = { + eventId: incomingMessage.id, + topic: incomingMessage.topic, + payload: JSON.stringify(incomingMessage.payload), + status: InboxStatus.PENDING, + receivedAt: incomingMessage.receivedAt, + processedAt: null + } + this.inboxRepository.save(inboxMessage) + this.logger.info('message saved to inbox', { inboxMessage }) + } +} diff --git a/microservices/coordinator-service/src/application/messaging/mqtt-message-dispatcher.ts b/microservices/coordinator-service/src/application/messaging/mqtt-message-dispatcher.ts deleted file mode 100644 index 66516897..00000000 --- a/microservices/coordinator-service/src/application/messaging/mqtt-message-dispatcher.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { MessageProcessor } from './message-processor' -import { Logger } from '../ports/logger' -import { InboxRepository } from '../ports/inbox.repository' -import { Clock } from '../ports/clock' -import { randomUUID } from 'crypto' -import { InboxStatus } from '@/infrastructure/persistence/sqlite/models/inbox-message' - -export class MqttMessageDispatcher { - constructor( - private readonly processors: MessageProcessor[], - private readonly inboxRepository: InboxRepository, - private readonly clock: Clock, - private readonly logger: Logger - ) {} - - handle(topic: string, message: Buffer): void { - this.inboxRepository.save({ - eventId: randomUUID(), - topic, - payload: message.toString(), - status: InboxStatus.PENDING, - receivedAt: this.clock.now(), - processedAt: null - }) - - try { - const processor = this.processors.find((p) => p.canHandle(topic)) - if (!processor) { - return - } - processor.process(topic, message) - } catch (err) { - this.logger.error('message processing failed', { err, topic }) - } - } -} From 12307fe7131867f3fabb60ab49b44c405b6bb20a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 7 Jul 2026 23:18:06 +0200 Subject: [PATCH 035/125] refactor(coordinator): replace IncomingMessage with InboxMessage in message processing interfaces --- .../src/application/messaging/message-processor.ts | 6 +++--- .../messaging/processor/heartbeat-processor.ts | 8 ++++---- .../messaging/processor/telemetry-processor.ts | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/microservices/coordinator-service/src/application/messaging/message-processor.ts b/microservices/coordinator-service/src/application/messaging/message-processor.ts index 4181bf20..bf18f89c 100644 --- a/microservices/coordinator-service/src/application/messaging/message-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/message-processor.ts @@ -1,4 +1,4 @@ -import { IncomingMessage } from './incoming-message' +import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' /** * Interface for processing messages based on their topic. @@ -13,7 +13,7 @@ export interface MessageProcessor { /** * Processes the incoming message. - * @param incomingMessage The incoming message to be processed. + * @param inboxMessage The inbox message to be processed. */ - process(incomingMessage: IncomingMessage): void + process(inboxMessage: InboxMessage): void } diff --git a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts index e8c9e38a..eabc4b65 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts @@ -1,7 +1,7 @@ import { MessageProcessor } from '@/application/messaging/message-processor' import { ProcessHeartbeat } from '../../heartbeat/process-heartbeat' import { HeartbeatInput } from '../../heartbeat/dto/heartbeat-input' -import { IncomingMessage } from '../incoming-message' +import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' export class HeartbeatProcessor implements MessageProcessor { constructor( @@ -16,10 +16,10 @@ export class HeartbeatProcessor implements MessageProcessor { return topic.startsWith(prefix) } - process(incomingMessage: IncomingMessage): void { + process(inboxMessage: InboxMessage): void { const input: HeartbeatInput = { - machineId: incomingMessage.topic.split('/').pop(), - ...JSON.parse(incomingMessage.payload.toString()) + machineId: inboxMessage.topic.split('/').pop(), + ...JSON.parse(inboxMessage.payload.toString()) } this.processHeartbeat.execute(input) } diff --git a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts index 7d337f6e..cd2f9b3f 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts @@ -2,7 +2,7 @@ import { MessageProcessor } from '@/application/messaging/message-processor' import { MachineConfig } from '@/domain/machine/machine-config' import { ProcessTelemetry } from '../../telemetry/process-telemetry' import { TelemetryInput } from '../../telemetry/dto/telemetry-input' -import { IncomingMessage } from '../incoming-message' +import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' export class TelemetryProcessor implements MessageProcessor { constructor( @@ -18,10 +18,10 @@ export class TelemetryProcessor implements MessageProcessor { return topic.startsWith(prefix) } - process(incomingMessage: IncomingMessage): void { + process(inboxMessage: InboxMessage): void { const input: TelemetryInput = { - machineId: incomingMessage.topic.split('/').pop(), - ...JSON.parse(incomingMessage.payload.toString()) + machineId: inboxMessage.topic.split('/').pop(), + ...JSON.parse(inboxMessage.payload.toString()) } this.processTelemetry.execute(input, this.machineConfig) } From 9f6e754f7172738bce0d3a702430bd2d5416bc35 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 7 Jul 2026 23:21:32 +0200 Subject: [PATCH 036/125] fix(inbox): change payload serialization --- .../src/application/messaging/inbox-receiver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts b/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts index ffa554b2..045e4ff2 100644 --- a/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts +++ b/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts @@ -13,7 +13,7 @@ export class InboxReceiver { const inboxMessage = { eventId: incomingMessage.id, topic: incomingMessage.topic, - payload: JSON.stringify(incomingMessage.payload), + payload: incomingMessage.payload.toString(), status: InboxStatus.PENDING, receivedAt: incomingMessage.receivedAt, processedAt: null From 243994aee34f240541cf3ec54978926e4f77ab32 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 7 Jul 2026 23:33:24 +0200 Subject: [PATCH 037/125] feat(inbox): add findPending method to InboxRepository interface --- .../src/application/ports/inbox.repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/microservices/coordinator-service/src/application/ports/inbox.repository.ts b/microservices/coordinator-service/src/application/ports/inbox.repository.ts index 9635b234..1e4b5500 100644 --- a/microservices/coordinator-service/src/application/ports/inbox.repository.ts +++ b/microservices/coordinator-service/src/application/ports/inbox.repository.ts @@ -2,4 +2,5 @@ import { InboxMessage } from '../../infrastructure/persistence/sqlite/models/inb export interface InboxRepository { save(message: InboxMessage): void + findPending(): InboxMessage[] } From 4a77b0a3dbe89937ab1612b80183c58f1493f5ac Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 7 Jul 2026 23:41:59 +0200 Subject: [PATCH 038/125] feat(inbox): implement findPending method to retrieve pending messages --- .../sqlite/sqlite-inbox.repository.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts index 597a0ce4..6d2f545e 100644 --- a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts @@ -26,4 +26,29 @@ export class SqliteInboxRepository implements InboxRepository { ] this.db.prepare(query).run(params) } + + findPending(): InboxMessage[] { + const query = ` + SELECT + event_id, + topic, + payload, + status, + received_at, + processed_at + FROM coordinator_inbox + WHERE status = 'PENDING' + ORDER BY received_at ASC + ` + const rows = this.db.prepare(query).all() as InboxMessage[] + + return rows.map((row) => ({ + eventId: row.eventId, + topic: row.topic, + payload: row.payload, + status: row.status as InboxMessage['status'], + receivedAt: new Date(row.receivedAt), + processedAt: row.processedAt ? new Date(row.processedAt) : null + })) + } } From 40a069773c5af99ac079e133f95920bc89c8a9be Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 7 Jul 2026 23:43:11 +0200 Subject: [PATCH 039/125] feat(coordinator): add inbox worker to process pending inbox messages --- .../src/application/messaging/inbox-worker.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 microservices/coordinator-service/src/application/messaging/inbox-worker.ts diff --git a/microservices/coordinator-service/src/application/messaging/inbox-worker.ts b/microservices/coordinator-service/src/application/messaging/inbox-worker.ts new file mode 100644 index 00000000..d02b4ac5 --- /dev/null +++ b/microservices/coordinator-service/src/application/messaging/inbox-worker.ts @@ -0,0 +1,27 @@ +import { MessageProcessor } from './message-processor' +import { Logger } from '../ports/logger' +import { InboxRepository } from '../ports/inbox.repository' + +export class InboxWorker { + constructor( + private readonly processors: MessageProcessor[], + private readonly inboxRepository: InboxRepository, + private readonly logger: Logger + ) {} + + run() { + this.logger.info('Inbox worker started') + const messages = this.inboxRepository.findPending() + + for (const msg of messages) { + try { + const processor = this.processors.find((h) => h.canHandle(msg.topic)) + if (!processor) continue + processor.process(msg) + this.logger.info('message processed successfully', { msg }) + } catch (err) { + this.logger.error('message processing failed', { err, msg }) + } + } + } +} From f375919bff5432e5755927a27192af49f9058380 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 7 Jul 2026 23:44:32 +0200 Subject: [PATCH 040/125] feat(coordinator): integrate InboxReceiver and InboxWorker for message processing --- microservices/coordinator-service/src/main.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 15c28a9e..6474e719 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -14,12 +14,13 @@ import { createMqttConsumer } from './infrastructure/mqtt/consumer' import { getConfig } from './config/env' import { ProcessHeartbeat } from './application/heartbeat/process-heartbeat' import { TelemetryProcessor } from './application/messaging/processor/telemetry-processor' -import { MqttMessageDispatcher } from './application/messaging/mqtt-message-dispatcher' +import { InboxReceiver } from './application/messaging/inbox-receiver' import { HeartbeatProcessor } from './application/messaging/processor/heartbeat-processor' -import { TelemetryMessageMapper } from './application/telemetry/mapper/map-telemetry-message' -import { HeartbeatMessageMapper } from './application/heartbeat/mapper/map-heartbeat-message' import { SqliteInboxRepository } from './infrastructure/persistence/sqlite/sqlite-inbox.repository' import { createSqliteDatabase } from './infrastructure/persistence/sqlite/create-sqlite-database' +import { randomUUID } from 'crypto' +import { IncomingMessage } from './application/messaging/incoming-message' +import { InboxWorker } from './application/messaging/inbox-worker' function buildMachineConfig(): MachineConfig { return new MachineConfig( @@ -55,22 +56,18 @@ function bootstrap() { const processHeartbeat = new ProcessHeartbeat(clock, logger) const machineConfig = buildMachineConfig() - const dispatcher = new MqttMessageDispatcher( + const inboxReceiver = new InboxReceiver(inboxRepository, logger) + + const inboxWorker = new InboxWorker( [ new TelemetryProcessor( processTelemetry, machineConfig, - new TelemetryMessageMapper(), config.mqtt.topic ), - new HeartbeatProcessor( - processHeartbeat, - new HeartbeatMessageMapper(), - config.mqtt.heartbeatTopic - ) + new HeartbeatProcessor(processHeartbeat, config.mqtt.heartbeatTopic) ], inboxRepository, - clock, logger ) @@ -78,8 +75,18 @@ function bootstrap() { mqtt.subscribe(`${config.mqtt.heartbeatTopic}/+`, { qos: 0 }) mqtt.onMessage((topic, message) => { - dispatcher.handle(topic, message) + const incomingMessage: IncomingMessage = { + id: randomUUID(), + topic, + payload: message, + receivedAt: clock.now() + } + inboxReceiver.receive(incomingMessage) }) + + setInterval(() => { + inboxWorker.run() + }, 1000) } bootstrap() From d3241a3b61894748d9ef16679f8e3c6510278d24 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 8 Jul 2026 00:23:06 +0200 Subject: [PATCH 041/125] chore(deps): update dependencies in coordinator-service --- .../coordinator-service/package-lock.json | 234 ++++++++++-------- 1 file changed, 132 insertions(+), 102 deletions(-) diff --git a/microservices/coordinator-service/package-lock.json b/microservices/coordinator-service/package-lock.json index c7a70c9d..c53211bb 100644 --- a/microservices/coordinator-service/package-lock.json +++ b/microservices/coordinator-service/package-lock.json @@ -99,21 +99,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -122,9 +122,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -800,14 +800,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -819,9 +819,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", "funding": { @@ -842,9 +842,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -859,9 +859,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -876,9 +876,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ "x64" ], @@ -893,9 +893,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ "x64" ], @@ -910,9 +910,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ "arm" ], @@ -927,13 +927,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -944,13 +947,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -961,13 +967,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -978,13 +987,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -995,13 +1007,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1012,13 +1027,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1029,9 +1047,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -1046,9 +1064,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ "wasm32" ], @@ -1056,18 +1074,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ "arm64" ], @@ -1082,9 +1100,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -1162,9 +1180,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -2123,9 +2141,9 @@ } }, "node_modules/eslint": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", - "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", "dev": true, "license": "MIT", "peer": true, @@ -2904,6 +2922,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2925,6 +2946,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2946,6 +2970,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2967,6 +2994,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3268,9 +3298,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -3542,9 +3572,9 @@ "license": "MIT" }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -3768,13 +3798,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3784,21 +3814,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, "node_modules/safe-buffer": { @@ -4364,16 +4394,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "bin": { @@ -4390,7 +4420,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", From df338cf0d5a51201cda37c02d93dc388ece0c3cf Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 8 Jul 2026 00:25:03 +0200 Subject: [PATCH 042/125] chore(deps): update dependencies in machine-simulator-service --- .../package-lock.json | 246 ++++++++++-------- 1 file changed, 138 insertions(+), 108 deletions(-) diff --git a/microservices/machine-simulator-service/package-lock.json b/microservices/machine-simulator-service/package-lock.json index cde1a2fc..300f8faa 100644 --- a/microservices/machine-simulator-service/package-lock.json +++ b/microservices/machine-simulator-service/package-lock.json @@ -97,21 +97,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -120,9 +120,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -748,14 +748,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -767,9 +767,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", "funding": { @@ -783,9 +783,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -800,9 +800,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -817,9 +817,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ "x64" ], @@ -834,9 +834,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ "x64" ], @@ -851,9 +851,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ "arm" ], @@ -868,13 +868,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -885,13 +888,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -902,13 +908,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -919,13 +928,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -936,13 +948,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -953,13 +968,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -970,9 +988,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -987,9 +1005,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ "wasm32" ], @@ -997,18 +1015,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ "arm64" ], @@ -1023,9 +1041,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -1054,9 +1072,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -1880,9 +1898,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", "dev": true, "license": "MIT" }, @@ -2133,9 +2151,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2672,6 +2690,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2693,6 +2714,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2714,6 +2738,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2735,6 +2762,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2943,9 +2973,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.14", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz", - "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -3188,9 +3218,9 @@ "license": "MIT" }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -3382,13 +3412,13 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3398,21 +3428,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, "node_modules/safe-buffer": { @@ -3608,9 +3638,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -3923,16 +3953,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "bin": { @@ -3949,7 +3979,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", From 616684498ec5a0cb34dbc39bfd50a094fd6cee3a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 17 Aug 2026 18:41:14 +0200 Subject: [PATCH 043/125] feat(inbox): track processing attempts and update status --- .../src/application/ports/inbox.repository.ts | 6 +++++ .../sqlite/create-sqlite-database.ts | 1 + .../sqlite/models/inbox-message.ts | 4 ++- .../sqlite/sqlite-inbox.repository.ts | 26 ++++++++++++++++++- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/microservices/coordinator-service/src/application/ports/inbox.repository.ts b/microservices/coordinator-service/src/application/ports/inbox.repository.ts index 1e4b5500..40fe373a 100644 --- a/microservices/coordinator-service/src/application/ports/inbox.repository.ts +++ b/microservices/coordinator-service/src/application/ports/inbox.repository.ts @@ -2,5 +2,11 @@ import { InboxMessage } from '../../infrastructure/persistence/sqlite/models/inb export interface InboxRepository { save(message: InboxMessage): void + updateStatus( + eventId: string, + status: string, + attempts: number, + processedAt: Date | null + ): void findPending(): InboxMessage[] } diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts index 5e415b9d..6ddabb63 100644 --- a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts @@ -13,6 +13,7 @@ export function createSqliteDatabase(inboxDbPath: string): Database.Database { topic TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL, + attempts INTEGER NOT NULL, received_at TEXT NOT NULL, processed_at TEXT )`) diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts index 22e8b8c9..bc6a8d30 100644 --- a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts @@ -1,7 +1,8 @@ export enum InboxStatus { PENDING = 'PENDING', PROCESSED = 'PROCESSED', - FAILED = 'FAILED' + FAILED = 'FAILED', + UNSUPPORTED = 'UNSUPPORTED' } export type InboxMessage = { @@ -9,6 +10,7 @@ export type InboxMessage = { topic: string payload: string status: InboxStatus + attempts: number receivedAt: Date processedAt: Date | null } diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts index 6d2f545e..ef59924a 100644 --- a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts @@ -11,22 +11,44 @@ export class SqliteInboxRepository implements InboxRepository { topic, payload, status, + attempts, received_at, processed_at ) - VALUES (?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?) ` const params = [ message.eventId, message.topic, message.payload, message.status, + message.attempts, message.receivedAt.toISOString(), message.processedAt ? message.processedAt.toISOString() : null ] this.db.prepare(query).run(params) } + updateStatus( + eventId: string, + status: string, + attempts: number, + processedAt: Date | null + ): void { + const query = ` + UPDATE coordinator_inbox + SET status = ?, attempts = ?, processed_at = ? + WHERE event_id = ? + ` + const params = [ + status, + attempts, + processedAt ? processedAt.toISOString() : null, + eventId + ] + this.db.prepare(query).run(params) + } + findPending(): InboxMessage[] { const query = ` SELECT @@ -34,6 +56,7 @@ export class SqliteInboxRepository implements InboxRepository { topic, payload, status, + attempts, received_at, processed_at FROM coordinator_inbox @@ -47,6 +70,7 @@ export class SqliteInboxRepository implements InboxRepository { topic: row.topic, payload: row.payload, status: row.status as InboxMessage['status'], + attempts: row.attempts, receivedAt: new Date(row.receivedAt), processedAt: row.processedAt ? new Date(row.processedAt) : null })) From ff4faced9983ef3b6dac14dcabf861002aeb12f4 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 17 Aug 2026 19:03:26 +0200 Subject: [PATCH 044/125] feat(inbox): add update status for processed, unsupported messages and processing failures --- .../application/messaging/inbox-receiver.ts | 8 +++- .../src/application/messaging/inbox-worker.ts | 38 +++++++++++++++++-- microservices/coordinator-service/src/main.ts | 1 + 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts b/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts index 045e4ff2..91e2cbf6 100644 --- a/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts +++ b/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts @@ -1,6 +1,9 @@ import { Logger } from '../ports/logger' import { InboxRepository } from '../ports/inbox.repository' -import { InboxStatus } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { + InboxMessage, + InboxStatus +} from '@/infrastructure/persistence/sqlite/models/inbox-message' import { IncomingMessage } from './incoming-message' export class InboxReceiver { @@ -10,11 +13,12 @@ export class InboxReceiver { ) {} receive(incomingMessage: IncomingMessage): void { - const inboxMessage = { + const inboxMessage: InboxMessage = { eventId: incomingMessage.id, topic: incomingMessage.topic, payload: incomingMessage.payload.toString(), status: InboxStatus.PENDING, + attempts: 0, receivedAt: incomingMessage.receivedAt, processedAt: null } diff --git a/microservices/coordinator-service/src/application/messaging/inbox-worker.ts b/microservices/coordinator-service/src/application/messaging/inbox-worker.ts index d02b4ac5..b551e1e0 100644 --- a/microservices/coordinator-service/src/application/messaging/inbox-worker.ts +++ b/microservices/coordinator-service/src/application/messaging/inbox-worker.ts @@ -1,11 +1,14 @@ import { MessageProcessor } from './message-processor' import { Logger } from '../ports/logger' import { InboxRepository } from '../ports/inbox.repository' +import { InboxStatus } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { SystemClock } from '@/infrastructure/time/system-clock' export class InboxWorker { constructor( private readonly processors: MessageProcessor[], private readonly inboxRepository: InboxRepository, + private readonly clock: SystemClock, private readonly logger: Logger ) {} @@ -14,13 +17,40 @@ export class InboxWorker { const messages = this.inboxRepository.findPending() for (const msg of messages) { + const processor = this.processors.find((h) => h.canHandle(msg.topic)) + + if (!processor) { + this.inboxRepository.updateStatus( + msg.eventId, + InboxStatus.UNSUPPORTED, + msg.attempts, + this.clock.now() + ) + + this.logger.warn('No processor found for message', { msg }) + continue + } + try { - const processor = this.processors.find((h) => h.canHandle(msg.topic)) - if (!processor) continue processor.process(msg) - this.logger.info('message processed successfully', { msg }) + + this.inboxRepository.updateStatus( + msg.eventId, + InboxStatus.PROCESSED, + msg.attempts + 1, + this.clock.now() + ) + + this.logger.info('Message processed successfully', { msg }) } catch (err) { - this.logger.error('message processing failed', { err, msg }) + this.inboxRepository.updateStatus( + msg.eventId, + InboxStatus.FAILED, + msg.attempts + 1, + this.clock.now() + ) + + this.logger.error('Message processing failed', { err, msg }) } } } diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 6474e719..b763cf7c 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -68,6 +68,7 @@ function bootstrap() { new HeartbeatProcessor(processHeartbeat, config.mqtt.heartbeatTopic) ], inboxRepository, + clock, logger ) From b69b59d6091bb3ba19d7f31ed862c562bed6d932 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 18 Aug 2026 13:21:30 +0200 Subject: [PATCH 045/125] refactor(inbox): improve findPending method --- .../sqlite/sqlite-inbox.repository.ts | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts index ef59924a..5bbc364f 100644 --- a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts @@ -1,4 +1,7 @@ -import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { + InboxMessage, + InboxStatus +} from '@/infrastructure/persistence/sqlite/models/inbox-message' import { InboxRepository } from '@/application/ports/inbox.repository' import { Database } from 'better-sqlite3' @@ -51,28 +54,37 @@ export class SqliteInboxRepository implements InboxRepository { findPending(): InboxMessage[] { const query = ` - SELECT - event_id, - topic, - payload, - status, - attempts, - received_at, - processed_at - FROM coordinator_inbox - WHERE status = 'PENDING' - ORDER BY received_at ASC - ` - const rows = this.db.prepare(query).all() as InboxMessage[] + SELECT + event_id, + topic, + payload, + status, + attempts, + received_at, + processed_at + FROM coordinator_inbox + WHERE status = 'PENDING' + ORDER BY received_at ASC + ` + + const rows = this.db.prepare(query).all() as { + event_id: string + topic: string + payload: string + status: InboxStatus + attempts: number + received_at: string + processed_at: string | null + }[] return rows.map((row) => ({ - eventId: row.eventId, + eventId: row.event_id, topic: row.topic, payload: row.payload, - status: row.status as InboxMessage['status'], + status: row.status, attempts: row.attempts, - receivedAt: new Date(row.receivedAt), - processedAt: row.processedAt ? new Date(row.processedAt) : null + receivedAt: new Date(row.received_at), + processedAt: row.processed_at ? new Date(row.processed_at) : null })) } } From 280f3120f2df84e189c81e3a21eb24900e5c1c8f Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 18 Aug 2026 15:25:25 +0200 Subject: [PATCH 046/125] feat(env): add environment configuration files example for coordinator and machine simulator services --- .../coordinator-service/.env.example | 8 ++++++++ .../machine-simulator-service/.env.example | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 microservices/coordinator-service/.env.example create mode 100644 microservices/machine-simulator-service/.env.example diff --git a/microservices/coordinator-service/.env.example b/microservices/coordinator-service/.env.example new file mode 100644 index 00000000..094a22ce --- /dev/null +++ b/microservices/coordinator-service/.env.example @@ -0,0 +1,8 @@ +NODE_ENV=development +MQTT_URL=mqtt://mqtt-broker:1883 +MQTT_TOPIC=factory/telemetry +INTERVAL_MS=2000 +MQTT_HEARTBEAT_TOPIC=factory/heartbeat +HEARTBEAT_INTERVAL_MS=5000 + +INBOX_DB_PATH=./data/coordinator_inbox.db \ No newline at end of file diff --git a/microservices/machine-simulator-service/.env.example b/microservices/machine-simulator-service/.env.example new file mode 100644 index 00000000..604eb2f5 --- /dev/null +++ b/microservices/machine-simulator-service/.env.example @@ -0,0 +1,20 @@ +NODE_ENV=development +MQTT_URL=mqtt://mqtt-broker:1883 +MQTT_TOPIC=factory/telemetry +INTERVAL_MS=2000 +MQTT_HEARTBEAT_TOPIC=factory/heartbeat +HEARTBEAT_INTERVAL_MS=5000 + +OUTBOX_DB_PATH=./data/machine_simulator_outbox.db + +MACHINE_ID=machine-1 +OPERATING_TEMPERATURE_MIN=20 +OPERATING_TEMPERATURE_MAX=85 +VIBRATION_MIN=0 +VIBRATION_MAX=4.5 +PRESSURE_MIN=0 +PRESSURE_MAX=10 +POWER_CONSUMPTION_MIN=0 +POWER_CONSUMPTION_MAX=1200 +EMISSIONS_MIN=0 +EMISSIONS_MAX=80 \ No newline at end of file From e570e04a6823ee0c3e8465dff5f152ab4da0e0bf Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 18 Aug 2026 19:16:36 +0200 Subject: [PATCH 047/125] feat(inbox): move InboxWorker from messaging to workers package --- .../src/application/{messaging => workers}/inbox-worker.ts | 2 +- microservices/coordinator-service/src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename microservices/coordinator-service/src/application/{messaging => workers}/inbox-worker.ts (95%) diff --git a/microservices/coordinator-service/src/application/messaging/inbox-worker.ts b/microservices/coordinator-service/src/application/workers/inbox-worker.ts similarity index 95% rename from microservices/coordinator-service/src/application/messaging/inbox-worker.ts rename to microservices/coordinator-service/src/application/workers/inbox-worker.ts index b551e1e0..a475bb41 100644 --- a/microservices/coordinator-service/src/application/messaging/inbox-worker.ts +++ b/microservices/coordinator-service/src/application/workers/inbox-worker.ts @@ -1,4 +1,4 @@ -import { MessageProcessor } from './message-processor' +import { MessageProcessor } from '../messaging/message-processor' import { Logger } from '../ports/logger' import { InboxRepository } from '../ports/inbox.repository' import { InboxStatus } from '@/infrastructure/persistence/sqlite/models/inbox-message' diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index b763cf7c..9bdfe8a0 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -20,7 +20,7 @@ import { SqliteInboxRepository } from './infrastructure/persistence/sqlite/sqlit import { createSqliteDatabase } from './infrastructure/persistence/sqlite/create-sqlite-database' import { randomUUID } from 'crypto' import { IncomingMessage } from './application/messaging/incoming-message' -import { InboxWorker } from './application/messaging/inbox-worker' +import { InboxWorker } from './application/workers/inbox-worker' function buildMachineConfig(): MachineConfig { return new MachineConfig( From 13287665d7119fc745eb8c0751530e7c590d26b0 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 19 Aug 2026 14:56:44 +0200 Subject: [PATCH 048/125] feat(machine): add MachineStatus for machine state management: ONLINE, OFFLINE --- .../coordinator-service/src/domain/machine/machine-status.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 microservices/coordinator-service/src/domain/machine/machine-status.ts diff --git a/microservices/coordinator-service/src/domain/machine/machine-status.ts b/microservices/coordinator-service/src/domain/machine/machine-status.ts new file mode 100644 index 00000000..d6cc22dd --- /dev/null +++ b/microservices/coordinator-service/src/domain/machine/machine-status.ts @@ -0,0 +1,4 @@ +export enum MachineStatus { + ONLINE = 'ONLINE', + OFFLINE = 'OFFLINE' +} From 67ed84abc6f2fe856b857d2555f32cae20291430 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 19 Aug 2026 15:03:15 +0200 Subject: [PATCH 049/125] feat(machine): add MachineStatusPolicy interface for evaluating machine status --- .../machine/policies/machine-status-policy.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 microservices/coordinator-service/src/domain/machine/policies/machine-status-policy.ts diff --git a/microservices/coordinator-service/src/domain/machine/policies/machine-status-policy.ts b/microservices/coordinator-service/src/domain/machine/policies/machine-status-policy.ts new file mode 100644 index 00000000..97cd0201 --- /dev/null +++ b/microservices/coordinator-service/src/domain/machine/policies/machine-status-policy.ts @@ -0,0 +1,14 @@ +import { MachineStatus } from '../machine-status' + +/** + * A policy that defines how to evaluate the status of a machine based on its last heartbeat timestamp and the current time. + */ +export interface MachineStatusPolicy { + /** + * Evaluates the status of a machine based on its last heartbeat timestamp and the current time. + * @param lastHeartbeatAt The timestamp of the last heartbeat received from the machine. + * @param now The current timestamp to compare against the last heartbeat. + * @returns The evaluated status of the machine. + */ + evaluate(lastHeartbeatAt: Date, now: Date): MachineStatus +} From cd596f923bac6ac69733810a591e47c917d0d93a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 19 Aug 2026 15:24:07 +0200 Subject: [PATCH 050/125] feat(machine): implement HeartbeatTimeoutPolicy --- .../machine/policies/heatbeat-timeout-policy.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts diff --git a/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts b/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts new file mode 100644 index 00000000..a51d4a95 --- /dev/null +++ b/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts @@ -0,0 +1,16 @@ +import { MachineStatus } from '../machine-status' +import { MachineStatusPolicy } from './machine-status-policy' + +export class HeartbeatTimeoutPolicy implements MachineStatusPolicy { + constructor(private readonly timeoutMs: number) {} + + evaluate(lastHeartbeatAt: Date, now: Date): MachineStatus { + const elapsed = now.getTime() - lastHeartbeatAt.getTime() + + if (elapsed >= this.timeoutMs) { + return MachineStatus.OFFLINE + } + + return MachineStatus.ONLINE + } +} From 4bbe5c801e21ed3fa226827b24b6dba6a376cdb3 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 19 Aug 2026 15:25:29 +0200 Subject: [PATCH 051/125] test(machine): add unit tests for heartbeat timeout evaluation logic --- .../machine/heartbeat-timeout-policy.spec.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 microservices/coordinator-service/test/domain/machine/heartbeat-timeout-policy.spec.ts diff --git a/microservices/coordinator-service/test/domain/machine/heartbeat-timeout-policy.spec.ts b/microservices/coordinator-service/test/domain/machine/heartbeat-timeout-policy.spec.ts new file mode 100644 index 00000000..59075db5 --- /dev/null +++ b/microservices/coordinator-service/test/domain/machine/heartbeat-timeout-policy.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { HeartbeatTimeoutPolicy } from '@/domain/machine/policies/heatbeat-timeout-policy' + +describe('HeartbeatTimeoutPolicy', () => { + const policy = new HeartbeatTimeoutPolicy(10_000) + + it('should return ONLINE when heartbeat is within timeout', () => { + const lastHeartbeatAt = new Date('2026-08-18T12:00:00.000Z') + const now = new Date('2026-08-18T12:00:05.000Z') + + const result = policy.evaluate(lastHeartbeatAt, now) + + expect(result).toBe('ONLINE') + }) + + it('should return OFFLINE when heartbeat reaches timeout', () => { + const lastHeartbeatAt = new Date('2026-08-18T12:00:00.000Z') + const now = new Date('2026-08-18T12:00:10.000Z') + + const result = policy.evaluate(lastHeartbeatAt, now) + + expect(result).toBe('OFFLINE') + }) + + it('should return OFFLINE when heartbeat exceeds timeout', () => { + const lastHeartbeatAt = new Date('2026-08-18T12:00:00.000Z') + const now = new Date('2026-08-18T12:00:15.000Z') + + const result = policy.evaluate(lastHeartbeatAt, now) + + expect(result).toBe('OFFLINE') + }) +}) From a36c2ce6bec08da0231f6d917424baec2cfb91b4 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 19 Aug 2026 15:39:07 +0200 Subject: [PATCH 052/125] refactor(machine): simplify evaluate method --- .../domain/machine/policies/heatbeat-timeout-policy.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts b/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts index a51d4a95..11f237e5 100644 --- a/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts +++ b/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts @@ -7,10 +7,8 @@ export class HeartbeatTimeoutPolicy implements MachineStatusPolicy { evaluate(lastHeartbeatAt: Date, now: Date): MachineStatus { const elapsed = now.getTime() - lastHeartbeatAt.getTime() - if (elapsed >= this.timeoutMs) { - return MachineStatus.OFFLINE - } - - return MachineStatus.ONLINE + return elapsed >= this.timeoutMs + ? MachineStatus.OFFLINE + : MachineStatus.ONLINE } } From 839ec803e0dedec0c23933c160840a2df083eb7b Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 20 Aug 2026 13:10:57 +0200 Subject: [PATCH 053/125] feat(logger): enhance PinoLogger --- .../src/infrastructure/logger/pino-logger.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts b/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts index 4bab20a0..6a5cd112 100644 --- a/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts +++ b/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts @@ -5,9 +5,13 @@ import { NodeEnv } from '@/config/env' export class PinoLogger implements Logger { private readonly logger: pino.Logger - constructor(nodeEnv: NodeEnv) { + constructor( + private readonly nodeEnv: NodeEnv, + logger?: pino.Logger + ) { this.logger = - nodeEnv === 'development' + logger ?? + (nodeEnv === 'development' ? pino({ transport: { target: 'pino-pretty', @@ -16,13 +20,14 @@ export class PinoLogger implements Logger { translateTime: 'SYS:standard', ignore: 'pid,hostname' } - } + }, + level: 'info' }) - : pino() + : pino()) } - child(context: object): pino.Logger { - return this.logger.child(context) + child(context: object): Logger { + return new PinoLogger(this.nodeEnv, this.logger.child(context)) } info(message: string, metadata?: object): void { this.logger.info(metadata ?? {}, message) From 042b124f50e4e67f92fa5b8317638a0307cb7946 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 20 Aug 2026 13:11:39 +0200 Subject: [PATCH 054/125] refactor(inbox-worker): replace SystemClock with Clock interface --- .../src/application/workers/inbox-worker.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/microservices/coordinator-service/src/application/workers/inbox-worker.ts b/microservices/coordinator-service/src/application/workers/inbox-worker.ts index a475bb41..7eaea938 100644 --- a/microservices/coordinator-service/src/application/workers/inbox-worker.ts +++ b/microservices/coordinator-service/src/application/workers/inbox-worker.ts @@ -2,17 +2,17 @@ import { MessageProcessor } from '../messaging/message-processor' import { Logger } from '../ports/logger' import { InboxRepository } from '../ports/inbox.repository' import { InboxStatus } from '@/infrastructure/persistence/sqlite/models/inbox-message' -import { SystemClock } from '@/infrastructure/time/system-clock' +import { Clock } from '../ports/clock' export class InboxWorker { constructor( private readonly processors: MessageProcessor[], private readonly inboxRepository: InboxRepository, - private readonly clock: SystemClock, + private readonly clock: Clock, private readonly logger: Logger ) {} - run() { + run(): void { this.logger.info('Inbox worker started') const messages = this.inboxRepository.findPending() From eb4ffb6363a2bee4274df1df049bc00a883918ff Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 20 Aug 2026 13:43:38 +0200 Subject: [PATCH 055/125] feat(heartbeat-repository): add interface for managing heartbeat events --- .../src/application/ports/heartbeats.repository.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 microservices/coordinator-service/src/application/ports/heartbeats.repository.ts diff --git a/microservices/coordinator-service/src/application/ports/heartbeats.repository.ts b/microservices/coordinator-service/src/application/ports/heartbeats.repository.ts new file mode 100644 index 00000000..d37b68e0 --- /dev/null +++ b/microservices/coordinator-service/src/application/ports/heartbeats.repository.ts @@ -0,0 +1,13 @@ +import { HeartbeatEvent } from '../heartbeat/dto/heartbeat-event' + +/** + * Repository for managing heartbeat events. + */ +export interface HeartbeatRepository { + /** + * Finds the latest heartbeat event for each machine. + * @param topicPrefix The topic prefix to filter heartbeat events by. + * @returns An array of the latest heartbeat events for each machine. + */ + findLatestForEachMachine(topicPrefix: string): HeartbeatEvent[] +} From 42b9ce317490806e8f5a791a0bf7b0ead3113315 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 20 Aug 2026 13:44:48 +0200 Subject: [PATCH 056/125] feat(heartbeat-repository): implement SqliteHeartbeatRepository --- .../sqlite/sqlite-heartbeat.repository.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-heartbeat.repository.ts diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-heartbeat.repository.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-heartbeat.repository.ts new file mode 100644 index 00000000..ce45908a --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-heartbeat.repository.ts @@ -0,0 +1,62 @@ +import { Database } from 'better-sqlite3' +import { HeartbeatEvent } from '@/application/heartbeat/dto/heartbeat-event' +import { HeartbeatRepository } from '@/application/ports/heartbeats.repository' + +export class SqliteHeartbeatRepository implements HeartbeatRepository { + constructor(private readonly db: Database) {} + findLatestForEachMachine(topicPrefix: string): HeartbeatEvent[] { + const query = ` + SELECT + event_id, + topic, + payload, + received_at + FROM ( + SELECT + event_id, + topic, + payload, + received_at, + ROW_NUMBER() OVER ( + PARTITION BY topic + ORDER BY received_at DESC + ) AS row_number + FROM coordinator_inbox + WHERE topic LIKE ? + AND status = 'PROCESSED' + ) + WHERE row_number = 1 + ` + + const rows = this.db.prepare(query).all(`${topicPrefix}/%`) as { + event_id: string + topic: string + payload: string + received_at: string + }[] + + return rows.map((row) => { + const machineId = row.topic.split('/').pop() + + if (!machineId) { + throw new Error(`Invalid heartbeat topic: ${row.topic}`) + } + + const payload = JSON.parse(row.payload) as { + timestamp: string + } + + const occurredAt = new Date(payload.timestamp) + + if (Number.isNaN(occurredAt.getTime())) { + throw new Error(`Invalid heartbeat timestamp for event ${row.event_id}`) + } + + return { + machineId, + occurredAt, + receivedAt: new Date(row.received_at) + } + }) + } +} From 8434a6ddeec6eb94d30a3b397c76437ca54e04b4 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 20 Aug 2026 13:45:52 +0200 Subject: [PATCH 057/125] feat(config): add heartbeatTimeoutMs to AppConfig and .env.example --- microservices/coordinator-service/.env.example | 1 + microservices/coordinator-service/src/config/env.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/microservices/coordinator-service/.env.example b/microservices/coordinator-service/.env.example index 094a22ce..68800dcb 100644 --- a/microservices/coordinator-service/.env.example +++ b/microservices/coordinator-service/.env.example @@ -4,5 +4,6 @@ MQTT_TOPIC=factory/telemetry INTERVAL_MS=2000 MQTT_HEARTBEAT_TOPIC=factory/heartbeat HEARTBEAT_INTERVAL_MS=5000 +HEARTBEAT_TIMEOUT_MS=10000 INBOX_DB_PATH=./data/coordinator_inbox.db \ No newline at end of file diff --git a/microservices/coordinator-service/src/config/env.ts b/microservices/coordinator-service/src/config/env.ts index 812464c5..fea2b676 100644 --- a/microservices/coordinator-service/src/config/env.ts +++ b/microservices/coordinator-service/src/config/env.ts @@ -18,6 +18,7 @@ export type AppConfig = { } intervalMs: number heartbeatIntervalMs: number + heartbeatTimeoutMs: number inboxDbPath: string } @@ -31,6 +32,7 @@ export function getConfig(): AppConfig { }, intervalMs: Number(process.env.INTERVAL_MS), heartbeatIntervalMs: Number(process.env.HEARTBEAT_INTERVAL_MS), + heartbeatTimeoutMs: Number(process.env.HEARTBEAT_TIMEOUT_MS), inboxDbPath: process.env.INBOX_DB_PATH! } } From 30451ec5666b2a499b24c3e04a0b583e9a449931 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 20 Aug 2026 13:47:05 +0200 Subject: [PATCH 058/125] feat(heartbeat-monitor): implement HeartbeatMonitor for managing heartbeat timeout with status --- .../application/workers/heartbeat-monitor.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts diff --git a/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts b/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts new file mode 100644 index 00000000..4af24b60 --- /dev/null +++ b/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts @@ -0,0 +1,30 @@ +import { Clock } from '@/application/ports/clock' +import { HeartbeatRepository } from '../ports/heartbeats.repository' +import { HeartbeatTimeoutPolicy } from '@/domain/machine/policies/heatbeat-timeout-policy' +import { Logger } from '../ports/logger' + +export class HeartbeatMonitor { + constructor( + private readonly heartbeatRepository: HeartbeatRepository, + private readonly timeoutPolicy: HeartbeatTimeoutPolicy, + private readonly topicPrefix: string, + private readonly clock: Clock, + private readonly logger: Logger + ) {} + + run(): void { + const heartbeats = this.heartbeatRepository.findLatestForEachMachine( + this.topicPrefix + ) + const now = this.clock.now() + + for (const heartbeat of heartbeats) { + const status = this.timeoutPolicy.evaluate(heartbeat.receivedAt, now) + + this.logger.info('Heartbeat status', { + machineId: heartbeat.machineId, + status + }) + } + } +} From 933c1e64184d51f7a78b15bccb0dd0c881f9bb0a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 20 Aug 2026 13:48:06 +0200 Subject: [PATCH 059/125] feat(heartbeat-monitor): add HeartbeatMonitor into main for heartbeat processing --- microservices/coordinator-service/src/main.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 9bdfe8a0..976fd37d 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -21,6 +21,9 @@ import { createSqliteDatabase } from './infrastructure/persistence/sqlite/create import { randomUUID } from 'crypto' import { IncomingMessage } from './application/messaging/incoming-message' import { InboxWorker } from './application/workers/inbox-worker' +import { SqliteHeartbeatRepository } from './infrastructure/persistence/sqlite/sqlite-heartbeat.repository' +import { HeartbeatTimeoutPolicy } from './domain/machine/policies/heatbeat-timeout-policy' +import { HeartbeatMonitor } from './application/workers/heartbeat-monitor' function buildMachineConfig(): MachineConfig { return new MachineConfig( @@ -49,6 +52,7 @@ function bootstrap() { ) const db = createSqliteDatabase(config.inboxDbPath) const inboxRepository = new SqliteInboxRepository(db) + const heartbeatRepository = new SqliteHeartbeatRepository(db) const policies = [new TemperaturePolicy()] const anomalyDetector = new AnomalyDetector(policies) @@ -69,7 +73,14 @@ function bootstrap() { ], inboxRepository, clock, - logger + baseLogger.child({ service: 'inbox-worker' }) + ) + const heartbeatMonitor = new HeartbeatMonitor( + heartbeatRepository, + new HeartbeatTimeoutPolicy(config.heartbeatTimeoutMs), + config.mqtt.heartbeatTopic, + clock, + baseLogger.child({ service: 'heartbeat-monitor' }) ) mqtt.subscribe(`${config.mqtt.topic}/+`, { qos: 1 }) @@ -88,6 +99,10 @@ function bootstrap() { setInterval(() => { inboxWorker.run() }, 1000) + + setInterval(() => { + heartbeatMonitor.run() + }, 3000) } bootstrap() From d9a090cff809f3fe742535a1856915a896fefc15 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 21 Aug 2026 18:39:20 +0200 Subject: [PATCH 060/125] feat(machine): replace MachineStatus with MachineReachabilityStatus for improved clarity --- .../src/domain/machine/machine-reachability-status.ts | 4 ++++ .../src/domain/machine/machine-status.ts | 4 ---- .../domain/machine/policies/heatbeat-timeout-policy.ts | 8 ++++---- .../src/domain/machine/policies/machine-status-policy.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 microservices/coordinator-service/src/domain/machine/machine-reachability-status.ts delete mode 100644 microservices/coordinator-service/src/domain/machine/machine-status.ts diff --git a/microservices/coordinator-service/src/domain/machine/machine-reachability-status.ts b/microservices/coordinator-service/src/domain/machine/machine-reachability-status.ts new file mode 100644 index 00000000..c96a4867 --- /dev/null +++ b/microservices/coordinator-service/src/domain/machine/machine-reachability-status.ts @@ -0,0 +1,4 @@ +export enum MachineReachabilityStatus { + ONLINE = 'operational', + OFFLINE = 'off' +} diff --git a/microservices/coordinator-service/src/domain/machine/machine-status.ts b/microservices/coordinator-service/src/domain/machine/machine-status.ts deleted file mode 100644 index d6cc22dd..00000000 --- a/microservices/coordinator-service/src/domain/machine/machine-status.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum MachineStatus { - ONLINE = 'ONLINE', - OFFLINE = 'OFFLINE' -} diff --git a/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts b/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts index 11f237e5..48f25260 100644 --- a/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts +++ b/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts @@ -1,14 +1,14 @@ -import { MachineStatus } from '../machine-status' +import { MachineReachabilityStatus } from '../machine-reachability-status' import { MachineStatusPolicy } from './machine-status-policy' export class HeartbeatTimeoutPolicy implements MachineStatusPolicy { constructor(private readonly timeoutMs: number) {} - evaluate(lastHeartbeatAt: Date, now: Date): MachineStatus { + evaluate(lastHeartbeatAt: Date, now: Date): MachineReachabilityStatus { const elapsed = now.getTime() - lastHeartbeatAt.getTime() return elapsed >= this.timeoutMs - ? MachineStatus.OFFLINE - : MachineStatus.ONLINE + ? MachineReachabilityStatus.OFFLINE + : MachineReachabilityStatus.ONLINE } } diff --git a/microservices/coordinator-service/src/domain/machine/policies/machine-status-policy.ts b/microservices/coordinator-service/src/domain/machine/policies/machine-status-policy.ts index 97cd0201..3e61a1b1 100644 --- a/microservices/coordinator-service/src/domain/machine/policies/machine-status-policy.ts +++ b/microservices/coordinator-service/src/domain/machine/policies/machine-status-policy.ts @@ -1,4 +1,4 @@ -import { MachineStatus } from '../machine-status' +import { MachineReachabilityStatus } from '../machine-reachability-status' /** * A policy that defines how to evaluate the status of a machine based on its last heartbeat timestamp and the current time. @@ -8,7 +8,7 @@ export interface MachineStatusPolicy { * Evaluates the status of a machine based on its last heartbeat timestamp and the current time. * @param lastHeartbeatAt The timestamp of the last heartbeat received from the machine. * @param now The current timestamp to compare against the last heartbeat. - * @returns The evaluated status of the machine. + * @returns The reachability status of the machine, either ONLINE or OFFLINE, based on the evaluation of the policy. */ - evaluate(lastHeartbeatAt: Date, now: Date): MachineStatus + evaluate(lastHeartbeatAt: Date, now: Date): MachineReachabilityStatus } From 24f38d411bb386c691acb71b01a7dafd43df74b6 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 21 Aug 2026 18:59:27 +0200 Subject: [PATCH 061/125] feat(config): add coreRest configuration to AppConfig with url and token --- microservices/coordinator-service/.env.example | 7 ++++++- microservices/coordinator-service/src/config/env.ts | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/.env.example b/microservices/coordinator-service/.env.example index 68800dcb..e21426fb 100644 --- a/microservices/coordinator-service/.env.example +++ b/microservices/coordinator-service/.env.example @@ -1,9 +1,14 @@ NODE_ENV=development + MQTT_URL=mqtt://mqtt-broker:1883 MQTT_TOPIC=factory/telemetry -INTERVAL_MS=2000 MQTT_HEARTBEAT_TOPIC=factory/heartbeat + +INTERVAL_MS=2000 HEARTBEAT_INTERVAL_MS=5000 HEARTBEAT_TIMEOUT_MS=10000 +CORE_REST_URL=http://core-rest-service:3000 +CORE_REST_SERVICE_TOKEN=change-me-in-development + INBOX_DB_PATH=./data/coordinator_inbox.db \ No newline at end of file diff --git a/microservices/coordinator-service/src/config/env.ts b/microservices/coordinator-service/src/config/env.ts index fea2b676..111c8c9e 100644 --- a/microservices/coordinator-service/src/config/env.ts +++ b/microservices/coordinator-service/src/config/env.ts @@ -16,6 +16,10 @@ export type AppConfig = { topic: string heartbeatTopic: string } + coreRest: { + url: string + serviceToken: string + } intervalMs: number heartbeatIntervalMs: number heartbeatTimeoutMs: number @@ -30,6 +34,10 @@ export function getConfig(): AppConfig { topic: process.env.MQTT_TOPIC!, heartbeatTopic: process.env.MQTT_HEARTBEAT_TOPIC! }, + coreRest: { + url: process.env.CORE_REST_URL!, + serviceToken: process.env.CORE_REST_SERVICE_TOKEN! + }, intervalMs: Number(process.env.INTERVAL_MS), heartbeatIntervalMs: Number(process.env.HEARTBEAT_INTERVAL_MS), heartbeatTimeoutMs: Number(process.env.HEARTBEAT_TIMEOUT_MS), From e860a4a6e19b35c508b77ed20a01026ab4b1d4c8 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 21 Aug 2026 19:22:44 +0200 Subject: [PATCH 062/125] feat(machine): add MachineId to MachineCOnfig and update related configurations --- .../coordinator-service/src/domain/machine/machine-config.ts | 2 ++ microservices/coordinator-service/src/main.ts | 2 ++ microservices/coordinator-service/test/utils/fake-config.ts | 3 +++ 3 files changed, 7 insertions(+) diff --git a/microservices/coordinator-service/src/domain/machine/machine-config.ts b/microservices/coordinator-service/src/domain/machine/machine-config.ts index 93bd70a7..c40730a7 100644 --- a/microservices/coordinator-service/src/domain/machine/machine-config.ts +++ b/microservices/coordinator-service/src/domain/machine/machine-config.ts @@ -4,8 +4,10 @@ import { Emission } from './value-objects/emission' import { PowerConsumption } from './value-objects/power-consumption' import { Temperature } from './value-objects/temperature' import { Vibration } from './value-objects/vibration' +import { MachineId } from './value-objects/machine-id' export class MachineConfig { constructor( + public readonly machineId: MachineId, public readonly temperature: Range, public readonly powerConsumption: Range, public readonly emissions: Range, diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 976fd37d..f4cb953e 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -24,9 +24,11 @@ import { InboxWorker } from './application/workers/inbox-worker' import { SqliteHeartbeatRepository } from './infrastructure/persistence/sqlite/sqlite-heartbeat.repository' import { HeartbeatTimeoutPolicy } from './domain/machine/policies/heatbeat-timeout-policy' import { HeartbeatMonitor } from './application/workers/heartbeat-monitor' +import { MachineId } from './domain/machine/value-objects/machine-id' function buildMachineConfig(): MachineConfig { return new MachineConfig( + new MachineId('6a8771c13ec0b58b0b1ac2af'), new Range(new Temperature(0), new Temperature(100)), new Range( new PowerConsumption(0), diff --git a/microservices/coordinator-service/test/utils/fake-config.ts b/microservices/coordinator-service/test/utils/fake-config.ts index 34e4b980..f0a48d34 100644 --- a/microservices/coordinator-service/test/utils/fake-config.ts +++ b/microservices/coordinator-service/test/utils/fake-config.ts @@ -8,9 +8,12 @@ import { Vibration } from '@/domain/machine/value-objects/vibration' import { Pressure } from '@/domain/machine/value-objects/pressure' import { MACHINE_LIMITS } from '../constants/machine-limits' +import { MachineId } from '@/domain/machine/value-objects/machine-id' export function fakeConfig(overrides?: any): MachineConfig { return new MachineConfig( + new MachineId(overrides?.machineId ?? 'fake-machine-id'), + new Range( new Temperature( overrides?.temperature?.min ?? MACHINE_LIMITS.TEMPERATURE.MIN From 43f46e75c35299def412a7539de8e69f56124d87 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 21 Aug 2026 23:28:04 +0200 Subject: [PATCH 063/125] refactor(eslint): update ESLint rules --- microservices/coordinator-service/eslint.config.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/eslint.config.js b/microservices/coordinator-service/eslint.config.js index b3a59e9b..6340ecee 100644 --- a/microservices/coordinator-service/eslint.config.js +++ b/microservices/coordinator-service/eslint.config.js @@ -14,7 +14,18 @@ export default [ sourceType: 'module' }, rules: { - '@typescript-eslint/no-unused-vars': 'warn' + '@typescript-eslint/no-unused-vars': 'warn', + } + }, + + { + files: ['test/**/*.ts'], + languageOptions: { + ecmaVersion: 2024, + sourceType: 'module' + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off' } } ] \ No newline at end of file From b5ea14d15ef6777f4c00f02465ba6941e29ffeec Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 21 Aug 2026 23:34:06 +0200 Subject: [PATCH 064/125] refactor(test): update HeartbeatTimeoutPolicy spec expectations to use consistent operational status --- .../domain/machine/heartbeat-timeout-policy.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/microservices/coordinator-service/test/domain/machine/heartbeat-timeout-policy.spec.ts b/microservices/coordinator-service/test/domain/machine/heartbeat-timeout-policy.spec.ts index 59075db5..cf84bd48 100644 --- a/microservices/coordinator-service/test/domain/machine/heartbeat-timeout-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/machine/heartbeat-timeout-policy.spec.ts @@ -4,30 +4,30 @@ import { HeartbeatTimeoutPolicy } from '@/domain/machine/policies/heatbeat-timeo describe('HeartbeatTimeoutPolicy', () => { const policy = new HeartbeatTimeoutPolicy(10_000) - it('should return ONLINE when heartbeat is within timeout', () => { + it('should return operational when heartbeat is within timeout', () => { const lastHeartbeatAt = new Date('2026-08-18T12:00:00.000Z') const now = new Date('2026-08-18T12:00:05.000Z') const result = policy.evaluate(lastHeartbeatAt, now) - expect(result).toBe('ONLINE') + expect(result).toBe('operational') }) - it('should return OFFLINE when heartbeat reaches timeout', () => { + it('should return off when heartbeat reaches timeout', () => { const lastHeartbeatAt = new Date('2026-08-18T12:00:00.000Z') const now = new Date('2026-08-18T12:00:10.000Z') const result = policy.evaluate(lastHeartbeatAt, now) - expect(result).toBe('OFFLINE') + expect(result).toBe('off') }) - it('should return OFFLINE when heartbeat exceeds timeout', () => { + it('should return off when heartbeat exceeds timeout', () => { const lastHeartbeatAt = new Date('2026-08-18T12:00:00.000Z') const now = new Date('2026-08-18T12:00:15.000Z') const result = policy.evaluate(lastHeartbeatAt, now) - expect(result).toBe('OFFLINE') + expect(result).toBe('off') }) }) From b2753891e2c7eb7b09a41a0275de687822ef5388 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 22 Aug 2026 12:21:03 +0200 Subject: [PATCH 065/125] feat(core-rest-service): add CoreRestService interface for fetching machine configurations --- .../src/application/ports/core-rest-service.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 microservices/coordinator-service/src/application/ports/core-rest-service.ts diff --git a/microservices/coordinator-service/src/application/ports/core-rest-service.ts b/microservices/coordinator-service/src/application/ports/core-rest-service.ts new file mode 100644 index 00000000..8d2de81e --- /dev/null +++ b/microservices/coordinator-service/src/application/ports/core-rest-service.ts @@ -0,0 +1,16 @@ +import { MachineConfig } from '@/domain/machine/machine-config' + +/** + * Defines the interface for the core REST service. + */ +export interface CoreRestService { + /** + * Fetches machine configurations from the core REST API. + * @param params The parameters for fetching machine configurations, including limit and offset. + * @returns A promise that resolves to an array of MachineConfig objects. + */ + getMachineConfigs(params: { + limit: number + offset: number + }): Promise +} From 1366a783210167b4d2027f53a944294fb2da5ccf Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 22 Aug 2026 12:25:57 +0200 Subject: [PATCH 066/125] feat(core-rest-service): add MachineDto interface for machine data representation --- .../adapters/core-rest/dto/machine.dto.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts new file mode 100644 index 00000000..c9a8f88b --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts @@ -0,0 +1,59 @@ +export interface MachineDto { + id: string + serial: string + name: string + + location: { + areaId: string + } + + machineState: { + currentState: 'operational' | 'off' | 'anomaly' + anomalyDetails: unknown[] + } + + specifications: { + powerConsumption: { + measurementUnit: string + normalRange: { + min: number + max: number + } + } + + emissions: { + measurementUnit: string + normalRange: { + min: number + max: number + } + } + + operatingTemperature: { + measurementUnit: string + normalRange: { + min: number + max: number + } + } + + vibration: { + measurementUnit: string + normalRange: { + min: number + max: number + } + } + + pressure: { + measurementUnit: string + normalRange: { + min: number + max: number + } + } + } + + createdAt: string + updatedAt: string +} From e0856ddeb0f4d5c9c140d66ef9254a4b30017098 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 22 Aug 2026 12:26:56 +0200 Subject: [PATCH 067/125] feat(core-rest-service): implement toMachineConfig function for mapping MachineDto to MachineConfig --- .../core-rest/mapper/machine-config.mapper.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/machine-config.mapper.ts diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/machine-config.mapper.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/machine-config.mapper.ts new file mode 100644 index 00000000..0ab8f4be --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/machine-config.mapper.ts @@ -0,0 +1,41 @@ +import { MachineConfig } from '@/domain' +import { Range } from '@/domain/shared/value-objects/range/range' + +import { MachineDto } from '../dto/machine.dto' +import { Temperature } from '@/domain/machine/value-objects/temperature' +import { MachineId } from '@/domain/machine/value-objects/machine-id' +import { Pressure } from '@/domain/machine/value-objects/pressure' +import { Vibration } from '@/domain/machine/value-objects/vibration' +import { Emission } from '@/domain/machine/value-objects/emission' +import { PowerConsumption } from '@/domain/machine/value-objects/power-consumption' + +export function toMachineConfig(dto: MachineDto): MachineConfig { + return new MachineConfig( + new MachineId(dto.id), + + new Range( + new Temperature(dto.specifications.operatingTemperature.normalRange.min), + new Temperature(dto.specifications.operatingTemperature.normalRange.max) + ), + + new Range( + new PowerConsumption(dto.specifications.powerConsumption.normalRange.min), + new PowerConsumption(dto.specifications.powerConsumption.normalRange.max) + ), + + new Range( + new Emission(dto.specifications.emissions.normalRange.min), + new Emission(dto.specifications.emissions.normalRange.max) + ), + + new Range( + new Vibration(dto.specifications.vibration.normalRange.min), + new Vibration(dto.specifications.vibration.normalRange.max) + ), + + new Range( + new Pressure(dto.specifications.pressure.normalRange.min), + new Pressure(dto.specifications.pressure.normalRange.max) + ) + ) +} From 69603f380bb2f7651132ea8a0473dea202f4274e Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 22 Aug 2026 12:27:32 +0200 Subject: [PATCH 068/125] feat(core-rest-service): implement HttpCoreRestService --- .../core-rest/http-rest-core-service.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts new file mode 100644 index 00000000..f0803bab --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts @@ -0,0 +1,42 @@ +import { CoreRestService } from '@/application/ports/core-rest-service' +import { Logger } from '@/application/ports/logger' +import { MachineConfig } from '@/domain' +import { toMachineConfig } from './mapper/machine-config.mapper' +import { MachineDto } from './dto/machine.dto' + +export class HttpCoreRestService implements CoreRestService { + constructor( + private readonly baseUrl: string, + private readonly serviceToken: string, + private readonly logger: Logger + ) {} + + async getMachineConfigs(params: { + limit: number + offset: number + }): Promise { + const searchParams = new URLSearchParams({ + limit: params.limit.toString(), + offset: params.offset.toString() + }) + const url = `${this.baseUrl}/api/machines?${searchParams.toString()}` + + this.logger.info(`Getting machine configs via Core REST API at ${url}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Service-Token': this.serviceToken + } + }) + + if (!response.ok) { + const body = await response.text() + throw new Error(`Core REST returned ${response.status}: ${body}`) + } + + const data: MachineDto[] = await response.json() + return data.map(toMachineConfig) + } +} From 6702507942c326c5afed5b143e6cebcb9ff75020 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 22 Aug 2026 12:33:01 +0200 Subject: [PATCH 069/125] feat(telemetry-processor): update machineConfig to machineConfigs map for improved handling of multiple configurations --- .../messaging/processor/telemetry-processor.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts index cd2f9b3f..0f8fc989 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts @@ -7,7 +7,7 @@ import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-m export class TelemetryProcessor implements MessageProcessor { constructor( private readonly processTelemetry: ProcessTelemetry, - private readonly machineConfig: MachineConfig, + private readonly machineConfigs: Map, private readonly telemetryTopicPrefix: string ) {} @@ -23,6 +23,10 @@ export class TelemetryProcessor implements MessageProcessor { machineId: inboxMessage.topic.split('/').pop(), ...JSON.parse(inboxMessage.payload.toString()) } - this.processTelemetry.execute(input, this.machineConfig) + const machineConfig = this.machineConfigs.get(input.machineId) + if (!machineConfig) { + throw new Error(`No configuration found for machine: ${input.machineId}`) + } + this.processTelemetry.execute(input, machineConfig) } } From 48204a26120b5334dcf6d7b6e567d2145eb34ffc Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 22 Aug 2026 12:33:59 +0200 Subject: [PATCH 070/125] feat(coordinator-service): refactor bootstrap to use HttpCoreRestService for fetching machine configurations --- microservices/coordinator-service/src/main.ts | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index f4cb953e..c5b3222a 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -3,13 +3,6 @@ import { ProcessTelemetry } from './application/telemetry/process-telemetry' import { AnomalyDetector } from './domain/anomaly/services/anomaly-detector' import { TemperaturePolicy } from './domain/anomaly/services/policies/temperature-policy' import { SystemClock } from './infrastructure/time/system-clock' -import { Temperature } from './domain/machine/value-objects/temperature' -import { Emission } from './domain/machine/value-objects/emission' -import { PowerConsumption } from './domain/machine/value-objects/power-consumption' -import { Pressure } from './domain/machine/value-objects/pressure' -import { Vibration } from './domain/machine/value-objects/vibration' -import { MachineConfig } from './domain/machine/machine-config' -import { Range } from './domain/shared/value-objects/range/range' import { createMqttConsumer } from './infrastructure/mqtt/consumer' import { getConfig } from './config/env' import { ProcessHeartbeat } from './application/heartbeat/process-heartbeat' @@ -24,23 +17,9 @@ import { InboxWorker } from './application/workers/inbox-worker' import { SqliteHeartbeatRepository } from './infrastructure/persistence/sqlite/sqlite-heartbeat.repository' import { HeartbeatTimeoutPolicy } from './domain/machine/policies/heatbeat-timeout-policy' import { HeartbeatMonitor } from './application/workers/heartbeat-monitor' -import { MachineId } from './domain/machine/value-objects/machine-id' +import { HttpCoreRestService } from './infrastructure/adapters/core-rest/http-rest-core-service' -function buildMachineConfig(): MachineConfig { - return new MachineConfig( - new MachineId('6a8771c13ec0b58b0b1ac2af'), - new Range(new Temperature(0), new Temperature(100)), - new Range( - new PowerConsumption(0), - new PowerConsumption(200) - ), - new Range(new Emission(0), new Emission(50)), - new Range(new Vibration(0), new Vibration(10)), - new Range(new Pressure(0), new Pressure(5)) - ) -} - -function bootstrap() { +async function bootstrap() { const config = getConfig() const clock = new SystemClock() const baseLogger = new PinoLogger(config.nodeEnv) @@ -56,11 +35,24 @@ function bootstrap() { const inboxRepository = new SqliteInboxRepository(db) const heartbeatRepository = new SqliteHeartbeatRepository(db) + const coreRestService = new HttpCoreRestService( + config.coreRest.url, + config.coreRest.serviceToken, + baseLogger.child({ service: 'http-core-rest-service' }) + ) + const policies = [new TemperaturePolicy()] const anomalyDetector = new AnomalyDetector(policies) const processTelemetry = new ProcessTelemetry(anomalyDetector, clock, logger) const processHeartbeat = new ProcessHeartbeat(clock, logger) - const machineConfig = buildMachineConfig() + const machineConfigs = await coreRestService.getMachineConfigs({ + limit: 50, + offset: 0 + }) + + const configsByMachineId = new Map( + machineConfigs.map((config) => [config.machineId.value, config]) + ) const inboxReceiver = new InboxReceiver(inboxRepository, logger) @@ -68,7 +60,7 @@ function bootstrap() { [ new TelemetryProcessor( processTelemetry, - machineConfig, + configsByMachineId, config.mqtt.topic ), new HeartbeatProcessor(processHeartbeat, config.mqtt.heartbeatTopic) From ddc2934dba92bc513b82b22f68c8fdc1d55378aa Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Sat, 22 Aug 2026 12:34:15 +0200 Subject: [PATCH 071/125] refactor(docker-compose): update MACHINE_ID for machine-simulator-1 to a specific identifier --- docker/docker-compose.simulators.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.simulators.yml b/docker/docker-compose.simulators.yml index 6659cd29..6c32e512 100644 --- a/docker/docker-compose.simulators.yml +++ b/docker/docker-compose.simulators.yml @@ -26,7 +26,7 @@ services: - mqtt-broker environment: NODE_ENV: development - MACHINE_ID: machine-1 + MACHINE_ID: 6a8771c13ec0b58b0b1ac2af MQTT_URL: mqtt://mqtt-broker:1883 MQTT_TOPIC: factory/telemetry INTERVAL_MS: 2000 From 6376217ef6a788a56648d8409ab79f312cc4102d Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 25 Aug 2026 12:27:02 +0200 Subject: [PATCH 072/125] fix(imports): update path import to use node:path --- .../infrastructure/persistence/sqlite/create-sqlite-database.ts | 2 +- .../infrastructure/persistence/sqlite/create-sqlite-database.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts index 6ddabb63..929f0a2e 100644 --- a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts @@ -1,6 +1,6 @@ import fs from 'node:fs' import Database from 'better-sqlite3' -import path from 'path/win32' +import path from 'node:path' export function createSqliteDatabase(inboxDbPath: string): Database.Database { const dataDir = path.resolve('data') diff --git a/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts b/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts index abf0de5c..bc3bfd8d 100644 --- a/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts +++ b/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts @@ -1,6 +1,6 @@ import fs from 'node:fs' import Database from 'better-sqlite3' -import path from 'path/win32' +import path from 'node:path' export function createSqliteDatabase(outboxDbPath: string): Database.Database { const dataDir = path.resolve('data') From ae89ddf93bdffbc1e1ab91494f5286244322a4ae Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 25 Aug 2026 13:02:28 +0200 Subject: [PATCH 073/125] feat(docker-compose): add volumes for coordinator and machine simulator data --- docker/docker-compose.simulators.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.simulators.yml b/docker/docker-compose.simulators.yml index 6c32e512..c5928692 100644 --- a/docker/docker-compose.simulators.yml +++ b/docker/docker-compose.simulators.yml @@ -11,6 +11,7 @@ services: context: ../microservices/coordinator-service depends_on: - mqtt-broker + - core-rest-service environment: NODE_ENV: development MQTT_URL: mqtt://mqtt-broker:1883 @@ -18,6 +19,8 @@ services: INTERVAL_MS: 2000 MQTT_HEARTBEAT_TOPIC: factory/heartbeat HEARTBEAT_INTERVAL_MS: 5000 + volumes: + - coordinator_data:/app/data machine-simulator-1: build: @@ -31,4 +34,10 @@ services: MQTT_TOPIC: factory/telemetry INTERVAL_MS: 2000 MQTT_HEARTBEAT_TOPIC: factory/heartbeat - HEARTBEAT_INTERVAL_MS: 5000 \ No newline at end of file + HEARTBEAT_INTERVAL_MS: 5000 + volumes: + - simulator_data:/app/data + +volumes: + coordinator_data: + simulator_data: \ No newline at end of file From 0c5e1a0bc47490862248df659ebe95161c8085de Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 25 Aug 2026 13:10:05 +0200 Subject: [PATCH 074/125] refactor(machine-dto): change anomalyDetails type --- .../src/infrastructure/adapters/core-rest/dto/machine.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts index c9a8f88b..f2978a9d 100644 --- a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts @@ -9,7 +9,7 @@ export interface MachineDto { machineState: { currentState: 'operational' | 'off' | 'anomaly' - anomalyDetails: unknown[] + anomalyDetails: string[] } specifications: { From d006d6bbe29d11bf45ddd2a86565b0f701480733 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 25 Aug 2026 13:15:38 +0200 Subject: [PATCH 075/125] feat(telemetry): add eventId and sequenceNumber to TelemetryEvent and TelemetryInput types --- .../src/application/telemetry/dto/telemetry-event.ts | 2 ++ .../src/application/telemetry/dto/telemetry-input.ts | 1 + .../src/application/telemetry/process-telemetry.ts | 3 +++ .../src/generator/telemetry-generator.ts | 5 +++++ .../machine-simulator-service/src/types/telemetry-event.ts | 1 + 5 files changed, 12 insertions(+) diff --git a/microservices/coordinator-service/src/application/telemetry/dto/telemetry-event.ts b/microservices/coordinator-service/src/application/telemetry/dto/telemetry-event.ts index c1b1aa31..c311e5ed 100644 --- a/microservices/coordinator-service/src/application/telemetry/dto/telemetry-event.ts +++ b/microservices/coordinator-service/src/application/telemetry/dto/telemetry-event.ts @@ -1,5 +1,7 @@ export type TelemetryEvent = { + eventId: string machineId: string + sequenceNumber: number occurredAt: Date processedAt: Date operatingTemperature: number diff --git a/microservices/coordinator-service/src/application/telemetry/dto/telemetry-input.ts b/microservices/coordinator-service/src/application/telemetry/dto/telemetry-input.ts index 882e204d..967e2c33 100644 --- a/microservices/coordinator-service/src/application/telemetry/dto/telemetry-input.ts +++ b/microservices/coordinator-service/src/application/telemetry/dto/telemetry-input.ts @@ -1,5 +1,6 @@ export type TelemetryInput = { machineId: string + sequenceNumber: number occurredAt: string operatingTemperature: string powerConsumption: string diff --git a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts index 7d198840..a21b9f75 100644 --- a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts +++ b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts @@ -5,6 +5,7 @@ import { Logger } from '../ports/logger' import { ProcessTelemetryResult } from './dto/process-telemetry-result' import { TelemetryEvent } from './dto/telemetry-event' import { TelemetryInput } from './dto/telemetry-input' +import { randomUUID } from 'crypto' export class ProcessTelemetry { constructor( @@ -33,7 +34,9 @@ export class ProcessTelemetry { this.logger.info('Processing telemetry data', input) const event: TelemetryEvent = { + eventId: randomUUID(), machineId, + sequenceNumber: input.sequenceNumber, occurredAt, processedAt: this.clock.now(), operatingTemperature: toFiniteNumber( diff --git a/microservices/machine-simulator-service/src/generator/telemetry-generator.ts b/microservices/machine-simulator-service/src/generator/telemetry-generator.ts index 61e9d67c..57d1fca6 100644 --- a/microservices/machine-simulator-service/src/generator/telemetry-generator.ts +++ b/microservices/machine-simulator-service/src/generator/telemetry-generator.ts @@ -3,12 +3,17 @@ import { SystemClock } from '../infrastructure/time/system-clock' import type { TelemetryEvent } from '../types/telemetry-event' import { random } from '../utils/random' +let sequenceNumber: number = 0 + export function generateTelemetry( config: TelemetryConfig, clock: SystemClock ): TelemetryEvent { + sequenceNumber++ + return { machineId: config.machineId, + sequenceNumber, occurredAt: clock.now().toISOString(), operatingTemperature: random( config.operatingTemperature.min, diff --git a/microservices/machine-simulator-service/src/types/telemetry-event.ts b/microservices/machine-simulator-service/src/types/telemetry-event.ts index e499d462..5e09742f 100644 --- a/microservices/machine-simulator-service/src/types/telemetry-event.ts +++ b/microservices/machine-simulator-service/src/types/telemetry-event.ts @@ -1,6 +1,7 @@ export type TelemetryEvent = { machineId: string occurredAt: string + sequenceNumber: number powerConsumption: number emissions: number operatingTemperature: number From 06433bce741d1d3a04747af3757cf57e8b8d0454 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 26 Aug 2026 20:26:44 +0200 Subject: [PATCH 076/125] feat(tests): add eventId and sequenceNumber to anomaly detector tests --- .../test/domain/anomaly/services/anomaly-detector.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts index dfd7d1d9..6aa2d6e0 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts @@ -12,7 +12,9 @@ describe('AnomalyDetector', () => { const detector = new AnomalyDetector([new TemperaturePolicy()]) const event: TelemetryEvent = { + eventId: 'event-1', machineId: MACHINE_VALUES.ID, + sequenceNumber: 1, occurredAt: new Date('2025-12-31T23:59:00.000Z'), processedAt: new Date('2026-01-01T00:00:00Z'), operatingTemperature: MACHINE_VALUES.TEMPERATURE.OVER, @@ -29,14 +31,16 @@ describe('AnomalyDetector', () => { }) const anomalies = detector.detect(event, config) expect(anomalies).toHaveLength(1) - expect(anomalies[0].sensorType).toBe('TEMPERATURE') + expect(anomalies[0].sensorType).toBe('operatingTemperature') expect(anomalies[0].value).toBe(event.operatingTemperature) }) it('should return an empty array when no policies detect anomalies', () => { const detector = new AnomalyDetector([new TemperaturePolicy()]) const event: TelemetryEvent = { + eventId: 'event-1', machineId: MACHINE_VALUES.ID, + sequenceNumber: 1, occurredAt: new Date('2025-12-31T23:59:00.000Z'), processedAt: new Date('2026-01-01T00:00:00Z'), operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, From b64ccb7b2b263afa0fd97c033a39f24afd88ceba Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 26 Aug 2026 20:36:22 +0200 Subject: [PATCH 077/125] refactor(sensor-type): update SensorType values to match correct casing --- .../src/domain/anomaly/value-objects/sensor-type.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/microservices/coordinator-service/src/domain/anomaly/value-objects/sensor-type.ts b/microservices/coordinator-service/src/domain/anomaly/value-objects/sensor-type.ts index 827498c1..5cb696e4 100644 --- a/microservices/coordinator-service/src/domain/anomaly/value-objects/sensor-type.ts +++ b/microservices/coordinator-service/src/domain/anomaly/value-objects/sensor-type.ts @@ -1,7 +1,7 @@ export enum SensorType { - TEMPERATURE = 'TEMPERATURE', - VIBRATION = 'VIBRATION', - PRESSURE = 'PRESSURE', - EMISSIONS = 'EMISSIONS', - POWER_CONSUMPTION = 'POWER_CONSUMPTION' + TEMPERATURE = 'operatingTemperature', + VIBRATION = 'vibration', + PRESSURE = 'pressure', + EMISSIONS = 'emissions', + POWER_CONSUMPTION = 'powerConsumption' } From 5f171e96a45e7498fc9bb672eca1de4d0494493c Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 3 Sep 2026 18:00:49 +0200 Subject: [PATCH 078/125] feat(telemetry): add TelemetryDto interface for telemetry data structure --- .../adapters/core-rest/dto/telemetry.dto.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/telemetry.dto.ts diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/telemetry.dto.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/telemetry.dto.ts new file mode 100644 index 00000000..a1216116 --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/telemetry.dto.ts @@ -0,0 +1,17 @@ +export interface TelemetryDto { + eventId: string + machineId: string + sequenceNumber: number + capturedAt: string + readings: { + powerConsumption: number + emissions: number + operatingTemperature: number + vibration: number + pressure: number + } + anomaly: { + isAnomaly: boolean + details: string[] + } +} From 3bc1000d83be3d4cde7a89c57e9d91c80a29587c Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 3 Sep 2026 18:01:22 +0200 Subject: [PATCH 079/125] feat(telemetry): implement toTelemetryDto function for mapping telemetry data --- .../core-rest/mapper/telemetry.mapper.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/telemetry.mapper.ts diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/telemetry.mapper.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/telemetry.mapper.ts new file mode 100644 index 00000000..5d1d5aed --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/telemetry.mapper.ts @@ -0,0 +1,24 @@ +import { TelemetryDto } from '../dto/telemetry.dto' +import { ProcessTelemetryResult } from '@/application/telemetry/dto/process-telemetry-result' + +export function toTelemetryDto( + telemetryData: ProcessTelemetryResult +): TelemetryDto { + return { + eventId: telemetryData.event.eventId, + machineId: telemetryData.event.machineId, + sequenceNumber: telemetryData.event.sequenceNumber, + capturedAt: telemetryData.event.occurredAt.toISOString(), + readings: { + powerConsumption: telemetryData.event.powerConsumption, + emissions: telemetryData.event.emissions, + operatingTemperature: telemetryData.event.operatingTemperature, + vibration: telemetryData.event.vibration, + pressure: telemetryData.event.pressure + }, + anomaly: { + isAnomaly: telemetryData.anomalies.length > 0, + details: telemetryData.anomalies.map((anomaly) => anomaly.sensorType) + } + } +} From 90b29231a44a8d895ae51638581b3753720ba29b Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 3 Sep 2026 18:01:55 +0200 Subject: [PATCH 080/125] feat(core-rest-service): add publishTelemetry method to interface for telemetry data publishing --- .../src/application/ports/core-rest-service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/microservices/coordinator-service/src/application/ports/core-rest-service.ts b/microservices/coordinator-service/src/application/ports/core-rest-service.ts index 8d2de81e..3dae4742 100644 --- a/microservices/coordinator-service/src/application/ports/core-rest-service.ts +++ b/microservices/coordinator-service/src/application/ports/core-rest-service.ts @@ -1,4 +1,5 @@ import { MachineConfig } from '@/domain/machine/machine-config' +import { ProcessTelemetryResult } from '../telemetry/dto/process-telemetry-result' /** * Defines the interface for the core REST service. @@ -13,4 +14,10 @@ export interface CoreRestService { limit: number offset: number }): Promise + + /** + * Publishes telemetry data to the core REST API. + * @param telemetryData The telemetry data to publish. + */ + publishTelemetry(telemetryData: ProcessTelemetryResult): Promise } From 9815187761085a61c9a6be15f7d6c1946398371a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 3 Sep 2026 18:04:40 +0200 Subject: [PATCH 081/125] feat(core-rest-service): implement publishTelemetry method --- .../core-rest/http-rest-core-service.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts index f0803bab..ec182499 100644 --- a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts @@ -3,6 +3,8 @@ import { Logger } from '@/application/ports/logger' import { MachineConfig } from '@/domain' import { toMachineConfig } from './mapper/machine-config.mapper' import { MachineDto } from './dto/machine.dto' +import { ProcessTelemetryResult } from '@/application/telemetry/dto/process-telemetry-result' +import { toTelemetryDto } from './mapper/telemetry.mapper' export class HttpCoreRestService implements CoreRestService { constructor( @@ -39,4 +41,26 @@ export class HttpCoreRestService implements CoreRestService { const data: MachineDto[] = await response.json() return data.map(toMachineConfig) } + + async publishTelemetry(telemetryData: ProcessTelemetryResult): Promise { + const url = `${this.baseUrl}/api/telemetry` + + this.logger.info(`Publishing telemetry data via Core REST API at ${url}`) + + const dto = toTelemetryDto(telemetryData) + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Service-Token': this.serviceToken + }, + body: JSON.stringify(dto) + }) + + if (!response.ok) { + const body = await response.text() + throw new Error(`Core REST returned ${response.status}: ${body}`) + } + } } From d6867265abd018da56d96e859d1eca56fedb1de1 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 3 Sep 2026 18:05:32 +0200 Subject: [PATCH 082/125] feat(messaging): update process methods to return Promises for async handling --- .../src/application/messaging/message-processor.ts | 2 +- .../application/messaging/processor/heartbeat-processor.ts | 2 +- .../src/application/workers/inbox-worker.ts | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/microservices/coordinator-service/src/application/messaging/message-processor.ts b/microservices/coordinator-service/src/application/messaging/message-processor.ts index bf18f89c..b08e5110 100644 --- a/microservices/coordinator-service/src/application/messaging/message-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/message-processor.ts @@ -15,5 +15,5 @@ export interface MessageProcessor { * Processes the incoming message. * @param inboxMessage The inbox message to be processed. */ - process(inboxMessage: InboxMessage): void + process(inboxMessage: InboxMessage): Promise } diff --git a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts index eabc4b65..0acb762f 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts @@ -16,7 +16,7 @@ export class HeartbeatProcessor implements MessageProcessor { return topic.startsWith(prefix) } - process(inboxMessage: InboxMessage): void { + async process(inboxMessage: InboxMessage): Promise { const input: HeartbeatInput = { machineId: inboxMessage.topic.split('/').pop(), ...JSON.parse(inboxMessage.payload.toString()) diff --git a/microservices/coordinator-service/src/application/workers/inbox-worker.ts b/microservices/coordinator-service/src/application/workers/inbox-worker.ts index 7eaea938..ef03966d 100644 --- a/microservices/coordinator-service/src/application/workers/inbox-worker.ts +++ b/microservices/coordinator-service/src/application/workers/inbox-worker.ts @@ -12,8 +12,9 @@ export class InboxWorker { private readonly logger: Logger ) {} - run(): void { + async run(): Promise { this.logger.info('Inbox worker started') + const messages = this.inboxRepository.findPending() for (const msg of messages) { @@ -32,7 +33,7 @@ export class InboxWorker { } try { - processor.process(msg) + await processor.process(msg) this.inboxRepository.updateStatus( msg.eventId, @@ -45,7 +46,7 @@ export class InboxWorker { } catch (err) { this.inboxRepository.updateStatus( msg.eventId, - InboxStatus.FAILED, + InboxStatus.PENDING, msg.attempts + 1, this.clock.now() ) From 0ecc95129cd9916e6fc2603fa6038b010883f64c Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 3 Sep 2026 18:06:03 +0200 Subject: [PATCH 083/125] feat(telemetry): refactor TelemetryProcessor to support async processing and integrate coreRestService --- .../messaging/processor/telemetry-processor.ts | 9 ++++++--- microservices/coordinator-service/src/main.ts | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts index 0f8fc989..9499d0a4 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts @@ -3,12 +3,14 @@ import { MachineConfig } from '@/domain/machine/machine-config' import { ProcessTelemetry } from '../../telemetry/process-telemetry' import { TelemetryInput } from '../../telemetry/dto/telemetry-input' import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { CoreRestService } from '@/application/ports/core-rest-service' export class TelemetryProcessor implements MessageProcessor { constructor( private readonly processTelemetry: ProcessTelemetry, private readonly machineConfigs: Map, - private readonly telemetryTopicPrefix: string + private readonly telemetryTopicPrefix: string, + private readonly coreRestService: CoreRestService ) {} canHandle(topic: string): boolean { @@ -18,7 +20,7 @@ export class TelemetryProcessor implements MessageProcessor { return topic.startsWith(prefix) } - process(inboxMessage: InboxMessage): void { + async process(inboxMessage: InboxMessage): Promise { const input: TelemetryInput = { machineId: inboxMessage.topic.split('/').pop(), ...JSON.parse(inboxMessage.payload.toString()) @@ -27,6 +29,7 @@ export class TelemetryProcessor implements MessageProcessor { if (!machineConfig) { throw new Error(`No configuration found for machine: ${input.machineId}`) } - this.processTelemetry.execute(input, machineConfig) + const result = this.processTelemetry.execute(input, machineConfig) + await this.coreRestService.publishTelemetry(result) } } diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index c5b3222a..2130711c 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -61,7 +61,8 @@ async function bootstrap() { new TelemetryProcessor( processTelemetry, configsByMachineId, - config.mqtt.topic + config.mqtt.topic, + coreRestService ), new HeartbeatProcessor(processHeartbeat, config.mqtt.heartbeatTopic) ], From 40b356285acc45bf5f3c5f98861d21cedc4fb5f0 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 4 Sep 2026 11:47:05 +0200 Subject: [PATCH 084/125] chore(machine-dto): change currentState type to string for flexibility --- .../src/infrastructure/adapters/core-rest/dto/machine.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts index f2978a9d..dd14f2a3 100644 --- a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/dto/machine.dto.ts @@ -8,7 +8,7 @@ export interface MachineDto { } machineState: { - currentState: 'operational' | 'off' | 'anomaly' + currentState: string anomalyDetails: string[] } From a4e41fddd048c35833238d1202339b0aab4c68b5 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 4 Sep 2026 11:47:54 +0200 Subject: [PATCH 085/125] feat(core-rest-service): add updateMachineState method for machine state updates --- .../src/application/ports/core-rest-service.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/microservices/coordinator-service/src/application/ports/core-rest-service.ts b/microservices/coordinator-service/src/application/ports/core-rest-service.ts index 3dae4742..55a3ec4b 100644 --- a/microservices/coordinator-service/src/application/ports/core-rest-service.ts +++ b/microservices/coordinator-service/src/application/ports/core-rest-service.ts @@ -20,4 +20,16 @@ export interface CoreRestService { * @param telemetryData The telemetry data to publish. */ publishTelemetry(telemetryData: ProcessTelemetryResult): Promise + + /** + * Updates the state of a machine in the core REST API. + * @param machineId The ID of the machine to update. + * @param currentState The new state of the machine. + * @param anomalyDetails Optional details about any anomalies detected. + */ + updateMachineState( + machineId: string, + currentState: string, + anomalyDetails?: string[] + ): Promise } From a3b092b13399ac5a1e88b83b4dafd561b66cf708 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 4 Sep 2026 18:06:13 +0200 Subject: [PATCH 086/125] feat(core-rest-service): implement updateMachineState --- .../core-rest/http-rest-core-service.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts index ec182499..ec526d0c 100644 --- a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts @@ -63,4 +63,36 @@ export class HttpCoreRestService implements CoreRestService { throw new Error(`Core REST returned ${response.status}: ${body}`) } } + + async updateMachineState( + machineId: string, + currentState: string, + anomalyDetails: string[] = [] + ): Promise { + const url = `${this.baseUrl}/api/machines/${machineId}/state` + + this.logger.info( + `Updating machine state for machine ${machineId} to ${currentState} via Core REST API at ${url}` + ) + + const response = await fetch(url, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-Service-Token': this.serviceToken + }, + body: JSON.stringify({ + currentState, + anomalyDetails + }) + }) + + if (!response.ok) { + const body = await response.text() + throw new Error(`Core REST returned ${response.status}: ${body}`) + } + + const data: MachineDto = await response.json() + return toMachineConfig(data) + } } From 45eb71977bbbee05d0eae1d09c56e523c6e44597 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 4 Sep 2026 18:08:10 +0200 Subject: [PATCH 087/125] feat(telemetry): enhance process method to update machine state based on anomalies --- .../messaging/processor/telemetry-processor.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts index 9499d0a4..b97c65c0 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts @@ -31,5 +31,18 @@ export class TelemetryProcessor implements MessageProcessor { } const result = this.processTelemetry.execute(input, machineConfig) await this.coreRestService.publishTelemetry(result) + + if (result.anomalies.length > 0) { + await this.coreRestService.updateMachineState( + input.machineId, + 'anomaly', + result.anomalies.map((anomaly) => anomaly.sensorType) + ) + } else { + await this.coreRestService.updateMachineState( + input.machineId, + 'operational' + ) + } } } From 887bcf00ddf5e48b77956fc5c4ac810574c14073 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 4 Sep 2026 18:11:28 +0200 Subject: [PATCH 088/125] chore(tests): add sequenceNumber to telemetry input --- .../test/application/telemetry/process-telemetry.spec.ts | 5 +++++ .../coordinator-service/test/constants/machine-values.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts b/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts index 2b1918fc..1893d661 100644 --- a/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts +++ b/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts @@ -25,6 +25,7 @@ describe('ProcessTelemetry', () => { const input: TelemetryInput = { machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, occurredAt: occurredAtDate.toISOString(), operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE.toString(), powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE.toString(), @@ -62,6 +63,7 @@ describe('ProcessTelemetry', () => { const input: TelemetryInput = { machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, occurredAt: occurredAtDate.toISOString(), operatingTemperature: MACHINE_VALUES.TEMPERATURE.OVER.toString(), powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE.toString(), @@ -98,6 +100,7 @@ describe('ProcessTelemetry', () => { const input: TelemetryInput = { machineId: '', + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, occurredAt: occurredAtDate.toISOString(), operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE.toString(), powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE.toString(), @@ -122,6 +125,7 @@ describe('ProcessTelemetry', () => { const input: TelemetryInput = { machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, occurredAt: 'invalid-date', operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE.toString(), powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE.toString(), @@ -146,6 +150,7 @@ describe('ProcessTelemetry', () => { const input: TelemetryInput = { machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, occurredAt: occurredAtDate.toISOString(), operatingTemperature: 'NaN', powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE.toString(), diff --git a/microservices/coordinator-service/test/constants/machine-values.ts b/microservices/coordinator-service/test/constants/machine-values.ts index 486d0a34..0fa04158 100644 --- a/microservices/coordinator-service/test/constants/machine-values.ts +++ b/microservices/coordinator-service/test/constants/machine-values.ts @@ -1,6 +1,8 @@ export const MACHINE_VALUES = { ID: 'M1', + SEQUENCE_NUMBER: 1, + TEMPERATURE: { SAFE: 55, OVER: 90, From bdc9f88c581a3c06c282cd79703894e42cd3ebcf Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 4 Sep 2026 18:13:14 +0200 Subject: [PATCH 089/125] feat(heartbeat-monitor): integrate coreRestService for machine state updates based on heartbeat status --- .../application/workers/heartbeat-monitor.ts | 17 ++++++++++++++++- microservices/coordinator-service/src/main.ts | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts b/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts index 4af24b60..f5062b3e 100644 --- a/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts +++ b/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts @@ -2,17 +2,20 @@ import { Clock } from '@/application/ports/clock' import { HeartbeatRepository } from '../ports/heartbeats.repository' import { HeartbeatTimeoutPolicy } from '@/domain/machine/policies/heatbeat-timeout-policy' import { Logger } from '../ports/logger' +import { CoreRestService } from '../ports/core-rest-service' +import { MachineReachabilityStatus } from '@/domain/machine/machine-reachability-status' export class HeartbeatMonitor { constructor( private readonly heartbeatRepository: HeartbeatRepository, private readonly timeoutPolicy: HeartbeatTimeoutPolicy, private readonly topicPrefix: string, + private readonly coreRestService: CoreRestService, private readonly clock: Clock, private readonly logger: Logger ) {} - run(): void { + async run(): Promise { const heartbeats = this.heartbeatRepository.findLatestForEachMachine( this.topicPrefix ) @@ -25,6 +28,18 @@ export class HeartbeatMonitor { machineId: heartbeat.machineId, status }) + + if (status === MachineReachabilityStatus.OFFLINE) { + await this.coreRestService.updateMachineState( + heartbeat.machineId, + 'off' + ) + } else if (status === MachineReachabilityStatus.ONLINE) { + await this.coreRestService.updateMachineState( + heartbeat.machineId, + 'operational' + ) + } } } } diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 2130711c..a7ff2735 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -74,6 +74,7 @@ async function bootstrap() { heartbeatRepository, new HeartbeatTimeoutPolicy(config.heartbeatTimeoutMs), config.mqtt.heartbeatTopic, + coreRestService, clock, baseLogger.child({ service: 'heartbeat-monitor' }) ) From 05bbfe89a6c9362f21a5e75cd61a9ec2e8c5d806 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 4 Sep 2026 18:19:23 +0200 Subject: [PATCH 090/125] chore(tests): add eventId and sequenceNumber to telemetry events in tests --- .../anomaly/services/policies/temperature-policy.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts index 4da80a84..7d0b9321 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts @@ -11,7 +11,9 @@ describe('TemperaturePolicy', () => { it('should return an anomaly with correct details when temperature is over the maximum limit', () => { const policy = new TemperaturePolicy() const event: TelemetryEvent = { + eventId: 'test-event-id', machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, occurredAt: new Date('2025-12-31T23:59:00.000Z'), processedAt: new Date('2026-01-01T00:00:00.000Z'), operatingTemperature: MACHINE_VALUES.TEMPERATURE.OVER, @@ -35,7 +37,9 @@ describe('TemperaturePolicy', () => { it('should detect temperature anomalies under the minimum limit', () => { const policy = new TemperaturePolicy() const event: TelemetryEvent = { + eventId: 'test-event-id', machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, occurredAt: new Date('2025-12-31T23:59:00.000Z'), processedAt: new Date('2026-01-01T00:00:00.000Z'), operatingTemperature: MACHINE_VALUES.TEMPERATURE.UNDER, @@ -59,7 +63,9 @@ describe('TemperaturePolicy', () => { it('should not detect any anomaly when temperature is within limits', () => { const policy = new TemperaturePolicy() const event: TelemetryEvent = { + eventId: 'test-event-id', machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, occurredAt: new Date('2025-12-31T23:59:00.000Z'), processedAt: new Date('2026-01-01T00:00:00.000Z'), operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, From c171191e593383f3aa83d7fd8a0b8edf844a3515 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 7 Sep 2026 17:57:35 +0200 Subject: [PATCH 091/125] feat(random): add randomOutsideRange function for generating values outside a specified range --- microservices/machine-simulator-service/src/utils/random.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/microservices/machine-simulator-service/src/utils/random.ts b/microservices/machine-simulator-service/src/utils/random.ts index 5c78ac16..68bd0117 100644 --- a/microservices/machine-simulator-service/src/utils/random.ts +++ b/microservices/machine-simulator-service/src/utils/random.ts @@ -1,3 +1,8 @@ export function random(min: number, max: number): number { return Math.random() * (max - min) + min } + +export function randomOutsideRange(min: number, max: number): number { + const margin = Math.max((max - min) * 0.1, 1) + return random(max + 0.01, max + margin) +} From 834e9e9789407c5b418f2785ce6d99ff21fca6d3 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 7 Sep 2026 17:59:49 +0200 Subject: [PATCH 092/125] feat(telemetry): add anomaly handling for telemetry generation using randomOutsideRange --- .../src/generator/telemetry-generator.ts | 41 +++++++++++++------ .../src/types/telemetry-config.ts | 4 ++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/microservices/machine-simulator-service/src/generator/telemetry-generator.ts b/microservices/machine-simulator-service/src/generator/telemetry-generator.ts index 57d1fca6..c06a17c8 100644 --- a/microservices/machine-simulator-service/src/generator/telemetry-generator.ts +++ b/microservices/machine-simulator-service/src/generator/telemetry-generator.ts @@ -1,7 +1,7 @@ import type { TelemetryConfig } from '../config/env' import { SystemClock } from '../infrastructure/time/system-clock' import type { TelemetryEvent } from '../types/telemetry-event' -import { random } from '../utils/random' +import { random, randomOutsideRange } from '../utils/random' let sequenceNumber: number = 0 @@ -15,16 +15,33 @@ export function generateTelemetry( machineId: config.machineId, sequenceNumber, occurredAt: clock.now().toISOString(), - operatingTemperature: random( - config.operatingTemperature.min, - config.operatingTemperature.max - ), - vibration: random(config.vibration.min, config.vibration.max), - powerConsumption: random( - config.powerConsumption.min, - config.powerConsumption.max - ), - emissions: random(config.emissions.min, config.emissions.max), - pressure: random(config.pressure.min, config.pressure.max) + operatingTemperature: config.anomalies.includes('temperature') + ? randomOutsideRange( + config.operatingTemperature.min, + config.operatingTemperature.max + ) + : random( + config.operatingTemperature.min, + config.operatingTemperature.max + ), + + vibration: config.anomalies.includes('vibration') + ? randomOutsideRange(config.vibration.min, config.vibration.max) + : random(config.vibration.min, config.vibration.max), + + powerConsumption: config.anomalies.includes('powerConsumption') + ? randomOutsideRange( + config.powerConsumption.min, + config.powerConsumption.max + ) + : random(config.powerConsumption.min, config.powerConsumption.max), + + emissions: config.anomalies.includes('emissions') + ? randomOutsideRange(config.emissions.min, config.emissions.max) + : random(config.emissions.min, config.emissions.max), + + pressure: config.anomalies.includes('pressure') + ? randomOutsideRange(config.pressure.min, config.pressure.max) + : random(config.pressure.min, config.pressure.max) } } diff --git a/microservices/machine-simulator-service/src/types/telemetry-config.ts b/microservices/machine-simulator-service/src/types/telemetry-config.ts index 2339d1e5..79462c2f 100644 --- a/microservices/machine-simulator-service/src/types/telemetry-config.ts +++ b/microservices/machine-simulator-service/src/types/telemetry-config.ts @@ -1,3 +1,6 @@ +export type AnomalyType = + 'temperature' | 'vibration' | 'pressure' | 'powerConsumption' | 'emissions' + export type TelemetryConfig = { machineId: string operatingTemperature: { min: number; max: number } @@ -5,4 +8,5 @@ export type TelemetryConfig = { emissions: { min: number; max: number } vibration: { min: number; max: number } pressure: { min: number; max: number } + anomalies: AnomalyType[] } From 885244ad1cd4106ba5beb0fb3f5f6bd97e25fe61 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 7 Sep 2026 18:00:44 +0200 Subject: [PATCH 093/125] feat(tests): add test for generating telemetry with specified anomalies --- .../test/generator/telemetry-generator.spec.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts b/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts index c05d4a9c..4d55effa 100644 --- a/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts +++ b/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest' import { generateTelemetry } from '../../src/generator/telemetry-generator' import type { TelemetryEvent } from '../../src/types/telemetry-event' import { MACHINE_LIMITS } from '../constants/machine-limits' +import { AnomalyType } from '../../src/types/telemetry-config' describe('generateTelemetry', () => { const fakeClock = { now: () => new Date('2026-01-01T00:00:00Z') } @@ -26,7 +27,8 @@ describe('generateTelemetry', () => { pressure: { min: MACHINE_LIMITS.PRESSURE.MIN, max: MACHINE_LIMITS.PRESSURE.MAX - } + }, + anomalies: [] } it('should generate telemetry with correct structure', () => { @@ -77,4 +79,18 @@ describe('generateTelemetry', () => { ) expect(telemetryEvent.pressure).toBeLessThanOrEqual(testConfig.pressure.max) }) + + it('should generate telemetry with anomalies if specified', () => { + const testConfigWithAnomalies = { + ...testConfig, + anomalies: ['temperature'] as AnomalyType[] + } + const telemetryEvent: TelemetryEvent = generateTelemetry( + testConfigWithAnomalies, + fakeClock + ) + expect(telemetryEvent.operatingTemperature).toBeGreaterThan( + testConfig.operatingTemperature.max + ) + }) }) From baf16d3e1c030593caddde1910354bf75940227a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 7 Sep 2026 18:03:14 +0200 Subject: [PATCH 094/125] feat(config): add anomaly parsing function and integrate into AppConfig --- .../src/config/env.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/microservices/machine-simulator-service/src/config/env.ts b/microservices/machine-simulator-service/src/config/env.ts index 82f92276..7ac082ab 100644 --- a/microservices/machine-simulator-service/src/config/env.ts +++ b/microservices/machine-simulator-service/src/config/env.ts @@ -1,5 +1,5 @@ import dotenv from 'dotenv' -import type { TelemetryConfig } from '../types/telemetry-config' +import type { AnomalyType, TelemetryConfig } from '../types/telemetry-config' dotenv.config() @@ -10,6 +10,30 @@ function toNodeEnv(value?: string): NodeEnv { return 'development' } +function parseAnomalies(value?: string): AnomalyType[] { + if (!value) return [] + + const validAnomalies: AnomalyType[] = [ + 'temperature', + 'vibration', + 'pressure', + 'powerConsumption', + 'emissions' + ] + + const anomalies = value + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + + for (const anomaly of anomalies) { + if (!validAnomalies.includes(anomaly as AnomalyType)) { + throw new Error(`Invalid anomaly type: ${anomaly}`) + } + } + return anomalies as AnomalyType[] +} + export type AppConfig = { nodeEnv: NodeEnv mqtt: { @@ -60,7 +84,9 @@ export function getConfig(): AppConfig { pressure: { min: Number(process.env.PRESSURE_MIN!), max: Number(process.env.PRESSURE_MAX!) - } + }, + + anomalies: parseAnomalies(process.env.ANOMALIES) } } } From 9f829e62c3e4ffcd5f5b09c7fcae5c77939e2f13 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 7 Sep 2026 18:21:47 +0200 Subject: [PATCH 095/125] feat(docker): add ANOMALIES environment variable to machine simulator --- docker/docker-compose.simulators.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/docker-compose.simulators.yml b/docker/docker-compose.simulators.yml index c5928692..79db498f 100644 --- a/docker/docker-compose.simulators.yml +++ b/docker/docker-compose.simulators.yml @@ -35,6 +35,7 @@ services: INTERVAL_MS: 2000 MQTT_HEARTBEAT_TOPIC: factory/heartbeat HEARTBEAT_INTERVAL_MS: 5000 + ANOMALIES: ${ANOMALIES:-} volumes: - simulator_data:/app/data From cab2ab19bbde71fb3cebf51233f8d029dd9c6506 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 7 Sep 2026 22:52:42 +0200 Subject: [PATCH 096/125] feat(config): enhance anomaly parsing with telemetry ranges and refactor related types --- .../src/config/env.ts | 81 ++++++++++--------- .../src/types/telemetry-config.ts | 14 ++-- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/microservices/machine-simulator-service/src/config/env.ts b/microservices/machine-simulator-service/src/config/env.ts index 7ac082ab..8bd40156 100644 --- a/microservices/machine-simulator-service/src/config/env.ts +++ b/microservices/machine-simulator-service/src/config/env.ts @@ -1,5 +1,9 @@ import dotenv from 'dotenv' -import type { AnomalyType, TelemetryConfig } from '../types/telemetry-config' +import { + type AnomalyType, + type TelemetryConfig, + type TelemetryRanges +} from '../types/telemetry-config' dotenv.config() @@ -10,27 +14,23 @@ function toNodeEnv(value?: string): NodeEnv { return 'development' } -function parseAnomalies(value?: string): AnomalyType[] { +function parseAnomalies( + value: string | undefined, + telemetryRanges: TelemetryRanges +): AnomalyType[] { if (!value) return [] - const validAnomalies: AnomalyType[] = [ - 'temperature', - 'vibration', - 'pressure', - 'powerConsumption', - 'emissions' - ] - const anomalies = value .split(',') .map((value) => value.trim()) .filter(Boolean) for (const anomaly of anomalies) { - if (!validAnomalies.includes(anomaly as AnomalyType)) { + if (!Object.hasOwn(telemetryRanges, anomaly)) { throw new Error(`Invalid anomaly type: ${anomaly}`) } } + return anomalies as AnomalyType[] } @@ -48,45 +48,50 @@ export type AppConfig = { } export function getConfig(): AppConfig { + const telemetryRanges: TelemetryRanges = { + operatingTemperature: { + min: Number(process.env.OPERATING_TEMPERATURE_MIN!), + max: Number(process.env.OPERATING_TEMPERATURE_MAX!) + }, + + powerConsumption: { + min: Number(process.env.POWER_CONSUMPTION_MIN!), + max: Number(process.env.POWER_CONSUMPTION_MAX!) + }, + + emissions: { + min: Number(process.env.EMISSIONS_MIN!), + max: Number(process.env.EMISSIONS_MAX!) + }, + + vibration: { + min: Number(process.env.VIBRATION_MIN!), + max: Number(process.env.VIBRATION_MAX!) + }, + + pressure: { + min: Number(process.env.PRESSURE_MIN!), + max: Number(process.env.PRESSURE_MAX!) + } + } + return { nodeEnv: toNodeEnv(process.env.NODE_ENV), + mqtt: { url: process.env.MQTT_URL!, topic: process.env.MQTT_TOPIC!, heartbeatTopic: process.env.MQTT_HEARTBEAT_TOPIC! }, + intervalMs: Number(process.env.INTERVAL_MS!), heartbeatIntervalMs: Number(process.env.HEARTBEAT_INTERVAL_MS!), outboxDbPath: process.env.OUTBOX_DB_PATH!, + telemetry: { machineId: process.env.MACHINE_ID!, - - operatingTemperature: { - min: Number(process.env.OPERATING_TEMPERATURE_MIN!), - max: Number(process.env.OPERATING_TEMPERATURE_MAX!) - }, - - powerConsumption: { - min: Number(process.env.POWER_CONSUMPTION_MIN!), - max: Number(process.env.POWER_CONSUMPTION_MAX!) - }, - - emissions: { - min: Number(process.env.EMISSIONS_MIN!), - max: Number(process.env.EMISSIONS_MAX!) - }, - - vibration: { - min: Number(process.env.VIBRATION_MIN!), - max: Number(process.env.VIBRATION_MAX!) - }, - - pressure: { - min: Number(process.env.PRESSURE_MIN!), - max: Number(process.env.PRESSURE_MAX!) - }, - - anomalies: parseAnomalies(process.env.ANOMALIES) + ...telemetryRanges, + anomalies: parseAnomalies(process.env.ANOMALIES, telemetryRanges) } } } diff --git a/microservices/machine-simulator-service/src/types/telemetry-config.ts b/microservices/machine-simulator-service/src/types/telemetry-config.ts index 79462c2f..b7a6c2f0 100644 --- a/microservices/machine-simulator-service/src/types/telemetry-config.ts +++ b/microservices/machine-simulator-service/src/types/telemetry-config.ts @@ -1,12 +1,14 @@ -export type AnomalyType = - 'temperature' | 'vibration' | 'pressure' | 'powerConsumption' | 'emissions' - -export type TelemetryConfig = { - machineId: string +export type TelemetryRanges = { operatingTemperature: { min: number; max: number } powerConsumption: { min: number; max: number } emissions: { min: number; max: number } vibration: { min: number; max: number } pressure: { min: number; max: number } - anomalies: AnomalyType[] } + +export type AnomalyType = keyof TelemetryRanges + +export type TelemetryConfig = { + machineId: string + anomalies: AnomalyType[] +} & TelemetryRanges From 24266a4a5380e4502b02ab60e52aed7af8d84daf Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 11:11:13 +0200 Subject: [PATCH 097/125] refactor(telemetry): refactor telemetry generation adding value generation and update test accordingly --- .../src/generator/telemetry-generator.ts | 45 +++++++------------ .../generator/telemetry-generator.spec.ts | 2 +- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/microservices/machine-simulator-service/src/generator/telemetry-generator.ts b/microservices/machine-simulator-service/src/generator/telemetry-generator.ts index c06a17c8..140f58d3 100644 --- a/microservices/machine-simulator-service/src/generator/telemetry-generator.ts +++ b/microservices/machine-simulator-service/src/generator/telemetry-generator.ts @@ -1,10 +1,22 @@ import type { TelemetryConfig } from '../config/env' import { SystemClock } from '../infrastructure/time/system-clock' +import { TelemetryRanges } from '../types/telemetry-config' import type { TelemetryEvent } from '../types/telemetry-event' import { random, randomOutsideRange } from '../utils/random' let sequenceNumber: number = 0 +function generateValue( + config: TelemetryConfig, + key: T +): number { + const range = config[key] as { min: number; max: number } + + return config.anomalies.includes(key) + ? randomOutsideRange(range.min, range.max) + : random(range.min, range.max) +} + export function generateTelemetry( config: TelemetryConfig, clock: SystemClock @@ -15,33 +27,10 @@ export function generateTelemetry( machineId: config.machineId, sequenceNumber, occurredAt: clock.now().toISOString(), - operatingTemperature: config.anomalies.includes('temperature') - ? randomOutsideRange( - config.operatingTemperature.min, - config.operatingTemperature.max - ) - : random( - config.operatingTemperature.min, - config.operatingTemperature.max - ), - - vibration: config.anomalies.includes('vibration') - ? randomOutsideRange(config.vibration.min, config.vibration.max) - : random(config.vibration.min, config.vibration.max), - - powerConsumption: config.anomalies.includes('powerConsumption') - ? randomOutsideRange( - config.powerConsumption.min, - config.powerConsumption.max - ) - : random(config.powerConsumption.min, config.powerConsumption.max), - - emissions: config.anomalies.includes('emissions') - ? randomOutsideRange(config.emissions.min, config.emissions.max) - : random(config.emissions.min, config.emissions.max), - - pressure: config.anomalies.includes('pressure') - ? randomOutsideRange(config.pressure.min, config.pressure.max) - : random(config.pressure.min, config.pressure.max) + operatingTemperature: generateValue(config, 'operatingTemperature'), + vibration: generateValue(config, 'vibration'), + powerConsumption: generateValue(config, 'powerConsumption'), + emissions: generateValue(config, 'emissions'), + pressure: generateValue(config, 'pressure') } } diff --git a/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts b/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts index 4d55effa..b314042f 100644 --- a/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts +++ b/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts @@ -83,7 +83,7 @@ describe('generateTelemetry', () => { it('should generate telemetry with anomalies if specified', () => { const testConfigWithAnomalies = { ...testConfig, - anomalies: ['temperature'] as AnomalyType[] + anomalies: ['operatingTemperature'] as AnomalyType[] } const telemetryEvent: TelemetryEvent = generateTelemetry( testConfigWithAnomalies, From 41c6f5e81e447a48271fdc45bb6cbb34fb5707f8 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:03:20 +0200 Subject: [PATCH 098/125] feat(anomaly): implement VibrationPolicy to evaluate vibration anomalies --- .../services/policies/vibration-policy.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts new file mode 100644 index 00000000..2ca914a6 --- /dev/null +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts @@ -0,0 +1,28 @@ +import { randomUUID } from 'crypto' +import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' +import { MachineConfig } from '@/domain/machine/machine-config' +import { MachineId } from '@/domain/machine/value-objects/machine-id' +import { Anomaly } from '@/domain/anomaly/anomaly' +import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' +import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' +import { AnomalyPolicy } from './anomaly-policy' +import { Vibration } from '@/domain/machine/value-objects/vibration' + +export class VibrationPolicy implements AnomalyPolicy { + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + const anomalies: Anomaly[] = [] + if (!config.vibration.contains(new Vibration(event.vibration))) { + anomalies.push( + new Anomaly( + new AnomalyEventId(randomUUID()), + new MachineId(event.machineId), + SensorType.VIBRATION, + event.vibration, + event.occurredAt, + event.processedAt + ) + ) + } + return anomalies + } +} From d4d37381170b6348bd5ccb8f5f40d48a93ee23d3 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:05:15 +0200 Subject: [PATCH 099/125] feat(anomaly): add VibrationPolicy to anomaly detection policies --- microservices/coordinator-service/src/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index a7ff2735..0f322cb1 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -18,6 +18,7 @@ import { SqliteHeartbeatRepository } from './infrastructure/persistence/sqlite/s import { HeartbeatTimeoutPolicy } from './domain/machine/policies/heatbeat-timeout-policy' import { HeartbeatMonitor } from './application/workers/heartbeat-monitor' import { HttpCoreRestService } from './infrastructure/adapters/core-rest/http-rest-core-service' +import { VibrationPolicy } from './domain/anomaly/services/policies/vibration-policy' async function bootstrap() { const config = getConfig() @@ -41,7 +42,7 @@ async function bootstrap() { baseLogger.child({ service: 'http-core-rest-service' }) ) - const policies = [new TemperaturePolicy()] + const policies = [new TemperaturePolicy(), new VibrationPolicy()] const anomalyDetector = new AnomalyDetector(policies) const processTelemetry = new ProcessTelemetry(anomalyDetector, clock, logger) const processHeartbeat = new ProcessHeartbeat(clock, logger) From ec4252958467b614ddd004f65a7be8b108885409 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:05:48 +0200 Subject: [PATCH 100/125] feat(telemetry): add test for generating telemetry with multiple anomalies --- .../test/generator/telemetry-generator.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts b/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts index b314042f..b3aefc86 100644 --- a/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts +++ b/microservices/machine-simulator-service/test/generator/telemetry-generator.spec.ts @@ -93,4 +93,19 @@ describe('generateTelemetry', () => { testConfig.operatingTemperature.max ) }) + + it('should generate telemetry with multiple anomalies if specified', () => { + const testConfigWithAnomalies = { + ...testConfig, + anomalies: ['operatingTemperature', 'vibration'] as AnomalyType[] + } + const telemetryEvent: TelemetryEvent = generateTelemetry( + testConfigWithAnomalies, + fakeClock + ) + expect(telemetryEvent.operatingTemperature).toBeGreaterThan( + testConfig.operatingTemperature.max + ) + expect(telemetryEvent.vibration).toBeGreaterThan(testConfig.vibration.max) + }) }) From 664418d3cca6c227bd88665c90bb4a381494286c Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:20:25 +0200 Subject: [PATCH 101/125] feat(anomaly): implement PressurePolicy for pressure anomaly detection --- .../services/policies/pressure-policy.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts new file mode 100644 index 00000000..e1af1806 --- /dev/null +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts @@ -0,0 +1,28 @@ +import { randomUUID } from 'crypto' +import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' +import { MachineConfig } from '@/domain/machine/machine-config' +import { MachineId } from '@/domain/machine/value-objects/machine-id' +import { Anomaly } from '@/domain/anomaly/anomaly' +import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' +import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' +import { AnomalyPolicy } from './anomaly-policy' +import { Pressure } from '@/domain/machine/value-objects/pressure' + +export class PressurePolicy implements AnomalyPolicy { + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + const anomalies: Anomaly[] = [] + if (!config.pressure.contains(new Pressure(event.pressure))) { + anomalies.push( + new Anomaly( + new AnomalyEventId(randomUUID()), + new MachineId(event.machineId), + SensorType.PRESSURE, + event.pressure, + event.occurredAt, + event.processedAt + ) + ) + } + return anomalies + } +} From c2475b5e260722c37442243aed87d2cc35af4328 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:21:42 +0200 Subject: [PATCH 102/125] feat(anomaly): add PressurePolicy to anomaly detection policies --- microservices/coordinator-service/src/main.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 0f322cb1..44da8fe9 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -19,6 +19,7 @@ import { HeartbeatTimeoutPolicy } from './domain/machine/policies/heatbeat-timeo import { HeartbeatMonitor } from './application/workers/heartbeat-monitor' import { HttpCoreRestService } from './infrastructure/adapters/core-rest/http-rest-core-service' import { VibrationPolicy } from './domain/anomaly/services/policies/vibration-policy' +import { PressurePolicy } from './domain/anomaly/services/policies/pressure-policy' async function bootstrap() { const config = getConfig() @@ -42,7 +43,11 @@ async function bootstrap() { baseLogger.child({ service: 'http-core-rest-service' }) ) - const policies = [new TemperaturePolicy(), new VibrationPolicy()] + const policies = [ + new TemperaturePolicy(), + new VibrationPolicy(), + new PressurePolicy() + ] const anomalyDetector = new AnomalyDetector(policies) const processTelemetry = new ProcessTelemetry(anomalyDetector, clock, logger) const processHeartbeat = new ProcessHeartbeat(clock, logger) From 533dc8a57ce344eae007d184ab4295b943fe9cff Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:26:09 +0200 Subject: [PATCH 103/125] feat(anomaly): implement EmissionPolicy --- .../services/policies/emissions-policy.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 microservices/coordinator-service/src/domain/anomaly/services/policies/emissions-policy.ts diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/emissions-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/emissions-policy.ts new file mode 100644 index 00000000..0765016d --- /dev/null +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/emissions-policy.ts @@ -0,0 +1,28 @@ +import { randomUUID } from 'crypto' +import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' +import { MachineConfig } from '@/domain/machine/machine-config' +import { MachineId } from '@/domain/machine/value-objects/machine-id' +import { Anomaly } from '@/domain/anomaly/anomaly' +import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' +import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' +import { AnomalyPolicy } from './anomaly-policy' +import { Emission } from '@/domain/machine/value-objects/emission' + +export class EmissionPolicy implements AnomalyPolicy { + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + const anomalies: Anomaly[] = [] + if (!config.emissions.contains(new Emission(event.emissions))) { + anomalies.push( + new Anomaly( + new AnomalyEventId(randomUUID()), + new MachineId(event.machineId), + SensorType.EMISSIONS, + event.emissions, + event.occurredAt, + event.processedAt + ) + ) + } + return anomalies + } +} From b960a46fafe4ca07a30dc6177d9858be3ada0a70 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:26:40 +0200 Subject: [PATCH 104/125] feat(anomaly): add EmissionPolicy to anomaly detection policies --- microservices/coordinator-service/src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 44da8fe9..27f30f91 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -20,6 +20,7 @@ import { HeartbeatMonitor } from './application/workers/heartbeat-monitor' import { HttpCoreRestService } from './infrastructure/adapters/core-rest/http-rest-core-service' import { VibrationPolicy } from './domain/anomaly/services/policies/vibration-policy' import { PressurePolicy } from './domain/anomaly/services/policies/pressure-policy' +import { EmissionPolicy } from './domain/anomaly/services/policies/emissions-policy' async function bootstrap() { const config = getConfig() @@ -46,7 +47,8 @@ async function bootstrap() { const policies = [ new TemperaturePolicy(), new VibrationPolicy(), - new PressurePolicy() + new PressurePolicy(), + new EmissionPolicy() ] const anomalyDetector = new AnomalyDetector(policies) const processTelemetry = new ProcessTelemetry(anomalyDetector, clock, logger) From 8fa6f109e64814424fe09f418157f650921e0577 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:28:24 +0200 Subject: [PATCH 105/125] refactor(anomaly): update import path for EmissionPolicy --- .../policies/{emissions-policy.ts => emission-policy.ts} | 0 microservices/coordinator-service/src/main.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename microservices/coordinator-service/src/domain/anomaly/services/policies/{emissions-policy.ts => emission-policy.ts} (100%) diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/emissions-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts similarity index 100% rename from microservices/coordinator-service/src/domain/anomaly/services/policies/emissions-policy.ts rename to microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 27f30f91..b36fbeb1 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -20,7 +20,7 @@ import { HeartbeatMonitor } from './application/workers/heartbeat-monitor' import { HttpCoreRestService } from './infrastructure/adapters/core-rest/http-rest-core-service' import { VibrationPolicy } from './domain/anomaly/services/policies/vibration-policy' import { PressurePolicy } from './domain/anomaly/services/policies/pressure-policy' -import { EmissionPolicy } from './domain/anomaly/services/policies/emissions-policy' +import { EmissionPolicy } from './domain/anomaly/services/policies/emission-policy' async function bootstrap() { const config = getConfig() From 3efae3f990fdcf04e16b2a08b2594cd1d3120bb2 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:42:13 +0200 Subject: [PATCH 106/125] feat(anomaly): implement PowerConsumptionPolicy --- .../policies/power-consumption-policy.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts new file mode 100644 index 00000000..97f180ae --- /dev/null +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts @@ -0,0 +1,32 @@ +import { randomUUID } from 'crypto' +import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' +import { MachineConfig } from '@/domain/machine/machine-config' +import { MachineId } from '@/domain/machine/value-objects/machine-id' +import { Anomaly } from '@/domain/anomaly/anomaly' +import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' +import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' +import { AnomalyPolicy } from './anomaly-policy' +import { PowerConsumption } from '@/domain/machine/value-objects/power-consumption' + +export class PowerConsumptionPolicy implements AnomalyPolicy { + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + const anomalies: Anomaly[] = [] + if ( + !config.powerConsumption.contains( + new PowerConsumption(event.powerConsumption) + ) + ) { + anomalies.push( + new Anomaly( + new AnomalyEventId(randomUUID()), + new MachineId(event.machineId), + SensorType.POWER_CONSUMPTION, + event.powerConsumption, + event.occurredAt, + event.processedAt + ) + ) + } + return anomalies + } +} From 3f3f0ad0d445d5daf0396812fc7af56fd1a14e3a Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 8 Sep 2026 18:43:36 +0200 Subject: [PATCH 107/125] feat(anomaly): add PowerConsumptionPolicy to anomaly detection policies --- microservices/coordinator-service/src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index b36fbeb1..41abb317 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -21,6 +21,7 @@ import { HttpCoreRestService } from './infrastructure/adapters/core-rest/http-re import { VibrationPolicy } from './domain/anomaly/services/policies/vibration-policy' import { PressurePolicy } from './domain/anomaly/services/policies/pressure-policy' import { EmissionPolicy } from './domain/anomaly/services/policies/emission-policy' +import { PowerConsumptionPolicy } from './domain/anomaly/services/policies/power-consumption-policy' async function bootstrap() { const config = getConfig() @@ -48,7 +49,8 @@ async function bootstrap() { new TemperaturePolicy(), new VibrationPolicy(), new PressurePolicy(), - new EmissionPolicy() + new EmissionPolicy(), + new PowerConsumptionPolicy() ] const anomalyDetector = new AnomalyDetector(policies) const processTelemetry = new ProcessTelemetry(anomalyDetector, clock, logger) From ff6922e529c011c2d47b3891f901192fbc628d20 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 9 Sep 2026 11:13:45 +0200 Subject: [PATCH 108/125] test(anomaly): add unit tests for EmissionPolicy, PowerConsumptionPolicy, PressurePolicy, and VibrationPolicy --- .../services/policies/emission-policy.spec.ts | 60 +++++++++++++++++++ .../policies/power-consumption-policy.spec.ts | 60 +++++++++++++++++++ .../services/policies/pressure-policy.spec.ts | 60 +++++++++++++++++++ .../policies/vibration-policy.spec.ts | 60 +++++++++++++++++++ 4 files changed, 240 insertions(+) create mode 100644 microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts create mode 100644 microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts create mode 100644 microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts create mode 100644 microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts new file mode 100644 index 00000000..df9f3d15 --- /dev/null +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' +import { MACHINE_VALUES } from '@test/constants/machine-values' +import { fakeConfig } from '@test/utils/fake-config' +import { MachineConfig } from '@/domain/machine/machine-config' +import { MACHINE_LIMITS } from '@test/constants/machine-limits' +import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' +import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' +import { EmissionPolicy } from '@/domain/anomaly/services/policies/emission-policy' + +describe('EmissionPolicy', () => { + it('should return an anomaly with correct details when emission is over the maximum limit', () => { + const policy = new EmissionPolicy() + const event: TelemetryEvent = { + eventId: 'test-event-id', + machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00.000Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + emissions: MACHINE_VALUES.EMISSION.OVER, + vibration: MACHINE_VALUES.VIBRATION.SAFE, + pressure: MACHINE_VALUES.PRESSURE.SAFE + } + const config: MachineConfig = fakeConfig({ + emissions: { + min: MACHINE_LIMITS.EMISSION.MIN, + max: MACHINE_LIMITS.EMISSION.MAX + } + }) + const anomalies = policy.evaluate(event, config) + expect(anomalies).toHaveLength(1) + expect(anomalies[0].sensorType).toBe(SensorType.EMISSIONS) + expect(anomalies[0].value).toBe(event.emissions) + }) + + it('should not detect any anomaly when emissions is within limits', () => { + const policy = new EmissionPolicy() + const event: TelemetryEvent = { + eventId: 'test-event-id', + machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00.000Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + emissions: MACHINE_VALUES.EMISSION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.SAFE, + pressure: MACHINE_VALUES.PRESSURE.SAFE + } + const config: MachineConfig = fakeConfig({ + emissions: { + min: MACHINE_LIMITS.EMISSION.MIN, + max: MACHINE_LIMITS.EMISSION.MAX + } + }) + const anomalies = policy.evaluate(event, config) + expect(anomalies).toHaveLength(0) + }) +}) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts new file mode 100644 index 00000000..83fce05b --- /dev/null +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' +import { MACHINE_VALUES } from '@test/constants/machine-values' +import { fakeConfig } from '@test/utils/fake-config' +import { MachineConfig } from '@/domain/machine/machine-config' +import { MACHINE_LIMITS } from '@test/constants/machine-limits' +import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' +import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' +import { PowerConsumptionPolicy } from '@/domain/anomaly/services/policies/power-consumption-policy' + +describe('PowerConsumptionPolicy', () => { + it('should return an anomaly with correct details when power consumption is over the maximum limit', () => { + const policy = new PowerConsumptionPolicy() + const event: TelemetryEvent = { + eventId: 'test-event-id', + machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00.000Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.OVER, + emissions: MACHINE_VALUES.EMISSION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.SAFE, + pressure: MACHINE_VALUES.PRESSURE.SAFE + } + const config: MachineConfig = fakeConfig({ + powerConsumption: { + min: MACHINE_LIMITS.POWER_CONSUMPTION.MIN, + max: MACHINE_LIMITS.POWER_CONSUMPTION.MAX + } + }) + const anomalies = policy.evaluate(event, config) + expect(anomalies).toHaveLength(1) + expect(anomalies[0].sensorType).toBe(SensorType.POWER_CONSUMPTION) + expect(anomalies[0].value).toBe(event.powerConsumption) + }) + + it('should not detect any anomaly when power consumption is within limits', () => { + const policy = new PowerConsumptionPolicy() + const event: TelemetryEvent = { + eventId: 'test-event-id', + machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00.000Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + emissions: MACHINE_VALUES.EMISSION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.SAFE, + pressure: MACHINE_VALUES.PRESSURE.SAFE + } + const config: MachineConfig = fakeConfig({ + powerConsumption: { + min: MACHINE_LIMITS.POWER_CONSUMPTION.MIN, + max: MACHINE_LIMITS.POWER_CONSUMPTION.MAX + } + }) + const anomalies = policy.evaluate(event, config) + expect(anomalies).toHaveLength(0) + }) +}) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts new file mode 100644 index 00000000..bd22c182 --- /dev/null +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' +import { MACHINE_VALUES } from '@test/constants/machine-values' +import { fakeConfig } from '@test/utils/fake-config' +import { MachineConfig } from '@/domain/machine/machine-config' +import { MACHINE_LIMITS } from '@test/constants/machine-limits' +import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' +import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' +import { PressurePolicy } from '@/domain/anomaly/services/policies/pressure-policy' + +describe('PressurePolicy', () => { + it('should return an anomaly with correct details when pressure is over the maximum limit', () => { + const policy = new PressurePolicy() + const event: TelemetryEvent = { + eventId: 'test-event-id', + machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00.000Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + emissions: MACHINE_VALUES.EMISSION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.SAFE, + pressure: MACHINE_VALUES.PRESSURE.OVER + } + const config: MachineConfig = fakeConfig({ + pressure: { + min: MACHINE_LIMITS.PRESSURE.MIN, + max: MACHINE_LIMITS.PRESSURE.MAX + } + }) + const anomalies = policy.evaluate(event, config) + expect(anomalies).toHaveLength(1) + expect(anomalies[0].sensorType).toBe(SensorType.PRESSURE) + expect(anomalies[0].value).toBe(event.pressure) + }) + + it('should not detect any anomaly when pressure is within limits', () => { + const policy = new PressurePolicy() + const event: TelemetryEvent = { + eventId: 'test-event-id', + machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00.000Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + emissions: MACHINE_VALUES.EMISSION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.SAFE, + pressure: MACHINE_VALUES.PRESSURE.SAFE + } + const config: MachineConfig = fakeConfig({ + pressure: { + min: MACHINE_LIMITS.PRESSURE.MIN, + max: MACHINE_LIMITS.PRESSURE.MAX + } + }) + const anomalies = policy.evaluate(event, config) + expect(anomalies).toHaveLength(0) + }) +}) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts new file mode 100644 index 00000000..fd138b8d --- /dev/null +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' +import { MACHINE_VALUES } from '@test/constants/machine-values' +import { fakeConfig } from '@test/utils/fake-config' +import { MachineConfig } from '@/domain/machine/machine-config' +import { MACHINE_LIMITS } from '@test/constants/machine-limits' +import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' +import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' +import { VibrationPolicy } from '@/domain/anomaly/services/policies/vibration-policy' + +describe('VibrationPolicy', () => { + it('should return an anomaly with correct details when vibration is over the maximum limit', () => { + const policy = new VibrationPolicy() + const event: TelemetryEvent = { + eventId: 'test-event-id', + machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00.000Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + emissions: MACHINE_VALUES.EMISSION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.OVER, + pressure: MACHINE_VALUES.PRESSURE.SAFE + } + const config: MachineConfig = fakeConfig({ + vibration: { + min: MACHINE_LIMITS.VIBRATION.MIN, + max: MACHINE_LIMITS.VIBRATION.MAX + } + }) + const anomalies = policy.evaluate(event, config) + expect(anomalies).toHaveLength(1) + expect(anomalies[0].sensorType).toBe(SensorType.VIBRATION) + expect(anomalies[0].value).toBe(event.vibration) + }) + + it('should not detect any anomaly when vibration is within limits', () => { + const policy = new VibrationPolicy() + const event: TelemetryEvent = { + eventId: 'test-event-id', + machineId: MACHINE_VALUES.ID, + sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00.000Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + emissions: MACHINE_VALUES.EMISSION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.SAFE, + pressure: MACHINE_VALUES.PRESSURE.SAFE + } + const config: MachineConfig = fakeConfig({ + vibration: { + min: MACHINE_LIMITS.VIBRATION.MIN, + max: MACHINE_LIMITS.VIBRATION.MAX + } + }) + const anomalies = policy.evaluate(event, config) + expect(anomalies).toHaveLength(0) + }) +}) From 020a2eda718cd66a9d83e67120ab891e73882bd6 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Thu, 10 Sep 2026 12:03:40 +0200 Subject: [PATCH 109/125] test(anomaly): enhance anomaly detection tests with other cases --- .../anomaly/services/anomaly-detector.spec.ts | 90 +++++++++++++++++-- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts index 6aa2d6e0..8d042347 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts @@ -6,9 +6,34 @@ import { MachineConfig } from '@/domain/machine/machine-config' import { MACHINE_LIMITS } from '@test/constants/machine-limits' import { MACHINE_VALUES } from '@test/constants/machine-values' import { fakeConfig } from '@test/utils/fake-config' +import { VibrationPolicy } from '@/domain/anomaly/services/policies/vibration-policy' describe('AnomalyDetector', () => { - it('should aggregate anomalies from policies', () => { + it('should return an empty array when no policies detect anomalies', () => { + const detector = new AnomalyDetector([new TemperaturePolicy()]) + const event: TelemetryEvent = { + eventId: 'event-1', + machineId: MACHINE_VALUES.ID, + sequenceNumber: 1, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + emissions: MACHINE_VALUES.EMISSION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.SAFE, + pressure: MACHINE_VALUES.PRESSURE.SAFE + } + const config: MachineConfig = fakeConfig({ + temperature: { + min: MACHINE_LIMITS.TEMPERATURE.MIN, + max: MACHINE_LIMITS.TEMPERATURE.MAX + } + }) + const anomalies = detector.detect(event, config) + expect(anomalies).toHaveLength(0) + }) + + it('should detect an anomaly from a single policy', () => { const detector = new AnomalyDetector([new TemperaturePolicy()]) const event: TelemetryEvent = { @@ -31,12 +56,63 @@ describe('AnomalyDetector', () => { }) const anomalies = detector.detect(event, config) expect(anomalies).toHaveLength(1) - expect(anomalies[0].sensorType).toBe('operatingTemperature') - expect(anomalies[0].value).toBe(event.operatingTemperature) + expect(anomalies).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + sensorType: 'operatingTemperature', + value: event.operatingTemperature + }) + ]) + ) }) - it('should return an empty array when no policies detect anomalies', () => { + it('should detect anomalies from multiple policies', () => { + const detector = new AnomalyDetector([ + new TemperaturePolicy(), + new VibrationPolicy() + ]) + + const event: TelemetryEvent = { + eventId: 'event-1', + machineId: MACHINE_VALUES.ID, + sequenceNumber: 1, + occurredAt: new Date('2025-12-31T23:59:00.000Z'), + processedAt: new Date('2026-01-01T00:00:00Z'), + operatingTemperature: MACHINE_VALUES.TEMPERATURE.OVER, + powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + emissions: MACHINE_VALUES.EMISSION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.OVER, + pressure: MACHINE_VALUES.PRESSURE.SAFE + } + const config: MachineConfig = fakeConfig({ + temperature: { + min: MACHINE_LIMITS.TEMPERATURE.MIN, + max: MACHINE_LIMITS.TEMPERATURE.MAX + }, + vibration: { + min: MACHINE_LIMITS.VIBRATION.MIN, + max: MACHINE_LIMITS.VIBRATION.MAX + } + }) + const anomalies = detector.detect(event, config) + expect(anomalies).toHaveLength(2) + expect(anomalies).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + sensorType: 'operatingTemperature', + value: event.operatingTemperature + }), + expect.objectContaining({ + sensorType: 'vibration', + value: event.vibration + }) + ]) + ) + }) + + it('should not detect an anomaly when no policy is defined for the sensor type', () => { const detector = new AnomalyDetector([new TemperaturePolicy()]) + const event: TelemetryEvent = { eventId: 'event-1', machineId: MACHINE_VALUES.ID, @@ -46,13 +122,17 @@ describe('AnomalyDetector', () => { operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, + vibration: MACHINE_VALUES.VIBRATION.OVER, pressure: MACHINE_VALUES.PRESSURE.SAFE } const config: MachineConfig = fakeConfig({ temperature: { min: MACHINE_LIMITS.TEMPERATURE.MIN, max: MACHINE_LIMITS.TEMPERATURE.MAX + }, + vibration: { + min: MACHINE_LIMITS.VIBRATION.MIN, + max: MACHINE_LIMITS.VIBRATION.MAX } }) const anomalies = detector.detect(event, config) From 66bab0f5d40b97741775c9a01c1fd522338cb886 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 21 Sep 2026 19:12:54 +0200 Subject: [PATCH 110/125] feat(errors): add MachineNotFoundError class for error handling --- .../src/application/errors/machine-not-found.error.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 microservices/coordinator-service/src/application/errors/machine-not-found.error.ts diff --git a/microservices/coordinator-service/src/application/errors/machine-not-found.error.ts b/microservices/coordinator-service/src/application/errors/machine-not-found.error.ts new file mode 100644 index 00000000..26f28e84 --- /dev/null +++ b/microservices/coordinator-service/src/application/errors/machine-not-found.error.ts @@ -0,0 +1,6 @@ +export class MachineNotFoundError extends Error { + constructor(machineId: string) { + super(`Machine not found: ${machineId}`) + this.name = 'MachineNotFoundError' + } +} From 9c608f986d7d33825f16c4efc885dbb1a1d6ee8d Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 21 Sep 2026 19:14:23 +0200 Subject: [PATCH 111/125] feat(errors): throw MachineNotFoundError for 404 responses in HttpCoreRestService --- .../adapters/core-rest/http-rest-core-service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts index ec526d0c..e35fe944 100644 --- a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts @@ -5,6 +5,7 @@ import { toMachineConfig } from './mapper/machine-config.mapper' import { MachineDto } from './dto/machine.dto' import { ProcessTelemetryResult } from '@/application/telemetry/dto/process-telemetry-result' import { toTelemetryDto } from './mapper/telemetry.mapper' +import { MachineNotFoundError } from '@/application/errors/machine-not-found.error' export class HttpCoreRestService implements CoreRestService { constructor( @@ -87,6 +88,10 @@ export class HttpCoreRestService implements CoreRestService { }) }) + if (response.status === 404) { + throw new MachineNotFoundError(machineId) + } + if (!response.ok) { const body = await response.text() throw new Error(`Core REST returned ${response.status}: ${body}`) From 3016d4b7bc26e32e223508b2274bd85f33c3e501 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 22 Sep 2026 12:02:28 +0200 Subject: [PATCH 112/125] chore(machine): normalize MachineId initialization and update tests to use MACHINE_IDS constants --- .../src/domain/machine/value-objects/machine-id.ts | 10 +++++++--- .../test/constants/machine-values.ts | 7 ++++++- .../test/domain/anomaly/anomaly.spec.ts | 6 +++--- .../test/domain/machine/machine-factory.spec.ts | 5 +++-- .../test/domain/machine/machine.spec.ts | 10 +++++----- .../domain/machine/value-objects/machine-id.spec.ts | 9 +++++---- .../coordinator-service/test/utils/fake-config.ts | 3 ++- 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/microservices/coordinator-service/src/domain/machine/value-objects/machine-id.ts b/microservices/coordinator-service/src/domain/machine/value-objects/machine-id.ts index 4c8ad363..67646e35 100644 --- a/microservices/coordinator-service/src/domain/machine/value-objects/machine-id.ts +++ b/microservices/coordinator-service/src/domain/machine/value-objects/machine-id.ts @@ -1,10 +1,14 @@ import { InvalidMachineIdError } from '../errors/invalid-machine-id.error' export class MachineId { - constructor(public readonly value: string) { - if (!value || value.trim().length === 0) { - throw new InvalidMachineIdError() + public readonly value: string + + constructor(value: string) { + const normalizedValue = value.trim() + if (!/^[a-fA-F0-9]{24}$/.test(normalizedValue)) { + throw new InvalidMachineIdError(normalizedValue) } + this.value = normalizedValue } equals(other: MachineId): boolean { diff --git a/microservices/coordinator-service/test/constants/machine-values.ts b/microservices/coordinator-service/test/constants/machine-values.ts index 0fa04158..2ee58dbf 100644 --- a/microservices/coordinator-service/test/constants/machine-values.ts +++ b/microservices/coordinator-service/test/constants/machine-values.ts @@ -1,5 +1,10 @@ +export const MACHINE_IDS = { + DEFAULT: '6a8771c13ec0b58b0b1ac2af', + SECOND: '7a8771a63ec0b58b0b1ac2ae' +} as const + export const MACHINE_VALUES = { - ID: 'M1', + ID: MACHINE_IDS.DEFAULT, SEQUENCE_NUMBER: 1, diff --git a/microservices/coordinator-service/test/domain/anomaly/anomaly.spec.ts b/microservices/coordinator-service/test/domain/anomaly/anomaly.spec.ts index fb21dfce..4f25ae4c 100644 --- a/microservices/coordinator-service/test/domain/anomaly/anomaly.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/anomaly.spec.ts @@ -12,14 +12,14 @@ describe('Anomaly', () => { const processedAt = new Date('2026-01-01T00:00:00.000Z') const anomaly = new Anomaly( new AnomalyEventId('A1'), - new MachineId('M1'), + new MachineId(MACHINE_VALUES.ID), SensorType.TEMPERATURE, MACHINE_VALUES.TEMPERATURE.OVER, occurredAt, processedAt ) expect(anomaly.id).toEqual(new AnomalyEventId('A1')) - expect(anomaly.machineId).toEqual(new MachineId('M1')) + expect(anomaly.machineId).toEqual(new MachineId(MACHINE_VALUES.ID)) expect(anomaly.sensorType).toEqual(SensorType.TEMPERATURE) expect(anomaly.value).toEqual(MACHINE_VALUES.TEMPERATURE.OVER) expect(anomaly.occurredAt).toEqual(occurredAt) @@ -31,7 +31,7 @@ describe('Anomaly', () => { () => new Anomaly( new AnomalyEventId(''), - new MachineId('M1'), + new MachineId(MACHINE_VALUES.ID), SensorType.TEMPERATURE, MACHINE_VALUES.TEMPERATURE.OVER, new Date('2025-12-31T23:59:00.000Z'), diff --git a/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts b/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts index 4f57bcbd..a4f0d80a 100644 --- a/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts +++ b/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts @@ -1,10 +1,11 @@ import { describe, expect, it } from 'vitest' import { MachineFactory } from '@/domain/machine/machine-factory' +import { MACHINE_IDS } from '@test/constants/machine-values' describe('MachineFactory', () => { it('should create a machine from sensors data', () => { const telemetryInput = { - id: 'M1', + id: MACHINE_IDS.DEFAULT, temperature: 50, powerConsumption: 100, emissions: 20, @@ -22,7 +23,7 @@ describe('MachineFactory', () => { ) expect(machine).toMatchObject({ - id: expect.objectContaining({ value: 'M1' }), + id: expect.objectContaining({ value: MACHINE_IDS.DEFAULT }), temperature: expect.objectContaining({ value: 50 }), powerConsumption: expect.objectContaining({ value: 100 }), emissions: expect.objectContaining({ value: 20 }), diff --git a/microservices/coordinator-service/test/domain/machine/machine.spec.ts b/microservices/coordinator-service/test/domain/machine/machine.spec.ts index dce9e114..39ce2d40 100644 --- a/microservices/coordinator-service/test/domain/machine/machine.spec.ts +++ b/microservices/coordinator-service/test/domain/machine/machine.spec.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest' import { Machine } from '@/domain/machine/machine' import { fakeConfig } from '@test/utils/fake-config' -import { MACHINE_VALUES } from '@test/constants/machine-values' +import { MACHINE_IDS, MACHINE_VALUES } from '@test/constants/machine-values' import { MachineConfig } from '@/domain/machine/machine-config' import { MACHINE_LIMITS } from '@test/constants/machine-limits' import { fakeMachine } from '@test/utils/fake-machine' @@ -9,7 +9,7 @@ import { fakeMachine } from '@test/utils/fake-machine' describe('Machine', () => { it('is equal to another machine with the same id', () => { const machine1 = fakeMachine({ - id: 'M1', + id: MACHINE_VALUES.ID, temperature: MACHINE_VALUES.TEMPERATURE.SAFE, powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, emissions: MACHINE_VALUES.EMISSION.SAFE, @@ -17,7 +17,7 @@ describe('Machine', () => { pressure: MACHINE_VALUES.PRESSURE.SAFE }) const machine2 = fakeMachine({ - id: 'M1', + id: MACHINE_VALUES.ID, temperature: MACHINE_VALUES.TEMPERATURE.OVER, powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.OVER, emissions: MACHINE_VALUES.EMISSION.OVER, @@ -28,8 +28,8 @@ describe('Machine', () => { }) it('is not equal to another machine with different id', () => { - const machine1 = fakeMachine({ id: 'M1' }) - const machine2 = fakeMachine({ id: 'M2' }) + const machine1 = fakeMachine({ id: MACHINE_IDS.DEFAULT }) + const machine2 = fakeMachine({ id: MACHINE_IDS.SECOND }) expect(machine1.isEqual(machine2)).toBe(false) }) diff --git a/microservices/coordinator-service/test/domain/machine/value-objects/machine-id.spec.ts b/microservices/coordinator-service/test/domain/machine/value-objects/machine-id.spec.ts index ff1c31f4..34005a4e 100644 --- a/microservices/coordinator-service/test/domain/machine/value-objects/machine-id.spec.ts +++ b/microservices/coordinator-service/test/domain/machine/value-objects/machine-id.spec.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest' import { MachineId } from '@/domain/machine/value-objects/machine-id' import { InvalidMachineIdError } from '@/domain/machine/errors/invalid-machine-id.error' +import { MACHINE_IDS } from '@test/constants/machine-values' describe('MachineId', () => { it('throws error when id is empty', () => { @@ -8,14 +9,14 @@ describe('MachineId', () => { }) it('equals method should return true for same value', () => { - const id1 = new MachineId('M1') - const id2 = new MachineId('M1') + const id1 = new MachineId(MACHINE_IDS.DEFAULT) + const id2 = new MachineId(MACHINE_IDS.DEFAULT) expect(id1.equals(id2)).toBe(true) }) it('equals method should return false for different values', () => { - const id1 = new MachineId('M1') - const id2 = new MachineId('M2') + const id1 = new MachineId(MACHINE_IDS.DEFAULT) + const id2 = new MachineId(MACHINE_IDS.SECOND) expect(id1.equals(id2)).toBe(false) }) }) diff --git a/microservices/coordinator-service/test/utils/fake-config.ts b/microservices/coordinator-service/test/utils/fake-config.ts index f0a48d34..77de0bf8 100644 --- a/microservices/coordinator-service/test/utils/fake-config.ts +++ b/microservices/coordinator-service/test/utils/fake-config.ts @@ -9,10 +9,11 @@ import { Pressure } from '@/domain/machine/value-objects/pressure' import { MACHINE_LIMITS } from '../constants/machine-limits' import { MachineId } from '@/domain/machine/value-objects/machine-id' +import { MACHINE_IDS } from '@test/constants/machine-values' export function fakeConfig(overrides?: any): MachineConfig { return new MachineConfig( - new MachineId(overrides?.machineId ?? 'fake-machine-id'), + new MachineId(overrides?.machineId ?? MACHINE_IDS.DEFAULT), new Range( new Temperature( From ca6a1d52475bf8ebf2656143096c936cd51d3e63 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 22 Sep 2026 12:17:21 +0200 Subject: [PATCH 113/125] feat(errors): add ConfigurationNotFoundError for handling missing configurations --- .../src/application/errors/configuration-not-found.error.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 microservices/coordinator-service/src/application/errors/configuration-not-found.error.ts diff --git a/microservices/coordinator-service/src/application/errors/configuration-not-found.error.ts b/microservices/coordinator-service/src/application/errors/configuration-not-found.error.ts new file mode 100644 index 00000000..64ee8494 --- /dev/null +++ b/microservices/coordinator-service/src/application/errors/configuration-not-found.error.ts @@ -0,0 +1,6 @@ +export class ConfigurationNotFoundError extends Error { + constructor(machineId: string) { + super(`Configuration not found for machine with ID: ${machineId}`) + this.name = 'ConfigurationNotFoundError' + } +} From c18f35dc6bf149c18fe706a5b7eef09b0c017d49 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 22 Sep 2026 12:19:08 +0200 Subject: [PATCH 114/125] refactor(errors): update InvalidMachineIdError --- .../src/domain/machine/errors/invalid-machine-id.error.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/microservices/coordinator-service/src/domain/machine/errors/invalid-machine-id.error.ts b/microservices/coordinator-service/src/domain/machine/errors/invalid-machine-id.error.ts index 92f8740d..4d6519c1 100644 --- a/microservices/coordinator-service/src/domain/machine/errors/invalid-machine-id.error.ts +++ b/microservices/coordinator-service/src/domain/machine/errors/invalid-machine-id.error.ts @@ -1,6 +1,6 @@ export class InvalidMachineIdError extends Error { - constructor() { - super('MachineId cannot be empty') + constructor(machineId: string) { + super(`MachineId must be a 24-character hexadecimal string: ${machineId}`) this.name = 'InvalidMachineIdError' } } From d1a3e7897e1f439b4324b322eaad93a195904009 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 22 Sep 2026 12:50:02 +0200 Subject: [PATCH 115/125] feat(processors): enhance Heartbeat and Telemetry processors with error handling for machine configurations and add validation for machine ids --- .../processor/heartbeat-processor.ts | 18 ++++++- .../processor/telemetry-processor.ts | 13 +++-- .../application/workers/heartbeat-monitor.ts | 50 +++++++++++++------ .../src/application/workers/inbox-worker.ts | 37 +++++++++++--- microservices/coordinator-service/src/main.ts | 14 ++++-- 5 files changed, 102 insertions(+), 30 deletions(-) diff --git a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts index 0acb762f..b52008fd 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts @@ -2,10 +2,15 @@ import { MessageProcessor } from '@/application/messaging/message-processor' import { ProcessHeartbeat } from '../../heartbeat/process-heartbeat' import { HeartbeatInput } from '../../heartbeat/dto/heartbeat-input' import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { MachineConfig } from '@/domain/machine/machine-config' +import { MachineId } from '@/domain/machine/value-objects/machine-id' +import { ConfigurationNotFoundError } from '@/application/errors/configuration-not-found.error' +import { InvalidMachineIdError } from '@/domain/machine/errors/invalid-machine-id.error' export class HeartbeatProcessor implements MessageProcessor { constructor( private readonly processHeartbeat: ProcessHeartbeat, + private readonly machineConfigs: Map, private readonly heartbeatTopicPrefix: string ) {} @@ -17,8 +22,19 @@ export class HeartbeatProcessor implements MessageProcessor { } async process(inboxMessage: InboxMessage): Promise { + const machineId = inboxMessage.topic.split('/').pop() + + if (!machineId) { + throw new InvalidMachineIdError('') + } + + const machineIdVO = new MachineId(machineId) + if (!this.machineConfigs.has(machineIdVO.value)) { + throw new ConfigurationNotFoundError(machineIdVO.value) + } + const input: HeartbeatInput = { - machineId: inboxMessage.topic.split('/').pop(), + machineId: machineIdVO.value, ...JSON.parse(inboxMessage.payload.toString()) } this.processHeartbeat.execute(input) diff --git a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts index b97c65c0..00ede859 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts @@ -4,6 +4,8 @@ import { ProcessTelemetry } from '../../telemetry/process-telemetry' import { TelemetryInput } from '../../telemetry/dto/telemetry-input' import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' import { CoreRestService } from '@/application/ports/core-rest-service' +import { ConfigurationNotFoundError } from '@/application/errors/configuration-not-found.error' +import { MachineId } from '@/domain/machine/value-objects/machine-id' export class TelemetryProcessor implements MessageProcessor { constructor( @@ -21,13 +23,16 @@ export class TelemetryProcessor implements MessageProcessor { } async process(inboxMessage: InboxMessage): Promise { + const machineId = inboxMessage.topic.split('/').pop() + const payload = JSON.parse(inboxMessage.payload.toString()) const input: TelemetryInput = { - machineId: inboxMessage.topic.split('/').pop(), - ...JSON.parse(inboxMessage.payload.toString()) + machineId, + ...payload } - const machineConfig = this.machineConfigs.get(input.machineId) + const machineIdVO = new MachineId(input.machineId) + const machineConfig = this.machineConfigs.get(machineIdVO.value) if (!machineConfig) { - throw new Error(`No configuration found for machine: ${input.machineId}`) + throw new ConfigurationNotFoundError(machineIdVO.value) } const result = this.processTelemetry.execute(input, machineConfig) await this.coreRestService.publishTelemetry(result) diff --git a/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts b/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts index f5062b3e..28888910 100644 --- a/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts +++ b/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts @@ -4,6 +4,9 @@ import { HeartbeatTimeoutPolicy } from '@/domain/machine/policies/heatbeat-timeo import { Logger } from '../ports/logger' import { CoreRestService } from '../ports/core-rest-service' import { MachineReachabilityStatus } from '@/domain/machine/machine-reachability-status' +import { MachineNotFoundError } from '../errors/machine-not-found.error' +import { InvalidMachineIdError } from '@/domain/machine/errors/invalid-machine-id.error' +import { MachineId } from '@/domain/machine/value-objects/machine-id' export class HeartbeatMonitor { constructor( @@ -22,23 +25,40 @@ export class HeartbeatMonitor { const now = this.clock.now() for (const heartbeat of heartbeats) { - const status = this.timeoutPolicy.evaluate(heartbeat.receivedAt, now) - - this.logger.info('Heartbeat status', { - machineId: heartbeat.machineId, - status + const machineId = new MachineId(heartbeat.machineId) + this.logger.info('Evaluating heartbeat for machine', { + machineId: machineId.value, + lastHeartbeatAt: heartbeat.receivedAt, + now }) - if (status === MachineReachabilityStatus.OFFLINE) { - await this.coreRestService.updateMachineState( - heartbeat.machineId, - 'off' - ) - } else if (status === MachineReachabilityStatus.ONLINE) { - await this.coreRestService.updateMachineState( - heartbeat.machineId, - 'operational' - ) + const status = this.timeoutPolicy.evaluate(heartbeat.receivedAt, now) + + try { + if (status === MachineReachabilityStatus.OFFLINE) { + await this.coreRestService.updateMachineState( + heartbeat.machineId, + 'off' + ) + } else if (status === MachineReachabilityStatus.ONLINE) { + await this.coreRestService.updateMachineState( + heartbeat.machineId, + 'operational' + ) + } + } catch (error) { + if ( + error instanceof MachineNotFoundError || + error instanceof InvalidMachineIdError + ) { + this.logger.warn( + 'Ignoring heartbeat for unknown machine or invalid machine ID', + { + machineId: heartbeat.machineId + } + ) + continue + } } } } diff --git a/microservices/coordinator-service/src/application/workers/inbox-worker.ts b/microservices/coordinator-service/src/application/workers/inbox-worker.ts index ef03966d..f2b20456 100644 --- a/microservices/coordinator-service/src/application/workers/inbox-worker.ts +++ b/microservices/coordinator-service/src/application/workers/inbox-worker.ts @@ -3,6 +3,8 @@ import { Logger } from '../ports/logger' import { InboxRepository } from '../ports/inbox.repository' import { InboxStatus } from '@/infrastructure/persistence/sqlite/models/inbox-message' import { Clock } from '../ports/clock' +import { ConfigurationNotFoundError } from '../errors/configuration-not-found.error' +import { InvalidMachineIdError } from '@/domain/machine/errors/invalid-machine-id.error' export class InboxWorker { constructor( @@ -20,6 +22,11 @@ export class InboxWorker { for (const msg of messages) { const processor = this.processors.find((h) => h.canHandle(msg.topic)) + this.logger.info('Processing message', { + eventId: msg.eventId, + topic: msg.topic + }) + if (!processor) { this.inboxRepository.updateStatus( msg.eventId, @@ -44,14 +51,30 @@ export class InboxWorker { this.logger.info('Message processed successfully', { msg }) } catch (err) { - this.inboxRepository.updateStatus( - msg.eventId, - InboxStatus.PENDING, - msg.attempts + 1, - this.clock.now() - ) + if ( + err instanceof ConfigurationNotFoundError || + err instanceof InvalidMachineIdError + ) { + this.inboxRepository.updateStatus( + msg.eventId, + InboxStatus.FAILED, + msg.attempts + 1, + this.clock.now() + ) + this.logger.error('Configuration not found or invalid machine ID', { + err, + msg + }) + } else { + this.inboxRepository.updateStatus( + msg.eventId, + InboxStatus.PENDING, + msg.attempts + 1, + this.clock.now() + ) - this.logger.error('Message processing failed', { err, msg }) + this.logger.error('Message processing failed', { err, msg }) + } } } } diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 41abb317..8475f1a3 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -74,7 +74,11 @@ async function bootstrap() { config.mqtt.topic, coreRestService ), - new HeartbeatProcessor(processHeartbeat, config.mqtt.heartbeatTopic) + new HeartbeatProcessor( + processHeartbeat, + configsByMachineId, + config.mqtt.heartbeatTopic + ) ], inboxRepository, clock, @@ -103,11 +107,15 @@ async function bootstrap() { }) setInterval(() => { - inboxWorker.run() + inboxWorker.run().catch((error) => { + logger.error('Telemetry processing failed', { error }) + }) }, 1000) setInterval(() => { - heartbeatMonitor.run() + heartbeatMonitor.run().catch((error) => { + logger.error('Heartbeat monitor failed', { error }) + }) }, 3000) } From 9a236174dd0e2009e9d077e54a7165f9ffb17146 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 22 Sep 2026 12:51:09 +0200 Subject: [PATCH 116/125] feat(docker): update machine-simulator service to use dynamic MACHINE_ID environment variable --- docker/docker-compose.simulators.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.simulators.yml b/docker/docker-compose.simulators.yml index 79db498f..280f0d0f 100644 --- a/docker/docker-compose.simulators.yml +++ b/docker/docker-compose.simulators.yml @@ -22,14 +22,14 @@ services: volumes: - coordinator_data:/app/data - machine-simulator-1: + machine-simulator: build: context: ../microservices/machine-simulator-service depends_on: - mqtt-broker environment: NODE_ENV: development - MACHINE_ID: 6a8771c13ec0b58b0b1ac2af + MACHINE_ID: ${MACHINE_ID} MQTT_URL: mqtt://mqtt-broker:1883 MQTT_TOPIC: factory/telemetry INTERVAL_MS: 2000 From 8163a6bbfa9f3a7f656daeb80064418195577eb4 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 23 Sep 2026 16:16:52 +0200 Subject: [PATCH 117/125] chore(deps): update package versions in package-lock.json --- .../coordinator-service/package-lock.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/microservices/coordinator-service/package-lock.json b/microservices/coordinator-service/package-lock.json index c53211bb..f635d3b5 100644 --- a/microservices/coordinator-service/package-lock.json +++ b/microservices/coordinator-service/package-lock.json @@ -1840,16 +1840,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/broker-factory": { @@ -2622,9 +2622,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", "license": "MIT", "engines": { "node": ">= 12" @@ -3298,9 +3298,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -3572,9 +3572,9 @@ "license": "MIT" }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -3592,7 +3592,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, From ec1e82ba96baedadf7e1e43bc7ca0cbbee9c7581 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Wed, 30 Sep 2026 11:53:07 +0200 Subject: [PATCH 118/125] refactor(telemetry): decouple anomaly detection from application DTO and refactor telemetry processing to use MachineFactory --- .../telemetry/process-telemetry.ts | 41 +++++++++++++------ .../anomaly/services/anomaly-detector.ts | 4 +- .../services/policies/anomaly-policy.ts | 4 +- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts index a21b9f75..e0873939 100644 --- a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts +++ b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts @@ -1,4 +1,4 @@ -import { MachineConfig } from '@/domain' +import { MachineConfig, MachineFactory } from '@/domain' import { AnomalyDetector } from '@/domain/anomaly/services/anomaly-detector' import { Clock } from '../ports/clock' import { Logger } from '../ports/logger' @@ -33,26 +33,41 @@ export class ProcessTelemetry { } this.logger.info('Processing telemetry data', input) + const operatingTemperature = toFiniteNumber( + input.operatingTemperature, + 'operatingTemperature' + ) + const powerConsumption = toFiniteNumber( + input.powerConsumption, + 'powerConsumption' + ) + const emissions = toFiniteNumber(input.emissions, 'emissions') + const vibration = toFiniteNumber(input.vibration, 'vibration') + const pressure = toFiniteNumber(input.pressure, 'pressure') + + const machine = MachineFactory.createFromSensors( + machineId, + operatingTemperature, + powerConsumption, + emissions, + vibration, + pressure + ) + const event: TelemetryEvent = { eventId: randomUUID(), machineId, sequenceNumber: input.sequenceNumber, occurredAt, processedAt: this.clock.now(), - operatingTemperature: toFiniteNumber( - input.operatingTemperature, - 'operatingTemperature' - ), - powerConsumption: toFiniteNumber( - input.powerConsumption, - 'powerConsumption' - ), - emissions: toFiniteNumber(input.emissions, 'emissions'), - vibration: toFiniteNumber(input.vibration, 'vibration'), - pressure: toFiniteNumber(input.pressure, 'pressure') + operatingTemperature, + powerConsumption, + emissions, + vibration, + pressure } - const anomalies = this.anomalyDetector.detect(event, machineConfig) + const anomalies = this.anomalyDetector.detect(machine, machineConfig) if (anomalies.length > 0) { this.logger.warn('Anomalies detected', { machineId: event.machineId, diff --git a/microservices/coordinator-service/src/domain/anomaly/services/anomaly-detector.ts b/microservices/coordinator-service/src/domain/anomaly/services/anomaly-detector.ts index 1796cc0e..acd5983b 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/anomaly-detector.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/anomaly-detector.ts @@ -1,12 +1,12 @@ -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { MachineConfig } from '@/domain/machine/machine-config' import { Anomaly } from '../anomaly' import { AnomalyPolicy } from './policies/anomaly-policy' +import { Machine } from '@/domain/machine/machine' export class AnomalyDetector { constructor(private readonly policies: AnomalyPolicy[]) {} - detect(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + detect(event: Machine, config: MachineConfig): Anomaly[] { return this.policies.flatMap((p) => p.evaluate(event, config)) } } diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/anomaly-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/anomaly-policy.ts index 018ef4b1..fe4f21e9 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/anomaly-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/anomaly-policy.ts @@ -1,7 +1,7 @@ -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { MachineConfig } from '@/domain/machine/machine-config' import { Anomaly } from '@/domain/anomaly/anomaly' +import { Machine } from '@/domain' export interface AnomalyPolicy { - evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] + evaluate(event: Machine, config: MachineConfig): Anomaly[] } From 2fd70d8c6036b96d02ab715bcf0f0754c1fa6f14 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 2 Oct 2026 16:53:04 +0200 Subject: [PATCH 119/125] feat(id-generator): add IdGenerator interface and UUIDGenerator implementation for unique identifier generation --- .../src/domain/ports/id-generator.ts | 10 ++++++++++ .../src/infrastructure/adapters/uuid-generator.ts | 15 +++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 microservices/coordinator-service/src/domain/ports/id-generator.ts create mode 100644 microservices/coordinator-service/src/infrastructure/adapters/uuid-generator.ts diff --git a/microservices/coordinator-service/src/domain/ports/id-generator.ts b/microservices/coordinator-service/src/domain/ports/id-generator.ts new file mode 100644 index 00000000..8b0cb59f --- /dev/null +++ b/microservices/coordinator-service/src/domain/ports/id-generator.ts @@ -0,0 +1,10 @@ +/** + * Interface for generating unique identifiers. + */ +export interface IdGenerator { + /** + * Generates a unique identifier. + * @returns A unique identifier as a string. + */ + generateId(): string +} diff --git a/microservices/coordinator-service/src/infrastructure/adapters/uuid-generator.ts b/microservices/coordinator-service/src/infrastructure/adapters/uuid-generator.ts new file mode 100644 index 00000000..a2dfdc26 --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/adapters/uuid-generator.ts @@ -0,0 +1,15 @@ +import { IdGenerator } from '@/domain/ports/id-generator' +import { randomUUID } from 'crypto' + +/** + * Implementation of the IdGenerator interface that generates unique identifiers using UUIDs. + */ +export class UUIDGenerator implements IdGenerator { + /** + * This method generates a unique identifier using the randomUUID function from the crypto module. + * @returns A unique identifier as a string. + */ + generateId(): string { + return randomUUID() + } +} From d7a7e8f7c0047e67163b4fb7a7a9903c658eb29e Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 2 Oct 2026 17:11:31 +0200 Subject: [PATCH 120/125] feat(telemetry): refactor telemetry event handling and remove TelemetryEvent type in favor of Machine, and add generator id into policies --- .../telemetry/dto/process-telemetry-result.ts | 4 +-- .../telemetry/dto/telemetry-event.ts | 12 ------- .../telemetry/process-telemetry.ts | 32 ++++++------------- .../services/policies/emission-policy.ts | 18 +++++------ .../policies/power-consumption-policy.ts | 22 ++++++------- .../services/policies/pressure-policy.ts | 18 +++++------ .../services/policies/temperature-policy.ts | 20 ++++++------ .../services/policies/vibration-policy.ts | 18 +++++------ .../src/domain/machine/machine-factory.ts | 12 +++++-- .../src/domain/machine/machine.ts | 5 ++- microservices/coordinator-service/src/main.ts | 13 +++++--- 11 files changed, 78 insertions(+), 96 deletions(-) delete mode 100644 microservices/coordinator-service/src/application/telemetry/dto/telemetry-event.ts diff --git a/microservices/coordinator-service/src/application/telemetry/dto/process-telemetry-result.ts b/microservices/coordinator-service/src/application/telemetry/dto/process-telemetry-result.ts index 7dcfe8ba..dda596d8 100644 --- a/microservices/coordinator-service/src/application/telemetry/dto/process-telemetry-result.ts +++ b/microservices/coordinator-service/src/application/telemetry/dto/process-telemetry-result.ts @@ -1,7 +1,7 @@ +import { Machine } from '@/domain/machine/machine' import { Anomaly } from '../../../domain/anomaly/anomaly' -import { TelemetryEvent } from './telemetry-event' export type ProcessTelemetryResult = { - event: TelemetryEvent + machine: Machine anomalies: Anomaly[] } diff --git a/microservices/coordinator-service/src/application/telemetry/dto/telemetry-event.ts b/microservices/coordinator-service/src/application/telemetry/dto/telemetry-event.ts deleted file mode 100644 index c311e5ed..00000000 --- a/microservices/coordinator-service/src/application/telemetry/dto/telemetry-event.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type TelemetryEvent = { - eventId: string - machineId: string - sequenceNumber: number - occurredAt: Date - processedAt: Date - operatingTemperature: number - powerConsumption: number - emissions: number - vibration: number - pressure: number -} diff --git a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts index e0873939..80c463e5 100644 --- a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts +++ b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts @@ -1,11 +1,9 @@ -import { MachineConfig, MachineFactory } from '@/domain' +import { Machine, MachineConfig, MachineFactory } from '@/domain' import { AnomalyDetector } from '@/domain/anomaly/services/anomaly-detector' import { Clock } from '../ports/clock' import { Logger } from '../ports/logger' import { ProcessTelemetryResult } from './dto/process-telemetry-result' -import { TelemetryEvent } from './dto/telemetry-event' import { TelemetryInput } from './dto/telemetry-input' -import { randomUUID } from 'crypto' export class ProcessTelemetry { constructor( @@ -45,40 +43,30 @@ export class ProcessTelemetry { const vibration = toFiniteNumber(input.vibration, 'vibration') const pressure = toFiniteNumber(input.pressure, 'pressure') - const machine = MachineFactory.createFromSensors( + const machine: Machine = MachineFactory.create( machineId, operatingTemperature, powerConsumption, emissions, vibration, - pressure - ) - - const event: TelemetryEvent = { - eventId: randomUUID(), - machineId, - sequenceNumber: input.sequenceNumber, + pressure, occurredAt, - processedAt: this.clock.now(), - operatingTemperature, - powerConsumption, - emissions, - vibration, - pressure - } + this.clock.now(), + input.sequenceNumber + ) const anomalies = this.anomalyDetector.detect(machine, machineConfig) if (anomalies.length > 0) { this.logger.warn('Anomalies detected', { - machineId: event.machineId, + machineId: machine.id.value, anomalies }) } else { - this.logger.info('No anomalies detected', { machineId: event.machineId }) + this.logger.info('No anomalies detected', { machineId: machine.id.value }) } - this.logger.info('Processed telemetry event', { event }) + this.logger.info('Processed telemetry event', { machine: machine }) - return { event, anomalies } + return { machine, anomalies } } } diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts index 0765016d..ee37c383 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts @@ -1,23 +1,23 @@ -import { randomUUID } from 'crypto' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { MachineConfig } from '@/domain/machine/machine-config' -import { MachineId } from '@/domain/machine/value-objects/machine-id' import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { Emission } from '@/domain/machine/value-objects/emission' +import { Machine } from '@/domain/machine/machine' +import { IdGenerator } from '@/domain/ports/id-generator' export class EmissionPolicy implements AnomalyPolicy { - evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + constructor(private readonly generator: IdGenerator) {} + + evaluate(event: Machine, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] - if (!config.emissions.contains(new Emission(event.emissions))) { + if (!config.emissions.contains(event.emissions)) { anomalies.push( new Anomaly( - new AnomalyEventId(randomUUID()), - new MachineId(event.machineId), + new AnomalyEventId(this.generator.generateId()), + event.id, SensorType.EMISSIONS, - event.emissions, + event.emissions.value, event.occurredAt, event.processedAt ) diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts index 97f180ae..b057f70b 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts @@ -1,27 +1,23 @@ -import { randomUUID } from 'crypto' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { MachineConfig } from '@/domain/machine/machine-config' -import { MachineId } from '@/domain/machine/value-objects/machine-id' import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { PowerConsumption } from '@/domain/machine/value-objects/power-consumption' +import { Machine } from '@/domain/machine/machine' +import { IdGenerator } from '@/domain/ports/id-generator' export class PowerConsumptionPolicy implements AnomalyPolicy { - evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + constructor(private readonly generator: IdGenerator) {} + + evaluate(event: Machine, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] - if ( - !config.powerConsumption.contains( - new PowerConsumption(event.powerConsumption) - ) - ) { + if (!config.powerConsumption.contains(event.powerConsumption)) { anomalies.push( new Anomaly( - new AnomalyEventId(randomUUID()), - new MachineId(event.machineId), + new AnomalyEventId(this.generator.generateId()), + event.id, SensorType.POWER_CONSUMPTION, - event.powerConsumption, + event.powerConsumption.value, event.occurredAt, event.processedAt ) diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts index e1af1806..346ffdf3 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts @@ -1,23 +1,23 @@ -import { randomUUID } from 'crypto' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { MachineConfig } from '@/domain/machine/machine-config' -import { MachineId } from '@/domain/machine/value-objects/machine-id' import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { Pressure } from '@/domain/machine/value-objects/pressure' +import { Machine } from '@/domain/machine/machine' +import { IdGenerator } from '@/domain/ports/id-generator' export class PressurePolicy implements AnomalyPolicy { - evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + constructor(private readonly generator: IdGenerator) {} + + evaluate(event: Machine, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] - if (!config.pressure.contains(new Pressure(event.pressure))) { + if (!config.pressure.contains(event.pressure)) { anomalies.push( new Anomaly( - new AnomalyEventId(randomUUID()), - new MachineId(event.machineId), + new AnomalyEventId(this.generator.generateId()), + event.id, SensorType.PRESSURE, - event.pressure, + event.pressure.value, event.occurredAt, event.processedAt ) diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/temperature-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/temperature-policy.ts index a53e4ff4..98573957 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/temperature-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/temperature-policy.ts @@ -1,25 +1,23 @@ -import { randomUUID } from 'crypto' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { MachineConfig } from '@/domain/machine/machine-config' -import { MachineId } from '@/domain/machine/value-objects/machine-id' import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { Temperature } from '@/domain/machine/value-objects/temperature' +import { Machine } from '@/domain/machine/machine' +import { IdGenerator } from '@/domain/ports/id-generator' export class TemperaturePolicy implements AnomalyPolicy { - evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + constructor(private readonly generator: IdGenerator) {} + + evaluate(event: Machine, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] - if ( - !config.temperature.contains(new Temperature(event.operatingTemperature)) - ) { + if (!config.temperature.contains(event.temperature)) { anomalies.push( new Anomaly( - new AnomalyEventId(randomUUID()), - new MachineId(event.machineId), + new AnomalyEventId(this.generator.generateId()), + event.id, SensorType.TEMPERATURE, - event.operatingTemperature, + event.temperature.value, event.occurredAt, event.processedAt ) diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts index 2ca914a6..8141e176 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts @@ -1,23 +1,23 @@ -import { randomUUID } from 'crypto' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { MachineConfig } from '@/domain/machine/machine-config' -import { MachineId } from '@/domain/machine/value-objects/machine-id' import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { Vibration } from '@/domain/machine/value-objects/vibration' +import { Machine } from '@/domain/machine/machine' +import { IdGenerator } from '@/domain/ports/id-generator' export class VibrationPolicy implements AnomalyPolicy { - evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + constructor(private readonly generator: IdGenerator) {} + + evaluate(event: Machine, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] - if (!config.vibration.contains(new Vibration(event.vibration))) { + if (!config.vibration.contains(event.vibration)) { anomalies.push( new Anomaly( - new AnomalyEventId(randomUUID()), - new MachineId(event.machineId), + new AnomalyEventId(this.generator.generateId()), + event.id, SensorType.VIBRATION, - event.vibration, + event.vibration.value, event.occurredAt, event.processedAt ) diff --git a/microservices/coordinator-service/src/domain/machine/machine-factory.ts b/microservices/coordinator-service/src/domain/machine/machine-factory.ts index 40223199..e4c63c5f 100644 --- a/microservices/coordinator-service/src/domain/machine/machine-factory.ts +++ b/microservices/coordinator-service/src/domain/machine/machine-factory.ts @@ -7,13 +7,16 @@ import { Vibration } from './value-objects/vibration' import { MachineId } from './value-objects/machine-id' export class MachineFactory { - static createFromSensors( + static create( id: string, temperature: number, powerConsumption: number, emissions: number, vibration: number, - pressure: number + pressure: number, + occurredAt: Date, + processedAt: Date, + sequenceNumber: number ): Machine { return new Machine( new MachineId(id), @@ -21,7 +24,10 @@ export class MachineFactory { new PowerConsumption(powerConsumption), new Emission(emissions), new Vibration(vibration), - new Pressure(pressure) + new Pressure(pressure), + new Date(occurredAt), + new Date(processedAt), + sequenceNumber ) } } diff --git a/microservices/coordinator-service/src/domain/machine/machine.ts b/microservices/coordinator-service/src/domain/machine/machine.ts index 0990949a..b593a4ab 100644 --- a/microservices/coordinator-service/src/domain/machine/machine.ts +++ b/microservices/coordinator-service/src/domain/machine/machine.ts @@ -25,7 +25,10 @@ export class Machine { public powerConsumption: PowerConsumption, public emissions: Emission, public vibration: Vibration, - public pressure: Pressure + public pressure: Pressure, + public occurredAt: Date, + public processedAt: Date, + public sequenceNumber: number ) {} /** diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 8475f1a3..6e2d204c 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -22,6 +22,7 @@ import { VibrationPolicy } from './domain/anomaly/services/policies/vibration-po import { PressurePolicy } from './domain/anomaly/services/policies/pressure-policy' import { EmissionPolicy } from './domain/anomaly/services/policies/emission-policy' import { PowerConsumptionPolicy } from './domain/anomaly/services/policies/power-consumption-policy' +import { UUIDGenerator } from './infrastructure/adapters/uuid-generator' async function bootstrap() { const config = getConfig() @@ -45,12 +46,14 @@ async function bootstrap() { baseLogger.child({ service: 'http-core-rest-service' }) ) + const generator = new UUIDGenerator() + const policies = [ - new TemperaturePolicy(), - new VibrationPolicy(), - new PressurePolicy(), - new EmissionPolicy(), - new PowerConsumptionPolicy() + new TemperaturePolicy(generator), + new VibrationPolicy(generator), + new PressurePolicy(generator), + new EmissionPolicy(generator), + new PowerConsumptionPolicy(generator) ] const anomalyDetector = new AnomalyDetector(policies) const processTelemetry = new ProcessTelemetry(anomalyDetector, clock, logger) From 13433f188dced725515167b1efaef33588a48c65 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Fri, 2 Oct 2026 17:15:26 +0200 Subject: [PATCH 121/125] refactor(tests): update tests after TelemetryEvent removal --- .../telemetry/process-telemetry.spec.ts | 12 +- .../test/constants/machine-values.ts | 10 +- .../anomaly/services/anomaly-detector.spec.ts | 124 +++++++++--------- .../services/policies/emission-policy.spec.ts | 62 ++++----- .../policies/power-consumption-policy.spec.ts | 62 ++++----- .../services/policies/pressure-policy.spec.ts | 62 ++++----- .../policies/temperature-policy.spec.ts | 91 ++++++------- .../policies/vibration-policy.spec.ts | 62 ++++----- .../domain/machine/machine-factory.spec.ts | 7 +- .../test/utils/fake-machine.ts | 10 +- 10 files changed, 263 insertions(+), 239 deletions(-) diff --git a/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts b/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts index 1893d661..e9798527 100644 --- a/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts +++ b/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts @@ -36,8 +36,8 @@ describe('ProcessTelemetry', () => { const result = useCase.execute(input, fakeConfig()) - expect(result.event.machineId).toBe(MACHINE_VALUES.ID) - expect(result.event.operatingTemperature).toBe( + expect(result.machine.id.value).toBe(MACHINE_VALUES.ID) + expect(result.machine.temperature.value).toBe( MACHINE_VALUES.TEMPERATURE.SAFE ) expect(result.anomalies).toHaveLength(0) @@ -74,12 +74,12 @@ describe('ProcessTelemetry', () => { const result = useCase.execute(input, fakeConfig()) - expect(result.event.machineId).toBe(MACHINE_VALUES.ID) - expect(result.event.operatingTemperature).toBe( + expect(result.machine.id.value).toBe(MACHINE_VALUES.ID) + expect(result.machine.temperature.value).toBe( MACHINE_VALUES.TEMPERATURE.OVER ) - expect(result.event.occurredAt).toEqual(occurredAtDate) - expect(result.event.processedAt).toEqual(processedAtDate) + expect(result.machine.occurredAt).toEqual(occurredAtDate) + expect(result.machine.processedAt).toEqual(processedAtDate) expect(result.anomalies).toHaveLength(1) expect(result.anomalies[0].sensorType).toBe(SensorType.TEMPERATURE) expect(result.anomalies[0].value).toBe(MACHINE_VALUES.TEMPERATURE.OVER) diff --git a/microservices/coordinator-service/test/constants/machine-values.ts b/microservices/coordinator-service/test/constants/machine-values.ts index 2ee58dbf..8733b4dc 100644 --- a/microservices/coordinator-service/test/constants/machine-values.ts +++ b/microservices/coordinator-service/test/constants/machine-values.ts @@ -6,8 +6,6 @@ export const MACHINE_IDS = { export const MACHINE_VALUES = { ID: MACHINE_IDS.DEFAULT, - SEQUENCE_NUMBER: 1, - TEMPERATURE: { SAFE: 55, OVER: 90, @@ -32,5 +30,11 @@ export const MACHINE_VALUES = { PRESSURE: { SAFE: 6, OVER: 12 - } + }, + + OCCURRED_AT: new Date('2025-12-31T23:59:00.000Z'), + + PROCESSED_AT: new Date('2026-01-01T00:00:00Z'), + + SEQUENCE_NUMBER: 1 } diff --git a/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts index 8d042347..75df3b8e 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts @@ -1,66 +1,70 @@ import { describe, expect, it } from 'vitest' import { AnomalyDetector } from '@/domain/anomaly/services/anomaly-detector' import { TemperaturePolicy } from '@/domain/anomaly/services/policies/temperature-policy' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { MachineConfig } from '@/domain/machine/machine-config' import { MACHINE_LIMITS } from '@test/constants/machine-limits' import { MACHINE_VALUES } from '@test/constants/machine-values' import { fakeConfig } from '@test/utils/fake-config' import { VibrationPolicy } from '@/domain/anomaly/services/policies/vibration-policy' +import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' +import { MachineFactory } from '@/domain/machine/machine-factory' +import { Machine } from '@/domain/machine/machine' describe('AnomalyDetector', () => { + const generator = new UUIDGenerator() + it('should return an empty array when no policies detect anomalies', () => { - const detector = new AnomalyDetector([new TemperaturePolicy()]) - const event: TelemetryEvent = { - eventId: 'event-1', - machineId: MACHINE_VALUES.ID, - sequenceNumber: 1, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const detector = new AnomalyDetector([new TemperaturePolicy(generator)]) + + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) + const config: MachineConfig = fakeConfig({ temperature: { min: MACHINE_LIMITS.TEMPERATURE.MIN, max: MACHINE_LIMITS.TEMPERATURE.MAX } }) - const anomalies = detector.detect(event, config) + const anomalies = detector.detect(machine, config) expect(anomalies).toHaveLength(0) }) it('should detect an anomaly from a single policy', () => { - const detector = new AnomalyDetector([new TemperaturePolicy()]) + const detector = new AnomalyDetector([new TemperaturePolicy(generator)]) - const event: TelemetryEvent = { - eventId: 'event-1', - machineId: MACHINE_VALUES.ID, - sequenceNumber: 1, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.OVER, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.OVER, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ temperature: { min: MACHINE_LIMITS.TEMPERATURE.MIN, max: MACHINE_LIMITS.TEMPERATURE.MAX } }) - const anomalies = detector.detect(event, config) + const anomalies = detector.detect(machine, config) expect(anomalies).toHaveLength(1) expect(anomalies).toEqual( expect.arrayContaining([ expect.objectContaining({ sensorType: 'operatingTemperature', - value: event.operatingTemperature + value: machine.temperature.value }) ]) ) @@ -68,22 +72,21 @@ describe('AnomalyDetector', () => { it('should detect anomalies from multiple policies', () => { const detector = new AnomalyDetector([ - new TemperaturePolicy(), - new VibrationPolicy() + new TemperaturePolicy(generator), + new VibrationPolicy(generator) ]) - const event: TelemetryEvent = { - eventId: 'event-1', - machineId: MACHINE_VALUES.ID, - sequenceNumber: 1, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.OVER, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.OVER, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.OVER, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.OVER, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ temperature: { min: MACHINE_LIMITS.TEMPERATURE.MIN, @@ -94,37 +97,36 @@ describe('AnomalyDetector', () => { max: MACHINE_LIMITS.VIBRATION.MAX } }) - const anomalies = detector.detect(event, config) + const anomalies = detector.detect(machine, config) expect(anomalies).toHaveLength(2) expect(anomalies).toEqual( expect.arrayContaining([ expect.objectContaining({ sensorType: 'operatingTemperature', - value: event.operatingTemperature + value: machine.temperature.value }), expect.objectContaining({ sensorType: 'vibration', - value: event.vibration + value: machine.vibration.value }) ]) ) }) it('should not detect an anomaly when no policy is defined for the sensor type', () => { - const detector = new AnomalyDetector([new TemperaturePolicy()]) + const detector = new AnomalyDetector([new TemperaturePolicy(generator)]) - const event: TelemetryEvent = { - eventId: 'event-1', - machineId: MACHINE_VALUES.ID, - sequenceNumber: 1, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.OVER, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.OVER, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ temperature: { min: MACHINE_LIMITS.TEMPERATURE.MIN, @@ -135,7 +137,7 @@ describe('AnomalyDetector', () => { max: MACHINE_LIMITS.VIBRATION.MAX } }) - const anomalies = detector.detect(event, config) + const anomalies = detector.detect(machine, config) expect(anomalies).toHaveLength(0) }) }) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts index df9f3d15..f542284e 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts @@ -3,58 +3,60 @@ import { MACHINE_VALUES } from '@test/constants/machine-values' import { fakeConfig } from '@test/utils/fake-config' import { MachineConfig } from '@/domain/machine/machine-config' import { MACHINE_LIMITS } from '@test/constants/machine-limits' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { EmissionPolicy } from '@/domain/anomaly/services/policies/emission-policy' +import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' +import { MachineFactory } from '@/domain/machine/machine-factory' +import { Machine } from '@/domain/machine/machine' describe('EmissionPolicy', () => { + const generator = new UUIDGenerator() + it('should return an anomaly with correct details when emission is over the maximum limit', () => { - const policy = new EmissionPolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.OVER, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new EmissionPolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.OVER, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ emissions: { min: MACHINE_LIMITS.EMISSION.MIN, max: MACHINE_LIMITS.EMISSION.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(1) expect(anomalies[0].sensorType).toBe(SensorType.EMISSIONS) - expect(anomalies[0].value).toBe(event.emissions) + expect(anomalies[0].value).toBe(machine.emissions.value) }) it('should not detect any anomaly when emissions is within limits', () => { - const policy = new EmissionPolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new EmissionPolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ emissions: { min: MACHINE_LIMITS.EMISSION.MIN, max: MACHINE_LIMITS.EMISSION.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(0) }) }) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts index 83fce05b..a4943ddc 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts @@ -3,58 +3,60 @@ import { MACHINE_VALUES } from '@test/constants/machine-values' import { fakeConfig } from '@test/utils/fake-config' import { MachineConfig } from '@/domain/machine/machine-config' import { MACHINE_LIMITS } from '@test/constants/machine-limits' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { PowerConsumptionPolicy } from '@/domain/anomaly/services/policies/power-consumption-policy' +import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' +import { MachineFactory } from '@/domain/machine/machine-factory' +import { Machine } from '@/domain/machine/machine' describe('PowerConsumptionPolicy', () => { + const generator = new UUIDGenerator() + it('should return an anomaly with correct details when power consumption is over the maximum limit', () => { - const policy = new PowerConsumptionPolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.OVER, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new PowerConsumptionPolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.OVER, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ powerConsumption: { min: MACHINE_LIMITS.POWER_CONSUMPTION.MIN, max: MACHINE_LIMITS.POWER_CONSUMPTION.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(1) expect(anomalies[0].sensorType).toBe(SensorType.POWER_CONSUMPTION) - expect(anomalies[0].value).toBe(event.powerConsumption) + expect(anomalies[0].value).toBe(machine.powerConsumption.value) }) it('should not detect any anomaly when power consumption is within limits', () => { - const policy = new PowerConsumptionPolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new PowerConsumptionPolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ powerConsumption: { min: MACHINE_LIMITS.POWER_CONSUMPTION.MIN, max: MACHINE_LIMITS.POWER_CONSUMPTION.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(0) }) }) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts index bd22c182..caa1a724 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts @@ -3,58 +3,60 @@ import { MACHINE_VALUES } from '@test/constants/machine-values' import { fakeConfig } from '@test/utils/fake-config' import { MachineConfig } from '@/domain/machine/machine-config' import { MACHINE_LIMITS } from '@test/constants/machine-limits' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { PressurePolicy } from '@/domain/anomaly/services/policies/pressure-policy' +import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' +import { MachineFactory } from '@/domain/machine/machine-factory' +import { Machine } from '@/domain/machine/machine' describe('PressurePolicy', () => { + const generator = new UUIDGenerator() + it('should return an anomaly with correct details when pressure is over the maximum limit', () => { - const policy = new PressurePolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.OVER - } + const policy = new PressurePolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.OVER, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ pressure: { min: MACHINE_LIMITS.PRESSURE.MIN, max: MACHINE_LIMITS.PRESSURE.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(1) expect(anomalies[0].sensorType).toBe(SensorType.PRESSURE) - expect(anomalies[0].value).toBe(event.pressure) + expect(anomalies[0].value).toBe(machine.pressure.value) }) it('should not detect any anomaly when pressure is within limits', () => { - const policy = new PressurePolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new PressurePolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ pressure: { min: MACHINE_LIMITS.PRESSURE.MIN, max: MACHINE_LIMITS.PRESSURE.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(0) }) }) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts index 7d0b9321..e1614a04 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts @@ -3,84 +3,85 @@ import { MACHINE_VALUES } from '@test/constants/machine-values' import { fakeConfig } from '@test/utils/fake-config' import { MachineConfig } from '@/domain/machine/machine-config' import { MACHINE_LIMITS } from '@test/constants/machine-limits' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { TemperaturePolicy } from '@/domain/anomaly/services/policies/temperature-policy' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' +import { MachineFactory } from '@/domain/machine/machine-factory' +import { Machine } from '@/domain/machine/machine' +import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' describe('TemperaturePolicy', () => { + const generator = new UUIDGenerator() + it('should return an anomaly with correct details when temperature is over the maximum limit', () => { - const policy = new TemperaturePolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.OVER, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new TemperaturePolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.OVER, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ temperature: { min: MACHINE_LIMITS.TEMPERATURE.MIN, max: MACHINE_LIMITS.TEMPERATURE.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(1) expect(anomalies[0].sensorType).toBe(SensorType.TEMPERATURE) - expect(anomalies[0].value).toBe(event.operatingTemperature) + expect(anomalies[0].value).toBe(machine.temperature.value) }) it('should detect temperature anomalies under the minimum limit', () => { - const policy = new TemperaturePolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.UNDER, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new TemperaturePolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.UNDER, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ temperature: { min: MACHINE_LIMITS.TEMPERATURE.MIN, max: MACHINE_LIMITS.TEMPERATURE.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(1) expect(anomalies[0].sensorType).toBe(SensorType.TEMPERATURE) - expect(anomalies[0].value).toBe(event.operatingTemperature) + expect(anomalies[0].value).toBe(machine.temperature.value) }) it('should not detect any anomaly when temperature is within limits', () => { - const policy = new TemperaturePolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new TemperaturePolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ temperature: { min: MACHINE_LIMITS.TEMPERATURE.MIN, max: MACHINE_LIMITS.TEMPERATURE.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(0) }) }) diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts index fd138b8d..9014adb9 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts @@ -3,58 +3,60 @@ import { MACHINE_VALUES } from '@test/constants/machine-values' import { fakeConfig } from '@test/utils/fake-config' import { MachineConfig } from '@/domain/machine/machine-config' import { MACHINE_LIMITS } from '@test/constants/machine-limits' -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { VibrationPolicy } from '@/domain/anomaly/services/policies/vibration-policy' +import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' +import { MachineFactory } from '@/domain/machine/machine-factory' +import { Machine } from '@/domain/machine/machine' describe('VibrationPolicy', () => { + const generator = new UUIDGenerator() + it('should return an anomaly with correct details when vibration is over the maximum limit', () => { - const policy = new VibrationPolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.OVER, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new VibrationPolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.OVER, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ vibration: { min: MACHINE_LIMITS.VIBRATION.MIN, max: MACHINE_LIMITS.VIBRATION.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(1) expect(anomalies[0].sensorType).toBe(SensorType.VIBRATION) - expect(anomalies[0].value).toBe(event.vibration) + expect(anomalies[0].value).toBe(machine.vibration.value) }) it('should not detect any anomaly when vibration is within limits', () => { - const policy = new VibrationPolicy() - const event: TelemetryEvent = { - eventId: 'test-event-id', - machineId: MACHINE_VALUES.ID, - sequenceNumber: MACHINE_VALUES.SEQUENCE_NUMBER, - occurredAt: new Date('2025-12-31T23:59:00.000Z'), - processedAt: new Date('2026-01-01T00:00:00.000Z'), - operatingTemperature: MACHINE_VALUES.TEMPERATURE.SAFE, - powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE, - emissions: MACHINE_VALUES.EMISSION.SAFE, - vibration: MACHINE_VALUES.VIBRATION.SAFE, - pressure: MACHINE_VALUES.PRESSURE.SAFE - } + const policy = new VibrationPolicy(generator) + const machine: Machine = MachineFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 + ) const config: MachineConfig = fakeConfig({ vibration: { min: MACHINE_LIMITS.VIBRATION.MIN, max: MACHINE_LIMITS.VIBRATION.MAX } }) - const anomalies = policy.evaluate(event, config) + const anomalies = policy.evaluate(machine, config) expect(anomalies).toHaveLength(0) }) }) diff --git a/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts b/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts index a4f0d80a..f70d683a 100644 --- a/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts +++ b/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts @@ -13,13 +13,16 @@ describe('MachineFactory', () => { pressure: 5 } - const machine = MachineFactory.createFromSensors( + const machine = MachineFactory.create( telemetryInput.id, telemetryInput.temperature, telemetryInput.powerConsumption, telemetryInput.emissions, telemetryInput.vibration, - telemetryInput.pressure + telemetryInput.pressure, + new Date('2025-12-31T23:59:00.000Z'), + new Date('2026-01-01T00:00:00Z'), + 1 ) expect(machine).toMatchObject({ diff --git a/microservices/coordinator-service/test/utils/fake-machine.ts b/microservices/coordinator-service/test/utils/fake-machine.ts index feafdc32..c6cc147a 100644 --- a/microservices/coordinator-service/test/utils/fake-machine.ts +++ b/microservices/coordinator-service/test/utils/fake-machine.ts @@ -8,13 +8,19 @@ export function fakeMachine(overrides?: { emissions?: number vibration?: number pressure?: number + occurredAt?: Date + processedAt?: Date + sequenceNumber?: number }) { - return MachineFactory.createFromSensors( + return MachineFactory.create( overrides?.id ?? MACHINE_VALUES.ID, overrides?.temperature ?? MACHINE_VALUES.TEMPERATURE.SAFE, overrides?.powerConsumption ?? MACHINE_VALUES.POWER_CONSUMPTION.SAFE, overrides?.emissions ?? MACHINE_VALUES.EMISSION.SAFE, overrides?.vibration ?? MACHINE_VALUES.VIBRATION.SAFE, - overrides?.pressure ?? MACHINE_VALUES.PRESSURE.SAFE + overrides?.pressure ?? MACHINE_VALUES.PRESSURE.SAFE, + overrides?.occurredAt ?? MACHINE_VALUES.OCCURRED_AT, + overrides?.processedAt ?? MACHINE_VALUES.PROCESSED_AT, + overrides?.sequenceNumber ?? MACHINE_VALUES.SEQUENCE_NUMBER ) } From e26024320c08ada9cb072f9645bc329dbab5133d Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Mon, 12 Oct 2026 20:38:59 +0200 Subject: [PATCH 122/125] refactor(inbox): move inbox message model to application layer --- .../models => application/messaging}/inbox-message.ts | 0 .../src/application/messaging/inbox-receiver.ts | 2 +- .../src/application/messaging/message-processor.ts | 2 +- .../messaging/processor/heartbeat-processor.ts | 2 +- .../messaging/processor/telemetry-processor.ts | 2 +- .../src/application/ports/inbox.repository.ts | 2 +- .../src/application/workers/inbox-worker.ts | 2 +- .../persistence/sqlite/sqlite-inbox.repository.ts | 8 ++++---- 8 files changed, 10 insertions(+), 10 deletions(-) rename microservices/coordinator-service/src/{infrastructure/persistence/sqlite/models => application/messaging}/inbox-message.ts (100%) diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts b/microservices/coordinator-service/src/application/messaging/inbox-message.ts similarity index 100% rename from microservices/coordinator-service/src/infrastructure/persistence/sqlite/models/inbox-message.ts rename to microservices/coordinator-service/src/application/messaging/inbox-message.ts diff --git a/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts b/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts index 91e2cbf6..c9511955 100644 --- a/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts +++ b/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts @@ -3,7 +3,7 @@ import { InboxRepository } from '../ports/inbox.repository' import { InboxMessage, InboxStatus -} from '@/infrastructure/persistence/sqlite/models/inbox-message' +} from '@/application/messaging/inbox-message' import { IncomingMessage } from './incoming-message' export class InboxReceiver { diff --git a/microservices/coordinator-service/src/application/messaging/message-processor.ts b/microservices/coordinator-service/src/application/messaging/message-processor.ts index b08e5110..86135a6f 100644 --- a/microservices/coordinator-service/src/application/messaging/message-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/message-processor.ts @@ -1,4 +1,4 @@ -import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { InboxMessage } from '@/application/messaging/inbox-message' /** * Interface for processing messages based on their topic. diff --git a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts index b52008fd..50f2b223 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/heartbeat-processor.ts @@ -1,7 +1,7 @@ import { MessageProcessor } from '@/application/messaging/message-processor' import { ProcessHeartbeat } from '../../heartbeat/process-heartbeat' import { HeartbeatInput } from '../../heartbeat/dto/heartbeat-input' -import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { InboxMessage } from '@/application/messaging/inbox-message' import { MachineConfig } from '@/domain/machine/machine-config' import { MachineId } from '@/domain/machine/value-objects/machine-id' import { ConfigurationNotFoundError } from '@/application/errors/configuration-not-found.error' diff --git a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts index 00ede859..73537399 100644 --- a/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts +++ b/microservices/coordinator-service/src/application/messaging/processor/telemetry-processor.ts @@ -2,7 +2,7 @@ import { MessageProcessor } from '@/application/messaging/message-processor' import { MachineConfig } from '@/domain/machine/machine-config' import { ProcessTelemetry } from '../../telemetry/process-telemetry' import { TelemetryInput } from '../../telemetry/dto/telemetry-input' -import { InboxMessage } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { InboxMessage } from '@/application/messaging/inbox-message' import { CoreRestService } from '@/application/ports/core-rest-service' import { ConfigurationNotFoundError } from '@/application/errors/configuration-not-found.error' import { MachineId } from '@/domain/machine/value-objects/machine-id' diff --git a/microservices/coordinator-service/src/application/ports/inbox.repository.ts b/microservices/coordinator-service/src/application/ports/inbox.repository.ts index 40fe373a..92a1dca2 100644 --- a/microservices/coordinator-service/src/application/ports/inbox.repository.ts +++ b/microservices/coordinator-service/src/application/ports/inbox.repository.ts @@ -1,4 +1,4 @@ -import { InboxMessage } from '../../infrastructure/persistence/sqlite/models/inbox-message' +import { InboxMessage } from '../messaging/inbox-message' export interface InboxRepository { save(message: InboxMessage): void diff --git a/microservices/coordinator-service/src/application/workers/inbox-worker.ts b/microservices/coordinator-service/src/application/workers/inbox-worker.ts index f2b20456..8d814779 100644 --- a/microservices/coordinator-service/src/application/workers/inbox-worker.ts +++ b/microservices/coordinator-service/src/application/workers/inbox-worker.ts @@ -1,7 +1,7 @@ import { MessageProcessor } from '../messaging/message-processor' import { Logger } from '../ports/logger' import { InboxRepository } from '../ports/inbox.repository' -import { InboxStatus } from '@/infrastructure/persistence/sqlite/models/inbox-message' +import { InboxStatus } from '@/application/messaging/inbox-message' import { Clock } from '../ports/clock' import { ConfigurationNotFoundError } from '../errors/configuration-not-found.error' import { InvalidMachineIdError } from '@/domain/machine/errors/invalid-machine-id.error' diff --git a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts index 5bbc364f..a2e3217c 100644 --- a/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts @@ -1,7 +1,7 @@ import { InboxMessage, InboxStatus -} from '@/infrastructure/persistence/sqlite/models/inbox-message' +} from '@/application/messaging/inbox-message' import { InboxRepository } from '@/application/ports/inbox.repository' import { Database } from 'better-sqlite3' @@ -10,8 +10,8 @@ export class SqliteInboxRepository implements InboxRepository { save(message: InboxMessage): void { const query = ` INSERT OR IGNORE INTO coordinator_inbox ( - event_id, - topic, + event_id, + topic, payload, status, attempts, @@ -54,7 +54,7 @@ export class SqliteInboxRepository implements InboxRepository { findPending(): InboxMessage[] { const query = ` - SELECT + SELECT event_id, topic, payload, From 72de2a6adfc4703be091ebe3bf26449dfe2429d8 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 13 Oct 2026 10:57:22 +0200 Subject: [PATCH 123/125] refactor(telemetry): restructure telemetry domain model and factory and related tests --- .../telemetry/dto/process-telemetry-result.ts | 4 +- .../telemetry/process-telemetry.ts | 7 +++- .../anomaly/services/anomaly-detector.ts | 4 +- .../services/policies/anomaly-policy.ts | 4 +- .../services/policies/emission-policy.ts | 4 +- .../policies/power-consumption-policy.ts | 4 +- .../services/policies/pressure-policy.ts | 4 +- .../services/policies/temperature-policy.ts | 4 +- .../services/policies/vibration-policy.ts | 4 +- .../coordinator-service/src/domain/index.ts | 4 +- .../telemetry-event-id-invalid.error.ts | 6 +++ ...achine-factory.ts => telemetry-factory.ts} | 11 +++-- .../machine/{machine.ts => telemetry.ts} | 28 ++++++++----- .../value-objects/telemetry-event-id.ts | 13 ++++++ .../core-rest/mapper/telemetry.mapper.ts | 18 ++++----- microservices/coordinator-service/src/main.ts | 7 +++- .../telemetry/process-telemetry.spec.ts | 10 ++++- .../test/constants/machine-values.ts | 2 + .../anomaly/services/anomaly-detector.spec.ts | 16 +++++--- .../services/policies/emission-policy.spec.ts | 10 +++-- .../policies/power-consumption-policy.spec.ts | 10 +++-- .../services/policies/pressure-policy.spec.ts | 10 +++-- .../policies/temperature-policy.spec.ts | 13 +++--- .../policies/vibration-policy.spec.ts | 10 +++-- .../domain/machine/machine-factory.spec.ts | 37 ----------------- .../test/domain/machine/machine.spec.ts | 26 ++++++------ .../domain/machine/telemetry-factory.spec.ts | 40 +++++++++++++++++++ .../test/utils/fake-machine.ts | 6 ++- 28 files changed, 191 insertions(+), 125 deletions(-) create mode 100644 microservices/coordinator-service/src/domain/machine/errors/telemetry-event-id-invalid.error.ts rename microservices/coordinator-service/src/domain/machine/{machine-factory.ts => telemetry-factory.ts} (76%) rename microservices/coordinator-service/src/domain/machine/{machine.ts => telemetry.ts} (75%) create mode 100644 microservices/coordinator-service/src/domain/machine/value-objects/telemetry-event-id.ts delete mode 100644 microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts create mode 100644 microservices/coordinator-service/test/domain/machine/telemetry-factory.spec.ts diff --git a/microservices/coordinator-service/src/application/telemetry/dto/process-telemetry-result.ts b/microservices/coordinator-service/src/application/telemetry/dto/process-telemetry-result.ts index dda596d8..b2132ab2 100644 --- a/microservices/coordinator-service/src/application/telemetry/dto/process-telemetry-result.ts +++ b/microservices/coordinator-service/src/application/telemetry/dto/process-telemetry-result.ts @@ -1,7 +1,7 @@ -import { Machine } from '@/domain/machine/machine' +import { TelemetryEvent } from '@/domain/machine/telemetry' import { Anomaly } from '../../../domain/anomaly/anomaly' export type ProcessTelemetryResult = { - machine: Machine + machine: TelemetryEvent anomalies: Anomaly[] } diff --git a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts index 80c463e5..85c95383 100644 --- a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts +++ b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts @@ -1,13 +1,15 @@ -import { Machine, MachineConfig, MachineFactory } from '@/domain' +import { TelemetryEvent, MachineConfig, TelemetryFactory } from '@/domain' import { AnomalyDetector } from '@/domain/anomaly/services/anomaly-detector' import { Clock } from '../ports/clock' import { Logger } from '../ports/logger' import { ProcessTelemetryResult } from './dto/process-telemetry-result' import { TelemetryInput } from './dto/telemetry-input' +import { IdGenerator } from '@/domain/ports/id-generator' export class ProcessTelemetry { constructor( private readonly anomalyDetector: AnomalyDetector, + private readonly generator: IdGenerator, private readonly clock: Clock, private readonly logger: Logger ) {} @@ -43,8 +45,9 @@ export class ProcessTelemetry { const vibration = toFiniteNumber(input.vibration, 'vibration') const pressure = toFiniteNumber(input.pressure, 'pressure') - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( machineId, + this.generator.generateId(), operatingTemperature, powerConsumption, emissions, diff --git a/microservices/coordinator-service/src/domain/anomaly/services/anomaly-detector.ts b/microservices/coordinator-service/src/domain/anomaly/services/anomaly-detector.ts index acd5983b..d7bdfbcc 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/anomaly-detector.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/anomaly-detector.ts @@ -1,12 +1,12 @@ import { MachineConfig } from '@/domain/machine/machine-config' import { Anomaly } from '../anomaly' import { AnomalyPolicy } from './policies/anomaly-policy' -import { Machine } from '@/domain/machine/machine' +import { TelemetryEvent } from '@/domain/machine/telemetry' export class AnomalyDetector { constructor(private readonly policies: AnomalyPolicy[]) {} - detect(event: Machine, config: MachineConfig): Anomaly[] { + detect(event: TelemetryEvent, config: MachineConfig): Anomaly[] { return this.policies.flatMap((p) => p.evaluate(event, config)) } } diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/anomaly-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/anomaly-policy.ts index fe4f21e9..44d109d7 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/anomaly-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/anomaly-policy.ts @@ -1,7 +1,7 @@ import { MachineConfig } from '@/domain/machine/machine-config' import { Anomaly } from '@/domain/anomaly/anomaly' -import { Machine } from '@/domain' +import { TelemetryEvent } from '@/domain' export interface AnomalyPolicy { - evaluate(event: Machine, config: MachineConfig): Anomaly[] + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] } diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts index ee37c383..bfaff887 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts @@ -3,13 +3,13 @@ import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { Machine } from '@/domain/machine/machine' +import { TelemetryEvent } from '@/domain/machine/telemetry' import { IdGenerator } from '@/domain/ports/id-generator' export class EmissionPolicy implements AnomalyPolicy { constructor(private readonly generator: IdGenerator) {} - evaluate(event: Machine, config: MachineConfig): Anomaly[] { + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] if (!config.emissions.contains(event.emissions)) { anomalies.push( diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts index b057f70b..a364f4b1 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts @@ -3,13 +3,13 @@ import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { Machine } from '@/domain/machine/machine' +import { TelemetryEvent } from '@/domain/machine/telemetry' import { IdGenerator } from '@/domain/ports/id-generator' export class PowerConsumptionPolicy implements AnomalyPolicy { constructor(private readonly generator: IdGenerator) {} - evaluate(event: Machine, config: MachineConfig): Anomaly[] { + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] if (!config.powerConsumption.contains(event.powerConsumption)) { anomalies.push( diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts index 346ffdf3..6ce5f95b 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts @@ -3,13 +3,13 @@ import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { Machine } from '@/domain/machine/machine' +import { TelemetryEvent } from '@/domain/machine/telemetry' import { IdGenerator } from '@/domain/ports/id-generator' export class PressurePolicy implements AnomalyPolicy { constructor(private readonly generator: IdGenerator) {} - evaluate(event: Machine, config: MachineConfig): Anomaly[] { + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] if (!config.pressure.contains(event.pressure)) { anomalies.push( diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/temperature-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/temperature-policy.ts index 98573957..67aed129 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/temperature-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/temperature-policy.ts @@ -3,13 +3,13 @@ import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { Machine } from '@/domain/machine/machine' +import { TelemetryEvent } from '@/domain/machine/telemetry' import { IdGenerator } from '@/domain/ports/id-generator' export class TemperaturePolicy implements AnomalyPolicy { constructor(private readonly generator: IdGenerator) {} - evaluate(event: Machine, config: MachineConfig): Anomaly[] { + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] if (!config.temperature.contains(event.temperature)) { anomalies.push( diff --git a/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts b/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts index 8141e176..d9da2082 100644 --- a/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts @@ -3,13 +3,13 @@ import { Anomaly } from '@/domain/anomaly/anomaly' import { AnomalyEventId } from '@/domain/anomaly/value-objects/anomaly-event-id' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { AnomalyPolicy } from './anomaly-policy' -import { Machine } from '@/domain/machine/machine' +import { TelemetryEvent } from '@/domain/machine/telemetry' import { IdGenerator } from '@/domain/ports/id-generator' export class VibrationPolicy implements AnomalyPolicy { constructor(private readonly generator: IdGenerator) {} - evaluate(event: Machine, config: MachineConfig): Anomaly[] { + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { const anomalies: Anomaly[] = [] if (!config.vibration.contains(event.vibration)) { anomalies.push( diff --git a/microservices/coordinator-service/src/domain/index.ts b/microservices/coordinator-service/src/domain/index.ts index 8bd7cb19..bb85b5c0 100644 --- a/microservices/coordinator-service/src/domain/index.ts +++ b/microservices/coordinator-service/src/domain/index.ts @@ -1,3 +1,3 @@ -export { Machine } from './machine/machine' -export { MachineFactory } from './machine/machine-factory' +export { TelemetryEvent } from './machine/telemetry' +export { TelemetryFactory } from './machine/telemetry-factory' export * from './machine/machine-config' diff --git a/microservices/coordinator-service/src/domain/machine/errors/telemetry-event-id-invalid.error.ts b/microservices/coordinator-service/src/domain/machine/errors/telemetry-event-id-invalid.error.ts new file mode 100644 index 00000000..742e7f3f --- /dev/null +++ b/microservices/coordinator-service/src/domain/machine/errors/telemetry-event-id-invalid.error.ts @@ -0,0 +1,6 @@ +export class TelemetryEventIdInvalidError extends Error { + constructor() { + super('TelemetryEventId cannot be empty') + this.name = 'TelemetryEventIdInvalidError' + } +} diff --git a/microservices/coordinator-service/src/domain/machine/machine-factory.ts b/microservices/coordinator-service/src/domain/machine/telemetry-factory.ts similarity index 76% rename from microservices/coordinator-service/src/domain/machine/machine-factory.ts rename to microservices/coordinator-service/src/domain/machine/telemetry-factory.ts index e4c63c5f..0eb593d5 100644 --- a/microservices/coordinator-service/src/domain/machine/machine-factory.ts +++ b/microservices/coordinator-service/src/domain/machine/telemetry-factory.ts @@ -1,14 +1,16 @@ import { Emission } from './value-objects/emission' -import { Machine } from './machine' +import { TelemetryEvent } from './telemetry' import { PowerConsumption } from './value-objects/power-consumption' import { Pressure } from './value-objects/pressure' import { Temperature } from './value-objects/temperature' import { Vibration } from './value-objects/vibration' import { MachineId } from './value-objects/machine-id' +import { TelemetryEventId } from './value-objects/telemetry-event-id' -export class MachineFactory { +export class TelemetryFactory { static create( id: string, + eventId: string, temperature: number, powerConsumption: number, emissions: number, @@ -17,9 +19,10 @@ export class MachineFactory { occurredAt: Date, processedAt: Date, sequenceNumber: number - ): Machine { - return new Machine( + ): TelemetryEvent { + return new TelemetryEvent( new MachineId(id), + new TelemetryEventId(eventId), new Temperature(temperature), new PowerConsumption(powerConsumption), new Emission(emissions), diff --git a/microservices/coordinator-service/src/domain/machine/machine.ts b/microservices/coordinator-service/src/domain/machine/telemetry.ts similarity index 75% rename from microservices/coordinator-service/src/domain/machine/machine.ts rename to microservices/coordinator-service/src/domain/machine/telemetry.ts index b593a4ab..c65f4ec5 100644 --- a/microservices/coordinator-service/src/domain/machine/machine.ts +++ b/microservices/coordinator-service/src/domain/machine/telemetry.ts @@ -1,26 +1,32 @@ -import type { Emission } from './value-objects/emission' -import type { MachineConfig } from './machine-config' -import type { PowerConsumption } from './value-objects/power-consumption' -import type { Pressure } from './value-objects/pressure' -import type { Temperature } from './value-objects/temperature' -import type { Vibration } from './value-objects/vibration' -import type { MachineId } from './value-objects/machine-id' +import { MachineId } from './value-objects/machine-id' +import { PowerConsumption } from './value-objects/power-consumption' +import { Emission } from './value-objects/emission' +import { Vibration } from './value-objects/vibration' +import { Pressure } from './value-objects/pressure' +import { Temperature } from './value-objects/temperature' +import { TelemetryEventId } from './value-objects/telemetry-event-id' +import { MachineConfig } from './machine-config' /** - * Represents a machine in a factory, with its current state and behavior to check if it's operating within the specified configuration limits. + * Represents a telemetry event for a machine, containing measurement of sensors and timestamps. */ -export class Machine { +export class TelemetryEvent { /** - * Creates a new {@link Machine} instance with the given parameters. + * Creates a new {@link TelemetryEvent} instance with the given parameters. * @param id the unique identifier of the machine + * @param eventId the unique identifier of the telemetry event * @param temperature the current temperature of the machine * @param powerConsumption the current power consumption of the machine * @param emissions the current emissions of the machine * @param vibration the current vibration level of the machine * @param pressure the current pressure of the machine + * @param occurredAt the timestamp when the telemetry event was recorded + * @param processedAt the timestamp when the telemetry event was processed + * @param sequenceNumber the sequence number of the telemetry event */ constructor( public readonly id: MachineId, + public eventId: TelemetryEventId, public temperature: Temperature, public powerConsumption: PowerConsumption, public emissions: Emission, @@ -36,7 +42,7 @@ export class Machine { * @param other the other machine to compare with * @returns true if the machines are equal, false otherwise */ - isEqual(other: Machine): boolean { + isEqual(other: TelemetryEvent): boolean { return this.id.equals(other.id) } diff --git a/microservices/coordinator-service/src/domain/machine/value-objects/telemetry-event-id.ts b/microservices/coordinator-service/src/domain/machine/value-objects/telemetry-event-id.ts new file mode 100644 index 00000000..bd51b9dd --- /dev/null +++ b/microservices/coordinator-service/src/domain/machine/value-objects/telemetry-event-id.ts @@ -0,0 +1,13 @@ +import { TelemetryEventIdInvalidError } from '../errors/telemetry-event-id-invalid.error' + +export class TelemetryEventId { + constructor(public readonly value: string) { + if (!value || value.trim().length === 0) { + throw new TelemetryEventIdInvalidError() + } + } + + equals(other: TelemetryEventId): boolean { + return this.value === other.value + } +} diff --git a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/telemetry.mapper.ts b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/telemetry.mapper.ts index 5d1d5aed..fb064dad 100644 --- a/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/telemetry.mapper.ts +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/mapper/telemetry.mapper.ts @@ -5,16 +5,16 @@ export function toTelemetryDto( telemetryData: ProcessTelemetryResult ): TelemetryDto { return { - eventId: telemetryData.event.eventId, - machineId: telemetryData.event.machineId, - sequenceNumber: telemetryData.event.sequenceNumber, - capturedAt: telemetryData.event.occurredAt.toISOString(), + eventId: telemetryData.machine.eventId.value, + machineId: telemetryData.machine.id.value, + sequenceNumber: telemetryData.machine.sequenceNumber, + capturedAt: telemetryData.machine.occurredAt.toISOString(), readings: { - powerConsumption: telemetryData.event.powerConsumption, - emissions: telemetryData.event.emissions, - operatingTemperature: telemetryData.event.operatingTemperature, - vibration: telemetryData.event.vibration, - pressure: telemetryData.event.pressure + powerConsumption: telemetryData.machine.powerConsumption.value, + emissions: telemetryData.machine.emissions.value, + operatingTemperature: telemetryData.machine.temperature.value, + vibration: telemetryData.machine.vibration.value, + pressure: telemetryData.machine.pressure.value }, anomaly: { isAnomaly: telemetryData.anomalies.length > 0, diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index 6e2d204c..42dbfa32 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -56,7 +56,12 @@ async function bootstrap() { new PowerConsumptionPolicy(generator) ] const anomalyDetector = new AnomalyDetector(policies) - const processTelemetry = new ProcessTelemetry(anomalyDetector, clock, logger) + const processTelemetry = new ProcessTelemetry( + anomalyDetector, + generator, + clock, + logger + ) const processHeartbeat = new ProcessHeartbeat(clock, logger) const machineConfigs = await coreRestService.getMachineConfigs({ limit: 50, diff --git a/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts b/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts index e9798527..8c35316d 100644 --- a/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts +++ b/microservices/coordinator-service/test/application/telemetry/process-telemetry.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { createAnomalyDetectorMock } from '@test/mocks/anomaly-detector.mock' import { createLoggerMock } from '@test/mocks/logger.mock' import { fakeConfig } from '@test/utils/fake-config' @@ -10,6 +10,9 @@ import { TelemetryInput } from '@/application/telemetry/dto/telemetry-input' describe('ProcessTelemetry', () => { const occurredAtDate: Date = new Date('2025-12-31T23:59:00.000Z') const processedAtDate: Date = new Date('2026-01-01T00:00:00.000Z') + const fakeGenerator = { + generateId: vi.fn(() => MACHINE_VALUES.EVENT_ID) + } it('should process telemetry correctly', () => { const logger = createLoggerMock() @@ -19,6 +22,7 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) @@ -57,6 +61,7 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) @@ -94,6 +99,7 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) @@ -119,6 +125,7 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) @@ -144,6 +151,7 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) diff --git a/microservices/coordinator-service/test/constants/machine-values.ts b/microservices/coordinator-service/test/constants/machine-values.ts index 8733b4dc..ff1391b7 100644 --- a/microservices/coordinator-service/test/constants/machine-values.ts +++ b/microservices/coordinator-service/test/constants/machine-values.ts @@ -6,6 +6,8 @@ export const MACHINE_IDS = { export const MACHINE_VALUES = { ID: MACHINE_IDS.DEFAULT, + EVENT_ID: '563efbc3-6e91-4d98-b10d-67e68edddc5c', + TEMPERATURE: { SAFE: 55, OVER: 90, diff --git a/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts index 75df3b8e..22c8180d 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts @@ -7,8 +7,8 @@ import { MACHINE_VALUES } from '@test/constants/machine-values' import { fakeConfig } from '@test/utils/fake-config' import { VibrationPolicy } from '@/domain/anomaly/services/policies/vibration-policy' import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' -import { MachineFactory } from '@/domain/machine/machine-factory' -import { Machine } from '@/domain/machine/machine' +import { TelemetryFactory } from '@/domain/machine/telemetry-factory' +import { TelemetryEvent } from '@/domain/machine/telemetry' describe('AnomalyDetector', () => { const generator = new UUIDGenerator() @@ -16,8 +16,9 @@ describe('AnomalyDetector', () => { it('should return an empty array when no policies detect anomalies', () => { const detector = new AnomalyDetector([new TemperaturePolicy(generator)]) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, @@ -41,8 +42,9 @@ describe('AnomalyDetector', () => { it('should detect an anomaly from a single policy', () => { const detector = new AnomalyDetector([new TemperaturePolicy(generator)]) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.OVER, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, @@ -76,8 +78,9 @@ describe('AnomalyDetector', () => { new VibrationPolicy(generator) ]) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.OVER, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, @@ -116,8 +119,9 @@ describe('AnomalyDetector', () => { it('should not detect an anomaly when no policy is defined for the sensor type', () => { const detector = new AnomalyDetector([new TemperaturePolicy(generator)]) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts index f542284e..407912f2 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts @@ -6,16 +6,17 @@ import { MACHINE_LIMITS } from '@test/constants/machine-limits' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { EmissionPolicy } from '@/domain/anomaly/services/policies/emission-policy' import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' -import { MachineFactory } from '@/domain/machine/machine-factory' -import { Machine } from '@/domain/machine/machine' +import { TelemetryFactory } from '@/domain/machine/telemetry-factory' +import { TelemetryEvent } from '@/domain/machine/telemetry' describe('EmissionPolicy', () => { const generator = new UUIDGenerator() it('should return an anomaly with correct details when emission is over the maximum limit', () => { const policy = new EmissionPolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.OVER, @@ -39,8 +40,9 @@ describe('EmissionPolicy', () => { it('should not detect any anomaly when emissions is within limits', () => { const policy = new EmissionPolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts index a4943ddc..3cf75a2c 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts @@ -6,16 +6,17 @@ import { MACHINE_LIMITS } from '@test/constants/machine-limits' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { PowerConsumptionPolicy } from '@/domain/anomaly/services/policies/power-consumption-policy' import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' -import { MachineFactory } from '@/domain/machine/machine-factory' -import { Machine } from '@/domain/machine/machine' +import { TelemetryFactory } from '@/domain/machine/telemetry-factory' +import { TelemetryEvent } from '@/domain/machine/telemetry' describe('PowerConsumptionPolicy', () => { const generator = new UUIDGenerator() it('should return an anomaly with correct details when power consumption is over the maximum limit', () => { const policy = new PowerConsumptionPolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.OVER, MACHINE_VALUES.EMISSION.SAFE, @@ -39,8 +40,9 @@ describe('PowerConsumptionPolicy', () => { it('should not detect any anomaly when power consumption is within limits', () => { const policy = new PowerConsumptionPolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts index caa1a724..b439d523 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts @@ -6,16 +6,17 @@ import { MACHINE_LIMITS } from '@test/constants/machine-limits' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { PressurePolicy } from '@/domain/anomaly/services/policies/pressure-policy' import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' -import { MachineFactory } from '@/domain/machine/machine-factory' -import { Machine } from '@/domain/machine/machine' +import { TelemetryFactory } from '@/domain/machine/telemetry-factory' +import { TelemetryEvent } from '@/domain/machine/telemetry' describe('PressurePolicy', () => { const generator = new UUIDGenerator() it('should return an anomaly with correct details when pressure is over the maximum limit', () => { const policy = new PressurePolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, @@ -39,8 +40,9 @@ describe('PressurePolicy', () => { it('should not detect any anomaly when pressure is within limits', () => { const policy = new PressurePolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts index e1614a04..e2e71d17 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/temperature-policy.spec.ts @@ -5,8 +5,8 @@ import { MachineConfig } from '@/domain/machine/machine-config' import { MACHINE_LIMITS } from '@test/constants/machine-limits' import { TemperaturePolicy } from '@/domain/anomaly/services/policies/temperature-policy' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' -import { MachineFactory } from '@/domain/machine/machine-factory' -import { Machine } from '@/domain/machine/machine' +import { TelemetryFactory } from '@/domain/machine/telemetry-factory' +import { TelemetryEvent } from '@/domain/machine/telemetry' import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' describe('TemperaturePolicy', () => { @@ -14,8 +14,9 @@ describe('TemperaturePolicy', () => { it('should return an anomaly with correct details when temperature is over the maximum limit', () => { const policy = new TemperaturePolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.OVER, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, @@ -39,8 +40,9 @@ describe('TemperaturePolicy', () => { it('should detect temperature anomalies under the minimum limit', () => { const policy = new TemperaturePolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.UNDER, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, @@ -64,8 +66,9 @@ describe('TemperaturePolicy', () => { it('should not detect any anomaly when temperature is within limits', () => { const policy = new TemperaturePolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, diff --git a/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts index 9014adb9..2787752f 100644 --- a/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts @@ -6,16 +6,17 @@ import { MACHINE_LIMITS } from '@test/constants/machine-limits' import { SensorType } from '@/domain/anomaly/value-objects/sensor-type' import { VibrationPolicy } from '@/domain/anomaly/services/policies/vibration-policy' import { UUIDGenerator } from '@/infrastructure/adapters/uuid-generator' -import { MachineFactory } from '@/domain/machine/machine-factory' -import { Machine } from '@/domain/machine/machine' +import { TelemetryFactory } from '@/domain/machine/telemetry-factory' +import { TelemetryEvent } from '@/domain/machine/telemetry' describe('VibrationPolicy', () => { const generator = new UUIDGenerator() it('should return an anomaly with correct details when vibration is over the maximum limit', () => { const policy = new VibrationPolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, @@ -39,8 +40,9 @@ describe('VibrationPolicy', () => { it('should not detect any anomaly when vibration is within limits', () => { const policy = new VibrationPolicy(generator) - const machine: Machine = MachineFactory.create( + const machine: TelemetryEvent = TelemetryFactory.create( MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, MACHINE_VALUES.TEMPERATURE.SAFE, MACHINE_VALUES.POWER_CONSUMPTION.SAFE, MACHINE_VALUES.EMISSION.SAFE, diff --git a/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts b/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts deleted file mode 100644 index f70d683a..00000000 --- a/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { MachineFactory } from '@/domain/machine/machine-factory' -import { MACHINE_IDS } from '@test/constants/machine-values' - -describe('MachineFactory', () => { - it('should create a machine from sensors data', () => { - const telemetryInput = { - id: MACHINE_IDS.DEFAULT, - temperature: 50, - powerConsumption: 100, - emissions: 20, - vibration: 1.2, - pressure: 5 - } - - const machine = MachineFactory.create( - telemetryInput.id, - telemetryInput.temperature, - telemetryInput.powerConsumption, - telemetryInput.emissions, - telemetryInput.vibration, - telemetryInput.pressure, - new Date('2025-12-31T23:59:00.000Z'), - new Date('2026-01-01T00:00:00Z'), - 1 - ) - - expect(machine).toMatchObject({ - id: expect.objectContaining({ value: MACHINE_IDS.DEFAULT }), - temperature: expect.objectContaining({ value: 50 }), - powerConsumption: expect.objectContaining({ value: 100 }), - emissions: expect.objectContaining({ value: 20 }), - vibration: expect.objectContaining({ value: 1.2 }), - pressure: expect.objectContaining({ value: 5 }) - }) - }) -}) diff --git a/microservices/coordinator-service/test/domain/machine/machine.spec.ts b/microservices/coordinator-service/test/domain/machine/machine.spec.ts index 39ce2d40..89e1dc84 100644 --- a/microservices/coordinator-service/test/domain/machine/machine.spec.ts +++ b/microservices/coordinator-service/test/domain/machine/machine.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { Machine } from '@/domain/machine/machine' +import { TelemetryEvent } from '@/domain/machine/telemetry' import { fakeConfig } from '@test/utils/fake-config' import { MACHINE_IDS, MACHINE_VALUES } from '@test/constants/machine-values' import { MachineConfig } from '@/domain/machine/machine-config' @@ -34,7 +34,7 @@ describe('Machine', () => { }) it('returns true when temperature exceeds max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ temperature: MACHINE_VALUES.TEMPERATURE.OVER }) const config: MachineConfig = fakeConfig({ @@ -46,7 +46,7 @@ describe('Machine', () => { }) it('returns false when temperature is below max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ temperature: MACHINE_VALUES.TEMPERATURE.SAFE }) const config: MachineConfig = fakeConfig({ @@ -58,7 +58,7 @@ describe('Machine', () => { }) it('returns true when temperature is below min', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ temperature: MACHINE_VALUES.TEMPERATURE.UNDER }) const config: MachineConfig = fakeConfig({ @@ -70,7 +70,7 @@ describe('Machine', () => { }) it('returns false when temperature is above min', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ temperature: MACHINE_VALUES.TEMPERATURE.SAFE }) const config: MachineConfig = fakeConfig({ @@ -82,7 +82,7 @@ describe('Machine', () => { }) it('returns true when power consumption exceeds max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.OVER }) const config: MachineConfig = fakeConfig({ @@ -94,7 +94,7 @@ describe('Machine', () => { }) it('returns false when power consumption is below max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ powerConsumption: MACHINE_VALUES.POWER_CONSUMPTION.SAFE }) const config: MachineConfig = fakeConfig({ @@ -106,7 +106,7 @@ describe('Machine', () => { }) it('returns true when emissions exceed max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ emissions: MACHINE_VALUES.EMISSION.OVER }) const config: MachineConfig = fakeConfig({ @@ -118,7 +118,7 @@ describe('Machine', () => { }) it('returns false when emissions are below max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ emissions: MACHINE_VALUES.EMISSION.SAFE }) const config: MachineConfig = fakeConfig({ @@ -130,7 +130,7 @@ describe('Machine', () => { }) it('returns true when vibration exceeds max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ vibration: MACHINE_VALUES.VIBRATION.OVER }) const config: MachineConfig = fakeConfig({ @@ -142,7 +142,7 @@ describe('Machine', () => { }) it('returns false when vibration is below max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ vibration: MACHINE_VALUES.VIBRATION.SAFE }) const config: MachineConfig = fakeConfig({ @@ -154,7 +154,7 @@ describe('Machine', () => { }) it('returns true when pressure exceeds max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ pressure: MACHINE_VALUES.PRESSURE.OVER }) const config: MachineConfig = fakeConfig({ @@ -166,7 +166,7 @@ describe('Machine', () => { }) it('returns false when pressure is below max', () => { - const machine: Machine = fakeMachine({ + const machine: TelemetryEvent = fakeMachine({ pressure: MACHINE_VALUES.PRESSURE.SAFE }) const config: MachineConfig = fakeConfig({ diff --git a/microservices/coordinator-service/test/domain/machine/telemetry-factory.spec.ts b/microservices/coordinator-service/test/domain/machine/telemetry-factory.spec.ts new file mode 100644 index 00000000..02c28ce5 --- /dev/null +++ b/microservices/coordinator-service/test/domain/machine/telemetry-factory.spec.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' +import { MACHINE_VALUES } from '@test/constants/machine-values' +import { TelemetryFactory } from '@/domain/machine/telemetry-factory' + +describe('TelemetryFactory', () => { + it('should create telemetry from sensor data', () => { + const telemetry = TelemetryFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_ID, + MACHINE_VALUES.TEMPERATURE.SAFE, + MACHINE_VALUES.POWER_CONSUMPTION.SAFE, + MACHINE_VALUES.EMISSION.SAFE, + MACHINE_VALUES.VIBRATION.SAFE, + MACHINE_VALUES.PRESSURE.SAFE, + MACHINE_VALUES.OCCURRED_AT, + MACHINE_VALUES.PROCESSED_AT, + MACHINE_VALUES.SEQUENCE_NUMBER + ) + + expect(telemetry).toMatchObject({ + id: expect.objectContaining({ value: MACHINE_VALUES.ID }), + eventId: expect.objectContaining({ value: MACHINE_VALUES.EVENT_ID }), + temperature: expect.objectContaining({ + value: MACHINE_VALUES.TEMPERATURE.SAFE + }), + powerConsumption: expect.objectContaining({ + value: MACHINE_VALUES.POWER_CONSUMPTION.SAFE + }), + emissions: expect.objectContaining({ + value: MACHINE_VALUES.EMISSION.SAFE + }), + vibration: expect.objectContaining({ + value: MACHINE_VALUES.VIBRATION.SAFE + }), + pressure: expect.objectContaining({ + value: MACHINE_VALUES.PRESSURE.SAFE + }) + }) + }) +}) diff --git a/microservices/coordinator-service/test/utils/fake-machine.ts b/microservices/coordinator-service/test/utils/fake-machine.ts index c6cc147a..08e497ff 100644 --- a/microservices/coordinator-service/test/utils/fake-machine.ts +++ b/microservices/coordinator-service/test/utils/fake-machine.ts @@ -1,8 +1,9 @@ -import { MachineFactory } from '@/domain/machine/machine-factory' +import { TelemetryFactory } from '@/domain/machine/telemetry-factory' import { MACHINE_VALUES } from '@test/constants/machine-values' export function fakeMachine(overrides?: { id?: string + eventId?: string temperature?: number powerConsumption?: number emissions?: number @@ -12,8 +13,9 @@ export function fakeMachine(overrides?: { processedAt?: Date sequenceNumber?: number }) { - return MachineFactory.create( + return TelemetryFactory.create( overrides?.id ?? MACHINE_VALUES.ID, + overrides?.eventId ?? MACHINE_VALUES.EVENT_ID, overrides?.temperature ?? MACHINE_VALUES.TEMPERATURE.SAFE, overrides?.powerConsumption ?? MACHINE_VALUES.POWER_CONSUMPTION.SAFE, overrides?.emissions ?? MACHINE_VALUES.EMISSION.SAFE, From cd6ddf1fd3e671bebaf89ea5edb3755a17e609f5 Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 13 Oct 2026 11:18:41 +0200 Subject: [PATCH 124/125] feat(deps): add dependency-cruiser for architecture testing --- .../coordinator-service/package-lock.json | 488 +++++++++++++++++- .../coordinator-service/package.json | 2 + 2 files changed, 479 insertions(+), 11 deletions(-) diff --git a/microservices/coordinator-service/package-lock.json b/microservices/coordinator-service/package-lock.json index f635d3b5..7c59b152 100644 --- a/microservices/coordinator-service/package-lock.json +++ b/microservices/coordinator-service/package-lock.json @@ -19,6 +19,7 @@ "@types/node": "^25.9.4", "@vitest/coverage-v8": "^4.1.10", "@vitest/ui": "^4.1.10", + "dependency-cruiser": "^18.4.0", "pino-pretty": "^13.1.3", "prettier": "^3.9.4", "rimraf": "^6.1.3", @@ -1694,12 +1695,11 @@ } }, "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1713,11 +1713,43 @@ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn-loose": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz", + "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1736,6 +1768,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1904,12 +1952,49 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -1917,6 +2002,16 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/commist": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", @@ -2034,6 +2129,54 @@ "license": "MIT", "peer": true }, + "node_modules/dependency-cruiser": { + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-18.4.0.tgz", + "integrity": "sha512-LLBYQ2XYmOCMG+liUWI2ReRYnnFXIbnzvCGHTohXAViSkawXr3T35fF/FV2cxpmB661pfkJ3ZXn95jhcEQ9GqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "8.18.0", + "acorn-jsx": "5.3.2", + "acorn-jsx-walk": "2.0.0", + "acorn-loose": "8.5.2", + "acorn-walk": "8.3.5", + "commander": "15.0.0", + "enhanced-resolve": "5.25.1", + "ignore": "7.0.9", + "interpret": "3.1.1", + "is-installed-globally": "1.0.0", + "json5": "2.2.3", + "picomatch": "4.0.7", + "prompts": "2.4.2", + "rechoir": "0.8.0", + "safe-regex": "2.1.1", + "semver": "7.8.5", + "tsconfig-paths-webpack-plugin": "4.2.0", + "watskeburt": "6.0.0" + }, + "bin": { + "depcruise": "bin/dependency-cruiser.mjs", + "depcruise-baseline": "bin/depcruise-baseline.mjs", + "depcruise-fmt": "bin/depcruise-fmt.mjs", + "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", + "dependency-cruise": "bin/dependency-cruiser.mjs", + "dependency-cruiser": "bin/dependency-cruiser.mjs" + }, + "engines": { + "node": "^22||^24||>=26" + } + }, + "node_modules/dependency-cruiser/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2064,6 +2207,20 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.25.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.25.1.tgz", + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -2077,6 +2234,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", @@ -2506,6 +2673,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -2544,6 +2721,39 @@ "node": ">=10.13.0" } }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2554,6 +2764,19 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/help-me": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", @@ -2621,6 +2844,16 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/ip-address": { "version": "10.7.2", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", @@ -2630,6 +2863,22 @@ "node": ">= 12" } }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2655,6 +2904,36 @@ "node": ">=0.10.0" } }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2753,6 +3032,19 @@ "license": "MIT", "peer": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2764,6 +3056,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3465,6 +3767,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -3497,9 +3806,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -3685,6 +3994,20 @@ ], "license": "MIT" }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -3771,6 +4094,51 @@ "node": ">= 12.13.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -3851,6 +4219,16 @@ ], "license": "MIT" }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", @@ -3878,9 +4256,9 @@ "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3981,6 +4359,13 @@ "node": ">=18" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -4056,6 +4441,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", @@ -4082,6 +4477,33 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar-fs": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", @@ -4244,6 +4666,37 @@ "typescript": ">=4.8.4" } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4561,6 +5014,19 @@ } } }, + "node_modules/watskeburt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-6.0.0.tgz", + "integrity": "sha512-jfiuDABaxSkC71T6oZ3vCS99roYkSHm/+As+G0Dz8taAHQb+SJBvLEm5RlsgG71XdfAj3rv7eudUBTgwcQUPlQ==", + "dev": true, + "license": "MIT", + "bin": { + "watskeburt": "dist/run-cli.js" + }, + "engines": { + "node": "^22.13||^24||>=26" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/microservices/coordinator-service/package.json b/microservices/coordinator-service/package.json index e1b4c20e..fdeaf79b 100644 --- a/microservices/coordinator-service/package.json +++ b/microservices/coordinator-service/package.json @@ -16,6 +16,7 @@ "test": "vitest run", "test:coverage": "vitest run --coverage", "test:coverage:reporter": "vitest run --coverage --reporter=html", + "test:architecture": "dependency-cruiser --config .dependency-cruiser.cjs src", "docs": "typedoc --tsconfig tsconfig.json --entryPointStrategy expand src --out docs", "docs:clean": "rimraf docs", "docs:watch": "typedoc --tsconfig tsconfig.json --entryPointStrategy expand src --watch" @@ -40,6 +41,7 @@ "@types/node": "^25.9.4", "@vitest/coverage-v8": "^4.1.10", "@vitest/ui": "^4.1.10", + "dependency-cruiser": "^18.4.0", "pino-pretty": "^13.1.3", "prettier": "^3.9.4", "rimraf": "^6.1.3", From c7ba97a34d4c207f1fa6bb21309a42bb22a16d1c Mon Sep 17 00:00:00 2001 From: Giulia Nardicchia Date: Tue, 13 Oct 2026 11:20:56 +0200 Subject: [PATCH 125/125] feat(architecture): generate dependency-cruiser configuration for module dependency analysis (add domain-application-infrastructure checks) --- .../.dependency-cruiser.cjs | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 microservices/coordinator-service/.dependency-cruiser.cjs diff --git a/microservices/coordinator-service/.dependency-cruiser.cjs b/microservices/coordinator-service/.dependency-cruiser.cjs new file mode 100644 index 00000000..51087496 --- /dev/null +++ b/microservices/coordinator-service/.dependency-cruiser.cjs @@ -0,0 +1,419 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + { + name: 'no-unreachable-from-domain', + comment: 'The domain layer should not depend on any other layer', + severity: 'error', + from: { + path: '^(src/domain)' + }, + to: { + pathNot: '^(src/domain)' + } + }, + { + name: 'no-unreachable-from-application', + comment: 'The application layer should only depend on the domain layer', + severity: 'error', + from: { + path: '^(src/application)' + }, + to: { + pathNot: + '^(src/domain)|^(src/application)' + } + }, + { + name: 'no-unreachable-from-infrastructure', + comment: + 'The infrastructure layer may depend on domain, application, infrastructure, configuration and external packages', + severity: 'error', + from: { + path: '^src/infrastructure' + }, + to: { + path: '^src/', + pathNot: + '^(src/domain)|^(src/application)|^(src/infrastructure)|^(src/config)' + } + }, + { + name: 'no-circular', + severity: 'warn', + comment: + "This dependency is part of a circular relationship. You might want to revise " + + "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", + from: {}, + to: { + circular: true + } + }, + { + name: 'no-orphans', + comment: + "This is an orphan module - it's likely not used (anymore?). Either use it or " + + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + + "add an exception for it in your dependency-cruiser configuration. By default " + + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", + severity: 'warn', + from: { + orphan: true, + pathNot: [ + '(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files + '[.]d[.]ts$', // TypeScript declaration files + '(^|/)tsconfig[.]json$', // TypeScript config + '(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs + ] + }, + to: {}, + }, + { + name: 'no-deprecated-core', + comment: + 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "bound to exist - node doesn't deprecate lightly.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'core' + ], + path: [ + '^v8/tools/codemap$', + '^v8/tools/consarray$', + '^v8/tools/csvparser$', + '^v8/tools/logreader$', + '^v8/tools/profile_view$', + '^v8/tools/profile$', + '^v8/tools/SourceMap$', + '^v8/tools/splaytree$', + '^v8/tools/tickprocessor-driver$', + '^v8/tools/tickprocessor$', + '^node-inspect/lib/_inspect$', + '^node-inspect/lib/internal/inspect_client$', + '^node-inspect/lib/internal/inspect_repl$', + '^async_hooks$', + '^punycode$', + '^domain$', + '^constants$', + '^sys$', + '^_linklist$', + '^_stream_wrap$' + ], + } + }, + { + name: 'not-to-deprecated', + comment: + 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + + 'version of that module, or find an alternative. Deprecated modules are a security risk.', + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'deprecated' + ] + } + }, + { + name: 'no-non-package-json', + severity: 'error', + comment: + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + + "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + + "in your package.json.", + from: {}, + to: { + dependencyTypes: [ + 'npm-no-pkg', + 'npm-unknown' + ] + } + }, + { + name: 'not-to-unresolvable', + comment: + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + + 'module: add it to your package.json. In all other cases you likely already know what to do.', + severity: 'error', + from: {}, + to: { + couldNotResolve: true + } + }, + { + name: 'no-duplicate-dep-types', + comment: + "Likely this module depends on an external ('npm') package that occurs more than once " + + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + + "maintenance problems later on.", + severity: 'warn', + from: {}, + to: { + moreThanOneDependencyType: true, + // as it's common to use a devDependency for type-only imports: don't + // consider type-only dependencyTypes for this rule + dependencyTypesNot: ["type-only"] + } + }, + + // rules you might want to tweak for your specific situation: + { + name: 'not-to-test', + comment: + "This module depends on code within a folder that should only contain tests. As tests don't " + + "implement functionality this is odd. Either you're writing a test outside the test folder " + + "or there's something in the test folder that isn't a test.", + severity: 'error', + from: { + pathNot: '^(test)' + }, + to: { + path: '^(test)' + } + }, + { + name: 'not-to-spec', + comment: + 'This module depends on a spec (test) file. The responsibility of a spec file is to test code. ' + + "If there's something in a spec that's of use to other modules, it doesn't have that single " + + 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', + severity: 'error', + from: {}, + to: { + path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + } + }, + { + name: 'not-to-dev-dep', + severity: 'error', + comment: + "This module depends on an npm package from the 'devDependencies' section of your " + + 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + + 'section of your package.json. If this module is development only - add it to the ' + + 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + from: { + path: '^(src)', + pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + }, + to: { + dependencyTypes: [ + 'npm-dev', + ], + // type only dependencies are not a problem as they don't end up in the + // production code or are ignored by the runtime. + dependencyTypesNot: [ + 'type-only' + ], + pathNot: [ + 'node_modules/@types/' + ] + } + }, + { + name: 'optional-deps-used', + severity: 'info', + comment: + "This module depends on an npm package that is declared as an optional dependency " + + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + + "If you use an optional dependency here by design - add an exception to your" + + "dependency-cruiser configuration.", + from: {}, + to: { + dependencyTypes: [ + 'npm-optional' + ] + } + }, + { + name: 'peer-deps-used', + comment: + "This module depends on an npm package that is declared as a peer dependency " + + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + + "other cases - maybe not so much. If the use of a peer dependency is intentional " + + "add an exception to your dependency-cruiser configuration.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'npm-peer' + ] + } + } + ], + options: { + // Which modules not to follow further when encountered + doNotFollow: { + // path: an array of regular expressions in strings to match against + path: ['node_modules'] + }, + + // Which modules to exclude + // exclude : { + // // path: an array of regular expressions in strings to match against + // path: '', + // }, + + // Which modules to exclusively include (array of regular expressions in strings) + // dependency-cruiser will skip everything that doesn't match this pattern + // includeOnly : [''], + + // List of module systems to cruise. + // When left out dependency-cruiser will fall back to the list of _all_ + // module systems it knows of ('amd', 'cjs', 'es6', 'tsd']). It's the + // default because it's the safe option. It comes at a performance penalty, though + // As in practice only commonjs ('cjs') and ecmascript modules ('es6') + // are in wide use, you can limit the moduleSystems to those. + // moduleSystems: ['cjs', 'es6'], + + // false: don't look at JSDoc imports (the default) + // true: detect dependencies in JSDoc-style import statements. + // Implies parser: 'tsc', which a.o. means the typescript compiler will need + // to be installed in the same spot you run dependency-cruiser from. + // detectJSDocImports: true, + + // false: don't look at process.getBuiltinModule calls (the default) + // true: dependency-cruiser will detect calls to process.getBuiltinModule/ + // globalThis.process.getBuiltinModule as imports. + // detectProcessBuiltinModuleCalls: true, + + // prefix for links in html, d2, mermaid and dot/ svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' + // to open it on your online repo or `vscode://file/${process.cwd()}/` to + // open it in visual studio code), + // prefix: `vscode://file/${process.cwd()}/`, + + // suffix for links in output. E.g. put .html here if you use it to link to + // your coverage reports. + // suffix: '.html', + + // false (the default): ignore dependencies that only exist before typescript-to-javascript compilation + // true: also detect dependencies that only exist before typescript-to-javascript compilation + // 'specify': for each dependency identify whether it only exists before compilation or also after + tsPreCompilationDeps: true, + + // list of extensions to scan that aren't javascript or compile-to-javascript. + // Empty by default. Only put extensions in here that you want to take into + // account that are _not_ parsable. + // extraExtensionsToScan: ['.json', '.jpg', '.png', '.svg', '.webp'], + + // if true combines the package.jsons found from the module up to the base + // folder the cruise is initiated from. Useful for how (some) mono-repos + // manage dependencies & dependency definitions. + // combinedDependencies: false, + + // if true leave symlinks untouched, otherwise use the realpath + // preserveSymlinks: false, + + // TypeScript project file ('tsconfig.json') to use for + // (1) compilation and + // (2) resolution (e.g. with the paths property) + // + // The (optional) fileName attribute specifies which file to take (relative to + // dependency-cruiser's current working directory). When not provided + // defaults to './tsconfig.json'. + tsConfig: { + fileName: 'tsconfig.json' + }, + + // Webpack configuration to use to get resolve options from. + // + // The (optional) fileName attribute specifies which file to take (relative + // to dependency-cruiser's current working directory. When not provided defaults + // to './webpack.conf.js'. + // + // The (optional) 'env' and 'arguments' attributes contain the parameters + // to be passed if your webpack config is a function and takes them (see + // webpack documentation for details) + // webpackConfig: { + // fileName: 'webpack.config.js', + // env: {}, + // arguments: {} + // }, + + // Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use + // for compilation + // babelConfig: { + // fileName: '.babelrc', + // }, + + // List of strings you have in use in addition to cjs/ es6 requires + // & imports to declare module dependencies. Use this e.g. if you've + // re-declared require, use a require-wrapper or use window.require as + // a hack. + // exoticRequireStrings: [], + + // options to pass on to enhanced-resolve, the package dependency-cruiser + // uses to resolve module references to disk. The values below should be + // suitable for most situations + // + // If you use webpack: you can also set these in webpack.conf.js. The set + // there will override the ones specified here. + enhancedResolveOptions: { + // What to consider as an 'exports' field in package.jsons + exportsFields: ['exports'], + + // List of conditions to check for in the exports field. + // Only works when the 'exportsFields' array is non-empty. + conditionNames: ['import', 'require', 'node', 'default', 'types'], + + // The extensions, by default are the same as the ones dependency-cruiser + // can access (run `npx dependency-cruiser --info` to see which ones that are in + // _your_ environment). If that list is larger than you need you can pass + // the extensions you actually use (e.g. ['.js', '.jsx']). This can speed + // up module resolution, which is the most expensive step. + // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + + // What to consider a 'main' field in package.json + mainFields: ["module", "main", "types", "typings"], + + // A list of alias fields in package.jsons + // See https://github.com/defunctzombie/package-browser-field-spec and + // the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) + // documentation. + // Defaults to an empty array (= don't use alias fields). + // aliasFields: ['browser'], + }, + + // skipAnalysisNotInRules will make dependency-cruiser execute + // analysis strictly necessary for checking the rule set only. + // See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules + skipAnalysisNotInRules: true, + + reporterOptions: { + dot: { + // Pattern of modules to consolidate to. The default pattern in this configuration + // collapses everything in node_modules to one folder deep so you see + // the external modules, but not their innards. + collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + // Options to tweak the appearance of your graph. See + // https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions + // If you don't specify a theme dependency-cruiser falls back to a built-in one. + // theme: { + // graph: { + // // splines: 'ortho' - straight lines; slow on big graphs + // // splines: 'true' - bezier curves; fast but not as nice as ortho + // splines: 'true' + // }, + // }, + }, + archi: { + // Pattern of modules to consolidate to. + collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + // Options to tweak the appearance of your graph. If you don't specify a + // theme for 'archi' dependency-cruiser will use the one specified in the + // dot section above and otherwise use the default one. + // theme: { }, + }, + text: { + highlightFocused: true + }, + } + } +}; +// generated: dependency-cruiser@18.4.0 on 2026-09-24T15:14:44.723Z