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.
Latest, late, duplicate, stale
scene.observe returns what happened to each reading:
| Status | Meaning | Event |
|---|---|---|
latest | Newer than the current state: it becomes the current state. | observation, latest: true |
late | Older than the current state: it joins the history, in order, and changes nothing else. | observation, latest: false |
duplicate | The same reading again: same source, quantity and observedAt, and the same seq (or the same value when there is no seq). Ignored. | none |
rejected | Unknown 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.
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"); // trueClocks
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).
Something wrong or unclear? Tell your contact at Prieston Technologies.