Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECO-4983] Implement the "transient disconnect timeouts" parts of room lifecycle spec #87

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 Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ struct ContentView: View {
func showRoomStatus() async throws {
for await status in try await room().status.onChange(bufferingPolicy: .unbounded) {
withAnimation {
if status.current == .attaching {
if status.current.isAttaching {
statusInfo = "\(status.current)...".capitalized
} else {
statusInfo = "\(status.current)".capitalized
Expand Down
2 changes: 1 addition & 1 deletion Example/AblyChatExample/Mocks/MockClients.swift
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ actor MockRoomStatus: RoomStatus {

private func createSubscription() -> MockSubscription<RoomStatusChange> {
let subscription = MockSubscription<RoomStatusChange>(randomElement: {
RoomStatusChange(current: [.attached, .attached, .attached, .attached, .attaching, .attaching, .suspended(error: .createUnknownError())].randomElement()!, previous: .attaching)
RoomStatusChange(current: [.attached, .attached, .attached, .attached, .attaching(error: nil), .attaching(error: nil), .suspended(error: .createUnknownError())].randomElement()!, previous: .attaching(error: nil))
lawrence-forooghian marked this conversation as resolved.
Show resolved Hide resolved
}, interval: 8)
mockSubscriptions.append(subscription)
return subscription
Expand Down
164 changes: 145 additions & 19 deletions Sources/AblyChat/RoomLifecycleManager.swift

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion Sources/AblyChat/RoomStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public protocol RoomStatus: AnyObject, Sendable {

public enum RoomLifecycle: Sendable, Equatable {
case initialized
case attaching
case attaching(error: ARTErrorInfo?)
case attached
case detaching
case detached
Expand All @@ -21,6 +21,14 @@ public enum RoomLifecycle: Sendable, Equatable {
// 1. testing (e.g. `#expect(status.isFailed)`)
// 2. testing that a status does _not_ have a particular case (e.g. if !status.isFailed), which a `case` statement cannot succinctly express

public var isAttaching: Bool {
if case .attaching = self {
true
} else {
false
}
}

public var isSuspended: Bool {
if case .suspended = self {
true
Expand Down
17 changes: 17 additions & 0 deletions Tests/AblyChatTests/Helpers/Subscription+RoomStatusChange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ extension Subscription where Element == RoomStatusChange {
var error: ARTErrorInfo
}

struct StatusChangeWithOptionalError {
/// A status change whose `current` has an optional associated error; ``error`` provides access to this error.
var statusChange: RoomStatusChange
/// The error associated with `statusChange.current`.
var error: ARTErrorInfo?
}

func suspendedElements() async -> AsyncCompactMapSequence<Subscription<RoomStatusChange>, Subscription<RoomStatusChange>.StatusChangeWithError> {
compactMap { statusChange in
if case let .suspended(error) = statusChange.current {
Expand All @@ -31,4 +38,14 @@ extension Subscription where Element == RoomStatusChange {
}
}
}

func attachingElements() async -> AsyncCompactMapSequence<Subscription<RoomStatusChange>, Subscription<RoomStatusChange>.StatusChangeWithOptionalError> {
compactMap { statusChange in
if case let .attaching(error) = statusChange.current {
StatusChangeWithOptionalError(statusChange: statusChange, error: error)
} else {
nil
}
}
}
}
13 changes: 11 additions & 2 deletions Tests/AblyChatTests/Helpers/TestLogger.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
@testable import AblyChat

struct TestLogger: InternalLogger {
func log(message _: String, level _: LogLevel, codeLocation _: CodeLocation) {
// No-op; currently we don’t log in tests to keep the test logs easy to read. Can reconsider if necessary.
// By default, we don’t log in tests to keep the test logs easy to read. You can set this property to `true` to temporarily turn logging on if you want to debug a test.
static let loggingEnabled = false

private let underlyingLogger = DefaultInternalLogger(logHandler: nil, logLevel: .trace)

func log(message: String, level: LogLevel, codeLocation: CodeLocation) {
guard Self.loggingEnabled else {
return
}

underlyingLogger.log(message: message, level: level, codeLocation: codeLocation)
}
}
27 changes: 27 additions & 0 deletions Tests/AblyChatTests/Mocks/MockSimpleClock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,36 @@ import Foundation

/// A mock implementation of ``SimpleClock`` which records its arguments but does not actually sleep.
actor MockSimpleClock: SimpleClock {
private let sleepBehavior: SleepBehavior

enum SleepBehavior {
case success
case fromFunction(@Sendable () async throws -> Void)
}

init(sleepBehavior: SleepBehavior? = nil) {
self.sleepBehavior = sleepBehavior ?? .success
_sleepCallArgumentsAsyncSequence = AsyncStream<TimeInterval>.makeStream()
}
lawrence-forooghian marked this conversation as resolved.
Show resolved Hide resolved

private(set) var sleepCallArguments: [TimeInterval] = []

/// Emits an element each time ``sleep(timeInterval:)`` is called.
var sleepCallArgumentsAsyncSequence: AsyncStream<TimeInterval> {
_sleepCallArgumentsAsyncSequence.stream
}

private let _sleepCallArgumentsAsyncSequence: (stream: AsyncStream<TimeInterval>, continuation: AsyncStream<TimeInterval>.Continuation)

func sleep(timeInterval: TimeInterval) async throws {
sleepCallArguments.append(timeInterval)
_sleepCallArgumentsAsyncSequence.continuation.yield(timeInterval)

switch sleepBehavior {
case .success:
break
case let .fromFunction(function):
try await function()
}
}
}
Loading