The data modality ships as a subpath of the ClutchCall SDK. Import the Data client, bind a clientId, then publish and subscribe against hierarchical topics. One Data instance reuses a single MoQT session across every publish and subscribe.

Import

import { Data } from "@clutchcall/sdk/data";
Other languages expose the same surface under the equivalent subpath (github.com/clutchcall/clutchcall-sdk/go, clutchcall, com.clutchcall:clutchcall-sdk, ClutchCall.SDK); the method names and wire format are identical.

Data

The client. Construct one per process or device.
const data = new Data({
  relayHost: "relay.clutchcall.dev",   // optional; defaults to the platform relay
  token:     process.env.CLUTCHCALL_CREDENTIALS!,
  clientId:  "device-7",
});

Constructor options

token
string
required
Bearer token for the relay session. Carries the tenant and the clientId claims. The same token authorizes every other modality.
clientId
string
required
This client’s stable id. Attached as fromClientId on every published message so subscribers can attribute the source.
relayHost
string
Relay hostname. Defaults to the platform relay.
onState
(state, reason?) => void
Optional connection-state callback. Fires as the underlying QUIC/MoQT session moves through Connecting → Connected → Reconnecting → Closed / Failed. The session auto-reconnects and replays subscriptions.
webTransport
WebTransportFactory
Inject a custom WebTransport factory — a Node polyfill or a test double. Optional; the browser SDK uses the native implementation.

Methods

publish(args)

Publish one message. The SDK adds the (fromClientId, topic) header and selects the MoQT track from the topic’s top-level segment.
await data.publish({
  topic:    "sensors/room1/temperature",
  payload:  new TextEncoder().encode("23.5"),
  reliable: false,   // default: lossy datagram lane
  retained: false,   // default: not cached
});
topic
string
required
Full hierarchical topic, e.g. sensors/room1/temperature. UTF-8, ≤ 255 bytes.
payload
Uint8Array | bytes
required
Opaque payload bytes. Encode JSON / text / protobuf yourself.
reliable
boolean
default:"false"
false → lossy QUIC datagram lane (lowest latency). true → ordered, retried subgroup-stream lane (guaranteed in-order per publisher).
retained
boolean
default:"false"
true → the relay caches the latest payload on this topic and delivers it to every late-joining subscriber on attach. Publish a zero-length payload with retained: true to clear it.
Returns a Promise<void> (TS) / None (Python). Resolves once the frame is handed to the transport.
The top-level segment must be concrete. publish({ topic: "sensors/..." }) is fine; a topic that begins with + or # throws — the first segment selects the track.

subscribe(args, onMessage)

Subscribe with an MQTT-style filter. The callback fires for every matching message. The SDK opens one MoQT subscription on the filter’s top-level segment and filters the rest of the path client-side.
const sub = await data.subscribe(
  { topicFilter: "sensors/+/temperature" },
  (msg) => {
    console.log(
      msg.topic, "←", msg.fromClientId, "=",
      new TextDecoder().decode(msg.payload),
      msg.retained ? "(retained)" : "",
    );
  },
);
topicFilter
string
required
MQTT-style filter. + matches one segment, # matches the rest (trailing only). The top-level segment must be concrete.
onMessage
(msg: DataMessage) => void
required
Invoked for every message whose topic matches the filter. Malformed frames are dropped silently before the callback.
Returns a DataSubscription (TS: Promise<DataSubscription>). Call .close() to stop receiving.

close()

Tear down every publication and subscription and close the MoQT session. Idempotent.
await data.close();

Handles and types

DataMessage

What onMessage receives.
interface DataMessage {
  topic:        string;      // full topic the publisher sent
  fromClientId: string;      // publisher's clientId
  payload:      Uint8Array;  // opaque bytes
  retained:     boolean;     // true if a cached/bootstrap value, not live
}
topic
string
The full topic string the publisher sent (not the filter).
fromClientId
string
The clientId of the publisher — for attribution / per-client demux.
payload
Uint8Array | bytes
The opaque message bytes.
retained
boolean
true when this is a retained/bootstrap value the relay re-emitted on attach; false for live traffic. Lets you treat a snapshot differently from updates.

DataSubscription

The handle returned by subscribe. Carries topicFilter and exposes:
sub.close();   // stop receiving for this filter

Connection state and events

The data client does not raise per-message events beyond your onMessage callback. Lifecycle is surfaced through the optional onState constructor callback, which mirrors the underlying session:
StateMeaning
ConnectingFirst QUIC/MoQT handshake in flight.
ConnectedSession up; publishes and subscribes flow.
ReconnectingTransport dropped; the SDK is re-establishing and will replay subscriptions.
ClosedYou called close().
FailedUnrecoverable — token rejected, relay unreachable.
On reconnect the SDK replays your subscriptions automatically, and the relay re-emits matching retained values, so a transient drop re-bootstraps current state without app code.

Wire helpers (advanced)

For interop tests or callers that want to stay wire-compatible while bypassing the high-level client, the subpath also exports the raw codec and matcher:
import {
  encodeDataFrame, decodeDataFrame,
  topicMatches, topLevelSegment,
} from "@clutchcall/sdk/data";

const frame = encodeDataFrame("device-7", "sensors/room1/temp", payload);
const decoded = decodeDataFrame(frame);        // { fromClientId, topic, payload }
topicMatches("sensors/room1/temp", "sensors/+/temp");   // true
topLevelSegment("sensors/+/temp");                       // "sensors"
Both from_client_id and topic are bounded at 255 bytes; topLevelSegment throws on a leading wildcard. These mirror the frame layout in Details.

Dropping to the substrate

The data modality is a thin convention over MoQT frames. If you need a shape it does not cover, import @clutchcall/sdk/moqt and use MoqtClient.publishFrame / subscribeFrame directly under your own namespace — the same auto-reconnect, capability routing, and relay fan-out apply. See Realtime Tracks.