| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 |
- /**
- * 运行链路领域接口(票 05 只交 REST 侧的分区读取;消费方是票 15/16)。
- *
- * ADR 0005 的 `runtimeApi`:WebSocket 是运行链路,前端消费的是**版本化领域事件**,
- * 不把 `/ws/deviceStatus` 的裸 JSON 当依赖。本文件先交 REST 侧能落地的部分
- * (新鲜度、设备状态增量帧 → 领域事件的**转换**),连接与重连归票 15。
- *
- * 增量帧的字段名有个坑(调研 §1 已核实):WS 线上帧的状态键是 `data.status`,
- * 而后端 DTO 命名为 `monitoringState`。转换层两个键都认,避免接错一边就静默丢状态。
- */
- import { toFreshness } from '../domain/freshness.js'
- import { fetchSection } from './domainRequest.js'
- import { SECTION_STATE, section } from '../domain/sectionState.js'
- /** WS 消息类型(后端 `DeviceStatusWebSocketHandler`)。 */
- export const WS_MESSAGE = {
- SUBSCRIBE: 'cockpitSubscribe',
- SUBSCRIBED: 'cockpitSubscribed',
- DELTA: 'cockpitDeviceStatusDelta',
- AUTHENTICATE: 'authenticate',
- }
- /**
- * 设备状态增量帧 → 领域事件。
- * 只做字段归一,不做取舍:是否采纳(旧消息要丢弃)由票 15 按 `serverUpdatedAt` 判。
- * @param {object} frame WS 线上帧
- * @returns {object|null} 认不出的帧回 null(调用方据此忽略,不猜)
- */
- export function toDeviceStatusEvent(frame) {
- const raw = frame && typeof frame === 'object' ? frame : null
- if (!raw || raw.type !== WS_MESSAGE.DELTA) return null
- const payload = raw.data && typeof raw.data === 'object' ? raw.data : {}
- const deviceCode = raw.deviceCode || raw.device_code || payload.deviceCode || ''
- if (!deviceCode) return null
- return {
- deviceCode,
- // 线上帧用 status,DTO 用 monitoringState:两个都认。
- status: payload.status || payload.monitoringState || '',
- statusTime: payload.statusTime || '',
- serverUpdatedAt: payload.serverUpdatedAt || raw.serverUpdatedAt || '',
- reason: payload.reason || '',
- // 增量帧不带坐标与行业(只能靠 deviceCode 回联 REST 快照)——票 15 据此设计合并。
- sampledAt: payload.sampledAt || '',
- }
- }
- /** 订阅确认帧 → 领域事件。 */
- export function toSubscribedEvent(frame) {
- const raw = frame && typeof frame === 'object' ? frame : null
- if (!raw || raw.type !== WS_MESSAGE.SUBSCRIBED) return null
- return { scope: raw.scope || '', serverUpdatedAt: raw.serverUpdatedAt || '' }
- }
- /**
- * 设备新鲜度分区(票 16 的统一新鲜度模型)。
- * @param {string} deviceCode 设备编码
- * @param {object} [options] `{ calibrationPeriodSeconds, config, fetchImpl }`
- */
- export async function fetchFreshnessSection(deviceCode, options = {}) {
- if (!deviceCode) {
- return section(SECTION_STATE.FAILED, { reason: '缺少设备编码' })
- }
- const result = await fetchSection(`/equipments/${encodeURIComponent(deviceCode)}/freshness`, {
- params: { calibrationPeriodSeconds: options.calibrationPeriodSeconds },
- toData: toFreshness,
- config: options.config,
- fetchImpl: options.fetchImpl,
- })
- if (result.state !== SECTION_STATE.SUCCESS) return result
- const freshness = result.data || {}
- // 后端四态 → 分区状态:EXPIRED/STALE 归「过期但保留旧值」,EMPTY 归「确实没有」。
- if (freshness.freshness === 'EMPTY') {
- return section(SECTION_STATE.EMPTY, { mode: result.mode, data: freshness, freshness: 'EMPTY' })
- }
- if (freshness.freshness === 'EXPIRED' || freshness.freshness === 'STALE') {
- return section(SECTION_STATE.STALE, {
- mode: result.mode,
- reason: freshness.staleReason || '',
- data: freshness,
- freshness: freshness.freshness,
- retained: freshness.retained === true,
- })
- }
- return section(SECTION_STATE.SUCCESS, { mode: result.mode, data: freshness, freshness: freshness.freshness || 'CURRENT' })
- }
|