perf(contact-lookup-details): fetch contact log types once, not once per log - #76
Merged
Merged
Conversation
…per log `getContactLogTypes()` was called inside the `logs.map()` callback in `getContactLogsByContactId`, so the same small `Contact_Log_Types` lookup table was refetched once per log that had a type set. A contact with 50 typed logs meant 50 identical fetches on every page load. Performance only — the mapped output was always correct. It is now fetched once and indexed into a Map. The map becomes synchronous, so `Promise.all` and the async callback go away. The naive hoist would not have been behavior-neutral. The old code fetched the lookup table *only* when at least one log had a type, so a contact with no logs — or only untyped ones — made no request and could not fail on one. Hoisting unconditionally would newly throw there if the lookup fetch failed. A `logs.some(...)` guard preserves the old behavior exactly. Why the suite missed this: the file was at 100% statements / 100% branches the whole time. The test mocked `getContactLogTypes` and never asserted a call count, so the loop was invisible. The guard is now `toHaveBeenCalledTimes(1)` against a five-log fixture spanning two type IDs plus a null, and `not.toHaveBeenCalled()` for the all-untyped and empty-logs paths. Verified by mutation: restoring the call inside the `map` fails the count assertion and nothing else. The other three tests in the block pin behavior rather than efficiency, which is the correct split. Also scoped one pre-existing `mockResolvedValue` to `mockResolvedValueOnce` — `vi.clearAllMocks()` clears calls but not implementations, so it was leaking a resolved value into later tests in the file. Service-level memoization of `getContactLogTypes()` was considered and deliberately skipped. After this change the remaining callers are one per page load and one per `contact-logs.tsx` mount; caching on a process-wide singleton would hide a newly added contact log type until restart, for a single-digit request saving. Closes `.claude/TODO/n-plus-1-contact-log-types-lookup.md`; TestCoverage §5.6 updated (its stale `§7.6` cross-reference went with the file — §7 is the per-file appendix and has no subsections). Verification: 579 tests / 32 files pass, no coverage threshold failures (`contact-lookup-details/actions.ts` stays at 100%/100%), eslint clean, `tsc --noEmit` clean, `npm run build` succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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
.claude/TODO/n-plus-1-contact-log-types-lookup.md.The problem
getContactLogTypes()was called inside thelogs.map()callback ingetContactLogsByContactId, so the same smallContact_Log_Typeslookup table was refetched once per log that had a type set. A contact with 50 typed logs meant 50 identical fetches on every page load.Performance only — the mapped output was always correct.
The fix
Fetch once, index into a
Map. The map becomes synchronous, soPromise.alland the async callback go away.The naive hoist would not have been behavior-neutral, which is the one non-obvious part of this change. The old code fetched the lookup table only when at least one log had a type — so a contact with no logs, or only untyped ones, made no request and could not fail on one. Hoisting unconditionally would newly throw there if the lookup fetch failed. The
logs.some(...)guard preserves the old behavior exactly.Why the suite missed it
The file was at 100% statements / 100% branches the whole time. The test mocked
getContactLogTypesand never asserted a call count, so the loop was invisible. This is the §1 thesis ofTestCoverage.mdlanding again: high coverage is not evidence of correctness.New guards:
toHaveBeenCalledTimes(1)against a five-log fixture spanning two type IDs plus a null, asserting both the count and the full mapped outputnot.toHaveBeenCalled()for the all-untyped and empty-logs paths — this is what pins thesomeguardnull, not''Verified by mutation: restoring the call inside the
mapfails the count assertion and nothing else. The other three tests pin behavior rather than efficiency, which is the correct split.Also scoped one pre-existing
mockResolvedValuetomockResolvedValueOnce—vi.clearAllMocks()clears calls but not implementations, so it was leaking a resolved value into later tests in the file.Considered and skipped: service-level memoization
The TODO floated caching
getContactLogTypes()inContactLogService. Deliberately not done. After this change the remaining callers are one per page load and one percontact-logs.tsxmount; caching on a process-wide singleton would hide a newly added contact log type until restart, for a single-digit request saving. Rationale recorded in §5.6 so it isn't relitigated.Verification
contact-lookup-details/actions.tsstays at 100%/100%)eslintclean,tsc --noEmitclean,npm run buildsucceedsTestCoverage.md§5.6 flipped to ✅ and the §1 summary table now lists only §5.7 as open. The TODO's stale§7.6cross-reference went with the deleted file (§7 is the per-file appendix and has no subsections).🤖 Generated with Claude Code