* Returns true if a write was performed; false if the write was skipped * (no changes, or auth-loss guard tripped). Callers use this to decide * whether to invalidate the cache -- invalidating after a skipped write * destroys the good cached state the auth-loss guard depends on.
( file: string, createDefault: () => A, mergeFn: (current: A) => A, )
| 1225 | * destroys the good cached state the auth-loss guard depends on. |
| 1226 | */ |
| 1227 | function saveConfigWithLock<A extends object>( |
| 1228 | file: string, |
| 1229 | createDefault: () => A, |
| 1230 | mergeFn: (current: A) => A, |
| 1231 | ): boolean { |
| 1232 | const defaultConfig = createDefault() |
| 1233 | const dir = dirname(file) |
| 1234 | const fs = getFsImplementation() |
| 1235 | |
| 1236 | // Ensure directory exists (mkdirSync is already recursive in FsOperations) |
| 1237 | fs.mkdirSync(dir) |
| 1238 | |
| 1239 | let release |
| 1240 | try { |
| 1241 | const lockFilePath = `${file}.lock` |
| 1242 | const startTime = Date.now() |
| 1243 | release = lockfile.lockSync(file, { |
| 1244 | lockfilePath: lockFilePath, |
| 1245 | onCompromised: err => { |
| 1246 | // Default onCompromised throws from a setTimeout callback, which |
| 1247 | // becomes an unhandled exception. Log instead -- the lock being |
| 1248 | // stolen (e.g. after a 10s event-loop stall) is recoverable. |
| 1249 | logForDebugging(`Config lock compromised: ${err}`, { level: 'error' }) |
| 1250 | }, |
| 1251 | }) |
| 1252 | const lockTime = Date.now() - startTime |
| 1253 | if (lockTime > 100) { |
| 1254 | logForDebugging( |
| 1255 | 'Lock acquisition took longer than expected - another NCode instance may be running', |
| 1256 | ) |
| 1257 | logEvent('ncode_config_lock_contention', { |
| 1258 | lock_time_ms: lockTime, |
| 1259 | }) |
| 1260 | } |
| 1261 | |
| 1262 | // Check for stale write - file changed since we last read it |
| 1263 | // Only check for global config file since lastReadFileStats tracks that specific file |
| 1264 | if (lastReadFileStats && file === getGlobalNcodeFile()) { |
| 1265 | try { |
| 1266 | const currentStats = fs.statSync(file) |
| 1267 | if ( |
| 1268 | currentStats.mtimeMs !== lastReadFileStats.mtime || |
| 1269 | currentStats.size !== lastReadFileStats.size |
| 1270 | ) { |
| 1271 | logEvent('ncode_config_stale_write', { |
| 1272 | read_mtime: lastReadFileStats.mtime, |
| 1273 | write_mtime: currentStats.mtimeMs, |
| 1274 | read_size: lastReadFileStats.size, |
| 1275 | write_size: currentStats.size, |
| 1276 | }) |
| 1277 | } |
| 1278 | } catch (e) { |
| 1279 | const code = getErrnoCode(e) |
| 1280 | if (code !== 'ENOENT') { |
| 1281 | throw e |
| 1282 | } |
| 1283 | // File doesn't exist yet, no stale check needed |
| 1284 | } |
no test coverage detected