Swift Combine:将一个发布者的更新排在另一个发布者之后

时间:2021-06-02 22:54:01

标签: swift combine

我有一种情况,我的代码需要进行一次网络调用来获取一堆项目,但在等待这些项目下来时,另一个网络调用可能会获取对这些项目的更新。我希望能够将这些次要结果排入队列,直到第一个结果完成。有没有办法通过Combine来实现这一点?

重要的是,在提出第二个请求之前,我无法等待。它实际上是与第一个请求同时进行的 websocket 连接,更新来自我无法控制的 websocket。

更新

在检查了马特关于联合收割机的彻底book 之后,我决定选择.prepend()。但正如 Matt 在评论中警告我的那样,.prepend() 在第一个发布者完成之前甚至不会订阅另一个发布者。这意味着我错过了之前发送的任何信号。我需要的是一个将值排入队列的 Subject,但这也许并不难制作。无论如何,这就是我得到的:

最初我打算使用 .append(),但我意识到使用 .prepend() 我可以避免保留对出版商之一的引用。所以这是我所拥有的简化版本。这里面可能有语法错误,因为我已经从我(雇主)的代码中删减了它。

ItemFeed,它处理获取项目列表并同时处理项目更新事件。后者可以在初始项目列表之前到达,因此必须通过组合进行排序才能在它之后到达。我尝试通过将初始项目源添加到更新 PassthroughSubject 来实现此目的。

下面是一个 XCTestCase,它模拟长时间的初始项目加载,并在加载完成之前添加更新。它尝试订阅对项目列表的更改,并尝试测试第一次更新是最初的 63 个项目,后续更新是 64 个项目(在这种情况下,“更新”导致添加一个项目)。< /p>

不幸的是,虽然初始列表已发布,但更新从未到达。我还尝试删除 .output(at:) 运算符,但两个接收器仅被调用一次。

在测试用例设置延迟“获取”并订阅 feed.items 中的更改后,它调用 feed.handleItemUpatedEvent。这调用了 ItemFeed.updateItems.send(_:),但不幸的是它被遗忘了。

class
ItemFeed
{
    typealias   InitialItemsSource      =   Deferred<Future<[[String : Any]], Error>>
    
                let updateItems         =   PassthroughSubject<[Item], Error>()
                var funnel              :   AnyCancellable?
    
    @Published  var items               =   [Item]()
    
    
    
    init(initialItemSource inSource: InitialItemsSource)
    {
        //  Passthrough subject each time items are updated…
        
        var pub = self.updateItems.eraseToAnyPublisher()
        
        //  Prepend the initial items we need to fetch…
        
        let initialItems = source.tryMap { try $0.map { try Item(object: $0) } }
        pub = pub.prepend(initialItems).eraseToAnyPublisher()
        
        //  Sink on the funnel to add or update to self.items…
        
        self.funnel =
            pub.sink { inCompletion in
                //  Handle errors
            }
            receiveValue: {
                self.update(items: inItems)
            }
    }
    
    func handleItemUpdatedEvent(_ inItem: Item) {
        self.updateItems.send([inItem])
    }
    
    func update(items inItems: [Item]) {
        //  Update or add inItems to self.items
    }
}

class
ItemFeedTests : XCTestCase
{
    func
    testShouldUpdateItems()
        throws
    {
        //  Set up a mock source of items…
        
        let source = fetchItems(named: "items", delay: 3.0)      //  63 items
        
        let expectation = XCTestExpectation(description: "testShouldUpdateItems")
        expectation.expectedFulfillmentCount = 2
        
        let feed = ItemFeed(initialItemSource: source)
        
        let sub1 = feed.$items
                    .output(at: 0)
                    .receive(on: DispatchQueue.main)
                    .sink { inItems in
                        expectation.fulfill()
                        
                        debugLog("Got first items: \(inItems.count)")
                        XCTAssertEqual(inItems.count, 63)
                    }
        
        let sub2 = feed.$items
                    .output(at: 1)
                    .receive(on: DispatchQueue.main)
                    .sink { inItems in
                        expectation.fulfill()

                        debugLog("Got second items: \(inItems.count)")
                        XCTAssertEqual(inItems.count, 64)
                    }
        
        //  Send an update right away…
        
        let item = try loadItem(named: "Item3")
        feed.handleItemUpdatedEvent(item)
        
        XCTAssertEqual(feed.items.count, 0)         //  Should be no items yet
        
        //  Wait for stuff to complete…
        
        wait(for: [expectation], timeout: 10.0)
        
        sub1.cancel()           //  Not necessary, but silence the compiler warning
        sub2.cancel()
    }
}   

2 个答案:

答案 0 :(得分:0)

经过反复试验,我找到了解决方案。我创建了一个自定义发布者和订阅,它会立即订阅其上游发布者并开始对元素进行排队(最多达到一些可指定的容量)。然后它等待订阅者出现,并为该订阅者提供到目前为止的所有值,然后继续提供值。这是一个 marble 图表:

enter image description here

然后我将它与 .prepend() 结合使用,如下所示:

extension
Publisher
{
    func
    enqueue<P>(gatedBy inGate: P, capacity inCapacity: Int = .max)
        -> AnyPublisher<Self.Output, Self.Failure>
        where
            P : Publisher,
            P.Output == Output,
            P.Failure == Failure
    {
        let qp = Publishers.Queueing(upstream: self, capacity: inCapacity)
        let r = qp.prepend(inGate).eraseToAnyPublisher()
        return r
    }
}

这就是你使用它的方式......

func
testShouldReturnAllItemsInOrder()
{
    let gate = PassthroughSubject<Int, Never>()
    let stream = PassthroughSubject<Int, Never>()
    
    var results = [Int]()
    
    let sub = stream.enqueue(gatedBy: gate)
                .sink
                { inElement in
                    debugLog("element: \(inElement)")
                    results.append(inElement)
                }
    stream.send(3)
    stream.send(4)
    stream.send(5)
    
    XCTAssertEqual(results.count, 0)
    
    gate.send(1)
    gate.send(2)
    gate.send(completion: .finished)
    
    XCTAssertEqual(results.count, 5)
    XCTAssertEqual(results, [1,2,3,4,5])
    
    sub.cancel()
}

这会打印您所期望的内容:

element: 1
element: 2
element: 3
element: 4
element: 5

它运行良好,因为创建 .enqueue(gatedBy:) 运算符会创建排队发布者 qp,它立即订阅 stream 并将其发送的任何值加入队列。然后它在 .prepend() 上调用 qp,它首先订阅 gate,并等待它完成。当它完成时,它然后订阅 qp,它立即向它提供所有排队的值,然后继续向它提供来自上游发布者的值。

一旦我的雇主允许我分享它,我就会发布完整的代码。

答案 1 :(得分:0)

接受的答案实际上没有任何代码,但我能够找到解决方案。我最终创建了一个订阅“门发布者”的自定义发布者,并创建了一个为上游发布者创建接收器的订阅。我缓冲来自上游的值并根据需求发出门发布者值直到它完成,然后我切换到根据需求向下游发送缓冲区。棘手的部分是跟踪上游/门发布者并将需求发送给正确的人。