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: Failureimport { 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)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
=================================
*/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
=================================
*/