0

I'm trying to get Publisher which vends Observables to its clients Consumer, to determine when one of its consumers has disposed of its Observable.

Annoyingly. my code was working fine, until I removed an RxSwift .debug from within the Consumer code.

Is there some alternative way I might get this working?

private class Subscriber {
    var ids: [Int]
    // This property exists so I can watch whether the observable has 
    // gone nil (which I though would happen when its been disposed, but it 
    // seems to happen immediately)
    weak var observable: Observable<[Updates]>?
}
class Publisher {
    private let relay: BehaviorRelay<[Int: Updates]> 
    private var subscribers: [Subscriber] = []

    func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
        let observable = relay
           .map { map in
               return map
                   .filter { ids.contains($0.key) }
                   .map { $0.value }
           }
           .filter { !$0.isEmpty }
           .asObservable()

        let subscriber = Subscriber(ids: ids, observable: observable)
        subscribers.append(subscriber)
        return observable
    }

    private func repeatTimer() {
        let updates: [Updates] = ....

        // I need to be able to determine at this point whether the subscriber's 
        // observable has been disposed of, so I can remove it from the list of
        // subscribers. However `subscriber.observable` is always nil here.
        // PS: I am happy for this to happen before the `repeatTimer` func fires
        subscribers.remove(where: { subscriber in
            return subscriber.observable == nil
        })

        relay.accept(updates)
    }
}

class Client {

    private var disposeBag: DisposeBag?
    private let publisher = Publisher()

    func startWatching() {
        let disposeBag = DisposeBag()
        self.disposeBag = disposeBag

        publisher
            // with the `.debug` below things work OK, without it the 
            ///`Publisher.Subscriber.observable` immediately becomes nil
            //.debug("Consumer") 
            .updatesStream(for: [1, 2, 3])
            .subscribe(onNext: { values in
                print(values)
            })
           .disposed(by: disposeBag)
    }

    func stopWatching() {
        disposeBag = nil
    }
}
Oliver Pearmain
  • 19,885
  • 13
  • 86
  • 90
  • You should learn to embrace the *functional* nature of FRP. Observable types should always be declared using `let` never `var` and especially not weak vars. I'm not sure the context in which you are calling `repeatTimer` but the function seems against the grain of how Observables should be used... Maybe if you give more context, one of us can help you build an abstraction that better fits the paradigm. – Daniel T. Nov 30 '21 at 00:21
  • Appreciate the reply. `repeatTimer` isn't actually a thing thats in the production code, I just added this to demonstrate that external updates can manifest at random times and from unknown sources. Whilst I respect your suggestion of embracing FRP unfortunately this isn't code I can change in this instance. – Oliver Pearmain Dec 08 '21 at 10:59
  • The problem here is that Observables are never disposed. Subscriptions are disposed and there could be many subscriptions on any particular Observable. So it doesn't make any sense to even ask if "an Observable has been disposed of." Again, we need more context in order to provide a solution. – Daniel T. Dec 08 '21 at 12:55

1 Answers1

0

I think this is a very bad idea, but it solves the requested problem... If I had to put this code in one of my projects, I would be very worried about race conditions...

struct Subscriber {
    let ids: [Int]
    var subscribeCount: Int = 0
    let lock = NSRecursiveLock()
}

class Publisher {
    private let relay = BehaviorRelay<[Int: Updates]>(value: [:])
    private var subscribers: [Subscriber] = []

    func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
        var subscriber = Subscriber(ids: ids)
        let observable = relay
            .map { map in
                return map
                    .filter { ids.contains($0.key) }
                    .map { $0.value }
            }
            .filter { !$0.isEmpty }
            .do(
                onSubscribe: {
                    subscriber.lock.lock()
                    subscriber.subscribeCount += 1
                    subscriber.lock.unlock()
                },
                onDispose: {
                    subscriber.lock.lock()
                    subscriber.subscribeCount -= 1
                    subscriber.lock.unlock()
                })
                .asObservable()

                subscribers.append(subscriber)
                return observable
                }

    private func repeatTimer() {
        subscribers.removeAll(where: { subscriber in
            subscriber.subscribeCount == 0
        })
    }
}
Daniel T.
  • 32,821
  • 6
  • 50
  • 72