-
-
Notifications
You must be signed in to change notification settings - Fork 184
Add support for Promises #1049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Add support for Promises #1049
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
ac74c29
Initial lualib promise class implementation
Perryvw 7ca3030
First promise tests
Perryvw d388154
More promise tests
Perryvw 46b82c1
Promise class implementation
Perryvw f0b325d
Implemented Promise.all
Perryvw d2db835
Promise.any
Perryvw 7f662e6
Promise.race
Perryvw c0bbb42
Promise.allSettled
Perryvw fd8108f
Merge branch 'master' into promises
Perryvw 8f800b9
fix prettier
Perryvw 47f179e
Add promise example usage test
Perryvw 5931c3a
Added missing lualib dependencies for PromiseConstructor functions
Perryvw 95c5fe3
Immediately call then/catch/finally callbacks on promises that are al…
Perryvw cc74c3a
Transform all references to Promise to __TS__Promise
Perryvw 3fad722
PR feedback
Perryvw 40efc17
Merge branch 'master' into promises
Perryvw 20660f0
Removed incorrect asyncs
Perryvw 1b74429
Add test for direct chaining
Perryvw 780abba
Add test for finally and correct wrong behavior it caught
Perryvw c0e82ef
Added test throwing in parallel and chained then onFulfilleds
Perryvw cca6949
Fixed pull request link in ArrayIsArray lualib comment
Perryvw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
lolleko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
lolleko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
lolleko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) { | ||
lolleko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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); | ||
| } | ||
| ); | ||
| } | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| }); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.