Last active
July 10, 2018 11:12
-
-
Save yogurt1/776b2dadbf2e67374fc7cb305053472a to your computer and use it in GitHub Desktop.
JS Promise + #abort(). PS typescript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type CancelFn = () => void; | |
type SideEffect<T> = (onCancel: (cancelFn: CancelFn) => void) => Promise<T>; | |
type Reject = (error: Error) => void; | |
class CancelError extends Error { | |
readonly cancelled = true; | |
} | |
const rejectOnce = (reject: Reject): Reject => { | |
let called = false; | |
return error => { | |
if (!called) { | |
called = true; | |
reject(error); | |
} | |
}; | |
}; | |
class JSEffect<T> implements PromiseLike<T> { | |
private _reject?: Reject; | |
private _promise: Promise<T>; | |
private _onCancel?: CancelFn; | |
private _cancelled: boolean = false; | |
constructor(sideEffect: SideEffect<T>) { | |
this._promise = new Promise<T>((resolve, reject) => { | |
this._reject = rejectOnce(reject); | |
sideEffect((cancelFn: CancelFn) => { | |
this._onCancel = cancelFn; | |
}) | |
.then(value => { | |
if (this._cancelled) { | |
this._rejectCancelled(); | |
} else { | |
resolve(value); | |
} | |
}) | |
.catch(this._reject); | |
}); | |
} | |
private _rejectCancelled() { | |
if (this._reject) { | |
this._reject(new CancelError()); | |
} | |
} | |
isCancelled(): boolean { | |
return this._cancelled = true; | |
} | |
abort() { | |
this._cancelled = true; | |
this._rejectCancelled(); | |
if (this._onCancel) { | |
this._onCancel(); | |
} | |
} | |
then(...args) { | |
return new JSEffect(onCancel => { | |
onCancel(() => this.abort()); | |
return this._promise.then.apply(this._promise, args); | |
}); | |
} | |
catch(...args) { | |
return new JSEffect(onCancel => { | |
onCancel(() => this.abort()); | |
return this._promise.catch.apply(this._promise, args); | |
}); | |
} | |
getPromise(): Promise<T> { | |
return this._promise; | |
} | |
} | |
// EXAMPLE | |
const cancellableFetch = <T = any>( | |
url: string, | |
data?: T, | |
): JSEffect<Response> => { | |
const abortController = new AbortController(); | |
return new JSEffect(onCancel => { | |
onCancel(() => abortController.abort()); | |
return fetch(url, { | |
body: JSON.stringify(data), | |
signal: abortController.signal, | |
}); | |
}); | |
}; | |
const fetchQuoteOfTheDay = () => new JSEffect(async onCancel => { | |
const fetchEff = cancellableFetch<never>('/api/randomQuote'); | |
onCancel(() => fetchEff.abort()); | |
try { | |
await new Promise(resolve => setTimeout(resolve, 5500)) | |
const response = await fetchEff; | |
const quote = response.text(); | |
return quote; | |
} catch (error) { | |
throw error; | |
} | |
}) | |
class App /* extends React.Component */ { | |
state = { | |
quote: '', | |
}; | |
private setState: any | |
private fetcher: JSEffect<any>; | |
componentDidMount() { | |
this.fetcher = fetchQuote(); | |
try { | |
const quote = await this.fetcher; | |
this.setState({ quote }); | |
} catch (error) { | |
if (this.fetcher.isCancelled()) { | |
return; | |
} | |
alert('something went wrong'); | |
} | |
} | |
componentWillUnmount() { | |
this.fetcher.abort(); | |
} | |
render() { | |
const { quote } = this.state; | |
return <h1>{quote ? `Quote of the day: ${quote}` : 'Loading...'}</h1> | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment