---
title: kylehughes/coalesced
framework: Swift Package Catalog
role: article
path: packages/kylehughes/coalesced
---

# kylehughes/coalesced

*Coalesce overlapping async calls by key into one in-flight invocation, without caching completed results.*

## Quick start

`api` is an actor-based API client. Its `fetchUser` method has type `@Sendable (User.ID) async throws -> User`. `User.ID` is `Hashable & Sendable`, `User` is `Sendable`, and an ID fully identifies the fetch within this API client's scope.

`AppServices` holds the app's shared dependencies. The app creates one for this API client and stores the coalesced `fetchUser` closure in it:

```swift import Coalesced

struct AppServices: Sendable {     let fetchUser: @Sendable (User.ID) async throws -> User }

let services = AppServices(     fetchUser: coalesced(api.fetchUser) ) ```

The app gives `services`, or a copy, to each feature. The profile and activity features independently request the same `userID`.

In a profile feature:

```swift let user = try await services.fetchUser(userID) ```

In an activity feature:

```swift let author = try await services.fetchUser(userID) ```

If these calls overlap, `api.fetchUser` runs once. Every caller still waiting receives its `User` or error. Copying `services` does not create another coalescing scope; all copies share the `fetchUser` closure's coalescing scope. When the flight finishes, Coalesced removes it. A later call starts a new fetch.

## Installation

Add the latest stable release to your package:

```swift let package = Package(     name: "App",     dependencies: [         .package(url: "https://github.com/kylehughes/Coalesced.git", from: "1.0.0"),     ],     targets: [         .target(             name: "App",             dependencies: [                 .product(name: "Coalesced", package: "Coalesced"),             ]         ),     ] ) ```

In Xcode, choose File > Add Package Dependencies, enter the repository URL, and select version `1.0.0` with the Up to Next Major Version rule. Coalesced has no external SwiftPM dependencies.

## Usage

Both overloads accept an `@Sendable` async throwing operation, including an actor-isolated method reference. They return an ordinary `@Sendable` closure with the same parameters and result. Every argument and the result must be `Sendable`. Choose the overload based on which calls can safely share one in-flight invocation.

### Automatic keys

`coalesced(_:)` uses the complete argument pack as the key, so every argument must also be `Hashable`. Calls match only when every corresponding argument compares equal. Those equality rules must make it safe for the callers to share one invocation. Key equality and hashing must remain stable for the entire flight. Do not mutate key-relevant state on reference arguments, including from inside the operation.

The `fetchUser` example uses one argument. Zero- and multi-argument operations keep their signatures too:

```swift let refreshConfiguration: @Sendable () async throws -> Configuration =     coalesced(api.refreshConfiguration)

let searchUsers: @Sendable (String, Int) async throws -> [User] =     coalesced(api.searchUsers) ```

With no arguments, every overlapping call has the same empty key. For `searchUsers`, both the query and page participate in the key. Store each returned closure somewhere all callers that should share work can reach it. Calling `coalesced` again, even with the same method reference, creates a separate scope.

### Custom keys

Use `coalesced(_:keyedBy:)` when the complete argument pack should not be the key. The `keyedBy:` closure receives every argument and returns a `Hashable & Sendable` key. The operation's arguments only need to be `Sendable`.

> [!IMPORTANT] > Return equal keys only when every caller sharing that key can safely receive the result or error from an operation > run with the full arguments of whichever caller starts the flight. Key equality and hashing must remain stable for the > entire flight; do not mutate key-relevant reference state, including from inside the operation.

Suppose `User` is `Sendable` but not `Hashable`. It has a stable `id` and cached display metadata. `api.fetchAvatar` builds its request from `id` alone:

```swift let fetchAvatar: @Sendable (User) async throws -> Avatar =     coalesced(api.fetchAvatar, keyedBy: { $0.id }) ```

Store this closure with the app's shared services. Every feature that should share avatar requests must use that same closure. Cached display metadata is excluded because it does not affect the operation's request, result, or error. If another property can affect any of them, include it in the key.

The caller that creates a flight supplies the complete `User` value. Later callers with the same `id` join that invocation rather than replacing its arguments. See [Behavior](#behavior) for cancellation and the remaining execution-context rules.

## Behavior

### Flights

A flight is the coordinator's shared record for one operation invocation. When a key has no active flight, the first registered caller creates one and schedules its task. Calls with an equal key that arrive before the operation finishes wait for the same result or error. If final-waiter cancellation removes the flight before its task passes the startability check, the operation is never invoked. If the operation throws, every caller still waiting receives that error. Coalesced removes a finished flight and does not cache its result. A later call creates a new flight.

Side effects inside a shared invocation do not happen separately for each waiting caller. Keep any logging, accounting, or mutation that must happen for every caller outside the wrapped operation.

### Cancellation

Canceling one caller does not cancel work that another caller still needs. If final-caller cancellation removes the flight before the shared task passes its startability check, the operation is never invoked. Otherwise Coalesced cooperatively cancels the underlying task. The operation may already be running or may still be invoked, and it can continue until it observes cancellation.

If cancellation races with completion, the caller receives `CancellationError` when cancellation wins. If completion wins, the caller receives the operation's result or error.

### Scope and execution context

Each call to `coalesced` creates an independent coalescing scope. Copies of the same returned closure share that scope; wrapping an operation again creates another.

The caller that creates a flight supplies the operation's argument values, task priority, and task-local values if the operation begins. An actor-isolated operation stays on its actor.

## Compatibility

- Swift 6.3 or later. - Xcode 26.5 or later on Apple platforms. - iOS 16+, macOS 13+, tvOS 16+, watchOS 9+, and visionOS 1+. - On Linux, Coalesced uses `Synchronization.Mutex`. CI tests Swift 6.3.0 and a selected current Swift 6.3 patch. CI   does not cover every Linux distribution. - Windows is not tested and is not part of the declared support.

## Documentation

[Read the DocC documentation on GitHub Pages.](https://kylehughes.github.io/Coalesced/)

The [design note](Documentation/Design.md) covers the coordinator's state machine, cancellation races, recursive calls, the context supplied by the caller that creates a flight, and implementation invariants.

The [CI charter](Documentation/CI.md) records the support contract, test profiles, toolchain policy, timeouts, coverage, and sanitizer policy.

## Contributions

Coalesced is not accepting source contributions at this time. [Report bugs on GitHub.](https://github.com/kylehughes/Coalesced/issues)

## Provenance

Coalesced was conceived, implemented, tested, and documented by an automated software factory. No human has reviewed its source code.

## "Author"

[Kyle Hughes](https://kylehugh.es)

[![Bluesky][bluesky_image]][bluesky_url]<br> [![LinkedIn][linkedin_image]][linkedin_url]<br> [![Mastodon][mastodon_image]][mastodon_url]

[bluesky_image]: https://img.shields.io/badge/Bluesky-0285FF?logo=bluesky&logoColor=fff [bluesky_url]: https://bsky.app/profile/kylehugh.es [linkedin_image]: https://img.shields.io/badge/LinkedIn-0A66C2?logo=linkedin&logoColor=fff [linkedin_url]: https://www.linkedin.com/in/kyle-hughes [mastodon_image]: https://img.shields.io/mastodon/follow/109356914477272810?domain=https%3A%2F%2Fmister.computer&style=social [mastodon_url]: https://mister.computer/@kyle

## License

Coalesced is available under the [MIT license](LICENSE).

## Package Metadata

Repository: kylehughes/coalesced

Default branch: main

README: README.md
