Barazo AppView backend
barazo.forum
1import { decodeFirst } from 'cborg'
2
3/**
4 * Header of an AT Protocol XRPC event stream frame.
5 *
6 * @see https://atproto.com/specs/event-stream
7 */
8export interface FrameHeader {
9 /** Operation type: 1 = regular message, -1 = error */
10 op: number
11 /** Lexicon sub-type in short form (e.g. '#labels'), present when op = 1 */
12 t?: string
13}
14
15/**
16 * Decoded AT Protocol event stream frame (header + body).
17 */
18export interface DecodedFrame {
19 header: FrameHeader
20 body: Record<string, unknown>
21}
22
23/**
24 * Decode an AT Protocol XRPC event stream binary frame.
25 *
26 * Each WebSocket frame contains two concatenated CBOR objects:
27 * 1. A header with `op` (operation type) and optional `t` (message type)
28 * 2. The message body
29 *
30 * @param data - Raw binary data (Uint8Array, ArrayBuffer, or Buffer)
31 * @returns The decoded header and body
32 * @throws If the data cannot be decoded as two consecutive CBOR objects
33 *
34 * @see https://atproto.com/specs/event-stream
35 */
36export function decodeEventStreamFrame(data: Uint8Array | ArrayBuffer | Buffer): DecodedFrame {
37 const bytes = data instanceof Uint8Array ? data : new Uint8Array(data)
38
39 if (bytes.length === 0) {
40 throw new Error('Empty event stream frame')
41 }
42
43 const [header, remainder] = decodeFirst(bytes) as [FrameHeader, Uint8Array]
44
45 if (remainder.length === 0) {
46 throw new Error('Truncated event stream frame: missing body after header')
47 }
48
49 const [body] = decodeFirst(remainder) as [Record<string, unknown>, Uint8Array]
50
51 return { header, body }
52}