Last active
April 9, 2025 23:40
-
-
Save jakehawken/afd47203fe22cf08d2324157a5cd92bf to your computer and use it in GitHub Desktop.
PubSub: The absolutely lightest weight one-to-many pub/sub I could conceive of while also being respectful of memory pressure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// An object which contains a weak reference to another object. | |
/// Allows you to keep a reference to something if it's still in | |
/// memory, but not increase its reference count. | |
class WeakBox<T: AnyObject>: CustomStringConvertible, Hashable { | |
private(set) weak var value: T? | |
init(_ value: T) { | |
self.value = value | |
} | |
fileprivate init(_ value: T?) { | |
self.value = value | |
} | |
public var description: String { | |
var valueString = "nil" | |
if let val = value { | |
valueString = "\(val)" | |
} | |
return "WeakBox<\(T.self)>(\(valueString))" | |
} | |
static func == (lhs: WeakBox<T>, rhs: WeakBox<T>) -> Bool { | |
lhs === rhs | |
} | |
func hash(into hasher: inout Hasher) { | |
hasher.combine("WeakBox<\(T.self)>") | |
hasher.combine(memoryAddressOf(self)) | |
} | |
} | |
/// Allows you to do one-to-many pub/sub with any number of subscribers without needlessly incrementing | |
/// their reference counts or holding onto stale callback blocks that nobody is waiting for. | |
class PubSub<T> { | |
private let lockQueue = DispatchQueue(label: "Publisher<\(type(of: T.self))>(\(UUID().uuidString)") | |
private var subscriberMap = [WeakBox<AnyObject>: (T) -> Void]() | |
/// Adds a callback and names the object that will be listening for it. If that object has been | |
/// released from memory by the time `publish(_:)` is called or `addSubscriber(_:)` is called again, | |
/// the callback will be released from memory as well. | |
func addSubscriber(_ subscriber: AnyObject, callback: @escaping (T) -> Void) { | |
lockQueue.async { [weak self] in | |
guard let self else { | |
return | |
} | |
var updatedMap = self.subscriberMap.pruned() | |
updatedMap[WeakBox(subscriber)] = callback | |
self.subscriberMap = updatedMap | |
} | |
} | |
/// Publishes a value to all listening objects that are still alive in memory. If they are no longer | |
/// in memory, their callback will be released as well. | |
func publish(_ valueToPublish: T) { | |
lockQueue.async { [weak self] in | |
guard let self else { | |
return | |
} | |
self.subscriberMap.forEach { (box, callback) in | |
guard box.value != nil else { | |
return | |
} | |
callback(valueToPublish) | |
} | |
self.subscriberMap = self.subscriberMap.pruned() | |
} | |
} | |
} | |
private extension Dictionary where Key == WeakBox<AnyObject> { | |
/// Returns a new dictionary where all pairs in which the | |
/// box's value is nil have been removed. | |
func pruned() -> Self { | |
filter { (box, _) in box.value != nil } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment