Effect Курс addFinalizer

addFinalizer

Финализаторы и порядок выполнения.

Что такое финализатор

Финализатор — это эффект, который гарантированно выполняется при закрытии scope. Это аналог finally блока, но с важными преимуществами:

┌──────────────────────────────────────────────────────────────┐
│           try/finally  vs  Effect.addFinalizer               │
├───────────────────────────┬──────────────────────────────────┤
│ try/finally               │ Effect.addFinalizer              │
├───────────────────────────┼──────────────────────────────────┤
│ Привязан к блоку кода     │ Привязан к scope                 │
│ Не знает причину выхода   │ Получает Exit (Success/Failure)  │
│ Не композируется          │ Полностью композируемый          │
│ Вложенность при N ресурсах│ Плоская структура                │
│ Ошибка в finally подавляет│ Ошибки в финализаторах           │
│ основную ошибку           │ собираются в Cause               │
│ Нет типизации             │ Scope в системе типов            │
└───────────────────────────┴──────────────────────────────────┘

Финализатор — это функция типа:

type Finalizer = (exit: Exit<unknown, unknown>) => Effect<void, never, never>

Обратите внимание: финализатор не может упасть (never в канале ошибки) и не имеет зависимостей (never в Requirements). Это гарантирует, что финализатор всегда сможет выполниться.


Effect.addFinalizer vs Scope.addFinalizer

В Effect есть два API для добавления финализаторов:

Effect.addFinalizer (высокоуровневый)

Добавляет финализатор к текущему scope из контекста. Это самый удобный и часто используемый API:


const program = Effect.gen(function* () {
  // Финализатор добавляется к scope, который будет предоставлен
  yield* Effect.addFinalizer((exit) =>
    Console.log(`Cleanup with exit: ${exit._tag}`)
  )
  return "result"
})

// Тип: Effect<string, never, Scope>
// Нужно предоставить Scope через Effect.scoped

Характеристики:

  • Автоматически получает scope из контекста
  • Требует Scope в Requirements
  • Финализатор получает Exit

Scope.addFinalizer (низкоуровневый)

Добавляет финализатор к конкретному scope объекту:


const program = Effect.gen(function* () {
  const scope = yield* Scope.make()

  // Явно указываем, к какому scope привязать финализатор
  yield* Scope.addFinalizer(scope, Console.log("Cleanup"))
})

// Тип: Effect<void, never, never>
// НЕ требует Scope в Requirements

Характеристики:

  • Требует явную ссылку на scope
  • Не добавляет Scope в Requirements
  • Финализатор НЕ получает Exit (просто эффект очистки)

Когда что использовать

┌────────────────────────────────────────────────────────────┐
│                                                            │
│  Effect.addFinalizer        Scope.addFinalizer             │
│  ┌──────────────────────┐   ┌───────────────────────────┐  │
│  │ 95% случаев          │   │ Ручное управление scope   │  │
│  │ Effect.scoped        │   │ Множественные scope       │  │
│  │ Layer.scoped         │   │ Scope.extend              │  │
│  │ acquireRelease       │   │ Тонкий контроль           │  │
│  └──────────────────────┘   └───────────────────────────┘  │
│                                                            │
└────────────────────────────────────────────────────────────┘

Exit в финализаторе

Финализатор, добавленный через Effect.addFinalizer, получает Exit — значение, описывающее результат выполнения scope:


const smartFinalizer = (exit: Exit.Exit<unknown, unknown>) => {
  // Паттерн: разные действия в зависимости от причины закрытия

  if (Exit.isSuccess(exit)) {
    return Console.log("✅ Graceful shutdown")
  }

  // Exit.isFailure — может быть Fail, Die или Interrupt
  const cause = exit.cause

  if (Cause.isFailType(cause)) {
    return Console.log(`❌ Failed: ${cause.error}`)
  }

  if (Cause.isDieType(cause)) {
    return Console.log(`💀 Defect: ${cause.defect}`)
  }

  if (Cause.isInterruptType(cause)) {
    return Console.log(`⚡ Interrupted by fiber: ${cause.fiberId}`)
  }

  // Composite causes (Sequential, Parallel)
  return Console.log(`🔴 Complex failure: ${Cause.pretty(cause)}`)
}

Три сценария Exit

  ┌─────────────────────────────────────────────────────────┐
  │                                                         │
  │  Success                                                │
  │  ┌──────────────────────────────────────────────┐       │
  │  │ exit._tag === "Success"                      │       │
  │  │ exit.value — результат выполнения            │       │
  │  │ Сценарий: нормальное завершение работы       │       │
  │  └──────────────────────────────────────────────┘       │
  │                                                         │
  │  Failure (Fail)                                         │
  │  ┌──────────────────────────────────────────────┐       │
  │  │ exit._tag === "Failure"                      │       │
  │  │ exit.cause._tag === "Fail"                   │       │
  │  │ Сценарий: ожидаемая ошибка                   │       │
  │  └──────────────────────────────────────────────┘       │
  │                                                         │
  │  Failure (Interrupt)                                    │
  │  ┌──────────────────────────────────────────────┐       │
  │  │ exit._tag === "Failure"                      │       │
  │  │ exit.cause._tag === "Interrupt"              │       │
  │  │ Сценарий: файбер был прерван                 │       │
  │  └──────────────────────────────────────────────┘       │
  │                                                         │
  └─────────────────────────────────────────────────────────┘

Примеры с разными Exit


const withFinalizer = (label: string) =>
  Effect.addFinalizer((exit) =>
    Console.log(`[${label}] Exit: ${exit._tag}`)
  )

// Сценарий 1: Success
const success = Effect.scoped(
  Effect.gen(function* () {
    yield* withFinalizer("Success test")
    return "ok"
  })
)
// Output: [Success test] Exit: Success

// Сценарий 2: Failure
const failure = Effect.scoped(
  Effect.gen(function* () {
    yield* withFinalizer("Failure test")
    return yield* Effect.fail("boom")
  })
)
// Output: [Failure test] Exit: Failure

// Сценарий 3: Interrupt
const interrupt = Effect.scoped(
  Effect.gen(function* () {
    yield* withFinalizer("Interrupt test")
    return yield* Effect.interrupt
  })
)
// Output: [Interrupt test] Exit: Failure (cause: Interrupt)

Порядок выполнения финализаторов

LIFO-порядок (стек)

Финализаторы выполняются в порядке, обратном добавлению — как стек (Last In, First Out):


const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() => Console.log("Finalizer A"))  // 1st added
    yield* Effect.addFinalizer(() => Console.log("Finalizer B"))  // 2nd added
    yield* Effect.addFinalizer(() => Console.log("Finalizer C"))  // 3rd added
    yield* Console.log("Work done")
  })
)

Effect.runPromise(program)
/*
  Work done
  Finalizer C   ← 3rd added, 1st executed
  Finalizer B   ← 2nd added, 2nd executed
  Finalizer A   ← 1st added, 3rd executed
*/

Почему LIFO?

LIFO-порядок не случаен — он решает фундаментальную проблему зависимых ресурсов:

  Порядок acquire (FIFO):          Порядок release (LIFO):
  1. TCP Connection                3. TCP Connection
  2. TLS Handshake (needs TCP)     2. TLS Handshake
  3. HTTP Session (needs TLS)      1. HTTP Session

  ┌────────────┐
  │ TCP Conn   │ ← Открыт первым
  ├────────────┤
  │ TLS Layer  │ ← Зависит от TCP
  ├────────────┤
  │ HTTP Sess  │ ← Зависит от TLS, закрыт первым
  └────────────┘

Если закрыть TCP-соединение до HTTP-сессии — сессия “повиснет” и операции закрытия могут таймаутнуть или выбросить ошибки.

LIFO с acquireRelease

Тот же принцип работает с acquireRelease:


const makeResource = (name: string) =>
  Effect.acquireRelease(
    Console.log(`Acquire: ${name}`).pipe(Effect.as(name)),
    (n) => Console.log(`Release: ${n}`)
  )

const program = Effect.scoped(
  Effect.gen(function* () {
    const tcp = yield* makeResource("TCP")
    const tls = yield* makeResource("TLS")
    const http = yield* makeResource("HTTP")
    yield* Console.log(`Using: ${tcp}, ${tls}, ${http}`)
  })
)

Effect.runPromise(program)
/*
  Acquire: TCP
  Acquire: TLS
  Acquire: HTTP
  Using: TCP, TLS, HTTP
  Release: HTTP    ← LIFO: HTTP закрыт первым
  Release: TLS
  Release: TCP     ← TCP закрыт последним
*/

Финализаторы из разных задач в одном scope

Когда несколько задач добавляют финализаторы в один scope, порядок определяется моментом добавления:


const task1 = Effect.gen(function* () {
  yield* Console.log("Task 1: start")
  yield* Effect.addFinalizer(() => Console.log("Task 1: cleanup"))
  yield* Console.log("Task 1: end")
})

const task2 = Effect.gen(function* () {
  yield* Console.log("Task 2: start")
  yield* Effect.addFinalizer(() => Console.log("Task 2: cleanup"))
  yield* Console.log("Task 2: end")
})

const program = Effect.scoped(
  Effect.gen(function* () {
    yield* task1
    yield* task2
  })
)

Effect.runPromise(program)
/*
  Task 1: start
  Task 1: end
  Task 2: start
  Task 2: end
  Task 2: cleanup   ← task2 добавил позже — выполнен первым
  Task 1: cleanup
*/

Финализаторы и ошибки

Финализаторы выполняются при ошибке

Все финализаторы гарантированно выполняются, даже если основной эффект упал с ошибкой:


const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() => Console.log("Finalizer A"))
    yield* Effect.addFinalizer(() => Console.log("Finalizer B"))

    yield* Console.log("Starting work...")
    yield* Effect.fail("Critical error")
    // Код ниже не выполнится, но ОБА финализатора выполнятся
    yield* Console.log("This never runs")
  })
)

Effect.runPromiseExit(program).then(console.log)
/*
  Starting work...
  Finalizer B
  Finalizer A
  { _tag: 'Failure', cause: { _tag: 'Fail', failure: 'Critical error' } }
*/

Ошибка между двумя acquireRelease

Если ошибка происходит между двумя acquire, освобождается только первый ресурс:


const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.acquireRelease(
      Console.log("Acquire A").pipe(Effect.as("A")),
      (a) => Console.log(`Release ${a}`)
    )

    yield* Effect.fail("Error between acquires")

    // Этот acquire никогда не выполнится
    yield* Effect.acquireRelease(
      Console.log("Acquire B").pipe(Effect.as("B")),
      (b) => Console.log(`Release ${b}`)
    )
  })
)

Effect.runPromiseExit(program).then(console.log)
/*
  Acquire A
  Release A          ← Только A был acquire, только A release
  { _tag: 'Failure', ... }
*/

Ошибки внутри финализаторов

Финализатор не должен выбрасывать ошибки (тип Effect<void, never, never>). Но если используется Effect.die или возникает дефект, Effect соберёт его в общий Cause:


const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() =>
      Console.log("Good finalizer")
    )
    yield* Effect.addFinalizer(() =>
      Effect.gen(function* () {
        yield* Console.log("Problematic finalizer - before die")
        yield* Effect.die("Finalizer defect!")
      })
    )
    yield* Effect.addFinalizer(() =>
      Console.log("Another good finalizer")
    )
  })
)

// Все три финализатора попытаются выполниться
// Дефект в одном не блокирует остальные
Effect.runPromiseExit(program).then(console.log)

Финализаторы и прерывание

Финализаторы также выполняются при прерывании (interrupt) файбера:


const longRunningTask = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer((exit) =>
      Console.log(`Task finalized. Exit: ${exit._tag}`)
    )
    yield* Console.log("Task started, sleeping...")
    yield* Effect.sleep("10 seconds")
    yield* Console.log("Task completed") // Не выполнится при прерывании
  })
)

const program = Effect.gen(function* () {
  const fiber = yield* Effect.fork(longRunningTask)
  yield* Effect.sleep("1 second")
  yield* Console.log("Interrupting task...")
  yield* Fiber.interrupt(fiber)
  yield* Console.log("Task interrupted")
})

Effect.runPromise(program)
/*
  Task started, sleeping...
  Interrupting task...
  Task finalized. Exit: Failure   ← Interrupt = Failure exit
  Task interrupted
*/

Uninterruptible финализаторы

По умолчанию финализаторы выполняются в uninterruptible-режиме — их нельзя прервать. Это гарантирует, что очистка завершится полностью:


const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() =>
      Effect.gen(function* () {
        yield* Console.log("Finalizer: starting cleanup...")
        // Даже если файбер прерван, этот sleep выполнится полностью
        yield* Effect.sleep("2 seconds")
        yield* Console.log("Finalizer: cleanup complete")
      })
    )
    return yield* Effect.interrupt
  })
)

API Reference

Effect.addFinalizer [STABLE]


// Добавляет финализатор к текущему scope из контекста
Effect.addFinalizer<R>(
  finalizer: (exit: Exit<unknown, unknown>) => Effect<void, never, R>
): Effect<void, never, R | Scope>

💡 На практике R в финализаторе обычно never, так как финализатор не должен иметь зависимостей.

Scope.addFinalizer [STABLE]


// Добавляет финализатор к конкретному scope
Scope.addFinalizer(
  scope: Scope.Scope,
  finalizer: Effect<void, never, never>
): Effect<void, never, never>

Effect.onExit [STABLE]

Альтернатива addFinalizer, которая не привязана к scope:


// Выполняет эффект при любом завершении текущего эффекта
const program = Effect.succeed(42).pipe(
  Effect.onExit((exit) =>
    Console.log(`Exited with: ${exit._tag}`)
  )
)

// НЕ требует Scope! Работает inline
Effect.runPromise(program)
// Exited with: Success

Effect.ensuring [STABLE]

Упрощённый вариант — не получает Exit:


const program = Effect.succeed(42).pipe(
  Effect.ensuring(Console.log("Always runs"))
)

Паттерны использования

Паттерн 1: Логирование lifecycle


const withLifecycleLogging = (name: string) =>
  Effect.gen(function* () {
    yield* Console.log(`[${name}] Started`)
    yield* Effect.addFinalizer((exit) =>
      Console.log(`[${name}] Stopped (${exit._tag})`)
    )
  })

const program = Effect.scoped(
  Effect.gen(function* () {
    yield* withLifecycleLogging("DatabasePool")
    yield* withLifecycleLogging("HttpServer")
    yield* withLifecycleLogging("MetricsExporter")
    yield* Console.log("App running...")
  })
)

Паттерн 2: Cleanup с таймаутом


const withTimedCleanup = (
  name: string,
  cleanup: Effect.Effect<void>,
  timeout: Duration.DurationInput
) =>
  Effect.addFinalizer(() =>
    cleanup.pipe(
      Effect.timeout(timeout),
      Effect.catchAll(() =>
        Console.log(`[${name}] Cleanup timed out, forcing...`)
      )
    )
  )

Паттерн 3: Conditional cleanup


const conditionalCleanup = (resourceName: string) =>
  Effect.addFinalizer((exit) => {
    if (Exit.isSuccess(exit)) {
      return Console.log(`[${resourceName}] Normal shutdown`)
    }

    const cause = exit.cause
    if (Cause.isInterruptedOnly(cause)) {
      return Console.log(`[${resourceName}] Interrupted - saving state...`)
    }

    return Console.log(`[${resourceName}] Error - attempting recovery...`)
  })

Паттерн 4: Отмена регистрации


interface ServiceRegistry {
  readonly register: (name: string) => Effect.Effect<void>
  readonly deregister: (name: string) => Effect.Effect<void>
}

const registerWithCleanup = (
  registry: ServiceRegistry,
  serviceName: string
) =>
  Effect.gen(function* () {
    yield* registry.register(serviceName)
    yield* Console.log(`Registered: ${serviceName}`)
    yield* Effect.addFinalizer(() =>
      Effect.gen(function* () {
        yield* registry.deregister(serviceName)
        yield* Console.log(`Deregistered: ${serviceName}`)
      })
    )
  })

Примеры

Пример 1: Полный lifecycle с метриками


const program = Effect.scoped(
  Effect.gen(function* () {
    const startTime = Date.now()

    // Финализатор: логирование метрик при завершении
    yield* Effect.addFinalizer((exit) =>
      Effect.sync(() => {
        const duration = Date.now() - startTime
        const status = exit._tag === "Success" ? "OK" : "ERROR"
        console.log(
          `[Metrics] Duration: ${duration}ms, Status: ${status}`
        )
      })
    )

    // Финализатор: flush буферов
    yield* Effect.addFinalizer(() =>
      Console.log("[Buffer] Flushing remaining data...")
    )

    // Основная работа
    yield* Console.log("Processing batch...")
    yield* Effect.sleep("100 millis")
    yield* Console.log("Batch processed")
    return 42
  })
)

Effect.runPromise(program).then(console.log)
/*
  Processing batch...
  Batch processed
  [Buffer] Flushing remaining data...
  [Metrics] Duration: ~100ms, Status: OK
  42
*/

Пример 2: Цепочка финализаторов для микросервиса


const bootstrapMicroservice = Effect.scoped(
  Effect.gen(function* () {
    // 1. Инициализация (порядок: сверху вниз)
    yield* Console.log("1. Config loaded")
    yield* Effect.addFinalizer(() =>
      Console.log("6. Config cleanup")
    )

    yield* Console.log("2. Database connected")
    yield* Effect.addFinalizer(() =>
      Console.log("5. Database disconnected")
    )

    yield* Console.log("3. Cache warmed up")
    yield* Effect.addFinalizer(() =>
      Console.log("4. Cache flushed")
    )

    yield* Console.log("--- Microservice running ---")
  })
)

Effect.runPromise(bootstrapMicroservice)
/*
  1. Config loaded
  2. Database connected
  3. Cache warmed up
  --- Microservice running ---
  4. Cache flushed           ← Shutdown в LIFO
  5. Database disconnected
  6. Config cleanup
*/

Пример 3: onExit vs addFinalizer


// onExit — не требует Scope, работает inline
const withOnExit = Effect.gen(function* () {
  const result = yield* Effect.succeed(42).pipe(
    Effect.onExit((exit) =>
      Console.log(`onExit: ${exit._tag}`)
    )
  )
  yield* Console.log(`Result: ${result}`)
  return result
})

// addFinalizer — требует Scope, выполняется при закрытии scope
const withAddFinalizer = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer((exit) =>
      Console.log(`addFinalizer: ${exit._tag}`)
    )
    const result = 42
    yield* Console.log(`Result: ${result}`)
    return result
  })
)

Effect.runPromise(withOnExit)
// onExit: Success
// Result: 42

Effect.runPromise(withAddFinalizer)
// Result: 42
// addFinalizer: Success   ← Выполняется ПОСЛЕ возврата результата

Распространённые ошибки

Ошибка 1: Финализатор с зависимостями


class Logger extends Context.Tag("Logger")<Logger, {
  readonly log: (msg: string) => Effect.Effect<void>
}>() {}

// ⚠️ Финализатор зависит от Logger — это может создать проблемы,
// если Logger уже был освобождён к моменту вызова финализатора
const risky = Effect.gen(function* () {
  const logger = yield* Logger
  yield* Effect.addFinalizer(() =>
    // Это работает, если Logger доступен при закрытии scope
    logger.log("Cleanup")
  )
})

// ✅ Безопаснее: захватить нужные данные до финализатора
const safe = Effect.gen(function* () {
  const logger = yield* Logger
  // Захватываем ссылку на метод
  const log = logger.log
  yield* Effect.addFinalizer(() => log("Cleanup"))
})

Ошибка 2: Долгие операции в финализаторе без таймаута


// ❌ Финализатор может зависнуть навсегда
const bad = Effect.addFinalizer(() =>
  Effect.promise(() =>
    fetch("https://api.example.com/deregister")
      .then(() => undefined)
  )
)

// ✅ Всегда ставьте таймаут на сетевые операции в финализаторах
const good = Effect.addFinalizer(() =>
  Effect.promise(() =>
    fetch("https://api.example.com/deregister")
      .then(() => undefined)
  ).pipe(
    Effect.timeout("5 seconds"),
    Effect.catchAll(() => Console.log("Deregister timed out, skipping"))
  )
)

Ошибка 3: Порядок финализаторов не соответствует зависимостям


// ❌ Неправильный порядок: network закрыт до session
const bad = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() => Console.log("Close session"))
    yield* Effect.addFinalizer(() => Console.log("Close network"))
    // LIFO: network закроется первым, но session от него зависит!
  })
)

// ✅ Правильный порядок: сначала acquire зависимость
const good = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() => Console.log("Close network"))
    yield* Effect.addFinalizer(() => Console.log("Close session"))
    // LIFO: session закроется первым (правильно!)
  })
)

Упражнения

Упражнение

Упражнение 1: Три финализатора с Exit

Легко

Создайте эффект с тремя финализаторами, каждый из которых логирует свой номер и тег Exit. Запустите для Success и Failure.

import { Effect, Console } from "effect"

const makeProgram = (shouldFail: boolean) =>
  Effect.scoped(
    Effect.gen(function* () {
      yield* Effect.addFinalizer((exit) =>
        Console.log(`Finalizer 1: ${exit._tag}`)
      )
      yield* Effect.addFinalizer((exit) =>
        Console.log(`Finalizer 2: ${exit._tag}`)
      )
      yield* Effect.addFinalizer((exit) =>
        Console.log(`Finalizer 3: ${exit._tag}`)
      )

      if (shouldFail) return yield* Effect.fail("error")
      return "ok"
    })
  )

// Success
Effect.runPromiseExit(makeProgram(false)).then(console.log)
// Finalizer 3: Success
// Finalizer 2: Success
// Finalizer 1: Success

// Failure
Effect.runPromiseExit(makeProgram(true)).then(console.log)
// Finalizer 3: Failure
// Finalizer 2: Failure
// Finalizer 1: Failure
Упражнение

Упражнение 2: Lifecycle logger utility

Средне

Создайте утилитную функцию withLifecycle, которая оборачивает любой эффект, логируя начало выполнения, результат и время работы.

import { Effect, Console } from "effect"

const withLifecycle = <A, E, R>(
  name: string,
  effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, R> =>
  Effect.gen(function* () {
    const start = Date.now()
    yield* Console.log(`[${name}] Starting...`)

    const result = yield* effect.pipe(
      Effect.onExit((exit) =>
        Effect.sync(() => {
          const duration = Date.now() - start
          console.log(
            `[${name}] Completed in ${duration}ms ` +
            `(${exit._tag})`
          )
        })
      )
    )

    return result
  })

// Использование
const task = Effect.gen(function* () {
  yield* Effect.sleep("50 millis")
  return 42
})

Effect.runPromise(withLifecycle("MyTask", task))
// [MyTask] Starting...
// [MyTask] Completed in ~50ms (Success)
Упражнение

Упражнение 3: Ordered resource manager

Сложно

Создайте менеджер ресурсов, который отслеживает все созданные ресурсы и при закрытии scope выводит полный отчёт о порядке создания и закрытия ресурсов.

import { Effect, Console, Ref } from "effect"

interface ResourceEvent {
  readonly name: string
  readonly action: "acquired" | "released"
  readonly timestamp: number
}

const createResourceManager = Effect.gen(function* () {
  const events = yield* Ref.make<ReadonlyArray<ResourceEvent>>([])

  const track = (name: string) =>
    Effect.acquireRelease(
      Effect.gen(function* () {
        yield* Ref.update(events, (prev) => [
          ...prev,
          { name, action: "acquired" as const, timestamp: Date.now() }
        ])
        yield* Console.log(`+ Acquired: ${name}`)
        return name
      }),
      (n) =>
        Effect.gen(function* () {
          yield* Ref.update(events, (prev) => [
            ...prev,
            { name: n, action: "released" as const, timestamp: Date.now() }
          ])
          yield* Console.log(`- Released: ${n}`)
        })
    )

  // Финализатор для вывода отчёта (выполнится последним в LIFO)
  yield* Effect.addFinalizer(() =>
    Effect.gen(function* () {
      const log = yield* Ref.get(events)
      yield* Console.log("\n=== Resource Lifecycle Report ===")
      log.forEach((e) =>
        console.log(`  ${e.action === "acquired" ? "+" : "-"} ${e.name}`)
      )
      yield* Console.log("=================================")
    })
  )

  return { track }
})

const program = Effect.scoped(
  Effect.gen(function* () {
    const rm = yield* createResourceManager
    yield* rm.track("Config")
    yield* rm.track("Database")
    yield* rm.track("Cache")
    yield* Console.log("\nAll resources active\n")
  })
)

Effect.runPromise(program)
/*
  + Acquired: Config
  + Acquired: Database
  + Acquired: Cache

  All resources active

  - Released: Cache
  - Released: Database
  - Released: Config

  === Resource Lifecycle Report ===
    + Config
    + Database
    + Cache
    - Cache
    - Database
    - Config
  =================================
*/