freshness.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. /**
  2. * 新鲜度领域对象(票 05 交字段与词汇,票 16 交渲染与轮询判定)。
  3. *
  4. * 后端形状(`freshness-semantics-demo.html`、`CockpitFreshnessResponse`):
  5. * `{ updatedAt, statusTime, serverUpdatedAt, freshness: CURRENT|STALE|EXPIRED|EMPTY,
  6. * staleReason, calibrationPeriodSeconds, missedCycles, dataAgeSeconds, retained, message }`
  7. *
  8. * 票 16 的验收原句「连续两个配置周期未成功更新时显示过期或不可用」对应 `missedCycles >= 2`,
  9. * 本层把这条**判据**定下来(纯函数、可断言),但「什么时候去拉、轮询还是推送」归票 15/16。
  10. *
  11. * 硬约束(票 16「缺失指标显示占位符,不显示虚假零值」):
  12. * 时间为 null 时保持 null,界面显示占位符;`dataAgeSeconds` 缺失也不回 0
  13. * (0 秒会被读成「刚刚更新过」,比空白更误导)。
  14. */
  15. import { fieldReader } from './fields.js'
  16. /** 后端新鲜度四态。 */
  17. export const FRESHNESS = {
  18. CURRENT: 'CURRENT',
  19. STALE: 'STALE',
  20. EXPIRED: 'EXPIRED',
  21. EMPTY: 'EMPTY',
  22. }
  23. /** 判定为「已过期」所需的未更新周期数(票 16 的「连续两个配置周期」)。 */
  24. export const EXPIRED_MISSED_CYCLES = 2
  25. /**
  26. * 新鲜度响应 → 领域对象。
  27. * @param {object} raw 后端 `CockpitFreshnessResponse`
  28. */
  29. export function toFreshness(raw) {
  30. const reader = fieldReader(raw)
  31. return {
  32. deviceCode: reader.optionalStr('deviceCode'),
  33. // 三个时间各自独立:缺失就是 null(界面显示占位符)。
  34. updatedAt: reader.optionalStr('updatedAt'),
  35. statusTime: reader.optionalStr('statusTime'),
  36. serverUpdatedAt: reader.optionalStr('serverUpdatedAt'),
  37. freshness: reader.optionalStr('freshness'),
  38. staleReason: reader.optionalStr('staleReason'),
  39. calibrationPeriodSeconds: reader.optionalNum('calibrationPeriodSeconds'),
  40. missedCycles: reader.optionalNum('missedCycles'),
  41. dataAgeSeconds: reader.optionalNum('dataAgeSeconds'),
  42. // 「保留旧数据」:这是过期与不可用的分界(票 16 要求保留但不伪装成实时)。
  43. retained: reader.bool('retained', false),
  44. message: reader.optionalStr('message'),
  45. missing: reader.missing,
  46. }
  47. }
  48. /**
  49. * 是否已过期。优先用后端给的 `freshness`,缺失时才按 `missedCycles` 回退判定
  50. * ——前端不推翻后端结论,只在后端没给结论时兜底。
  51. * @param {object} freshness toFreshness() 的结果
  52. */
  53. export function isExpired(freshness) {
  54. const current = freshness || {}
  55. if (current.freshness === FRESHNESS.EXPIRED) return true
  56. if (current.freshness === FRESHNESS.CURRENT || current.freshness === FRESHNESS.STALE) return false
  57. if (typeof current.missedCycles === 'number') return current.missedCycles >= EXPIRED_MISSED_CYCLES
  58. return false
  59. }
  60. /** 是否还有可用值(过期但保留旧值仍算「有值」)。 */
  61. export function hasValue(freshness) {
  62. const current = freshness || {}
  63. if (current.freshness === FRESHNESS.EMPTY) return false
  64. if (current.retained) return true
  65. return !!current.updatedAt
  66. }
  67. /**
  68. * 数据年龄文案。缺失时给占位符而不是「0 秒」。
  69. * @param {object} freshness toFreshness() 的结果
  70. */
  71. export function dataAgeText(freshness) {
  72. const age = (freshness && freshness.dataAgeSeconds)
  73. if (typeof age !== 'number' || !Number.isFinite(age) || age < 0) return '—'
  74. if (age < 60) return `${Math.round(age)} 秒前`
  75. if (age < 3600) return `${Math.round(age / 60)} 分钟前`
  76. return `${Math.round(age / 3600)} 小时前`
  77. }