Создание Layer
Полное руководство по конструкторам Layer.
Теория
Выбор правильного конструктора
Каждый конструктор Layer предназначен для конкретного сценария. Выбор определяется ответами на три вопроса:
Вопрос 1: Нужны ли побочные эффекты при создании сервиса?
Нет → Layer.succeed / Layer.sync
Да → Вопрос 2
Вопрос 2: Нужно ли управлять жизненным циклом (acquire/release)?
Нет → Layer.effect
Да → Layer.scoped
Вопрос 3: Нужен ли доступ к зависимостям?
Нет → Layer.succeed / Layer.sync (для простых)
Да → Layer.effect / Layer.scoped (yield* Tag внутри)
Диаграмма принятия решения:
┌──────────────────────┐
│ Создание сервиса │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Есть побочные │
│ эффекты? │
└──┬──────────────┬────┘
Нет│ │Да
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ Значение │ │ Нужен │
│ статичное? │ │ lifecycle? │
└──┬──────┬───┘ └──┬──────────┬───┘
Да │ Нет│ Нет│ │Да
▼ ▼ ▼ ▼
succeed sync effect scoped
Конструкторы Layer
Layer.succeed
Layer.succeed — самый простой конструктор. Создаёт Layer из готового значения без каких-либо эффектов.
Сигнатура
function succeed<T extends Context.Tag<any, any>>(
tag: T,
service: Context.Tag.Service<T>
): Layer<Context.Tag.Identifier<T>>
Когда использовать
- Сервис не требует инициализации
- Значение известно на этапе определения
- Нет зависимостей от других сервисов
- Нет побочных эффектов
Примеры
// === Простая конфигурация ===
class AppConfig extends Context.Tag("AppConfig")<
AppConfig,
{
readonly port: number
readonly host: string
readonly env: "development" | "production" | "test"
}
>() {}
const AppConfigLive = Layer.succeed(AppConfig, {
port: 3000,
host: "0.0.0.0",
env: "production"
})
// Тип: Layer<AppConfig, never, never>
// === Сервис с методами, возвращающими Effect ===
class MathService extends Context.Tag("MathService")<
MathService,
{
readonly add: (a: number, b: number) => Effect.Effect<number>
readonly multiply: (a: number, b: number) => Effect.Effect<number>
}
>() {}
const MathServiceLive = Layer.succeed(MathService, {
add: (a, b) => Effect.succeed(a + b),
multiply: (a, b) => Effect.succeed(a * b)
})
// Тип: Layer<MathService, never, never>
⚠️ Важно: Layer.succeed захватывает значение по ссылке. Если значение содержит мутабельное состояние, все потребители получат одну и ту же ссылку. Это поведение по дизайну, но требует осознанности:
// ⚠️ Все потребители разделяют один Map
const CacheLive = Layer.succeed(Cache, {
store: new Map<string, string>(),
get: (key) => Effect.sync(() => /* ... */ undefined),
set: (key, value) => Effect.sync(() => { /* ... */ })
})
Layer.sync
Layer.sync создаёт Layer из синхронной функции. Функция вызывается при конструировании Layer (лениво).
Сигнатура
function sync<T extends Context.Tag<any, any>>(
tag: T,
service: () => Context.Tag.Service<T>
): Layer<Context.Tag.Identifier<T>>
Когда использовать
- Значение нужно вычислить при инициализации (но без побочных эффектов)
- Создание значения требует вычислений (чтение из памяти, парсинг)
- Нужна ленивая инициализация
Примеры
class Clock extends Context.Tag("Clock")<
Clock,
{
readonly now: () => Effect.Effect<number>
readonly startedAt: number
}
>() {}
// Значение startedAt вычисляется при конструировании Layer
const ClockLive = Layer.sync(Clock, () => ({
now: () => Effect.sync(() => Date.now()),
startedAt: Date.now()
}))
// === Конфиг из переменных окружения (синхронно) ===
class EnvConfig extends Context.Tag("EnvConfig")<
EnvConfig,
{
readonly nodeEnv: string
readonly debug: boolean
}
>() {}
const EnvConfigLive = Layer.sync(EnvConfig, () => ({
nodeEnv: process.env["NODE_ENV"] ?? "development",
debug: process.env["DEBUG"] === "true"
}))
Отличие от Layer.succeed
// Layer.succeed — значение вычисляется СРАЗУ при определении
const a = Layer.succeed(Tag, { timestamp: Date.now() })
// timestamp зафиксировано в момент определения
// Layer.sync — функция вызывается ЛЕНИВО при конструировании
const b = Layer.sync(Tag, () => ({ timestamp: Date.now() }))
// timestamp вычисляется при первом использовании Layer
Layer.fail и Layer.failSync
Создают Layer, который всегда завершается ошибкой. Полезны для тестирования и fallback-стратегий.
Сигнатура
function fail<E>(error: E): Layer<never, E>
function failSync<E>(error: () => E): Layer<never, E>
Примеры
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly reason: string
}> {}
// Layer, который всегда падает — полезен для тестирования error paths
const DatabaseFailing = Layer.fail(
new DatabaseError({ reason: "Connection refused" })
)
// Тип: Layer<never, DatabaseError, never>
// Ленивая версия
const DatabaseFailingLazy = Layer.failSync(() =>
new DatabaseError({ reason: `Failed at ${new Date().toISOString()}` })
)
Layer.effect
Layer.effect — основной конструктор для создания Layer из Effect. Это самый универсальный конструктор, позволяющий использовать зависимости, выполнять побочные эффекты и обрабатывать ошибки.
Сигнатура
function effect<T extends Context.Tag<any, any>, E, R>(
tag: T,
effect: Effect<Context.Tag.Service<T>, E, R>
): Layer<Context.Tag.Identifier<T>, E, R>
Когда использовать
- Сервис зависит от других сервисов (через
yield* Tag) - Инициализация включает асинхронные операции
- Конструирование может завершиться ошибкой
- Нужно выполнить побочные эффекты при создании
Примеры
// === Сервис с зависимостями ===
class Config extends Context.Tag("Config")<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>() {}
// ConfigLive — без зависимостей
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "postgres://localhost:5432/app"
})
})
// LoggerLive — зависит от Config
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
// yield* Config — доступ к зависимости
const config = yield* Config
return {
log: (message) =>
Effect.gen(function* () {
const { logLevel } = yield* config.getConfig
console.log(`[${logLevel}] ${message}`)
})
}
})
)
// Тип: Layer<Logger, never, Config>
// === Сервис с ошибками при конструировании ===
class ConnectionError extends Data.TaggedError("ConnectionError")<{
readonly url: string
readonly cause: string
}> {}
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>() {}
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
const { connection } = yield* config.getConfig
// Симуляция проверки подключения
yield* logger.log(`Connecting to database: ${connection}`)
if (connection.includes("invalid")) {
return yield* Effect.fail(
new ConnectionError({ url: connection, cause: "Invalid connection string" })
)
}
yield* logger.log("Database connected successfully")
return {
query: (sql: string) =>
Effect.gen(function* () {
yield* logger.log(`Executing: ${sql}`)
return { result: `Results from ${connection}` }
})
}
})
)
// Тип: Layer<Database, ConnectionError, Config | Logger>
Паттерн: Layer.effect с Effect.acquireRelease (без Scope)
Иногда нужно создать ресурс в Layer.effect, но без полного lifecycle. В таком случае ресурс будет жить столько же, сколько Scope программы:
class HttpClient extends Context.Tag("HttpClient")<
HttpClient,
{ readonly fetch: (url: string) => Effect.Effect<string> }
>() {}
// Простой клиент без управления lifecycle
const HttpClientLive = Layer.effect(
HttpClient,
Effect.gen(function* () {
yield* Effect.log("HttpClient initialized")
return {
fetch: (url) => Effect.succeed(`Response from ${url}`)
}
})
)
Layer.scoped
Layer.scoped — конструктор для создания Layer из scoped Effect. Ключевое отличие от Layer.effect — поддержка acquireRelease для управления жизненным циклом ресурсов.
Сигнатура
function scoped<T extends Context.Tag<any, any>, E, R>(
tag: T,
effect: Effect<Context.Tag.Service<T>, E, R>
): Layer<Context.Tag.Identifier<T>, E, Exclude<R, Scope>>
⚠️ Обратите внимание: Scope автоматически исключается из RIn. Layer сам управляет Scope.
Когда использовать
- Сервис оборачивает ресурс с жизненным циклом (connection pool, file handle, socket)
- Нужна гарантия освобождения ресурса при завершении программы
- Используется
Effect.acquireReleaseилиEffect.addFinalizer
Примеры
// === Connection Pool ===
class ConnectionPool extends Context.Tag("ConnectionPool")<
ConnectionPool,
{
readonly getConnection: () => Effect.Effect<{ readonly id: string }>
readonly size: number
}
>() {}
const ConnectionPoolLive = Layer.scoped(
ConnectionPool,
Effect.gen(function* () {
// Acquire: создаём пул
const pool = yield* Effect.acquireRelease(
Effect.sync(() => {
console.log("[Pool] Creating connection pool (size: 10)")
return {
connections: Array.from({ length: 10 }, (_, i) => ({
id: `conn-${i}`,
inUse: false
}))
}
}),
(pool) =>
Effect.sync(() => {
console.log(`[Pool] Closing all ${pool.connections.length} connections`)
pool.connections.forEach((c) => {
console.log(`[Pool] Closed ${c.id}`)
})
})
)
let nextIdx = 0
return {
getConnection: () =>
Effect.sync(() => {
const conn = pool.connections[nextIdx % pool.connections.length]!
nextIdx++
return { id: conn.id }
}),
size: pool.connections.length
}
})
)
// Тип: Layer<ConnectionPool, never, never>
// Scope НЕ появляется в RIn — Layer управляет им сам
// === Пример использования ===
const program = Effect.gen(function* () {
const pool = yield* ConnectionPool
console.log(`Pool size: ${pool.size}`)
const conn = yield* pool.getConnection()
console.log(`Got connection: ${conn.id}`)
})
Effect.runPromise(Effect.provide(program, ConnectionPoolLive))
// Output:
// [Pool] Creating connection pool (size: 10)
// Pool size: 10
// Got connection: conn-0
// [Pool] Closing all 10 connections
// [Pool] Closed conn-0
// ... (все 10)
Layer.scoped с addFinalizer
Альтернативный способ — добавить финализатор без acquireRelease:
class TempDirectory extends Context.Tag("TempDirectory")<
TempDirectory,
{ readonly path: string }
>() {}
const TempDirectoryLive = Layer.scoped(
TempDirectory,
Effect.gen(function* () {
const path = `/tmp/app-${Date.now()}`
console.log(`[TempDir] Creating: ${path}`)
// Финализатор — будет вызван при закрытии Scope
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
console.log(`[TempDir] Removing: ${path}`)
})
)
return { path }
})
)
Порядок финализации
Финализаторы выполняются в обратном порядке — LIFO (Last In, First Out):
Создание: Config → Logger → Database → Cache
Финализация: Cache → Database → Logger → Config
Layer.function
Layer.function создаёт Layer, который преобразует один сервис в другой через чистую функцию.
Сигнатура
function function_<A extends Context.Tag<any, any>, B extends Context.Tag<any, any>>(
tagA: A,
tagB: B,
f: (a: Context.Tag.Service<A>) => Context.Tag.Service<B>
): Layer<Context.Tag.Identifier<B>, never, Context.Tag.Identifier<A>>
Когда использовать
- Нужно адаптировать один сервис к интерфейсу другого
- Преобразование чистое (без побочных эффектов)
- Паттерн Adapter
Примеры
// Внутренний сервис с одним интерфейсом
class InternalLogger extends Context.Tag("InternalLogger")<
InternalLogger,
{
readonly write: (level: string, message: string) => Effect.Effect<void>
}
>() {}
// Публичный интерфейс логгера
class AppLogger extends Context.Tag("AppLogger")<
AppLogger,
{
readonly info: (message: string) => Effect.Effect<void>
readonly error: (message: string) => Effect.Effect<void>
readonly debug: (message: string) => Effect.Effect<void>
}
>() {}
// Адаптируем InternalLogger к AppLogger
const AppLoggerFromInternal = Layer.function(InternalLogger, AppLogger, (internal) => ({
info: (msg) => internal.write("INFO", msg),
error: (msg) => internal.write("ERROR", msg),
debug: (msg) => internal.write("DEBUG", msg)
}))
// Тип: Layer<AppLogger, never, InternalLogger>
// Можно скомпоновать с реальным InternalLogger
const InternalLoggerLive = Layer.succeed(InternalLogger, {
write: (level, message) => Effect.log(`[${level}] ${message}`)
})
const AppLoggerLive = AppLoggerFromInternal.pipe(
Layer.provide(InternalLoggerLive)
)
// Тип: Layer<AppLogger, never, never>
Layer.context
Layer.context создаёт Layer, который запрашивает контекст R и предоставляет его как есть. По сути, это identity Layer.
Сигнатура
function context<R>(): Layer<R, never, R>
Когда использовать
- Нужно явно передать зависимости «как есть»
- Компоновка с другими Layer, где часть зависимостей просто пробрасывается
Пример
class Config extends Context.Tag("Config")<Config, { readonly port: number }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => void }>() {}
// Identity layer — просто пробрасывает Config
const passthrough = Layer.context<Config>()
// Тип: Layer<Config, never, Config>
// Полезно при merge: собираем Layer, который пробрасывает Config и добавляет Logger
const combined = Layer.merge(
passthrough,
Layer.succeed(Logger, { log: console.log })
)
// Тип: Layer<Config | Logger, never, Config>
Layer.succeedContext
Layer.succeedContext создаёт Layer из готового объекта Context.
Сигнатура
function succeedContext<R>(context: Context.Context<R>): Layer<R>
Пример
class Config extends Context.Tag("Config")<Config, { readonly port: number }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => void }>() {}
// Создаём Context вручную
const ctx = Context.empty().pipe(
Context.add(Config, { port: 8080 }),
Context.add(Logger, { log: console.log })
)
// Layer из готового контекста
const AppLayer = Layer.succeedContext(ctx)
// Тип: Layer<Config | Logger, never, never>
Effect.Service API
Начиная с Effect 3.x, существует упрощённый способ определения сервисов — Effect.Service. Он объединяет определение Tag, реализацию и Layer в одном месте.
Базовый синтаксис
class MyService extends Effect.Service<MyService>()("MyService", {
// Определение реализации (одно из):
succeed: { /* статическое значение */ },
// ИЛИ
sync: () => { /* синхронная инициализация */ },
// ИЛИ
effect: Effect.gen(function* () { /* эффектная инициализация */ }),
// ИЛИ
scoped: Effect.gen(function* () { /* scoped инициализация */ }),
// Опционально: зависимости
dependencies: [OtherService.Default]
}) {}
Генерируемые Layer
| Layer | Описание |
|---|---|
MyService.Default | Layer с включёнными зависимостями |
MyService.DefaultWithoutDependencies | Layer без зависимостей (они должны быть предоставлены отдельно) |
Полный пример
// === Config Service ===
class Config extends Effect.Service<Config>()("Config", {
succeed: {
port: 3000,
host: "localhost",
dbUrl: "postgres://localhost:5432/app"
}
}) {}
// === Logger Service ===
class Logger extends Effect.Service<Logger>()("Logger", {
effect: Effect.gen(function* () {
const config = yield* Config
return {
info: (msg: string) => Effect.log(`[${config.host}] INFO: ${msg}`),
error: (msg: string) => Effect.log(`[${config.host}] ERROR: ${msg}`)
}
}),
dependencies: [Config.Default]
}) {}
// === Database Service ===
class Database extends Effect.Service<Database>()("Database", {
scoped: Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
const connection = yield* Effect.acquireRelease(
Effect.gen(function* () {
yield* logger.info(`Connecting to ${config.dbUrl}`)
return { url: config.dbUrl, connected: true as const }
}),
(conn) =>
Effect.gen(function* () {
yield* logger.info(`Disconnecting from ${conn.url}`)
})
)
return {
query: (sql: string) =>
Effect.gen(function* () {
yield* logger.info(`SQL: ${sql}`)
return { rows: [], connection: connection.url }
})
}
}),
dependencies: [Config.Default, Logger.Default]
}) {}
// === Использование ===
const program = Effect.gen(function* () {
const db = yield* Database
const result = yield* db.query("SELECT 1")
return result
})
// Database.Default уже содержит все зависимости!
Effect.runPromise(Effect.provide(program, Database.Default)).then(console.log)
Мокирование через Effect.Service
// Мок через конструктор
const dbMock = new Database({
query: (sql) => Effect.succeed({ rows: [{ mock: true }], connection: "mock" })
})
const testProgram = program.pipe(
Effect.provideService(Database, dbMock)
)
Layer.launch
Layer.launch конвертирует Layer в Effect, конструирует его и держит alive до прерывания. Идеально подходит для серверов.
Сигнатура
function launch<ROut, E, RIn>(
self: Layer<ROut, E, RIn>
): Effect<never, E, RIn>
Пример
class HttpServer extends Context.Tag("HttpServer")<HttpServer, void>() {}
const HttpServerLive = Layer.scoped(
HttpServer,
Effect.gen(function* () {
yield* Effect.acquireRelease(
Effect.sync(() => console.log("Server started on :3000")),
() => Effect.sync(() => console.log("Server stopped"))
)
yield* Effect.addFinalizer(() =>
Console.log("Cleaning up server resources...")
)
})
)
// launch конструирует Layer и держит его пока не прервут
Effect.runFork(Layer.launch(HttpServerLive))
// Output: Server started on :3000
// (процесс не завершается, ждёт прерывания)
Layer.tap и Layer.tapError
Позволяют выполнять побочные эффекты при успешном или неуспешном конструировании Layer без изменения его типа.
Сигнатуры
function tap<ROut, XR>(
self: Layer<ROut, E, RIn>,
f: (context: Context.Context<ROut>) => Effect<any, never, XR>
): Layer<ROut, E, RIn | XR>
function tapError<E, XE, XR>(
self: Layer<ROut, E, RIn>,
f: (error: E) => Effect<any, XE, XR>
): Layer<ROut, E | XE, RIn | XR>
Пример
class HttpServer extends Context.Tag("HttpServer")<HttpServer, void>() {}
const HttpServerLive = Layer.effect(
HttpServer,
Effect.gen(function* () {
const host = yield* Config.string("HOST")
console.log(`Listening on ${host}`)
})
).pipe(
Layer.tap((_ctx) =>
Console.log("Layer construction succeeded")
),
Layer.tapError((error) =>
Console.log(`Layer construction failed: ${error}`)
)
)
Обработка ошибок: catchAll, orElse
Layer.catchAll
Перехватывает ошибку конструирования и возвращает fallback Layer:
class Database extends Context.Tag("Database")<
Database,
{ readonly type: string }
>() {}
class DbError extends Data.TaggedError("DbError")<{
readonly message: string
}> {}
const PostgresLayer: Layer.Layer<Database, DbError> = Layer.effect(
Database,
Effect.gen(function* () {
return yield* Effect.fail(new DbError({ message: "Connection refused" }))
})
)
const SqliteLayer: Layer.Layer<Database> = Layer.succeed(Database, {
type: "sqlite"
})
// При ошибке PostgresLayer — переключаемся на SQLite
const DatabaseLive = PostgresLayer.pipe(
Layer.catchAll((error) => {
console.log(`Postgres failed: ${error.message}, falling back to SQLite`)
return SqliteLayer
})
)
// Тип: Layer<Database, never, never>
Layer.orElse
Упрощённый fallback без доступа к ошибке:
const DatabaseLive2 = PostgresLayer.pipe(
Layer.orElse(() => SqliteLayer)
)
Layer.retry
Повторяет попытку конструирования Layer по заданному Schedule:
class ExternalApi extends Context.Tag("ExternalApi")<
ExternalApi,
{ readonly call: (endpoint: string) => Effect.Effect<unknown> }
>() {}
class ApiError extends Data.TaggedError("ApiError")<{
readonly status: number
}> {}
const ExternalApiLive = Layer.effect(
ExternalApi,
Effect.gen(function* () {
yield* Effect.log("Attempting to connect to external API...")
// Симуляция подключения
return {
call: (endpoint: string) => Effect.succeed({ data: endpoint })
}
})
).pipe(
// Повторять до 3 раз с экспоненциальной задержкой
Layer.retry(Schedule.exponential("1 second").pipe(Schedule.compose(Schedule.recurs(3))))
)
Сравнение конструкторов
| Конструктор | Зависимости | Эффекты | Lifecycle | Тип |
|---|---|---|---|---|
Layer.succeed | ❌ | ❌ | ❌ | Layer<A> |
Layer.sync | ❌ | Синхронные | ❌ | Layer<A> |
Layer.fail | ❌ | ❌ | ❌ | Layer<never, E> |
Layer.effect | ✅ | ✅ | ❌ | Layer<A, E, R> |
Layer.scoped | ✅ | ✅ | ✅ | Layer<A, E, R> |
Layer.function | ✅ (один) | ❌ | ❌ | Layer<B, never, A> |
Layer.context | ✅ (pass) | ❌ | ❌ | Layer<R, never, R> |
Layer.succeedContext | ❌ | ❌ | ❌ | Layer<R> |
Effect.Service | ✅ | ✅ | ✅ | Генерирует Layer автоматически |
Примеры
Пример: Production-ready сервисы
// === Typed Errors ===
class ConfigError extends Data.TaggedError("ConfigError")<{
readonly key: string
readonly message: string
}> {}
class RedisError extends Data.TaggedError("RedisError")<{
readonly command: string
readonly cause: string
}> {}
// === Services ===
class Config extends Context.Tag("Config")<
Config,
{
readonly get: (key: string) => Effect.Effect<string, ConfigError>
}
>() {}
class Redis extends Context.Tag("Redis")<
Redis,
{
readonly get: (key: string) => Effect.Effect<string | null, RedisError>
readonly set: (key: string, value: string, ttl?: number) => Effect.Effect<void, RedisError>
}
>() {}
class Cache extends Context.Tag("Cache")<
Cache,
{
readonly lookup: <A>(
key: string,
compute: Effect.Effect<A>
) => Effect.Effect<A, RedisError>
}
>() {}
// === Layer Implementations ===
// Config из environment
const ConfigLive = Layer.sync(Config, () => ({
get: (key: string) => {
const value = process.env[key]
return value !== undefined
? Effect.succeed(value)
: Effect.fail(new ConfigError({ key, message: `${key} not found in environment` }))
}
}))
// Redis с lifecycle
const RedisLive = Layer.scoped(
Redis,
Effect.gen(function* () {
const config = yield* Config
const url = yield* config.get("REDIS_URL").pipe(
Effect.catchAll(() => Effect.succeed("redis://localhost:6379"))
)
const client = yield* Effect.acquireRelease(
Effect.sync(() => {
console.log(`[Redis] Connecting to ${url}`)
return { url, store: new Map<string, string>() }
}),
(client) =>
Effect.sync(() => {
console.log(`[Redis] Disconnecting from ${client.url}`)
client.store.clear()
})
)
return {
get: (key) =>
Effect.sync(() => client.store.get(key) ?? null),
set: (key, value, _ttl) =>
Effect.sync(() => { client.store.set(key, value) })
}
})
)
// Тип: Layer<Redis, never, Config>
// Cache с паттерном cache-aside
const CacheLive = Layer.effect(
Cache,
Effect.gen(function* () {
const redis = yield* Redis
return {
lookup: <A>(key: string, compute: Effect.Effect<A>) =>
Effect.gen(function* () {
const cached = yield* redis.get(key)
if (cached !== null) {
return JSON.parse(cached) as A
}
const value = yield* compute
yield* redis.set(key, JSON.stringify(value))
return value
})
}
})
)
// Тип: Layer<Cache, never, Redis>
Упражнения
Выбор правильного конструктора
Для каждого сценария определите, какой конструктор Layer использовать, и реализуйте:
// 1. Сервис генерации UUID (без зависимостей, без побочных эффектов)
// 2. Сервис текущего времени (значение вычисляется при инициализации)
// 3. Сервис форматирования (зависит от Config, нет lifecycle)
// 4. HTTP-клиент (зависит от Config, имеет lifecycle — open/close)
// 1. UUID — Layer.succeed (чистое значение)
class UuidService extends Context.Tag("UuidService")<
UuidService,
{ readonly generate: () => Effect.Effect<string> }
>() {}
const UuidServiceLive = Layer.succeed(UuidService, {
generate: () => Effect.sync(() => crypto.randomUUID())
})
// 2. Clock — Layer.sync (вычисление при инициализации)
class Clock extends Context.Tag("Clock")<
Clock,
{ readonly startedAt: number; readonly now: () => Effect.Effect<number> }
>() {}
const ClockLive = Layer.sync(Clock, () => ({
startedAt: Date.now(),
now: () => Effect.sync(() => Date.now())
}))
// 3. Formatter — Layer.effect (зависимость от Config)
class Config extends Context.Tag("Config")<Config, { readonly locale: string }>() {}
class Formatter extends Context.Tag("Formatter")<
Formatter,
{ readonly formatDate: (date: Date) => Effect.Effect<string> }
>() {}
const FormatterLive = Layer.effect(
Formatter,
Effect.gen(function* () {
const config = yield* Config
return {
formatDate: (date) =>
Effect.succeed(date.toLocaleDateString(config.locale))
}
})
)
// 4. HttpClient — Layer.scoped (lifecycle)
class HttpClient extends Context.Tag("HttpClient")<
HttpClient,
{ readonly get: (url: string) => Effect.Effect<string> }
>() {}
const HttpClientLive = Layer.scoped(
HttpClient,
Effect.gen(function* () {
const config = yield* Config
yield* Effect.acquireRelease(
Effect.log(`[HttpClient] Initialized for locale: ${config.locale}`),
() => Effect.log("[HttpClient] Closed")
)
return {
get: (url) => Effect.succeed(`Response from ${url}`)
}
})
)Effect.Service с тестированием
Реализуйте NotificationService через Effect.Service с двумя стратегиями отправки:
// NotificationService с методами:
// send(to: string, message: string): Effect.Effect<void, NotificationError>
// sendBatch(messages: ReadonlyArray<{to: string, message: string}>): Effect.Effect<number, NotificationError>
//
// Зависимости: Logger, Config
//
// Требования:
// 1. Реализуйте через Effect.Service
// 2. Создайте тестовый мок через конструктор
// 3. Напишите программу, которая отправляет 3 уведомления
class NotificationError extends Data.TaggedError("NotificationError")<{
readonly reason: string
}>() {}
class Logger extends Effect.Service<Logger>()("Logger", {
succeed: {
info: (msg: string) => Effect.log(`[INFO] ${msg}`),
error: (msg: string) => Effect.log(`[ERROR] ${msg}`)
}
}) {}
class Config extends Effect.Service<Config>()("Config", {
succeed: {
smtpHost: "smtp.example.com",
maxBatchSize: 100
}
}) {}
class NotificationService extends Effect.Service<NotificationService>()("NotificationService", {
effect: Effect.gen(function* () {
const logger = yield* Logger
const config = yield* Config
return {
send: (to: string, message: string) =>
Effect.gen(function* () {
yield* logger.info(`Sending to ${to} via ${config.smtpHost}: ${message}`)
}),
sendBatch: (messages: ReadonlyArray<{ to: string; message: string }>) =>
Effect.gen(function* () {
if (messages.length > config.maxBatchSize) {
return yield* Effect.fail(
new NotificationError({ reason: `Batch size ${messages.length} exceeds max ${config.maxBatchSize}` })
)
}
for (const msg of messages) {
yield* logger.info(`[Batch] → ${msg.to}: ${msg.message}`)
}
return messages.length
})
}
}),
dependencies: [Logger.Default, Config.Default]
}) {}
// === Production ===
const program = Effect.gen(function* () {
const ns = yield* NotificationService
yield* ns.send("alice@example.com", "Welcome!")
const count = yield* ns.sendBatch([
{ to: "bob@example.com", message: "Update 1" },
{ to: "carol@example.com", message: "Update 2" }
])
return count
})
Effect.runPromise(Effect.provide(program, NotificationService.Default)).then(console.log)
// === Test с mock ===
const mockNotification = new NotificationService({
send: (_to, _message) => Effect.void,
sendBatch: (messages) => Effect.succeed(messages.length)
})
const testProgram = program.pipe(
Effect.provideService(NotificationService, mockNotification)
)
Effect.runPromise(testProgram).then((n) => console.log(`Sent: ${n}`))Layer.scoped с graceful shutdown
Создайте WebSocket-сервер как Layer с полным lifecycle:
// WebSocketServer с:
// - Инициализация: открытие порта, логирование
// - Финализация: закрытие всех соединений, остановка сервера
// - Зависимости: Config, Logger
// - Ошибка конструирования: PortInUseError
//
// Требования:
// 1. Используйте Layer.scoped
// 2. acquireRelease для сервера
// 3. addFinalizer для дополнительной очистки
// 4. Layer.retry при PortInUseError (3 попытки)
// 5. Layer.tap для логирования успеха конструирования
class PortInUseError extends Data.TaggedError("PortInUseError")<{
readonly port: number
}>() {}
class Config extends Context.Tag("Config")<
Config,
{ readonly wsPort: number; readonly maxConnections: number }
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
class WebSocketServer extends Context.Tag("WebSocketServer")<
WebSocketServer,
{
readonly broadcast: (message: string) => Effect.Effect<void>
readonly connectionCount: () => Effect.Effect<number>
}
>() {}
let attemptCount = 0
const WebSocketServerLive = Layer.scoped(
WebSocketServer,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
attemptCount++
yield* logger.log(`[WS] Attempt #${attemptCount} to start on port ${config.wsPort}`)
// Simulate: first attempt might fail
if (attemptCount === 1 && config.wsPort === 8080) {
// Пропускаем fail для демонстрации
}
// Acquire: start server
const server = yield* Effect.acquireRelease(
Effect.gen(function* () {
yield* logger.log(`[WS] Server started on port ${config.wsPort}`)
return {
port: config.wsPort,
connections: new Set<string>(),
running: true
}
}),
(srv) =>
Effect.gen(function* () {
yield* logger.log(`[WS] Closing ${srv.connections.size} connections`)
srv.connections.clear()
yield* logger.log(`[WS] Server on port ${srv.port} stopped`)
})
)
// Additional cleanup
yield* Effect.addFinalizer((exit) =>
logger.log(`[WS] Finalizer executed. Exit: ${exit._tag}`)
)
return {
broadcast: (message: string) =>
Effect.gen(function* () {
yield* logger.log(`[WS] Broadcasting to ${server.connections.size} clients: ${message}`)
}),
connectionCount: () =>
Effect.succeed(server.connections.size)
}
})
).pipe(
Layer.retry(Schedule.recurs(3)),
Layer.tap((_ctx) => Effect.log("[WS] Layer construction completed successfully"))
)
// === Composition ===
const ConfigLive = Layer.succeed(Config, {
wsPort: 8080,
maxConnections: 1000
})
const LoggerLive = Layer.succeed(Logger, {
log: (msg) => Effect.log(msg)
})
const AppLayer = WebSocketServerLive.pipe(
Layer.provide(Layer.merge(ConfigLive, LoggerLive))
)
// === Program ===
const program = Effect.gen(function* () {
const ws = yield* WebSocketServer
yield* ws.broadcast("Hello, clients!")
const count = yield* ws.connectionCount()
return `Broadcasting to ${count} clients`
})
Effect.runPromise(Effect.provide(program, AppLayer)).then(console.log)🔗 Далее: Композиция Layer: provide, provideMerge — как собирать Layer в единый граф зависимостей