@@ -70,6 +70,56 @@ export async function acquireFileLock(filePath: string, options: AcquireFileLock
7070 }
7171
7272 let state : LockState = 'held' ;
73+ let releaseInFlight : Promise < void > | undefined ;
74+ const performRelease = async ( ) : Promise < void > => {
75+ if ( state === 'released' || state === 'retained' ) {
76+ return ;
77+ }
78+ const releaseMarkerPath = path . join ( lockPath , releaseMarkerName ) ;
79+ if ( state === 'held' ) {
80+ try {
81+ await fsapi . rename ( ownerMarker , releaseMarkerPath ) ;
82+ } catch ( error ) {
83+ if ( hasErrorCode ( error , 'ENOENT' ) ) {
84+ throw createLockError ( 'Lock ownership was compromised' , 'ECOMPROMISED' , lockPath ) ;
85+ }
86+ throw error ;
87+ }
88+ state = 'releasing' ;
89+ }
90+ // state === 'releasing': the release marker exists; retire the canonical
91+ // directory. Resumable: if retirement fails and ownership cannot be restored,
92+ // the handle stays 'releasing' so a later release() retries retirement instead
93+ // of leaving the handle unable to make progress.
94+ const retiredPath = getRetiredLockPath ( lockPath ) ;
95+ try {
96+ await retireCanonicalLockDirectory ( lockPath , retiredPath ) ;
97+ } catch ( error ) {
98+ const restored = await fsapi
99+ . rename ( releaseMarkerPath , ownerMarker )
100+ . then (
101+ ( ) => true ,
102+ ( ) => false ,
103+ ) ;
104+ if ( restored ) {
105+ state = 'held' ;
106+ throw createLockError (
107+ 'Failed to retire the lock directory; ownership was restored' ,
108+ 'ELOCKRELEASEFAILED' ,
109+ lockPath ,
110+ error ,
111+ ) ;
112+ }
113+ throw createLockError (
114+ 'Failed to retire the lock directory; release can be retried' ,
115+ 'ELOCKRELEASEFAILED' ,
116+ lockPath ,
117+ error ,
118+ ) ;
119+ }
120+ state = 'released' ;
121+ await cleanupRetiredLock ( retiredPath , releaseMarkerName ) ;
122+ } ;
73123 return {
74124 retain : async ( ) => {
75125 if ( state !== 'held' ) {
@@ -82,54 +132,18 @@ export async function acquireFileLock(filePath: string, options: AcquireFileLock
82132 throw createLockError ( 'Failed to mark the lock as retained' , 'ERETAINFAILED' , lockPath ) ;
83133 }
84134 } ,
85- release : async ( ) => {
86- if ( state === 'released' || state === 'retained' ) {
87- return ;
88- }
89- const releaseMarkerPath = path . join ( lockPath , releaseMarkerName ) ;
90- if ( state === 'held' ) {
91- try {
92- await fsapi . rename ( ownerMarker , releaseMarkerPath ) ;
93- } catch ( error ) {
94- if ( hasErrorCode ( error , 'ENOENT' ) ) {
95- throw createLockError ( 'Lock ownership was compromised' , 'ECOMPROMISED' , lockPath ) ;
96- }
97- throw error ;
98- }
99- state = 'releasing' ;
100- }
101- // state === 'releasing': the release marker exists; retire the canonical
102- // directory. Resumable: if retirement fails and ownership cannot be restored,
103- // the handle stays 'releasing' so a later release() retries retirement instead
104- // of leaving the handle unable to make progress.
105- const retiredPath = getRetiredLockPath ( lockPath ) ;
106- try {
107- await retireCanonicalLockDirectory ( lockPath , retiredPath ) ;
108- } catch ( error ) {
109- const restored = await fsapi
110- . rename ( releaseMarkerPath , ownerMarker )
111- . then (
112- ( ) => true ,
113- ( ) => false ,
114- ) ;
115- if ( restored ) {
116- state = 'held' ;
117- throw createLockError (
118- 'Failed to retire the lock directory; ownership was restored' ,
119- 'ELOCKRELEASEFAILED' ,
120- lockPath ,
121- error ,
122- ) ;
123- }
124- throw createLockError (
125- 'Failed to retire the lock directory; release can be retried' ,
126- 'ELOCKRELEASEFAILED' ,
127- lockPath ,
128- error ,
129- ) ;
135+ release : ( ) => {
136+ // Serialize concurrent release() calls on the same handle: without this,
137+ // two callers can both observe state === 'held' before either owner-marker
138+ // rename completes, and the loser sees ENOENT and reports ECOMPROMISED even
139+ // though the lock was validly released. Sharing one in-flight promise de-dupes
140+ // concurrent calls; clearing it on settle preserves retry-after-failure.
141+ if ( ! releaseInFlight ) {
142+ releaseInFlight = performRelease ( ) . finally ( ( ) => {
143+ releaseInFlight = undefined ;
144+ } ) ;
130145 }
131- state = 'released' ;
132- await cleanupRetiredLock ( retiredPath , releaseMarkerName ) ;
146+ return releaseInFlight ;
133147 } ,
134148 } ;
135149 } catch ( error ) {
0 commit comments