Skip to content

Commit

Permalink
refactor: simplify frontend API data hook
Browse files Browse the repository at this point in the history
* Removes the need for a distinct "response handler" for simulation
  endpoints.

* Removes an unused field from an argument type.

* Improves some potentially confusing naming.
  • Loading branch information
digitalcora committed Sep 4, 2024
1 parent 28f32d6 commit 90bd2b5
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 85 deletions.
12 changes: 5 additions & 7 deletions assets/src/components/v2/screen_container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, {
} from "react";
import useApiResponse, {
ApiResponse,
SimulationApiResponse,
SimulationData,
useDUPApiResponse,
useTriptychApiResponse,
} from "Hooks/v2/use_api_response";
Expand All @@ -17,9 +17,7 @@ import Widget, { WidgetData } from "Components/v2/widget";
import useAudioReadout from "Hooks/v2/use_audio_readout";
import { isDup, isOFM, isTriptych } from "Util/outfront";

type ResponseMapper = (
apiResponse: ApiResponse,
) => WidgetData | SimulationApiResponse;
type ResponseMapper = (apiResponse: ApiResponse) => WidgetData | SimulationData;

const defaultResponseMapper: ResponseMapper = (apiResponse) => {
switch (apiResponse.state) {
Expand Down Expand Up @@ -96,9 +94,9 @@ const ScreenLayout: ComponentType<ScreenLayoutProps> = ({
const responseMapper = useContext(ResponseMapperContext);
const ErrorBoundaryOrFragment = isOFM() ? Fragment : WidgetTreeErrorBoundary;

// We know this can only be `WidgetData` and not `SimulationApiResponse` here
// because `ScreenPage` is only used in contexts where a "non-simulation" API
// response will be received (and vice-versa for `SimulationScreenLayout`).
// We know this can only be `WidgetData` and not `SimulationData` here because
// `ScreenPage` is only used in contexts where a "non-simulation" API response
// will be received (and vice-versa for `SimulationScreenLayout`).
// TODO: Refactor how this works so the cast isn't needed, since it suppresses
// any real type errors we might have.
const widgetData = responseMapper(apiResponse) as WidgetData;
Expand Down
4 changes: 2 additions & 2 deletions assets/src/components/v2/simulation_screen_container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import Widget, { WidgetData } from "./widget";
import {
ApiResponse,
SimulationApiResponse,
SimulationData,
useSimulationApiResponse,
} from "Hooks/v2/use_api_response";
import WidgetTreeErrorBoundary from "Components/v2/widget_tree_error_boundary";
Expand All @@ -22,7 +22,7 @@ const SimulationScreenLayout: ComponentType<SimulationScreenLayoutProps> = ({
}) => {
const responseMapper = useContext(ResponseMapperContext);
// See `ScreenLayout` for the explanation of this cast.
const data = responseMapper(apiResponse) as SimulationApiResponse;
const data = responseMapper(apiResponse) as SimulationData;
const { fullPage, flexZone } = data;

// If "alternateView" was provided as an option, we use the simulation version of screen normal
Expand Down
108 changes: 32 additions & 76 deletions assets/src/hooks/v2/use_api_response.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,70 +13,46 @@ import useRefreshRate from "./use_refresh_rate";

const MINUTE_IN_MS = 60_000;

interface RawResponse {
data: WidgetData | null;
force_reload: boolean;
disabled: boolean;
}
type SimulationResponse = { full_page: WidgetData; flex_zone: WidgetData[] };

interface SimulationRawResponse {
data: {
full_page: WidgetData;
flex_zone: WidgetData[];
};
force_reload: boolean;
type RawResponse = {
data: SimulationResponse | WidgetData | null;
disabled: boolean;
}
force_reload: boolean;
};

type SimulationData = { fullPage: WidgetData; flexZone: WidgetData[] };

type ApiResponse =
// The request was successful.
| { state: "success"; data: WidgetData }
| { state: "simulation_success"; data: SimulationApiResponse }
// The request was successful, but this screen is currently disabled via config.
| { state: "simulation_success"; data: SimulationData }
// The request was successful, but this screen is disabled via config.
| { state: "disabled" }
// Either:
// - The request failed.
// - The server responded, but did not successfully fetch data. Riders may still be able to find data from other sources.
// - The server responded, but did not successfully fetch data. Riders may
// still be able to find data from other sources.
| { state: "failure" }
| { state: "loading" };

type SimulationApiResponse =
// The request was successful.
{
fullPage: WidgetData;
flexZone: WidgetData[];
};

const FAILURE_RESPONSE: ApiResponse = { state: "failure" };
const LOADING_RESPONSE: ApiResponse = { state: "loading" };

const rawResponseToApiResponse = ({
data,
disabled,
}: RawResponse): ApiResponse => {
if (disabled) {
const rawResponseToApiResponse = (response: RawResponse): ApiResponse => {
if (response.disabled) {
return { state: "disabled" };
} else if (data != null) {
return { state: "success", data };
} else {
return { state: "failure" };
}
};

const rawResponseToSimulationApiResponse = ({
data,
disabled,
}: SimulationRawResponse): ApiResponse => {
if (disabled) {
return { state: "disabled" };
} else if (data != null) {
return {
state: "simulation_success",
data: {
fullPage: data.full_page,
flexZone: data.flex_zone,
},
};
} else if (response.data) {
const data = response.data;

if ("full_page" in data) {
return {
state: "simulation_success",
data: { fullPage: data.full_page, flexZone: data.flex_zone },
};
} else {
return { state: "success", data };
}
} else {
return { state: "failure" };
}
Expand Down Expand Up @@ -169,9 +145,7 @@ const getApiPath = (id: string, routePart: string) => {

interface UseApiResponseArgs {
id: string;
failureModeElapsedMs?: number;
routePart?: string;
responseHandler?: (json: any) => ApiResponse;
}

interface UseApiResponseReturn {
Expand All @@ -183,7 +157,6 @@ interface UseApiResponseReturn {
const useBaseApiResponse = ({
id,
routePart = "",
responseHandler = rawResponseToApiResponse,
}: UseApiResponseArgs): UseApiResponseReturn => {
const { refreshRateMs, refreshRateOffsetMs } = useRefreshRate();
const [apiResponse, setApiResponse] = useState<ApiResponse>(LOADING_RESPONSE);
Expand All @@ -195,13 +168,13 @@ const useBaseApiResponse = ({
try {
const now = Date.now();
const result = await fetch(apiPath);
const json = await result.json();
const rawResponse: RawResponse = await result.json();

if (json.force_reload) {
if (rawResponse.force_reload) {
window.location.reload();
}

const apiResponse = responseHandler(json);
const apiResponse = rawResponseToApiResponse(rawResponse);

if (apiResponse.state == "failure") {
doFailureBuffer(lastSuccess, setApiResponse, apiResponse);
Expand Down Expand Up @@ -255,19 +228,10 @@ const useInspectorControls = (
}, [lastSuccess]);
};

const useApiResponse = ({ id }) =>
useBaseApiResponse({
id,
routePart: "",
responseHandler: rawResponseToApiResponse,
});
const useApiResponse = ({ id }) => useBaseApiResponse({ id, routePart: "" });

const useSimulationApiResponse = ({ id }) =>
useBaseApiResponse({
id,
routePart: "/simulation",
responseHandler: rawResponseToSimulationApiResponse,
});
useBaseApiResponse({ id, routePart: "/simulation" });

// For OFM apps--DUP, triptych--we need to request a different
// route that's more permissive of CORS, since these clients are loaded from a local html file
Expand All @@ -279,19 +243,11 @@ const useSimulationApiResponse = ({ id }) =>
// The /triptych endpoint has the CORS stuff, plus an additional step that maps the player name of
// the individual triptych pane to a screen ID representing the collective trio.
const useDUPApiResponse = ({ id }) =>
useBaseApiResponse({
id,
routePart: "/dup",
responseHandler: rawResponseToApiResponse,
});
useBaseApiResponse({ id, routePart: "/dup" });

const useTriptychApiResponse = ({ id }) =>
useBaseApiResponse({
id,
routePart: "/triptych",
responseHandler: rawResponseToApiResponse,
});
useBaseApiResponse({ id, routePart: "/triptych" });

export default useApiResponse;
export { ApiResponse, SimulationApiResponse };
export type { ApiResponse, SimulationData };
export { useSimulationApiResponse, useDUPApiResponse, useTriptychApiResponse };

0 comments on commit 90bd2b5

Please sign in to comment.