OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / hyper-svc / lib / frame.js

63 lines · javascript ref:
/**
 * Length-prefixed JSON framing.
 *
 * Wire format: [4 bytes BE length][JSON payload].
 *
 * One frame = one JSON object. Multiple frames can be pipelined on a single
 * stream — FrameDecoder handles incremental reads where a frame may arrive
 * across multiple data chunks.
 */

import b4a from 'b4a'

const MAX_FRAME = 16 * 1024 * 1024 // 16 MiB hard cap to avoid runaway buffers

export function encodeFrame (obj) {
  const json = JSON.stringify(obj)
  const payload = b4a.from(json, 'utf8')
  if (payload.length > MAX_FRAME) {
    throw new Error(`Frame too large: ${payload.length} > ${MAX_FRAME}`)
  }
  const frame = b4a.alloc(4 + payload.length)
  frame.writeUInt32BE(payload.length, 0)
  payload.copy(frame, 4)
  return frame
}

export class FrameDecoder {
  /**
   * @param {(obj: any) => void} onFrame - called once per complete frame
   */
  constructor (onFrame) {
    if (typeof onFrame !== 'function') throw new Error('onFrame is required')
    this._onFrame = onFrame
    this._buf = b4a.alloc(0)
  }

  /**
   * Feed a chunk into the decoder. Calls onFrame for each complete frame.
   * Throws if a frame exceeds MAX_FRAME or contains invalid JSON.
   */
  push (chunk) {
    this._buf = b4a.concat([this._buf, chunk])

    while (this._buf.length >= 4) {
      const len = this._buf.readUInt32BE(0)
      if (len > MAX_FRAME) {
        throw new Error(`Frame too large: ${len} > ${MAX_FRAME}`)
      }
      if (this._buf.length < 4 + len) break // wait for more bytes

      const payload = this._buf.subarray(4, 4 + len)
      this._buf = this._buf.subarray(4 + len)

      const json = b4a.toString(payload, 'utf8')
      const obj = JSON.parse(json) // throws on bad JSON
      this._onFrame(obj)
    }
  }

  /** Number of bytes currently buffered (waiting for the rest of a frame). */
  get pending () { return this._buf.length }
}