KloradDocs

Connect a polling API

Read a vendor endpoint on an interval, validate what comes back, and route it to your shadows.

Many systems you will meet, from traffic management to building management, offer only a pull API: you ask for the current state, they answer. pollingSource turns such an API into a source of readings.

polling-api.ts
import { z } from "zod";
import { connectSource, pollingSource } from "@klorad/api/integration";
import { createScene } from "@klorad/api/world";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({
  id: "vms-12",
  name: "Sign VMS-12",
  position: { east: 120, north: 40 },
  correspondence: "shadow",
  binding: { source: "atms", entity: "vms-12" },
});

// What the vendor promises to send. Anything else is refused before it reaches the scene.
const SignsResponse = z.object({
  signs: z.array(z.object({ id: z.string(), message: z.string(), updatedAt: z.string().datetime() })),
});

const atms = pollingSource({
  id: "atms",
  intervalMs: 15_000,
  read: async () => {
    const response = await fetch("https://atms.example.com/api/signs");
    const body = SignsResponse.parse(await response.json());
    return body.signs.map((s) => ({ entity: s.id, quantity: "message", value: s.message, observedAt: Date.parse(s.updatedAt) }));
  },
  onError: (error) => console.warn("ATMS poll failed", error),
});

const connection = connectSource(scene, atms);

// Counts every reading: received, latest, late, duplicate, unbound and rejected.
const { received, unbound } = connection.stats();

export { connection, received, unbound };

What matters

Bind by the vendor's own ids. The source reports entity: s.id exactly as the vendor names it, and each shadow's binding.entity uses the same id. Mapping happens in bindings, not in the source.

Use the vendor's timestamp. observedAt must be when the vendor observed the state, not when you polled. A poll that returns an unchanged sign with an unchanged updatedAt then becomes a duplicate and costs nothing, and an out of date answer cannot overwrite a newer one.

Validate at the boundary. The schema runs before anything reaches the scene. If the vendor changes its payload, parse throws, the poll fails, onError hears about it, and the twin keeps its last good state instead of filling with nonsense.

Keep credentials on the server. If the API needs a key, run the polling source in server code (a route handler, a worker) and forward readings to browsers; never ship the key to the client.

When something does not show up

Look at connection.stats(). A growing unbound count means readings arrive for entities no object is bound to: compare the vendor's ids with your bindings. A growing duplicate count is normal for polling.

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