feat: execute HTTP and query runtime contracts
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user