Layer<ROut, E, RIn>
Layer — это декларативный blueprint для конструирования сервисов.
Теория
Зачем нужен Layer
В предыдущем модуле мы узнали, как определять сервисы через Context.Tag и предоставлять их с помощью Effect.provideService. Этот подход работает для простых случаев, но в production-системах сервисы редко существуют изолированно. Типичная ситуация:
DatabaseService зависит от ConfigService и LoggerService
LoggerService зависит от ConfigService
CacheService зависит от ConfigService и DatabaseService
Если мы попробуем вручную передавать зависимости через Effect.provideService, то столкнёмся с рядом проблем:
- Утечка деталей реализации — интерфейс сервиса начинает содержать информацию о своих зависимостях
- Ручное управление порядком инициализации — мы должны сами следить, что Config создан раньше Logger
- Дублирование инстансов — один и тот же Config может быть создан несколько раз
- Отсутствие управления ресурсами — нет гарантии, что соединения будут закрыты при завершении
Layer решает все эти проблемы, предоставляя декларативный способ описания графа зависимостей с автоматическим разрешением, мемоизацией и управлением жизненным циклом.
Ментальная модель
Представьте Layer как рецепт или чертёж для создания сервиса:
┌─────────────────────────────────────────────────┐
│ Layer │
│ │
│ RequirementsIn ──────► [Construction] ──────► RequirementsOut │
│ (ингредиенты) (процесс) (готовый продукт) │
│ │ │
│ ▼ │
│ Error │
│ (что может │
│ пойти не так) │
└─────────────────────────────────────────────────┘
Важно понимать: Layer — это описание, а не результат. Он не выполняет конструирование до тех пор, пока не будет предоставлен программе через Effect.provide. Это ключевое свойство — Layer ленив.
Разница между Layer и Effect
Новички часто путают Layer и Effect. Вот принципиальные различия:
Effect<A, E, R> — описание вычисления, которое:
• Производит значение типа A
• Может завершиться ошибкой типа E
• Требует окружение типа R
• Выполняется КАЖДЫЙ раз при запуске
Layer<ROut, E, RIn> — описание конструирования сервиса, которое:
• Производит сервис типа ROut (добавляет в Context)
• Может завершиться ошибкой типа E при конструировании
• Требует сервисы типа RIn для конструирования
• Выполняется ОДИН раз (мемоизация по умолчанию)
Ещё одно важное различие — направление потока данных. В Effect<A, E, R> тип R стоит на третьем месте и означает “что мне нужно”. В Layer<ROut, E, RIn> тип ROut стоит на первом месте и означает “что я произвожу”, а RIn на третьем — “что мне нужно для производства”.
Концепция ФП
Layer как морфизм в категории Context
С точки зрения теории категорий, Layer можно рассматривать как морфизм (стрелку) между контекстами:
Layer<ROut, E, RIn> : Context<RIn> ──► Context<ROut>
Это объясняет, почему Layer поддерживает композицию — морфизмы в категории естественно компонуются:
Layer<A, never, B> : Context<B> ──► Context<A>
Layer<B, never, C> : Context<C> ──► Context<B>
─────────────────────────────────────────────────
Layer<A, never, C> : Context<C> ──► Context<A> (вертикальная композиция)
Layer как Reader + Resource
Если провести аналогию с классическими паттернами ФП:
- Reader Monad предоставляет доступ к окружению — Layer делает то же через
RIn - Resource Monad управляет жизненным циклом — Layer делает то же через интеграцию с
Scope - Coproduct / Sum описывает объединение — Layer использует union types для
RInиROut
Layer объединяет все три концепции в единую абстракцию, предоставляя type-safe конструирование с управлением зависимостями и ресурсами.
Принцип инверсии зависимостей (DIP)
Layer — это реализация Dependency Inversion Principle из SOLID на уровне типов:
БЕЗ LAYER С LAYER
┌──────────────────┐ ┌──────────────────┐
│ Application │ │ Application │
│ │ │ │
│ new Database() │ │ yield* Database │
│ new Logger() │ │ yield* Logger │
└──────────────────┘ └────────┬─────────┘
│ │
▼ ▼
Конкретные классы Абстракции (Tag/Interface)
(жёсткая связь) │
▼
Layer (конструктор)
(связывание на границе)
Программный код зависит только от абстракций (Tag), а конкретные реализации предоставляются через Layer на границе приложения.
Анатомия типа Layer
Сигнатура типа
// Полная сигнатура
type Layer<out ROut, out E = never, in RIn = never>
Разберём каждый параметр типа:
┌─── Сервис(ы), которые Layer производит (ковариантный)
│ ┌─── Ошибка при конструировании (ковариантный)
│ │ ┌─── Зависимости для конструирования (контравариантный)
▼ ▼ ▼
Layer<ROut, E, RIn>
| Параметр | Вариантность | По умолчанию | Описание |
|---|---|---|---|
ROut | out (ковариантный) | — | Сервис(ы), добавляемые в Context |
E | out (ковариантный) | never | Тип ошибки при конструировании |
RIn | in (контравариантный) | never | Требуемые зависимости |
Примеры типов
// Layer без зависимостей и без ошибок
// "Я произвожу Config, мне ничего не нужно"
type SimpleLayer = Layer.Layer<Config>
// Эквивалент: Layer.Layer<Config, never, never>
// Layer с зависимостями
// "Я произвожу Logger, мне нужен Config"
type DependentLayer = Layer.Layer<Logger, never, Config>
// Layer с возможной ошибкой
// "Я произвожу Database, могу упасть с DbError, мне нужны Config и Logger"
type FailableLayer = Layer.Layer<Database, DbError, Config | Logger>
// Layer производящий несколько сервисов
// "Я произвожу и Config, и Logger одновременно"
type MultiOutputLayer = Layer.Layer<Config | Logger>
Вариантность на практике
Ковариантность ROut означает, что Layer производящий больше сервисов можно использовать там, где нужен Layer производящий меньше:
class Config extends Context.Tag("Config")<Config, { readonly port: number }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => Effect.Effect<void> }>() {}
declare const fullLayer: Layer.Layer<Config | Logger>
// Можно использовать там, где нужен только Config
// потому что ROut ковариантен
const program: Effect.Effect<void, never, Config> = Effect.gen(function* () {
const config = yield* Config
console.log(config.port)
})
// fullLayer предоставляет Config | Logger, что покрывает Config
const runnable = Effect.provide(program, fullLayer)
Контравариантность RIn означает, что Layer требующий меньше зависимостей можно использовать там, где ожидается Layer с большими требованиями — ведь он сможет работать в более богатом окружении.
Связь с Context и Tag
От Tag к Layer
В Module 03 мы определяли сервисы и предоставляли их напрямую:
class Config extends Context.Tag("Config")<
Config,
{ readonly port: number; readonly host: string }
>() {}
// Прямое предоставление — работает для простых случаев
const program = Effect.gen(function* () {
const config = yield* Config
return `${config.host}:${config.port}`
})
const runnable = Effect.provideService(program, Config, {
port: 3000,
host: "localhost"
})
Layer абстрагирует этот процесс, позволяя описать как создать сервис:
class Config extends Context.Tag("Config")<
Config,
{ readonly port: number; readonly host: string }
>() {}
// Layer описывает КАК создать Config
const ConfigLive: Layer.Layer<Config> = Layer.succeed(Config, {
port: 3000,
host: "localhost"
})
// Программа не знает, как Config создаётся
const program = Effect.gen(function* () {
const config = yield* Config
return `${config.host}:${config.port}`
})
// Подключение Layer к программе
const runnable = Effect.provide(program, ConfigLive)
Naming Convention
В Effect-сообществе принято соглашение об именах:
| Суффикс | Назначение | Пример |
|---|---|---|
Live | Production-реализация | DatabaseLive |
Test | Тестовая реализация | DatabaseTest |
Mock | Mock с контролируемым поведением | DatabaseMock |
Dev | Версия для локальной разработки | DatabaseDev |
class Database extends Context.Tag("Database")<
Database,
{
readonly query: (sql: string) => Effect.Effect<unknown>
}
>() {}
// Production: реальное подключение к PostgreSQL
const DatabaseLive: Layer.Layer<Database, never, Config> = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config
// ... реальное подключение
return { query: (sql) => Effect.succeed({ rows: [] }) }
})
)
// Test: in-memory реализация
const DatabaseTest: Layer.Layer<Database> = Layer.succeed(Database, {
query: (sql) => Effect.succeed({ rows: [{ id: 1 }] })
})
Граф зависимостей
Визуализация
Рассмотрим типичное приложение с несколькими сервисами:
┌──────────┐
│ Config │ Layer<Config>
│ (no deps)│
└────┬─────┘
│
┌──────────┴──────────┐
│ │
┌─────▼─────┐ ┌─────▼─────┐
│ Logger │ │ Cache │
│ needs: │ │ needs: │
│ Config │ │ Config │
└─────┬─────┘ └─────┬─────┘
│ │
└──────────┬──────────┘
│
┌─────▼─────┐
│ Database │
│ needs: │
│ Config │
│ Logger │
│ Cache │
└───────────┘
Каждый узел в этом графе — это Layer. Стрелки показывают направление зависимостей. Layer автоматически:
- Топологически сортирует зависимости — Config создаётся первым
- Мемоизирует — Config создаётся один раз и разделяется между Logger, Cache и Database
- Управляет жизненным циклом — ресурсы освобождаются в обратном порядке
Типы в графе
class Config extends Context.Tag("Config")<
Config,
{ readonly dbUrl: string; readonly cacheUrl: string }
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
class Cache extends Context.Tag("Cache")<
Cache,
{ readonly get: (key: string) => Effect.Effect<string | null> }
>() {}
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>() {}
// Типы Layer отражают граф:
const ConfigLive: Layer.Layer<Config> = Layer.succeed(Config, { dbUrl: "postgres://...", cacheUrl: "redis://..." })
const LoggerLive: Layer.Layer<Logger, never, Config> = Layer.effect(Logger, Effect.gen(function* () { /* ... */ yield* Config; return { log: (msg) => Effect.log(msg) } }))
const CacheLive: Layer.Layer<Cache, never, Config> = Layer.effect(Cache, Effect.gen(function* () { /* ... */ yield* Config; return { get: (_key) => Effect.succeed(null) } }))
const DatabaseLive: Layer.Layer<Database, never, Config | Logger | Cache> = Layer.effect(Database, Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
const cache = yield* Cache
return {
query: (sql) => Effect.gen(function* () {
yield* logger.log(`Executing: ${sql}`)
return { rows: [] }
})
}
}))
Жизненный цикл Layer
Этапы
1. Описание (Definition Time)
────────────────────────────
const MyLayer = Layer.effect(Tag, ...)
→ Создаётся blueprint, ничего не выполняется
2. Предоставление (Provision Time)
────────────────────────────────
Effect.provide(program, MyLayer)
→ Layer привязывается к программе, но всё ещё не выполняется
3. Конструирование (Construction Time)
────────────────────────────────────
Effect.runPromise(runnable)
→ Layer выполняется: зависимости разрешаются, сервисы создаются
→ Ресурсы приобретаются (acquire)
→ Мемоизация: каждый Layer конструируется ровно один раз
4. Использование (Usage Time)
──────────────────────────
yield* MyService
→ Программа получает доступ к сконструированным сервисам
5. Финализация (Finalization Time)
───────────────────────────────
→ Программа завершается (успешно или с ошибкой)
→ Scope закрывается
→ Ресурсы освобождаются в обратном порядке (release)
Scope и Layer
Layer тесно интегрирован с Scope. Когда Layer использует ресурсы (например, соединение с базой данных), он может объявить acquire/release через Layer.scoped:
class DbConnection extends Context.Tag("DbConnection")<
DbConnection,
{ readonly execute: (sql: string) => Effect.Effect<unknown> }
>() {}
const DbConnectionLive: Layer.Layer<DbConnection> = Layer.scoped(
DbConnection,
Effect.gen(function* () {
// Acquire: открыть соединение
const connection = yield* Effect.acquireRelease(
Effect.sync(() => {
console.log("Opening DB connection")
return { raw: "connection-handle" }
}),
(conn) => Effect.sync(() => {
console.log(`Closing DB connection: ${conn.raw}`)
})
)
return {
execute: (sql) => Effect.sync(() => {
console.log(`Executing on ${connection.raw}: ${sql}`)
return { rows: [] }
})
}
})
)
При использовании Layer.scoped ресурс будет автоматически освобождён при закрытии Scope программы.
API Reference
Основные типы
// Основной тип Layer
interface Layer<out ROut, out E = never, in RIn = never> {
// Layer — это opaque тип, нет публичных полей
}
// Namespace Layer содержит все конструкторы и комбинаторы
namespace Layer {
// Конструкторы (подробно в следующей статье)
function succeed<T extends Context.Tag<any, any>>(
tag: T,
service: Context.Tag.Service<T>
): Layer<Context.Tag.Identifier<T>>
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>
function scoped<T extends Context.Tag<any, any>, E, R>(
tag: T,
effect: Effect<Context.Tag.Service<T>, E, Exclude<R, Scope>>
): Layer<Context.Tag.Identifier<T>, E, Exclude<R, Scope>>
// Композиция (подробно в статьях 03-05)
function provide<ROut2, E2, RIn2>(
self: Layer<ROut, E, RIn>,
that: Layer<ROut2, E2, RIn2>
): Layer<ROut, E | E2, RIn2 | Exclude<RIn, ROut2>>
function merge<ROut2, E2, RIn2>(
self: Layer<ROut, E, RIn>,
that: Layer<ROut2, E2, RIn2>
): Layer<ROut | ROut2, E | E2, RIn | RIn2>
// Мемоизация (подробно в статье 06)
function fresh<ROut, E, RIn>(
self: Layer<ROut, E, RIn>
): Layer<ROut, E, RIn>
function memoize<ROut, E, RIn>(
self: Layer<ROut, E, RIn>
): Effect<Layer<ROut, E, RIn>, never, Scope>
}
Предоставление Layer программе
// Основной способ — через Effect.provide
const runnable = Effect.provide(program, layer)
// Можно предоставлять несколько Layer
const runnable2 = program.pipe(
Effect.provide(layer1),
Effect.provide(layer2)
)
Примеры
Пример 1: Минимальный Layer
// 1. Определяем сервис
class Greeting extends Context.Tag("Greeting")<
Greeting,
{ readonly greet: (name: string) => string }
>() {}
// 2. Создаём Layer
const GreetingLive = Layer.succeed(Greeting, {
greet: (name) => `Hello, ${name}!`
})
// 3. Используем сервис в программе
const program = Effect.gen(function* () {
const greeting = yield* Greeting
return greeting.greet("Effect")
})
// 4. Связываем и запускаем
const runnable = Effect.provide(program, GreetingLive)
Effect.runPromise(runnable).then(console.log)
// Output: Hello, Effect!
Пример 2: Layer с зависимостью
// Сервис Config
class Config extends Context.Tag("Config")<
Config,
{
readonly logLevel: string
readonly appName: string
}
>() {}
// Сервис Logger зависит от Config
class Logger extends Context.Tag("Logger")<
Logger,
{
readonly info: (msg: string) => Effect.Effect<void>
readonly error: (msg: string) => Effect.Effect<void>
}
>() {}
// Layer для Config — не имеет зависимостей
const ConfigLive = Layer.succeed(Config, {
logLevel: "INFO",
appName: "MyApp"
})
// Layer для Logger — зависит от Config
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
return {
info: (msg) => Effect.log(`[${config.appName}] [${config.logLevel}] ${msg}`),
error: (msg) => Effect.log(`[${config.appName}] [ERROR] ${msg}`)
}
})
)
// Программа использует Logger
const program = Effect.gen(function* () {
const logger = yield* Logger
yield* logger.info("Application started")
yield* logger.info("Processing request")
})
// Собираем граф: LoggerLive нуждается в Config, предоставляем его
const AppLayer = LoggerLive.pipe(
Layer.provide(ConfigLive)
)
// AppLayer: Layer<Logger, never, never> — все зависимости разрешены
const runnable = Effect.provide(program, AppLayer)
Effect.runPromise(runnable)
Пример 3: Layer с обработкой ошибок
// Типизированная ошибка конструирования
class ConfigError extends Data.TaggedError("ConfigError")<{
readonly message: string
}>() {}
class Config extends Context.Tag("Config")<
Config,
{ readonly dbUrl: string }
>() {}
// Layer может завершиться ошибкой
const ConfigFromEnv: Layer.Layer<Config, ConfigError> = Layer.effect(
Config,
Effect.gen(function* () {
const dbUrl = process.env["DATABASE_URL"]
if (dbUrl === undefined) {
return yield* Effect.fail(
new ConfigError({ message: "DATABASE_URL is not set" })
)
}
return { dbUrl }
})
)
// Fallback Layer
const ConfigDefault: Layer.Layer<Config> = Layer.succeed(Config, {
dbUrl: "postgres://localhost:5432/dev"
})
// Используем catchAll для graceful degradation
const ConfigLive = ConfigFromEnv.pipe(
Layer.catchAll((_error) => ConfigDefault)
)
// ConfigLive: Layer<Config, never, never>
Упражнения
Определение простых Layer
Создайте три сервиса и соответствующие Layer:
// 1. TimeService — предоставляет текущее время
// Метод: now: () => Effect.Effect<Date>
// Layer: TimeServiceLive (без зависимостей)
// 2. RandomService — генерирует случайные числа
// Метод: nextInt: (max: number) => Effect.Effect<number>
// Layer: RandomServiceLive (без зависимостей)
// 3. IdGenerator — генерирует уникальные ID
// Метод: generate: () => Effect.Effect<string>
// Layer: IdGeneratorLive (зависит от TimeService и RandomService)
// Напишите программу, которая генерирует 3 ID и выводит их
class TimeService extends Context.Tag("TimeService")<
TimeService,
{ readonly now: () => Effect.Effect<Date> }
>() {}
class RandomService extends Context.Tag("RandomService")<
RandomService,
{ readonly nextInt: (max: number) => Effect.Effect<number> }
>() {}
class IdGenerator extends Context.Tag("IdGenerator")<
IdGenerator,
{ readonly generate: () => Effect.Effect<string> }
>() {}
const TimeServiceLive = Layer.succeed(TimeService, {
now: () => Effect.sync(() => new Date())
})
const RandomServiceLive = Layer.succeed(RandomService, {
nextInt: (max) => Effect.sync(() => Math.floor(Math.random() * max))
})
const IdGeneratorLive = Layer.effect(
IdGenerator,
Effect.gen(function* () {
const time = yield* TimeService
const random = yield* RandomService
return {
generate: () => Effect.gen(function* () {
const now = yield* time.now()
const rand = yield* random.nextInt(10000)
return `${now.getTime()}-${rand}`
})
}
})
)
const AppLayer = IdGeneratorLive.pipe(
Layer.provide(Layer.merge(TimeServiceLive, RandomServiceLive))
)
const program = Effect.gen(function* () {
const idGen = yield* IdGenerator
const id1 = yield* idGen.generate()
const id2 = yield* idGen.generate()
const id3 = yield* idGen.generate()
return [id1, id2, id3] as const
})
Effect.runPromise(Effect.provide(program, AppLayer)).then(console.log)Layer с ошибками и fallback
Реализуйте систему конфигурации с несколькими источниками:
// 1. ConfigFromFile — читает конфиг из файла (может упасть с FileError)
// 2. ConfigFromEnv — читает конфиг из env переменных (может упасть с EnvError)
// 3. ConfigDefault — дефолтная конфигурация (всегда успешна)
//
// Создайте ConfigLive, который:
// - Сначала пытается прочитать из файла
// - Если не удалось — из env переменных
// - Если и это не удалось — использует дефолтные значения
class FileError extends Data.TaggedError("FileError")<{
readonly path: string
}>() {}
class EnvError extends Data.TaggedError("EnvError")<{
readonly variable: string
}>() {}
class Config extends Context.Tag("Config")<
Config,
{
readonly port: number
readonly host: string
readonly source: string
}
>() {}
const ConfigFromFile: Layer.Layer<Config, FileError> = Layer.effect(
Config,
Effect.gen(function* () {
// Симуляция: файл не найден
return yield* Effect.fail(new FileError({ path: "/etc/app/config.json" }))
})
)
const ConfigFromEnv: Layer.Layer<Config, EnvError> = Layer.effect(
Config,
Effect.gen(function* () {
const port = process.env["APP_PORT"]
if (port === undefined) {
return yield* Effect.fail(new EnvError({ variable: "APP_PORT" }))
}
return {
port: parseInt(port, 10),
host: process.env["APP_HOST"] ?? "localhost",
source: "environment"
}
})
)
const ConfigDefault: Layer.Layer<Config> = Layer.succeed(Config, {
port: 3000,
host: "localhost",
source: "default"
})
// Каскадный fallback
const ConfigLive = ConfigFromFile.pipe(
Layer.catchAll((_fileError) =>
ConfigFromEnv.pipe(
Layer.catchAll((_envError) => ConfigDefault)
)
)
)
const program = Effect.gen(function* () {
const config = yield* Config
return `Server at ${config.host}:${config.port} (source: ${config.source})`
})
Effect.runPromise(Effect.provide(program, ConfigLive)).then(console.log)
// Output: Server at localhost:3000 (source: default)Полный граф зависимостей
Спроектируйте и реализуйте граф зависимостей для микросервиса:
// Сервисы:
// 1. Config — конфигурация (без зависимостей)
// 2. Logger — логирование (зависит от Config)
// 3. Metrics — метрики (зависит от Config)
// 4. DbPool — пул соединений (зависит от Config, Logger)
// 5. Cache — кэш (зависит от Config, Logger)
// 6. UserRepository — репозиторий (зависит от DbPool, Cache, Logger)
// 7. AuthService — аутентификация (зависит от UserRepository, Logger, Config)
//
// Требования:
// - Все Layer должны иметь корректные типы
// - Создайте MainLive, который разрешает все зависимости
// - Напишите программу, которая использует AuthService
// - Config должен быть создан ОДИН раз (мемоизация)
// === Service Definitions ===
class Config extends Context.Tag("Config")<
Config,
{ readonly dbUrl: string; readonly cacheUrl: string; readonly logLevel: string }
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (level: string, msg: string) => Effect.Effect<void> }
>() {}
class Metrics extends Context.Tag("Metrics")<
Metrics,
{ readonly increment: (metric: string) => Effect.Effect<void> }
>() {}
class DbPool extends Context.Tag("DbPool")<
DbPool,
{ readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>> }
>() {}
class Cache extends Context.Tag("Cache")<
Cache,
{ readonly get: (key: string) => Effect.Effect<string | null>; readonly set: (key: string, value: string) => Effect.Effect<void> }
>() {}
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{ readonly findById: (id: string) => Effect.Effect<{ readonly id: string; readonly name: string } | null> }
>() {}
class AuthService extends Context.Tag("AuthService")<
AuthService,
{ readonly authenticate: (token: string) => Effect.Effect<{ readonly userId: string; readonly role: string }> }
>() {}
// === Layer Implementations ===
const ConfigLive = Layer.succeed(Config, {
dbUrl: "postgres://localhost:5432/app",
cacheUrl: "redis://localhost:6379",
logLevel: "INFO"
})
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
return {
log: (level, msg) => Effect.log(`[${config.logLevel}/${level}] ${msg}`)
}
})
)
const MetricsLive = Layer.effect(
Metrics,
Effect.gen(function* () {
const config = yield* Config
return {
increment: (metric) => Effect.log(`[METRIC] ${metric} +1`)
}
})
)
const DbPoolLive = Layer.effect(
DbPool,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
yield* logger.log("INFO", `Connecting to ${config.dbUrl}`)
return {
query: (sql) => Effect.gen(function* () {
yield* logger.log("DEBUG", `SQL: ${sql}`)
return [{ id: "1", name: "Alice" }] as ReadonlyArray<unknown>
})
}
})
)
const CacheLive = Layer.effect(
Cache,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
yield* logger.log("INFO", `Connecting to cache: ${config.cacheUrl}`)
const store = new Map<string, string>()
return {
get: (key) => Effect.sync(() => store.get(key) ?? null),
set: (key, value) => Effect.sync(() => { store.set(key, value) })
}
})
)
const UserRepositoryLive = Layer.effect(
UserRepository,
Effect.gen(function* () {
const db = yield* DbPool
const cache = yield* Cache
const logger = yield* Logger
return {
findById: (id) => Effect.gen(function* () {
const cached = yield* cache.get(`user:${id}`)
if (cached !== null) {
yield* logger.log("DEBUG", `Cache hit for user:${id}`)
return JSON.parse(cached) as { readonly id: string; readonly name: string }
}
const rows = yield* db.query(`SELECT * FROM users WHERE id = '${id}'`)
const user = rows[0] as { readonly id: string; readonly name: string } | undefined
if (user !== undefined) {
yield* cache.set(`user:${id}`, JSON.stringify(user))
}
return user ?? null
})
}
})
)
const AuthServiceLive = Layer.effect(
AuthService,
Effect.gen(function* () {
const users = yield* UserRepository
const logger = yield* Logger
const config = yield* Config
return {
authenticate: (token) => Effect.gen(function* () {
yield* logger.log("INFO", `Authenticating token: ${token.slice(0, 8)}...`)
const user = yield* users.findById("1")
if (user === null) {
return yield* Effect.fail(new Error("User not found"))
}
return { userId: user.id, role: "admin" }
})
}
})
)
// === Layer Composition ===
// Базовые слои
const BaseLive = Layer.merge(ConfigLive, LoggerLive.pipe(Layer.provide(ConfigLive)))
// Инфраструктурные слои
const InfraLive = Layer.mergeAll(
DbPoolLive,
CacheLive,
MetricsLive
).pipe(Layer.provide(BaseLive))
// Доменные слои
const DomainLive = UserRepositoryLive.pipe(
Layer.provide(Layer.merge(InfraLive, BaseLive))
)
// Главный Layer
const MainLive = AuthServiceLive.pipe(
Layer.provide(
Layer.mergeAll(DomainLive, BaseLive, InfraLive)
)
)
// === Program ===
const program = Effect.gen(function* () {
const auth = yield* AuthService
const result = yield* auth.authenticate("bearer-token-12345678")
return result
})
Effect.runPromise(Effect.provide(program, MainLive)).then(console.log)🔗 Далее: Создание Layer — подробное руководство по всем конструкторам Layer