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
Replace @mainactor isolation with DispatchQueueMutex in EventEmitter
The liveobjects plugin uses DispatchQueue-based concurrency, not
@mainactor. Adapt the EventEmitter (copied from ably-swift) to
match:
- Remove @mainactor from all protocols and classes
- Add Sendable conformance throughout
- Store mutable state in DispatchQueueMutex
- Prefix all methods with nosync_ (must be called on internal queue)
- Rename typealiases: MainActorEventListener → EventListener,
MainActorNamedEventListener → NamedEventListener (now @sendable)
- SubscriptionController takes internalQueue in init, uses
DispatchQueueMutex for its registrations
- DefaultInternalEventEmitter snapshots listeners inside withoutSync,
calls them outside to avoid exclusivity violations on re-entry
- Update tests to use ably_syncNoDeadlock and nosync_ methods
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
// Take snapshots inside withoutSync to ensure RTE6a compliance
120
+
let(allSnapshot, namedSnapshot)= mutableStateMutex.withoutSync{ state ->([ListenerRegistration<EventListener<Event,Data>>],[ListenerRegistration<NamedEventListener<Data>>])in
// Note: We _have_ to do something other than the off(listener:) that the IDL gives, because closures don't have identity in Swift. I've gone for this approach, instead of the return value used in chat-js and LiveObjects, because it makes it easy to unsubscribe from _within_ the closure, which happens e.g. if you want to listen for one of various state changes and then unsubscribe. The "controller" and "signal" language was taken from the AbortController used in the Web's `fetch()` API.
29
28
30
-
/// A subscription controller's `signal` can be passed to `EventEmitter`'s `on` or `once` methods. If you call `off()` on the controller then the listener will no longer be called.
29
+
/// A subscription controller's `signal` can be passed to `EventEmitter`'s `nosync_on` or `nosync_once` methods. If you call `nosync_off()` on the controller then the listener will no longer be called.
31
30
///
32
-
/// - Note: Subscription lifetime is independent of that of the controller. That is, if you relinquish all references to a controller then the listener will still be called. Only calling `off()` (on the controller or on the `EventEmitter`) will end the subscription.
33
-
@MainActor
34
-
publicprotocolSubscriptionControllerProtocol{
31
+
/// - Note: Subscription lifetime is independent of that of the controller. That is, if you relinquish all references to a controller then the listener will still be called. Only calling `nosync_off()` (on the controller or on the `EventEmitter`) will end the subscription.
32
+
internalprotocolSubscriptionControllerProtocol{
35
33
associatedtypeSignal
36
34
37
35
varsignal:Signal{get}
38
36
39
-
/// Cancels any subscriptions for which this controller's signal was used. The listener that was passed to `on` or `once` will not be called again.
37
+
/// Cancels any subscriptions for which this controller's signal was used. The listener that was passed to `nosync_on` or `nosync_once` will not be called again.
40
38
///
41
39
/// Calling this method will not affect any future subscriptions that use the same signal.
42
-
funcoff()
40
+
funcnosync_off()
43
41
}
44
42
45
43
/// The `SubscriptionControllerProtocol` implementation used by the SDK.
// (Note: I wonder whether having a mock InternalEventEmitter might be useful sometimes, when all you care about is whether a given event was emitted without having to go through the palaver of getting this data back out using a callback.)
0 commit comments