Effect Курс Scope.extend

Scope.extend

Тонкое управление временем жизни ресурсов.

Мотивация: ограничения автоматического слияния scope

По умолчанию, когда несколько scoped-ресурсов используются в одном Effect.scoped, их scope сливаются — все финализаторы выполняются при закрытии общего scope:


const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() => Console.log("Release A"))
    yield* Console.log("Using resource A")

    yield* Effect.addFinalizer(() => Console.log("Release B"))
    yield* Console.log("Using resource B")

    // Ресурс A живёт, даже когда уже не нужен
  })
)
// Release B, Release A — оба при выходе из scoped

Это поведение подходит для большинства случаев, но иногда нужно:

┌────────────────────────────────────────────────────────────┐
│ Когда автоматическое слияние scope недостаточно:           │
├────────────────────────────────────────────────────────────┤
│ 1. Ресурс нужен только на часть выполнения                 │
│    (закрыть DB connection после миграции, но до запуска    │
│     HTTP сервера)                                          │
│                                                            │
│ 2. Файбер должен пережить родительский файбер              │
│    (background worker, который живёт пока жив scope)       │
│                                                            │
│ 3. Разные ресурсы должны иметь разное время жизни          │
│    (кеш живёт 5 минут, соединение — до конца запроса)      │
│                                                            │
│ 4. Файбер должен быть привязан к конкретному scope         │
│    (не к родителю, не к daemon, а к определённому scope)   │
└────────────────────────────────────────────────────────────┘

Scope.extend

Что делает Scope.extend

Scope.extend позволяет привязать scoped-эффект к конкретному scope вместо текущего. Эффект выполняется, но его финализаторы регистрируются в указанном scope, а не в текущем:

  Без Scope.extend:                С Scope.extend:
  ┌─────────────────────────┐     ┌─────────────────────────┐
  │ Current Scope           │     │ Current Scope           │
  │ ┌────────────────────┐  │     │                         │
  │ │ task (finalizer    │  │     │ task (выполняется       │
  │ │ регистрируется     │  │     │ здесь, но finalizer     │
  │ │ здесь)             │  │     │ → target scope)         │
  │ └────────────────────┘  │     │                         │
  └─────────────────────────┘     └─────────────────────────┘
                                  ┌─────────────────────────┐
                                  │ Target Scope            │
                                  │ ┌────────────────────┐  │
                                  │ │ finalizer          │  │
                                  │ │ регистрируется     │  │
                                  │ │ здесь              │  │
                                  │ └────────────────────┘  │
                                  └─────────────────────────┘

Сигнатура


// Scope.extend привязывает scoped-эффект к указанному scope
// Scope.extend: (scope: Scope) => <A, E, R>(
//   effect: Effect<A, E, R | Scope>
// ) => Effect<A, E, R>

Базовый пример


const task = Effect.gen(function* () {
  yield* Console.log("Task executing")
  yield* Effect.addFinalizer(() => Console.log("Task finalizer"))
})

const program = Effect.gen(function* () {
  const myScope = yield* Scope.make()

  // Выполняем task, но финализатор уходит в myScope
  yield* task.pipe(Scope.extend(myScope))

  yield* Console.log("Task done, but finalizer not yet run")

  // Финализатор выполнится только при закрытии myScope
  yield* Scope.close(myScope, Exit.void)

  yield* Console.log("Now finalizer has run")
})

Effect.runPromise(program)
/*
  Task executing
  Task done, but finalizer not yet run
  Task finalizer
  Now finalizer has run
*/

Ручное создание и закрытие scope

Полный контроль над временем жизни

С помощью Scope.make(), Scope.extend и Scope.close вы получаете полный контроль:


const task1 = Effect.gen(function* () {
  yield* Console.log("Task 1: started")
  yield* Effect.addFinalizer(() => Console.log("Task 1: finalizer"))
})

const task2 = Effect.gen(function* () {
  yield* Console.log("Task 2: started")
  yield* Effect.addFinalizer(() => Console.log("Task 2: finalizer"))
})

const program = Effect.gen(function* () {
  // Создаём два отдельных scope
  const scope1 = yield* Scope.make()
  const scope2 = yield* Scope.make()

  // Привязываем каждую задачу к своему scope
  yield* task1.pipe(Scope.extend(scope1))
  yield* task2.pipe(Scope.extend(scope2))

  // Закрываем scope1 — task1 finalizer выполняется
  yield* Scope.close(scope1, Exit.void)

  yield* Console.log("--- Between closes ---")

  // Закрываем scope2 — task2 finalizer выполняется
  yield* Scope.close(scope2, Exit.void)
})

Effect.runPromise(program)
/*
  Task 1: started
  Task 2: started
  Task 1: finalizer
  --- Between closes ---
  Task 2: finalizer
*/

Сравнение с merged scope

  Merged (по умолчанию):          Separate scopes:
  ┌───────────────────────────┐   ┌──────────────────────────┐
  │ Shared Scope              │   │ scope1                   │
  │                           │   │ ┌────────┐               │
  │ Task 1 ──fin──┐           │   │ │ Task 1 │──fin──┐       │
  │               │           │   │ └────────┘       │       │
  │ Task 2 ──fin──┤           │   │           close(scope1)  │
  │               │           │   │           ↓ Task 1 fin   │
  │               ▼           │   └──────────────────────────┘
  │           close           │   ┌──────────────────────────┐
  │           ↓ Task 2 fin    │   │ scope2                   │
  │           ↓ Task 1 fin    │   │ ┌────────┐               │
  └───────────────────────────┘   │ │ Task 2 │──fin──┐       │
                                  │ └────────┘       │       │
                                  │           close(scope2)  │
                                  │           ↓ Task 2 fin   │
                                  └──────────────────────────┘

Передача Exit при закрытии

Scope.close принимает Exit, который передаётся всем финализаторам в этом scope:


const task = Effect.gen(function* () {
  yield* Effect.addFinalizer((exit) =>
    Console.log(`Finalizer: exit = ${exit._tag}`)
  )
})

const program = Effect.gen(function* () {
  const scope = yield* Scope.make()
  yield* task.pipe(Scope.extend(scope))

  // Закрываем с ошибкой — финализатор получит Failure
  yield* Scope.close(scope, Exit.fail("manual error"))
})

Effect.runPromise(program)
// Finalizer: exit = Failure

Effect.forkScoped

Проблема: время жизни файберов

В стандартной модели конкурентности Effect дочерний файбер привязан к родительскому:

  Parent fiber
  ┌─────────────────────────────────┐
  │ fork(child) ─── child fiber     │
  │                  │              │
  │ parent ends ─────┤              │
  │                  ▼              │
  │            child interrupted    │  ← Структурная конкурентность
  └─────────────────────────────────┘

Иногда нужно, чтобы файбер пережил родителя и был привязан к scope:

  Scope
  ┌──────────────────────────────────────────┐
  │                                          │
  │  Parent fiber                            │
  │  ┌────────────────────────────┐          │
  │  │ forkScoped(child)          │          │
  │  │ parent ends                │          │
  │  └────────────────────────────┘          │
  │                                          │
  │  Child fiber (всё ещё работает!)         │
  │  ┌────────────────────────────┐          │
  │  │ ...                        │          │
  │  └────────────────────────────┘          │
  │                                          │
  │  scope.close() → child interrupted       │
  └──────────────────────────────────────────┘

Определение

Effect.forkScoped создаёт файбер, привязанный к текущему scope. Файбер будет прерван при закрытии scope, а не при завершении родительского файбера:


// Child fiber, который повторяется каждую секунду
const child = Effect.repeat(
  Console.log("child: still running!"),
  Schedule.fixed("1 second")
)

//      ┌─── Требует Scope
//      ▼
const parent = Effect.gen(function* () {
  yield* Console.log("parent: started!")
  // Файбер привязан к scope, а не к parent
  yield* Effect.forkScoped(child)
  yield* Effect.sleep("3 seconds")
  yield* Console.log("parent: finished!")
})

// Scope живёт 5 секунд
const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Console.log("Scope started!")
    yield* Effect.fork(parent) // parent завершится через 3 сек
    yield* Effect.sleep("5 seconds") // scope живёт 5 сек
    yield* Console.log("Closing scope!")
  })
)

Effect.runFork(program)
/*
  Scope started!
  parent: started!
  child: still running!
  child: still running!
  child: still running!
  parent: finished!           ← parent завершился на 3 сек
  child: still running!       ← child продолжает работать!
  child: still running!       ← child привязан к scope, не к parent
  Closing scope!              ← scope закрывается на 5 сек
                              ← child прерывается вместе со scope
*/

forkScoped vs fork vs forkDaemon

┌──────────────────┬──────────────────────────────────────────┐
│ Оператор         │ Время жизни файбера                      │
├──────────────────┼──────────────────────────────────────────┤
│ Effect.fork      │ Привязан к РОДИТЕЛЬСКОМУ файберу         │
│                  │ Прерывается когда parent завершается     │
│                  │ (структурная конкурентность)             │
├──────────────────┼──────────────────────────────────────────┤
│ Effect.forkScoped│ Привязан к SCOPE                         │
│                  │ Переживает parent, прерывается при       │
│                  │ закрытии scope                           │
│                  │ Требует Scope в Requirements             │
├──────────────────┼──────────────────────────────────────────┤
│ Effect.forkDaemon│ ПОЛНОСТЬЮ независимый                    │
│                  │ Не привязан ни к parent, ни к scope      │
│                  │ Живёт пока работает runtime              │
│                  │ ⚠️ Используйте с осторожностью           │
└──────────────────┴──────────────────────────────────────────┘

Визуально:

  Runtime
  ┌──────────────────────────────────────────────────────┐
  │                                                      │
  │  Scope                                               │
  │  ┌──────────────────────────────────────────────┐    │
  │  │                                              │    │
  │  │  Parent fiber                                │    │
  │  │  ┌────────────────────────────────────┐      │    │
  │  │  │                                    │      │    │
  │  │  │  fork(A)     → A живёт с parent    │      │    │
  │  │  │  forkScoped(B)→ B живёт со scope   │      │    │
  │  │  │  forkDaemon(C)→ C живёт с runtime  │      │    │
  │  │  │                                    │      │    │
  │  │  └────────────────────────────────────┘      │    │
  │  │                                              │    │
  │  │  B ← всё ещё работает после parent           │    │
  │  │                                              │    │
  │  └── scope.close() → B прерывается ─────────────┘    │
  │                                                      │
  │  C ← всё ещё работает после scope!                   │
  │                                                      │
  └──────────────────────────────────────────────────────┘

Effect.forkIn

Определение

Effect.forkIn — более гибкий вариант forkScoped, позволяющий указать конкретный scope, к которому привязать файбер:


const child = Effect.repeat(
  Console.log("child: running!"),
  Schedule.fixed("1 second")
)

const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() =>
      Console.log("Outer scope closing!")
    )

    // Захватываем текущий scope
    const outerScope = yield* Effect.scope

    // Создаём внутренний scope
    yield* Effect.scoped(
      Effect.gen(function* () {
        yield* Effect.addFinalizer(() =>
          Console.log("Inner scope closing!")
        )

        // Форкаем child в OUTER scope (переживёт inner scope)
        yield* Effect.forkIn(child, outerScope)

        yield* Effect.sleep("3 seconds")
      })
    )
    // Inner scope закрыт, но child продолжает работать в outer scope

    yield* Console.log("Between scopes")
    yield* Effect.sleep("2 seconds")
    // Outer scope закрывается → child прерывается
  })
)

Effect.runFork(program)
/*
  child: running!
  child: running!
  child: running!
  Inner scope closing!        ← inner scope закрыт
  Between scopes
  child: running!             ← child всё ещё работает!
  child: running!
  Outer scope closing!        ← outer scope закрыт → child прерван
*/

forkScoped vs forkIn

┌──────────────────────────────────────────────────────┐
│                                                      │
│  forkScoped                forkIn(effect, scope)     │
│  ┌──────────────────┐     ┌──────────────────────┐   │
│  │ Привязка к       │     │ Привязка к ЛЮБОМУ    │   │
│  │ ТЕКУЩЕМУ scope   │     │ scope (явно указан)  │   │
│  │ из контекста     │     │                      │   │
│  │                  │     │ Не требует Scope     │   │
│  │ Требует Scope    │     │ в Requirements       │   │
│  │ в Requirements   │     │                      │   │
│  └──────────────────┘     └──────────────────────┘   │
│                                                      │
└──────────────────────────────────────────────────────┘

Закрытие scope с незавершёнными задачами

Важный нюанс: закрытие scope не прерывает задачи, расширенные в этот scope через Scope.extend. Но задачи, форкнутые через forkScoped или forkIn, прерываются при закрытии scope.

Scope.extend — задача продолжает выполняться


const task = Effect.gen(function* () {
  yield* Effect.sleep("1 second")
  yield* Console.log("Task completed")
  yield* Effect.addFinalizer(() => Console.log("Task finalizer"))
})

const program = Effect.gen(function* () {
  const scope = yield* Scope.make()

  // Закрываем scope СРАЗУ
  yield* Scope.close(scope, Exit.void)
  yield* Console.log("Scope closed")

  // Задача всё равно выполнится (Scope.extend не форкает)
  yield* task.pipe(Scope.extend(scope))
})

Effect.runPromise(program)
/*
  Scope closed
  Task completed        ← задача выполнилась после закрытия scope
  Task finalizer
*/

⚠️ Это поведение может быть неинтуитивным! Scope.extend только перенаправляет, куда регистрируются финализаторы, но не управляет выполнением задачи.

forkScoped — файбер прерывается


const worker = Effect.repeat(
  Console.log("Worker tick"),
  Schedule.fixed("500 millis")
)

const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.forkScoped(worker)
    yield* Effect.sleep("2 seconds")
    yield* Console.log("Scope closing...")
    // При закрытии scope worker будет прерван
  })
)

Effect.runFork(program)
/*
  Worker tick
  Worker tick
  Worker tick
  Worker tick
  Scope closing...
  ← worker прерван
*/

API Reference

Scope.extend [STABLE]


// Привязывает scoped-эффект к указанному scope
Scope.extend(
  scope: Scope.Scope
): <A, E, R>(
  effect: Effect<A, E, R | Scope>
) => Effect<A, E, Exclude<R, Scope>>

Используется с pipe:

yield* myEffect.pipe(Scope.extend(targetScope))

Scope.make [STABLE]


// Создаёт новый Scope.Closeable
Scope.make(): Effect<Scope.Closeable, never, never>

Scope.close [STABLE]


// Закрывает scope, выполняя все финализаторы
Scope.close(
  scope: Scope.Closeable,
  exit: Exit<unknown, unknown>
): Effect<void, never, never>

Effect.forkScoped [STABLE]


// Форкает файбер, привязанный к текущему scope
Effect.forkScoped<A, E, R>(
  effect: Effect<A, E, R>
): Effect<Fiber.RuntimeFiber<A, E>, never, R | Scope>

Effect.forkIn [STABLE]


// Форкает файбер, привязанный к указанному scope
Effect.forkIn(
  effect: Effect<A, E, R>,
  scope: Scope.Scope
): Effect<Fiber.RuntimeFiber<A, E>, never, R>

Effect.scope [STABLE]


// Получает текущий scope из контекста
Effect.scope: Effect<Scope.Scope, never, Scope.Scope>

Паттерны использования

Паттерн 1: Раннее освобождение ресурса

Ресурс нужен только для определённой фазы работы:


const program = Effect.gen(function* () {
  // Фаза 1: Миграция базы (нужен migration client)
  const migrationScope = yield* Scope.make()

  const migrationClient = Effect.gen(function* () {
    yield* Console.log("[Migration] Client created")
    yield* Effect.addFinalizer(() =>
      Console.log("[Migration] Client closed")
    )
    return { migrate: Effect.succeed("migrated") }
  })

  const client = yield* migrationClient.pipe(Scope.extend(migrationScope))
  yield* client.migrate
  yield* Console.log("[Migration] Complete")

  // Закрываем migration client — он больше не нужен
  yield* Scope.close(migrationScope, Exit.void)

  // Фаза 2: Запуск HTTP-сервера (migration client уже освобождён)
  yield* Console.log("[Server] Starting (migration resources freed)")
  yield* Effect.sleep("1 second")
  yield* Console.log("[Server] Running")
})

Effect.runPromise(program)
/*
  [Migration] Client created
  [Migration] Complete
  [Migration] Client closed       ← закрыт до запуска сервера
  [Server] Starting (migration resources freed)
  [Server] Running
*/

Паттерн 2: Background worker с forkScoped


const metricsAggregator = Effect.scoped(
  Effect.gen(function* () {
    const counter = yield* Ref.make(0)

    // Background worker, привязанный к scope
    yield* Effect.forkScoped(
      Effect.repeat(
        Effect.gen(function* () {
          const count = yield* Ref.updateAndGet(counter, (n) => n + 1)
          yield* Console.log(`[Metrics] Flush #${count}`)
        }),
        Schedule.fixed("1 second")
      )
    )

    // Основная работа
    yield* Console.log("[App] Processing requests...")
    yield* Effect.sleep("3 seconds")
    yield* Console.log("[App] Done processing")

    // При выходе из scoped — worker прерывается
  })
)

Effect.runFork(metricsAggregator)
/*
  [App] Processing requests...
  [Metrics] Flush #1
  [Metrics] Flush #2
  [Metrics] Flush #3
  [App] Done processing
  ← worker прерван при закрытии scope
*/

Паттерн 3: Вложенные scope с разным временем жизни


const program = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.addFinalizer(() =>
      Console.log("[Outer] Scope closing")
    )

    // Outer scope — захватываем
    const outerScope = yield* Effect.scope

    // Inner scope — короткоживущий
    yield* Effect.scoped(
      Effect.gen(function* () {
        yield* Effect.addFinalizer(() =>
          Console.log("[Inner] Scope closing")
        )

        // Worker привязан к OUTER scope — переживёт inner
        yield* Effect.forkIn(
          Effect.repeat(
            Console.log("[Worker] tick"),
            Schedule.fixed("1 second")
          ),
          outerScope
        )

        yield* Console.log("[Inner] Working for 2 seconds...")
        yield* Effect.sleep("2 seconds")
        yield* Console.log("[Inner] Done")
      })
    )

    yield* Console.log("[Outer] Inner scope closed, worker still running")
    yield* Effect.sleep("3 seconds")
    yield* Console.log("[Outer] Closing...")
  })
)

Effect.runFork(program)
/*
  [Inner] Working for 2 seconds...
  [Worker] tick
  [Worker] tick
  [Inner] Done
  [Inner] Scope closing
  [Outer] Inner scope closed, worker still running
  [Worker] tick          ← worker пережил inner scope!
  [Worker] tick
  [Worker] tick
  [Outer] Closing...
  [Outer] Scope closing  ← теперь worker прерван
*/

Примеры

Пример 1: Pipeline с промежуточной очисткой


const makeResource = (name: string) =>
  Effect.gen(function* () {
    yield* Console.log(`+ ${name}`)
    yield* Effect.addFinalizer(() => Console.log(`- ${name}`))
    return name
  })

const pipeline = Effect.gen(function* () {
  // Этап 1: Загрузка данных
  const loadScope = yield* Scope.make()
  const loader = yield* makeResource("DataLoader").pipe(
    Scope.extend(loadScope)
  )
  yield* Console.log(`Loading with ${loader}...`)
  yield* Scope.close(loadScope, Exit.void)
  yield* Console.log("Data loaded, loader freed")

  // Этап 2: Трансформация
  const transformScope = yield* Scope.make()
  const transformer = yield* makeResource("Transformer").pipe(
    Scope.extend(transformScope)
  )
  yield* Console.log(`Transforming with ${transformer}...`)
  yield* Scope.close(transformScope, Exit.void)
  yield* Console.log("Data transformed, transformer freed")

  // Этап 3: Сохранение
  const saveScope = yield* Scope.make()
  const saver = yield* makeResource("Saver").pipe(
    Scope.extend(saveScope)
  )
  yield* Console.log(`Saving with ${saver}...`)
  yield* Scope.close(saveScope, Exit.void)
  yield* Console.log("Pipeline complete!")
})

Effect.runPromise(pipeline)
/*
  + DataLoader
  Loading with DataLoader...
  - DataLoader              ← освобождён после загрузки
  Data loaded, loader freed
  + Transformer
  Transforming with Transformer...
  - Transformer             ← освобождён после трансформации
  Data transformed, transformer freed
  + Saver
  Saving with Saver...
  - Saver                   ← освобождён после сохранения
  Pipeline complete!
*/

Пример 2: Graceful shutdown с forkScoped


const gracefulApp = Effect.scoped(
  Effect.gen(function* () {
    const requestCount = yield* Ref.make(0)

    // Health check worker
    const healthFiber = yield* Effect.forkScoped(
      Effect.repeat(
        Effect.gen(function* () {
          const count = yield* Ref.get(requestCount)
          yield* Console.log(`[Health] OK, requests served: ${count}`)
        }),
        Schedule.fixed("1 second")
      )
    )

    // Обработка "запросов"
    yield* Effect.forEach(
      [1, 2, 3, 4, 5] as const,
      (i) =>
        Effect.gen(function* () {
          yield* Effect.sleep("500 millis")
          yield* Ref.update(requestCount, (n) => n + 1)
          yield* Console.log(`[App] Request ${i} processed`)
        }),
      { concurrency: 2 }
    )

    const total = yield* Ref.get(requestCount)
    yield* Console.log(`[App] All ${total} requests processed`)

    // При выходе из scoped — health check worker остановится
    yield* Console.log("[App] Shutting down gracefully...")
  })
)

Effect.runPromise(gracefulApp)

Пример 3: Connection pool с scoped workers


interface Connection {
  readonly id: number
  readonly query: (sql: string) => Effect.Effect<string>
}

const createConnection = (id: number) =>
  Effect.gen(function* () {
    yield* Console.log(`[Conn ${id}] Opened`)
    yield* Effect.addFinalizer(() =>
      Console.log(`[Conn ${id}] Closed`)
    )
    const conn: Connection = {
      id,
      query: (sql) => Effect.succeed(`[Conn ${id}] Result: ${sql}`)
    }
    return conn
  })

const program = Effect.gen(function* () {
  // Scope для pool
  const poolScope = yield* Scope.make()

  // Создаём 3 подключения в pool scope
  const conn1 = yield* createConnection(1).pipe(Scope.extend(poolScope))
  const conn2 = yield* createConnection(2).pipe(Scope.extend(poolScope))
  const conn3 = yield* createConnection(3).pipe(Scope.extend(poolScope))

  // Используем подключения
  yield* Console.log(yield* conn1.query("SELECT 1"))
  yield* Console.log(yield* conn2.query("SELECT 2"))
  yield* Console.log(yield* conn3.query("SELECT 3"))

  yield* Console.log("--- Draining pool ---")
  yield* Scope.close(poolScope, Exit.void)
  yield* Console.log("Pool drained")
})

Effect.runPromise(program)
/*
  [Conn 1] Opened
  [Conn 2] Opened
  [Conn 3] Opened
  [Conn 1] Result: SELECT 1
  [Conn 2] Result: SELECT 2
  [Conn 3] Result: SELECT 3
  --- Draining pool ---
  [Conn 3] Closed     ← LIFO
  [Conn 2] Closed
  [Conn 1] Closed
  Pool drained
*/

Распространённые ошибки

Ошибка 1: Использование ресурса после закрытия его scope


// ❌ Ресурс использован после закрытия scope
const bad = Effect.gen(function* () {
  const scope = yield* Scope.make()

  const resource = yield* Effect.gen(function* () {
    yield* Effect.addFinalizer(() => Console.log("Released"))
    return { data: "important" }
  }).pipe(Scope.extend(scope))

  yield* Scope.close(scope, Exit.void)  // Ресурс освобождён

  // ❌ Ресурс "мёртв" — scope закрыт
  console.log(resource.data)  // Может работать, но семантически неверно
})

// ✅ Правильно: использовать до закрытия scope
const good = Effect.gen(function* () {
  const scope = yield* Scope.make()

  const resource = yield* Effect.gen(function* () {
    yield* Effect.addFinalizer(() => Console.log("Released"))
    return { data: "important" }
  }).pipe(Scope.extend(scope))

  console.log(resource.data)  // ✅ Scope ещё открыт

  yield* Scope.close(scope, Exit.void)
})

Ошибка 2: forkScoped без scope


// ❌ forkScoped требует Scope в контексте
const bad = Effect.gen(function* () {
  // Ошибка компиляции: Scope не предоставлен
  // yield* Effect.forkScoped(Console.log("tick"))
})

// ✅ Правильно: обернуть в Effect.scoped
const good = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.forkScoped(Console.log("tick"))
    yield* Effect.sleep("1 second")
  })
)

Ошибка 3: Путать forkScoped и forkDaemon


// forkDaemon — файбер НИКОГДА не будет прерван автоматически
// ⚠️ Может привести к утечке файберов!
const risky = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.forkDaemon(
      Effect.repeat(
        Console.log("I'll run forever!"),
        Schedule.fixed("1 second")
      )
    )
  })
)

// forkScoped — файбер будет прерван при закрытии scope
// ✅ Безопасно — файбер контролируется
const safe = Effect.scoped(
  Effect.gen(function* () {
    yield* Effect.forkScoped(
      Effect.repeat(
        Console.log("I'll stop when scope closes"),
        Schedule.fixed("1 second")
      )
    )
    yield* Effect.sleep("3 seconds")
  })
)

Упражнения

Упражнение

Упражнение 1: Два scope с разным временем жизни

Легко

Создайте два scope. В первый привяжите ресурс “TempFile”, во второй — “Database”. Закройте TempFile scope после использования, но оставьте Database открытым.

import { Console, Effect, Exit, Scope } from "effect"

const exercise1 = Effect.gen(function* () {
  const tempScope = yield* Scope.make()
  const dbScope = yield* Scope.make()

  yield* Effect.gen(function* () {
    yield* Console.log("TempFile created")
    yield* Effect.addFinalizer(() => Console.log("TempFile deleted"))
  }).pipe(Scope.extend(tempScope))

  yield* Effect.gen(function* () {
    yield* Console.log("Database connected")
    yield* Effect.addFinalizer(() => Console.log("Database disconnected"))
  }).pipe(Scope.extend(dbScope))

  yield* Console.log("Processing with temp file...")
  yield* Scope.close(tempScope, Exit.void)
  yield* Console.log("Temp file cleaned, database still active")

  yield* Console.log("More database work...")
  yield* Scope.close(dbScope, Exit.void)
  yield* Console.log("All done")
})

Effect.runPromise(exercise1)
/*
  TempFile created
  Database connected
  Processing with temp file...
  TempFile deleted
  Temp file cleaned, database still active
  More database work...
  Database disconnected
  All done
*/
Упражнение

Упражнение 2: Background task с forkScoped

Средне

Создайте приложение с background health-check, который работает пока открыт scope. Main effect должен выполнить 5 операций и завершиться, после чего health-check прерывается.

import { Effect, Console, Schedule, Ref } from "effect"

const exercise2 = Effect.scoped(
  Effect.gen(function* () {
    const opsCount = yield* Ref.make(0)

    // Background health check
    yield* Effect.forkScoped(
      Effect.repeat(
        Effect.gen(function* () {
          const ops = yield* Ref.get(opsCount)
          yield* Console.log(`[HealthCheck] ops completed: ${ops}`)
        }),
        Schedule.fixed("300 millis")
      )
    )

    // Main work: 5 операций
    for (const i of [1, 2, 3, 4, 5] as const) {
      yield* Effect.sleep("200 millis")
      yield* Ref.update(opsCount, (n) => n + 1)
      yield* Console.log(`[Main] Operation ${i} done`)
    }

    yield* Console.log("[Main] All operations complete, closing scope...")
  })
)

Effect.runPromise(exercise2)
Упражнение

Упражнение 3: Multi-phase pipeline с forkIn

Сложно

Реализуйте pipeline из трёх фаз. Каждая фаза имеет свой scope и background logger. Logger каждой фазы должен пережить свою фазу и быть привязан к внешнему scope, чтобы собрать финальный отчёт.

import { Console, Effect, Schedule, Ref } from "effect"

const exercise3 = Effect.scoped(
  Effect.gen(function* () {
    const logs = yield* Ref.make<ReadonlyArray<string>>([])
    const outerScope = yield* Effect.scope

    yield* Effect.addFinalizer(() =>
      Effect.gen(function* () {
        const allLogs = yield* Ref.get(logs)
        yield* Console.log("\n=== Final Report ===")
        allLogs.forEach((l) => console.log(`  ${l}`))
        yield* Console.log("====================")
      })
    )

    // Фаза 1
    yield* Effect.scoped(
      Effect.gen(function* () {
        yield* Effect.addFinalizer(() => Console.log("[Phase 1] Cleanup"))

        // Logger привязан к outer scope
        yield* Effect.forkIn(
          Ref.update(logs, (prev) => [...prev, "Phase 1 completed"]),
          outerScope
        )

        yield* Console.log("[Phase 1] Executing...")
        yield* Effect.sleep("500 millis")
      })
    )
    yield* Console.log("[Phase 1] Scope closed")

    // Фаза 2
    yield* Effect.scoped(
      Effect.gen(function* () {
        yield* Effect.addFinalizer(() => Console.log("[Phase 2] Cleanup"))

        yield* Effect.forkIn(
          Ref.update(logs, (prev) => [...prev, "Phase 2 completed"]),
          outerScope
        )

        yield* Console.log("[Phase 2] Executing...")
        yield* Effect.sleep("500 millis")
      })
    )
    yield* Console.log("[Phase 2] Scope closed")

    // Фаза 3
    yield* Effect.scoped(
      Effect.gen(function* () {
        yield* Effect.addFinalizer(() => Console.log("[Phase 3] Cleanup"))

        yield* Effect.forkIn(
          Ref.update(logs, (prev) => [...prev, "Phase 3 completed"]),
          outerScope
        )

        yield* Console.log("[Phase 3] Executing...")
        yield* Effect.sleep("500 millis")
      })
    )
    yield* Console.log("[Phase 3] Scope closed")

    yield* Console.log("\nAll phases complete!")
  })
)

Effect.runPromise(exercise3)
/*
  [Phase 1] Executing...
  [Phase 1] Cleanup
  [Phase 1] Scope closed
  [Phase 2] Executing...
  [Phase 2] Cleanup
  [Phase 2] Scope closed
  [Phase 3] Executing...
  [Phase 3] Cleanup
  [Phase 3] Scope closed

  All phases complete!

  === Final Report ===
    Phase 1 completed
    Phase 2 completed
    Phase 3 completed
  ====================
*/
Упражнение

Упражнение 4: Resource pool с динамическим размером

Сложно

Создайте pool ресурсов, где каждый ресурс имеет свой scope. Реализуйте acquire (берёт ресурс из pool), release (возвращает в pool) и drain (закрывает все ресурсы).

import { Console, Effect, Exit, Queue, Ref, Scope } from "effect"

interface PooledResource {
  readonly id: number
  readonly scope: Scope.Scope.Closeable
}

const createPool = (size: number) =>
  Effect.gen(function* () {
    const queue = yield* Queue.bounded<PooledResource>(size)
    const activeCount = yield* Ref.make(0)

    // Создаём ресурсы
    for (let i = 1; i <= size; i++) {
      const scope = yield* Scope.make()
      yield* Scope.addFinalizer(
        scope,
        Console.log(`[Resource ${i}] Finalized`)
      )
      yield* Queue.offer(queue, { id: i, scope })
      yield* Ref.update(activeCount, (n) => n + 1)
    }

    yield* Console.log(`[Pool] Created with ${size} resources`)

    return {
      acquire: Effect.gen(function* () {
        const resource = yield* Queue.take(queue)
        yield* Console.log(`[Pool] Acquired resource ${resource.id}`)
        return resource
      }),

      release: (resource: PooledResource) =>
        Effect.gen(function* () {
          yield* Queue.offer(queue, resource)
          yield* Console.log(`[Pool] Released resource ${resource.id}`)
        }),

      drain: Effect.gen(function* () {
        const count = yield* Ref.get(activeCount)
        yield* Console.log(`[Pool] Draining ${count} resources...`)
        for (let i = 0; i < count; i++) {
          const resource = yield* Queue.take(queue)
          yield* Scope.close(resource.scope, Exit.void)
        }
        yield* Console.log("[Pool] All resources drained")
      })
    }
  })

const program = Effect.gen(function* () {
  const pool = yield* createPool(3)

  const r1 = yield* pool.acquire
  const r2 = yield* pool.acquire

  yield* Console.log(`Working with ${r1.id} and ${r2.id}...`)

  yield* pool.release(r1)
  yield* pool.release(r2)

  yield* pool.drain
})

Effect.runPromise(program)
/*
  [Pool] Created with 3 resources
  [Pool] Acquired resource 1
  [Pool] Acquired resource 2
  Working with 1 and 2...
  [Pool] Released resource 1
  [Pool] Released resource 2
  [Pool] Draining 3 resources...
  [Resource 1] Finalized
  [Resource 2] Finalized
  [Resource 3] Finalized
  [Pool] All resources drained
*/