This commit is contained in:
seb
2026-08-05 19:11:12 +02:00
parent 9fcee1f851
commit bbec785d7c

View File

@@ -76,12 +76,15 @@ export class StorageManager extends ApiClient {
#articleIndexCache = null; #articleIndexCache = null;
/** @type {Map<string, object>} sku → article detail */ /** @type {Map<string, object>} sku → article detail */
#articlesBySku = new Map(); #articlesBySku = new Map();
/** @type {Map<string, Set<() => void>>} sku → refreshers */ /** @type {Map<string, Set<(article: object) => void>>} sku → article listeners */
#articleWatchers = new Map();
/** @type {Map<string, Set<() => void>>} sku → history refreshers */
#historyWatchers = new Map(); #historyWatchers = new Map();
constructor(baseUrl = window.location.origin) { constructor(baseUrl = window.location.origin) {
super(baseUrl); super(baseUrl);
this.#initCaches(); this.#initCaches();
this.article.watch = (sku, onArticle) => this.watchArticle(sku, onArticle);
} }
/** Settled article detail for sku, or undefined. */ /** Settled article detail for sku, or undefined. */
@@ -93,7 +96,12 @@ export class StorageManager extends ApiClient {
setArticle(article) { setArticle(article) {
requireObject(article, 'article'); requireObject(article, 'article');
requireDefined(article.sku, 'article.sku'); requireDefined(article.sku, 'article.sku');
this.#articlesBySku.set(String(article.sku), article); const key = String(article.sku);
this.#articlesBySku.set(key, article);
const watchers = this.#articleWatchers.get(key);
if (watchers) {
for (const onArticle of watchers) onArticle(article);
}
return article; return article;
} }
@@ -108,6 +116,42 @@ export class StorageManager extends ApiClient {
return this.setArticle(res.article); return this.setArticle(res.article);
} }
/**
* Subscribe to article detail updates (local setArticle + SSE `article-<sku>`).
* @param {string} sku
* @param {(article: object) => void} onArticle
* @returns {{ article: object | null, unwatch: () => void }}
*/
watchArticle(sku, onArticle) {
const key = String(sku);
const needListen = !this.#articleWatchers.has(key) && !this.#historyWatchers.has(key);
let watchers = this.#articleWatchers.get(key);
if (!watchers) {
watchers = new Set();
this.#articleWatchers.set(key, watchers);
}
watchers.add(onArticle);
if (needListen) {
this.#events?.addEventListener(`article-${key}`, this.#onArticleSignal);
}
return {
article: this.getArticle(key) ?? null,
unwatch: () => {
const set = this.#articleWatchers.get(key);
if (!set) return;
set.delete(onArticle);
if (set.size === 0) {
this.#articleWatchers.delete(key);
if (!this.#historyWatchers.has(key)) {
this.#events?.removeEventListener(`article-${key}`, this.#onArticleSignal);
}
}
},
};
}
/** /**
* Parsed category map + root id from settled caches, or null if any piece missing. * Parsed category map + root id from settled caches, or null if any piece missing.
* @returns {{ categories: object, rootCategoryId: unknown } | null} * @returns {{ categories: object, rootCategoryId: unknown } | null}
@@ -153,13 +197,17 @@ export class StorageManager extends ApiClient {
notifyError(err); notifyError(err);
}); });
const needListen = !this.#articleWatchers.has(key) && !this.#historyWatchers.has(key);
let watchers = this.#historyWatchers.get(key); let watchers = this.#historyWatchers.get(key);
if (!watchers) { if (!watchers) {
watchers = new Set(); watchers = new Set();
this.#historyWatchers.set(key, watchers); this.#historyWatchers.set(key, watchers);
this.#events?.addEventListener(`article-${key}`, this.#onArticleHistorySignal);
} }
watchers.add(run); watchers.add(run);
if (needListen) {
this.#events?.addEventListener(`article-${key}`, this.#onArticleSignal);
}
run(); run();
@@ -170,16 +218,22 @@ export class StorageManager extends ApiClient {
set.delete(run); set.delete(run);
if (set.size === 0) { if (set.size === 0) {
this.#historyWatchers.delete(key); this.#historyWatchers.delete(key);
this.#events?.removeEventListener(`article-${key}`, this.#onArticleHistorySignal); if (!this.#articleWatchers.has(key)) {
this.#events?.removeEventListener(`article-${key}`, this.#onArticleSignal);
}
} }
}; };
} }
#onArticleHistorySignal = (event) => { #onArticleSignal = (event) => {
const sku = event.type.slice('article-'.length); const sku = event.type.slice('article-'.length);
const watchers = this.#historyWatchers.get(sku); if (this.#articleWatchers.has(sku)) {
if (!watchers) return; void this.loadArticle(sku).catch((err) => notifyError(err));
for (const run of watchers) run(); }
const historyWatchers = this.#historyWatchers.get(sku);
if (historyWatchers) {
for (const run of historyWatchers) run();
}
}; };
/** Patch local articleIndex name if already loaded; no-op otherwise. */ /** Patch local articleIndex name if already loaded; no-op otherwise. */
@@ -261,8 +315,11 @@ export class StorageManager extends ApiClient {
console.error('[storageMgr] articles SSE refresh failed', err); console.error('[storageMgr] articles SSE refresh failed', err);
}); });
}); });
for (const sku of this.#historyWatchers.keys()) { for (const sku of new Set([
es.addEventListener(`article-${sku}`, this.#onArticleHistorySignal); ...this.#articleWatchers.keys(),
...this.#historyWatchers.keys(),
])) {
es.addEventListener(`article-${sku}`, this.#onArticleSignal);
} }
es.onerror = () => { es.onerror = () => {
console.error('[storageMgr] events stream error'); console.error('[storageMgr] events stream error');