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
51 changes: 30 additions & 21 deletions src/lualib/Await.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,53 @@

import { __TS__Promise } from "./Promise";

const cocreate = coroutine.create;
const coresume = coroutine.resume;
const costatus = coroutine.status;
const coyield = coroutine.yield;

// Be extremely careful editing this function. A single non-tail function call may ruin chained awaits performance
// eslint-disable-next-line @typescript-eslint/promise-function-async
export function __TS__AsyncAwaiter(this: void, generator: (this: void) => void) {
return new Promise((resolve, reject) => {
let resolved = false;
const asyncCoroutine = coroutine.create(generator);
const asyncCoroutine = cocreate(generator);

// eslint-disable-next-line @typescript-eslint/promise-function-async
function adopt(value: unknown) {
return value instanceof __TS__Promise ? value : Promise.resolve(value);
}
function fulfilled(value: unknown) {
const [success, resultOrError] = coroutine.resume(asyncCoroutine, value);
function fulfilled(value: unknown): void {
const [success, resultOrError] = coresume(asyncCoroutine, value);
if (success) {
step(resultOrError);
} else {
reject(resultOrError);
// `step` never throws. Tail call return is important!
return step(resultOrError);
}
// `reject` should never throw. Tail call return is important!
return reject(resultOrError);
}
function step(result: unknown) {
if (resolved) return;
if (coroutine.status(asyncCoroutine) === "dead") {
resolve(result);
} else {
adopt(result).then(fulfilled, reject);

function step(this: void, result: unknown): void {
if (resolved) {
return;
}
if (costatus(asyncCoroutine) === "dead") {
// `resolve` never throws. Tail call return is important!
return resolve(result);
}
// We cannot use `then` because we need to avoid calling `coroutine.resume` from inside `pcall`
// `fulfilled` and `reject` should never throw. Tail call return is important!
return __TS__Promise.resolve(result).addCallbacks(fulfilled, reject);
}
const [success, resultOrError] = coroutine.resume(asyncCoroutine, (v: unknown) => {

const [success, resultOrError] = coresume(asyncCoroutine, (v: unknown) => {
resolved = true;
adopt(v).then(resolve, reject);
return __TS__Promise.resolve(v).addCallbacks(resolve, reject);
});
if (success) {
step(resultOrError);
return step(resultOrError);
} else {
reject(resultOrError);
return reject(resultOrError);
}
});
}

export function __TS__Await(this: void, thing: unknown) {
return coroutine.yield(thing);
return coyield(thing);
}
226 changes: 127 additions & 99 deletions src/lualib/Promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,111 +9,113 @@ export const enum PromiseState {
Rejected,
}

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

function promiseDeferred<T>() {
let resolve: FulfillCallback<T, unknown>;
let reject: RejectCallback<unknown>;
const promise = new Promise<T>((res, rej) => {
type PromiseExecutor<T> = ConstructorParameters<typeof Promise<T>>[0];
type PromiseResolve<T> = Parameters<PromiseExecutor<T>>[0];
type PromiseReject = Parameters<PromiseExecutor<unknown>>[1];
type PromiseResolveCallback<TValue, TResult> = (value: TValue) => TResult | PromiseLike<TResult>;
type PromiseRejectCallback<TResult> = (reason: any) => TResult | PromiseLike<TResult>;

function makeDeferredPromiseFactory(this: void) {
let resolve: PromiseResolve<any>;
let reject: PromiseReject;
const executor: PromiseExecutor<any> = (res, rej) => {
resolve = res;
reject = rej;
});

// @ts-ignore This is alright because TS doesnt understand the callback will immediately be called
return { promise, resolve, reject };
};
return function <T>(this: void) {
const promise = new Promise<T>(executor);
return $multi(promise, resolve, reject);
};
}

function isPromiseLike<T>(thing: unknown): thing is PromiseLike<T> {
return thing instanceof __TS__Promise;
const makeDeferredPromise = makeDeferredPromiseFactory();

function isPromiseLike<T>(this: void, value: unknown): value is PromiseLike<T> {
return value instanceof __TS__Promise;
}

function doNothing(): void {}

const pcall = _G.pcall;

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

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

// @ts-ignore
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> {
public static resolve<T>(this: void, value: T | PromiseLike<T>): __TS__Promise<Awaited<T>> {
if (value instanceof __TS__Promise) {
return value;
}
// Create and return a promise instance that is already resolved
const promise = new __TS__Promise<TData>(() => {});
const promise = new __TS__Promise<Awaited<T>>(doNothing);
promise.state = PromiseState.Fulfilled;
promise.value = data;
promise.value = value as Awaited<T>;
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> {
public static reject<T = never>(this: void, reason?: any): __TS__Promise<T> {
// Create and return a promise instance that is already rejected
const promise = new __TS__Promise<never>(() => {});
const promise = new __TS__Promise<T>(doNothing);
promise.state = 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) {
constructor(executor: PromiseExecutor<T>) {
// Avoid unnecessary local functions allocations by using `pcall` explicitly
const [success, error] = pcall(
executor,
undefined,
v => this.resolve(v),
err => this.reject(err)
);
if (!success) {
// When a promise executor throws, the promise should be rejected with the thrown object as reason
this.reject(e);
this.reject(error);
}
}

// 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>
onFulfilled?: PromiseResolveCallback<T, TResult1>,
onRejected?: PromiseRejectCallback<TResult2>
): Promise<TResult1 | TResult2> {
const { promise, resolve, reject } = promiseDeferred<T | TResult1 | TResult2>();

const isFulfilled = this.state === PromiseState.Fulfilled;
const isRejected = this.state === PromiseState.Rejected;

if (onFulfilled) {
const internalCallback = this.createPromiseResolvingCallback(onFulfilled, resolve, reject);
this.fulfilledCallbacks.push(internalCallback);
const [promise, resolve, reject] = makeDeferredPromise<T | TResult1 | TResult2>();

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

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

if (isRejected) {
// If promise already rejected, immediately call callback
internalCallback(this.rejectionReason);
}
} else {
onFulfilled ? this.createPromiseResolvingCallback(onFulfilled, resolve, reject) : resolve,
// We always want to reject our child promise if this promise is rejected, even if we have no handler
this.rejectedCallbacks.push(err => reject(err));
}
onRejected ? this.createPromiseResolvingCallback(onRejected, resolve, reject) : reject
);

if (isFulfilled) {
// If promise already resolved, also resolve returned promise
resolve(this.value!);
}
return promise as Promise<TResult1 | TResult2>;
}

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

return promise as Promise<TResult1 | TResult2>;
this.fulfilledCallbacks.push(fulfilledCallback as any);
this.rejectedCallbacks.push(rejectedCallback);
}

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

private resolve(data: T): void {
if (data instanceof __TS__Promise) {
data.then(
private resolve(value: T | PromiseLike<T>): void {
if (isPromiseLike(value)) {
// Tail call return is important!
return (value as __TS__Promise<T>).addCallbacks(
v => this.resolve(v),
err => this.reject(err)
);
return;
}

// Resolve this promise, if it is still pending. This function is passed to the constructor function.
if (this.state === PromiseState.Pending) {
this.state = PromiseState.Fulfilled;
this.value = data;
this.value = value;

for (const callback of this.fulfilledCallbacks) {
callback(data);
}
for (const callback of this.finallyCallbacks) {
callback();
}
// Tail call return is important!
return this.invokeCallbacks(this.fulfilledCallbacks, value);
}
}

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

for (const callback of this.rejectedCallbacks) {
callback(reason);
// Tail call return is important!
return this.invokeCallbacks(this.rejectedCallbacks, reason);
}
}

private invokeCallbacks<T>(callbacks: ReadonlyArray<(value: T) => void>, value: T): void {
const callbacksLength = callbacks.length;
const finallyCallbacks = this.finallyCallbacks;
const finallyCallbacksLength = finallyCallbacks.length;

if (callbacksLength !== 0) {
for (const i of $range(1, callbacksLength - 1)) {
callbacks[i - 1](value);
}
for (const callback of this.finallyCallbacks) {
callback();
// Tail call optimization for a common case.
if (finallyCallbacksLength === 0) {
return callbacks[callbacksLength - 1](value);
}
callbacks[callbacksLength - 1](value);
}

if (finallyCallbacksLength !== 0) {
for (const i of $range(1, finallyCallbacksLength - 1)) {
finallyCallbacks[i - 1]();
}
return finallyCallbacks[finallyCallbacksLength - 1]();
}
}

private createPromiseResolvingCallback<TResult1, TResult2>(
f: FulfillCallback<T, TResult1> | RejectCallback<TResult2>,
resolve: FulfillCallback<TResult1 | TResult2, unknown>,
reject: RejectCallback<unknown>
f: PromiseResolveCallback<T, TResult1> | PromiseRejectCallback<TResult2>,
resolve: (data: TResult1 | TResult2) => void,
reject: (reason: any) => void
) {
return (value: T) => {
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);
return (value: T): void => {
const [success, resultOrError] = pcall<
undefined,
[T],
TResult1 | PromiseLike<TResult1> | TResult2 | PromiseLike<TResult2>
>(f, undefined, value);
if (!success) {
// Tail call return is important!
return reject(resultOrError);
}
// Tail call return is important!
return this.handleCallbackValue(resultOrError, resolve, reject);
};
}

private handleCallbackData<TResult1, TResult2, TResult extends TResult1 | TResult2>(
data: TResult | PromiseLike<TResult>,
resolve: FulfillCallback<TResult1 | TResult2, unknown>,
reject: RejectCallback<unknown>
) {
if (isPromiseLike<TResult>(data)) {
const nextpromise = data as __TS__Promise<TResult>;
private handleCallbackValue<TResult1, TResult2, TResult extends TResult1 | TResult2>(
value: TResult | PromiseLike<TResult>,
resolve: (data: TResult1 | TResult2) => void,
reject: (reason: any) => void
): void {
if (isPromiseLike<TResult>(value)) {
const nextpromise = value as __TS__Promise<TResult>;
if (nextpromise.state === 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!);
// the promise returned by then gets fulfilled with that promise's value.
// Tail call return is important!
return resolve(nextpromise.value!);
} else if (nextpromise.state === 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);
// the promise returned by then gets fulfilled with that promise's value.
// Tail call return is important!
return 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);
// We cannot use `then` because we need to do tail call, and `then` returns a Promise.
// `resolve` and `reject` should never throw.
return nextpromise.addCallbacks(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);
// Tail call return is important!
return resolve(value);
}
}
}
Loading