export const ASYNC_BASE_STATES = Object.freeze([ "initial-loading", "success", "empty", "terminal-error", ]); export const ASYNC_OVERLAYS = Object.freeze([ "refreshing", "stale-degraded", "mutation-pending", "mutation-conflict", ]); /** * @typedef {{ * data?: unknown, * isInitialLoading?: boolean, * failure?: import("../../contracts/errors.js").ApiFailure, * isFetching?: boolean, * isStale?: boolean, * isDegraded?: boolean, * isMutationPending?: boolean, * hasMutationConflict?: boolean * }} AsyncSignals */ /** @param {AsyncSignals} signals */ export function deriveAsyncState(signals) { const hasData = signals.data !== undefined && signals.data !== null; const empty = hasData && ((Array.isArray(signals.data) && signals.data.length === 0) || signals.data === ""); let base; if (signals.isInitialLoading && !hasData) { base = "initial-loading"; } else if (signals.failure && !hasData) { base = "terminal-error"; } else if (empty) { base = "empty"; } else if (hasData) { base = "success"; } else { base = "initial-loading"; } const overlay = Object.freeze({ refreshing: Boolean(signals.isFetching && hasData), staleDegraded: Boolean(signals.isStale && signals.isDegraded && hasData), mutationPending: Boolean(signals.isMutationPending && hasData), mutationConflict: Boolean(signals.hasMutationConflict && hasData), }); const state = { base, data: base === "success" || base === "empty" ? signals.data : undefined, failure: base === "terminal-error" ? signals.failure : undefined, overlay, indicator: selectOverlayIndicator(overlay), }; return Object.freeze(state); } /** * @param {{ * refreshing: boolean, * staleDegraded: boolean, * mutationPending: boolean, * mutationConflict: boolean * }} overlay */ export function selectOverlayIndicator(overlay) { if (overlay.mutationConflict) return "mutation-conflict"; if (overlay.mutationPending) return "mutation-pending"; if (overlay.staleDegraded) return "stale-degraded"; if (overlay.refreshing) return "refreshing"; return null; }