1/**
2 * Coordinates shared access inside transactions with read and write locks.
3 *
4 * A `TxReentrantLock` lets many fibers hold read locks at the same time, or one
5 * fiber hold a write lock for exclusive access. Lock ownership is tracked by
6 * fiber, so a fiber that already holds the lock can acquire it again and later
7 * release each acquisition. Attempts that cannot proceed retry transactionally
8 * until the lock becomes available. This module includes manual, scoped, and
9 * wrapper-style operations for read and write locking.
10 *
11 * @since 4.0.0
12 */
13import * as Effect from "./Effect.ts"
14import * as HashMap from "./HashMap.ts"
15import type { Inspectable } from "./Inspectable.ts"
16import { NodeInspectSymbol, toJson } from "./Inspectable.ts"
17import * as Option from "./Option.ts"
18import type { Pipeable } from "./Pipeable.ts"
19import { pipeArguments } from "./Pipeable.ts"
20import { hasProperty } from "./Predicate.ts"
21import type * as Scope from "./Scope.ts"
22import * as TxRef from "./TxRef.ts"
23
24const TypeId = "~effect/TxReentrantLock"
25
26/**
27 * @category models
28 * @since 4.0.0
29 */
30interface LockState {
31 readonly readers: HashMap.HashMap<number, number>
32 readonly writer: Option.Option<readonly [fiberId: number, count: number]>
33}
34
35const emptyState: LockState = {
36 readers: HashMap.empty<number, number>(),
37 writer: Option.none()
38}
39
40/**
41 * A TxReentrantLock provides a transactional read/write lock with reentrant semantics.
42 * Multiple readers can hold the lock concurrently, or a single writer can hold exclusive
43 * access. A fiber holding the write lock may acquire additional read/write locks (reentrancy).
44 *
45 * **Example** (Using read and write locks)
46 *
47 * ```ts import.meta.vitest
48 * import { Effect, TxReentrantLock } from "effect"
49 *
50 * const program = Effect.gen(function*() {
51 * const lock = yield* TxReentrantLock.make()
52 *
53 * // Multiple readers can proceed concurrently
54 * const read = yield* TxReentrantLock.withReadLock(lock, Effect.succeed("reading"))
55 *
56 * // Writer gets exclusive access
57 * const write = yield* TxReentrantLock.withWriteLock(lock, Effect.succeed("writing"))
58 * return [read, write]
59 * })
60 *
61 * await Effect.runPromise(program) // => ["reading", "writing"]
62 * ```
63 *
64 * @category models
65 * @since 4.0.0
66 */
67export interface TxReentrantLock extends Inspectable, Pipeable {
68 readonly [TypeId]: typeof TypeId
69 /** @internal */
70 readonly stateRef: TxRef.TxRef<LockState>
71}
72
73const TxReentrantLockProto: Omit<TxReentrantLock, typeof TypeId | "stateRef"> = {
74 [NodeInspectSymbol](this: TxReentrantLock) {
75 return toJson(this)
76 },
77 toJSON(this: TxReentrantLock) {
78 return { _id: "TxReentrantLock" }
79 },
80 toString() {
81 return "TxReentrantLock"
82 },
83 pipe() {
84 return pipeArguments(this, arguments)
85 }
86}
87
88// =============================================================================
89// Constructors
90// =============================================================================
91
92/**
93 * Creates a new TxReentrantLock.
94 *
95 * **Example** (Creating a reentrant lock)
96 *
97 * ```ts import.meta.vitest
98 * import { Effect, TxReentrantLock } from "effect"
99 *
100 * const program = Effect.gen(function*() {
101 * const lock = yield* TxReentrantLock.make()
102 * return yield* TxReentrantLock.locked(lock)
103 * })
104 *
105 * await Effect.runPromise(program) // => false
106 * ```
107 *
108 * @category constructors
109 * @since 2.0.0
110 */
111export const make = (): Effect.Effect<TxReentrantLock> =>
112 Effect.gen(function*() {
113 const stateRef = yield* TxRef.make<LockState>(emptyState)
114 const self = Object.create(TxReentrantLockProto)
115 self[TypeId] = TypeId
116 self.stateRef = stateRef
117 return self
118 }).pipe(Effect.tx)
119
120// =============================================================================
121// Mutations
122// =============================================================================
123
124/**
125 * Acquires a read lock. Blocks if another fiber holds the write lock.
126 * If the current fiber already holds the write lock, the read lock is granted (reentrancy).
127 * Returns the current number of read locks held by this fiber.
128 *
129 * **Example** (Acquiring a read lock)
130 *
131 * ```ts import.meta.vitest
132 * import { Effect, TxReentrantLock } from "effect"
133 *
134 * const program = Effect.gen(function*() {
135 * const lock = yield* TxReentrantLock.make()
136 * const count = yield* TxReentrantLock.acquireRead(lock)
137 * yield* TxReentrantLock.releaseRead(lock)
138 * return count
139 * })
140 *
141 * await Effect.runPromise(program) // => 1
142 * ```
143 *
144 * @category mutations
145 * @since 2.0.0
146 */
147export const acquireRead = (self: TxReentrantLock): Effect.Effect<number> =>
148 Effect.withFiber((fiber) =>
149 Effect.gen(function*() {
150 const state = yield* TxRef.get(self.stateRef)
151 const fiberId = fiber.id
152
153 // If another fiber holds the write lock, retry
154 if (Option.isSome(state.writer) && state.writer.value[0] !== fiberId) {
155 return yield* Effect.txRetry
156 }
157
158 // Grant read lock
159 const currentCount = Option.getOrElse(HashMap.get(state.readers, fiberId), () => 0)
160 const newCount = currentCount + 1
161 yield* TxRef.set(self.stateRef, {
162 ...state,
163 readers: HashMap.set(state.readers, fiberId, newCount)
164 })
165 return newCount
166 }).pipe(Effect.tx)
167 )
168
169/**
170 * Acquires the write lock for the current fiber.
171 *
172 * **When to use**
173 *
174 * Use to enter an exclusive section manually when `withWriteLock` is not the
175 * right shape.
176 *
177 * **Details**
178 *
179 * Blocks if any other fiber holds a read or write lock. If the current fiber
180 * already holds the write lock, the count is incremented. If the current fiber
181 * holds a read lock, the write lock is granted as an upgrade.
182 *
183 * Returns the current number of write locks held by this fiber.
184 *
185 * **Example** (Acquiring a write lock)
186 *
187 * ```ts import.meta.vitest
188 * import { Effect, TxReentrantLock } from "effect"
189 *
190 * const program = Effect.gen(function*() {
191 * const lock = yield* TxReentrantLock.make()
192 * const count = yield* TxReentrantLock.acquireWrite(lock)
193 * yield* TxReentrantLock.releaseWrite(lock)
194 * return count
195 * })
196 *
197 * await Effect.runPromise(program) // => 1
198 * ```
199 *
200 * @category mutations
201 * @since 2.0.0
202 */
203export const acquireWrite = (self: TxReentrantLock): Effect.Effect<number> =>
204 Effect.withFiber((fiber) =>
205 Effect.gen(function*() {
206 const state = yield* TxRef.get(self.stateRef)
207 const fiberId = fiber.id
208
209 // If another fiber holds the write lock, retry
210 if (Option.isSome(state.writer) && state.writer.value[0] !== fiberId) {
211 return yield* Effect.txRetry
212 }
213
214 // If other fibers hold read locks, retry
215 for (const [readerId] of state.readers) {
216 if (readerId !== fiberId && Option.getOrElse(HashMap.get(state.readers, readerId), () => 0) > 0) {
217 return yield* Effect.txRetry
218 }
219 }
220
221 // Grant write lock
222 if (Option.isSome(state.writer)) {
223 // Reentrant: increment write count
224 const newCount = state.writer.value[1] + 1
225 yield* TxRef.set(self.stateRef, {
226 ...state,
227 writer: Option.some([fiberId, newCount] as const)
228 })
229 return newCount
230 }
231
232 // First write lock acquisition
233 yield* TxRef.set(self.stateRef, {
234 ...state,
235 writer: Option.some([fiberId, 1] as const)
236 })
237 return 1
238 }).pipe(Effect.tx)
239 )
240
241/**
242 * Releases one read lock held by the current fiber.
243 *
244 * **When to use**
245 *
246 * Use to leave a manually acquired read lock.
247 *
248 * **Details**
249 *
250 * Returns the remaining number of read locks held by this fiber.
251 *
252 * **Example** (Releasing a read lock)
253 *
254 * ```ts import.meta.vitest
255 * import { Effect, TxReentrantLock } from "effect"
256 *
257 * const program = Effect.gen(function*() {
258 * const lock = yield* TxReentrantLock.make()
259 * yield* TxReentrantLock.acquireRead(lock)
260 * return yield* TxReentrantLock.releaseRead(lock)
261 * })
262 *
263 * await Effect.runPromise(program) // => 0
264 * ```
265 *
266 * @category mutations
267 * @since 2.0.0
268 */
269const releaseReadFor = (self: TxReentrantLock, fiberId: number): Effect.Effect<number> =>
270 Effect.gen(function*() {
271 const state = yield* TxRef.get(self.stateRef)
272 const currentCount = Option.getOrElse(HashMap.get(state.readers, fiberId), () => 0)
273
274 if (currentCount <= 0) return 0
275
276 const newCount = currentCount - 1
277 const newReaders = newCount === 0
278 ? HashMap.remove(state.readers, fiberId)
279 : HashMap.set(state.readers, fiberId, newCount)
280
281 yield* TxRef.set(self.stateRef, { ...state, readers: newReaders })
282 return newCount
283 }).pipe(Effect.tx)
284
285/**
286 * Releases one read lock held by the current fiber.
287 *
288 * **When to use**
289 *
290 * Use to leave a manually acquired read lock.
291 *
292 * **Details**
293 *
294 * Returns the remaining number of read locks held by this fiber.
295 *
296 * @category mutations
297 * @since 2.0.0
298 */
299export const releaseRead = (self: TxReentrantLock): Effect.Effect<number> =>
300 Effect.withFiber((fiber) => releaseReadFor(self, fiber.id))
301
302/**
303 * Releases one write lock held by the current fiber.
304 *
305 * **When to use**
306 *
307 * Use to leave a manually acquired write lock.
308 *
309 * **Details**
310 *
311 * Returns the remaining number of write locks held by this fiber.
312 *
313 * **Example** (Releasing a write lock)
314 *
315 * ```ts import.meta.vitest
316 * import { Effect, TxReentrantLock } from "effect"
317 *
318 * const program = Effect.gen(function*() {
319 * const lock = yield* TxReentrantLock.make()
320 * yield* TxReentrantLock.acquireWrite(lock)
321 * return yield* TxReentrantLock.releaseWrite(lock)
322 * })
323 *
324 * await Effect.runPromise(program) // => 0
325 * ```
326 *
327 * @category mutations
328 * @since 2.0.0
329 */
330const releaseWriteFor = (self: TxReentrantLock, fiberId: number): Effect.Effect<number> =>
331 Effect.gen(function*() {
332 const state = yield* TxRef.get(self.stateRef)
333
334 if (Option.isNone(state.writer) || state.writer.value[0] !== fiberId) return 0
335
336 const newCount = state.writer.value[1] - 1
337 const newWriter = newCount <= 0
338 ? Option.none<readonly [number, number]>()
339 : Option.some([fiberId, newCount] as const)
340
341 yield* TxRef.set(self.stateRef, { ...state, writer: newWriter })
342 return newCount
343 }).pipe(Effect.tx)
344
345/**
346 * Releases one write lock held by the current fiber.
347 *
348 * **When to use**
349 *
350 * Use to leave a manually acquired write lock.
351 *
352 * **Details**
353 *
354 * Returns the remaining number of write locks held by this fiber.
355 *
356 * @category mutations
357 * @since 2.0.0
358 */
359export const releaseWrite = (self: TxReentrantLock): Effect.Effect<number> =>
360 Effect.withFiber((fiber) => releaseWriteFor(self, fiber.id))
361
362/**
363 * Acquires a read lock for the duration of the scope.
364 * The lock is automatically released when the scope closes.
365 *
366 * **Example** (Holding a scoped read lock)
367 *
368 * ```ts import.meta.vitest
369 * import { Effect, TxReentrantLock } from "effect"
370 *
371 * const program = Effect.gen(function*() {
372 * const lock = yield* TxReentrantLock.make()
373 *
374 * const held = yield* Effect.scoped(
375 * Effect.gen(function*() {
376 * yield* TxReentrantLock.readLock(lock)
377 * // read lock is held for the duration of the scope
378 * return yield* TxReentrantLock.readLocks(lock)
379 * })
380 * )
381 * // read lock is released
382 * return [held, yield* TxReentrantLock.readLocks(lock)]
383 * })
384 *
385 * await Effect.runPromise(program) // => [1, 0]
386 * ```
387 *
388 * @category mutations
389 * @since 2.0.0
390 */
391export const readLock = (self: TxReentrantLock): Effect.Effect<number, never, Scope.Scope> =>
392 Effect.withFiber((fiber) =>
393 Effect.acquireRelease(
394 acquireRead(self),
395 () => releaseReadFor(self, fiber.id)
396 )
397 )
398
399/**
400 * Acquires a write lock for the duration of the scope.
401 * The lock is automatically released when the scope closes.
402 *
403 * **Example** (Holding a scoped write lock)
404 *
405 * ```ts import.meta.vitest
406 * import { Effect, TxReentrantLock } from "effect"
407 *
408 * const program = Effect.gen(function*() {
409 * const lock = yield* TxReentrantLock.make()
410 *
411 * const held = yield* Effect.scoped(
412 * Effect.gen(function*() {
413 * yield* TxReentrantLock.writeLock(lock)
414 * // write lock is held for the duration of the scope
415 * return yield* TxReentrantLock.writeLocks(lock)
416 * })
417 * )
418 * // write lock is released
419 * return [held, yield* TxReentrantLock.writeLocks(lock)]
420 * })
421 *
422 * await Effect.runPromise(program) // => [1, 0]
423 * ```
424 *
425 * @category mutations
426 * @since 2.0.0
427 */
428export const writeLock = (self: TxReentrantLock): Effect.Effect<number, never, Scope.Scope> =>
429 Effect.withFiber((fiber) =>
430 Effect.acquireRelease(
431 acquireWrite(self),
432 () => releaseWriteFor(self, fiber.id)
433 )
434 )
435
436/**
437 * Runs the provided effect while holding a read lock. The lock is automatically
438 * released after the effect completes, fails, or is interrupted.
439 *
440 * **Example** (Running an effect with a read lock)
441 *
442 * ```ts import.meta.vitest
443 * import { Effect, TxReentrantLock } from "effect"
444 *
445 * const program = Effect.gen(function*() {
446 * const lock = yield* TxReentrantLock.make()
447 * return yield* TxReentrantLock.withReadLock(
448 * lock,
449 * Effect.succeed("read data")
450 * )
451 * })
452 *
453 * await Effect.runPromise(program) // => "read data"
454 * ```
455 *
456 * @category mutations
457 * @since 2.0.0
458 */
459export const withReadLock: {
460 <A, E, R>(effect: Effect.Effect<A, E, R>): (self: TxReentrantLock) => Effect.Effect<A, E, R>
461 <A, E, R>(self: TxReentrantLock, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
462} = ((...args: Array<any>) => {
463 if (args.length === 1) {
464 const [effect] = args
465 return (self: TxReentrantLock) =>
466 Effect.acquireUseRelease(
467 acquireRead(self),
468 () => effect,
469 () => releaseRead(self)
470 )
471 }
472 const [self, effect] = args
473 return Effect.acquireUseRelease(
474 acquireRead(self),
475 () => effect,
476 () => releaseRead(self)
477 )
478}) as any
479
480/**
481 * Runs the provided effect while holding a write lock. The lock is automatically
482 * released after the effect completes, fails, or is interrupted.
483 *
484 * **Example** (Running an effect with a write lock)
485 *
486 * ```ts import.meta.vitest
487 * import { Effect, TxReentrantLock } from "effect"
488 *
489 * const program = Effect.gen(function*() {
490 * const lock = yield* TxReentrantLock.make()
491 * return yield* TxReentrantLock.withWriteLock(
492 * lock,
493 * Effect.succeed("wrote data")
494 * )
495 * })
496 *
497 * await Effect.runPromise(program) // => "wrote data"
498 * ```
499 *
500 * @category mutations
501 * @since 2.0.0
502 */
503export const withWriteLock: {
504 <A, E, R>(effect: Effect.Effect<A, E, R>): (self: TxReentrantLock) => Effect.Effect<A, E, R>
505 <A, E, R>(self: TxReentrantLock, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
506} = ((...args: Array<any>) => {
507 if (args.length === 1) {
508 const [effect] = args
509 return (self: TxReentrantLock) =>
510 Effect.acquireUseRelease(
511 acquireWrite(self),
512 () => effect,
513 () => releaseWrite(self)
514 )
515 }
516 const [self, effect] = args
517 return Effect.acquireUseRelease(
518 acquireWrite(self),
519 () => effect,
520 () => releaseWrite(self)
521 )
522}) as any
523
524/**
525 * Runs an effect while holding a write lock.
526 *
527 * **When to use**
528 *
529 * Use when you need to run an effect with exclusive write access through a
530 * `TxReentrantLock` and prefer the concise lock helper.
531 *
532 * **Example** (Running an effect with exclusive access)
533 *
534 * ```ts import.meta.vitest
535 * import { Effect, TxReentrantLock } from "effect"
536 *
537 * const program = Effect.gen(function*() {
538 * const lock = yield* TxReentrantLock.make()
539 * return yield* TxReentrantLock.withLock(
540 * lock,
541 * Effect.succeed("exclusive operation")
542 * )
543 * })
544 *
545 * await Effect.runPromise(program) // => "exclusive operation"
546 * ```
547 *
548 * @category mutations
549 * @since 2.0.0
550 */
551export const withLock: {
552 <A, E, R>(effect: Effect.Effect<A, E, R>): (self: TxReentrantLock) => Effect.Effect<A, E, R>
553 <A, E, R>(self: TxReentrantLock, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
554} = withWriteLock
555
556// =============================================================================
557// Getters
558// =============================================================================
559
560/**
561 * Returns the total number of read locks held across all fibers.
562 *
563 * **Example** (Counting read locks)
564 *
565 * ```ts import.meta.vitest
566 * import { Effect, TxReentrantLock } from "effect"
567 *
568 * const program = Effect.gen(function*() {
569 * const lock = yield* TxReentrantLock.make()
570 * yield* TxReentrantLock.acquireRead(lock)
571 * const count = yield* TxReentrantLock.readLocks(lock)
572 * yield* TxReentrantLock.releaseRead(lock)
573 * return count
574 * })
575 *
576 * await Effect.runPromise(program) // => 1
577 * ```
578 *
579 * @category getters
580 * @since 2.0.0
581 */
582export const readLocks = (self: TxReentrantLock): Effect.Effect<number> =>
583 Effect.gen(function*() {
584 const state = yield* TxRef.get(self.stateRef)
585 let total = 0
586 for (const [, count] of state.readers) {
587 total += count
588 }
589 return total
590 })
591
592/**
593 * Returns the number of write locks held (0 or the reentrant count).
594 *
595 * **Example** (Counting write locks)
596 *
597 * ```ts import.meta.vitest
598 * import { Effect, TxReentrantLock } from "effect"
599 *
600 * const program = Effect.gen(function*() {
601 * const lock = yield* TxReentrantLock.make()
602 * return yield* TxReentrantLock.writeLocks(lock)
603 * })
604 *
605 * await Effect.runPromise(program) // => 0
606 * ```
607 *
608 * @category getters
609 * @since 2.0.0
610 */
611export const writeLocks = (self: TxReentrantLock): Effect.Effect<number> =>
612 Effect.gen(function*() {
613 const state = yield* TxRef.get(self.stateRef)
614 return Option.isSome(state.writer) ? state.writer.value[1] : 0
615 })
616
617/**
618 * Checks whether the lock is held by any fiber (read or write).
619 *
620 * **Example** (Checking whether a lock is held)
621 *
622 * ```ts import.meta.vitest
623 * import { Effect, TxReentrantLock } from "effect"
624 *
625 * const program = Effect.gen(function*() {
626 * const lock = yield* TxReentrantLock.make()
627 * return yield* TxReentrantLock.locked(lock)
628 * })
629 *
630 * await Effect.runPromise(program) // => false
631 * ```
632 *
633 * @category getters
634 * @since 2.0.0
635 */
636export const locked = (self: TxReentrantLock): Effect.Effect<boolean> =>
637 Effect.gen(function*() {
638 const state = yield* TxRef.get(self.stateRef)
639 return HashMap.size(state.readers) > 0 || Option.isSome(state.writer)
640 })
641
642/**
643 * Checks whether any fiber holds a read lock.
644 *
645 * **Example** (Checking whether a read lock is held)
646 *
647 * ```ts import.meta.vitest
648 * import { Effect, TxReentrantLock } from "effect"
649 *
650 * const program = Effect.gen(function*() {
651 * const lock = yield* TxReentrantLock.make()
652 * return yield* TxReentrantLock.readLocked(lock)
653 * })
654 *
655 * await Effect.runPromise(program) // => false
656 * ```
657 *
658 * @category getters
659 * @since 2.0.0
660 */
661export const readLocked = (self: TxReentrantLock): Effect.Effect<boolean> =>
662 Effect.gen(function*() {
663 const state = yield* TxRef.get(self.stateRef)
664 return HashMap.size(state.readers) > 0
665 })
666
667/**
668 * Checks whether any fiber holds a write lock.
669 *
670 * **Example** (Checking whether a write lock is held)
671 *
672 * ```ts import.meta.vitest
673 * import { Effect, TxReentrantLock } from "effect"
674 *
675 * const program = Effect.gen(function*() {
676 * const lock = yield* TxReentrantLock.make()
677 * return yield* TxReentrantLock.writeLocked(lock)
678 * })
679 *
680 * await Effect.runPromise(program) // => false
681 * ```
682 *
683 * @category getters
684 * @since 2.0.0
685 */
686export const writeLocked = (self: TxReentrantLock): Effect.Effect<boolean> =>
687 Effect.gen(function*() {
688 const state = yield* TxRef.get(self.stateRef)
689 return Option.isSome(state.writer)
690 })
691
692// =============================================================================
693// Guards
694// =============================================================================
695
696/**
697 * Checks whether the given value is a TxReentrantLock.
698 *
699 * **Example** (Checking for TxReentrantLock values)
700 *
701 * ```ts import.meta.vitest
702 * import { TxReentrantLock } from "effect"
703 *
704 * const someValue: unknown = {}
705 *
706 * TxReentrantLock.isTxReentrantLock(someValue) // => false
707 * ```
708 *
709 * @category guards
710 * @since 4.0.0
711 */
712export const isTxReentrantLock = (u: unknown): u is TxReentrantLock => hasProperty(u, TypeId)