@@ -8,30 +8,48 @@ type MemoryIndexFileOps = {
88 wait : ( ms : number ) => Promise < void > ;
99} ;
1010
11- type MoveMemoryIndexFilesOptions = {
11+ type MemoryIndexFileOptions = {
1212 fileOps ?: MemoryIndexFileOps ;
1313 maxRenameAttempts ?: number ;
1414 renameRetryDelayMs ?: number ;
15+ maxRemoveAttempts ?: number ;
16+ removeRetryDelayMs ?: number ;
1517} ;
1618
19+ type ResolvedMemoryIndexFileOptions = Required < MemoryIndexFileOptions > ;
20+
1721const defaultFileOps : MemoryIndexFileOps = {
1822 rename : fs . rename ,
1923 rm : fs . rm ,
2024 wait : sleep ,
2125} ;
2226
23- const transientRenameErrorCodes = new Set ( [ "EBUSY" , "EPERM" , "EACCES" ] ) ;
27+ const transientFileErrorCodes = new Set ( [ "EBUSY" , "EPERM" , "EACCES" ] ) ;
2428const defaultMaxRenameAttempts = 6 ;
2529const defaultRenameRetryDelayMs = 25 ;
30+ const defaultMaxRemoveAttempts = 10 ;
31+ const defaultRemoveRetryDelayMs = 50 ;
32+
33+ function isTransientFileError ( err : unknown ) : boolean {
34+ return transientFileErrorCodes . has ( ( err as NodeJS . ErrnoException ) . code ?? "" ) ;
35+ }
2636
27- function isTransientRenameError ( err : unknown ) : boolean {
28- return transientRenameErrorCodes . has ( ( err as NodeJS . ErrnoException ) . code ?? "" ) ;
37+ function resolveMemoryIndexFileOptions (
38+ options : MemoryIndexFileOptions = { } ,
39+ ) : ResolvedMemoryIndexFileOptions {
40+ return {
41+ fileOps : options . fileOps ?? defaultFileOps ,
42+ maxRenameAttempts : Math . max ( 1 , options . maxRenameAttempts ?? defaultMaxRenameAttempts ) ,
43+ renameRetryDelayMs : options . renameRetryDelayMs ?? defaultRenameRetryDelayMs ,
44+ maxRemoveAttempts : Math . max ( 1 , options . maxRemoveAttempts ?? defaultMaxRemoveAttempts ) ,
45+ removeRetryDelayMs : options . removeRetryDelayMs ?? defaultRemoveRetryDelayMs ,
46+ } ;
2947}
3048
3149async function renameWithRetry (
3250 source : string ,
3351 target : string ,
34- options : Required < MoveMemoryIndexFilesOptions > ,
52+ options : ResolvedMemoryIndexFileOptions ,
3553) : Promise < void > {
3654 for ( let attempt = 1 ; attempt <= options . maxRenameAttempts ; attempt ++ ) {
3755 try {
@@ -41,7 +59,7 @@ async function renameWithRetry(
4159 if ( ( err as NodeJS . ErrnoException ) . code === "ENOENT" ) {
4260 return ;
4361 }
44- if ( ! isTransientRenameError ( err ) || attempt === options . maxRenameAttempts ) {
62+ if ( ! isTransientFileError ( err ) || attempt === options . maxRenameAttempts ) {
4563 throw err ;
4664 }
4765 await options . fileOps . wait ( options . renameRetryDelayMs * attempt ) ;
@@ -53,13 +71,9 @@ async function renameWithRetry(
5371export async function moveMemoryIndexFiles (
5472 sourceBase : string ,
5573 targetBase : string ,
56- options : MoveMemoryIndexFilesOptions = { } ,
74+ options : MemoryIndexFileOptions = { } ,
5775) : Promise < void > {
58- const resolvedOptions : Required < MoveMemoryIndexFilesOptions > = {
59- fileOps : options . fileOps ?? defaultFileOps ,
60- maxRenameAttempts : Math . max ( 1 , options . maxRenameAttempts ?? defaultMaxRenameAttempts ) ,
61- renameRetryDelayMs : options . renameRetryDelayMs ?? defaultRenameRetryDelayMs ,
62- } ;
76+ const resolvedOptions = resolveMemoryIndexFileOptions ( options ) ;
6377 const suffixes = [ "" , "-wal" , "-shm" ] ;
6478 for ( const suffix of suffixes ) {
6579 const source = `${ sourceBase } ${ suffix } ` ;
@@ -68,12 +82,33 @@ export async function moveMemoryIndexFiles(
6882 }
6983}
7084
71- async function removeMemoryIndexFiles (
85+ async function rmWithRetry ( path : string , options : ResolvedMemoryIndexFileOptions ) : Promise < void > {
86+ for ( let attempt = 1 ; attempt <= options . maxRemoveAttempts ; attempt ++ ) {
87+ try {
88+ await options . fileOps . rm ( path , { force : true } ) ;
89+ return ;
90+ } catch ( err ) {
91+ if ( ( err as NodeJS . ErrnoException ) . code === "ENOENT" ) {
92+ return ;
93+ }
94+ if ( ! isTransientFileError ( err ) || attempt === options . maxRemoveAttempts ) {
95+ throw err ;
96+ }
97+ await options . fileOps . wait ( options . removeRetryDelayMs * attempt ) ;
98+ }
99+ }
100+ throw new Error ( "rm retry loop exited unexpectedly" ) ;
101+ }
102+
103+ export async function removeMemoryIndexFiles (
72104 basePath : string ,
73- fileOps : MemoryIndexFileOps = defaultFileOps ,
105+ options : MemoryIndexFileOptions = { } ,
74106) : Promise < void > {
107+ const resolvedOptions = resolveMemoryIndexFileOptions ( options ) ;
75108 const suffixes = [ "" , "-wal" , "-shm" ] ;
76- await Promise . all ( suffixes . map ( ( suffix ) => fileOps . rm ( `${ basePath } ${ suffix } ` , { force : true } ) ) ) ;
109+ for ( const suffix of suffixes ) {
110+ await rmWithRetry ( `${ basePath } ${ suffix } ` , resolvedOptions ) ;
111+ }
77112}
78113
79114async function swapMemoryIndexFiles ( targetPath : string , tempPath : string ) : Promise < void > {
@@ -92,13 +127,23 @@ export async function runMemoryAtomicReindex<T>(params: {
92127 targetPath : string ;
93128 tempPath : string ;
94129 build : ( ) => Promise < T > ;
130+ beforeTempCleanup ?: ( ) => Promise < void > | void ;
131+ fileOptions ?: MemoryIndexFileOptions ;
95132} ) : Promise < T > {
96133 try {
97134 const result = await params . build ( ) ;
98135 await swapMemoryIndexFiles ( params . targetPath , params . tempPath ) ;
99136 return result ;
100137 } catch ( err ) {
101- await removeMemoryIndexFiles ( params . tempPath ) ;
138+ try {
139+ await params . beforeTempCleanup ?.( ) ;
140+ await removeMemoryIndexFiles ( params . tempPath , params . fileOptions ) ;
141+ } catch ( cleanupErr ) {
142+ throw new AggregateError (
143+ [ err , cleanupErr ] ,
144+ "memory atomic reindex failed and temp cleanup failed" ,
145+ ) ;
146+ }
102147 throw err ;
103148 }
104149}
0 commit comments