# Schema 3.4.0

scoreReasons becomes a string[], and both delta codecs arrive

Version coverage: /docs/reference/versions/


{{< callout context="caution" title="Schema and wire-format change" icon="alert-triangle" >}}
`ClientSample.scoreReasons` changes shape, so producers and consumers should move to the 3.4.0
generated schemas together. ([3.6.0](../v3-6-0/) changed it once more, to a map of contributions.)
{{< /callout >}}

## Changed — scoreReasons becomes a list

`ClientSample.scoreReasons` changed from an optional string to an optional **array of strings**,
so several independent reasons can be attached to a calculated score instead of encoding all the
detail into one string value.

```typescript
// before
scoreReasons?: string | null;

// 3.4.0
scoreReasons?: string[] | null;
```

## Added — @observertc/samples-protobuf-codec

**One package that both encodes and decodes**, replacing the separate
`@observertc/samples-encoder` and `@observertc/samples-decoder`. Same wire idea as before: each
message carries only what changed since the previous sample, over the all-optional `ClientSample`
proto, where explicit presence is what "changed" means.

- Dual ESM/`CommonJS` build with an `exports` map and full type declarations; no `Buffer` or other
  Node built-ins, so it runs in the browser that produces the samples.
- `ProtobufCodecError` with a `code` (`MALFORMED_INPUT`, `STREAM_DESYNC`, `INVALID_VALUE`,
  `INVALID_OPTION`) and a `context.path` pointing at the offending value, plus a non-throwing
  `decoder.tryDecode()`.
- `identifiers: { callId | clientId | peerConnectionId | trackId: 'utf8' | 'uuid' }` **replaces the
  `*IsUuid` booleans**. A value declared `uuid` that is not a UUID is now an error rather than a
  silent fallback.
- `createClientSampleCodec()` builds both halves from one options object, so they cannot disagree
  about identifier packing.
- `reset()` on either half is a keyframe: the next message is a full snapshot.
- Silent by default — pass a `logger` to hear anything.
- Regression suite with **byte-exact golden fixtures** for two recorded streams, plus randomised
  streams checked against an independent forward-fill model.

The generator emits only the sample and protobuf bindings into the package: the codec reads the
field list, scalar types and nesting **off the protobuf descriptor at runtime**, so a new schema
field reaches the published codec without any new per-field code.

[Full documentation →](/docs/codecs/protobuf/)

## Added — @observertc/samples-json-codec

The same delta codec over plain JSON, with **zero runtime dependencies** and a ~2.1 KB gzipped
bundle (the protobuf codec is ~29.8 KB, since it carries the protobuf runtime and descriptor).

- **A delta is a `ClientSample` with the unchanged parts left out** — nothing renamed, tagged or
  wrapped — so a message is readable in a log without a decoder, and a golden-fixture diff shows
  exactly which field started or stopped being sent.
- Identical semantics to the protobuf codec: forward-fill, keyed collections, `reset()` as a
  keyframe, matching error codes. **The two are interchangeable.**
- On the recorded test stream, JSON is 2.73× the protobuf payload raw and **1.41× gzipped** — so
  with `permessage-deflate` or any compressing transport the real cost is about 40%.
- `NaN` and `Infinity` are rejected rather than silently serialised to `null`.
- The decoder returns a sample it does not retain, so a pipeline that annotates samples in place
  cannot corrupt the stream.
- The whole codec is two pure functions — a `diff` and a `merge` that are exact inverses — in under
  300 lines. The only configuration is which arrays are keyed collections; a new schema field needs
  no change to it.

Unlike the protobuf artifact it does not require `proto` — JSON is self-describing, so the package
only needs the sample types.

[Full documentation →](/docs/codecs/json/)

## Fixed — what the old encoder / decoder pair got wrong

Corrected in the new package; the old packages are unchanged.

| Bug | Consequence |
|---|---|
| `false` could never be transmitted | The boolean encoder skipped any falsy value, so `active`, `nominated` and `powerEfficientDecoder` could go `true` but never come back |
| An empty string could never be transmitted | Same cause |
| Two entries of the same type in one sample's `clientEvents` lost the second one's `type` | All events shared one stateful field encoder |
| `ClientMetaData.timestamp` was dropped entirely on encode | — |
| `ClientMetaData.peerConnectionId` and `.trackId` ignored the uuid settings | Always written as utf8 |
| `codecId`, `mid` and `transportId` used a one-time-pass encoder | A value that legitimately changed mid-call was never re-sent. They are ordinary delta fields now |
| `attachments` was compared by reference | An object rebuilt with identical content was re-sent on every sample. It is compared by value now |
| A decoder that joined a stream late returned a half-built sample | It now raises `STREAM_DESYNC` |

## Removed — the encoder and decoder packages

{{< callout context="caution" title="samples-encoder and samples-decoder are deprecated" icon="alert-triangle" >}}
They stopped at `3.3.0` and **will not be regenerated, versioned or published again**; their sources
were removed from the repository along with the `encoder` and `decoder` generator artifacts.

`3.3.0` stays installable from npm — removing the sources is not an unpublish — and **the wire
format is unchanged**, so either package still interoperates with
`@observertc/samples-protobuf-codec` and the two ends of a stream can migrate independently.
{{< /callout >}}

See [the protobuf codec's migration table](/docs/codecs/protobuf/#migrating-from-samples-encoder--samples-decoder)
for the API mapping.

[← Back to version history](/docs/schema/versions/) · [3.3.0 →](../v3-3-0/)

