@@ -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 maxPresenseGetRetryDuration : 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 , maxPresenseGetRetryDuration : 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 . maxPresenseGetRetryDuration = maxPresenseGetRetryDuration
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 < maxPresenseGetRetryDuration {
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 \( maxPresenseGetRetryDuration ) 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,67 @@ 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
+ #if DEBUG
237
+ /// The `DefaultTyping` emits a `TestTypingEvent` each time ``start`` or ``stop`` is called.
238
+ internal struct TestTypingEvent : Equatable {
239
+ let timestamp = Date ( )
240
+ }
241
+
242
+ /// Subscription of typing start events for testing purposes.
243
+ private var testStartTypingEventSubscriptions : [ Subscription < TestTypingEvent > ] = [ ]
244
+
245
+ /// Subscription of typing stop events for testing purposes.
246
+ private var testStopTypingEventSubscriptions : [ Subscription < TestTypingEvent > ] = [ ]
247
+
248
+ /// Subscription of presence get events for testing purposes.
249
+ private var testPresenceGetTypingEventSubscriptions : [ Subscription < TestTypingEvent > ] = [ ]
250
+
251
+ /// Subscription of retry presence get events for testing purposes.
252
+ private var testPresenceGetRetryTypingEventSubscriptions : [ Subscription < TestTypingEvent > ] = [ ]
253
+
254
+ /// Returns a subscription which emits typing start events for testing purposes.
255
+ internal func testsOnly_subscribeToStartTestTypingEvents( ) -> Subscription < TestTypingEvent > {
256
+ let subscription = Subscription < TestTypingEvent > ( bufferingPolicy: . unbounded)
257
+ testStartTypingEventSubscriptions. append ( subscription)
258
+ return subscription
259
+ }
260
+
261
+ /// Returns a subscription which emits typing stop events for testing purposes.
262
+ internal func testsOnly_subscribeToStopTestTypingEvents( ) -> Subscription < TestTypingEvent > {
263
+ let subscription = Subscription < TestTypingEvent > ( bufferingPolicy: . unbounded)
264
+ testStopTypingEventSubscriptions. append ( subscription)
265
+ return subscription
266
+ }
267
+
268
+ /// Returns a subscription which emits presence get events for testing purposes.
269
+ internal func testsOnly_subscribeToPresenceGetTypingEvents( ) -> Subscription < TestTypingEvent > {
270
+ let subscription = Subscription < TestTypingEvent > ( bufferingPolicy: . unbounded)
271
+ testPresenceGetTypingEventSubscriptions. append ( subscription)
272
+ return subscription
273
+ }
274
+
275
+ /// Returns a subscription which emits retry presence get events for testing purposes.
276
+ internal func testsOnly_subscribeToPresenceGetRetryTypingEvents( ) -> Subscription < TestTypingEvent > {
277
+ let subscription = Subscription < TestTypingEvent > ( bufferingPolicy: . unbounded)
278
+ testPresenceGetRetryTypingEventSubscriptions. append ( subscription)
279
+ return subscription
280
+ }
281
+ #endif
216
282
}
217
283
284
+ #if DEBUG
285
+ extension DefaultTyping : @unchecked Sendable { }
286
+ #endif
287
+
218
288
private final actor EventTracker {
219
289
private var latestEventID : UUID = . init( )
220
290
0 commit comments