Skip to content

Instantly share code, notes, and snippets.

@yogurt1
Last active July 10, 2018 11:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yogurt1/776b2dadbf2e67374fc7cb305053472a to your computer and use it in GitHub Desktop.
Save yogurt1/776b2dadbf2e67374fc7cb305053472a to your computer and use it in GitHub Desktop.
JS Promise + #abort(). PS typescript
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