diff --git a/app/.dockerignore b/app/.dockerignore index f06235c..69e11c9 100644 --- a/app/.dockerignore +++ b/app/.dockerignore @@ -1,2 +1,4 @@ node_modules dist +.env +.env.* diff --git a/app/Dockerfile b/app/Dockerfile index 723aacb..a789219 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,13 +1,15 @@ FROM node:22-alpine WORKDIR /app +ENV NODE_ENV=production COPY package.json package-lock.json ./ RUN npm ci COPY tsconfig.json ./ COPY public ./public -# Runtime mappers read JSON files from src/mappings, so they must be copied into the image. COPY src ./src -CMD ["npm", "run", "start"] +RUN npm run build && npm prune --omit=dev + +CMD ["node", "dist/index.js"] diff --git a/app/package-lock.json b/app/package-lock.json index 1d81405..b1b654b 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -6,11 +6,13 @@ "": { "name": "mqtt-worker", "dependencies": { - "@types/node": "^25.9.1", "mqtt": "^5.15.1", - "tsx": "^4.22.4", - "typescript": "^6.0.3", "ws": "^8.21.0" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "tsx": "^4.22.4", + "typescript": "^6.0.3" } }, "node_modules/@babel/runtime": { @@ -29,6 +31,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -45,6 +48,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -61,6 +65,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -77,6 +82,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -93,6 +99,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -109,6 +116,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -125,6 +133,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -141,6 +150,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -157,6 +167,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -173,6 +184,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -189,6 +201,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -205,6 +218,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -221,6 +235,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -237,6 +252,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -253,6 +269,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -269,6 +286,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -285,6 +303,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -301,6 +320,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -317,6 +337,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -333,6 +354,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -349,6 +371,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -365,6 +388,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -381,6 +405,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -397,6 +422,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -413,6 +439,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -429,6 +456,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -607,6 +635,7 @@ "version": "0.28.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -679,6 +708,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -923,6 +953,7 @@ "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.28.0" @@ -947,6 +978,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/app/package.json b/app/package.json index 3399ecd..6e595dd 100644 --- a/app/package.json +++ b/app/package.json @@ -5,13 +5,15 @@ "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", - "start": "tsx src/index.ts" + "start": "node dist/index.js" }, "dependencies": { - "@types/node": "^25.9.1", "mqtt": "^5.15.1", - "tsx": "^4.22.4", - "typescript": "^6.0.3", "ws": "^8.21.0" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "tsx": "^4.22.4", + "typescript": "^6.0.3" } } diff --git a/app/src/handlers/edge5-modbus-v2.ts b/app/src/handlers/edge5-modbus-v2.ts index ee6d67a..e5bfaed 100644 --- a/app/src/handlers/edge5-modbus-v2.ts +++ b/app/src/handlers/edge5-modbus-v2.ts @@ -7,20 +7,9 @@ export const TOPIC = 'data/edge5/modbus/v2' const mapper = createMetricMapper({ filePath: process.env.EDGE5_MODBUS_V2_MAPPING_FILE as string, }) -const REGISTER_COUNT = mapper.tagCount function parseValues(payload: Buffer): number[] { - const value = JSON.parse(payload.toString('utf8')) as unknown - - if ( - Array.isArray(value) && - value.length === REGISTER_COUNT && - value.every((item) => typeof item === 'number' && Number.isFinite(item)) - ) { - return value - } - - throw new Error(`Expected JSON number[${REGISTER_COUNT}]`) + return JSON.parse(payload.toString('utf8')) as number[] } async function postMetrics(metrics: MappedMetric[]): Promise { diff --git a/app/src/handlers/edge5-modbus.ts b/app/src/handlers/edge5-modbus.ts index 5416393..7b6b25f 100644 --- a/app/src/handlers/edge5-modbus.ts +++ b/app/src/handlers/edge5-modbus.ts @@ -4,7 +4,6 @@ import type { TopicHandler } from '../types.js' export const TOPIC = 'data/edge5/modbus/v1' const EDGE = 'edge5' -const REGISTER_COUNT = 125 type Edge5ModbusMetric = { edge: string @@ -14,23 +13,7 @@ type Edge5ModbusMetric = { } function parseValues(payload: Buffer): number[] { - let value: unknown - - try { - value = JSON.parse(payload.toString('utf8')) as unknown - } catch { - throw new Error(`Expected JSON number[${REGISTER_COUNT}]`) - } - - if ( - Array.isArray(value) && - value.length === REGISTER_COUNT && - value.every((item) => typeof item === 'number' && Number.isFinite(item)) - ) { - return value - } - - throw new Error(`Expected JSON number[${REGISTER_COUNT}]`) + return JSON.parse(payload.toString('utf8')) as number[] } function toMetrics(values: number[], timestamp: number): Edge5ModbusMetric[] { @@ -47,12 +30,7 @@ async function postMetrics(metrics: Edge5ModbusMetric[]): Promise { if (!response.ok) { const text = await response.text() - console.error('edge5.modbus.post_error', { - status: response.status, - statusText: response.statusText, - body: text.slice(0, 500), - }) - return + throw new Error(text) } console.log('edge5.modbus.posted', { count: metrics.length }) @@ -61,15 +39,7 @@ async function postMetrics(metrics: Edge5ModbusMetric[]): Promise { export const handleEdge5Modbus: TopicHandler = async (topic, payload) => { console.log('edge5.modbus.received', { topic, bytes: payload.length }) - let values: number[] - - try { - values = parseValues(payload) - } catch (err) { - console.warn('edge5.modbus.invalid_payload', { topic, err }) - return - } - + const values = parseValues(payload) const metrics = toMetrics(values, Date.now()) await postMetrics(metrics) } diff --git a/app/src/helpers/parse.ts b/app/src/helpers/parse.ts index b132076..27b05ee 100644 --- a/app/src/helpers/parse.ts +++ b/app/src/helpers/parse.ts @@ -7,14 +7,6 @@ export function parseJson(payload: Buffer): T { return JSON.parse(text) as T } -export function parseJsonArray(payload: Buffer): unknown[] { - const value = parseJson(payload) - if (!Array.isArray(value)) { - throw new Error('Expected JSON array') - } - return value -} - export function decodeBase64(payload: Buffer): Buffer { return Buffer.from(payload.toString('utf8'), 'base64') } diff --git a/app/src/helpers/validate.ts b/app/src/helpers/validate.ts deleted file mode 100644 index f37fffb..0000000 --- a/app/src/helpers/validate.ts +++ /dev/null @@ -1,39 +0,0 @@ -const EXPECTED_CHUNK_LENGTH = 125 - -export function isNonEmptyString(value: unknown): value is string { - return typeof value === 'string' && value.length > 0 -} - -export function isFiniteNumber(value: unknown): value is number { - return typeof value === 'number' && Number.isFinite(value) -} - -export function isArrayOfLength( - value: unknown, - length: number, -): value is unknown[] { - return Array.isArray(value) && value.length === length -} - -export function isArrayOfLength125(value: unknown): value is unknown[] { - return isArrayOfLength(value, EXPECTED_CHUNK_LENGTH) -} - -export function isArrayOfChunks125( - value: unknown, -): value is unknown[][] { - if (!Array.isArray(value)) { - return false - } - return value.every((chunk) => isArrayOfLength125(chunk)) -} - -export function assertValid( - value: unknown, - guard: (v: unknown) => v is T, - label: string, -): asserts value is T { - if (!guard(value)) { - throw new Error(`Invalid payload: ${label}`) - } -} diff --git a/app/src/mapping.ts b/app/src/mapping.ts index 929f98c..cbbb8ca 100644 --- a/app/src/mapping.ts +++ b/app/src/mapping.ts @@ -14,7 +14,6 @@ type MappingConfig = { export type MetricMapper = { edge: string - tagCount: number mapValues: (values: number[], timestamp: number) => MappedMetric[] } @@ -34,7 +33,6 @@ export function createMetricMapper(options: MetricMapperOptions): MetricMapper { return { edge: config.edge, - tagCount: config.tags.length, mapValues: (values, timestamp) => values.map((value, index) => ({ edge: config.edge, diff --git a/app/tsconfig.json b/app/tsconfig.json index a3f07a9..02b90f0 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -7,8 +7,7 @@ "esModuleInterop": true, "skipLibCheck": true, "outDir": "dist", - "rootDir": "src", - "noEmit": true + "rootDir": "src" }, "include": ["src/**/*.ts"] }