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
7 changes: 0 additions & 7 deletions Sources/StreamAttachments/HTTP/RequestDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,6 @@ extension ClientError {
}
}

extension ErrorPayload {
/// Returns `true` if the code determines that the token is expired.
var isExpiredTokenError: Bool {
code == StreamErrorCode.expiredToken
}
}

/// https://getstream.io/chat/docs/ios-swift/api_errors_response/
enum StreamErrorCode {
/// Usually returned when trying to perform an API call without a token.
Expand Down
8 changes: 8 additions & 0 deletions Sources/StreamCore/Concurrency/Atomic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public final class Atomic<T>: @unchecked Sendable {
queue.sync { _value = changes(_value) }
}

public func mutate(_ changes: (_ value: inout T) -> Void) {
mutate { value in
var updated = value
changes(&updated)
return updated
}
}

/// Update the value safely.
/// - Parameter changes: a block with changes. It should return a new value.
public func callAsFunction(_ changes: (_ value: T) -> T) { mutate(changes) }
Expand Down
11 changes: 8 additions & 3 deletions Sources/StreamCore/Errors/ErrorPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@ public struct ErrorPayload: LocalizedError, Codable, CustomDebugStringConvertibl
}

extension ErrorPayload {
/// Returns `true` if the code determines that the token is expired.
public var isExpiredTokenError: Bool {
code == StreamErrorCode.expiredToken
}

/// Returns `true` if code is withing invalid token codes range.
var isInvalidTokenError: Bool {
ClosedRange.tokenInvalidErrorCodes ~= code
public var isInvalidTokenError: Bool {
ClosedRange.tokenInvalidErrorCodes ~= code || code == StreamErrorCode.accessKeyInvalid
}
Comment on lines +38 to 40
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From Chat's implementation


/// Returns `true` if status code is withing client error codes range.
var isClientError: Bool {
public var isClientError: Bool {
ClosedRange.clientErrorCodes ~= statusCode
}
}
12 changes: 11 additions & 1 deletion Sources/StreamCore/Errors/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ open class ClientError: Error, ReflectiveStringConvertible, @unchecked Sendable

public let apiError: APIError?

var errorDescription: String? {
public var errorDescription: String? {
if let apiError {
apiError.message
} else {
Expand Down Expand Up @@ -138,3 +138,13 @@ struct APIErrorContainer: Codable {
}

extension APIError: Error {}

/// https://getstream.io/chat/docs/ios-swift/api_errors_response/
enum StreamErrorCode {
/// Usually returned when trying to perform an API call without a token.
static let accessKeyInvalid = 2
static let expiredToken = 40
static let notYetValidToken = 41
static let invalidTokenDate = 42
static let invalidTokenSignature = 43
}
2 changes: 1 addition & 1 deletion Sources/StreamCore/Utils/Data+Gzip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ extension Data {
///
/// - Returns: Gzip-compressed `Data` instance.
/// - Throws: `GzipError`
public func gzipped() throws -> Data {
func gzipped() throws -> Data {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No need to be public anymore (previous PR did that)

guard !isEmpty else {
return Data()
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamCore/Utils/InternetConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extension Notification {
///
/// Basically, it's a wrapper over legacy monitor based on `Reachability` (iOS 11 only)
/// and default monitor based on `Network`.`NWPathMonitor` (iOS 12+).
public final class InternetConnection: @unchecked Sendable {
open class InternetConnection: @unchecked Sendable {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test tools need to subclass it on the chat side

/// The current Internet connection status.
@Published private(set) var status: InternetConnectionStatus {
didSet {
Expand Down
6 changes: 3 additions & 3 deletions Sources/StreamCore/Utils/Timers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import Combine
import Foundation

public protocol StreamTimer {
public protocol TimerScheduling {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Naming conflict: did not know there was StreamTimer in the UIKit module and Timer was conflicting with Foundation.

/// Schedules a new timer.
///
/// - Parameters:
Expand Down Expand Up @@ -33,7 +33,7 @@ public protocol StreamTimer {
static func currentTime() -> Date
}

extension StreamTimer {
extension TimerScheduling {
public static func currentTime() -> Date {
Date()
}
Expand All @@ -58,7 +58,7 @@ extension DispatchWorkItem: TimerControl {}
extension DispatchWorkItem: @retroactive @unchecked Sendable {}

/// Default real-world implementations of timers.
public struct DefaultTimer: StreamTimer {
public struct DefaultTimer: TimerScheduling {
@discardableResult
public static func schedule(
timeInterval: TimeInterval,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class IOSBackgroundTaskScheduler: BackgroundTaskScheduler, @unchecked Sen

@MainActor
public func beginTask(expirationHandler: (@Sendable () -> Void)?) -> Bool {
endTask()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From Chat

activeBackgroundTask = app?.beginBackgroundTask { [weak self] in
self?._endTask()
expirationHandler?()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import CoreData
import Foundation

/// The type that keeps track of active chat components and asks them to reconnect when it's needed
public protocol ConnectionRecoveryHandler: ConnectionStateDelegate, Sendable {}
public protocol ConnectionRecoveryHandler: ConnectionStateDelegate, Sendable {
func start()
func stop()
}
Comment on lines +9 to +12
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Chat needs this to support some flows


/// The type is designed to obtain missing events that happened in watched channels while user
/// was not connected to the web-socket.
Expand All @@ -24,7 +27,7 @@ public final class DefaultConnectionRecoveryHandler: ConnectionRecoveryHandler,
private let eventNotificationCenter: EventNotificationCenter
private let backgroundTaskScheduler: BackgroundTaskScheduler?
private let internetConnection: InternetConnection
private let reconnectionTimerType: StreamTimer.Type
private let reconnectionTimerType: TimerScheduling.Type
private let keepConnectionAliveInBackground: Bool
private nonisolated(unsafe) var reconnectionStrategy: RetryStrategy
private nonisolated(unsafe) var reconnectionTimer: TimerControl?
Expand All @@ -38,7 +41,7 @@ public final class DefaultConnectionRecoveryHandler: ConnectionRecoveryHandler,
backgroundTaskScheduler: BackgroundTaskScheduler?,
internetConnection: InternetConnection,
reconnectionStrategy: RetryStrategy,
reconnectionTimerType: StreamTimer.Type,
reconnectionTimerType: TimerScheduling.Type,
keepConnectionAliveInBackground: Bool
) {
self.init(
Expand All @@ -51,7 +54,6 @@ public final class DefaultConnectionRecoveryHandler: ConnectionRecoveryHandler,
keepConnectionAliveInBackground: keepConnectionAliveInBackground,
reconnectionPolicies: [
WebSocketAutomaticReconnectionPolicy(webSocketClient),
InternetAvailabilityReconnectionPolicy(internetConnection),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is interesting, I struggled with reconnection when building and running demo app and as soon as I dropped this, things started to work. I spent a day on this, so not planning to spend more time on this. Chat's recovery handler also did not check for internet connection (makes sense since the monitor is not 100% accurate and URLRequest can trigger connection establishment even when the monitor says there is no connection. This is also what Apple suggest (can't remember which WWDC on top of my head))

BackgroundStateReconnectionPolicy(backgroundTaskScheduler)
]
)
Expand All @@ -63,7 +65,7 @@ public final class DefaultConnectionRecoveryHandler: ConnectionRecoveryHandler,
backgroundTaskScheduler: BackgroundTaskScheduler?,
internetConnection: InternetConnection,
reconnectionStrategy: RetryStrategy,
reconnectionTimerType: StreamTimer.Type,
reconnectionTimerType: TimerScheduling.Type,
keepConnectionAliveInBackground: Bool,
reconnectionPolicies: [AutomaticReconnectionPolicy]
) {
Expand All @@ -75,14 +77,22 @@ public final class DefaultConnectionRecoveryHandler: ConnectionRecoveryHandler,
self.reconnectionTimerType = reconnectionTimerType
self.keepConnectionAliveInBackground = keepConnectionAliveInBackground
self.reconnectionPolicies = reconnectionPolicies

subscribeOnNotifications()
start()
}

deinit {
public func start() {
subscribeOnNotifications()
}

public func stop() {
unsubscribeFromNotifications()
cancelReconnectionTimer()
}

deinit {
stop()
}
}

// MARK: - Subscriptions
Expand All @@ -94,21 +104,19 @@ private extension DefaultConnectionRecoveryHandler {
onEnteringBackground: { [weak self] in self?.appDidEnterBackground() },
onEnteringForeground: { [weak self] in self?.appDidBecomeActive() }
)

internetConnection.notificationCenter.addObserver(
self,
selector: #selector(internetConnectionAvailabilityDidChange(_:)),
name: .internetConnectionAvailabilityDidChange,
object: nil
)
}
internetConnection.notificationCenter.addObserver(
self,
selector: #selector(internetConnectionAvailabilityDidChange(_:)),
name: .internetConnectionAvailabilityDidChange,
object: nil
)
}

func unsubscribeFromNotifications() {
Task { @MainActor [backgroundTaskScheduler] in
backgroundTaskScheduler?.stopListeningForAppStateUpdates()
}

internetConnection.notificationCenter.removeObserver(
self,
name: .internetConnectionStatusDidChange,
Expand Down
17 changes: 10 additions & 7 deletions Sources/StreamCore/WebSocket/Client/ConnectionStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,24 @@ public extension ConnectionStatus {
self = .disconnecting

case let .disconnected(source):
let isWaitingForReconnect = webSocketConnectionState.isAutomaticReconnectionEnabled || source.serverError?
.isInvalidTokenError == true

let isWaitingForReconnect = webSocketConnectionState.isAutomaticReconnectionEnabled
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From chat

self = isWaitingForReconnect ? .connecting : .disconnected(error: source.serverError)
}
}
}

typealias ConnectionId = String
public typealias ConnectionId = String

/// A web socket connection state.
public enum WebSocketConnectionState: Equatable {
public enum WebSocketConnectionState: Equatable, Sendable {
/// Provides additional information about the source of disconnecting.
public enum DisconnectionSource: Equatable {
public indirect enum DisconnectionSource: Equatable, Sendable {
/// A user initiated web socket disconnecting.
case userInitiated

/// The connection timed out while trying to connect.
case timeout(from: WebSocketConnectionState)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Chat needs this state for special reconnection flows


/// A server initiated web socket disconnecting, an optional error object is provided.
case serverInitiated(error: ClientError? = nil)

Expand Down Expand Up @@ -95,7 +96,7 @@ public enum WebSocketConnectionState: Equatable {
case disconnecting(source: DisconnectionSource)

/// Checks if the connection state is connected.
var isConnected: Bool {
public var isConnected: Bool {
if case .connected = self {
return true
}
Expand Down Expand Up @@ -141,6 +142,8 @@ public enum WebSocketConnectionState: Equatable {
return true
case .userInitiated:
return false
case .timeout:
return false
}
}
}
Loading