diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml new file mode 100644 index 0000000..b76e5c5 --- /dev/null +++ b/.github/workflows/demo.yml @@ -0,0 +1,55 @@ +name: Demo + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + swift-demo: + name: Swift Demo (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Build Swift Demo + run: swift build --target SwiftDemo + + - name: Run Swift Demo (all demos) + run: | + printf 'a\n\n\n\n\n\nq\n' | swift run SwiftDemo + + objc-demo: + name: ObjC Demo (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Build ObjC Demo + run: | + clang -fobjc-arc -framework Foundation -framework Network \ + -I ObjC/NWAsyncSocketObjC/include \ + ObjC/NWAsyncSocketObjC/NWStreamBuffer.m \ + ObjC/NWAsyncSocketObjC/NWSSEParser.m \ + ObjC/NWAsyncSocketObjC/NWReadRequest.m \ + ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m \ + ObjC/ObjCDemo/main.m \ + -o ObjCDemo + + - name: Run ObjC Demo (all demos) + run: | + printf 'a\n\n\n\n\n\nq\n' | ./ObjCDemo + + swift-tests: + name: Swift Tests (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Run Swift tests + run: swift test diff --git a/Examples/SwiftDemo/main.swift b/Examples/SwiftDemo/main.swift new file mode 100644 index 0000000..61f7bdd --- /dev/null +++ b/Examples/SwiftDemo/main.swift @@ -0,0 +1,528 @@ +import Foundation +import NWAsyncSocket + +// ============================================================================ +// MARK: - NWAsyncSocket Swift Demo +// ============================================================================ +// +// This demo lets you interactively verify the core components of NWAsyncSocket: +// 1. StreamBuffer — sticky-packet / split-packet handling +// 2. SSEParser — Server-Sent Events incremental parsing +// 3. UTF-8 Safety — multi-byte character boundary detection +// 4. ReadRequest — read-request queue types +// 5. NWAsyncSocket — connection usage pattern (Network.framework only) +// +// Run: swift run SwiftDemo +// ============================================================================ + +// MARK: - Helpers + +/// Print a section header. +func printHeader(_ title: String) { + let line = String(repeating: "=", count: 60) + print("\n\(line)") + print(" \(title)") + print(line) +} + +/// Print a sub-section header. +func printSubHeader(_ title: String) { + print("\n--- \(title) ---") +} + +/// Pause and wait for the user to press Enter. +func waitForUser() { + print("\nPress Enter to continue...") + _ = readLine() +} + +// MARK: - 1. StreamBuffer Demo + +func demoStreamBuffer() { + printHeader("1. StreamBuffer — Sticky-Packet / Split-Packet Handling") + + let buffer = StreamBuffer() + + // ---- 1a. Sticky packet (粘包) ---- + printSubHeader("1a. Sticky Packet (粘包) — Multiple messages in one TCP segment") + + let stickyData = "Hello\r\nWorld\r\nFoo\r\n".data(using: .utf8)! + print("Appending combined data: \"Hello\\r\\nWorld\\r\\nFoo\\r\\n\"") + buffer.append(stickyData) + print("Buffer size: \(buffer.count) bytes") + + let delimiter = "\r\n".data(using: .utf8)! + var messageIndex = 1 + while let chunk = buffer.readData(toDelimiter: delimiter) { + let text = String(data: chunk, encoding: .utf8)! + print(" Message \(messageIndex): \"\(text.replacingOccurrences(of: "\r\n", with: "\\r\\n"))\"") + messageIndex += 1 + } + print("Buffer remaining: \(buffer.count) bytes (expected: 0)") + print("✅ Sticky packet correctly split into \(messageIndex - 1) messages") + + // ---- 1b. Split packet (拆包) ---- + printSubHeader("1b. Split Packet (拆包) — One message split across TCP segments") + + buffer.reset() + let part1 = "Hel".data(using: .utf8)! + let part2 = "lo World".data(using: .utf8)! + print("Appending part 1: \"Hel\" (\(part1.count) bytes)") + buffer.append(part1) + + let result1 = buffer.readData(toLength: 11) + print("Attempt to read 11 bytes: \(result1 == nil ? "nil (not enough data yet)" : "got data")") + + print("Appending part 2: \"lo World\" (\(part2.count) bytes)") + buffer.append(part2) + print("Buffer size: \(buffer.count) bytes") + + if let result2 = buffer.readData(toLength: 11) { + let text = String(data: result2, encoding: .utf8)! + print("Read 11 bytes: \"\(text)\"") + print("✅ Split packet correctly reassembled") + } + + // ---- 1c. Delimiter-based read ---- + printSubHeader("1c. Delimiter-Based Read") + + buffer.reset() + buffer.append("key1=value1&key2=value2&key3=value3".data(using: .utf8)!) + print("Buffer content: \"key1=value1&key2=value2&key3=value3\"") + + let ampersand = "&".data(using: .utf8)! + var pairs: [String] = [] + while let data = buffer.readData(toDelimiter: ampersand) { + pairs.append(String(data: data, encoding: .utf8)!) + } + // Read remaining data + let remaining = buffer.readAllData() + if !remaining.isEmpty { + pairs.append(String(data: remaining, encoding: .utf8)!) + } + print("Parsed pairs:") + for pair in pairs { + print(" \"\(pair)\"") + } + print("✅ Delimiter-based reading works correctly") + + // ---- 1d. readAllData ---- + printSubHeader("1d. Read All Data") + + buffer.reset() + buffer.append("Part A ".data(using: .utf8)!) + buffer.append("Part B ".data(using: .utf8)!) + buffer.append("Part C".data(using: .utf8)!) + let all = buffer.readAllData() + print("Read all: \"\(String(data: all, encoding: .utf8)!)\"") + print("Buffer empty after readAll: \(buffer.isEmpty)") + print("✅ readAllData works correctly") + + waitForUser() +} + +// MARK: - 2. SSEParser Demo + +func demoSSEParser() { + printHeader("2. SSEParser — Server-Sent Events Parsing") + + let parser = SSEParser() + + // ---- 2a. Single complete SSE event ---- + printSubHeader("2a. Single Complete SSE Event") + + let sseData1 = "event: chat\ndata: Hello from the server!\n\n".data(using: .utf8)! + print("Feed: \"event: chat\\ndata: Hello from the server!\\n\\n\"") + let events1 = parser.parse(sseData1) + for event in events1 { + print(" Parsed Event → type: \"\(event.event)\", data: \"\(event.data)\"") + } + print("✅ Single event parsed correctly") + + // ---- 2b. Multiple events in one chunk ---- + printSubHeader("2b. Multiple Events in One Chunk") + + parser.reset() + let sseData2 = "data: first message\n\ndata: second message\n\nevent: custom\ndata: third with type\n\n".data(using: .utf8)! + print("Feed: 3 events in a single chunk") + let events2 = parser.parse(sseData2) + print(" Parsed \(events2.count) events:") + for (i, event) in events2.enumerated() { + print(" [\(i+1)] type: \"\(event.event)\", data: \"\(event.data)\"") + } + print("✅ Multiple events parsed correctly") + + // ---- 2c. Split across chunks (LLM streaming simulation) ---- + printSubHeader("2c. Split Across Chunks — LLM Streaming Simulation") + + parser.reset() + let chunks = [ + "data: {\"tok", + "en\": \"Hel\"}\n", + "\ndata: {\"token\"", + ": \"lo\"}\n\ndata", + ": {\"token\": \" World\"}\n\n" + ] + print("Feeding \(chunks.count) partial chunks to simulate LLM streaming:") + var allEvents: [SSEEvent] = [] + for (i, chunk) in chunks.enumerated() { + let display = chunk.replacingOccurrences(of: "\n", with: "\\n") + let parsed = parser.parse(chunk) + allEvents.append(contentsOf: parsed) + print(" Chunk \(i+1): \"\(display)\" → \(parsed.count) event(s)") + } + print("\nTotal events parsed: \(allEvents.count)") + for (i, event) in allEvents.enumerated() { + print(" [\(i+1)] data: \"\(event.data)\"") + } + print("✅ Split SSE chunks reassembled correctly") + + // ---- 2d. Event with id and retry ---- + printSubHeader("2d. Event with ID and Retry Fields") + + parser.reset() + let sseData4 = "id: 42\nretry: 3000\nevent: update\ndata: payload here\n\n".data(using: .utf8)! + print("Feed: event with id=42, retry=3000, type=update") + let events4 = parser.parse(sseData4) + for event in events4 { + print(" type: \"\(event.event)\", data: \"\(event.data)\", id: \(event.id ?? "nil"), retry: \(event.retry.map(String.init) ?? "nil")") + } + print(" lastEventId: \(parser.lastEventId ?? "nil")") + print("✅ ID and retry fields parsed correctly") + + // ---- 2e. Comments ignored ---- + printSubHeader("2e. Comments Are Ignored") + + parser.reset() + let sseData5 = ": this is a comment\ndata: visible data\n\n".data(using: .utf8)! + print("Feed: \":this is a comment\\ndata: visible data\\n\\n\"") + let events5 = parser.parse(sseData5) + print(" Parsed \(events5.count) event(s)") + if let e = events5.first { + print(" data: \"\(e.data)\"") + } + print("✅ Comments correctly ignored") + + // ---- 2f. Multi-line data ---- + printSubHeader("2f. Multi-Line Data Field") + + parser.reset() + let sseData6 = "data: line one\ndata: line two\ndata: line three\n\n".data(using: .utf8)! + print("Feed: 3 data fields in one event") + let events6 = parser.parse(sseData6) + if let e = events6.first { + print(" data: \"\(e.data)\"") + print(" (contains \\n between lines: \(e.data.contains("\n") ? "yes" : "no"))") + } + print("✅ Multi-line data joined correctly") + + waitForUser() +} + +// MARK: - 3. UTF-8 Safety Demo + +func demoUTF8Safety() { + printHeader("3. UTF-8 Safety — Multi-Byte Character Boundary Detection") + + let buffer = StreamBuffer() + + // ---- 3a. Complete multi-byte characters ---- + printSubHeader("3a. Complete Multi-Byte Characters") + + let emoji = "Hello 🌍🚀".data(using: .utf8)! + buffer.append(emoji) + print("Appended: \"Hello 🌍🚀\" (\(emoji.count) bytes)") + if let str = buffer.readUTF8SafeString() { + print("UTF-8 safe read: \"\(str)\"") + } + print("✅ Complete multi-byte characters read correctly") + + // ---- 3b. Incomplete multi-byte at boundary ---- + printSubHeader("3b. Incomplete Multi-Byte at Boundary") + + buffer.reset() + let chinese = "你好世界".data(using: .utf8)! // 12 bytes (3 bytes per char) + let partial = chinese.prefix(10) // Cuts the 4th character + buffer.append(Data(partial)) + print("Appended first 10 bytes of \"你好世界\" (12 bytes total)") + print("Buffer size: \(buffer.count)") + + let safeCount = StreamBuffer.utf8SafeByteCount(buffer.data) + print("UTF-8 safe byte count: \(safeCount) (expected: 9 = 3 chars × 3 bytes)") + + if let str = buffer.readUTF8SafeString() { + print("UTF-8 safe read: \"\(str)\"") + print("Remaining bytes in buffer: \(buffer.count) (the incomplete trailing byte)") + } + + // Now complete the character + let rest = chinese.suffix(from: 10) + buffer.append(Data(rest)) + print("\nAppended remaining \(rest.count) bytes") + if let str = buffer.readUTF8SafeString() { + print("UTF-8 safe read: \"\(str)\"") + } + print("Buffer empty: \(buffer.isEmpty)") + print("✅ Incomplete multi-byte characters handled safely") + + // ---- 3c. Static utf8SafeByteCount ---- + printSubHeader("3c. utf8SafeByteCount Static Method") + + // 2-byte character (é = 0xC3 0xA9) + let twoByteChar = "café".data(using: .utf8)! + let truncated2 = Data(twoByteChar.prefix(twoByteChar.count - 1)) + let safe2 = StreamBuffer.utf8SafeByteCount(truncated2) + print("\"café\" has \(twoByteChar.count) bytes; truncated to \(truncated2.count)") + print(" Safe byte count: \(safe2)") + + // 4-byte character (𝕳 = U+1D573) + let fourByte = "A𝕳B".data(using: .utf8)! + let truncated4 = Data(fourByte.prefix(3)) // 'A' + first 2 bytes of 𝕳 + let safe4 = StreamBuffer.utf8SafeByteCount(truncated4) + print("\"A𝕳B\" has \(fourByte.count) bytes; truncated to 3 bytes") + print(" Safe byte count: \(safe4) (only 'A' is complete)") + print("✅ utf8SafeByteCount works correctly for all multi-byte sequences") + + waitForUser() +} + +// MARK: - 4. ReadRequest Demo + +func demoReadRequest() { + printHeader("4. ReadRequest — Read-Request Queue Types") + + // ---- 4a. Available request ---- + printSubHeader("4a. ReadRequest.available") + let r1 = ReadRequest(type: .available, timeout: -1, tag: 1) + print(" type: available, timeout: \(r1.timeout), tag: \(r1.tag)") + + // ---- 4b. toLength request ---- + printSubHeader("4b. ReadRequest.toLength") + let r2 = ReadRequest(type: .toLength(1024), timeout: 30, tag: 2) + if case .toLength(let len) = r2.type { + print(" type: toLength(\(len)), timeout: \(r2.timeout), tag: \(r2.tag)") + } + + // ---- 4c. toDelimiter request ---- + printSubHeader("4c. ReadRequest.toDelimiter") + let delimData = "\r\n".data(using: .utf8)! + let r3 = ReadRequest(type: .toDelimiter(delimData), timeout: 60, tag: 3) + if case .toDelimiter(let d) = r3.type { + print(" type: toDelimiter(\\r\\n, \(d.count) bytes), timeout: \(r3.timeout), tag: \(r3.tag)") + } + + // ---- 4d. Simulate a read queue ---- + printSubHeader("4d. Simulating a Read Queue") + + let buffer = StreamBuffer() + var readQueue: [ReadRequest] = [ + ReadRequest(type: .toLength(5), timeout: -1, tag: 10), + ReadRequest(type: .toDelimiter("\n".data(using: .utf8)!), timeout: -1, tag: 11), + ReadRequest(type: .available, timeout: -1, tag: 12), + ] + + print("Queue: [toLength(5), toDelimiter(\\n), available]") + print("Feeding data: \"HelloWorld\\nExtra\"") + + buffer.append("HelloWorld\nExtra".data(using: .utf8)!) + + var satisfied = 0 + while !readQueue.isEmpty { + let request = readQueue[0] + var result: Data? + + switch request.type { + case .available: + if !buffer.isEmpty { + result = buffer.readAllData() + } + case .toLength(let length): + result = buffer.readData(toLength: length) + case .toDelimiter(let delimiter): + result = buffer.readData(toDelimiter: delimiter) + } + + if let data = result { + readQueue.removeFirst() + satisfied += 1 + let text = String(data: data, encoding: .utf8) ?? "" + print(" Tag \(request.tag): \"\(text.replacingOccurrences(of: "\n", with: "\\n"))\" (\(data.count) bytes)") + } else { + break + } + } + print("Satisfied \(satisfied) of 3 requests, buffer remaining: \(buffer.count)") + print("✅ Read queue processing works correctly") + + waitForUser() +} + +// MARK: - 5. NWAsyncSocket Usage Pattern + +func demoNWAsyncSocketUsage() { + printHeader("5. NWAsyncSocket — Connection Usage Pattern") + + #if canImport(Network) + print(""" + NWAsyncSocket is available on this platform (Network.framework). + + Below is a live demo showing how to create and configure a socket. + (Actual network connections require a running server.) + + """) + + // Demonstrate object creation and configuration + let socket = NWAsyncSocket(delegateQueue: .main) + print("Created NWAsyncSocket instance") + print(" isConnected: \(socket.isConnected)") + print(" connectedHost: \(socket.connectedHost ?? "nil")") + print(" connectedPort: \(socket.connectedPort)") + + socket.enableTLS() + print("\n enableTLS() called — TLS will be used on next connect") + + socket.enableSSEParsing() + print(" enableSSEParsing() called — SSE events will be parsed automatically") + + socket.enableStreamingText() + print(" enableStreamingText() called — UTF-8 strings will be delivered") + + socket.userData = ["key": "value"] + print(" userData set: \(socket.userData ?? "nil")") + + print(""" + + To test with a real server, implement NWAsyncSocketDelegate: + + class MyHandler: NWAsyncSocketDelegate { + func socket(_ sock: NWAsyncSocket, didConnectToHost host: String, port: UInt16) { + print("Connected to \\(host):\\(port)") + // Send an HTTP request + let http = "GET / HTTP/1.1\\r\\nHost: \\(host)\\r\\n\\r\\n" + sock.write(http.data(using: .utf8)!, withTimeout: 30, tag: 1) + sock.readData(withTimeout: 30, tag: 1) + } + + func socket(_ sock: NWAsyncSocket, didRead data: Data, withTag tag: Int) { + print("Received \\(data.count) bytes") + if let text = String(data: data, encoding: .utf8) { + print(text.prefix(200)) + } + sock.readData(withTimeout: -1, tag: tag + 1) + } + + func socket(_ sock: NWAsyncSocket, didWriteDataWithTag tag: Int) { + print("Write complete (tag \\(tag))") + } + + func socketDidDisconnect(_ sock: NWAsyncSocket, withError error: Error?) { + print("Disconnected: \\(error?.localizedDescription ?? "clean")") + } + + func socket(_ sock: NWAsyncSocket, didReceiveSSEEvent event: SSEEvent) { + print("SSE Event: type=\\(event.event) data=\\(event.data)") + } + } + + let handler = MyHandler() + let socket = NWAsyncSocket(delegate: handler, delegateQueue: .main) + socket.enableTLS() + try socket.connect(toHost: "example.com", onPort: 443) + """) + #else + print(""" + ⚠️ Network.framework is not available on this platform. + NWAsyncSocket requires iOS 13+ / macOS 10.15+ / tvOS 13+ / watchOS 6+. + + The core components (StreamBuffer, SSEParser, ReadRequest) demonstrated + above work on all platforms and are the building blocks of the library. + + To test NWAsyncSocket itself, run this demo on macOS or in an iOS app. + + Here is the typical usage pattern: + + import NWAsyncSocket + + class MyHandler: NWAsyncSocketDelegate { + func socket(_ sock: NWAsyncSocket, didConnectToHost host: String, port: UInt16) { + print("Connected to \\(host):\\(port)") + sock.readData(withTimeout: -1, tag: 0) + } + + func socket(_ sock: NWAsyncSocket, didRead data: Data, withTag tag: Int) { + print("Received \\(data.count) bytes") + sock.readData(withTimeout: -1, tag: tag + 1) + } + + func socket(_ sock: NWAsyncSocket, didWriteDataWithTag tag: Int) { + print("Write complete (tag \\(tag))") + } + + func socketDidDisconnect(_ sock: NWAsyncSocket, withError error: Error?) { + print("Disconnected: \\(error?.localizedDescription ?? "clean")") + } + } + + let handler = MyHandler() + let socket = NWAsyncSocket(delegate: handler, delegateQueue: .main) + socket.enableTLS() + try socket.connect(toHost: "example.com", onPort: 443) + """) + #endif + + waitForUser() +} + +// MARK: - Main Menu + +func printMenu() { + printHeader("NWAsyncSocket Swift Demo") + print(""" + Choose a demo to run: + + 1. StreamBuffer — Sticky-packet / Split-packet handling + 2. SSEParser — Server-Sent Events incremental parsing + 3. UTF-8 Safety — Multi-byte character boundary detection + 4. ReadRequest — Read-request queue types + 5. NWAsyncSocket — Connection usage pattern + a. Run all demos + q. Quit + + """) +} + +func main() { + var running = true + while running { + printMenu() + print("Enter choice: ", terminator: "") + guard let input = readLine()?.trimmingCharacters(in: .whitespaces).lowercased() else { + continue + } + switch input { + case "1": + demoStreamBuffer() + case "2": + demoSSEParser() + case "3": + demoUTF8Safety() + case "4": + demoReadRequest() + case "5": + demoNWAsyncSocketUsage() + case "a": + demoStreamBuffer() + demoSSEParser() + demoUTF8Safety() + demoReadRequest() + demoNWAsyncSocketUsage() + case "q": + print("\nGoodbye! 👋") + running = false + default: + print("Invalid choice. Please enter 1-5, a, or q.") + } + } +} + +main() diff --git a/ObjC/ObjCDemo/main.m b/ObjC/ObjCDemo/main.m new file mode 100644 index 0000000..7e8a148 --- /dev/null +++ b/ObjC/ObjCDemo/main.m @@ -0,0 +1,598 @@ +// +// main.m +// NWAsyncSocket Objective-C Demo +// +// A standalone interactive demo that lets users verify the core components +// of the Objective-C version of NWAsyncSocket: +// +// 1. NWStreamBuffer — Sticky-packet / Split-packet handling +// 2. NWSSEParser — Server-Sent Events incremental parsing +// 3. UTF-8 Safety — Multi-byte character boundary detection +// 4. NWReadRequest — Read-request queue types +// 5. GCDAsyncSocket — Connection usage pattern +// +// Build (from repository root): +// clang -framework Foundation \ +// -I ObjC/NWAsyncSocketObjC/include \ +// ObjC/NWAsyncSocketObjC/NWStreamBuffer.m \ +// ObjC/NWAsyncSocketObjC/NWSSEParser.m \ +// ObjC/NWAsyncSocketObjC/NWReadRequest.m \ +// ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m \ +// ObjC/ObjCDemo/main.m \ +// -o ObjCDemo +// +// Run: +// ./ObjCDemo +// + +#import +#import "NWStreamBuffer.h" +#import "NWSSEParser.h" +#import "NWReadRequest.h" +#import "GCDAsyncSocket.h" +#import "GCDAsyncSocketDelegate.h" + +// ============================================================================ +#pragma mark - Helpers +// ============================================================================ + +static void printHeader(NSString *title) { + NSString *line = [@"" stringByPaddingToLength:60 + withString:@"=" + startingAtIndex:0]; + printf("\n%s\n %s\n%s\n", + line.UTF8String, title.UTF8String, line.UTF8String); +} + +static void printSubHeader(NSString *title) { + printf("\n--- %s ---\n", title.UTF8String); +} + +static void waitForUser(void) { + printf("\nPress Enter to continue..."); + char buf[256]; + fgets(buf, sizeof(buf), stdin); +} + +// ============================================================================ +#pragma mark - 1. NWStreamBuffer Demo +// ============================================================================ + +static void demoStreamBuffer(void) { + printHeader(@"1. NWStreamBuffer — Sticky-Packet / Split-Packet Handling"); + + NWStreamBuffer *buffer = [[NWStreamBuffer alloc] init]; + + // ---- 1a. Sticky packet (粘包) ---- + printSubHeader(@"1a. Sticky Packet (粘包) — Multiple messages in one TCP segment"); + + NSData *stickyData = [@"Hello\r\nWorld\r\nFoo\r\n" dataUsingEncoding:NSUTF8StringEncoding]; + printf("Appending combined data: \"Hello\\r\\nWorld\\r\\nFoo\\r\\n\"\n"); + [buffer appendData:stickyData]; + printf("Buffer size: %lu bytes\n", (unsigned long)buffer.count); + + NSData *delimiter = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]; + NSInteger messageIndex = 1; + NSData *chunk = nil; + while ((chunk = [buffer readDataToDelimiter:delimiter]) != nil) { + NSString *text = [[NSString alloc] initWithData:chunk encoding:NSUTF8StringEncoding]; + NSString *display = [[text stringByReplacingOccurrencesOfString:@"\r\n" withString:@"\\r\\n"] + stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; + printf(" Message %ld: \"%s\"\n", (long)messageIndex, display.UTF8String); + messageIndex++; + } + printf("Buffer remaining: %lu bytes (expected: 0)\n", (unsigned long)buffer.count); + printf("✅ Sticky packet correctly split into %ld messages\n", (long)(messageIndex - 1)); + + // ---- 1b. Split packet (拆包) ---- + printSubHeader(@"1b. Split Packet (拆包) — One message split across TCP segments"); + + [buffer reset]; + NSData *part1 = [@"Hel" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *part2 = [@"lo World" dataUsingEncoding:NSUTF8StringEncoding]; + printf("Appending part 1: \"Hel\" (%lu bytes)\n", (unsigned long)part1.length); + [buffer appendData:part1]; + + NSData *result1 = [buffer readDataToLength:11]; + printf("Attempt to read 11 bytes: %s\n", result1 == nil ? "nil (not enough data yet)" : "got data"); + + printf("Appending part 2: \"lo World\" (%lu bytes)\n", (unsigned long)part2.length); + [buffer appendData:part2]; + printf("Buffer size: %lu bytes\n", (unsigned long)buffer.count); + + NSData *result2 = [buffer readDataToLength:11]; + if (result2) { + NSString *text = [[NSString alloc] initWithData:result2 encoding:NSUTF8StringEncoding]; + printf("Read 11 bytes: \"%s\"\n", text.UTF8String); + printf("✅ Split packet correctly reassembled\n"); + } + + // ---- 1c. Delimiter-based read ---- + printSubHeader(@"1c. Delimiter-Based Read"); + + [buffer reset]; + [buffer appendData:[@"key1=value1&key2=value2&key3=value3" dataUsingEncoding:NSUTF8StringEncoding]]; + printf("Buffer content: \"key1=value1&key2=value2&key3=value3\"\n"); + + NSData *ampersand = [@"&" dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableArray *pairs = [NSMutableArray array]; + NSData *pairData = nil; + while ((pairData = [buffer readDataToDelimiter:ampersand]) != nil) { + NSString *pair = [[NSString alloc] initWithData:pairData encoding:NSUTF8StringEncoding]; + [pairs addObject:pair]; + } + // Read remaining data + NSData *remaining = [buffer readAllData]; + if (remaining.length > 0) { + NSString *pair = [[NSString alloc] initWithData:remaining encoding:NSUTF8StringEncoding]; + [pairs addObject:pair]; + } + printf("Parsed pairs:\n"); + for (NSString *pair in pairs) { + printf(" \"%s\"\n", pair.UTF8String); + } + printf("✅ Delimiter-based reading works correctly\n"); + + // ---- 1d. readAllData ---- + printSubHeader(@"1d. Read All Data"); + + [buffer reset]; + [buffer appendData:[@"Part A " dataUsingEncoding:NSUTF8StringEncoding]]; + [buffer appendData:[@"Part B " dataUsingEncoding:NSUTF8StringEncoding]]; + [buffer appendData:[@"Part C" dataUsingEncoding:NSUTF8StringEncoding]]; + NSData *allData = [buffer readAllData]; + NSString *allText = [[NSString alloc] initWithData:allData encoding:NSUTF8StringEncoding]; + printf("Read all: \"%s\"\n", allText.UTF8String); + printf("Buffer empty after readAll: %s\n", buffer.isEmpty ? "YES" : "NO"); + printf("✅ readAllData works correctly\n"); + + waitForUser(); +} + +// ============================================================================ +#pragma mark - 2. NWSSEParser Demo +// ============================================================================ + +static void demoSSEParser(void) { + printHeader(@"2. NWSSEParser — Server-Sent Events Parsing"); + + NWSSEParser *parser = [[NWSSEParser alloc] init]; + + // ---- 2a. Single complete SSE event ---- + printSubHeader(@"2a. Single Complete SSE Event"); + + NSData *sseData1 = [@"event: chat\ndata: Hello from the server!\n\n" dataUsingEncoding:NSUTF8StringEncoding]; + printf("Feed: \"event: chat\\ndata: Hello from the server!\\n\\n\"\n"); + NSArray *events1 = [parser parseData:sseData1]; + for (NWSSEEvent *event in events1) { + printf(" Parsed Event → type: \"%s\", data: \"%s\"\n", + event.event.UTF8String, event.data.UTF8String); + } + printf("✅ Single event parsed correctly\n"); + + // ---- 2b. Multiple events in one chunk ---- + printSubHeader(@"2b. Multiple Events in One Chunk"); + + [parser reset]; + NSData *sseData2 = [@"data: first message\n\ndata: second message\n\nevent: custom\ndata: third with type\n\n" + dataUsingEncoding:NSUTF8StringEncoding]; + printf("Feed: 3 events in a single chunk\n"); + NSArray *events2 = [parser parseData:sseData2]; + printf(" Parsed %lu events:\n", (unsigned long)events2.count); + for (NSUInteger i = 0; i < events2.count; i++) { + NWSSEEvent *event = events2[i]; + printf(" [%lu] type: \"%s\", data: \"%s\"\n", + (unsigned long)(i + 1), event.event.UTF8String, event.data.UTF8String); + } + printf("✅ Multiple events parsed correctly\n"); + + // ---- 2c. Split across chunks (LLM streaming simulation) ---- + printSubHeader(@"2c. Split Across Chunks — LLM Streaming Simulation"); + + [parser reset]; + NSArray *chunks = @[ + @"data: {\"tok", + @"en\": \"Hel\"}\n", + @"\ndata: {\"token\"", + @": \"lo\"}\n\ndata", + @": {\"token\": \" World\"}\n\n" + ]; + printf("Feeding %lu partial chunks to simulate LLM streaming:\n", (unsigned long)chunks.count); + NSMutableArray *allEvents = [NSMutableArray array]; + for (NSUInteger i = 0; i < chunks.count; i++) { + NSString *chunkStr = chunks[i]; + NSString *display = [[chunkStr stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"] + stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; + NSArray *parsed = [parser parseString:chunkStr]; + [allEvents addObjectsFromArray:parsed]; + printf(" Chunk %lu: \"%s\" → %lu event(s)\n", + (unsigned long)(i + 1), display.UTF8String, (unsigned long)parsed.count); + } + printf("\nTotal events parsed: %lu\n", (unsigned long)allEvents.count); + for (NSUInteger i = 0; i < allEvents.count; i++) { + printf(" [%lu] data: \"%s\"\n", + (unsigned long)(i + 1), allEvents[i].data.UTF8String); + } + printf("✅ Split SSE chunks reassembled correctly\n"); + + // ---- 2d. Event with id and retry ---- + printSubHeader(@"2d. Event with ID and Retry Fields"); + + [parser reset]; + NSData *sseData4 = [@"id: 42\nretry: 3000\nevent: update\ndata: payload here\n\n" + dataUsingEncoding:NSUTF8StringEncoding]; + printf("Feed: event with id=42, retry=3000, type=update\n"); + NSArray *events4 = [parser parseData:sseData4]; + for (NWSSEEvent *event in events4) { + printf(" type: \"%s\", data: \"%s\", id: %s, retry: %ld\n", + event.event.UTF8String, + event.data.UTF8String, + event.eventId ? event.eventId.UTF8String : "nil", + (long)event.retry); + } + printf(" lastEventId: %s\n", parser.lastEventId ? parser.lastEventId.UTF8String : "nil"); + printf("✅ ID and retry fields parsed correctly\n"); + + // ---- 2e. Comments ignored ---- + printSubHeader(@"2e. Comments Are Ignored"); + + [parser reset]; + NSData *sseData5 = [@": this is a comment\ndata: visible data\n\n" dataUsingEncoding:NSUTF8StringEncoding]; + printf("Feed: \": this is a comment\\ndata: visible data\\n\\n\"\n"); + NSArray *events5 = [parser parseData:sseData5]; + printf(" Parsed %lu event(s)\n", (unsigned long)events5.count); + if (events5.count > 0) { + printf(" data: \"%s\"\n", events5[0].data.UTF8String); + } + printf("✅ Comments correctly ignored\n"); + + // ---- 2f. Multi-line data ---- + printSubHeader(@"2f. Multi-Line Data Field"); + + [parser reset]; + NSData *sseData6 = [@"data: line one\ndata: line two\ndata: line three\n\n" dataUsingEncoding:NSUTF8StringEncoding]; + printf("Feed: 3 data fields in one event\n"); + NSArray *events6 = [parser parseData:sseData6]; + if (events6.count > 0) { + NWSSEEvent *e = events6[0]; + printf(" data: \"%s\"\n", e.data.UTF8String); + printf(" (contains newlines: %s)\n", + [e.data containsString:@"\n"] ? "yes" : "no"); + } + printf("✅ Multi-line data joined correctly\n"); + + waitForUser(); +} + +// ============================================================================ +#pragma mark - 3. UTF-8 Safety Demo +// ============================================================================ + +static void demoUTF8Safety(void) { + printHeader(@"3. UTF-8 Safety — Multi-Byte Character Boundary Detection"); + + NWStreamBuffer *buffer = [[NWStreamBuffer alloc] init]; + + // ---- 3a. Complete multi-byte characters ---- + printSubHeader(@"3a. Complete Multi-Byte Characters"); + + NSData *emoji = [@"Hello 🌍🚀" dataUsingEncoding:NSUTF8StringEncoding]; + [buffer appendData:emoji]; + printf("Appended: \"Hello 🌍🚀\" (%lu bytes)\n", (unsigned long)emoji.length); + NSString *str = [buffer readUTF8SafeString]; + if (str) { + printf("UTF-8 safe read: \"%s\"\n", str.UTF8String); + } + printf("✅ Complete multi-byte characters read correctly\n"); + + // ---- 3b. Incomplete multi-byte at boundary ---- + printSubHeader(@"3b. Incomplete Multi-Byte at Boundary"); + + [buffer reset]; + NSData *chinese = [@"你好世界" dataUsingEncoding:NSUTF8StringEncoding]; // 12 bytes + NSData *partial = [chinese subdataWithRange:NSMakeRange(0, 10)]; + [buffer appendData:partial]; + printf("Appended first 10 bytes of \"你好世界\" (%lu bytes total)\n", (unsigned long)chinese.length); + printf("Buffer size: %lu\n", (unsigned long)buffer.count); + + NSUInteger safeCount = [NWStreamBuffer utf8SafeByteCountForData:buffer.data]; + printf("UTF-8 safe byte count: %lu (expected: 9 = 3 chars × 3 bytes)\n", (unsigned long)safeCount); + + NSString *safeStr = [buffer readUTF8SafeString]; + if (safeStr) { + printf("UTF-8 safe read: \"%s\"\n", safeStr.UTF8String); + printf("Remaining bytes in buffer: %lu (the incomplete trailing byte)\n", + (unsigned long)buffer.count); + } + + // Now complete the character + NSData *rest = [chinese subdataWithRange:NSMakeRange(10, chinese.length - 10)]; + [buffer appendData:rest]; + printf("\nAppended remaining %lu bytes\n", (unsigned long)rest.length); + NSString *safeStr2 = [buffer readUTF8SafeString]; + if (safeStr2) { + printf("UTF-8 safe read: \"%s\"\n", safeStr2.UTF8String); + } + printf("Buffer empty: %s\n", buffer.isEmpty ? "YES" : "NO"); + printf("✅ Incomplete multi-byte characters handled safely\n"); + + // ---- 3c. Static utf8SafeByteCount ---- + printSubHeader(@"3c. utf8SafeByteCountForData: Static Method"); + + // 2-byte character (é = 0xC3 0xA9) + NSData *twoByteData = [@"café" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *truncated2 = [twoByteData subdataWithRange:NSMakeRange(0, twoByteData.length - 1)]; + NSUInteger safe2 = [NWStreamBuffer utf8SafeByteCountForData:truncated2]; + printf("\"café\" has %lu bytes; truncated to %lu\n", + (unsigned long)twoByteData.length, (unsigned long)truncated2.length); + printf(" Safe byte count: %lu\n", (unsigned long)safe2); + + // 4-byte character (𝕳 = U+1D573) + NSData *fourByteData = [@"A𝕳B" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *truncated4 = [fourByteData subdataWithRange:NSMakeRange(0, 3)]; // 'A' + first 2 bytes of 𝕳 + NSUInteger safe4 = [NWStreamBuffer utf8SafeByteCountForData:truncated4]; + printf("\"A𝕳B\" has %lu bytes; truncated to 3 bytes\n", (unsigned long)fourByteData.length); + printf(" Safe byte count: %lu (only 'A' is complete)\n", (unsigned long)safe4); + printf("✅ utf8SafeByteCountForData works correctly for all multi-byte sequences\n"); + + waitForUser(); +} + +// ============================================================================ +#pragma mark - 4. NWReadRequest Demo +// ============================================================================ + +static void demoReadRequest(void) { + printHeader(@"4. NWReadRequest — Read-Request Queue Types"); + + // ---- 4a. Available request ---- + printSubHeader(@"4a. NWReadRequest — available"); + NWReadRequest *r1 = [NWReadRequest availableRequestWithTimeout:-1 tag:1]; + printf(" type: available, timeout: %.1f, tag: %ld\n", r1.timeout, r1.tag); + + // ---- 4b. toLength request ---- + printSubHeader(@"4b. NWReadRequest — toLength"); + NWReadRequest *r2 = [NWReadRequest toLengthRequest:1024 timeout:30 tag:2]; + printf(" type: toLength(%lu), timeout: %.1f, tag: %ld\n", + (unsigned long)r2.length, r2.timeout, r2.tag); + + // ---- 4c. toDelimiter request ---- + printSubHeader(@"4c. NWReadRequest — toDelimiter"); + NSData *delimData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]; + NWReadRequest *r3 = [NWReadRequest toDelimiterRequest:delimData timeout:60 tag:3]; + printf(" type: toDelimiter(\\r\\n, %lu bytes), timeout: %.1f, tag: %ld\n", + (unsigned long)r3.delimiter.length, r3.timeout, r3.tag); + + // ---- 4d. Simulate a read queue ---- + printSubHeader(@"4d. Simulating a Read Queue"); + + NWStreamBuffer *buffer = [[NWStreamBuffer alloc] init]; + NSMutableArray *readQueue = [NSMutableArray arrayWithArray:@[ + [NWReadRequest toLengthRequest:5 timeout:-1 tag:10], + [NWReadRequest toDelimiterRequest:[@"\n" dataUsingEncoding:NSUTF8StringEncoding] + timeout:-1 tag:11], + [NWReadRequest availableRequestWithTimeout:-1 tag:12], + ]]; + + printf("Queue: [toLength(5), toDelimiter(\\n), available]\n"); + printf("Feeding data: \"HelloWorld\\nExtra\"\n"); + + [buffer appendData:[@"HelloWorld\nExtra" dataUsingEncoding:NSUTF8StringEncoding]]; + + NSInteger satisfied = 0; + while (readQueue.count > 0) { + NWReadRequest *request = readQueue[0]; + NSData *result = nil; + + switch (request.type) { + case NWReadRequestTypeAvailable: + if (!buffer.isEmpty) { + result = [buffer readAllData]; + } + break; + case NWReadRequestTypeToLength: + result = [buffer readDataToLength:request.length]; + break; + case NWReadRequestTypeToDelimiter: + result = [buffer readDataToDelimiter:request.delimiter]; + break; + } + + if (result) { + [readQueue removeObjectAtIndex:0]; + satisfied++; + NSString *text = [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding]; + NSString *display = [text stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; + printf(" Tag %ld: \"%s\" (%lu bytes)\n", + request.tag, display.UTF8String, (unsigned long)result.length); + } else { + break; + } + } + printf("Satisfied %ld of 3 requests, buffer remaining: %lu\n", + (long)satisfied, (unsigned long)buffer.count); + printf("✅ Read queue processing works correctly\n"); + + waitForUser(); +} + +// ============================================================================ +#pragma mark - 5. GCDAsyncSocket Usage Pattern +// ============================================================================ + +// Sample delegate class for demonstration +@interface DemoSocketDelegate : NSObject +@end + +@implementation DemoSocketDelegate + +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { + NSLog(@"Connected to %@:%u", host, port); + // Send an HTTP GET request + NSString *http = [NSString stringWithFormat: + @"GET / HTTP/1.1\r\nHost: %@\r\nConnection: close\r\n\r\n", host]; + NSData *data = [http dataUsingEncoding:NSUTF8StringEncoding]; + [sock writeData:data withTimeout:30 tag:1]; + [sock readDataWithTimeout:30 tag:1]; +} + +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { + NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSLog(@"Received %lu bytes (tag %ld):", (unsigned long)data.length, tag); + if (text) { + // Print first 200 chars + if (text.length > 200) { + NSLog(@"%@...", [text substringToIndex:200]); + } else { + NSLog(@"%@", text); + } + } + [sock readDataWithTimeout:-1 tag:tag + 1]; +} + +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag { + NSLog(@"Write complete (tag %ld)", tag); +} + +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error { + NSLog(@"Disconnected: %@", error ? error.localizedDescription : @"clean"); +} + +- (void)socket:(GCDAsyncSocket *)sock didReceiveSSEEvent:(NWSSEEvent *)event { + NSLog(@"SSE Event: type=%@ data=%@", event.event, event.data); +} + +- (void)socket:(GCDAsyncSocket *)sock didReceiveString:(NSString *)string { + NSLog(@"Streaming text: %@", string); +} + +@end + +static void demoGCDAsyncSocketUsage(void) { + printHeader(@"5. GCDAsyncSocket — Connection Usage Pattern"); + + printf("Creating GCDAsyncSocket with delegate...\n\n"); + + DemoSocketDelegate *delegate = [[DemoSocketDelegate alloc] init]; + GCDAsyncSocket *socket = [[GCDAsyncSocket alloc] initWithDelegate:delegate + delegateQueue:dispatch_get_main_queue()]; + + printf("Created GCDAsyncSocket instance\n"); + printf(" isConnected: %s\n", socket.isConnected ? "YES" : "NO"); + printf(" connectedHost: %s\n", socket.connectedHost ? socket.connectedHost.UTF8String : "nil"); + printf(" connectedPort: %u\n", socket.connectedPort); + + [socket enableTLS]; + printf("\n enableTLS called — TLS will be used on next connect\n"); + + [socket enableSSEParsing]; + printf(" enableSSEParsing called — SSE events will be parsed automatically\n"); + + [socket enableStreamingText]; + printf(" enableStreamingText called — UTF-8 strings will be delivered\n"); + + socket.userData = @{@"key": @"value"}; + printf(" userData set: %s\n", [socket.userData description].UTF8String); + + printf("\n" + "The DemoSocketDelegate class above shows how to implement all\n" + "GCDAsyncSocketDelegate methods:\n\n" + " @interface DemoSocketDelegate : NSObject \n" + " @end\n\n" + " @implementation DemoSocketDelegate\n\n" + " - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host\n" + " port:(uint16_t)port {\n" + " NSLog(@\"Connected to %%@:%%u\", host, port);\n" + " NSString *http = [NSString stringWithFormat:\n" + " @\"GET / HTTP/1.1\\r\\nHost: %%@\\r\\n\\r\\n\", host];\n" + " [sock writeData:[http dataUsingEncoding:NSUTF8StringEncoding]\n" + " withTimeout:30 tag:1];\n" + " [sock readDataWithTimeout:30 tag:1];\n" + " }\n\n" + " - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data\n" + " withTag:(long)tag {\n" + " NSLog(@\"Received %%lu bytes\", (unsigned long)data.length);\n" + " [sock readDataWithTimeout:-1 tag:tag + 1];\n" + " }\n\n" + " - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {\n" + " NSLog(@\"Write complete (tag %%ld)\", tag);\n" + " }\n\n" + " - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {\n" + " NSLog(@\"Disconnected: %%@\",\n" + " error ? error.localizedDescription : @\"clean\");\n" + " }\n\n" + " - (void)socket:(GCDAsyncSocket *)sock didReceiveSSEEvent:(NWSSEEvent *)event {\n" + " NSLog(@\"SSE Event: type=%%@ data=%%@\", event.event, event.data);\n" + " }\n\n" + " @end\n\n" + "To test with a real server:\n\n" + " DemoSocketDelegate *delegate = [[DemoSocketDelegate alloc] init];\n" + " GCDAsyncSocket *socket = [[GCDAsyncSocket alloc] initWithDelegate:delegate\n" + " delegateQueue:dispatch_get_main_queue()];\n" + " [socket enableTLS];\n" + " NSError *err = nil;\n" + " [socket connectToHost:@\"example.com\" onPort:443 error:&err];\n" + ); + + waitForUser(); +} + +// ============================================================================ +#pragma mark - Main Menu +// ============================================================================ + +static void printMenu(void) { + printHeader(@"NWAsyncSocket Objective-C Demo"); + printf( + "Choose a demo to run:\n\n" + " 1. NWStreamBuffer — Sticky-packet / Split-packet handling\n" + " 2. NWSSEParser — Server-Sent Events incremental parsing\n" + " 3. UTF-8 Safety — Multi-byte character boundary detection\n" + " 4. NWReadRequest — Read-request queue types\n" + " 5. GCDAsyncSocket — Connection usage pattern\n" + " a. Run all demos\n" + " q. Quit\n\n" + ); +} + +int main(int argc, const char * argv[]) { + @autoreleasepool { + BOOL running = YES; + while (running) { + printMenu(); + printf("Enter choice: "); + fflush(stdout); + char buf[256]; + if (fgets(buf, sizeof(buf), stdin) == NULL) break; + + // Trim whitespace and newline + NSString *input = [[NSString stringWithUTF8String:buf] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + input = [input lowercaseString]; + + if ([input isEqualToString:@"1"]) { + demoStreamBuffer(); + } else if ([input isEqualToString:@"2"]) { + demoSSEParser(); + } else if ([input isEqualToString:@"3"]) { + demoUTF8Safety(); + } else if ([input isEqualToString:@"4"]) { + demoReadRequest(); + } else if ([input isEqualToString:@"5"]) { + demoGCDAsyncSocketUsage(); + } else if ([input isEqualToString:@"a"]) { + demoStreamBuffer(); + demoSSEParser(); + demoUTF8Safety(); + demoReadRequest(); + demoGCDAsyncSocketUsage(); + } else if ([input isEqualToString:@"q"]) { + printf("\nGoodbye! 👋\n"); + running = NO; + } else { + printf("Invalid choice. Please enter 1-5, a, or q.\n"); + } + } + } + return 0; +} diff --git a/Package.swift b/Package.swift index 8ca16d3..f42f20b 100644 --- a/Package.swift +++ b/Package.swift @@ -16,12 +16,21 @@ let package = Package( name: "NWAsyncSocket", targets: ["NWAsyncSocket"] ), + .executable( + name: "SwiftDemo", + targets: ["SwiftDemo"] + ), ], targets: [ .target( name: "NWAsyncSocket", path: "Sources/NWAsyncSocket" ), + .executableTarget( + name: "SwiftDemo", + dependencies: ["NWAsyncSocket"], + path: "Examples/SwiftDemo" + ), .testTarget( name: "NWAsyncSocketTests", dependencies: ["NWAsyncSocket"], diff --git a/README.md b/README.md index a0962e4..b832b38 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,8 @@ NWAsyncSocket/ │ ├── StreamBuffer.swift # Byte buffer with UTF-8 safety │ ├── SSEParser.swift # SSE event parser │ └── ReadRequest.swift # Read request queue model +├── Examples/SwiftDemo/ # Swift interactive demo +│ └── main.swift # Run: swift run SwiftDemo ├── ObjC/NWAsyncSocketObjC/ # Objective-C version │ ├── include/ # Public headers │ │ ├── GCDAsyncSocket.h # Main class (drop-in replacement) @@ -204,6 +206,8 @@ NWAsyncSocket/ │ ├── NWStreamBuffer.m │ ├── NWSSEParser.m │ └── NWReadRequest.m +├── ObjC/ObjCDemo/ # Objective-C interactive demo +│ └── main.m # Build with clang (see Demo section) ├── ObjC/NWAsyncSocketObjCTests/ # ObjC XCTest cases │ ├── NWStreamBufferTests.m │ ├── NWSSEParserTests.m @@ -231,6 +235,50 @@ swift test Add the ObjC source and test files to an Xcode project and run the XCTest test suite. +## Demo + +Interactive demos are provided for both Swift and Objective-C to help you verify all core components. + +### Swift Demo + +Run the interactive Swift demo via SPM: + +```bash +swift run SwiftDemo +``` + +The demo menu lets you test each component individually or run all at once: + +1. **StreamBuffer** — sticky-packet / split-packet handling, delimiter-based reads +2. **SSEParser** — single/multi/split SSE events, LLM streaming simulation, ID/retry fields +3. **UTF-8 Safety** — multi-byte character boundary detection, incomplete sequence handling +4. **ReadRequest** — all read-request queue types with simulated queue processing +5. **NWAsyncSocket** — connection setup and delegate usage pattern (Network.framework only) + +### Objective-C Demo + +Build the ObjC demo on macOS: + +```bash +clang -framework Foundation \ + -I ObjC/NWAsyncSocketObjC/include \ + ObjC/NWAsyncSocketObjC/NWStreamBuffer.m \ + ObjC/NWAsyncSocketObjC/NWSSEParser.m \ + ObjC/NWAsyncSocketObjC/NWReadRequest.m \ + ObjC/NWAsyncSocketObjC/GCDAsyncSocket.m \ + ObjC/ObjCDemo/main.m \ + -o ObjCDemo +./ObjCDemo +``` + +The ObjC demo provides the same interactive menu and covers: + +1. **NWStreamBuffer** — sticky-packet / split-packet handling, delimiter-based reads +2. **NWSSEParser** — single/multi/split SSE events, LLM streaming simulation, ID/retry fields +3. **UTF-8 Safety** — multi-byte boundary detection with `utf8SafeByteCountForData:` +4. **NWReadRequest** — all read-request queue types with simulated queue processing +5. **GCDAsyncSocket** — connection setup, delegate implementation, and usage pattern + ## API Compatibility with GCDAsyncSocket | GCDAsyncSocket (CocoaAsyncSocket) | NWAsyncSocket (Swift) | GCDAsyncSocket (this library) |