@@ -66,6 +66,7 @@ export namespace EffectFlock {
6666 )
6767
6868 const decodeMeta = Schema . decodeUnknownSync ( LockMetaJson )
69+ const decodeMetaOption = Schema . decodeUnknownOption ( LockMetaJson )
6970 const encodeMeta = Schema . encodeSync ( LockMetaJson )
7071
7172 // ---------------------------------------------------------------------------
@@ -74,6 +75,7 @@ export namespace EffectFlock {
7475
7576 export interface Interface {
7677 readonly acquire : ( key : string , dir ?: string ) => Effect . Effect < void , LockError , Scope . Scope >
78+ readonly tryAcquire : ( key : string , dir ?: string ) => Effect . Effect < boolean , LockError , Scope . Scope >
7779 readonly withLock : {
7880 ( key : string , dir ?: string ) : < A , E , R > ( body : Effect . Effect < A , E , R > ) => Effect . Effect < A , E | LockError , R >
7981 < A , E , R > ( body : Effect . Effect < A , E , R > , key : string , dir ?: string ) : Effect . Effect < A , E | LockError , R >
@@ -150,6 +152,26 @@ export namespace EffectFlock {
150152 const isStale = Effect . fnUntraced ( function * ( lockDir : string , heartbeatPath : string , metaPath : string ) {
151153 const now = wall ( )
152154
155+ const raw = yield * fs . readFileString ( metaPath ) . pipe (
156+ Effect . map ( Option . some ) ,
157+ Effect . catchIf ( isPathGone , ( ) => Effect . succeed ( Option . none ( ) ) ) ,
158+ Effect . orDie ,
159+ )
160+ const owner = Option . isSome ( raw ) ? Option . getOrUndefined ( decodeMetaOption ( raw . value ) ) : undefined
161+ if ( owner ?. hostname === hostname ) {
162+ const alive = yield * Effect . try ( {
163+ try : ( ) => {
164+ process . kill ( owner . pid , 0 )
165+ return true
166+ } ,
167+ catch : ( cause ) => {
168+ const code = cause && typeof cause === "object" && "code" in cause ? cause . code : undefined
169+ return code !== "ESRCH"
170+ } ,
171+ } ) . pipe ( Effect . orElseSucceed ( ( ) => false ) )
172+ if ( ! alive ) return true
173+ }
174+
153175 const hb = yield * safeStat ( heartbeatPath )
154176 if ( hb ) return now - mtimeMs ( hb ) > STALE_MS
155177
@@ -243,26 +265,44 @@ export namespace EffectFlock {
243265 catch : ( cause ) => new ReleaseError ( { detail : "metadata invalid" , cause } ) ,
244266 } ) . pipe ( Effect . orDie )
245267
246- if ( parsed . token !== handle . token ) return yield * Effect . die ( new ReleaseError ( { detail : "token mismatch" } ) )
268+ if ( parsed . token !== handle . token ) yield * Effect . die ( new ReleaseError ( { detail : "token mismatch" } ) )
247269
248270 yield * forceRemove ( handle . lockDir )
249271 } )
250272
251273 // -- build service --
252274
275+ const heartbeat = Effect . fnUntraced ( function * ( handle : Handle ) {
276+ yield * fs
277+ . utimes ( handle . heartbeatPath , new Date ( ) , new Date ( ) )
278+ . pipe ( Effect . ignore , Effect . repeat ( Schedule . spaced ( HEARTBEAT_MS ) ) , Effect . forkScoped )
279+ } )
280+
253281 const acquire = Effect . fn ( "EffectFlock.acquire" ) ( function * ( key : string , dir ?: string ) {
254282 const lockDir = dir ?? lockRoot
255283 yield * ensureDir ( lockDir )
284+ const handle = yield * Effect . acquireRelease (
285+ acquireHandle ( path . join ( lockDir , Hash . fast ( key ) + ".lock" ) , key ) ,
286+ ( handle ) => release ( handle ) ,
287+ )
288+ yield * heartbeat ( handle )
289+ } )
256290
257- const lockfile = path . join ( lockDir , Hash . fast ( key ) + ".lock" )
258-
259- // acquireRelease: acquire is uninterruptible, release is guaranteed
260- const handle = yield * Effect . acquireRelease ( acquireHandle ( lockfile , key ) , ( handle ) => release ( handle ) )
261-
262- // Heartbeat fiber — scoped, so it's interrupted before release runs
263- yield * fs
264- . utimes ( handle . heartbeatPath , new Date ( ) , new Date ( ) )
265- . pipe ( Effect . ignore , Effect . repeat ( Schedule . spaced ( HEARTBEAT_MS ) ) , Effect . forkScoped )
291+ const tryAcquire = Effect . fn ( "EffectFlock.tryAcquire" ) ( function * ( key : string , dir ?: string ) {
292+ return yield * Effect . uninterruptibleMask ( ( ) =>
293+ Effect . gen ( function * ( ) {
294+ const lockDir = dir ?? lockRoot
295+ yield * ensureDir ( lockDir )
296+ const handle = yield * tryAcquireLockDir ( path . join ( lockDir , Hash . fast ( key ) + ".lock" ) , key ) . pipe (
297+ Effect . map ( Option . some ) ,
298+ Effect . catchTag ( "NotAcquired" , ( ) => Effect . succeed ( Option . none ( ) ) ) ,
299+ )
300+ if ( Option . isNone ( handle ) ) return false
301+ yield * Effect . addFinalizer ( ( ) => release ( handle . value ) )
302+ yield * heartbeat ( handle . value )
303+ return true
304+ } ) ,
305+ )
266306 } )
267307
268308 const withLock : Interface [ "withLock" ] = Function . dual (
@@ -276,7 +316,7 @@ export namespace EffectFlock {
276316 ) ,
277317 )
278318
279- return Service . of ( { acquire, withLock } )
319+ return Service . of ( { acquire, tryAcquire , withLock } )
280320 } ) ,
281321 )
282322
0 commit comments