Skip to content

feat: Add AsyncSequence.flatMapLatest operator#382

Merged
phausler merged 13 commits intoapple:mainfrom
peterfriese:flatMapLatest/statemachine
Mar 4, 2026
Merged

feat: Add AsyncSequence.flatMapLatest operator#382
phausler merged 13 commits intoapple:mainfrom
peterfriese:flatMapLatest/statemachine

Conversation

@peterfriese
Copy link
Copy Markdown
Contributor

@peterfriese peterfriese commented Nov 29, 2025

This PR introduces the flatMapLatest operator for AsyncSequence, refined through collaboration with Philippe Hausler.

What's included:

  • flatMapLatest operator: A new operator that transforms elements of an asynchronous sequence into new asynchronous sequences, automatically cancelling previous inner sequences when a new element arrives from the outer sequence.
  • Refined API Surface (Credit: Philippe Hausler):
    • Uses opaque return types (some AsyncSequence) for better encapsulation.
    • Full support for typed throws, ensuring type-safe error propagation.
    • Added switchToLatest() shorthand for AsyncSequence of AsyncSequence.
  • Performance & Robustness:
    • High-performance State Machine: Implementation uses a state machine pattern to ensure thread-safe operation and efficient coordination.
    • Race Condition Fix (Credit: Peter Friese): Resolved a critical concurrency issue where inner stream producers could yield values after cancellation, causing potential crashes or inconsistent state.
  • Comprehensive Testing:
    • Extensive unit tests covering error propagation, cancellation, and empty sequences.
    • Added a specific concurrency regression test (test_concurrency_crash_repro) to ensure the race condition fix remains effective.
  • Documentation & Evolution:
    • Updated Evolution Proposal (Evolution/00nn-flatMapLatest.md) reflecting the pitchable state with the refined API.
    • Added mapError operator (dependency) to support the refined implementation.

Motivation:

The flatMapLatest operator (and its shorthand switchToLatest) addresses the common problem of managing asynchronous operations where only the result of the most recent operation is relevant. This is essential for building responsive, data-driven user interfaces in Swift.

This PR is an implementation of #381


This PR represents an integrated effort combining the original proposal with refinements for modern Swift Concurrency patterns.

This introduces a simplified version of the flatMapLatest operator for AsyncSequence, along with a basic unit test. Note that this implementation uses unstructured concurrency and may have race conditions regarding task cancellation.
- Replaces naive implementation with a thread-safe approach using ManagedCriticalState.
- Introduces generation tracking to prevent race conditions where cancelled inner sequences could yield stale values.
- Adds test_interleaving_race_condition to verify correctness under concurrent load.
- Ensures Swift 6 Sendable compliance.
- Replaces AsyncThrowingStream implementation with a custom AsyncSequence, Storage, and StateMachine.
- Implements explicit state management using Lock for thread safety.
- Handles concurrency between outer and inner sequences robustly.
- Ensures correct cancellation propagation and error handling.
- Verified with test_interleaving_race_condition.
- Moves AsyncFlatMapLatestSequence.swift to Sources/AsyncAlgorithms/FlatMapLatest/
- Extracts FlatMapLatestStateMachine and FlatMapLatestStorage into their own files.
- Aligns project structure with other complex operators like CombineLatest and Debounce.
- Added test_outer_throwing to verify outer sequence error propagation
- Added test_inner_throwing to verify inner sequence error propagation
- Added test_cancellation to verify proper cancellation handling
- Added test_empty_outer to verify empty outer sequence handling
- Added test_empty_inner to verify empty inner sequence handling
- Fixed test_simple_sequence to be more robust against timing issues
- Changed from synchronous map with throwIf to AsyncThrowingStream
- Added delay to ensure proper error propagation timing
- Fixes intermittent test failures
- Removed LLM-style thinking comments
- Kept only essential, professional explanations
- No functional changes, all tests pass
- Added FlatMapLatest.md documentation guide
- Authored by Peter Friese
- Includes introduction, code samples, and use cases
- Covers search-as-you-type, location-based data, and dynamic config examples
- Compares with similar operators in ReactiveX and Combine
- Created SAA-00nn proposal for flatMapLatest operator
- Includes motivation, detailed design, and examples
- Covers implementation strategy with state machine
- Compares with ReactiveX switchMap and Combine switchToLatest
Copy link
Copy Markdown
Member

@phausler phausler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are more detailed review notes that can be made but these are perhaps the most impactful for the pitch phase.

self.storage = FlatMapLatestStorage(base: base, transform: transform)
}

public func next() async throws -> Element? {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couple of notes here that might inform the rest of the potential pitch:
the next method should likely be the typed throws variant (because as it currently stands this algorithm always throws, which is less than ideal if the base components never throw)
that would then make the overload for the old variant of next (the unaddorned non-typed throws version as you have written) would be rethrows not throws

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on using typed throws, thanks for the suggestion! I see you implemented this in PR #395, so I will not change it here for the time being.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so my pr was just to update things, feel free to merge my branch into yours and continue iterating. the major concerns here are more for the review with regards to the closure throwing or being async etc. I can help out with getting the implementation over the line.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cherry-picked your suggestions into this PR, thanks!

}

enum Action {
case startInnerTask(Inner, generation: Int, previousTask: Task<Void, Never>?, previousContinuation: UnsafeContinuation<Void, Error>?)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to potentially have the flatMap and switchToLatest families eventually only be driven by one task, do you think that is something we could transition to from this implementation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried following the implementation of the other algorithms.

Maybe this is something we can look into once the initial version has been merged?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes this could be an optimization later down the road.

phausler and others added 4 commits March 4, 2026 11:56
* Implement AsyncMapErrorSequence

* Update the proposal, implementation and tests with formatting, additional details and modifications for being able to push it to a review

* Foratting and preambles

* Update Copyright date for error sequence

Co-authored-by: Clive <clive819@users.noreply.github.com>

* Update copyright year for mapError tests

Co-authored-by: Clive <clive819@users.noreply.github.com>

* Update availablity and modify the mapError algorithm to be opaque

* Correct pre 6.0 build avail parameters

* Make the formatting of the algorithm accesor a bit more consistent

* Update the 5.8 package builder to define 1.2

* re-run formatter

---------

Co-authored-by: Clive <clive819@gmail.com>
Co-authored-by: Clive <clive819@users.noreply.github.com>
@peterfriese peterfriese force-pushed the flatMapLatest/statemachine branch from e775682 to cf88997 Compare March 4, 2026 10:56
@peterfriese peterfriese marked this pull request as ready for review March 4, 2026 11:16
@phausler
Copy link
Copy Markdown
Member

phausler commented Mar 4, 2026

So there are a few formatting issues and housekeeping tasks I can take care of - namely I will be marking this as 1.3 (but same OS requirement set) to keep track of the versioning. All in all this looks great! thanks for the hard work on getting this over the line.

@phausler phausler merged commit 5043186 into apple:main Mar 4, 2026
24 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants