Client.repoInfo doesn't share a lookup that is already running. When the hour-long TTL runs out, compute() looks up each shown PR's repo 6 at a time (attention.js:369-370). The PRs come grouped by repo (lists.flat()), so almost every PR in a repo sends its own getRepoByRepoDid.
Evidence
- Live fetch log: every hour, one burst of 19
org.tangled.repo.getRepoByRepoDidrequests in about 4 s (e.g. 05:18:49.8 to 05:18:53.8, then 06:33:44 and 07:33:49), for the 8 repos in the cache (diagnostics.json:cache.repos: 8). That's 11 extra requests an hour. At the 220 ms pacing they add about 2.4 s to that refresh. - Scratch check (
/tmp/claude-audit2-scratch/dupe.js): 33 PRs over 8 repos throughmapLimit(…, 6, repoInfo)with a 50 ms lookup gives 33 lookups, not 8. client.js:531-557: the cache is only filled afterawait this._repoRecord(...)andawait this.handleOf(owner)return.
Proposal. Keep the in-flight promise per repo DID, so concurrent callers await the same lookup, and drop it once it settles. invalidateRepo should drop it too. Check whether handleOf/identity and knotEndpoint have the same race (cold start), and fix them the same way if they do. This adds no requests and no state anyone sees. It removes about 11 requests per hour here, and about one per waiting PR per hour in general.
Done when a unit test with a counting _repoRecord gets one lookup per repo for many concurrent repoInfo calls, and the hourly burst in the fetch log matches the number of repos.
PR: https://tangled.org/permadeath.com/gnome-shell-extension-tangled-reviewer/pulls/39 shares running repo and identity lookups between concurrent callers. knotEndpoint is only called inside a repo lookup, so that sharing covers it.