Skip to content

Step 4: ReliaBLEManager → nonisolated Sendable final class + rename BluetoothManagerBluetoothActor #18

Description

@itsniper

Goal

Make ReliaBLEManager a nonisolated Sendable final class that forwards async calls to BluetoothActor. Collapse the now-internal BluetoothManager into the actor type (renaming BluetoothManagerBluetoothActor). Delete dead code.

Context

See the investigation report at docs/investigations/swift6-concurrency-audit-2026-05-13.md, sections Investigator Findings §4, Recommendations §A ("Why nonisolated ReliaBLEManager and not @MainActor"), and Risks §7 (naming).

This step finishes the architectural goal: the library is callable cleanly from @MainActor SwiftUI and from background actors, without forcing a MainActor hop on background callers. A @MainActor façade would have forced that hop; a nonisolated Sendable façade does not.

Deliverable (Step 4 of 5 in the migration plan)

  1. Mark public final class ReliaBLEManager: Sendable. All members become forwarders.
  2. Public actions become async (authorizeBluetooth, startScanning, stopScanning, future connect/disconnect).
  3. currentState becomes get async.
  4. Rename BluetoothManagerBluetoothActor. Move the file's responsibilities entirely inside the actor; remove the legacy NSObject shape. Keep BluetoothManager.swift as the filename (it now declares the actor) so DocC anchor history stays sane, or rename to BluetoothActor.swift if anchor breakage is acceptable — call this out in the PR for review.
  5. Collapse / inline PeripheralManager into the actor (its queue.sync pattern becomes plain actor isolation). The standalone PeripheralManager type goes away.
  6. Delete dead code:
    • testFunction() in ReliaBLEManager.swift:106
    • AuthorizationError.unauthorized if it's still unused after the refactor (the only case currently is denied/restricted/unknown)
  7. Preserve forceMock: true literal at the factory call site (load-bearing per CLAUDE.md).
  8. Update the Demo's CentralViewModel to call await manager.… everywhere; wrap actions in Task { … } from @MainActor view methods.

Acceptance criteria

  • ReliaBLEManager: Sendable. A unit test captures a ReliaBLEManager reference into a Task.detached and calls every public method — must compile.
  • BluetoothActor is the global actor. BluetoothManager symbol no longer exists.
  • PeripheralManager symbol no longer exists (inlined into the actor).
  • All public methods that mutate / read state are async.
  • forceMock: true literal remains at the factory call site.
  • DocC catalog updated (architecture page reflects the renames; ReliaBLEManagerBluetoothActor link from the manager page).

Not in scope

  • Logging polish + strict-concurrency flag flip is Step 5.
  • Removing the Combine surface happened in Step 3 (Remove Combine #12); this step can assume AsyncStream is already in place.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions