Skip to content

Stateful Decoding

Stateful encoding sends full frames and patches over an ordered channel. Decoding requires matching session state on the receiver — this guide covers how patches work on the consumer side, language support, and recovery patterns.

For the producer side, see Stateful Streams.

What the receiver must do

text
1. Maintain session decoder state across frames (same order as producer)
2. First frame: decode as full Dynamic message → store as baseline
3. Patch frames: apply operations against baseline → update stored value
4. On disconnect or decode error: discard state, wait for new full frame

Patches are not self-contained. A patch frame references field IDs and base snapshots from prior frames in the same session.

Language support

LanguageFull frame decodePatch decodeSession decoder API
Rustdecode()SessionEncoder::decode_message()TwilicCodec, SessionEncoder
Pythondecode()decode_message()TwilicCodec, SessionEncoder
GoDecode()DecodeMessage()TwilicCodec, SessionEncoder
Javadecode()✓ via SessionEncoderSessionEncoder
JavaScriptdecode()Limited — encode APIs primarycreateSessionEncoder()

JavaScript note @twilic/core currently exposes session encode APIs (encode, encodePatch, encodeMicroBatch). For WebSocket clients that need patch decode in JS, use simulate mode to evaluate savings, or decode on a Rust/Go/Python backend service. :::

Rust receiver example

rust
use twilic::{create_session_encoder, SessionOptions, Value};

let mut dec = create_session_encoder(SessionOptions::default());

for frame in incoming_frames {
match dec.decode(&frame) {
Ok(value) => {
// Full frame or successfully applied patch
render_dashboard(&value);
}
Err(e) if e.to_string().contains("StatelessRetryRequired") => {
// Unknown base reference — request full frame from producer
request_full_snapshot();
}
Err(e) => eprintln!("decode error: {e}"),
}
}

Low-level message decode for debugging:

rust
use twilic::{TwilicCodec, Message};

let mut codec = TwilicCodec::default();
let msg = codec.decode_message(&bytes)?;
// Inspect Message::StatePatch { base_ref, operations, .. }

Python receiver example

python
import twilic

dec = twilic.create_session_encoder()

for frame in incoming_frames:
try:
value = dec.decode(frame)
render_dashboard(value)
except twilic.ErrStatelessRetryRequired:
request_full_snapshot()

Go receiver example

go
dec := twilic.NewSessionEncoder(twilic.SessionOptions{})

for _, frame := range incomingFrames {
value, err := dec.Decode(frame)
if err != nil {
if twilic.IsStatelessRetryRequired(err) {
requestFullSnapshot()
continue
}
log.Printf("decode error: %v", err)
continue
}
renderDashboard(value)
}

UnknownReferencePolicy

When the decoder encounters a base ID, shape reference, or dictionary ID it does not know:

PolicyBehavior
failFast (default)Decode error immediately
statelessRetryReturns StatelessRetryRequired — receiver requests full frame

Configure on both producer and consumer for consistent behavior:

ts
createSessionEncoder({ unknownReferencePolicy: "statelessRetry" });

See Session Encoder.

Recovery protocol

Implement this on both sides after disconnect or state drift:

Client-side recovery

ts
let awaitingBaseline = true;

ws.onmessage = (event) => {
const bytes = new Uint8Array(event.data);

if (awaitingBaseline) {
const value = decode(bytes); // stateless decode works on full frames
baseline = value;
awaitingBaseline = false;
render(value);
return;
}

try {
const value = applySessionPatch(baseline, bytes); // language SDK
baseline = value;
render(value);
} catch {
awaitingBaseline = true;
ws.send(JSON.stringify({ type: "request_snapshot" }));
}
};

ws.onopen = () => {
awaitingBaseline = true;
};

Server-side recovery

ts
enc.reset(); // clear encoder state
ws.send(enc.encode(currentMetrics)); // full baseline
// resume encodePatch() on subsequent ticks

WebSocket demo

The examples repository includes:

bash
pnpm example:websocket:simulate   # size comparison (recommended first)
pnpm example:websocket # live server
pnpm example:websocket:client # client logs frame sizes

The simulate script compares JSON, full encode(), and encodePatch() per tick without requiring patch decode on the client.

When to decode stateful vs stateless

Frame typeDecoder
First frame after connectStateless decode() — works in all SDKs
Patch frameSession decoder with accumulated state
Batch in streamSession decode() or stateless decode() depending on message kind
HTTP response bodyAlways stateless decode()

Released under the CC-BY-4.0 License.