Schema 3.5.0

Payload representation changes

Producers and consumers should move to the 3.5.0 generated schemas together, or use a reader that accepts both — observer-js does.

Changed — payloads are records

ClientEvent.payload, ClientIssue.payload, ClientMetaData.payload and ExtensionStat.payload changed from an optional string to an optional map of primitivesRecord<string, boolean | string | number> in the generated TypeScript (Avro: ["null", {"type": "map", "values": ["boolean", "string", "double"]}], keeping the null-first optional convention).

Producers no longer JSON.stringify every payload before buffering it, and consumers read payload fields directly instead of parsing a nested JSON document.

// before
{ "type": "MEDIA_TRACK_MUTED", "payload": "{\"trackId\":\"t-1\",\"kind\":\"audio\"}" }

// 3.5.0
{ "type": "MEDIA_TRACK_MUTED", "payload": { "trackId": "t-1", "kind": "audio" } }

Why — measured, not argued

Measured on Node 22 with realistic samples carrying 35–40 payloads:

Effect
Payload bytes on the wire~12% smaller — a pre-stringified payload gets every quote escaped when the whole sample serialises
Serialising the sample~18% faster end to end (0.37 ms → 0.30 ms per sample)
The events portion alone~54% faster (0.042 ms → 0.020 ms)
Server-side decode~3% saved — one parse instead of 1 + N

Each payload was previously walked twice: once by its own JSON.stringify at buffering time (30–40 calls per sample, now zero, along with their intermediate-string garbage) and once more character-by-character for escaping. Payload fields also become directly queryable without a second parse.

Codec impact

  • Protobuf: the payload still travels as a JSON string, because proto3 cannot express a union-valued map — the same convention attachments uses. The protobuf codec converts it through its JSON-fields mechanism, keyed on the field name, so all four payload fields are covered and protobuf field numbering is unchanged.
  • JSON: no change needed. Events, issues, meta items and extension stats are value lists written whole, and object values were already handled generically.

The TypeScript generator learned to render union-valued Avro maps for this.

Upgrading

// before
const payload = JSON.parse(event.payload ?? '{}');

// 3.5.0
const payload = event.payload ?? {};

A reader that must accept both generations does what the observer does: pass an object through untouched, parse a string.

3.7.0 widened this again

A payload value stayed a primitive in 3.5.0 and 3.6.0. 3.7.0 widened it to free-form JSON, so a payload may nest objects and arrays — again with no change to either wire format.

← Back to version history · 3.4.0 →