Fix/init and timeouts - #8
Closed
SimonSchick wants to merge 4 commits into
Closed
Conversation
connectDevice hardcoded selectConfiguration(1) and claimInterface(0), then read endpoints out of configurations[0].interfaces[0]. Those are two different addressing spaces: selectConfiguration takes a configurationValue and claimInterface an interfaceNumber, neither of which is an array index. A device whose configuration is numbered differently rejects outright, and a composite device whose bulk interface is not number 0 - the usual shape for a printer bound to WinUSB on Windows - gets one interface claimed while transfers are issued on another's endpoints, so every write fails with "endpoint not part of a claimed interface". It now selects by configurationValue, picks the interface that actually exposes a bulk output endpoint, claims it by interfaceNumber, and reads the endpoints back from the alternate that is active after the claim. Driver conflicts were only mapped to DriverAccessDeniedError for open(). On Windows the device usually opens fine and it is selectConfiguration or claimInterface that the OS refuses, because usbprint.sys or a vendor driver already owns the interface, so users got a bare DOMException instead of the guidance that error exists to provide. transferOut's result was discarded, so a stalled endpoint or a short write silently swallowed the job and the caller then waited out its full timeout for a reply that could never come. A transfer that is an exact multiple of the endpoint packet size now gets a terminating zero length packet, without which many devices keep waiting for more data. Also: dispose no longer throws, since it runs from three callers that cannot handle a rejection - notably setup()'s failure path, where a close error replaced the real diagnosis; getTransferLength can no longer round down to a zero-length read; a stalled or babbling read returns no data rather than a torn frame; clearHalt is awaited instead of left floating; and receive copies out of the transfer buffer rather than aliasing it. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
…ever Any single failure from the data provider stopped the read loop permanently. The channel stayed open, the interface stayed claimed, and connected kept reporting true, so the device looked healthy but could never receive another byte and every subsequent operation waited out its full timeout - forever. One transient NetworkError from a bus reset, a hub renegotiation or a sleep/wake cycle was enough to trigger it, which is the "works, then everything times out" report from macOS. Transient faults are now retried with a short backoff before the listener gives up, and a throw from the consumer's input handler no longer takes the loop down with it. Teardown is no longer reported as a fault. Disposing closes the device, which rejects the in-flight transfer; reporting that made every clean disconnect look like a failure and misfired consumer reconnect logic. The 500ms poll timer was never cleared, so timers piled up as fast as the loop turned over - enough to exhaust memory against a device that answers quickly. start() is now idempotent, since two loops would race on the same endpoint and interleave their reads, and it no longer starts a loop on an already-disposed listener. An unexpected exception type used to reject the promise executor itself, which nothing awaited, so the listener stopped with no error reported at all. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
handleConnect and handleDisconnect were registered directly as async
listeners on navigator.usb events, so any rejection escaped into the
browser's event dispatch unhandled. A device that failed to set up raised
no event, was never tracked, and could not be retried: from the user's
point of view the device was plugged in and the page did nothing. Both
handlers now report through a new deviceError event.
handleConnect also checked its device map and then awaited the device
constructor before writing back. Two connect events for the same device
ran two constructors concurrently against the same handle; the loser
usually failed and the winner's entry was overwritten, leaving a fully
opened channel that nothing referenced and nothing could dispose. Setups
in progress are now registered synchronously so a second event joins the
first attempt.
A dispose failure during disconnect suppressed the disconnectedDevice
event entirely. The device is gone either way, and swallowing the event
leaks consumer state.
Exclusion filters OR'd their fields together, so
{ vendorId, productId } excluded an entire vendor rather than the one
product named. Matching now follows the WebUSB algorithm: every field a
filter specifies must agree, and an empty filter matches everything.
Also adds a dispose() so the manager's listeners can actually be detached
- binding them inline made them unremovable, leaking across SPA
navigation and hot reload - switches forceReconnect to allSettled so one
device cannot abandon the rest, forwards the dropped innerException on
DeviceNotReadyError, and stops deriving error names from
this.constructor.name, which a minifier rewrites.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
TypeScript 5.7 to 6.0, eslint 9 to 10, vitest 2 to 5, vite 6 to 8, and typescript-eslint to 8.69. TypeScript is held at 6.0 rather than 7, because typescript-eslint 8.69 declares <6.1.0 and TS 7 would silently disable every type-aware rule. eslint.config.js is replaced with a stripped strict flat config: recommended plus strictTypeChecked and stylisticTypeChecked with projectService, so the type-aware rules actually run. No react, prettier, import sorting or custom rule packs. The rules that map to bugs already found in this codebase - no-floating-promises, no-misused-promises, require-await, await-thenable, no-unnecessary-condition, consistent-return, switch-exhaustiveness-check and the no-unsafe-* family - are pinned to error. Purely aesthetic rules are off so they cannot drown the signal. tsconfig gains noImplicitReturns, noUncheckedIndexedAccess, exactOptionalPropertyTypes and noFallthroughCasesInSwitch. The first of those would have caught the parseRaw candidate-dropping bug on its own. The remaining source and test fixes here are what the new rules surfaced: descriptor strings became string | null in @types/w3c-web-usb, dispose was left floating in a test, and a few redundant conditions. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018BxSJm7tBc8v6f7MTHjbhP
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.
HIGHLY VIP, EXTREMELY VIBE CODED, HERE MOSTLY TO KEEP TRACK OF THINGS, FIXES AND IMPROVEMENTS WILL BE UPSTREAMED.