/// Mutable state storage shared by ``State`` and ``Binding``. /// /// Maintains a subscriber list (explicit binding callbacks, held strongly) /// and a dependent map (``DependencyTracker`` instances, held strongly). /// ``notify()`` snapshots both before invoking callbacks, making reentrant /// mutations safe. Top-level notifications reuse their snapshot buffers; a /// reentrant notification allocates a temporary snapshot because the outer /// notification owns those buffers. @_spi(Portico) @MainActor public final class StateBox { private var value: Value private var nextID = 0 private var subscribers: [Int: (Value) -> Void] = [:] private var dependents: [ObjectIdentifier: DependencyTracker] = [:] /// Reused iteration buffers for ``notify()``. Cleared while keeping /// capacity at the end of every top-level notify, so steady-state /// mutation allocates nothing and no callback or tracker is retained past /// the notify that snapshotted it. private var subscriberScratch: ContiguousArray<(Value) -> Void> = [] private var dependentScratch: ContiguousArray = [] /// Whether a top-level ``notify()`` currently owns the scratch buffers. private var isNotifying = false @_spi(Portico) public init(_ value: Value) { self.value = value } /// Tracking read — registers the current ``DependencyTracker`` (if any) /// as a dependent, then returns the value. @_spi(Portico) public func get() -> Value { if let t = DependencyTracker.current { let oid = ObjectIdentifier(t) if dependents[oid] == nil { dependents[oid] = t t.addDeregistration { [weak self] in self?.dependents[oid] = nil } } } return value } /// Non-tracking read. Does not register a dependency. @_spi(Portico) public func peek() -> Value { value } /// Stores a new value and notifies all subscribers and dependents. @_spi(Portico) public func set(_ newValue: Value) { value = newValue notify() } /// Registers a callback that fires on every ``set(_:)`` call until the /// returned token is cancelled. @_spi(Portico) public func subscribe( _ onChange: @escaping (Value) -> Void ) -> SubscriptionToken { let id = nextID nextID += 1 subscribers[id] = onChange return SubscriptionToken { [weak self] in self?.subscribers[id] = nil } } // MARK: - Private private func notify() { if subscribers.isEmpty && dependents.isEmpty { return } let v = value // A reentrant notify cannot borrow the buffers the outer loop is // iterating, so it falls back to the allocating snapshot. Nested // writes still run synchronously to completion, exactly as before. if isNotifying { for cb in Array(subscribers.values) { cb(v) } for t in Array(dependents.values) { t.reevaluate() } return } isNotifying = true defer { isNotifying = false subscriberScratch.removeAll(keepingCapacity: true) dependentScratch.removeAll(keepingCapacity: true) } subscriberScratch.append(contentsOf: subscribers.values) for cb in subscriberScratch { cb(v) } // Snapshot dependents after subscribers so subscriber mutations are // reflected, preserving the existing ordering contract. dependentScratch.append(contentsOf: dependents.values) for t in dependentScratch { t.reevaluate() } } }