From 96e410bf9f2b241a1bdb8cfc44cea476494a76f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Fedor?= Date: Sat, 16 Mar 2024 18:27:12 +0100 Subject: [PATCH] Refactor FeatureManager --- .../Community/ProfileHome/CProfileHome.js | 8 +- src/js/Content/Features/Store/App/CApp.js | 8 +- .../Features/Store/Wishlist/CWishlist.js | 4 +- src/js/Content/Modules/Feature/Feature.ts | 4 +- .../Content/Modules/Feature/FeatureManager.js | 126 ------------------ .../Content/Modules/Feature/FeatureManager.ts | 98 ++++++++++++++ 6 files changed, 108 insertions(+), 140 deletions(-) delete mode 100644 src/js/Content/Modules/Feature/FeatureManager.js create mode 100644 src/js/Content/Modules/Feature/FeatureManager.ts diff --git a/src/js/Content/Features/Community/ProfileHome/CProfileHome.js b/src/js/Content/Features/Community/ProfileHome/CProfileHome.js index 0e1ebaa8c..c8468ecad 100644 --- a/src/js/Content/Features/Community/ProfileHome/CProfileHome.js +++ b/src/js/Content/Features/Community/ProfileHome/CProfileHome.js @@ -1,4 +1,4 @@ -import {Background, ContextType, SteamId} from "../../../modulesContent"; +import {Background, ContextType, FeatureManager, SteamId} from "../../../modulesContent"; import {CCommunityBase} from "../CCommunityBase"; import FEarlyAccess from "../../Common/FEarlyAccess"; import FCommunityProfileLinks from "./FCommunityProfileLinks"; @@ -48,12 +48,10 @@ export class CProfileHome extends CCommunityBase { FEarlyAccess.show(document.querySelectorAll(".game_info_cap, .showcase_slot:not(.showcase_achievement)")); // Need to wait on custom background and style (LNY2020 may set the background) to be fetched and set - FPinnedBackground.dependencies = [FCustomBackground, FCustomStyle]; - FPinnedBackground.weakDependency = true; + FeatureManager.dependency(FPinnedBackground, [FCustomBackground, true], [FCustomStyle, true]); // Required for LNY2020 to check whether the profile has a (custom) background - FCustomStyle.dependencies = [FCustomBackground]; - FCustomStyle.weakDependency = true; + FeatureManager.dependency(FCustomStyle, [FCustomBackground, true]) } profileDataPromise() { diff --git a/src/js/Content/Features/Store/App/CApp.js b/src/js/Content/Features/Store/App/CApp.js index 5fbda4c3f..7b4b6199e 100644 --- a/src/js/Content/Features/Store/App/CApp.js +++ b/src/js/Content/Features/Store/App/CApp.js @@ -1,5 +1,5 @@ import {GameId} from "../../../../modulesCore"; -import {Background, ContextType} from "../../../modulesContent"; +import {Background, ContextType, FeatureManager} from "../../../modulesContent"; import FMediaExpander from "../../Common/FMediaExpander"; import {CStoreBase} from "../Common/CStoreBase"; import FCustomizer from "../Common/FCustomizer"; @@ -137,12 +137,10 @@ export class CApp extends CStoreBase { this.data = this.storePageDataPromise().catch(err => { console.error(err); }); // FPackBreakdown skips purchase options with a package info button to avoid false positives - FPackageInfoButton.dependencies = [FPackBreakdown]; - FPackageInfoButton.weakDependency = true; + FeatureManager.dependency(FPackageInfoButton, [FPackBreakdown, true]); // HDPlayer needs to wait for mp4 sources to be set - FHDPlayer.dependencies = [FForceMP4]; - FHDPlayer.weakDependency = true; + FeatureManager.dependency(FPackageInfoButton, [FPackBreakdown, true]); } storePageDataPromise() { diff --git a/src/js/Content/Features/Store/Wishlist/CWishlist.js b/src/js/Content/Features/Store/Wishlist/CWishlist.js index b7549a888..a2c140288 100644 --- a/src/js/Content/Features/Store/Wishlist/CWishlist.js +++ b/src/js/Content/Features/Store/Wishlist/CWishlist.js @@ -1,5 +1,5 @@ import {HTMLParser, TimeUtils} from "../../../../modulesCore"; -import {ContextType, User} from "../../../modulesContent"; +import {ContextType, FeatureManager, User} from "../../../modulesContent"; import {CStoreBase} from "../Common/CStoreBase"; import FAlternativeLinuxIcon from "../Common/FAlternativeLinuxIcon"; import FWishlistHighlights from "./FWishlistHighlights"; @@ -47,7 +47,7 @@ export class CWishlist extends CStoreBase { } // Maintain the order of the buttons - FEmptyWishlist.dependencies = [FExportWishlist]; + FeatureManager.dependency(FEmptyWishlist, [FExportWishlist, false]); } async applyFeatures() { diff --git a/src/js/Content/Modules/Feature/Feature.ts b/src/js/Content/Modules/Feature/Feature.ts index 42f360ce1..46df6118e 100644 --- a/src/js/Content/Modules/Feature/Feature.ts +++ b/src/js/Content/Modules/Feature/Feature.ts @@ -4,11 +4,11 @@ class Feature> { protected context: C, ) {} - public checkPrerequisites(): boolean { + public checkPrerequisites(): boolean|Promise { return true; } - public apply(): void { + public apply(): void|Promise { throw new Error("Stub"); } diff --git a/src/js/Content/Modules/Feature/FeatureManager.js b/src/js/Content/Modules/Feature/FeatureManager.js deleted file mode 100644 index 3eedeea6c..000000000 --- a/src/js/Content/Modules/Feature/FeatureManager.js +++ /dev/null @@ -1,126 +0,0 @@ -import {Errors} from "../../../Core/Errors/Errors"; - -// Polyfill from https://gist.github.com/davidbarral/d0d4da70fa9e6f615595d01f54276e0b#file-promises-js -if (!Promise.allSettled) { - Promise.allSettled = promises => Promise.all( - promises.map(promise => promise - .then(value => ({ - "status": "fulfilled", - value, - })) - .catch(reason => ({ - "status": "rejected", - reason, - }))) - ); -} - -class FeatureManager { - static async apply(features) { - - this._promisesMap = new Map(); - - this._stats = { - "completed": 0, - "failed": 0, - "dependency": 0, - }; - - while (features.length > 0) { - - const feature = features.pop(); - const promise = this._generateFeatureChain(feature); - - if (promise === null) { - features.unshift(feature); - } else { - this._promisesMap.set(feature.constructor, promise); - } - } - - await Promise.allSettled(Array.from(this._promisesMap.values())); - - console.log( - "Feature loading complete, %i successfully loaded, %i failed to load, %i didn't load due to dependency errors", - this._stats.completed, - this._stats.failed, - this._stats.dependency - ); - } - - static _generateFeatureChain(feature) { - - let ready = true; - let promise = Promise.resolve(true); - - if (Array.isArray(feature.constructor.dependencies)) { - - // Ensure that all dependencies have generated a dependency chain for themselves - for (const dep of feature.constructor.dependencies) { - if (this._promisesMap.has(dep)) { continue; } - - ready = false; - break; - } - - if (ready) { - - // Promise that waits for all dependencies to finish executing - promise = Promise.all( - Array.from(this._promisesMap.entries()) - .filter(([ftr]) => feature.constructor.dependencies.includes(ftr)) - .map(([, promise]) => promise) - ); - } - } - - if (!ready) { return null; } - - return promise - .then(previousCheck => { // Check if the dependencies have all their prerequisites fulfilled - let prev = true; - - if (!feature.constructor.weakDependency) { - if (Array.isArray(previousCheck)) { - prev = previousCheck.every(res => res); - } else { - prev = previousCheck; - } - } - - return prev && feature.checkPrerequisites(); - }) - .then(async fulfilled => { // If the feature's prerequisites are fulfilled, apply it - if (fulfilled) { - await feature.apply(); - ++this._stats.completed; - } - return fulfilled; - }) - .catch(err => { - - const featureName = feature.constructor.name; - - if (err instanceof Errors.FeatureDependencyError) { - console.warn( - "Not applying feature %s due to an error in the dependency chain (namely %s)", - featureName, - err.featureName - ); - ++this._stats.dependency; - throw err; - } - - console.group(featureName); - console.error("Error while applying feature %s", featureName); - console.error(err); - console.groupEnd(); - - ++this._stats.failed; - throw new Errors.FeatureDependencyError("Failed to apply", featureName); - }); - - } -} - -export {FeatureManager}; diff --git a/src/js/Content/Modules/Feature/FeatureManager.ts b/src/js/Content/Modules/Feature/FeatureManager.ts new file mode 100644 index 000000000..cf96789c1 --- /dev/null +++ b/src/js/Content/Modules/Feature/FeatureManager.ts @@ -0,0 +1,98 @@ +import type {Feature} from "./Feature"; +import {Errors} from "../../../Core/Errors/Errors"; + +class FeatureManager { + + private static featureMap: Map; + private static promiseMap: Map>; + private static dependencies: Map> = new Map(); + + private static stats = { + "completed": 0, + "failed": 0, + "dependency": 0, + }; + + static dependency(dependent: Function, ...dependencies: Array<[Function, boolean]>) { + this.dependencies.set(dependent, new Map(dependencies)); + } + + static async apply(features: Feature[]) { + this.promiseMap = new Map(); + this.featureMap = new Map(features.map(feature => [feature.constructor, feature])); + + let promises = features.map(feature => this.getFeaturePromise(feature)); + await Promise.allSettled(promises); + + console.log( + "Feature loading complete, %i successfully loaded, %i failed to load, %i didn't load due to dependency errors", + this.stats.completed, + this.stats.failed, + this.stats.dependency + ); + } + + private static getFeaturePromise(feature: Feature): Promise { + const func = feature.constructor; + + let promise = this.promiseMap.get(func); + if (!promise) { + promise = this.applyInternal(feature); + this.promiseMap.set(func, promise); + + } + return promise; + } + + private static async applyInternal(feature: Feature): Promise { + const func = feature.constructor; + + const dependencies = this.dependencies.get(func); + if (dependencies) { + try { + let promises = []; + for (let [dependencyFunc, weak] of dependencies) { + promises.push((async () => { + const dependency = this.featureMap.get(dependencyFunc); + if (!dependency) { + throw new Errors.FeatureDependencyError("Dependency feature not found", dependencyFunc.name); + } + let pass = await this.getFeaturePromise(dependency); + return weak || pass; + })()); + } + + if (!(await Promise.all(promises)).every(res => res)) { + return false; + } + } catch(e) { + console.warn( + "Not applying feature %s due to an error in the dependency chain", + func.name + ); + ++this.stats.dependency; + } + } + + let pass = Boolean(await feature.checkPrerequisites()); + if (pass) { + try { + await feature.apply(); + ++this.stats.completed; + } catch(e) { + const featureName = func.name; + + console.group(featureName); + console.error("Error while applying feature %s", featureName); + console.error(e); + console.groupEnd(); + + ++this.stats.failed; + throw new Errors.FeatureDependencyError("Failed to apply", featureName); + } + } + return pass; + } +} + +export {FeatureManager};