From e1e2b427e5f76323d54b792d6b18ba6ae270a02a Mon Sep 17 00:00:00 2001 From: Murad Date: Sat, 21 Feb 2026 14:39:08 +0300 Subject: [PATCH 1/4] fix: resolve race condition in auth bento password animation --- src/lib/animations/index.ts | 44 +++++++++++++++--- .../bento/(animations)/auth.svelte | 46 +++++++++++++++---- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/lib/animations/index.ts b/src/lib/animations/index.ts index 341ef1872a..d15c065b59 100644 --- a/src/lib/animations/index.ts +++ b/src/lib/animations/index.ts @@ -1,36 +1,68 @@ -export function write(text: string, cb: (v: string) => void, duration = 500) { +export function write( + text: string, + cb: (v: string) => void, + duration = 500, + { signal, startIndex = 0 }: { signal?: AbortSignal; startIndex?: number } = {} +) { if (text.length === 0) { cb(''); return Promise.resolve(); } const step = duration / text.length; - let i = 0; - return new Promise((resolve) => { + let i = startIndex; + + return new Promise((resolve, reject) => { const interval = setInterval(() => { + if (signal?.aborted) { + clearInterval(interval); + return reject(new Error('Aborted')); + } + cb(text.slice(0, ++i)); if (i === text.length) { clearInterval(interval); resolve(); } }, step); + + signal?.addEventListener('abort', () => { + clearInterval(interval); + reject(new Error('Aborted')); + }); }); } -export function unwrite(text: string, cb: (v: string) => void, duration = 500) { +export function unwrite( + text: string, + cb: (v: string) => void, + duration = 500, + { signal, startIndex }: { signal?: AbortSignal; startIndex?: number } = {} +) { if (text.length === 0) { cb(''); return Promise.resolve(); } const step = duration / text.length; - let i = text.length; - return new Promise((resolve) => { + let i = startIndex ?? text.length; + + return new Promise((resolve, reject) => { const interval = setInterval(() => { + if (signal?.aborted) { + clearInterval(interval); + return reject(new Error('Aborted')); + } + cb(text.slice(0, --i)); if (i === 0) { clearInterval(interval); resolve(); } }, step); + + signal?.addEventListener('abort', () => { + clearInterval(interval); + reject(new Error('Aborted')); + }); }); } diff --git a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte index f1b2ebf338..ce2296cd8b 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte @@ -14,17 +14,33 @@ let password = $state(''); let button: HTMLButtonElement; + let controller: AbortController | null = null; + $effect(() => { inView( container, () => { if (!isMobile()) return; - write('•••••••••••••', (v) => (password = v), 1000).then(() => { - animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); - }); + controller?.abort(); + controller = new AbortController(); + + write('•••••••••••••', (v) => (password = v), 1000, { + signal: controller.signal, + startIndex: password.length + }) + .then(() => { + animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); + }) + .catch(() => {}); + return () => { - unwrite('•••••••••••••', (v) => (password = v)); + controller?.abort(); + controller = new AbortController(); + unwrite('•••••••••••••', (v) => (password = v), 500, { + signal: controller.signal, + startIndex: password.length + }).catch(() => {}); }; }, { amount: 'all' } @@ -33,11 +49,25 @@ hover(container, () => { if (isMobile()) return; - write('•••••••••••••', (v) => (password = v), 1000).then(() => { - animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); - }); + controller?.abort(); + controller = new AbortController(); + + write('•••••••••••••', (v) => (password = v), 1000, { + signal: controller.signal, + startIndex: password.length + }) + .then(() => { + animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); + }) + .catch(() => {}); + return () => { - unwrite('•••••••••••••', (v) => (password = v)); + controller?.abort(); + controller = new AbortController(); + unwrite('•••••••••••••', (v) => (password = v), 500, { + signal: controller.signal, + startIndex: password.length + }).catch(() => {}); }; }); }); From 5bc067755b0f37ea893af2daf5cc5ee431646d8d Mon Sep 17 00:00:00 2001 From: Murad Date: Sat, 21 Feb 2026 14:52:05 +0300 Subject: [PATCH 2/4] fix: address coderabbit review comments --- src/lib/animations/index.ts | 24 ++++++++++++------- .../bento/(animations)/auth.svelte | 4 ++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/lib/animations/index.ts b/src/lib/animations/index.ts index d15c065b59..0beaa3ab5c 100644 --- a/src/lib/animations/index.ts +++ b/src/lib/animations/index.ts @@ -25,10 +25,14 @@ export function write( } }, step); - signal?.addEventListener('abort', () => { - clearInterval(interval); - reject(new Error('Aborted')); - }); + signal?.addEventListener( + 'abort', + () => { + clearInterval(interval); + reject(new Error('Aborted')); + }, + { once: true } + ); }); } @@ -59,10 +63,14 @@ export function unwrite( } }, step); - signal?.addEventListener('abort', () => { - clearInterval(interval); - reject(new Error('Aborted')); - }); + signal?.addEventListener( + 'abort', + () => { + clearInterval(interval); + reject(new Error('Aborted')); + }, + { once: true } + ); }); } diff --git a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte index ce2296cd8b..6707b94b91 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte @@ -70,6 +70,10 @@ }).catch(() => {}); }; }); + + return () => { + controller?.abort(); + }; }); From 255bad764448f4261f72c5cac7458c0db88cab71 Mon Sep 17 00:00:00 2001 From: Murad Date: Sat, 21 Feb 2026 14:54:58 +0300 Subject: [PATCH 3/4] fix: add bounds guards to write and unwrite to prevent infinite loop --- src/lib/animations/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/animations/index.ts b/src/lib/animations/index.ts index 0beaa3ab5c..7317585936 100644 --- a/src/lib/animations/index.ts +++ b/src/lib/animations/index.ts @@ -8,6 +8,10 @@ export function write( cb(''); return Promise.resolve(); } + if (startIndex >= text.length) { + cb(text); + return Promise.resolve(); + } const step = duration / text.length; let i = startIndex; @@ -48,6 +52,11 @@ export function unwrite( } const step = duration / text.length; let i = startIndex ?? text.length; + + if (i <= 0) { + cb(''); + return Promise.resolve(); + } return new Promise((resolve, reject) => { const interval = setInterval(() => { From c1d223e7934ca251bc5e5e78fc8ce13d254fed8c Mon Sep 17 00:00:00 2001 From: Murad Date: Sat, 21 Feb 2026 15:00:04 +0300 Subject: [PATCH 4/4] fix: suppress only abort errors in animation promise chains --- .../(components)/bento/(animations)/auth.svelte | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte index 6707b94b91..206bda3d32 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte @@ -32,7 +32,9 @@ .then(() => { animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); }) - .catch(() => {}); + .catch((err: unknown) => { + if (err instanceof Error && err.message !== 'Aborted') console.error(err); + }); return () => { controller?.abort(); @@ -40,7 +42,9 @@ unwrite('•••••••••••••', (v) => (password = v), 500, { signal: controller.signal, startIndex: password.length - }).catch(() => {}); + }).catch((err: unknown) => { + if (err instanceof Error && err.message !== 'Aborted') console.error(err); + }); }; }, { amount: 'all' } @@ -59,7 +63,9 @@ .then(() => { animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); }) - .catch(() => {}); + .catch((err: unknown) => { + if (err instanceof Error && err.message !== 'Aborted') console.error(err); + }); return () => { controller?.abort(); @@ -67,7 +73,9 @@ unwrite('•••••••••••••', (v) => (password = v), 500, { signal: controller.signal, startIndex: password.length - }).catch(() => {}); + }).catch((err: unknown) => { + if (err instanceof Error && err.message !== 'Aborted') console.error(err); + }); }; });