Spaces:
Sleeping
Sleeping
import { Injectable } from '@angular/core'; | |
import { environment } from '../../../environments/environment'; | |
import {BehaviorSubject, Observable, OperatorFunction, shareReplay, throwError, timer} from 'rxjs'; | |
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; | |
import { StateModel } from '../models/state.model'; | |
import { AuthenticationService } from './authentication.service'; | |
import { catchError, retry } from 'rxjs/operators'; | |
({ | |
providedIn: 'root', | |
}) | |
export class AppStateService { | |
private readonly _apiUrl = environment.apiUrl; | |
private readonly _state = new BehaviorSubject<StateModel>({ | |
leaderboards: [], | |
submissions: [], | |
datasets: [], | |
tasks: [], | |
controlPanelSubmissions: [], | |
adminSessionStatus: '', | |
}); | |
public readonly state$ = this._state.asObservable(); | |
constructor(public http: HttpClient, public authService: AuthenticationService) { | |
console.log(this._apiUrl); | |
this.refreshTasks(); | |
this.refreshDatasets(); | |
this.refreshLeaderboards(); | |
this.refreshSubmissions(); | |
authService.$authStatus.subscribe((status: string) => { | |
this._setState({ | |
...this.getState(), | |
adminSessionStatus: status, | |
}); | |
}); | |
} | |
private _setState(state: StateModel): void { | |
this._state.next(state); | |
} | |
public getState(): StateModel { | |
return this._state.getValue(); | |
} | |
private makeRequest<T>(request: Observable<T>, stateKey?: keyof StateModel, callback?: (data: any) => void) { | |
let obs = request.pipe( | |
this.retryStrategy(), | |
shareReplay(1), | |
catchError(this.handleRequestError.bind(this)) | |
); | |
obs.subscribe( | |
(data) => { | |
if (callback) { | |
callback(data); | |
} else if (stateKey) { | |
this._setState({ | |
...this.getState(), | |
[stateKey]: data | |
}); | |
} | |
} | |
); | |
return obs; | |
} | |
private handleRequestError(error: any): Observable<never> { | |
let errorMessage = 'An unknown error occurred!'; | |
if (error.error instanceof ErrorEvent) { | |
errorMessage = `Client Error: ${error.error.message}`; | |
} else { | |
switch (error.status) { | |
case 400: | |
errorMessage = `Bad Request: ${error.message}`; | |
break; | |
case 401: | |
errorMessage = 'Unauthorized: Please log in again.'; | |
this.authService.logout(); | |
break; | |
case 404: | |
errorMessage = `Not Found: The requested resource was not found.`; | |
break; | |
case 500: | |
errorMessage = `Internal Server Error: Please try again later.`; | |
break; | |
case 503: | |
errorMessage = `Service Unavailable: Please try again later.`; | |
break; | |
default: | |
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`; | |
break; | |
} | |
} | |
// Rethrow the error so it can be handled globally | |
return throwError(() => new Error(errorMessage)); | |
} | |
// Improved retry strategy with exponential backoff and jitter | |
private retryStrategy<T>() { | |
return <OperatorFunction<T, T>>((source) => | |
source.pipe( | |
retry({ | |
count: 3, // Maximum of 3 retry attempts | |
delay: (error, retryCount) => { | |
if (![500, 503].includes(error.status)) { | |
// Do not retry for errors other than 500 and 503 | |
return throwError(() => error); | |
} | |
// Exponential backoff with jitter | |
const jitter = Math.random() * 500; // Jitter value between 0-500ms | |
const backoffDelay = Math.pow(2, retryCount) * 1000 + jitter; // Exponential backoff | |
console.log(`Retrying request after ${backoffDelay}ms (attempt #${retryCount})`); | |
return timer(backoffDelay); | |
} | |
}) | |
) | |
); | |
} | |
// ======================== | |
// Public API | |
// ======================== | |
public submit( | |
modelName: string, | |
modelLink: string, | |
teamName: string, | |
contactEmail: string, | |
task: string, | |
dataset: string, | |
isPublic: boolean, | |
fileContent: string | |
) { | |
return this.makeRequest( | |
this.http.post<Object>(`${this._apiUrl}/submission/${task}/${dataset}`, { | |
modelName, | |
modelLink, | |
teamName, | |
contactEmail, | |
isPublic, | |
fileContent, | |
}) | |
); | |
} | |
public refreshTasks() { | |
return this.makeRequest(this.http.get(this._apiUrl + '/tasks'), 'tasks'); | |
} | |
public refreshDatasets() { | |
return this.makeRequest(this.http.get(this._apiUrl + '/datasets'), 'datasets'); | |
} | |
public refreshLeaderboards() { | |
return this.makeRequest(this.http.get(this._apiUrl + '/leaderboards'), 'leaderboards'); | |
} | |
public refreshSubmissions() { | |
return this.makeRequest(this.http.get(this._apiUrl + '/submissions'), 'submissions'); | |
} | |
public authenticate(password: string) { | |
let obs = this.authService.login(password); | |
obs.subscribe( | |
next => { | |
console.log('Login successful'); | |
this._setState({ | |
...this.getState(), | |
adminSessionStatus: 'authenticated', | |
}); | |
}); | |
return obs; | |
} | |
public refreshControlPanel() { | |
const headers = this.authService.getAuthHeaders(); | |
return this.makeRequest( | |
this.http.get(`${this._apiUrl}/control-panel-submissions`, { headers }), | |
'controlPanelSubmissions' | |
); | |
} | |
public updateSubmission(entry: any) { | |
const headers = this.authService.getAuthHeaders(); | |
return this.makeRequest( | |
this.http.put(`${this._apiUrl}/submission/${entry.id}`, entry, { headers }), | |
undefined, | |
() => this.refreshControlPanel() // Refresh control panel after update | |
); | |
} | |
public deleteSubmission(id: number) { | |
const headers = this.authService.getAuthHeaders(); | |
return this.makeRequest( | |
this.http.delete(`${this._apiUrl}/submission/${id}`, { headers }), | |
undefined, | |
() => this.refreshControlPanel() // Refresh control panel after deletion | |
); | |
} | |
} | |