From 1c7bc99309792cfb40b32029f89e1183dc81bf6f Mon Sep 17 00:00:00 2001 From: JadianZheng Date: Mon, 14 Jul 2025 22:31:49 +0800 Subject: [PATCH 1/7] Add /r/r support --- Sources/EventSource/EventParser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/EventSource/EventParser.swift b/Sources/EventSource/EventParser.swift index 05aa31c..c19fefc 100644 --- a/Sources/EventSource/EventParser.swift +++ b/Sources/EventSource/EventParser.swift @@ -39,7 +39,7 @@ struct ServerEventParser: EventParser { } private func splitBuffer(for data: Data) -> (completeData: [Data], remainingData: Data) { - let separators: [[UInt8]] = [[Self.lf, Self.lf], [Self.cr, Self.lf, Self.cr, Self.lf]] + let separators: [[UInt8]] = [[Self.cr, Self.cr], [Self.lf, Self.lf], [Self.cr, Self.lf, Self.cr, Self.lf]] // find last range of our separator, most likely to be fast enough let (chosenSeparator, lastSeparatorRange) = findLastSeparator(in: data, separators: separators) From 6220e78c043abbc28ec1e3d3338a5fb837926ef8 Mon Sep 17 00:00:00 2001 From: Jadian Date: Thu, 24 Jul 2025 15:29:02 +0800 Subject: [PATCH 2/7] Add end-of-line Mix mode support, no mater \r\n\n or \n\r\n or \r\r --- Sources/EventSource/Data+Split.swift | 47 ++++++++++ Sources/EventSource/EventParser.swift | 85 +------------------ Sources/EventSource/EventSourceABNF.swift | 50 +++++++++++ Sources/EventSource/ServerEvent.swift | 24 ++++-- Tests/EventSourceTests/EventParserTests.swift | 2 +- 5 files changed, 115 insertions(+), 93 deletions(-) create mode 100644 Sources/EventSource/Data+Split.swift create mode 100644 Sources/EventSource/EventSourceABNF.swift diff --git a/Sources/EventSource/Data+Split.swift b/Sources/EventSource/Data+Split.swift new file mode 100644 index 0000000..d06e376 --- /dev/null +++ b/Sources/EventSource/Data+Split.swift @@ -0,0 +1,47 @@ +// +// Data+Split.swift +// EventSource +// +// Created by JadianZheng on 2025/7/24. +// + +import Foundation + +extension Data { + func split(separators: [[UInt8]]) -> (completeData: [Data], remainingData: Data) { + var currentIndex = startIndex + var messages = [Data]() + + while currentIndex < endIndex { + var foundSeparator: [UInt8]? = nil + var foundRange: Range? = nil + + let remainingData = self[currentIndex.. [EVEvent] { - let (separatedMessages, remainingData) = splitBuffer(for: buffer + data) + let (separatedMessages, remainingData) = (buffer + data).split(separators: doubleSeparators) + buffer = remainingData return parseBuffer(for: separatedMessages) } @@ -37,83 +35,4 @@ struct ServerEventParser: EventParser { return messages } - - private func splitBuffer(for data: Data) -> (completeData: [Data], remainingData: Data) { - let separators: [[UInt8]] = [[Self.cr, Self.cr], [Self.lf, Self.lf], [Self.cr, Self.lf, Self.cr, Self.lf]] - - // find last range of our separator, most likely to be fast enough - let (chosenSeparator, lastSeparatorRange) = findLastSeparator(in: data, separators: separators) - guard let separator = chosenSeparator, let lastSeparator = lastSeparatorRange else { - return ([], data) - } - - // chop everything before the last separator, going forward, O(n) complexity - let bufferRange = data.startIndex ..< lastSeparator.upperBound - let remainingRange = lastSeparator.upperBound ..< data.endIndex - let rawMessages: [Data] = if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, visionOS 1.0, *) { - data[bufferRange].split(separator: separator) - } else { - data[bufferRange].split(by: separator) - } - - // now clean up the messages and return - let cleanedMessages = rawMessages.map { cleanMessageData($0) } - return (cleanedMessages, data[remainingRange]) - } - - private func findLastSeparator(in data: Data, separators: [[UInt8]]) -> ([UInt8]?, Range?) { - var chosenSeparator: [UInt8]? - var lastSeparatorRange: Range? - for separator in separators { - if let range = data.lastRange(of: separator) { - if lastSeparatorRange == nil || range.upperBound > lastSeparatorRange!.upperBound { - chosenSeparator = separator - lastSeparatorRange = range - } - } - } - return (chosenSeparator, lastSeparatorRange) - } - - private func cleanMessageData(_ messageData: Data) -> Data { - var cleanData = messageData - - // remove trailing CR/LF characters from the end - while !cleanData.isEmpty, cleanData.last == Self.cr || cleanData.last == Self.lf { - cleanData = cleanData.dropLast() - } - - // also clean internal lines within each message to remove trailing \r - let cleanedLines = cleanData.split(separator: Self.lf) - .map { line in line.trimming(while: { $0 == Self.cr }) } - .joined(separator: [Self.lf]) - - return Data(cleanedLines) - } -} - -fileprivate extension Data { - @available(macOS, deprecated: 13.0, obsoleted: 13.0, message: "This method is not recommended on macOS 13.0+") - @available(iOS, deprecated: 16.0, obsoleted: 16.0, message: "This method is not recommended on iOS 16.0+") - @available(watchOS, deprecated: 9.0, obsoleted: 9.0, message: "This method is not recommended on watchOS 9.0+") - @available(tvOS, deprecated: 16.0, obsoleted: 16.0, message: "This method is not recommended on tvOS 16.0+") - @available(visionOS, deprecated: 1.0, obsoleted: 1.1, message: "This method is not recommended on visionOS 1.0+") - func split(by separator: [UInt8]) -> [Data] { - var chunks: [Data] = [] - var pos = startIndex - // Find next occurrence of separator after current position - while let r = self[pos...].range(of: Data(separator)) { - // Append if non-empty - if r.lowerBound > pos { - chunks.append(self[pos.. $1.count } + +let doubleSeparators: [[UInt8]] = [ + [cr, lf, cr, lf], // \r\n\r\n + [lf, cr, lf], // \n\r\n + [cr, cr, lf], // \r\r\n + [cr, lf, lf], // \r\n\n + [cr, lf, cr], // \r\n\r + [cr, cr], // \r\r + [lf, lf] // \n\n +].sorted { $0.count > $1.count } diff --git a/Sources/EventSource/ServerEvent.swift b/Sources/EventSource/ServerEvent.swift index 51e4893..d60783f 100644 --- a/Sources/EventSource/ServerEvent.swift +++ b/Sources/EventSource/ServerEvent.swift @@ -68,22 +68,28 @@ public struct ServerEvent: EVEvent { } public static func parse(from data: Data, mode: EventSource.Mode = .default) -> ServerEvent? { - let rows: [Data] = switch mode { - case .default: - data.split(separator: ServerEventParser.lf) // Separate event fields - case .dataOnly: - [data] // Do not split data in data-only mode - } + let recivedStr = String(data: data, encoding: .utf8) + + let rows: [Data] = { + switch mode { + case .default: + let (separatedMessages, remainingData) = data.split(separators: singleSeparators) + return separatedMessages + [remainingData] + + case .dataOnly: + return [data] // Do not split data in data-only mode + } + }() var message = ServerEvent() for row in rows { // Skip the line if it is empty or it starts with a colon character - if row.isEmpty || row.first == ServerEventParser.colon { + if row.isEmpty || row.first == colon { continue } - let keyValue = row.split(separator: ServerEventParser.colon, maxSplits: 1) + let keyValue = row.split(separator: colon, maxSplits: 1) let key = keyValue[0].utf8String // If value starts with a SPACE character, remove it from value @@ -111,7 +117,7 @@ public struct ServerEvent: EVEvent { // If the line is not empty but does not contain a colon character // add it to the other fields using the whole line as the field name, // and the empty string as the field value. - if row.contains(ServerEventParser.colon) == false { + if row.contains(colon) == false { let string = row.utf8String if var other = message.other { other[string] = "" diff --git a/Tests/EventSourceTests/EventParserTests.swift b/Tests/EventSourceTests/EventParserTests.swift index e737c38..59e2143 100644 --- a/Tests/EventSourceTests/EventParserTests.swift +++ b/Tests/EventSourceTests/EventParserTests.swift @@ -189,7 +189,7 @@ struct EventParserTests { // Test with mixed LF (\n) and CR+LF (\r\n) - using separate events let textMixed = "data: test mixedline1\n\n" + - "data: mixedline2\r\n\n" + + "data: mixedline2\n\r\n" + "event: update\r\ndata: mixedtest\n\n" + "id: 4\nevent: pong\r\ndata: mixedpong\r\n\n" From 57454d727fb13a5282c3ee36b10ee4664b2be1da Mon Sep 17 00:00:00 2001 From: Jadian Date: Thu, 24 Jul 2025 19:50:01 +0800 Subject: [PATCH 3/7] Fix unknown stop connect or parse fail, still get the remain text. --- README.md | 2 +- Sources/EventSource/EventParser.swift | 4 ++-- Sources/EventSource/EventSource.swift | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 24c265b..5cde4ef 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Task { print("Received an error:", error.localizedDescription) case .event(let event): print("Received an event", event.data ?? "") - case .closed: + case .closed(let undecodeText): print("Connection was closed.") } } diff --git a/Sources/EventSource/EventParser.swift b/Sources/EventSource/EventParser.swift index fe63323..9117f90 100644 --- a/Sources/EventSource/EventParser.swift +++ b/Sources/EventSource/EventParser.swift @@ -16,15 +16,15 @@ public protocol EventParser: Sendable { struct ServerEventParser: EventParser { private let mode: EventSource.Mode private var buffer = Data() + + var undeocdeText: String? { .init(data: buffer, encoding: .utf8) } init(mode: EventSource.Mode = .default) { self.mode = mode } - mutating func parse(_ data: Data) -> [EVEvent] { let (separatedMessages, remainingData) = (buffer + data).split(separators: doubleSeparators) - buffer = remainingData return parseBuffer(for: separatedMessages) } diff --git a/Sources/EventSource/EventSource.swift b/Sources/EventSource/EventSource.swift index b0419de..465d03f 100644 --- a/Sources/EventSource/EventSource.swift +++ b/Sources/EventSource/EventSource.swift @@ -35,7 +35,7 @@ public struct EventSource: Sendable { case error(Error) case event(EVEvent) case open - case closed + case closed(String?) } private let mode: Mode @@ -277,7 +277,7 @@ public extension EventSource { private func close(stream continuation: AsyncStream.Continuation, urlSession: URLSession) { let previousState = self.readyState if previousState != .closed { - continuation.yield(.closed) + continuation.yield(.closed((eventParser as? ServerEventParser)?.undeocdeText)) continuation.finish() } cancel(urlSession: urlSession) From 33bb29e128c9b549295bc4d6b54f397c189551e5 Mon Sep 17 00:00:00 2001 From: Jadian Date: Thu, 24 Jul 2025 19:53:31 +0800 Subject: [PATCH 4/7] Fix cancel task outside, can't not trigger the close event. --- Sources/EventSource/EventSource.swift | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Sources/EventSource/EventSource.swift b/Sources/EventSource/EventSource.swift index 465d03f..8488f5f 100644 --- a/Sources/EventSource/EventSource.swift +++ b/Sources/EventSource/EventSource.swift @@ -198,19 +198,10 @@ public extension EventSource { } } - #if compiler(>=6.0) continuation.onTermination = { @Sendable [weak self] _ in sessionDelegateTask.cancel() - Task { self?.close(stream: continuation, urlSession: urlSession) } + self?.close(stream: continuation, urlSession: urlSession) } - #else - continuation.onTermination = { @Sendable _ in - sessionDelegateTask.cancel() - Task { [weak self] in - await self?.close(stream: continuation, urlSession: urlSession) - } - } - #endif urlSessionDataTask.resume() readyState = .connecting From 307928dafa8b556fdbcec71a04f7832df6248d7b Mon Sep 17 00:00:00 2001 From: Jadian Date: Fri, 25 Jul 2025 10:05:59 +0800 Subject: [PATCH 5/7] Delete debug code --- Sources/EventSource/ServerEvent.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/EventSource/ServerEvent.swift b/Sources/EventSource/ServerEvent.swift index d60783f..05e15f4 100644 --- a/Sources/EventSource/ServerEvent.swift +++ b/Sources/EventSource/ServerEvent.swift @@ -68,8 +68,6 @@ public struct ServerEvent: EVEvent { } public static func parse(from data: Data, mode: EventSource.Mode = .default) -> ServerEvent? { - let recivedStr = String(data: data, encoding: .utf8) - let rows: [Data] = { switch mode { case .default: From 89eccc42147dbef355edd226d734838233c756bf Mon Sep 17 00:00:00 2001 From: Jadian Date: Tue, 5 Aug 2025 19:54:19 +0800 Subject: [PATCH 6/7] Make the request debug able. --- README.md | 6 ++++++ Sources/EventSource/EventSource.swift | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5cde4ef..179ef2e 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,12 @@ Task { print("Connection was opened.") case .error(let error): print("Received an error:", error.localizedDescription) + case .recived(let data): + if let text = String(data: data, encoding: .utf8) { + print("Recived one stream: \(text)") + } else { + print("Recived one stream: \(data)") + } case .event(let event): print("Received an event", event.data ?? "") case .closed(let undecodeText): diff --git a/Sources/EventSource/EventSource.swift b/Sources/EventSource/EventSource.swift index 8488f5f..67f0b6f 100644 --- a/Sources/EventSource/EventSource.swift +++ b/Sources/EventSource/EventSource.swift @@ -32,10 +32,11 @@ public struct EventSource: Sendable { /// Event type. public enum EventType: Sendable { - case error(Error) - case event(EVEvent) case open + case recived(Data) + case event(EVEvent) case closed(String?) + case error(Error) } private let mode: Mode @@ -193,6 +194,7 @@ public extension EventSource { completionHandler: completionHandler ) case let .didReceiveData(data): + continuation.yield(.recived(data)) parseMessages(from: data, stream: continuation, urlSession: urlSession) } } From 99aa3b18eecd7d0f4d29108a93dd2b363c980b54 Mon Sep 17 00:00:00 2001 From: Jadian Date: Fri, 8 Aug 2025 11:40:21 +0800 Subject: [PATCH 7/7] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 179ef2e..42e99c1 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Task { } ``` -Use `dataTask.cancel()` to explicitly close the connection. However, in that case `.closed` event won't be emitted. +Use `dataTask.cancel()` to explicitly close the connection. ### Data-only mode