remove default env values. correct package json with env vars . #6

Merged
APervov merged 3 commits from edge5modbus into main 2026-06-19 08:18:08 +00:00
14 changed files with 70 additions and 111 deletions

View File

@@ -1,2 +1,4 @@
node_modules node_modules
dist dist
.env
.env.*

View File

@@ -1,13 +1,14 @@
FROM node:22-alpine FROM node:22-alpine
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci RUN npm ci
COPY tsconfig.json ./ 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 COPY src ./src
NBizyaev marked this conversation as resolved
Review

в проекте нет папки public

(обычно, такую или подобную папку (assets) создают в проекте тогда,
когда нужно использовать какие-то файлы, которые лежат прямо в проекте,
например, файлы маппингов можно было бы вынести в общую папку mappings
и копировать её в контейнер, чтобы быть уверенным, что когда приложению понадобится читать файл маппинга,
то он будет найдет в общей папке mappings. Рекомендую. )

в проекте нет папки public (обычно, такую или подобную папку (assets) создают в проекте тогда, когда нужно использовать какие-то файлы, которые лежат прямо в проекте, например, файлы маппингов можно было бы вынести в общую папку mappings и копировать её в контейнер, чтобы быть уверенным, что когда приложению понадобится читать файл маппинга, то он будет найдет в общей папке mappings. Рекомендую. )
CMD ["npm", "run", "dev"] RUN npm run build && npm prune --omit=dev
NBizyaev marked this conversation as resolved Outdated

CMD ["npm", "run", "start"] - это антипаттерн для prod.
npm запускает node как дочерний процесс (например, SIGTERM от Docker при docker stop до node не доходит — контейнер висит 10 секунд до SIGKILL)

prod надо запускать так:

CMD ["node", "dist/index.js"] 

Скрипт

npm start  

используется во время разработки, чтоб проверить билд после npm build

CMD ["npm", "run", "start"] - это антипаттерн для prod. npm запускает node как дочерний процесс (например, SIGTERM от Docker при docker stop до node не доходит — контейнер висит 10 секунд до SIGKILL) prod надо запускать так: ``` CMD ["node", "dist/index.js"] ``` Скрипт ``` npm start ``` используется во время разработки, чтоб проверить билд после `npm build`
CMD ["node", "dist/index.js"]

38
app/package-lock.json generated
View File

@@ -6,11 +6,13 @@
"": { "": {
"name": "mqtt-worker", "name": "mqtt-worker",
"dependencies": { "dependencies": {
"@types/node": "^25.9.1",
"mqtt": "^5.15.1", "mqtt": "^5.15.1",
"tsx": "^4.22.4",
"typescript": "^6.0.3",
"ws": "^8.21.0" "ws": "^8.21.0"
},
"devDependencies": {
"@types/node": "^25.9.1",
"tsx": "^4.22.4",
"typescript": "^6.0.3"
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
@@ -29,6 +31,7 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -45,6 +48,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -61,6 +65,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -77,6 +82,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -93,6 +99,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -109,6 +116,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -125,6 +133,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -141,6 +150,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -157,6 +167,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -173,6 +184,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -189,6 +201,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -205,6 +218,7 @@
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -221,6 +235,7 @@
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -237,6 +252,7 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -253,6 +269,7 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -269,6 +286,7 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -285,6 +303,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -301,6 +320,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -317,6 +337,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -333,6 +354,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -349,6 +371,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -365,6 +388,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -381,6 +405,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -397,6 +422,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -413,6 +439,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -429,6 +456,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -607,6 +635,7 @@
"version": "0.28.0", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -679,6 +708,7 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -923,6 +953,7 @@
"version": "4.22.4", "version": "4.22.4",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "~0.28.0" "esbuild": "~0.28.0"
@@ -947,6 +978,7 @@
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",

View File

@@ -8,10 +8,12 @@
"start": "node --env-file=.env dist/index.js" "start": "node --env-file=.env dist/index.js"
NBizyaev marked this conversation as resolved Outdated

мы зациклились
"node --env-file=.env dist/index.js" - это корректная строчка
потому что это запуск билда разработчиком
на его, разработчика, компьютере
и переменные окружения он, разработчик, положил в файл .env или .env.* и разумеется хочет чтобы при запуске билда у него локально, эти переменные окружения из этого файла использовались в его, разработчика, коде.

Для запуска билда в продакшне мы будем использовать
CMD ["node", "dist/index.js"]
без указания конкретного .env
так как там в продакшн окружении будут указаны свои собственные значения переменных продакшн окружения.

мы зациклились "node --env-file=.env dist/index.js" - это корректная строчка потому что это запуск билда разработчиком на его, разработчика, компьютере и переменные окружения он, разработчик, положил в файл .env или .env.* и разумеется хочет чтобы при запуске билда у него локально, эти переменные окружения из этого файла использовались в его, разработчика, коде. Для запуска билда в продакшне мы будем использовать CMD ["node", "dist/index.js"] без указания конкретного .env так как там в продакшн окружении будут указаны свои собственные значения переменных продакшн окружения.
}, },
"dependencies": { "dependencies": {
"@types/node": "^25.9.1",
"mqtt": "^5.15.1", "mqtt": "^5.15.1",
"tsx": "^4.22.4",
"typescript": "^6.0.3",
"ws": "^8.21.0" "ws": "^8.21.0"
},
"devDependencies": {
"@types/node": "^25.9.1",
"tsx": "^4.22.4",
"typescript": "^6.0.3"
} }
} }

View File

@@ -1,7 +1,4 @@
const DEFAULT_CLOUD_INGEST_URL = 'http://localhost:3101/ingest' export const cloudIngestUrl = process.env.CLOUD_INGEST_URL as string
export const cloudIngestUrl =
process.env.CLOUD_INGEST_URL ?? DEFAULT_CLOUD_INGEST_URL
export function postCloudIngest(body: unknown): Promise<Response> { export function postCloudIngest(body: unknown): Promise<Response> {
return fetch(cloudIngestUrl, { return fetch(cloudIngestUrl, {

View File

@@ -4,25 +4,21 @@ import type { TopicHandler } from '../types.js'
export const TOPIC = 'data/edge5/modbus/v2' export const TOPIC = 'data/edge5/modbus/v2'
const REGISTER_COUNT = 125
const mapper = createMetricMapper({ const mapper = createMetricMapper({
filePath: filePath: process.env.EDGE5_MODBUS_V2_MAPPING_FILE as string,
process.env.EDGE5_MODBUS_V2_MAPPING_FILE ??
new URL('../mappings/edge5-modbus.json', import.meta.url),
}) })
const REGISTER_COUNT = mapper.tagCount
function parseValues(payload: Buffer): number[] { function parseValues(payload: Buffer): number[] {
const value = JSON.parse(payload.toString('utf8')) as unknown const values = JSON.parse(payload.toString('utf8')) as number[]
assertRegisterCount(values)
return values
}
if ( function assertRegisterCount(values: number[]): void {
Array.isArray(value) && if (values.length !== REGISTER_COUNT) {
value.length === REGISTER_COUNT && throw new Error(`Expected ${REGISTER_COUNT} modbus values, got ${values.length}`)
value.every((item) => typeof item === 'number' && Number.isFinite(item))
) {
return value
} }
throw new Error(`Expected JSON number[${REGISTER_COUNT}]`)
} }
async function postMetrics(metrics: MappedMetric[]): Promise<void> { async function postMetrics(metrics: MappedMetric[]): Promise<void> {

View File

@@ -14,23 +14,15 @@ type Edge5ModbusMetric = {
} }
function parseValues(payload: Buffer): number[] { function parseValues(payload: Buffer): number[] {
let value: unknown const values = JSON.parse(payload.toString('utf8')) as number[]
assertRegisterCount(values)
return values
}
try { function assertRegisterCount(values: number[]): void {
value = JSON.parse(payload.toString('utf8')) as unknown if (values.length !== REGISTER_COUNT) {
} catch { throw new Error(`Expected ${REGISTER_COUNT} modbus values, got ${values.length}`)
throw new Error(`Expected JSON number[${REGISTER_COUNT}]`)
} }
if (
Array.isArray(value) &&
value.length === REGISTER_COUNT &&
NBizyaev marked this conversation as resolved Outdated

Мы зациклились.
Я просил убрать бессмысленные перехваты try .. catch
но не саму валидацию вообще как понятие
просто в случае возникновения исключения, оно и так возникнет и мы узнаем, что возникло исключения
НО
валидацию на то, что в массиве действительно 125 элементов убирать не надо.
вдруг там придёт не 125 элементов - это проблема - и вот тут мы не узнаем, что возникло исключение.

Мы зациклились. Я просил убрать бессмысленные перехваты try .. catch но не саму валидацию вообще как понятие просто в случае возникновения исключения, оно и так возникнет и мы узнаем, что возникло исключения НО валидацию на то, что в массиве действительно 125 элементов убирать не надо. вдруг там придёт не 125 элементов - это проблема - и вот тут мы не узнаем, что возникло исключение.
value.every((item) => typeof item === 'number' && Number.isFinite(item))
) {
return value
}
throw new Error(`Expected JSON number[${REGISTER_COUNT}]`)
} }
function toMetrics(values: number[], timestamp: number): Edge5ModbusMetric[] { function toMetrics(values: number[], timestamp: number): Edge5ModbusMetric[] {
@@ -47,12 +39,7 @@ async function postMetrics(metrics: Edge5ModbusMetric[]): Promise<void> {
if (!response.ok) { if (!response.ok) {
const text = await response.text() const text = await response.text()
console.error('edge5.modbus.post_error', { throw new Error(text)
status: response.status,
statusText: response.statusText,
body: text.slice(0, 500),
})
return
} }
console.log('edge5.modbus.posted', { count: metrics.length }) console.log('edge5.modbus.posted', { count: metrics.length })
@@ -61,15 +48,7 @@ async function postMetrics(metrics: Edge5ModbusMetric[]): Promise<void> {
export const handleEdge5Modbus: TopicHandler = async (topic, payload) => { export const handleEdge5Modbus: TopicHandler = async (topic, payload) => {
console.log('edge5.modbus.received', { topic, bytes: payload.length }) console.log('edge5.modbus.received', { topic, bytes: payload.length })
let values: number[] const values = parseValues(payload)
try {
values = parseValues(payload)
} catch (err) {
console.warn('edge5.modbus.invalid_payload', { topic, err })
return
}
const metrics = toMetrics(values, Date.now()) const metrics = toMetrics(values, Date.now())
await postMetrics(metrics) await postMetrics(metrics)
} }

View File

@@ -7,14 +7,6 @@ export function parseJson<T = unknown>(payload: Buffer): T {
return JSON.parse(text) as 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 { export function decodeBase64(payload: Buffer): Buffer {
return Buffer.from(payload.toString('utf8'), 'base64') return Buffer.from(payload.toString('utf8'), 'base64')
} }

View File

@@ -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<T>(
value: unknown,
guard: (v: unknown) => v is T,
label: string,
): asserts value is T {
if (!guard(value)) {
throw new Error(`Invalid payload: ${label}`)
}
}

View File

@@ -3,7 +3,7 @@ import { createServer } from 'node:http'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
const httpPort = Number(process.env.HTTP_PORT ?? 80) const httpPort = Number(process.env.HTTP_PORT)
const publicDir = join(dirname(fileURLToPath(import.meta.url)), '../../public') const publicDir = join(dirname(fileURLToPath(import.meta.url)), '../../public')
const indexPath = join(publicDir, 'index.html') const indexPath = join(publicDir, 'index.html')

View File

@@ -14,7 +14,6 @@ type MappingConfig = {
export type MetricMapper = { export type MetricMapper = {
edge: string edge: string
tagCount: number
mapValues: (values: number[], timestamp: number) => MappedMetric[] mapValues: (values: number[], timestamp: number) => MappedMetric[]
} }
@@ -34,7 +33,6 @@ export function createMetricMapper(options: MetricMapperOptions): MetricMapper {
return { return {
edge: config.edge, edge: config.edge,
tagCount: config.tags.length,
mapValues: (values, timestamp) => mapValues: (values, timestamp) =>
values.map((value, index) => ({ values.map((value, index) => ({
edge: config.edge, edge: config.edge,

View File

@@ -3,7 +3,7 @@ import mqtt from 'mqtt'
import { getRegisteredTopics } from './registry.js' import { getRegisteredTopics } from './registry.js'
import { routeMessage } from './router.js' import { routeMessage } from './router.js'
const mqttUrl = process.env.MQTT_URL ?? 'mqtt://localhost:1883' const mqttUrl = process.env.MQTT_URL as string
export const mqttClient = mqtt.connect(mqttUrl) export const mqttClient = mqtt.connect(mqttUrl)

View File

@@ -1,7 +1,7 @@
import type { IncomingMessage } from 'http' import type { IncomingMessage } from 'http'
import { WebSocket, WebSocketServer } from 'ws' import { WebSocket, WebSocketServer } from 'ws'
const wsPort = Number(process.env.WS_PORT ?? 9090) const wsPort = Number(process.env.WS_PORT)
const wss = new WebSocketServer({ port: wsPort, perMessageDeflate: false }) const wss = new WebSocketServer({ port: wsPort, perMessageDeflate: false })
const clients = new Set<WebSocket>() const clients = new Set<WebSocket>()

View File

@@ -7,8 +7,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src"
"noEmit": true
}, },
"include": ["src/**/*.ts"] "include": ["src/**/*.ts"]
} }