/** * @file supplierAutomat.js * @description Manages cached supplier list (`GET /api/supplier`) for the active company. * * Upstream Triggers: * - `authAutomat`: Subscribes to auth state changes (`syncWithAuth`) — triggers * `actions.load()` on `'authed'` with a `selectedCompanyId`, and `actions.clear()` on `'anon'`. * - `sseAutomat`: Calls `actions.reload()` upon receiving a `'supplier'` SSE event. * - UI / Routes: Calls to `actions.load()`, `actions.reload()`, or `actions.setSuppliers()`. */ import { Automat } from 'react-automat'; import { supplier as supplierApi } from '../api'; import { requireArray, requireDefined, requireObject } from '../utils/apiShape'; import authAutomat from './authAutomat'; /** * @typedef {Object} Supplier * @property {string|number} id Supplier identifier. * @property {string} name Supplier name. */ /** * @typedef {Object} SupplierState * @property {Array} suppliers Company-scoped suppliers. * @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state. * @property {Error|null} error Last load error. */ /** @type {SupplierState} */ const initialState = { suppliers: [], status: 'idle', error: null, }; let supplierAutomat; /** * Fetches the company-scoped supplier list. * * @returns {Promise>} Supplier list. */ async function fetchSuppliers() { const data = await supplierApi.list(); requireObject(data, 'GET /api/supplier'); return requireArray(data.suppliers, 'suppliers'); } /** In-flight `load()` promise so concurrent consumers share one request. */ let inflightLoad = null; /** * Loads the supplier list once, deduplicating concurrent callers. * * @returns {Promise>} Loaded suppliers. */ function loadSuppliers() { if (inflightLoad) return inflightLoad; inflightLoad = (async () => { supplierAutomat.setState({ status: 'loading', error: null }); try { const suppliers = await fetchSuppliers(); supplierAutomat.setState({ suppliers, status: 'ready', error: null }); return suppliers; } catch (error) { supplierAutomat.setState({ status: 'failed', error }); throw error; } finally { inflightLoad = null; } })(); return inflightLoad; } const actions = { /** * Loads the supplier list, exposing a loading/failed lifecycle. * * @returns {Promise>} Loaded suppliers. */ load() { return loadSuppliers(); }, /** * Silently re-fetches the supplier list while keeping the current state. * * @returns {Promise>} Fresh suppliers. */ async reload() { try { const suppliers = await fetchSuppliers(); supplierAutomat.setState({ suppliers, status: 'ready', error: null }); return suppliers; } catch (error) { supplierAutomat.setState({ error }); throw error; } }, /** * Creates a supplier and refreshes the cached list. * * @param {{name: string}} body Creation payload. * @returns {Promise} Created supplier. */ async create(body) { const data = await supplierApi.create(body); requireObject(data, 'POST /api/supplier'); requireObject(data.supplier, 'supplier'); requireDefined(data.supplier.id, 'supplier.id'); await supplierAutomat.actions.reload(); return data.supplier; }, /** * Renames a supplier and refreshes the cached list. * * @param {string|number} id Supplier identifier. * @param {{name: string}} body Update payload. * @returns {Promise} Updated supplier. */ async update(id, body) { const data = await supplierApi.update(id, body); requireObject(data, 'PATCH /api/supplier/:id'); requireObject(data.supplier, 'supplier'); requireDefined(data.supplier.id, 'supplier.id'); await supplierAutomat.actions.reload(); return data.supplier; }, /** * Deletes a supplier and refreshes the cached list. * * @param {string|number} id Supplier identifier. * @returns {Promise} */ async remove(id) { await supplierApi.delete(id); await supplierAutomat.actions.reload(); }, /** * Replaces the cached list without a round-trip. * * @param {Array} suppliers Suppliers to cache. * @returns {void} */ setSuppliers(suppliers) { supplierAutomat.setState({ suppliers: Array.isArray(suppliers) ? suppliers : [], status: 'ready', error: null, }); }, /** * Forgets the cached list and returns to the initial state. * * @returns {void} */ clear() { supplierAutomat.setState({ ...initialState }); }, }; /** * Company-scoped supplier cache. Loads automatically once an authenticated * session with a selected company exists and is cleared when auth is gone. * * @type {Automat} */ supplierAutomat = new Automat(initialState, actions, { name: 'supplier' }); /** * Derives the cache key from the auth state: the selected company while * authenticated, otherwise `null`. * * @param {import('./authAutomat').AuthState} authState Auth automat state. * @returns {string|null} Cache key. */ function authKey(authState) { if (authState?.status !== 'authed' || !authState.user) return null; const companyId = authState.user.selectedCompanyId; return companyId == null ? null : String(companyId); } let lastAuthKey; /** * Loads the supplier list when auth/company becomes available and forgets it * when the session ends. * * @param {import('./authAutomat').AuthState} authState Auth automat state. * @returns {void} */ function syncWithAuth(authState) { const key = authKey(authState); if (key === lastAuthKey) return; lastAuthKey = key; if (key == null) { supplierAutomat.actions.clear(); } else { void supplierAutomat.actions.load().catch(() => {}); } } authAutomat.subscribe(syncWithAuth); syncWithAuth(authAutomat.state); export default supplierAutomat;