What does flatMap(maxPublishers:) do, and why can limiting it silently lose values?
Predict first
Three values are sent to a subject while the first inner publisher is active. What happens?
import Combine
var bag = Set<AnyCancellable>()
let outer = PassthroughSubject<Int, Never>()
var inners: [Int: PassthroughSubject<String, Never>] = [:]
outer.flatMap(maxPublishers: .max(1)) { i -> AnyPublisher<String, Never> in
let s = PassthroughSubject<String, Never>()
inners[i] = s
print("subscribed to inner", i)
return s.eraseToAnyPublisher()
}.sink { print("got", $0) }.store(in: &bag)
outer.send(1); outer.send(2); outer.send(3)
inners[1]?.send("a")
inners[1]?.send(completion: .finished)
print("inner 2 ever created:", inners[2] != nil)
- subscribed to inner 1 / got a / inner 2 ever created: false
- subscribed to inner 1 / got a / subscribed to inner 2 / subscribed to inner 3
- subscribed to inner 1 / subscribed to inner 2 / subscribed to inner 3
Predict first
The same limit with a sequence publisher as the source.
import Combine
import Foundation
var bag = Set<AnyCancellable>()
func work(_ i: Int) -> AnyPublisher<String, Never> {
Just("job \(i)").delay(for: .milliseconds(30), scheduler: DispatchQueue.main).eraseToAnyPublisher()
}
[1, 2, 3].publisher.flatMap(maxPublishers: .max(1)) { i -> AnyPublisher<String, Never> in
print("start", i)
return work(i)
}
.sink(receiveCompletion: { _ in print("completed") }, receiveValue: { print($0) })
.store(in: &bag)
RunLoop.main.run(until: Date().addingTimeInterval(0.5))
- start 1 / job 1 / start 2 / job 2 / start 3 / job 3 / completed
- start 1 / start 2 / start 3 / job 1 / job 2 / job 3 / completed
- start 1 / job 1 / completed
Mental model. A single-lane bridge with a gate: a platoon of soldiers (a sequence) waits at the gate and crosses in turn, but tourists taking photos from a moving bus (a subject) do not wait, and the pictures they could have taken are gone.
flatMap subscribes to a new inner publisher for every upstream value, so with no limit a burst of values starts a burst of requests. maxPublishers: .max(n) makes it request only n upstream values at a time, so only n inner publishers are active; it asks for the next outer value when an inner one finishes. With .max(1) that gives you sequential execution, like a serial queue: the check with a sequence publisher shows “start 1, job 1, start 2, job 2, start 3, job 3” strictly in turn, which is how you make dependent or rate-limited requests run one after another.
The catch is who is upstream. A sequence or any publisher that respects demand simply waits until asked, so nothing is lost. A PassthroughSubject, a notification or a text field does not buffer for you: when flatMap has no demand outstanding, those values are dropped. In the first check, three values are sent to a subject while inner 1 is still active; 2 and 3 are lost, and inner 2 is never created even after inner 1 completes. If you need “queue the rest”, insert an explicit buffering stage (buffer(size:prefetch:whenFull:)) or move the queueing to a concurrency primitive such as an AsyncStream consumed by one loop. Choose switchToLatest when only the newest request matters (Q305), and maxPublishers when every request matters but they must not overlap.
A subject does not buffer when there is no demand, so values 2 and 3 are dropped.
The sequence waits for demand, so the three jobs run strictly one after another.
Common traps
- Applying
maxPublishersto a subject or event stream and expecting a queue. Values that arrive without demand are dropped. - Using unlimited
flatMapon user input. Every keystroke starts a request; combine withdebounceandswitchToLatest.
They might follow up with
- How do I buffer instead?
buffer(size: 100, prefetch: .keepFull, whenFull: .dropOldest)before the flatMap. - What does
.max(1)remind you of?concatMapin other reactive libraries: inner publishers one after another.
Related questions
- 11.7.1 What do collect, prefix, dropFirst, drop(while:), scan, reduce, removeDuplicates, compactMap, output(in:)…
- 11.7.3 Where should a catch go in a search pipeline, and what happens to the stream if you put it outside the…
- 11.1.2 What is demand and backpressure in Combine, and how does a custom subscriber control it?
- 11.2.2 How do flatMap and switchToLatest differ, and when do you use each?
← What do collect, prefix, dropFirst, drop(while:), scan, reduce… Where should a catch go in a search pipeline, and what happens to the… →