-
Notifications
You must be signed in to change notification settings - Fork 208
/
DelayedPromise.ts
136 lines (121 loc) · 5.95 KB
/
DelayedPromise.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Utils
*/
/**
* Similar to a normal Promise, a DelayedPromise represents the eventual completion (or failure)
* and resulting value of an asynchronous operation ***that has not yet started***.
*
* The asynchronous operation behind a DelayedPromise will start when any of the following occurs:
* - The DelayedPromise is `await`ed.
* - A callback is attached via `.then()` or `.catch(() => { })`.
* - The asynchronous operation is explicitly started via `.start()`
*
* Just as normal Promises will never return to their pending state once fulfilled or rejected,
* a DelayedPromise will never re-execute its asynchronous operation more than **once**.
*
* Ultimately, a DelayedPromise is nothing more than some syntactic sugar that allows you to
* represent an (asynchronously) lazily-loaded value as an instance property instead of a method.
* You could also accomplish something similar by defining an async function as a property getter.
* However, since a property defined as a DelayedPromise will not start simply by being accessed,
* additional (non-lazily-loaded) "nested" properties can be added.
*
* [!alert text="*Remember:* Unlike regular Promises in JavaScript, DelayedPromises represent processes that **may not** already be happening." kind="warning"]
* @beta
*/
export class DelayedPromise<T> implements Promise<T> {
/**
* Constructs a DelayedPromise object.
* @param startCallback The asynchronous callback to execute when this DelayedPromise should be "started".
*/
constructor(startCallback: () => Promise<T>) {
let pending: Promise<T> | undefined;
this.start = async () => {
pending = pending || startCallback();
return pending;
};
}
// We need this in order to fulfill the Promise interface defined in lib.es2015.symbol.wellknown.d.ts
public readonly [Symbol.toStringTag] = "Promise" as const;
/**
* Explicitly starts the asynchronous operation behind this DelayedPromise (if it hasn't started already).
*/
public start: () => Promise<T>;
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @return A Promise for the completion of which ever callback is executed.
*/
public async then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2> {
return this.start().then(onfulfilled, onrejected);
}
/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @return A Promise for the completion of the callback.
*/
public async catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult> {
return this.start().catch(onrejected);
}
/**
* Attaches a callback for only the finally clause of the Promise.
* @param onrejected The callback to execute when the Promise is finalized.
* @return A Promise for the completion of the callback.
*/
public async finally(onFinally?: (() => void) | undefined | null): Promise<T> {
return this.start().finally(onFinally);
}
}
// This keeps us from accidentally overriding one of DelayedPromise's methods in the DelayedPromiseWithProps constructor
/**
* @beta
*/
export interface NoDelayedPromiseMethods {
[propName: string]: any;
start?: never;
then?: never;
catch?: never;
}
// See definition of DelayedPromiseWithProps below
/**
* @beta
*/
export interface DelayedPromiseWithPropsConstructor {
/**
* Constructs a DelayedPromiseWithProps object, which is at once both:
* - A DelayedPromise object representing the eventual completion (or failure)
* of an asynchronous operation returning a value of type `TPayload`
* - _and_ a readonly "wrapper" around an instance of type `TProps`
*
* @param props An object with properties and methods that will be accessible
* as if they were readonly properties of the DelayedPromiseWithProps object being constructed.
* @param startCallback The asynchronous callback to execute when as soon as this DelayedPromise should be "started".
*/
new <TProps extends NoDelayedPromiseMethods, TPayload>(props: TProps, startCallback: () => Promise<TPayload>): Readonly<TProps> & DelayedPromise<TPayload>; // eslint-disable-line @typescript-eslint/prefer-function-type
}
// Because the property getters that wrap `props` are dynamically added, TypeScript isn't aware of them.
// So by defining this as a class _expression_, we can cast the constructed type to Readonly<TProps> & DelayedPromise<TPayload>
/**
* @beta
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export const DelayedPromiseWithProps = (class <TProps extends NoDelayedPromiseMethods, TPayload> extends DelayedPromise<TPayload> {
constructor(props: TProps, cb: () => Promise<TPayload>) {
super(cb);
const handler = {
get: (target: TProps, name: string) => {
return (name in this) ? this[name as keyof this] : target[name as keyof TProps];
},
};
return new Proxy(props, handler) as Readonly<TProps> & DelayedPromise<TPayload>;
}
}) as DelayedPromiseWithPropsConstructor;
/* eslint-disable @typescript-eslint/no-redeclare */
/** Define the type of a DelayedPromiseWithProps instance
* @beta
*/
export type DelayedPromiseWithProps<TProps, TPayload> = Readonly<TProps> & DelayedPromise<TPayload>;