Angular. Manually retry http request
Деньги эквивалент не труда, а ценности. Никто в обычных условиях не заплатит вам за то, что вы целый день писали хороший код, а потом его стерли никому не показав. © VolCh
Некоторое время назад на работе я решил достаточно необычную задачу, но в последствии на backend`е переделали логику и код был удалён из проекта. Хочу его сохранить.
В используемом ПО ISPsystem реализован механизм очереди задач. Все действия приходящие с frontend'a на backend попадают как задачи в очередь. Есть воркеры, которые берут задачи из очереди и выполняют их.
- Очередь можно посмотреть «вне очереди». Придёт массив.
- Если массив пустой, то все задачи завершены.
- Если задача находится в ожидании или в обработке, то она придёт в массиве.
- Если она завершится, то пропадёт из массива, при этом она может завершится статусом
Complete
илиFail
. - Статус задачи можно посмотреть на отдельном хендлере.
Для общения с backend'ом используется long polling
В общем случае задача состояла в установке демонстрационной темы для CMS:
- Пользователю показывается список тем.
- Он кликает по выбранной теме
- Создается демонстрационный сайт и в отдельной вкладке показывается его содержимое.
Чтобы предотвартить блокировку всплывающего окна применяется следующий подход:
- Открывается отдельная вкладка, на которой находится Preloader.
- При готовности темы CMS, на открытой вкладке меняется URL.
Frontend задача состояла:
- Сходить POST'ом на установку темы. В ответ сразу получить id задачи. На backend'е сначала будет установлен демонстрационный сайт (если он еще не создан) и после этого на него будет установлена выбранная пользователем тема.
- Паралельно ожидать, что успешно завершится задача установки демосайта и установки темы по полученному id.
- Если обе установки завершены успешно, то открыть демонстрацию.
/**
* Подписаться на задачу установки демо темы. Опрос идет через метод taskWait
* из core API. Пока задача выполняется, она приходит в массиве. Когда задача
* завершается (complete или fail), то оно исчезает из массива. После этого
* через наш backend проверяется статус задачи.
* @param id - идентификатор задачи
* @param delayDuration - задержка между опросами статуса задачи
* @param retryNumber - количество опросов
*/
private _taskWait(
id: number,
delayDuration: number,
retryNumber: number
): Observable<Readonly<ITaskStatus>> {
let lastNotify = 0;
return defer(() => this._coreApi.taskWait([id], lastNotify)).pipe(
map(res => {
lastNotify = res.last_notify;
if (res.task.some(t => t.id === id)) {
throw new Error('Task not completed yet');
}
return res;
}),
retryWhen(errors =>
concat(
errors.pipe(
delay(delayDuration),
take(retryNumber)
),
throwError(new Error('Task limit exceeded'))
)
),
mergeMap(() => this._taskApiService.status(id)),
map(res => {
if (res.status !== TaskStatusState.COMPLETE) {
throw new Error('Task "demo create" failed');
}
return res;
})
);
}
/**
* Получить поток состояния демонстрации темы
* @param delayDuration - задержка между запросами
* @param retryNumber - количество запросов
*/
private _demoState(delayDuration: number, retryNumber: number): Observable<IDemo> {
return this._demoApiService.demo().pipe(
map(demo => {
if (demo.status !== DemoState.ACTIVE) {
throw new Error('Not ready');
}
return demo;
}),
retryWhen(errors =>
concat(
errors.pipe(
delay(delayDuration),
take(retryNumber)
),
throwError(new Error('Retry limit exceeded'))
)
)
);
}
/**
* Открыть демонстрацию темы
*/
public openDemo(): void {
const name = this.activeTemplate.slug;
const delayBetweenAttempts = 2000;
const attemptNumber = 25;
// открывать окно следует заранее и неасинхронно, чтобы избежать
// блокировки всплывающего окна браузером;
// непосредственно открытие демо шаблона идёт подменой url
const lang = this._translateService.currentLang;
const demoTab = this._openLinkInNewWindow(`/assets/template-demo.html?lang=${lang}`);
this.demoLoading = true;
this._demoApiService
.setDemoTheme(name)
.pipe(
mergeMap(res =>
forkJoin(
this._taskWait(res.task, delayBetweenAttempts, attemptNumber),
this._demoState(delayBetweenAttempts, attemptNumber)
)
),
)
.subscribe(
([task, demo]) => {
demoTab.location.href = demo.url;
this.demoLoading = false;
this._changeDetectorRef.markForCheck();
},
() => {
this._userNotificationService.error(
'SITE.NEW.TEMPLATE.DEMO.NOTIFICATION.CREATE_FAILED',
null,
{ name }
);
demoTab.close();
this.demoLoading = false;
this._changeDetectorRef.markForCheck();
}
);
}
Эксперименты по отлову ошибок в rxjs