Skip to content

Commit 0aa07c3

Browse files
committed
fix: infinity update loops under certain conditions
re: #157
1 parent 8625a09 commit 0aa07c3

File tree

2 files changed

+80
-35
lines changed

2 files changed

+80
-35
lines changed

packages/carta-md/src/lib/internal/components/Input.svelte

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
-->
66

77
<script lang="ts">
8-
import { onMount, type Snippet } from 'svelte';
98
import type { Carta } from '../carta';
9+
import type { UIEventHandler } from 'svelte/elements';
1010
import type { TextAreaProps } from '../textarea-props';
11+
import { onMount, type Snippet } from 'svelte';
1112
import { debounce } from '../utils';
1213
import { BROWSER } from 'esm-env';
1314
import { speculativeHighlightUpdate } from '../speculative';
14-
import type { UIEventHandler } from 'svelte/elements';
1515
1616
interface Props {
1717
/**
@@ -61,8 +61,6 @@
6161
let textarea: HTMLTextAreaElement;
6262
let highlightElem: HTMLDivElement;
6363
let wrapperElem: HTMLDivElement;
64-
let highlighted = $state(value);
65-
let mounted = $state(false);
6664
let currentlyHighlightedValue = value;
6765
6866
const simpleUUID = Math.random().toString(36).substring(2);
@@ -94,6 +92,9 @@
9492
}
9593
};
9694
95+
/**
96+
* Focus the textarea element.
97+
*/
9798
const focus = () => {
9899
// Allow text selection
99100
const selectedText = window.getSelection()?.toString();
@@ -106,24 +107,51 @@
106107
* Highlight the text in the textarea.
107108
* @param text The text to highlight.
108109
*/
109-
const highlight = async (text: string) => {
110+
async function highlight(text: string) {
110111
const highlighter = await carta.highlighter();
111-
if (!highlighter) return;
112+
if (!highlighter) return null;
112113
113114
const html = highlighter.codeToHtml(text);
115+
const timestamp = new Date().getTime();
114116
115117
if (carta.sanitizer) {
116-
highlighted = carta.sanitizer(html);
118+
return { html: carta.sanitizer(html), timestamp };
117119
} else {
118-
highlighted = html;
120+
return { html, timestamp };
119121
}
122+
}
120123
121-
currentlyHighlightedValue = value;
124+
/**
125+
* Debounced version of the highlight function.
126+
*/
127+
const debouncedHighlight = debounce(highlight, highlightDelay);
122128
123-
requestAnimationFrame(resize);
124-
};
129+
/**
130+
* Returns the highlighted text using a speculative update.
131+
* @param text The text to highlight.
132+
*/
133+
function speculativeHighlight(value: string) {
134+
const timestamp = new Date().getTime();
125135
126-
const debouncedHighlight = debounce(highlight, highlightDelay);
136+
if (!mounted) return { html: '', timestamp };
137+
138+
const currentOverlay = highlightElem.innerHTML;
139+
if (highlightElem) {
140+
try {
141+
const html = speculativeHighlightUpdate(currentlyHighlightedValue, value, currentOverlay);
142+
currentlyHighlightedValue = value;
143+
144+
return { html, timestamp };
145+
} catch (e) {
146+
console.error(`Error executing speculative update: ${e}.`);
147+
}
148+
}
149+
150+
return {
151+
html: highlightElem.innerHTML,
152+
timestamp
153+
};
154+
}
127155
128156
/**
129157
* Highlight the nested languages in the markdown, loading the necessary
@@ -137,35 +165,50 @@
137165
138166
if (!highlighter) return;
139167
const { updated } = await loadNestedLanguages(highlighter, text);
140-
if (updated) debouncedHighlight(text);
168+
if (updated) overlayPromise = debouncedHighlight(text);
141169
}, 300);
142170
143-
const onValueChange = (value: string) => {
144-
if (highlightElem) {
145-
try {
146-
highlighted = speculativeHighlightUpdate(currentlyHighlightedValue, value, highlighted);
147-
currentlyHighlightedValue = value;
148-
requestAnimationFrame(resize);
149-
} catch (e) {
150-
console.error(`Error executing speculative update: ${e}.`);
151-
}
152-
}
153-
154-
debouncedHighlight(value);
155-
171+
/**
172+
* Value change handler.
173+
*/
174+
const onchange = (value: string) => {
156175
highlightNestedLanguages(value);
157176
};
158177
178+
/**
179+
* Runes
180+
*/
181+
let mounted = $state(false);
182+
let overlayPromise = $derived(debouncedHighlight(value));
183+
let overlay = $state<Awaited<typeof overlayPromise>>(null);
184+
let speculativeOverlay = $derived(speculativeHighlight(value));
185+
let displayedOverlay = $derived(
186+
overlay && overlay.timestamp > speculativeOverlay.timestamp ? overlay : speculativeOverlay
187+
);
188+
189+
$effect(() => {
190+
if (BROWSER) onchange(value);
191+
});
192+
193+
$effect(() => {
194+
overlayPromise.then(async (o) => (overlay = await o));
195+
});
196+
159197
$effect(() => {
160-
if (BROWSER) onValueChange(value);
198+
if (mounted) {
199+
displayedOverlay; // When the overlay changes
200+
requestAnimationFrame(resize); // Resize the textarea
201+
}
161202
});
162203
204+
/**
205+
* Mount callback
206+
*/
163207
onMount(() => {
164208
mounted = true;
165209
// Resize once the DOM is updated.
166210
requestAnimationFrame(resize);
167-
});
168-
onMount(() => {
211+
169212
carta.$setInput(textarea, elem!, () => {
170213
value = textarea.value;
171214
});
@@ -197,7 +240,8 @@
197240
aria-hidden="true"
198241
bind:this={highlightElem}
199242
>
200-
<!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}
243+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
244+
{@html displayedOverlay.html}
201245
</div>
202246

203247
<textarea

packages/carta-md/src/lib/internal/utils.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ export type NonNullable<T> = Exclude<T, null | undefined>;
1313
* @param cb Callback function.
1414
* @param wait The time to wait in milliseconds.
1515
*/
16-
export function debounce<T extends unknown[]>(cb: (...args: T) => unknown, wait = 1000) {
16+
export function debounce<T extends unknown[], K>(cb: (...args: T) => K, wait = 1000) {
1717
let timeout: NodeJS.Timeout;
18-
return (...args: T) => {
19-
clearTimeout(timeout);
20-
timeout = setTimeout(() => cb(...args), wait);
21-
};
18+
return (...args: T) =>
19+
new Promise<K>((resolve) => {
20+
clearTimeout(timeout);
21+
timeout = setTimeout(() => resolve(cb(...args)), wait);
22+
});
2223
}
2324

2425
/**

0 commit comments

Comments
 (0)