Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
container:
image: swift:latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build
run: swift build -v
- name: Run tests
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ on:
jobs:
build:
name: Build macOS
runs-on: macos-14
runs-on: macos-15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Xcode version
run: sudo xcode-select -s /Applications/Xcode_15.3.app
- name: Build
run: swift build -v
- name: Run tests
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Package.resolved
.nova/
.vscode
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ let package = Package(
],
targets: [
.target(
name: "EventSource"),
name: "EventSource",
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]),
.testTarget(
name: "EventSourceTests",
dependencies: ["EventSource"]),
Expand Down
3 changes: 2 additions & 1 deletion Package@swift-5.9.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ let package = Package(
],
targets: [
.target(
name: "EventSource"),
name: "EventSource",
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]),
.testTarget(
name: "EventSourceTests",
dependencies: ["EventSource"]),
Expand Down
71 changes: 56 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRecouse%2FEventSource%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/Recouse/EventSource)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRecouse%2FEventSource%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Recouse/EventSource)

EventSource is a Swift package that provides a simple implementation of a client for [Server-Sent
Events](https://html.spec.whatwg.org/multipage/server-sent-events.html) (SSE). It allows you to easily
receive real-time updates from a server over a persistent HTTP connection, using a simple and efficient
interface.
EventSource is a Swift package that provides a simple implementation of a client for [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html) (SSE). It allows you to easily receive real-time updates from a server over a persistent HTTP connection, using a simple and efficient interface.

It also leverages Swift concurrency features to provide a more expressive and intuitive way to handle asynchronous operations.

> [!Note]
> Please note that this package was originally developed to be used in conjunction with another package,
and as such, it may not cover all specification details. Please be aware of this limitation when
evaluating whether EventSource is suitable for your specific use case.
> Please note that this package was originally developed to be used in conjunction with another package, and as such, it may not cover all specification details. Please be aware of this limitation when evaluating whether EventSource is suitable for your specific use case.

## Features

- [x] Simple Swift API for SSE
- [x] Supports data-only mode
- [x] Data race safety with Swift 6
- [ ] Broadcast event stream to multiple consumers (WIP)

## Installation

The module name of the package is `EventSource`. Choose one of the instructions below to install and add
the following import statement to your source code.
The module name of the package is `EventSource`. Choose one of the instructions below to install and add the following import statement to your source code.

```swift
import EventSource
Expand Down Expand Up @@ -52,21 +53,21 @@ And then, include "EventSource" as a dependency for your target:

## Usage

Using EventSource is easy. Simply create a new task from an instance of EventSource with the URLRequest of the SSE endpoint you want to connect to, and await for events:
Using EventSource is easy. Simply create a new data task from an instance of EventSource with the URLRequest of the SSE endpoint you want to connect to, and await for events:
```swift
import EventSource

let eventSource = EventSource()
let dataTask = eventSource.dataTask(for: urlRequest)
let dataTask = await eventSource.dataTask(for: urlRequest)

for await event in dataTask.events() {
for await event in await dataTask.events() {
switch event {
case .open:
print("Connection was opened.")
case .error(let error):
print("Received an error:", error.localizedDescription)
case .message(let message):
print("Received a message", message.data ?? "")
case .event(let event):
print("Received an event", event.data ?? "")
case .closed:
print("Connection was closed.")
}
Expand All @@ -75,6 +76,47 @@ for await event in dataTask.events() {

Use `dataTask.cancel()` to explicitly close the connection. However, in that case `.closed` event won't be emitted.

### Data-only mode

EventSource can be used in data-only mode, making it suitable for popular APIs like [OpenAI](https://platform.openai.com/docs/overview). Below is an example using OpenAI's [completions](https://platform.openai.com/docs/guides/text-generation) API:
```swift
var urlRequest = URLRequest(url: URL(string: "https://api.openai.com/v1/chat/completions")!)
urlRequest.allHTTPHeaderFields = [
"Content-Type": "application/json",
"Authorization": "Bearer \(accessToken)"
]
urlRequest.httpMethod = "POST"
urlRequest.httpBody = """
{
"model": "gpt-4o-mini",
"messages": [
{"role": "user", "content": "Why is the sky blue?"}
],
"stream": true
}
""".data(using: .utf8)!

let eventSource = EventSource(mode: .dataOnly)
let dataTask = await eventSource.dataTask(for: urlRequest)

var response: String = ""

for await event in await dataTask.events() {
switch event {
case .event(let event):
if let eventData = event.data, let data = eventData.data(using: .utf8) {
let chunk = try? JSONDecoder().decode(ChatCompletionChunk.self, from: data)
let string = chunk?.choices.first?.delta.content ?? ""
response += string
}
default:
break
}
}

print(response)
```

## Compatibility

* macOS 10.15+
Expand All @@ -94,4 +136,3 @@ Contributions to are always welcomed! For more details see [CONTRIBUTING.md](CON
## License

EventSource is released under the MIT License. See [LICENSE](LICENSE) for more information.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// MessageParser.swift
// EventParser.swift
// EventSource
//
// Copyright © 2023 Firdavs Khaydarov (Recouse). All rights reserved.
Expand All @@ -8,28 +8,35 @@

import Foundation

public struct MessageParser {
public var parse: (_ data: Data) -> [ServerMessage]
public protocol EventParser: Sendable {
func parse(_ data: Data) -> [EVEvent]
}

public extension MessageParser {
/// ``ServerEventParser`` is used to parse text data into ``ServerEvent``.
public struct ServerEventParser: EventParser {
let mode: EventSource.Mode

init(mode: EventSource.Mode = .default) {
self.mode = mode
}

static let lf: UInt8 = 0x0A
static let colon: UInt8 = 0x3A
static let live = Self(parse: { data in

public func parse(_ data: Data) -> [EVEvent] {
// Split message with double newline
let rawMessages: [Data]
if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, visionOS 1.0, *) {
rawMessages = data.split(separator: [Self.lf, Self.lf])
} else {
rawMessages = data.split(by: [Self.lf, Self.lf])
}

// Parse data to ServerMessage model
let messages: [ServerMessage] = rawMessages.compactMap(ServerMessage.parse(from:))
let messages: [ServerEvent] = rawMessages.compactMap { ServerEvent.parse(from: $0, mode: mode) }

return messages
})
}
}

fileprivate extension Data {
Expand Down
Loading