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 DemoApp/Screens/DemoReminderListVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ extension DemoReminderListVC: MessageReminderListControllerDelegate, EventsContr
updateRemindersData()
}

func eventsController(_ controller: EventsController, didReceiveEvent event: any StreamChat.Event) {
func eventsController(_ controller: EventsController, didReceiveEvent event: any Event) {
if event is MessageReminderDueEvent {
updateReminderListsWithNewNowDate()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ class DemoLivestreamChatChannelVC: _ViewController,
messageListVC.scrollToBottomButton.content = .init(messages: skippedMessagesAmount, mentions: 0)
}

func eventsController(_ controller: EventsController, didReceiveEvent event: any StreamChat.Event) {
func eventsController(_ controller: EventsController, didReceiveEvent event: any Event) {
if event is NewMessagePendingEvent {
if livestreamChannelController.isPaused {
pauseBannerView.setState(.resuming)
Expand Down
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", branch: "chat-json-encoding-decoding")
.package(url: "https://github.com/GetStream/stream-core-swift.git", exact: "0.5.0")
],
targets: [
.target(
Expand Down
6 changes: 3 additions & 3 deletions Sources/StreamChat/APIClient/RequestDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ struct DefaultRequestDecoder: RequestDecoder {
log.debug("URL request response: \(httpResponse), data:\n\(data.debugPrettyPrintedJSON))", subsystems: .httpRequests)

guard httpResponse.statusCode < 300 else {
let serverError: ErrorPayload
let serverError: APIError
do {
serverError = try JSONDecoder.default.decode(ErrorPayload.self, from: data)
serverError = try JSONDecoder.default.decode(APIError.self, from: data)
} catch {
log
.error(
Expand All @@ -58,7 +58,7 @@ struct DefaultRequestDecoder: RequestDecoder {
throw ClientError.Unknown("Unknown error. Server response: \(httpResponse).")
}

if serverError.isExpiredTokenError {
if serverError.isTokenExpiredError {
log.info("Request failed because of an expired token.", subsystems: .httpRequests)
throw ClientError.ExpiredToken()
}
Expand Down
33 changes: 18 additions & 15 deletions Sources/StreamChat/ChatClient+Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ extension ChatClient {

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

Expand All @@ -57,7 +60,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?) -> PersistentEventNotificationCenter = { PersistentEventNotificationCenter(database: $0, manualEventHandler: $1) }

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

Expand All @@ -110,20 +115,18 @@ extension ChatClient {
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
keepConnectionAliveInBackground: $4
)
}

Expand Down
9 changes: 5 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: PersistentEventNotificationCenter

/// 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 webSocketEncoder: 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.webSocketEncoder = 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 Down
6 changes: 2 additions & 4 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
eventNotificationCenter: PersistentEventNotificationCenter
) -> 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 {
) -> PersistentEventNotificationCenter {
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
}
}
42 changes: 35 additions & 7 deletions Sources/StreamChat/Repositories/ConnectionRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,25 @@ class ConnectionRepository: @unchecked Sendable {
set { connectionQueue.async(flags: .barrier) { self._connectionId = newValue }}
}

let webSocketConnectEndpoint = AllocatedUnfairLock<Endpoint<EmptyResponse>?>(nil)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Endpoint used to be in WebSocketClient, but the one from StreamCore does not use endpoint type, so this logic was moved to here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, and it would make it easier to migrate away from endpoints to generated code.

let isClientInActiveMode: Bool
private let syncRepository: SyncRepository
private let webSocketEncoder: RequestEncoder?
private let webSocketClient: WebSocketClient?
private let apiClient: APIClient
private let timerType: TimerScheduling.Type

init(
isClientInActiveMode: Bool,
syncRepository: SyncRepository,
webSocketEncoder: RequestEncoder?,
webSocketClient: WebSocketClient?,
apiClient: APIClient,
timerType: TimerScheduling.Type
) {
self.isClientInActiveMode = isClientInActiveMode
self.syncRepository = syncRepository
self.webSocketEncoder = webSocketEncoder
self.webSocketClient = webSocketClient
self.apiClient = apiClient
self.timerType = timerType
Expand Down Expand Up @@ -80,6 +84,7 @@ class ConnectionRepository: @unchecked Sendable {
}
}
}
updateWebSocketConnectURLRequest()
webSocketClient?.connect()
}

Expand Down Expand Up @@ -114,29 +119,52 @@ class ConnectionRepository: @unchecked Sendable {

/// Updates the WebSocket endpoint to use the passed token and user information for the connection
func updateWebSocketEndpoint(with token: Token, userInfo: UserInfo?) {
webSocketClient?.connectEndpoint = .webSocketConnect(userInfo: userInfo ?? .init(id: token.userId))
webSocketConnectEndpoint.value = .webSocketConnect(userInfo: userInfo ?? .init(id: token.userId))
}

/// Updates the WebSocket endpoint to use the passed user id
func updateWebSocketEndpoint(with currentUserId: UserId) {
webSocketClient?.connectEndpoint = .webSocketConnect(userInfo: UserInfo(id: currentUserId))
webSocketConnectEndpoint.value = .webSocketConnect(userInfo: UserInfo(id: currentUserId))
}

private func updateWebSocketConnectURLRequest() {
guard let webSocketClient, let webSocketEncoder, let webSocketConnectEndpoint = webSocketConnectEndpoint.value else { return }
let request: URLRequest? = {
do {
return try webSocketEncoder.encodeRequest(for: webSocketConnectEndpoint)
} catch {
log.error(error.localizedDescription, error: error)
return nil
}
}()
guard let request else { return }
webSocketClient.connectRequest = request
}

func handleConnectionUpdate(
state: WebSocketConnectionState,
onExpiredToken: () -> Void
) {
let event = ConnectionStatusUpdated(webSocketConnectionState: state)
if event.connectionStatus != connectionStatus {
// Publish Connection event with the new state
webSocketClient?.publishEvent(event)
}
Comment on lines +148 to +152
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used to be in WebSocketClient. Since this is a custom event, I left it here instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense


connectionStatus = .init(webSocketConnectionState: state)

// We should notify waiters if connectionId was obtained (i.e. state is .connected)
// or for .disconnected state except for disconnect caused by an expired token
let shouldNotifyConnectionIdWaiters: Bool
let connectionId: String?
switch state {
case let .connected(connectionId: id):
case let .connected(healthCheckInfo: healthCheckInfo):
shouldNotifyConnectionIdWaiters = true
connectionId = id
case let .disconnected(source) where source.serverError?.isExpiredTokenError == true:
connectionId = healthCheckInfo.connectionId
syncRepository.syncLocalState {
log.info("Local state sync completed", subsystems: .offlineSupport)
}
Comment on lines +164 to +166
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was in WebSocketClient, now it is here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this is chat specific logic

case let .disconnected(source) where source.serverError?.isTokenExpiredError == true:
onExpiredToken()
shouldNotifyConnectionIdWaiters = false
connectionId = nil
Expand All @@ -146,7 +174,7 @@ class ConnectionRepository: @unchecked Sendable {
case .initialized,
.connecting,
.disconnecting,
.waitingForConnectionId:
.authenticating:
shouldNotifyConnectionIdWaiters = false
connectionId = nil
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChat/Repositories/MessageRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ class MessageRepository: @unchecked Sendable {
) {
log.error("Sending the message with id \(messageId) failed with error: \(error)")

if let clientError = error as? ClientError, let errorPayload = clientError.errorPayload {
if let clientError = error as? ClientError, let errorPayload = clientError.apiError {
// If the message already exists on the server we do not want to mark it as failed,
// since this will cause an unrecoverable state, where the user will keep resending
// the message and it will always fail. Right now, the only way to check this error is
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChat/StateLayer/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1501,7 +1501,7 @@ extension Chat {
func dispatchSubscribeHandler<E>(_ event: E, callback: @escaping @Sendable (E) -> Void) where E: Event {
Task.mainActor {
guard let cid = try? self.cid else { return }
guard EventNotificationCenter.channelFilter(cid: cid, event: event) else { return }
guard PersistentEventNotificationCenter.channelFilter(cid: cid, event: event) else { return }
callback(event)
}
}
Expand Down
Loading
Loading