""" Streaming protocol utilities for efficient frame transmission. Provides: - Frame differencing: Only send changed lines - Run-length encoding: Compress repeated lines - Binary encoding: Compact message format """ import json import zlib from dataclasses import dataclass from enum import IntEnum class MessageType(IntEnum): """Message types for streaming protocol.""" FULL_FRAME = 1 DIFF_FRAME = 2 STATE = 3 CLEAR = 4 PING = 5 PONG = 6 @dataclass class FrameDiff: """Represents a diff between two frames.""" width: int height: int changed_lines: list[tuple[int, str]] # (line_index, content) def compute_diff(old_buffer: list[str], new_buffer: list[str]) -> FrameDiff: """Compute differences between old and new buffer. Args: old_buffer: Previous frame buffer new_buffer: Current frame buffer Returns: FrameDiff with only changed lines """ height = len(new_buffer) changed_lines = [] for i, line in enumerate(new_buffer): if i >= len(old_buffer) or line != old_buffer[i]: changed_lines.append((i, line)) return FrameDiff( width=len(new_buffer[0]) if new_buffer else 0, height=height, changed_lines=changed_lines, ) def encode_rle(lines: list[tuple[int, str]]) -> list[tuple[int, str, int]]: """Run-length encode consecutive identical lines. Args: lines: List of (index, content) tuples (must be sorted by index) Returns: List of (start_index, content, run_length) tuples """ if not lines: return [] encoded = [] start_idx = lines[0][0] current_line = lines[0][1] current_rle = 1 for idx, line in lines[1:]: if line == current_line: current_rle += 1 else: encoded.append((start_idx, current_line, current_rle)) start_idx = idx current_line = line current_rle = 1 encoded.append((start_idx, current_line, current_rle)) return encoded def decode_rle(encoded: list[tuple[int, str, int]]) -> list[tuple[int, str]]: """Decode run-length encoded lines. Args: encoded: List of (start_index, content, run_length) tuples Returns: List of (index, content) tuples """ result = [] for start_idx, line, rle in encoded: for i in range(rle): result.append((start_idx + i, line)) return result def compress_frame(buffer: list[str], level: int = 6) -> bytes: """Compress a frame buffer using zlib. Args: buffer: Frame buffer (list of lines) level: Compression level (0-9) Returns: Compressed bytes """ content = "\n".join(buffer) return zlib.compress(content.encode("utf-8"), level) def decompress_frame(data: bytes, height: int) -> list[str]: """Decompress a frame buffer. Args: data: Compressed bytes height: Number of lines in original buffer Returns: Frame buffer (list of lines) """ content = zlib.decompress(data).decode("utf-8") lines = content.split("\n") if len(lines) > height: lines = lines[:height] while len(lines) < height: lines.append("") return lines def encode_binary_message( msg_type: MessageType, width: int, height: int, payload: bytes ) -> bytes: """Encode a binary message. Message format: - 1 byte: message type - 2 bytes: width (uint16) - 2 bytes: height (uint16) - 4 bytes: payload length (uint32) - N bytes: payload Args: msg_type: Message type width: Frame width height: Frame height payload: Message payload Returns: Encoded binary message """ import struct header = struct.pack("!BHHI", msg_type.value, width, height, len(payload)) return header + payload def decode_binary_message(data: bytes) -> tuple[MessageType, int, int, bytes]: """Decode a binary message. Args: data: Binary message data Returns: Tuple of (msg_type, width, height, payload) """ import struct msg_type_val, width, height, payload_len = struct.unpack("!BHHI", data[:9]) payload = data[9 : 9 + payload_len] return MessageType(msg_type_val), width, height, payload def encode_diff_message(diff: FrameDiff, use_rle: bool = True) -> bytes: """Encode a diff message for transmission. Args: diff: Frame diff use_rle: Whether to use run-length encoding Returns: Encoded diff payload """ if use_rle: encoded_lines = encode_rle(diff.changed_lines) data = [[idx, line, rle] for idx, line, rle in encoded_lines] else: data = [[idx, line] for idx, line in diff.changed_lines] payload = json.dumps(data).encode("utf-8") return payload def decode_diff_message(payload: bytes, use_rle: bool = True) -> list[tuple[int, str]]: """Decode a diff message. Args: payload: Encoded diff payload use_rle: Whether run-length encoding was used Returns: List of (line_index, content) tuples """ data = json.loads(payload.decode("utf-8")) if use_rle: return decode_rle([(idx, line, rle) for idx, line, rle in data]) else: return [(idx, line) for idx, line in data] def should_use_diff( old_buffer: list[str], new_buffer: list[str], threshold: float = 0.3 ) -> bool: """Determine if diff or full frame is more efficient. Args: old_buffer: Previous frame new_buffer: Current frame threshold: Max changed ratio to use diff (0.0-1.0) Returns: True if diff is more efficient """ if not old_buffer or not new_buffer: return False diff = compute_diff(old_buffer, new_buffer) total_lines = len(new_buffer) changed_ratio = len(diff.changed_lines) / total_lines if total_lines > 0 else 1.0 return changed_ratio <= threshold def apply_diff(old_buffer: list[str], diff: FrameDiff) -> list[str]: """Apply a diff to an old buffer to get the new buffer. Args: old_buffer: Previous frame buffer diff: Frame diff to apply Returns: New frame buffer """ new_buffer = list(old_buffer) for line_idx, content in diff.changed_lines: if line_idx < len(new_buffer): new_buffer[line_idx] = content else: while len(new_buffer) < line_idx: new_buffer.append("") new_buffer.append(content) while len(new_buffer) < diff.height: new_buffer.append("") return new_buffer[: diff.height]