add mapper. add v2 handler. add tag description short and full versions. delete topic[1-5]. #5
@@ -12,24 +12,11 @@ type DemoPlcPayload = {
|
||||
value: number
|
||||
}
|
||||
|
||||
type DemoPlcMetric = DemoPlcPayload & {
|
||||
type DemoPlcMetric = {
|
||||
edge: string
|
||||
|
NBizyaev marked this conversation as resolved
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
function isDemoPlcPayload(value: unknown): value is DemoPlcPayload {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const candidate = value as Partial<DemoPlcPayload>
|
||||
|
||||
return (
|
||||
typeof candidate.tag === 'string' &&
|
||||
candidate.tag.length > 0 &&
|
||||
typeof candidate.value === 'number' &&
|
||||
Number.isFinite(candidate.value)
|
||||
)
|
||||
tag: string
|
||||
value: number
|
||||
}
|
||||
|
||||
|
NBizyaev marked this conversation as resolved
NBizyaev
commented
Избыточная валидация Избыточная валидация
|
||||
function toMetric(payload: DemoPlcPayload, timestamp: number): DemoPlcMetric {
|
||||
@@ -52,12 +39,7 @@ async function postMetric(metric: DemoPlcMetric): Promise<void> {
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
console.error('demo.plc.post_error', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: text.slice(0, 500),
|
||||
})
|
||||
return
|
||||
throw new Error(text)
|
||||
}
|
||||
|
||||
console.log('demo.plc.posted', { tag: metric.tag })
|
||||
@@ -66,20 +48,7 @@ async function postMetric(metric: DemoPlcMetric): Promise<void> {
|
||||
export const handleDemoPlc: TopicHandler = async (topic, payload) => {
|
||||
console.log('demo.plc.received', { topic, bytes: payload.length })
|
||||
|
||||
let value: unknown
|
||||
|
||||
try {
|
||||
value = parseJson(payload)
|
||||
} catch (err) {
|
||||
console.warn('demo.plc.invalid_json', { topic, err })
|
||||
return
|
||||
}
|
||||
|
||||
if (!isDemoPlcPayload(value)) {
|
||||
console.warn('demo.plc.invalid_payload', { topic })
|
||||
return
|
||||
}
|
||||
|
||||
const value = parseJson<DemoPlcPayload>(payload)
|
||||
const metric = toMetric(value, Date.now())
|
||||
await postMetric(metric)
|
||||
}
|
||||
|
NBizyaev marked this conversation as resolved
Outdated
NBizyaev
commented
Просто бросить исключение new Error(text) Просто бросить исключение new Error(text)
|
||||
|
||||
@@ -43,12 +43,7 @@ async function postMetrics(metrics: MappedMetric[]): Promise<void> {
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
|
NBizyaev marked this conversation as resolved
NBizyaev
commented
Просто бросить исключение new Error(text) Просто бросить исключение new Error(text)
|
||||
console.error('edge5.modbus.v2.post_error', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: text.slice(0, 500),
|
||||
})
|
||||
return
|
||||
throw new Error(text)
|
||||
}
|
||||
|
||||
console.log('edge5.modbus.v2.posted', { count: metrics.length })
|
||||
@@ -57,15 +52,7 @@ async function postMetrics(metrics: MappedMetric[]): Promise<void> {
|
||||
export const handleEdge5ModbusV2: TopicHandler = async (topic, payload) => {
|
||||
console.log('edge5.modbus.v2.received', { topic, bytes: payload.length })
|
||||
|
||||
let values: number[]
|
||||
|
||||
try {
|
||||
values = parseValues(payload)
|
||||
} catch (err) {
|
||||
console.warn('edge5.modbus.v2.invalid_payload', { topic, err })
|
||||
return
|
||||
}
|
||||
|
||||
const values = parseValues(payload)
|
||||
const metrics = mapper.mapValues(values, Date.now())
|
||||
await postMetrics(metrics)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { isAbsolute, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export type MappedMetric = {
|
||||
edge: string
|
||||
@@ -23,88 +21,30 @@ export type MetricMapper = {
|
||||
}
|
||||
|
||||
type MetricMapperOptions = {
|
||||
// filePath is set as a URL from import.meta.url for bundled mappings,
|
||||
// or as a path string resolved by Node from the process working directory.
|
||||
filePath: string | URL
|
||||
}
|
||||
|
||||
function resolveMappingPath(filePath: string | URL): string {
|
||||
if (filePath instanceof URL) {
|
||||
return fileURLToPath(filePath)
|
||||
}
|
||||
|
||||
return isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath)
|
||||
}
|
||||
|
||||
function readMappingConfig(filePath: string | URL): MappingConfig {
|
||||
|
NBizyaev marked this conversation as resolved
Outdated
NBizyaev
commented
Просто в комментарии в коде напиши, в каком формате и где ты задаёшь и ожидаешь filePath, Просто в комментарии в коде напиши, в каком формате и где ты задаёшь и ожидаешь filePath,
соблюдай утвержденный самим собой контракт,
и удали функцию проверки filePath.
|
||||
const resolvedPath = resolveMappingPath(filePath)
|
||||
let raw: unknown
|
||||
|
||||
try {
|
||||
raw = JSON.parse(readFileSync(resolvedPath, 'utf8')) as unknown
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to read mapping file ${resolvedPath}: ${String(err)}`)
|
||||
}
|
||||
|
||||
if (!isMappingConfig(raw)) {
|
||||
throw new Error(`Invalid mapping file ${resolvedPath}`)
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
|
||||
function isMappingConfig(value: unknown): value is MappingConfig {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const candidate = value as Partial<MappingConfig>
|
||||
|
||||
return (
|
||||
typeof candidate.edge === 'string' &&
|
||||
candidate.edge.length > 0 &&
|
||||
Array.isArray(candidate.tags) &&
|
||||
candidate.tags.length > 0 &&
|
||||
candidate.tags.every(isMappingTagEntry)
|
||||
)
|
||||
}
|
||||
|
||||
function isMappingTagEntry(value: unknown): value is MappingTagEntry {
|
||||
return (
|
||||
(typeof value === 'string' && value.length > 0) ||
|
||||
(Boolean(value) &&
|
||||
typeof value === 'object' &&
|
||||
((typeof (value as { tag?: unknown }).tag === 'string' &&
|
||||
(value as { tag: string }).tag.length > 0) ||
|
||||
(typeof (value as { key?: unknown }).key === 'string' &&
|
||||
(value as { key: string }).key.length > 0)))
|
||||
)
|
||||
}
|
||||
|
||||
function getTag(entry: MappingTagEntry): string {
|
||||
if (typeof entry === 'string') {
|
||||
return entry
|
||||
}
|
||||
|
||||
return 'tag' in entry ? entry.tag : entry.key
|
||||
return JSON.parse(readFileSync(filePath, 'utf8')) as MappingConfig
|
||||
}
|
||||
|
||||
export function createMetricMapper(options: MetricMapperOptions): MetricMapper {
|
||||
const config = readMappingConfig(options.filePath)
|
||||
const tags = config.tags.map(getTag)
|
||||
const tags = config.tags.map((entry) =>
|
||||
typeof entry === 'string' ? entry : 'tag' in entry ? entry.tag : entry.key,
|
||||
)
|
||||
|
||||
return {
|
||||
edge: config.edge,
|
||||
tagCount: tags.length,
|
||||
|
NBizyaev marked this conversation as resolved
NBizyaev
commented
Обработка ошибок хандлера должна быть не в самом хандлере, а выше - на уровне роутинга входящих пакетов из топиков на хандлеры (app\src\router.ts), а все проблемы/ошибки/несоответствия ожиданиям/ваыпадения из позитивного сценария -- должны бросать исключение Обработка ошибок хандлера должна быть не в самом хандлере, а выше - на уровне роутинга входящих пакетов из топиков на хандлеры (app\src\router.ts), а все проблемы/ошибки/несоответствия ожиданиям/ваыпадения из позитивного сценария -- должны бросать исключение
в том числе некорректные данные,
в том числе отсутствие каких-либо файлов,
в том числе вообще все ошибки, которые не будут покрыты отдельным специальным if.
В этом смысле чем меньше отдельных специальных if - тем лучше, потому что проблемы всё равно обработаются и выловятся уровнем выше, но кода при этом будет меньше.
|
||||
mapValues: (values, timestamp) => {
|
||||
if (values.length !== tags.length) {
|
||||
throw new Error(`Expected ${tags.length} values, got ${values.length}`)
|
||||
}
|
||||
|
||||
return values.map((value, index) => ({
|
||||
mapValues: (values, timestamp) =>
|
||||
|
NBizyaev marked this conversation as resolved
Outdated
NBizyaev
commented
Если ты используешь чтение внешнего файла, то этот файл должен существовать Если ты используешь чтение внешнего файла, то этот файл должен существовать
в контейнере, в котором этот код будет задеплоен,
это значит, что копирование этих файлов должно появиться в Dockerfile
или (если они туда уже копируются) хотя бы комментарий в Dockerfile о том, что там что-то такое есть, что программа будет искать, считывать и потом использовать.
|
||||
values.map((value, index) => ({
|
||||
edge: config.edge,
|
||||
timestamp,
|
||||
tag: tags[index],
|
||||
value,
|
||||
}))
|
||||
},
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
Избыточная валидация