web-dev-qa-db-ja.com

Swift Combine:上流の値をバッファし、安定したレートでそれらを放出しますか?

IOS 13での新しいCombineフレームワークの使用。

非常に不規則な速度で値を送信している上流パブリッシャーがいるとします。場合によっては、数秒または数分が値なしで通過し、その後、値のストリームが一度に送信されることがあります。上流の値をサブスクライブし、それらをバッファーに入れて、定期的な既知のケイデンスで送信するカスタムパブリッシャーを作成しますが、すべて使い尽くされた場合は何も発行しません。

具体的な例の場合:

  • t = 0〜5000ミリ秒:アップストリーム値は公開されていません
  • t = 5001ms:アップストリームは "a"を公開します
  • t = 5002ms:アップストリームは「b」を公開します
  • t = 5003ms:アップストリームが「c」を公開
  • t = 5004msから10000ms:アップストリーム値は公開されていません
  • t = 10001ms:アップストリームは "d"を公開します

アップストリームにサブスクライブした私のパブリッシャーは、1秒ごとに値を生成します:

  • t = 0〜5000ミリ秒:値は公開されていません
  • t = 5001ms:「a」を公開
  • t = 6001ms:「b」を公開
  • t = 7001ms:「c」を公開
  • t = 7001ms〜10001ms:値は公開されていません
  • t = 10001ms:「d」を公開

Combineの既存のパブリッシャーまたはオペレーターは、ここでquiteしたいことを実行していないようです。

  • throttle および debounce は、特定のケイデンスで上流の値をサンプリングし、欠落している値をドロップします(たとえば、 "a "ケイデンスが1000msの場合)
  • delay は、すべての値に同じ遅延を追加しますが、間隔を空けません(たとえば、私の遅延が1000ミリ秒の場合、「a」を6001ミリ秒で、「b」を6002ミリ秒でパブリッシュします。 6003ミリ秒で「c」)
  • buffer は有望に思われますが、それを使用する方法、つまりオンデマンドでバッファから値を強制的に公開する方法を理解することはできません。シンクをbufferに接続すると、バッファリングはまったく行われず、すべての値が即座に公開されるように見えました。

ZipmergecombineLatestのようなある種の結合演算子を使用して、それをTimerパブリッシャーと結合することを考えました、そしてそれはおそらく正しいアプローチですが、私はできません私が望む振る舞いをするようにそれを構成する方法を正確に理解してください。

編集

これは私が何をしようとしているのかをうまく説明している大理石の図です:

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

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

編集2:単体テスト

modulatedPublisher(私の希望するバッファリングされたパブリッシャー)が希望どおりに機能する場合に合格する必要がある単体テストを次に示します。完璧ではありませんが、受信したイベント(受信時刻を含む)を格納し、イベント間の時間間隔を比較して、目的の間隔より小さくないことを確認します。

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)
}

私が得た最も近いものは、次のようにZipを使用しています。

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()
    }
}

これは最初の3つのイベント(1、2、3)を適切に調整しますが、2番目の3つ(4、5、6)は調整しません。出力:

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

Zipには内部バッファリング能力があるため、これは起こっていると思います。最初の3つのアップストリームイベントはバッファリングされ、タイマーのケイデンスで出力されますが、6秒の待機中にタイマーのイベントがバッファーされます。ペアリングされ、すぐに解雇されます。

4
UberJason

たぶん......だろう Publishers.CollectByTimeここのどこかで役に立ちますか?

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

単一の壊れていないパイプラインを可能にするために、私が以前のRobの回答を調整してカスタムパブリッシャーに変換したことを述べたかっただけです(彼のソリューションの下のコメントを参照)。私の適応は以下ですが、すべての功績はまだ彼にあります。このカスタムパブリッシャーは内部でそれらを使用するため、Robのstep演算子とSteppingSubscriberも引き続き使用します。

編集:modulated演算子の一部としてバッファーで更新されます。それ以外の場合は、アップストリームイベントをバッファーするためにアタッチする必要があります。

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