diff --git a/src/lib/components/ArticleComposer.svelte b/src/lib/components/ArticleComposer.svelte index f180755..4f810d6 100644 --- a/src/lib/components/ArticleComposer.svelte +++ b/src/lib/components/ArticleComposer.svelte @@ -23,7 +23,9 @@ let coverUploading = $state(false); let initialized = $state(false); let lastContent = ''; - let saveSequence = 0; + let saveTimer: number | undefined; + let saveInFlight = false; + let saveQueued = false; const groupedTags = $derived.by(() => { const groups: Record = {}; for (const tag of data.tags as ComposerTag[]) (groups[tag.group] ??= []).push(tag); @@ -35,6 +37,40 @@ coverImageId, coverAlt, coverCaption, version }; } + function scheduleSave() { + if (saveTimer !== undefined) window.clearTimeout(saveTimer); + saveTimer = window.setTimeout(() => { + saveTimer = undefined; + void saveDraft(); + }, 900); + } + + async function saveDraft() { + if (saveInFlight || !saveQueued) return; + saveInFlight = true; + saveQueued = false; + const draft = { ...payload(), tagIds: [...selectedTags] }; + try { + const response = await fetch('/api/articles/draft', { + method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(draft) + }); + const result = await response.json(); + if (response.status === 409) { + saveState = 'conflict'; + return; + } + if (!response.ok) throw new Error(result.message || 'Draft save failed.'); + version = result.version; + saveState = saveQueued ? 'saving' : 'saved'; + } catch (error) { + saveState = 'error'; + toast.error('Draft not saved', error instanceof Error ? error.message : 'Try again.'); + } finally { + saveInFlight = false; + if (saveQueued && saveState !== 'conflict') scheduleSave(); + } + } + $effect(() => { const content = JSON.stringify({ title, deck, document, selectedTags, channel, coverImageId, coverAlt, coverCaption }); if (!initialized) { @@ -44,29 +80,13 @@ } if (content === lastContent) return; lastContent = content; - const sequence = ++saveSequence; + saveQueued = true; saveState = 'saving'; - const timer = window.setTimeout(async () => { - try { - const response = await fetch('/api/articles/draft', { - method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload()) - }); - const result = await response.json(); - if (sequence !== saveSequence) return; - if (response.status === 409) { - saveState = 'conflict'; - return; - } - if (!response.ok) throw new Error(result.message || 'Draft save failed.'); - version = result.version; - saveState = 'saved'; - } catch (error) { - if (sequence !== saveSequence) return; - saveState = 'error'; - toast.error('Draft not saved', error instanceof Error ? error.message : 'Try again.'); - } - }, 900); - return () => window.clearTimeout(timer); + if (!saveInFlight) scheduleSave(); + return () => { + if (saveTimer !== undefined) window.clearTimeout(saveTimer); + saveTimer = undefined; + }; }); function toggleTag(id: string) { @@ -115,7 +135,7 @@

{visibility === 'private' ? 'Only you can open this draft.' : visibility === 'unlisted' ? 'Anyone with the direct URL can read it.' : 'Listed in discovery and visible to everyone.'}

{#if data.article.pendingRevisionId}MODERATION REVIEW PENDING{/if}
- +