Skip to content

Latest commit

 

History

History
327 lines (233 loc) · 7.49 KB

File metadata and controls

327 lines (233 loc) · 7.49 KB

WYSIWYG Editor - Combined Pattern Implementation Summary

What We Implemented

Successfully implemented the Combined Pattern based on research into ProseMirror, CodeMirror, and Trix editors to fix race condition bugs causing text corruption.

Changes Made

1. Added Synchronization Flags

// Prevent race conditions and feedback loops
let isUpdatingFromUser = $state(false);
let isUpdatingFromEvaluation = $state(false);

Purpose: Ensure one-way data flow at any given time, preventing circular updates.

2. Modified handleInput() Function

Before:

function handleInput() {
	rawText = textareaElement.value; // ← Triggers bind:value feedback!
	doc.updateRawText(rawText);
	updateCursorPosition();
	scheduleEvaluation();
}

After:

function handleInput() {
	if (!textareaElement || isUpdatingFromEvaluation) return;

	isUpdatingFromUser = true; // ← Set flag FIRST

	try {
		rawText = textareaElement.value; // Now safe from feedback
		doc.updateRawText(rawText);
		updateCursorPosition();
		scheduleEvaluation();
	} finally {
		// Clear flag AFTER Svelte processes the change
		queueMicrotask(() => {
			isUpdatingFromUser = false;
		});
	}
}

Key Improvements:

  • Flag set BEFORE state changes
  • try/finally ensures flag always cleared
  • queueMicrotask() ensures Svelte's reactivity completes
  • Skips if evaluation is in progress

3. Modified evaluateDocument() Function

Added:

async function evaluateDocument() {
	if (isUpdatingFromUser) {
		return; // Skip evaluation if user is typing
	}

	isEvaluating = true;
	isUpdatingFromEvaluation = true; // ← Set flag

	try {
		// ... evaluation logic ...
		// CRITICAL: NEVER write to textarea.value!
	} finally {
		isEvaluating = false;
		isUpdatingFromEvaluation = false; // ← Clear flag
	}
}

Key Improvements:

  • Skips evaluation during active typing
  • Sets flag to prevent input handler interference
  • Explicitly documented: NEVER writes to textarea

4. Removed bind:value (Replaced with Manual Sync)

Before:

<textarea bind:value={rawText} oninput={handleInput} />

After:

<textarea oninput={handleInput} />
<!-- NO bind:value -->

Added manual synchronization:

// Set initial value in onMount
onMount(() => {
	if (textareaElement) {
		textareaElement.value = rawText;
	}
	// ...
});

// Controlled synchronization with $effect
$effect(() => {
	// NEVER update during user input or evaluation
	if (isUpdatingFromUser || isUpdatingFromEvaluation) {
		return;
	}

	// Only for programmatic changes or initial load
	if (textareaElement && textareaElement.value !== rawText) {
		console.log('[WYSIWYG] $effect: Syncing (should be rare)');
		textareaElement.value = rawText;
	}
});

5. Data Flow Architecture

Old (Broken) Flow:

User types
  ↓
input event
  ↓
handleInput() sets rawText
  ↓
bind:value sees rawText changed
  ↓
bind:value writes back to textarea  ← RACE CONDITION!
  ↓
Next keystroke corrupted

New (Fixed) Flow:

User types
  ↓
input event
  ↓
isUpdatingFromUser = true  ← Flag prevents feedback
  ↓
handleInput() sets rawText
  ↓
$effect sees flag, skips textarea update
  ↓
queueMicrotask clears flag
  ↓
No race condition!

Test Results

Before Implementation

Character corruption examples:

  • "salary" → "slryaa" (reordered)
  • "bonus" → "sunob" (reversed)
  • "$500,000" → "$500, 000" (phantom space)
  • "monthly_salary" → "mo_salary" ('y' deleted)

Test failures: Frequent and severe

After Implementation

Test Results:

  • ✅ 26 out of 27 tests PASS
  • ✅ NO character reordering
  • ✅ NO character reversal
  • ✅ NO phantom spaces in most tests
  • ⚠️ 1 test still flaky (drops '$' occasionally)

Improvement: ~96% success rate vs previous ~60-70% with severe corruption


What This Fixes

✅ Fixed Issues

  1. Character reordering - Characters no longer scrambled during typing
  2. Character reversal - Text no longer appears backwards
  3. Race condition - bind:value no longer fights with user input
  4. Feedback loop - One-way data flow established
  5. Cross-browser reliability - Pattern works in Chromium, should work in Safari

⚠️ Partially Fixed

  1. Flaky character dropping - Rare ($1 dropped in 1/27 tests)
    • Occurs only with specific timing (random delays in fuzz test)
    • Much improved from before (was dropping/reordering frequently)

🔍 Still To Verify

  1. Safari-specific testing - Need to test on actual Safari browser
  2. WebKit engine - Should run tests with --project=webkit
  3. Production usage - Real-world typing patterns may reveal edge cases

Key Principles Applied

  1. Never write to textarea during input events
  2. Use flags to prevent circular updates
  3. Separate user input from programmatic updates
  4. Use microtasks for timing control
  5. Evaluation should NEVER modify textarea
  6. Textarea is source of truth (read-only from our perspective)

Pattern Source

Based on research into production editors:

  • ProseMirror/CodeMirror: "updating" flag pattern
  • Trix: Level 2 input events, beforeinput handling
  • Common approach: Prevent feedback loops with synchronization guards

Next Steps

Immediate

  1. ✅ Implementation complete
  2. ⏭️ Test in Safari/WebKit
  3. ⏭️ Monitor flaky test (fuzz test)
  4. ⏭️ User testing in production-like environment

Future Improvements

  1. Consider beforeinput event (Level 2 InputEvents)
  2. Add more stress tests with rapid typing
  3. Add Safari-specific timing tests
  4. Performance profiling for updateCursorPosition()

Documentation

  1. ✅ Implementation documented
  2. ✅ Pattern explained
  3. ✅ Test results recorded
  4. ⏭️ Update architecture docs

Risk Assessment

Risk Level: LOW to MEDIUM

  • Implementation is conservative (adds guards, doesn't remove safety)
  • Test coverage is comprehensive (27 tests)
  • Pattern is proven in production editors
  • 96% test success rate

Remaining Risks:

  • Flaky character dropping (needs more investigation)
  • Safari-specific behavior untested
  • Edge cases in real-world usage

Mitigation:

  • Comprehensive test suite catches regressions
  • Flags can be inspected for debugging
  • Pattern can be refined based on user feedback

Performance Impact

Expected: Minimal to none

  • Flags are simple boolean checks
  • queueMicrotask() is fast
  • $effect only runs when flags allow
  • Removed redundant bind:value updates

Measured: Not yet profiled


Code Maintainability

Improved:

  • Clear separation of concerns (user input vs evaluation)
  • Well-documented synchronization logic
  • Explicit data flow (no hidden reactivity)
  • Easy to debug (flags can be logged)

Added Complexity:

  • Two synchronization flags to track
  • Manual textarea value management
  • More lines of code

Balance: Acceptable tradeoff for reliability


Conclusion

The combined pattern successfully addresses the root cause of text corruption bugs by preventing bind:value feedback loops and ensuring one-way data flow. While one test remains slightly flaky, the improvement from severe corruption to occasional single-character dropping is substantial.

The implementation is based on proven patterns from production editors (ProseMirror, CodeMirror, Trix) and should provide a solid foundation for reliable text editing across browsers.

Status:Ready for Safari/WebKit testing and user validation