EventSource

open class EventSource : NSObject, URLSessionDataDelegate, @unchecked Sendable

Implementation of the W3C Server-Sent Events (SSE) protocol for streaming data from Horizon.

This class implements a client for the Server-Sent Events protocol, which enables servers to push real-time updates to clients over HTTP. The implementation is used throughout the SDK to stream live data from Stellar Horizon servers, including ledgers, transactions, operations, effects, and account changes.

Protocol Overview

Server-Sent Events provide a unidirectional channel from server to client over HTTP. The connection remains open, and the server pushes updates as formatted text messages. Key features include:

  • Automatic reconnection with configurable retry intervals
  • Event identification for resuming from last received message
  • Named events for routing different message types
  • Long-lived connections with appropriate timeouts

Usage Example

// Stream ledger updates from Horizon
let url = "https://horizon.stellar.org/ledgers?cursor=now"
let eventSource = EventSource(url: url)

eventSource.onOpen { response in
    print("Connection opened, status: \(response?.statusCode ?? 0)")
}

eventSource.onMessage { id, event, data in
    print("Received message - ID: \(id ?? "none"), Event: \(event ?? "none")")
    // Parse data as JSON ledger response
    if let ledgerData = data?.data(using: .utf8) {
        // Process ledger data
    }
}

eventSource.onError { error in
    print("Connection error: \(error?.localizedDescription ?? "unknown")")
}

// Close when done
eventSource.close()

Event Handlers

The EventSource supports three types of callbacks:

  • onOpen: Called when connection is established
  • onMessage: Called for each message received
  • onError: Called when errors occur

Additionally, named event listeners can be registered for specific event types:

eventSource.addEventListener("ledger-update") { id, event, data in
    // Handle ledger-specific events
}

Automatic Reconnection

The implementation automatically reconnects on connection failures using an exponential backoff strategy. The retry interval can be set by the server using the retry: field in the event stream, defaulting to 3000 milliseconds.

Last Event ID

To support reliable streaming, the last received event ID is persisted and sent with reconnection requests using the Last-Event-Id header. This allows the server to resume streaming from where it left off.

See also:

  • Current connection state (connecting, open, or closed) of the Server-Sent Events stream.

    Declaration

    Swift

    open internal(set) var readyState: EventSourceState { get }
  • Milliseconds to wait before attempting reconnection after connection failure.

    Declaration

    Swift

    open fileprivate(set) var retryTime: Int { get }
  • Creates a new EventSource instance and establishes connection to the specified URL.

    The initializer creates an EventSource that immediately begins connecting to the server. Custom headers can be provided for authentication or other purposes.

    Note

    The connection starts automatically after initialization. Use onOpen, onMessage, and onError callbacks to handle connection lifecycle events.

    Declaration

    Swift

    public init(url: String, headers: [String : String] = [:])

    Parameters

    url

    The URL string of the Server-Sent Events endpoint. Must be a valid URL.

    headers

    Optional dictionary of HTTP headers to include in the request. Useful for adding authentication tokens or custom headers.

  • Closes the Server-Sent Events connection and prevents automatic reconnection.

    Declaration

    Swift

    open func close()
  • Registers a callback to be invoked when the connection opens.

    The callback is called on the main thread when the Server-Sent Events connection is successfully established and transitions to the open state.

    Example

    eventSource.onOpen { response in
        if let statusCode = response?.statusCode {
            print("Connected with status: \(statusCode)")
        }
    }
    

    Declaration

    Swift

    open func onOpen(_ onOpenCallback: @escaping (HTTPURLResponse?) -> Void)

    Parameters

    onOpenCallback

    Closure called when connection opens.

  • Registers a callback to be invoked when an error occurs.

    The callback is called on the main thread when connection errors occur, including network failures, timeouts, or server errors. If an error occurred before this callback was registered, it will be immediately invoked with that error.

    Example

    eventSource.onError { error in
        print("Connection error: \(error?.localizedDescription ?? "unknown")")
        // Handle reconnection or cleanup
    }
    

    Note

    The EventSource automatically attempts to reconnect after errors unless explicitly closed.

    Declaration

    Swift

    open func onError(_ onErrorCallback: @escaping (NSError?) -> Void)

    Parameters

    onErrorCallback

    Closure called when errors occur.

  • Registers a callback to be invoked when a message is received.

    The callback is called on the main thread for each Server-Sent Event message received from the server. Messages without an explicit event type are dispatched to this handler with event type “message”.

    Example

    eventSource.onMessage { id, event, data in
        guard let jsonData = data?.data(using: .utf8),
              let ledger = try? JSONDecoder().decode(LedgerResponse.self, from: jsonData) else {
            return
        }
        print("Received ledger: \(ledger.sequence)")
    }
    

    Declaration

    Swift

    open func onMessage(_ onMessageCallback: @escaping @Sendable (_ id: String?, _ event: String?, _ data: String?) -> Void)

    Parameters

    onMessageCallback

    Closure called when messages are received.

  • Registers a handler for messages with a specific event type.

    Allows routing of different event types to specific handlers. The Server-Sent Events protocol supports named events, enabling the server to send different types of messages that can be handled separately.

    Example

    // Register handler for specific event types
    eventSource.addEventListener("create") { id, event, data in
        print("Create event received")
    }
    
    eventSource.addEventListener("update") { id, event, data in
        print("Update event received")
    }
    

    Note

    Only one handler can be registered per event type. Registering a new handler for an event type will replace any existing handler for that type.

    Declaration

    Swift

    open func addEventListener(_ event: String, handler: @escaping @Sendable (_ id: String?, _ event: String?, _ data: String?) -> Void)

    Parameters

    event

    The event type name that was matched.

    handler

    Closure called when a message with the specified event type is received.

  • Removes the handler for a specific event type.

    After removal, messages with the specified event type will no longer be dispatched to a handler.

    Declaration

    Swift

    open func removeEventListener(_ event: String)

    Parameters

    event

    The event type name to stop listening for.

  • Returns an array of all registered event type names.

    Declaration

    Swift

    open func events() -> Array<String>

    Return Value

    Array of event type names that currently have registered handlers. Does not include the default “message” event type handled by onMessage.

  • URLSessionDataDelegate method called when data is received from the stream.

    Declaration

    Swift

    open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
  • URLSessionDataDelegate method called when the initial response is received from the server.

    Declaration

    Swift

    open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
  • URLSessionDelegate method called when the connection completes or encounters an error.

    Declaration

    Swift

    open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
  • Creates an AsyncStream for receiving Server-Sent Events.

    This method provides a modern async/await alternative to the callback-based API. Use this when you prefer structured concurrency over callbacks.

    Example:

    let eventSource = EventSource(url: "https://horizon.stellar.org/ledgers?cursor=now")
    let stream = eventSource.eventStream()
    
    for await (id, event, data) in stream {
        print("Event: \(event ?? "message")")
        if let ledgerData = data?.data(using: .utf8) {
            // Process ledger data
        }
    }
    

    Note

    The stream will continue until the EventSource is closed or an error occurs. Only one AsyncStream should be active per EventSource instance.

    Declaration

    Swift

    open func eventStream() -> AsyncStream<(id: String?, event: String?, data: String?)>

    Return Value

    An AsyncStream that yields tuples of (id, event, data) for each message

  • Generates a Basic Authentication header value from username and password.

    Declaration

    Swift

    open class func basicAuth(_ username: String, password: String) -> String

    Parameters

    username

    The username for authentication

    password

    The password for authentication

    Return Value

    A properly formatted “Basic {base64}” authentication header value