KloradDocs

Time

Readings are ordered by when they were observed, not when they arrived. Late data never overwrites a newer state.

Sensors and vendor APIs do not deliver in order. A gateway buffers, a mobile network drops and retries, a poll returns yesterday's value with today's response. If a twin takes the last reading that arrived as the truth, it will sometimes show the past as the present. Klorad's Time Spectrum exists to make that impossible.

Two instants per reading

Every observation carries two instants:

  • observedAt: when the physical world was in that state. The source supplies it.
  • receivedAt: when Klorad learnt about it. The scene stamps it from its clock.

The current state of a quantity is the reading with the latest observedAt (ties broken by the source's seq). Arrival order decides nothing.

observedarrived21.8 °Clatest22.0 °Clatest21.9 °Clate
21.9 arrives last but was observed before 22.0: it joins the history and the current state stays 22.0.

Latest, late, duplicate, stale

scene.observe returns what happened to each reading:

StatusMeaningEvent
latestNewer than the current state: it becomes the current state.observation, latest: true
lateOlder than the current state: it joins the history, in order, and changes nothing else.observation, latest: false
duplicateThe same reading again: same source, quantity and observedAt, and the same seq (or the same value when there is no seq). Ignored.none
rejectedUnknown object, a Digital Object, or an invalid reading.none

A reading is stale when it is older than the object's staleAfterMs (or the scene's, five minutes by default). Staleness is evaluated against the scene's clock when you ask (scene.isStale), so a sensor that goes quiet turns stale without any event. The three.js renderer re-checks it as time passes; in React, useShadow(id).isStale(quantity) evaluates it at render time.

observations.ts
import { createManualClock, createScene } from "@klorad/api/world";

const clock = createManualClock(Date.parse("2026-10-09T10:00:00Z"));
const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } }, clock, staleAfterMs: 60_000 });
scene.add({
  id: "climate",
  name: "Hall climate",
  position: { east: -6, north: 2, up: 7 },
  correspondence: "shadow",
  binding: { source: "bms", entity: "ahu-3" },
});

const t = clock.now();
const reading = (observedAt: number, value: number, seq: number) =>
  scene.observe({ objectId: "climate", quantity: "temperature", unit: "°C", value, observedAt, seq, source: "bms" });

reading(t - 2_000, 21.8, 41).status; // "latest"
reading(t - 1_000, 22.0, 42).status; // "latest"
reading(t - 1_500, 21.9, 43).status; // "late": kept in the history, does not replace 22.0
reading(t - 1_000, 22.0, 42).status; // "duplicate": nothing changes, no event

scene.latest("climate", "temperature")?.value; // 22.0
scene.history("climate", "temperature").length; // 3
clock.advance(61_000);
scene.isStale("climate", "temperature"); // true

Clocks

A scene reads time from a Clock. The default is the system clock. createManualClock gives you one you advance yourself, which makes tests of staleness and ordering exact; see Test with a manual clock.

The history keeps the last 500 readings per object and quantity by default (historyLimit on createScene).

Run it liveSandbox: Digital ShadowTimestamped readings from a fixture source: late arrivals, duplicates and staleness handled by the Time Spectrum.

Something wrong or unclear? Tell your contact at Prieston Technologies.