Skip to content

Instantly share code, notes, and snippets.

@jakehawken
Last active April 9, 2025 23:40
Show Gist options
  • Save jakehawken/afd47203fe22cf08d2324157a5cd92bf to your computer and use it in GitHub Desktop.
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.
/// 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