Skip to content
Closed
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
3 changes: 3 additions & 0 deletions DemoApp/Extensions/StreamCore+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//
// Copyright © 2025 Stream.io Inc. All rights reserved.
//
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.0.0"),
.package(url: "https://github.com/GetStream/stream-core-swift.git", exact: "0.4.1")
.package(url: "https://github.com/GetStream/stream-core-swift.git", branch: "chat-web-socket-migration")
],
targets: [
.target(
Expand Down
1 change: 0 additions & 1 deletion Sources/StreamChat/APIClient/RequestEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,6 @@ protocol ConnectionDetailsProviderDelegate: AnyObject {
}

public extension ClientError {
final class InvalidURL: ClientError, @unchecked Sendable {}
final class InvalidJSON: ClientError, @unchecked Sendable {}
final class MissingConnectionId: ClientError, @unchecked Sendable {}
}
38 changes: 19 additions & 19 deletions Sources/StreamChat/ChatClient+Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation

extension ChatClient {
/// An object containing all dependencies of `Client`
struct Environment: Sendable {
struct Environment: @unchecked Sendable {
var apiClientBuilder: @Sendable (
_ sessionConfiguration: URLSessionConfiguration,
_ requestEncoder: RequestEncoder,
Expand All @@ -25,15 +25,15 @@ extension ChatClient {

var webSocketClientBuilder: (@Sendable (
_ sessionConfiguration: URLSessionConfiguration,
_ requestEncoder: RequestEncoder,
_ eventDecoder: AnyEventDecoder,
_ notificationCenter: EventNotificationCenter
) -> WebSocketClient)? = {
WebSocketClient(
return WebSocketClient(
sessionConfiguration: $0,
requestEncoder: $1,
eventDecoder: $2,
eventNotificationCenter: $3
eventDecoder: $1,
eventNotificationCenter: $2,
webSocketClientType: .coordinator,
connectRequest: nil
)
}

Expand All @@ -57,7 +57,7 @@ extension ChatClient {

var eventDecoderBuilder: @Sendable () -> EventDecoder = { EventDecoder() }

var notificationCenterBuilder: @Sendable (_ database: DatabaseContainer, _ manualEventHandler: ManualEventHandler?) -> EventNotificationCenter = { EventNotificationCenter(database: $0, manualEventHandler: $1) }
var notificationCenterBuilder: @Sendable (_ database: DatabaseContainer, _ manualEventHandler: ManualEventHandler?) -> EventPersistentNotificationCenter = { EventPersistentNotificationCenter(database: $0, manualEventHandler: $1) }

var internetConnection: @Sendable (_ center: NotificationCenter, _ monitor: InternetConnectionMonitor) -> InternetConnection = {
InternetConnection(notificationCenter: $0, monitor: $1)
Expand All @@ -76,16 +76,18 @@ extension ChatClient {
var connectionRepositoryBuilder: @Sendable (
_ isClientInActiveMode: Bool,
_ syncRepository: SyncRepository,
_ webSocketRequestEncoder: RequestEncoder?,
_ webSocketClient: WebSocketClient?,
_ apiClient: APIClient,
_ timerType: Timer.Type
_ timerType: TimerScheduling.Type
) -> ConnectionRepository = {
ConnectionRepository(
isClientInActiveMode: $0,
syncRepository: $1,
webSocketClient: $2,
apiClient: $3,
timerType: $4
webSocketRequestEncoder: $2,
webSocketClient: $3,
apiClient: $4,
timerType: $5
)
}

Expand All @@ -103,27 +105,25 @@ extension ChatClient {
}
}

var timerType: Timer.Type = DefaultTimer.self
var timerType: TimerScheduling.Type = DefaultTimer.self

var tokenExpirationRetryStrategy: RetryStrategy = DefaultRetryStrategy()

var connectionRecoveryHandlerBuilder: @Sendable (
_ webSocketClient: WebSocketClient,
_ eventNotificationCenter: EventNotificationCenter,
_ syncRepository: SyncRepository,
_ backgroundTaskScheduler: BackgroundTaskScheduler?,
_ internetConnection: InternetConnection,
_ keepConnectionAliveInBackground: Bool
) -> ConnectionRecoveryHandler = {
DefaultConnectionRecoveryHandler(
webSocketClient: $0,
eventNotificationCenter: $1,
syncRepository: $2,
backgroundTaskScheduler: $3,
internetConnection: $4,
backgroundTaskScheduler: $2,
internetConnection: $3,
reconnectionStrategy: DefaultRetryStrategy(),
reconnectionTimerType: DefaultTimer.self,
keepConnectionAliveInBackground: $5
reconnectionTimerType: StreamCore.DefaultTimer.self,
keepConnectionAliveInBackground: $4
)
}

Expand All @@ -132,7 +132,7 @@ extension ChatClient {
_ databaseContainer: DatabaseContainer,
_ connectionRepository: ConnectionRepository,
_ tokenExpirationRetryStrategy: RetryStrategy,
_ timerType: Timer.Type
_ timerType: TimerScheduling.Type
) -> AuthenticationRepository = {
AuthenticationRepository(
apiClient: $0,
Expand Down
12 changes: 8 additions & 4 deletions Sources/StreamChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class ChatClient: @unchecked Sendable {
private(set) var connectionRecoveryHandler: ConnectionRecoveryHandler?

/// The notification center used to send and receive notifications about incoming events.
private(set) var eventNotificationCenter: EventNotificationCenter
private(set) var eventNotificationCenter: EventPersistentNotificationCenter

/// The registry that contains all the attachment payloads associated with their attachment types.
/// For the meantime this is a static property to avoid breaking changes. On v5, this can be changed.
Expand Down Expand Up @@ -99,6 +99,7 @@ public class ChatClient: @unchecked Sendable {

/// The `WebSocketClient` instance `Client` uses to communicate with Stream WS servers.
let webSocketClient: WebSocketClient?
let webSocketRequestEncoder: RequestEncoder?

/// The `DatabaseContainer` instance `Client` uses to store and cache data.
let databaseContainer: DatabaseContainer
Expand Down Expand Up @@ -184,13 +185,13 @@ public class ChatClient: @unchecked Sendable {
channelListUpdater
)
let webSocketClient = factory.makeWebSocketClient(
requestEncoder: webSocketEncoder,
urlSessionConfiguration: urlSessionConfiguration,
eventNotificationCenter: eventNotificationCenter
)
let connectionRepository = environment.connectionRepositoryBuilder(
config.isClientInActiveMode,
syncRepository,
webSocketEncoder,
webSocketClient,
apiClient,
environment.timerType
Expand All @@ -207,6 +208,7 @@ public class ChatClient: @unchecked Sendable {
self.databaseContainer = databaseContainer
self.apiClient = apiClient
self.webSocketClient = webSocketClient
self.webSocketRequestEncoder = webSocketEncoder
self.eventNotificationCenter = eventNotificationCenter
self.offlineRequestsRepository = offlineRequestsRepository
self.connectionRepository = connectionRepository
Expand Down Expand Up @@ -268,7 +270,6 @@ public class ChatClient: @unchecked Sendable {
connectionRecoveryHandler = environment.connectionRecoveryHandlerBuilder(
webSocketClient,
eventNotificationCenter,
syncRepository,
environment.backgroundTaskSchedulerBuilder(),
environment.internetConnection(eventNotificationCenter, environment.internetMonitor),
config.staysConnectedInBackground
Expand Down Expand Up @@ -718,7 +719,7 @@ extension ChatClient: AuthenticationRepositoryDelegate {
}

extension ChatClient: ConnectionStateDelegate {
func webSocketClient(_ client: WebSocketClient, didUpdateConnectionState state: WebSocketConnectionState) {
public func webSocketClient(_ client: WebSocketClient, didUpdateConnectionState state: WebSocketConnectionState) {
connectionRepository.handleConnectionUpdate(
state: state,
onExpiredToken: { [weak self] in
Expand All @@ -735,6 +736,9 @@ extension ChatClient: ConnectionStateDelegate {
}
case .connected:
reconnectionTimeoutHandler?.stop()
syncRepository.syncLocalState {
log.info("Local state sync completed", subsystems: .offlineSupport)
}
default:
break
}
Expand Down
4 changes: 1 addition & 3 deletions Sources/StreamChat/ChatClientFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,11 @@ class ChatClientFactory {
}

func makeWebSocketClient(
requestEncoder: RequestEncoder,
urlSessionConfiguration: URLSessionConfiguration,
eventNotificationCenter: EventNotificationCenter
) -> WebSocketClient? {
environment.webSocketClientBuilder?(
urlSessionConfiguration,
requestEncoder,
EventDecoder(),
eventNotificationCenter
)
Expand Down Expand Up @@ -114,7 +112,7 @@ class ChatClientFactory {
func makeEventNotificationCenter(
databaseContainer: DatabaseContainer,
currentUserId: @escaping () -> UserId?
) -> EventNotificationCenter {
) -> EventPersistentNotificationCenter {
let center = environment.notificationCenterBuilder(databaseContainer, nil)
let middlewares: [EventMiddleware] = [
EventDataProcessorMiddleware(),
Expand Down
18 changes: 0 additions & 18 deletions Sources/StreamChat/Config/ChatClientConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,21 +227,3 @@ extension ChatClientConfig {
public var latestMessagesLimit = 5
}
}

/// A struct representing an API key of the chat app.
///
/// An API key can be obtained by registering on [our website](https://getstream.io/chat/trial/\).
///
public struct APIKey: Equatable, Sendable {
/// The string representation of the API key
public let apiKeyString: String

/// Creates a new `APIKey` from the provided string. Fails, if the string is empty.
///
/// - Warning: The `apiKeyString` must be a non-empty value, otherwise an assertion failure is raised.
///
public init(_ apiKeyString: String) {
log.assert(apiKeyString.isEmpty == false, "APIKey can't be initialize with an empty string.")
self.apiKeyString = apiKeyString
}
}
4 changes: 0 additions & 4 deletions Sources/StreamChat/Config/Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ public extension Token {
}
}

extension ClientError {
public final class InvalidToken: ClientError, @unchecked Sendable {}
}

private extension String {
var jwtPayload: [String: Any]? {
let parts = split(separator: ".")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,26 @@ public class PollVoteListController: DataController, DelegateCallable, DataStore
super.init()
eventsObserver = client.subscribe { [weak self] event in
guard let self else { return }
var vote: PollVote?
if let event = event as? PollVoteCastedEvent {
vote = event.vote
} else if let event = event as? PollVoteChangedEvent {
vote = event.vote
}
guard let vote else { return }
if vote.isAnswer == true && self.query.pollId == vote.pollId && self.query.optionId == nil {
self.pollsRepository.link(pollVote: vote, to: query)
}
self.didReceiveEvent(event)
}
}

func didReceiveEvent(_ event: Event) {
var vote: PollVote?
if let event = event as? PollVoteCastedEvent {
vote = event.vote
} else if let event = event as? PollVoteChangedEvent {
vote = event.vote
}
guard let vote else { return }
if vote.isAnswer == true
&& query.pollId == vote.pollId
&& query.optionId == nil {
pollsRepository.link(pollVote: vote, to: query)
} else if vote.isAnswer == false
&& query.pollId == vote.pollId
&& query.optionId == vote.optionId {
pollsRepository.link(pollVote: vote, to: query)
}
}

Expand Down Expand Up @@ -270,24 +280,3 @@ extension PollVoteListController {
}
}
}

extension PollVoteListController: EventsControllerDelegate {
public func eventsController(_ controller: EventsController, didReceiveEvent event: any Event) {
var vote: PollVote?
if let event = event as? PollVoteCastedEvent {
vote = event.vote
} else if let event = event as? PollVoteChangedEvent {
vote = event.vote
}
guard let vote else { return }
if vote.isAnswer == true
&& query.pollId == vote.pollId
&& query.optionId == nil {
pollsRepository.link(pollVote: vote, to: query)
} else if vote.isAnswer == false
&& query.pollId == vote.pollId
&& query.optionId == vote.optionId {
pollsRepository.link(pollVote: vote, to: query)
}
}
}
73 changes: 2 additions & 71 deletions Sources/StreamChat/Errors/ClientError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,81 +4,12 @@

import Foundation

/// A Client error.
public class ClientError: Error, CustomStringConvertible, @unchecked Sendable {
public struct Location: Equatable {
public let file: String
public let line: Int
}

/// The file and line number which emitted the error.
public let location: Location?

public let message: String?

/// An underlying error.
public let underlyingError: Error?

public var errorDescription: String? { underlyingError.map(String.init(describing:)) }

/// The error payload if the underlying error comes from a server error.
public var errorPayload: ErrorPayload? { underlyingError as? ErrorPayload }

/// Retrieve the localized description for this error.
public var localizedDescription: String { message ?? errorDescription ?? "" }

public private(set) lazy var description = "Error \(type(of: self)) in \(location?.file ?? ""):\(location?.line ?? 0)"
+ (localizedDescription.isEmpty ? "" : " -> ")
+ localizedDescription

/// A client error based on an external general error.
/// - Parameters:
/// - error: an external error.
/// - file: a file name source of an error.
/// - line: a line source of an error.
public init(with error: Error? = nil, _ file: StaticString = #file, _ line: UInt = #line) {
message = nil
underlyingError = error
location = .init(file: "\(file)", line: Int(line))
}

/// An error based on a message.
/// - Parameters:
/// - message: an error message.
/// - file: a file name source of an error.
/// - line: a line source of an error.
public init(_ message: String, _ file: StaticString = #file, _ line: UInt = #line) {
self.message = message
location = .init(file: "\(file)", line: Int(line))
underlyingError = nil
}
}

extension ClientError {
/// An unexpected error.
public final class Unexpected: ClientError, @unchecked Sendable {}

/// An unknown error.
public final class Unknown: ClientError, @unchecked Sendable {}
}

// This should probably live only in the test target since it's not "true" equatable
extension ClientError: Equatable {
public static func == (lhs: ClientError, rhs: ClientError) -> Bool {
type(of: lhs) == type(of: rhs)
&& String(describing: lhs.underlyingError) == String(describing: rhs.underlyingError)
&& String(describing: lhs.localizedDescription) == String(describing: rhs.localizedDescription)
}
}

extension ClientError {
/// Returns `true` the stream code determines that the token is expired.
var isExpiredTokenError: Bool {
(underlyingError as? ErrorPayload)?.isExpiredTokenError == true
}

/// Returns `true` if underlaying error is `ErrorPayload` with code is inside invalid token codes range.
var isInvalidTokenError: Bool {
(underlyingError as? ErrorPayload)?.isInvalidTokenError == true
}
/// The error payload if the underlying error comes from a server error.
public var errorPayload: ErrorPayload? { underlyingError as? ErrorPayload }
}
Loading
Loading