WebSocket Frame Decoder
Hard · Networking
WebSocket messages travel as frames (RFC 6455), and libraries like ws decode them from raw TCP bytes. Implement class FrameDecoder with push(bytes), where bytes is an array of numbers cut at any point. push returns the events completed by these bytes.
Frame layout:
- Byte 0: bit 7 is FIN (the last frame of a message), bits 6-4 are RSV1-3 (must be 0, there are no extensions here), and bits 3-0 are the opcode: 0 continuation, 1 text, 2 binary, 8 close, 9 ping, 10 pong.
- Byte 1: bit 7 is MASK and bits 6-0 are the payload length len. If len is 126, the real length is in the next 2 bytes (big-endian); if it is 127, in the next 8 bytes (big-endian; it always fits in a JavaScript number here). This decoder does not insist on the shortest length encoding.
- If MASK is set, 4 masking-key bytes follow, and payload byte i must be XORed with key[i % 4]. (A real server must reject unmasked frames from clients; this decoder accepts both, like a generic parser.)
- Then the payload.
Events:
- Text and binary messages can be split into fragments: a first frame with opcode 1 or 2 and FIN = 0, then continuation frames (opcode 0), the last one with FIN = 1. Emit one event for the whole message: { type: "text", data } with the joined bytes decoded as UTF-8, or { type: "binary", data } with the bytes as an array of numbers.
- Control frames may arrive between fragments. Ping and pong give { type: "ping" } or { type: "pong" } with data, the payload decoded as UTF-8. Close gives { type: "close", code, reason }: code is the first 2 payload bytes (big-endian) and reason the rest as UTF-8; an empty close payload gives code: null and reason: "". After a close event, ignore all further bytes.
A protocol error emits { type: "error", reason }, and all further bytes are ignored:
- "reserved bits set": any RSV bit is 1.
- "unknown opcode": opcodes 3-7 and 11-15.
- "invalid control frame": a control frame (opcode 8-10) with FIN = 0 or a payload longer than 125 bytes, or a close payload of exactly 1 byte.
- "unexpected continuation": a continuation frame while no fragmented message is in progress.
- "expected continuation": a new text or binary frame while a fragmented message is in progress.
- "invalid utf-8": a text message or close reason that is not valid UTF-8 (new TextDecoder("utf-8", { fatal: true }) throws for it).
The driver runDecoder(streams) is already written: for each stream (a list of chunks) it creates a new decoder, pushes the chunks in order and collects the events.