Skip to content

Commit 50dbbe6

Browse files
committed
Added typing tests.
1 parent 3fdad4d commit 50dbbe6

File tree

3 files changed

+401
-5
lines changed

3 files changed

+401
-5
lines changed

Sources/AblyChat/DefaultTyping.swift

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ internal final class DefaultTyping: Typing {
66
private let clientID: String
77
private let logger: InternalLogger
88
private let timeout: TimeInterval
9+
private let maxPresenceGetRetryDuration: TimeInterval // Max duration as specified in CHA-T6c1
910
private let timerManager = TimerManager()
1011

11-
internal init(featureChannel: FeatureChannel, roomID: String, clientID: String, logger: InternalLogger, timeout: TimeInterval) {
12+
internal init(featureChannel: FeatureChannel, roomID: String, clientID: String, logger: InternalLogger, timeout: TimeInterval, maxPresenceGetRetryDuration: TimeInterval = 30.0) {
1213
self.roomID = roomID
1314
self.featureChannel = featureChannel
1415
self.clientID = clientID
1516
self.logger = logger
1617
self.timeout = timeout
18+
self.maxPresenceGetRetryDuration = maxPresenceGetRetryDuration
1719
}
1820

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

3940
var totalElapsedTime: TimeInterval = 0
4041
var delay: TimeInterval = baseDelay
4142

42-
while totalElapsedTime < maxRetryDuration {
43+
while totalElapsedTime < maxPresenceGetRetryDuration {
4344
do {
4445
// (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.
4546
let latestTypingMembers = try await get()
46-
47+
#if DEBUG
48+
for subscription in testPresenceGetTypingEventSubscriptions {
49+
subscription.emit(.init())
50+
}
51+
#endif
4752
// (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.
4853
let isLatestEvent = await eventTracker.isLatestEvent(currentEventID)
4954
guard isLatestEvent else {
@@ -67,9 +72,14 @@ internal final class DefaultTyping: Typing {
6772

6873
// Exponential backoff (double the delay)
6974
delay = min(delay * 2, maxDelay)
75+
#if DEBUG
76+
for subscription in testPresenceGetRetryTypingEventSubscriptions {
77+
subscription.emit(.init())
78+
}
79+
#endif
7080
}
7181
}
72-
logger.log(message: "Failed to fetch presence set after \(maxRetryDuration) seconds. Giving up.", level: .error)
82+
logger.log(message: "Failed to fetch presence set after \(maxPresenceGetRetryDuration) seconds. Giving up.", level: .error)
7383
}
7484
}
7585

@@ -160,6 +170,11 @@ internal final class DefaultTyping: Typing {
160170
// (CHA-T5b) If typing is in progress, he CHA-T3 timeout is cancelled. The client then leaves presence.
161171
await timerManager.cancelTimer()
162172
channel.presence.leaveClient(clientID, data: nil)
173+
#if DEBUG
174+
for subscription in testStopTypingEventSubscriptions {
175+
subscription.emit(.init())
176+
}
177+
#endif
163178
} else {
164179
// (CHA-T5a) If typing is not in progress, this operation is no-op.
165180
logger.log(message: "User is not typing. No need to leave presence.", level: .debug)
@@ -209,12 +224,68 @@ internal final class DefaultTyping: Typing {
209224
try await stop()
210225
}
211226
}
227+
#if DEBUG
228+
for subscription in testStartTypingEventSubscriptions {
229+
subscription.emit(.init())
230+
}
231+
#endif
212232
}
213233
}
214234
}
215235
}
236+
237+
#if DEBUG
238+
/// The `DefaultTyping` emits a `TestTypingEvent` each time ``start`` or ``stop`` is called.
239+
internal struct TestTypingEvent: Equatable {
240+
internal let timestamp = Date()
241+
}
242+
243+
/// Subscription of typing start events for testing purposes.
244+
private var testStartTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []
245+
246+
/// Subscription of typing stop events for testing purposes.
247+
private var testStopTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []
248+
249+
/// Subscription of presence get events for testing purposes.
250+
private var testPresenceGetTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []
251+
252+
/// Subscription of retry presence get events for testing purposes.
253+
private var testPresenceGetRetryTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []
254+
255+
/// Returns a subscription which emits typing start events for testing purposes.
256+
internal func testsOnly_subscribeToStartTestTypingEvents() -> Subscription<TestTypingEvent> {
257+
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
258+
testStartTypingEventSubscriptions.append(subscription)
259+
return subscription
260+
}
261+
262+
/// Returns a subscription which emits typing stop events for testing purposes.
263+
internal func testsOnly_subscribeToStopTestTypingEvents() -> Subscription<TestTypingEvent> {
264+
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
265+
testStopTypingEventSubscriptions.append(subscription)
266+
return subscription
267+
}
268+
269+
/// Returns a subscription which emits presence get events for testing purposes.
270+
internal func testsOnly_subscribeToPresenceGetTypingEvents() -> Subscription<TestTypingEvent> {
271+
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
272+
testPresenceGetTypingEventSubscriptions.append(subscription)
273+
return subscription
274+
}
275+
276+
/// Returns a subscription which emits retry presence get events for testing purposes.
277+
internal func testsOnly_subscribeToPresenceGetRetryTypingEvents() -> Subscription<TestTypingEvent> {
278+
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
279+
testPresenceGetRetryTypingEventSubscriptions.append(subscription)
280+
return subscription
281+
}
282+
#endif
216283
}
217284

285+
#if DEBUG
286+
extension DefaultTyping: @unchecked Sendable {}
287+
#endif
288+
218289
private final actor EventTracker {
219290
private var latestEventID: UUID = .init()
220291

0 commit comments

Comments
 (0)