Repository navigation
Preserve recoverable hub state when disconnectAll partially fails #47
Description
Workflow
A game automation service disconnects every controller in a ControllerHub during shutdown or recovery. If individual disconnects fail, the caller needs per-controller outcomes from that bulk operation, without losing original per-controller errors.
Dated external observations
- Steam for Linux #13665, opened 2026-09-29, reports virtual-gamepad recreation leaving already-running app consumers with stale handles until relaunch.
- Steam for Linux #13029, opened 2026-03-23, reports Steam virtual-gamepad input resetting after unrelated ASUS HID events across several games.
These are host-side lifecycle reports. They do not show an OpenController disconnectAll() failure and are only adjacent evidence for recovery workflows; they establish neither prevalence nor adapter retry behavior.
Repository gap and interpretation
packages/core/src/hub.ts currently awaits Promise.all() across controller disconnects and clears the map only if all fulfill. A rejection therefore leaves successful and failed entries mixed together in the hub with no per-ID outcome. The README and Cycle 5 decision record already state an important precondition: callers must await in-flight add() calls before calling disconnectAll().
The improvement is deterministic bulk-cleanup accounting. Retaining a failed controller entry permits inspection, but retry success depends on whether that controller's adapter remains usable; this issue must not promise universal retryability.
Proposed outcome
Snapshot the currently registered controller entries at disconnectAll() invocation, attempt every entry, and wait for every attempt to settle before returning. Remove successful disconnects, retain failed entries under their IDs, and report each failed ID with its original error. Add documentation that the method does not wait for in-flight additions and that callers must await them first; an add() begun after the snapshot is outside that call's scope.
Acceptance criteria
- Snapshot registered controller IDs/controllers at method invocation and attempt disconnect for every entry in that snapshot, even if some reject.
- Remove entries whose disconnect succeeds; retain entries whose disconnect fails so callers can inspect them and decide whether their adapter is usable for a retry.
- On partial failure, reject after all snapshot attempts settle with a stable exported
ControllerHubDisconnectAllError(or equally stable typed public error) whose readonlyfailuresByIdrecord maps logical controller IDs to the original error instances. - Keep the existing no-failure behavior: the hub becomes empty after all snapshot entries disconnect successfully.
- Define concurrent-add behavior:
disconnectAll()does not wait for pendingadd()calls; the documented precondition is to await all in-flightadd()calls before calling it. An add begun after the snapshot is not included in that disconnect operation. - Test that every snapshotted controller is attempted and settled before rejection, successes are removed, failures remain, per-ID errors are preserved, and the documented snapshot/precondition behavior holds. Callers must serialize concurrent
disconnectAll()invocations; concurrent calls are outside this change. Test a retry only with a fake adapter that remains usable after its first failure; do not imply all adapters support retry. - Update the README's
ControllerHubsection with the partial-failure, snapshot, and retry-limit contract.
Validation
Add a focused packages/core/src/tests/hub-disconnect-all.test.ts file rather than appending to controller.test.ts or the timed-press cancellation tests; run it plus core typecheck/build. Use deterministic fake adapters for partial failures and for an adapter that explicitly permits retry. No Steam/host hardware validation is needed for this API behavior.
Non-goals
- No automatic Steam/OS hotplug monitoring, reconnection, physical-device discovery, or host compatibility matrix.
- No change to logical-ID semantics covered by Fix ControllerHub docs and test logical ID lifecycle #27 or to the existing await-add precondition.
- No blanket swallowing of cleanup errors and no promise that failed adapters can always be retried.
Confidence and limits
Low to medium. The partial-failure behavior is directly visible in hub.ts; the Steam reports are only adjacent lifecycle context and do not demonstrate this SDK failure. Select only if bulk-cleanup accounting has standalone value to maintainers.
Ownership and dependencies
Lane: packages/core/src/hub.ts, focused hub tests, and the README ControllerHub section. Independent of #20 (native CLI test cleanup) and #27 (logical-ID documentation/lifecycle). No shared-file dependency with #44's deferred protocol design or #46's helper handshake.
Activity