///|
/// Errors raised by the newline-delimited transport boundary.
///
/// Framing deliberately does not parse JSON. Every non-empty line is emitted
/// as a UTF-8 frame so that transport noise cannot be silently discarded; the
/// JSON-RPC codec remains the next, explicit validation step.
pub(all) suberror FramingError {
InvalidMaxFrameSize
Closed
EmptyFrame
InvalidUtf8
FrameTooLarge(limit~ : Int, actual~ : Int)
UnexpectedEof
} derive(Eq, Debug)
///|
/// Immutable state for a newline-delimited decoder.
pub(all) struct FramingState {
pending : Bytes
max_frame_bytes : Int
closed : Bool
} derive(Eq, Debug)
///|
/// Frames emitted by one feed operation and the state for the next operation.
pub(all) struct FramingStep {
state : FramingState
frames : Array[String]
} derive(Eq, Debug)
///|
/// Create a decoder with a positive maximum payload size in bytes.
#warnings("-unused_value")
pub fn framing_state(max_frame_bytes~ : Int) -> FramingState raise FramingError {
if max_frame_bytes <= 0 {
raise InvalidMaxFrameSize
}
{ pending: b"", max_frame_bytes, closed: false }
}
///|
/// Feed an arbitrary byte chunk. A chunk may contain partial or multiple
/// frames. CRLF is accepted by removing the CR immediately before LF.
#warnings("-unused_value")
pub fn framing_feed(
state : FramingState,
chunk : Bytes,
) -> FramingStep raise FramingError {
if state.closed {
raise Closed
}
let combined = Bytes::add(state.pending, chunk)
let frames : Array[String] = []
let mut line_start = 0
for index, byte in combined.iter2() {
if byte == b'\n' {
let mut line_end = index
if index > line_start && combined[index - 1] == b'\r' {
line_end = index - 1
}
let line_length = line_end - line_start
if line_length == 0 {
raise EmptyFrame
}
if line_length > state.max_frame_bytes {
raise FrameTooLarge(limit=state.max_frame_bytes, actual=line_length)
}
let line = combined.view(start=line_start, end=line_end)
let text = @utf8.decode(line) catch { _ => raise InvalidUtf8 }
frames.push(text)
line_start = index + 1
}
}
let pending_length = combined.length() - line_start
if pending_length > state.max_frame_bytes {
raise FrameTooLarge(limit=state.max_frame_bytes, actual=pending_length)
}
let pending = combined
.view(start=line_start, end=combined.length())
.to_owned()
{ state: { ..state, pending, }, frames }
}
///|
/// Finish the stream. A partial line is never silently discarded.
#warnings("-unused_value")
pub fn framing_finish(state : FramingState) -> FramingStep raise FramingError {
if state.closed {
raise Closed
}
if !state.pending.is_empty() {
raise UnexpectedEof
}
{ state: { ..state, closed: true }, frames: [] }
}