diff --git a/src/api/ApiClient.js b/src/api/ApiClient.js index 0e1a33d..6e51612 100644 --- a/src/api/ApiClient.js +++ b/src/api/ApiClient.js @@ -108,6 +108,12 @@ export class ApiClient { get: (sku) => this.#json('GET', `/api/article/${enc(sku)}`), /** GET /api/article/{sku}/history → { history: [{ id, date, text }, ...] } */ history: (sku) => this.#json('GET', `/api/article/${enc(sku)}/history`), + /** + * Load history and refetch on SSE `article-`. + * `onHistory(history)` / `onError(err)` — returns unwatch(). + */ + historyWatch: (sku, onHistory, onError) => + this.watchArticleHistory(sku, onHistory, onError), /** PATCH /api/article/{sku} — body: at least one of { name, unitamount, unitSi } */ update: async (sku, body) => { const nameChange = body != null && Object.hasOwn(body, 'name'); diff --git a/src/api/StorageManager.js b/src/api/StorageManager.js index 4798c72..f26c21f 100644 --- a/src/api/StorageManager.js +++ b/src/api/StorageManager.js @@ -72,12 +72,67 @@ export class StorageManager extends ApiClient { #events = null; /** @type {ReturnType | null} */ #articleIndexCache = null; + /** @type {Map void>>} sku → refreshers */ + #historyWatchers = new Map(); constructor(baseUrl = window.location.origin) { super(baseUrl); this.#initCaches(); } + /** + * Fetch article history, then refetch whenever SSE emits `article-`. + * @param {string} sku + * @param {(history: unknown[]) => void} onHistory + * @param {(err: unknown) => void} onError + * @returns {() => void} unwatch + */ + watchArticleHistory(sku, onHistory, onError) { + const key = String(sku); + let active = true; + + const refresh = () => + this.article.history(key).then((res) => { + if (!active) return; + requireObject(res, 'GET /api/article/{sku}/history'); + onHistory(requireArray(res.history, 'history')); + }); + + const run = () => + void refresh().catch((err) => { + if (!active) return; + onError(err); + }); + + let watchers = this.#historyWatchers.get(key); + if (!watchers) { + watchers = new Set(); + this.#historyWatchers.set(key, watchers); + this.#events?.addEventListener(`article-${key}`, this.#onArticleHistorySignal); + } + watchers.add(run); + + run(); + + return () => { + active = false; + const set = this.#historyWatchers.get(key); + if (!set) return; + set.delete(run); + if (set.size === 0) { + this.#historyWatchers.delete(key); + this.#events?.removeEventListener(`article-${key}`, this.#onArticleHistorySignal); + } + }; + } + + #onArticleHistorySignal = (event) => { + const sku = event.type.slice('article-'.length); + const watchers = this.#historyWatchers.get(sku); + if (!watchers) return; + for (const run of watchers) run(); + }; + /** Patch local articleIndex name if already loaded; no-op otherwise. */ setArticleIndexName(sku, name) { const articles = this.#articleIndexCache.value; @@ -147,6 +202,9 @@ export class StorageManager extends ApiClient { console.error('[storageMgr] articles SSE refresh failed', err); }); }); + for (const sku of this.#historyWatchers.keys()) { + es.addEventListener(`article-${sku}`, this.#onArticleHistorySignal); + } es.onerror = () => { console.error('[storageMgr] events stream error'); }; diff --git a/src/components/article/tabs/ArticleHistoryTab.jsx b/src/components/article/tabs/ArticleHistoryTab.jsx index 0738d64..30ff34d 100644 --- a/src/components/article/tabs/ArticleHistoryTab.jsx +++ b/src/components/article/tabs/ArticleHistoryTab.jsx @@ -17,13 +17,20 @@ export default class ArticleHistoryTab extends Component { constructor(props) { super(props); this.state = { history: null }; + this.unwatchHistory = null; } componentDidMount() { - void this.context.article - .history(this.props.sku) - .then((res) => this.setState({ history: res.history })) - .catch(notifyError); + this.unwatchHistory = this.context.article.historyWatch( + this.props.sku, + (history) => this.setState({ history }), + notifyError, + ); + } + + componentWillUnmount() { + this.unwatchHistory?.(); + this.unwatchHistory = null; } render() {