Skip to content

Commit 26eb2d2

Browse files
authored
Merge pull request #15 from typelets/perf/reduce-sync-latency
fix: resolve TipTap v3 compatibility issues and clean up Monaco edito…
2 parents 55661a1 + dcbde51 commit 26eb2d2

File tree

4 files changed

+68
-34
lines changed

4 files changed

+68
-34
lines changed

src/components/editor/extensions/ExecutableCodeBlockNodeView.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,30 @@ export function ExecutableCodeBlockNodeView({ node, updateAttributes, selected:
102102
const monacoRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
103103
const resizeStartY = useRef<number>(0);
104104
const resizeStartHeight = useRef<number>(300);
105+
const isUpdatingFromMonaco = useRef(false);
105106

106107
const language = node.attrs.language || 'javascript';
107108
const isExecutable = codeExecutionService.isLanguageSupported(language as SupportedLanguage);
108109

109110

110111

111112
useEffect(() => {
112-
setCode(node.textContent);
113-
}, [node.textContent]);
113+
// COMPLETELY DISABLE this effect to prevent any Monaco updates from external sources
114+
// This should eliminate all blinking by never touching Monaco after initialization
115+
// External sync will be handled differently if needed
116+
return;
117+
118+
// Only update code if it's not coming from Monaco itself
119+
// This prevents the circular update loop that causes blinking
120+
const nodeText = node.textContent;
121+
if (nodeText !== code && !isUpdatingFromMonaco.current) {
122+
setCode(nodeText);
123+
// Since we're using defaultValue, manually update Monaco's content
124+
if (monacoRef.current?.getValue() !== nodeText) {
125+
monacoRef.current?.setValue(nodeText);
126+
}
127+
}
128+
}, [node.textContent, code]);
114129

115130

116131
useEffect(() => {
@@ -122,7 +137,7 @@ export function ExecutableCodeBlockNodeView({ node, updateAttributes, selected:
122137
setEditorHeight(newHeight);
123138

124139
// Trigger Monaco layout when height changes (throttled)
125-
if (monacoRef.current) {
140+
if (monacoRef.current && !isUpdatingFromMonaco.current) {
126141
monacoRef.current.layout();
127142
}
128143
};
@@ -140,10 +155,10 @@ export function ExecutableCodeBlockNodeView({ node, updateAttributes, selected:
140155
document.removeEventListener('mousemove', handleMouseMove);
141156
document.removeEventListener('mouseup', handleMouseUp);
142157
if (updateTimeoutRef.current) {
143-
if (updateTimeoutRef.current) {
144158
clearTimeout(updateTimeoutRef.current);
145159
}
146-
}
160+
// Reset the Monaco update flag on cleanup
161+
isUpdatingFromMonaco.current = false;
147162
};
148163
}, [isResizing]);
149164

@@ -155,7 +170,8 @@ export function ExecutableCodeBlockNodeView({ node, updateAttributes, selected:
155170
const triggerLayout = () => {
156171
clearTimeout(resizeTimeout);
157172
resizeTimeout = setTimeout(() => {
158-
if (monacoRef.current) {
173+
// Don't trigger layout if we're updating from Monaco to prevent blinking
174+
if (monacoRef.current && !isUpdatingFromMonaco.current) {
159175
monacoRef.current.layout();
160176
}
161177
}, 100); // Reduced debounce for better responsiveness
@@ -188,14 +204,13 @@ export function ExecutableCodeBlockNodeView({ node, updateAttributes, selected:
188204

189205
const handleCodeChange = (value: string | undefined) => {
190206
if (value !== undefined) {
191-
setCode(value);
192-
// Debounce the node content update to avoid performance issues
207+
// Minimal debounced update - only update TipTap node
193208
if (updateTimeoutRef.current) {
194209
clearTimeout(updateTimeoutRef.current);
195210
}
196211
updateTimeoutRef.current = setTimeout(() => {
197212
updateNodeContent(value);
198-
}, 300);
213+
}, 300); // Increased debounce to reduce updates
199214
}
200215
};
201216

@@ -359,10 +374,11 @@ export function ExecutableCodeBlockNodeView({ node, updateAttributes, selected:
359374
{/* Code Content */}
360375
<div className={`relative w-full ${monacoThemeOverride === 'light' ? 'monaco-light-override' : 'monaco-dark-override'}`}>
361376
<Editor
377+
key={`monaco-${node.attrs.language}-${getPos()}`}
362378
height={`${editorHeight}px`}
363379
width="100%"
364380
language={language}
365-
value={code}
381+
defaultValue={code}
366382
onChange={handleCodeChange}
367383
theme="vs-dark"
368384
options={{
@@ -410,7 +426,10 @@ export function ExecutableCodeBlockNodeView({ node, updateAttributes, selected:
410426
if (entries[0].isIntersecting) {
411427
// Small delay to ensure DOM is ready
412428
setTimeout(() => {
413-
monacoEditor.layout();
429+
// Don't layout if updating from Monaco to prevent blinking
430+
if (!isUpdatingFromMonaco.current) {
431+
monacoEditor.layout();
432+
}
414433
}, 50);
415434
}
416435
});

src/components/editor/hooks/useEditorEffects.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,42 @@ export function useEditorEffects({
2323
setBaseFontSize,
2424
lastContentRef,
2525
}: UseEditorEffectsProps) {
26+
2627
// Sync editor content with note changes
2728
useEffect(() => {
2829
if (!editor || !note) return;
2930

3031
const currentContent = editor.getHTML();
3132
if (note.content !== currentContent) {
32-
const { from, to } = editor.state.selection;
33-
editor.commands.setContent(note.content || '', { emitUpdate: false });
34-
lastContentRef.current = note.content || '';
35-
36-
// Update word and character counts
37-
const text = editor.state.doc.textContent;
38-
updateCounts(text);
39-
40-
try {
41-
const docSize = editor.state.doc.content.size;
42-
if (from <= docSize && to <= docSize) {
43-
editor.commands.setTextSelection({ from, to });
33+
const editorHasFocus = editor.isFocused;
34+
35+
if (!editorHasFocus) {
36+
const { from, to } = editor.state.selection;
37+
38+
editor.commands.setContent(note.content || '', {
39+
emitUpdate: false,
40+
parseOptions: {
41+
preserveWhitespace: 'full'
42+
}
43+
});
44+
lastContentRef.current = note.content || '';
45+
46+
const text = editor.state.doc.textContent;
47+
updateCounts(text);
48+
49+
try {
50+
const docSize = editor.state.doc.content.size;
51+
if (from <= docSize && to <= docSize) {
52+
editor.commands.setTextSelection({ from, to });
53+
}
54+
} catch {
55+
// Ignore cursor restoration errors
4456
}
45-
} catch {
46-
// Ignore cursor restoration errors
57+
} else {
58+
lastContentRef.current = currentContent;
4759
}
4860
}
61+
4962
}, [note, editor, updateCounts, lastContentRef]);
5063

5164
// Initialize word count when editor is ready

src/components/editor/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,11 @@ export default function Index({
319319
setSaveStatus('error');
320320
console.error('Failed to save note:', error);
321321
}
322-
}, 1000); // Wait 1 second after user stops typing
322+
}, 300); // Reduced from 1000ms to 300ms for faster sync
323323
}
324324
},
325325
},
326-
[note?.id, note?.hidden, updateCounts]
326+
[note?.id]
327327
);
328328

329329
// Use custom hook for editor effects
@@ -343,6 +343,7 @@ export default function Index({
343343
if (editor && note) {
344344
const currentContent = editor.getHTML();
345345

346+
346347
// Only update if content actually changed
347348
if (note.hidden && currentContent !== '[HIDDEN]') {
348349
editor.commands.setContent('[HIDDEN]');

src/hooks/useNotesOperations.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export function useNotesOperations({
148148
clearTimeout(existingTimeout);
149149
}
150150

151-
const immediateUpdates = ['starred', 'archived', 'deleted', 'folderId'];
151+
const immediateUpdates = ['starred', 'archived', 'deleted', 'folderId', 'title'];
152152
const needsImmediateSave = Object.keys(updates).some((key) =>
153153
immediateUpdates.includes(key)
154154
);
@@ -204,6 +204,11 @@ export function useNotesOperations({
204204
void loadData();
205205
}
206206
} else {
207+
// Send WebSocket update immediately for instant cross-tab sync
208+
if (webSocket.isAuthenticated) {
209+
webSocket.sendNoteUpdate(noteId, updates);
210+
}
211+
207212
const timeout = setTimeout(async () => {
208213
try {
209214
if (
@@ -218,6 +223,7 @@ export function useNotesOperations({
218223
);
219224
}
220225

226+
// API call for server persistence (follows WebSocket update)
221227
await api.updateNote(noteId, {
222228
title: updates.title,
223229
content: updates.content,
@@ -228,11 +234,6 @@ export function useNotesOperations({
228234
tags: updates.tags,
229235
});
230236

231-
// Send WebSocket notification about note update (delayed updates)
232-
if (webSocket.isAuthenticated) {
233-
webSocket.sendNoteUpdate(noteId, updates);
234-
}
235-
236237
saveTimeoutsRef.current.delete(noteId);
237238
} catch (error) {
238239
const secureError = sanitizeError(error, 'Failed to update note');
@@ -245,7 +246,7 @@ export function useNotesOperations({
245246
}
246247
void loadData();
247248
}
248-
}, 1500);
249+
}, 500); // Reduced from 1500ms to 500ms for faster sync
249250

250251
saveTimeoutsRef.current.set(noteId, timeout);
251252
}

0 commit comments

Comments
 (0)