166 lines
4.9 KiB
JavaScript
166 lines
4.9 KiB
JavaScript
/**
|
|
* @file createListAutomat.js
|
|
* @description Factory function for company-scoped list automats with
|
|
* deduplicated `load()`, silent `reload()`, and automatic session synchronization.
|
|
*
|
|
* Upstream Triggers:
|
|
* - `authAutomat`: Subscribes each created list automat to auth state — calls
|
|
* `actions.load()` on `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
|
|
*
|
|
* Used by:
|
|
* - `deliveryAutomat`, `quoteAutomat`, `orderAutomat`,
|
|
* `entityAutomat`, `customergroupAutomat`, `numberAutomat`.
|
|
*
|
|
* NOT used by `articleIndexAutomat`: that store is derived from the loaded
|
|
* category views rather than fetched from a whole-company endpoint.
|
|
*/
|
|
|
|
import { Automat } from 'react-automat';
|
|
import authAutomat from './authAutomat';
|
|
|
|
/**
|
|
* @typedef {Object} ListAutomatState
|
|
* @property {Array<object>} items Loaded records (empty until the first success).
|
|
* @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state.
|
|
* @property {Error|string|null} error Last load error.
|
|
*/
|
|
|
|
/**
|
|
* Creates a company-scoped list store following the brand/unit/warehouse
|
|
* pattern: `load()` deduplicates concurrent callers, `reload()` refreshes
|
|
* silently, and `reset()` clears on logout.
|
|
*
|
|
* This replaces the stale-while-revalidate list caches that previously lived
|
|
* in `StorageManager` (articleIndex, delivery, quote, order, entity,
|
|
* customergroup, number).
|
|
*
|
|
* @template T
|
|
* @param {object} options Factory options.
|
|
* @param {string} options.name Automat registry name (also used for logging).
|
|
* @param {() => Promise<Array<T>>} options.fetch Loads the full record list.
|
|
* @param {(items: Array<T>) => Array<T>} [options.sort] Optional stable ordering.
|
|
* @returns {Automat<ListAutomatState>} Configured list automat instance.
|
|
*/
|
|
export function createListAutomat({ name, fetch: fetchItems, sort }) {
|
|
const initialState = { items: [], status: 'idle', error: null };
|
|
|
|
let automat;
|
|
/** In-flight `load()` promise so concurrent consumers share one request. */
|
|
let inflightLoad = null;
|
|
|
|
/**
|
|
* Fetches and normalizes the record list.
|
|
*
|
|
* @returns {Promise<Array<T>>} Records from the backend.
|
|
*/
|
|
const fetchRecords = async () => {
|
|
const items = await fetchItems();
|
|
return sort ? sort(items) : items;
|
|
};
|
|
|
|
const actions = {
|
|
/**
|
|
* Loads the list, deduplicating concurrent callers.
|
|
*
|
|
* @returns {Promise<Array<T>>} Loaded records.
|
|
*/
|
|
load() {
|
|
if (inflightLoad) return inflightLoad;
|
|
inflightLoad = (async () => {
|
|
automat.setState({ status: 'loading', error: null });
|
|
try {
|
|
const items = await fetchRecords();
|
|
automat.setState({ items, status: 'ready', error: null });
|
|
return items;
|
|
} catch (error) {
|
|
automat.setState({ status: 'failed', error });
|
|
throw error;
|
|
} finally {
|
|
inflightLoad = null;
|
|
}
|
|
})();
|
|
return inflightLoad;
|
|
},
|
|
|
|
/**
|
|
* Silently re-fetches the list while keeping the current state, so
|
|
* subscribers do not flash a loading state.
|
|
*
|
|
* @returns {Promise<Array<T>>} Fresh records.
|
|
*/
|
|
async reload() {
|
|
try {
|
|
const items = await fetchRecords();
|
|
automat.setState({ items, status: 'ready', error: null });
|
|
return items;
|
|
} catch (error) {
|
|
automat.setState({ error });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Replaces the cached list without a round-trip.
|
|
*
|
|
* @param {Array<T>} items Records to cache.
|
|
* @returns {Array<T>} Cached records.
|
|
*/
|
|
replace(items) {
|
|
automat.setState({ items, status: 'ready', error: null });
|
|
return items;
|
|
},
|
|
|
|
/**
|
|
* Re-fetches the list once the selected company is known.
|
|
*
|
|
* @returns {Promise<Array<T>>} Loaded records.
|
|
*/
|
|
ensureLoaded() {
|
|
if (automat.state.status === 'ready') {
|
|
return Promise.resolve(automat.state.items);
|
|
}
|
|
return actions.load();
|
|
},
|
|
|
|
/** Reset to idle state (called on logout). */
|
|
reset() {
|
|
inflightLoad = null;
|
|
automat.setState(initialState);
|
|
},
|
|
};
|
|
|
|
automat = new Automat(initialState, actions, { name });
|
|
|
|
// Load on session/company establishment, reset on logout.
|
|
authAutomat.subscribe((auth) => {
|
|
const companyId = auth.user?.selectedCompanyId;
|
|
if (auth.status === 'authed' && companyId) {
|
|
void actions.load().catch(() => {});
|
|
} else if (auth.status === 'anon') {
|
|
actions.reset();
|
|
}
|
|
});
|
|
|
|
return automat;
|
|
}
|
|
|
|
/**
|
|
* Selects the record list of a list automat.
|
|
*
|
|
* @param {Automat<ListAutomatState>} automat List automat.
|
|
* @returns {Array<object>} Current records.
|
|
*/
|
|
export function selectListItems(automat) {
|
|
return automat.state.items;
|
|
}
|
|
|
|
/**
|
|
* Selects the load status of a list automat.
|
|
*
|
|
* @param {Automat<ListAutomatState>} automat List automat.
|
|
* @returns {'idle'|'loading'|'ready'|'failed'} Current status.
|
|
*/
|
|
export function selectListStatus(automat) {
|
|
return automat.state.status;
|
|
}
|