Skip to content

Commit 223b49e

Browse files
authored
Optimize Promise and Await (fixes #1499), fix Promise.resolve bug (#1530)
* fix promise * remove unneeded adopt
1 parent 1cd16ba commit 223b49e

File tree

4 files changed

+202
-120
lines changed

4 files changed

+202
-120
lines changed

src/lualib/Await.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,53 @@
1616

1717
import { __TS__Promise } from "./Promise";
1818

19+
const cocreate = coroutine.create;
20+
const coresume = coroutine.resume;
21+
const costatus = coroutine.status;
22+
const coyield = coroutine.yield;
23+
24+
// Be extremely careful editing this function. A single non-tail function call may ruin chained awaits performance
1925
// eslint-disable-next-line @typescript-eslint/promise-function-async
2026
export function __TS__AsyncAwaiter(this: void, generator: (this: void) => void) {
2127
return new Promise((resolve, reject) => {
2228
let resolved = false;
23-
const asyncCoroutine = coroutine.create(generator);
29+
const asyncCoroutine = cocreate(generator);
2430

25-
// eslint-disable-next-line @typescript-eslint/promise-function-async
26-
function adopt(value: unknown) {
27-
return value instanceof __TS__Promise ? value : Promise.resolve(value);
28-
}
29-
function fulfilled(value: unknown) {
30-
const [success, resultOrError] = coroutine.resume(asyncCoroutine, value);
31+
function fulfilled(value: unknown): void {
32+
const [success, resultOrError] = coresume(asyncCoroutine, value);
3133
if (success) {
32-
step(resultOrError);
33-
} else {
34-
reject(resultOrError);
34+
// `step` never throws. Tail call return is important!
35+
return step(resultOrError);
3536
}
37+
// `reject` should never throw. Tail call return is important!
38+
return reject(resultOrError);
3639
}
37-
function step(result: unknown) {
38-
if (resolved) return;
39-
if (coroutine.status(asyncCoroutine) === "dead") {
40-
resolve(result);
41-
} else {
42-
adopt(result).then(fulfilled, reject);
40+
41+
function step(this: void, result: unknown): void {
42+
if (resolved) {
43+
return;
4344
}
45+
if (costatus(asyncCoroutine) === "dead") {
46+
// `resolve` never throws. Tail call return is important!
47+
return resolve(result);
48+
}
49+
// We cannot use `then` because we need to avoid calling `coroutine.resume` from inside `pcall`
50+
// `fulfilled` and `reject` should never throw. Tail call return is important!
51+
return __TS__Promise.resolve(result).addCallbacks(fulfilled, reject);
4452
}
45-
const [success, resultOrError] = coroutine.resume(asyncCoroutine, (v: unknown) => {
53+
54+
const [success, resultOrError] = coresume(asyncCoroutine, (v: unknown) => {
4655
resolved = true;
47-
adopt(v).then(resolve, reject);
56+
return __TS__Promise.resolve(v).addCallbacks(resolve, reject);
4857
});
4958
if (success) {
50-
step(resultOrError);
59+
return step(resultOrError);
5160
} else {
52-
reject(resultOrError);
61+
return reject(resultOrError);
5362
}
5463
});
5564
}
5665

5766
export function __TS__Await(this: void, thing: unknown) {
58-
return coroutine.yield(thing);
67+
return coyield(thing);
5968
}

src/lualib/Promise.ts

Lines changed: 127 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -9,111 +9,113 @@ export const enum PromiseState {
99
Rejected,
1010
}
1111

12-
type FulfillCallback<TData, TResult> = (value: TData) => TResult | PromiseLike<TResult>;
13-
type RejectCallback<TResult> = (reason: any) => TResult | PromiseLike<TResult>;
14-
15-
function promiseDeferred<T>() {
16-
let resolve: FulfillCallback<T, unknown>;
17-
let reject: RejectCallback<unknown>;
18-
const promise = new Promise<T>((res, rej) => {
12+
type PromiseExecutor<T> = ConstructorParameters<typeof Promise<T>>[0];
13+
type PromiseResolve<T> = Parameters<PromiseExecutor<T>>[0];
14+
type PromiseReject = Parameters<PromiseExecutor<unknown>>[1];
15+
type PromiseResolveCallback<TValue, TResult> = (value: TValue) => TResult | PromiseLike<TResult>;
16+
type PromiseRejectCallback<TResult> = (reason: any) => TResult | PromiseLike<TResult>;
17+
18+
function makeDeferredPromiseFactory(this: void) {
19+
let resolve: PromiseResolve<any>;
20+
let reject: PromiseReject;
21+
const executor: PromiseExecutor<any> = (res, rej) => {
1922
resolve = res;
2023
reject = rej;
21-
});
22-
23-
// @ts-ignore This is alright because TS doesnt understand the callback will immediately be called
24-
return { promise, resolve, reject };
24+
};
25+
return function <T>(this: void) {
26+
const promise = new Promise<T>(executor);
27+
return $multi(promise, resolve, reject);
28+
};
2529
}
2630

27-
function isPromiseLike<T>(thing: unknown): thing is PromiseLike<T> {
28-
return thing instanceof __TS__Promise;
31+
const makeDeferredPromise = makeDeferredPromiseFactory();
32+
33+
function isPromiseLike<T>(this: void, value: unknown): value is PromiseLike<T> {
34+
return value instanceof __TS__Promise;
2935
}
3036

37+
function doNothing(): void {}
38+
39+
const pcall = _G.pcall;
40+
3141
export class __TS__Promise<T> implements Promise<T> {
3242
public state = PromiseState.Pending;
3343
public value?: T;
3444
public rejectionReason?: any;
3545

36-
private fulfilledCallbacks: Array<FulfillCallback<T, unknown>> = [];
37-
private rejectedCallbacks: Array<RejectCallback<unknown>> = [];
46+
private fulfilledCallbacks: Array<PromiseResolve<T>> = [];
47+
private rejectedCallbacks: PromiseReject[] = [];
3848
private finallyCallbacks: Array<() => void> = [];
3949

4050
// @ts-ignore
4151
public [Symbol.toStringTag]: string; // Required to implement interface, no output Lua
4252

4353
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
44-
public static resolve<TData>(this: void, data: TData): Promise<TData> {
54+
public static resolve<T>(this: void, value: T | PromiseLike<T>): __TS__Promise<Awaited<T>> {
55+
if (value instanceof __TS__Promise) {
56+
return value;
57+
}
4558
// Create and return a promise instance that is already resolved
46-
const promise = new __TS__Promise<TData>(() => {});
59+
const promise = new __TS__Promise<Awaited<T>>(doNothing);
4760
promise.state = PromiseState.Fulfilled;
48-
promise.value = data;
61+
promise.value = value as Awaited<T>;
4962
return promise;
5063
}
5164

5265
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject
53-
public static reject(this: void, reason: any): Promise<never> {
66+
public static reject<T = never>(this: void, reason?: any): __TS__Promise<T> {
5467
// Create and return a promise instance that is already rejected
55-
const promise = new __TS__Promise<never>(() => {});
68+
const promise = new __TS__Promise<T>(doNothing);
5669
promise.state = PromiseState.Rejected;
5770
promise.rejectionReason = reason;
5871
return promise;
5972
}
6073

61-
constructor(executor: (resolve: (data: T) => void, reject: (reason: any) => void) => void) {
62-
try {
63-
executor(this.resolve.bind(this), this.reject.bind(this));
64-
} catch (e) {
74+
constructor(executor: PromiseExecutor<T>) {
75+
// Avoid unnecessary local functions allocations by using `pcall` explicitly
76+
const [success, error] = pcall(
77+
executor,
78+
undefined,
79+
v => this.resolve(v),
80+
err => this.reject(err)
81+
);
82+
if (!success) {
6583
// When a promise executor throws, the promise should be rejected with the thrown object as reason
66-
this.reject(e);
84+
this.reject(error);
6785
}
6886
}
6987

7088
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
7189
public then<TResult1 = T, TResult2 = never>(
72-
onFulfilled?: FulfillCallback<T, TResult1>,
73-
onRejected?: RejectCallback<TResult2>
90+
onFulfilled?: PromiseResolveCallback<T, TResult1>,
91+
onRejected?: PromiseRejectCallback<TResult2>
7492
): Promise<TResult1 | TResult2> {
75-
const { promise, resolve, reject } = promiseDeferred<T | TResult1 | TResult2>();
76-
77-
const isFulfilled = this.state === PromiseState.Fulfilled;
78-
const isRejected = this.state === PromiseState.Rejected;
79-
80-
if (onFulfilled) {
81-
const internalCallback = this.createPromiseResolvingCallback(onFulfilled, resolve, reject);
82-
this.fulfilledCallbacks.push(internalCallback);
93+
const [promise, resolve, reject] = makeDeferredPromise<T | TResult1 | TResult2>();
8394

84-
if (isFulfilled) {
85-
// If promise already resolved, immediately call callback
86-
internalCallback(this.value!);
87-
}
88-
} else {
95+
this.addCallbacks(
8996
// We always want to resolve our child promise if this promise is resolved, even if we have no handler
90-
this.fulfilledCallbacks.push(v => resolve(v));
91-
}
92-
93-
if (onRejected) {
94-
const internalCallback = this.createPromiseResolvingCallback(onRejected, resolve, reject);
95-
this.rejectedCallbacks.push(internalCallback);
96-
97-
if (isRejected) {
98-
// If promise already rejected, immediately call callback
99-
internalCallback(this.rejectionReason);
100-
}
101-
} else {
97+
onFulfilled ? this.createPromiseResolvingCallback(onFulfilled, resolve, reject) : resolve,
10298
// We always want to reject our child promise if this promise is rejected, even if we have no handler
103-
this.rejectedCallbacks.push(err => reject(err));
104-
}
99+
onRejected ? this.createPromiseResolvingCallback(onRejected, resolve, reject) : reject
100+
);
105101

106-
if (isFulfilled) {
107-
// If promise already resolved, also resolve returned promise
108-
resolve(this.value!);
109-
}
102+
return promise as Promise<TResult1 | TResult2>;
103+
}
110104

111-
if (isRejected) {
112-
// If promise already rejected, also reject returned promise
113-
reject(this.rejectionReason);
105+
// Both callbacks should never throw!
106+
public addCallbacks(fulfilledCallback: (value: T) => void, rejectedCallback: (rejectionReason: any) => void): void {
107+
if (this.state === PromiseState.Fulfilled) {
108+
// If promise already resolved, immediately call callback. We don't even need to store rejected callback
109+
// Tail call return is important!
110+
return fulfilledCallback(this.value!);
111+
}
112+
if (this.state === PromiseState.Rejected) {
113+
// Similar thing
114+
return rejectedCallback(this.rejectionReason);
114115
}
115116

116-
return promise as Promise<TResult1 | TResult2>;
117+
this.fulfilledCallbacks.push(fulfilledCallback as any);
118+
this.rejectedCallbacks.push(rejectedCallback);
117119
}
118120

119121
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
@@ -134,26 +136,22 @@ export class __TS__Promise<T> implements Promise<T> {
134136
return this;
135137
}
136138

137-
private resolve(data: T): void {
138-
if (data instanceof __TS__Promise) {
139-
data.then(
139+
private resolve(value: T | PromiseLike<T>): void {
140+
if (isPromiseLike(value)) {
141+
// Tail call return is important!
142+
return (value as __TS__Promise<T>).addCallbacks(
140143
v => this.resolve(v),
141144
err => this.reject(err)
142145
);
143-
return;
144146
}
145147

146148
// Resolve this promise, if it is still pending. This function is passed to the constructor function.
147149
if (this.state === PromiseState.Pending) {
148150
this.state = PromiseState.Fulfilled;
149-
this.value = data;
151+
this.value = value;
150152

151-
for (const callback of this.fulfilledCallbacks) {
152-
callback(data);
153-
}
154-
for (const callback of this.finallyCallbacks) {
155-
callback();
156-
}
153+
// Tail call return is important!
154+
return this.invokeCallbacks(this.fulfilledCallbacks, value);
157155
}
158156
}
159157

@@ -163,55 +161,85 @@ export class __TS__Promise<T> implements Promise<T> {
163161
this.state = PromiseState.Rejected;
164162
this.rejectionReason = reason;
165163

166-
for (const callback of this.rejectedCallbacks) {
167-
callback(reason);
164+
// Tail call return is important!
165+
return this.invokeCallbacks(this.rejectedCallbacks, reason);
166+
}
167+
}
168+
169+
private invokeCallbacks<T>(callbacks: ReadonlyArray<(value: T) => void>, value: T): void {
170+
const callbacksLength = callbacks.length;
171+
const finallyCallbacks = this.finallyCallbacks;
172+
const finallyCallbacksLength = finallyCallbacks.length;
173+
174+
if (callbacksLength !== 0) {
175+
for (const i of $range(1, callbacksLength - 1)) {
176+
callbacks[i - 1](value);
168177
}
169-
for (const callback of this.finallyCallbacks) {
170-
callback();
178+
// Tail call optimization for a common case.
179+
if (finallyCallbacksLength === 0) {
180+
return callbacks[callbacksLength - 1](value);
171181
}
182+
callbacks[callbacksLength - 1](value);
183+
}
184+
185+
if (finallyCallbacksLength !== 0) {
186+
for (const i of $range(1, finallyCallbacksLength - 1)) {
187+
finallyCallbacks[i - 1]();
188+
}
189+
return finallyCallbacks[finallyCallbacksLength - 1]();
172190
}
173191
}
174192

175193
private createPromiseResolvingCallback<TResult1, TResult2>(
176-
f: FulfillCallback<T, TResult1> | RejectCallback<TResult2>,
177-
resolve: FulfillCallback<TResult1 | TResult2, unknown>,
178-
reject: RejectCallback<unknown>
194+
f: PromiseResolveCallback<T, TResult1> | PromiseRejectCallback<TResult2>,
195+
resolve: (data: TResult1 | TResult2) => void,
196+
reject: (reason: any) => void
179197
) {
180-
return (value: T) => {
181-
try {
182-
this.handleCallbackData(f(value), resolve, reject);
183-
} catch (e) {
184-
// If a handler function throws an error, the promise returned by then gets rejected with the thrown error as its value
185-
reject(e);
198+
return (value: T): void => {
199+
const [success, resultOrError] = pcall<
200+
undefined,
201+
[T],
202+
TResult1 | PromiseLike<TResult1> | TResult2 | PromiseLike<TResult2>
203+
>(f, undefined, value);
204+
if (!success) {
205+
// Tail call return is important!
206+
return reject(resultOrError);
186207
}
208+
// Tail call return is important!
209+
return this.handleCallbackValue(resultOrError, resolve, reject);
187210
};
188211
}
189212

190-
private handleCallbackData<TResult1, TResult2, TResult extends TResult1 | TResult2>(
191-
data: TResult | PromiseLike<TResult>,
192-
resolve: FulfillCallback<TResult1 | TResult2, unknown>,
193-
reject: RejectCallback<unknown>
194-
) {
195-
if (isPromiseLike<TResult>(data)) {
196-
const nextpromise = data as __TS__Promise<TResult>;
213+
private handleCallbackValue<TResult1, TResult2, TResult extends TResult1 | TResult2>(
214+
value: TResult | PromiseLike<TResult>,
215+
resolve: (data: TResult1 | TResult2) => void,
216+
reject: (reason: any) => void
217+
): void {
218+
if (isPromiseLike<TResult>(value)) {
219+
const nextpromise = value as __TS__Promise<TResult>;
197220
if (nextpromise.state === PromiseState.Fulfilled) {
198221
// If a handler function returns an already fulfilled promise,
199-
// the promise returned by then gets fulfilled with that promise's value
200-
resolve(nextpromise.value!);
222+
// the promise returned by then gets fulfilled with that promise's value.
223+
// Tail call return is important!
224+
return resolve(nextpromise.value!);
201225
} else if (nextpromise.state === PromiseState.Rejected) {
202226
// If a handler function returns an already rejected promise,
203-
// the promise returned by then gets fulfilled with that promise's value
204-
reject(nextpromise.rejectionReason);
227+
// the promise returned by then gets fulfilled with that promise's value.
228+
// Tail call return is important!
229+
return reject(nextpromise.rejectionReason);
205230
} else {
206231
// If a handler function returns another pending promise object, the resolution/rejection
207232
// of the promise returned by then will be subsequent to the resolution/rejection of
208233
// the promise returned by the handler.
209-
data.then(resolve, reject);
234+
// We cannot use `then` because we need to do tail call, and `then` returns a Promise.
235+
// `resolve` and `reject` should never throw.
236+
return nextpromise.addCallbacks(resolve, reject);
210237
}
211238
} else {
212239
// If a handler returns a value, the promise returned by then gets resolved with the returned value as its value
213240
// If a handler doesn't return anything, the promise returned by then gets resolved with undefined
214-
resolve(data);
241+
// Tail call return is important!
242+
return resolve(value);
215243
}
216244
}
217245
}

0 commit comments

Comments
 (0)