This commit is contained in:
seb
2026-10-09 19:44:55 +02:00
parent 645a9153a0
commit 14ae017b64
5 changed files with 125 additions and 33 deletions

View File

@@ -21,7 +21,8 @@ import sseAutomat, { getSseClientId } from './sseAutomat';
/**
* @typedef {Object} BrandState
* @property {Array<object>} brands
* @property {Array<object>} brands All brands (full list, decoupled from page UI).
* @property {number} total Unpaged brand count.
* @property {'idle'|'loading'|'succeeded'|'failed'} status
* @property {string|null} error
*/
@@ -29,6 +30,7 @@ import sseAutomat, { getSseClientId } from './sseAutomat';
/** @type {BrandState} */
const initialState = {
brands: [],
total: 0,
status: 'idle',
error: null,
};
@@ -44,20 +46,33 @@ 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 : []);
/** Max page size the API accepts; full list is fetched by paging through. */
const FETCH_PAGE_SIZE = 100;
async function fetchAllBrands() {
let offset = 0;
let total = Infinity;
const collected = [];
for (;;) {
const res = await brandApi.list({ offset, limit: FETCH_PAGE_SIZE });
if (offset === 0) total = Number(res.total ?? res.brands?.length ?? 0);
const page = Array.isArray(res.brands) ? res.brands : [];
collected.push(...page);
offset += page.length;
if (page.length === 0 || collected.length >= total) break;
}
return { brands: sortBrands(collected), total: Number.isFinite(total) ? total : collected.length };
}
const actions = {
/** Load all brands for the selected company. */
/** Load all brands for the selected company (pages through the API). */
async load() {
if (inflightLoad) return inflightLoad;
brandAutomat.setState({ status: 'loading', error: null });
inflightLoad = (async () => {
try {
const brands = await fetchBrands();
brandAutomat.setState({ brands, status: 'succeeded' });
const { brands, total } = await fetchAllBrands();
brandAutomat.setState({ brands, total, status: 'succeeded' });
return brands;
} catch (err) {
brandAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
@@ -70,7 +85,7 @@ const actions = {
},
/**
* Silently re-fetches the brand list while keeping the current state
* Silently re-fetches all brands 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.
*
@@ -80,8 +95,8 @@ const actions = {
if (inflightLoad) return inflightLoad;
inflightLoad = (async () => {
try {
const brands = await fetchBrands();
brandAutomat.setState({ brands, status: 'succeeded', error: null });
const { brands, total } = await fetchAllBrands();
brandAutomat.setState({ brands, total, status: 'succeeded', error: null });
return brands;
} catch (err) {
brandAutomat.setState({ error: err?.message ?? String(err) });
@@ -104,7 +119,7 @@ const actions = {
...brandAutomat.state.brands.filter((b) => b.id !== row.id),
row,
]);
brandAutomat.setState({ brands });
brandAutomat.setState({ brands, total: brands.length });
return row;
},
@@ -130,7 +145,7 @@ const actions = {
async remove(id) {
await brandApi.delete(id);
const brands = brandAutomat.state.brands.filter((b) => b.id !== id);
brandAutomat.setState({ brands });
brandAutomat.setState({ brands, total: brands.length });
},
/**