Skip to content

Commit ef66240

Browse files
Copy EventEmitter from ably-swift
Copy EventEmitter.swift, InternalEventEmitter.swift, DefaultInternalEventEmitter.swift and its tests from ably-swift commit 98996f1, making the EventEmitter protocol internal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ace39c1 commit ef66240

4 files changed

Lines changed: 842 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import Foundation
2+
3+
/// An implementation of ``InternalEventEmitter``.
4+
/// Conforms to the RTE specification for EventEmitter behavior.
5+
@MainActor
6+
internal class DefaultInternalEventEmitter<Event: Hashable, Data>: InternalEventEmitter {
7+
// Storage for listener registrations
8+
private var allEventListeners: [ListenerRegistration<MainActorEventListener<Event, Data>>] = []
9+
private var namedEventListeners: [Event: [ListenerRegistration<MainActorNamedEventListener<Data>>]] = [:]
10+
11+
// MARK: - EventEmitter conformance
12+
13+
/// RTE3: Registers listener for all events
14+
internal func on(_ listener: @escaping MainActorEventListener<Event, Data>) {
15+
let registration = ListenerRegistration(listener: listener, once: false)
16+
allEventListeners.append(registration)
17+
}
18+
19+
/// RTE3: Registers listener for all events with signal
20+
internal func on(signalledBy signal: SubscriptionController.Signal, _ listener: @escaping MainActorEventListener<Event, Data>) {
21+
let registration = ListenerRegistration(listener: listener, once: false)
22+
allEventListeners.append(registration)
23+
24+
// Register this registration with the controller so it can remove it when off() is called
25+
signal.controller?.addRegistration(self, registrationId: registration.id)
26+
}
27+
28+
/// RTE3: Registers listener for specific event
29+
internal func on(_ event: Event, _ listener: @escaping MainActorNamedEventListener<Data>) {
30+
let registration = ListenerRegistration(listener: listener, once: false)
31+
namedEventListeners[event, default: []].append(registration)
32+
}
33+
34+
/// RTE3: Registers listener for specific event with signal
35+
internal func on(_ event: Event, signalledBy signal: SubscriptionController.Signal, _ listener: @escaping MainActorNamedEventListener<Data>) {
36+
let registration = ListenerRegistration(listener: listener, once: false)
37+
namedEventListeners[event, default: []].append(registration)
38+
39+
// Register this registration with the controller
40+
signal.controller?.addRegistration(self, registrationId: registration.id)
41+
}
42+
43+
/// RTE4: Registers one-time listener for all events
44+
internal func once(_ listener: @escaping MainActorEventListener<Event, Data>) {
45+
let registration = ListenerRegistration(listener: listener, once: true)
46+
allEventListeners.append(registration)
47+
}
48+
49+
/// RTE4: Registers one-time listener for all events with signal
50+
internal func once(signalledBy signal: SubscriptionController.Signal, _ listener: @escaping MainActorEventListener<Event, Data>) {
51+
let registration = ListenerRegistration(listener: listener, once: true)
52+
allEventListeners.append(registration)
53+
54+
// Register this registration with the controller
55+
signal.controller?.addRegistration(self, registrationId: registration.id)
56+
}
57+
58+
/// RTE4: Registers one-time listener for specific event
59+
internal func once(_ event: Event, _ listener: @escaping MainActorNamedEventListener<Data>) {
60+
let registration = ListenerRegistration(listener: listener, once: true)
61+
namedEventListeners[event, default: []].append(registration)
62+
}
63+
64+
/// RTE4: Registers one-time listener for specific event with signal
65+
internal func once(_ event: Event, signalledBy signal: SubscriptionController.Signal, _ listener: @escaping MainActorNamedEventListener<Data>) {
66+
let registration = ListenerRegistration(listener: listener, once: true)
67+
namedEventListeners[event, default: []].append(registration)
68+
69+
// Register this registration with the controller
70+
signal.controller?.addRegistration(self, registrationId: registration.id)
71+
}
72+
73+
/// RTE5: Removes all listeners
74+
internal func off() {
75+
allEventListeners.removeAll()
76+
namedEventListeners.removeAll()
77+
}
78+
79+
/// RTE5: Removes all listeners for a specific event
80+
internal func off(_ event: Event) {
81+
namedEventListeners.removeValue(forKey: event)
82+
}
83+
84+
// MARK: - InternalEventEmitter conformance
85+
86+
/// RTE6: Emits an event, calling registered listeners
87+
/// RTE6a: The set of listeners must not change during emit
88+
internal func emit(event: Event, data: Data) {
89+
// Create snapshots to ensure RTE6a compliance - listeners called during emit don't affect this invocation
90+
let allEventListenersSnapshot = allEventListeners
91+
let namedEventListenersSnapshot = namedEventListeners[event] ?? []
92+
93+
// Collect all listener IDs that need to be removed
94+
var idsToRemove: [UUID] = []
95+
96+
// Call all-event listeners
97+
for registration in allEventListenersSnapshot {
98+
// Call listener per RTE6
99+
registration.listener(event, data)
100+
101+
// Mark for removal if it was a once listener
102+
if registration.once {
103+
idsToRemove.append(registration.id)
104+
}
105+
}
106+
107+
// Call named event listeners
108+
for registration in namedEventListenersSnapshot {
109+
// Call listener per RTE6
110+
registration.listener(data)
111+
112+
// Mark for removal if it was a once listener
113+
if registration.once {
114+
idsToRemove.append(registration.id)
115+
}
116+
}
117+
118+
// Remove all once listeners in one pass
119+
for id in idsToRemove {
120+
removeRegistration(id: id)
121+
}
122+
}
123+
124+
// MARK: - Registration removal (called internally and by SubscriptionController)
125+
126+
internal func removeRegistration(id: UUID) {
127+
// Remove from all-event listeners
128+
allEventListeners.removeAll { registration in
129+
registration.id == id
130+
}
131+
132+
// Remove from named-event listeners
133+
for (event, listeners) in namedEventListeners {
134+
namedEventListeners[event] = listeners.filter { registration in
135+
registration.id != id
136+
}
137+
if namedEventListeners[event]?.isEmpty == true {
138+
namedEventListeners.removeValue(forKey: event)
139+
}
140+
}
141+
}
142+
143+
// MARK: - Private Types
144+
145+
private struct ListenerRegistration<Listener> {
146+
let id = UUID()
147+
let listener: Listener
148+
let once: Bool
149+
150+
init(listener: Listener, once: Bool) {
151+
self.listener = listener
152+
self.once = once
153+
}
154+
}
155+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import Foundation
2+
3+
public typealias MainActorEventListener<Event, Data> = @MainActor (Event, Data) -> Void
4+
public typealias MainActorNamedEventListener<Event> = @MainActor (Event) -> Void
5+
6+
@MainActor
7+
internal protocol EventEmitter<Event, Data>: AnyObject {
8+
associatedtype Event
9+
associatedtype Data
10+
associatedtype Signal
11+
12+
func on(_ listener: @escaping MainActorEventListener<Event, Data>)
13+
func on(signalledBy signal: Signal, _ listener: @escaping MainActorEventListener<Event, Data>)
14+
15+
func on(_ event: Event, _ listener: @escaping MainActorNamedEventListener<Data>)
16+
func on(_ event: Event, signalledBy signal: Signal, _ listener: @escaping MainActorNamedEventListener<Data>)
17+
18+
func once(_ listener: @escaping MainActorEventListener<Event, Data>)
19+
func once(signalledBy signal: Signal, _ listener: @escaping MainActorEventListener<Event, Data>)
20+
21+
func once(_ event: Event, _ listener: @escaping MainActorNamedEventListener<Data>)
22+
func once(_ event: Event, signalledBy signal: Signal, _ listener: @escaping MainActorNamedEventListener<Data>)
23+
24+
func off()
25+
func off(_ event: Event)
26+
}
27+
28+
// 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+
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.
31+
///
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+
public protocol SubscriptionControllerProtocol {
35+
associatedtype Signal
36+
37+
var signal: Signal { get }
38+
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.
40+
///
41+
/// Calling this method will not affect any future subscriptions that use the same signal.
42+
func off()
43+
}
44+
45+
/// The `SubscriptionControllerProtocol` implementation used by the SDK.
46+
@MainActor
47+
public final class SubscriptionController: SubscriptionControllerProtocol {
48+
public init() {
49+
signal = Signal()
50+
signal.controller = self
51+
}
52+
53+
public class Signal {
54+
internal let id = UUID()
55+
// Store weak reference to the controller that owns this signal
56+
internal weak var controller: SubscriptionController?
57+
}
58+
59+
public let signal: Signal
60+
61+
// Store registrations that this controller manages
62+
private var registrations: [Registration] = []
63+
64+
public func off() {
65+
// Remove all registrations that this controller created
66+
for registration in registrations {
67+
registration.removeFromEmitter()
68+
}
69+
registrations.removeAll()
70+
}
71+
72+
internal func addRegistration(_ emitter: DefaultInternalEventEmitter<some Hashable, some Any>, registrationId: UUID) {
73+
let registration = Registration(emitter: emitter, registrationId: registrationId)
74+
registrations.append(registration)
75+
76+
// Clean up deallocated emitters periodically
77+
registrations.removeAll { registration in
78+
registration.isEmitterDeallocated
79+
}
80+
}
81+
82+
// MARK: - Private Types
83+
84+
private struct Registration {
85+
private weak var emitter: (any AnyObject)?
86+
private let emitterRemoveMethod: @MainActor (UUID) -> Void
87+
private let registrationId: UUID
88+
89+
init(emitter: DefaultInternalEventEmitter<some Hashable, some Any>, registrationId: UUID) {
90+
self.emitter = emitter
91+
self.registrationId = registrationId
92+
// Capture the remove method to avoid protocol overhead
93+
emitterRemoveMethod = { @MainActor [weak emitter] id in
94+
emitter?.removeRegistration(id: id)
95+
}
96+
}
97+
98+
var isEmitterDeallocated: Bool {
99+
emitter == nil
100+
}
101+
102+
@MainActor
103+
func removeFromEmitter() {
104+
emitterRemoveMethod(registrationId)
105+
}
106+
}
107+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// An extension of ``EventEmitter`` for use inside the codebase. It represents an `EventEmitter` that can be instructed to emit an event.
2+
internal protocol InternalEventEmitter<Event, Data>: EventEmitter {
3+
func emit(event: Event, data: Data)
4+
}
5+
6+
// (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

Comments
 (0)