( cache: PersistedStatsCache, )
| 212 | * Uses a temp file + rename pattern to prevent corruption. |
| 213 | */ |
| 214 | export async function saveStatsCache( |
| 215 | cache: PersistedStatsCache, |
| 216 | ): Promise<void> { |
| 217 | const fs = getFsImplementation() |
| 218 | const cachePath = getStatsCachePath() |
| 219 | const tempPath = `${cachePath}.${randomBytes(8).toString('hex')}.tmp` |
| 220 | |
| 221 | try { |
| 222 | // Ensure the directory exists |
| 223 | const configDir = getClaudeConfigHomeDir() |
| 224 | try { |
| 225 | await fs.mkdir(configDir) |
| 226 | } catch { |
| 227 | // Directory already exists or other error - proceed |
| 228 | } |
| 229 | |
| 230 | // Write to temp file with fsync for atomic write safety |
| 231 | const content = jsonStringify(cache, null, 2) |
| 232 | const handle = await open(tempPath, 'w', 0o600) |
| 233 | try { |
| 234 | await handle.writeFile(content, { encoding: 'utf-8' }) |
| 235 | await handle.sync() |
| 236 | } finally { |
| 237 | await handle.close() |
| 238 | } |
| 239 | |
| 240 | // Atomic rename |
| 241 | await fs.rename(tempPath, cachePath) |
| 242 | logForDebugging( |
| 243 | `Stats cache saved successfully (lastComputedDate: ${cache.lastComputedDate})`, |
| 244 | ) |
| 245 | } catch (error) { |
| 246 | logError(error) |
| 247 | // Clean up temp file |
| 248 | try { |
| 249 | await fs.unlink(tempPath) |
| 250 | } catch { |
| 251 | // Ignore cleanup errors |
| 252 | } |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * Merge new stats into an existing cache. |
no test coverage detected