Skip to content

v0.82.1 — Clicking a token to impersonate it froze the map

  • Fixes an unbounded render loop in useAutoArtwork that locked up the map tab. A performance trace of 39 seconds shows the shape precisely: ~49,000 renders, ~96,000 console.warn calls, 11 seconds of garbage collection — and three network requests in total. Nothing was waiting on anything; the loop fed itself.
  • The cause is one missing pair of braces. useImageGenAuthFetch() returns a { authFetch } wrapper; only the function inside it is memoised, so the wrapper is a new object on every render. Five of the six call sites destructure it. useAutoArtwork held the wrapper, which made it two bugs at once. It went to listModels() and generate() where the ImageGeneratorExtension contract requires a function, so every call rejected with a TypeError before reaching the network — which is why auto-generated monster art never worked and why the trace has no requests in it. And it was listed as a dependency of the effect that calls listModels, so the effect re-ran on every render. The .catch then called setModels([]), a fresh array is never Object.is the old one, that re-rendered, the re-render minted a new wrapper, and the effect ran again.
  • The compiler had already found it, and nothing was listening. Both lines were TS2322 errors on main — an object where a function was required, stated exactly. They survived because apps/web is deliberately not typechecked in CI: it carries a standing population of pre-existing tsc errors, documented in scripts/verify.mjs, and a new one lands in that crowd without a signal. This fix takes the count from 192 to 190; it does not make the 190th harder to add. Worth naming as the gap that let a one-line type error reach production rather than treating it as fixed here.
  • setModels([]) is now identity-preserving when the list is already empty. The loop needed both halves — an unstable dependency and a state write that always counted as a change — and this is the half that turns a future unstable dependency into one wasted effect run instead of a frozen tab.
  • Regression test: useAutoArtwork.renderLoop.test.ts runs the real hook against a miniature React that re-renders until state stops changing and fails at a cap of 25 passes. Against the unfixed hook it reports never settled, which is the production symptom rather than a proxy for it.
  • The trigger being token impersonation is incidental, and worth writing down so the next trace is read faster: generator is captured by useMemo(…, []), so an instance only arms the loop if the genai module had already registered when it mounted. Any remount of the map subtree after module load would have done it.

Also in this release

  • CI stops depending on an apt repository it never reads. The e2e job installs chromium with playwright install --with-deps, and --with-deps shells out to apt-get update — which refreshes every source baked into the runner image, Google's Chrome repository included. dl.google.com served a Packages.gz whose SHA256 disagreed with the Release file advertising it; apt treats a bad index as fatal (exit 100), playwright install exited 1, and a build that reads no Google package failed on a Google CDN blip. The two third-party lists — Chrome and microsoft-prod — are removed before the install, so apt-get update talks only to the Ubuntu archives the dependencies actually come from.
  • And it retries. Those archives can flake too and cannot be removed, so the install runs up to three times with a widening pause rather than failing the whole build on one apt hiccup. ciPlaywrightInstall.test.ts holds both halves in place, next to the guard on CI's registry auth for the same reason: packages/server-core is a suite CI actually runs.