( updater: (current: Settings) => Settings | Promise<Settings> )
| 88 | * @returns The updated settings |
| 89 | */ |
| 90 | export async function updateSettings( |
| 91 | updater: (current: Settings) => Settings | Promise<Settings> |
| 92 | ): Promise<Settings> { |
| 93 | // Timing constants |
| 94 | const LOCK_RETRY_INTERVAL_MS = 100; // How long to wait between lock attempts |
| 95 | const MAX_LOCK_ATTEMPTS = 50; // Maximum number of attempts (5 seconds total) |
| 96 | const STALE_LOCK_TIMEOUT_MS = 10000; // Consider lock stale after 10 seconds |
| 97 | |
| 98 | if (!existsSync(configuration.happyHomeDir)) { |
| 99 | await mkdir(configuration.happyHomeDir, { recursive: true }); |
| 100 | } |
| 101 | |
| 102 | const lockFile = configuration.settingsFile + '.lock'; |
| 103 | const tmpFile = configuration.settingsFile + '.tmp'; |
| 104 | let fileHandle; |
| 105 | let attempts = 0; |
| 106 | |
| 107 | // Acquire exclusive lock with retries |
| 108 | while (attempts < MAX_LOCK_ATTEMPTS) { |
| 109 | try { |
| 110 | // 'wx' = create exclusively, fail if exists (cross-platform compatible) |
| 111 | fileHandle = await open(lockFile, 'wx'); |
| 112 | break; |
| 113 | } catch (err: any) { |
| 114 | if (err.code === 'EEXIST') { |
| 115 | // Lock file exists, wait and retry |
| 116 | attempts++; |
| 117 | await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS)); |
| 118 | |
| 119 | // Check for stale lock |
| 120 | try { |
| 121 | const stats = await stat(lockFile); |
| 122 | if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) { |
| 123 | await unlink(lockFile).catch(() => { }); |
| 124 | } |
| 125 | } catch { } |
| 126 | } else { |
| 127 | throw err; |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | if (!fileHandle) { |
| 133 | throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1000} seconds`); |
| 134 | } |
| 135 | |
| 136 | try { |
| 137 | // Read current settings with defaults |
| 138 | const current = await readSettings() || { ...defaultSettings }; |
| 139 | |
| 140 | // Apply update |
| 141 | const updated = await updater(current); |
| 142 | |
| 143 | // Write atomically using rename |
| 144 | await writeFile(tmpFile, JSON.stringify(updated, null, 2)); |
| 145 | await rename(tmpFile, configuration.settingsFile); // Atomic on POSIX |
| 146 | |
| 147 | return updated; |
no test coverage detected