1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Exit, Fiber, TxSemaphore } from "effect"
3
4describe("TxSemaphore", () => {
5 describe("constructors", () => {
6 it.effect("make creates semaphore with specified permits", () =>
7 Effect.tx(Effect.gen(function*() {
8 const semaphore = yield* TxSemaphore.make(5)
9 const available = yield* TxSemaphore.available(semaphore)
10 const capacity = yield* TxSemaphore.capacity(semaphore)
11
12 assert.strictEqual(available, 5)
13 assert.strictEqual(capacity, 5)
14 })))
15
16 it.effect("make with zero permits creates empty semaphore", () =>
17 Effect.tx(Effect.gen(function*() {
18 const semaphore = yield* TxSemaphore.make(0)
19 const available = yield* TxSemaphore.available(semaphore)
20 const capacity = yield* TxSemaphore.capacity(semaphore)
21
22 assert.strictEqual(available, 0)
23 assert.strictEqual(capacity, 0)
24 })))
25
26 it.effect("make with negative permits causes defect", () =>
27 Effect.tx(Effect.gen(function*() {
28 const result = yield* Effect.exit(TxSemaphore.make(-1))
29 assert.isTrue(Exit.hasDies(result))
30 })))
31 })
32
33 describe("basic operations", () => {
34 it.effect("acquire and release work correctly", () =>
35 Effect.tx(Effect.gen(function*() {
36 const semaphore = yield* TxSemaphore.make(3)
37
38 yield* TxSemaphore.acquire(semaphore)
39 const available = yield* TxSemaphore.available(semaphore)
40 assert.strictEqual(available, 2)
41
42 yield* TxSemaphore.release(semaphore)
43 const availableAfter = yield* TxSemaphore.available(semaphore)
44 assert.strictEqual(availableAfter, 3)
45 })))
46
47 it.effect("acquireN and releaseN work correctly", () =>
48 Effect.tx(Effect.gen(function*() {
49 const semaphore = yield* TxSemaphore.make(5)
50
51 yield* TxSemaphore.acquireN(semaphore, 3)
52 const available = yield* TxSemaphore.available(semaphore)
53 assert.strictEqual(available, 2)
54
55 yield* TxSemaphore.releaseN(semaphore, 2)
56 const availableAfter = yield* TxSemaphore.available(semaphore)
57 assert.strictEqual(availableAfter, 4)
58 })))
59
60 it.effect("tryAcquire succeeds when permits available", () =>
61 Effect.tx(Effect.gen(function*() {
62 const semaphore = yield* TxSemaphore.make(2)
63
64 const first = yield* TxSemaphore.tryAcquire(semaphore)
65 assert.strictEqual(first, true)
66
67 const available = yield* TxSemaphore.available(semaphore)
68 assert.strictEqual(available, 1)
69 })))
70
71 it.effect("tryAcquire fails when no permits available", () =>
72 Effect.tx(Effect.gen(function*() {
73 const semaphore = yield* TxSemaphore.make(1)
74
75 // Acquire the only permit
76 yield* TxSemaphore.acquire(semaphore)
77
78 // Try to acquire when none available
79 const result = yield* TxSemaphore.tryAcquire(semaphore)
80 assert.strictEqual(result, false)
81
82 const available = yield* TxSemaphore.available(semaphore)
83 assert.strictEqual(available, 0)
84 })))
85
86 it.effect("tryAcquireN succeeds when enough permits available", () =>
87 Effect.tx(Effect.gen(function*() {
88 const semaphore = yield* TxSemaphore.make(5)
89
90 const result = yield* TxSemaphore.tryAcquireN(semaphore, 3)
91 assert.strictEqual(result, true)
92
93 const available = yield* TxSemaphore.available(semaphore)
94 assert.strictEqual(available, 2)
95 })))
96
97 it.effect("tryAcquireN fails when not enough permits available", () =>
98 Effect.tx(Effect.gen(function*() {
99 const semaphore = yield* TxSemaphore.make(2)
100
101 const result = yield* TxSemaphore.tryAcquireN(semaphore, 3)
102 assert.strictEqual(result, false)
103
104 const available = yield* TxSemaphore.available(semaphore)
105 assert.strictEqual(available, 2)
106 })))
107 })
108
109 describe("scoped operations", () => {
110 it.effect("withPermit automatically manages permit lifecycle", () =>
111 Effect.gen(function*() {
112 const semaphore = yield* Effect.tx(TxSemaphore.make(2))
113
114 const result = yield* TxSemaphore.withPermit(
115 semaphore,
116 Effect.gen(function*() {
117 const available = yield* Effect.tx(TxSemaphore.available(semaphore))
118 assert.strictEqual(available, 1) // One permit acquired
119 return "success"
120 })
121 )
122
123 assert.strictEqual(result, "success")
124
125 // Permit should be released
126 const finalAvailable = yield* Effect.tx(TxSemaphore.available(semaphore))
127 assert.strictEqual(finalAvailable, 2)
128 }))
129
130 it.effect("withPermits automatically manages multiple permits", () =>
131 Effect.gen(function*() {
132 const semaphore = yield* Effect.tx(TxSemaphore.make(5))
133
134 const result = yield* TxSemaphore.withPermits(
135 semaphore,
136 3,
137 Effect.gen(function*() {
138 const available = yield* Effect.tx(TxSemaphore.available(semaphore))
139 assert.strictEqual(available, 2) // Three permits acquired
140 return ["result1", "result2", "result3"]
141 })
142 )
143
144 assert.deepStrictEqual(result, ["result1", "result2", "result3"])
145
146 // All permits should be released
147 const finalAvailable = yield* Effect.tx(TxSemaphore.available(semaphore))
148 assert.strictEqual(finalAvailable, 5)
149 }))
150
151 it.effect("withPermitScoped works within scoped context", () =>
152 Effect.gen(function*() {
153 const semaphore = yield* Effect.tx(TxSemaphore.make(3))
154
155 yield* Effect.scoped(
156 Effect.gen(function*() {
157 yield* TxSemaphore.withPermitScoped(semaphore)
158
159 const available = yield* Effect.tx(TxSemaphore.available(semaphore))
160 assert.strictEqual(available, 2) // One permit acquired for scope
161 })
162 )
163
164 // Permit should be released when scope closes
165 const finalAvailable = yield* Effect.tx(TxSemaphore.available(semaphore))
166 assert.strictEqual(finalAvailable, 3)
167 }))
168 })
169
170 describe("edge cases", () => {
171 it.effect("acquireN with zero permits causes defect", () =>
172 Effect.tx(Effect.gen(function*() {
173 const semaphore = yield* TxSemaphore.make(5)
174 const result = yield* Effect.exit(TxSemaphore.acquireN(semaphore, 0))
175 assert.isTrue(Exit.hasDies(result))
176 })))
177
178 it.effect("acquireN with negative permits causes defect", () =>
179 Effect.tx(Effect.gen(function*() {
180 const semaphore = yield* TxSemaphore.make(5)
181 const result = yield* Effect.exit(TxSemaphore.acquireN(semaphore, -1))
182 assert.isTrue(Exit.hasDies(result))
183 })))
184
185 it.effect("releaseN with zero permits causes defect", () =>
186 Effect.tx(Effect.gen(function*() {
187 const semaphore = yield* TxSemaphore.make(5)
188 const result = yield* Effect.exit(TxSemaphore.releaseN(semaphore, 0))
189 assert.isTrue(Exit.hasDies(result))
190 })))
191
192 it.effect("releaseN with negative permits causes defect", () =>
193 Effect.tx(Effect.gen(function*() {
194 const semaphore = yield* TxSemaphore.make(5)
195 const result = yield* Effect.exit(TxSemaphore.releaseN(semaphore, -1))
196 assert.isTrue(Exit.hasDies(result))
197 })))
198
199 it.effect("release does not exceed capacity", () =>
200 Effect.tx(Effect.gen(function*() {
201 const semaphore = yield* TxSemaphore.make(3)
202
203 // Release more permits than capacity
204 yield* TxSemaphore.release(semaphore)
205 yield* TxSemaphore.release(semaphore)
206
207 const available = yield* TxSemaphore.available(semaphore)
208 assert.strictEqual(available, 3) // Should not exceed capacity
209 })))
210
211 it.effect("releaseN does not exceed capacity", () =>
212 Effect.tx(Effect.gen(function*() {
213 const semaphore = yield* TxSemaphore.make(5)
214
215 // Release more permits than capacity
216 yield* TxSemaphore.releaseN(semaphore, 10)
217
218 const available = yield* TxSemaphore.available(semaphore)
219 assert.strictEqual(available, 5) // Should not exceed capacity
220 })))
221 })
222
223 describe("type guards", () => {
224 it.effect("isTxSemaphore correctly identifies TxSemaphore instances", () =>
225 Effect.tx(Effect.gen(function*() {
226 const semaphore = yield* TxSemaphore.make(5)
227 const notSemaphore = { some: "object" }
228
229 assert.isTrue(TxSemaphore.isTxSemaphore(semaphore))
230 assert.isFalse(TxSemaphore.isTxSemaphore(notSemaphore))
231 assert.isFalse(TxSemaphore.isTxSemaphore(null))
232 assert.isFalse(TxSemaphore.isTxSemaphore(undefined))
233 assert.isFalse(TxSemaphore.isTxSemaphore([5]))
234 })))
235 })
236
237 describe("concurrency", () => {
238 it.effect("withPermit bounds concurrent work and releases for the next waiter", () =>
239 Effect.gen(function*() {
240 const semaphore = yield* Effect.tx(TxSemaphore.make(3))
241
242 const fiber1 = yield* Effect.forkChild(
243 TxSemaphore.withPermit(semaphore, Effect.succeed(1))
244 )
245
246 const fiber2 = yield* Effect.forkChild(
247 TxSemaphore.withPermit(semaphore, Effect.succeed(2))
248 )
249
250 const fiber3 = yield* Effect.forkChild(
251 TxSemaphore.withPermit(semaphore, Effect.succeed(3))
252 )
253
254 const [result1, result2, result3] = yield* Effect.all([
255 Fiber.join(fiber1),
256 Fiber.join(fiber2),
257 Fiber.join(fiber3)
258 ])
259
260 assert.strictEqual(result1, 1)
261 assert.strictEqual(result2, 2)
262 assert.strictEqual(result3, 3)
263
264 // All permits should be released
265 const finalAvailable = yield* Effect.tx(TxSemaphore.available(semaphore))
266 assert.strictEqual(finalAvailable, 3)
267 }))
268 })
269
270 describe("transactional behavior", () => {
271 it.effect("operations are atomic within transactions", () =>
272 Effect.tx(Effect.gen(function*() {
273 const semaphore = yield* TxSemaphore.make(5)
274
275 yield* TxSemaphore.acquire(semaphore)
276 yield* TxSemaphore.acquire(semaphore)
277 yield* TxSemaphore.release(semaphore)
278
279 const available = yield* TxSemaphore.available(semaphore)
280 assert.strictEqual(available, 4) // Net effect: -1 permit
281 })))
282 })
283})