Instance-isolated BluetoothActor: retire @globalActor singleton (#65) - #66
Merged
Conversation
Route the test-only willRestoreState hook through the real BluetoothDelegateShim → AsyncStream → process(_:) path and expose it via Mock.simulateWillRestoreState, replacing the process(.willRestore) backdoor. Groundwork for the instance-isolation migration (#65). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace @globalActor BluetoothActor.shared with a plain actor owned by each ReliaBLEManager. Add EventPipeline/TaskRegistry teardown boxes, ensureCentralManager(), terminal shutdown(), and migrate the test harness to manager.bluetooth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document retain graph, scrub singleton wording from Sources/, and replace the makeManager auto-shutdown hack with Mock.tearDown(_:) (serialized suite still tears down the previous stack by default; tearDownPrevious: false enables multi-stack tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add spec-based Mock.installStateRestoration / makeRestoredManager, migrate faithful restore tests to real central-init willRestoreState, delete shim-injection restore machinery, and keep empty-filter + defer-scan as direct-handler unit tests (mock forces isScanning). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add two-manager independent-state and auth-cancellation isolation tests (concurrent live stacks), demote disconnected restore seeding to a direct-handler unit test, and extend testHandleWillRestoreState with peripheralIds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upstream-ready feature request for isScanning-on-restore, disconnected restored peripherals, and on-demand willRestoreState (item 7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the stale @globalActor/singleton narrative with the shipped model: each ReliaBLEManager owns its own BluetoothActor instance and is a fully isolated stack (actor, central, discovered peripherals, connection state, streams). - AGENTS.md: rewrite the "Swift Concurrency" section — instance actor owned by the manager, one stack per manager, process-global auth, shared radio, and the two-halves restore-id rule (unique among live managers, reused across launches). - Concurrency.md: replace the @globalActor diagram/prose with the per-manager actor instance and a "One stack per manager" section covering per-stack discovery, global auth, shared radio, restore-id rule, and stream-retains-actor lifetime. - GettingStarted.md: correct the now-false "process-wide actor singleton" config note (config is per manager) and add multi-manager guidance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public ReliaBLEManager doc comment used a ``BluetoothActor`` symbol link, but BluetoothActor is internal and absent from the public symbol graph, so `docc convert --warnings-as-errors` failed to resolve it. Use plain code font instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Broadcast authorization-derived state on the first ensureCentralManager() call even when no central is created; guard process(_:) after shutdown; drop unused manager log property; correct Concurrency.md lazy-init wording; add regression test for notDetermined init broadcast. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The four stream registrars inserted into their continuation maps
unconditionally, so a stream created after shutdown() left a live-but-
orphaned continuation that never finished — the consumer's `for await`
would hang forever. Guard each registrar with
`guard !isShutdown else { continuation.finish(); return }`, symmetric
with shutdown() finishing already-registered subscribers, so "terminal
means terminal" holds on every path. Adds a test asserting a post-
shutdown subscription completes immediately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
itsniper
commented
Jul 23, 2026
itsniper
left a comment
Member
Author
There was a problem hiding this comment.
DocC changes need better organization
Use peer non-restoring/restoring CBCentralManagerDelegate shims so willRestoreState is only present when a restore identifier is set, matching CBCentralManagerOptionRestoreIdentifierKey. Demo injects a nil-safe optional bleManager environment instead of constructing a second central (or fatalErroring) when SwiftUI evaluates defaultValue.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #65. Follow-up to #42.
Replaces the process-wide
@globalActor BluetoothActor.sharedsingleton with a plainactor BluetoothActorinstance owned perReliaBLEManager. Each manager is now a fully isolated stack — its own actor,CBCentralManager, discovered peripherals, connection state, and streams. Public API onReliaBLEManageris source-compatible (no signature changes; stillSendable, still@MainActor-callable).Design doc:
docs/designs/bluetoothactor-instance-isolation-2026-07-19.md(oracle-reviewed, marked IMPLEMENTED).What changed
@globalActor/static let shared; addedinit(log:reconnectPolicy:restoreIdentifier:)(config only, no central created in init — lazy-auth contract preserved).ReliaBLEManagerowns an internalbluetoothinstance.ensureInitialized→ensureCentralManager()— idempotent, actor-serialized; stream/snapshot getters stay non-creating, operational methods gate on it. Stream factories remainnonisolatedso the manager's sync getters (var state, …) don't becomeasync(compile-time proof added to the Sendable test).EventPipeline+TaskRegistryas@unchecked Sendablenonisolated letboxes;deinitfinishes the pipeline and cancels tasks. Added a terminal internalshutdown()that clears volatile state only (persisted reconnect intent survives for cold relaunch). Consumer-task-before-factory ordering insetupCentralManager().simulateStateRestorationfixture, sowillRestoreStatefires during centralinit(the real production event). DeletedrestoreDeliveryWaiters,testDeliverWillRestoreStateThroughDelegate,testSetRestoreIdentifier,testClearDiscoveredSnapshotsPreservingLiveReferences, and the restore-id-mismatch warning path.Concurrency.md,GettingStarted.md) rewritten to the one-stack-per-manager model: unique-restore-id rule (both halves), process-global authorization, shared radio, stream-retains-actor lifetime.docs/plans/corebluetoothmock-upstream-gaps-2026-07-21.mdcaptures three verified CoreBluetoothMock 1.0.6 limitations (forcedisScanningon restored scan key; no.disconnectedrestored state; no on-demandwillRestoreState) that keep a couple of scenarios as direct-handler unit tests.Verification
swift build+swift testgreen — 56 tests pass.Notes
BluetoothActorsymbol link in aReliaBLEManager.swiftcomment (actor is internal). Not addressed here.🤖 Generated with Claude Code