我有一种情况,我的代码需要进行一次网络调用来获取一堆项目,但在等待这些项目下来时,另一个网络调用可能会获取对这些项目的更新。我希望能够将这些次要结果排入队列,直到第一个结果完成。有没有办法通过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()
}
}
答案 0 :(得分:0)
经过反复试验,我找到了解决方案。我创建了一个自定义发布者和订阅,它会立即订阅其上游发布者并开始对元素进行排队(最多达到一些可指定的容量)。然后它等待订阅者出现,并为该订阅者提供到目前为止的所有值,然后继续提供值。这是一个 marble 图表:
然后我将它与 .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)
接受的答案实际上没有任何代码,但我能够找到解决方案。我最终创建了一个订阅“门发布者”的自定义发布者,并创建了一个为上游发布者创建接收器的订阅。我缓冲来自上游的值并根据需求发出门发布者值直到它完成,然后我切换到根据需求向下游发送缓冲区。棘手的部分是跟踪上游/门发布者并将需求发送给正确的人。