Skip to content

Commit

Permalink
Added typing tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
maratal committed Jan 19, 2025
1 parent 371eabd commit 612bbc8
Show file tree
Hide file tree
Showing 3 changed files with 400 additions and 5 deletions.
80 changes: 75 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 maxPresenseGetRetryDuration: 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, maxPresenseGetRetryDuration: TimeInterval = 30.0) {
self.roomID = roomID
self.featureChannel = featureChannel
self.clientID = clientID
self.logger = logger
self.timeout = timeout
self.maxPresenseGetRetryDuration = maxPresenseGetRetryDuration
}

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 < maxPresenseGetRetryDuration {
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 \(maxPresenseGetRetryDuration) 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,67 @@ 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 {
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
}

/// 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

0 comments on commit 612bbc8

Please sign in to comment.