diff --git a/src/Options.tsx b/src/Options.tsx index b714af1a..85f4d0c0 100644 --- a/src/Options.tsx +++ b/src/Options.tsx @@ -8,6 +8,7 @@ import { Link } from 'react-router-dom'; import Settings from './permalink/Settings'; import Acknowledgement from './Acknowledgment'; import { RawOptions } from './permalink/SettingsTypes'; +import { withCancel } from './utils/Cancel'; interface State { settings: Settings; @@ -25,6 +26,10 @@ export default class Options extends React.Component, Stat changeTriforceRequired: () => void; changeSkywardStrike: () => void; + // If the user switches branches quickly, or the GitHub API quickly responds with a release, + // we must cancel the previous load, otherwise we run into problems. + cancelSettingsLoad?: () => void; + constructor(props: Record) { super(props); this.state = { @@ -33,12 +38,7 @@ export default class Options extends React.Component, Stat latestVersion: '', source: 'main', }; - const versionData = this.getVersionData(); - versionData.then((value) => { - // pull the name of the latest version - const latestVersion = value[0].tag_name; - this.setState({ source: latestVersion, latestVersion }) - }); + /* _.forEach(regions, (region) => { this[_.camelCase(`changeRegion${region.internal}`)] = this.changeBannedLocation.bind(this, region.internal); @@ -66,10 +66,16 @@ export default class Options extends React.Component, Stat this.changeSkywardStrike = this.changeBinaryOption.bind(this, 'Upgraded Skyward Strike'); this.permalinkChanged = this.permalinkChanged.bind(this); this.updateSource = this.updateSource.bind(this); + } + + componentDidMount() { + this.updateSettingsFromSource(this.state.source); - this.state.settings.init(this.state.source).then(() => { - this.state.settings.loadDefaults(); - this.setState({ ready: true }); + this.getVersionData().then((value) => { + // pull the name of the latest version + const latestVersion = value[0].tag_name; + this.setState({ latestVersion }); + this.updateSettingsFromSource(latestVersion); }); } @@ -140,11 +146,21 @@ export default class Options extends React.Component, Stat updateSource(e: ChangeEvent) { const { value } = e.target; - this.state.settings.init(value).then(() => { - this.state.settings.loadDefaults(); - this.setState({ source: value }); - this.forceUpdate(); - }); + this.updateSettingsFromSource(value); + } + + updateSettingsFromSource(source: string) { + this.cancelSettingsLoad?.(); + const [cancelToken, cancel] = withCancel(); + this.cancelSettingsLoad = cancel; + + const settings = new Settings(); + settings.init(source).then(() => { + if (!cancelToken.canceled) { + settings.loadDefaults(); + this.setState({ settings, source, ready: true }); + } + }).finally(() => this.forceUpdate()); } render() { diff --git a/src/utils/Cancel.ts b/src/utils/Cancel.ts new file mode 100644 index 00000000..6ed02189 --- /dev/null +++ b/src/utils/Cancel.ts @@ -0,0 +1,40 @@ +/** + * A CancelToken should be passed to cancelable functions. Those functions should then check the state of the + * token and return early, or use checkCanceled to throw a CanceledError if the token has been canceled. Callers + * of cancelable functions should catch CanceledError. + */ +export interface CancelToken { + readonly canceled: boolean; + checkCanceled: () => void; +} + +/** + * Indicates that the function was canceled by a call to the cancellation token's cancel function. + */ +export class CanceledError extends Error { + constructor() { + super('canceled'); + this.name = 'CanceledError'; + } +} + +/** + * Returns a cancel token and a cancellation function. The token can be passed to functions and checked + * to see whether it has been canceled. The function can be called to cancel the token. + */ +export function withCancel(): [CancelToken, () => void] { + let isCanceled = false; + return [ + { + get canceled() { + return isCanceled; + }, + checkCanceled() { + if (isCanceled) { + throw new CanceledError(); + } + }, + }, + () => (isCanceled = true), + ]; +}