aboutsummaryrefslogtreecommitdiff
path: root/Hotline/Library/NetSocketNew.swift
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-10-27 10:11:57 -0700
committerDustin Mierau <dustin@mierau.me>2025-10-27 10:11:57 -0700
commit26c6e0ade508f319ed995a1e6c5a53bf7c610c34 (patch)
tree5f48b236adca496b0022db61661546f4a4d9e216 /Hotline/Library/NetSocketNew.swift
parentb7e909cccfaa8611522f488134f9f28f643b83eb (diff)
Further work on NetSocketNew. Added NetSocketDecodable/NetSocketEncodable so types can decode/encode themselves on/off the wire.
Diffstat (limited to 'Hotline/Library/NetSocketNew.swift')
-rw-r--r--Hotline/Library/NetSocketNew.swift547
1 files changed, 189 insertions, 358 deletions
diff --git a/Hotline/Library/NetSocketNew.swift b/Hotline/Library/NetSocketNew.swift
index 909c44d..753a201 100644
--- a/Hotline/Library/NetSocketNew.swift
+++ b/Hotline/Library/NetSocketNew.swift
@@ -1,6 +1,6 @@
// NetSocketNew.swift
-// Created by Dustin Mierau @mierau
+// Created by Dustin Mierau • @mierau
import Foundation
import Network
@@ -433,6 +433,28 @@ public actor NetSocketNew {
}
try await write(bytes)
}
+
+ /// Write a boolean as a single byte (0 or 1)
+ /// - Parameter value: Boolean value
+ public func write(_ value: Bool) async throws {
+ try await write(value ? UInt8(0x01) : UInt8(0x00))
+ }
+
+ /// Write a Float as its IEEE 754 bit pattern
+ /// - Parameters:
+ /// - value: Float value
+ /// - endian: Byte order (default: big-endian)
+ public func write(_ value: Float, endian: Endian = .big) async throws {
+ try await write(value.bitPattern, endian: endian)
+ }
+
+ /// Write a Double as its IEEE 754 bit pattern
+ /// - Parameters:
+ /// - value: Double value
+ /// - endian: Byte order (default: big-endian)
+ public func write(_ value: Double, endian: Endian = .big) async throws {
+ try await write(value.bitPattern, endian: endian)
+ }
/// Write a string to the socket, optionally length-prefixed
///
@@ -511,7 +533,7 @@ public actor NetSocketNew {
length = Int(v)
}
if length > cfg.maxFrameBytes { throw NetSocketError.framingExceeded(max: cfg.maxFrameBytes) }
- return try await readExactly(length)
+ return try await read(length)
}
/// Send an encodable value as a length-prefixed frame
@@ -568,7 +590,7 @@ public actor NetSocketNew {
/// - Throws: `NetSocketError` if insufficient data or connection closed
public func read<T: FixedWidthInteger>(_ type: T.Type = T.self, endian: Endian = .big) async throws -> T {
let size = MemoryLayout<T>.size
- let data = try await readExactly(size)
+ let data = try await read(size)
let value: T = data.withUnsafeBytes { raw in
raw.load(as: T.self)
}
@@ -585,8 +607,8 @@ public actor NetSocketNew {
/// - encoding: Text encoding (default: UTF-8)
/// - Returns: Decoded string
/// - Throws: `NetSocketError` if decoding fails or insufficient data
- public func readString(length: Int, encoding: String.Encoding = .utf8) async throws -> String {
- let data = try await readExactly(length)
+ public func read(_ length: Int, encoding: String.Encoding = .utf8) async throws -> String {
+ let data = try await read(length)
guard let s = String(data: data, encoding: encoding) else { throw NetSocketError.decodeFailed(NSError()) }
return s
}
@@ -599,12 +621,57 @@ public actor NetSocketNew {
/// - includeDelimiter: Whether to include delimiter in result (default: false)
/// - Returns: String read from stream (delimiter consumed but not included unless specified)
/// - Throws: `NetSocketError` if decoding fails, max bytes exceeded, or connection closed
- public func readString(until delimiter: Delimiter, maxBytes: Int? = nil, includeDelimiter: Bool = false) async throws -> String {
- let bytes = try await readUntil(delimiter: delimiter.data, maxBytes: maxBytes, includeDelimiter: includeDelimiter)
+ public func read(until delimiter: Delimiter, maxBytes: Int? = nil, includeDelimiter: Bool = false) async throws -> String {
+ let bytes = try await read(past: delimiter.data, maxBytes: maxBytes, includeDelimiter: includeDelimiter)
guard let s = String(data: bytes, encoding: .utf8) else { throw NetSocketError.decodeFailed(NSError()) }
return s
}
-
+
+ /// Read a pascal string (1-byte length prefix followed by string data)
+ ///
+ /// This method reads a single byte for the length, then reads that many bytes and attempts
+ /// to decode them as a string. It tries multiple encodings for compatibility with legacy
+ /// protocols like Hotline: UTF-8, Shift-JIS, Windows-1251, and falls back to MacRoman.
+ ///
+ /// - Returns: The decoded string, or nil if length is 0
+ /// - Throws: `NetSocketError` if reading fails or no encoding succeeds
+ public func readPascalString() async throws -> String? {
+ let length = try await read(UInt8.self)
+ guard length > 0 else { return nil }
+
+ let data = try await read(Int(length))
+
+ // Try auto-detection with common encodings
+ let allowedEncodings = [
+ String.Encoding.utf8.rawValue,
+ String.Encoding.shiftJIS.rawValue,
+ String.Encoding.unicode.rawValue,
+ String.Encoding.windowsCP1251.rawValue
+ ]
+
+ var decodedString: NSString?
+ let detected = NSString.stringEncoding(
+ for: data,
+ encodingOptions: [.allowLossyKey: false],
+ convertedString: &decodedString,
+ usedLossyConversion: nil
+ )
+
+ if allowedEncodings.contains(detected), let str = decodedString as? String {
+ return str
+ }
+
+ // Fallback to MacRoman for classic Mac compatibility
+ guard let str = String(data: data, encoding: .macOSRoman) else {
+ throw NetSocketError.decodeFailed(NSError(
+ domain: "NetSocketNew",
+ code: -1,
+ userInfo: [NSLocalizedDescriptionKey: "Failed to decode pascal string with any known encoding"]
+ ))
+ }
+ return str
+ }
+
/// Read data until a delimiter is found
///
/// Searches the buffer for the delimiter pattern and returns all data up to (and optionally including)
@@ -616,12 +683,12 @@ public actor NetSocketNew {
/// - includeDelimiter: Whether to include delimiter in result (default: false)
/// - Returns: Data read from stream
/// - Throws: `NetSocketError.framingExceeded` if max bytes exceeded, or connection errors
- public func readUntil(delimiter: Data, maxBytes: Int? = nil, includeDelimiter: Bool = false) async throws -> Data {
+ public func read(past delimiter: Data, maxBytes: Int? = nil, includeDelimiter: Bool = false) async throws -> Data {
while true {
try Task.checkCancellation()
if let r = search(delimiter: delimiter) {
let consumeLen = r.upperBound - head
- let data = try await readExactly(consumeLen)
+ let data = try await read(consumeLen)
return includeDelimiter ? data : data.dropLast(delimiter.count)
}
if let maxBytes, availableBytes >= maxBytes {
@@ -640,7 +707,7 @@ public actor NetSocketNew {
/// - Parameter count: Number of bytes to read
/// - Returns: Exactly `count` bytes
/// - Throws: `NetSocketError.insufficientData` if connection closes before enough data arrives
- public func readExactly(_ count: Int) async throws -> Data {
+ public func read(_ count: Int) async throws -> Data {
try await ensureReadable(count)
let start = head
let end = head + count
@@ -659,7 +726,7 @@ public actor NetSocketNew {
}
/// Skip until delimiter is found (discards delimiter too)
- public func skipUntil(delimiter: Data) async throws {
+ public func skip(past delimiter: Data) async throws {
while true {
try Task.checkCancellation()
if let r = search(delimiter: delimiter) {
@@ -887,7 +954,7 @@ public extension NetSocketNew {
while remaining > 0 {
try Task.checkCancellation()
let n = Int(min(Int64(chunkSize), remaining))
- let chunk = try await readExactly(n) // reuses your internal buffer, bounded by n
+ let chunk = try await read(n) // reuses your internal buffer, bounded by n
fh.write(chunk)
remaining -= Int64(n)
written += Int64(n)
@@ -969,7 +1036,7 @@ public extension NetSocketNew {
while remaining > 0 {
try Task.checkCancellation()
let n = Int(min(Int64(chunkSize), remaining))
- let chunk = try await readExactly(n)
+ let chunk = try await read(n)
fh.write(chunk)
remaining -= Int64(n)
written += Int64(n)
@@ -1025,379 +1092,143 @@ fileprivate extension NetSocketNew {
}
}
-/// Errors that can occur during binary encoding/decoding
-public enum BinaryError: Error, CustomStringConvertible, Sendable {
- /// Attempted to read past end of data
- case outOfBounds(need: Int, have: Int)
- /// String encoding/decoding failed
- case badStringEncoding
- /// Invalid value encountered
- case invalidValue(String)
-
- public var description: String {
- switch self {
- case .outOfBounds(let need, let have): return "Out of bounds: need \(need), have \(have)"
- case .badStringEncoding: return "String decoding failed"
- case .invalidValue(let s): return "Invalid value: \(s)"
- }
- }
-}
-
-/// Protocol for types that can encode themselves to binary format
-public protocol BinaryEncodable: Sendable {
- /// Encode this value to a ByteWriter
- /// - Parameters:
- /// - w: Writer to encode to
- /// - endian: Byte order for multi-byte values
- func encode(to w: inout ByteWriter, endian: Endian)
-}
-
-/// Protocol for types that can decode themselves from binary format
-public protocol BinaryDecodable: Sendable {
- /// Decode a value from a ByteReader
- /// - Parameters:
- /// - r: Reader to decode from
- /// - endian: Byte order for multi-byte values
- init(from r: inout ByteReader, endian: Endian) throws
-}
+// MARK: - Stream-based Encoding/Decoding
-/// Convenience typealias for types that are both encodable and decodable
-public typealias BinaryCodable = BinaryEncodable & BinaryDecodable
-
-extension UInt8: BinaryCodable {
- public func encode(to w: inout ByteWriter, endian: Endian) {
- w.write(self, endian: endian)
- }
- public init(from r: inout ByteReader, endian: Endian) throws {
- self = try r.read(UInt8.self, endian: endian)
- }
-}
-
-extension UInt16: BinaryCodable {
- public func encode(to w: inout ByteWriter, endian: Endian) {
- w.write(self, endian: endian)
- }
- public init(from r: inout ByteReader, endian: Endian) throws {
- self = try r.read(UInt16.self, endian: endian)
- }
-}
-
-extension UInt32: BinaryCodable {
- public func encode(to w: inout ByteWriter, endian: Endian) {
- w.write(self, endian: endian)
- }
- public init(from r: inout ByteReader, endian: Endian) throws {
- self = try r.read(UInt32.self, endian: endian)
- }
-}
-
-/// A writer for building binary data structures
+/// Protocol for types that can encode themselves to binary data
///
-/// ByteWriter provides methods to serialize integers, floats, strings, and arrays into binary format.
-/// All multi-byte values are written in the specified endianness.
+/// Types conforming to `NetSocketEncodable` produce binary data that can be sent over
+/// a socket. Unlike writing field-by-field to the socket, encodable types build complete
+/// binary messages that are sent in a single write operation for efficiency.
///
/// Example:
/// ```swift
-/// var w = ByteWriter()
-/// w.write(UInt16(100), endian: .big)
-/// try w.write("Hello", prefix: .u16(.big))
-/// let data = w.finalize()
+/// struct MyMessage: NetSocketEncodable {
+/// let id: UInt32
+/// let name: String
+///
+/// func encode(endian: Endian) throws -> Data {
+/// var data = Data()
+/// // Encode fields to data...
+/// return data
+/// }
+/// }
+///
+/// try await socket.send(message)
/// ```
-public struct ByteWriter: Sendable {
- /// The accumulated binary data
- public private(set) var data = Data()
-
- public init() {}
-
- /// Write a fixed-width integer
- /// - Parameters:
- /// - v: Integer value
- /// - endian: Byte order (default: big-endian)
- public mutating func write<T: FixedWidthInteger>(_ v: T, endian: Endian = .big) {
- var x = v
- switch endian {
- case .big: x = T(bigEndian: v)
- case .little: x = T(littleEndian: v)
- }
- withUnsafeBytes(of: x) { data.append(contentsOf: $0) }
- }
-
- /// Write a boolean as a single byte (0 or 1)
- /// - Parameter value: Boolean value
- public mutating func write(_ value: Bool) {
- data.append(value ? 1 : 0)
- }
-
- /// Write a Float as its IEEE 754 bit pattern
- /// - Parameters:
- /// - value: Float value
- /// - endian: Byte order (default: big-endian)
- public mutating func write(_ value: Float, endian: Endian = .big) {
- write(value.bitPattern, endian: endian)
- }
-
- /// Write a Double as its IEEE 754 bit pattern
- /// - Parameters:
- /// - value: Double value
- /// - endian: Byte order (default: big-endian)
- public mutating func write(_ value: Double, endian: Endian = .big) {
- write(value.bitPattern, endian: endian)
- }
-
- /// Write raw bytes, optionally with a length prefix
- /// - Parameters:
- /// - bytes: Data to write
- /// - prefix: Optional length prefix
- /// - endian: Byte order for length prefix (ignored if prefix is nil)
- /// - Throws: `BinaryError.invalidValue` if data size exceeds prefix capacity
- public mutating func write(_ bytes: Data, prefix: LengthPrefix? = nil, endian: Endian = .big) throws {
- if let prefix {
- // Validate size fits in prefix type
- switch prefix {
- case .u8 where bytes.count > Int(UInt8.max):
- throw BinaryError.invalidValue("Data size \(bytes.count) exceeds u8 max (255)")
- case .u16 where bytes.count > Int(UInt16.max):
- throw BinaryError.invalidValue("Data size \(bytes.count) exceeds u16 max (65535)")
- case .u32 where bytes.count > Int(UInt32.max):
- throw BinaryError.invalidValue("Data size \(bytes.count) exceeds u32 max")
- default:
- break
- }
-
- switch prefix {
- case .u8: data.append(UInt8(truncatingIfNeeded: bytes.count))
- case .u16(let e): write(UInt16(bytes.count), endian: e)
- case .u32(let e): write(UInt32(bytes.count), endian: e)
- case .u64(let e): write(UInt64(bytes.count), endian: e)
- }
- }
- data.append(bytes)
- }
-
- /// Write a length-prefixed string
- /// - Parameters:
- /// - string: String to write
- /// - prefix: Length prefix type (default: u16 big-endian)
- /// - encoding: Text encoding (default: UTF-8)
- /// - Throws: `BinaryError.badStringEncoding` if encoding fails, or size validation errors
- public mutating func write(_ string: String, prefix: LengthPrefix = .u16(), encoding: String.Encoding = .utf8) throws {
- guard let d = string.data(using: encoding) else {
- throw BinaryError.badStringEncoding
- }
- try write(d, prefix: prefix)
- }
-
- /// Write an array of encodable elements with element count prefix
+public protocol NetSocketEncodable: Sendable {
+ /// Encode this value to binary data
///
- /// The prefix contains the **number of elements**, not the byte size. Each element is
- /// encoded by calling its `encode(to:endian:)` method.
+ /// Implementations should build a complete binary message and return it as Data.
+ /// The data will be sent to the socket in a single write operation.
///
- /// - Parameters:
- /// - array: Array of elements to write
- /// - prefix: Length prefix for element count (default: u32 big-endian)
- /// - endian: Byte order for multi-byte values within elements
- /// - Throws: `BinaryError.invalidValue` if array count exceeds prefix capacity
- public mutating func write<Element: BinaryEncodable>(_ array: [Element], prefix: LengthPrefix = .u32(), endian: Endian = .big) throws {
- // Validate array count fits in prefix type
- switch prefix {
- case .u8 where array.count > Int(UInt8.max):
- throw BinaryError.invalidValue("Array count \(array.count) exceeds u8 max (255)")
- case .u16 where array.count > Int(UInt16.max):
- throw BinaryError.invalidValue("Array count \(array.count) exceeds u16 max (65535)")
- case .u32 where array.count > Int(UInt32.max):
- throw BinaryError.invalidValue("Array count \(array.count) exceeds u32 max")
- default:
- break
- }
-
- // Number of elements, not byte length:
- switch prefix {
- case .u8: data.append(UInt8(array.count))
- case .u16(let e): write(UInt16(array.count), endian: e)
- case .u32(let e): write(UInt32(array.count), endian: e)
- case .u64(let e): write(UInt64(array.count), endian: e)
- }
- for el in array { el.encode(to: &self, endian: endian) }
- }
-
- /// Get the final binary data
- /// - Returns: All accumulated binary data
- public func finalize() -> Data { data }
+ /// - Parameter endian: Byte order for multi-byte values
+ /// - Returns: Encoded binary data ready to send
+ /// - Throws: Encoding errors
+ func encode(endian: Endian) throws -> Data
}
-/// A reader for parsing binary data structures
+/// Protocol for types that can decode themselves directly from a socket stream
///
-/// ByteReader provides methods to deserialize integers, floats, strings, and arrays from binary format.
-/// The reader maintains an offset that automatically advances as data is read.
+/// Types conforming to `NetSocketDecodable` read field-by-field directly from the socket
+/// using async reads. This enables true streaming without buffering entire messages.
+///
+/// **Important**: If decoding throws after consuming some bytes (e.g., validation fails),
+/// the socket will be left with those bytes consumed. In practice, this usually means the
+/// connection should be closed. For most protocols this is acceptable since decode errors
+/// indicate corrupt data or protocol violations.
///
/// Example:
/// ```swift
-/// var r = ByteReader(data: binaryData)
-/// let value = try r.read(UInt16.self, endian: .big)
-/// let text = try r.readString(prefix: .u16(.big))
+/// struct MyMessage: NetSocketDecodable {
+/// let id: UInt32
+/// let name: String
+///
+/// init(from socket: NetSocketNew, endian: Endian) async throws {
+/// self.id = try await socket.read(UInt32.self, endian: endian)
+/// let nameLen = try await socket.read(UInt16.self, endian: endian)
+/// let nameData = try await socket.readExactly(Int(nameLen))
+/// guard let name = String(data: nameData, encoding: .utf8) else {
+/// throw NetSocketError.decodeFailed(NSError())
+/// }
+/// self.name = name
+/// }
+/// }
+///
+/// let message = try await socket.receive(MyMessage.self)
/// ```
-public struct ByteReader: Sendable {
- /// The binary data being read
- public let data: Data
- /// Current read position
- public private(set) var offset: Int = 0
-
- public init(data: Data) { self.data = data }
-
- @inline(__always)
- private mutating func take(_ count: Int) throws -> UnsafeRawBufferPointer {
- guard offset + count <= data.count else { throw BinaryError.outOfBounds(need: count, have: data.count - offset) }
- let start = data.startIndex + offset
- offset += count
- return data[start..<(start + count)].withUnsafeBytes { $0 }
- }
-
- /// Read a fixed-width integer
- /// - Parameters:
- /// - type: Integer type to read
- /// - endian: Byte order (default: big-endian)
- /// - Returns: Decoded integer value
- /// - Throws: `BinaryError.outOfBounds` if insufficient data
- public mutating func read<T: FixedWidthInteger>(_ type: T.Type = T.self, endian: Endian = .big) throws -> T {
- let size = MemoryLayout<T>.size
- let raw = try take(size)
- let v = raw.load(as: T.self)
- switch endian {
- case .big: return T(bigEndian: v)
- case .little: return T(littleEndian: v)
- }
- }
-
- /// Read a boolean (0 = false, non-zero = true)
- /// - Returns: Boolean value
- /// - Throws: `BinaryError.outOfBounds` if insufficient data
- public mutating func readBool() throws -> Bool {
- let b: UInt8 = try read()
- return b != 0
- }
-
- /// Read a Float from its IEEE 754 bit pattern
- /// - Parameters:
- /// - type: Float type (for type inference)
- /// - endian: Byte order (default: big-endian)
- /// - Returns: Float value
- /// - Throws: `BinaryError.outOfBounds` if insufficient data
- public mutating func read(_ type: Float.Type = Float.self, endian: Endian = .big) throws -> Float {
- let bits: UInt32 = try read(endian: endian)
- return Float(bitPattern: bits)
- }
-
- /// Read a Double from its IEEE 754 bit pattern
- /// - Parameters:
- /// - type: Double type (for type inference)
- /// - endian: Byte order (default: big-endian)
- /// - Returns: Double value
- /// - Throws: `BinaryError.outOfBounds` if insufficient data
- public mutating func read(_ type: Double.Type = Double.self, endian: Endian = .big) throws -> Double {
- let bits: UInt64 = try read(endian: endian)
- return Double(bitPattern: bits)
- }
-
- /// Read a fixed number of raw bytes
- /// - Parameter count: Number of bytes to read
- /// - Returns: Data containing exactly `count` bytes
- /// - Throws: `BinaryError.outOfBounds` if insufficient data
- public mutating func readData(count: Int) throws -> Data {
- let raw = try take(count)
- return Data(raw)
- }
-
- /// Read length-prefixed raw bytes
- /// - Parameters:
- /// - prefix: Length prefix type
- /// - endian: Byte order for length prefix
- /// - Returns: Data of the specified length
- /// - Throws: `BinaryError` if prefix value is invalid or insufficient data
- public mutating func readData(prefix: LengthPrefix, endian: Endian = .big) throws -> Data {
- let len: Int
- switch prefix {
- case .u8: len = Int(try read(UInt8.self))
- case .u16(let e): len = Int(try read(UInt16.self, endian: e))
- case .u32(let e): len = Int(try read(UInt32.self, endian: e))
- case .u64(let e):
- let v: UInt64 = try read(UInt64.self, endian: e)
- if v > Int.max { throw BinaryError.invalidValue("u64 length too large") }
- len = Int(v)
- }
- return try readData(count: len)
- }
-
- /// Read a length-prefixed string
- /// - Parameters:
- /// - prefix: Length prefix type (default: u16 big-endian)
- /// - encoding: Text encoding (default: UTF-8)
- /// - Returns: Decoded string
- /// - Throws: `BinaryError` if decoding fails or insufficient data
- public mutating func readString(prefix: LengthPrefix = .u16(),
- encoding: String.Encoding = .utf8) throws -> String {
- let d = try readData(prefix: prefix)
- guard let s = String(data: d, encoding: encoding) else { throw BinaryError.badStringEncoding }
- return s
- }
-
- /// Read an array of decodable elements
+public protocol NetSocketDecodable: Sendable {
+ /// Decode a value by reading directly from the socket stream
+ ///
+ /// This initializer should read all necessary fields from the socket using
+ /// methods like `read(_:endian:)`, `readExactly(_:)`, `readString(length:)`, etc.
///
- /// The prefix contains the **number of elements**, not the byte size.
+ /// The socket handles waiting for data to arrive, so you can read field by field
+ /// without worrying about buffering.
///
/// - Parameters:
- /// - type: Element type to decode
- /// - prefix: Length prefix for element count (default: u32 big-endian)
- /// - endian: Byte order for multi-byte values within elements
- /// - Returns: Array of decoded elements
- /// - Throws: `BinaryError` if count is invalid, decoding fails, or insufficient data
- public mutating func readArray<Element: BinaryDecodable>(_ type: Element.Type,
- prefix: LengthPrefix = .u32(),
- endian: Endian = .big) throws -> [Element] {
- let count: Int
- switch prefix {
- case .u8: count = Int(try read(UInt8.self))
- case .u16(let e): count = Int(try read(UInt16.self, endian: e))
- case .u32(let e): count = Int(try read(UInt32.self, endian: e))
- case .u64(let e):
- let v: UInt64 = try read(UInt64.self, endian: e)
- if v > Int.max { throw BinaryError.invalidValue("array count too large") }
- count = Int(v)
- }
- var out: [Element] = []
- out.reserveCapacity(count)
- for _ in 0..<count {
- out.append(try Element(from: &self, endian: endian))
- }
- return out
- }
-
- /// Whether the reader has reached the end of the data
- public var isAtEnd: Bool { offset >= data.count }
+ /// - socket: Socket to read from
+ /// - endian: Byte order for multi-byte values
+ /// - Throws: Network errors, insufficient data, or custom decoding errors
+ init(from socket: NetSocketNew, endian: Endian) async throws
}
public extension NetSocketNew {
- /// Send a binary-encodable value as a length-prefixed frame
+ /// Send an encodable value to the socket
+ ///
+ /// The type encodes itself to binary data, which is then sent in a single write operation.
+ ///
+ /// Example:
+ /// ```swift
+ /// struct MyMessage: NetSocketEncodable {
+ /// let id: UInt32
+ /// let name: String
+ ///
+ /// func encode(endian: Endian) throws -> Data {
+ /// var data = Data()
+ /// // Build binary message...
+ /// return data
+ /// }
+ /// }
+ ///
+ /// try await socket.send(message)
+ /// ```
+ ///
/// - Parameters:
- /// - value: Value conforming to BinaryEncodable
+ /// - value: Value conforming to NetSocketEncodable
/// - endian: Byte order (default: big-endian)
- /// - prefix: Length prefix type (default: u32 big-endian)
/// - Throws: Encoding or network errors
- func sendBinary<T: BinaryEncodable>(_ value: T, endian: Endian = .big, prefix: LengthPrefix = .u32()) async throws {
- var w = ByteWriter()
- value.encode(to: &w, endian: endian)
- try await sendFrame(w.finalize(), prefix: prefix)
+ func send<T: NetSocketEncodable>(_ value: T, endian: Endian = .big) async throws {
+ let data = try value.encode(endian: endian)
+ try await write(data)
}
- /// Receive and decode a binary value from a length-prefixed frame
+ /// Receive and decode a value directly from the socket stream (no length prefix)
+ ///
+ /// The type reads field-by-field from the socket as needed, enabling true streaming
+ /// without buffering entire messages. Useful for protocols where message size isn't
+ /// known upfront or for progressive decoding.
+ ///
+ /// Example:
+ /// ```swift
+ /// struct ServerEntry: NetSocketDecodable {
+ /// let id: UInt32
+ /// let name: String
+ ///
+ /// init(from socket: NetSocketNew, endian: Endian) async throws {
+ /// self.id = try await socket.read(UInt32.self, endian: endian)
+ /// // Read variable-length string...
+ /// }
+ /// }
+ ///
+ /// let entry = try await socket.receive(ServerEntry.self)
+ /// ```
+ ///
/// - Parameters:
- /// - type: Type conforming to BinaryDecodable
+ /// - type: Type conforming to NetSocketDecodable
/// - endian: Byte order (default: big-endian)
- /// - prefix: Length prefix type (default: u32 big-endian)
/// - Returns: Decoded value
/// - Throws: Decoding or network errors
- func receiveBinary<T: BinaryDecodable>(_ type: T.Type, endian: Endian = .big, prefix: LengthPrefix = .u32()) async throws -> T {
- let data = try await receiveFrame(prefix: prefix)
- var r = ByteReader(data: data)
- return try T(from: &r, endian: endian)
+ func receive<T: NetSocketDecodable>(_ type: T.Type, endian: Endian = .big) async throws -> T {
+ return try await T(from: self, endian: endian)
}
}