Не принимай устаревший ответ
Чистый reducer с идентификатором активного запроса
Реализуй reduce для поиска. Start получает монотонно возрастающий положительный ID от владельца эффектов. Start с ID не больше текущего игнорируется. При новом Start очисти ошибку, поставь loading=true и сохрани прежние items. Loaded/Failed применяются только к текущему ID и только пока loading=true. Повторный терминальный результат игнорируется.
При успехе замени items и заверши загрузку; при ошибке сохрани items, запиши сообщение и заверши загрузку. Исходный state не меняется; входные списки по договору не мутируются после передачи.
Проверки: Start(1), Start(2), Loaded(1) → всё ещё загрузка 2; Loaded(2) → новый список; затем Failed(2) → успех не меняется. Failed старого ID не очищает текущую загрузку. Start(2) после завершения 2 игнорируется; Start(3) начинает новый поиск.
Это самостоятельный Kotlin-код без Android и корутин. Кейс развивает UDF из draft/Architecture interview.md.
Сохранённый код превышает предел редактора. Скачай его перед сбросом; исходная запись сохранена.
ID защищает от результата чужого запроса; loading защищает от второго терминального ответа на тот же запрос. Новый Start сохраняет полезные старые данные, поэтому UI может показывать кеш во время обновления. Это договор конкретной задачи, а не требование любой MVI-реализации.
Эффект возвращает действие и не мутирует state самостоятельно. Такой переход можно проверять таблицей входов/выходов, включая произвольный порядок завершения запросов. UI events.
Синхронизация вызовов reducer — обязанность владельца состояния. Чистота функции сама не делает параллельное присваивание результата атомарным. Решение не выполнялось в Kotlin.