mirror of
https://github.com/WowUp/WowUp.git
synced 2026-04-23 07:17:00 -04:00
tighten up HTTP and Circuitbreaker code
This commit is contained in:
@@ -4,7 +4,6 @@ import { AddonSearchResultDependency } from "../models/wowup/addon-search-result
|
||||
import { CurseDependency } from "../../common/curse/curse-dependency";
|
||||
import { CurseDependencyType } from "../../common/curse/curse-dependency-type";
|
||||
import * as _ from "lodash";
|
||||
import * as CircuitBreaker from "opossum";
|
||||
import { from, Observable } from "rxjs";
|
||||
import { first, map, timeout } from "rxjs/operators";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -32,17 +31,13 @@ import { ElectronService } from "../services";
|
||||
import { CachingService } from "../services/caching/caching-service";
|
||||
import { AddonProvider } from "./addon-provider";
|
||||
import { AppConfig } from "../../environments/environment";
|
||||
import { CircuitBreakerWrapper, NetworkService } from "app/services/network/network.service";
|
||||
|
||||
const API_URL = "https://addons-ecs.forgesvc.net/api/v2";
|
||||
const CHANGELOG_CACHE_TTL_MS = 30 * 60 * 1000;
|
||||
const CHANGELOG_FETCH_TIMEOUT_MS = 1500;
|
||||
|
||||
export class CurseAddonProvider implements AddonProvider {
|
||||
private readonly _circuitBreaker: CircuitBreaker<[clientType: () => Promise<any>], any>;
|
||||
|
||||
private getCircuitBreaker<T>() {
|
||||
return this._circuitBreaker as CircuitBreaker<[clientType: () => Promise<T>], T>;
|
||||
}
|
||||
private readonly _circuitBreaker: CircuitBreakerWrapper;
|
||||
|
||||
public readonly name = ADDON_PROVIDER_CURSEFORGE;
|
||||
public readonly forceIgnore = false;
|
||||
@@ -54,22 +49,13 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
constructor(
|
||||
private _httpClient: HttpClient,
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService
|
||||
private _electronService: ElectronService,
|
||||
_networkService: NetworkService
|
||||
) {
|
||||
this._circuitBreaker = new CircuitBreaker((action) => this.sendRequest(action), {
|
||||
resetTimeout: 60000,
|
||||
});
|
||||
|
||||
this._circuitBreaker.on("open", () => {
|
||||
console.log(`${this.name} circuit breaker open`);
|
||||
});
|
||||
this._circuitBreaker.on("close", () => {
|
||||
console.log(`${this.name} circuit breaker close`);
|
||||
});
|
||||
this._circuitBreaker = _networkService.getCircuitBreaker(`${this.name}_main`);
|
||||
}
|
||||
|
||||
public async getChangelog(clientType: WowClientType, externalId: string, externalReleaseId: string): Promise<string> {
|
||||
console.debug("GET CHANGE LOG");
|
||||
const cacheKey = `changelog_${externalId}_${externalReleaseId}`;
|
||||
const cachedChangelog = this._cachingService.get<string>(cacheKey);
|
||||
if (cachedChangelog) {
|
||||
@@ -78,10 +64,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
|
||||
try {
|
||||
const url = new URL(`${API_URL}/addon/${externalId}/file/${externalReleaseId}/changelog`);
|
||||
const changelogResponse = await this._httpClient
|
||||
.get(url.toString(), { responseType: "text" })
|
||||
.pipe(first(), timeout(CHANGELOG_FETCH_TIMEOUT_MS))
|
||||
.toPromise();
|
||||
const changelogResponse = await this._circuitBreaker.getText(url);
|
||||
|
||||
this._cachingService.set(cacheKey, changelogResponse, CHANGELOG_CACHE_TTL_MS);
|
||||
|
||||
@@ -201,24 +184,14 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
return gameVersionFlavor === this.getGameVersionFlavor(clientType);
|
||||
}
|
||||
|
||||
private async getAddonsByFingerprintsW(fingerprints: number[]) {
|
||||
private getAddonsByFingerprintsW(fingerprints: number[]) {
|
||||
const url = `${AppConfig.wowUpHubUrl}/curseforge/addons/fingerprint`;
|
||||
|
||||
console.log(`Wowup Fetching fingerprints`, JSON.stringify(fingerprints));
|
||||
|
||||
return await this._httpClient
|
||||
.post<CurseFingerprintsResponse>(url, {
|
||||
fingerprints,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
// If CurseForge API is ever fixed, put this back.
|
||||
// return await this.getCircuitBreaker<CurseFingerprintsResponse>().fire(
|
||||
// async () =>
|
||||
// await this._httpClient
|
||||
// .post<CurseFingerprintsResponse>(url, fingerprints)
|
||||
// .toPromise()
|
||||
// );
|
||||
return this._circuitBreaker.postJson<CurseFingerprintsResponse>(url, {
|
||||
fingerprints,
|
||||
});
|
||||
}
|
||||
|
||||
private async getAddonsByFingerprints(fingerprints: number[]): Promise<CurseFingerprintsResponse> {
|
||||
@@ -226,9 +199,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
|
||||
console.log(`Curse Fetching fingerprints`, JSON.stringify(fingerprints));
|
||||
|
||||
return await this.getCircuitBreaker<CurseFingerprintsResponse>().fire(
|
||||
async () => await this._httpClient.post<CurseFingerprintsResponse>(url, fingerprints).toPromise()
|
||||
);
|
||||
return await this._circuitBreaker.postJson(url, fingerprints);
|
||||
}
|
||||
|
||||
private async getAllIds(addonIds: number[]): Promise<CurseSearchResult[]> {
|
||||
@@ -238,9 +209,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
|
||||
const url = `${API_URL}/addon`;
|
||||
|
||||
return await this.getCircuitBreaker<CurseSearchResult[]>().fire(
|
||||
async () => await this._httpClient.post<CurseSearchResult[]>(url, addonIds).toPromise()
|
||||
);
|
||||
return await this._circuitBreaker.postJson<CurseSearchResult[]>(url, addonIds);
|
||||
}
|
||||
|
||||
private sendRequest<T>(action: () => Promise<T>): Promise<T> {
|
||||
@@ -354,19 +323,13 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
url.searchParams.set("gameId", "1");
|
||||
url.searchParams.set("searchFilter", query);
|
||||
|
||||
return await this.getCircuitBreaker<CurseSearchResult[]>().fire(
|
||||
async () => await this._httpClient.get<CurseSearchResult[]>(url.toString()).toPromise()
|
||||
);
|
||||
return await this._circuitBreaker.getJson<CurseSearchResult[]>(url);
|
||||
}
|
||||
|
||||
getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult> {
|
||||
const url = `${API_URL}/addon/${addonId}`;
|
||||
|
||||
return from(
|
||||
this.getCircuitBreaker<CurseSearchResult>().fire(
|
||||
async () => await this._httpClient.get<CurseSearchResult>(url).toPromise()
|
||||
)
|
||||
).pipe(
|
||||
return from(this._circuitBreaker.getJson<CurseSearchResult>(url)).pipe(
|
||||
map((result) => {
|
||||
if (!result) {
|
||||
return null;
|
||||
@@ -470,9 +433,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
updatedCount: 0,
|
||||
};
|
||||
|
||||
const result = await this.getCircuitBreaker<CurseGetFeaturedResponse>().fire(
|
||||
async () => await this._httpClient.post<CurseGetFeaturedResponse>(url, body).toPromise()
|
||||
);
|
||||
const result = await this._circuitBreaker.postJson<CurseGetFeaturedResponse>(url, body);
|
||||
|
||||
if (!result) {
|
||||
return [];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ADDON_PROVIDER_TUKUI } from "../../common/constants";
|
||||
import * as _ from "lodash";
|
||||
import * as CircuitBreaker from "opossum";
|
||||
import { from, Observable } from "rxjs";
|
||||
import { map, switchMap, timeout } from "rxjs/operators";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -13,25 +12,24 @@ import { AddonFolder } from "../models/wowup/addon-folder";
|
||||
import { AddonSearchResult } from "../models/wowup/addon-search-result";
|
||||
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
|
||||
import { CachingService } from "../services/caching/caching-service";
|
||||
import { NetworkService } from "../services/network/network.service";
|
||||
import { CircuitBreakerWrapper, NetworkService } from "../services/network/network.service";
|
||||
import { AddonProvider } from "./addon-provider";
|
||||
import { AppConfig } from "../../environments/environment";
|
||||
|
||||
const API_URL = "https://www.tukui.org/api.php";
|
||||
const CLIENT_API_URL = "https://www.tukui.org/client-api.php";
|
||||
const WOWUP_API_URL = AppConfig.wowUpHubUrl;
|
||||
const CHANGELOG_FETCH_TIMEOUT_MS = 1500;
|
||||
const CHANGELOG_CACHE_TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
export class TukUiAddonProvider implements AddonProvider {
|
||||
private readonly _circuitBreaker: CircuitBreaker<[clientType: WowClientType], TukUiAddon[]>;
|
||||
private readonly _changelogCircuitBreaker: CircuitBreaker<[clientType: TukUiAddon], string>;
|
||||
private readonly _circuitBreaker: CircuitBreakerWrapper;
|
||||
|
||||
public readonly name = ADDON_PROVIDER_TUKUI;
|
||||
public readonly forceIgnore = false;
|
||||
public readonly allowReinstall = true;
|
||||
public readonly allowChannelChange = false;
|
||||
public readonly allowEdit = true;
|
||||
|
||||
public enabled = true;
|
||||
|
||||
constructor(
|
||||
@@ -39,20 +37,13 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
private _cachingService: CachingService,
|
||||
private _networkService: NetworkService
|
||||
) {
|
||||
this._circuitBreaker = this._networkService.getCircuitBreaker<[clientType: WowClientType], TukUiAddon[]>(
|
||||
`${this.name}_main`,
|
||||
this.fetchApiResults
|
||||
);
|
||||
|
||||
this._changelogCircuitBreaker = this._networkService.getCircuitBreaker<[clientType: TukUiAddon], string>(
|
||||
`${this.name}_changelog`,
|
||||
this.fetchChangelogHtml
|
||||
);
|
||||
this._circuitBreaker = this._networkService.getCircuitBreaker(`${this.name}_main`);
|
||||
}
|
||||
|
||||
public async getChangelog(clientType: WowClientType, externalId: string, externalReleaseId: string): Promise<string> {
|
||||
const addons = await this.getAllAddons(clientType);
|
||||
return _.find(addons, (addon) => addon.id.toString() === externalId)?.changelog;
|
||||
const addon = _.find(addons, (addon) => addon.id.toString() === externalId.toString());
|
||||
return await this.formatChangelog(addon);
|
||||
}
|
||||
|
||||
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
|
||||
@@ -182,7 +173,7 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
private async formatChangelog(addon: TukUiAddon) {
|
||||
if (["-1", "-2"].includes(addon.id.toString())) {
|
||||
try {
|
||||
return await this._changelogCircuitBreaker.fire(addon);
|
||||
return await this.fetchChangelogHtml(addon);
|
||||
} catch (e) {
|
||||
console.error("Failed to get changelog", e);
|
||||
}
|
||||
@@ -198,10 +189,7 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
return cachedChangelog;
|
||||
}
|
||||
|
||||
const html = await this._httpClient
|
||||
.get(addon.changelog, { responseType: "text" })
|
||||
.pipe(timeout(CHANGELOG_FETCH_TIMEOUT_MS))
|
||||
.toPromise();
|
||||
const html = await this._circuitBreaker.getText(addon.changelog);
|
||||
|
||||
this._cachingService.set(cacheKey, html, CHANGELOG_CACHE_TTL_MS);
|
||||
|
||||
@@ -253,7 +241,7 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
const addons = await this._circuitBreaker.fire(clientType);
|
||||
const addons = await this.fetchApiResults(clientType);
|
||||
|
||||
this._cachingService.set(cacheKey, addons);
|
||||
return addons;
|
||||
@@ -278,10 +266,11 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.append(query, "all");
|
||||
|
||||
const addons = await this._httpClient.get<TukUiAddon[]>(url.toString()).toPromise();
|
||||
const addons = await this._circuitBreaker.getJson<TukUiAddon[]>(url);
|
||||
|
||||
if (this.isRetail(clientType)) {
|
||||
addons.push(await this.getTukUiRetailAddon().toPromise());
|
||||
addons.push(await this.getElvUiRetailAddon().toPromise());
|
||||
addons.push(await this.getTukUiRetailAddon());
|
||||
addons.push(await this.getElvUiRetailAddon());
|
||||
}
|
||||
|
||||
return addons;
|
||||
@@ -295,11 +284,11 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
return this.getClientApiAddon("elvui");
|
||||
}
|
||||
|
||||
private getClientApiAddon(addonName: string): Observable<TukUiAddon> {
|
||||
private getClientApiAddon(addonName: string): Promise<TukUiAddon> {
|
||||
const url = new URL(CLIENT_API_URL);
|
||||
url.searchParams.append("ui", addonName);
|
||||
|
||||
return this._httpClient.get<TukUiAddon>(url.toString());
|
||||
return this._circuitBreaker.getJson<TukUiAddon>(url);
|
||||
}
|
||||
|
||||
private isRetail(clientType: WowClientType) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ADDON_PROVIDER_WOWINTERFACE } from "../../common/constants";
|
||||
import * as _ from "lodash";
|
||||
import * as CircuitBreaker from "opossum";
|
||||
import { from, Observable } from "rxjs";
|
||||
import { first, map, timeout } from "rxjs/operators";
|
||||
import { map } from "rxjs/operators";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Addon } from "../entities/addon";
|
||||
import { WowClientType } from "../models/warcraft/wow-client-type";
|
||||
@@ -12,18 +11,16 @@ import { AddonChannelType } from "../models/wowup/addon-channel-type";
|
||||
import { AddonFolder } from "../models/wowup/addon-folder";
|
||||
import { AddonSearchResult } from "../models/wowup/addon-search-result";
|
||||
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
|
||||
import { ElectronService } from "../services";
|
||||
import { CachingService } from "../services/caching/caching-service";
|
||||
import { FileService } from "../services/files/file.service";
|
||||
import { AddonProvider } from "./addon-provider";
|
||||
import { convertBbcode } from "../utils/bbcode.utils";
|
||||
import { CircuitBreakerWrapper, NetworkService } from "app/services/network/network.service";
|
||||
|
||||
const API_URL = "https://api.mmoui.com/v4/game/WOW";
|
||||
const ADDON_URL = "https://www.wowinterface.com/downloads/info";
|
||||
const CHANGELOG_FETCH_TIMEOUT_MS = 1500;
|
||||
|
||||
export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
private readonly _circuitBreaker: CircuitBreaker<[addonId: string], AddonDetailsResponse>;
|
||||
private readonly _circuitBreaker: CircuitBreakerWrapper;
|
||||
|
||||
public readonly name = ADDON_PROVIDER_WOWINTERFACE;
|
||||
public readonly forceIgnore = false;
|
||||
@@ -35,19 +32,9 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
constructor(
|
||||
private _httpClient: HttpClient,
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService
|
||||
private _networkService: NetworkService
|
||||
) {
|
||||
this._circuitBreaker = new CircuitBreaker(this.getAddonDetails, {
|
||||
resetTimeout: 60000,
|
||||
});
|
||||
|
||||
this._circuitBreaker.on("open", () => {
|
||||
console.log(`${this.name} circuit breaker open`);
|
||||
});
|
||||
this._circuitBreaker.on("close", () => {
|
||||
console.log(`${this.name} circuit breaker close`);
|
||||
});
|
||||
this._circuitBreaker = this._networkService.getCircuitBreaker(`${this.name}_main`);
|
||||
}
|
||||
|
||||
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
|
||||
@@ -84,7 +71,7 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
throw new Error(`Addon ID not found ${addonUri}`);
|
||||
}
|
||||
|
||||
var addon = await this._circuitBreaker.fire(addonId);
|
||||
var addon = await this.getAddonDetails(addonId);
|
||||
if (addon == null) {
|
||||
throw new Error(`Bad addon api response ${addonUri}`);
|
||||
}
|
||||
@@ -102,7 +89,7 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
}
|
||||
|
||||
public getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult> {
|
||||
return from(this._circuitBreaker.fire(addonId)).pipe(
|
||||
return from(this.getAddonDetails(addonId)).pipe(
|
||||
map((result) => (result ? this.toAddonSearchResult(result, "") : undefined))
|
||||
);
|
||||
}
|
||||
@@ -129,7 +116,7 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
continue;
|
||||
}
|
||||
|
||||
const details = await this._circuitBreaker.fire(addonFolder.toc.wowInterfaceId);
|
||||
const details = await this.getAddonDetails(addonFolder.toc.wowInterfaceId);
|
||||
|
||||
addonFolder.matchingAddon = this.toAddon(details, clientType, addonChannelType, addonFolder);
|
||||
}
|
||||
@@ -152,17 +139,11 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
throw new Error(`Unhandled URL: ${addonUri}`);
|
||||
}
|
||||
|
||||
private getAddonDetails = (addonId: string): Promise<AddonDetailsResponse> => {
|
||||
private getAddonDetails = async (addonId: string): Promise<AddonDetailsResponse> => {
|
||||
const url = new URL(`${API_URL}/filedetails/${addonId}.json`);
|
||||
|
||||
return this._httpClient
|
||||
.get<AddonDetailsResponse[]>(url.toString())
|
||||
.pipe(
|
||||
first(),
|
||||
timeout(CHANGELOG_FETCH_TIMEOUT_MS),
|
||||
map((responses) => _.first(responses))
|
||||
)
|
||||
.toPromise();
|
||||
const responses = await this._circuitBreaker.getJson<AddonDetailsResponse[]>(url);
|
||||
return _.first(responses);
|
||||
};
|
||||
|
||||
private getThumbnailUrl(response: AddonDetailsResponse) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { from, Observable } from "rxjs";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { ADDON_PROVIDER_HUB, WOWUP_GET_SCAN_RESULTS } from "../../common/constants";
|
||||
import { WowUpScanResult } from "../../common/wowup/wowup-scan-result";
|
||||
@@ -25,6 +25,7 @@ import * as _ from "lodash";
|
||||
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
|
||||
import { getGameVersion } from "../utils/addon.utils";
|
||||
import { map } from "rxjs/operators";
|
||||
import { CircuitBreakerWrapper, NetworkService } from "app/services/network/network.service";
|
||||
|
||||
const API_URL = AppConfig.wowUpHubUrl;
|
||||
|
||||
@@ -33,6 +34,8 @@ export interface GetAddonBatchResponse {
|
||||
}
|
||||
|
||||
export class WowUpAddonProvider implements AddonProvider {
|
||||
private readonly _circuitBreaker: CircuitBreakerWrapper;
|
||||
|
||||
public readonly name = ADDON_PROVIDER_HUB;
|
||||
public readonly forceIgnore = false;
|
||||
public readonly allowReinstall = true;
|
||||
@@ -40,16 +43,26 @@ export class WowUpAddonProvider implements AddonProvider {
|
||||
public readonly allowEdit = true;
|
||||
public enabled = true;
|
||||
|
||||
constructor(private _httpClient: HttpClient, private _electronService: ElectronService) {}
|
||||
constructor(
|
||||
private _httpClient: HttpClient,
|
||||
private _electronService: ElectronService,
|
||||
_networkService: NetworkService
|
||||
) {
|
||||
this._circuitBreaker = _networkService.getCircuitBreaker(
|
||||
`${this.name}_main`,
|
||||
AppConfig.defaultHttpResetTimeoutMs,
|
||||
AppConfig.wowUpHubHttpTimeoutMs
|
||||
);
|
||||
}
|
||||
|
||||
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
|
||||
const gameType = this.getWowGameType(clientType);
|
||||
const url = new URL(`${API_URL}/addons/batch/${gameType}`);
|
||||
const response = await this._httpClient
|
||||
.post<GetAddonBatchResponse>(url.toString(), {
|
||||
addonIds: _.map(addonIds, (id) => parseInt(id, 10)),
|
||||
})
|
||||
.toPromise();
|
||||
const addonIdList = _.map(addonIds, (id) => parseInt(id, 10));
|
||||
|
||||
const response = await this._circuitBreaker.postJson<GetAddonBatchResponse>(url, {
|
||||
addonIds: addonIdList,
|
||||
});
|
||||
|
||||
const searchResults = _.map(response?.addons, (addon) => this.getSearchResult(addon));
|
||||
return searchResults;
|
||||
@@ -58,7 +71,7 @@ export class WowUpAddonProvider implements AddonProvider {
|
||||
public async getFeaturedAddons(clientType: WowClientType): Promise<AddonSearchResult[]> {
|
||||
const gameType = this.getWowGameType(clientType);
|
||||
const url = new URL(`${API_URL}/addons/featured/${gameType}?count=30`);
|
||||
const addons = await this._httpClient.get<WowUpGetAddonsResponse>(url.toString()).toPromise();
|
||||
const addons = await this._circuitBreaker.getJson<WowUpGetAddonsResponse>(url);
|
||||
|
||||
const searchResults = _.map(addons?.addons, (addon) => this.getSearchResult(addon));
|
||||
return searchResults;
|
||||
@@ -68,7 +81,7 @@ export class WowUpAddonProvider implements AddonProvider {
|
||||
const gameType = this.getWowGameType(clientType);
|
||||
const url = new URL(`${API_URL}/addons/search/${gameType}?query=${query}&limit=10`);
|
||||
|
||||
const addons = await this._httpClient.get<WowUpSearchAddonsResponse>(url.toString()).toPromise();
|
||||
const addons = await this._circuitBreaker.getJson<WowUpSearchAddonsResponse>(url);
|
||||
const searchResults = _.map(addons?.addons, (addon) => this.getSearchResult(addon));
|
||||
|
||||
return searchResults;
|
||||
@@ -91,7 +104,7 @@ export class WowUpAddonProvider implements AddonProvider {
|
||||
|
||||
getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult> {
|
||||
const url = new URL(`${API_URL}/addons/${addonId}`);
|
||||
return this._httpClient.get<WowUpGetAddonResponse>(url.toString()).pipe(
|
||||
return from(this._circuitBreaker.getJson<WowUpGetAddonResponse>(url)).pipe(
|
||||
map((result) => {
|
||||
return this.getSearchResult(result.addon);
|
||||
})
|
||||
@@ -120,7 +133,7 @@ export class WowUpAddonProvider implements AddonProvider {
|
||||
console.debug("ScanResults", scanResults.length);
|
||||
const fingerprints = scanResults.map((result) => result.fingerprint);
|
||||
console.log("fingerprintRequest", JSON.stringify(fingerprints));
|
||||
const fingerprintResponse = await this.getAddonsByFingerprints(fingerprints).toPromise();
|
||||
const fingerprintResponse = await this.getAddonsByFingerprints(fingerprints);
|
||||
|
||||
console.debug("fingerprintResponse", fingerprintResponse);
|
||||
|
||||
@@ -180,10 +193,10 @@ export class WowUpAddonProvider implements AddonProvider {
|
||||
return release.game_type === this.getWowGameType(clientType);
|
||||
}
|
||||
|
||||
private getAddonsByFingerprints(fingerprints: string[]): Observable<GetAddonsByFingerprintResponse> {
|
||||
private getAddonsByFingerprints(fingerprints: string[]): Promise<GetAddonsByFingerprintResponse> {
|
||||
const url = `${API_URL}/addons/fingerprint`;
|
||||
|
||||
return this._httpClient.post<any>(url, {
|
||||
return this._circuitBreaker.postJson<any>(url, {
|
||||
fingerprints,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { RaiderIoAddonProvider } from "../../addon-providers/raiderio-provider";
|
||||
import { CachingService } from "../caching/caching-service";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
import { WowUpService } from "../wowup/wowup.service";
|
||||
import { FileService } from "../files/file.service";
|
||||
import { NetworkService } from "../network/network.service";
|
||||
|
||||
@Injectable({
|
||||
@@ -23,7 +22,6 @@ export class AddonProviderFactory {
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService,
|
||||
private _httpClient: HttpClient,
|
||||
private _fileService: FileService,
|
||||
private _wowupService: WowUpService,
|
||||
private _networkService: NetworkService
|
||||
) {}
|
||||
@@ -33,7 +31,7 @@ export class AddonProviderFactory {
|
||||
}
|
||||
|
||||
public createCurseAddonProvider(): CurseAddonProvider {
|
||||
return new CurseAddonProvider(this._httpClient, this._cachingService, this._electronService);
|
||||
return new CurseAddonProvider(this._httpClient, this._cachingService, this._electronService, this._networkService);
|
||||
}
|
||||
|
||||
public createTukUiAddonProvider(): TukUiAddonProvider {
|
||||
@@ -41,12 +39,7 @@ export class AddonProviderFactory {
|
||||
}
|
||||
|
||||
public createWowInterfaceAddonProvider(): WowInterfaceAddonProvider {
|
||||
return new WowInterfaceAddonProvider(
|
||||
this._httpClient,
|
||||
this._cachingService,
|
||||
this._electronService,
|
||||
this._fileService
|
||||
);
|
||||
return new WowInterfaceAddonProvider(this._httpClient, this._cachingService, this._networkService);
|
||||
}
|
||||
|
||||
public createGitHubAddonProvider(): GitHubAddonProvider {
|
||||
@@ -54,7 +47,7 @@ export class AddonProviderFactory {
|
||||
}
|
||||
|
||||
public createWowUpAddonProvider(): WowUpAddonProvider {
|
||||
return new WowUpAddonProvider(this._httpClient, this._electronService);
|
||||
return new WowUpAddonProvider(this._httpClient, this._electronService, this._networkService);
|
||||
}
|
||||
|
||||
public getAll(): AddonProvider[] {
|
||||
|
||||
@@ -1,12 +1,75 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { Injectable } from "@angular/core";
|
||||
import { AppConfig } from "environments/environment";
|
||||
import * as CircuitBreaker from "opossum";
|
||||
import { Subject } from "rxjs";
|
||||
import { first, tap, timeout } from "rxjs/operators";
|
||||
|
||||
export interface CircuitBreakerChangeEvent {
|
||||
state: "open" | "closed";
|
||||
}
|
||||
|
||||
const DEFAULT_RESET_TIMEOUT_MS = 60 * 1000;
|
||||
export class CircuitBreakerWrapper {
|
||||
private readonly _name: string;
|
||||
private readonly _cb: CircuitBreaker;
|
||||
private readonly _httpClient: HttpClient;
|
||||
private readonly _defaultTimeoutMs: number;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
httpClient: HttpClient,
|
||||
resetTimeoutMs = AppConfig.defaultHttpResetTimeoutMs,
|
||||
httpTimeoutMs = AppConfig.defaultHttpTimeoutMs
|
||||
) {
|
||||
this._name = name;
|
||||
this._httpClient = httpClient;
|
||||
this._defaultTimeoutMs = httpTimeoutMs;
|
||||
this._cb = new CircuitBreaker(this.internalAction, {
|
||||
resetTimeout: resetTimeoutMs,
|
||||
});
|
||||
this._cb.on("open", () => {
|
||||
console.log(`${name} circuit breaker open`);
|
||||
});
|
||||
this._cb.on("close", () => {
|
||||
console.log(`${name} circuit breaker close`);
|
||||
});
|
||||
}
|
||||
|
||||
public async fire<TOUT>(action: () => Promise<TOUT>): Promise<TOUT> {
|
||||
return (await this._cb.fire(action)) as TOUT;
|
||||
}
|
||||
|
||||
public getJson<T>(url: URL | string, timeoutMs?: number): Promise<T> {
|
||||
return this.fire(() =>
|
||||
this._httpClient
|
||||
.get<T>(url.toString())
|
||||
.pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs))
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
public getText(url: URL | string, timeoutMs?: number): Promise<string> {
|
||||
return this.fire(() =>
|
||||
this._httpClient
|
||||
.get(url.toString(), { responseType: "text" })
|
||||
.pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs))
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
public postJson<T>(url: URL | string, body: any, timeoutMs?: number): Promise<T> {
|
||||
return this.fire<T>(() =>
|
||||
this._httpClient
|
||||
.post<T>(url.toString(), body)
|
||||
.pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs))
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
private internalAction = (action: () => Promise<any>) => {
|
||||
return action?.call(this);
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
@@ -16,21 +79,13 @@ export class NetworkService {
|
||||
|
||||
public breakerChanged$ = this._breakerChangedSrc.asObservable();
|
||||
|
||||
public getCircuitBreaker<TI extends unknown[] = unknown[], TR = unknown>(
|
||||
name: string,
|
||||
action: (...args: TI) => Promise<TR>,
|
||||
resetTimeoutMs: number = DEFAULT_RESET_TIMEOUT_MS
|
||||
): CircuitBreaker<TI, TR> {
|
||||
const cb = new CircuitBreaker(action, {
|
||||
resetTimeout: resetTimeoutMs,
|
||||
});
|
||||
cb.on("open", () => {
|
||||
console.log(`${name} circuit breaker open`);
|
||||
});
|
||||
cb.on("close", () => {
|
||||
console.log(`${name} circuit breaker close`);
|
||||
});
|
||||
public constructor(private _httpClient: HttpClient) {}
|
||||
|
||||
return cb;
|
||||
public getCircuitBreaker(
|
||||
name: string,
|
||||
resetTimeoutMs: number = AppConfig.defaultHttpResetTimeoutMs,
|
||||
httpTimeoutMs: number = AppConfig.defaultHttpTimeoutMs
|
||||
): CircuitBreakerWrapper {
|
||||
return new CircuitBreakerWrapper(name, this._httpClient, resetTimeoutMs, httpTimeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,6 @@ export class CurseFolderScanner {
|
||||
try {
|
||||
nativePath = this.getRealPath(fileInfo);
|
||||
} catch (e) {
|
||||
log.debug(`Include file path does not exist: ${fileInfo}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,6 @@ export class WowUpFolderScanner {
|
||||
try {
|
||||
nativePath = this.getRealPath(fileInfo);
|
||||
} catch (e) {
|
||||
log.debug(`Include file path does not exist: ${fileInfo}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,4 +10,7 @@ export const AppConfig = {
|
||||
},
|
||||
autoUpdateIntervalMs: 3600000, // 1 hour
|
||||
appUpdateIntervalMs: 3600000, // 1 hour
|
||||
defaultHttpTimeoutMs: 2000,
|
||||
defaultHttpResetTimeoutMs: 60000,
|
||||
wowUpHubHttpTimeoutMs: 10000,
|
||||
};
|
||||
|
||||
@@ -10,4 +10,7 @@ export const AppConfig = {
|
||||
},
|
||||
autoUpdateIntervalMs: 3600000, // 1 hour
|
||||
appUpdateIntervalMs: 3600000, // 1 hour
|
||||
defaultHttpTimeoutMs: 2000,
|
||||
defaultHttpResetTimeoutMs: 60000,
|
||||
wowUpHubHttpTimeoutMs: 10000,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user