Scope.extend
Тонкое управление временем жизни ресурсов.
Мотивация: ограничения автоматического слияния scope
По умолчанию, когда несколько scoped-ресурсов используются в одном Effect.scoped, их scope сливаются — все финализаторы выполняются при закрытии общего scope:
const program = Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("Release A"))
yield* Console.log("Using resource A")
yield* Effect.addFinalizer(() => Console.log("Release B"))
yield* Console.log("Using resource B")
// Ресурс A живёт, даже когда уже не нужен
})
)
// Release B, Release A — оба при выходе из scoped
Это поведение подходит для большинства случаев, но иногда нужно:
┌────────────────────────────────────────────────────────────┐
│ Когда автоматическое слияние scope недостаточно: │
├────────────────────────────────────────────────────────────┤
│ 1. Ресурс нужен только на часть выполнения │
│ (закрыть DB connection после миграции, но до запуска │
│ HTTP сервера) │
│ │
│ 2. Файбер должен пережить родительский файбер │
│ (background worker, который живёт пока жив scope) │
│ │
│ 3. Разные ресурсы должны иметь разное время жизни │
│ (кеш живёт 5 минут, соединение — до конца запроса) │
│ │
│ 4. Файбер должен быть привязан к конкретному scope │
│ (не к родителю, не к daemon, а к определённому scope) │
└────────────────────────────────────────────────────────────┘
Scope.extend
Что делает Scope.extend
Scope.extend позволяет привязать scoped-эффект к конкретному scope вместо текущего. Эффект выполняется, но его финализаторы регистрируются в указанном scope, а не в текущем:
Без Scope.extend: С Scope.extend:
┌─────────────────────────┐ ┌─────────────────────────┐
│ Current Scope │ │ Current Scope │
│ ┌────────────────────┐ │ │ │
│ │ task (finalizer │ │ │ task (выполняется │
│ │ регистрируется │ │ │ здесь, но finalizer │
│ │ здесь) │ │ │ → target scope) │
│ └────────────────────┘ │ │ │
└─────────────────────────┘ └─────────────────────────┘
┌─────────────────────────┐
│ Target Scope │
│ ┌────────────────────┐ │
│ │ finalizer │ │
│ │ регистрируется │ │
│ │ здесь │ │
│ └────────────────────┘ │
└─────────────────────────┘
Сигнатура
// Scope.extend привязывает scoped-эффект к указанному scope
// Scope.extend: (scope: Scope) => <A, E, R>(
// effect: Effect<A, E, R | Scope>
// ) => Effect<A, E, R>
Базовый пример
const task = Effect.gen(function* () {
yield* Console.log("Task executing")
yield* Effect.addFinalizer(() => Console.log("Task finalizer"))
})
const program = Effect.gen(function* () {
const myScope = yield* Scope.make()
// Выполняем task, но финализатор уходит в myScope
yield* task.pipe(Scope.extend(myScope))
yield* Console.log("Task done, but finalizer not yet run")
// Финализатор выполнится только при закрытии myScope
yield* Scope.close(myScope, Exit.void)
yield* Console.log("Now finalizer has run")
})
Effect.runPromise(program)
/*
Task executing
Task done, but finalizer not yet run
Task finalizer
Now finalizer has run
*/
Ручное создание и закрытие scope
Полный контроль над временем жизни
С помощью Scope.make(), Scope.extend и Scope.close вы получаете полный контроль:
const task1 = Effect.gen(function* () {
yield* Console.log("Task 1: started")
yield* Effect.addFinalizer(() => Console.log("Task 1: finalizer"))
})
const task2 = Effect.gen(function* () {
yield* Console.log("Task 2: started")
yield* Effect.addFinalizer(() => Console.log("Task 2: finalizer"))
})
const program = Effect.gen(function* () {
// Создаём два отдельных scope
const scope1 = yield* Scope.make()
const scope2 = yield* Scope.make()
// Привязываем каждую задачу к своему scope
yield* task1.pipe(Scope.extend(scope1))
yield* task2.pipe(Scope.extend(scope2))
// Закрываем scope1 — task1 finalizer выполняется
yield* Scope.close(scope1, Exit.void)
yield* Console.log("--- Between closes ---")
// Закрываем scope2 — task2 finalizer выполняется
yield* Scope.close(scope2, Exit.void)
})
Effect.runPromise(program)
/*
Task 1: started
Task 2: started
Task 1: finalizer
--- Between closes ---
Task 2: finalizer
*/
Сравнение с merged scope
Merged (по умолчанию): Separate scopes:
┌───────────────────────────┐ ┌──────────────────────────┐
│ Shared Scope │ │ scope1 │
│ │ │ ┌────────┐ │
│ Task 1 ──fin──┐ │ │ │ Task 1 │──fin──┐ │
│ │ │ │ └────────┘ │ │
│ Task 2 ──fin──┤ │ │ close(scope1) │
│ │ │ │ ↓ Task 1 fin │
│ ▼ │ └──────────────────────────┘
│ close │ ┌──────────────────────────┐
│ ↓ Task 2 fin │ │ scope2 │
│ ↓ Task 1 fin │ │ ┌────────┐ │
└───────────────────────────┘ │ │ Task 2 │──fin──┐ │
│ └────────┘ │ │
│ close(scope2) │
│ ↓ Task 2 fin │
└──────────────────────────┘
Передача Exit при закрытии
Scope.close принимает Exit, который передаётся всем финализаторам в этом scope:
const task = Effect.gen(function* () {
yield* Effect.addFinalizer((exit) =>
Console.log(`Finalizer: exit = ${exit._tag}`)
)
})
const program = Effect.gen(function* () {
const scope = yield* Scope.make()
yield* task.pipe(Scope.extend(scope))
// Закрываем с ошибкой — финализатор получит Failure
yield* Scope.close(scope, Exit.fail("manual error"))
})
Effect.runPromise(program)
// Finalizer: exit = Failure
Effect.forkScoped
Проблема: время жизни файберов
В стандартной модели конкурентности Effect дочерний файбер привязан к родительскому:
Parent fiber
┌─────────────────────────────────┐
│ fork(child) ─── child fiber │
│ │ │
│ parent ends ─────┤ │
│ ▼ │
│ child interrupted │ ← Структурная конкурентность
└─────────────────────────────────┘
Иногда нужно, чтобы файбер пережил родителя и был привязан к scope:
Scope
┌──────────────────────────────────────────┐
│ │
│ Parent fiber │
│ ┌────────────────────────────┐ │
│ │ forkScoped(child) │ │
│ │ parent ends │ │
│ └────────────────────────────┘ │
│ │
│ Child fiber (всё ещё работает!) │
│ ┌────────────────────────────┐ │
│ │ ... │ │
│ └────────────────────────────┘ │
│ │
│ scope.close() → child interrupted │
└──────────────────────────────────────────┘
Определение
Effect.forkScoped создаёт файбер, привязанный к текущему scope. Файбер будет прерван при закрытии scope, а не при завершении родительского файбера:
// Child fiber, который повторяется каждую секунду
const child = Effect.repeat(
Console.log("child: still running!"),
Schedule.fixed("1 second")
)
// ┌─── Требует Scope
// ▼
const parent = Effect.gen(function* () {
yield* Console.log("parent: started!")
// Файбер привязан к scope, а не к parent
yield* Effect.forkScoped(child)
yield* Effect.sleep("3 seconds")
yield* Console.log("parent: finished!")
})
// Scope живёт 5 секунд
const program = Effect.scoped(
Effect.gen(function* () {
yield* Console.log("Scope started!")
yield* Effect.fork(parent) // parent завершится через 3 сек
yield* Effect.sleep("5 seconds") // scope живёт 5 сек
yield* Console.log("Closing scope!")
})
)
Effect.runFork(program)
/*
Scope started!
parent: started!
child: still running!
child: still running!
child: still running!
parent: finished! ← parent завершился на 3 сек
child: still running! ← child продолжает работать!
child: still running! ← child привязан к scope, не к parent
Closing scope! ← scope закрывается на 5 сек
← child прерывается вместе со scope
*/
forkScoped vs fork vs forkDaemon
┌──────────────────┬──────────────────────────────────────────┐
│ Оператор │ Время жизни файбера │
├──────────────────┼──────────────────────────────────────────┤
│ Effect.fork │ Привязан к РОДИТЕЛЬСКОМУ файберу │
│ │ Прерывается когда parent завершается │
│ │ (структурная конкурентность) │
├──────────────────┼──────────────────────────────────────────┤
│ Effect.forkScoped│ Привязан к SCOPE │
│ │ Переживает parent, прерывается при │
│ │ закрытии scope │
│ │ Требует Scope в Requirements │
├──────────────────┼──────────────────────────────────────────┤
│ Effect.forkDaemon│ ПОЛНОСТЬЮ независимый │
│ │ Не привязан ни к parent, ни к scope │
│ │ Живёт пока работает runtime │
│ │ ⚠️ Используйте с осторожностью │
└──────────────────┴──────────────────────────────────────────┘
Визуально:
Runtime
┌──────────────────────────────────────────────────────┐
│ │
│ Scope │
│ ┌──────────────────────────────────────────────┐ │
│ │ │ │
│ │ Parent fiber │ │
│ │ ┌────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ fork(A) → A живёт с parent │ │ │
│ │ │ forkScoped(B)→ B живёт со scope │ │ │
│ │ │ forkDaemon(C)→ C живёт с runtime │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────┘ │ │
│ │ │ │
│ │ B ← всё ещё работает после parent │ │
│ │ │ │
│ └── scope.close() → B прерывается ─────────────┘ │
│ │
│ C ← всё ещё работает после scope! │
│ │
└──────────────────────────────────────────────────────┘
Effect.forkIn
Определение
Effect.forkIn — более гибкий вариант forkScoped, позволяющий указать конкретный scope, к которому привязать файбер:
const child = Effect.repeat(
Console.log("child: running!"),
Schedule.fixed("1 second")
)
const program = Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Console.log("Outer scope closing!")
)
// Захватываем текущий scope
const outerScope = yield* Effect.scope
// Создаём внутренний scope
yield* Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Console.log("Inner scope closing!")
)
// Форкаем child в OUTER scope (переживёт inner scope)
yield* Effect.forkIn(child, outerScope)
yield* Effect.sleep("3 seconds")
})
)
// Inner scope закрыт, но child продолжает работать в outer scope
yield* Console.log("Between scopes")
yield* Effect.sleep("2 seconds")
// Outer scope закрывается → child прерывается
})
)
Effect.runFork(program)
/*
child: running!
child: running!
child: running!
Inner scope closing! ← inner scope закрыт
Between scopes
child: running! ← child всё ещё работает!
child: running!
Outer scope closing! ← outer scope закрыт → child прерван
*/
forkScoped vs forkIn
┌──────────────────────────────────────────────────────┐
│ │
│ forkScoped forkIn(effect, scope) │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Привязка к │ │ Привязка к ЛЮБОМУ │ │
│ │ ТЕКУЩЕМУ scope │ │ scope (явно указан) │ │
│ │ из контекста │ │ │ │
│ │ │ │ Не требует Scope │ │
│ │ Требует Scope │ │ в Requirements │ │
│ │ в Requirements │ │ │ │
│ └──────────────────┘ └──────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
Закрытие scope с незавершёнными задачами
Важный нюанс: закрытие scope не прерывает задачи, расширенные в этот scope через Scope.extend. Но задачи, форкнутые через forkScoped или forkIn, прерываются при закрытии scope.
Scope.extend — задача продолжает выполняться
const task = Effect.gen(function* () {
yield* Effect.sleep("1 second")
yield* Console.log("Task completed")
yield* Effect.addFinalizer(() => Console.log("Task finalizer"))
})
const program = Effect.gen(function* () {
const scope = yield* Scope.make()
// Закрываем scope СРАЗУ
yield* Scope.close(scope, Exit.void)
yield* Console.log("Scope closed")
// Задача всё равно выполнится (Scope.extend не форкает)
yield* task.pipe(Scope.extend(scope))
})
Effect.runPromise(program)
/*
Scope closed
Task completed ← задача выполнилась после закрытия scope
Task finalizer
*/
⚠️ Это поведение может быть неинтуитивным! Scope.extend только перенаправляет, куда регистрируются финализаторы, но не управляет выполнением задачи.
forkScoped — файбер прерывается
const worker = Effect.repeat(
Console.log("Worker tick"),
Schedule.fixed("500 millis")
)
const program = Effect.scoped(
Effect.gen(function* () {
yield* Effect.forkScoped(worker)
yield* Effect.sleep("2 seconds")
yield* Console.log("Scope closing...")
// При закрытии scope worker будет прерван
})
)
Effect.runFork(program)
/*
Worker tick
Worker tick
Worker tick
Worker tick
Scope closing...
← worker прерван
*/
API Reference
Scope.extend [STABLE]
// Привязывает scoped-эффект к указанному scope
Scope.extend(
scope: Scope.Scope
): <A, E, R>(
effect: Effect<A, E, R | Scope>
) => Effect<A, E, Exclude<R, Scope>>
Используется с pipe:
yield* myEffect.pipe(Scope.extend(targetScope))
Scope.make [STABLE]
// Создаёт новый Scope.Closeable
Scope.make(): Effect<Scope.Closeable, never, never>
Scope.close [STABLE]
// Закрывает scope, выполняя все финализаторы
Scope.close(
scope: Scope.Closeable,
exit: Exit<unknown, unknown>
): Effect<void, never, never>
Effect.forkScoped [STABLE]
// Форкает файбер, привязанный к текущему scope
Effect.forkScoped<A, E, R>(
effect: Effect<A, E, R>
): Effect<Fiber.RuntimeFiber<A, E>, never, R | Scope>
Effect.forkIn [STABLE]
// Форкает файбер, привязанный к указанному scope
Effect.forkIn(
effect: Effect<A, E, R>,
scope: Scope.Scope
): Effect<Fiber.RuntimeFiber<A, E>, never, R>
Effect.scope [STABLE]
// Получает текущий scope из контекста
Effect.scope: Effect<Scope.Scope, never, Scope.Scope>
Паттерны использования
Паттерн 1: Раннее освобождение ресурса
Ресурс нужен только для определённой фазы работы:
const program = Effect.gen(function* () {
// Фаза 1: Миграция базы (нужен migration client)
const migrationScope = yield* Scope.make()
const migrationClient = Effect.gen(function* () {
yield* Console.log("[Migration] Client created")
yield* Effect.addFinalizer(() =>
Console.log("[Migration] Client closed")
)
return { migrate: Effect.succeed("migrated") }
})
const client = yield* migrationClient.pipe(Scope.extend(migrationScope))
yield* client.migrate
yield* Console.log("[Migration] Complete")
// Закрываем migration client — он больше не нужен
yield* Scope.close(migrationScope, Exit.void)
// Фаза 2: Запуск HTTP-сервера (migration client уже освобождён)
yield* Console.log("[Server] Starting (migration resources freed)")
yield* Effect.sleep("1 second")
yield* Console.log("[Server] Running")
})
Effect.runPromise(program)
/*
[Migration] Client created
[Migration] Complete
[Migration] Client closed ← закрыт до запуска сервера
[Server] Starting (migration resources freed)
[Server] Running
*/
Паттерн 2: Background worker с forkScoped
const metricsAggregator = Effect.scoped(
Effect.gen(function* () {
const counter = yield* Ref.make(0)
// Background worker, привязанный к scope
yield* Effect.forkScoped(
Effect.repeat(
Effect.gen(function* () {
const count = yield* Ref.updateAndGet(counter, (n) => n + 1)
yield* Console.log(`[Metrics] Flush #${count}`)
}),
Schedule.fixed("1 second")
)
)
// Основная работа
yield* Console.log("[App] Processing requests...")
yield* Effect.sleep("3 seconds")
yield* Console.log("[App] Done processing")
// При выходе из scoped — worker прерывается
})
)
Effect.runFork(metricsAggregator)
/*
[App] Processing requests...
[Metrics] Flush #1
[Metrics] Flush #2
[Metrics] Flush #3
[App] Done processing
← worker прерван при закрытии scope
*/
Паттерн 3: Вложенные scope с разным временем жизни
const program = Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Console.log("[Outer] Scope closing")
)
// Outer scope — захватываем
const outerScope = yield* Effect.scope
// Inner scope — короткоживущий
yield* Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Console.log("[Inner] Scope closing")
)
// Worker привязан к OUTER scope — переживёт inner
yield* Effect.forkIn(
Effect.repeat(
Console.log("[Worker] tick"),
Schedule.fixed("1 second")
),
outerScope
)
yield* Console.log("[Inner] Working for 2 seconds...")
yield* Effect.sleep("2 seconds")
yield* Console.log("[Inner] Done")
})
)
yield* Console.log("[Outer] Inner scope closed, worker still running")
yield* Effect.sleep("3 seconds")
yield* Console.log("[Outer] Closing...")
})
)
Effect.runFork(program)
/*
[Inner] Working for 2 seconds...
[Worker] tick
[Worker] tick
[Inner] Done
[Inner] Scope closing
[Outer] Inner scope closed, worker still running
[Worker] tick ← worker пережил inner scope!
[Worker] tick
[Worker] tick
[Outer] Closing...
[Outer] Scope closing ← теперь worker прерван
*/
Примеры
Пример 1: Pipeline с промежуточной очисткой
const makeResource = (name: string) =>
Effect.gen(function* () {
yield* Console.log(`+ ${name}`)
yield* Effect.addFinalizer(() => Console.log(`- ${name}`))
return name
})
const pipeline = Effect.gen(function* () {
// Этап 1: Загрузка данных
const loadScope = yield* Scope.make()
const loader = yield* makeResource("DataLoader").pipe(
Scope.extend(loadScope)
)
yield* Console.log(`Loading with ${loader}...`)
yield* Scope.close(loadScope, Exit.void)
yield* Console.log("Data loaded, loader freed")
// Этап 2: Трансформация
const transformScope = yield* Scope.make()
const transformer = yield* makeResource("Transformer").pipe(
Scope.extend(transformScope)
)
yield* Console.log(`Transforming with ${transformer}...`)
yield* Scope.close(transformScope, Exit.void)
yield* Console.log("Data transformed, transformer freed")
// Этап 3: Сохранение
const saveScope = yield* Scope.make()
const saver = yield* makeResource("Saver").pipe(
Scope.extend(saveScope)
)
yield* Console.log(`Saving with ${saver}...`)
yield* Scope.close(saveScope, Exit.void)
yield* Console.log("Pipeline complete!")
})
Effect.runPromise(pipeline)
/*
+ DataLoader
Loading with DataLoader...
- DataLoader ← освобождён после загрузки
Data loaded, loader freed
+ Transformer
Transforming with Transformer...
- Transformer ← освобождён после трансформации
Data transformed, transformer freed
+ Saver
Saving with Saver...
- Saver ← освобождён после сохранения
Pipeline complete!
*/
Пример 2: Graceful shutdown с forkScoped
const gracefulApp = Effect.scoped(
Effect.gen(function* () {
const requestCount = yield* Ref.make(0)
// Health check worker
const healthFiber = yield* Effect.forkScoped(
Effect.repeat(
Effect.gen(function* () {
const count = yield* Ref.get(requestCount)
yield* Console.log(`[Health] OK, requests served: ${count}`)
}),
Schedule.fixed("1 second")
)
)
// Обработка "запросов"
yield* Effect.forEach(
[1, 2, 3, 4, 5] as const,
(i) =>
Effect.gen(function* () {
yield* Effect.sleep("500 millis")
yield* Ref.update(requestCount, (n) => n + 1)
yield* Console.log(`[App] Request ${i} processed`)
}),
{ concurrency: 2 }
)
const total = yield* Ref.get(requestCount)
yield* Console.log(`[App] All ${total} requests processed`)
// При выходе из scoped — health check worker остановится
yield* Console.log("[App] Shutting down gracefully...")
})
)
Effect.runPromise(gracefulApp)
Пример 3: Connection pool с scoped workers
interface Connection {
readonly id: number
readonly query: (sql: string) => Effect.Effect<string>
}
const createConnection = (id: number) =>
Effect.gen(function* () {
yield* Console.log(`[Conn ${id}] Opened`)
yield* Effect.addFinalizer(() =>
Console.log(`[Conn ${id}] Closed`)
)
const conn: Connection = {
id,
query: (sql) => Effect.succeed(`[Conn ${id}] Result: ${sql}`)
}
return conn
})
const program = Effect.gen(function* () {
// Scope для pool
const poolScope = yield* Scope.make()
// Создаём 3 подключения в pool scope
const conn1 = yield* createConnection(1).pipe(Scope.extend(poolScope))
const conn2 = yield* createConnection(2).pipe(Scope.extend(poolScope))
const conn3 = yield* createConnection(3).pipe(Scope.extend(poolScope))
// Используем подключения
yield* Console.log(yield* conn1.query("SELECT 1"))
yield* Console.log(yield* conn2.query("SELECT 2"))
yield* Console.log(yield* conn3.query("SELECT 3"))
yield* Console.log("--- Draining pool ---")
yield* Scope.close(poolScope, Exit.void)
yield* Console.log("Pool drained")
})
Effect.runPromise(program)
/*
[Conn 1] Opened
[Conn 2] Opened
[Conn 3] Opened
[Conn 1] Result: SELECT 1
[Conn 2] Result: SELECT 2
[Conn 3] Result: SELECT 3
--- Draining pool ---
[Conn 3] Closed ← LIFO
[Conn 2] Closed
[Conn 1] Closed
Pool drained
*/
Распространённые ошибки
Ошибка 1: Использование ресурса после закрытия его scope
// ❌ Ресурс использован после закрытия scope
const bad = Effect.gen(function* () {
const scope = yield* Scope.make()
const resource = yield* Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("Released"))
return { data: "important" }
}).pipe(Scope.extend(scope))
yield* Scope.close(scope, Exit.void) // Ресурс освобождён
// ❌ Ресурс "мёртв" — scope закрыт
console.log(resource.data) // Может работать, но семантически неверно
})
// ✅ Правильно: использовать до закрытия scope
const good = Effect.gen(function* () {
const scope = yield* Scope.make()
const resource = yield* Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("Released"))
return { data: "important" }
}).pipe(Scope.extend(scope))
console.log(resource.data) // ✅ Scope ещё открыт
yield* Scope.close(scope, Exit.void)
})
Ошибка 2: forkScoped без scope
// ❌ forkScoped требует Scope в контексте
const bad = Effect.gen(function* () {
// Ошибка компиляции: Scope не предоставлен
// yield* Effect.forkScoped(Console.log("tick"))
})
// ✅ Правильно: обернуть в Effect.scoped
const good = Effect.scoped(
Effect.gen(function* () {
yield* Effect.forkScoped(Console.log("tick"))
yield* Effect.sleep("1 second")
})
)
Ошибка 3: Путать forkScoped и forkDaemon
// forkDaemon — файбер НИКОГДА не будет прерван автоматически
// ⚠️ Может привести к утечке файберов!
const risky = Effect.scoped(
Effect.gen(function* () {
yield* Effect.forkDaemon(
Effect.repeat(
Console.log("I'll run forever!"),
Schedule.fixed("1 second")
)
)
})
)
// forkScoped — файбер будет прерван при закрытии scope
// ✅ Безопасно — файбер контролируется
const safe = Effect.scoped(
Effect.gen(function* () {
yield* Effect.forkScoped(
Effect.repeat(
Console.log("I'll stop when scope closes"),
Schedule.fixed("1 second")
)
)
yield* Effect.sleep("3 seconds")
})
)
Упражнения
Упражнение 1: Два scope с разным временем жизни
Создайте два scope. В первый привяжите ресурс “TempFile”, во второй — “Database”. Закройте TempFile scope после использования, но оставьте Database открытым.
import { Console, Effect, Exit, Scope } from "effect"
const exercise1 = Effect.gen(function* () {
const tempScope = yield* Scope.make()
const dbScope = yield* Scope.make()
yield* Effect.gen(function* () {
yield* Console.log("TempFile created")
yield* Effect.addFinalizer(() => Console.log("TempFile deleted"))
}).pipe(Scope.extend(tempScope))
yield* Effect.gen(function* () {
yield* Console.log("Database connected")
yield* Effect.addFinalizer(() => Console.log("Database disconnected"))
}).pipe(Scope.extend(dbScope))
yield* Console.log("Processing with temp file...")
yield* Scope.close(tempScope, Exit.void)
yield* Console.log("Temp file cleaned, database still active")
yield* Console.log("More database work...")
yield* Scope.close(dbScope, Exit.void)
yield* Console.log("All done")
})
Effect.runPromise(exercise1)
/*
TempFile created
Database connected
Processing with temp file...
TempFile deleted
Temp file cleaned, database still active
More database work...
Database disconnected
All done
*/import { Console, Effect, Exit, Scope } from "effect"
const exercise1 = Effect.gen(function* () {
const tempScope = yield* Scope.make()
const dbScope = yield* Scope.make()
yield* Effect.gen(function* () {
yield* Console.log("TempFile created")
yield* Effect.addFinalizer(() => Console.log("TempFile deleted"))
}).pipe(Scope.extend(tempScope))
yield* Effect.gen(function* () {
yield* Console.log("Database connected")
yield* Effect.addFinalizer(() => Console.log("Database disconnected"))
}).pipe(Scope.extend(dbScope))
yield* Console.log("Processing with temp file...")
yield* Scope.close(tempScope, Exit.void)
yield* Console.log("Temp file cleaned, database still active")
yield* Console.log("More database work...")
yield* Scope.close(dbScope, Exit.void)
yield* Console.log("All done")
})
Effect.runPromise(exercise1)
/*
TempFile created
Database connected
Processing with temp file...
TempFile deleted
Temp file cleaned, database still active
More database work...
Database disconnected
All done
*/Упражнение 2: Background task с forkScoped
Создайте приложение с background health-check, который работает пока открыт scope. Main effect должен выполнить 5 операций и завершиться, после чего health-check прерывается.
import { Effect, Console, Schedule, Ref } from "effect"
const exercise2 = Effect.scoped(
Effect.gen(function* () {
const opsCount = yield* Ref.make(0)
// Background health check
yield* Effect.forkScoped(
Effect.repeat(
Effect.gen(function* () {
const ops = yield* Ref.get(opsCount)
yield* Console.log(`[HealthCheck] ops completed: ${ops}`)
}),
Schedule.fixed("300 millis")
)
)
// Main work: 5 операций
for (const i of [1, 2, 3, 4, 5] as const) {
yield* Effect.sleep("200 millis")
yield* Ref.update(opsCount, (n) => n + 1)
yield* Console.log(`[Main] Operation ${i} done`)
}
yield* Console.log("[Main] All operations complete, closing scope...")
})
)
Effect.runPromise(exercise2)import { Effect, Console, Schedule, Ref } from "effect"
const exercise2 = Effect.scoped(
Effect.gen(function* () {
const opsCount = yield* Ref.make(0)
// Background health check
yield* Effect.forkScoped(
Effect.repeat(
Effect.gen(function* () {
const ops = yield* Ref.get(opsCount)
yield* Console.log(`[HealthCheck] ops completed: ${ops}`)
}),
Schedule.fixed("300 millis")
)
)
// Main work: 5 операций
for (const i of [1, 2, 3, 4, 5] as const) {
yield* Effect.sleep("200 millis")
yield* Ref.update(opsCount, (n) => n + 1)
yield* Console.log(`[Main] Operation ${i} done`)
}
yield* Console.log("[Main] All operations complete, closing scope...")
})
)
Effect.runPromise(exercise2)Упражнение 3: Multi-phase pipeline с forkIn
Реализуйте pipeline из трёх фаз. Каждая фаза имеет свой scope и background logger. Logger каждой фазы должен пережить свою фазу и быть привязан к внешнему scope, чтобы собрать финальный отчёт.
import { Console, Effect, Schedule, Ref } from "effect"
const exercise3 = Effect.scoped(
Effect.gen(function* () {
const logs = yield* Ref.make<ReadonlyArray<string>>([])
const outerScope = yield* Effect.scope
yield* Effect.addFinalizer(() =>
Effect.gen(function* () {
const allLogs = yield* Ref.get(logs)
yield* Console.log("\n=== Final Report ===")
allLogs.forEach((l) => console.log(` ${l}`))
yield* Console.log("====================")
})
)
// Фаза 1
yield* Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("[Phase 1] Cleanup"))
// Logger привязан к outer scope
yield* Effect.forkIn(
Ref.update(logs, (prev) => [...prev, "Phase 1 completed"]),
outerScope
)
yield* Console.log("[Phase 1] Executing...")
yield* Effect.sleep("500 millis")
})
)
yield* Console.log("[Phase 1] Scope closed")
// Фаза 2
yield* Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("[Phase 2] Cleanup"))
yield* Effect.forkIn(
Ref.update(logs, (prev) => [...prev, "Phase 2 completed"]),
outerScope
)
yield* Console.log("[Phase 2] Executing...")
yield* Effect.sleep("500 millis")
})
)
yield* Console.log("[Phase 2] Scope closed")
// Фаза 3
yield* Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("[Phase 3] Cleanup"))
yield* Effect.forkIn(
Ref.update(logs, (prev) => [...prev, "Phase 3 completed"]),
outerScope
)
yield* Console.log("[Phase 3] Executing...")
yield* Effect.sleep("500 millis")
})
)
yield* Console.log("[Phase 3] Scope closed")
yield* Console.log("\nAll phases complete!")
})
)
Effect.runPromise(exercise3)
/*
[Phase 1] Executing...
[Phase 1] Cleanup
[Phase 1] Scope closed
[Phase 2] Executing...
[Phase 2] Cleanup
[Phase 2] Scope closed
[Phase 3] Executing...
[Phase 3] Cleanup
[Phase 3] Scope closed
All phases complete!
=== Final Report ===
Phase 1 completed
Phase 2 completed
Phase 3 completed
====================
*/import { Console, Effect, Schedule, Ref } from "effect"
const exercise3 = Effect.scoped(
Effect.gen(function* () {
const logs = yield* Ref.make<ReadonlyArray<string>>([])
const outerScope = yield* Effect.scope
yield* Effect.addFinalizer(() =>
Effect.gen(function* () {
const allLogs = yield* Ref.get(logs)
yield* Console.log("\n=== Final Report ===")
allLogs.forEach((l) => console.log(` ${l}`))
yield* Console.log("====================")
})
)
// Фаза 1
yield* Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("[Phase 1] Cleanup"))
// Logger привязан к outer scope
yield* Effect.forkIn(
Ref.update(logs, (prev) => [...prev, "Phase 1 completed"]),
outerScope
)
yield* Console.log("[Phase 1] Executing...")
yield* Effect.sleep("500 millis")
})
)
yield* Console.log("[Phase 1] Scope closed")
// Фаза 2
yield* Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("[Phase 2] Cleanup"))
yield* Effect.forkIn(
Ref.update(logs, (prev) => [...prev, "Phase 2 completed"]),
outerScope
)
yield* Console.log("[Phase 2] Executing...")
yield* Effect.sleep("500 millis")
})
)
yield* Console.log("[Phase 2] Scope closed")
// Фаза 3
yield* Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("[Phase 3] Cleanup"))
yield* Effect.forkIn(
Ref.update(logs, (prev) => [...prev, "Phase 3 completed"]),
outerScope
)
yield* Console.log("[Phase 3] Executing...")
yield* Effect.sleep("500 millis")
})
)
yield* Console.log("[Phase 3] Scope closed")
yield* Console.log("\nAll phases complete!")
})
)
Effect.runPromise(exercise3)
/*
[Phase 1] Executing...
[Phase 1] Cleanup
[Phase 1] Scope closed
[Phase 2] Executing...
[Phase 2] Cleanup
[Phase 2] Scope closed
[Phase 3] Executing...
[Phase 3] Cleanup
[Phase 3] Scope closed
All phases complete!
=== Final Report ===
Phase 1 completed
Phase 2 completed
Phase 3 completed
====================
*/Упражнение 4: Resource pool с динамическим размером
Создайте pool ресурсов, где каждый ресурс имеет свой scope. Реализуйте acquire (берёт ресурс из pool), release (возвращает в pool) и drain (закрывает все ресурсы).
import { Console, Effect, Exit, Queue, Ref, Scope } from "effect"
interface PooledResource {
readonly id: number
readonly scope: Scope.Scope.Closeable
}
const createPool = (size: number) =>
Effect.gen(function* () {
const queue = yield* Queue.bounded<PooledResource>(size)
const activeCount = yield* Ref.make(0)
// Создаём ресурсы
for (let i = 1; i <= size; i++) {
const scope = yield* Scope.make()
yield* Scope.addFinalizer(
scope,
Console.log(`[Resource ${i}] Finalized`)
)
yield* Queue.offer(queue, { id: i, scope })
yield* Ref.update(activeCount, (n) => n + 1)
}
yield* Console.log(`[Pool] Created with ${size} resources`)
return {
acquire: Effect.gen(function* () {
const resource = yield* Queue.take(queue)
yield* Console.log(`[Pool] Acquired resource ${resource.id}`)
return resource
}),
release: (resource: PooledResource) =>
Effect.gen(function* () {
yield* Queue.offer(queue, resource)
yield* Console.log(`[Pool] Released resource ${resource.id}`)
}),
drain: Effect.gen(function* () {
const count = yield* Ref.get(activeCount)
yield* Console.log(`[Pool] Draining ${count} resources...`)
for (let i = 0; i < count; i++) {
const resource = yield* Queue.take(queue)
yield* Scope.close(resource.scope, Exit.void)
}
yield* Console.log("[Pool] All resources drained")
})
}
})
const program = Effect.gen(function* () {
const pool = yield* createPool(3)
const r1 = yield* pool.acquire
const r2 = yield* pool.acquire
yield* Console.log(`Working with ${r1.id} and ${r2.id}...`)
yield* pool.release(r1)
yield* pool.release(r2)
yield* pool.drain
})
Effect.runPromise(program)
/*
[Pool] Created with 3 resources
[Pool] Acquired resource 1
[Pool] Acquired resource 2
Working with 1 and 2...
[Pool] Released resource 1
[Pool] Released resource 2
[Pool] Draining 3 resources...
[Resource 1] Finalized
[Resource 2] Finalized
[Resource 3] Finalized
[Pool] All resources drained
*/import { Console, Effect, Exit, Queue, Ref, Scope } from "effect"
interface PooledResource {
readonly id: number
readonly scope: Scope.Scope.Closeable
}
const createPool = (size: number) =>
Effect.gen(function* () {
const queue = yield* Queue.bounded<PooledResource>(size)
const activeCount = yield* Ref.make(0)
// Создаём ресурсы
for (let i = 1; i <= size; i++) {
const scope = yield* Scope.make()
yield* Scope.addFinalizer(
scope,
Console.log(`[Resource ${i}] Finalized`)
)
yield* Queue.offer(queue, { id: i, scope })
yield* Ref.update(activeCount, (n) => n + 1)
}
yield* Console.log(`[Pool] Created with ${size} resources`)
return {
acquire: Effect.gen(function* () {
const resource = yield* Queue.take(queue)
yield* Console.log(`[Pool] Acquired resource ${resource.id}`)
return resource
}),
release: (resource: PooledResource) =>
Effect.gen(function* () {
yield* Queue.offer(queue, resource)
yield* Console.log(`[Pool] Released resource ${resource.id}`)
}),
drain: Effect.gen(function* () {
const count = yield* Ref.get(activeCount)
yield* Console.log(`[Pool] Draining ${count} resources...`)
for (let i = 0; i < count; i++) {
const resource = yield* Queue.take(queue)
yield* Scope.close(resource.scope, Exit.void)
}
yield* Console.log("[Pool] All resources drained")
})
}
})
const program = Effect.gen(function* () {
const pool = yield* createPool(3)
const r1 = yield* pool.acquire
const r2 = yield* pool.acquire
yield* Console.log(`Working with ${r1.id} and ${r2.id}...`)
yield* pool.release(r1)
yield* pool.release(r2)
yield* pool.drain
})
Effect.runPromise(program)
/*
[Pool] Created with 3 resources
[Pool] Acquired resource 1
[Pool] Acquired resource 2
Working with 1 and 2...
[Pool] Released resource 1
[Pool] Released resource 2
[Pool] Draining 3 resources...
[Resource 1] Finalized
[Resource 2] Finalized
[Resource 3] Finalized
[Pool] All resources drained
*/