Show article history in a table via GET /article/{sku}/history.

This commit is contained in:
seb
2026-08-05 09:35:47 +02:00
parent debd246012
commit 60e6858fd5
10 changed files with 297 additions and 96 deletions

View File

@@ -4,16 +4,25 @@ import { ApiClient } from './ApiClient';
/**
* Stale-while-revalidate cache: readers keep the current promise; update()
* swaps only after a successful reload. Failed updates leave the cache intact.
* replace() writes local data without a round-trip.
*/
function createCache(label, load) {
let promise = load().catch((err) => {
console.error(`[storageMgr] ${label} failed`, err);
throw err;
});
/** Settled value, or `undefined` if not loaded yet / last load failed. */
let current;
let promise = load()
.then((data) => {
current = data;
return data;
})
.catch((err) => {
console.error(`[storageMgr] ${label} failed`, err);
throw err;
});
async function update() {
try {
const data = await load();
current = data;
promise = Promise.resolve(data);
return data;
} catch (err) {
@@ -22,11 +31,36 @@ function createCache(label, load) {
}
}
function replace(data) {
current = data;
promise = Promise.resolve(data);
return data;
}
function clear() {
current = undefined;
promise = load()
.then((data) => {
current = data;
return data;
})
.catch((err) => {
console.error(`[storageMgr] ${label} failed`, err);
throw err;
});
}
return {
update,
replace,
clear,
get promise() {
return promise;
},
/** Loaded value, or `undefined` if the initial load has not settled successfully. */
get value() {
return current;
},
};
}
@@ -36,15 +70,33 @@ function createCache(label, load) {
export class StorageManager extends ApiClient {
/** @type {EventSource | null} */
#events = null;
/** @type {ReturnType<typeof createCache> | null} */
#articleIndexCache = null;
constructor(baseUrl = window.location.origin) {
super(baseUrl);
this.#initCaches();
}
/** Patch local articleIndex name if already loaded; no-op otherwise. */
setArticleIndexName(sku, name) {
const articles = this.#articleIndexCache.value;
if (!articles) return;
this.#articleIndexCache.replace(
articles.map((a) => (String(a.sku) === String(sku) ? { ...a, name } : a)),
);
}
/** Drop local articleIndex if loaded (forces reload on next read). */
clearArticleIndex() {
if (!this.#articleIndexCache.value) return;
this.#articleIndexCache.clear();
}
#initCaches() {
const authMeCache = createCache('GET /auth/me', () => this.#loadAuthMe());
const articleIndexCache = createCache('GET /articleIndex', () => this.#loadArticleIndex());
this.#articleIndexCache = createCache('GET /articleIndex', () => this.#loadArticleIndex());
const articleIndexCache = this.#articleIndexCache;
const brandCache = createCache('GET /brand', () => this.#loadBrands());
const unitCache = createCache('GET /unit', () => this.#loadUnits());
const companyCache = createCache('GET /company', () => this.#loadCompanies());
@@ -87,12 +139,12 @@ export class StorageManager extends ApiClient {
const es = new EventSource(this.urlFor('/api/events'), { withCredentials: true });
es.addEventListener('category', () => {
void this.updateCategoryTree().catch((err) => {
console.error('[storageMgr] category event refresh failed', err);
console.error('[storageMgr] category SSE refresh failed', err);
});
});
es.addEventListener('articles', () => {
void this.updateArticleIndex().catch((err) => {
console.error('[storageMgr] articles event refresh failed', err);
console.error('[storageMgr] articles SSE refresh failed', err);
});
});
es.onerror = () => {