mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
useCustomFirmware() was called multiple times with the same file which resulted in the same file being parsed 3 times. Instead we should only call useCustomFirmware() and pass the results as props instead.
218 lines
6.6 KiB
TypeScript
218 lines
6.6 KiB
TypeScript
// SPDX-License-Identifier: MIT
|
|
// Copyright (c) 2022-2023 The Pybricks Authors
|
|
// based on https://usehooks-ts.com/react-hook/use-fetch
|
|
|
|
import { FirmwareMetadata, FirmwareReader } from '@pybricks/firmware';
|
|
import cityHubZip from '@pybricks/firmware/build/cityhub.zip';
|
|
import essentialHubZip from '@pybricks/firmware/build/essentialhub.zip';
|
|
import moveHubZip from '@pybricks/firmware/build/movehub.zip';
|
|
import primeHubZip from '@pybricks/firmware/build/primehub.zip';
|
|
import technicHubZip from '@pybricks/firmware/build/technichub.zip';
|
|
import { useEffect, useMemo, useReducer, useRef } from 'react';
|
|
import { useDispatch } from 'react-redux';
|
|
import { useIsMounted } from 'usehooks-ts';
|
|
import { alertsShowAlert } from '../../alerts/actions';
|
|
import { Hub } from '../../components/hubPicker';
|
|
import { ensureError } from '../../utils';
|
|
|
|
export type FirmwareData = {
|
|
firmwareZip: ArrayBuffer;
|
|
licenseText: string;
|
|
metadata: FirmwareMetadata;
|
|
};
|
|
|
|
interface State {
|
|
/** The firmware.zip data or undefined if `fetch()` is not complete or on error. */
|
|
firmwareData?: FirmwareData;
|
|
/** Undefined `fetch()` is not complete yet or was successful, otherwise the error. */
|
|
firmwareError?: Error;
|
|
}
|
|
|
|
type Cache = { [url: string]: FirmwareData };
|
|
|
|
// discriminated union type
|
|
type Action =
|
|
| { type: 'loading' }
|
|
| { type: 'fetched'; payload: FirmwareData }
|
|
| { type: 'error'; payload: Error };
|
|
|
|
const firmwareZipMap = new Map<Hub, string>([
|
|
[Hub.Move, moveHubZip],
|
|
[Hub.City, cityHubZip],
|
|
[Hub.Technic, technicHubZip],
|
|
[Hub.Prime, primeHubZip],
|
|
[Hub.Essential, essentialHubZip],
|
|
[Hub.Inventor, primeHubZip],
|
|
]);
|
|
|
|
/**
|
|
* Gets Pybricks firmware .zip file for the specified hub type.
|
|
* @param hubType The hub type.
|
|
* @returns The current state.
|
|
*/
|
|
export function useFirmware(hubType: Hub): State {
|
|
const url = firmwareZipMap.get(hubType);
|
|
const cache = useRef<Cache>({});
|
|
const isMounted = useIsMounted();
|
|
|
|
const initialState: State = {
|
|
firmwareError: undefined,
|
|
firmwareData: undefined,
|
|
};
|
|
|
|
// Keep state logic separated
|
|
const fetchReducer = (state: State, action: Action): State => {
|
|
switch (action.type) {
|
|
case 'loading':
|
|
return { ...initialState };
|
|
case 'fetched':
|
|
return { ...initialState, firmwareData: action.payload };
|
|
case 'error':
|
|
return { ...initialState, firmwareError: action.payload };
|
|
default:
|
|
return state;
|
|
}
|
|
};
|
|
|
|
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
|
|
|
useEffect(() => {
|
|
// Do nothing if the url is not given
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
const fetchData = async () => {
|
|
dispatch({ type: 'loading' });
|
|
|
|
// If a cache exists for this url, return it
|
|
if (cache.current[url]) {
|
|
dispatch({ type: 'fetched', payload: cache.current[url] });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(response.statusText);
|
|
}
|
|
|
|
const firmwareZip = await response.arrayBuffer();
|
|
const reader = await FirmwareReader.load(firmwareZip);
|
|
const licenseText = await reader.readReadMeOss();
|
|
const metadata = await reader.readMetadata();
|
|
const data = { firmwareZip, licenseText, metadata };
|
|
|
|
cache.current[url] = data;
|
|
|
|
if (!isMounted()) {
|
|
return;
|
|
}
|
|
|
|
dispatch({ type: 'fetched', payload: data });
|
|
} catch (error) {
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
console.error(error);
|
|
}
|
|
|
|
if (!isMounted()) {
|
|
return;
|
|
}
|
|
|
|
dispatch({ type: 'error', payload: ensureError(error) });
|
|
}
|
|
};
|
|
|
|
void fetchData();
|
|
}, [url, isMounted]);
|
|
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Gets the data from the user-provided firmware file, if any.
|
|
* @param zipFile The user-provided zip file.
|
|
* @returns State consisting of unzipped data or error.
|
|
*/
|
|
export function useCustomFirmware(zipFile: File | undefined) {
|
|
const reduxDispatch = useDispatch();
|
|
const isMounted = useIsMounted();
|
|
|
|
const initialState: State = {
|
|
firmwareError: undefined,
|
|
firmwareData: undefined,
|
|
};
|
|
|
|
// Keep state logic separated
|
|
const fetchReducer = (state: State, action: Action): State => {
|
|
switch (action.type) {
|
|
case 'loading':
|
|
return { ...initialState };
|
|
case 'fetched':
|
|
return { ...initialState, firmwareData: action.payload };
|
|
case 'error':
|
|
return { ...initialState, firmwareError: action.payload };
|
|
default:
|
|
return state;
|
|
}
|
|
};
|
|
|
|
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
|
|
|
useEffect(() => {
|
|
if (!zipFile) {
|
|
dispatch({ type: 'loading' });
|
|
return;
|
|
}
|
|
|
|
// REVISIT: with no cache, we end up unzipping the same file multiple times.
|
|
|
|
const readFile = async () => {
|
|
dispatch({ type: 'loading' });
|
|
|
|
try {
|
|
const firmwareZip = await zipFile.arrayBuffer();
|
|
const reader = await FirmwareReader.load(firmwareZip);
|
|
const licenseText = await reader.readReadMeOss();
|
|
const metadata = await reader.readMetadata();
|
|
const data = {
|
|
firmwareZip,
|
|
licenseText,
|
|
metadata,
|
|
};
|
|
|
|
if (!isMounted()) {
|
|
return;
|
|
}
|
|
|
|
dispatch({ type: 'fetched', payload: data });
|
|
} catch (err) {
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
console.error(err);
|
|
}
|
|
|
|
if (!isMounted()) {
|
|
return;
|
|
}
|
|
|
|
const error = ensureError(err);
|
|
dispatch({ type: 'error', payload: error });
|
|
reduxDispatch(alertsShowAlert('alerts', 'unexpectedError', { error }));
|
|
}
|
|
};
|
|
|
|
readFile();
|
|
}, [zipFile, isMounted, reduxDispatch]);
|
|
|
|
const isCustomFirmwareRequested = useMemo(
|
|
() => state.firmwareData !== undefined,
|
|
[state.firmwareData],
|
|
);
|
|
|
|
return {
|
|
isCustomFirmwareRequested,
|
|
customFirmwareData: state.firmwareData,
|
|
customFirmwareError: state.firmwareError,
|
|
};
|
|
}
|