Effect.scoped
Автоматическое управление scope.
Проблема ручного управления scope
В предыдущей статье мы видели ручное управление scope через Scope.make() и Scope.close(). Этот подход мощный, но имеет недостатки:
// ⚠️ Ручное управление: много boilerplate, легко забыть close
const manual = Effect.gen(function* () {
const scope = yield* Scope.make()
yield* Scope.addFinalizer(scope, Console.log("Cleanup"))
// ... много кода ...
// Если забыть эту строку — утечка ресурса!
yield* Scope.close(scope, Exit.void)
})
Проблемы ручного подхода:
┌──────────────────────────────────────────────────────┐
│ Ручное управление Scope │
├──────────────────────────────────────────────────────┤
│ ❌ Можно забыть вызвать Scope.close() │
│ ❌ При исключении close может не выполниться │
│ ❌ Нужно вручную передавать Exit │
│ ❌ Много boilerplate кода │
│ ❌ Ошибки в основном коде могут обойти close │
└──────────────────────────────────────────────────────┘
Effect.scoped решает все эти проблемы, автоматизируя жизненный цикл scope.
Effect.scoped
Что делает Effect.scoped
Effect.scoped — это оператор, который:
- Создаёт новый
Scope - Предоставляет его эффекту через Context
- Выполняет эффект
- Закрывает scope после завершения (успех, ошибка или прерывание)
- Удаляет
Scopeиз Requirements типа
Effect.scoped(effect)
┌────────────────────────────────────────────┐
│ 1. scope = Scope.make() │
│ │
│ 2. Предоставить scope эффекту │
│ ┌────────────────────────────────┐ │
│ │ effect │ │
│ │ (имеет доступ к scope через │ │
│ │ Effect.addFinalizer, │ │
│ │ Effect.acquireRelease, etc.) │ │
│ └────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 3. result = Success(value) │
│ или Failure(cause) │
│ или Interrupted │
│ │ │
│ ▼ │
│ 4. Scope.close(scope, exit) │
│ → Все финализаторы в LIFO-порядке │
│ │
│ 5. Вернуть result │
└────────────────────────────────────────────┘
Базовый пример
// Эффект требует Scope
// ┌─── Scope в Requirements
// ▼
const needsScope: Effect.Effect<string, never, Scope.Scope> = Effect.gen(
function* () {
yield* Effect.addFinalizer(() => Console.log("Cleanup!"))
return "result"
}
)
// Effect.scoped убирает Scope из Requirements
// ┌─── Scope удалён!
// ▼
const noScope: Effect.Effect<string, never, never> = Effect.scoped(needsScope)
// Теперь можно запустить
Effect.runPromise(noScope).then(console.log)
/*
Cleanup!
result
*/
Как Effect.scoped взаимодействует с системой типов
Трансформация типа
Effect.scoped выполняет точную трансформацию типа — удаляет только Scope из Requirements, оставляя остальные зависимости:
class Logger extends Context.Tag("Logger")<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>() {}
// Effect<string, Error, Logger | Scope>
const needsLoggerAndScope = Effect.gen(function* () {
const logger = yield* Logger
yield* Effect.addFinalizer(() => logger.log("Resource cleaned up"))
return "result"
})
// Effect<string, Error, Logger> ← Scope удалён, Logger остался
const scopeResolved = Effect.scoped(needsLoggerAndScope)
Визуально:
Before Effect.scoped: After Effect.scoped:
┌───────────────────┐ ┌───────────────────┐
│ Effect< │ │ Effect< │
│ string, │ │ string, │
│ Error, │ │ Error, │
│ Logger | Scope │────▶│ Logger │
│ > │ │ > │
└───────────────────┘ └───────────────────┘
│
Scope автоматически
управляется
Вложенный scoped
Effect.scoped можно применять на разных уровнях, создавая вложенные scope:
const inner = Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("Inner cleanup"))
yield* Console.log("Inner scope active")
return "inner result"
})
)
const outer = Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("Outer cleanup"))
const result = yield* inner // Inner scope создаётся и закрывается здесь
yield* Console.log(`Got: ${result}`)
})
)
Effect.runPromise(outer)
/*
Inner scope active
Inner cleanup ← inner scope закрывается первым
Got: inner result
Outer cleanup ← outer scope закрывается после
*/
Слияние scope
Автоматическое слияние
Когда несколько scoped-ресурсов используются внутри одного Effect.scoped, их scope сливаются в один:
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.scoped(
Effect.gen(function* () {
// task1 и task2 делят один scope
yield* task1
yield* task2
})
)
Effect.runPromise(program)
/*
Task 1: started
Task 2: started
Task 2: finalizer ← LIFO: task2 добавлен позже
Task 1: finalizer
*/
Визуализация слияния
Effect.scoped(...)
┌──────────────────────────────────────────────┐
│ SHARED SCOPE │
│ │
│ task1 ─── addFinalizer("Task 1") ──┐ │
│ │ │
│ task2 ─── addFinalizer("Task 2") ──┤ │
│ │ │
│ ▼ │
│ Finalizer Stack │
│ ┌──────────┐ │
│ │ Task 2 │ │
│ │ Task 1 │ │
│ └──────────┘ │
│ │
└───────────────── close ──────────────────────┘
│
▼
Task 2: finalizer
Task 1: finalizer
Это поведение по умолчанию удобно в большинстве случаев. Когда нужен более тонкий контроль — используйте ручное создание scope (описано в статье 05).
Scoped Layers
Проблема: сервис с жизненным циклом
Многие сервисы в production нуждаются в инициализации и очистке:
Database Pool HTTP Server Message Queue
┌────────────┐ ┌────────────┐ ┌────────────┐
│ init pool │ │ start │ │ connect │
│ use pool │ │ serve │ │ consume │
│ close pool │ │ shutdown │ │ disconnect │
└────────────┘ └────────────┘ └────────────┘
Обычный Layer не имеет понятия о жизненном цикле — он создаёт сервис, но не управляет его закрытием. Layer.scoped решает эту проблему.
Что такое Scoped Layer
Scoped Layer — это Layer, который:
- Создаёт сервис через scoped-эффект (с
acquireRelease) - Автоматически закрывает ресурсы сервиса при завершении программы
- Не требует от потребителя знать о внутреннем scope
Layer.scoped
Создание scoped layer
// Определяем сервис
class DatabasePool extends Context.Tag("DatabasePool")<
DatabasePool,
{
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>
readonly poolSize: number
}
>() {}
// Scoped layer: ресурс с жизненным циклом
const DatabasePoolLive: Layer.Layer<DatabasePool> = Layer.scoped(
DatabasePool,
Effect.gen(function* () {
// Acquire pool
yield* Console.log("[DB Pool] Initializing connection pool...")
const pool = {
query: (sql: string) => Effect.succeed([{ sql, result: "data" }]),
poolSize: 10
}
// Регистрируем финализатор
yield* Effect.addFinalizer(() =>
Console.log("[DB Pool] Closing all connections...")
)
yield* Console.log(`[DB Pool] Ready with ${pool.poolSize} connections`)
return pool
})
)
Использование scoped layer
const program = Effect.gen(function* () {
const pool = yield* DatabasePool
yield* Console.log(`Pool size: ${pool.poolSize}`)
const result = yield* pool.query("SELECT * FROM users")
yield* Console.log(`Query result: ${JSON.stringify(result)}`)
})
// provide автоматически управляет scope layer
Effect.runPromise(
program.pipe(Effect.provide(DatabasePoolLive))
)
/*
[DB Pool] Initializing connection pool...
[DB Pool] Ready with 10 connections
Pool size: 10
Query result: [{"sql":"SELECT * FROM users","result":"data"}]
[DB Pool] Closing all connections...
*/
💡 Обратите внимание: потребитель (program) вообще не знает о scope и финализаторах. Вся логика жизненного цикла инкапсулирована в Layer.
Layer.scoped с acquireRelease
Более идиоматичный способ — использовать acquireRelease внутри layer:
class HttpServer extends Context.Tag("HttpServer")<
HttpServer,
{
readonly port: number
readonly handleRequest: (path: string) => Effect.Effect<string>
}
>() {}
const HttpServerLive = Layer.scoped(
HttpServer,
Effect.acquireRelease(
// Acquire: запуск сервера
Effect.gen(function* () {
const port = 8080
yield* Console.log(`[Server] Starting on port ${port}...`)
return {
port,
handleRequest: (path: string) =>
Effect.succeed(`200 OK: ${path}`)
}
}),
// Release: остановка сервера
(server) => Console.log(`[Server] Shutting down port ${server.port}...`)
)
)
Effect.Service с scoped
Более современный способ определения сервисов с жизненным циклом — Effect.Service с параметром scoped:
class CacheService extends Effect.Service<CacheService>()("CacheService", {
scoped: Effect.gen(function* () {
// Acquire
const cache = new Map<string, string>()
yield* Console.log("[Cache] Initialized")
// Финализатор
yield* Effect.addFinalizer(() =>
Console.log(`[Cache] Cleared ${cache.size} entries`)
)
return {
get: (key: string) =>
Effect.sync(() => cache.get(key) ?? null),
set: (key: string, value: string) =>
Effect.sync(() => { cache.set(key, value) }),
size: Effect.sync(() => cache.size)
}
})
}) {}
Использование:
const program = Effect.gen(function* () {
const cache = yield* CacheService
yield* cache.set("user:1", "Alice")
yield* cache.set("user:2", "Bob")
const user = yield* cache.get("user:1")
const size = yield* cache.size
yield* Console.log(`User: ${user}, Cache size: ${size}`)
})
Effect.runPromise(
program.pipe(Effect.provide(CacheService.Default))
)
/*
[Cache] Initialized
User: Alice, Cache size: 2
[Cache] Cleared 2 entries
*/
CacheService.Default — автоматически сгенерированный Layer<CacheService>, который управляет scope. Потребителю не нужно знать о Scope.
acquireRelease внутри Effect.Service
class MetricsCollector extends Effect.Service<MetricsCollector>()(
"MetricsCollector",
{
scoped: Effect.acquireRelease(
// Acquire
Effect.gen(function* () {
yield* Console.log("[Metrics] Starting collector...")
const metrics = new Map<string, number>()
return {
increment: (name: string) =>
Effect.sync(() => {
metrics.set(name, (metrics.get(name) ?? 0) + 1)
}),
report: Effect.sync(() =>
Object.fromEntries(metrics.entries())
)
}
}),
// Release
(collector) =>
Effect.gen(function* () {
const report = yield* collector.report
yield* Console.log(`[Metrics] Final report: ${JSON.stringify(report)}`)
yield* Console.log("[Metrics] Collector stopped")
})
)
}
) {}
API Reference
Effect.scoped [STABLE]
// Удаляет Scope из Requirements, автоматически управляя жизненным циклом
Effect.scoped<A, E, R>(
effect: Effect<A, E, R | Scope>
): Effect<A, E, Exclude<R, Scope>>
Layer.scoped [STABLE]
// Создаёт Layer из scoped-эффекта
Layer.scoped<T extends Context.Tag<any, any>>(
tag: T,
effect: Effect<Context.Tag.Service<T>, E, R | Scope>
): Layer<Context.Tag.Identifier<T>, E, Exclude<R, Scope>>
Effect.scope [STABLE]
// Получает текущий Scope из контекста
Effect.scope: Effect<Scope, never, Scope>
Layer.scopedDiscard [STABLE]
// Layer, который выполняет scoped-эффект, но не предоставляет сервис
Layer.scopedDiscard<E, R>(
effect: Effect<unknown, E, R | Scope>
): Layer<never, E, Exclude<R, Scope>>
Полезен для background-задач, которые должны жить в scope layer:
// Фоновая задача: heartbeat
const HeartbeatLayer = Layer.scopedDiscard(
Effect.gen(function* () {
const fiber = yield* Effect.fork(
Effect.repeat(
Console.log("[Heartbeat] ping"),
Schedule.spaced("5 seconds")
)
)
yield* Effect.addFinalizer(() =>
Console.log("[Heartbeat] stopped")
)
})
)
Примеры
Пример 1: Полноценный сервис с lifecycle
// Сервис с жизненным циклом
class EventBus extends Context.Tag("EventBus")<
EventBus,
{
readonly publish: (event: string) => Effect.Effect<void>
readonly subscribe: (
handler: (event: string) => void
) => Effect.Effect<void>
}
>() {}
const EventBusLive = Layer.scoped(
EventBus,
Effect.gen(function* () {
const subscribers: Array<(event: string) => void> = []
yield* Console.log("[EventBus] Starting...")
yield* Effect.addFinalizer(() =>
Effect.gen(function* () {
yield* Console.log(
`[EventBus] Shutting down. ${subscribers.length} subscribers notified.`
)
subscribers.length = 0
})
)
return {
publish: (event: string) =>
Effect.sync(() => {
subscribers.forEach((handler) => handler(event))
}),
subscribe: (handler: (event: string) => void) =>
Effect.sync(() => {
subscribers.push(handler)
})
}
})
)
const program = Effect.gen(function* () {
const bus = yield* EventBus
yield* bus.subscribe((e) => console.log(`Received: ${e}`))
yield* bus.publish("user.created")
yield* bus.publish("order.placed")
})
Effect.runPromise(program.pipe(Effect.provide(EventBusLive)))
/*
[EventBus] Starting...
Received: user.created
Received: order.placed
[EventBus] Shutting down. 1 subscribers notified.
*/
Пример 2: Композиция scoped layers
// Два сервиса с жизненным циклом
class ConfigService extends Context.Tag("ConfigService")<
ConfigService,
{ readonly get: (key: string) => Effect.Effect<string> }
>() {}
class ApiClient extends Context.Tag("ApiClient")<
ApiClient,
{ readonly fetch: (url: string) => Effect.Effect<string> }
>() {}
const ConfigLive = Layer.scoped(
ConfigService,
Effect.gen(function* () {
yield* Console.log("[Config] Loading configuration...")
yield* Effect.addFinalizer(() => Console.log("[Config] Cleanup"))
return {
get: (key: string) => Effect.succeed(`value-of-${key}`)
}
})
)
// ApiClient зависит от ConfigService
const ApiClientLive = Layer.scoped(
ApiClient,
Effect.gen(function* () {
const config = yield* ConfigService
const baseUrl = yield* config.get("api.baseUrl")
yield* Console.log(`[ApiClient] Initialized with baseUrl: ${baseUrl}`)
yield* Effect.addFinalizer(() =>
Console.log("[ApiClient] Closing connections...")
)
return {
fetch: (url: string) =>
Effect.succeed(`Response from ${baseUrl}/${url}`)
}
})
)
// Композиция: ApiClient → ConfigService
const AppLayer = ApiClientLive.pipe(Layer.provide(ConfigLive))
const program = Effect.gen(function* () {
const api = yield* ApiClient
const result = yield* api.fetch("users")
yield* Console.log(`Result: ${result}`)
})
Effect.runPromise(program.pipe(Effect.provide(AppLayer)))
/*
[Config] Loading configuration...
[ApiClient] Initialized with baseUrl: value-of-api.baseUrl
Result: Response from value-of-api.baseUrl/users
[ApiClient] Closing connections...
[Config] Cleanup
*/
Пример 3: Scoped layer с acquireRelease и зависимостями
class DbConfig extends Context.Tag("DbConfig")<
DbConfig,
{ readonly connectionString: string; readonly poolSize: number }
>() {}
class DbPool extends Context.Tag("DbPool")<
DbPool,
{
readonly query: (sql: string) => Effect.Effect<string>
readonly activeConnections: Effect.Effect<number>
}
>() {}
class DbPoolError extends Data.TaggedError("DbPoolError")<{
readonly message: string
}> {}
const DbConfigLive = Layer.succeed(DbConfig, {
connectionString: "postgres://localhost:5432/mydb",
poolSize: 20
})
const DbPoolLive = Layer.scoped(
DbPool,
Effect.gen(function* () {
const config = yield* DbConfig
// Acquire pool
yield* Console.log(
`[Pool] Connecting to ${config.connectionString}, ` +
`pool size: ${config.poolSize}`
)
let activeCount = 0
yield* Effect.addFinalizer(() =>
Console.log(`[Pool] Draining ${activeCount} active connections...`)
)
return {
query: (sql: string) =>
Effect.gen(function* () {
activeCount++
yield* Effect.sleep("10 millis") // Simulate query
activeCount--
return `Result of: ${sql}`
}),
activeConnections: Effect.sync(() => activeCount)
}
})
)
const AppLayer = DbPoolLive.pipe(Layer.provide(DbConfigLive))
const program = Effect.gen(function* () {
const pool = yield* DbPool
const r1 = yield* pool.query("SELECT 1")
const r2 = yield* pool.query("SELECT 2")
yield* Console.log(`Results: ${r1}, ${r2}`)
})
Effect.runPromise(program.pipe(Effect.provide(AppLayer)))
Распространённые ошибки
Ошибка 1: Двойной Effect.scoped
const resource = Effect.acquireRelease(
Effect.succeed("data"),
() => Console.log("released")
)
// ❌ Лишний scoped — ресурс освобождается сразу после acquire
const bad = Effect.scoped(
Effect.gen(function* () {
// Вложенный scoped создаёт внутренний scope
const data = yield* Effect.scoped(resource)
// Здесь ресурс уже освобождён!
yield* Console.log(`Using: ${data}`) // Потенциально опасно
})
)
// ✅ Правильно: один scoped на верхнем уровне
const good = Effect.scoped(
Effect.gen(function* () {
const data = yield* resource
yield* Console.log(`Using: ${data}`)
})
)
Ошибка 2: Забыть, что Layer.scoped управляет scope автоматически
class MyService extends Context.Tag("MyService")<
MyService,
{ readonly doWork: Effect.Effect<void> }
>() {}
const MyServiceLive = Layer.scoped(
MyService,
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Effect.log("cleanup"))
return { doWork: Effect.log("working") }
})
)
// ❌ Не нужно оборачивать в Effect.scoped — Layer уже управляет scope
// Effect.runPromise(
// Effect.scoped(program.pipe(Effect.provide(MyServiceLive)))
// )
// ✅ Просто provide — scope управляется внутри Layer
Effect.runPromise(
Effect.gen(function* () {
const svc = yield* MyService
yield* svc.doWork
}).pipe(Effect.provide(MyServiceLive))
)
Ошибка 3: Возвращать scoped-эффект из use
// ❌ acquireRelease вернёт Effect<..., Scope>, а use ожидает использование
const bad = Effect.acquireUseRelease(
Effect.succeed("resource"),
// Use возвращает Effect, требующий Scope — но scope уже закрыт
(res) => Effect.addFinalizer(() => Console.log("nested finalizer")),
() => Console.log("release")
)
Упражнения
Упражнение 1: Простой Effect.scoped
Создайте эффект, который добавляет два финализатора и возвращает строку. Оберните в Effect.scoped и убедитесь, что финализаторы выполняются в LIFO-порядке.
import { Effect, Console } from "effect"
const exercise1 = Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("First added"))
yield* Effect.addFinalizer(() => Console.log("Second added"))
return "done"
})
)
Effect.runPromise(exercise1).then(console.log)
// Second added
// First added
// doneimport { Effect, Console } from "effect"
const exercise1 = Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() => Console.log("First added"))
yield* Effect.addFinalizer(() => Console.log("Second added"))
return "done"
})
)
Effect.runPromise(exercise1).then(console.log)
// Second added
// First added
// doneУпражнение 2: Scoped layer для сервиса
Создайте scoped layer для HealthChecker — сервиса, который при создании логирует “Health checker started”, а при закрытии — “Health checker stopped”.
import { Effect, Layer, Console, Context } from "effect"
class HealthChecker extends Context.Tag("HealthChecker")<
HealthChecker,
{ readonly check: Effect.Effect<boolean> }
>() {}
const HealthCheckerLive = Layer.scoped(
HealthChecker,
Effect.gen(function* () {
yield* Console.log("Health checker started")
yield* Effect.addFinalizer(() => Console.log("Health checker stopped"))
return { check: Effect.succeed(true) }
})
)
const program = Effect.gen(function* () {
const hc = yield* HealthChecker
const healthy = yield* hc.check
yield* Console.log(`System healthy: ${healthy}`)
})
Effect.runPromise(program.pipe(Effect.provide(HealthCheckerLive)))
// Health checker started
// System healthy: true
// Health checker stoppedimport { Effect, Layer, Console, Context } from "effect"
class HealthChecker extends Context.Tag("HealthChecker")<
HealthChecker,
{ readonly check: Effect.Effect<boolean> }
>() {}
const HealthCheckerLive = Layer.scoped(
HealthChecker,
Effect.gen(function* () {
yield* Console.log("Health checker started")
yield* Effect.addFinalizer(() => Console.log("Health checker stopped"))
return { check: Effect.succeed(true) }
})
)
const program = Effect.gen(function* () {
const hc = yield* HealthChecker
const healthy = yield* hc.check
yield* Console.log(`System healthy: ${healthy}`)
})
Effect.runPromise(program.pipe(Effect.provide(HealthCheckerLive)))
// Health checker started
// System healthy: true
// Health checker stoppedУпражнение 3: Два scoped-сервиса с зависимостями
Создайте Logger (scoped) и UserService (scoped, зависит от Logger). Убедитесь в правильном порядке инициализации и очистки.
import { Effect, Layer, Console, Context } from "effect"
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly info: (msg: string) => Effect.Effect<void> }
>() {}
class UserService extends Context.Tag("UserService")<
UserService,
{ readonly getUser: (id: string) => Effect.Effect<string> }
>() {}
const LoggerLive = Layer.scoped(
Logger,
Effect.gen(function* () {
yield* Console.log("[Logger] Initialized")
yield* Effect.addFinalizer(() => Console.log("[Logger] Shutdown"))
return { info: (msg: string) => Console.log(`[LOG] ${msg}`) }
})
)
const UserServiceLive = Layer.scoped(
UserService,
Effect.gen(function* () {
const logger = yield* Logger
yield* logger.info("UserService starting...")
yield* Effect.addFinalizer(() =>
logger.info("UserService shutting down...")
)
return {
getUser: (id: string) =>
Effect.gen(function* () {
yield* logger.info(`Fetching user ${id}`)
return `User-${id}`
})
}
})
)
const AppLayer = UserServiceLive.pipe(Layer.provide(LoggerLive))
const program = Effect.gen(function* () {
const users = yield* UserService
const user = yield* users.getUser("42")
yield* Console.log(`Found: ${user}`)
})
Effect.runPromise(program.pipe(Effect.provide(AppLayer)))
/*
[Logger] Initialized
[LOG] UserService starting...
[LOG] Fetching user 42
Found: User-42
[LOG] UserService shutting down...
[Logger] Shutdown
*/import { Effect, Layer, Console, Context } from "effect"
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly info: (msg: string) => Effect.Effect<void> }
>() {}
class UserService extends Context.Tag("UserService")<
UserService,
{ readonly getUser: (id: string) => Effect.Effect<string> }
>() {}
const LoggerLive = Layer.scoped(
Logger,
Effect.gen(function* () {
yield* Console.log("[Logger] Initialized")
yield* Effect.addFinalizer(() => Console.log("[Logger] Shutdown"))
return { info: (msg: string) => Console.log(`[LOG] ${msg}`) }
})
)
const UserServiceLive = Layer.scoped(
UserService,
Effect.gen(function* () {
const logger = yield* Logger
yield* logger.info("UserService starting...")
yield* Effect.addFinalizer(() =>
logger.info("UserService shutting down...")
)
return {
getUser: (id: string) =>
Effect.gen(function* () {
yield* logger.info(`Fetching user ${id}`)
return `User-${id}`
})
}
})
)
const AppLayer = UserServiceLive.pipe(Layer.provide(LoggerLive))
const program = Effect.gen(function* () {
const users = yield* UserService
const user = yield* users.getUser("42")
yield* Console.log(`Found: ${user}`)
})
Effect.runPromise(program.pipe(Effect.provide(AppLayer)))
/*
[Logger] Initialized
[LOG] UserService starting...
[LOG] Fetching user 42
Found: User-42
[LOG] UserService shutting down...
[Logger] Shutdown
*/Упражнение 4: Application bootstrap с несколькими scoped layers
Создайте полную application bootstrap с тремя scoped сервисами: Config, Database, HttpServer. Каждый должен логировать инициализацию и очистку. Database зависит от Config, HttpServer зависит от Database. Убедитесь в корректном порядке lifecycle.
import { Effect, Layer, Console, Context } from "effect"
class Config extends Context.Tag("Config")<Config, {
readonly dbUrl: string
readonly port: number
}>() {}
class Database extends Context.Tag("Database")<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>() {}
class HttpServer extends Context.Tag("HttpServer")<HttpServer, {
readonly start: Effect.Effect<void>
}>() {}
const ConfigLive = Layer.scoped(
Config,
Effect.gen(function* () {
yield* Console.log("[1/3] Loading config...")
yield* Effect.addFinalizer(() => Console.log("[3/3] Config released"))
return { dbUrl: "postgres://localhost/db", port: 3000 }
})
)
const DatabaseLive = Layer.scoped(
Database,
Effect.gen(function* () {
const config = yield* Config
yield* Console.log(`[2/3] Connecting to ${config.dbUrl}...`)
yield* Effect.addFinalizer(() =>
Console.log("[2/3] Database disconnected")
)
return {
query: (sql: string) => Effect.succeed(`Result: ${sql}`)
}
})
)
const HttpServerLive = Layer.scoped(
HttpServer,
Effect.gen(function* () {
const config = yield* Config
const db = yield* Database
yield* Console.log(`[3/3] Starting HTTP server on :${config.port}...`)
yield* Effect.addFinalizer(() =>
Console.log("[1/3] HTTP server stopped")
)
return {
start: Console.log(`Server ready on :${config.port}`)
}
})
)
const AppLayer = HttpServerLive.pipe(
Layer.provide(Layer.merge(DatabaseLive, ConfigLive).pipe(
Layer.provide(ConfigLive)
))
)
// Более точная композиция:
const AppLayerV2 = HttpServerLive.pipe(
Layer.provide(DatabaseLive),
Layer.provide(ConfigLive)
)
const program = Effect.gen(function* () {
const server = yield* HttpServer
yield* server.start
yield* Console.log("Application running!")
})
Effect.runPromise(program.pipe(Effect.provide(AppLayerV2)))
/*
[1/3] Loading config...
[2/3] Connecting to postgres://localhost/db...
[3/3] Starting HTTP server on :3000...
Server ready on :3000
Application running!
[1/3] HTTP server stopped ← LIFO: последний инициализирован — первый остановлен
[2/3] Database disconnected
[3/3] Config released
*/import { Effect, Layer, Console, Context } from "effect"
class Config extends Context.Tag("Config")<Config, {
readonly dbUrl: string
readonly port: number
}>() {}
class Database extends Context.Tag("Database")<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>() {}
class HttpServer extends Context.Tag("HttpServer")<HttpServer, {
readonly start: Effect.Effect<void>
}>() {}
const ConfigLive = Layer.scoped(
Config,
Effect.gen(function* () {
yield* Console.log("[1/3] Loading config...")
yield* Effect.addFinalizer(() => Console.log("[3/3] Config released"))
return { dbUrl: "postgres://localhost/db", port: 3000 }
})
)
const DatabaseLive = Layer.scoped(
Database,
Effect.gen(function* () {
const config = yield* Config
yield* Console.log(`[2/3] Connecting to ${config.dbUrl}...`)
yield* Effect.addFinalizer(() =>
Console.log("[2/3] Database disconnected")
)
return {
query: (sql: string) => Effect.succeed(`Result: ${sql}`)
}
})
)
const HttpServerLive = Layer.scoped(
HttpServer,
Effect.gen(function* () {
const config = yield* Config
const db = yield* Database
yield* Console.log(`[3/3] Starting HTTP server on :${config.port}...`)
yield* Effect.addFinalizer(() =>
Console.log("[1/3] HTTP server stopped")
)
return {
start: Console.log(`Server ready on :${config.port}`)
}
})
)
const AppLayer = HttpServerLive.pipe(
Layer.provide(Layer.merge(DatabaseLive, ConfigLive).pipe(
Layer.provide(ConfigLive)
))
)
// Более точная композиция:
const AppLayerV2 = HttpServerLive.pipe(
Layer.provide(DatabaseLive),
Layer.provide(ConfigLive)
)
const program = Effect.gen(function* () {
const server = yield* HttpServer
yield* server.start
yield* Console.log("Application running!")
})
Effect.runPromise(program.pipe(Effect.provide(AppLayerV2)))
/*
[1/3] Loading config...
[2/3] Connecting to postgres://localhost/db...
[3/3] Starting HTTP server on :3000...
Server ready on :3000
Application running!
[1/3] HTTP server stopped ← LIFO: последний инициализирован — первый остановлен
[2/3] Database disconnected
[3/3] Config released
*/