From ef804451cf67ba0ce0842e39b6b5a19b61d095c6 Mon Sep 17 00:00:00 2001 From: imperosol Date: Tue, 19 May 2026 12:29:18 +0200 Subject: [PATCH] feat: `versionedLocalStorage` --- core/static/bundled/core/cache.ts | 50 ++++++++++++++++++- docs/tutorial/front/localstorage.md | 45 ++++++++++++----- .../static/bundled/eboutic/eboutic-index.ts | 19 ++++--- sas/static/bundled/sas/user/pictures-index.ts | 2 +- 4 files changed, 91 insertions(+), 25 deletions(-) diff --git a/core/static/bundled/core/cache.ts b/core/static/bundled/core/cache.ts index 5d44c986..57a175f4 100644 --- a/core/static/bundled/core/cache.ts +++ b/core/static/bundled/core/cache.ts @@ -1,13 +1,21 @@ +/** + * For more detailed infos on how to use this file, + * check /docs/tutorial/front/localstorage.md, + * or https://ae-utbm.github.io/sith/tutorial/front/localstorage/ + */ + // increment this number when a breaking change is made with localStorage const CURRENT_CACHE_VERSION = 1; +/** + * Remove keys that are no longer used from localStorage + */ export function cacheBuster() { const version = Number.parseInt(localStorage.getItem("version") ?? "0", 10); if (version === CURRENT_CACHE_VERSION) { // The cache schema is up-to-date. Nothing to do. return; } - localStorage.removeItem("basket"); localStorage.removeItem("basket1"); // remove all storage items which key is in the form // `userXXXPictures` or `userXXXPicturesNumber` @@ -22,3 +30,43 @@ export function cacheBuster() { }); localStorage.setItem("version", CURRENT_CACHE_VERSION.toString()); } + +interface VersionedStorageItem { + version?: number; + val: T | undefined; +} + +export const versionedLocalStorage = { + ...localStorage, + /** + * set this item in localStorage, alongside its version. + * + * Note: this expects an object, not a JSON string, because the parsing + * into JSON needs to be done inside the function. + */ + setItem(key: string, value: T, { version }: { version: number }) { + localStorage.setItem(key, JSON.stringify({ version: version, val: value })); + }, + /** + * Get the item linked with the given key and version from localStorage. + * + * Note: if the given key exists in localStorage but doesn't satisfy + * the given version, it will be cleared from cache. + * + * @return the object if found and with the good version, else null; + */ + getItem(key: string, { version }: { version: number }): T | null { + const stored = localStorage.getItem(key); + if (!stored) { + // this key doesn't exist, return null; + return null; + } + const obj: VersionedStorageItem = JSON.parse(stored); + if (obj.version !== version || obj.val === undefined) { + // The version is wrong, return null and remove this item from cache + localStorage.removeItem(key); + return null; + } + return obj.val; + }, +}; diff --git a/docs/tutorial/front/localstorage.md b/docs/tutorial/front/localstorage.md index bdcaf17c..43275bc9 100644 --- a/docs/tutorial/front/localstorage.md +++ b/docs/tutorial/front/localstorage.md @@ -22,19 +22,9 @@ Pour résoudre le premier de ces deux problèmes, il y a un script permettant d'annuler une partie du cache. Ce dernier se trouve dans le fichier `core/static/bundled/core/cache.ts`. -Vous devrez modifier ce cache chaque fois que vous effectuerez -un changement de schéma, c'est-à-dire dans un des cas suivants : - -- une des clefs du cache n'est plus utilisée -- la clef n'a pas changé, mais la manière dont les données attachées à cette clef - sont formées a été modifiée. - -!!!Note - - Si vous ne faites qu'ajouter des données, sans modifier ni supprimer - celles qui sont là, vous n'avez pas besoin d'invalider le cache. - -Vous devez effectuer deux modifications dans ce fichier : +Vous devrez modifier ce fichier chaque fois qu'un élément du localStorage +cessera d'être utilisé. +Les modifications à apporter sont les suivantes : - incrémenter la version du cache - ajouter une ligne permettant de retirer votre clef du cache @@ -54,3 +44,32 @@ export function cacheBuster() { localStorage.setItem("version", CURRENT_CACHE_VERSION.toString()); } ``` + +## Versionnage d'une clef + +Dans le cas où une paire clef-valeur du localStorage subit un changement +dans son schéma de données, utilisez `versionedLocalStorage` : + +```typescript +import { versionedLocalStorage } from "#core:core/cache"; + +const foo = () => { + let obj = versionedLocalStorage.getItem("", { version: 1 }); + if (obj === null) { + obj = fetchMyObject(); + versionedLocalStorage.setItem("", obj, { version: 1 }) + } + // Do something with obj... +} +``` + +!!!Warning + + Il existe une différence d'usage entre `localStorage` et `versionedLocalStorage` : + les valeurs données à `localStorage` doivent être des strings (généralement + obtenus avec `JSON.stringify`), tandis que `versionedLocalStorage` utilise + directement des objets JS. + + Cette différence résulte du fait que `versionedLocalStorage` doit légèrement + modifier les données pour y inclure la version, et gérer en interne + la conversion en JSON. \ No newline at end of file diff --git a/eboutic/static/bundled/eboutic/eboutic-index.ts b/eboutic/static/bundled/eboutic/eboutic-index.ts index 5421d428..3d0f81d8 100644 --- a/eboutic/static/bundled/eboutic/eboutic-index.ts +++ b/eboutic/static/bundled/eboutic/eboutic-index.ts @@ -1,4 +1,4 @@ -export {}; +import { versionedLocalStorage } from "#core:core/cache"; interface BasketItem { priceId: number; @@ -8,6 +8,7 @@ interface BasketItem { } const BASKET_CACHE_KEY = "basket"; +const BASKET_CACHE_VERSION = 1; document.addEventListener("alpine:init", () => { Alpine.data("basket", (lastPurchaseTime?: number) => ({ @@ -33,18 +34,16 @@ document.addEventListener("alpine:init", () => { }, loadBasket(): BasketItem[] { - if (localStorage.getItem(BASKET_CACHE_KEY) === null) { - return []; - } - try { - return JSON.parse(localStorage.getItem(BASKET_CACHE_KEY)); - } catch (_err) { - return []; - } + const cached = versionedLocalStorage.getItem(BASKET_CACHE_KEY, { + version: BASKET_CACHE_VERSION, + }); + return cached ?? []; }, saveBasket() { - localStorage.setItem(BASKET_CACHE_KEY, JSON.stringify(this.basket)); + versionedLocalStorage.setItem(BASKET_CACHE_KEY, this.basket, { + version: BASKET_CACHE_VERSION, + }); localStorage.setItem("basketTimestamp", Date.now().toString()); }, diff --git a/sas/static/bundled/sas/user/pictures-index.ts b/sas/static/bundled/sas/user/pictures-index.ts index 448a92cd..99d6a7d7 100644 --- a/sas/static/bundled/sas/user/pictures-index.ts +++ b/sas/static/bundled/sas/user/pictures-index.ts @@ -47,7 +47,7 @@ document.addEventListener("alpine:init", () => { // cache only the pictures of the last 4 visited profiles sessionStorage.setItem(storageKey, JSON.stringify(cacheContent.slice(-4))); } catch { - // an exception is raised if the localstorage is entirely filled. + // an exception is raised if the storage is entirely filled. // To be as safe as possible, delete the cached pictures. // A cache hit is not worth the page breaking. sessionStorage.removeItem(storageKey);