timestamps.ts 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. /**
  2. * Timestamp normalisation for values coming out of the smartbotic-database.
  3. *
  4. * The daemon stamps documents with `_created_at` and `_updated_at` in
  5. * NANOSECONDS - around 1.78e18. Everything else in the platform works in
  6. * milliseconds: Date.now() in nodes, TimeUtils::nowMs() in C++, and the
  7. * startedAt / finishedAt the engine writes onto an execution.
  8. *
  9. * Two traps live here, and the workflow list fell into both at once:
  10. *
  11. * - The metadata keys are snake_case (`_created_at`). Reading `_createdAt`
  12. * returns undefined and every date renders as "unknown".
  13. * - A nanosecond value is far larger than the largest date JavaScript accepts
  14. * (8.64e15 ms). Passing one to new Date() or date-fns yields Invalid Date,
  15. * so fixing only the key name still renders "unknown" - which makes the
  16. * first fix look like it did nothing.
  17. */
  18. // The largest instant the ECMAScript Date type can represent, in milliseconds.
  19. const MAX_JS_DATE_MS = 8640000000000000
  20. // Well past any plausible millisecond timestamp (1e15 ms is the year 33658) and
  21. // well below any plausible nanosecond one, so it separates the two units
  22. // without having to be told which was meant.
  23. const NANOSECOND_THRESHOLD = 1e15
  24. /**
  25. * Convert a database timestamp to milliseconds, tolerating either unit.
  26. * Returns null when the value is missing or cannot be a real instant, so
  27. * callers can render their own placeholder rather than an Invalid Date.
  28. */
  29. export function toMillis(value: unknown): number | null {
  30. const raw = typeof value === 'string' ? Number(value) : value
  31. if (typeof raw !== 'number' || !isFinite(raw) || raw <= 0) {
  32. return null
  33. }
  34. const millis = raw > NANOSECOND_THRESHOLD ? Math.floor(raw / 1000000) : raw
  35. if (millis > MAX_JS_DATE_MS) {
  36. return null
  37. }
  38. return millis
  39. }
  40. /**
  41. * Read a document's timestamp regardless of which spelling it carries.
  42. *
  43. * The database writes snake_case, but parts of this codebase were written
  44. * against camelCase and some responses are reshaped before they arrive. Trying
  45. * both costs nothing and removes a whole class of silent "unknown".
  46. */
  47. export function readTimestamp(
  48. data: Record<string, any> | null | undefined,
  49. ...keys: string[]
  50. ): number | null {
  51. if (!data) {
  52. return null
  53. }
  54. for (const key of keys) {
  55. const millis = toMillis(data[key])
  56. if (millis !== null) {
  57. return millis
  58. }
  59. }
  60. return null
  61. }