From ce6dced3ad7adef826b74a5692c5e53fcf8b52cd Mon Sep 17 00:00:00 2001 From: Sarahtonin Date: Mon, 31 Aug 2026 20:01:56 +0200 Subject: [PATCH] Added catalogue version to make webpages not need to repeatedly fetch info from API. --- Pages/Garden.php | 1 + Pages/Profile.php | 1 + Pages/Shop.php | 1 + Scripts/Garden/Content.js | 361 ++++++++++++++++++++++++++++++++++++ Scripts/Garden/Mutations.js | 61 +----- Scripts/Garden/Plants.js | 34 +--- 6 files changed, 374 insertions(+), 85 deletions(-) create mode 100644 Scripts/Garden/Content.js diff --git a/Pages/Garden.php b/Pages/Garden.php index 46551fb..fbf987f 100644 --- a/Pages/Garden.php +++ b/Pages/Garden.php @@ -135,6 +135,7 @@ $PageSection = "Garden"; + diff --git a/Pages/Profile.php b/Pages/Profile.php index 6a2cd61..5754a59 100644 --- a/Pages/Profile.php +++ b/Pages/Profile.php @@ -293,6 +293,7 @@ $PageTitle = "Profile"; + diff --git a/Pages/Shop.php b/Pages/Shop.php index 16eeda2..ae48008 100644 --- a/Pages/Shop.php +++ b/Pages/Shop.php @@ -61,6 +61,7 @@ $PageSection = "Shop"; + diff --git a/Scripts/Garden/Content.js b/Scripts/Garden/Content.js new file mode 100644 index 0000000..7ab3c2a --- /dev/null +++ b/Scripts/Garden/Content.js @@ -0,0 +1,361 @@ +const GameContentCacheName = + "SarahtoninGardenContent"; + +const GameContentCacheSchemaVersion = 1; + +/* + * Once content has been checked, page loads for the next + * five minutes can use the local cache without any API call. + * After that, only the tiny ContentVersion endpoint is needed + * unless the catalogue has actually changed. + */ +const GameContentVersionCheckInterval = + 5 * 60 * 1000; + + +let GameContentLoadPromise = null; + + +async function LoadGameContent() { + if (GameContentLoadPromise === null) { + GameContentLoadPromise = + LoadGameContentInternal(); + } + + + try { + return await GameContentLoadPromise; + } catch (Error) { + GameContentLoadPromise = null; + + throw Error; + } +} + + +async function LoadGameContentInternal() { + const Cache = + ReadGameContentCache(); + + const Now = Date.now(); + + + if ( + Cache !== null && + Now - Cache.CheckedAt < + GameContentVersionCheckInterval + ) { + ApplyGameContent( + Cache.Plants, + Cache.Mutations + ); + + return GetLoadedGameContent(); + } + + + try { + const ContentVersion = + await FetchGameContentVersion(); + + + if ( + Cache !== null && + Cache.ContentVersion === + ContentVersion + ) { + Cache.CheckedAt = Now; + + WriteGameContentCache( + Cache + ); + + ApplyGameContent( + Cache.Plants, + Cache.Mutations + ); + + return GetLoadedGameContent(); + } + + + return await RefreshGameContent( + ContentVersion + ); + } catch (VersionError) { + console.warn( + "Couldn't check Garden content version. " + + "Refreshing the catalogues directly.", + VersionError + ); + + + try { + return await RefreshGameContent( + null + ); + } catch (RefreshError) { + if (Cache !== null) { + console.warn( + "Couldn't refresh Garden content. " + + "Using the last cached catalogue.", + RefreshError + ); + + ApplyGameContent( + Cache.Plants, + Cache.Mutations + ); + + return GetLoadedGameContent(); + } + + throw RefreshError; + } + } +} + + +async function FetchGameContentVersion() { + const Response = + await fetch( + ApiUrl + + "/ContentVersion.php", + { + method: "GET", + + headers: { + "Accept": + "application/json" + }, + + cache: "no-store" + } + ); + + + if (!Response.ok) { + throw new Error( + "Content version API returned HTTP " + + Response.status + ); + } + + + const Result = + await Response.json(); + + + if ( + !Result.Success || + typeof Result.Version !== + "string" || + Result.Version.length === 0 + ) { + throw new Error( + Result.Error ?? + "Content version API returned invalid data." + ); + } + + + return Result.Version; +} + + +async function RefreshGameContent( + ContentVersion +) { + const [ + FreshPlants, + FreshMutations + ] = + await Promise.all([ + FetchPlantsFromApi(), + FetchMutationsFromApi() + ]); + + + const Cache = { + SchemaVersion: + GameContentCacheSchemaVersion, + + ContentVersion: + ContentVersion, + + CheckedAt: + Date.now(), + + Plants: + FreshPlants, + + Mutations: + FreshMutations + }; + + + WriteGameContentCache( + Cache + ); + + ApplyGameContent( + FreshPlants, + FreshMutations + ); + + + return GetLoadedGameContent(); +} + + +function ApplyGameContent( + LoadedPlants, + LoadedMutations +) { + Plants = LoadedPlants; + MutationSets = LoadedMutations; +} + + +function GetLoadedGameContent() { + return { + Plants: Plants, + Mutations: MutationSets + }; +} + + +function ReadGameContentCache() { + let RawCache; + + + try { + RawCache = + localStorage.getItem( + GameContentCacheName + ); + } catch (Error) { + console.warn( + "Couldn't read the Garden content cache.", + Error + ); + + return null; + } + + + if (RawCache === null) { + return null; + } + + + try { + const Cache = + JSON.parse( + RawCache + ); + + + if ( + !IsValidGameContentCache( + Cache + ) + ) { + RemoveGameContentCache(); + + return null; + } + + + return Cache; + } catch (Error) { + RemoveGameContentCache(); + + return null; + } +} + + +function IsValidGameContentCache( + Cache +) { + return ( + Cache !== null && + typeof Cache === "object" && + !Array.isArray(Cache) && + + Cache.SchemaVersion === + GameContentCacheSchemaVersion && + + Number.isFinite( + Cache.CheckedAt + ) && + + Cache.CheckedAt >= 0 && + + ( + Cache.ContentVersion === + null || + typeof Cache.ContentVersion === + "string" + ) && + + IsGameContentObject( + Cache.Plants + ) && + + IsGameContentObject( + Cache.Mutations + ) + ); +} + + +function IsGameContentObject( + Value +) { + return ( + Value !== null && + typeof Value === "object" && + !Array.isArray(Value) + ); +} + + +function WriteGameContentCache( + Cache +) { + try { + localStorage.setItem( + GameContentCacheName, + JSON.stringify( + Cache + ) + ); + } catch (Error) { + console.warn( + "Couldn't write the Garden content cache.", + Error + ); + } +} + + +function RemoveGameContentCache() { + try { + localStorage.removeItem( + GameContentCacheName + ); + } catch (Error) { + console.warn( + "Couldn't clear the Garden content cache.", + Error + ); + } +} + + +function ClearGameContentCache() { + RemoveGameContentCache(); + GameContentLoadPromise = null; +} diff --git a/Scripts/Garden/Mutations.js b/Scripts/Garden/Mutations.js index b405b48..495350b 100644 --- a/Scripts/Garden/Mutations.js +++ b/Scripts/Garden/Mutations.js @@ -1,30 +1,7 @@ let MutationSets = {}; -let MutationsLoadPromise = null; -let GameContentLoadPromise = null; - -async function LoadMutations() { - if (MutationsLoadPromise !== null) { - return MutationsLoadPromise; - } - - - MutationsLoadPromise = - LoadMutationsFromApi(); - - - try { - return await MutationsLoadPromise; - } catch (Error) { - MutationsLoadPromise = null; - - throw Error; - } -} - - -async function LoadMutationsFromApi() { +async function FetchMutationsFromApi() { const Response = await fetch( ApiUrl + "/Mutations.php", @@ -34,7 +11,9 @@ async function LoadMutationsFromApi() { headers: { "Accept": "application/json" - } + }, + + cache: "no-store" } ); @@ -67,35 +46,5 @@ async function LoadMutationsFromApi() { } - MutationSets = - Result.Mutations; - - - return MutationSets; -} - - -async function LoadGameContent() { - if (GameContentLoadPromise === null) { - GameContentLoadPromise = - Promise.all([ - LoadPlants(), - LoadMutations() - ]); - } - - - try { - await GameContentLoadPromise; - } catch (Error) { - GameContentLoadPromise = null; - - throw Error; - } - - - return { - Plants: Plants, - Mutations: MutationSets - }; + return Result.Mutations; } diff --git a/Scripts/Garden/Plants.js b/Scripts/Garden/Plants.js index 57bcc71..4dc108e 100644 --- a/Scripts/Garden/Plants.js +++ b/Scripts/Garden/Plants.js @@ -1,29 +1,7 @@ let Plants = {}; -let PlantsLoadPromise = null; - -async function LoadPlants() { - if (PlantsLoadPromise !== null) { - return PlantsLoadPromise; - } - - - PlantsLoadPromise = - LoadPlantsFromApi(); - - - try { - return await PlantsLoadPromise; - } catch (Error) { - PlantsLoadPromise = null; - - throw Error; - } -} - - -async function LoadPlantsFromApi() { +async function FetchPlantsFromApi() { const Response = await fetch( ApiUrl + "/Plants.php", @@ -33,7 +11,9 @@ async function LoadPlantsFromApi() { headers: { "Accept": "application/json" - } + }, + + cache: "no-store" } ); @@ -66,9 +46,5 @@ async function LoadPlantsFromApi() { } - Plants = - Result.Plants; - - - return Plants; + return Result.Plants; }