Раздел 46 · Effect: Тихая Академия

Ресурс в слое

middle~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В прошлом уроке область закрывал Effect.scoped вокруг куска работы. Ресурс жил столько, сколько этот кусок. Но у провода другая жизнь: он нужен всем сменам, всем кадрам и всем функциям, которые берут Wire из контекста. Его жизнь совпадает с жизнью слоя, а не с куском работы.

Слой со своей областью

Провод на линии собирается так:

export const lineLayer = (
  send: (line: string) => Effect.Effect<void, SendFailed>,
): Layer.Layer<Wire, WireDown, Depot | Journal> =>
  Layer.effect(
    Wire,
    Effect.gen(function* () {
      yield* resource('линия');
      const journal = yield* Journal;
      return Wire.of({ send: (line) => Effect.tap(send(line), () => journal.write(`> ${line}`)) });
    }),
  );

Сборка провода берёт resource('линия'), тот самый, что в прошлом уроке требовал Scope. А в типе слоя Scope нет: Layer<Wire, WireDown, Depot | Journal>. Ошибка открытия стала ошибкой сборки слоя, склад и журнал стали его требованиями, а область исчезла.

Её дал сам слой. У каждого собранного слоя есть своя область. Всё, что сборка положила в неё, закроется, когда слой разберут: когда кончится Effect.provide, внутри которого его собрали. В Effect 3 для этого была отдельная функция Layer.scoped; в Effect 4 её нет, Layer.effect выдаёт область всегда.

const run = async (work: Effect.Effect<unknown, unknown, Wire | Journal>) => {
  const log: string[] = [];
  await Effect.runPromiseExit(
    work.pipe(
      Effect.provide(lineLayer(() => Effect.void)),
      Effect.provide(Journal.layer),
      Effect.provide(Depot.layerTest(log)),
    ),
  );
  console.log(log);
};

await run(Wire.use((wire) => wire.send('T-1->T-3|5|Ack|5')));
await run(Effect.fail(new WireDown({ reason: 'обрыв' })));
[ 'открыт линия', 'закрыт линия: ok' ]
[ 'открыт линия', 'закрыт линия: fail' ]

Линию открыла сборка, закрыл конец программы. Финализатор слоя получает исход программы: программа упала, и склад записал fail. Работа о линии не знает ничего: она берёт Wire из контекста, как в уроке 47.

Когда слой разбирают

Слой живёт ровно столько, сколько Effect.provide, который его собрал. С тем, что ты знаешь о мемоизации из урока 50, это даёт три следствия.

  • Один provide на краю значит одна линия на всю программу: открыта при старте, закрыта при выходе.
  • provide внутри функции, которую зовут на каждый кадр, значит линия на каждый кадр: открыть, отправить, закрыть. Башня Ринцвальда из урока 50 теперь не только теряла журнал, но и дёргала склад на каждом кадре.
  • Вложенный provide того же слоя линию не откроет второй раз: слой уже собран снаружи, и его область общая. С { local: true } откроет вторую линию и закроет её, как только кончится внутренний кусок.

Порядок разборки обратный порядку сборки, как у финализаторов в одной области. Провод собран после журнала, потому что журнал ему нужен; значит, провод разберут раньше журнала. Финализатор провода может писать в журнал, и журнал ещё жив.

Под капотом

Effect.provide(слой) делает три шага. Заводит область. Собирает слой внутри неё: всё, что сборка положила в область, остаётся там. Исполняет программу с собранным контекстом и закрывает область исходом программы. Вложенный provide того же слоя находит его в таблице MemoMap из урока 50 и берёт готовый провод вместе с его линией: линия открывается один раз, и финализатор у неё один, в области внешнего provide.

Где тут дыра

// провод Ринцвальда: линию открывает при сборке, а закрывать некому
const lineWire = Layer.effect(
  Wire,
  Effect.gen(function* () {
    yield* Depot.use((depot) => depot.open('линия'));
    return Wire.of({ send: () => Effect.void });
  }),
);

const log: string[] = [];
await Effect.runPromise(
  Wire.use((wire) => wire.send('T-1->T-3|5|Ack|5')).pipe(
    Effect.provide(lineWire),
    Effect.provide(Depot.layerTest(log)),
  ),
);
console.log(log);
[ 'открыт линия' ]

Отправка прошла, программа кончилась, линия открыта. Область у слоя была, но в неё ничего не положили: open без acquireRelease не оставляет возврата. На одной программе это незаметно. В тестах каждая сборка открывает ещё одну линию, и через сотню тестов склад отказывается выдавать новые.

Лечится тем, что открытие идёт через resource, то есть через acquireRelease: возврат ложится в область слоя, и закрывать его будет кому.

Башня, этап 21

Провод башни переезжает на линию. Сборка слоя открывает линию и кладёт её возврат в область слоя, провод пишет принятые строки в журнал, а линия закрывается вместе со слоем и узнаёт, чем кончилась программа.

Практика

Дальше

Ринцвальд нажимает кнопку «гасить» посреди ночи и смотрит в журнал.

Что можно оборвать посередине, а что нельзя, разберём в следующем уроке.