| 92 | * Save a draft to disk. |
| 93 | */ |
| 94 | export function saveDraft(key: string, data: object): boolean { |
| 95 | const draftGeneration = readGeneration((data as { draftGeneration?: unknown }).draftGeneration); |
| 96 | const deletedGeneration = readTombstoneGeneration(key); |
| 97 | if (draftGeneration !== null && deletedGeneration !== null && draftGeneration <= deletedGeneration) { |
| 98 | return false; |
| 99 | } |
| 100 | const storedGeneration = readStoredDraftGeneration(key); |
| 101 | if (draftGeneration !== null && storedGeneration !== null && draftGeneration < storedGeneration) { |
| 102 | return false; |
| 103 | } |
| 104 | |
| 105 | // Write-then-rename so a crash mid-write can't leave a truncated draft — |
| 106 | // loadDraft would silently return null at exactly the recovery moment |
| 107 | // drafts exist for. rename() is atomic within a directory. |
| 108 | const finalPath = draftPath(key); |
| 109 | const tmpPath = `${finalPath}.tmp`; |
| 110 | writeFileSync(tmpPath, JSON.stringify(data), "utf-8"); |
| 111 | renameSync(tmpPath, finalPath); |
| 112 | if (draftGeneration === null) { |
| 113 | clearTombstone(key); |
| 114 | } |
| 115 | return true; |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Load a draft from disk. Returns null if not found. |