← full-stack-fastapi-template  /  frontend/src/client/core/CancelablePromise.ts

1
export class CancelError extends Error {
2
	constructor(message: string) {
3
		super(message);
4
		this.name = 'CancelError';
5
	}
6
7
	public get isCancelled(): boolean {
8
		return true;
9
	}
10
}
11
12
export interface OnCancel {
13
	readonly isResolved: boolean;
14
	readonly isRejected: boolean;
15
	readonly isCancelled: boolean;
16
17
	(cancelHandler: () => void): void;
18
}
19
20
export class CancelablePromise<T> implements Promise<T> {
21
	private _isResolved: boolean;
22
	private _isRejected: boolean;
23
	private _isCancelled: boolean;
24
	readonly cancelHandlers: (() => void)[];
25
	readonly promise: Promise<T>;
26
	private _resolve?: (value: T | PromiseLike<T>) => void;
27
	private _reject?: (reason?: unknown) => void;
28
29
	constructor(
30
		executor: (
31
			resolve: (value: T | PromiseLike<T>) => void,
32
			reject: (reason?: unknown) => void,
33
			onCancel: OnCancel
34
		) => void
35
	) {
36
		this._isResolved = false;
37
		this._isRejected = false;
38
		this._isCancelled = false;
39
		this.cancelHandlers = [];
40
		this.promise = new Promise<T>((resolve, reject) => {
41
			this._resolve = resolve;
42
			this._reject = reject;
43
44
			const onResolve = (value: T | PromiseLike<T>): void => {
45
				if (this._isResolved || this._isRejected || this._isCancelled) {
46
					return;
47
				}
48
				this._isResolved = true;
49
				if (this._resolve) this._resolve(value);
50
			};
51
52
			const onReject = (reason?: unknown): void => {
53
				if (this._isResolved || this._isRejected || this._isCancelled) {
54
					return;
55
				}
56
				this._isRejected = true;
57
				if (this._reject) this._reject(reason);
58
			};
59
60
			const onCancel = (cancelHandler: () => void): void => {
61
				if (this._isResolved || this._isRejected || this._isCancelled) {
62
					return;
63
				}
64
				this.cancelHandlers.push(cancelHandler);
65
			};
66
67
			Object.defineProperty(onCancel, 'isResolved', {
68
				get: (): boolean => this._isResolved,
69
			});
70
71
			Object.defineProperty(onCancel, 'isRejected', {
72
				get: (): boolean => this._isRejected,
73
			});
74
75
			Object.defineProperty(onCancel, 'isCancelled', {
76
				get: (): boolean => this._isCancelled,
77
			});
78
79
			return executor(onResolve, onReject, onCancel as OnCancel);
80
		});
81
	}
82
83
	get [Symbol.toStringTag]() {
84
		return "Cancellable Promise";
85
	}
86
87
	public then<TResult1 = T, TResult2 = never>(
88
		onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
89
		onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
90
	): Promise<TResult1 | TResult2> {
91
		return this.promise.then(onFulfilled, onRejected);
92
	}
93
94
	public catch<TResult = never>(
95
		onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
96
	): Promise<T | TResult> {
97
		return this.promise.catch(onRejected);
98
	}
99
100
	public finally(onFinally?: (() => void) | null): Promise<T> {
101
		return this.promise.finally(onFinally);
102
	}
103
104
	public cancel(): void {
105
		if (this._isResolved || this._isRejected || this._isCancelled) {
106
			return;
107
		}
108
		this._isCancelled = true;
109
		if (this.cancelHandlers.length) {
110
			try {
111
				for (const cancelHandler of this.cancelHandlers) {
112
					cancelHandler();
113
				}
114
			} catch (error) {
115
				console.warn('Cancellation threw an error', error);
116
				return;
117
			}
118
		}
119
		this.cancelHandlers.length = 0;
120
		if (this._reject) this._reject(new CancelError('Request aborted'));
121
	}
122
123
	public get isCancelled(): boolean {
124
		return this._isCancelled;
125
	}
126
}