Горизонтальная композиция
Объединение нескольких независимых Layer.
Теория
Горизонтальная vs вертикальная композиция
В предыдущей статье мы изучили вертикальную композицию (provide), где выход одного Layer подключается ко входу другого. Горизонтальная композиция (merge) работает иначе — она объединяет два независимых Layer в один, который предоставляет оба набора сервисов:
Вертикальная (provide) Горизонтальная (merge)
────────────────────── ──────────────────────
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Logger │ │ Config │ │ Logger │
│ needs: │ │ (ROut) │ │ (ROut) │
│ Config │ └──────────┘ └──────────┘
└────┬─────┘ │ │
│ └─────┬──────┘
┌────▼─────┐ merge│
│ Config │ ┌────────▼────────┐
│ (source) │ │ Config | Logger │
└──────────┘ │ (combined) │
└─────────────────┘
"Последовательно" "Параллельно"
Когда использовать merge
Merge используется когда:
- Нужно объединить несколько независимых сервисов в один Layer
- Несколько Layer имеют общие зависимости, но не зависят друг от друга
- Нужно собрать «пакет» сервисов одного архитектурного слоя
- Финальная сборка MainLive из нескольких разрешённых Layer
Концепция ФП
Merge как Product в категории
С точки зрения теории категорий, Layer.merge создаёт произведение (product) двух Layer:
Layer<A, E1, R1> × Layer<B, E2, R2> = Layer<A | B, E1 | E2, R1 | R2>
Это аналог произведения типов (A & B или [A, B]), но на уровне контекстов:
- Выходы объединяются:
A | B(оба сервиса доступны) - Ошибки объединяются:
E1 | E2(любая из ошибок может возникнуть) - Зависимости объединяются:
R1 | R2(нужны все зависимости)
Ассоциативность и коммутативность
Layer.merge обладает важными алгебраическими свойствами:
Ассоциативность:
merge(merge(A, B), C) ≡ merge(A, merge(B, C))
Коммутативность (по выходу):
merge(A, B) производит A | B
merge(B, A) производит B | A
// A | B ≡ B | A в TypeScript, так что результат эквивалентен
Идентичность:
merge(A, Layer.empty) ≡ A
Эти свойства гарантируют, что порядок объединения не влияет на результат.
Layer.merge
Сигнатура
function merge<ROut, E, RIn, ROut2, E2, RIn2>(
self: Layer<ROut, E, RIn>,
that: Layer<ROut2, E2, RIn2>
): Layer<ROut | ROut2, E | E2, RIn | RIn2>
Разбор типов:
self: Layer<ROut, E, RIn> — первый Layer
that: Layer<ROut2, E2, RIn2> — второй Layer
Результат:
ROut = ROut | ROut2 (объединение выходов)
E = E | E2 (объединение ошибок)
RIn = RIn | RIn2 (объединение зависимостей)
Базовые примеры
class Config extends Context.Tag("Config")<
Config,
{ readonly port: number }
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{ readonly log: (msg: string) => Effect.Effect<void> }
>() {}
class Metrics extends Context.Tag("Metrics")<
Metrics,
{ readonly track: (event: string) => Effect.Effect<void> }
>() {}
// Три независимых Layer
const ConfigLive = Layer.succeed(Config, { port: 3000 })
const LoggerLive = Layer.succeed(Logger, { log: (msg) => Effect.log(msg) })
const MetricsLive = Layer.succeed(Metrics, { track: (event) => Effect.log(`[METRIC] ${event}`) })
// Merge двух Layer
const ConfigAndLogger = Layer.merge(ConfigLive, LoggerLive)
// Тип: Layer<Config | Logger, never, never>
// Программа может использовать оба сервиса
const program = Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
yield* logger.log(`Port: ${config.port}`)
})
Effect.runPromise(Effect.provide(program, ConfigAndLogger))
Merge Layer с зависимостями
Когда оба Layer имеют зависимости, они объединяются:
class Config extends Context.Tag("Config")<Config, { readonly env: string }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => Effect.Effect<void> }>() {}
class Database extends Context.Tag("Database")<Database, { readonly query: (sql: string) => Effect.Effect<unknown> }>() {}
class Cache extends Context.Tag("Cache")<Cache, { readonly get: (key: string) => Effect.Effect<string | null> }>() {}
// Logger зависит от Config
const LoggerLive: Layer.Layer<Logger, never, Config> = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
return { log: (msg) => Effect.log(`[${config.env}] ${msg}`) }
})
)
// Cache зависит от Config
const CacheLive: Layer.Layer<Cache, never, Config> = Layer.effect(
Cache,
Effect.gen(function* () {
const config = yield* Config
return { get: (_key) => Effect.succeed(null) }
})
)
// Merge: зависимости объединяются
const LoggerAndCache = Layer.merge(LoggerLive, CacheLive)
// Тип: Layer<Logger | Cache, never, Config>
// ↑ общая зависимость
Merge с разными зависимостями
class Config extends Context.Tag("Config")<Config, { readonly url: string }>() {}
class Credentials extends Context.Tag("Credentials")<Credentials, { readonly token: string }>() {}
class ApiClient extends Context.Tag("ApiClient")<ApiClient, { readonly call: (endpoint: string) => Effect.Effect<unknown> }>() {}
class AuthClient extends Context.Tag("AuthClient")<AuthClient, { readonly verify: (token: string) => Effect.Effect<boolean> }>() {}
// ApiClient зависит от Config
const ApiClientLive: Layer.Layer<ApiClient, never, Config> = Layer.effect(
ApiClient,
Effect.gen(function* () {
const config = yield* Config
return { call: (endpoint) => Effect.succeed({ url: config.url, endpoint }) }
})
)
// AuthClient зависит от Credentials
const AuthClientLive: Layer.Layer<AuthClient, never, Credentials> = Layer.effect(
AuthClient,
Effect.gen(function* () {
const creds = yield* Credentials
return { verify: (_token) => Effect.succeed(creds.token.length > 0) }
})
)
// Merge: разные зависимости
const ClientsLive = Layer.merge(ApiClientLive, AuthClientLive)
// Тип: Layer<ApiClient | AuthClient, never, Config | Credentials>
// ↑ ОБЕ зависимости
Pipe-стиль
// Функциональный стиль
const combined1 = Layer.merge(ConfigLive, LoggerLive)
// Pipe-стиль
const combined2 = ConfigLive.pipe(Layer.merge(LoggerLive))
// Цепочка merge через pipe
const combined3 = ConfigLive.pipe(
Layer.merge(LoggerLive),
Layer.merge(MetricsLive)
)
// Тип: Layer<Config | Logger | Metrics, never, never>
Layer.mergeAll
Сигнатура
function mergeAll<Layers extends ReadonlyArray<Layer.Layer<any, any, any>>>(
...layers: Layers
): Layer</* union of all ROut */, /* union of all E */, /* union of all RIn */>
mergeAll — variadic версия merge. Принимает произвольное количество 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> }>() {}
class Metrics extends Context.Tag("Metrics")<Metrics, { readonly track: (event: string) => Effect.Effect<void> }>() {}
class Tracer extends Context.Tag("Tracer")<Tracer, { readonly span: (name: string) => Effect.Effect<void> }>() {}
const ConfigLive = Layer.succeed(Config, { port: 3000 })
const LoggerLive = Layer.succeed(Logger, { log: (msg) => Effect.log(msg) })
const MetricsLive = Layer.succeed(Metrics, { track: (event) => Effect.log(`[METRIC] ${event}`) })
const TracerLive = Layer.succeed(Tracer, { span: (name) => Effect.log(`[SPAN] ${name}`) })
// mergeAll вместо цепочки merge
const AllServices = Layer.mergeAll(ConfigLive, LoggerLive, MetricsLive, TracerLive)
// Тип: Layer<Config | Logger | Metrics | Tracer, never, never>
// Эквивалент цепочки merge:
const AllServices2 = ConfigLive.pipe(
Layer.merge(LoggerLive),
Layer.merge(MetricsLive),
Layer.merge(TracerLive)
)
mergeAll с зависимостями
class Config extends Context.Tag("Config")<Config, { readonly env: string }>() {}
class S3 extends Context.Tag("S3")<S3, { readonly upload: (key: string) => Effect.Effect<void> }>() {}
class ElasticSearch extends Context.Tag("ElasticSearch")<ElasticSearch, { readonly index: (doc: unknown) => Effect.Effect<void> }>() {}
class Database extends Context.Tag("Database")<Database, { readonly query: (sql: string) => Effect.Effect<unknown> }>() {}
class S3Error extends Data.TaggedError("S3Error")<{ readonly key: string }> {}
class EsError extends Data.TaggedError("EsError")<{ readonly index: string }> {}
class DbError extends Data.TaggedError("DbError")<{ readonly sql: string }> {}
// Все три зависят от Config, каждый со своей ошибкой
const S3Live: Layer.Layer<S3, S3Error, Config> = Layer.effect(S3, Effect.gen(function* () {
yield* Config
return { upload: (key) => Effect.log(`[S3] Upload: ${key}`) }
}))
const ElasticSearchLive: Layer.Layer<ElasticSearch, EsError, Config> = Layer.effect(ElasticSearch, Effect.gen(function* () {
yield* Config
return { index: (doc) => Effect.log(`[ES] Index: ${JSON.stringify(doc)}`) }
}))
const DatabaseLive: Layer.Layer<Database, DbError, Config> = Layer.effect(Database, Effect.gen(function* () {
yield* Config
return { query: (sql) => Effect.succeed({ result: sql }) }
}))
// mergeAll объединяет всё
const InfrastructureLive = Layer.mergeAll(S3Live, ElasticSearchLive, DatabaseLive)
// Тип: Layer<S3 | ElasticSearch | Database, S3Error | EsError | DbError, Config>
// ↑ все три выхода ↑ все три ошибки ↑ общая зависимость
Поведение типов при merge
Автоматическая дедупликация зависимостей
TypeScript’s union type автоматически дедуплицирует одинаковые зависимости:
declare const layer1: Layer.Layer<"A", never, "Config" | "Logger">
declare const layer2: Layer.Layer<"B", never, "Config" | "Metrics">
const merged = Layer.merge(layer1, layer2)
// Тип: Layer<"A" | "B", never, "Config" | "Logger" | "Metrics">
// ↑ Config не дублируется
Перекрытие выходов
Если два Layer производят одинаковый сервис, последний «выигрывает» (правый приоритет):
class Config extends Context.Tag("Config")<
Config,
{ readonly source: string }
>() {}
const ConfigFromFile = Layer.succeed(Config, { source: "file" })
const ConfigFromEnv = Layer.succeed(Config, { source: "env" })
// При merge двух Layer с одинаковым выходом — тип один
const merged = Layer.merge(ConfigFromFile, ConfigFromEnv)
// Тип: Layer<Config, never, never>
// ⚠️ Какой Config будет использован? Зависит от реализации.
// На практике лучше избегать таких ситуаций.
⚠️ Важно: избегайте merge Layer, которые производят один и тот же сервис. Это может привести к непредсказуемому поведению. Если нужно выбрать между реализациями, используйте Layer.orElse или Layer.catchAll.
Комбинирование с provide
Типичный паттерн: merge + provide
Самый распространённый паттерн — объединить несколько базовых Layer через merge, затем предоставить их как зависимости верхнеуровневому Layer:
class Config extends Context.Tag("Config")<Config, { readonly env: string }>() {}
class Logger extends Context.Tag("Logger")<Logger, { readonly log: (msg: string) => Effect.Effect<void> }>() {}
class Database extends Context.Tag("Database")<Database, { readonly query: (sql: string) => Effect.Effect<unknown> }>() {}
const ConfigLive = Layer.succeed(Config, { env: "production" })
const LoggerLive = Layer.effect(Logger, Effect.gen(function* () {
const config = yield* Config
return { log: (msg) => Effect.log(`[${config.env}] ${msg}`) }
}))
const DatabaseLive = Layer.effect(Database, Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
return {
query: (sql) => Effect.gen(function* () {
yield* logger.log(`SQL: ${sql}`)
return { rows: [] }
})
}
}))
// Шаг 1: merge Config + Logger (после разрешения Config для Logger)
const BaseLive = Layer.merge(
ConfigLive,
LoggerLive.pipe(Layer.provide(ConfigLive))
)
// Тип: Layer<Config | Logger, never, never>
// Шаг 2: provide база для Database
const MainLive = DatabaseLive.pipe(
Layer.provide(BaseLive)
)
// Тип: Layer<Database, never, never>
Паттерн: mergeAll для тестирования
class S3 extends Context.Tag("S3")<S3, { readonly createBucket: Effect.Effect<{ name: string }> }>() {}
class ElasticSearch extends Context.Tag("ElasticSearch")<ElasticSearch, { readonly createIndex: Effect.Effect<{ id: string }> }>() {}
class Database extends Context.Tag("Database")<Database, { readonly createEntry: (bucket: string, index: string) => Effect.Effect<{ id: string }> }>() {}
// Тестовые реализации
const S3Test = Layer.succeed(S3, {
createBucket: Effect.succeed({ name: "test-bucket" })
})
const ElasticSearchTest = Layer.succeed(ElasticSearch, {
createIndex: Effect.succeed({ id: "test-index" })
})
const DatabaseTest = Layer.succeed(Database, {
createEntry: (bucket, index) => Effect.succeed({ id: `entry-${bucket}-${index}` })
})
// Все тестовые сервисы одной строкой
const TestLayer = Layer.mergeAll(S3Test, ElasticSearchTest, DatabaseTest)
// Тип: Layer<S3 | ElasticSearch | Database, never, never>
API Reference
Layer.merge [STABLE]
function merge<ROut, E, RIn, ROut2, E2, RIn2>(
self: Layer<ROut, E, RIn>,
that: Layer<ROut2, E2, RIn2>
): Layer<ROut | ROut2, E | E2, RIn | RIn2>
Объединяет два Layer. Результирующий Layer предоставляет сервисы обоих и требует зависимости обоих.
Layer.mergeAll [STABLE]
function mergeAll<Layers extends ReadonlyArray<Layer.Layer<any, any, any>>>(
...layers: Layers
): Layer</* union of all outputs */, /* union of all errors */, /* union of all inputs */>
Variadic merge — объединяет произвольное количество Layer.
Примеры
Пример: Сборка observability-стека
// === Observability Services ===
class Config extends Context.Tag("Config")<
Config,
{
readonly serviceName: string
readonly version: string
readonly metricsPort: number
}
>() {}
class Logger extends Context.Tag("Logger")<
Logger,
{
readonly info: (msg: string) => Effect.Effect<void>
readonly warn: (msg: string) => Effect.Effect<void>
readonly error: (msg: string, err?: unknown) => Effect.Effect<void>
}
>() {}
class Metrics extends Context.Tag("Metrics")<
Metrics,
{
readonly counter: (name: string, labels?: Record<string, string>) => Effect.Effect<void>
readonly histogram: (name: string, value: number) => Effect.Effect<void>
readonly gauge: (name: string, value: number) => Effect.Effect<void>
}
>() {}
class Tracer extends Context.Tag("Tracer")<
Tracer,
{
readonly startSpan: (name: string) => Effect.Effect<{ readonly end: () => Effect.Effect<void> }>
readonly addAttribute: (key: string, value: string) => Effect.Effect<void>
}
>() {}
class HealthCheck extends Context.Tag("HealthCheck")<
HealthCheck,
{
readonly check: () => Effect.Effect<{
readonly status: "healthy" | "degraded" | "unhealthy"
readonly checks: ReadonlyArray<{ name: string; status: string }>
}>
}
>() {}
// === Implementations ===
const ConfigLive = Layer.succeed(Config, {
serviceName: "order-service",
version: "1.2.3",
metricsPort: 9090
})
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
const prefix = `[${config.serviceName}@${config.version}]`
return {
info: (msg) => Effect.log(`${prefix} INFO: ${msg}`),
warn: (msg) => Effect.log(`${prefix} WARN: ${msg}`),
error: (msg, err) => Effect.log(`${prefix} ERROR: ${msg} ${err ?? ""}`)
}
})
)
const MetricsLive = Layer.effect(
Metrics,
Effect.gen(function* () {
const config = yield* Config
yield* Effect.log(`Metrics server starting on :${config.metricsPort}`)
return {
counter: (name, labels) =>
Effect.log(`COUNTER ${name} +1 ${JSON.stringify(labels ?? {})}`),
histogram: (name, value) =>
Effect.log(`HISTOGRAM ${name} = ${value}`),
gauge: (name, value) =>
Effect.log(`GAUGE ${name} = ${value}`)
}
})
)
const TracerLive = Layer.effect(
Tracer,
Effect.gen(function* () {
const config = yield* Config
return {
startSpan: (name) =>
Effect.gen(function* () {
const start = Date.now()
yield* Effect.log(`SPAN START: ${config.serviceName}/${name}`)
return {
end: () =>
Effect.log(`SPAN END: ${config.serviceName}/${name} (${Date.now() - start}ms)`)
}
}),
addAttribute: (key, value) =>
Effect.log(`ATTR: ${key}=${value}`)
}
})
)
const HealthCheckLive = Layer.effect(
HealthCheck,
Effect.gen(function* () {
const config = yield* Config
return {
check: () =>
Effect.succeed({
status: "healthy" as const,
checks: [
{ name: config.serviceName, status: "ok" },
{ name: "database", status: "ok" },
{ name: "cache", status: "ok" }
]
})
}
})
)
// === Composition: merge всех observability-сервисов ===
// Все 4 зависят от Config → merge после предоставления
const ObservabilityLive = Layer.mergeAll(
LoggerLive,
MetricsLive,
TracerLive,
HealthCheckLive
).pipe(
Layer.provideMerge(ConfigLive)
)
// Тип: Layer<Config | Logger | Metrics | Tracer | HealthCheck, never, never>
// === Использование ===
const program = Effect.gen(function* () {
const logger = yield* Logger
const metrics = yield* Metrics
const tracer = yield* Tracer
const health = yield* HealthCheck
// Начинаем трейс
const span = yield* tracer.startSpan("process-order")
yield* tracer.addAttribute("orderId", "ORD-001")
// Бизнес-логика
yield* logger.info("Processing order ORD-001")
yield* metrics.counter("orders.processed", { type: "standard" })
yield* metrics.histogram("order.processing_time", 150)
// Завершаем трейс
yield* span.end()
// Health check
const status = yield* health.check()
yield* logger.info(`Health: ${status.status}`)
return status
})
Effect.runPromise(Effect.provide(program, ObservabilityLive)).then(console.log)
Пример: Feature flags через merge
// Feature flags как отдельные сервисы
class FeatureNewUI extends Context.Tag("FeatureNewUI")<FeatureNewUI, { readonly enabled: boolean }>() {}
class FeatureDarkMode extends Context.Tag("FeatureDarkMode")<FeatureDarkMode, { readonly enabled: boolean }>() {}
class FeatureBetaApi extends Context.Tag("FeatureBetaApi")<FeatureBetaApi, { readonly enabled: boolean }>() {}
// Каждый feature flag — отдельный Layer
const NewUIFlag = Layer.succeed(FeatureNewUI, { enabled: true })
const DarkModeFlag = Layer.succeed(FeatureDarkMode, { enabled: false })
const BetaApiFlag = Layer.succeed(FeatureBetaApi, { enabled: true })
// Все флаги одним пакетом
const FeatureFlagsLive = Layer.mergeAll(NewUIFlag, DarkModeFlag, BetaApiFlag)
// Программа проверяет флаги
const renderApp = Effect.gen(function* () {
const newUI = yield* FeatureNewUI
const darkMode = yield* FeatureDarkMode
const betaApi = yield* FeatureBetaApi
return {
ui: newUI.enabled ? "v2" : "v1",
theme: darkMode.enabled ? "dark" : "light",
apiVersion: betaApi.enabled ? "beta" : "stable"
}
})
Effect.runPromise(Effect.provide(renderApp, FeatureFlagsLive)).then(console.log)
// { ui: "v2", theme: "light", apiVersion: "beta" }
Упражнения
Объединение базовых сервисов
Создайте 4 простых сервиса (без зависимостей):
- Clock — метод
now(): Effect<Date> - Random — метод
next(): Effect<number> - Uuid — метод
generate(): Effect<string> - Hash — метод
sha256(input: string): Effect<string>
Объедините их через mergeAll в UtilsLive.
Напишите программу, использующую все 4 сервиса.
class Clock extends Context.Tag("Clock")<Clock, { readonly now: () => Effect.Effect<Date> }>() {}
class Random extends Context.Tag("Random")<Random, { readonly next: () => Effect.Effect<number> }>() {}
class Uuid extends Context.Tag("Uuid")<Uuid, { readonly generate: () => Effect.Effect<string> }>() {}
class Hash extends Context.Tag("Hash")<Hash, { readonly sha256: (input: string) => Effect.Effect<string> }>() {}
const ClockLive = Layer.succeed(Clock, { now: () => Effect.sync(() => new Date()) })
const RandomLive = Layer.succeed(Random, { next: () => Effect.sync(() => Math.random()) })
const UuidLive = Layer.succeed(Uuid, { generate: () => Effect.sync(() => crypto.randomUUID()) })
const HashLive = Layer.succeed(Hash, {
sha256: (input) => Effect.sync(() => {
let hash = 0
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash |= 0
}
return Math.abs(hash).toString(16).padStart(8, "0")
})
})
const UtilsLive = Layer.mergeAll(ClockLive, RandomLive, UuidLive, HashLive)
const program = Effect.gen(function* () {
const clock = yield* Clock
const random = yield* Random
const uuid = yield* Uuid
const hash = yield* Hash
const now = yield* clock.now()
const rand = yield* random.next()
const id = yield* uuid.generate()
const hashed = yield* hash.sha256(`${now.toISOString()}-${rand}-${id}`)
return { time: now.toISOString(), random: rand, uuid: id, hash: hashed }
})
Effect.runPromise(Effect.provide(program, UtilsLive)).then(console.log)merge с общими зависимостями
Три сервиса зависят от общего Config:
- EmailSender (Config) — отправка email
- SmsSender (Config) — отправка SMS
- PushSender (Config) — push-уведомления
- Создайте mergeAll из всех трёх
- Предоставьте Config через provide
- Программа: отправить одно сообщение через все три канала
class Config extends Context.Tag("Config")<
Config,
{ readonly apiKey: string; readonly sender: string }
>() {}
class EmailSender extends Context.Tag("EmailSender")<
EmailSender,
{ readonly send: (to: string, subject: string, body: string) => Effect.Effect<void> }
>() {}
class SmsSender extends Context.Tag("SmsSender")<
SmsSender,
{ readonly send: (to: string, message: string) => Effect.Effect<void> }
>() {}
class PushSender extends Context.Tag("PushSender")<
PushSender,
{ readonly send: (userId: string, title: string, body: string) => Effect.Effect<void> }
>() {}
const ConfigLive = Layer.succeed(Config, {
apiKey: "sk-prod-123",
sender: "noreply@example.com"
})
const EmailSenderLive = Layer.effect(EmailSender, Effect.gen(function* () {
const config = yield* Config
return {
send: (to, subject, body) =>
Effect.log(`[EMAIL] From: ${config.sender} To: ${to} Subject: ${subject}`)
}
}))
const SmsSenderLive = Layer.effect(SmsSender, Effect.gen(function* () {
const config = yield* Config
return {
send: (to, message) =>
Effect.log(`[SMS] API: ${config.apiKey.slice(0, 8)}... To: ${to} Msg: ${message}`)
}
}))
const PushSenderLive = Layer.effect(PushSender, Effect.gen(function* () {
const config = yield* Config
return {
send: (userId, title, body) =>
Effect.log(`[PUSH] API: ${config.apiKey.slice(0, 8)}... User: ${userId} Title: ${title}`)
}
}))
const NotificationLive = Layer.mergeAll(EmailSenderLive, SmsSenderLive, PushSenderLive).pipe(
Layer.provide(ConfigLive)
)
const program = Effect.gen(function* () {
const email = yield* EmailSender
const sms = yield* SmsSender
const push = yield* PushSender
yield* email.send("user@example.com", "Welcome!", "Hello from our app")
yield* sms.send("+1234567890", "Welcome to our app!")
yield* push.send("user-123", "Welcome!", "You've been registered")
})
Effect.runPromise(Effect.provide(program, NotificationLive))Динамический merge на основе конфигурации
Создайте систему плагинов:
- Plugin A: Analytics (optional)
- Plugin B: ErrorTracking (optional)
- Plugin C: FeatureFlags (optional)
Конфигурация определяет, какие плагины активны. Используйте merge для создания Layer только с активными плагинами. Неактивные плагины предоставляют no-op реализации.
class Analytics extends Context.Tag("Analytics")<
Analytics,
{ readonly track: (event: string) => Effect.Effect<void> }
>() {}
class ErrorTracking extends Context.Tag("ErrorTracking")<
ErrorTracking,
{ readonly capture: (error: Error) => Effect.Effect<void> }
>() {}
class FeatureFlags extends Context.Tag("FeatureFlags")<
FeatureFlags,
{ readonly isEnabled: (flag: string) => Effect.Effect<boolean> }
>() {}
const AnalyticsReal = Layer.succeed(Analytics, {
track: (event) => Effect.log(`[Analytics] Tracked: ${event}`)
})
const ErrorTrackingReal = Layer.succeed(ErrorTracking, {
capture: (error) => Effect.log(`[ErrorTracking] Captured: ${error.message}`)
})
const FeatureFlagsReal = Layer.succeed(FeatureFlags, {
isEnabled: (flag) => Effect.succeed(flag === "new-checkout")
})
const AnalyticsNoop = Layer.succeed(Analytics, {
track: (_event) => Effect.void
})
const ErrorTrackingNoop = Layer.succeed(ErrorTracking, {
capture: (_error) => Effect.void
})
const FeatureFlagsNoop = Layer.succeed(FeatureFlags, {
isEnabled: (_flag) => Effect.succeed(false)
})
interface PluginConfig {
readonly analytics: boolean
readonly errorTracking: boolean
readonly featureFlags: boolean
}
const buildPluginLayer = (config: PluginConfig) =>
Layer.mergeAll(
config.analytics ? AnalyticsReal : AnalyticsNoop,
config.errorTracking ? ErrorTrackingReal : ErrorTrackingNoop,
config.featureFlags ? FeatureFlagsReal : FeatureFlagsNoop
)
const program = Effect.gen(function* () {
const analytics = yield* Analytics
const errors = yield* ErrorTracking
const flags = yield* FeatureFlags
yield* analytics.track("page.viewed")
yield* errors.capture(new Error("test error"))
const newCheckout = yield* flags.isEnabled("new-checkout")
return { newCheckout }
})
const ProdPlugins = buildPluginLayer({
analytics: true,
errorTracking: true,
featureFlags: true
})
const TestPlugins = buildPluginLayer({
analytics: false,
errorTracking: false,
featureFlags: true
})
Effect.runPromise(Effect.provide(program, ProdPlugins)).then(console.log)
Effect.runPromise(Effect.provide(program, TestPlugins)).then(console.log)🔗 Далее: Вертикальные пайплайны и граф зависимостей — полная картина композиции Layer