157 lines
3.8 KiB
TypeScript
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,
|
|
});
|
|
}
|