1import { assert, describe, it } from "@effect/vitest"
2import { Channel, Context, Fiber, References, Stream, Tracer } from "effect"
3import * as Arr from "effect/Array"
4import * as Cause from "effect/Cause"
5import * as Data from "effect/Data"
6import * as Effect from "effect/Effect"
7import * as Exit from "effect/Exit"
8import * as Latch from "effect/Latch"
9import * as Layer from "effect/Layer"
10import * as Ref from "effect/Ref"
11import * as Scheduler from "effect/Scheduler"
12import * as Scope from "effect/Scope"
13
14describe("Layer", () => {
15 it.effect("layers can be acquired in parallel", () =>
16 Effect.gen(function*() {
17 const BoolTag = Context.Service<boolean>("boolean")
18 const latch = Latch.makeUnsafe()
19 const layer1 = Layer.effectContext<never, never, never>(Effect.never)
20 const layer2 = Layer.effectContext(
21 Effect.acquireRelease(
22 latch.open.pipe(
23 Effect.map((bool) => Context.make(BoolTag, bool))
24 ),
25 () => Effect.void
26 )
27 )
28 const env = layer1.pipe(Layer.merge(layer2), Layer.build)
29 const fiber = yield* Effect.forkDetach(Effect.scoped(env))
30 yield* latch.await
31 const result = yield* Fiber.interrupt(fiber)
32 assert.isUndefined(result)
33 }))
34
35 it.effect("sharing with merge", () =>
36 Effect.gen(function*() {
37 const array: Array<string> = []
38 const layer = makeLayer1(array)
39 const env = layer.pipe(Layer.merge(layer), Layer.build)
40 yield* Effect.scoped(env)
41 assert.deepStrictEqual(array, [acquire1, release1])
42 }))
43
44 it.effect("preserves a failure when a shared layer build is interrupted", () =>
45 Effect.gen(function*() {
46 const Shared = Context.Service<number>("Shared")
47 const Dying = Context.Service<number>("Dying")
48 const Runtime = Context.Service<boolean>("Runtime")
49
50 for (const kind of ["die", "fail"] as const) {
51 const sharedStarted = Latch.makeUnsafe()
52 const outerStarted = Latch.makeUnsafe()
53 const shared = Layer.effect(Shared)(sharedStarted.open.pipe(Effect.andThen(Effect.never)))
54 const dying = Layer.effect(Dying)(
55 Effect.all([sharedStarted.await, outerStarted.await]).pipe(
56 Effect.andThen(Effect.yieldNow),
57 Effect.andThen(kind === "die" ? Effect.die("layer defect") : Effect.fail("layer failure"))
58 )
59 )
60 const runtime = Layer.effect(Runtime)(Effect.as(Effect.all([Shared, Dying]), true)).pipe(
61 Layer.provide([shared, dying])
62 )
63 const outerShared = Layer.fromBuild((memoMap, scope) =>
64 sharedStarted.await.pipe(
65 Effect.andThen(outerStarted.open),
66 Effect.andThen(shared.build(memoMap, scope))
67 )
68 )
69
70 const exit = yield* Layer.mergeAll(runtime, outerShared).pipe(Layer.build, Effect.scoped, Effect.exit)
71 assert.isTrue(kind === "die" ? Exit.hasDies(exit) : Exit.hasFails(exit))
72 }
73 }))
74
75 it.effect("sharing itself with merge", () =>
76 Effect.gen(function*() {
77 const service1 = new Service1()
78 const layer = Layer.succeed(Service1Tag)(service1)
79 const env = layer.pipe(Layer.merge(layer), Layer.merge(layer), Layer.build)
80 const result = yield* env.pipe(
81 Effect.map((context) => Context.get(context, Service1Tag))
82 )
83 assert.strictEqual(result, service1)
84 }))
85
86 it.effect("suspend is lazy", () =>
87 Effect.gen(function*() {
88 let evaluated = 0
89 const layer = Layer.suspend(() => {
90 evaluated++
91 return Layer.succeed(Service1Tag)(new Service1())
92 })
93
94 assert.strictEqual(evaluated, 0)
95 yield* Effect.scoped(Layer.build(layer))
96 assert.strictEqual(evaluated, 1)
97 yield* Effect.scoped(Layer.build(layer))
98 assert.strictEqual(evaluated, 2)
99 }))
100
101 it.effect("suspend preserves sharing", () =>
102 Effect.gen(function*() {
103 const array: Array<string> = []
104 let evaluated = 0
105 const layer = Layer.suspend(() => {
106 evaluated++
107 return makeLayer1(array)
108 })
109
110 yield* Effect.scoped(layer.pipe(Layer.merge(layer), Layer.build))
111
112 assert.strictEqual(evaluated, 1)
113 assert.deepStrictEqual(array, [acquire1, release1])
114 }))
115
116 it.effect("finalizers", () =>
117 Effect.gen(function*() {
118 const arr: Array<string> = []
119 const layer1 = makeLayer1(arr)
120 const layer2 = makeLayer2(arr)
121 const env = layer1.pipe(Layer.merge(layer2), Layer.build)
122 yield* Effect.scoped(env)
123 assert.isDefined(arr.slice(0, 2).find((s) => s === acquire1))
124 assert.isDefined(arr.slice(0, 2).find((s) => s === acquire2))
125 assert.isDefined(arr.slice(2, 4).find((s) => s === release1))
126 assert.isDefined(arr.slice(2, 4).find((s) => s === release2))
127 }))
128
129 it.effect("catch - uses an alternative layer", () =>
130 Effect.gen(function*() {
131 const arr: Array<string> = []
132 const layer1 = makeLayer1(arr)
133 const layer2 = makeLayer2(arr)
134 const env = Layer.effectDiscard(Effect.fail("failed!")).pipe(
135 Layer.provideMerge(layer1),
136 Layer.catch(() => layer2),
137 Layer.build
138 )
139 yield* Effect.scoped(env)
140 assert.deepStrictEqual(arr, [acquire1, release1, acquire2, release2])
141 }))
142
143 it.effect("catchTag - uses an alternative layer", () =>
144 Effect.gen(function*() {
145 class LayerError extends Data.TaggedError("LayerError") {}
146 const arr: Array<string> = []
147 const layer1 = makeLayer1(arr)
148 const layer2 = makeLayer2(arr)
149 const env = Layer.effectDiscard(Effect.fail(new LayerError())).pipe(
150 Layer.provideMerge(layer1),
151 Layer.catchTag("LayerError", () => layer2),
152 Layer.build
153 )
154 yield* Effect.scoped(env)
155 assert.deepStrictEqual(arr, [acquire1, release1, acquire2, release2])
156 }))
157
158 it.effect("catchCause recovers with a fallback layer and exposes the original cause", () =>
159 Effect.gen(function*() {
160 // Edge case: catchCause should receive the full Cause, not just the typed error,
161 // and the replacement layer should still be built normally.
162 const causes: Array<Cause.Cause<string>> = []
163 const context = yield* Layer.effect(Service1Tag)(Effect.fail("failed!")).pipe(
164 Layer.catchCause((cause) => {
165 causes.push(cause)
166 return Layer.succeed(Service1Tag)(new Service1())
167 }),
168 Layer.build,
169 Effect.scoped
170 )
171
172 assert.strictEqual(yield* Context.get(context, Service1Tag).one(), 1)
173 assert.strictEqual(causes.length, 1)
174 assert.isTrue(Cause.hasFails(causes[0]))
175 }))
176
177 it.effect("tap - executes effect with success services and preserves output", () =>
178 Effect.gen(function*() {
179 const arr: Array<string> = []
180 const env = makeLayer1(arr).pipe(
181 Layer.tap((context) =>
182 Effect.sync(() => {
183 arr.push(`tap:${Context.get(context, Service1Tag).constructor.name}`)
184 })
185 ),
186 Layer.build
187 )
188 const context = yield* Effect.scoped(env)
189 const service = Context.get(context, Service1Tag)
190 assert.strictEqual(yield* service.one(), 1)
191 assert.deepStrictEqual(arr, [acquire1, "tap:Service1", release1])
192 }))
193
194 it.effect("tapError - executes effect and preserves original error", () =>
195 Effect.gen(function*() {
196 const arr: Array<string> = []
197 const error = yield* Layer.effectDiscard(Effect.fail("failed!")).pipe(
198 Layer.tapError((e) =>
199 Effect.sync(() => {
200 arr.push(`tapError:${e}`)
201 })
202 ),
203 Layer.build,
204 Effect.scoped,
205 Effect.flip
206 )
207 assert.strictEqual(error, "failed!")
208 assert.deepStrictEqual(arr, ["tapError:failed!"])
209 }))
210
211 it.effect("tapCause - executes effect and preserves original cause", () =>
212 Effect.gen(function*() {
213 const arr: Array<string> = []
214 const exit = yield* Layer.effectDiscard(Effect.die("boom")).pipe(
215 Layer.tapCause((cause) =>
216 Effect.sync(() => {
217 arr.push(`tapCause:${Cause.hasDies(cause)}`)
218 })
219 ),
220 Layer.build,
221 Effect.scoped,
222 Effect.exit
223 )
224 assert.strictEqual(exit._tag, "Failure")
225 if (exit._tag === "Failure") {
226 assert.isTrue(Cause.hasDies(exit.cause))
227 }
228 assert.deepStrictEqual(arr, ["tapCause:true"])
229 }))
230
231 it.effect("fresh with merge", () =>
232 Effect.gen(function*() {
233 const arr: Array<string> = []
234 const layer = makeLayer1(arr)
235 const env = layer.pipe(Layer.merge(Layer.fresh(layer)), Layer.build)
236 yield* Effect.scoped(env)
237 assert.deepStrictEqual(arr, [acquire1, acquire1, release1, release1])
238 }))
239
240 it.effect("fresh with provide", () =>
241 Effect.gen(function*() {
242 const arr: Array<string> = []
243 const layer = makeLayer1(arr)
244 const env = Layer.fresh(layer).pipe(
245 Layer.provide(layer),
246 Layer.build
247 )
248 yield* Effect.scoped(env)
249 assert.deepStrictEqual(arr, [acquire1, acquire1, release1, release1])
250 }))
251
252 it.effect("with multiple layers", () =>
253 Effect.gen(function*() {
254 const arr: Array<string> = []
255 const layer = makeLayer1(arr)
256 const env = layer.pipe(
257 Layer.merge(layer),
258 Layer.merge(layer.pipe(Layer.merge(layer), Layer.fresh)),
259 Layer.build
260 )
261 yield* Effect.scoped(env)
262 assert.deepStrictEqual(arr, [acquire1, acquire1, release1, release1])
263 }))
264 it.effect("with identical fresh layers", () =>
265 Effect.gen(function*() {
266 const arr: Array<string> = []
267 const layer1 = makeLayer1(arr)
268 const layer2 = makeLayer2(arr)
269 const layer3 = makeLayer3(arr)
270 const env = layer2.pipe(
271 Layer.merge(
272 layer3.pipe(
273 Layer.provide(layer1),
274 Layer.fresh
275 )
276 ),
277 Layer.provide(Layer.fresh(layer1)),
278 Layer.build
279 )
280 yield* Effect.scoped(env)
281 assert.deepStrictEqual(arr, [
282 acquire1,
283 acquire2,
284 acquire1,
285 acquire3,
286 release3,
287 release1,
288 release2,
289 release1
290 ])
291 }))
292 it.effect("interruption with merge", () =>
293 Effect.gen(function*() {
294 const arr: Array<string> = []
295 const layer1 = makeLayer1(arr)
296 const layer2 = makeLayer2(arr)
297 const env = layer1.pipe(Layer.merge(layer2), Layer.build)
298 const fiber = yield* Effect.forkChild(Effect.scoped(env))
299 yield* Fiber.interrupt(fiber)
300 if (arr.find((s) => s === acquire1) !== undefined) {
301 assert.isTrue(arr.some((s) => s === release1))
302 }
303 if (arr.find((s) => s === acquire2) !== undefined) {
304 assert.isTrue(arr.some((s) => s === release2))
305 }
306 }))
307 it.effect("interruption with provide", () =>
308 Effect.gen(function*() {
309 const arr: Array<string> = []
310 const layer1 = makeLayer1(arr)
311 const layer2 = makeLayer2(arr)
312 const env = layer2.pipe(Layer.provide(layer1), Layer.build)
313 const fiber = yield* Effect.forkChild(Effect.scoped(env))
314 yield* Fiber.interrupt(fiber)
315 if (arr.find((s) => s === acquire1) !== undefined) {
316 assert.isTrue(arr.some((s) => s === release1))
317 }
318 if (arr.find((s) => s === acquire2) !== undefined) {
319 assert.isTrue(arr.some((s) => s === release2))
320 }
321 }))
322 it.effect("interruption with multiple layers", () =>
323 Effect.gen(function*() {
324 const arr: Array<string> = []
325 const layer1 = makeLayer1(arr)
326 const layer2 = makeLayer2(arr)
327 const layer3 = makeLayer3(arr)
328 const env = layer3.pipe(
329 Layer.provide(layer1),
330 Layer.merge(layer2),
331 Layer.provide(layer1),
332 Layer.build
333 )
334 const fiber = yield* Effect.forkChild(Effect.scoped(env))
335 yield* Fiber.interrupt(fiber)
336 if (arr.find((s) => s === acquire1) !== undefined) {
337 assert.isTrue(arr.some((s) => s === release1))
338 }
339 if (arr.find((s) => s === acquire2) !== undefined) {
340 assert.isTrue(arr.some((s) => s === release2))
341 }
342 if (arr.find((s) => s === acquire3) !== undefined) {
343 assert.isTrue(arr.some((s) => s === release3))
344 }
345 }))
346
347 it.effect("finalizers with provide", () =>
348 Effect.gen(function*() {
349 const arr: Array<string> = []
350 const layer1 = makeLayer1(arr)
351 const layer2 = makeLayer2(arr)
352 const env = layer2.pipe(Layer.provide(layer1), Layer.build)
353 yield* Effect.scoped(env)
354 assert.deepStrictEqual(arr, [acquire1, acquire2, release2, release1])
355 }))
356
357 it.effect("finalizers with multiple layers with provideTo", () =>
358 Effect.gen(function*() {
359 const arr: Array<string> = []
360 const layer1 = makeLayer1(arr)
361 const layer2 = makeLayer2(arr)
362 const layer3 = makeLayer3(arr)
363 const env = layer3.pipe(Layer.provide(layer2), Layer.provide(layer1), Layer.build)
364 yield* Effect.scoped(env)
365 assert.deepStrictEqual(arr, [acquire1, acquire2, acquire3, release3, release2, release1])
366 }))
367
368 it.effect("orDie does not interfere with sharing", () =>
369 Effect.gen(function*() {
370 const arr: Array<string> = []
371 const layer1 = makeLayer1(arr)
372 const layer2 = makeLayer2(arr)
373 const layer3 = makeLayer3(arr)
374 const env = layer3.pipe(
375 Layer.provide(layer1),
376 Layer.provide(layer2),
377 Layer.provide(Layer.orDie(layer1)),
378 Layer.build
379 )
380 yield* Effect.scoped(env)
381 assert.strictEqual(arr[0], acquire1)
382 assert.isTrue(arr.slice(1, 3).some((s) => s === acquire2))
383 assert.isTrue(arr.slice(1, 3).some((s) => s === acquire3))
384 assert.isTrue(arr.slice(3, 5).some((s) => s === release3))
385 assert.isTrue(arr.slice(3, 5).some((s) => s === release2))
386 assert.strictEqual(arr[5], release1)
387 }))
388
389 it.effect("launch keeps the layer open until interrupted and then finalizes", () =>
390 Effect.gen(function*() {
391 // Edge case: launch never completes on its own, so interruption must still
392 // close the layer scope and run finalizers.
393 const arr: Array<string> = []
394 const fiber = yield* Effect.forkChild(Layer.launch(makeLayer1(arr)))
395 yield* Effect.yieldNow
396 assert.deepStrictEqual(arr, [acquire1])
397 yield* Fiber.interrupt(fiber)
398 assert.deepStrictEqual(arr, [acquire1, release1])
399 }))
400
401 describe("mock", () => {
402 it.effect("allows passing partial service", () =>
403 Effect.gen(function*() {
404 class Service1 extends Context.Service<Service1, {
405 one: Effect.Effect<number>
406 two(): Effect.Effect<number>
407 three: Stream.Stream<number>
408 }>()("Service1") {}
409 yield* Effect.gen(function*() {
410 const service = yield* Service1
411 assert.strictEqual(yield* service.one, 123)
412 yield* service.two().pipe(
413 Effect.catchDefect(Effect.fail),
414 Effect.flip
415 )
416 yield* service.three.pipe(
417 Stream.catchCause(Stream.fail),
418 Stream.runDrain,
419 Effect.flip
420 )
421 }).pipe(
422 Effect.provide(
423 Layer.mock(Service1)({
424 one: Effect.succeed(123)
425 })
426 )
427 )
428 }))
429
430 it.effect("allows passing partial service in dual form", () =>
431 Effect.gen(function*() {
432 class Service1 extends Context.Service<Service1, {
433 one: Effect.Effect<number>
434 two(): Effect.Effect<number>
435 }>()("Service1") {}
436 yield* Effect.gen(function*() {
437 const service = yield* Service1
438 assert.strictEqual(yield* service.one, 123)
439 yield* service.two().pipe(
440 Effect.catchDefect(Effect.fail),
441 Effect.flip
442 )
443 }).pipe(
444 Effect.provide(
445 Layer.mock(Service1, {
446 one: Effect.succeed(123)
447 })
448 )
449 )
450 }))
451
452 it.effect("missing mock members fail as unimplemented effects and channels", () =>
453 Effect.gen(function*() {
454 // Edge case: missing mock members are represented by a value that can behave
455 // as a function, Effect, Stream, or Channel depending on how it is used.
456 class Service1 extends Context.Service<Service1, {
457 effect(): Effect.Effect<number>
458 channel: Channel.Channel<number, never, void>
459 }>()("Service1") {}
460 yield* Effect.gen(function*() {
461 const service = yield* Service1
462 const effectDefect = yield* service.effect().pipe(
463 Effect.catchDefect(Effect.fail),
464 Effect.flip
465 )
466 assert.strictEqual((effectDefect as Error).name, "UnimplementedError")
467
468 const channelExit = yield* service.channel.pipe(
469 Channel.runDrain,
470 Effect.exit
471 )
472 assert.strictEqual(channelExit._tag, "Failure")
473
474 const transformed = yield* (service.channel as unknown as {
475 transform: () => Effect.Effect<Effect.Effect<never>>
476 }).transform()
477 const transformedDefect = yield* transformed.pipe(
478 Effect.catchDefect(Effect.fail),
479 Effect.flip
480 )
481 assert.strictEqual((transformedDefect as Error).name, "UnimplementedError")
482 }).pipe(
483 Effect.provide(Layer.mock(Service1)({}))
484 )
485 }))
486 })
487
488 describe("MemoMap", () => {
489 class Shared extends Context.Service<Shared, { readonly n: number }>()("Shared") {}
490
491 const releaseCounted = (released: Ref.Ref<number>) =>
492 Layer.effect(
493 Shared,
494 Effect.acquireRelease(Effect.succeed({ n: 1 }), () => Ref.update(released, (n) => n + 1))
495 )
496
497 // A small MaxOpsBeforeYield budget makes a fiber yield before each op, so
498 // sweeping the budget moves an interrupt through every step of a build.
499 const budgets = Arr.range(3, 64)
500
501 const settle = Effect.gen(function*() {
502 for (let i = 0; i < 200; i++) yield* Effect.yieldNow
503 })
504
505 it.effect("interrupting a build does not hang current or later requesters", () =>
506 Effect.gen(function*() {
507 for (const ops of budgets) {
508 const memoMap = Layer.makeMemoMapUnsafe()
509 const layer = Layer.effect(Shared, Effect.succeed({ n: 1 }))
510 const first = yield* Effect.forkChild(
511 Effect.provideService(
512 Layer.buildWithMemoMap(layer, memoMap, yield* Scope.make()),
513 Scheduler.MaxOpsBeforeYield,
514 ops
515 ),
516 { startImmediately: true }
517 )
518 const waiter = yield* Effect.forkChild(Layer.buildWithMemoMap(layer, memoMap, yield* Scope.make()), {
519 startImmediately: true
520 })
521 yield* Fiber.interrupt(first)
522 const later = yield* Effect.forkChild(Layer.buildWithMemoMap(layer, memoMap, yield* Scope.make()), {
523 startImmediately: true
524 })
525 yield* settle
526 assert.isDefined(waiter.pollUnsafe(), `waiter hung with ${ops} ops`)
527 assert.isDefined(later.pollUnsafe(), `later requester hung with ${ops} ops`)
528 }
529 }))
530
531 it.effect("an interrupted reuse does not keep the shared layer alive", () =>
532 Effect.gen(function*() {
533 for (const closed of [false, true]) {
534 for (const ops of budgets) {
535 const released = yield* Ref.make(0)
536 const memoMap = Layer.makeMemoMapUnsafe()
537 const layer = releaseCounted(released)
538 const scope1 = yield* Scope.make()
539 const scope2 = yield* Scope.make()
540 yield* Layer.buildWithMemoMap(layer, memoMap, scope1)
541 if (closed) yield* Scope.close(scope2, Exit.void)
542 const reuse = yield* Effect.forkChild(
543 Effect.provideService(Layer.buildWithMemoMap(layer, memoMap, scope2), Scheduler.MaxOpsBeforeYield, ops),
544 { startImmediately: true }
545 )
546 yield* Fiber.interrupt(reuse)
547 if (!closed) yield* Scope.close(scope2, Exit.void)
548 yield* Scope.close(scope1, Exit.void)
549 assert.strictEqual(yield* Ref.get(released), 1, `closed scope: ${closed}, ${ops} ops`)
550 }
551 }
552 }))
553
554 it.effect("running a get effect twice does not release the shared layer early", () =>
555 Effect.gen(function*() {
556 const released = yield* Ref.make(0)
557 const memoMap = Layer.makeMemoMapUnsafe()
558 const layer = releaseCounted(released)
559 const scope1 = yield* Scope.make()
560 const scope2 = yield* Scope.make()
561 yield* Layer.buildWithMemoMap(layer, memoMap, scope1)
562 const reuse = memoMap.get(layer, scope2)
563 assert.isDefined(reuse)
564 yield* reuse
565 yield* reuse
566 yield* Scope.close(scope2, Exit.void)
567 assert.strictEqual(yield* Ref.get(released), 0)
568 yield* Scope.close(scope1, Exit.void)
569 assert.strictEqual(yield* Ref.get(released), 1)
570 }))
571
572 it.effect("a get effect that never runs does not keep the shared layer alive", () =>
573 Effect.gen(function*() {
574 const released = yield* Ref.make(0)
575 const memoMap = Layer.makeMemoMapUnsafe()
576 const layer = releaseCounted(released)
577 const scope1 = yield* Scope.make()
578 const scope2 = yield* Scope.make()
579 yield* Layer.buildWithMemoMap(layer, memoMap, scope1)
580 assert.isDefined(memoMap.get(layer, scope2))
581 yield* Scope.close(scope2, Exit.void)
582 yield* Scope.close(scope1, Exit.void)
583 assert.strictEqual(yield* Ref.get(released), 1)
584 }))
585
586 it("does not memoize a build before its Effect executes", () => {
587 const memoMap = Layer.makeMemoMapUnsafe()
588 const scope = Scope.makeUnsafe()
589 const layer = Layer.effectDiscard(Effect.void)
590
591 // @effect-diagnostics-next-line floatingEffect:off
592 Layer.buildWithMemoMap(layer, memoMap, scope)
593
594 assert.strictEqual((memoMap as any).map.size, 0)
595 })
596
597 it.effect("memoizes suspend across builds", () =>
598 Effect.gen(function*() {
599 const arr: Array<string> = []
600 let evaluated = 0
601 const layer = Layer.suspend(() => {
602 evaluated++
603 return makeLayer1(arr)
604 })
605 const memoMap = Layer.makeMemoMapUnsafe()
606 const scope1 = yield* Scope.make()
607 const scope2 = yield* Scope.make()
608
609 yield* Layer.buildWithMemoMap(layer, memoMap, scope1)
610 yield* Layer.buildWithMemoMap(layer, memoMap, scope2)
611 yield* Scope.close(scope2, Exit.void)
612
613 assert.strictEqual(evaluated, 1)
614 assert.deepStrictEqual(arr, [acquire1])
615
616 yield* Scope.close(scope1, Exit.void)
617
618 assert.deepStrictEqual(arr, [acquire1, release1])
619 }))
620
621 it.effect("memoizes layer across builds", () =>
622 Effect.gen(function*() {
623 const arr: Array<string> = []
624 const layer1 = makeLayer1(arr)
625 const layer2 = makeLayer2(arr).pipe(
626 Layer.provide(layer1)
627 )
628 const memoMap = Layer.makeMemoMapUnsafe()
629 const scope1 = yield* Scope.make()
630 const scope2 = yield* Scope.make()
631
632 yield* Layer.buildWithMemoMap(layer1, memoMap, scope1)
633 yield* Layer.buildWithMemoMap(layer2, memoMap, scope2)
634 yield* Scope.close(scope2, Exit.void)
635 yield* Layer.buildWithMemoMap(layer2, memoMap, scope1)
636 yield* Scope.close(scope1, Exit.void)
637
638 assert.deepStrictEqual(arr, [acquire1, acquire2, release2, acquire2, release2, release1])
639 }))
640
641 it.effect("layers are not released early", () =>
642 Effect.gen(function*() {
643 const arr: Array<string> = []
644 const layer1 = makeLayer1(arr)
645 const layer2 = makeLayer2(arr).pipe(
646 Layer.provide(layer1)
647 )
648 const memoMap = Layer.makeMemoMapUnsafe()
649 const scope1 = yield* Scope.make()
650 const scope2 = yield* Scope.make()
651
652 yield* Layer.buildWithMemoMap(layer1, memoMap, scope1)
653 yield* Layer.buildWithMemoMap(layer2, memoMap, scope2)
654 yield* Scope.close(scope1, Exit.void)
655 yield* Scope.close(scope2, Exit.void)
656
657 assert.deepStrictEqual(arr, [acquire1, acquire2, release2, release1])
658 }))
659
660 it.effect("forked memo maps reuse parent layers but isolate child layers", () =>
661 Effect.gen(function*() {
662 // Edge case: a child MemoMap should reuse entries already present in the parent,
663 // but entries first built in the child must not be written back to the parent.
664 const arr: Array<string> = []
665 const parent = yield* Layer.makeMemoMap
666 const child = yield* Layer.forkMemoMap(parent)
667 assert.strictEqual((child as any)["~effect/Layer/MemoMap"], "~effect/Layer/MemoMap")
668
669 const scope1 = yield* Scope.make()
670 const scope2 = yield* Scope.make()
671 const parentLayer = makeLayer1(arr)
672 const childLayer = makeLayer2(arr)
673
674 yield* Layer.buildWithMemoMap(parentLayer, parent, scope1)
675 yield* Layer.buildWithMemoMap(parentLayer, child, scope2)
676 yield* Layer.buildWithMemoMap(childLayer, child, scope2)
677 yield* Layer.buildWithMemoMap(childLayer, parent, scope1)
678
679 yield* Scope.close(scope2, Exit.void)
680 yield* Scope.close(scope1, Exit.void)
681
682 assert.deepStrictEqual(arr, [acquire1, acquire2, acquire2, release2, release2, release1])
683 }))
684 })
685
686 describe("tracing", () => {
687 it.effect("withSpan forwards captureStackTrace to the layer stack frame", () =>
688 Effect.gen(function*() {
689 const Frame = Context.Service<References.StackFrame | undefined>("Frame")
690 const layer = Layer.effect(Frame, References.CurrentStackFrame)
691
692 // By default the frame records this call site, not Layer's internals.
693 const captured = yield* Effect.provide(Frame, Layer.withSpan(layer, "captured"))
694 assert.include(captured!.stack()!, "Layer.test.ts")
695
696 const disabled = yield* Effect.provide(
697 Frame,
698 layer.pipe(Layer.withSpan("disabled", { captureStackTrace: false }))
699 )
700 assert.isUndefined(disabled!.stack())
701
702 let calls = 0
703 const supplied = yield* Effect.provide(
704 Frame,
705 Layer.withSpan(layer, "supplied", {
706 captureStackTrace: () => {
707 calls++
708 return "at construct (app.ts:1:1)"
709 }
710 })
711 )
712 assert.strictEqual(calls, 0)
713 assert.strictEqual(supplied!.stack(), "at construct (app.ts:1:1)")
714 assert.strictEqual(calls, 1)
715 }))
716
717 it.effect("withSpan supports data-first usage and runs the onEnd finalizer", () =>
718 Effect.gen(function*() {
719 // Edge case: the data-first overload has separate runtime branching, and onEnd
720 // must run when the layer scope closes.
721 const SpanName = Context.Service<string>("SpanName")
722 const exits: Array<Exit.Exit<unknown, unknown>> = []
723 const scope = yield* Scope.make()
724 const layer = Layer.effect(SpanName)(
725 Effect.map(Tracer.ParentSpan, (span) => span._tag === "Span" ? span.name : span.spanId)
726 )
727 const context = yield* Layer.withSpan(layer, "layer-span", {
728 onEnd: (_span, exit) => Effect.sync(() => exits.push(exit))
729 }).pipe(Layer.buildWithScope(scope))
730
731 assert.strictEqual(Context.get(context, SpanName), "layer-span")
732 assert.deepStrictEqual(exits, [])
733
734 yield* Scope.close(scope, Exit.void)
735
736 assert.strictEqual(exits.length, 1)
737 assert.strictEqual(exits[0]._tag, "Success")
738 }))
739
740 it.effect("withParentSpan supports data-last usage with external spans", () =>
741 Effect.gen(function*() {
742 // Edge case: external spans do not get stack-frame wrapping, but they should
743 // still be installed as the parent span for layer construction.
744 const SpanId = Context.Service<string>("SpanId")
745 const parent = Tracer.externalSpan({
746 spanId: "0000000000000001",
747 traceId: "00000000000000000000000000000001",
748 sampled: true
749 })
750 const context = yield* Layer.effect(SpanId)(
751 Effect.map(Tracer.ParentSpan, (span) => span.spanId)
752 ).pipe(
753 Layer.withParentSpan(parent),
754 Layer.build,
755 Effect.scoped
756 )
757
758 assert.strictEqual(Context.get(context, SpanId), parent.spanId)
759 }))
760 })
761})
762
763const acquire1 = "Acquiring Module 1"
764const acquire2 = "Acquiring Module 2"
765const acquire3 = "Acquiring Module 3"
766const release1 = "Releasing Module 1"
767const release2 = "Releasing Module 2"
768const release3 = "Releasing Module 3"
769
770export class Service1 {
771 one(): Effect.Effect<number> {
772 return Effect.succeed(1)
773 }
774}
775const Service1Tag = Context.Service<Service1>("Service1")
776const makeLayer1 = (array: Array<string>): Layer.Layer<Service1> => {
777 return Layer.effect(Service1Tag)(
778 Effect.acquireRelease(
779 Effect.sync(() => {
780 array.push(acquire1)
781 return new Service1()
782 }),
783 () => Effect.sync(() => array.push(release1))
784 )
785 )
786}
787class Service2 {
788 two(): Effect.Effect<number> {
789 return Effect.succeed(2)
790 }
791}
792const Service2Tag = Context.Service<Service2>("Service2")
793const makeLayer2 = (array: Array<string>): Layer.Layer<Service2> => {
794 return Layer.effect(Service2Tag)(
795 Effect.acquireRelease(
796 Effect.sync(() => {
797 array.push(acquire2)
798 return new Service2()
799 }),
800 () => Effect.sync(() => array.push(release2))
801 )
802 )
803}
804class Service3 {
805 three(): Effect.Effect<number> {
806 return Effect.succeed(3)
807 }
808}
809const Service3Tag = Context.Service<Service3>("Service3")
810const makeLayer3 = (array: Array<string>): Layer.Layer<Service3> => {
811 return Layer.effect(Service3Tag)(
812 Effect.acquireRelease(
813 Effect.sync(() => {
814 array.push(acquire3)
815 return new Service3()
816 }),
817 () => Effect.sync(() => array.push(release3))
818 )
819 )
820}