1
const FRAME_HEADER_LENGTH = 4;2
const MAX_UINT32 = 0xffff_ffff;3
const PAYLOAD_BLOCK_SIZE = 64 * 1024;5
/** Default upper bound for one framed CBOR payload. */6
export const DEFAULT_MAX_FRAME_LENGTH = 16 * 1024 * 1024;8
export interface FrameDecoderOptions {9
maxFrameLength?: number;10
}12
export class FrameError extends Error {13
constructor(message: string) {14
super(message);15
this.name = "FrameError";16
}17
}19
function resolveMaxFrameLength(options: FrameDecoderOptions | undefined): number {20
const value = options?.maxFrameLength ?? DEFAULT_MAX_FRAME_LENGTH;21
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_UINT32) {22
throw new RangeError(`maxFrameLength must be an integer between 0 and ${MAX_UINT32}`);23
}24
return value;25
}27
/** Prefixes a payload with its unsigned 32-bit big-endian byte length. */28
export function encodeFrame(payload: Uint8Array): Uint8Array {29
if (!(payload instanceof Uint8Array)) throw new TypeError("Frame payload must be a Uint8Array");30
if (payload.byteLength > MAX_UINT32) throw new RangeError("Frame payload exceeds the unsigned 32-bit length limit");31
const frame = new Uint8Array(FRAME_HEADER_LENGTH + payload.byteLength);32
const length = payload.byteLength;33
frame[0] = length >>> 24;34
frame[1] = length >>> 16;35
frame[2] = length >>> 8;36
frame[3] = length;37
frame.set(payload, FRAME_HEADER_LENGTH);38
return frame;39
}41
type DecoderState = "open" | "ended" | "failed";43
/** Incrementally splits arbitrary byte chunks into length-prefixed payloads. */44
export class FrameDecoder {45
private readonly header = new Uint8Array(FRAME_HEADER_LENGTH);46
private headerLength = 0;47
private readonly maxFrameLength: number;48
private payloadBlocks: Uint8Array[] = [];49
private currentPayloadBlock: Uint8Array | undefined;50
private currentPayloadBlockLength = 0;51
private expectedPayloadLength: number | undefined;52
private payloadLength = 0;53
private state: DecoderState = "open";55
constructor(options?: FrameDecoderOptions) {56
this.maxFrameLength = resolveMaxFrameLength(options);57
}59
push(chunk: Uint8Array): Uint8Array[] {60
if (this.state === "ended") throw new FrameError("Frame decoder has ended");61
if (this.state === "failed") throw new FrameError("Frame decoder has failed");62
if (!(chunk instanceof Uint8Array)) throw new TypeError("Frame chunk must be a Uint8Array");64
const frames: Uint8Array[] = [];65
let chunkOffset = 0;66
while (chunkOffset < chunk.byteLength) {67
if (this.expectedPayloadLength === undefined) {68
const headerBytes = Math.min(FRAME_HEADER_LENGTH - this.headerLength, chunk.byteLength - chunkOffset);69
this.header.set(chunk.subarray(chunkOffset, chunkOffset + headerBytes), this.headerLength);70
this.headerLength += headerBytes;71
chunkOffset += headerBytes;72
if (this.headerLength < FRAME_HEADER_LENGTH) continue;74
const frameLength =75
this.header[0]! * 0x1_000_000 + this.header[1]! * 0x1_0000 + this.header[2]! * 0x100 + this.header[3]!;76
this.headerLength = 0;77
if (frameLength > this.maxFrameLength) {78
this.fail(`Frame length ${frameLength} exceeds configured limit of ${this.maxFrameLength}`);79
}80
if (frameLength === 0) {81
frames.push(new Uint8Array());82
continue;83
}84
this.expectedPayloadLength = frameLength;85
this.payloadBlocks = [];86
this.currentPayloadBlock = undefined;87
this.currentPayloadBlockLength = 0;88
this.payloadLength = 0;89
}91
const expectedPayloadLength = this.expectedPayloadLength;92
if (expectedPayloadLength === undefined) continue;93
while (chunkOffset < chunk.byteLength && this.payloadLength < expectedPayloadLength) {94
let block = this.currentPayloadBlock;95
if (!block || this.currentPayloadBlockLength === block.byteLength) {96
block = new Uint8Array(Math.min(PAYLOAD_BLOCK_SIZE, expectedPayloadLength - this.payloadLength));97
this.payloadBlocks.push(block);98
this.currentPayloadBlock = block;99
this.currentPayloadBlockLength = 0;100
}101
const payloadBytes = Math.min(102
block.byteLength - this.currentPayloadBlockLength,103
chunk.byteLength - chunkOffset,104
);105
block.set(chunk.subarray(chunkOffset, chunkOffset + payloadBytes), this.currentPayloadBlockLength);106
this.currentPayloadBlockLength += payloadBytes;107
this.payloadLength += payloadBytes;108
chunkOffset += payloadBytes;109
}110
if (this.payloadLength === expectedPayloadLength) {111
if (this.payloadBlocks.length === 1) {112
frames.push(this.payloadBlocks[0]!);113
} else {114
const payload = new Uint8Array(expectedPayloadLength);115
let offset = 0;116
for (const payloadBlock of this.payloadBlocks) {117
payload.set(payloadBlock, offset);118
offset += payloadBlock.byteLength;119
}120
frames.push(payload);121
}122
this.payloadBlocks = [];123
this.currentPayloadBlock = undefined;124
this.currentPayloadBlockLength = 0;125
this.expectedPayloadLength = undefined;126
this.payloadLength = 0;127
}128
}129
return frames;130
}132
end(): void {133
if (this.state === "ended") throw new FrameError("Frame decoder has ended");134
if (this.state === "failed") throw new FrameError("Frame decoder has failed");135
if (this.headerLength !== 0 || this.expectedPayloadLength !== undefined) {136
this.fail("Truncated frame at end of stream");137
}138
this.state = "ended";139
}141
private fail(message: string): never {142
this.state = "failed";143
this.headerLength = 0;144
this.payloadBlocks = [];145
this.currentPayloadBlock = undefined;146
this.currentPayloadBlockLength = 0;147
this.expectedPayloadLength = undefined;148
this.payloadLength = 0;149
throw new FrameError(message);150
}151
}