esc

Type to search...

Binary frames

Most apps send JSON over the wire. Some apps need binary: MessagePack or protobuf for compact payloads, audio chunks, image tiles, anything that benefits from skipping JSON parse cost. The manager treats the wire type as a generic parameter, so binary mode is fully type-safe and entirely opt-in.

ArrayBuffer round trip

Set binaryType: "arraybuffer" and parameterize the manager with the binary wire types. The library plumbs binaryType into the underlying WebSocket instance so incoming frames arrive as ArrayBuffer.

manager.ts
import { WebSocketManager } from "@luciodale/react-socket";

type TClientMsg = { type: "ping" } | { type: "frame"; bytes: number };
type TServerMsg = { type: "pong" } | { type: "frame"; bytes: number };

function encode(msg: TClientMsg): ArrayBuffer {
  const json = JSON.stringify(msg);
  const bytes = new TextEncoder().encode(json);
  const ab = new ArrayBuffer(bytes.byteLength);
  new Uint8Array(ab).set(bytes);
  return ab;
}

function decode(raw: ArrayBuffer): TServerMsg {
  return JSON.parse(new TextDecoder().decode(new Uint8Array(raw)));
}

const manager = new WebSocketManager<
  TClientMsg,
  TServerMsg,
  "type",
  ArrayBuffer,  // wire type sent
  ArrayBuffer   // wire type received
>({
  url: "wss://api.example.com/ws",
  binaryType: "arraybuffer",
  serialize: encode,
  deserialize: decode,
});

Blob mode

Blob mode works the same way, but synchronous deserialization is awkward because Blob → bytes is async. Most apps prefer arraybuffer unless you specifically want the browser to defer materializing the bytes.

manager.ts
// Blobs are easier when you receive larger frames you don't need to
// process synchronously. The browser keeps the data on disk-or-memory
// until you await blob.text() / blob.arrayBuffer().
const manager = new WebSocketManager<
  TClientMsg,
  TServerMsg,
  "type",
  Blob,
  Blob
>({
  url: getWsUrl(),
  binaryType: "blob",
  serialize: (msg) => new Blob([JSON.stringify(msg)]),
  deserialize: (raw) => {
    // For Blob deserialization you usually pre-fetch metadata or stream
    // the body separately. Sync deserialization is awkward; consider
    // arraybuffer mode unless you specifically need Blob semantics.
    throw new Error("implement async pre-decode in your bridge component");
  },
});

Mixed string + binary

A connection can carry both. Widen the wire generics to a union and branch in serialize / deserialize:

manager.ts
// String and binary on the same connection.
const manager = new WebSocketManager<
  TClientMsg,
  TServerMsg,
  "type",
  string | ArrayBuffer,  // serialize can return either
  string | ArrayBuffer   // deserialize must accept either
>({
  url: getWsUrl(),
  binaryType: "arraybuffer",
  serialize: (msg) =>
    msg.type === "blob" ? encode(msg) : JSON.stringify(msg),
  deserialize: (raw) =>
    typeof raw === "string"
      ? JSON.parse(raw)
      : decode(raw),
});

MessagePack example

A common reason to reach for binary is replacing JSON with MessagePack or protobuf. The encode/decode functions slot directly into serialize / deserialize:

manager.ts
import { encode, decode } from "@msgpack/msgpack";

const manager = new WebSocketManager<
  TClientMsg,
  TServerMsg,
  "type",
  Uint8Array,    // msgpack returns Uint8Array
  ArrayBuffer
>({
  url: getWsUrl(),
  binaryType: "arraybuffer",
  serialize: (msg) => encode(msg),
  deserialize: (raw) => decode(new Uint8Array(raw)) as TServerMsg,
});

Things that still work

  • Discriminator narrowing on useSocketEvent works on the deserialized object, not the raw bytes.
  • Subscriptions, ping/pong, in-flight ack tracking, optimistic updates — everything operates on TClientMsg / TServerMsg, not the wire type.
  • Debug events expose the binary raw field, so the inspector still shows that a frame happened (even if it cannot pretty-print the bytes).

Testing binary

MockTransport's sentMessages holds the raw wire payload. For binary, assert against the ArrayBuffer directly. For string payloads, parse with JSON.parse(transport.sentMessages.at(-1) as string); for binary, decode with your own helper before comparing.

Next steps

  • Configuration — full list of manager options including binaryType
  • Backpressure — batching high-frequency binary frames into a single render
  • Testing — asserting against binary sentMessages