# Data — SDK Methods

> The typed Data pub/sub surface: the Data client, publish, subscribe with + / # filters, DataMessage, retained messages, and the wire helpers.

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

**TypeScript:**
```ts
import { Data } from "@clutchcall/sdk/data";
```

**Python:**
```python
from clutchcall.data import 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.

**TypeScript:**
```ts
const data = new Data({
  relayHost: "relay.clutchcall.dev",   // optional; defaults to the platform relay
  token:     process.env.CLUTCHCALL_CREDENTIALS!,
  clientId:  "device-7",
});
```

**Python:**
```python
import os
data = Data(
    relay_host="relay.clutchcall.dev",   # optional
    token=os.environ["CLUTCHCALL_CREDENTIALS"],
    client_id="device-7",
)
```

### Constructor options

<ParamField path="token" type="string" required>
  Bearer token for the relay session. Carries the tenant and the `clientId`
  claims. The same token authorizes every other modality.
</ParamField>

<ParamField path="clientId" type="string" required>
  This client's stable id. Attached as `fromClientId` on every published
  message so subscribers can attribute the source.
</ParamField>

<ParamField path="relayHost" type="string">
  Relay hostname. Defaults to the platform relay.
</ParamField>

<ParamField path="onState" type="(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.
</ParamField>

<ParamField path="webTransport" type="WebTransportFactory">
  Inject a custom WebTransport factory — a Node polyfill or a test double.
  Optional; the browser SDK uses the native implementation.
</ParamField>

## 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.

**TypeScript:**
```ts
await data.publish({
  topic:    "sensors/room1/temperature",
  payload:  new TextEncoder().encode("23.5"),
  reliable: false,   // default: lossy datagram lane
  retained: false,   // default: not cached
});
```

**Python:**
```python
data.publish(
    topic="sensors/room1/temperature",
    payload=b"23.5",
    reliable=False,
    retained=False,
)
```

<ParamField path="topic" type="string" required>
  Full hierarchical topic, e.g. `sensors/room1/temperature`. UTF-8, ≤ 255 bytes.
</ParamField>

<ParamField path="payload" type="Uint8Array | bytes" required>
  Opaque payload bytes. Encode JSON / text / protobuf yourself.
</ParamField>

<ParamField path="reliable" type="boolean" default="false">
  `false` → lossy QUIC datagram lane (lowest latency). `true` → ordered, retried
  subgroup-stream lane (guaranteed in-order per publisher).
</ParamField>

<ParamField path="retained" type="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.
</ParamField>

**Returns** a `Promise<void>` (TS) / `None` (Python). Resolves once the frame is
handed to the transport.

> **WARNING:**
> 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.

**TypeScript:**
```ts
const sub = await data.subscribe(
  { topicFilter: "sensors/+/temperature" },
  (msg) => {
    console.log(
      msg.topic, "←", msg.fromClientId, "=",
      new TextDecoder().decode(msg.payload),
      msg.retained ? "(retained)" : "",
    );
  },
);
```

**Python:**
```python
sub = data.subscribe(
    topic_filter="sensors/+/temperature",
    on_message=lambda m: print(
        m.topic, "←", m.from_client_id, "=", m.payload,
        "(retained)" if m.retained else "",
    ),
)
```

<ParamField path="topicFilter" type="string" required>
  MQTT-style filter. `+` matches one segment, `#` matches the rest (trailing
  only). The top-level segment must be concrete.
</ParamField>

<ParamField path="onMessage" type="(msg: DataMessage) => void" required>
  Invoked for every message whose topic matches the filter. Malformed frames are
  dropped silently before the callback.
</ParamField>

**Returns** a `DataSubscription` (TS: `Promise<DataSubscription>`). Call
`.close()` to stop receiving.

### `close()`

Tear down every publication and subscription and close the MoQT session.
Idempotent.

**TypeScript:**
```ts
await data.close();
```

**Python:**
```python
data.close()
```

## Handles and types

### `DataMessage`

What `onMessage` receives.

**TypeScript:**
```ts
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
}
```

**Python:**
```python
@dataclass
class DataMessage:
    topic: str
    from_client_id: str
    payload: bytes
    retained: bool
```

<ResponseField name="topic" type="string">
  The full topic string the publisher sent (not the filter).
</ResponseField>
<ResponseField name="fromClientId" type="string">
  The `clientId` of the publisher — for attribution / per-client demux.
</ResponseField>
<ResponseField name="payload" type="Uint8Array | bytes">
  The opaque message bytes.
</ResponseField>
<ResponseField name="retained" type="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.
</ResponseField>

### `DataSubscription`

The handle returned by `subscribe`. Carries `topicFilter` and exposes:

**TypeScript:**
```ts
sub.close();   // stop receiving for this filter
```

**Python:**
```python
sub.close()
```

## 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:

| State          | Meaning                                                   |
| -------------- | --------------------------------------------------------- |
| `Connecting`   | First QUIC/MoQT handshake in flight.                      |
| `Connected`    | Session up; publishes and subscribes flow.                |
| `Reconnecting` | Transport dropped; the SDK is re-establishing and will replay subscriptions. |
| `Closed`       | You called `close()`.                                     |
| `Failed`       | Unrecoverable — token rejected, relay unreachable.        |

> **NOTE:**
> 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:

**TypeScript:**
```ts
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"
```

**Python:**
```python
from clutchcall.data import (
    encode_data_frame, decode_data_frame,
    topic_matches, top_level_segment,
)

frame = encode_data_frame("device-7", "sensors/room1/temp", payload)
decoded = decode_data_frame(frame)             # DecodedDataFrame
topic_matches("sensors/room1/temp", "sensors/+/temp")    # True
top_level_segment("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](/modalities/data/details#wire-model).

## 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](/concepts/realtime-tracks).
