Advanced Metal Research
GitHub Contact AMR

TypeScript contracts

On this page
  1. Example: typed calls from Node
  2. uint64 precision
  3. Telemetry decoders
  4. What is exported
  5. Related pages

rt-core/clients/ts/ holds two generated TypeScript files for applications that talk to rt-control from Node or from a web front end behind a server:

FileContents
rt_control_api_generated.tsAn interface for every contract type, the Reason… and Operation… constants, the Capabilities list, ContractVersion, RequestSchema, ResponseSchema and CapabilitiesDigest.
rt_protocol_generated.tsLittle-endian decoders for the binary telemetry stream: decodeTelemetryBatchHeaderV1, decodeCycleCaptureRecordV2, decodeCycleCaptureAxisV2, their byte sizes, CycleCaptureRecordV2LayoutDigest and TelemetryLayoutMismatchError.

They have no imports and no runtime dependencies. Compile them with your own TypeScript toolchain, targeting ES2020 or later; nothing needs installing in rt-core. There is no HTTP client: rt-control listens on a Unix socket or on mutual TLS, which a browser cannot reach directly. Call it from Node, or through a server of your own.

Both files are regenerated from the contract by make generate-api and make generate-protocol, and make check-api and make check-protocol fail if they drift.

Example: typed calls from Node#

describe.tsTypeScript
import http from "node:http";
import {
  CapabilitiesDigest, RequestSchema, ReasonControlAlreadyOwned,
  type Description, type Grant, type Response,
} from "./rt_control_api_generated.ts";

const socketPath = "/run/rosie-rt-core/control.sock";

function call(method: string, path: string, body?: object): Promise<Response> {
  return new Promise((resolve, reject) => {
    const req = http.request({ socketPath, method, path, headers: { "Content-Type": "application/json" } }, (res) => {
      let text = "";
      res.setEncoding("utf8");
      res.on("data", (chunk) => (text += chunk));
      res.on("end", () => resolve(JSON.parse(text) as Response));
    });
    req.on("error", reject);
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

const d = (await call("GET", "/v1/describe")).data as Description;
if (d.capabilities_digest !== CapabilitiesDigest) throw new Error("contract mismatch");
console.log(d.backend, d.axes.map((a) => `${a.id} [${a.min_position}, ${a.max_position}] ${a.position_unit}`));

const r = await call("POST", "/v1/control", {
  schema: RequestSchema, operation: "acquire", controller: "ts-demo",
  binding: { pair_id: "cell-a", revision: 1, configuration_sha256: d.configuration_sha256, machine_sha256: d.machine_sha256 },
});
if (r.error === ReasonControlAlreadyOwned) console.log("someone else has control");
else if (!r.error) console.log("generation", (r.data as Grant).generation);

This sketch acquires but never renews or releases, so the grant simply expires after its lease. A real client must renew at a third of the lease and release when done; see Control authority.

uint64 precision#

JSON integer fields are typed number, which is exact only up to 2^53. Values such as deadline_host_ns, time_ns and event sequences can exceed that on a long-running host. When you need exact uint64 JSON values, parse with a lossless JSON parser. The binary telemetry decoders have no such problem: every 64-bit field decodes to bigint.

Request is declared with request_id?: string | null, but the server refuses null: omit the field, or send a string of 1..64 printable ASCII bytes.

Telemetry decoders#

The telemetry stream is a sequence of batches: a 312-byte header, then RecordCount records of CycleCaptureRecordV2Bytes (5392) bytes. Each decoder takes an exact-size Uint8Array and throws on a wrong length or a non-zero reserved field. decodeTelemetryBatchHeaderV1 also throws TelemetryLayoutMismatchError (with storedDigest and expectedDigest) when the batch uses another record layout.

telemetry.tsTypeScript
import {
  decodeTelemetryBatchHeaderV1, decodeCycleCaptureRecordV2,
  TelemetryBatchHeaderV1Bytes, CycleCaptureRecordV2Bytes,
} from "./rt_protocol_generated.ts";

// Feed raw chunks from GET /v1/telemetry/stream?after=<cursor>. HTTP chunks need
// not line up with batches, so buffer until a whole batch is available.
let pending = new Uint8Array(0);
let cursor = 0n;

export function onChunk(chunk: Uint8Array) {
  const joined = new Uint8Array(pending.length + chunk.length);
  joined.set(pending);
  joined.set(chunk, pending.length);
  pending = joined;
  for (;;) {
    if (pending.length < TelemetryBatchHeaderV1Bytes) return;
    const h = decodeTelemetryBatchHeaderV1(pending.subarray(0, TelemetryBatchHeaderV1Bytes));
    if (h.Magic !== 0x31425452 || h.Version !== 1 || h.RecordBytes !== CycleCaptureRecordV2Bytes) throw new Error("bad header");
    const size = TelemetryBatchHeaderV1Bytes + h.RecordCount * CycleCaptureRecordV2Bytes;
    if (pending.length < size) return;
    if (h.Dropped !== 0n) console.warn(`lost ${h.Dropped} records`);
    for (let i = 0; i < h.RecordCount; i++) {
      const at = TelemetryBatchHeaderV1Bytes + i * CycleCaptureRecordV2Bytes;
      const rec = decodeCycleCaptureRecordV2(pending.subarray(at, at + CycleCaptureRecordV2Bytes));
      console.log(rec.Seq, rec.Ax[0].Position); // Position is in drive counts
    }
    cursor = h.LastSequence; // resume point
    pending = pending.slice(size);
  }
}

Before accepting a batch, also check AxisCount (1..16), RecordCount (at most 4096), that each record's Seq follows on from the last and that Axes matches the header. If the adapter or daemon incarnation changes, stop and reconcile before you reset the cursor. The layout, field by field, is in Events and telemetry.

What is exported#

ExportKind
ContractVersion, RequestSchema, ResponseSchema, CapabilitiesDigestConstants: 2, rosie.rt-control.request.v1, rosie.rt-control.response.v1 and the schema digest.
Reason… (for example ReasonNotReady)One string constant per reason code.
Operation… (for example OperationStartProgram)One string constant per operation.
CapabilitiesThe 33 capabilities with name, state and transport, as const.
Fence, Binding, Grant, Request, Response, Description, ProcessStatus, Event, EventBatch …One interface per contract type. Native receipts (CommandResult, JogObservation, GrantObservation) keep their PascalCase field names.
TelemetryBatchHeaderV1, CycleCaptureRecordV2, CycleCaptureAxisV2Decoded telemetry interfaces, with 64-bit fields as bigint.