This commit is contained in:
seb
2026-10-09 19:13:57 +02:00
parent e9190f1ea9
commit 645a9153a0
2 changed files with 169 additions and 38 deletions

View File

@@ -5,12 +5,19 @@
* Upstream Triggers:
* - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on
* `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
* - `sseAutomat`: Subscribes to the single `'brand'` event carrying
* `{ sender, brandId }` — any create/rename/delete/picture change triggers
* `actions.reload()`, which silently re-fetches the list. Subscribed views
* (e.g. `ArtikelMarkenPage` via `brandAutomat.subscribe(...)`) then
* re-render from the updated state. This is the reload pattern. Events sent
* by this tab (matching `sender`) are ignored.
* - UI / Settings: Mutating actions (`create`, `rename`, `remove`, `addImage`, `deleteImage`).
*/
import { Automat } from 'react-automat';
import { brand as brandApi } from '../api';
import authAutomat from './authAutomat';
import sseAutomat, { getSseClientId } from './sseAutomat';
/**
* @typedef {Object} BrandState
@@ -34,19 +41,56 @@ function sortBrands(brands) {
let brandAutomat;
/** In-flight `load()`/`reload()` promise so concurrent callers share one request. */
let inflightLoad = null;
async function fetchBrands() {
const res = await brandApi.list();
return sortBrands(Array.isArray(res.brands) ? res.brands : []);
}
const actions = {
/** Load all brands for the selected company. */
async load() {
if (inflightLoad) return inflightLoad;
brandAutomat.setState({ status: 'loading', error: null });
try {
const res = await brandApi.list();
const brands = sortBrands(Array.isArray(res.brands) ? res.brands : []);
brandAutomat.setState({ brands, status: 'succeeded' });
return brands;
} catch (err) {
brandAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
throw err;
}
inflightLoad = (async () => {
try {
const brands = await fetchBrands();
brandAutomat.setState({ brands, status: 'succeeded' });
return brands;
} catch (err) {
brandAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
throw err;
} finally {
inflightLoad = null;
}
})();
return inflightLoad;
},
/**
* Silently re-fetches the brand list while keeping the current state
* visible (no loading flash). Subscribed views re-render from the updated
* state — this is the reload pattern for SSE-driven refreshes.
*
* @returns {Promise<Array<object>>} Fresh brands.
*/
async reload() {
if (inflightLoad) return inflightLoad;
inflightLoad = (async () => {
try {
const brands = await fetchBrands();
brandAutomat.setState({ brands, status: 'succeeded', error: null });
return brands;
} catch (err) {
brandAutomat.setState({ error: err?.message ?? String(err) });
throw err;
} finally {
inflightLoad = null;
}
})();
return inflightLoad;
},
/**
@@ -134,4 +178,21 @@ authAutomat.subscribe((auth) => {
}
});
// SSE: single `brand` event with `{ sender, brandId }` payload for every
// create/rename/delete/picture change. Silently reloads the list; subscribed
// views re-render from state. Own events are skipped.
sseAutomat.actions.on('brand', (event) => {
try {
const payload = JSON.parse(event?.data ?? '{}');
if (payload && typeof payload.sender === 'string' && payload.sender) {
try {
if (payload.sender === getSseClientId()) return;
} catch {}
}
} catch {
// Unparseable payload: fall through to reload.
}
void brandAutomat.actions.reload().catch(() => {});
});
export default brandAutomat;