Три provide подряд из прошлого урока работают, но у них два недостатка. Порядок важен и нигде не записан, кроме головы того, кто писал. И сборка размазана по месту запуска: тест на ночь и запуск ночи повторяют одни и те же три строки.
Слои можно собирать друг из друга ещё до того, как их отдали квитанции. Получится один слой, «башня целиком», с понятным типом. Его кладут одним provide.
Граф зависимостей
Нарисуй, кто кому нужен:
смена ──▶ Wire ──▶ Journal
│ ▲
├──▶ Ledger │
└──────────────────┘
Смене нужны все три сервиса. Проводу нужен журнал. Книге не нужно ничего. Стрелки не образуют круга, и это главное: такой граф можно собрать снизу вверх. Сначала журнал, потом провод, которому журнал отдают, книгу в любой момент. Это граф слоёв.
Три способа сложить слои
Layer.merge и Layer.mergeAll кладут слои рядом. Каждый собирается сам по себе, результат даёт всё, что дают они, и просит всё, что просят они:
Layer.mergeAll(Wire.layerTest, Ledger.layer([1, 2]))
// Layer<Wire | Ledger, never, Journal>
mergeAll ничего не передаёт от одного слоя другому. Провод просит журнал, и объединение просит журнал.
Layer.provide кормит один слой другим. a.pipe(Layer.provide(b)) значит: собери b, отдай его a, а наружу выдай только a:
Layer.mergeAll(Wire.layerTest, Ledger.layer([1, 2])).pipe(Layer.provide(Journal.layer))
// Layer<Wire | Ledger, never, never>
Журнал ушёл из требований, проводу его дали. Но и в том, что слой даёт, журнала нет: Layer.provide прячет то, чем кормил.
Layer.provideMerge кормит и оставляет на виду. Собери b, отдай его a, а наружу выдай и a, и b:
export const testLayer = (sent: Iterable<number>) =>
Layer.mergeAll(Wire.layerTest, Ledger.layer(sent)).pipe(
Layer.provideMerge(Journal.layer),
Layer.satisfiesServicesType<never>(),
);
// Layer<Wire | Ledger | Journal, never, never>
Это и есть башня для тестов: даёт три сервиса, ничего не просит. Журнал в ней один: провод получил тот же экземпляр, что выдан наружу, и смена, которая возьмёт журнал из контекста, возьмёт его же.
Что делает последняя строка, satisfiesServicesType, разберём ниже, после ошибки, которую она ловит.
Перевод с Ук
Возьми Layer.provide вместо provideMerge и отдай слой смене:
const hidden = Layer.mergeAll(Wire.layerTest, Ledger.layer([1, 2])).pipe(
Layer.provide(Journal.layer),
);
const report = Effect.runPromise(nightShift(['T-5->T-1|10|Ping|']).pipe(Effect.provide(hidden)));
ex49a.ts(10,34): error TS2379: Argument of type 'Effect<NightReport, never, Journal>' is not assignable to parameter of type 'Effect<NightReport, never, never>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Type 'Journal' is not assignable to type 'never'.
Последняя строка, как всегда: в R остался Journal. Слой hidden дал проводу журнал, но спрятал его: наружу вышли только Wire | Ledger. Смене журнал тоже нужен, сводку она пишет сама, и взять его неоткуда. Ошибка не в слое, со слоем всё в порядке. Ошибка в том, что смене нужен сервис, который слой держит внутри.
provide годится, когда внутренний сервис нужен только слою: например, соединение с базой, которое никто, кроме хранилища, не трогает. Когда сервис нужен и слою, и программе, нужен provideMerge.
Проверка на месте сборки
Теперь другая ошибка. Забудь provideMerge совсем и сложи три слоя рядом:
export const testLayer = (sent: Iterable<number>) =>
Layer.mergeAll(Wire.layerTest, Ledger.layer(sent), Journal.layer).pipe(
Layer.satisfiesServicesType<never>(),
);
ex49b.ts(7,5): error TS2345: Argument of type '<ROut, E, RIn2 extends never>(layer: Layer<ROut, E, RIn2>) => Layer<ROut, E, RIn2>' is not assignable to parameter of type '(_: Layer<Wire | Journal | Ledger, never, Journal>) => Layer<Wire | Journal | Ledger, never, never>'.
Types of parameters 'layer' and '_' are incompatible.
Type 'Layer<Wire | Journal | Ledger, never, Journal>' is not assignable to type 'Layer<Wire | Journal | Ledger, never, never>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Type 'Journal' is not assignable to type 'never'.
Журнал в слое есть, но mergeAll не отдаёт его проводу: соседи друг друга не кормят. Объединение даёт журнал и тут же просит его для провода. Без последней строки это скомпилировалось бы, и ошибка вылезла бы далеко отсюда, в runPromise у того, кто положит слой.
Layer.satisfiesServicesType<never>() это проверка без действия: она возвращает тот же слой, но требует, чтобы его RIn подходил под never. Ошибка встаёт на месте сборки, строка 7, с той же последней строкой, что и всегда: Journal не подходит к never. Ставь её в конец каждой сборки, которая должна быть самодостаточной.
Где провод настоящий
Для ночи на Магистрали нужен провод, который отправляет по-настоящему и пишет в журнал только то, что провод принял:
// внутри class Wire
static readonly layer = (send: (line: string) => Effect.Effect<void, SendFailed>) =>
Layer.effect(
Wire,
Effect.gen(function* () {
const journal = yield* Journal;
return Wire.of({ send: (line) => Effect.tap(send(line), () => journal.write(`> ${line}`)) });
}),
);
Effect.tap пишет в журнал только после успеха send: оборванная строка в журнал не попадёт, а SendFailed пойдёт дальше. Башня поверх старого объекта собирается так же, как тестовая, только с настоящим проводом:
export const towerLayer = (tower: Tower) =>
Layer.mergeAll(Wire.layer((line) => sendLine(tower.send, line)), Ledger.layer(tower.sent)).pipe(
Layer.provideMerge(Journal.layer),
Layer.satisfiesServicesType<never>(),
);
export const runShift = (tower: Tower, lines: readonly string[]): Promise<readonly string[]> =>
Effect.runPromise(
nightShift(lines).pipe(
Effect.andThen(Journal.use((journal) => journal.entries)),
Effect.provide(towerLayer(tower)),
),
);
const night = ['T-3->T-1|5|Text|%D0%B3', 'T-9->T-1|7|Ping|', 'T-5->T-1|10|Ping|'];
const tower = {
send: async (line: string) => {
if (line.includes('T-9')) throw new Error('обрыв');
},
sent: new Set([1, 2]),
};
console.log(await runShift(tower, night));
[
'> T-1->T-3|5|Ack|5',
'> T-1->T-5|10|Ping|',
'обработано 2, ошибок 1, призраков 0, дублей 0, сбоев 0'
]
Ответ на T-9 оборвался: в журнале его нет, в сводке он ошибка. С testLayer([1, 2]) та же ночь даёт три строки > и ни одной ошибки: дублёр не рвётся.
Сколько журналов вышло
Граф собран, журнал один. Посмотри, как одна и та же башня собирается по-разному и сколько журналов получается. Первые две сборки ты уже знаешь; остальные, где журнал расходится, понадобятся в следующем уроке.
Где тут дыра
const testLayer = Layer.mergeAll(Wire.layerTest, Ledger.layer([1, 2]), Journal.layer) as Layer.Layer<
Wire | Ledger | Journal
>;
const exit = await Effect.runPromiseExit(
nightShift(['T-5->T-1|10|Ping|']).pipe(Effect.provide(testLayer)),
);
if (exit._tag === 'Failure')
console.log(exit.cause.reasons.map((reason) => (reason._tag === 'Die' ? String(reason.defect) : reason._tag)));
[ 'Error: Service not found: bashnya/Journal' ]
as заставил компилятор замолчать, но не дал проводу журнал. При сборке Wire.layerTest делает yield* Journal, смотрит в словарь по ключу 'bashnya/Journal' и ничего не находит: соседний Journal.layer в mergeAll проводу ничего не передаёт. Падение это дефект, Die, при первой же ночи, а не ошибка компилятора за минуту до коммита.
Лечится тем, что as уходит, а на его место встаёт Layer.provideMerge(Journal.layer). Компилятор ругался по делу. R и RIn это единственное место, где граф проверяют до запуска; приведение типа его отключает.
Башня, этап 14
Башня собирается одной вещью. Настоящий провод пишет в журнал только принятое, towerLayer собирает башню поверх старого объекта, testLayer собирает её для тестов, обе с пустыми требованиями. runShift на краю прогоняет ночь и отдаёт журнал.
Практика
Дальше
Ринцвальд дождался, пока Рыдкулл уйдёт, и показывает свой обработчик.
Утром журнала ночи у Ринцвальда нет: каждый кадр завёл свой журнал и унёс его с собой. Виджет выше уже подсказывает, как так вышло. Сколько раз Effect собирает слой и когда собирает его заново, разберём в следующем уроке.