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 diff --git a/docker/docker-compose.simulators.yml b/docker/docker-compose.simulators.yml index 5b0f2b75..280f0d0f 100644 --- a/docker/docker-compose.simulators.yml +++ b/docker/docker-compose.simulators.yml @@ -11,22 +11,34 @@ services: context: ../microservices/coordinator-service depends_on: - mqtt-broker + - core-rest-service environment: + 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 + volumes: + - coordinator_data:/app/data - machine-simulator-1: + machine-simulator: build: context: ../microservices/machine-simulator-service depends_on: - mqtt-broker environment: - MACHINE_ID: machine-1 + NODE_ENV: development + MACHINE_ID: ${MACHINE_ID} MQTT_URL: mqtt://mqtt-broker:1883 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 + ANOMALIES: ${ANOMALIES:-} + volumes: + - simulator_data:/app/data + +volumes: + coordinator_data: + simulator_data: \ No newline at end of file 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 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 diff --git a/microservices/coordinator-service/.env.example b/microservices/coordinator-service/.env.example new file mode 100644 index 00000000..e21426fb --- /dev/null +++ b/microservices/coordinator-service/.env.example @@ -0,0 +1,14 @@ +NODE_ENV=development + +MQTT_URL=mqtt://mqtt-broker:1883 +MQTT_TOPIC=factory/telemetry +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/Dockerfile b/microservices/coordinator-service/Dockerfile index c7c18c3b..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 install +RUN npm ci COPY . . 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 diff --git a/microservices/coordinator-service/package-lock.json b/microservices/coordinator-service/package-lock.json index 01435d8e..7c59b152 100644 --- a/microservices/coordinator-service/package-lock.json +++ b/microservices/coordinator-service/package-lock.json @@ -9,14 +9,17 @@ "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", + "dependency-cruiser": "^18.4.0", "pino-pretty": "^13.1.3", "prettier": "^3.9.4", "rimraf": "^6.1.3", @@ -97,21 +100,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 +123,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, @@ -798,14 +801,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", @@ -817,9 +820,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": { @@ -840,9 +843,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" ], @@ -857,9 +860,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" ], @@ -874,9 +877,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" ], @@ -891,9 +894,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" ], @@ -908,9 +911,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" ], @@ -925,13 +928,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": [ @@ -942,13 +948,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": [ @@ -959,13 +968,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": [ @@ -976,13 +988,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": [ @@ -993,13 +1008,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": [ @@ -1010,13 +1028,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": [ @@ -1027,9 +1048,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" ], @@ -1044,9 +1065,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" ], @@ -1054,18 +1075,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" ], @@ -1080,9 +1101,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" ], @@ -1160,9 +1181,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, @@ -1170,6 +1191,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", @@ -1664,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" }, @@ -1683,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", @@ -1706,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", @@ -1774,6 +1852,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", @@ -1787,16 +1888,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": { @@ -1851,6 +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", @@ -1858,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", @@ -1943,6 +2097,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", @@ -1951,11 +2129,58 @@ "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", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1977,12 +2202,25 @@ "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" } }, + "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", @@ -1996,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", @@ -2060,9 +2308,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, @@ -2249,6 +2497,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 +2606,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 +2652,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 +2673,22 @@ "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", + "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", @@ -2436,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", @@ -2446,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", @@ -2507,15 +2838,47 @@ "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/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.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" } }, + "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", @@ -2541,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", @@ -2639,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", @@ -2650,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", @@ -2808,6 +3224,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2829,6 +3248,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2850,6 +3272,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2871,6 +3296,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3053,6 +3481,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 +3528,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", @@ -3154,9 +3600,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.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -3172,6 +3618,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 +3631,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 +3680,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" @@ -3304,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", @@ -3336,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": { @@ -3411,9 +3881,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.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -3431,7 +3901,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3439,6 +3909,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", @@ -3497,11 +3994,24 @@ ], "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", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -3535,6 +4045,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", @@ -3560,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", @@ -3587,13 +4166,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": { @@ -3603,21 +4182,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": { @@ -3640,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", @@ -3667,10 +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==", - "dev": true, + "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" @@ -3711,6 +4299,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", @@ -3726,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", @@ -3801,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", @@ -3827,6 +4477,110 @@ "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", + "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", @@ -3912,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", @@ -3937,6 +4722,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", @@ -4050,16 +4847,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": { @@ -4076,7 +4873,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", @@ -4217,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", @@ -4313,7 +5123,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..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" @@ -36,9 +37,11 @@ }, "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", + "dependency-cruiser": "^18.4.0", "pino-pretty": "^13.1.3", "prettier": "^3.9.4", "rimraf": "^6.1.3", @@ -49,11 +52,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 } } 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' + } +} 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' + } +} 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/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/inbox-message.ts b/microservices/coordinator-service/src/application/messaging/inbox-message.ts new file mode 100644 index 00000000..bc6a8d30 --- /dev/null +++ b/microservices/coordinator-service/src/application/messaging/inbox-message.ts @@ -0,0 +1,16 @@ +export enum InboxStatus { + PENDING = 'PENDING', + PROCESSED = 'PROCESSED', + FAILED = 'FAILED', + UNSUPPORTED = 'UNSUPPORTED' +} + +export type InboxMessage = { + eventId: string + topic: string + payload: string + status: InboxStatus + attempts: number + receivedAt: Date + processedAt: Date | null +} 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..c9511955 --- /dev/null +++ b/microservices/coordinator-service/src/application/messaging/inbox-receiver.ts @@ -0,0 +1,28 @@ +import { Logger } from '../ports/logger' +import { InboxRepository } from '../ports/inbox.repository' +import { + InboxMessage, + InboxStatus +} from '@/application/messaging/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: InboxMessage = { + eventId: incomingMessage.id, + topic: incomingMessage.topic, + payload: incomingMessage.payload.toString(), + status: InboxStatus.PENDING, + attempts: 0, + 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/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 +} 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/message-processor.ts b/microservices/coordinator-service/src/application/messaging/message-processor.ts index 7459fe24..86135a6f 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 { InboxMessage } from '@/application/messaging/inbox-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 inboxMessage The inbox message to be processed. */ - process(topic: string, message: Buffer): 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 7479a9c2..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,12 +1,16 @@ 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 { 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' +import { InvalidMachineIdError } from '@/domain/machine/errors/invalid-machine-id.error' export class HeartbeatProcessor implements MessageProcessor { constructor( private readonly processHeartbeat: ProcessHeartbeat, - private readonly heartbeatMessageMapper: HeartbeatMessageMapper, + private readonly machineConfigs: Map, private readonly heartbeatTopicPrefix: string ) {} @@ -17,11 +21,22 @@ export class HeartbeatProcessor implements MessageProcessor { return topic.startsWith(prefix) } - process(topic: string, message: Buffer): void { - const input: HeartbeatInput = this.heartbeatMessageMapper.map( - topic, - message - ) + 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: 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 89b8a56d..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,14 +2,17 @@ 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 { 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' export class TelemetryProcessor implements MessageProcessor { constructor( private readonly processTelemetry: ProcessTelemetry, - private readonly machineConfig: MachineConfig, - private readonly telemetryMessageMapper: TelemetryMessageMapper, - private readonly telemetryTopicPrefix: string + private readonly machineConfigs: Map, + private readonly telemetryTopicPrefix: string, + private readonly coreRestService: CoreRestService ) {} canHandle(topic: string): boolean { @@ -19,11 +22,32 @@ export class TelemetryProcessor implements MessageProcessor { return topic.startsWith(prefix) } - process(topic: string, message: Buffer): void { - const input: TelemetryInput = this.telemetryMessageMapper.map( - topic, - message - ) - this.processTelemetry.execute(input, this.machineConfig) + async process(inboxMessage: InboxMessage): Promise { + const machineId = inboxMessage.topic.split('/').pop() + const payload = JSON.parse(inboxMessage.payload.toString()) + const input: TelemetryInput = { + machineId, + ...payload + } + const machineIdVO = new MachineId(input.machineId) + const machineConfig = this.machineConfigs.get(machineIdVO.value) + if (!machineConfig) { + throw new ConfigurationNotFoundError(machineIdVO.value) + } + 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' + ) + } } } 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..55a3ec4b --- /dev/null +++ b/microservices/coordinator-service/src/application/ports/core-rest-service.ts @@ -0,0 +1,35 @@ +import { MachineConfig } from '@/domain/machine/machine-config' +import { ProcessTelemetryResult } from '../telemetry/dto/process-telemetry-result' + +/** + * 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 + + /** + * Publishes telemetry data to the core REST API. + * @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 +} 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[] +} 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..92a1dca2 --- /dev/null +++ b/microservices/coordinator-service/src/application/ports/inbox.repository.ts @@ -0,0 +1,12 @@ +import { InboxMessage } from '../messaging/inbox-message' + +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/application/telemetry/dto/process-telemetry-result.ts b/microservices/coordinator-service/src/application/telemetry/dto/process-telemetry-result.ts index 7dcfe8ba..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 { TelemetryEvent } from '@/domain/machine/telemetry' import { Anomaly } from '../../../domain/anomaly/anomaly' -import { TelemetryEvent } from './telemetry-event' export type ProcessTelemetryResult = { - event: TelemetryEvent + machine: TelemetryEvent 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 c1b1aa31..00000000 --- a/microservices/coordinator-service/src/application/telemetry/dto/telemetry-event.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type TelemetryEvent = { - machineId: string - occurredAt: Date - processedAt: Date - operatingTemperature: number - powerConsumption: number - emissions: number - vibration: number - pressure: 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/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 - } - } -} diff --git a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts index 7d198840..85c95383 100644 --- a/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts +++ b/microservices/coordinator-service/src/application/telemetry/process-telemetry.ts @@ -1,14 +1,15 @@ -import { MachineConfig } 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 { TelemetryEvent } from './dto/telemetry-event' 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 ) {} @@ -32,35 +33,43 @@ export class ProcessTelemetry { } this.logger.info('Processing telemetry data', input) - const event: TelemetryEvent = { + 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: TelemetryEvent = TelemetryFactory.create( machineId, + this.generator.generateId(), + operatingTemperature, + powerConsumption, + emissions, + vibration, + pressure, 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') - } + this.clock.now(), + input.sequenceNumber + ) - 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, + 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/application/workers/heartbeat-monitor.ts b/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts new file mode 100644 index 00000000..28888910 --- /dev/null +++ b/microservices/coordinator-service/src/application/workers/heartbeat-monitor.ts @@ -0,0 +1,65 @@ +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' +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( + private readonly heartbeatRepository: HeartbeatRepository, + private readonly timeoutPolicy: HeartbeatTimeoutPolicy, + private readonly topicPrefix: string, + private readonly coreRestService: CoreRestService, + private readonly clock: Clock, + private readonly logger: Logger + ) {} + + async run(): Promise { + const heartbeats = this.heartbeatRepository.findLatestForEachMachine( + this.topicPrefix + ) + const now = this.clock.now() + + for (const heartbeat of heartbeats) { + const machineId = new MachineId(heartbeat.machineId) + this.logger.info('Evaluating heartbeat for machine', { + machineId: machineId.value, + lastHeartbeatAt: heartbeat.receivedAt, + now + }) + + 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 new file mode 100644 index 00000000..8d814779 --- /dev/null +++ b/microservices/coordinator-service/src/application/workers/inbox-worker.ts @@ -0,0 +1,81 @@ +import { MessageProcessor } from '../messaging/message-processor' +import { Logger } from '../ports/logger' +import { InboxRepository } from '../ports/inbox.repository' +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' + +export class InboxWorker { + constructor( + private readonly processors: MessageProcessor[], + private readonly inboxRepository: InboxRepository, + private readonly clock: Clock, + private readonly logger: Logger + ) {} + + async run(): Promise { + this.logger.info('Inbox worker started') + + const messages = this.inboxRepository.findPending() + + 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, + InboxStatus.UNSUPPORTED, + msg.attempts, + this.clock.now() + ) + + this.logger.warn('No processor found for message', { msg }) + continue + } + + try { + await processor.process(msg) + + this.inboxRepository.updateStatus( + msg.eventId, + InboxStatus.PROCESSED, + msg.attempts + 1, + this.clock.now() + ) + + this.logger.info('Message processed successfully', { msg }) + } catch (err) { + 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 }) + } + } + } + } +} diff --git a/microservices/coordinator-service/src/config/env.ts b/microservices/coordinator-service/src/config/env.ts index 85b674da..111c8c9e 100644 --- a/microservices/coordinator-service/src/config/env.ts +++ b/microservices/coordinator-service/src/config/env.ts @@ -2,24 +2,45 @@ 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 heartbeatTopic: string } + coreRest: { + url: string + serviceToken: string + } intervalMs: number heartbeatIntervalMs: number + heartbeatTimeoutMs: number + inboxDbPath: string } export function getConfig(): AppConfig { return { + nodeEnv: toNodeEnv(process.env.NODE_ENV), mqtt: { url: process.env.MQTT_URL!, 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) + heartbeatIntervalMs: Number(process.env.HEARTBEAT_INTERVAL_MS), + heartbeatTimeoutMs: Number(process.env.HEARTBEAT_TIMEOUT_MS), + inboxDbPath: process.env.INBOX_DB_PATH! } } 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..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,7 +1,7 @@ -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 { TelemetryEvent } from '@/domain/machine/telemetry' export class AnomalyDetector { constructor(private readonly policies: AnomalyPolicy[]) {} 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..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,6 +1,6 @@ -import { TelemetryEvent } from '@/application/telemetry/dto/telemetry-event' import { MachineConfig } from '@/domain/machine/machine-config' import { Anomaly } from '@/domain/anomaly/anomaly' +import { TelemetryEvent } from '@/domain' export interface AnomalyPolicy { 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 new file mode 100644 index 00000000..bfaff887 --- /dev/null +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/emission-policy.ts @@ -0,0 +1,28 @@ +import { MachineConfig } from '@/domain/machine/machine-config' +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 { TelemetryEvent } from '@/domain/machine/telemetry' +import { IdGenerator } from '@/domain/ports/id-generator' + +export class EmissionPolicy implements AnomalyPolicy { + constructor(private readonly generator: IdGenerator) {} + + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + const anomalies: Anomaly[] = [] + if (!config.emissions.contains(event.emissions)) { + anomalies.push( + new Anomaly( + new AnomalyEventId(this.generator.generateId()), + event.id, + SensorType.EMISSIONS, + event.emissions.value, + event.occurredAt, + event.processedAt + ) + ) + } + return anomalies + } +} 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..a364f4b1 --- /dev/null +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/power-consumption-policy.ts @@ -0,0 +1,28 @@ +import { MachineConfig } from '@/domain/machine/machine-config' +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 { TelemetryEvent } from '@/domain/machine/telemetry' +import { IdGenerator } from '@/domain/ports/id-generator' + +export class PowerConsumptionPolicy implements AnomalyPolicy { + constructor(private readonly generator: IdGenerator) {} + + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + const anomalies: Anomaly[] = [] + if (!config.powerConsumption.contains(event.powerConsumption)) { + anomalies.push( + new Anomaly( + new AnomalyEventId(this.generator.generateId()), + event.id, + SensorType.POWER_CONSUMPTION, + event.powerConsumption.value, + event.occurredAt, + event.processedAt + ) + ) + } + return anomalies + } +} 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..6ce5f95b --- /dev/null +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/pressure-policy.ts @@ -0,0 +1,28 @@ +import { MachineConfig } from '@/domain/machine/machine-config' +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 { TelemetryEvent } from '@/domain/machine/telemetry' +import { IdGenerator } from '@/domain/ports/id-generator' + +export class PressurePolicy implements AnomalyPolicy { + constructor(private readonly generator: IdGenerator) {} + + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + const anomalies: Anomaly[] = [] + if (!config.pressure.contains(event.pressure)) { + anomalies.push( + new Anomaly( + new AnomalyEventId(this.generator.generateId()), + event.id, + SensorType.PRESSURE, + event.pressure.value, + event.occurredAt, + event.processedAt + ) + ) + } + return anomalies + } +} 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..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 @@ -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 { TelemetryEvent } from '@/domain/machine/telemetry' +import { IdGenerator } from '@/domain/ports/id-generator' export class TemperaturePolicy implements AnomalyPolicy { + constructor(private readonly generator: IdGenerator) {} + evaluate(event: TelemetryEvent, 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 new file mode 100644 index 00000000..d9da2082 --- /dev/null +++ b/microservices/coordinator-service/src/domain/anomaly/services/policies/vibration-policy.ts @@ -0,0 +1,28 @@ +import { MachineConfig } from '@/domain/machine/machine-config' +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 { TelemetryEvent } from '@/domain/machine/telemetry' +import { IdGenerator } from '@/domain/ports/id-generator' + +export class VibrationPolicy implements AnomalyPolicy { + constructor(private readonly generator: IdGenerator) {} + + evaluate(event: TelemetryEvent, config: MachineConfig): Anomaly[] { + const anomalies: Anomaly[] = [] + if (!config.vibration.contains(event.vibration)) { + anomalies.push( + new Anomaly( + new AnomalyEventId(this.generator.generateId()), + event.id, + SensorType.VIBRATION, + event.vibration.value, + event.occurredAt, + event.processedAt + ) + ) + } + return anomalies + } +} 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' } 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/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' } } 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-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/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/policies/heatbeat-timeout-policy.ts b/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts new file mode 100644 index 00000000..48f25260 --- /dev/null +++ b/microservices/coordinator-service/src/domain/machine/policies/heatbeat-timeout-policy.ts @@ -0,0 +1,14 @@ +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): MachineReachabilityStatus { + const elapsed = now.getTime() - lastHeartbeatAt.getTime() + + return elapsed >= this.timeoutMs + ? 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 new file mode 100644 index 00000000..3e61a1b1 --- /dev/null +++ b/microservices/coordinator-service/src/domain/machine/policies/machine-status-policy.ts @@ -0,0 +1,14 @@ +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. + */ +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 reachability status of the machine, either ONLINE or OFFLINE, based on the evaluation of the policy. + */ + evaluate(lastHeartbeatAt: Date, now: Date): MachineReachabilityStatus +} diff --git a/microservices/coordinator-service/src/domain/machine/machine-factory.ts b/microservices/coordinator-service/src/domain/machine/telemetry-factory.ts similarity index 57% rename from microservices/coordinator-service/src/domain/machine/machine-factory.ts rename to microservices/coordinator-service/src/domain/machine/telemetry-factory.ts index 40223199..0eb593d5 100644 --- a/microservices/coordinator-service/src/domain/machine/machine-factory.ts +++ b/microservices/coordinator-service/src/domain/machine/telemetry-factory.ts @@ -1,27 +1,36 @@ 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 { - static createFromSensors( +export class TelemetryFactory { + static create( id: string, + eventId: string, temperature: number, powerConsumption: number, emissions: number, vibration: number, - pressure: number - ): Machine { - return new Machine( + pressure: number, + occurredAt: Date, + processedAt: Date, + sequenceNumber: number + ): TelemetryEvent { + return new TelemetryEvent( new MachineId(id), + new TelemetryEventId(eventId), new Temperature(temperature), 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/telemetry.ts similarity index 72% rename from microservices/coordinator-service/src/domain/machine/machine.ts rename to microservices/coordinator-service/src/domain/machine/telemetry.ts index 0990949a..c65f4ec5 100644 --- a/microservices/coordinator-service/src/domain/machine/machine.ts +++ b/microservices/coordinator-service/src/domain/machine/telemetry.ts @@ -1,31 +1,40 @@ -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, public vibration: Vibration, - public pressure: Pressure + public pressure: Pressure, + public occurredAt: Date, + public processedAt: Date, + public sequenceNumber: number ) {} /** @@ -33,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/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/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/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/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..dd14f2a3 --- /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: string + anomalyDetails: string[] + } + + 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 +} 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[] + } +} 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..e35fe944 --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/adapters/core-rest/http-rest-core-service.ts @@ -0,0 +1,103 @@ +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' +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( + 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) + } + + 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}`) + } + } + + 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.status === 404) { + throw new MachineNotFoundError(machineId) + } + + 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) + } +} 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) + ) + ) +} 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..fb064dad --- /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.machine.eventId.value, + machineId: telemetryData.machine.id.value, + sequenceNumber: telemetryData.machine.sequenceNumber, + capturedAt: telemetryData.machine.occurredAt.toISOString(), + readings: { + 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, + details: telemetryData.anomalies.map((anomaly) => anomaly.sensorType) + } + } +} 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() + } +} diff --git a/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts b/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts index 168df271..6a5cd112 100644 --- a/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts +++ b/microservices/coordinator-service/src/infrastructure/logger/pino-logger.ts @@ -1,22 +1,33 @@ import pino from 'pino' import { Logger } from '@/application/ports/logger' +import { NodeEnv } from '@/config/env' export class PinoLogger implements Logger { + private readonly logger: pino.Logger + constructor( - private logger = pino({ - transport: { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: 'SYS:standard', - ignore: 'pid,hostname' - } - } - }) - ) {} + private readonly nodeEnv: NodeEnv, + logger?: pino.Logger + ) { + this.logger = + logger ?? + (nodeEnv === 'development' + ? pino({ + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname' + } + }, + level: 'info' + }) + : pino()) + } child(context: object): Logger { - return new PinoLogger(this.logger.child(context)) + return new PinoLogger(this.nodeEnv, this.logger.child(context)) } info(message: string, metadata?: object): void { this.logger.info(metadata ?? {}, message) 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/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..929f0a2e --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/create-sqlite-database.ts @@ -0,0 +1,21 @@ +import fs from 'node:fs' +import Database from 'better-sqlite3' +import path from 'node:path' + +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, + attempts INTEGER NOT NULL, + received_at TEXT NOT NULL, + processed_at TEXT + )`) + return db +} 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) + } + }) + } +} 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..a2e3217c --- /dev/null +++ b/microservices/coordinator-service/src/infrastructure/persistence/sqlite/sqlite-inbox.repository.ts @@ -0,0 +1,90 @@ +import { + InboxMessage, + InboxStatus +} from '@/application/messaging/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, + attempts, + received_at, + processed_at + ) + 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 + 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.event_id, + topic: row.topic, + payload: row.payload, + status: row.status, + attempts: row.attempts, + receivedAt: new Date(row.received_at), + processedAt: row.processed_at ? new Date(row.processed_at) : null + })) + } +} diff --git a/microservices/coordinator-service/src/main.ts b/microservices/coordinator-service/src/main.ts index a4cfdd6a..42dbfa32 100644 --- a/microservices/coordinator-service/src/main.ts +++ b/microservices/coordinator-service/src/main.ts @@ -3,80 +3,128 @@ 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' import { TelemetryProcessor } from './application/messaging/processor/telemetry-processor' -import { CoordinatorMessageConsumer } from './application/messaging/coordinator-message-consumer' +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/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 { 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/emission-policy' +import { PowerConsumptionPolicy } from './domain/anomaly/services/policies/power-consumption-policy' +import { UUIDGenerator } from './infrastructure/adapters/uuid-generator' -function buildMachineConfig(): MachineConfig { - return new MachineConfig( - 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() { - const baseLogger = new PinoLogger() +async 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 config = getConfig() - 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 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 generator = new UUIDGenerator() + + const policies = [ + 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, - new SystemClock(), + generator, + clock, logger ) - const processHeartbeat = new ProcessHeartbeat(new SystemClock(), logger) - const machineConfig = buildMachineConfig() + const processHeartbeat = new ProcessHeartbeat(clock, logger) + 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) - const consumer = new CoordinatorMessageConsumer( + const inboxWorker = new InboxWorker( [ new TelemetryProcessor( processTelemetry, - machineConfig, - new TelemetryMessageMapper(), - config.mqtt.topic + configsByMachineId, + config.mqtt.topic, + coreRestService ), new HeartbeatProcessor( processHeartbeat, - new HeartbeatMessageMapper(), + configsByMachineId, config.mqtt.heartbeatTopic ) ], - logger + inboxRepository, + clock, + baseLogger.child({ service: 'inbox-worker' }) + ) + const heartbeatMonitor = new HeartbeatMonitor( + heartbeatRepository, + new HeartbeatTimeoutPolicy(config.heartbeatTimeoutMs), + config.mqtt.heartbeatTopic, + coreRestService, + clock, + baseLogger.child({ service: 'heartbeat-monitor' }) ) - 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) + const incomingMessage: IncomingMessage = { + id: randomUUID(), + topic, + payload: message, + receivedAt: clock.now() + } + inboxReceiver.receive(incomingMessage) }) + + setInterval(() => { + inboxWorker.run().catch((error) => { + logger.error('Telemetry processing failed', { error }) + }) + }, 1000) + + setInterval(() => { + heartbeatMonitor.run().catch((error) => { + logger.error('Heartbeat monitor failed', { error }) + }) + }, 3000) } bootstrap() 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..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,12 +22,14 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) 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(), @@ -35,8 +40,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) @@ -56,12 +61,14 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) 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(), @@ -72,12 +79,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) @@ -92,12 +99,14 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) 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(), @@ -116,12 +125,14 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) 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(), @@ -140,12 +151,14 @@ describe('ProcessTelemetry', () => { const useCase = new ProcessTelemetry( detector as any, + fakeGenerator, fakeClock, logger as any ) 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..ff1391b7 100644 --- a/microservices/coordinator-service/test/constants/machine-values.ts +++ b/microservices/coordinator-service/test/constants/machine-values.ts @@ -1,5 +1,12 @@ +export const MACHINE_IDS = { + DEFAULT: '6a8771c13ec0b58b0b1ac2af', + SECOND: '7a8771a63ec0b58b0b1ac2ae' +} as const + export const MACHINE_VALUES = { - ID: 'M1', + ID: MACHINE_IDS.DEFAULT, + + EVENT_ID: '563efbc3-6e91-4d98-b10d-67e68edddc5c', TEMPERATURE: { SAFE: 55, @@ -25,5 +32,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/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/anomaly/services/anomaly-detector.spec.ts b/microservices/coordinator-service/test/domain/anomaly/services/anomaly-detector.spec.ts index dfd7d1d9..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 @@ -1,57 +1,147 @@ 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 { TelemetryFactory } from '@/domain/machine/telemetry-factory' +import { TelemetryEvent } from '@/domain/machine/telemetry' describe('AnomalyDetector', () => { - it('should aggregate anomalies from policies', () => { - const detector = new AnomalyDetector([new TemperaturePolicy()]) - - const event: TelemetryEvent = { - machineId: MACHINE_VALUES.ID, - 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 generator = new UUIDGenerator() + + it('should return an empty array when no policies detect anomalies', () => { + const detector = new AnomalyDetector([new TemperaturePolicy(generator)]) + + 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, + 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(generator)]) + + 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, + 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(machine, config) expect(anomalies).toHaveLength(1) - expect(anomalies[0].sensorType).toBe('TEMPERATURE') - expect(anomalies[0].value).toBe(event.operatingTemperature) + expect(anomalies).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + sensorType: 'operatingTemperature', + value: machine.temperature.value + }) + ]) + ) }) - it('should return an empty array when no policies detect anomalies', () => { - const detector = new AnomalyDetector([new TemperaturePolicy()]) - const event: TelemetryEvent = { - machineId: MACHINE_VALUES.ID, - 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 - } + it('should detect anomalies from multiple policies', () => { + const detector = new AnomalyDetector([ + new TemperaturePolicy(generator), + new VibrationPolicy(generator) + ]) + + 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, + 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, + max: MACHINE_LIMITS.TEMPERATURE.MAX + }, + vibration: { + min: MACHINE_LIMITS.VIBRATION.MIN, + max: MACHINE_LIMITS.VIBRATION.MAX + } + }) + const anomalies = detector.detect(machine, config) + expect(anomalies).toHaveLength(2) + expect(anomalies).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + sensorType: 'operatingTemperature', + value: machine.temperature.value + }), + expect.objectContaining({ + sensorType: '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(generator)]) + + 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, + 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, max: MACHINE_LIMITS.TEMPERATURE.MAX + }, + vibration: { + min: MACHINE_LIMITS.VIBRATION.MIN, + 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 new file mode 100644 index 00000000..407912f2 --- /dev/null +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/emission-policy.spec.ts @@ -0,0 +1,64 @@ +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 { 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 { 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: TelemetryEvent = TelemetryFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_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(machine, config) + expect(anomalies).toHaveLength(1) + expect(anomalies[0].sensorType).toBe(SensorType.EMISSIONS) + expect(anomalies[0].value).toBe(machine.emissions.value) + }) + + it('should not detect any anomaly when emissions is within limits', () => { + const policy = new EmissionPolicy(generator) + 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, + 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(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 new file mode 100644 index 00000000..3cf75a2c --- /dev/null +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/power-consumption-policy.spec.ts @@ -0,0 +1,64 @@ +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 { 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 { 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: TelemetryEvent = TelemetryFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_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(machine, config) + expect(anomalies).toHaveLength(1) + expect(anomalies[0].sensorType).toBe(SensorType.POWER_CONSUMPTION) + expect(anomalies[0].value).toBe(machine.powerConsumption.value) + }) + + it('should not detect any anomaly when power consumption is within limits', () => { + const policy = new PowerConsumptionPolicy(generator) + 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, + 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(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 new file mode 100644 index 00000000..b439d523 --- /dev/null +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/pressure-policy.spec.ts @@ -0,0 +1,64 @@ +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 { 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 { 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: TelemetryEvent = 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.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(machine, config) + expect(anomalies).toHaveLength(1) + expect(anomalies[0].sensorType).toBe(SensorType.PRESSURE) + expect(anomalies[0].value).toBe(machine.pressure.value) + }) + + it('should not detect any anomaly when pressure is within limits', () => { + const policy = new PressurePolicy(generator) + 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, + 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(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 4da80a84..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 @@ -3,78 +3,88 @@ 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 { TelemetryFactory } from '@/domain/machine/telemetry-factory' +import { TelemetryEvent } from '@/domain/machine/telemetry' +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 = { - machineId: MACHINE_VALUES.ID, - 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: TelemetryEvent = TelemetryFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_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 = { - machineId: MACHINE_VALUES.ID, - 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: TelemetryEvent = TelemetryFactory.create( + MACHINE_VALUES.ID, + MACHINE_VALUES.EVENT_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 = { - machineId: MACHINE_VALUES.ID, - 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: TelemetryEvent = 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, + 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 new file mode 100644 index 00000000..2787752f --- /dev/null +++ b/microservices/coordinator-service/test/domain/anomaly/services/policies/vibration-policy.spec.ts @@ -0,0 +1,64 @@ +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 { 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 { 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: TelemetryEvent = 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.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(machine, config) + expect(anomalies).toHaveLength(1) + expect(anomalies[0].sensorType).toBe(SensorType.VIBRATION) + expect(anomalies[0].value).toBe(machine.vibration.value) + }) + + it('should not detect any anomaly when vibration is within limits', () => { + const policy = new VibrationPolicy(generator) + 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, + 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(machine, config) + expect(anomalies).toHaveLength(0) + }) +}) 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..cf84bd48 --- /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 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('operational') + }) + + 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('off') + }) + + 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('off') + }) +}) 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 4f57bcbd..00000000 --- a/microservices/coordinator-service/test/domain/machine/machine-factory.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { MachineFactory } from '@/domain/machine/machine-factory' - -describe('MachineFactory', () => { - it('should create a machine from sensors data', () => { - const telemetryInput = { - id: 'M1', - temperature: 50, - powerConsumption: 100, - emissions: 20, - vibration: 1.2, - pressure: 5 - } - - const machine = MachineFactory.createFromSensors( - telemetryInput.id, - telemetryInput.temperature, - telemetryInput.powerConsumption, - telemetryInput.emissions, - telemetryInput.vibration, - telemetryInput.pressure - ) - - expect(machine).toMatchObject({ - id: expect.objectContaining({ value: 'M1' }), - 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 dce9e114..89e1dc84 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 { TelemetryEvent } from '@/domain/machine/telemetry' 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,13 +28,13 @@ 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) }) 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/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 34e4b980..77de0bf8 100644 --- a/microservices/coordinator-service/test/utils/fake-config.ts +++ b/microservices/coordinator-service/test/utils/fake-config.ts @@ -8,9 +8,13 @@ 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' +import { MACHINE_IDS } from '@test/constants/machine-values' export function fakeConfig(overrides?: any): MachineConfig { return new MachineConfig( + new MachineId(overrides?.machineId ?? MACHINE_IDS.DEFAULT), + new Range( new Temperature( overrides?.temperature?.min ?? MACHINE_LIMITS.TEMPERATURE.MIN diff --git a/microservices/coordinator-service/test/utils/fake-machine.ts b/microservices/coordinator-service/test/utils/fake-machine.ts index feafdc32..08e497ff 100644 --- a/microservices/coordinator-service/test/utils/fake-machine.ts +++ b/microservices/coordinator-service/test/utils/fake-machine.ts @@ -1,20 +1,28 @@ -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 vibration?: number pressure?: number + occurredAt?: Date + processedAt?: Date + sequenceNumber?: number }) { - return MachineFactory.createFromSensors( + 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, 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 ) } 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 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 diff --git a/microservices/machine-simulator-service/Dockerfile b/microservices/machine-simulator-service/Dockerfile index c7c18c3b..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 install +RUN npm ci COPY . . diff --git a/microservices/machine-simulator-service/package-lock.json b/microservices/machine-simulator-service/package-lock.json index 28509191..300f8faa 100644 --- a/microservices/machine-simulator-service/package-lock.json +++ b/microservices/machine-simulator-service/package-lock.json @@ -9,16 +9,18 @@ "version": "0.0.0", "license": "MIT", "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" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@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", @@ -95,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, @@ -118,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, @@ -746,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", @@ -765,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": { @@ -781,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" ], @@ -798,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" ], @@ -815,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" ], @@ -832,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" ], @@ -849,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" ], @@ -866,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": [ @@ -883,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": [ @@ -900,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": [ @@ -917,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": [ @@ -934,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": [ @@ -951,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": [ @@ -968,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" ], @@ -985,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" ], @@ -995,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" ], @@ -1021,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" ], @@ -1052,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, @@ -1062,6 +1082,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 +1645,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", @@ -1696,6 +1749,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": { @@ -1759,6 +1813,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": "*" @@ -1781,6 +1836,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 +1871,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" @@ -1820,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" }, @@ -2063,10 +2141,19 @@ "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", - "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": { @@ -2077,6 +2164,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": { @@ -2104,6 +2192,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": { @@ -2150,6 +2239,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 +2283,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 +2303,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 +2391,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", @@ -2366,6 +2479,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" @@ -2576,6 +2690,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2597,6 +2714,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2618,6 +2738,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2639,6 +2762,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2754,6 +2880,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 +2917,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", @@ -2829,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": [ { @@ -2847,6 +2991,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 +3004,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", @@ -3028,6 +3190,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", @@ -3055,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": [ { @@ -3083,6 +3246,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 +3356,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", @@ -3198,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": { @@ -3214,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": { @@ -3264,6 +3478,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", @@ -3280,7 +3495,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 +3533,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", @@ -3379,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" }, @@ -3398,6 +3657,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" @@ -3419,6 +3679,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 +3861,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", @@ -3598,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": { @@ -3624,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", diff --git a/microservices/machine-simulator-service/package.json b/microservices/machine-simulator-service/package.json index ef8b8de6..a52b207e 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,19 +28,25 @@ }, "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", + "pino-pretty": "^13.1.3", "prettier": "^3.9.4", "typescript": "^6.0.3", "typescript-eslint": "^8.62.1", "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 +} diff --git a/microservices/machine-simulator-service/src/config/env.ts b/microservices/machine-simulator-service/src/config/env.ts index f7726a64..8bd40156 100644 --- a/microservices/machine-simulator-service/src/config/env.ts +++ b/microservices/machine-simulator-service/src/config/env.ts @@ -1,9 +1,41 @@ import dotenv from 'dotenv' -import type { TelemetryConfig } from '../types/telemetry-config' +import { + type AnomalyType, + type TelemetryConfig, + type TelemetryRanges +} from '../types/telemetry-config' dotenv.config() +export type NodeEnv = 'development' | 'production' + +function toNodeEnv(value?: string): NodeEnv { + if (value === 'production') return 'production' + return 'development' +} + +function parseAnomalies( + value: string | undefined, + telemetryRanges: TelemetryRanges +): AnomalyType[] { + if (!value) return [] + + const anomalies = value + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + + for (const anomaly of anomalies) { + if (!Object.hasOwn(telemetryRanges, anomaly)) { + throw new Error(`Invalid anomaly type: ${anomaly}`) + } + } + + return anomalies as AnomalyType[] +} + export type AppConfig = { + nodeEnv: NodeEnv mqtt: { url: string topic: string @@ -11,46 +43,55 @@ export type AppConfig = { } intervalMs: number heartbeatIntervalMs: number + outboxDbPath: string telemetry: TelemetryConfig } 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!) - } + ...telemetryRanges, + anomalies: parseAnomalies(process.env.ANOMALIES, telemetryRanges) } } } diff --git a/microservices/machine-simulator-service/src/generator/telemetry-generator.ts b/microservices/machine-simulator-service/src/generator/telemetry-generator.ts index 61e9d67c..140f58d3 100644 --- a/microservices/machine-simulator-service/src/generator/telemetry-generator.ts +++ b/microservices/machine-simulator-service/src/generator/telemetry-generator.ts @@ -1,25 +1,36 @@ 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 } from '../utils/random' +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 ): TelemetryEvent { + sequenceNumber++ + return { 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: 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/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) 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..bc3bfd8d --- /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 'node:path' + +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 +} 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..bbc55dea --- /dev/null +++ b/microservices/machine-simulator-service/src/infrastructure/persistence/sqlite/sqlite-outbox.repository.ts @@ -0,0 +1,40 @@ +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) + } + + 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) + } +} diff --git a/microservices/machine-simulator-service/src/main.ts b/microservices/machine-simulator-service/src/main.ts index 428b366c..f37aa053 100644 --- a/microservices/machine-simulator-service/src/main.ts +++ b/microservices/machine-simulator-service/src/main.ts @@ -1,23 +1,55 @@ +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 logger = new PinoLogger(config.nodeEnv) 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 telemetryInterval = setInterval(async () => { + const event: TelemetryEvent = generateTelemetry(config.telemetry, clock) const topic = `${config.mqtt.topic}/${config.telemetry.machineId}` - mqtt.publish(topic, JSON.stringify(event)) - logger.info('published', { event }) + const outboxMessage: OutboxMessage = { + eventId: randomUUID(), + topic, + payload: JSON.stringify(event), + status: OutboxStatus.PENDING, + createdAt: clock.now(), + attempts: 0 + } + + outbox.save(outboxMessage) + + 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) const heartbeatInterval = setInterval(() => { @@ -25,7 +57,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) @@ -34,6 +67,7 @@ async function shutdown(signal: string) { clearInterval(telemetryInterval) clearInterval(heartbeatInterval) await mqtt.close() + db.close() process.exit(0) } 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 +} 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..9d68db85 --- /dev/null +++ b/microservices/machine-simulator-service/src/messaging/outbox/outbox.repository.ts @@ -0,0 +1,6 @@ +import { OutboxMessage } from './outbox-message' + +export interface OutboxRepository { + save(message: OutboxMessage): void + updateStatus(eventId: string, status: string, attempts: number): void +} diff --git a/microservices/machine-simulator-service/src/mqtt/client.ts b/microservices/machine-simulator-service/src/mqtt/client.ts index dbb37879..564cd434 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,24 @@ export function createMqttClient(url: string, logger: PinoLogger) { }) return { - publish(topic: string, message: string) { - logger.info(`published message to topic ${topic}`, { message }) - client.publish(topic, message) + 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 { diff --git a/microservices/machine-simulator-service/src/types/telemetry-config.ts b/microservices/machine-simulator-service/src/types/telemetry-config.ts index 2339d1e5..b7a6c2f0 100644 --- a/microservices/machine-simulator-service/src/types/telemetry-config.ts +++ b/microservices/machine-simulator-service/src/types/telemetry-config.ts @@ -1,8 +1,14 @@ -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 } } + +export type AnomalyType = keyof TelemetryRanges + +export type TelemetryConfig = { + machineId: string + anomalies: AnomalyType[] +} & TelemetryRanges 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 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) +} 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..b3aefc86 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,33 @@ describe('generateTelemetry', () => { ) expect(telemetryEvent.pressure).toBeLessThanOrEqual(testConfig.pressure.max) }) + + it('should generate telemetry with anomalies if specified', () => { + const testConfigWithAnomalies = { + ...testConfig, + anomalies: ['operatingTemperature'] as AnomalyType[] + } + const telemetryEvent: TelemetryEvent = generateTelemetry( + testConfigWithAnomalies, + fakeClock + ) + expect(telemetryEvent.operatingTemperature).toBeGreaterThan( + 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) + }) })