web-dev-qa-db-fra.com

Swift Combiner: tamponner les valeurs en amont et les émettre à un taux constant?

Utilisation du nouveau framework Combine dans iOS 13.

Supposons qu'un éditeur en amont envoie des valeurs à un rythme très irrégulier - parfois des secondes ou des minutes peuvent s'écouler sans aucune valeur, puis un flux de valeurs peut traverser en une seule fois. J'aimerais créer un éditeur personnalisé qui souscrit aux valeurs en amont, les met en mémoire tampon et les émet à une cadence régulière et connue à leur arrivée, mais ne publie rien s'ils ont tous été épuisés.

Pour un exemple concret:

  • t = 0 à 5000 ms: aucune valeur amont publiée
  • t = 5001ms: en amont publie "a"
  • t = 5002ms: en amont publie "b"
  • t = 5003ms: en amont publie "c"
  • t = 5004 ms à 10000 ms: aucune valeur en amont publiée
  • t = 10001ms: en amont publie "d"

Mon éditeur abonné à l'amont produirait des valeurs toutes les 1 seconde:

  • t = 0 à 5000 ms: aucune valeur publiée
  • t = 5001ms: publie "a"
  • t = 6001ms: publie "b"
  • t = 7001ms: publie "c"
  • t = 7001ms à 10001ms: aucune valeur publiée
  • t = 10001ms: publie "d"

Aucun des éditeurs ou opérateurs existants de Combine ne semble tout à fait faire ce que je veux ici.

  • throttle et debounce échantillonneraient simplement les valeurs en amont à une certaine cadence et enlèveraient celles qui sont manquantes (par exemple, ne publieraient que "a "si la cadence était de 1000 ms)
  • delay ajouterait le même délai à chaque valeur, mais ne les espacerait pas (par exemple, si mon délai était de 1000 ms, il publierait "a" à 6001 ms, "b" à 6002 ms, "c" à 6003 ms)
  • buffer semble prometteur, mais je n'arrive pas à comprendre comment l'utiliser - comment le forcer à publier une valeur à partir du tampon à la demande. Lorsque j'ai raccordé un évier à buffer, il a semblé publier instantanément toutes les valeurs, pas du tout en mémoire tampon.

J'ai pensé à utiliser une sorte d'opérateur de combinaison comme Zip ou merge ou combineLatest et à le combiner avec un éditeur Timer, et c'est probablement la bonne approche, mais je ne peux pas comprendre exactement comment le configurer pour donner le comportement que je veux.

Modifier

Voici un diagramme en marbre qui, espérons-le, illustre ce que je veux faire:

Upstream Publisher:
-A-B-C-------------------D-E-F--------|>

My Custom Operator:
-A----B----C-------------D----E----F--|>

Édition 2: Test unitaire

Voici un test unitaire qui devrait réussir si modulatedPublisher (mon éditeur tampon souhaité) fonctionne comme vous le souhaitez. Ce n'est pas parfait, mais il stocke les événements (y compris l'heure reçue) tels qu'ils sont reçus, puis compare les intervalles de temps entre les événements, en s'assurant qu'ils ne sont pas plus petits que l'intervalle souhaité.

func testCustomPublisher() {
    let expectation = XCTestExpectation(description: "async")
    var events = [Event]()

    let passthroughSubject = PassthroughSubject<Int, Never>()
    let cancellable = passthroughSubject
        .modulatedPublisher(interval: 1.0)
        .sink { value in
            events.append(Event(value: value, date: Date()))
            print("value received: \(value) at \(self.dateFormatter.string(from:Date()))")
        }

    // WHEN I send 3 events, wait 6 seconds, and send 3 more events
    passthroughSubject.send(1)
    passthroughSubject.send(2)
    passthroughSubject.send(3)

    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(6000)) {
        passthroughSubject.send(4)
        passthroughSubject.send(5)
        passthroughSubject.send(6)

        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(4000)) {

            // THEN I expect the stored events to be no closer together in time than the interval of 1.0s
            for i in 1 ..< events.count {
                let interval = events[i].date.timeIntervalSince(events[i-1].date)
                print("Interval: \(interval)")

                // There's some small error in the interval but it should be about 1 second since I'm using a 1s modulated publisher.
                XCTAssertTrue(interval > 0.99)
            }
            expectation.fulfill()
        }
    }

    wait(for: [expectation], timeout: 15)
}

Le plus proche que j'ai obtenu utilise Zip, comme ceci:

public extension Publisher where Self.Failure == Never {
    func modulatedPublisher(interval: TimeInterval) -> AnyPublisher<Output, Never> {
        let timerBuffer = Timer
        .publish(every: interval, on: .main, in: .common)
        .autoconnect()

      return timerBuffer
        .Zip(self, { $1 })                  // should emit one input element ($1) every timer tick
        .eraseToAnyPublisher()
    }
}

Cela ajuste correctement les trois premiers événements (1, 2 et 3), mais pas les trois derniers (4, 5 et 6). Le résultat:

value received: 1 at 3:54:07.0007
value received: 2 at 3:54:08.0008
value received: 3 at 3:54:09.0009
value received: 4 at 3:54:12.0012
value received: 5 at 3:54:12.0012
value received: 6 at 3:54:12.0012

Je crois que cela se produit parce que Zip a une certaine capacité de mise en mémoire tampon interne. Les trois premiers événements en amont sont mis en mémoire tampon et émis sur la cadence du minuteur, mais pendant les 6 secondes d'attente, les événements du minuteur sont mis en mémoire tampon - et lorsque la deuxième configuration des événements en amont est déclenchée, il y a déjà des événements du minuteur en attente dans la file d'attente, de sorte qu'ils 'est jumelé et a tiré immédiatement.

4
UberJason

Pourrait Publishers.CollectByTime être utile ici quelque part?

Publishers.CollectByTime(upstream: upstreamPublisher.share(), strategy: Publishers.TimeGroupingStrategy.byTime(RunLoop.main, .seconds(1)), options: nil)
0
Cenk Bilgen

Je voulais juste mentionner que j'ai adapté la réponse de Rob plus tôt et l'ai convertie en un éditeur personnalisé, afin de permettre un seul pipeline ininterrompu (voir les commentaires ci-dessous sa solution). Mon adaptation est ci-dessous, mais tout le mérite lui revient. Il utilise également toujours l'opérateur step et SteppingSubscriber de Rob, car cet éditeur personnalisé les utilise en interne.

Edit: mis à jour avec tampon dans le cadre de l'opérateur modulated, sinon il faudrait l'attacher pour mettre en tampon les événements en amont.

public extension Publisher {
    func modulated<Context: Scheduler>(_ pace: Context.SchedulerTimeType.Stride, scheduler: Context) -> AnyPublisher<Output, Failure> {
        let upstream = buffer(size: 1000, prefetch: .byRequest, whenFull: .dropNewest).eraseToAnyPublisher()
        return PacePublisher<Context, AnyPublisher>(pace: pace, scheduler: scheduler, source: upstream).eraseToAnyPublisher()
    }
}

final class PacePublisher<Context: Scheduler, Source: Publisher>: Publisher {
    typealias Output = Source.Output
    typealias Failure = Source.Failure

    let subject: PassthroughSubject<Output, Failure>
    let scheduler: Context
    let pace: Context.SchedulerTimeType.Stride

    lazy var internalSubscriber: SteppingSubscriber<Output, Failure> = SteppingSubscriber<Output, Failure>(stepper: stepper)
    lazy var stepper: ((SteppingSubscriber<Output, Failure>.Event) -> ()) = {
        switch $0 {
        case .input(let input, let promise):
            // Send the input from upstream now.
            self.subject.send(input)

            // Wait for the pace interval to elapse before requesting the
            // next input from upstream.
            self.scheduler.schedule(after: self.scheduler.now.advanced(by: self.pace)) {
                promise(.more)
            }

        case .completion(let completion):
            self.subject.send(completion: completion)
        }
    }

    init(pace: Context.SchedulerTimeType.Stride, scheduler: Context, source: Source) {
        self.scheduler = scheduler
        self.pace = pace
        self.subject = PassthroughSubject<Source.Output, Source.Failure>()

        source.subscribe(internalSubscriber)
    }

    public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
        subject.subscribe(subscriber)
        subject.send(subscription: PaceSubscription(subscriber: subscriber))
    }
}

public class PaceSubscription<S: Subscriber>: Subscription {
    private var subscriber: S?

    init(subscriber: S) {
        self.subscriber = subscriber
    }

    public func request(_ demand: Subscribers.Demand) {

    }

    public func cancel() {
        subscriber = nil
    }
}
0
UberJason