You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/// The interface that the lifecycle manager expects its contributing realtime channels to conform to.
4
5
///
5
-
/// We use this instead of the ``RealtimeChannel`` interface as its ``attach`` and ``detach`` methods are `async` instead of using callbacks. This makes it easier to write mocks for (since ``RealtimeChannel`` doesn’t express to the type system that the callbacks it receives need to be `Sendable`, it’s hard to, for example, create a mock that creates a `Task` and then calls the callback from inside this task).
6
+
/// We use this instead of the ``RealtimeChannelProtocol`` interface as:
7
+
///
8
+
/// - its ``attach`` and ``detach`` methods are `async` instead of using callbacks
9
+
/// - it uses `AsyncSequence` to emit state changes instead of using callbacks
10
+
///
11
+
/// This makes it easier to write mocks for (since ``RealtimeChannelProtocol`` doesn’t express to the type system that the callbacks it receives need to be `Sendable`, it’s hard to, for example, create a mock that creates a `Task` and then calls the callback from inside this task).
6
12
///
7
13
/// We choose to also mark the channel’s mutable state as `async`. This is a way of highlighting at the call site of accessing this state that, since `ARTRealtimeChannel` mutates this state on a separate thread, it’s possible for this state to have changed since the last time you checked it, or since the last time you performed an operation that might have mutated it, or since the last time you recieved an event informing you that it changed. To be clear, marking these as `async` doesn’t _solve_ these issues; it just makes them a bit more visible. We’ll decide how to address them in https://github.com/ably-labs/ably-chat-swift/issues/49.
/// Equivalent to subscribing to a `RealtimeChannelProtocol` object’s state changes via its `on(_:)` method. The subscription should use the ``BufferingPolicy.unbounded`` buffering policy.
22
+
///
23
+
/// It is marked as `async` purely to make it easier to write mocks for this method (i.e. to use an actor as a mock).
/// A realtime channel that contributes to the room lifecycle.
18
-
internalstructContributor{
19
-
/// The room feature that this contributor corresponds to. Used only for choosing which error to throw when a contributor operation fails.
20
-
internalvarfeature:RoomFeature
27
+
/// A realtime channel that contributes to the room lifecycle.
28
+
///
29
+
/// The identity implied by the `Identifiable` conformance must distinguish each of the contributors passed to a given ``RoomLifecycleManager`` instance.
/// Stores manager state relating to a given contributor.
45
+
privatestructContributorAnnotation{
46
+
// TODO: Not clear whether there can be multiple or just one (asked in https://github.com/ably/specification/pull/200/files#r1781927850)
47
+
varpendingDiscontinuityEvents:[ARTErrorInfo]=[]
23
48
}
24
49
25
50
internalprivate(set)varcurrent:RoomLifecycle
26
51
internalprivate(set)varerror:ARTErrorInfo?
52
+
// TODO: This currently allows the the tests to inject a value in order to test the spec points that are predicated on whether “a channel lifecycle operation is in progress”. In https://github.com/ably-labs/ably-chat-swift/issues/52 we’ll set this property based on whether there actually is a lifecycle operation in progress.
53
+
privatelethasOperationInProgress:Bool
54
+
/// Manager state that relates to individual contributors, keyed by contributors’ ``Contributor.id``. Stored separately from ``contributors`` so that the latter can be a `let`, to make it clear that the contributors remain fixed for the lifetime of the manager.
/// It is a programmer error to call this subscript getter with a contributor that was not one of those passed to ``init(contributors:pendingDiscontinuityEvents)``.
// The idea here is to make sure that, before the initializer completes, we are already listening for state changes, so that e.g. tests don’t miss a state change.
146
+
letsubscriptions=awaitwithTaskGroup(of:(contributor: Contributor, subscription:Subscription<ARTChannelStateChange>).self){ group in
// CHA-RL4: listen for state changes from our contributors
157
+
// TODO: Understand what happens when this task gets cancelled by `deinit`; I’m not convinced that the for-await loops will exit (https://github.com/ably-labs/ably-chat-swift/issues/29)
158
+
listenForStateChangesTask =Task{
159
+
awaitwithTaskGroup(of:Void.self){ group in
160
+
for(contributor, subscription)in subscriptions {
161
+
// This `@Sendable` is to make the compiler error "'self'-isolated value of type '() async -> Void' passed as a strongly transferred parameter; later accesses could race" go away. I don’t hugely understand what it means, but given the "'self'-isolated value" I guessed it was something vaguely to do with the fact that `async` actor initializers are actor-isolated and thought that marking it as `@Sendable` would sever this isolation and make the error go away, which it did 🤷. But there are almost certainly consequences that I am incapable of reasoning about with my current level of Swift concurrency knowledge.
/// Returns a subscription which emits the contributor state changes that have been handled by the manager.
197
+
///
198
+
/// A contributor state change is considered handled once the manager has performed all of the side effects that it will perform as a result of receiving this state change. Specifically, once:
199
+
///
200
+
/// - the manager has recorded all pending discontinuity events provoked by the state change (you can retrieve these using ``testsOnly_pendingDiscontinuityEventsForContributor(at:)``)
201
+
/// - the manager has performed all status changes provoked by the state change
202
+
/// - the manager has performed all contributor actions provoked by the state change, namely calls to ``RoomLifecycleContributorChannel.detach()`` or ``RoomLifecycleContributor.emitDiscontinuity(_:)``
logger.log(message:"Got state change \(stateChange) for contributor \(contributor)", level:.info)
213
+
214
+
// TODO: The spec, which is written for a single-threaded environment, is presumably operating on the assumption that the channel is currently in the state given by `stateChange.current` (https://github.com/ably-labs/ably-chat-swift/issues/49)
215
+
switch stateChange.event{
216
+
case.update:
217
+
// CHA-RL4a1 — if RESUMED then no-op
218
+
guard !stateChange.resumed else{
219
+
break
220
+
}
221
+
222
+
guardlet reason = stateChange.reason else{
223
+
// TODO: Decide the right thing to do here (https://github.com/ably-labs/ably-chat-swift/issues/74)
224
+
preconditionFailure("State change event with resumed == false should have a reason")
225
+
}
226
+
227
+
if hasOperationInProgress {
228
+
// CHA-RL4a3
229
+
logger.log(message:"Recording pending discontinuity event for contributor \(contributor)", level:.info)
logger.log(message:"Now that all contributors are ATTACHED, transitioning room to ATTACHED", level:.info)
255
+
changeStatus(to:.attached)
256
+
}
257
+
}
258
+
case .failed:
259
+
if !hasOperationInProgress{
260
+
// CHA-RL4b5
261
+
guardlet reason = stateChange.reason else{
262
+
// TODO: Decide the right thing to do here (https://github.com/ably-labs/ably-chat-swift/issues/74)
263
+
preconditionFailure("FAILED state change event should have a reason")
264
+
}
265
+
266
+
changeStatus(to:.failed, error: reason)
267
+
268
+
// TODO: CHA-RL4b5 is a bit unclear about how to handle failure, and whether they can be detached concurrently (asked in https://github.com/ably/specification/pull/200/files#r1777471810)
269
+
forcontributorin contributors {
270
+
do{
271
+
tryawait contributor.channel.detach()
272
+
}catch{
273
+
logger.log(message:"Failed to detach contributor \(contributor), error \(error)", level:.info)
274
+
}
275
+
}
276
+
}
277
+
case .suspended:
278
+
if !hasOperationInProgress {
279
+
// CHA-RL4b9
280
+
guardlet reason = stateChange.reason else{
281
+
// TODO: Decide the right thing to do here (https://github.com/ably-labs/ably-chat-swift/issues/74)
282
+
preconditionFailure("SUSPENDED state change event should have a reason")
283
+
}
284
+
285
+
changeStatus(to:.suspended, error: reason)
286
+
}
287
+
default:
288
+
break
289
+
}
290
+
291
+
#if DEBUG
292
+
for subscription in stateChangeHandledSubscriptions {
293
+
subscription.emit(stateChange)
294
+
}
295
+
#endif
296
+
}
297
+
82
298
/// Updates ``current`` and ``error`` and emits a status change event.
0 commit comments