KloradDocs

Integration

Sources report readings about their own entities; connectSource routes them to the shadows and twins bound to them.

The Integration layer couples the world to reality. It depends on World; World never imports it. Its central idea is that a source knows nothing about scenes: it reports readings about its own entities (entrance-01, vms-12, barrier-1), and the scene decides where they belong through each object's binding.

binding: { source: "gateway", entity: "entrance-01" }

That keeps vendor code and world code apart. You can swap a fixture for a real gateway, or one vendor for another, without touching a single Scene Object.

Sources

An ObservationSource has an id (matched against binding.source) and a start(sink) method that begins reporting and returns a function that stops it. Three ship today:

SourceWhat it doesUse it for
fixtureSourceGenerates readings on an interval from functions you give it.Demos, tests, the quickstart. Not a device.
pollingSourceCalls your read() on an interval and reports what it returns.Any system that only offers a pull API.
fixtureDeviceA simulated device: reports state and accepts commands.Demonstrating a Digital Twin's two way loop.

Writing your own is one object with two members, so a push source (a webhook receiver, a message queue consumer) is a few lines.

Connecting

connectSource(scene, source) starts the source and routes every reading to every shadow or twin bound to that source and entity. It returns a Connection whose stats() counts what happened, which is the first thing to look at when data does not show up:

CounterMeaning
receivedReadings the source reported.
latest, late, duplicateWhat the scene did with them (see Time).
unboundNo object is bound to that source and entity. Usually a typo in a binding.
rejectedThe scene refused the reading, for example an invalid value.
connect-source.ts
import { createScene } from "@klorad/api/world";
import { connectSource, fixtureSource, pollingSource } from "@klorad/api/integration";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({
  id: "entrance",
  name: "Entrance counter",
  position: { east: 16, north: -10 },
  correspondence: "shadow",
  binding: { source: "gateway", entity: "entrance-01" },
});

// A fixture: generated readings once a second. A demo of the data path, not a device.
const fixture = fixtureSource({
  id: "gateway",
  channels: [{ entity: "entrance-01", quantity: "occupancy", unit: "people", value: (tick) => 20 + (tick % 7) }],
});
const connection = connectSource(scene, fixture);

// A real system that only offers a pull API, for example a connector's getStatus.
interface SignStatus {
  message: string;
  updatedAt: string;
}
declare const signs: { getStatus(ids: string[]): Promise<Record<string, SignStatus>> };

const vms = pollingSource({
  id: "atms",
  intervalMs: 15_000,
  read: async () => {
    const status = await signs.getStatus(["vms-12", "vms-14"]);
    return Object.entries(status).map(([entity, s]) => ({
      entity,
      quantity: "message",
      value: s.message,
      observedAt: Date.parse(s.updatedAt),
    }));
  },
});

export { connection, vms };

Outside data is not trusted

Validate every vendor payload at the boundary, inside read() or your push handler, before it becomes a reading. Connect a polling API shows the pattern with Zod. Credentials for vendor APIs belong on a server, never in browser code.

Devices and commands

A Digital Twin's commands leave through an Actuator: an object with a source (matched against the twin's binding.source) and a send(command) method that returns whether the device accepted it. fixtureDevice is both a source and an actuator, so it closes the loop the way a real device does: it accepts a command, and after a latency reports the new state as a reading. See Interaction and actuation.

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.