@@ -21,7 +21,11 @@ const UNIFIED_SETTINGS_BACKUP_PATH = `${UNIFIED_SETTINGS_PATH}.bak`;
2121const RETRYABLE_FS_CODES = new Set ( [ "EBUSY" , "EPERM" ] ) ;
2222const TRANSIENT_READ_FS_CODES = new Set ( [ "EBUSY" , "EAGAIN" ] ) ;
2323let settingsWriteQueue : Promise < void > = Promise . resolve ( ) ;
24- let backupDerivedReadPending = false ;
24+
25+ type SettingsReadResult = {
26+ record : JsonRecord | null ;
27+ usedBackup : boolean ;
28+ } ;
2529
2630function isRetryableFsError ( error : unknown ) : boolean {
2731 const code = ( error as NodeJS . ErrnoException | undefined ) ?. code ;
@@ -137,8 +141,10 @@ function shouldFallbackToSettingsBackup(
137141 * When the current in-memory state was recovered from backup, snapshotting is
138142 * skipped so a corrupt primary cannot overwrite the only known-good backup.
139143 */
140- function trySnapshotUnifiedSettingsBackupSync ( ) : void {
141- if ( backupDerivedReadPending ) {
144+ function trySnapshotUnifiedSettingsBackupSync ( options ?: {
145+ skipBecauseBackupRead ?: boolean ;
146+ } ) : void {
147+ if ( options ?. skipBecauseBackupRead ) {
142148 return ;
143149 }
144150 if ( ! existsSync ( UNIFIED_SETTINGS_PATH ) ) {
@@ -160,8 +166,10 @@ function trySnapshotUnifiedSettingsBackupSync(): void {
160166/**
161167 * Async variant of `trySnapshotUnifiedSettingsBackupSync`.
162168 */
163- async function trySnapshotUnifiedSettingsBackupAsync ( ) : Promise < void > {
164- if ( backupDerivedReadPending ) {
169+ async function trySnapshotUnifiedSettingsBackupAsync ( options ?: {
170+ skipBecauseBackupRead ?: boolean ;
171+ } ) : Promise < void > {
172+ if ( options ?. skipBecauseBackupRead ) {
165173 return ;
166174 }
167175 if ( ! existsSync ( UNIFIED_SETTINGS_PATH ) ) {
@@ -190,26 +198,29 @@ async function trySnapshotUnifiedSettingsBackupAsync(): Promise<void> {
190198 * - Windows: file locking on Windows may cause reads to fail; in those cases this function returns `null`.
191199 * - Sensitive data: this function performs no token or secret redaction; any sensitive values present in the file are returned as-is and callers are responsible for redaction before logging or external exposure.
192200 */
193- function readSettingsRecordSync ( ) : JsonRecord | null {
201+ function readSettingsRecordSyncInternal ( ) : SettingsReadResult {
194202 const primaryExists = existsSync ( UNIFIED_SETTINGS_PATH ) ;
195203 try {
196204 const primaryRecord = readSettingsRecordSyncFromPath ( UNIFIED_SETTINGS_PATH ) ;
197205 if ( primaryRecord ) {
198- return primaryRecord ;
206+ return { record : primaryRecord , usedBackup : false } ;
199207 }
200208 } catch ( error ) {
201209 if ( ! shouldFallbackToSettingsBackup ( primaryExists , error ) ) {
202210 throw error ;
203211 }
204212 const backupRecord = readSettingsBackupSync ( ) ;
205213 if ( backupRecord ) {
206- backupDerivedReadPending = true ;
207- return backupRecord ;
214+ return { record : backupRecord , usedBackup : true } ;
208215 }
209216 throw error ;
210217 }
211218
212- return null ;
219+ return { record : null , usedBackup : false } ;
220+ }
221+
222+ function readSettingsRecordSync ( ) : JsonRecord | null {
223+ return readSettingsRecordSyncInternal ( ) . record ;
213224}
214225
215226/**
@@ -219,28 +230,31 @@ function readSettingsRecordSync(): JsonRecord | null {
219230 *
220231 * @returns The parsed settings record as an object clone, or `null` if unavailable or invalid.
221232 */
222- async function readSettingsRecordAsync ( ) : Promise < JsonRecord | null > {
233+ async function readSettingsRecordAsyncInternal ( ) : Promise < SettingsReadResult > {
223234 const primaryExists = existsSync ( UNIFIED_SETTINGS_PATH ) ;
224235 try {
225236 const primaryRecord = await readSettingsRecordAsyncFromPath (
226237 UNIFIED_SETTINGS_PATH ,
227238 ) ;
228239 if ( primaryRecord ) {
229- return primaryRecord ;
240+ return { record : primaryRecord , usedBackup : false } ;
230241 }
231242 } catch ( error ) {
232243 if ( ! shouldFallbackToSettingsBackup ( primaryExists , error ) ) {
233244 throw error ;
234245 }
235246 const backupRecord = await readSettingsBackupAsync ( ) ;
236247 if ( backupRecord ) {
237- backupDerivedReadPending = true ;
238- return backupRecord ;
248+ return { record : backupRecord , usedBackup : true } ;
239249 }
240250 throw error ;
241251 }
242252
243- return null ;
253+ return { record : null , usedBackup : false } ;
254+ }
255+
256+ async function readSettingsRecordAsync ( ) : Promise < JsonRecord | null > {
257+ return ( await readSettingsRecordAsyncInternal ( ) ) . record ;
244258}
245259
246260/**
@@ -275,20 +289,24 @@ function normalizeForWrite(record: JsonRecord): JsonRecord {
275289 *
276290 * @param record - The settings object to persist; it will be normalized to include the unified settings version.
277291 */
278- function writeSettingsRecordSync ( record : JsonRecord ) : void {
292+ function writeSettingsRecordSync (
293+ record : JsonRecord ,
294+ options ?: { skipBackupSnapshot ?: boolean } ,
295+ ) : void {
279296 mkdirSync ( getCodexMultiAuthDir ( ) , { recursive : true } ) ;
280297 const payload = normalizeForWrite ( record ) ;
281298 const data = `${ JSON . stringify ( payload , null , 2 ) } \n` ;
282299 const tempPath = `${ UNIFIED_SETTINGS_PATH } .${ process . pid } .${ Date . now ( ) } .tmp` ;
283- trySnapshotUnifiedSettingsBackupSync ( ) ;
300+ trySnapshotUnifiedSettingsBackupSync ( {
301+ skipBecauseBackupRead : options ?. skipBackupSnapshot ,
302+ } ) ;
284303 writeFileSync ( tempPath , data , "utf8" ) ;
285304 let moved = false ;
286305 try {
287306 for ( let attempt = 0 ; attempt < 5 ; attempt += 1 ) {
288307 try {
289308 renameSync ( tempPath , UNIFIED_SETTINGS_PATH ) ;
290309 moved = true ;
291- backupDerivedReadPending = false ;
292310 return ;
293311 } catch ( error ) {
294312 if ( ! isRetryableFsError ( error ) || attempt >= 4 ) {
@@ -328,20 +346,24 @@ function writeSettingsRecordSync(record: JsonRecord): void {
328346 *
329347 * @param record - The settings object to persist; it will be normalized (version set)
330348 */
331- async function writeSettingsRecordAsync ( record : JsonRecord ) : Promise < void > {
349+ async function writeSettingsRecordAsync (
350+ record : JsonRecord ,
351+ options ?: { skipBackupSnapshot ?: boolean } ,
352+ ) : Promise < void > {
332353 await fs . mkdir ( getCodexMultiAuthDir ( ) , { recursive : true } ) ;
333354 const payload = normalizeForWrite ( record ) ;
334355 const data = `${ JSON . stringify ( payload , null , 2 ) } \n` ;
335356 const tempPath = `${ UNIFIED_SETTINGS_PATH } .${ process . pid } .${ Date . now ( ) } .${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } .tmp` ;
336- await trySnapshotUnifiedSettingsBackupAsync ( ) ;
357+ await trySnapshotUnifiedSettingsBackupAsync ( {
358+ skipBecauseBackupRead : options ?. skipBackupSnapshot ,
359+ } ) ;
337360 await fs . writeFile ( tempPath , data , "utf8" ) ;
338361 let moved = false ;
339362 try {
340363 for ( let attempt = 0 ; attempt < 5 ; attempt += 1 ) {
341364 try {
342365 await fs . rename ( tempPath , UNIFIED_SETTINGS_PATH ) ;
343366 moved = true ;
344- backupDerivedReadPending = false ;
345367 return ;
346368 } catch ( error ) {
347369 if ( ! isRetryableFsError ( error ) || attempt >= 4 ) {
@@ -413,9 +435,12 @@ export function loadUnifiedPluginConfigSync(): JsonRecord | null {
413435 * @param pluginConfig - Key/value map representing plugin configuration to persist
414436 */
415437export function saveUnifiedPluginConfigSync ( pluginConfig : JsonRecord ) : void {
416- const record = readSettingsRecordSync ( ) ?? { } ;
417- record . pluginConfig = { ...pluginConfig } ;
418- writeSettingsRecordSync ( record ) ;
438+ const { record, usedBackup } = readSettingsRecordSyncInternal ( ) ;
439+ const nextRecord = record ?? { } ;
440+ nextRecord . pluginConfig = { ...pluginConfig } ;
441+ writeSettingsRecordSync ( nextRecord , {
442+ skipBackupSnapshot : usedBackup ,
443+ } ) ;
419444}
420445
421446/**
@@ -430,9 +455,12 @@ export function saveUnifiedPluginConfigSync(pluginConfig: JsonRecord): void {
430455 */
431456export async function saveUnifiedPluginConfig ( pluginConfig : JsonRecord ) : Promise < void > {
432457 await enqueueSettingsWrite ( async ( ) => {
433- const record = await readSettingsRecordAsync ( ) ?? { } ;
434- record . pluginConfig = { ...pluginConfig } ;
435- await writeSettingsRecordAsync ( record ) ;
458+ const { record, usedBackup } = await readSettingsRecordAsyncInternal ( ) ;
459+ const nextRecord = record ?? { } ;
460+ nextRecord . pluginConfig = { ...pluginConfig } ;
461+ await writeSettingsRecordAsync ( nextRecord , {
462+ skipBackupSnapshot : usedBackup ,
463+ } ) ;
436464 } ) ;
437465}
438466
@@ -473,8 +501,11 @@ export async function saveUnifiedDashboardSettings(
473501 dashboardDisplaySettings : JsonRecord ,
474502) : Promise < void > {
475503 await enqueueSettingsWrite ( async ( ) => {
476- const record = await readSettingsRecordAsync ( ) ?? { } ;
477- record . dashboardDisplaySettings = { ...dashboardDisplaySettings } ;
478- await writeSettingsRecordAsync ( record ) ;
504+ const { record, usedBackup } = await readSettingsRecordAsyncInternal ( ) ;
505+ const nextRecord = record ?? { } ;
506+ nextRecord . dashboardDisplaySettings = { ...dashboardDisplaySettings } ;
507+ await writeSettingsRecordAsync ( nextRecord , {
508+ skipBackupSnapshot : usedBackup ,
509+ } ) ;
479510 } ) ;
480511}
0 commit comments