/* 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/ export const enum PromiseState { Pending, Fulfilled, Rejected, } type PromiseExecutor = ConstructorParameters>[0]; type PromiseResolve = Parameters>[0]; type PromiseReject = Parameters>[1]; type PromiseResolveCallback = (value: TValue) => TResult | PromiseLike; type PromiseRejectCallback = (reason: any) => TResult | PromiseLike; function makeDeferredPromiseFactory(this: void) { let resolve: PromiseResolve; let reject: PromiseReject; const executor: PromiseExecutor = (res, rej) => { resolve = res; reject = rej; }; return function (this: void) { const promise = new Promise(executor); return $multi(promise, resolve, reject); }; } const makeDeferredPromise = makeDeferredPromiseFactory(); function isPromiseLike(this: void, value: unknown): value is PromiseLike { return value instanceof __TS__Promise; } function doNothing(): void {} const pcall = _G.pcall; export class __TS__Promise implements Promise { public state = PromiseState.Pending; public value?: T; public rejectionReason?: any; private fulfilledCallbacks: Array> = []; private rejectedCallbacks: PromiseReject[] = []; // @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(this: void, value: T | PromiseLike): __TS__Promise> { if (value instanceof __TS__Promise) { return value; } // Create and return a promise instance that is already resolved const promise = new __TS__Promise>(doNothing); promise.state = PromiseState.Fulfilled; promise.value = value as Awaited; return promise; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject public static reject(this: void, reason?: any): __TS__Promise { // Create and return a promise instance that is already rejected const promise = new __TS__Promise(doNothing); promise.state = PromiseState.Rejected; promise.rejectionReason = reason; return promise; } constructor(executor: PromiseExecutor) { // 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(error); } } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then public then( onFulfilled?: PromiseResolveCallback, onRejected?: PromiseRejectCallback ): Promise { const [promise, resolve, reject] = makeDeferredPromise(); this.addCallbacks( // We always want to resolve our child promise if this promise is resolved, even if we have no handler 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 onRejected ? this.createPromiseResolvingCallback(onRejected, resolve, reject) : reject ); return promise as Promise; } // 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); } this.fulfilledCallbacks.push(fulfilledCallback as any); this.rejectedCallbacks.push(rejectedCallback); } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch public catch(onRejected?: (reason: any) => TResult | PromiseLike): Promise { return this.then(undefined, onRejected); } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally // Delegates to .then() so that a new Promise is returned (per ES spec ยง27.2.5.3) // and the original fulfillment value / rejection reason is preserved. // reference: https://github.com/tc39/proposal-promise-finally/blob/fd934c0b42d59bf8d9446e737ba14d50a9067216/polyfill.js#L34-L41 public finally(onFinally?: () => void): Promise { if (typeof onFinally !== "function") { return this.then(onFinally, onFinally); } return this.then( x => new __TS__Promise(resolve => resolve(onFinally())).then(() => x), e => new __TS__Promise(resolve => resolve(onFinally())).then(() => { throw e; }) ); } private resolve(value: T | PromiseLike): void { if (isPromiseLike(value)) { // Tail call return is important! return (value as __TS__Promise).addCallbacks( v => this.resolve(v), err => this.reject(err) ); } // 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 = value; // Tail call return is important! return this.invokeCallbacks(this.fulfilledCallbacks, value); } } private reject(reason: any): void { // Reject this promise, if it is still pending. This function is passed to the constructor function. if (this.state === PromiseState.Pending) { this.state = PromiseState.Rejected; this.rejectionReason = reason; // Tail call return is important! return this.invokeCallbacks(this.rejectedCallbacks, reason); } } private invokeCallbacks(callbacks: ReadonlyArray<(value: T) => void>, value: T): void { const callbacksLength = callbacks.length; if (callbacksLength !== 0) { for (const i of $range(1, callbacksLength - 1)) { callbacks[i - 1](value); } // Tail call optimization for a common case. return callbacks[callbacksLength - 1](value); } } private createPromiseResolvingCallback( f: PromiseResolveCallback | PromiseRejectCallback, resolve: (data: TResult1 | TResult2) => void, reject: (reason: any) => void ) { return (value: T): void => { const [success, resultOrError] = pcall< undefined, [T], TResult1 | PromiseLike | TResult2 | PromiseLike >(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 handleCallbackValue( value: TResult | PromiseLike, resolve: (data: TResult1 | TResult2) => void, reject: (reason: any) => void ): void { if (isPromiseLike(value)) { const nextpromise = value as __TS__Promise; 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. // 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. // 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. // 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 // Tail call return is important! return resolve(value); } } }