Serialize spell checks and drop results the document outran - #94
Serialize spell checks and drop results the document outran#94Wavesonics wants to merge 1 commit into
Conversation
Checks now take a mutex, so overlapping full/partial passes can't interleave lookups against one spell-checker session or race each other's span swaps. Each pass records the document hash it computed over and re-checks it before installing spans: a full check recomputes, a partial one defers to the check the edit already scheduled. A full check that queues behind an equivalent one is skipped rather than re-scanning the document.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 16 |
| Duplication | 2 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Code review findingsHigh-effort review of this branch against main. 34 candidates, 8 refuted, 10 distinct defects confirmed or plausible. Drafting this PR until the guard design is reworked. They collapse to two root causes. The de-dup key is under-specified
The bookkeeping stamps the current checker, not the one that ran ( The guards are whole-document; the work they gate is range-local or queuedPartial checks snapshot the revision after the mutex wait ( A whole-document hash discards results that were still valid (
The retry loop abandons the document (
RemainingPartial checks queue behind up to three full re-scans (
The full-check log line sits outside the retry loop ( DirectionThe de-dup key fixes are mechanical: add the mode, record A smaller alternative: keep the |
Follow-up to the spell-check investigation in #93. That PR fixed why a right-click on a squiggle opened the wrong menu; this one fixes the other half, why spell check sometimes doesn't run at all after navigating out and back in.
What was wrong
SpellCheckStatehad no serialization between checks. Each individual check was already careful about cancellation (compute under suspension, then swap spans with no suspension points), but nothing stopped two checks being alive at once, and nothing noticed the document changing while a check was suspended in its lookups.Three consequences:
Two full checks run concurrently at init.
rememberSpellCheckStateruns one when the checker resolves, and a host with its own re-check effect (the sample app has one, for an Android import race) runs another. Both interleave suspendingisCorrectWordcalls into a single spell-checker session, and every word gets looked up twice.A stale full check wipes a newer partial check. A full check snapshots its candidates, suspends across N lookups, and on resume clears
misspelledWordsand swaps in spans computed against the pre-edit document. Anything the debounced partial check installed in the meantime is discarded, the reinstalled ranges are stale, andmisspelledWordsends up out of step with the spans on the document, sohandleSpanClickfinds no segment and the right-click falls back to the plain menu. On a large document behind an IPC spell checker, N lookups comfortably exceeds the 500ms debounce.The atomic swap held by convention only. "No suspension points between removal and re-add" is true on the main dispatcher but nothing stated or enforced it.
What changed
MutexserializesrunFullSpellCheck,runPartialSpellCheckandcheckWordSegment.MAX_FULL_CHECK_ATTEMPTS, then leaves the edited ranges to the debounced partial checks); a partial check drops its result, since the edit that invalidated it schedules its own.Tests
Four cases in
SpellCheckStateTest; three fail against the pre-change code:./gradlew checkis green.Not addressed here
The sample app's extra
LaunchedEffectis now harmless rather than actively racing, but it's still a workaround forsetTextnot emitting an edit operation, so the library never learns its document was replaced. Fixing that at the source is a separate change.getSuggestionsstill hits the spell checker outside the lock. Locking it would block the context menu behind a full document check, which is a worse trade for a hazard that is so far unconfirmed.