| 1 | /** |
| 2 | * Controls how long resources stay open. |
| 3 | * |
| 4 | * A scope is a lifetime boundary. Code can register cleanup effects on it, and |
| 5 | * closing the scope runs those cleanups with the `Exit` value that ended the |
| 6 | * work. Most application code uses higher-level APIs such as `Effect.scoped` |
| 7 | * and `Layer`, while this module is useful when code needs to create, provide, |
| 8 | * fork, close, or inspect scopes directly. |
| 9 | * |
| 10 | * @since 2.0.0 |
| 11 | */ |
| 12 | |
| 13 | import type * as Context from "./Context.ts" |
| 14 | import type { Effect } from "./Effect.ts" |
| 15 | import type { Exit } from "./Exit.ts" |
| 16 | import * as effect from "./internal/effect.ts" |
| 17 | |
| 18 | const TypeId = effect.ScopeTypeId |
| 19 | const CloseableTypeId = effect.ScopeCloseableTypeId |
| 20 | |
| 21 | /** |
| 22 | * A `Scope` represents a context where resources can be acquired and |
| 23 | * automatically cleaned up when the scope is closed. Scopes can use |
| 24 | * either sequential or parallel finalization strategies. |
| 25 | * |
| 26 | * **Example** (Managing scoped resources) |
| 27 | * |
| 28 | * ```ts import.meta.vitest |
| 29 | * import { Effect, Exit, Scope } from "effect" |
| 30 | * |
| 31 | * const program = Effect.gen(function*() { |
| 32 | * const scope = yield* Scope.make("sequential") |
| 33 | * |
| 34 | * const initial = [scope.strategy, scope.state._tag] |
| 35 | * yield* Scope.close(scope, Exit.void) |
| 36 | * return [initial, scope.state._tag] |
| 37 | * }) |
| 38 | * |
| 39 | * Effect.runSync(program) // => [["sequential", "Empty"], "Closed"] |
| 40 | * ``` |
| 41 | * |
| 42 | * @category services |
| 43 | * @since 2.0.0 |
| 44 | */ |
| 45 | export interface Scope { |
| 46 | readonly [TypeId]: typeof TypeId |
| 47 | readonly strategy: "sequential" | "parallel" |
| 48 | readonly parent: Scope | undefined |
| 49 | state: State.Open | State.Closed | State.Empty |
| 50 | } |
| 51 | /** |
| 52 | * A `Closeable` scope extends the base `Scope` interface with the ability |
| 53 | * to be closed, executing all registered finalizers. |
| 54 | * |
| 55 | * **Example** (Closing a scope) |
| 56 | * |
| 57 | * ```ts import.meta.vitest |
| 58 | * import { Effect, Exit, Scope } from "effect" |
| 59 | * |
| 60 | * const cleanups: Array<string> = [] |
| 61 | * const program = Effect.gen(function*() { |
| 62 | * const scope = yield* Scope.make() |
| 63 | * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup!"))) |
| 64 | * yield* Scope.close(scope, Exit.void) |
| 65 | * }) |
| 66 | * |
| 67 | * Effect.runSync(program) |
| 68 | * cleanups // => ["Cleanup!"] |
| 69 | * ``` |
| 70 | * |
| 71 | * @category models |
| 72 | * @since 2.0.0 |
| 73 | */ |
| 74 | export interface Closeable extends Scope { |
| 75 | readonly [CloseableTypeId]: typeof CloseableTypeId |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Scope states: `Empty` has no finalizers, `Open` has at least one, and |
| 80 | * `Closed` holds the exit value. |
| 81 | * |
| 82 | * **Example** (Checking scope states) |
| 83 | * |
| 84 | * ```ts import.meta.vitest |
| 85 | * import { Effect, Exit, Scope } from "effect" |
| 86 | * |
| 87 | * const program = Effect.gen(function*() { |
| 88 | * const scope = yield* Scope.make() |
| 89 | * const before = scope.state._tag |
| 90 | * yield* Scope.close(scope, Exit.void) |
| 91 | * return [before, scope.state._tag] |
| 92 | * }) |
| 93 | * |
| 94 | * Effect.runSync(program) // => ["Empty", "Closed"] |
| 95 | * ``` |
| 96 | * |
| 97 | * @since 4.0.0 |
| 98 | */ |
| 99 | export declare namespace State { |
| 100 | /** |
| 101 | * Represents an open scope with no finalizers currently registered. |
| 102 | * |
| 103 | * **Details** |
| 104 | * |
| 105 | * Adding a finalizer moves it to `Open`; removing the last one returns it |
| 106 | * to `Empty`. |
| 107 | * |
| 108 | * **Example** (Inspecting an empty scope state) |
| 109 | * |
| 110 | * ```ts import.meta.vitest |
| 111 | * import { Scope } from "effect" |
| 112 | * |
| 113 | * const scope = Scope.makeUnsafe() |
| 114 | * |
| 115 | * scope.state._tag // => "Empty" |
| 116 | * ``` |
| 117 | * |
| 118 | * @category models |
| 119 | * @since 4.0.0 |
| 120 | */ |
| 121 | export type Empty = { |
| 122 | readonly _tag: "Empty" |
| 123 | } |
| 124 | /** |
| 125 | * Represents an open scope state where finalizers can be added and |
| 126 | * the scope is still accepting new resources. |
| 127 | * |
| 128 | * **Details** |
| 129 | * |
| 130 | * Stores one finalizer inline and allocates the `finalizers` map when a |
| 131 | * second is added. |
| 132 | * |
| 133 | * **Example** (Inspecting an open scope state) |
| 134 | * |
| 135 | * ```ts import.meta.vitest |
| 136 | * import { Effect, Scope } from "effect" |
| 137 | * |
| 138 | * const scope = Scope.makeUnsafe() |
| 139 | * |
| 140 | * Effect.runSync(Scope.addFinalizer(scope, Effect.void)) |
| 141 | * const state = scope.state |
| 142 | * if (state._tag !== "Open") throw new Error("unexpected state") |
| 143 | * |
| 144 | * state._tag // => "Open" |
| 145 | * state.finalizer !== undefined // => true |
| 146 | * ``` |
| 147 | * |
| 148 | * @category models |
| 149 | * @since 4.0.0 |
| 150 | */ |
| 151 | export type Open = { |
| 152 | readonly _tag: "Open" |
| 153 | finalizerKey: {} | undefined |
| 154 | finalizer: ((exit: Exit<any, any>) => Effect<void>) | undefined |
| 155 | finalizers: Map<{}, (exit: Exit<any, any>) => Effect<void>> | undefined |
| 156 | } |
| 157 | /** |
| 158 | * Represents a closed scope state where finalizers have been executed |
| 159 | * and the scope is no longer accepting new resources. |
| 160 | * |
| 161 | * **Example** (Inspecting a closed scope state) |
| 162 | * |
| 163 | * ```ts import.meta.vitest |
| 164 | * import { Effect, Exit, Scope } from "effect" |
| 165 | * |
| 166 | * const program = Effect.gen(function*() { |
| 167 | * const scope = yield* Scope.make() |
| 168 | * |
| 169 | * yield* Scope.close(scope, Exit.succeed("Done")) |
| 170 | * if (scope.state._tag === "Closed") { |
| 171 | * return scope.state.exit |
| 172 | * } |
| 173 | * return Exit.die("unexpected state") |
| 174 | * }) |
| 175 | * |
| 176 | * Effect.runSync(program) // => Exit.succeed("Done") |
| 177 | * ``` |
| 178 | * |
| 179 | * @category models |
| 180 | * @since 4.0.0 |
| 181 | */ |
| 182 | export type Closed = { |
| 183 | readonly _tag: "Closed" |
| 184 | readonly exit: Exit<any, any> |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Service tag for the active resource lifetime. |
| 190 | * |
| 191 | * **When to use** |
| 192 | * |
| 193 | * Use to access the active lifetime when registering finalizers or sharing |
| 194 | * resources with the surrounding scope. |
| 195 | * |
| 196 | * **Example** (Accessing the scope service) |
| 197 | * |
| 198 | * ```ts import.meta.vitest |
| 199 | * import { Effect, Scope } from "effect" |
| 200 | * |
| 201 | * const cleanups: Array<string> = [] |
| 202 | * const program = Effect.gen(function*() { |
| 203 | * const scope = yield* Scope.Scope |
| 204 | * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup"))) |
| 205 | * }) |
| 206 | * |
| 207 | * Effect.runSync(Effect.scoped(program)) |
| 208 | * cleanups // => ["Cleanup"] |
| 209 | * ``` |
| 210 | * |
| 211 | * @category services |
| 212 | * @since 2.0.0 |
| 213 | */ |
| 214 | export const Scope: Context.Service<Scope, Scope> = effect.scopeTag |
| 215 | |
| 216 | /** |
| 217 | * Creates a new `Scope` with the specified finalizer strategy. |
| 218 | * |
| 219 | * **Example** (Creating a scope) |
| 220 | * |
| 221 | * ```ts import.meta.vitest |
| 222 | * import { Effect, Exit, Scope } from "effect" |
| 223 | * |
| 224 | * const cleanups: Array<string> = [] |
| 225 | * const program = Effect.gen(function*() { |
| 226 | * const scope = yield* Scope.make("sequential") |
| 227 | * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup 1"))) |
| 228 | * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup 2"))) |
| 229 | * yield* Scope.close(scope, Exit.void) |
| 230 | * }) |
| 231 | * |
| 232 | * Effect.runSync(program) |
| 233 | * cleanups // => ["Cleanup 2", "Cleanup 1"] |
| 234 | * ``` |
| 235 | * |
| 236 | * @category constructors |
| 237 | * @since 2.0.0 |
| 238 | */ |
| 239 | export const make: (finalizerStrategy?: "sequential" | "parallel") => Effect<Closeable> = effect.scopeMake |
| 240 | |
| 241 | /** |
| 242 | * Creates a new `Scope` synchronously without wrapping it in an `Effect`. |
| 243 | * This is useful when you need a scope immediately but should be used with caution |
| 244 | * as it doesn't provide the same safety guarantees as the `Effect`-wrapped version. |
| 245 | * |
| 246 | * **When to use** |
| 247 | * |
| 248 | * Use when a scope must be allocated synchronously and the caller will close it |
| 249 | * manually. |
| 250 | * |
| 251 | * **Example** (Creating a scope synchronously) |
| 252 | * |
| 253 | * ```ts import.meta.vitest |
| 254 | * import { Effect, Exit, Scope } from "effect" |
| 255 | * |
| 256 | * const scope = Scope.makeUnsafe("sequential") |
| 257 | * const cleanups: Array<string> = [] |
| 258 | * const program = Effect.gen(function*() { |
| 259 | * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup"))) |
| 260 | * yield* Scope.close(scope, Exit.void) |
| 261 | * }) |
| 262 | * |
| 263 | * Effect.runSync(program) |
| 264 | * cleanups // => ["Cleanup"] |
| 265 | * ``` |
| 266 | * |
| 267 | * @category constructors |
| 268 | * @since 4.0.0 |
| 269 | */ |
| 270 | export const makeUnsafe: (finalizerStrategy?: "sequential" | "parallel") => Closeable = effect.scopeMakeUnsafe |
| 271 | |
| 272 | /** |
| 273 | * Provides a concrete `Scope` to an effect. |
| 274 | * |
| 275 | * **When to use** |
| 276 | * |
| 277 | * Use to run an effect that requires `Scope` with a scope managed by the |
| 278 | * caller. |
| 279 | * |
| 280 | * **Details** |
| 281 | * |
| 282 | * Providing the scope removes the `Scope` requirement from the effect context. |
| 283 | * |
| 284 | * **Example** (Providing a scope) |
| 285 | * |
| 286 | * ```ts import.meta.vitest |
| 287 | * import { Effect, Exit, Scope } from "effect" |
| 288 | * |
| 289 | * const events: Array<string> = [] |
| 290 | * const program = Effect.gen(function*() { |
| 291 | * const scope = yield* Scope.Scope |
| 292 | * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("cleanup"))) |
| 293 | * events.push("working") |
| 294 | * }) |
| 295 | * |
| 296 | * const withScope = Effect.gen(function*() { |
| 297 | * const scope = yield* Scope.make() |
| 298 | * yield* Scope.provide(scope)(program) |
| 299 | * yield* Scope.close(scope, Exit.void) |
| 300 | * }) |
| 301 | * |
| 302 | * Effect.runSync(withScope) |
| 303 | * events // => ["working", "cleanup"] |
| 304 | * ``` |
| 305 | * |
| 306 | * @category combinators |
| 307 | * @since 4.0.0 |
| 308 | */ |
| 309 | export const provide: { |
| 310 | (value: Scope): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, Exclude<R, Scope>> |
| 311 | <A, E, R>(self: Effect<A, E, R>, value: Scope): Effect<A, E, Exclude<R, Scope>> |
| 312 | } = effect.provideScope |
| 313 | |
| 314 | /** |
| 315 | * Registers an exit-aware finalizer on a scope. |
| 316 | * |
| 317 | * **When to use** |
| 318 | * |
| 319 | * Use when cleanup needs to know whether the scope closed with success, |
| 320 | * failure, or interruption. |
| 321 | * |
| 322 | * **Details** |
| 323 | * |
| 324 | * If the scope is open, the finalizer runs when the scope closes and receives |
| 325 | * the scope's exit value. If the scope is already closed, the finalizer runs |
| 326 | * immediately with the stored exit value. |
| 327 | * |
| 328 | * **Example** (Adding an exit-aware finalizer) |
| 329 | * |
| 330 | * ```ts import.meta.vitest |
| 331 | * import { Effect, Exit, Scope } from "effect" |
| 332 | * |
| 333 | * const exits: Array<Exit.Exit<unknown, unknown>> = [] |
| 334 | * const withResource = Effect.gen(function*() { |
| 335 | * const scope = yield* Scope.make() |
| 336 | * yield* Scope.addFinalizerExit(scope, (exit) => Effect.sync(() => exits.push(exit))) |
| 337 | * yield* Scope.close(scope, Exit.void) |
| 338 | * }) |
| 339 | * |
| 340 | * Effect.runSync(withResource) |
| 341 | * exits // => [Exit.void] |
| 342 | * ``` |
| 343 | * |
| 344 | * @category combinators |
| 345 | * @since 2.0.0 |
| 346 | */ |
| 347 | export const addFinalizerExit: (scope: Scope, finalizer: (exit: Exit<any, any>) => Effect<unknown>) => Effect<void> = |
| 348 | effect.scopeAddFinalizerExit |
| 349 | |
| 350 | /** |
| 351 | * Registers a finalizer effect on a scope. |
| 352 | * |
| 353 | * **Details** |
| 354 | * |
| 355 | * If the scope is open, the finalizer runs when the scope closes, regardless of |
| 356 | * whether the scope closes successfully or with an error. If the scope is |
| 357 | * already closed, the finalizer runs immediately. |
| 358 | * |
| 359 | * **Example** (Adding finalizers) |
| 360 | * |
| 361 | * ```ts import.meta.vitest |
| 362 | * import { Effect, Exit, Scope } from "effect" |
| 363 | * |
| 364 | * const events: Array<string> = [] |
| 365 | * const program = Effect.gen(function*() { |
| 366 | * const scope = yield* Scope.make() |
| 367 | * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("cleanup 1"))) |
| 368 | * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("cleanup 2"))) |
| 369 | * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("cleanup 3"))) |
| 370 | * events.push("work") |
| 371 | * yield* Scope.close(scope, Exit.void) |
| 372 | * }) |
| 373 | * |
| 374 | * Effect.runSync(program) |
| 375 | * events // => ["work", "cleanup 3", "cleanup 2", "cleanup 1"] |
| 376 | * ``` |
| 377 | * |
| 378 | * @category combinators |
| 379 | * @since 2.0.0 |
| 380 | */ |
| 381 | export const addFinalizer: (scope: Scope, finalizer: Effect<unknown>) => Effect<void> = effect.scopeAddFinalizer |
| 382 | |
| 383 | /** |
| 384 | * Creates a closeable child scope registered with a parent scope. |
| 385 | * |
| 386 | * **Details** |
| 387 | * |
| 388 | * Closing the parent closes the child with the same exit value, and closing the |
| 389 | * child detaches it from the parent. The optional finalizer strategy configures |
| 390 | * the child scope and defaults to `"sequential"` when omitted. |
| 391 | * |
| 392 | * **Example** (Creating a child scope) |
| 393 | * |
| 394 | * ```ts import.meta.vitest |
| 395 | * import { Effect, Exit, Scope } from "effect" |
| 396 | * |
| 397 | * const cleanups: Array<string> = [] |
| 398 | * const nestedScopes = Effect.gen(function*() { |
| 399 | * const parentScope = yield* Scope.make("sequential") |
| 400 | * yield* Scope.addFinalizer(parentScope, Effect.sync(() => cleanups.push("parent"))) |
| 401 | * const childScope = yield* Scope.fork(parentScope, "parallel") |
| 402 | * yield* Scope.addFinalizer(childScope, Effect.sync(() => cleanups.push("child"))) |
| 403 | * yield* Scope.close(childScope, Exit.void) |
| 404 | * yield* Scope.close(parentScope, Exit.void) |
| 405 | * }) |
| 406 | * |
| 407 | * Effect.runSync(nestedScopes) |
| 408 | * cleanups // => ["child", "parent"] |
| 409 | * ``` |
| 410 | * |
| 411 | * @category combinators |
| 412 | * @since 2.0.0 |
| 413 | */ |
| 414 | export const fork: ( |
| 415 | scope: Scope, |
| 416 | finalizerStrategy?: "sequential" | "parallel" |
| 417 | ) => Effect<Closeable> = effect.scopeFork |
| 418 | |
| 419 | /** |
| 420 | * Creates a closeable child scope synchronously and registers it with a parent scope. |
| 421 | * |
| 422 | * **When to use** |
| 423 | * |
| 424 | * Use when a child scope must be created synchronously and the caller controls |
| 425 | * both parent and child scope lifetimes. |
| 426 | * |
| 427 | * **Details** |
| 428 | * |
| 429 | * Closing the parent closes the child with the same exit value, and closing the |
| 430 | * child detaches it from the parent. The optional finalizer strategy configures |
| 431 | * the child scope and defaults to `"sequential"` when omitted. |
| 432 | * |
| 433 | * **Example** (Creating a child scope synchronously) |
| 434 | * |
| 435 | * ```ts import.meta.vitest |
| 436 | * import { Effect, Exit, Scope } from "effect" |
| 437 | * |
| 438 | * const cleanups: Array<string> = [] |
| 439 | * const program = Effect.gen(function*() { |
| 440 | * const parentScope = Scope.makeUnsafe("sequential") |
| 441 | * const childScope = Scope.forkUnsafe(parentScope, "parallel") |
| 442 | * yield* Scope.addFinalizer(parentScope, Effect.sync(() => cleanups.push("parent"))) |
| 443 | * yield* Scope.addFinalizer(childScope, Effect.sync(() => cleanups.push("child"))) |
| 444 | * yield* Scope.close(childScope, Exit.void) |
| 445 | * yield* Scope.close(parentScope, Exit.void) |
| 446 | * }) |
| 447 | * |
| 448 | * Effect.runSync(program) |
| 449 | * cleanups // => ["child", "parent"] |
| 450 | * ``` |
| 451 | * |
| 452 | * @category combinators |
| 453 | * @since 4.0.0 |
| 454 | */ |
| 455 | export const forkUnsafe: (scope: Scope, finalizerStrategy?: "sequential" | "parallel") => Closeable = |
| 456 | effect.scopeForkUnsafe |
| 457 | |
| 458 | /** |
| 459 | * Closes a scope and runs its registered finalizers. |
| 460 | * |
| 461 | * **When to use** |
| 462 | * |
| 463 | * Use to close a scope manually with a specific exit value. |
| 464 | * |
| 465 | * **Details** |
| 466 | * |
| 467 | * Finalizers run in the scope's configured order and receive the supplied |
| 468 | * `Exit`. |
| 469 | * By default, finalizers run uninterruptibly, so interrupting the closing fiber |
| 470 | * waits for them to finish; a finalizer can explicitly restore interruptibility. |
| 471 | * |
| 472 | * **Example** (Running scope finalizers) |
| 473 | * |
| 474 | * ```ts import.meta.vitest |
| 475 | * import { Effect, Exit, Scope } from "effect" |
| 476 | * |
| 477 | * const events: Array<string> = [] |
| 478 | * const resourceManagement = Effect.gen(function*() { |
| 479 | * const scope = yield* Scope.make("sequential") |
| 480 | * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("database"))) |
| 481 | * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("file"))) |
| 482 | * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("memory"))) |
| 483 | * events.push("work") |
| 484 | * yield* Scope.close(scope, Exit.succeed("Success!")) |
| 485 | * }) |
| 486 | * |
| 487 | * Effect.runSync(resourceManagement) |
| 488 | * events // => ["work", "memory", "file", "database"] |
| 489 | * ``` |
| 490 | * |
| 491 | * @category combinators |
| 492 | * @since 2.0.0 |
| 493 | */ |
| 494 | export const close: <A, E>(self: Closeable, exit: Exit<A, E>) => Effect<void> = effect.scopeClose |
| 495 | |
| 496 | /** |
| 497 | * Closes a scope unsafely with the provided exit value. |
| 498 | * |
| 499 | * **When to use** |
| 500 | * |
| 501 | * Use when implementing lower-level scope machinery that must transition a |
| 502 | * scope to `Closed` immediately and can run the returned finalizer effect when |
| 503 | * one is produced. |
| 504 | * |
| 505 | * **Details** |
| 506 | * |
| 507 | * Returns an effect that runs registered finalizers, or `undefined` when the |
| 508 | * scope was already closed or no finalizers need to run. |
| 509 | * |
| 510 | * **Gotchas** |
| 511 | * |
| 512 | * Ignoring the returned effect skips registered finalizers. The caller must |
| 513 | * run the returned effect uninterruptibly: the scope is already closed, so |
| 514 | * interruption during finalization can permanently skip remaining finalizers. |
| 515 | * |
| 516 | * @see {@link close} for the usual effectful close operation that always returns an `Effect` |
| 517 | * |
| 518 | * @category unsafe |
| 519 | * @since 4.0.0 |
| 520 | */ |
| 521 | export const closeUnsafe: <A, E>(self: Closeable, exit_: Exit<A, E>) => Effect<void, never, never> | undefined = |
| 522 | effect.scopeCloseUnsafe |
| 523 | |
| 524 | /** |
| 525 | * Runs an effect with the provided closeable scope in its context and closes |
| 526 | * that scope when the effect exits. |
| 527 | * |
| 528 | * **When to use** |
| 529 | * |
| 530 | * Use when you already have a `Closeable` scope and want to run an effect that |
| 531 | * requires `Scope` while automatically closing that scope when the effect exits. |
| 532 | * |
| 533 | * **Details** |
| 534 | * |
| 535 | * The scope is closed with the same exit value as the effect, so registered |
| 536 | * finalizers can observe whether the effect succeeded, failed, or was |
| 537 | * interrupted. |
| 538 | * |
| 539 | * @see `provide` for providing a scope without closing it automatically |
| 540 | * @see `Effect.scoped` for creating and closing a fresh scope around a workflow |
| 541 | * |
| 542 | * @category combinators |
| 543 | * @since 2.0.0 |
| 544 | */ |
| 545 | export const use: { |
| 546 | (scope: Closeable): <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, Exclude<R, Scope>> |
| 547 | <A, E, R>(self: Effect<A, E, R>, scope: Closeable): Effect<A, E, Exclude<R, Scope>> |
| 548 | } = effect.scopeUse |