Repository navigation
refactor: collapse six duplicated implementations across drivers and state - #69
Merged
Merged
Conversation
Three of the four drivers carried a byte-identical `detach` block — remove the listener, `stopNotifications()` inside a try/catch, null the pair — and FTMS a fourth spelling of the same thing as a loop. That is four places to fix a teardown that ever turns out to leak, on the one path that decides whether a pad is left bound to a tab the user has walked away from. `subscribe`/`unsubscribe` now say it once, and the event name is a constant rather than a string literal repeated twelve times, where a typo is a listener that registers and never fires. The GATT fake was the other copy. It had its own `toHex` and its own `fullUuid`, duplicating the `hex` this package already exports and the `canonicalUuid` it did not. A fake whose UUID canonicalisation is a second implementation can agree with itself and disagree with `detectDriver`, which is the one thing a fake must never do, so `canonicalUuid` is exported and the fake imports both. The classic suite had rolled a third hex formatter of its own; it now shares the same one while still deriving the frame CRC independently, because an expectation taken from the code under test checks nothing. No behaviour change: all four protocol suites pass unedited on either side of it, which is the whole safety net here — there is no treadmill in CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TXUkCfJBbZiFPoJLoaiKBo
Three duplications in the web app, all of the kind where the copies have to be kept in step by hand. `isObj` was declared identically in `settings.ts`, `session.ts` and `backup.ts` — the last of which already imports both sanitisers from the other two and then kept its own copy of the primitive underneath them. It moves to `lib/validate.ts` with the array case pinned by a test, because that case is the reason the guard is written the way it is: `typeof [] === 'object'`, so the obvious spelling waves an array through and every field read off it is then silently `undefined`. localStorage was reached in seven places behind seven copies of the same try/catch, and the keys were declared in the three modules that own them and then written out *again* as string literals in `Recovery.tsx`, which needs the whole set to dump and to clear. That second list was maintained by hand, so a fifth stored key added anywhere would have dropped out of both the rescue download and the clear button — the last two controls that still work once everything else has failed. `lib/storage.ts` holds the keys once, derives the full list from them, and wraps the four accessors; `Recovery` reads the list rather than repeating it. `readJson` answers a missing, unreachable or unparseable store with `undefined`, deliberately distinct from a stored `null`: `restoreOpenSession` returns quietly when there is nothing to recover, but hands a value the app actually wrote to `sanitizeSession` so it is rejected *and reported* like any other bad record. The blanket catches in `loadSessions` and `restoreOpenSession` are kept, and now say what they are for. They are not about storage any more — they are about running at module scope, which `main.tsx` reaches before `render()`, so a throw there is a blank page with no error boundary and no Recovery screen behind it. Finally, `[...sessions.value, ...(currentSession.value ? [currentSession.value] : [])]` appeared six times, twice in components. That put "what counts as a session" in the view layer, where a change to it would have had to be chased out of the store. It is now one `computed`, which also memoises the concatenation — `streak` and `bestStreak` each walk `dailySeries(400)` and the streak chain asks for both on every render. No behaviour change: the existing 451 tests pass unchanged, and 15 new ones pin the storage wrappers' failure behaviour — which is what those seven try/catches existed for and what nothing previously tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TXUkCfJBbZiFPoJLoaiKBo
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
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.
Six places where the same code existed two to seven times over, each kept in step by hand. No behaviour change; the 451 existing tests pass unchanged and 15 new ones cover the wrappers this adds.
These came out of a duplication audit of the whole app. The remaining findings from that audit are not in this PR — see the end.
What was duplicated, and why each one mattered
Notification teardown, four times in
drivers.ts.classic,fitshowandks1234each carried a byte-identicaldetachblock — remove the listener,stopNotifications()in a try/catch, null the pair — and FTMS a fourth spelling of it as a loop. Four places to fix a teardown that ever turns out to leak, on the path that decides whether a pad is left bound to a tab somebody has walked away from. Nowsubscribe/unsubscribe. The event name is a constant too: it appeared as a string literal twelve times, and a typo in one is a listener that registers and never fires.The GATT fake reimplemented two helpers it could have imported. It had its own
toHex, duplicating thehexthe package already exports, and its ownfullUuid, duplicating acanonicalUuidthat was module-private. That second one is the sharp edge: a fake whose UUID canonicalisation is a separate implementation can agree with itself and disagree withdetectDriver, which is the one thing a fake must not do.canonicalUuidis now exported and the fake imports both. The classic suite had a third hex formatter of its own; it shares the same one now while still deriving the frame CRC independently, since an expectation taken from the code under test checks nothing.isObjdeclared three times, identically, insettings.ts,session.tsandbackup.ts— the last of which already imports both sanitisers from the other two and then kept its own copy of the primitive underneath them. Moved tolib/validate.ts, with the array case pinned by a test because that case is the reason the guard is written as it is:typeof [] === 'object', so the obvious spelling waves an array through and every field read off it is then silentlyundefined.Storage keys declared in three modules and listed again by hand in
Recovery.tsx. That screen needs the whole set to dump and to clear, so it held a second copy as string literals. A fifth stored key added anywhere would have dropped silently out of both the rescue download and the clear button — the last two controls that still work once everything else has failed.lib/storage.tsholds the keys once and derives the full list from them.The same
try/catcharound localStorage, seven times. Every one was needed — the accessors throw outright with site data blocked, andsetItemthrows on quota in private mode — and the answer was always the same. Now four wrappers.[...sessions.value, ...(currentSession.value ? [currentSession.value] : [])], six times, twice in components. That put "what counts as a session" in the view layer, where changing it would have had to be chased out of the store. Onecomputednow, which also memoises the concatenation:streakandbestStreakeach walkdailySeries(400), and the streak chain asks for both on every render.Two judgement calls worth a look
readJsondistinguishesundefinedfrom a storednull. Missing, unreachable and unparseable all come backundefined; a storednullcomes back asnull. That is not tidiness —restoreOpenSessionreturns quietly when there is nothing to recover, but hands a value the app actually wrote tosanitizeSessionso it gets rejected and reported like any other bad record. Collapsing the two would have turned one of those into silence.The blanket catches in
loadSessionsandrestoreOpenSessionare kept. I removed them at first and put them back: they are not about storage any more, they are about running at module scope.main.tsxcalls both beforerender(), so a throw there never reaches the error boundary and leaves a blank page instead of the Recovery screen — the exact failure this app went to some trouble to make impossible. They now say so in a comment.Verification
npm run check --workspaces,npm test,npm run lintandnpm run buildall clean. 451 existing tests pass unchanged; 15 added, covering the storage wrappers' failure behaviour under alocalStoragethat throws — which is what those seventry/catchblocks existed for and what nothing previously tested — plus the key registry and theisObjarray case. The four protocol suites passed unedited on either side of the driver changes, which is the real safety net there since there is no treadmill in CI.Not in this PR
The audit also turned up:
fmtMphexisting while ten call sites hand-rolltoMph(x).toFixed(1); the resume confirmation dialog written out twice; a copy-to-clipboard-then-flash pattern four times; the unsupported-browser fallback block twice; shared chart scaffolding betweenAreaandColumn(already drifted — axis labels sit atH - 3in one andH - 4in the other); four hand-rolled poll-until-deadline loops inconnection.ts; duplicated test fixtures (fakePad, thesession()builder); ~25 lines of boilerplate across the four static pages; and the model/protocol table in five copies, which has already drifted three separate ways. Happy to take any of those next.🤖 Generated with Claude Code
https://claude.ai/code/session_01TXUkCfJBbZiFPoJLoaiKBo
Generated by Claude Code