| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { Channel, Context, Fiber, References, Stream, Tracer } from "effect" |
| 3 | import * as Arr from "effect/Array" |
| 4 | import * as Cause from "effect/Cause" |
| 5 | import * as Data from "effect/Data" |
| 6 | import * as Effect from "effect/Effect" |
| 7 | import * as Exit from "effect/Exit" |
| 8 | import * as Latch from "effect/Latch" |
| 9 | import * as Layer from "effect/Layer" |
| 10 | import * as Ref from "effect/Ref" |
| 11 | import * as Scheduler from "effect/Scheduler" |
| 12 | import * as Scope from "effect/Scope" |
| 13 | |
| 14 | describe("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 | |
| 763 | const acquire1 = "Acquiring Module 1" |
| 764 | const acquire2 = "Acquiring Module 2" |
| 765 | const acquire3 = "Acquiring Module 3" |
| 766 | const release1 = "Releasing Module 1" |
| 767 | const release2 = "Releasing Module 2" |
| 768 | const release3 = "Releasing Module 3" |
| 769 | |
| 770 | export class Service1 { |
| 771 | one(): Effect.Effect<number> { |
| 772 | return Effect.succeed(1) |
| 773 | } |
| 774 | } |
| 775 | const Service1Tag = Context.Service<Service1>("Service1") |
| 776 | const 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 | } |
| 787 | class Service2 { |
| 788 | two(): Effect.Effect<number> { |
| 789 | return Effect.succeed(2) |
| 790 | } |
| 791 | } |
| 792 | const Service2Tag = Context.Service<Service2>("Service2") |
| 793 | const 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 | } |
| 804 | class Service3 { |
| 805 | three(): Effect.Effect<number> { |
| 806 | return Effect.succeed(3) |
| 807 | } |
| 808 | } |
| 809 | const Service3Tag = Context.Service<Service3>("Service3") |
| 810 | const 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 | } |