Skip to content

Preserve recoverable hub state when disconnectAll partially fails #47

Closed

Description

@SYMBaiEX

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 readonly failuresById record 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 pending add() calls; the documented precondition is to await all in-flight add() 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 ControllerHub section 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    cycle-7Tracked work for OpenController AI and Gaming Improvement Cycle 7cycle-7-candidateEvidence-backed candidate prepared for Cycle 7 reviewenhancementNew feature or request

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions