# Schedule ⏲️
Schedule — это абстракция для управления повторением эффектов, которая определяет, сколько раз и с какими интервалами операция должна выполняться или перезапускаться. Например в
Effect она используется для определения расписаний при обработке ошибок (
retry) или при обычном повторении эффектов(
repeat)
В
Effect они реализованы следующими компонентами:
-
Schedule - начальное значение состояния + функция которая по некоторому состоянию и входным данным выдает тройку - [новое состояние, выходное значение,
Decision]
-
Decision - решение о продолжении выполнения - закончить или продолжать; спустя какое-то время или сразу же
-
Driver - по сути стейтфул итератор - менеджит состояние
Schedule - выставляя наружу только входные данные => выходное значение
Решил создать упрощенную модель на промисах, чтобы понять, как работает.
interface Schedule<out Out, in In = unknown, in out S = any> {
readonly initial: S,
readonly step: (input: In, currentState: S) => [newState: S, Out, Decision],
}
type Decision = { tag: "Continue"; delay: 0 | number } | { tag: "Done" };
interface Driver<Out, in In, in out S> {
private state: S;
constructor(readonly schedule: Schedule<Out, In, S>)
next(input: In): Promise<Out>
}
Давайте например напишем
Schedule который будет стрелять 5 раза с интервалами как последовательность фибоначчи 0-1-1-2-3 секунд. Определим сначала
unfold - операцию обратную
reduce - из одного значения мы будем получать последовательность.
const unfold = <A,>(initial: A, f: (a: A) => A): Schedule<A, unknown, A> =>
new Schedule<A, unknown, A>(initial, (_, state) => [
f(state), // новое состояние
state, // выходное значение
{ tag: "Continue", delay: 0 }, // мгновенное продолжение
]);
Теперь
fibonacci - передаем начальное значение и функцию шага
const fibonacci = unfold<[number, number]>([0, 1], ([prev, cur]) => [cur,cur + prev])
давайте теперь напишем
map чтобы создавать новый
Scheduler с новым выходным значением — дергаем оригинальный и воздействуем на его выход
const map = <Out, Out1>(f: (out: Out) => Out1) =>
<In, S>(schedule: Schedule<Out, In, S>) =>
new Schedule<Out1, In, S>(schedule.initial, (input, state) => {
const [_state, _out, _decision] = schedule.step(input, state);
return [_state, f(_out), _decision];
});
Все остальные комбинаторы похожи структурой, допустим комбинатор
intersect принимает два расписания и возвращает новый
new Schedule в кишках которого выясняет, что вернут предыдущие и обрабатывает результат и возвращает свой - если какой-то
Schedule решил остановиться то останавливается сам, если оба эмитят то эмитит наименьшее время.
Далее это передается в
Driver который сохраняет состояние которое вернул
Schedule и передает ему же на вход при следующей итерации, может сброситься до начального значения - потомучто эта информация зашита в сам
Scheduler. Функция
repeat в цикле запускает
driver.next и запускает эффект, до тех пор пока не получит терминальную отмашку.
Tак как это просто _описание_ расписания - подменив
Driver мы можем изменить поведение - давайте исполним все, например, игнорируя интервалы. или встроим в какой-нибудь
rxjs?
Самая мощь - это
Schedule - он композабелен. Да даже в этом примере видно как из простого
unfold с различными маппингамми мы создали достаточно сложное расписание.
Люблю разбираться как устроены фичи под капотом - потомучто это знание не только углубляет понимание конкретной технологии, но и расширяет арсенал транслируемых навыков.
такой
Schedule я бы не допетрил написать с нуля самостоятельно, a теперь понял смысл и оказалось, что это просто свистопляска вокруг одной функции
declare function step(input: In, currentState: S): [newState: S, Out, Decision];
Песок с разбором и полным кодом