@@ -6,14 +6,16 @@ internal final class DefaultTyping: Typing {
6
6
private let clientID : String
7
7
private let logger : InternalLogger
8
8
private let timeout : TimeInterval
9
+ private let maxPresenceGetRetryDuration : TimeInterval // Max duration as specified in CHA-T6c1
9
10
private let timerManager = TimerManager ( )
10
11
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 ) {
12
13
self . roomID = roomID
13
14
self . featureChannel = featureChannel
14
15
self . clientID = clientID
15
16
self . logger = logger
16
17
self . timeout = timeout
18
+ self . maxPresenceGetRetryDuration = maxPresenceGetRetryDuration
17
19
}
18
20
19
21
internal nonisolated var channel : any RealtimeChannelProtocol {
@@ -32,18 +34,21 @@ internal final class DefaultTyping: Typing {
32
34
logger. log ( message: " Received presence message: \( message) " , level: . debug)
33
35
Task {
34
36
let currentEventID = await eventTracker. updateEventID ( )
35
- let maxRetryDuration : TimeInterval = 30.0 // Max duration as specified in CHA-T6c1
36
37
let baseDelay : TimeInterval = 1.0 // Initial retry delay
37
38
let maxDelay : TimeInterval = 5.0 // Maximum delay between retries
38
39
39
40
var totalElapsedTime : TimeInterval = 0
40
41
var delay : TimeInterval = baseDelay
41
42
42
- while totalElapsedTime < maxRetryDuration {
43
+ while totalElapsedTime < maxPresenceGetRetryDuration {
43
44
do {
44
45
// (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.
45
46
let latestTypingMembers = try await get ( )
46
-
47
+ #if DEBUG
48
+ for subscription in testPresenceGetTypingEventSubscriptions {
49
+ subscription. emit ( . init( ) )
50
+ }
51
+ #endif
47
52
// (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.
48
53
let isLatestEvent = await eventTracker. isLatestEvent ( currentEventID)
49
54
guard isLatestEvent else {
@@ -67,9 +72,14 @@ internal final class DefaultTyping: Typing {
67
72
68
73
// Exponential backoff (double the delay)
69
74
delay = min ( delay * 2 , maxDelay)
75
+ #if DEBUG
76
+ for subscription in testPresenceGetRetryTypingEventSubscriptions {
77
+ subscription. emit ( . init( ) )
78
+ }
79
+ #endif
70
80
}
71
81
}
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)
73
83
}
74
84
}
75
85
@@ -160,6 +170,11 @@ internal final class DefaultTyping: Typing {
160
170
// (CHA-T5b) If typing is in progress, he CHA-T3 timeout is cancelled. The client then leaves presence.
161
171
await timerManager. cancelTimer ( )
162
172
channel. presence. leaveClient ( clientID, data: nil )
173
+ #if DEBUG
174
+ for subscription in testStopTypingEventSubscriptions {
175
+ subscription. emit ( . init( ) )
176
+ }
177
+ #endif
163
178
} else {
164
179
// (CHA-T5a) If typing is not in progress, this operation is no-op.
165
180
logger. log ( message: " User is not typing. No need to leave presence. " , level: . debug)
@@ -209,12 +224,68 @@ internal final class DefaultTyping: Typing {
209
224
try await stop ( )
210
225
}
211
226
}
227
+ #if DEBUG
228
+ for subscription in testStartTypingEventSubscriptions {
229
+ subscription. emit ( . init( ) )
230
+ }
231
+ #endif
212
232
}
213
233
}
214
234
}
215
235
}
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
216
283
}
217
284
285
+ #if DEBUG
286
+ extension DefaultTyping : @unchecked Sendable { }
287
+ #endif
288
+
218
289
private final actor EventTracker {
219
290
private var latestEventID : UUID = . init( )
220
291
0 commit comments