v0.1.1 — Navigator safe from any actor context
Patch release fixing a runtime trap in `Navigator.next(_:)` when called from a Combine sink that resumed off the main actor.
What was broken
`Navigator.next(_:)` was `@MainActor`-isolated, requiring the caller to already be on main. This trapped at runtime (`_swift_task_checkIsolatedSwift` → `EXC_BREAKPOINT`) when called from a Combine pipeline that crossed schedulers — typical pattern: a view-model awaits an async use case that bridges back via `Future { promise in Task { promise(.success(value)) } }` (e.g. Zesame's `CombineWrapper`). The `Future`'s `Task` doesn't preserve caller actor isolation, so the downstream `.sink { navigator.next(.x) }` ran off-main and trapped.
The fix
`Navigator.next(_:)` is now `nonisolated` and bridges to main internally:
- On main: fast-path via `MainActor.assumeIsolated` — no `Task` overhead.
- Off main: dispatches the subject `send` via `Task { @mainactor in … }`, so coordinator subscribers always receive on the main actor regardless of which scheduler the upstream pipeline used.
This means consumer pipelines no longer need a `.receive(on: DispatchQueue.main)` between an actor-crossing operator (`Future`-bridging, custom schedulers, etc.) and the navigator call — `Navigator` is robust at the integration boundary.
Type changes
- `Navigator` is `@unchecked Sendable` (Combine's `PassthroughSubject` doesn't yet conform to `Sendable`). The unchecked claim is bounded: every `send` goes through `next(_:)` which guarantees main-actor isolation, the `@MainActor` `navigation` accessor covers reads, and Combine's own subscription bookkeeping is documented thread-safe. `@unchecked` will be removed when `PassthroughSubject` gains native `Sendable`.
- `Navigator.next(_:)` is now under `extension Navigator where NavigationStep: Sendable` (the step is captured across the actor hop). All known consumers (Zhip, the `SignUpDemo` example) already declare `Sendable` on their step enums.
Migration from 0.1.0
Trivial for the typical case — if your `NavigationStep` enum is a simple value type (no payloads or only `Sendable` payloads), it's already `Sendable` implicitly. Otherwise add `: Sendable`:
```swift
public enum SignUpStep: Sendable {
case signedUp(User) // User must also be Sendable
case userPressedHaveAccount
}
```
Consumer pipelines that previously needed `.receive(on: DispatchQueue.main)` between an actor-crossing publisher and the navigator call can drop it — `next(_:)` handles the hop now.
Credits
Diagnosed end-to-end by tracing `EXC_BREAKPOINT` across the Zesame → NanoViewController → Zhip integration boundary. The fix lives in NanoViewController because it's the layer that owns navigation-pulse delivery semantics; making `next(_:)` schedule-tolerant means every consumer pipeline is robust.
Full changelog: v0.1.0...v0.1.1