supplierAutomat

This commit is contained in:
seb
2026-09-21 10:55:17 +02:00
parent 98080f0514
commit 2046f96768
9 changed files with 222 additions and 34 deletions

View File

@@ -0,0 +1,201 @@
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<Supplier>} 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<Array<Supplier>>} 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<Array<Supplier>>} 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<Array<Supplier>>} Loaded suppliers.
*/
load() {
return loadSuppliers();
},
/**
* Silently re-fetches the supplier list while keeping the current state.
*
* @returns {Promise<Array<Supplier>>} 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<Supplier>} 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<Supplier>} 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<void>}
*/
async remove(id) {
await supplierApi.delete(id);
await supplierAutomat.actions.reload();
},
/**
* Replaces the cached list without a round-trip.
*
* @param {Array<Supplier>} 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<SupplierState>}
*/
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;