Skip to content

Commit f6fea1e

Browse files
authored
Add support for Promises (#1049)
* Initial lualib promise class implementation * First promise tests * More promise tests * Promise class implementation * Implemented Promise.all * Promise.any * Promise.race * Promise.allSettled * fix prettier * Add promise example usage test * Added missing lualib dependencies for PromiseConstructor functions * Immediately call then/catch/finally callbacks on promises that are already resolved * Transform all references to Promise to __TS__Promise * PR feedback * Removed incorrect asyncs * Add test for direct chaining * Add test for finally and correct wrong behavior it caught * Added test throwing in parallel and chained then onFulfilleds * Fixed pull request link in ArrayIsArray lualib comment
1 parent b5c6d3a commit f6fea1e

File tree

13 files changed

+1668
-5
lines changed

13 files changed

+1668
-5
lines changed

src/LuaLib.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ export enum LuaLibFeature {
6262
OptionalMethodCall = "OptionalMethodCall",
6363
ParseFloat = "ParseFloat",
6464
ParseInt = "ParseInt",
65+
Promise = "Promise",
66+
PromiseAll = "PromiseAll",
67+
PromiseAllSettled = "PromiseAllSettled",
68+
PromiseAny = "PromiseAny",
69+
PromiseRace = "PromiseRace",
6570
Set = "Set",
6671
SetDescriptor = "SetDescriptor",
6772
WeakMap = "WeakMap",
@@ -107,16 +112,40 @@ const luaLibDependencies: Partial<Record<LuaLibFeature, LuaLibFeature[]>> = {
107112
NumberToString: [LuaLibFeature.StringAccess],
108113
ObjectDefineProperty: [LuaLibFeature.CloneDescriptor, LuaLibFeature.SetDescriptor],
109114
ObjectFromEntries: [LuaLibFeature.Iterator, LuaLibFeature.Symbol],
115+
Promise: [
116+
LuaLibFeature.ArrayPush,
117+
LuaLibFeature.Class,
118+
LuaLibFeature.FunctionBind,
119+
LuaLibFeature.InstanceOf,
120+
LuaLibFeature.New,
121+
],
122+
PromiseAll: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator],
123+
PromiseAllSettled: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator],
124+
PromiseAny: [
125+
LuaLibFeature.ArrayPush,
126+
LuaLibFeature.InstanceOf,
127+
LuaLibFeature.New,
128+
LuaLibFeature.Promise,
129+
LuaLibFeature.Iterator,
130+
],
131+
PromiseRace: [
132+
LuaLibFeature.ArrayPush,
133+
LuaLibFeature.InstanceOf,
134+
LuaLibFeature.New,
135+
LuaLibFeature.Promise,
136+
LuaLibFeature.Iterator,
137+
],
110138
ParseFloat: [LuaLibFeature.StringAccess],
111139
ParseInt: [LuaLibFeature.StringSubstr, LuaLibFeature.StringSubstring],
112140
SetDescriptor: [LuaLibFeature.CloneDescriptor],
141+
Spread: [LuaLibFeature.Iterator, LuaLibFeature.StringAccess, LuaLibFeature.Unpack],
113142
StringSplit: [LuaLibFeature.StringSubstring, LuaLibFeature.StringAccess],
114143
SymbolRegistry: [LuaLibFeature.Symbol],
144+
115145
Map: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class],
116146
Set: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class],
117147
WeakMap: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class],
118148
WeakSet: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class],
119-
Spread: [LuaLibFeature.Iterator, LuaLibFeature.StringAccess, LuaLibFeature.Unpack],
120149
};
121150
/* eslint-enable @typescript-eslint/naming-convention */
122151

src/lualib/ArrayIsArray.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ declare type NextEmptyCheck = (this: void, table: any, index: undefined) => unkn
22

33
function __TS__ArrayIsArray(this: void, value: any): value is any[] {
44
// Workaround to determine if value is an array or not (fails in case of objects without keys)
5-
// See discussion in: https://github.com/TypeScriptToLua/TypeScriptToLua/pull/7
5+
// See discussion in: https://github.com/TypeScriptToLua/TypeScriptToLua/pull/737
66
return type(value) === "table" && (1 in value || (next as NextEmptyCheck)(value, undefined) === undefined);
77
}

src/lualib/Promise.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/* eslint-disable @typescript-eslint/promise-function-async */
2+
3+
// Promises implemented based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
4+
// and https://promisesaplus.com/
5+
6+
enum __TS__PromiseState {
7+
Pending,
8+
Fulfilled,
9+
Rejected,
10+
}
11+
12+
type FulfillCallback<TData, TResult> = (value: TData) => TResult | PromiseLike<TResult>;
13+
type RejectCallback<TResult> = (reason: any) => TResult | PromiseLike<TResult>;
14+
15+
function __TS__PromiseDeferred<T>() {
16+
let resolve: FulfillCallback<T, unknown>;
17+
let reject: RejectCallback<unknown>;
18+
const promise = new Promise<T>((res, rej) => {
19+
resolve = res;
20+
reject = rej;
21+
});
22+
23+
return { promise, resolve, reject };
24+
}
25+
26+
function __TS__IsPromiseLike<T>(thing: unknown): thing is PromiseLike<T> {
27+
return thing instanceof __TS__Promise;
28+
}
29+
30+
class __TS__Promise<T> implements Promise<T> {
31+
public state = __TS__PromiseState.Pending;
32+
public value?: T;
33+
public rejectionReason?: any;
34+
35+
private fulfilledCallbacks: Array<FulfillCallback<T, unknown>> = [];
36+
private rejectedCallbacks: Array<RejectCallback<unknown>> = [];
37+
private finallyCallbacks: Array<() => void> = [];
38+
39+
public [Symbol.toStringTag]: string; // Required to implement interface, no output Lua
40+
41+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
42+
public static resolve<TData>(this: void, data: TData): Promise<TData> {
43+
// Create and return a promise instance that is already resolved
44+
const promise = new __TS__Promise<TData>(() => {});
45+
promise.state = __TS__PromiseState.Fulfilled;
46+
promise.value = data;
47+
return promise;
48+
}
49+
50+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject
51+
public static reject(this: void, reason: any): Promise<never> {
52+
// Create and return a promise instance that is already rejected
53+
const promise = new __TS__Promise<never>(() => {});
54+
promise.state = __TS__PromiseState.Rejected;
55+
promise.rejectionReason = reason;
56+
return promise;
57+
}
58+
59+
constructor(executor: (resolve: (data: T) => void, reject: (reason: any) => void) => void) {
60+
try {
61+
executor(this.resolve.bind(this), this.reject.bind(this));
62+
} catch (e) {
63+
// When a promise executor throws, the promise should be rejected with the thrown object as reason
64+
this.reject(e);
65+
}
66+
}
67+
68+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
69+
public then<TResult1 = T, TResult2 = never>(
70+
onFulfilled?: FulfillCallback<T, TResult1>,
71+
onRejected?: RejectCallback<TResult2>
72+
): Promise<TResult1 | TResult2> {
73+
const { promise, resolve, reject } = __TS__PromiseDeferred<TResult1 | TResult2>();
74+
75+
if (onFulfilled) {
76+
const internalCallback = this.createPromiseResolvingCallback(onFulfilled, resolve, reject);
77+
this.fulfilledCallbacks.push(internalCallback);
78+
79+
if (this.state === __TS__PromiseState.Fulfilled) {
80+
// If promise already resolved, immediately call callback
81+
internalCallback(this.value);
82+
}
83+
} else {
84+
// We always want to resolve our child promise if this promise is resolved, even if we have no handler
85+
this.fulfilledCallbacks.push(() => resolve(undefined));
86+
}
87+
88+
if (onRejected) {
89+
const internalCallback = this.createPromiseResolvingCallback(onRejected, resolve, reject);
90+
this.rejectedCallbacks.push(internalCallback);
91+
92+
if (this.state === __TS__PromiseState.Rejected) {
93+
// If promise already rejected, immediately call callback
94+
internalCallback(this.rejectionReason);
95+
}
96+
}
97+
98+
return promise;
99+
}
100+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
101+
public catch<TResult = never>(onRejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<T | TResult> {
102+
return this.then(undefined, onRejected);
103+
}
104+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
105+
public finally(onFinally?: () => void): Promise<T> {
106+
if (onFinally) {
107+
this.finallyCallbacks.push(onFinally);
108+
109+
if (this.state !== __TS__PromiseState.Pending) {
110+
// If promise already resolved or rejected, immediately fire finally callback
111+
onFinally();
112+
}
113+
}
114+
return this;
115+
}
116+
117+
private resolve(data: T): void {
118+
// Resolve this promise, if it is still pending. This function is passed to the constructor function.
119+
if (this.state === __TS__PromiseState.Pending) {
120+
this.state = __TS__PromiseState.Fulfilled;
121+
this.value = data;
122+
123+
for (const callback of this.fulfilledCallbacks) {
124+
callback(data);
125+
}
126+
for (const callback of this.finallyCallbacks) {
127+
callback();
128+
}
129+
}
130+
}
131+
132+
private reject(reason: any): void {
133+
// Reject this promise, if it is still pending. This function is passed to the constructor function.
134+
if (this.state === __TS__PromiseState.Pending) {
135+
this.state = __TS__PromiseState.Rejected;
136+
this.rejectionReason = reason;
137+
138+
for (const callback of this.rejectedCallbacks) {
139+
callback(reason);
140+
}
141+
for (const callback of this.finallyCallbacks) {
142+
callback();
143+
}
144+
}
145+
}
146+
147+
private createPromiseResolvingCallback<TResult1, TResult2>(
148+
f: FulfillCallback<T, TResult1> | RejectCallback<TResult2>,
149+
resolve: FulfillCallback<TResult1 | TResult2, unknown>,
150+
reject: RejectCallback<unknown>
151+
) {
152+
return value => {
153+
try {
154+
this.handleCallbackData(f(value), resolve, reject);
155+
} catch (e) {
156+
// If a handler function throws an error, the promise returned by then gets rejected with the thrown error as its value
157+
reject(e);
158+
}
159+
};
160+
}
161+
private handleCallbackData<TResult1, TResult2, TResult extends TResult1 | TResult2>(
162+
data: TResult | PromiseLike<TResult>,
163+
resolve: FulfillCallback<TResult1 | TResult2, unknown>,
164+
reject: RejectCallback<unknown>
165+
) {
166+
if (__TS__IsPromiseLike<TResult>(data)) {
167+
const nextpromise = data as __TS__Promise<TResult>;
168+
if (nextpromise.state === __TS__PromiseState.Fulfilled) {
169+
// If a handler function returns an already fulfilled promise,
170+
// the promise returned by then gets fulfilled with that promise's value
171+
resolve(nextpromise.value);
172+
} else if (nextpromise.state === __TS__PromiseState.Rejected) {
173+
// If a handler function returns an already rejected promise,
174+
// the promise returned by then gets fulfilled with that promise's value
175+
reject(nextpromise.rejectionReason);
176+
} else {
177+
// If a handler function returns another pending promise object, the resolution/rejection
178+
// of the promise returned by then will be subsequent to the resolution/rejection of
179+
// the promise returned by the handler.
180+
data.then(resolve, reject);
181+
}
182+
} else {
183+
// If a handler returns a value, the promise returned by then gets resolved with the returned value as its value
184+
// If a handler doesn't return anything, the promise returned by then gets resolved with undefined
185+
resolve(data);
186+
}
187+
}
188+
}

src/lualib/PromiseAll.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
2+
// eslint-disable-next-line @typescript-eslint/promise-function-async
3+
function __TS__PromiseAll<T>(this: void, iterable: Iterable<T | PromiseLike<T>>): Promise<T[]> {
4+
const results: T[] = [];
5+
6+
const toResolve = new LuaTable<number, PromiseLike<T>>();
7+
let numToResolve = 0;
8+
9+
let i = 0;
10+
for (const item of iterable) {
11+
if (item instanceof __TS__Promise) {
12+
if (item.state === __TS__PromiseState.Fulfilled) {
13+
// If value is a resolved promise, add its value to our results array
14+
results[i] = item.value;
15+
} else if (item.state === __TS__PromiseState.Rejected) {
16+
// If value is a rejected promise, return a rejected promise with the rejection reason
17+
return Promise.reject(item.rejectionReason);
18+
} else {
19+
// If value is a pending promise, add it to the list of pending promises
20+
numToResolve++;
21+
toResolve.set(i, item);
22+
}
23+
} else {
24+
// If value is not a promise, add it to the results array
25+
results[i] = item as T;
26+
}
27+
i++;
28+
}
29+
30+
// If there are no remaining pending promises, return a resolved promise with the results
31+
if (numToResolve === 0) {
32+
return Promise.resolve(results);
33+
}
34+
35+
return new Promise((resolve, reject) => {
36+
for (const [index, promise] of pairs(toResolve)) {
37+
promise.then(
38+
data => {
39+
// When resolved, store value in results array
40+
results[index] = data;
41+
numToResolve--;
42+
if (numToResolve === 0) {
43+
// If there are no more promises to resolve, resolve with our filled results array
44+
resolve(results);
45+
}
46+
},
47+
reason => {
48+
// When rejected, immediately reject the returned promise
49+
reject(reason);
50+
}
51+
);
52+
}
53+
});
54+
}

src/lualib/PromiseAllSettled.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
2+
// eslint-disable-next-line @typescript-eslint/promise-function-async
3+
function __TS__PromiseAllSettled<T>(
4+
this: void,
5+
iterable: Iterable<T>
6+
): Promise<Array<PromiseSettledResult<T extends PromiseLike<infer U> ? U : T>>> {
7+
const results: Array<PromiseSettledResult<T extends PromiseLike<infer U> ? U : T>> = [];
8+
9+
const toResolve = new LuaTable<number, PromiseLike<T>>();
10+
let numToResolve = 0;
11+
12+
let i = 0;
13+
for (const item of iterable) {
14+
if (item instanceof __TS__Promise) {
15+
if (item.state === __TS__PromiseState.Fulfilled) {
16+
// If value is a resolved promise, add a fulfilled PromiseSettledResult
17+
results[i] = { status: "fulfilled", value: item.value };
18+
} else if (item.state === __TS__PromiseState.Rejected) {
19+
// If value is a rejected promise, add a rejected PromiseSettledResult
20+
results[i] = { status: "rejected", reason: item.rejectionReason };
21+
} else {
22+
// If value is a pending promise, add it to the list of pending promises
23+
numToResolve++;
24+
toResolve.set(i, item);
25+
}
26+
} else {
27+
// If value is not a promise, add it to the results as fulfilled PromiseSettledResult
28+
results[i] = { status: "fulfilled", value: item as any };
29+
}
30+
i++;
31+
}
32+
33+
// If there are no remaining pending promises, return a resolved promise with the results
34+
if (numToResolve === 0) {
35+
return Promise.resolve(results);
36+
}
37+
38+
return new Promise(resolve => {
39+
for (const [index, promise] of pairs(toResolve)) {
40+
promise.then(
41+
data => {
42+
// When resolved, add a fulfilled PromiseSettledResult
43+
results[index] = { status: "fulfilled", value: data as any };
44+
numToResolve--;
45+
if (numToResolve === 0) {
46+
// If there are no more promises to resolve, resolve with our filled results array
47+
resolve(results);
48+
}
49+
},
50+
reason => {
51+
// When resolved, add a rejected PromiseSettledResult
52+
results[index] = { status: "rejected", reason };
53+
numToResolve--;
54+
if (numToResolve === 0) {
55+
// If there are no more promises to resolve, resolve with our filled results array
56+
resolve(results);
57+
}
58+
}
59+
);
60+
}
61+
});
62+
}

0 commit comments

Comments
 (0)