tighten up HTTP and Circuitbreaker code

This commit is contained in:
jliddev
2020-12-23 16:15:42 -06:00
parent fe5906845a
commit 830e4b5919
10 changed files with 147 additions and 151 deletions

View File

@@ -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 [];

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,
});
}

View File

@@ -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[] {

View File

@@ -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);
}
}

View File

@@ -133,7 +133,6 @@ export class CurseFolderScanner {
try {
nativePath = this.getRealPath(fileInfo);
} catch (e) {
log.debug(`Include file path does not exist: ${fileInfo}`);
return;
}

View File

@@ -136,7 +136,6 @@ export class WowUpFolderScanner {
try {
nativePath = this.getRealPath(fileInfo);
} catch (e) {
log.debug(`Include file path does not exist: ${fileInfo}`);
return;
}

View File

@@ -10,4 +10,7 @@ export const AppConfig = {
},
autoUpdateIntervalMs: 3600000, // 1 hour
appUpdateIntervalMs: 3600000, // 1 hour
defaultHttpTimeoutMs: 2000,
defaultHttpResetTimeoutMs: 60000,
wowUpHubHttpTimeoutMs: 10000,
};

View File

@@ -10,4 +10,7 @@ export const AppConfig = {
},
autoUpdateIntervalMs: 3600000, // 1 hour
appUpdateIntervalMs: 3600000, // 1 hour
defaultHttpTimeoutMs: 2000,
defaultHttpResetTimeoutMs: 60000,
wowUpHubHttpTimeoutMs: 10000,
};