|
|
@@ -1,6 +1,6 @@
|
|
|
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
|
|
|
import { inject, Injectable } from '@angular/core';
|
|
|
-import { catchError, map, Observable, throwError } from 'rxjs';
|
|
|
+import { catchError, map, Observable, retry, throwError, timer } from 'rxjs';
|
|
|
import { API_CONFIG } from './app.constants';
|
|
|
import { requiresCloudSession, usesCloudGateway } from './cloud-action-migration';
|
|
|
import { ApiEnvelope } from './models';
|
|
|
@@ -21,9 +21,7 @@ export class ApiService {
|
|
|
const hasAnySession = Boolean(this.sessions.token());
|
|
|
const token = this.parseSessionToken();
|
|
|
if (usesCloudGateway(action, params, Boolean(token), hasAnySession)) return this.callCloud<T>(action, params, requiresCloudSession(action, params) ? token : '');
|
|
|
- return this.http
|
|
|
- .get<unknown>(this.actionUrl(action), { params: this.toHttpParams(params) })
|
|
|
- .pipe(map((response) => this.normalize<T>(response)), catchError((error) => this.handleError(error)));
|
|
|
+ return this.resilient<T>(this.http.get<unknown>(this.actionUrl(action), { params: this.toHttpParams(params) }));
|
|
|
}
|
|
|
|
|
|
post<T>(action: string, body: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
|
|
|
@@ -42,11 +40,9 @@ export class ApiService {
|
|
|
|
|
|
private postLegacy<T>(action: string, body: Record<string, unknown>): Observable<ApiEnvelope<T>> {
|
|
|
const encoded = this.toHttpParams(body);
|
|
|
- return this.http
|
|
|
- .post<unknown>(this.actionUrl(action), encoded.toString(), {
|
|
|
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
|
- })
|
|
|
- .pipe(map((response) => this.normalize<T>(response)), catchError((error) => this.handleError(error)));
|
|
|
+ return this.resilient<T>(this.http.post<unknown>(this.actionUrl(action), encoded.toString(), {
|
|
|
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
|
+ }));
|
|
|
}
|
|
|
|
|
|
callCloud<T>(action: string, params: Record<string, unknown> = {}, sessionToken = ''): Observable<ApiEnvelope<T>> {
|
|
|
@@ -54,22 +50,18 @@ export class ApiService {
|
|
|
...(sessionToken ? { token: sessionToken } : {}),
|
|
|
params: { action, ...params },
|
|
|
};
|
|
|
- return this.http
|
|
|
- .post<unknown>(API_CONFIG.cloudFunctionUrl, body, {
|
|
|
- headers: {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'X-Parse-Application-Id': API_CONFIG.parseAppId,
|
|
|
- },
|
|
|
- })
|
|
|
- .pipe(map((response) => this.normalize<T>(response)), catchError((error) => this.handleError(error)));
|
|
|
+ return this.resilient<T>(this.http.post<unknown>(API_CONFIG.cloudFunctionUrl, body, {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'X-Parse-Application-Id': API_CONFIG.parseAppId,
|
|
|
+ },
|
|
|
+ }));
|
|
|
}
|
|
|
|
|
|
upload(file: File): Observable<ApiEnvelope<string>> {
|
|
|
const formData = new FormData();
|
|
|
formData.append('file', file, file.name);
|
|
|
- return this.http
|
|
|
- .post<unknown>(this.actionUrl('upload'), formData)
|
|
|
- .pipe(map((response) => this.normalize<string>(response)), catchError((error) => this.handleError(error)));
|
|
|
+ return this.resilient<string>(this.http.post<unknown>(this.actionUrl('upload'), formData));
|
|
|
}
|
|
|
|
|
|
actionUrl(action: string): string {
|
|
|
@@ -105,6 +97,19 @@ export class ApiService {
|
|
|
return normalized;
|
|
|
}
|
|
|
|
|
|
+ private resilient<T>(request: Observable<unknown>): Observable<ApiEnvelope<T>> {
|
|
|
+ return request.pipe(
|
|
|
+ retry({
|
|
|
+ count: 2,
|
|
|
+ delay: (error, retryCount) => this.isTransientError(error)
|
|
|
+ ? timer(retryCount * 400)
|
|
|
+ : throwError(() => error),
|
|
|
+ }),
|
|
|
+ map((response) => this.normalize<T>(response)),
|
|
|
+ catchError((error) => this.handleError(error)),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
private toHttpParams(values: Record<string, unknown>): HttpParams {
|
|
|
let params = new HttpParams();
|
|
|
for (const [key, raw] of Object.entries(values)) {
|
|
|
@@ -119,9 +124,11 @@ export class ApiService {
|
|
|
if (error instanceof ApiError) return throwError(() => error);
|
|
|
if (error instanceof HttpErrorResponse) {
|
|
|
const payload = error.error as { retmsg?: string } | string | null;
|
|
|
- const message = typeof payload === 'object' && payload?.retmsg
|
|
|
- ? payload.retmsg
|
|
|
- : error.message || '网络请求失败';
|
|
|
+ const message = this.isTransientError(error)
|
|
|
+ ? '服务暂时繁忙,已自动重试但仍未成功,请稍后再试。'
|
|
|
+ : typeof payload === 'object' && payload?.retmsg
|
|
|
+ ? payload.retmsg
|
|
|
+ : error.message || '网络请求失败';
|
|
|
return throwError(() => new ApiError(message, error.status, payload));
|
|
|
}
|
|
|
return throwError(() => new ApiError(error instanceof Error ? error.message : '未知错误'));
|
|
|
@@ -131,4 +138,8 @@ export class ApiService {
|
|
|
const token = this.sessions.token();
|
|
|
return token.startsWith('r:') ? token : '';
|
|
|
}
|
|
|
+
|
|
|
+ private isTransientError(error: unknown): boolean {
|
|
|
+ return error instanceof HttpErrorResponse && [0, 502, 503, 504].includes(error.status);
|
|
|
+ }
|
|
|
}
|