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-5132] Added typing tests #217

Open
wants to merge 1 commit into
base: 114-tests
Choose a base branch
from
Open
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
81 changes: 76 additions & 5 deletions Sources/AblyChat/DefaultTyping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ internal final class DefaultTyping: Typing {
private let clientID: String
private let logger: InternalLogger
private let timeout: TimeInterval
private let maxPresenceGetRetryDuration: TimeInterval // Max duration as specified in CHA-T6c1
private let timerManager = TimerManager()

internal init(featureChannel: FeatureChannel, roomID: String, clientID: String, logger: InternalLogger, timeout: TimeInterval) {
internal init(featureChannel: FeatureChannel, roomID: String, clientID: String, logger: InternalLogger, timeout: TimeInterval, maxPresenceGetRetryDuration: TimeInterval = 30.0) {
self.roomID = roomID
self.featureChannel = featureChannel
self.clientID = clientID
self.logger = logger
self.timeout = timeout
self.maxPresenceGetRetryDuration = maxPresenceGetRetryDuration
}

internal nonisolated var channel: any RealtimeChannelProtocol {
Expand All @@ -32,18 +34,21 @@ internal final class DefaultTyping: Typing {
logger.log(message: "Received presence message: \(message)", level: .debug)
Task {
let currentEventID = await eventTracker.updateEventID()
let maxRetryDuration: TimeInterval = 30.0 // Max duration as specified in CHA-T6c1
let baseDelay: TimeInterval = 1.0 // Initial retry delay
let maxDelay: TimeInterval = 5.0 // Maximum delay between retries

var totalElapsedTime: TimeInterval = 0
var delay: TimeInterval = baseDelay

while totalElapsedTime < maxRetryDuration {
while totalElapsedTime < maxPresenceGetRetryDuration {
do {
// (CHA-T6c) When a presence event is received from the realtime client, the Chat client will perform a presence.get() operation to get the current presence set. This guarantees that we get a fully synced presence set. This is then used to emit the typing clients to the subscriber.
let latestTypingMembers = try await get()

#if DEBUG
for subscription in testPresenceGetTypingEventSubscriptions {
subscription.emit(.init())
}
#endif
// (CHA-T6c2) If multiple presence events are received resulting in concurrent presence.get() calls, then we guarantee that only the “latest” event is emitted. That is to say, if presence event A and B occur in that order, then only the typing event generated by B’s call to presence.get() will be emitted to typing subscribers.
let isLatestEvent = await eventTracker.isLatestEvent(currentEventID)
guard isLatestEvent else {
Expand All @@ -67,9 +72,14 @@ internal final class DefaultTyping: Typing {

// Exponential backoff (double the delay)
delay = min(delay * 2, maxDelay)
#if DEBUG
for subscription in testPresenceGetRetryTypingEventSubscriptions {
subscription.emit(.init())
}
#endif
}
}
logger.log(message: "Failed to fetch presence set after \(maxRetryDuration) seconds. Giving up.", level: .error)
logger.log(message: "Failed to fetch presence set after \(maxPresenceGetRetryDuration) seconds. Giving up.", level: .error)
}
}

Expand Down Expand Up @@ -160,6 +170,11 @@ internal final class DefaultTyping: Typing {
// (CHA-T5b) If typing is in progress, he CHA-T3 timeout is cancelled. The client then leaves presence.
await timerManager.cancelTimer()
channel.presence.leaveClient(clientID, data: nil)
#if DEBUG
for subscription in testStopTypingEventSubscriptions {
subscription.emit(.init())
}
#endif
} else {
// (CHA-T5a) If typing is not in progress, this operation is no-op.
logger.log(message: "User is not typing. No need to leave presence.", level: .debug)
Expand Down Expand Up @@ -209,12 +224,68 @@ internal final class DefaultTyping: Typing {
try await stop()
}
}
#if DEBUG
for subscription in testStartTypingEventSubscriptions {
subscription.emit(.init())
}
#endif
}
}
}
}

#if DEBUG
/// The `DefaultTyping` emits a `TestTypingEvent` each time ``start`` or ``stop`` is called.
internal struct TestTypingEvent: Equatable {
internal let timestamp = Date()
}

/// Subscription of typing start events for testing purposes.
private var testStartTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []

/// Subscription of typing stop events for testing purposes.
private var testStopTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []

/// Subscription of presence get events for testing purposes.
private var testPresenceGetTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []

/// Subscription of retry presence get events for testing purposes.
private var testPresenceGetRetryTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []

/// Returns a subscription which emits typing start events for testing purposes.
internal func testsOnly_subscribeToStartTestTypingEvents() -> Subscription<TestTypingEvent> {
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
testStartTypingEventSubscriptions.append(subscription)
return subscription
}

/// Returns a subscription which emits typing stop events for testing purposes.
internal func testsOnly_subscribeToStopTestTypingEvents() -> Subscription<TestTypingEvent> {
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
testStopTypingEventSubscriptions.append(subscription)
return subscription
}

maratal marked this conversation as resolved.
Show resolved Hide resolved
/// Returns a subscription which emits presence get events for testing purposes.
internal func testsOnly_subscribeToPresenceGetTypingEvents() -> Subscription<TestTypingEvent> {
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
testPresenceGetTypingEventSubscriptions.append(subscription)
return subscription
}

/// Returns a subscription which emits retry presence get events for testing purposes.
internal func testsOnly_subscribeToPresenceGetRetryTypingEvents() -> Subscription<TestTypingEvent> {
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
testPresenceGetRetryTypingEventSubscriptions.append(subscription)
return subscription
}
#endif
}

#if DEBUG
extension DefaultTyping: @unchecked Sendable {}
#endif

private final actor EventTracker {
private var latestEventID: UUID = .init()

Expand Down
Loading
Loading