Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion src/LuaLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export enum LuaLibFeature {
OptionalMethodCall = "OptionalMethodCall",
ParseFloat = "ParseFloat",
ParseInt = "ParseInt",
Promise = "Promise",
PromiseAll = "PromiseAll",
PromiseAllSettled = "PromiseAllSettled",
PromiseAny = "PromiseAny",
PromiseRace = "PromiseRace",
Set = "Set",
SetDescriptor = "SetDescriptor",
WeakMap = "WeakMap",
Expand Down Expand Up @@ -107,16 +112,40 @@ const luaLibDependencies: Partial<Record<LuaLibFeature, LuaLibFeature[]>> = {
NumberToString: [LuaLibFeature.StringAccess],
ObjectDefineProperty: [LuaLibFeature.CloneDescriptor, LuaLibFeature.SetDescriptor],
ObjectFromEntries: [LuaLibFeature.Iterator, LuaLibFeature.Symbol],
Promise: [
LuaLibFeature.ArrayPush,
LuaLibFeature.Class,
LuaLibFeature.FunctionBind,
LuaLibFeature.InstanceOf,
LuaLibFeature.New,
],
PromiseAll: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator],
PromiseAllSettled: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator],
PromiseAny: [
LuaLibFeature.ArrayPush,
LuaLibFeature.InstanceOf,
LuaLibFeature.New,
LuaLibFeature.Promise,
LuaLibFeature.Iterator,
],
PromiseRace: [
LuaLibFeature.ArrayPush,
LuaLibFeature.InstanceOf,
LuaLibFeature.New,
LuaLibFeature.Promise,
LuaLibFeature.Iterator,
],
ParseFloat: [LuaLibFeature.StringAccess],
ParseInt: [LuaLibFeature.StringSubstr, LuaLibFeature.StringSubstring],
SetDescriptor: [LuaLibFeature.CloneDescriptor],
Spread: [LuaLibFeature.Iterator, LuaLibFeature.StringAccess, LuaLibFeature.Unpack],
StringSplit: [LuaLibFeature.StringSubstring, LuaLibFeature.StringAccess],
SymbolRegistry: [LuaLibFeature.Symbol],

Map: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class],
Set: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class],
WeakMap: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class],
WeakSet: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class],
Spread: [LuaLibFeature.Iterator, LuaLibFeature.StringAccess, LuaLibFeature.Unpack],
};
/* eslint-enable @typescript-eslint/naming-convention */

Expand Down
2 changes: 1 addition & 1 deletion src/lualib/ArrayIsArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ declare type NextEmptyCheck = (this: void, table: any, index: undefined) => unkn

function __TS__ArrayIsArray(this: void, value: any): value is any[] {
// Workaround to determine if value is an array or not (fails in case of objects without keys)
// See discussion in: https://github.com/TypeScriptToLua/TypeScriptToLua/pull/7
// See discussion in: https://github.com/TypeScriptToLua/TypeScriptToLua/pull/737
return type(value) === "table" && (1 in value || (next as NextEmptyCheck)(value, undefined) === undefined);
}
188 changes: 188 additions & 0 deletions src/lualib/Promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/* eslint-disable @typescript-eslint/promise-function-async */

// Promises implemented based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
// and https://promisesaplus.com/

enum __TS__PromiseState {
Pending,
Fulfilled,
Rejected,
}

type FulfillCallback<TData, TResult> = (value: TData) => TResult | PromiseLike<TResult>;
type RejectCallback<TResult> = (reason: any) => TResult | PromiseLike<TResult>;

function __TS__PromiseDeferred<T>() {
let resolve: FulfillCallback<T, unknown>;
let reject: RejectCallback<unknown>;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});

return { promise, resolve, reject };
}

function __TS__IsPromiseLike<T>(thing: unknown): thing is PromiseLike<T> {
return thing instanceof __TS__Promise;
}

class __TS__Promise<T> implements Promise<T> {
public state = __TS__PromiseState.Pending;
public value?: T;
public rejectionReason?: any;

private fulfilledCallbacks: Array<FulfillCallback<T, unknown>> = [];
private rejectedCallbacks: Array<RejectCallback<unknown>> = [];
private finallyCallbacks: Array<() => void> = [];

public [Symbol.toStringTag]: string; // Required to implement interface, no output Lua

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
public static resolve<TData>(this: void, data: TData): Promise<TData> {
// Create and return a promise instance that is already resolved
const promise = new __TS__Promise<TData>(() => {});
promise.state = __TS__PromiseState.Fulfilled;
promise.value = data;
return promise;
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject
public static reject(this: void, reason: any): Promise<never> {
// Create and return a promise instance that is already rejected
const promise = new __TS__Promise<never>(() => {});
promise.state = __TS__PromiseState.Rejected;
promise.rejectionReason = reason;
return promise;
}

constructor(executor: (resolve: (data: T) => void, reject: (reason: any) => void) => void) {
try {
executor(this.resolve.bind(this), this.reject.bind(this));
} catch (e) {
// When a promise executor throws, the promise should be rejected with the thrown object as reason
this.reject(e);
}
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: FulfillCallback<T, TResult1>,
onRejected?: RejectCallback<TResult2>
): Promise<TResult1 | TResult2> {
const { promise, resolve, reject } = __TS__PromiseDeferred<TResult1 | TResult2>();

if (onFulfilled) {
const internalCallback = this.createPromiseResolvingCallback(onFulfilled, resolve, reject);
this.fulfilledCallbacks.push(internalCallback);

if (this.state === __TS__PromiseState.Fulfilled) {
// If promise already resolved, immediately call callback
internalCallback(this.value);
}
} else {
// We always want to resolve our child promise if this promise is resolved, even if we have no handler
this.fulfilledCallbacks.push(() => resolve(undefined));
}

if (onRejected) {
const internalCallback = this.createPromiseResolvingCallback(onRejected, resolve, reject);
this.rejectedCallbacks.push(internalCallback);

if (this.state === __TS__PromiseState.Rejected) {
// If promise already rejected, immediately call callback
internalCallback(this.rejectionReason);
}
}

return promise;
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
public catch<TResult = never>(onRejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<T | TResult> {
return this.then(undefined, onRejected);
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
public finally(onFinally?: () => void): Promise<T> {
if (onFinally) {
this.finallyCallbacks.push(onFinally);

if (this.state !== __TS__PromiseState.Pending) {
// If promise already resolved or rejected, immediately fire finally callback
onFinally();
}
}
return this;
}

private resolve(data: T): void {
// Resolve this promise, if it is still pending. This function is passed to the constructor function.
if (this.state === __TS__PromiseState.Pending) {
this.state = __TS__PromiseState.Fulfilled;
this.value = data;

for (const callback of this.fulfilledCallbacks) {
callback(data);
}
for (const callback of this.finallyCallbacks) {
callback();
}
}
}

private reject(reason: any): void {
// Reject this promise, if it is still pending. This function is passed to the constructor function.
if (this.state === __TS__PromiseState.Pending) {
this.state = __TS__PromiseState.Rejected;
this.rejectionReason = reason;

for (const callback of this.rejectedCallbacks) {
callback(reason);
}
for (const callback of this.finallyCallbacks) {
callback();
}
}
}

private createPromiseResolvingCallback<TResult1, TResult2>(
f: FulfillCallback<T, TResult1> | RejectCallback<TResult2>,
resolve: FulfillCallback<TResult1 | TResult2, unknown>,
reject: RejectCallback<unknown>
) {
return value => {
try {
this.handleCallbackData(f(value), resolve, reject);
} catch (e) {
// If a handler function throws an error, the promise returned by then gets rejected with the thrown error as its value
reject(e);
}
};
}
private handleCallbackData<TResult1, TResult2, TResult extends TResult1 | TResult2>(
data: TResult | PromiseLike<TResult>,
resolve: FulfillCallback<TResult1 | TResult2, unknown>,
reject: RejectCallback<unknown>
) {
if (__TS__IsPromiseLike<TResult>(data)) {
const nextpromise = data as __TS__Promise<TResult>;
if (nextpromise.state === __TS__PromiseState.Fulfilled) {
// If a handler function returns an already fulfilled promise,
// the promise returned by then gets fulfilled with that promise's value
resolve(nextpromise.value);
} else if (nextpromise.state === __TS__PromiseState.Rejected) {
// If a handler function returns an already rejected promise,
// the promise returned by then gets fulfilled with that promise's value
reject(nextpromise.rejectionReason);
} else {
// If a handler function returns another pending promise object, the resolution/rejection
// of the promise returned by then will be subsequent to the resolution/rejection of
// the promise returned by the handler.
data.then(resolve, reject);
}
} else {
// If a handler returns a value, the promise returned by then gets resolved with the returned value as its value
// If a handler doesn't return anything, the promise returned by then gets resolved with undefined
resolve(data);
}
}
}
54 changes: 54 additions & 0 deletions src/lualib/PromiseAll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
// eslint-disable-next-line @typescript-eslint/promise-function-async
function __TS__PromiseAll<T>(this: void, iterable: Iterable<T | PromiseLike<T>>): Promise<T[]> {
const results: T[] = [];

const toResolve = new LuaTable<number, PromiseLike<T>>();
let numToResolve = 0;

let i = 0;
for (const item of iterable) {
if (item instanceof __TS__Promise) {
if (item.state === __TS__PromiseState.Fulfilled) {
// If value is a resolved promise, add its value to our results array
results[i] = item.value;
} else if (item.state === __TS__PromiseState.Rejected) {
// If value is a rejected promise, return a rejected promise with the rejection reason
return Promise.reject(item.rejectionReason);
} else {
// If value is a pending promise, add it to the list of pending promises
numToResolve++;
toResolve.set(i, item);
}
} else {
// If value is not a promise, add it to the results array
results[i] = item as T;
}
i++;
}

// If there are no remaining pending promises, return a resolved promise with the results
if (numToResolve === 0) {
return Promise.resolve(results);
}

return new Promise((resolve, reject) => {
for (const [index, promise] of pairs(toResolve)) {
promise.then(
data => {
// When resolved, store value in results array
results[index] = data;
numToResolve--;
if (numToResolve === 0) {
// If there are no more promises to resolve, resolve with our filled results array
resolve(results);
}
},
reason => {
// When rejected, immediately reject the returned promise
reject(reason);
}
);
}
});
}
62 changes: 62 additions & 0 deletions src/lualib/PromiseAllSettled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
// eslint-disable-next-line @typescript-eslint/promise-function-async
function __TS__PromiseAllSettled<T>(
this: void,
iterable: Iterable<T>
): Promise<Array<PromiseSettledResult<T extends PromiseLike<infer U> ? U : T>>> {
const results: Array<PromiseSettledResult<T extends PromiseLike<infer U> ? U : T>> = [];

const toResolve = new LuaTable<number, PromiseLike<T>>();
let numToResolve = 0;

let i = 0;
for (const item of iterable) {
if (item instanceof __TS__Promise) {
if (item.state === __TS__PromiseState.Fulfilled) {
// If value is a resolved promise, add a fulfilled PromiseSettledResult
results[i] = { status: "fulfilled", value: item.value };
} else if (item.state === __TS__PromiseState.Rejected) {
// If value is a rejected promise, add a rejected PromiseSettledResult
results[i] = { status: "rejected", reason: item.rejectionReason };
} else {
// If value is a pending promise, add it to the list of pending promises
numToResolve++;
toResolve.set(i, item);
}
} else {
// If value is not a promise, add it to the results as fulfilled PromiseSettledResult
results[i] = { status: "fulfilled", value: item as any };
}
i++;
}

// If there are no remaining pending promises, return a resolved promise with the results
if (numToResolve === 0) {
return Promise.resolve(results);
}

return new Promise(resolve => {
for (const [index, promise] of pairs(toResolve)) {
promise.then(
data => {
// When resolved, add a fulfilled PromiseSettledResult
results[index] = { status: "fulfilled", value: data as any };
numToResolve--;
if (numToResolve === 0) {
// If there are no more promises to resolve, resolve with our filled results array
resolve(results);
}
},
reason => {
// When resolved, add a rejected PromiseSettledResult
results[index] = { status: "rejected", reason };
numToResolve--;
if (numToResolve === 0) {
// If there are no more promises to resolve, resolve with our filled results array
resolve(results);
}
}
);
}
});
}
Loading