В прошлом уроке область закрывал 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
Провод башни переезжает на линию. Сборка слоя открывает линию и кладёт её возврат в область слоя, провод пишет принятые строки в журнал, а линия закрывается вместе со слоем и узнаёт, чем кончилась программа.
Практика
Дальше
Ринцвальд нажимает кнопку «гасить» посреди ночи и смотрит в журнал.
Что можно оборвать посередине, а что нельзя, разберём в следующем уроке.