Files
clean-architecture-frontend…/src/application/view-models/async-state.ts
T

157 lines
3.8 KiB
TypeScript

import type { ApiFailure } from "../../contracts/errors.js";
export const ASYNC_BASE_STATES = Object.freeze([
"initial-loading",
"success",
"empty",
"terminal-error",
] as const);
export const ASYNC_OVERLAYS = Object.freeze([
"refreshing",
"stale-degraded",
"mutation-pending",
"mutation-conflict",
] as const);
export type AsyncOverlay =
| Readonly<{
refreshing: false;
staleDegraded: false;
mutationPending: false;
mutationConflict: false;
}>
| Readonly<{
refreshing: true;
staleDegraded: false;
mutationPending: false;
mutationConflict: false;
}>
| Readonly<{
refreshing: false;
staleDegraded: true;
mutationPending: false;
mutationConflict: false;
}>
| Readonly<{
refreshing: false;
staleDegraded: false;
mutationPending: true;
mutationConflict: false;
}>
| Readonly<{
refreshing: false;
staleDegraded: false;
mutationPending: false;
mutationConflict: true;
}>;
export type AsyncSignals = Readonly<{
data?: unknown;
isInitialLoading?: boolean;
failure?: ApiFailure;
isFetching?: boolean;
isStale?: boolean;
isDegraded?: boolean;
isMutationPending?: boolean;
hasMutationConflict?: boolean;
}>;
export type AsyncState = Readonly<{
base: (typeof ASYNC_BASE_STATES)[number];
data?: unknown;
failure?: ApiFailure;
overlay: AsyncOverlay;
indicator: (typeof ASYNC_OVERLAYS)[number] | null;
}>;
export function deriveAsyncState(signals: AsyncSignals): AsyncState {
const hasData = signals.data !== undefined && signals.data !== null;
const empty =
hasData &&
((Array.isArray(signals.data) && signals.data.length === 0) ||
signals.data === "");
const base =
signals.isInitialLoading && !hasData
? "initial-loading"
: signals.failure && !hasData
? "terminal-error"
: empty
? "empty"
: hasData
? "success"
: "initial-loading";
const indicator =
signals.hasMutationConflict && hasData
? "mutation-conflict"
: signals.isMutationPending && hasData
? "mutation-pending"
: signals.isStale && signals.isDegraded && hasData
? "stale-degraded"
: signals.isFetching && hasData
? "refreshing"
: null;
const overlay = overlayFor(indicator);
return Object.freeze({
base,
data: base === "success" || base === "empty" ? signals.data : undefined,
failure: base === "terminal-error" ? signals.failure : undefined,
overlay,
indicator,
});
}
export function selectOverlayIndicator(
overlay: AsyncOverlay,
): AsyncState["indicator"] {
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;
}
function overlayFor(indicator: AsyncState["indicator"]): AsyncOverlay {
if (indicator === "refreshing") {
return Object.freeze({
refreshing: true,
staleDegraded: false,
mutationPending: false,
mutationConflict: false,
});
}
if (indicator === "stale-degraded") {
return Object.freeze({
refreshing: false,
staleDegraded: true,
mutationPending: false,
mutationConflict: false,
});
}
if (indicator === "mutation-pending") {
return Object.freeze({
refreshing: false,
staleDegraded: false,
mutationPending: true,
mutationConflict: false,
});
}
if (indicator === "mutation-conflict") {
return Object.freeze({
refreshing: false,
staleDegraded: false,
mutationPending: false,
mutationConflict: true,
});
}
return Object.freeze({
refreshing: false,
staleDegraded: false,
mutationPending: false,
mutationConflict: false,
});
}