From e2c17175f5e0c0ea9f0a610ea82405ec75c80898 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 6 May 2026 21:35:15 +0200 Subject: [PATCH 1/8] feat: use a queue in user pictures localstorage --- sas/static/bundled/sas/user/pictures-index.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/sas/static/bundled/sas/user/pictures-index.ts b/sas/static/bundled/sas/user/pictures-index.ts index abbdc2c8..de08f858 100644 --- a/sas/static/bundled/sas/user/pictures-index.ts +++ b/sas/static/bundled/sas/user/pictures-index.ts @@ -22,35 +22,35 @@ document.addEventListener("alpine:init", () => { albums: [] as Album[], async fetchPictures(): Promise { - const localStorageKey = `user${config.userId}Pictures`; - const localStorageInvalidationKey = `user${config.userId}PicturesNumber`; - const lastCachedNumber = localStorage.getItem(localStorageInvalidationKey); + // Check the cache before hitting the API. + const localStorageKey = "userPictures"; + const cacheContent: { userId: number; pictures: PictureSchema[] }[] = JSON.parse( + localStorage.getItem(localStorageKey) || "[]", + ); + const userPictures = cacheContent.find((obj) => obj.userId === config.userId); if ( - lastCachedNumber !== null && - Number.parseInt(lastCachedNumber, 10) === config.nbPictures + userPictures !== undefined && + userPictures.pictures.length === config.nbPictures ) { - return JSON.parse(localStorage.getItem(localStorageKey)); + // The cached value is considered valid + // if it contains the right amount of pictures. + // This amount is known because it is given in the template. + return userPictures.pictures; } const pictures = await paginated(picturesFetchPictures, { // biome-ignore lint/style/useNamingConvention: from python api query: { users_identified: [config.userId] }, } as PicturesFetchPicturesData); + + cacheContent.push({ userId: config.userId, pictures: pictures }); try { - localStorage.setItem(localStorageInvalidationKey, config.nbPictures.toString()); - localStorage.setItem(localStorageKey, JSON.stringify(pictures)); + // cache only the pictures of the last 4 visited profiles + localStorage.setItem(localStorageKey, JSON.stringify(cacheContent.slice(-4))); } catch { - // an exception is raised if the localstorage is entirely filled - // so just delete all cached user pictures. + // an exception is raised if the localstorage is entirely filled. + // To be as safe as possible, delete the cached pictures. // A cache hit is not worth the page breaking. - Object.keys(localStorage) - .filter( - (key) => - key.startsWith("user") && - (key.endsWith("Pictures") || key.endsWith("PicturesNumber")), - ) - .forEach((key) => { - localStorage.removeItem(key); - }); + localStorage.removeItem(localStorageKey); } return pictures; }, From 0aed36c8d9e44a18fc9f3377bace30f48f73274c Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 11 May 2026 11:44:20 +0200 Subject: [PATCH 2/8] refactor: assemble main js files into a single bundle --- com/templates/com/screen_slideshow.jinja | 2 +- core/static/bundled/alpine-index.ts | 21 -------- core/static/bundled/base-bundle-index.ts | 53 +++++++++++++++++++ .../core/{navbar-index.ts => navbar.ts} | 15 +++--- core/static/bundled/country-flags-index.ts | 3 -- core/static/bundled/htmx-index.js | 11 ---- core/templates/core/base.jinja | 7 +-- package-lock.json | 11 ++++ package.json | 1 + 9 files changed, 77 insertions(+), 47 deletions(-) delete mode 100644 core/static/bundled/alpine-index.ts create mode 100644 core/static/bundled/base-bundle-index.ts rename core/static/bundled/core/{navbar-index.ts => navbar.ts} (84%) delete mode 100644 core/static/bundled/country-flags-index.ts delete mode 100644 core/static/bundled/htmx-index.js diff --git a/com/templates/com/screen_slideshow.jinja b/com/templates/com/screen_slideshow.jinja index 4a928cc4..0cd7449c 100644 --- a/com/templates/com/screen_slideshow.jinja +++ b/com/templates/com/screen_slideshow.jinja @@ -4,7 +4,7 @@ {% trans %}Slideshow{% endtrans %} - + { - $notifications: NotificationPlugin; - } -} - -Alpine.plugin([sort, limitedChoices, notifications]); -// biome-ignore lint/style/useNamingConvention: it's how it's named -Object.assign(window, { Alpine }); - -window.addEventListener("DOMContentLoaded", () => { - Alpine.start(); -}); diff --git a/core/static/bundled/base-bundle-index.ts b/core/static/bundled/base-bundle-index.ts new file mode 100644 index 00000000..810378d8 --- /dev/null +++ b/core/static/bundled/base-bundle-index.ts @@ -0,0 +1,53 @@ +import sort from "@alpinejs/sort"; +import Alpine from "alpinejs"; +import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; +import htmx from "htmx.org"; +import { limitedChoices } from "#core:alpine/limited-choices"; +import { expireOldStorage } from "#core:core/cache"; +import { default as navbar } from "#core:core/navbar"; +import { type NotificationPlugin, notificationsPlugin as notifications, } from "#core:utils/notifications"; + +/** + * Alpine + */ +declare module "alpinejs" { + interface Magics { + $notifications: NotificationPlugin; + } +} + +Alpine.plugin([sort, limitedChoices, notifications]); +// biome-ignore lint/style/useNamingConvention: it's how it's named +Object.assign(window, { Alpine }); + +window.addEventListener("DOMContentLoaded", () => { + Alpine.start(); +}); + +/** + * Polyfill for country flags (used for language choice) + */ +polyfillCountryFlagEmojis(); + +/** + * HTMX + */ +document.body.addEventListener("htmx:beforeRequest", (event: CustomEvent) => { + event.detail.target.ariaBusy = true; +}); + +document.body.addEventListener("htmx:beforeSwap", (event: CustomEvent) => { + event.detail.target.ariaBusy = null; +}); + +Object.assign(window, { htmx }); + +/** + * navbar + */ +navbar(); + +/** + * Script that clears the cache when the cache version changes + */ +expireOldStorage(); diff --git a/core/static/bundled/core/navbar-index.ts b/core/static/bundled/core/navbar.ts similarity index 84% rename from core/static/bundled/core/navbar-index.ts rename to core/static/bundled/core/navbar.ts index 18f7e3e3..9abc3aec 100644 --- a/core/static/bundled/core/navbar-index.ts +++ b/core/static/bundled/core/navbar.ts @@ -1,12 +1,10 @@ -import { exportToHtml } from "#core:utils/globals.ts"; - -exportToHtml("showMenu", () => { +function showMenu() { const navbar = document.getElementById("navbar-content"); const current = navbar.getAttribute("mobile-display"); navbar.setAttribute("mobile-display", current === "hidden" ? "revealed" : "hidden"); -}); +} -document.addEventListener("alpine:init", () => { +function navbarInit() { const menuItems = document.querySelectorAll(".navbar details[name='navbar'].menu"); const isDesktop = () => { return window.innerWidth >= 500; @@ -33,4 +31,9 @@ document.addEventListener("alpine:init", () => { } }); } -}); +} + +export default () => { + Object.assign(document, { showMenu }); + document.addEventListener("alpine:init", navbarInit); +}; diff --git a/core/static/bundled/country-flags-index.ts b/core/static/bundled/country-flags-index.ts deleted file mode 100644 index 1dc005c3..00000000 --- a/core/static/bundled/country-flags-index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; - -polyfillCountryFlagEmojis(); diff --git a/core/static/bundled/htmx-index.js b/core/static/bundled/htmx-index.js deleted file mode 100644 index 5880668d..00000000 --- a/core/static/bundled/htmx-index.js +++ /dev/null @@ -1,11 +0,0 @@ -import htmx from "htmx.org"; - -document.body.addEventListener("htmx:beforeRequest", (event) => { - event.detail.target.ariaBusy = true; -}); - -document.body.addEventListener("htmx:beforeSwap", (event) => { - event.detail.target.ariaBusy = null; -}); - -Object.assign(window, { htmx }); diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index 577546be..0d8a06fe 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -35,12 +35,9 @@ - + - - - - + {% block additional_css %}{% endblock %} {% block additional_js %}{% endblock %} diff --git a/package-lock.json b/package-lock.json index 8dd3583c..8778d436 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@biomejs/biome": "^2.4.15", "@hey-api/openapi-ts": "^0.94.5", "@types/alpinejs": "^3.13.11", + "@types/alpinejs__sort": "^3.13.0", "@types/cytoscape-cxtmenu": "^3.4.5", "@types/cytoscape-klay": "^3.1.5", "@types/js-cookie": "^3.0.6", @@ -2499,6 +2500,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/alpinejs__sort": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@types/alpinejs__sort/-/alpinejs__sort-3.13.0.tgz", + "integrity": "sha512-iR9vEy6e3yXbYAK45/hpulzlt8SSKTsvYUl/t5nuWjtbJPoGxzxUUqOm3egp83Gqtf//TyJnDCI4OTebAKDRAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/alpinejs": "*" + } + }, "node_modules/@types/codemirror": { "version": "5.60.17", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz", diff --git a/package.json b/package.json index 16604f0b..52faf51b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@biomejs/biome": "^2.4.15", "@hey-api/openapi-ts": "^0.94.5", "@types/alpinejs": "^3.13.11", + "@types/alpinejs__sort": "^3.13.0", "@types/cytoscape-cxtmenu": "^3.4.5", "@types/cytoscape-klay": "^3.1.5", "@types/js-cookie": "^3.0.6", From 73f422db234ace9fcb87b824b0b2e604a6e9012f Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 11 May 2026 11:49:45 +0200 Subject: [PATCH 3/8] refactor: move nested-key.d.ts --- core/static/bundled/{utils/types.d.ts => types/nested-key.d.ts} | 0 core/static/bundled/utils/csv.ts | 2 +- counter/static/bundled/counter/product-list-index.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename core/static/bundled/{utils/types.d.ts => types/nested-key.d.ts} (100%) diff --git a/core/static/bundled/utils/types.d.ts b/core/static/bundled/types/nested-key.d.ts similarity index 100% rename from core/static/bundled/utils/types.d.ts rename to core/static/bundled/types/nested-key.d.ts diff --git a/core/static/bundled/utils/csv.ts b/core/static/bundled/utils/csv.ts index d7356bfc..3172c525 100644 --- a/core/static/bundled/utils/csv.ts +++ b/core/static/bundled/utils/csv.ts @@ -1,4 +1,4 @@ -import type { NestedKeyOf } from "#core:utils/types.ts"; +import type { NestedKeyOf } from "#core:types/nested-key"; interface StringifyOptions { /** The columns to include in the resulting CSV. */ diff --git a/counter/static/bundled/counter/product-list-index.ts b/counter/static/bundled/counter/product-list-index.ts index 30d26b9a..1486c766 100644 --- a/counter/static/bundled/counter/product-list-index.ts +++ b/counter/static/bundled/counter/product-list-index.ts @@ -1,9 +1,9 @@ import { showSaveFilePicker } from "native-file-system-adapter"; import type TomSelect from "tom-select"; +import type { NestedKeyOf } from "#core:types/nested-key"; import { paginated } from "#core:utils/api"; import { csv } from "#core:utils/csv"; import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history"; -import type { NestedKeyOf } from "#core:utils/types"; import { type ProductSchema, type ProductSearchProductsDetailedData, From 4b369b73a775447303c5545809f1dab173c3e82d Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 11 May 2026 13:16:37 +0200 Subject: [PATCH 4/8] feat: automatic localstorage cleaning --- core/static/bundled/base-bundle-index.ts | 9 ++- core/static/bundled/core/localstorage.ts | 24 ++++++++ docs/tutorial/front/localstorage.md | 56 +++++++++++++++++++ .../static/bundled/eboutic/eboutic-index.ts | 3 +- mkdocs.yml | 2 + 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 core/static/bundled/core/localstorage.ts create mode 100644 docs/tutorial/front/localstorage.md diff --git a/core/static/bundled/base-bundle-index.ts b/core/static/bundled/base-bundle-index.ts index 810378d8..b3db11ff 100644 --- a/core/static/bundled/base-bundle-index.ts +++ b/core/static/bundled/base-bundle-index.ts @@ -3,9 +3,12 @@ import Alpine from "alpinejs"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import htmx from "htmx.org"; import { limitedChoices } from "#core:alpine/limited-choices"; -import { expireOldStorage } from "#core:core/cache"; +import { cacheBuster } from "#core:core/localstorage"; import { default as navbar } from "#core:core/navbar"; -import { type NotificationPlugin, notificationsPlugin as notifications, } from "#core:utils/notifications"; +import { + type NotificationPlugin, + notificationsPlugin as notifications, +} from "#core:utils/notifications"; /** * Alpine @@ -50,4 +53,4 @@ navbar(); /** * Script that clears the cache when the cache version changes */ -expireOldStorage(); +cacheBuster(); diff --git a/core/static/bundled/core/localstorage.ts b/core/static/bundled/core/localstorage.ts new file mode 100644 index 00000000..5d44c986 --- /dev/null +++ b/core/static/bundled/core/localstorage.ts @@ -0,0 +1,24 @@ +// increment this number when a breaking change is made with localStorage +const CURRENT_CACHE_VERSION = 1; + +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` + Object.keys(localStorage) + .filter( + (key) => + key.startsWith("user") && + (key.endsWith("Pictures") || key.endsWith("PicturesNumber")), + ) + .forEach((key) => { + localStorage.removeItem(key); + }); + localStorage.setItem("version", CURRENT_CACHE_VERSION.toString()); +} diff --git a/docs/tutorial/front/localstorage.md b/docs/tutorial/front/localstorage.md new file mode 100644 index 00000000..bdcaf17c --- /dev/null +++ b/docs/tutorial/front/localstorage.md @@ -0,0 +1,56 @@ +[Documentation du localStorage (mozilla)](https://developer.mozilla.org/fr/docs/Web/API/Window/localStorage) + +## Utilité et limitations + +Le `localStorage` est un cache géré directement par le navigateur. +Il permet de stocker des données directement chez le client. +Il s'agit donc d'un outil extrêmement puissant, qui permet d'éviter +beaucoup de requêtes au serveur, améliorant ainsi les temps de chargement. + +Cependant, il y a deux limitations majeures à prendre en compte : + +- le `localStorage` est entièrement géré par le client, + une fois le déploiement effectué, vous ne pouvez plus y toucher ; + vous devez donc être sûr de vous avant d'apporter des modifications + reposant sur le `localStorage`. +- la quantité de données stockable est limitée à 10Mo ; + une fois ce quota rempli, le navigateur lèvera une `QuotaExceededError`. + +## Invalidation du `localStorage` + +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 : + +- incrémenter la version du cache +- ajouter une ligne permettant de retirer votre clef du cache + +```ts hl_lines="2 11" +// increment this number when a breaking change is made with localStorage +const CURRENT_CACHE_VERSION = 2; // <-- changez cette ligne + +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(""); // <-- et rajoutez cette ligne + localStorage.setItem("version", CURRENT_CACHE_VERSION.toString()); +} +``` diff --git a/eboutic/static/bundled/eboutic/eboutic-index.ts b/eboutic/static/bundled/eboutic/eboutic-index.ts index 43080a58..5421d428 100644 --- a/eboutic/static/bundled/eboutic/eboutic-index.ts +++ b/eboutic/static/bundled/eboutic/eboutic-index.ts @@ -7,8 +7,7 @@ interface BasketItem { unitPrice: number; } -// increment the key number if the data schema of the cached basket changes -const BASKET_CACHE_KEY = "basket1"; +const BASKET_CACHE_KEY = "basket"; document.addEventListener("alpine:init", () => { Alpine.data("basket", (lastPurchaseTime?: number) => ({ diff --git a/mkdocs.yml b/mkdocs.yml index f537fa76..cd6df0de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,8 @@ nav: - Gestion des permissions: tutorial/perms.md - Gestion des groupes: tutorial/groups.md - Les fragments: tutorial/fragments.md + - Frontend: + - localStorage: tutorial/front/localstorage.md - API: - Développement: tutorial/api/dev.md - Connexion à l'API: tutorial/api/connect.md From 2228a3f9618060fd61705dc2e05154739a4541c2 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 17 May 2026 14:30:17 +0200 Subject: [PATCH 5/8] use sessionStorage to cache user pictures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le sessionStorage est automatiquement vidé à la fermeture de la page, ce qui, dans le cas des photos, est un peu plus fiable et correspond un peu mieux à nos besoins. --- sas/static/bundled/sas/user/pictures-index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sas/static/bundled/sas/user/pictures-index.ts b/sas/static/bundled/sas/user/pictures-index.ts index de08f858..448a92cd 100644 --- a/sas/static/bundled/sas/user/pictures-index.ts +++ b/sas/static/bundled/sas/user/pictures-index.ts @@ -23,9 +23,9 @@ document.addEventListener("alpine:init", () => { async fetchPictures(): Promise { // Check the cache before hitting the API. - const localStorageKey = "userPictures"; + const storageKey = "userPictures"; const cacheContent: { userId: number; pictures: PictureSchema[] }[] = JSON.parse( - localStorage.getItem(localStorageKey) || "[]", + sessionStorage.getItem(storageKey) || "[]", ); const userPictures = cacheContent.find((obj) => obj.userId === config.userId); if ( @@ -45,12 +45,12 @@ document.addEventListener("alpine:init", () => { cacheContent.push({ userId: config.userId, pictures: pictures }); try { // cache only the pictures of the last 4 visited profiles - localStorage.setItem(localStorageKey, JSON.stringify(cacheContent.slice(-4))); + sessionStorage.setItem(storageKey, JSON.stringify(cacheContent.slice(-4))); } catch { // an exception is raised if the localstorage is entirely filled. // To be as safe as possible, delete the cached pictures. // A cache hit is not worth the page breaking. - localStorage.removeItem(localStorageKey); + sessionStorage.removeItem(storageKey); } return pictures; }, From 6cec2e74d069092a0c9b6b4ac60feb80eee73255 Mon Sep 17 00:00:00 2001 From: imperosol Date: Tue, 19 May 2026 12:29:18 +0200 Subject: [PATCH 6/8] feat: `versionedLocalStorage` --- core/static/bundled/base-bundle-index.ts | 4 +- core/static/bundled/core/localstorage.ts | 60 +++++++++++++++++-- docs/tutorial/front/localstorage.md | 45 ++++++++++---- .../static/bundled/eboutic/eboutic-index.ts | 19 +++--- sas/static/bundled/sas/user/pictures-index.ts | 2 +- 5 files changed, 98 insertions(+), 32 deletions(-) diff --git a/core/static/bundled/base-bundle-index.ts b/core/static/bundled/base-bundle-index.ts index b3db11ff..1252b301 100644 --- a/core/static/bundled/base-bundle-index.ts +++ b/core/static/bundled/base-bundle-index.ts @@ -3,7 +3,7 @@ import Alpine from "alpinejs"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import htmx from "htmx.org"; import { limitedChoices } from "#core:alpine/limited-choices"; -import { cacheBuster } from "#core:core/localstorage"; +import { expireOldStorage } from "#core:core/localstorage"; import { default as navbar } from "#core:core/navbar"; import { type NotificationPlugin, @@ -53,4 +53,4 @@ navbar(); /** * Script that clears the cache when the cache version changes */ -cacheBuster(); +expireOldStorage(); diff --git a/core/static/bundled/core/localstorage.ts b/core/static/bundled/core/localstorage.ts index 5d44c986..521b7ac2 100644 --- a/core/static/bundled/core/localstorage.ts +++ b/core/static/bundled/core/localstorage.ts @@ -1,13 +1,21 @@ -// increment this number when a breaking change is made with localStorage -const CURRENT_CACHE_VERSION = 1; +/** + * 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/ + */ -export function cacheBuster() { +// increment this number when a breaking change is made with localStorage +const CURRENT_LOCALSTORAGE_VERSION = 1; + +/** + * Remove keys that are no longer used from localStorage + */ +export function expireOldStorage() { const version = Number.parseInt(localStorage.getItem("version") ?? "0", 10); - if (version === CURRENT_CACHE_VERSION) { + if (version === CURRENT_LOCALSTORAGE_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` @@ -20,5 +28,45 @@ export function cacheBuster() { .forEach((key) => { localStorage.removeItem(key); }); - localStorage.setItem("version", CURRENT_CACHE_VERSION.toString()); + localStorage.setItem("version", CURRENT_LOCALSTORAGE_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); From ce0ddcd184156093cd3ae7d1d0dc9bbe17e638fd Mon Sep 17 00:00:00 2001 From: imperosol Date: Thu, 21 May 2026 22:41:51 +0200 Subject: [PATCH 7/8] replace `exportToHtml` by `Object.assign(window, { obj })` --- com/static/bundled/com/moderation-alert-index.ts | 4 ++-- core/static/bundled/sentry-popup-index.ts | 6 +++--- core/static/bundled/utils/globals.ts | 14 -------------- docs/tutorial/front/localstorage.md | 2 +- eboutic/static/bundled/eboutic/eboutic-index.ts | 2 +- 5 files changed, 7 insertions(+), 21 deletions(-) diff --git a/com/static/bundled/com/moderation-alert-index.ts b/com/static/bundled/com/moderation-alert-index.ts index 70dc871c..029ff87e 100644 --- a/com/static/bundled/com/moderation-alert-index.ts +++ b/com/static/bundled/com/moderation-alert-index.ts @@ -1,4 +1,3 @@ -import { exportToHtml } from "#core:utils/globals.ts"; import { newsDeleteNews, newsFetchNewsDates, newsPublishNews } from "#openapi"; // This will be used in jinja templates, @@ -13,7 +12,8 @@ const AlertState = { // biome-ignore lint/style/useNamingConvention: this feels more like an enum DISPLAYED: 4, // When published at page generation }; -exportToHtml("AlertState", AlertState); +// biome-ignore lint/style/useNamingConvention: it's an enum, PascalCase is better +Object.assign(window, { AlertState }); document.addEventListener("alpine:init", () => { Alpine.data("moderationAlert", (newsId: number) => ({ diff --git a/core/static/bundled/sentry-popup-index.ts b/core/static/bundled/sentry-popup-index.ts index ea16c092..d8b84197 100644 --- a/core/static/bundled/sentry-popup-index.ts +++ b/core/static/bundled/sentry-popup-index.ts @@ -1,6 +1,5 @@ // biome-ignore lint/performance/noNamespaceImport: this is the recommended way from the documentation import * as Sentry from "@sentry/browser"; -import { exportToHtml } from "#core:utils/globals.ts"; interface LoggedUser { name: string; @@ -13,7 +12,7 @@ interface SentryOptions { user?: LoggedUser; } -exportToHtml("loadSentryPopup", (options: SentryOptions) => { +const loadSentryPopup = (options: SentryOptions) => { Sentry.init({ dsn: options.dsn, }); @@ -21,4 +20,5 @@ exportToHtml("loadSentryPopup", (options: SentryOptions) => { eventId: options.eventId, ...(options.user ?? {}), }); -}); +}; +Object.assign(window, { loadSentryPopup }); diff --git a/core/static/bundled/utils/globals.ts b/core/static/bundled/utils/globals.ts index b4f9a457..71c15cb0 100644 --- a/core/static/bundled/utils/globals.ts +++ b/core/static/bundled/utils/globals.ts @@ -5,17 +5,3 @@ declare global { const gettext: (text: string) => string; const interpolate: (fmt: string, args: string[] | T, isNamed?: boolean) => string; } - -/** - * Helper function to export typescript functions to regular html and jinja files - * Without it, you either have to use the any keyword and suppress warnings or do a - * very painful type conversion workaround which is only here to please the linter - * - * This is only useful if you're using typescript, this is equivalent to doing - * window.yourFunction = yourFunction - **/ -// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions -export function exportToHtml(name: string, func: any) { - // biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions - (window as any)[name] = func; -} diff --git a/docs/tutorial/front/localstorage.md b/docs/tutorial/front/localstorage.md index 43275bc9..fac974ab 100644 --- a/docs/tutorial/front/localstorage.md +++ b/docs/tutorial/front/localstorage.md @@ -51,7 +51,7 @@ 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"; +import { versionedLocalStorage } from "#core:core/localstorage"; const foo = () => { let obj = versionedLocalStorage.getItem("", { version: 1 }); diff --git a/eboutic/static/bundled/eboutic/eboutic-index.ts b/eboutic/static/bundled/eboutic/eboutic-index.ts index 3d0f81d8..18573158 100644 --- a/eboutic/static/bundled/eboutic/eboutic-index.ts +++ b/eboutic/static/bundled/eboutic/eboutic-index.ts @@ -1,4 +1,4 @@ -import { versionedLocalStorage } from "#core:core/cache"; +import { versionedLocalStorage } from "#core:core/localstorage"; interface BasketItem { priceId: number; From 5238e2e2d636e136d77966dbf4eaf78df051a584 Mon Sep 17 00:00:00 2001 From: imperosol Date: Thu, 21 May 2026 22:49:06 +0200 Subject: [PATCH 8/8] doc-comment explaining `base-bundle-index.ts` --- core/static/bundled/base-bundle-index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/static/bundled/base-bundle-index.ts b/core/static/bundled/base-bundle-index.ts index 1252b301..4dc085ad 100644 --- a/core/static/bundled/base-bundle-index.ts +++ b/core/static/bundled/base-bundle-index.ts @@ -1,3 +1,11 @@ +/** + * File containing main functions and library re-exports + * that should be accessible throughout the whole website. + * + * The idea is to group all that shared code into a single bundle, + * for more efficient tree-shaking and gzip compression. + */ + import sort from "@alpinejs/sort"; import Alpine from "alpinejs"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";