Angular. Manually retry http request

Деньги эквивалент не труда, а ценности. Никто в обычных условиях не заплатит вам за то, что вы целый день писали хороший код, а потом его стерли никому не показав. © VolCh

Некоторое время назад на работе я решил достаточно необычную задачу, но в последствии на backend`е переделали логику и код был удалён из проекта. Хочу его сохранить.

В используемом ПО ISPsystem реализован механизм очереди задач. Все действия приходящие с frontend'a на backend попадают как задачи в очередь. Есть воркеры, которые берут задачи из очереди и выполняют их.

  • Очередь можно посмотреть «вне очереди». Придёт массив.
  • Если массив пустой, то все задачи завершены.
  • Если задача находится в ожидании или в обработке, то она придёт в массиве.
  • Если она завершится, то пропадёт из массива, при этом она может завершится статусом Complete или Fail.
  • Статус задачи можно посмотреть на отдельном хендлере.

Для общения с backend'ом используется long polling

В общем случае задача состояла в установке демонстрационной темы для CMS:

  • Пользователю показывается список тем.
  • Он кликает по выбранной теме
  • Создается демонстрационный сайт и в отдельной вкладке показывается его содержимое.

Чтобы предотвартить блокировку всплывающего окна применяется следующий подход:

  • Открывается отдельная вкладка, на которой находится Preloader.
  • При готовности темы CMS, на открытой вкладке меняется URL.

Frontend задача состояла:

  1. Сходить POST'ом на установку темы. В ответ сразу получить id задачи. На backend'е сначала будет установлен демонстрационный сайт (если он еще не создан) и после этого на него будет установлена выбранная пользователем тема.
  2. Паралельно ожидать, что успешно завершится задача установки демосайта и установки темы по полученному id.
  3. Если обе установки завершены успешно, то открыть демонстрацию.
  /**
   * Подписаться на задачу установки демо темы. Опрос идет через метод 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

Похожие записи

ngx translate attribute

Используется конструкция

<img src="image.jpg" [alt]="'KEY' | translate"> 
19 августа 2018 г. в Angular

Angular Resolver

Resolver гарантированно получает асинхронные данные до создания компонента исходя из параметров маршрута.

Angular Let Directive

*ngIf не отображает содержимое в falsy случаях (0, null, undefined) на async pipe, в пакете @rx-angular/template предлагается решение