В прошлом уроке башня стала одной вещью, testLayer, и журнал в ней один. Но «один» в графе ещё не значит «один» в программе. Слой это фабрика, а фабрику можно позвать сколько угодно раз. Этот урок о том, когда Effect зовёт фабрику заново, а когда отдаёт уже собранное.
Башня на каждый кадр
Вот обработчик Ринцвальда:
const frames = ['T-3->T-1|5|Text|%D0%B3', 'мусор', 'T-5->T-1|10|Ping|'];
const journals = await Effect.runPromise(
Effect.forEach(frames, (line) =>
Effect.andThen(nightShift([line]), Journal.use((journal) => journal.entries)).pipe(
Effect.provide(testLayer([1, 2])),
),
),
);
console.log(journals);
[
[
'> T-1->T-3|5|Ack|5',
'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
],
[ 'обработано 0, ошибок 1, призраков 0, дублей 0, сбоев 0' ],
[
'> T-1->T-5|10|Ping|',
'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
]
]
Три кадра, три башни, три журнала по одной сводке. Ни один не помнит ночь целиком. И это не всё: если тот же кадр придёт дважды, каждая башня увидит его впервые, и дубль не поймает никто. Отчёт о ночи из трёх отчётов о кадре не склеить.
Ринцвальд хотел изоляции: один кадр сломался, остальным всё равно. Изоляцию даёт Effect.exit в processNight из урока 46, а не новая башня. Башня на кадр это цена без пользы.
Правило Effect 4
Сколько раз Effect собирает слой, решает одно правило с тремя частями.
- Внешний
provideпереиспользуют. Если слой уже собран снаружи, а программа внутри снова отдаёт его черезprovide, Effect возьмёт собранный. Второго журнала не будет. - Соседние
provideсобирают своё. Дваprovide, которые не вложены друг в друга, ничего не знают друг о друге. У РинцвальдаforEachзовётprovideтрижды, рядом, и каждый собирает башню заново. - Узнают слой по ссылке. «Тот же слой» значит тот же объект.
Wire.layerTestэто одно статическое поле, его узнают всегда.Ledger.layer([1, 2])это функция, и каждый вызов делает новый объект.
Третью часть проще всего проверить на счётчике:
let builds = 0;
class Book extends Context.Service<Book, number>()('Book') {
static readonly layer = (n: number) => Layer.effect(Book, Effect.sync(() => (builds++, n)));
}
const read = Book.useSync((n) => n);
const one = Book.layer(1);
Effect.runSync(Effect.all([read, read.pipe(Effect.provide(one))]).pipe(Effect.provide(one)));
// builds: 1
Effect.runSync(
Effect.all([read, read.pipe(Effect.provide(Book.layer(1)))]).pipe(Effect.provide(Book.layer(1))),
);
// builds: 2
В первом случае внутренний provide получил тот же объект one, что и внешний, и Effect отдал собранное. Во втором Book.layer(1) вызван дважды: два объекта, две сборки, хоть параметры и одинаковые. Это мемоизация по ссылке, и из неё следует практический совет: слой, который должен быть общим, держи в одной константе или в статическом поле, а не вызывай функцию в каждом месте.
Когда свой журнал нужен
Теперь обратная задача. Академия хочет день из нескольких смен: книга отправленных одна на весь день, а журнал у каждой смены свой, чтобы утром на стол легла отдельная тетрадь. День собран на testLayer, а каждая смена кладёт себе свои провод и журнал:
const entries = Journal.use((journal) => journal.entries);
const shiftLayer = Wire.layerTest.pipe(Layer.provideMerge(Journal.layer));
const program = (local: boolean) =>
Effect.gen(function* () {
yield* Journal.use((journal) => journal.write('день начат'));
const inner = yield* Effect.andThen(nightShift(['T-5->T-1|10|Ping|']), entries).pipe(
Effect.provide(shiftLayer, { local }),
);
return { inner, outer: yield* entries };
}).pipe(Effect.provide(testLayer([1, 2])));
Без local:
{
inner: [
'день начат',
'> T-1->T-5|10|Ping|',
'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
],
outer: [
'день начат',
'> T-1->T-5|10|Ping|',
'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
]
}
Смена хотела свою тетрадь, а получила дневную: правило 1 сработало ровно так, как написано. shiftLayer новый объект, но собран он из Wire.layerTest и Journal.layer, а их внешний testLayer уже собрал. Effect узнал оба по ссылке и отдал готовые. Запись смены легла в дневной журнал, рядом с «день начат».
С { local: true }:
{
inner: [
'> T-1->T-5|10|Ping|',
'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
],
outer: [ 'день начат' ]
}
Effect.provide(слой, { local: true }) собирает слой целиком заново, со всеми его частями, не заглядывая в то, что собрано снаружи. Свой провод, свой журнал, а книга, которую shiftLayer не даёт, приходит из дня, общая.
Есть и второй способ, точечный: Layer.fresh(Journal.layer). Это обёртка, которую не узнают по ссылке, поэтому собирается заново именно она, а всё остальное по общим правилам. local говорит «эту сборку целиком с нуля», fresh говорит «вот этот слой всегда с нуля».
Посмотри все шесть сборок в одном месте. Последние три это ровно то, что разобрано выше: башня на кадр, вложенная смена без local и с ним.
Под капотом
Когда provide собирает слой, он заводит таблицу MemoMap: ключ это объект слоя, значение это собранный кусок контекста. Перед тем как собрать любую часть графа, сборщик смотрит в таблицу. Нашёл, отдаёт найденное. Не нашёл, собирает и записывает.
Внешний provide кладёт свою таблицу в контекст программы, и вложенный provide её находит и пользуется ей же: отсюда правило 1. Соседние provide таблиц друг друга не видят: правило 2. Ключ это объект, а не его содержимое: правило 3. { local: true } заводит новую пустую таблицу для одной сборки. Layer.fresh возвращает новый объект-обёртку, которого в таблице нет и не будет, потому что ссылку на него при каждой сборке берут заново.
Где тут дыра
Дыра этого урока уже разобрана, это обработчик Ринцвальда в начале: Effect.provide(testLayer(...)) внутри функции, которую зовут на каждый кадр. Она коварна тем, что в тесте на один кадр всё работает и выглядит даже аккуратнее. Ищи её по признаку: provide стоит не на краю программы, а внутри функции, которую зовут много раз. Если такая функция нужна, слой отдают ей снаружи, а не собирают внутри.
Башня, этап 15
Башня учится работать днём из нескольких смен. ownShift даёт куску программы свои провод и журнал, даже если снаружи уже собраны общие. day прогоняет несколько ночей подряд на одной книге и отдаёт журнал каждой смены отдельно.
Практика
Дальше
Рыдкулл стоит у окна и смотрит на холм за рекой. Там поднимают леса.
Конфигурация и секреты ждут в следующем уроке.