The WYSIWYG editor has CRITICAL, FLAKY bugs causing text corruption and cursor issues during typing. These bugs are:
- Reproducible in automated tests (flaky)
- User-reported in Safari (consistent but hard to reproduce)
- Timing-dependent (race conditions)
- Data-corrupting (characters reordered, deleted, or phantom characters inserted)
Impact: Any deviation from user input is unacceptable for a text editor. Severity: CRITICAL Status: ROOT CAUSE IDENTIFIED
Evidence: Test output shows "salary" → "slryaa" Frequency: Flaky (timing-dependent) Root Cause: Race condition during typing + evaluation
Evidence: User saw "$500,000" → "$500, 000" Frequency: Flaky in Safari Root Cause: Same race condition as #1
Evidence: "monthly_salary" → "mo_salary" (y deleted) Frequency: Cannot reliably reproduce Root Cause: Same race condition as #1
Evidence: User reports cursor "catches up after couple seconds" Frequency: Consistent in Safari Root Cause: updateCursorPosition() may be slow, or visual cursor update delayed
User types
↓
Textarea input event
↓
handleInput() runs:
- rawText = textareaElement.value (read)
- doc.updateRawText(rawText) (update model)
- updateCursorPosition() (update visual)
- scheduleEvaluation() (debounce eval)
↓
Svelte reactivity:
- rawText changed → bind:value triggers
- Svelte updates textarea.value (WRITE BACK!)
↓
RACE CONDITION HERE
↓
Next keystroke arrives
↓
Textarea state is inconsistent
↓
Character corruption
- User types 'a' → Textarea has "a"
- Input event fires →
handleInput()setsrawText = "a" - Svelte sees
rawTextchanged → Triggers bind:value update - Svelte writes back
textarea.value = "a"(redundant!) - User types 'b' DURING step 4
- Browser is confused - value being set while input is happening
- Result: Characters dropped, reordered, or duplicated
The bug only manifests when:
- Typing happens DURING Svelte's reactivity update
- Evaluation is processing (adds more state changes)
- Browser is under load (slow rendering)
- Safari's stricter event handling
Without bind:value:
- Textarea has NO initial value
- No two-way sync between state and DOM
- Worse corruption: "bonus" → "sunob" (reversed!)
Conclusion: We need bind:value for initialization, but NOT for ongoing updates.
- ✅ Removed
onkeyuphandler - Fixed off-by-one errors - ✅ Removed
requestAnimationFrame- Fixed async cursor updates - ❌ Removed
bind:value- CATASTROPHIC text corruption - ❌ Used one-way
value={rawText}- Still caused character reordering
The problem is that handleInput() updates rawText, which triggers Svelte reactivity, which tries to update the textarea, creating a feedback loop.
Current (BROKEN):
// handleInput reads AND writes to rawText
function handleInput() {
rawText = textareaElement.value; // ← This triggers bind:value!
...
}<!-- This creates feedback loop -->
<textarea bind:value={rawText} oninput={handleInput} />Proposed Fix:
// Internal state for textarea, separate from reactive state
let textareaText = $state('');
function handleInput() {
if (!textareaElement) return;
// Update internal state (NO reactive bindings)
textareaText = textareaElement.value;
// Update document model
doc.updateRawText(textareaText);
// Update cursor immediately
updateCursorPosition();
// Schedule evaluation
scheduleEvaluation();
}<!-- NO bind:value, textarea is uncontrolled -->
<textarea
bind:this={textareaElement}
oninput={handleInput}
value={initialText} <!-- Only for SSR hydration -->
/>- Remove
bind:value- No two-way binding - Use
value={initialText}ONLY - Sets initial value for SSR - After mount, textarea is uncontrolled - Browser manages it
- We READ from textarea - Never write to it
- No Svelte reactivity on textarea value - No feedback loop
- Remove
bind:value={rawText} - Add
value={initialText}for SSR hydration - Ensure
handleInput()ONLY reads from textarea - Verify NO code path writes to
textarea.valueafter mount
- Run all existing tests
- Add new flaky-bug detection tests
- Run tests in WebKit (Safari engine)
- Test with various typing speeds
- Test during active evaluation
- Test on actual Safari browser
- Test cursor lag
- Test phantom spaces
- Test character corruption
- ✅ Cursor position after every character
- ✅ No phantom characters
- ✅ Rapid typing fidelity
- ✅ Delete/Backspace behavior
- ✅ Character preservation
- ❌ 1 flaky test (character reordering)
- Safari-specific timing tests (created:
wysiwyg-safari-timing.spec.ts) - WebKit browser testing
- Stress testing with rapid typing
- Concurrent typing + evaluation tests
##Evidence
Test: fuzz test: random edits should maintain exact character sequence
Mismatch after typing "salary"
Expected: "salary"
Got: "slryaa"
Test: continuous typing session
Expected: "bonus = $500"
Got: " = $500sunob"
Analysis: Characters "bonus" were REVERSED to "sunob" and moved to wrong position.
- IMMEDIATE: Do NOT remove
bind:valueagain - it causes worse corruption - INVESTIGATE: Why does
bind:value+oninputcreate race condition? - FIX: Find way to prevent Svelte from writing back to textarea during input
- TEST: Run Safari/WebKit tests to validate fix
- DOCUMENT: Update WYSIWYG architecture docs with findings
- Can we use
bind:thiswithoutbind:value? - Can we disable Svelte's two-way binding update during input?
- Should evaluation pause during active typing?
- Should we debounce cursor position updates?
- Is there a Svelte-specific way to handle controlled textareas?
##Recommendations
- Keep
bind:valuefor now (lesser of two evils) - Add more logging to catch character corruption
- Run tests in WebKit to reproduce Safari bugs
- Refactor to eliminate
bind:valueproperly - Use uncontrolled textarea with careful state management
- Add comprehensive timing tests
- Consider alternative WYSIWYG architectures (ContentEditable, ProseMirror, etc.)
- Performance profiling of cursor position calculation
- Safari-specific optimizations
CRITICAL - This affects core editing functionality. Any text corruption is unacceptable.
Timeline: Should be fixed before any production release.
User Impact: HIGH - Users cannot trust the editor with their data.