@@ -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+
218289private final actor EventTracker {
219290 private var latestEventID : UUID = . init( )
220291
0 commit comments