u
This commit is contained in:
148
src/automats/createListAutomat.js
Normal file
148
src/automats/createListAutomat.js
Normal file
@@ -0,0 +1,148 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user