feat: versionedLocalStorage

This commit is contained in:
imperosol
2026-05-19 12:29:18 +02:00
parent ebb62f5132
commit ef804451cf
4 changed files with 91 additions and 25 deletions
+49 -1
View File
@@ -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 // increment this number when a breaking change is made with localStorage
const CURRENT_CACHE_VERSION = 1; const CURRENT_CACHE_VERSION = 1;
/**
* Remove keys that are no longer used from localStorage
*/
export function cacheBuster() { export function cacheBuster() {
const version = Number.parseInt(localStorage.getItem("version") ?? "0", 10); const version = Number.parseInt(localStorage.getItem("version") ?? "0", 10);
if (version === CURRENT_CACHE_VERSION) { if (version === CURRENT_CACHE_VERSION) {
// The cache schema is up-to-date. Nothing to do. // The cache schema is up-to-date. Nothing to do.
return; return;
} }
localStorage.removeItem("basket");
localStorage.removeItem("basket1"); localStorage.removeItem("basket1");
// remove all storage items which key is in the form // remove all storage items which key is in the form
// `userXXXPictures` or `userXXXPicturesNumber` // `userXXXPictures` or `userXXXPicturesNumber`
@@ -22,3 +30,43 @@ export function cacheBuster() {
}); });
localStorage.setItem("version", CURRENT_CACHE_VERSION.toString()); localStorage.setItem("version", CURRENT_CACHE_VERSION.toString());
} }
interface VersionedStorageItem<T> {
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<T>(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<T>(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<T> = 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;
},
};
+32 -13
View File
@@ -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. d'annuler une partie du cache.
Ce dernier se trouve dans le fichier `core/static/bundled/core/cache.ts`. Ce dernier se trouve dans le fichier `core/static/bundled/core/cache.ts`.
Vous devrez modifier ce cache chaque fois que vous effectuerez Vous devrez modifier ce fichier chaque fois qu'un élément du localStorage
un changement de schéma, c'est-à-dire dans un des cas suivants : cessera d'être utilisé.
Les modifications à apporter sont les suivantes :
- 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 :
- incrémenter la version du cache - incrémenter la version du cache
- ajouter une ligne permettant de retirer votre clef 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()); 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("<key>", { version: 1 });
if (obj === null) {
obj = fetchMyObject();
versionedLocalStorage.setItem("<key>", 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.
@@ -1,4 +1,4 @@
export {}; import { versionedLocalStorage } from "#core:core/cache";
interface BasketItem { interface BasketItem {
priceId: number; priceId: number;
@@ -8,6 +8,7 @@ interface BasketItem {
} }
const BASKET_CACHE_KEY = "basket"; const BASKET_CACHE_KEY = "basket";
const BASKET_CACHE_VERSION = 1;
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("basket", (lastPurchaseTime?: number) => ({ Alpine.data("basket", (lastPurchaseTime?: number) => ({
@@ -33,18 +34,16 @@ document.addEventListener("alpine:init", () => {
}, },
loadBasket(): BasketItem[] { loadBasket(): BasketItem[] {
if (localStorage.getItem(BASKET_CACHE_KEY) === null) { const cached = versionedLocalStorage.getItem<BasketItem[]>(BASKET_CACHE_KEY, {
return []; version: BASKET_CACHE_VERSION,
} });
try { return cached ?? [];
return JSON.parse(localStorage.getItem(BASKET_CACHE_KEY));
} catch (_err) {
return [];
}
}, },
saveBasket() { 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()); localStorage.setItem("basketTimestamp", Date.now().toString());
}, },
@@ -47,7 +47,7 @@ document.addEventListener("alpine:init", () => {
// cache only the pictures of the last 4 visited profiles // cache only the pictures of the last 4 visited profiles
sessionStorage.setItem(storageKey, JSON.stringify(cacheContent.slice(-4))); sessionStorage.setItem(storageKey, JSON.stringify(cacheContent.slice(-4)));
} catch { } 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. // To be as safe as possible, delete the cached pictures.
// A cache hit is not worth the page breaking. // A cache hit is not worth the page breaking.
sessionStorage.removeItem(storageKey); sessionStorage.removeItem(storageKey);