Add Promise.async

This commit is contained in:
Eryn Lynn 2019-09-10 17:12:00 -04:00
parent c5fc5bff27
commit 264ff27213
4 changed files with 121 additions and 9 deletions

View file

@ -32,6 +32,7 @@ module.exports = {
],
themeConfig: {
activeHeaderLinks: false,
searchOptions: {
placeholder: 'Press S to search...'
},

View file

@ -4,22 +4,71 @@ title: Implementation Details
# Implementation Details
## Yielding in Promise executor
## Chaining
If you need to yield in the Promise executor, you must wrap your yielding code in a new thread to prevent your calling thread from yielding. The easiest way to do this is to wrap your code with the built-in `Promise.spawn`:
One of the best parts about Promises is that they are chainable.
Every time you call `andThen` or `catch`, it returns a *new* Promise, which resolves with whatever value you return from the success or failure handlers, respectively.
```lua
Promise.new(function(resolve)
somePromise:andThen(function(number)
return number + 1
end):andThen(print)
```
You can also return a Promise from your handler, and it will be chained onto:
```lua
Promise.async(function(resolve)
wait(1)
resolve(1)
end):andThen(function(x)
return Promise.async(function(resolve)
wait(1)
resolve(x + 1)
end)
end):andThen(print) --> 2
```
You can also call `:andThen` multiple times on a single Promise to have multiple branches off of a single Promise.
Resolving a Promise with a Promise will be chained as well:
```lua
Promise.async(function(resolve)
wait(1)
resolve(Promise.async(function(resolve)
wait(1)
resolve(1)
end))
end):andThen(print) --> 1
```
However, any value that is returned from the Promise executor (the function you pass into `Promise.async`) is discarded. Do not return values from the function executor.
## Yielding in Promise executor
If you need to yield in the Promise executor, you must wrap your yielding code in a new thread to prevent your calling thread from yielding. The easiest way to do this is to use the <ApiLink to="Promise.async" /> constructor instead of <ApiLink to="Promise.new" />:
```lua
Promise.async(function(resolve)
wait(1)
resolve()
end)
```
`Promise.async` uses `Promise.new` internally, except it wraps the Promise executor with <ApiLink to="Promise.spawn" />.
`Promise.async` is sugar for:
```lua
Promise.new(function(resolve, reject, onCancel)
Promise.spawn(function()
wait(1)
resolve()
-- ...
end)
end)
```
`Promise.spawn` uses a BindableEvent internally to launch your Promise body on a fresh thread after waiting for the next `RunService.Heartbeat` event.
The reason `Promise.spawn` includes this wait time is to ensure that your Promises have consistent timing. Otherwise, your Promise would run synchronously up to the first yield, and asynchronously afterwards. This can often lead to undesirable results. Additionally, Promises that never yield can resolve completely synchronously, and this can lead to unpredictable timing issues. Thus, we use `Promise.spawn` so there is always a guaranteed yield before execution.
`Promise.spawn` uses a BindableEvent internally to launch your Promise body on a fresh thread after waiting for the next `RunService.Heartbeat` event. The reason `Promise.spawn` includes this wait time is to ensure that your Promises have consistent timing. Otherwise, your Promise would run synchronously up to the first yield, and asynchronously afterwards. This can often lead to undesirable results. Additionally, Promises that never yield can resolve completely synchronously, and this can lead to unpredictable timing issues. Thus, we use `Promise.spawn` so there is always a guaranteed yield before execution.
::: danger Don't use regular spawn
`spawn` might seem like a tempting alternative to `Promise.spawn` here, but you should **never** use it!
@ -31,6 +80,11 @@ The reason `Promise.spawn` includes this wait time is to ensure that your Promis
`coroutine.wrap` is another possible stand-in for creating a BindableEvent and firing it off, but in the case of an error, the stack trace is reset when the coroutine executes. This can make troubleshooting extremely difficult because you don't know where to look in your code base for the source of the error. Creating a BindableEvent is relatively cheap, so you shouldn't need to worry about this causing performance problems in your game.
:::
### When to use `Promise.new`
In some cases, it is desirable for a Promise to execute completely synchronously. If you don't need to yield in your Promise executor, and you are aware of the timing implications of a completely synchronous Promise, then it is acceptable to use `Promise.new`.
However, in these situations, <ApiLink to="Promise.resolve" /> may be more appropriate.
## Cancellation details
If a Promise is already cancelled at the time of calling its onCancel hook, the hook will be called immediately.

View file

@ -26,8 +26,15 @@ docs:
functions:
- name: new
tags: [ 'constructor' ]
desc: |
Construct a new Promise that will be resolved or rejected with the given callbacks.
::: tip
Generally, it is recommended to use [[Promise.async]] instead. You cannot directly yield inside the `executor` function of [[Promise.new]].
:::
If you `resolve` with a Promise, it will be chained onto.
You may register an optional cancellation hook by using the `onCancel` argument.
* This should be used to abort any ongoing operations leading up to the promise being settled.
@ -60,6 +67,44 @@ docs:
- name: abortHandler
kind: function
returns: void
returns: Promise
- name: async
tags: [ 'constructor' ]
desc: |
The same as [[Promise.new]], except it implicitly uses `Promise.spawn` internally. Use this if you want to yield inside your Promise body.
::: tip
Promises created with [[Promise.async]] are guaranteed to yield for at least one frame, even if the executor function doesn't yield itself. <a href="/roblox-lua-promise/lib/Details.html#yielding-in-promise-executor">Learn more</a>
:::
static: true
params:
- name: asyncExecutor
type:
kind: function
params:
- name: resolve
type:
kind: function
params:
- name: "..."
type: ...any?
returns: void
- name: reject
type:
kind: function
params:
- name: "..."
type: ...any?
returns: void
- name: onCancel
type:
kind: function
params:
- name: abortHandler
kind: function
returns: void
returns: Promise
- name: resolve
desc: Creates an immediately resolved Promise with the given value.
static: true
@ -106,7 +151,10 @@ docs:
type: "...any?"
- name: andThen
desc: Chains onto an existing Promise and returns a new Promise.
desc: |
Chains onto an existing Promise and returns a new Promise.
Return a Promise from the success or failure handler and it will be chained onto.
params:
- name: successHandler
type:

View file

@ -189,6 +189,15 @@ function Promise.new(callback, parent)
return self
end
--[[
Promise.new, except Promise.spawn is implicit.
]]
function Promise.async(callback)
return Promise.new(function(...)
return Promise.spawn(callback, ...)
end)
end
--[[
Spawns a thread with predictable timing.
]]