1import { assert, describe, it } from "@effect/vitest"
2import { Deferred, Fiber, Option } from "effect"
3import * as Cause from "effect/Cause"
4import * as Effect from "effect/Effect"
5import * as Exit from "effect/Exit"
6import * as TestClock from "effect/testing/TestClock"
7
8describe("Deferred", () => {
9 describe("success", () => {
10 it.effect("succeed - should propagate the value", () =>
11 Effect.gen(function*() {
12 const deferred = yield* Deferred.make<number>()
13 assert.isTrue(yield* Deferred.succeed(deferred, 1))
14 assert.isFalse(yield* Deferred.succeed(deferred, 1))
15 assert.strictEqual(yield* Deferred.await(deferred), 1)
16 }))
17
18 it.effect("complete - should memoize the result of the provided effect", () =>
19 Effect.gen(function*() {
20 let value = 0
21 const complete = Effect.sync(() => {
22 value += 1
23 return value
24 })
25 const deferred = yield* Deferred.make<number>()
26 assert.isTrue(yield* Deferred.complete(deferred, complete))
27 assert.isFalse(yield* Deferred.complete(deferred, complete))
28 assert.strictEqual(value, 1)
29 assert.strictEqual(yield* Deferred.await(deferred), 1)
30 assert.strictEqual(yield* Deferred.await(deferred), 1)
31 assert.strictEqual(value, 1)
32 }))
33
34 it.effect("completeWith - should not memoize the provided effect", () =>
35 Effect.gen(function*() {
36 let value = 0
37 const complete = Effect.sync(() => {
38 value += 1
39 return value
40 })
41 const complete2 = Effect.sync(() => {
42 value += 10
43 return value
44 })
45 const deferred = yield* Deferred.make<number>()
46 assert.isTrue(yield* Deferred.completeWith(deferred, complete))
47 assert.isFalse(yield* Deferred.completeWith(deferred, complete2))
48 assert.strictEqual(value, 0)
49 assert.strictEqual(yield* Deferred.await(deferred), 1)
50 assert.strictEqual(yield* Deferred.await(deferred), 2)
51 assert.strictEqual(value, 2)
52 }))
53 })
54
55 describe("failure", () => {
56 it.effect("fail - should propagate the failure", () =>
57 Effect.gen(function*() {
58 const deferred = yield* Deferred.make<number, string>()
59 assert.isTrue(yield* Deferred.fail(deferred, "boom"))
60 assert.isFalse(yield* Deferred.succeed(deferred, 1))
61 const result = yield* Effect.exit(Deferred.await(deferred))
62 assert.deepStrictEqual(result, Exit.fail("boom"))
63 }))
64
65 it.effect("complete - should memoize the failure of the provided effect", () =>
66 Effect.gen(function*() {
67 const deferred = yield* Deferred.make<number, string>()
68 assert.isTrue(yield* Deferred.complete(deferred, Effect.fail("boom")))
69 assert.isFalse(yield* Deferred.complete(deferred, Effect.fail("boom2")))
70 const result1 = yield* Effect.exit(Deferred.await(deferred))
71 const result2 = yield* Effect.exit(Deferred.await(deferred))
72 assert.deepStrictEqual(result1, Exit.fail("boom"))
73 assert.deepStrictEqual(result2, Exit.fail("boom"))
74 }))
75
76 it.effect("completeWith - should memoize the provided effect", () =>
77 Effect.gen(function*() {
78 let i = 0
79 const failures = ["boom", "boom2"]
80 const complete = Effect.failSync(() => failures[i++])
81 const complete2 = Effect.failSync(() => "boom3")
82 const deferred = yield* Deferred.make<number, string>()
83 assert.isTrue(yield* Deferred.completeWith(deferred, complete))
84 assert.isFalse(yield* Deferred.completeWith(deferred, complete2))
85 const result = yield* Effect.exit(Deferred.await(deferred))
86 const result2 = yield* Effect.exit(Deferred.await(deferred))
87 assert.deepStrictEqual(result, Exit.fail("boom"))
88 assert.deepStrictEqual(result2, Exit.fail("boom2"))
89 }))
90 })
91
92 describe("interruption", () => {
93 it.effect("interrupt - should propagate the interruption", () =>
94 Effect.gen(function*() {
95 const deferred = yield* Deferred.make<number>()
96 assert.isTrue(yield* Deferred.interruptWith(deferred, -1))
97 assert.isFalse(yield* Deferred.interrupt(deferred))
98 const result = yield* Effect.exit(Deferred.await(deferred))
99 assert.deepStrictEqual(result, Exit.failCause(Cause.interrupt(-1)))
100 }))
101
102 it.effect("await - interrupting a suspended waiter removes it", () =>
103 Effect.gen(function*() {
104 const deferred = yield* Deferred.make<number>()
105 const interrupted = yield* Deferred.await(deferred).pipe(
106 Effect.forkChild({ startImmediately: true })
107 )
108 const live = yield* Deferred.await(deferred).pipe(
109 Effect.forkChild({ startImmediately: true })
110 )
111
112 assert.strictEqual(deferred.resumes?.length, 2)
113 yield* Fiber.interrupt(interrupted)
114 assert.isTrue(Exit.hasInterrupts(yield* Fiber.await(interrupted)))
115 assert.strictEqual(deferred.resumes?.length, 1)
116
117 assert.isTrue(yield* Deferred.succeed(deferred, 42))
118 assert.strictEqual(yield* Fiber.join(live), 42)
119 }))
120
121 it.effect("await - interrupting a waiter after completion does not die", () =>
122 Effect.gen(function*() {
123 const deferred = yield* Deferred.make<number>()
124 const interrupted = yield* Deferred.await(deferred).pipe(
125 Effect.forkChild({ startImmediately: true })
126 )
127 const live = yield* Deferred.await(deferred).pipe(
128 Effect.forkChild({ startImmediately: true })
129 )
130 assert.strictEqual(deferred.resumes?.length, 2)
131
132 // Waiters resume by running the completion effect, so a suspension
133 // inside it leaves the await's cleanup on the waiter's stack after
134 // completion has already cleared `resumes`. Interrupting the waiter
135 // from the same tick then runs that cleanup post-completion.
136 yield* Effect.sync(() => {
137 Deferred.doneUnsafe(deferred, Effect.as(Effect.yieldNow, 42))
138 assert.isUndefined(deferred.resumes)
139 interrupted.interruptUnsafe()
140 })
141
142 const exit = yield* Fiber.await(interrupted)
143 assert.isTrue(Exit.hasInterrupts(exit))
144 assert.isFalse(Exit.hasDies(exit))
145
146 assert.strictEqual(yield* Fiber.join(live), 42)
147 }))
148
149 it.effect("done with an interrupt cause resumes every suspended waiter", () =>
150 Effect.gen(function*() {
151 const deferred = yield* Deferred.make<number>()
152 const first = yield* Deferred.await(deferred).pipe(
153 Effect.forkChild({ startImmediately: true })
154 )
155 const second = yield* Deferred.await(deferred).pipe(
156 Effect.forkChild({ startImmediately: true })
157 )
158 assert.strictEqual(deferred.resumes?.length, 2)
159
160 // Resuming the first waiter with an interrupt cause kills it
161 // synchronously, so its await cleanup must not shift later waiters
162 // out from under the completion loop.
163 assert.isTrue(yield* Deferred.done(deferred, Exit.failCause(Cause.interrupt())))
164
165 const settled = yield* Fiber.awaitAll([first, second]).pipe(
166 Effect.timeoutOption(1000),
167 Effect.forkChild({ startImmediately: true })
168 )
169 yield* TestClock.adjust(1000)
170 const exits = yield* Fiber.join(settled)
171 assert.isTrue(Option.isSome(exits), "every waiter should settle")
172 for (const exit of Option.getOrThrow(exits)) {
173 assert.isTrue(Exit.hasInterrupts(exit))
174 }
175 }))
176 })
177
178 describe("polling", () => {
179 it.effect("poll - should return Option.none when the deferred is incomplete", () =>
180 Effect.gen(function*() {
181 const deferred = yield* Deferred.make<number>()
182 const result = yield* Deferred.poll(deferred)
183 assert.deepStrictEqual(result, Option.none())
184 }))
185
186 it.effect("poll - should return Option.some when the deferred was completed", () =>
187 Effect.gen(function*() {
188 const deferred = yield* Deferred.make<number>()
189 yield* Deferred.succeed(deferred, 1)
190 const polled = yield* Deferred.poll(deferred)
191 if (Option.isNone(polled)) {
192 assert.fail("expected Option.some")
193 return
194 }
195 const result = yield* polled.value
196 assert.deepStrictEqual(result, 1)
197 }))
198
199 it.effect("poll - should return Option.some when the deferred was failed", () =>
200 Effect.gen(function*() {
201 const deferred = yield* Deferred.make<number, string>()
202 yield* Deferred.fail(deferred, "boom")
203 const polled = yield* Deferred.poll(deferred)
204 if (Option.isNone(polled)) {
205 assert.fail("expected Option.some")
206 return
207 }
208 const result = yield* Effect.exit(polled.value)
209 assert.deepStrictEqual(result, Exit.fail("boom"))
210 }))
211
212 it.effect("poll - should return Option.some when the deferred was interrupted", () =>
213 Effect.gen(function*() {
214 const deferred = yield* Deferred.make<number, string>()
215 yield* Deferred.interruptWith(deferred, -1)
216 const polled = yield* Deferred.poll(deferred)
217 if (Option.isNone(polled)) {
218 assert.fail("expected Option.some")
219 return
220 }
221 const result = yield* Effect.exit(polled.value)
222 assert.deepStrictEqual(result, Exit.failCause(Cause.interrupt(-1)))
223 }))
224 })
225
226 describe("done", () => {
227 it.effect("isDone - should return true when suceeded", () =>
228 Effect.gen(function*() {
229 const deferred = yield* Deferred.make<number>()
230 yield* Deferred.succeed(deferred, 1)
231 assert.isTrue(yield* Deferred.isDone(deferred))
232 }))
233
234 it.effect("isDone - should return true when failed", () =>
235 Effect.gen(function*() {
236 const deferred = yield* Deferred.make<number, string>()
237 yield* Deferred.fail(deferred, "boom")
238 assert.isTrue(yield* Deferred.isDone(deferred))
239 }))
240 })
241})