Skip to content

Recompute Save's blocked state once the SignWriting fonts load - #27

Merged
AmitMY merged 2 commits into
sutton-signwriting:mainfrom
sign-language-processing:fix-saveable-font-race
Jul 28, 2026
Merged

AmitMY merged 2 commits into
sutton-signwriting:mainfrom
sign-language-processing:fix-saveable-font-race

Conversation

@AmitMY

@AmitMY AmitMY commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes the red E2E run on main (run 30279252538), from #26.

What broke

The save-validity tests failed with vm.swunorm() reporting invalid SWU while the Save button still read aria-disabled="false" — the button had the right data available and was showing the wrong state.

Two causes, both about font readiness:

1. ready never flipped. useFontStore seeded ready from document.fonts.check(). Chromium answers true for a face that is still downloading (document.fonts.status === 'loading'), so ready was already true at the first render and ensureSignWritingFonts() took its early return. The false→true transition that tells consumers to recompute never fired. Seeded false now, and the load always runs; cached and locally-installed fonts resolve within a frame, so returning visitors still see no placeholder.

2. The Save buttons didn't listen for it. Saveability is derived from glyph sizes, which font-ttf can only measure once those fonts are available — until then signNormalize throws and fswnorm falls back to the un-normalized sign, so an oversized sign measures as fitting. The buttons subscribed only to the sign store, which does not change when fonts arrive, so they kept that first wrong answer. They now go through useSaveable(), which also subscribes to font readiness — the same pattern useGlyph already uses.

Why it passed review and CI on the fork

It never reproduces on a machine with the SignWriting fonts installed: local(SuttonSignWritingLine) resolves with zero .woff2 requests, so the very first render already has real measurements. Cold CI runners download the fonts and lose the race.

Timeline from a forced cold load, before the fix:

t=0ms     fonts.check=true  fonts.status=loading  fswnorm=M500x705S2ff00482x255S10000485x705
t=3000ms  fonts.check=true  fonts.status=loaded   fswnorm=M518x963S2ff00482x483S10000485x933

The normalized sign goes out of lane at ~2.5s, and nothing re-rendered.

Testing

New test strips local() from the served CSS and delays the .woff2 by 1s to force the cold path on any machine. Sampling the button against swunorm() validity every 150ms across the font load:

  • previous build: 23 disagreements over 6s
  • this build: 0

The test fails on the previous build and passes on this one. Full suite: 93 passing.

🤖 Generated with Claude Code

AmitMY and others added 2 commits July 28, 2026 07:53
The main E2E run went red on the save-validity tests: `vm.swunorm()` reported
invalid SWU while the Save button still read aria-disabled="false".

Two causes, both about font readiness:

useFontStore seeded `ready` from document.fonts.check(). Chromium answers true
for a face that is still downloading (status 'loading'), so `ready` was already
true at the first render and ensureSignWritingFonts() took its early return —
the false→true flip that tells consumers to recompute never fired. Seed it false
and always run the load; cached and locally-installed fonts resolve within a
frame, so returning visitors still see no placeholder.

Saveability is derived from glyph sizes, which font-ttf can only measure once
those fonts are available — until then signNormalize falls back to the
un-normalized sign and an oversized sign measures as fitting. The Save buttons
subscribed only to the sign store, so they kept that first wrong answer. They
now go through useSaveable(), which also subscribes to font readiness, matching
the existing useGlyph pattern.

This never reproduces on a machine with the SignWriting fonts installed: local()
resolves with no download and the first render already has real measurements.
The new test strips local() and delays the woff2 to force the cold path — it
fails on the previous build and passes on this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three cleanups on the previous commit, no behavior change:

- useSaveable lived in the store module; every other hook lives in src/hooks,
  and having it there meant signStore imported fontStore purely to serve a
  component concern.
- The `!ready ||` short-circuit ran after the store selector had already
  computed saveable(). That is the one window where the computation is
  expensive: pre-fonts, font-ttf's symbolSize misses its cache, scans a blank
  152x152 canvas per symbol, and returns undefined without caching, so every
  call repeats the scan. Folding the gate into the selector skips it.
- The comment claimed cached fonts resolve with no placeholder;
  document.fonts.load() always resolves asynchronously, so there is one painted
  frame either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AmitMY
AmitMY merged commit 1cbdd15 into sutton-signwriting:main Jul 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant