mirror of
https://github.com/AmberGraceRblx/luau-promise.git
synced 2025-06-19 21:59:17 +00:00
Merge pull request #24 from evaera/use-xpcall
v3 (xpcall & better debugging)
This commit is contained in:
commit
061c3a8659
18 changed files with 1567 additions and 347 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,6 +1,3 @@
|
||||||
[submodule "modules/testez"]
|
[submodule "modules/testez"]
|
||||||
path = modules/testez
|
path = modules/testez
|
||||||
url = https://github.com/Roblox/testez.git
|
url = https://github.com/Roblox/testez.git
|
||||||
[submodule "modules/lemur"]
|
|
||||||
path = modules/lemur
|
|
||||||
url = https://github.com/LPGhatguy/lemur.git
|
|
||||||
|
|
9
.luacov
9
.luacov
|
@ -1,9 +0,0 @@
|
||||||
return {
|
|
||||||
include = {
|
|
||||||
"^lib",
|
|
||||||
},
|
|
||||||
exclude = {
|
|
||||||
"%.spec$",
|
|
||||||
"_spec$",
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -41,9 +41,11 @@ module.exports = {
|
||||||
|
|
||||||
sidebarDepth: 3,
|
sidebarDepth: 3,
|
||||||
sidebar: [
|
sidebar: [
|
||||||
'/lib/',
|
'/lib/WhyUsePromises',
|
||||||
'/lib/Usage',
|
'/lib/Tour',
|
||||||
'/lib/Examples'
|
'/lib/Examples',
|
||||||
|
'/CHANGELOG',
|
||||||
|
'/lib/'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
44
CHANGELOG.md
44
CHANGELOG.md
|
@ -1,12 +1,36 @@
|
||||||
# Next
|
# Changelog
|
||||||
|
|
||||||
- Change Promise.is to be safe when dealing with tables that have an `__index` metamethod that creates an error.
|
## [3.0.0] - 2020-06-02
|
||||||
|
### Changed
|
||||||
|
- Runtime errors are now represented by objects. You must call tostring on rejection values before assuming they are strings (this was always good practice, but is required now).
|
||||||
|
- Yielding is now allowed in `Promise.new`, `andThen`, and `Promise.try` executors.
|
||||||
|
- Errors now have much better stack traces due to using `xpcall` internally instead of `pcall`.
|
||||||
|
- Stack traces will now be more direct and not include as many internal calls within the Promise library.
|
||||||
|
- Chained promises from `resolve()` or returning from andThen now have improved rejection messages for debugging.
|
||||||
|
- `Promise.async` has been renamed to `Promise.defer` (`Promise.async` references same function for compatibility)
|
||||||
|
- Promises now have a `__tostring` metamethod, which returns `Promise(Resolved)` or whatever the current status is.
|
||||||
|
- `Promise:timeout()` now rejects with a `Promise.Error(Promise.Error.Kind.TimedOut)` object. (Formerly rejected with the string `"Timed out"`)
|
||||||
|
- Attaching a handler to a cancelled Promise now rejects with a `Promise.Error(Promise.Error.Kind.AlreadyCancelled)`. (Formerly rejected with the string `"Promise is cancelled"`)
|
||||||
|
- Let `Promise:expect()` throw rejection objects
|
||||||
|
|
||||||
# 2.5.1
|
### Added
|
||||||
|
|
||||||
|
- New Promise Error class is exposed at `Promise.Error`, which includes helpful static methods like `Promise.Error.is`.
|
||||||
|
- Added `Promise:now()` (#23)
|
||||||
|
- Added `Promise.each` (#21)
|
||||||
|
- Added `Promise.retry` (#16)
|
||||||
|
- Added `Promise.fromEvent` (#14)
|
||||||
|
- Improved test coverage for asynchronous and time-driven functions
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Changed `Promise.is` to be safe when dealing with tables that have an `__index` metamethod that creates an error.
|
||||||
|
- `Promise.delay` resolve value (time passed) is now more accurate (previously passed time based on when we started resuming threads instead of the current time. This is a very minor difference.)
|
||||||
|
|
||||||
|
## [2.5.1]
|
||||||
|
|
||||||
- Fix issue with rejecting with non-string not propagating correctly.
|
- Fix issue with rejecting with non-string not propagating correctly.
|
||||||
|
|
||||||
# 2.5.0
|
## [2.5.0]
|
||||||
|
|
||||||
- Add Promise.tap
|
- Add Promise.tap
|
||||||
- Fix bug with C functions not working when passed to andThen
|
- Fix bug with C functions not working when passed to andThen
|
||||||
|
@ -23,31 +47,31 @@
|
||||||
- Add `Promise.allSettled`
|
- Add `Promise.allSettled`
|
||||||
- `Promise.all` and `Promise.race` are now cancellable.
|
- `Promise.all` and `Promise.race` are now cancellable.
|
||||||
|
|
||||||
# 2.4.0
|
## [2.4.0]
|
||||||
|
|
||||||
- `Promise.is` now only checks if the object is "andThennable" (has an `andThen` method).
|
- `Promise.is` now only checks if the object is "andThennable" (has an `andThen` method).
|
||||||
|
|
||||||
# 2.3.1
|
## [2.3.1]
|
||||||
|
|
||||||
- Make unhandled rejection warning trigger on next Heartbeat
|
- Make unhandled rejection warning trigger on next Heartbeat
|
||||||
|
|
||||||
# 2.3.0
|
## [2.3.0]
|
||||||
|
|
||||||
- Remove `Promise.spawn` from the public API.
|
- Remove `Promise.spawn` from the public API.
|
||||||
- `Promise.async` still inherits the behavior from `Promise.spawn`.
|
- `Promise.async` still inherits the behavior from `Promise.spawn`.
|
||||||
- `Promise.async` now wraps the callback in `pcall` and rejects if an error occurred.
|
- `Promise.async` now wraps the callback in `pcall` and rejects if an error occurred.
|
||||||
- `Promise.new` has now has an explicit error message when attempting to yield inside of it.
|
- `Promise.new` has now has an explicit error message when attempting to yield inside of it.
|
||||||
|
|
||||||
# 2.2.0
|
## [2.2.0]
|
||||||
|
|
||||||
- `Promise.promisify` now uses `coroutine.wrap` instead of `Promise.spawn`
|
- `Promise.promisify` now uses `coroutine.wrap` instead of `Promise.spawn`
|
||||||
|
|
||||||
# 2.1.0
|
## [2.1.0]
|
||||||
|
|
||||||
- Add `finallyCall`, `andThenCall`
|
- Add `finallyCall`, `andThenCall`
|
||||||
- Add `awaitValue`
|
- Add `awaitValue`
|
||||||
|
|
||||||
# 2.0.0
|
## [2.0.0]
|
||||||
|
|
||||||
- Add Promise.race
|
- Add Promise.race
|
||||||
- Add Promise.async
|
- Add Promise.async
|
||||||
|
|
|
@ -7,6 +7,18 @@
|
||||||
"Lib": {
|
"Lib": {
|
||||||
"$path": "lib"
|
"$path": "lib"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"TestService": {
|
||||||
|
"$className": "TestService",
|
||||||
|
"$properties": {
|
||||||
|
"ExecuteWithStudioRun": true
|
||||||
|
},
|
||||||
|
"TestEZ": {
|
||||||
|
"$path": "modules/testez/src"
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"$path": "runTests.server.lua"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -22,7 +22,7 @@ This function demonstrates how to convert a function that yields into a function
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local function isPlayerInGroup(player, groupId)
|
local function isPlayerInGroup(player, groupId)
|
||||||
return Promise.async(function(resolve)
|
return Promise.new(function(resolve)
|
||||||
resolve(player:IsInGroup(groupId))
|
resolve(player:IsInGroup(groupId))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
313
lib/README.md
313
lib/README.md
|
@ -4,7 +4,7 @@ docs:
|
||||||
desc: A Promise is an object that represents a value that will exist in the future, but doesn't right now. Promises allow you to then attach callbacks that can run once the value becomes available (known as *resolving*), or if an error has occurred (known as *rejecting*).
|
desc: A Promise is an object that represents a value that will exist in the future, but doesn't right now. Promises allow you to then attach callbacks that can run once the value becomes available (known as *resolving*), or if an error has occurred (known as *rejecting*).
|
||||||
|
|
||||||
types:
|
types:
|
||||||
- name: PromiseStatus
|
- name: Status
|
||||||
desc: An enum value used to represent the Promise's status.
|
desc: An enum value used to represent the Promise's status.
|
||||||
kind: enum
|
kind: enum
|
||||||
type:
|
type:
|
||||||
|
@ -20,23 +20,33 @@ docs:
|
||||||
properties:
|
properties:
|
||||||
- name: Status
|
- name: Status
|
||||||
tags: [ 'read only', 'static', 'enums' ]
|
tags: [ 'read only', 'static', 'enums' ]
|
||||||
type: PromiseStatus
|
type: Status
|
||||||
desc: A table containing all members of the `PromiseStatus` enum, e.g., `Promise.Status.Resolved`.
|
desc: A table containing all members of the `Status` enum, e.g., `Promise.Status.Resolved`.
|
||||||
|
|
||||||
|
|
||||||
functions:
|
functions:
|
||||||
- name: new
|
- name: new
|
||||||
tags: [ 'constructor' ]
|
|
||||||
desc: |
|
desc: |
|
||||||
Construct a new Promise that will be resolved or rejected with the given callbacks.
|
Construct a new Promise that will be resolved or rejected with the given callbacks.
|
||||||
|
|
||||||
::: tip
|
|
||||||
If your Promise executor needs to yield, 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.
|
If you `resolve` with a Promise, it will be chained onto.
|
||||||
|
|
||||||
|
You can safely yield within the executor function and it will not block the creating thread.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local myFunction()
|
||||||
|
return Promise.new(function(resolve, reject, onCancel)
|
||||||
|
wait(1)
|
||||||
|
resolve("Hello world!")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
myFunction():andThen(print)
|
||||||
|
```
|
||||||
|
|
||||||
|
Errors that occur during execution will be caught and turned into a rejection automatically. If `error()` is called with a table, that table will be the rejection value. Otherwise, string errors will be converted into `Promise.Error(Promise.Error.Kind.ExecutionError)` objects for tracking debug information.
|
||||||
|
|
||||||
You may register an optional cancellation hook by using the `onCancel` argument.
|
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.
|
* This should be used to abort any ongoing operations leading up to the promise being settled.
|
||||||
* Call the `onCancel` function with a function callback as its only argument to set a hook which will in turn be called when/if the promise is cancelled.
|
* Call the `onCancel` function with a function callback as its only argument to set a hook which will in turn be called when/if the promise is cancelled.
|
||||||
* `onCancel` returns `true` if the Promise was already cancelled when you called `onCancel`.
|
* `onCancel` returns `true` if the Promise was already cancelled when you called `onCancel`.
|
||||||
|
@ -73,20 +83,16 @@ docs:
|
||||||
- type: boolean
|
- type: boolean
|
||||||
desc: "Returns `true` if the Promise was already cancelled at the time of calling `onCancel`."
|
desc: "Returns `true` if the Promise was already cancelled at the time of calling `onCancel`."
|
||||||
returns: Promise
|
returns: Promise
|
||||||
- name: async
|
- name: defer
|
||||||
tags: [ 'constructor' ]
|
since: 3.0.0
|
||||||
desc: |
|
desc: |
|
||||||
The same as [[Promise.new]], except it allows yielding. Use this if you want to yield inside your Promise body.
|
The same as [[Promise.new]], except execution begins after the next `Heartbeat` event.
|
||||||
|
|
||||||
If your Promise body does not need to yield, such as when attaching `resolve` to an event listener, you should use [[Promise.new]] instead.
|
This is a spiritual replacement for `spawn`, but it does not suffer from the same [issues](https://eryn.io/gist/3db84579866c099cdd5bb2ff37947cec) as `spawn`.
|
||||||
|
|
||||||
::: tip
|
|
||||||
Promises created with [[Promise.async]] don't begin executing until the next `RunService.Heartbeat` event, even if the executor function doesn't yield itself. This is to ensure that Promises produced from a function are either always synchronous or always asynchronous. <a href="/roblox-lua-promise/lib/Details.html#yielding-in-promise-executor">Learn more</a>
|
|
||||||
:::
|
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local function waitForChild(instance, childName, timeout)
|
local function waitForChild(instance, childName, timeout)
|
||||||
return Promise.async(function(resolve, reject)
|
return Promise.defer(function(resolve, reject)
|
||||||
local child = instance:WaitForChild(childName, timeout)
|
local child = instance:WaitForChild(childName, timeout)
|
||||||
|
|
||||||
;(child and resolve or reject)(child)
|
;(child and resolve or reject)(child)
|
||||||
|
@ -96,7 +102,7 @@ docs:
|
||||||
|
|
||||||
static: true
|
static: true
|
||||||
params:
|
params:
|
||||||
- name: asyncExecutor
|
- name: deferExecutor
|
||||||
type:
|
type:
|
||||||
kind: function
|
kind: function
|
||||||
params:
|
params:
|
||||||
|
@ -125,12 +131,49 @@ docs:
|
||||||
desc: "Returns `true` if the Promise was already cancelled at the time of calling `onCancel`."
|
desc: "Returns `true` if the Promise was already cancelled at the time of calling `onCancel`."
|
||||||
returns: Promise
|
returns: Promise
|
||||||
|
|
||||||
|
- name: try
|
||||||
|
desc: |
|
||||||
|
Begins a Promise chain, calling a function and returning a Promise resolving with its return value. If the function errors, the returned Promise will be rejected with the error. You can safely yield within the Promise.try callback.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
`Promise.try` is similar to [[Promise.promisify]], except the callback is invoked immediately instead of returning a new function.
|
||||||
|
:::
|
||||||
|
|
||||||
|
```lua
|
||||||
|
Promise.try(function()
|
||||||
|
return math.random(1, 2) == 1 and "ok" or error("Oh an error!")
|
||||||
|
end)
|
||||||
|
:andThen(function(text)
|
||||||
|
print(text)
|
||||||
|
end)
|
||||||
|
:catch(function(err)
|
||||||
|
warn("Something went wrong")
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
static: true
|
||||||
|
params:
|
||||||
|
- name: callback
|
||||||
|
type:
|
||||||
|
kind: function
|
||||||
|
params: "...: ...any?"
|
||||||
|
returns: "...any?"
|
||||||
|
- name: "..."
|
||||||
|
type: "...any?"
|
||||||
|
desc: Arguments for the callback
|
||||||
|
returns:
|
||||||
|
- type: "Promise<...any?>"
|
||||||
|
desc: The return value of the passed callback.
|
||||||
|
|
||||||
- name: promisify
|
- name: promisify
|
||||||
desc: |
|
desc: |
|
||||||
Wraps a function that yields into one that returns a Promise.
|
Wraps a function that yields into one that returns a Promise.
|
||||||
|
|
||||||
Any errors that occur while executing the function will be turned into rejections.
|
Any errors that occur while executing the function will be turned into rejections.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
`Promise.promisify` is similar to [[Promise.try]], except the callback is returned as a callable function instead of being invoked immediately.
|
||||||
|
:::
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local sleep = Promise.promisify(wait)
|
local sleep = Promise.promisify(wait)
|
||||||
|
|
||||||
|
@ -166,46 +209,22 @@ docs:
|
||||||
params: "value: ...any"
|
params: "value: ...any"
|
||||||
returns: Promise<...any>
|
returns: Promise<...any>
|
||||||
- name: reject
|
- name: reject
|
||||||
desc: Creates an immediately rejected Promise with the given value.
|
desc: |
|
||||||
|
Creates an immediately rejected Promise with the given value.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
Someone needs to consume this rejection (i.e. `:catch()` it), otherwise it will emit an unhandled Promise rejection warning on the next frame. Thus, you should not create and store rejected Promises for later use. Only create them on-demand as needed.
|
||||||
|
:::
|
||||||
static: true
|
static: true
|
||||||
params: "value: ...any"
|
params: "value: ...any"
|
||||||
returns: Promise<...any>
|
returns: Promise<...any>
|
||||||
- name: try
|
|
||||||
desc: |
|
|
||||||
Begins a Promise chain, calling a synchronous function and returning a Promise resolving with its return value. If the function errors, the returned Promise will be rejected with the error.
|
|
||||||
|
|
||||||
`Promise.try` is similar to [[Promise.promisify]], except the callback is invoked immediately instead of returning a new function, and unlike `promisify`, yielding is not allowed with `try`.
|
|
||||||
|
|
||||||
```lua
|
|
||||||
Promise.try(function()
|
|
||||||
return math.random(1, 2) == 1 and "ok" or error("Oh an error!")
|
|
||||||
end)
|
|
||||||
:andThen(function(text)
|
|
||||||
print(text)
|
|
||||||
end)
|
|
||||||
:catch(function(err)
|
|
||||||
warn("Something went wrong")
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
static: true
|
|
||||||
params:
|
|
||||||
- name: callback
|
|
||||||
type:
|
|
||||||
kind: function
|
|
||||||
params: "...: ...any?"
|
|
||||||
returns: "...any?"
|
|
||||||
- name: "..."
|
|
||||||
type: "...any?"
|
|
||||||
desc: Arguments for the callback
|
|
||||||
returns:
|
|
||||||
- type: "Promise<...any?>"
|
|
||||||
desc: The return value of the passed callback.
|
|
||||||
|
|
||||||
- name: all
|
- name: all
|
||||||
desc: |
|
desc: |
|
||||||
Accepts an array of Promises and returns a new promise that:
|
Accepts an array of Promises and returns a new promise that:
|
||||||
* is resolved after all input promises resolve.
|
* is resolved after all input promises resolve.
|
||||||
* is rejected if ANY input promises reject.
|
* is rejected if *any* input promises reject.
|
||||||
|
|
||||||
Note: Only the first return value from each promise will be present in the resulting array.
|
Note: Only the first return value from each promise will be present in the resulting array.
|
||||||
|
|
||||||
After any input Promise rejects, all other input Promises that are still pending will be cancelled if they have no other consumers.
|
After any input Promise rejects, all other input Promises that are still pending will be cancelled if they have no other consumers.
|
||||||
|
@ -215,15 +234,21 @@ docs:
|
||||||
|
|
||||||
- name: allSettled
|
- name: allSettled
|
||||||
desc: |
|
desc: |
|
||||||
Accepts an array of Promises and returns a new Promise that resolves with an array of in-place PromiseStatuses when all input Promises have settled. This is equivalent to mapping `promise:finally` over the array of Promises.
|
Accepts an array of Promises and returns a new Promise that resolves with an array of in-place Statuses when all input Promises have settled. This is equivalent to mapping `promise:finally` over the array of Promises.
|
||||||
static: true
|
static: true
|
||||||
params: "promises: array<Promise<T>>"
|
params: "promises: array<Promise<T>>"
|
||||||
returns: Promise<array<PromiseStatus>>
|
returns: Promise<array<Status>>
|
||||||
|
|
||||||
- name: race
|
- name: race
|
||||||
desc: |
|
desc: |
|
||||||
Accepts an array of Promises and returns a new promise that is resolved or rejected as soon as any Promise in the array resolves or rejects.
|
Accepts an array of Promises and returns a new promise that is resolved or rejected as soon as any Promise in the array resolves or rejects.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
If the first Promise to settle from the array settles with a rejection, the resulting Promise from `race` will reject.
|
||||||
|
|
||||||
|
If you instead want to tolerate rejections, and only care about at least one Promise resolving, you should use [[Promise.any]] or [[Promise.some]] instead.
|
||||||
|
:::
|
||||||
|
|
||||||
All other Promises that don't win the race will be cancelled if they have no other consumers.
|
All other Promises that don't win the race will be cancelled if they have no other consumers.
|
||||||
static: true
|
static: true
|
||||||
params: "promises: array<Promise<T>>"
|
params: "promises: array<Promise<T>>"
|
||||||
|
@ -260,8 +285,137 @@ docs:
|
||||||
params: "seconds: number"
|
params: "seconds: number"
|
||||||
returns: Promise<number>
|
returns: Promise<number>
|
||||||
static: true
|
static: true
|
||||||
|
|
||||||
|
- name: each
|
||||||
|
since: 3.0.0
|
||||||
|
desc: |
|
||||||
|
Iterates serially over the given an array of values, calling the predicate callback on each value before continuing.
|
||||||
|
|
||||||
|
If the predicate returns a Promise, we wait for that Promise to resolve before moving on to the next item
|
||||||
|
in the array.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
`Promise.each` is similar to `Promise.all`, except the Promises are ran in order instead of all at once.
|
||||||
|
|
||||||
|
But because Promises are eager, by the time they are created, they're already running. Thus, we need a way to defer creation of each Promise until a later time.
|
||||||
|
|
||||||
|
The predicate function exists as a way for us to operate on our data instead of creating a new closure for each Promise. If you would prefer, you can pass in an array of functions, and in the predicate, call the function and return its return value.
|
||||||
|
:::
|
||||||
|
|
||||||
|
```lua
|
||||||
|
Promise.each({
|
||||||
|
"foo",
|
||||||
|
"bar",
|
||||||
|
"baz",
|
||||||
|
"qux"
|
||||||
|
}, function(value, index)
|
||||||
|
return Promise.delay(1):andThen(function()
|
||||||
|
print(("%d) Got %s!"):format(index, value))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
--[[
|
||||||
|
(1 second passes)
|
||||||
|
> 1) Got foo!
|
||||||
|
(1 second passes)
|
||||||
|
> 2) Got bar!
|
||||||
|
(1 second passes)
|
||||||
|
> 3) Got baz!
|
||||||
|
(1 second passes)
|
||||||
|
> 4) Got qux!
|
||||||
|
]]
|
||||||
|
```
|
||||||
|
|
||||||
|
If the Promise a predicate returns rejects, the Promise from `Promise.each` is also rejected with the same value.
|
||||||
|
|
||||||
|
If the array of values contains a Promise, when we get to that point in the list, we wait for the Promise to resolve before calling the predicate with the value.
|
||||||
|
|
||||||
|
If a Promise in the array of values is already Rejected when `Promise.each` is called, `Promise.each` rejects with that value immediately (the predicate callback will never be called even once). If a Promise in the list is already Cancelled when `Promise.each` is called, `Promise.each` rejects with `Promise.Error(Promise.Error.Kind.AlreadyCancelled`). If a Promise in the array of values is Started at first, but later rejects, `Promise.each` will reject with that value and iteration will not continue once iteration encounters that value.
|
||||||
|
|
||||||
|
Returns a Promise containing an array of the returned/resolved values from the predicate for each item in the array of values.
|
||||||
|
|
||||||
|
If this Promise returned from `Promise.each` rejects or is cancelled for any reason, the following are true:
|
||||||
|
- Iteration will not continue.
|
||||||
|
- Any Promises within the array of values will now be cancelled if they have no other consumers.
|
||||||
|
- The Promise returned from the currently active predicate will be cancelled if it hasn't resolved yet.
|
||||||
|
params:
|
||||||
|
- name: list
|
||||||
|
type: "array<T | Promise<T>>"
|
||||||
|
- name: predicate
|
||||||
|
desc: The callback to call for each value in the list.
|
||||||
|
type:
|
||||||
|
kind: function
|
||||||
|
params: "value: T, index: number"
|
||||||
|
returns: U | Promise<U>
|
||||||
|
returns: Promise<array<U>>
|
||||||
|
static: true
|
||||||
|
|
||||||
|
- name: retry
|
||||||
|
since: 3.0.0
|
||||||
|
desc: |
|
||||||
|
Repeatedly calls a Promise-returning function up to `times` number of times, until the returned Promise resolves.
|
||||||
|
|
||||||
|
If the amount of retries is exceeded, the function will return the latest rejected Promise.
|
||||||
|
params:
|
||||||
|
- name: callback
|
||||||
|
type:
|
||||||
|
kind: function
|
||||||
|
params: "...: P"
|
||||||
|
returns: Promise<T>
|
||||||
|
- name: times
|
||||||
|
type: number
|
||||||
|
- name: "..."
|
||||||
|
type: "P"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
returns: Promise<T>
|
||||||
|
static: true
|
||||||
|
|
||||||
|
- name: fromEvent
|
||||||
|
since: 3.0.0
|
||||||
|
desc: |
|
||||||
|
Converts an event into a Promise which resolves the next time the event fires.
|
||||||
|
|
||||||
|
The optional `predicate` callback, if passed, will receive the event arguments and should return `true` or `false`, based on if this fired event should resolve the Promise or not. If `true`, the Promise resolves. If `false`, nothing happens and the predicate will be rerun the next time the event fires.
|
||||||
|
|
||||||
|
The Promise will resolve with the event arguments.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
This function will work given any object with a `Connect` method. This includes all Roblox events.
|
||||||
|
:::
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Creates a Promise which only resolves when `somePart` is touched by a part named `"Something specific"`.
|
||||||
|
return Promise.fromEvent(somePart.Touched, function(part)
|
||||||
|
return part.Name == "Something specific"
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
params:
|
||||||
|
- name: event
|
||||||
|
type:
|
||||||
|
kind: interface
|
||||||
|
type:
|
||||||
|
Connect:
|
||||||
|
type:
|
||||||
|
kind: function
|
||||||
|
params:
|
||||||
|
- name: callback
|
||||||
|
type:
|
||||||
|
kind: function
|
||||||
|
params: "...: P"
|
||||||
|
desc: Any object with a `Connect` method. This includes all Roblox events.
|
||||||
|
- name: predicate
|
||||||
|
optional: true
|
||||||
|
type:
|
||||||
|
kind: function
|
||||||
|
params: "...: P"
|
||||||
|
returns: boolean
|
||||||
|
desc: A function which determines if the Promise should resolve with the given value, or wait for the next event to check again.
|
||||||
|
returns: Promise<P>
|
||||||
|
static: true
|
||||||
|
|
||||||
- name: is
|
- name: is
|
||||||
desc: Returns whether the given object is a Promise. This only checks if the object is a table and has an `andThen` method.
|
desc: Checks whether the given object is a Promise via duck typing. This only checks if the object is a table and has an `andThen` method.
|
||||||
static: true
|
static: true
|
||||||
params: "object: any"
|
params: "object: any"
|
||||||
returns:
|
returns:
|
||||||
|
@ -273,6 +427,10 @@ docs:
|
||||||
desc: |
|
desc: |
|
||||||
Chains onto an existing Promise and returns a new Promise.
|
Chains onto an existing Promise and returns a new Promise.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Within the failure handler, you should never assume that the rejection value is a string. Some rejections within the Promise library are represented by [[Error]] objects. If you want to treat it as a string for debugging, you should call `tostring` on it first.
|
||||||
|
:::
|
||||||
|
|
||||||
Return a Promise from the success or failure handler and it will be chained onto.
|
Return a Promise from the success or failure handler and it will be chained onto.
|
||||||
params:
|
params:
|
||||||
- name: successHandler
|
- name: successHandler
|
||||||
|
@ -303,7 +461,12 @@ docs:
|
||||||
returns: Promise<T>
|
returns: Promise<T>
|
||||||
|
|
||||||
- name: catch
|
- name: catch
|
||||||
desc: Shorthand for `Promise:andThen(nil, failureHandler)`.
|
desc: |
|
||||||
|
Shorthand for `Promise:andThen(nil, failureHandler)`.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
Within the failure handler, you should never assume that the rejection value is a string. Some rejections within the Promise library are represented by [[Error]] objects. If you want to treat it as a string for debugging, you should call `tostring` on it first.
|
||||||
|
:::
|
||||||
params:
|
params:
|
||||||
- name: failureHandler
|
- name: failureHandler
|
||||||
type:
|
type:
|
||||||
|
@ -346,11 +509,15 @@ docs:
|
||||||
Set a handler that will be called regardless of the promise's fate. The handler is called when the promise is resolved, rejected, *or* cancelled.
|
Set a handler that will be called regardless of the promise's fate. The handler is called when the promise is resolved, rejected, *or* cancelled.
|
||||||
|
|
||||||
Returns a new promise chained from this promise.
|
Returns a new promise chained from this promise.
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
If the Promise is cancelled, any Promises chained off of it with `andThen` won't run. Only Promises chained with `finally` or `done` will run in the case of cancellation.
|
||||||
|
:::
|
||||||
params:
|
params:
|
||||||
- name: finallyHandler
|
- name: finallyHandler
|
||||||
type:
|
type:
|
||||||
kind: function
|
kind: function
|
||||||
params: "status: PromiseStatus"
|
params: "status: Status"
|
||||||
returns: ...any?
|
returns: ...any?
|
||||||
returns: Promise<...any?>
|
returns: Promise<...any?>
|
||||||
overloads:
|
overloads:
|
||||||
|
@ -358,7 +525,7 @@ docs:
|
||||||
- name: finallyHandler
|
- name: finallyHandler
|
||||||
type:
|
type:
|
||||||
kind: function
|
kind: function
|
||||||
params: "status: PromiseStatus"
|
params: "status: Status"
|
||||||
returns: Promise<T>
|
returns: Promise<T>
|
||||||
returns: Promise<T>
|
returns: Promise<T>
|
||||||
|
|
||||||
|
@ -379,7 +546,7 @@ docs:
|
||||||
- name: doneHandler
|
- name: doneHandler
|
||||||
type:
|
type:
|
||||||
kind: function
|
kind: function
|
||||||
params: "status: PromiseStatus"
|
params: "status: Status"
|
||||||
returns: ...any?
|
returns: ...any?
|
||||||
returns: Promise<...any?>
|
returns: Promise<...any?>
|
||||||
overloads:
|
overloads:
|
||||||
|
@ -387,7 +554,7 @@ docs:
|
||||||
- name: doneHandler
|
- name: doneHandler
|
||||||
type:
|
type:
|
||||||
kind: function
|
kind: function
|
||||||
params: "status: PromiseStatus"
|
params: "status: Status"
|
||||||
returns: Promise<T>
|
returns: Promise<T>
|
||||||
returns: Promise<T>
|
returns: Promise<T>
|
||||||
|
|
||||||
|
@ -518,16 +685,18 @@ docs:
|
||||||
returns: Promise<...any?>
|
returns: Promise<...any?>
|
||||||
|
|
||||||
- name: timeout
|
- name: timeout
|
||||||
params: "seconds: number, rejectionValue: any?"
|
params: "seconds: number, rejectionValue: T?"
|
||||||
desc: |
|
desc: |
|
||||||
Returns a new Promise that resolves if the chained Promise resolves within `seconds` seconds, or rejects if execution time exceeds `seconds`. The chained Promise will be cancelled if the timeout is reached.
|
Returns a new Promise that resolves if the chained Promise resolves within `seconds` seconds, or rejects if execution time exceeds `seconds`. The chained Promise will be cancelled if the timeout is reached.
|
||||||
|
|
||||||
|
Rejects with `rejectionValue` if it is non-nil. If a `rejectionValue` is not given, it will reject with a `Promise.Error(Promise.Error.Kind.TimedOut)`. This can be checked with [[Error.isKind]].
|
||||||
|
|
||||||
Sugar for:
|
Sugar for:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
Promise.race({
|
Promise.race({
|
||||||
Promise.delay(seconds):andThen(function()
|
Promise.delay(seconds):andThen(function()
|
||||||
return Promise.reject(rejectionValue == nil and "Timed out" or rejectionValue)
|
return Promise.reject(rejectionValue == nil and Promise.Error.new({ kind = Promise.Error.Kind.TimedOut }) or rejectionValue)
|
||||||
end),
|
end),
|
||||||
promise
|
promise
|
||||||
})
|
})
|
||||||
|
@ -542,6 +711,22 @@ docs:
|
||||||
|
|
||||||
Promises will only be cancelled if all of their consumers are also cancelled. This is to say that if you call `andThen` twice on the same promise, and you cancel only one of the child promises, it will not cancel the parent promise until the other child promise is also cancelled.
|
Promises will only be cancelled if all of their consumers are also cancelled. This is to say that if you call `andThen` twice on the same promise, and you cancel only one of the child promises, it will not cancel the parent promise until the other child promise is also cancelled.
|
||||||
|
|
||||||
|
- name: now
|
||||||
|
desc: |
|
||||||
|
Chains a Promise from this one that is resolved if this Promise is already resolved, and rejected if it is not resolved at the time of calling `:now()`. This can be used to ensure your `andThen` handler occurs on the same frame as the root Promise execution.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
doSomething()
|
||||||
|
:now()
|
||||||
|
:andThen(function(value)
|
||||||
|
print("Got", value, "synchronously.")
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
If this Promise is still running, Rejected, or Cancelled, the Promise returned from `:now()` will reject with the `rejectionValue` if passed, otherwise with a `Promise.Error(Promise.Error.Kind.NotResolvedInTime)`. This can be checked with [[Error.isKind]].
|
||||||
|
params: "rejectionValue: T?"
|
||||||
|
returns: Promise<T>
|
||||||
|
|
||||||
- name: await
|
- name: await
|
||||||
tags: [ 'yields' ]
|
tags: [ 'yields' ]
|
||||||
desc: |
|
desc: |
|
||||||
|
@ -560,7 +745,7 @@ docs:
|
||||||
tags: [ 'yields' ]
|
tags: [ 'yields' ]
|
||||||
desc: Yields the current thread until the given Promise completes. Returns the Promise's status, followed by the values that the promise resolved or rejected with.
|
desc: Yields the current thread until the given Promise completes. Returns the Promise's status, followed by the values that the promise resolved or rejected with.
|
||||||
returns:
|
returns:
|
||||||
- type: PromiseStatus
|
- type: Status
|
||||||
desc: The Promise's status.
|
desc: The Promise's status.
|
||||||
- type: ...any?
|
- type: ...any?
|
||||||
desc: The values that the Promise resolved or rejected with.
|
desc: The values that the Promise resolved or rejected with.
|
||||||
|
@ -582,7 +767,7 @@ docs:
|
||||||
|
|
||||||
- name: getStatus
|
- name: getStatus
|
||||||
desc: Returns the current Promise status.
|
desc: Returns the current Promise status.
|
||||||
returns: PromiseStatus
|
returns: Status
|
||||||
---
|
---
|
||||||
|
|
||||||
<ApiDocs />
|
<ApiDocs />
|
|
@ -1,17 +1,19 @@
|
||||||
---
|
---
|
||||||
title: Usage Guide
|
title: Tour of Promises
|
||||||
---
|
---
|
||||||
|
|
||||||
# Usage Guide
|
# Tour of Promises
|
||||||
|
|
||||||
|
Here's quick introduction to Promises. For more complete information, check out the [API Reference](/lib).
|
||||||
|
|
||||||
## Creating a Promise
|
## Creating a Promise
|
||||||
|
|
||||||
There are a few ways to create a Promise. If you need to call functions that yield, you should use <ApiLink to="Promise.async" />:
|
There are a few ways to create a Promise. The most common way is to call <ApiLink to="Promise.new" />:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local myFunction()
|
local myFunction()
|
||||||
return Promise.async(function(resolve, reject, onCancel)
|
return Promise.new(function(resolve, reject, onCancel)
|
||||||
wait(1)
|
somethingThatYields()
|
||||||
resolve("Hello world!")
|
resolve("Hello world!")
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -19,7 +21,11 @@ end
|
||||||
myFunction():andThen(print)
|
myFunction():andThen(print)
|
||||||
```
|
```
|
||||||
|
|
||||||
If you don't need to yield, you can use regular <ApiLink to="Promise.new" />:
|
Another example which resolves a Promise after the first time an event fires:
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
There's actually a built-in function called <ApiLink to="Promise.fromEvent" /> that does exactly this!
|
||||||
|
:::
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local myFunction()
|
local myFunction()
|
||||||
|
@ -50,7 +56,7 @@ end
|
||||||
myFunction():andThen(print)
|
myFunction():andThen(print)
|
||||||
```
|
```
|
||||||
|
|
||||||
If you already have a function that yields, and you want it to return a Promise instead, you can use <ApiLink to="Promise.promisify" />:
|
If you already have a function that yields, and you want it to return a Promise instead, you can use <ApiLink to="Promise.promisify" /> or <ApiLink to="Promise.try" />:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local function myYieldingFunction(waitTime, text)
|
local function myYieldingFunction(waitTime, text)
|
||||||
|
@ -64,7 +70,7 @@ myFunction(1.2, "Hello world!"):andThen(print):catch(function()
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rejection and errors
|
## Rejection and Errors
|
||||||
|
|
||||||
You must observe the result of a Promise, either with `catch` or `finally`, otherwise an unhandled Promise rejection warning will be printed to the console.
|
You must observe the result of a Promise, either with `catch` or `finally`, otherwise an unhandled Promise rejection warning will be printed to the console.
|
||||||
|
|
||||||
|
@ -85,12 +91,12 @@ end):andThen(print)
|
||||||
You can also return a Promise from your handler, and it will be chained onto:
|
You can also return a Promise from your handler, and it will be chained onto:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
Promise.async(function(resolve)
|
Promise.new(function(resolve)
|
||||||
wait(1)
|
somethingThatYields()
|
||||||
resolve(1)
|
resolve(1)
|
||||||
end):andThen(function(x)
|
end):andThen(function(x)
|
||||||
return Promise.async(function(resolve)
|
return Promise.new(function(resolve)
|
||||||
wait(1)
|
somethingThatYields()
|
||||||
resolve(x + 1)
|
resolve(x + 1)
|
||||||
end)
|
end)
|
||||||
end):andThen(print) --> 2
|
end):andThen(print) --> 2
|
||||||
|
@ -100,46 +106,48 @@ You can also call `:andThen` multiple times on a single Promise to have multiple
|
||||||
|
|
||||||
Resolving a Promise with a Promise will be chained as well:
|
Resolving a Promise with a Promise will be chained as well:
|
||||||
```lua
|
```lua
|
||||||
Promise.async(function(resolve)
|
Promise.new(function(resolve)
|
||||||
wait(1)
|
somethingThatYields()
|
||||||
resolve(Promise.async(function(resolve)
|
resolve(Promise.new(function(resolve)
|
||||||
wait(1)
|
somethingThatYields()
|
||||||
resolve(1)
|
resolve(1)
|
||||||
end))
|
end))
|
||||||
end):andThen(print) --> 1
|
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.
|
However, any value that is returned from the Promise executor (the function you pass into `Promise.new`) is discarded. Do not return values from the function executor.
|
||||||
|
|
||||||
## Yielding in Promise executor
|
## A Better Alternative to `spawn`, `wait`, and `delay`
|
||||||
|
|
||||||
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" />:
|
Using `spawn`, `wait`, or `delay` alongside asynchronous code can be tempting, but you should **never** use them!
|
||||||
|
|
||||||
|
`spawn`, `wait`, and `delay` do not resume threads at a consistent interval. If Roblox has resumed too many threads in a single Lua step, it will begin throttling and your thread that was meant to be resumed on the next frame could actually be resumed several seconds later. The unexpected delay caused by this behavior will cause cascading timing issues in your game and could lead to some potentially ugly bugs.
|
||||||
|
|
||||||
|
You should use <ApiLink to="Promise.delay" /> instead, which has an accurate custom scheduler.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
Promise.async(function(resolve)
|
Promise.delay(5):andThen(function()
|
||||||
wait(1)
|
print("5 seconds have passed!")
|
||||||
resolve()
|
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
`Promise.async` uses `Promise.new` internally, except it allows yielding while `Promise.new` does not.
|
For quickly launching a new thread (similar to `spawn`), you can use <ApiLink to="Promise.try" />:
|
||||||
|
|
||||||
`Promise.async` attaches a one-time listener to the next `RunService.Heartbeat` event to fire off the rest of your Promise executor, ensuring it always waits at least one step.
|
```lua
|
||||||
|
Promise.try(function()
|
||||||
|
somethingThatYields()
|
||||||
|
end)
|
||||||
|
-- Doesn't block this
|
||||||
|
someCode()
|
||||||
|
```
|
||||||
|
|
||||||
The reason `Promise.async` 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, Promise executors that only sometimes yield can lead to unexpected timing issues. Thus, we use `Promise.async` so there is always a guaranteed yield before execution.
|
As a convenience, <ApiLink to="Promise.timeout" /> exists, which will return a rejected Promise if the Promise you call it on doesn't resolve within the given amount of seconds:
|
||||||
|
|
||||||
::: danger Don't use regular spawn
|
```lua
|
||||||
Using `spawn` inside `Promise.new` might seem like a tempting alternative to `Promise.async` here, but you should **never** use it!
|
returnsAPromise():timeout(5):andThen(function()
|
||||||
|
print("This returned in at most 5 seconds")
|
||||||
`spawn` (and `wait`, for that matter) do not resume threads at a consistent interval. If Roblox has resumed too many threads in a single Lua step, it will begin throttling and your thread that was meant to be resumed on the next frame could actually be resumed several seconds later. The unexpected delay caused by this behavior will cause cascading timing issues in your game and could lead to some potentially ugly bugs.
|
end)
|
||||||
:::
|
```
|
||||||
|
|
||||||
### 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, then you should use `Promise.new`.
|
|
||||||
|
|
||||||
For example, an example of a situation where it might be appropriate to use Promise.new is when resolving after an event is fired.
|
|
||||||
|
|
||||||
However, in some situations, <ApiLink to="Promise.resolve" /> may be more appropriate.
|
|
||||||
|
|
||||||
## Cancellation
|
## Cancellation
|
||||||
Promises are cancellable, but abort semantics are optional. This means that you can cancel any Promise and it will never resolve or reject, even if the function is still working in the background. But you can optionally add a cancellation hook which allows you to abort ongoing operations with the third `onCancel` parameter given to your Promise executor.
|
Promises are cancellable, but abort semantics are optional. This means that you can cancel any Promise and it will never resolve or reject, even if the function is still working in the background. But you can optionally add a cancellation hook which allows you to abort ongoing operations with the third `onCancel` parameter given to your Promise executor.
|
||||||
|
@ -152,17 +160,17 @@ It's good practice to add an `onCancel` hook to all of your asynchronous Promise
|
||||||
Even if you don't plan to directly cancel a particular Promise, chaining with other Promises can cause it to become automatically cancelled if no one cares about the value anymore.
|
Even if you don't plan to directly cancel a particular Promise, chaining with other Promises can cause it to become automatically cancelled if no one cares about the value anymore.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
If you attach a `:andThen` or `:catch` handler to a Promise after it's been cancelled, the chained Promise will be instantly rejected with the error "Promise is cancelled". This also applies to Promises that you pass to `resolve`. However, `finally` does not have this constraint.
|
If you attach a `:andThen` or `:catch` handler to a Promise after it's been cancelled, the chained Promise will be instantly rejected with `Promise.Error(Promise.Error.Kind.AlreadyCancelled)`. This also applies to Promises that you pass to `resolve`. However, `finally` does not have this constraint.
|
||||||
|
|
||||||
::: warning
|
::: warning
|
||||||
If you cancel a Promise immediately after creating it without yielding in between, the fate of the Promise is dependent on if the Promise handler yields or not. If the Promise handler resolves without yielding, then the Promise will already be settled by the time you are able to cancel it, thus any consumers of the Promise will have already been called and cancellation is not possible.
|
If you cancel a Promise immediately after creating it without yielding in between, the fate of the Promise is dependent on if the Promise handler yields or not. If the Promise handler resolves without yielding, then the Promise will already be settled by the time you are able to cancel it, thus any consumers of the Promise will have already been called and cancellation is not possible.
|
||||||
|
|
||||||
If the Promise does yield, then cancelling it immediately *will* prevent its resolution. This is always the case when using `Promise.async`.
|
If the Promise does yield, then cancelling it immediately *will* prevent its resolution.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Attempting to cancel an already-settled Promise is ignored.
|
Attempting to cancel an already-settled Promise is ignored.
|
||||||
|
|
||||||
### Cancellation propagation
|
### Cancellation Propagation
|
||||||
When you cancel a Promise, the cancellation propagates up and down the Promise chain. Promises keep a list of other Promises that consume them (e.g. `andThen`).
|
When you cancel a Promise, the cancellation propagates up and down the Promise chain. Promises keep a list of other Promises that consume them (e.g. `andThen`).
|
||||||
|
|
||||||
When the upwards propagation encounters a Promise that no longer has any consumers, that Promise is cancelled as well. Note that it's impossible to cancel an already-settled Promise, so upwards propagation will stop when it reaches a settled Promise.
|
When the upwards propagation encounters a Promise that no longer has any consumers, that Promise is cancelled as well. Note that it's impossible to cancel an already-settled Promise, so upwards propagation will stop when it reaches a settled Promise.
|
256
lib/WhyUsePromises.md
Normal file
256
lib/WhyUsePromises.md
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
---
|
||||||
|
title: Why use Promises?
|
||||||
|
---
|
||||||
|
|
||||||
|
# Why use Promises?
|
||||||
|
|
||||||
|
Before diving in to Promises themselves, you might need some convincing of why we should even use Promises to begin with. That's totally fair! The following text should give you a brief introduction to Promises and a good understanding of why they are useful.
|
||||||
|
|
||||||
|
## Threads
|
||||||
|
|
||||||
|
When writing programs, it's possible to divide functions into two groups: "synchronous" and "asynchronous". A "synchronous operation" is one that can run to completion and generate any necessary return values with only the information available to your code at the time the operation begins. For example, a function that takes two Parts and returns the distance between them would be synchronous, because all information needed to compute that value is available when you call the function.
|
||||||
|
|
||||||
|
But sometimes situations arise where we call a function that needs access to a value that *doesn't* exist at call time. This could be because it requires a network request to get the data, or the user needs to input some text, or we're waiting for another process to finish computation and give us the value. In any case, we refer to this as an "asynchronous operation".
|
||||||
|
|
||||||
|
The simplest way to deal with this is to just stop execution of the thread, or "block". This means that when you call a function that needs some data that doesn't exist yet, the entire thread stops running and waits for the data to be ready before returning and continuing. This is actually how many low-level languages typically model asynchronous operations. To allow tasks to run at the same time, programs will create new threads that branch from parent threads and jump back on when they're finished blocking. However, this presents challenges with sharing memory and synchronizing data across threads, because at the operating system level threads truly are running in parallel.
|
||||||
|
|
||||||
|
## Coroutines
|
||||||
|
|
||||||
|
To simplify sharing memory and potentially reduce overhead, many programs will emulate a multi-threaded environment using green threads or coroutines, which are run concurrently inside of one OS thread. The key difference between OS threads and coroutines is that coroutines do not *actually* run in parallel -- only one coroutine is ever executing at a time. In the context of Lua, the term "thread" is used to refer to a coroutine, but they are not the same thing as OS threads.
|
||||||
|
|
||||||
|
To facilitate this emulation, a thread scheduler is introduced to keep track of the emulated threads and decide which thread to run next when the current thread yields. Yielding is similar to blocking, except when a coroutine yields, it signals to the thread scheduler that it can run other code and resume the thread at a later time.
|
||||||
|
|
||||||
|
When the game starts, each Script and LocalScript in your game becomes its own Lua thread in the thread scheduler and each script is run either to completion or until it yields. Once all of the scripts have gone through this process, Roblox does other things like updating humanoids and running physics. After all that's done, the next frame begins and this process repeats until the game closes.
|
||||||
|
|
||||||
|
So, what really happens when we call an asynchronous function like `Player:IsInGroup`? Well, the current Lua thread yields (letting other Lua code start running elsewhere in your game), and Roblox makes a new OS thread which blocks on an HTTP request to their internal group APIs in the background. Sometime in the future when that request comes back, the value jumps back onto the main Roblox thread and your Lua thread is scheduled to be resumed with the given arguments on the next step.
|
||||||
|
|
||||||
|
## Problems with the Coroutine Model
|
||||||
|
|
||||||
|
Coroutines fix the memory sharing problem of OS threads, but they still inherit other problems when used on their own:
|
||||||
|
|
||||||
|
- It's impossible to know if a function that you call is going to yield or not unless you look at the documentation or strictly abide by a naming convention (which is not realistic). Unintentionally yielding the thread is the source of a large class of bugs and race conditions that Roblox developers run into.
|
||||||
|
- When an asynchronous operation fails or an error is encountered, Lua functions usually either raise an error or return a success value followed by the actual value. Both of these methods lead to repeating the same tired patterns many times over for checking if the operation was successful, and make composing multiple asynchronous operations difficult.
|
||||||
|
- It is difficult to deal with running multiple asynchronous operations concurrently and then retrieve all of their values at the end without extraneous machinery.
|
||||||
|
- Coroutines lack easy access to introspection without manual work to enable it at the call site.
|
||||||
|
- Coroutines lack the ability to cancel an operation if the value is no longer needed without extraneous manual work at both the call site and the function implementation.
|
||||||
|
|
||||||
|
## Enter Promises
|
||||||
|
|
||||||
|
In Lua, Promises are an abstraction over coroutines. A "Promise" is just an object which we can use to represent a value that exists in the future, but doesn't right now. Promises are first-class citizens in other languages like JavaScript, which doesn't have coroutines and facilitates all asynchronous code through callbacks alone.
|
||||||
|
|
||||||
|
When calling an asynchronous function, instead of yielding, the function returns a Promise synchronously. The Promise object allows you to then attach a callback function which will be run later when the Promise *resolves*. The function you called is in charge of resolving the Promise with your value when it is done working.
|
||||||
|
|
||||||
|
Promises also have built-in error handling. In addition to resolving, a Promise can *reject*, which means that something went wrong when getting the future value we asked for. You can attach a different callback to be run when the Promise rejects so you can handle any error cases.
|
||||||
|
|
||||||
|
Let's take a look at this in action. We will make a function which wraps `HttpService:GetAsync` and instead of yielding, it will return a Promise.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local HttpService = game:GetService("HttpService")
|
||||||
|
local function httpGet(url)
|
||||||
|
return Promise.new(function(resolve, reject)
|
||||||
|
local ok, result = pcall(HttpService.GetAsync, HttpService, url)
|
||||||
|
|
||||||
|
if ok then
|
||||||
|
resolve(result)
|
||||||
|
else
|
||||||
|
reject(result)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's break this down. The `Promise.new` function accepts a function, called an *executor*, which receives a `resolve` function and a `reject` function. `Promise.new` calls the executor on the next Lua step. **Inside it, we have created a safe space to safely call yielding functions, which has no possibility of unintentionally delaying other parts of your code**. Since the Promise value itself was already returned from the `httpGet` function, we aren't delaying the return by yielding with `GetAsync`.
|
||||||
|
|
||||||
|
Let's use the value now:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local promise = httpGet("https://google.com")
|
||||||
|
|
||||||
|
promise:andThen(function(body)
|
||||||
|
print("Here's the Google homepage:", body)
|
||||||
|
end)
|
||||||
|
|
||||||
|
promise:catch(function(err)
|
||||||
|
warn("We failed to get the Google homepage!", err)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
So, we call the `andThen` method on the Promise returned from `httpGet`. If the Promise resolved, the handler we passed into `andThen` is called and given the resolved values as parameters (`body` in this example).
|
||||||
|
|
||||||
|
Likewise, we attach a failure handler with `catch` to be run if the Promise rejects.
|
||||||
|
|
||||||
|
But wait! In addition to attaching a callback, `andThen` and `catch` also return *new* Promises themselves! If the original Promise rejects, then the Promise returned from `andThen` will *also* reject with the same error, allowing is to rewrite our code like this:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
httpGet("https://google.com")
|
||||||
|
:andThen(function(body)
|
||||||
|
print("Here's the Google homepage:", body)
|
||||||
|
end)
|
||||||
|
:catch(function(err)
|
||||||
|
warn("We failed to get the Google homepage!", err)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
The Promise returned from `andThen` will resolve with whatever value you return from the callback.
|
||||||
|
|
||||||
|
And if that value returned from the `andThen` handler is itself a Promise, it is automatically chained onto and the Promise returned from `andThen` won't resolve until *that* Promise resolves.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
httpGet("https://google.com")
|
||||||
|
:andThen(function(body) -- not doing anything with body for this example
|
||||||
|
return httpGet("https://eryn.io") -- returning a new Promise here!
|
||||||
|
end)
|
||||||
|
:andThen(function(body) -- Doesn't get called until the above Promise resolves!
|
||||||
|
print("Here's the eryn.io homepage:", body)
|
||||||
|
end)
|
||||||
|
:catch(warn) -- Still catches errors from both Promises!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composing Promises
|
||||||
|
|
||||||
|
Promises are *composable*. This means that Promises can easily be used, interact with, and consume one another without manually threading values between them. We already saw above how returning a Promise from the `andThen` handler will chain onto it. Let's expand that idea by diving into some more ways you can compose Promises with each other:
|
||||||
|
|
||||||
|
Let's assume that we have a number of asynchronous functions which all return Promises, `async1`, `async2`, `async3`, `async3`, etc. Calling one of these functions will return a Promise. But what if we want to call all of them in sequence, each one after the one before it finishes? It's as simple as this:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
async1()
|
||||||
|
:andThen(async2)
|
||||||
|
:andThen(async3)
|
||||||
|
:andThen(async4)
|
||||||
|
:andThen(async5)
|
||||||
|
:catch(function(err)
|
||||||
|
warn("Oh no! This went wrong somewhere along the line:", err)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
In this sample, we first call `async1`, then we chain the rest of the functions together with `andThen`. If *any* of the Promises returned from these functions *reject*, then all remaining `andThen`'d functions are skipped and it will jump instantly to the `catch` handler.
|
||||||
|
|
||||||
|
And as a side note, if you forget to add a `catch` to a long chain of Promises and one of them errors, the Promise library is smart enough to emit a warning in the console. Always catch your Promises!
|
||||||
|
|
||||||
|
Let's think of another situation. What if we want to run all of the functions concurrently, and wait for all of them to be done? We don't want to run them one after another, because sometimes that can be wasteful. We want them all to run at once! We can do this with the static method `Promise.all`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
Promise.all({
|
||||||
|
async1(),
|
||||||
|
async2(),
|
||||||
|
async3(),
|
||||||
|
async4()
|
||||||
|
}):andThen(function(arrayOfResolvedValues)
|
||||||
|
print("Done running all 4 functions!")
|
||||||
|
end):catch(function(err)
|
||||||
|
warn("Uh oh, one of the Promises rejected! Abort mission!")
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
`Promise.all` accepts an array of Promise objects, and returns a new Promise. The new Promise will *resolve* with an array of resolved values in the same places as the Promises were in the array. The new Promise will *reject* if *any* of the Promises that were passed in rejects.
|
||||||
|
|
||||||
|
[`Promise.race`](https://eryn.io/roblox-lua-promise/lib/#race) is similar to `Promise.all`, except it will resolve or reject as soon as one of the Promises resolves or rejects.
|
||||||
|
|
||||||
|
We can call functions that return Promises from inside a Promise and safely yield for their result by using the `await` method of Promises. This is akin to the `await` keyword in languages like JavaScript. Sometimes it might be easier to just directly resolve with a Promise though, in which case that Promise is chained onto and the outer Promise won't resolve until the inner one does.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function async1()
|
||||||
|
return Promise.new(function(resolve, reject)
|
||||||
|
local ok, value = async2():await()
|
||||||
|
if not ok then
|
||||||
|
return reject(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
resolve(value + 1)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wait, nevermind.
|
||||||
|
|
||||||
|
Sometimes, we no longer need a value that we previously asked for (or we just want to stop a sequence of events). This could be for a variety of reasons: perhaps the user closed a menu that was loading, or a player's ability gets interrupted, or a player skips a cutscene.
|
||||||
|
|
||||||
|
When situations like these come up, we can *cancel* a Promise. Cancelling a Promise in its simplest form prevents the `andThen` or `catch` handlers from running. But we can also optionally attach a hook inside of the Promise executor so we know when the Promise has been cancelled, and stop doing work.
|
||||||
|
|
||||||
|
There is a third parameter sent to Promise executors, in addition to `resolve` and `reject`, called `onCancel`. `onCancel` allows you to register a callback which will be called whenever the Promise is cancelled. For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function tween(obj, tweenInfo, props)
|
||||||
|
return Promise.new(function(resolve, reject, onCancel)
|
||||||
|
local tween = TweenService:Create(obj, tweenInfo, props)
|
||||||
|
|
||||||
|
-- Register a callback to be called if the Promise is cancelled.
|
||||||
|
onCancel(function()
|
||||||
|
tween:Cancel()
|
||||||
|
end)
|
||||||
|
|
||||||
|
tween.Completed:Connect(resolve)
|
||||||
|
tween:Play()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Begin tweening immediately
|
||||||
|
local promise = tween(workspace.Part, TweenInfo.new(2), { Transparency = 0.5 }):andThen(function()
|
||||||
|
print("This is never printed.")
|
||||||
|
end):catch(function()
|
||||||
|
print("This is never printed.")
|
||||||
|
end):finally(function()
|
||||||
|
print("But this *is* printed!")
|
||||||
|
end)
|
||||||
|
wait(1)
|
||||||
|
promise:cancel() -- Cancel the Promise, which cancels the tween.
|
||||||
|
```
|
||||||
|
|
||||||
|
If we didn't register an `onCancel` callback, the Promise returned from the `tween` would never resolve or reject (so the `andThen` and `catch` handlers would never get called), but the tween would still finish.
|
||||||
|
|
||||||
|
For times when we need to do something no matter the fate of the Promise, whether it gets resolved, rejected, *or* cancelled, we can use `finally`. `finally` is like `andThen` and `catch`, except it *always* runs whenever the Promise is done running.
|
||||||
|
|
||||||
|
## Propagation
|
||||||
|
|
||||||
|
Cancelling a Promise will propagate upwards and cancel the entire chain of Promises. So to revisit our sequence example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local promise = async1()
|
||||||
|
:andThen(async2)
|
||||||
|
:andThen(async3)
|
||||||
|
:andThen(async4)
|
||||||
|
:andThen(async5)
|
||||||
|
:catch(function(err)
|
||||||
|
warn("Oh no! This went wrong somewhere along the line:", err)
|
||||||
|
end)
|
||||||
|
|
||||||
|
promise:cancel()
|
||||||
|
```
|
||||||
|
|
||||||
|
Cancelling `promise` (which is the Promise that `catch` returns here) will end up cancelling every Promise in the chain, all the way up to the Promise returned by `async1`. The reason this happens is because if we cancel the bottom-most Promise, we are no longer doing anything with the value, which means that no one is doing anything with the value from the Promise above it either, and so on all the way to the top. However, Promises will *not* be cancelled if they have more than one `andThen` handler attached to them, unless all of those are also cancelled.
|
||||||
|
|
||||||
|
Cancellation also propagates downwards. If a Promise is cancelled, and other Promises are dependent on that Promise, there's no way they could resolve or reject anymore, so they are cancelled as well.
|
||||||
|
|
||||||
|
So, now we understand the four possible states a Promise can be in: Started (running), Resolved, Rejected, and Cancelled. It's possible to read what state a Promise is in by calling `promise:getStatus()`.
|
||||||
|
|
||||||
|
## But I want to be able to use pre-existing functions that yield!
|
||||||
|
|
||||||
|
You can easily turn a yielding function into a Promise-returning one by calling `Promise.promisify` on it:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Assuming myFunctionAsync is a function that yields.
|
||||||
|
local myFunction = Promise.promisify(myFunctionAsync)
|
||||||
|
|
||||||
|
myFunction("some", "arguments"):andThen(print):catch(warn)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Problems, revisited
|
||||||
|
|
||||||
|
Now, let's revisit the problems we laid about before and see if we've solved them by using Promises:
|
||||||
|
|
||||||
|
- It's impossible to know if a function that you call is going to yield or not.
|
||||||
|
- Calling a function that returns a Promise will never yield! To use the value, we must call `andThen` or `await`, so we are sure that the caller knows that this is an asynchronous operation.
|
||||||
|
- When an asynchronous operation fails or an error is encountered, Lua functions usually either raise an error or return a success value followed by the actual value. Both of these methods lead to repeating the same patterns.
|
||||||
|
- We have `Promise:catch` to allow catching errors that will cascade down a Promise chain and jump to the nearst `catch` handler.
|
||||||
|
- It is difficult to deal with running multiple asynchronous operations concurrently and then retrieve all of their values at the end without extraneous machinery.
|
||||||
|
- We have `Promise.all`, `Promise.race`, or other utilities to make this a breeze.
|
||||||
|
- Coroutines lack easy access to introspection without manual work to enable it at the call site.
|
||||||
|
- We can just call `:getStatus` on the returned Promise!
|
||||||
|
- Coroutines lack the ability to cancel an operation if the value is no longer needed without extraneous manual work at both the call site and the function implementation.
|
||||||
|
- `promise:cancel()` is all we need!
|
||||||
|
|
||||||
|
Another point that's important to drive home is that you *can* do all of these things without Promises, but they require duplicated work each time you do them, which makes them incompatible with each other and that allows for slight differences between implementations which can lead to usage mistakes. Centralizing and abstracting all of this logic by using Promises ensures that all of your asynchronous APIs will be consistent and composable with one another.
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
Now that you are hopefully convinced of the benefits of using Promises in your code, move on to the [Guide](/lib/Tour.html) for a quick introduction, or dive in to the [API reference](/lib/)
|
568
lib/init.lua
568
lib/init.lua
|
@ -2,8 +2,6 @@
|
||||||
An implementation of Promises similar to Promise/A+.
|
An implementation of Promises similar to Promise/A+.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local ERROR_YIELD_NEW = "Yielding inside Promise.new is not allowed! Use Promise.async or create a new thread in the Promise executor!"
|
|
||||||
local ERROR_YIELD_THEN = "Yielding inside andThen/catch is not allowed! Instead, return a new Promise from andThen/catch."
|
|
||||||
local ERROR_NON_PROMISE_IN_LIST = "Non-promise value passed into %s at index %s"
|
local ERROR_NON_PROMISE_IN_LIST = "Non-promise value passed into %s at index %s"
|
||||||
local ERROR_NON_LIST = "Please pass a list of promises to %s"
|
local ERROR_NON_LIST = "Please pass a list of promises to %s"
|
||||||
local ERROR_NON_FUNCTION = "Please pass a handler function to %s!"
|
local ERROR_NON_FUNCTION = "Please pass a handler function to %s!"
|
||||||
|
@ -14,6 +12,107 @@ local MODE_KEY_METATABLE = {
|
||||||
|
|
||||||
local RunService = game:GetService("RunService")
|
local RunService = game:GetService("RunService")
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Creates an enum dictionary with some metamethods to prevent common mistakes.
|
||||||
|
]]
|
||||||
|
local function makeEnum(enumName, members)
|
||||||
|
local enum = {}
|
||||||
|
|
||||||
|
for _, memberName in ipairs(members) do
|
||||||
|
enum[memberName] = memberName
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable(enum, {
|
||||||
|
__index = function(_, k)
|
||||||
|
error(("%s is not in %s!"):format(k, enumName), 2)
|
||||||
|
end,
|
||||||
|
__newindex = function()
|
||||||
|
error(("Creating new members in %s is not allowed!"):format(enumName), 2)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
An object to represent runtime errors that occur during execution.
|
||||||
|
Promises that experience an error like this will be rejected with
|
||||||
|
an instance of this object.
|
||||||
|
]]
|
||||||
|
local Error do
|
||||||
|
Error = {
|
||||||
|
Kind = makeEnum("Promise.Error.Kind", {
|
||||||
|
"ExecutionError",
|
||||||
|
"AlreadyCancelled",
|
||||||
|
"NotResolvedInTime",
|
||||||
|
"TimedOut"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Error.__index = Error
|
||||||
|
|
||||||
|
function Error.new(options, parent)
|
||||||
|
options = options or {}
|
||||||
|
return setmetatable({
|
||||||
|
error = tostring(options.error) or "[This error has no error text.]",
|
||||||
|
trace = options.trace,
|
||||||
|
context = options.context,
|
||||||
|
kind = options.kind,
|
||||||
|
parent = parent,
|
||||||
|
createdTick = tick(),
|
||||||
|
createdTrace = debug.traceback()
|
||||||
|
}, Error)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Error.is(anything)
|
||||||
|
if type(anything) == "table" then
|
||||||
|
local metatable = getmetatable(anything)
|
||||||
|
|
||||||
|
if type(metatable) == "table" then
|
||||||
|
return rawget(anything, "error") ~= nil and type(rawget(metatable, "extend")) == "function"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Error.isKind(anything, kind)
|
||||||
|
assert(kind ~= nil, "Argument #2 to Promise.Error.isKind must not be nil")
|
||||||
|
|
||||||
|
return Error.is(anything) and anything.kind == kind
|
||||||
|
end
|
||||||
|
|
||||||
|
function Error:extend(options)
|
||||||
|
options = options or {}
|
||||||
|
|
||||||
|
options.kind = options.kind or self.kind
|
||||||
|
|
||||||
|
return Error.new(options, self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Error:getErrorChain()
|
||||||
|
local runtimeErrors = { self }
|
||||||
|
|
||||||
|
while runtimeErrors[#runtimeErrors].parent do
|
||||||
|
table.insert(runtimeErrors, runtimeErrors[#runtimeErrors].parent)
|
||||||
|
end
|
||||||
|
|
||||||
|
return runtimeErrors
|
||||||
|
end
|
||||||
|
|
||||||
|
function Error:__tostring()
|
||||||
|
local errorStrings = {
|
||||||
|
("-- Promise.Error(%s) --"):format(self.kind or "?"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, runtimeError in ipairs(self:getErrorChain()) do
|
||||||
|
table.insert(errorStrings, table.concat({
|
||||||
|
runtimeError.trace or runtimeError.error,
|
||||||
|
runtimeError.context
|
||||||
|
}, "\n"))
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(errorStrings, "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Packs a number of arguments into a table and returns its length.
|
Packs a number of arguments into a table and returns its length.
|
||||||
|
|
||||||
|
@ -30,24 +129,32 @@ local function packResult(success, ...)
|
||||||
return success, select("#", ...), { ... }
|
return success, select("#", ...), { ... }
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
|
||||||
Calls a non-yielding function in a new coroutine.
|
|
||||||
|
|
||||||
Handles errors if they happen.
|
local function makeErrorHandler(traceback)
|
||||||
]]
|
assert(traceback ~= nil)
|
||||||
local function runExecutor(yieldError, traceback, callback, ...)
|
|
||||||
-- Wrapped because C functions can't be passed to coroutine.create!
|
|
||||||
local co = coroutine.create(function(...)
|
|
||||||
return callback(...)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local ok, len, result = packResult(coroutine.resume(co, ...))
|
return function(err)
|
||||||
|
-- If the error object is already a table, forward it directly.
|
||||||
|
-- Should we extend the error here and add our own trace?
|
||||||
|
|
||||||
if ok and coroutine.status(co) ~= "dead" then
|
if type(err) == "table" then
|
||||||
error(yieldError .. "\n" .. traceback, 2)
|
return err
|
||||||
|
end
|
||||||
|
|
||||||
|
return Error.new({
|
||||||
|
error = err,
|
||||||
|
kind = Error.Kind.ExecutionError,
|
||||||
|
trace = debug.traceback(tostring(err), 2),
|
||||||
|
context = "Promise created at:\n\n" .. traceback
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return ok, len, result
|
--[[
|
||||||
|
Calls a Promise executor with error handling.
|
||||||
|
]]
|
||||||
|
local function runExecutor(traceback, callback, ...)
|
||||||
|
return packResult(xpcall(callback, makeErrorHandler(traceback), ...))
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -56,12 +163,12 @@ end
|
||||||
]]
|
]]
|
||||||
local function createAdvancer(traceback, callback, resolve, reject)
|
local function createAdvancer(traceback, callback, resolve, reject)
|
||||||
return function(...)
|
return function(...)
|
||||||
local ok, resultLength, result = runExecutor(ERROR_YIELD_THEN, traceback, callback, ...)
|
local ok, resultLength, result = runExecutor(traceback, callback, ...)
|
||||||
|
|
||||||
if ok then
|
if ok then
|
||||||
resolve(unpack(result, 1, resultLength))
|
resolve(unpack(result, 1, resultLength))
|
||||||
else
|
else
|
||||||
reject(result[1] .. "\n" .. traceback)
|
reject(result[1])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -70,21 +177,15 @@ local function isEmpty(t)
|
||||||
return next(t) == nil
|
return next(t) == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local Promise = {}
|
local Promise = {
|
||||||
|
Error = Error,
|
||||||
|
Status = makeEnum("Promise.Status", {"Started", "Resolved", "Rejected", "Cancelled"}),
|
||||||
|
_timeEvent = RunService.Heartbeat,
|
||||||
|
_getTime = tick,
|
||||||
|
}
|
||||||
Promise.prototype = {}
|
Promise.prototype = {}
|
||||||
Promise.__index = Promise.prototype
|
Promise.__index = Promise.prototype
|
||||||
|
|
||||||
Promise.Status = setmetatable({
|
|
||||||
Started = "Started",
|
|
||||||
Resolved = "Resolved",
|
|
||||||
Rejected = "Rejected",
|
|
||||||
Cancelled = "Cancelled",
|
|
||||||
}, {
|
|
||||||
__index = function(_, k)
|
|
||||||
error(("%s is not in Promise.Status!"):format(k), 2)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Constructs a new Promise with the given initializing callback.
|
Constructs a new Promise with the given initializing callback.
|
||||||
|
|
||||||
|
@ -97,20 +198,17 @@ Promise.Status = setmetatable({
|
||||||
Second parameter, parent, is used internally for tracking the "parent" in a
|
Second parameter, parent, is used internally for tracking the "parent" in a
|
||||||
promise chain. External code shouldn't need to worry about this.
|
promise chain. External code shouldn't need to worry about this.
|
||||||
]]
|
]]
|
||||||
function Promise.new(callback, parent)
|
function Promise._new(traceback, callback, parent)
|
||||||
if parent ~= nil and not Promise.is(parent) then
|
if parent ~= nil and not Promise.is(parent) then
|
||||||
error("Argument #2 to Promise.new must be a promise or nil", 2)
|
error("Argument #2 to Promise.new must be a promise or nil", 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
-- Used to locate where a promise was created
|
-- Used to locate where a promise was created
|
||||||
_source = debug.traceback(),
|
_source = traceback,
|
||||||
|
|
||||||
_status = Promise.Status.Started,
|
_status = Promise.Status.Started,
|
||||||
|
|
||||||
-- Will be set to the Lua error string if it occurs while executing.
|
|
||||||
_error = nil,
|
|
||||||
|
|
||||||
-- A table containing a list of all results, whether success or failure.
|
-- A table containing a list of all results, whether success or failure.
|
||||||
-- Only valid if _status is set to something besides Started
|
-- Only valid if _status is set to something besides Started
|
||||||
_values = nil,
|
_values = nil,
|
||||||
|
@ -131,9 +229,11 @@ function Promise.new(callback, parent)
|
||||||
_cancellationHook = nil,
|
_cancellationHook = nil,
|
||||||
|
|
||||||
-- The "parent" of this promise in a promise chain. Required for
|
-- The "parent" of this promise in a promise chain. Required for
|
||||||
-- cancellation propagation.
|
-- cancellation propagation upstream.
|
||||||
_parent = parent,
|
_parent = parent,
|
||||||
|
|
||||||
|
-- Consumers are Promises that have chained onto this one.
|
||||||
|
-- We track them for cancellation propagation downstream.
|
||||||
_consumers = setmetatable({}, MODE_KEY_METATABLE),
|
_consumers = setmetatable({}, MODE_KEY_METATABLE),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,57 +263,45 @@ function Promise.new(callback, parent)
|
||||||
return self._status == Promise.Status.Cancelled
|
return self._status == Promise.Status.Cancelled
|
||||||
end
|
end
|
||||||
|
|
||||||
local ok, _, result = runExecutor(
|
coroutine.wrap(function()
|
||||||
ERROR_YIELD_NEW,
|
local ok, _, result = runExecutor(
|
||||||
self._source,
|
self._source,
|
||||||
callback,
|
callback,
|
||||||
resolve,
|
resolve,
|
||||||
reject,
|
reject,
|
||||||
onCancel
|
onCancel
|
||||||
)
|
)
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
self._error = result[1] or "error"
|
reject(result[1])
|
||||||
reject((result[1] or "error") .. "\n" .. self._source)
|
end
|
||||||
end
|
end)()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
function Promise._newWithSelf(executor, ...)
|
function Promise.new(executor)
|
||||||
local args
|
return Promise._new(debug.traceback(nil, 2), executor)
|
||||||
local promise = Promise.new(function(...)
|
|
||||||
args = { ... }
|
|
||||||
end, ...)
|
|
||||||
|
|
||||||
-- we don't handle the length here since `args` will always be { resolve, reject, onCancelHook }
|
|
||||||
executor(promise, unpack(args))
|
|
||||||
|
|
||||||
return promise
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Promise._new(traceback, executor, ...)
|
function Promise:__tostring()
|
||||||
return Promise._newWithSelf(function(self, ...)
|
return ("Promise(%s)"):format(self:getStatus())
|
||||||
self._source = traceback
|
|
||||||
executor(...)
|
|
||||||
end, ...)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Promise.new, except pcall on a new thread is automatic.
|
Promise.new, except pcall on a new thread is automatic.
|
||||||
]]
|
]]
|
||||||
function Promise.async(callback)
|
function Promise.defer(callback)
|
||||||
local traceback = debug.traceback()
|
local traceback = debug.traceback(nil, 2)
|
||||||
local promise
|
local promise
|
||||||
promise = Promise._new(traceback, function(resolve, reject, onCancel)
|
promise = Promise._new(traceback, function(resolve, reject, onCancel)
|
||||||
local connection
|
local connection
|
||||||
connection = RunService.Heartbeat:Connect(function()
|
connection = Promise._timeEvent:Connect(function()
|
||||||
connection:Disconnect()
|
connection:Disconnect()
|
||||||
local ok, err = pcall(callback, resolve, reject, onCancel)
|
local ok, _, result = runExecutor(traceback, callback, resolve, reject, onCancel)
|
||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
promise._error = err or "error"
|
reject(result[1])
|
||||||
reject(err .. "\n" .. traceback)
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
@ -221,12 +309,15 @@ function Promise.async(callback)
|
||||||
return promise
|
return promise
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Backwards compatibility
|
||||||
|
Promise.async = Promise.defer
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Create a promise that represents the immediately resolved value.
|
Create a promise that represents the immediately resolved value.
|
||||||
]]
|
]]
|
||||||
function Promise.resolve(...)
|
function Promise.resolve(...)
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return Promise._new(debug.traceback(), function(resolve)
|
return Promise._new(debug.traceback(nil, 2), function(resolve)
|
||||||
resolve(unpack(values, 1, length))
|
resolve(unpack(values, 1, length))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -236,16 +327,28 @@ end
|
||||||
]]
|
]]
|
||||||
function Promise.reject(...)
|
function Promise.reject(...)
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return Promise._new(debug.traceback(), function(_, reject)
|
return Promise._new(debug.traceback(nil, 2), function(_, reject)
|
||||||
reject(unpack(values, 1, length))
|
reject(unpack(values, 1, length))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Runs a non-promise-returning function as a Promise with the
|
||||||
|
given arguments.
|
||||||
|
]]
|
||||||
|
function Promise._try(traceback, callback, ...)
|
||||||
|
local valuesLength, values = pack(...)
|
||||||
|
|
||||||
|
return Promise._new(traceback, function(resolve, reject)
|
||||||
|
resolve(callback(unpack(values, 1, valuesLength)))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Begins a Promise chain, turning synchronous errors into rejections.
|
Begins a Promise chain, turning synchronous errors into rejections.
|
||||||
]]
|
]]
|
||||||
function Promise.try(...)
|
function Promise.try(...)
|
||||||
return Promise.resolve():andThenCall(...)
|
return Promise._try(debug.traceback(nil, 2), ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -271,9 +374,7 @@ function Promise._all(traceback, promises, amount)
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise._newWithSelf(function(self, resolve, reject, onCancel)
|
return Promise._new(traceback, function(resolve, reject, onCancel)
|
||||||
self._source = traceback
|
|
||||||
|
|
||||||
-- An array to contain our resolved values from the given promises.
|
-- An array to contain our resolved values from the given promises.
|
||||||
local resolvedValues = {}
|
local resolvedValues = {}
|
||||||
local newPromises = {}
|
local newPromises = {}
|
||||||
|
@ -343,17 +444,17 @@ function Promise._all(traceback, promises, amount)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Promise.all(promises)
|
function Promise.all(promises)
|
||||||
return Promise._all(debug.traceback(), promises)
|
return Promise._all(debug.traceback(nil, 2), promises)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Promise.some(promises, amount)
|
function Promise.some(promises, amount)
|
||||||
assert(type(amount) == "number", "Bad argument #2 to Promise.some: must be a number")
|
assert(type(amount) == "number", "Bad argument #2 to Promise.some: must be a number")
|
||||||
|
|
||||||
return Promise._all(debug.traceback(), promises, amount)
|
return Promise._all(debug.traceback(nil, 2), promises, amount)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Promise.any(promises)
|
function Promise.any(promises)
|
||||||
return Promise._all(debug.traceback(), promises, 1):andThen(function(values)
|
return Promise._all(debug.traceback(nil, 2), promises, 1):andThen(function(values)
|
||||||
return values[1]
|
return values[1]
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -376,7 +477,7 @@ function Promise.allSettled(promises)
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise._new(debug.traceback(), function(resolve, _, onCancel)
|
return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
|
||||||
-- An array to contain our resolved values from the given promises.
|
-- An array to contain our resolved values from the given promises.
|
||||||
local fates = {}
|
local fates = {}
|
||||||
local newPromises = {}
|
local newPromises = {}
|
||||||
|
@ -428,7 +529,7 @@ function Promise.race(promises)
|
||||||
assert(Promise.is(promise), (ERROR_NON_PROMISE_IN_LIST):format("Promise.race", tostring(i)))
|
assert(Promise.is(promise), (ERROR_NON_PROMISE_IN_LIST):format("Promise.race", tostring(i)))
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise._new(debug.traceback(), function(resolve, reject, onCancel)
|
return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
|
||||||
local newPromises = {}
|
local newPromises = {}
|
||||||
local finished = false
|
local finished = false
|
||||||
|
|
||||||
|
@ -463,6 +564,106 @@ function Promise.race(promises)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Iterates serially over the given an array of values, calling the predicate callback on each before continuing.
|
||||||
|
If the predicate returns a Promise, we wait for that Promise to resolve before continuing to the next item
|
||||||
|
in the array. If the Promise the predicate returns rejects, the Promise from Promise.each is also rejected with
|
||||||
|
the same value.
|
||||||
|
|
||||||
|
Returns a Promise containing an array of the return values from the predicate for each item in the original list.
|
||||||
|
]]
|
||||||
|
function Promise.each(list, predicate)
|
||||||
|
assert(type(list) == "table", ERROR_NON_LIST:format("Promise.each"))
|
||||||
|
assert(type(predicate) == "function", ERROR_NON_FUNCTION:format("Promise.each"))
|
||||||
|
|
||||||
|
return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
|
||||||
|
local results = {}
|
||||||
|
local promisesToCancel = {}
|
||||||
|
|
||||||
|
local cancelled = false
|
||||||
|
|
||||||
|
local function cancel()
|
||||||
|
for _, promiseToCancel in ipairs(promisesToCancel) do
|
||||||
|
promiseToCancel:cancel()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
onCancel(function()
|
||||||
|
cancelled = true
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- We need to preprocess the list of values and look for Promises.
|
||||||
|
-- If we find some, we must register our andThen calls now, so that those Promises have a consumer
|
||||||
|
-- from us registered. If we don't do this, those Promises might get cancelled by something else
|
||||||
|
-- before we get to them in the series because it's not possible to tell that we plan to use it
|
||||||
|
-- unless we indicate it here.
|
||||||
|
|
||||||
|
local preprocessedList = {}
|
||||||
|
|
||||||
|
for index, value in ipairs(list) do
|
||||||
|
if Promise.is(value) then
|
||||||
|
if value:getStatus() == Promise.Status.Cancelled then
|
||||||
|
cancel()
|
||||||
|
return reject(Error.new({
|
||||||
|
error = "Promise is cancelled",
|
||||||
|
kind = Error.Kind.AlreadyCancelled,
|
||||||
|
context = ("The Promise that was part of the array at index %d passed into Promise.each was already cancelled when Promise.each began.\n\nThat Promise was created at:\n\n%s"):format(
|
||||||
|
index,
|
||||||
|
value._source
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
elseif value:getStatus() == Promise.Status.Rejected then
|
||||||
|
cancel()
|
||||||
|
return reject(select(2, value:await()))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Chain a new Promise from this one so we only cancel ours
|
||||||
|
local ourPromise = value:andThen(function(...)
|
||||||
|
return ...
|
||||||
|
end)
|
||||||
|
|
||||||
|
table.insert(promisesToCancel, ourPromise)
|
||||||
|
preprocessedList[index] = ourPromise
|
||||||
|
else
|
||||||
|
preprocessedList[index] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for index, value in ipairs(preprocessedList) do
|
||||||
|
if Promise.is(value) then
|
||||||
|
local success
|
||||||
|
success, value = value:await()
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
cancel()
|
||||||
|
return reject(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if cancelled then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local predicatePromise = Promise.resolve(predicate(value, index))
|
||||||
|
|
||||||
|
table.insert(promisesToCancel, predicatePromise)
|
||||||
|
|
||||||
|
local success, result = predicatePromise:await()
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
cancel()
|
||||||
|
return reject(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
results[index] = result
|
||||||
|
end
|
||||||
|
|
||||||
|
resolve(results)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Is the given object a Promise instance?
|
Is the given object a Promise instance?
|
||||||
]]
|
]]
|
||||||
|
@ -492,18 +693,7 @@ end
|
||||||
]]
|
]]
|
||||||
function Promise.promisify(callback)
|
function Promise.promisify(callback)
|
||||||
return function(...)
|
return function(...)
|
||||||
local traceback = debug.traceback()
|
return Promise._try(debug.traceback(nil, 2), callback, ...)
|
||||||
local length, values = pack(...)
|
|
||||||
return Promise._new(traceback, function(resolve, reject)
|
|
||||||
coroutine.wrap(function()
|
|
||||||
local ok, resultLength, resultValues = packResult(pcall(callback, unpack(values, 1, length)))
|
|
||||||
if ok then
|
|
||||||
resolve(unpack(resultValues, 1, resultLength))
|
|
||||||
else
|
|
||||||
reject((resultValues[1] or "error") .. "\n" .. traceback)
|
|
||||||
end
|
|
||||||
end)()
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -525,8 +715,8 @@ do
|
||||||
seconds = 1 / 60
|
seconds = 1 / 60
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise._new(debug.traceback(), function(resolve, _, onCancel)
|
return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
|
||||||
local startTime = tick()
|
local startTime = Promise._getTime()
|
||||||
local endTime = startTime + seconds
|
local endTime = startTime + seconds
|
||||||
|
|
||||||
local node = {
|
local node = {
|
||||||
|
@ -537,11 +727,12 @@ do
|
||||||
|
|
||||||
if connection == nil then -- first is nil when connection is nil
|
if connection == nil then -- first is nil when connection is nil
|
||||||
first = node
|
first = node
|
||||||
connection = RunService.Heartbeat:Connect(function()
|
connection = Promise._timeEvent:Connect(function()
|
||||||
local currentTime = tick()
|
local currentTime = Promise._getTime()
|
||||||
|
|
||||||
while first.endTime <= currentTime do
|
while first.endTime <= currentTime do
|
||||||
first.resolve(currentTime - first.startTime)
|
-- Don't use currentTime here, as this is the time when we started resolving,
|
||||||
|
-- not necessarily the time *right now*.
|
||||||
|
first.resolve(Promise._getTime() - first.startTime)
|
||||||
first = first.next
|
first = first.next
|
||||||
if first == nil then
|
if first == nil then
|
||||||
connection:Disconnect()
|
connection:Disconnect()
|
||||||
|
@ -549,7 +740,6 @@ do
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
first.previous = nil
|
first.previous = nil
|
||||||
currentTime = tick()
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
else -- first is non-nil
|
else -- first is non-nil
|
||||||
|
@ -609,10 +799,19 @@ end
|
||||||
--[[
|
--[[
|
||||||
Rejects the promise after `seconds` seconds.
|
Rejects the promise after `seconds` seconds.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:timeout(seconds, timeoutValue)
|
function Promise.prototype:timeout(seconds, rejectionValue)
|
||||||
|
local traceback = debug.traceback(nil, 2)
|
||||||
|
|
||||||
return Promise.race({
|
return Promise.race({
|
||||||
Promise.delay(seconds):andThen(function()
|
Promise.delay(seconds):andThen(function()
|
||||||
return Promise.reject(timeoutValue == nil and "Timed out" or timeoutValue)
|
return Promise.reject(rejectionValue == nil and Error.new({
|
||||||
|
kind = Error.Kind.TimedOut,
|
||||||
|
error = "Timed out",
|
||||||
|
context = ("Timeout of %d seconds exceeded.\n:timeout() called at:\n\n%s"):format(
|
||||||
|
seconds,
|
||||||
|
traceback
|
||||||
|
)
|
||||||
|
}) or rejectionValue)
|
||||||
end),
|
end),
|
||||||
self
|
self
|
||||||
})
|
})
|
||||||
|
@ -668,7 +867,11 @@ function Promise.prototype:_andThen(traceback, successHandler, failureHandler)
|
||||||
elseif self._status == Promise.Status.Cancelled then
|
elseif self._status == Promise.Status.Cancelled then
|
||||||
-- We don't want to call the success handler or the failure handler,
|
-- We don't want to call the success handler or the failure handler,
|
||||||
-- we just reject this promise outright.
|
-- we just reject this promise outright.
|
||||||
reject("Promise is cancelled")
|
reject(Error.new({
|
||||||
|
error = "Promise is cancelled",
|
||||||
|
kind = Error.Kind.AlreadyCancelled,
|
||||||
|
context = "Promise created at\n\n" .. traceback
|
||||||
|
}))
|
||||||
end
|
end
|
||||||
end, self)
|
end, self)
|
||||||
end
|
end
|
||||||
|
@ -683,7 +886,7 @@ function Promise.prototype:andThen(successHandler, failureHandler)
|
||||||
ERROR_NON_FUNCTION:format("Promise:andThen")
|
ERROR_NON_FUNCTION:format("Promise:andThen")
|
||||||
)
|
)
|
||||||
|
|
||||||
return self:_andThen(debug.traceback(), successHandler, failureHandler)
|
return self:_andThen(debug.traceback(nil, 2), successHandler, failureHandler)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -694,7 +897,7 @@ function Promise.prototype:catch(failureCallback)
|
||||||
failureCallback == nil or type(failureCallback) == "function",
|
failureCallback == nil or type(failureCallback) == "function",
|
||||||
ERROR_NON_FUNCTION:format("Promise:catch")
|
ERROR_NON_FUNCTION:format("Promise:catch")
|
||||||
)
|
)
|
||||||
return self:_andThen(debug.traceback(), nil, failureCallback)
|
return self:_andThen(debug.traceback(nil, 2), nil, failureCallback)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -703,7 +906,7 @@ end
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:tap(tapCallback)
|
function Promise.prototype:tap(tapCallback)
|
||||||
assert(type(tapCallback) == "function", ERROR_NON_FUNCTION:format("Promise:tap"))
|
assert(type(tapCallback) == "function", ERROR_NON_FUNCTION:format("Promise:tap"))
|
||||||
return self:_andThen(debug.traceback(), function(...)
|
return self:_andThen(debug.traceback(nil, 2), function(...)
|
||||||
local callbackReturn = tapCallback(...)
|
local callbackReturn = tapCallback(...)
|
||||||
|
|
||||||
if Promise.is(callbackReturn) then
|
if Promise.is(callbackReturn) then
|
||||||
|
@ -723,7 +926,7 @@ end
|
||||||
function Promise.prototype:andThenCall(callback, ...)
|
function Promise.prototype:andThenCall(callback, ...)
|
||||||
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:andThenCall"))
|
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:andThenCall"))
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return self:_andThen(debug.traceback(), function()
|
return self:_andThen(debug.traceback(nil, 2), function()
|
||||||
return callback(unpack(values, 1, length))
|
return callback(unpack(values, 1, length))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -733,7 +936,7 @@ end
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:andThenReturn(...)
|
function Promise.prototype:andThenReturn(...)
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return self:_andThen(debug.traceback(), function()
|
return self:_andThen(debug.traceback(nil, 2), function()
|
||||||
return unpack(values, 1, length)
|
return unpack(values, 1, length)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -827,7 +1030,7 @@ function Promise.prototype:finally(finallyHandler)
|
||||||
finallyHandler == nil or type(finallyHandler) == "function",
|
finallyHandler == nil or type(finallyHandler) == "function",
|
||||||
ERROR_NON_FUNCTION:format("Promise:finally")
|
ERROR_NON_FUNCTION:format("Promise:finally")
|
||||||
)
|
)
|
||||||
return self:_finally(debug.traceback(), finallyHandler)
|
return self:_finally(debug.traceback(nil, 2), finallyHandler)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -836,7 +1039,7 @@ end
|
||||||
function Promise.prototype:finallyCall(callback, ...)
|
function Promise.prototype:finallyCall(callback, ...)
|
||||||
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:finallyCall"))
|
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:finallyCall"))
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return self:_finally(debug.traceback(), function()
|
return self:_finally(debug.traceback(nil, 2), function()
|
||||||
return callback(unpack(values, 1, length))
|
return callback(unpack(values, 1, length))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -846,7 +1049,7 @@ end
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:finallyReturn(...)
|
function Promise.prototype:finallyReturn(...)
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return self:_finally(debug.traceback(), function()
|
return self:_finally(debug.traceback(nil, 2), function()
|
||||||
return unpack(values, 1, length)
|
return unpack(values, 1, length)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -859,7 +1062,7 @@ function Promise.prototype:done(finallyHandler)
|
||||||
finallyHandler == nil or type(finallyHandler) == "function",
|
finallyHandler == nil or type(finallyHandler) == "function",
|
||||||
ERROR_NON_FUNCTION:format("Promise:finallyO")
|
ERROR_NON_FUNCTION:format("Promise:finallyO")
|
||||||
)
|
)
|
||||||
return self:_finally(debug.traceback(), finallyHandler, true)
|
return self:_finally(debug.traceback(nil, 2), finallyHandler, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -868,7 +1071,7 @@ end
|
||||||
function Promise.prototype:doneCall(callback, ...)
|
function Promise.prototype:doneCall(callback, ...)
|
||||||
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:doneCall"))
|
assert(type(callback) == "function", ERROR_NON_FUNCTION:format("Promise:doneCall"))
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return self:_finally(debug.traceback(), function()
|
return self:_finally(debug.traceback(nil, 2), function()
|
||||||
return callback(unpack(values, 1, length))
|
return callback(unpack(values, 1, length))
|
||||||
end, true)
|
end, true)
|
||||||
end
|
end
|
||||||
|
@ -878,7 +1081,7 @@ end
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:doneReturn(...)
|
function Promise.prototype:doneReturn(...)
|
||||||
local length, values = pack(...)
|
local length, values = pack(...)
|
||||||
return self:_finally(debug.traceback(), function()
|
return self:_finally(debug.traceback(nil, 2), function()
|
||||||
return unpack(values, 1, length)
|
return unpack(values, 1, length)
|
||||||
end, true)
|
end, true)
|
||||||
end
|
end
|
||||||
|
@ -918,13 +1121,13 @@ end
|
||||||
--[[
|
--[[
|
||||||
Calls awaitStatus internally, returns (isResolved, values...)
|
Calls awaitStatus internally, returns (isResolved, values...)
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:await(...)
|
function Promise.prototype:await()
|
||||||
return awaitHelper(self:awaitStatus(...))
|
return awaitHelper(self:awaitStatus())
|
||||||
end
|
end
|
||||||
|
|
||||||
local function expectHelper(status, ...)
|
local function expectHelper(status, ...)
|
||||||
if status ~= Promise.Status.Resolved then
|
if status ~= Promise.Status.Resolved then
|
||||||
error((...) == nil and "" or tostring((...)), 3)
|
error((...) == nil and "Expected Promise rejected with no value." or (...), 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ...
|
return ...
|
||||||
|
@ -934,10 +1137,11 @@ end
|
||||||
Calls await and only returns if the Promise resolves.
|
Calls await and only returns if the Promise resolves.
|
||||||
Throws if the Promise rejects or gets cancelled.
|
Throws if the Promise rejects or gets cancelled.
|
||||||
]]
|
]]
|
||||||
function Promise.prototype:expect(...)
|
function Promise.prototype:expect()
|
||||||
return expectHelper(self:awaitStatus(...))
|
return expectHelper(self:awaitStatus())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Backwards compatibility
|
||||||
Promise.prototype.awaitValue = Promise.prototype.expect
|
Promise.prototype.awaitValue = Promise.prototype.expect
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
@ -985,10 +1189,27 @@ function Promise.prototype:_resolve(...)
|
||||||
self:_resolve(...)
|
self:_resolve(...)
|
||||||
end,
|
end,
|
||||||
function(...)
|
function(...)
|
||||||
-- The handler errored. Replace the inner stack trace with our outer stack trace.
|
local maybeRuntimeError = chainedPromise._values[1]
|
||||||
|
|
||||||
|
-- Backwards compatibility < v2
|
||||||
if chainedPromise._error then
|
if chainedPromise._error then
|
||||||
return self:_reject((chainedPromise._error or "") .. "\n" .. self._source)
|
maybeRuntimeError = Error.new({
|
||||||
|
error = chainedPromise._error,
|
||||||
|
kind = Error.Kind.ExecutionError,
|
||||||
|
context = "[No stack trace available as this Promise originated from an older version of the Promise library (< v2)]"
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if Error.isKind(maybeRuntimeError, Error.Kind.ExecutionError) then
|
||||||
|
return self:_reject(maybeRuntimeError:extend({
|
||||||
|
error = "This Promise was chained to a Promise that errored.",
|
||||||
|
trace = "",
|
||||||
|
context = ("The Promise at:\n\n%s\n...Rejected because it was chained to the following Promise, which encountered an error:\n"):format(
|
||||||
|
self._source
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
self:_reject(...)
|
self:_reject(...)
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
|
@ -1038,7 +1259,7 @@ function Promise.prototype:_reject(...)
|
||||||
local err = tostring((...))
|
local err = tostring((...))
|
||||||
|
|
||||||
coroutine.wrap(function()
|
coroutine.wrap(function()
|
||||||
RunService.Heartbeat:Wait()
|
Promise._timeEvent:Wait()
|
||||||
|
|
||||||
-- Someone observed the error, hooray!
|
-- Someone observed the error, hooray!
|
||||||
if not self._unhandledRejection then
|
if not self._unhandledRejection then
|
||||||
|
@ -1046,15 +1267,16 @@ function Promise.prototype:_reject(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Build a reasonable message
|
-- Build a reasonable message
|
||||||
local message
|
local message = ("Unhandled Promise rejection:\n\n%s\n\n%s"):format(
|
||||||
if self._error then
|
err,
|
||||||
message = ("Unhandled promise rejection:\n\n%s"):format(err)
|
self._source
|
||||||
else
|
)
|
||||||
message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
|
||||||
err,
|
if Promise.TEST then
|
||||||
self._source
|
-- Don't spam output when we're running tests.
|
||||||
)
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
warn(message)
|
warn(message)
|
||||||
end)()
|
end)()
|
||||||
end
|
end
|
||||||
|
@ -1075,15 +1297,95 @@ function Promise.prototype:_finalize()
|
||||||
callback(self._status)
|
callback(self._status)
|
||||||
end
|
end
|
||||||
|
|
||||||
if self._parent and self._error == nil then
|
-- Clear references to other Promises to allow gc
|
||||||
self._error = self._parent._error
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Allow family to be buried
|
|
||||||
if not Promise.TEST then
|
if not Promise.TEST then
|
||||||
self._parent = nil
|
self._parent = nil
|
||||||
self._consumers = nil
|
self._consumers = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Chains a Promise from this one that is resolved if this Promise is
|
||||||
|
resolved, and rejected if it is not resolved.
|
||||||
|
]]
|
||||||
|
function Promise.prototype:now(rejectionValue)
|
||||||
|
local traceback = debug.traceback(nil, 2)
|
||||||
|
if self:getStatus() == Promise.Status.Resolved then
|
||||||
|
return self:_andThen(traceback, function(...)
|
||||||
|
return ...
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
return Promise.reject(rejectionValue == nil and Error.new({
|
||||||
|
kind = Error.Kind.NotResolvedInTime,
|
||||||
|
error = "This Promise was not resolved in time for :now()",
|
||||||
|
context = ":now() was called at:\n\n" .. traceback
|
||||||
|
}) or rejectionValue)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Retries a Promise-returning callback N times until it succeeds.
|
||||||
|
]]
|
||||||
|
function Promise.retry(callback, times, ...)
|
||||||
|
assert(type(callback) == "function", "Parameter #1 to Promise.retry must be a function")
|
||||||
|
assert(type(times) == "number", "Parameter #2 to Promise.retry must be a number")
|
||||||
|
|
||||||
|
local args, length = {...}, select("#", ...)
|
||||||
|
|
||||||
|
return Promise.resolve(callback(...)):catch(function(...)
|
||||||
|
if times > 0 then
|
||||||
|
return Promise.retry(callback, times - 1, unpack(args, 1, length))
|
||||||
|
else
|
||||||
|
return Promise.reject(...)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Converts an event into a Promise with an optional predicate
|
||||||
|
]]
|
||||||
|
function Promise.fromEvent(event, predicate)
|
||||||
|
predicate = predicate or function()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
|
||||||
|
local connection
|
||||||
|
local shouldDisconnect = false
|
||||||
|
|
||||||
|
local function disconnect()
|
||||||
|
connection:Disconnect()
|
||||||
|
connection = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- We use shouldDisconnect because if the callback given to Connect is called before
|
||||||
|
-- Connect returns, connection will still be nil. This happens with events that queue up
|
||||||
|
-- events when there's nothing connected, such as RemoteEvents
|
||||||
|
|
||||||
|
connection = event:Connect(function(...)
|
||||||
|
local callbackValue = predicate(...)
|
||||||
|
|
||||||
|
if callbackValue == true then
|
||||||
|
resolve(...)
|
||||||
|
|
||||||
|
if connection then
|
||||||
|
disconnect()
|
||||||
|
else
|
||||||
|
shouldDisconnect = true
|
||||||
|
end
|
||||||
|
elseif type(callbackValue) ~= "boolean" then
|
||||||
|
error("Promise.fromEvent predicate should always return a boolean")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
if shouldDisconnect and connection then
|
||||||
|
return disconnect()
|
||||||
|
end
|
||||||
|
|
||||||
|
onCancel(function()
|
||||||
|
disconnect()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
return Promise
|
return Promise
|
||||||
|
|
|
@ -2,6 +2,24 @@ return function()
|
||||||
local Promise = require(script.Parent)
|
local Promise = require(script.Parent)
|
||||||
Promise.TEST = true
|
Promise.TEST = true
|
||||||
|
|
||||||
|
local timeEvent = Instance.new("BindableEvent")
|
||||||
|
Promise._timeEvent = timeEvent.Event
|
||||||
|
|
||||||
|
local advanceTime do
|
||||||
|
local injectedPromiseTime = 0
|
||||||
|
|
||||||
|
Promise._getTime = function()
|
||||||
|
return injectedPromiseTime
|
||||||
|
end
|
||||||
|
|
||||||
|
function advanceTime(delta)
|
||||||
|
delta = delta or (1/60)
|
||||||
|
|
||||||
|
injectedPromiseTime = injectedPromiseTime + delta
|
||||||
|
timeEvent:Fire(delta)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function pack(...)
|
local function pack(...)
|
||||||
local len = select("#", ...)
|
local len = select("#", ...)
|
||||||
|
|
||||||
|
@ -79,11 +97,109 @@ return function()
|
||||||
expect(promise).to.be.ok()
|
expect(promise).to.be.ok()
|
||||||
expect(callCount).to.equal(1)
|
expect(callCount).to.equal(1)
|
||||||
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
expect(promise._values[1]:find("hahah")).to.be.ok()
|
expect(tostring(promise._values[1]):find("hahah")).to.be.ok()
|
||||||
|
|
||||||
-- Loosely check for the pieces of the stack trace we expect
|
-- Loosely check for the pieces of the stack trace we expect
|
||||||
expect(promise._values[1]:find("init.spec")).to.be.ok()
|
expect(tostring(promise._values[1]):find("init.spec")).to.be.ok()
|
||||||
expect(promise._values[1]:find("new")).to.be.ok()
|
expect(tostring(promise._values[1]):find("runExecutor")).to.be.ok()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should work with C functions", function()
|
||||||
|
expect(function()
|
||||||
|
Promise.new(tick):andThen(tick)
|
||||||
|
end).to.never.throw()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should have a nice tostring", function()
|
||||||
|
expect(tostring(Promise.resolve()):gmatch("Promise(Resolved)")).to.be.ok()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should allow yielding", function()
|
||||||
|
local bindable = Instance.new("BindableEvent")
|
||||||
|
local promise = Promise.new(function(resolve)
|
||||||
|
bindable.Event:Wait()
|
||||||
|
resolve(5)
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
bindable:Fire()
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||||
|
expect(promise._values[1]).to.equal(5)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should preserve stack traces of resolve-chained promises", function()
|
||||||
|
local function nestedCall(text)
|
||||||
|
error(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
local promise = Promise.new(function(resolve)
|
||||||
|
resolve(Promise.new(function()
|
||||||
|
nestedCall("sample text")
|
||||||
|
end))
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
|
||||||
|
local trace = tostring(promise._values[1])
|
||||||
|
expect(trace:find("sample text")).to.be.ok()
|
||||||
|
expect(trace:find("nestedCall")).to.be.ok()
|
||||||
|
expect(trace:find("runExecutor")).to.be.ok()
|
||||||
|
expect(trace:find("runPlanNode")).to.be.ok()
|
||||||
|
expect(trace:find("...Rejected because it was chained to the following Promise, which encountered an error:")).to.be.ok()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should report errors from Promises with _error (< v2)", function()
|
||||||
|
local oldPromise = Promise.reject()
|
||||||
|
oldPromise._error = "Sample error"
|
||||||
|
|
||||||
|
local newPromise = Promise.resolve():andThenReturn(oldPromise)
|
||||||
|
|
||||||
|
expect(newPromise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
|
||||||
|
local trace = tostring(newPromise._values[1])
|
||||||
|
expect(trace:find("Sample error")).to.be.ok()
|
||||||
|
expect(trace:find("...Rejected because it was chained to the following Promise, which encountered an error:")).to.be.ok()
|
||||||
|
expect(trace:find("%[No stack trace available")).to.be.ok()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Promise.defer", function()
|
||||||
|
it("should execute after the time event", function()
|
||||||
|
local callCount = 0
|
||||||
|
local promise = Promise.defer(function(resolve, reject, onCancel, nothing)
|
||||||
|
expect(type(resolve)).to.equal("function")
|
||||||
|
expect(type(reject)).to.equal("function")
|
||||||
|
expect(type(onCancel)).to.equal("function")
|
||||||
|
expect(type(nothing)).to.equal("nil")
|
||||||
|
|
||||||
|
callCount = callCount + 1
|
||||||
|
|
||||||
|
resolve("foo")
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(callCount).to.equal(0)
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
|
||||||
|
advanceTime()
|
||||||
|
expect(callCount).to.equal(1)
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||||
|
|
||||||
|
advanceTime()
|
||||||
|
expect(callCount).to.equal(1)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Promise.delay", function()
|
||||||
|
it("Should schedule promise resolution", function()
|
||||||
|
local promise = Promise.delay(1)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
|
||||||
|
advanceTime()
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
|
||||||
|
advanceTime(1)
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -132,6 +248,19 @@ return function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("Promise:andThen", function()
|
describe("Promise:andThen", function()
|
||||||
|
it("should allow yielding", function()
|
||||||
|
local bindable = Instance.new("BindableEvent")
|
||||||
|
local promise = Promise.resolve():andThen(function()
|
||||||
|
bindable.Event:Wait()
|
||||||
|
return 5
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
bindable:Fire()
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||||
|
expect(promise._values[1]).to.equal(5)
|
||||||
|
end)
|
||||||
|
|
||||||
it("should chain onto resolved promises", function()
|
it("should chain onto resolved promises", function()
|
||||||
local args
|
local args
|
||||||
local argsLength
|
local argsLength
|
||||||
|
@ -200,6 +329,24 @@ return function()
|
||||||
expect(#chained._values).to.equal(0)
|
expect(#chained._values).to.equal(0)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("should reject on error in callback", function()
|
||||||
|
local callCount = 0
|
||||||
|
|
||||||
|
local promise = Promise.resolve(1):andThen(function()
|
||||||
|
callCount = callCount + 1
|
||||||
|
error("hahah")
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise).to.be.ok()
|
||||||
|
expect(callCount).to.equal(1)
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
expect(tostring(promise._values[1]):find("hahah")).to.be.ok()
|
||||||
|
|
||||||
|
-- Loosely check for the pieces of the stack trace we expect
|
||||||
|
expect(tostring(promise._values[1]):find("init.spec")).to.be.ok()
|
||||||
|
expect(tostring(promise._values[1]):find("runExecutor")).to.be.ok()
|
||||||
|
end)
|
||||||
|
|
||||||
it("should chain onto asynchronously resolved promises", function()
|
it("should chain onto asynchronously resolved promises", function()
|
||||||
local args
|
local args
|
||||||
local argsLength
|
local argsLength
|
||||||
|
@ -719,7 +866,7 @@ return function()
|
||||||
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
bindable:Fire()
|
bindable:Fire()
|
||||||
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
expect(promise._values[1]:find("errortext")).to.be.ok()
|
expect(tostring(promise._values[1]):find("errortext")).to.be.ok()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -772,11 +919,34 @@ return function()
|
||||||
Promise.try(function()
|
Promise.try(function()
|
||||||
error('errortext')
|
error('errortext')
|
||||||
end):catch(function(e)
|
end):catch(function(e)
|
||||||
errorText = e
|
errorText = tostring(e)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
expect(errorText:find("errortext")).to.be.ok()
|
expect(errorText:find("errortext")).to.be.ok()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("should reject with error objects", function()
|
||||||
|
local object = {}
|
||||||
|
local success, value = Promise.try(function()
|
||||||
|
error(object)
|
||||||
|
end):_unwrap()
|
||||||
|
|
||||||
|
expect(success).to.equal(false)
|
||||||
|
expect(value).to.equal(object)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should catch asynchronous errors", function()
|
||||||
|
local bindable = Instance.new("BindableEvent")
|
||||||
|
local promise = Promise.try(function()
|
||||||
|
bindable.Event:Wait()
|
||||||
|
error('errortext')
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
bindable:Fire()
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
expect(tostring(promise._values[1]):find("errortext")).to.be.ok()
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("Promise:andThenReturn", function()
|
describe("Promise:andThenReturn", function()
|
||||||
|
@ -1015,4 +1185,293 @@ return function()
|
||||||
expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
|
expect(promises[3]:getStatus()).to.equal(Promise.Status.Started)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe("Promise:await", function()
|
||||||
|
it("should return the correct values", function()
|
||||||
|
local promise = Promise.resolve(5, 6, nil, 7)
|
||||||
|
|
||||||
|
local a, b, c, d, e = promise:await()
|
||||||
|
|
||||||
|
expect(a).to.equal(true)
|
||||||
|
expect(b).to.equal(5)
|
||||||
|
expect(c).to.equal(6)
|
||||||
|
expect(d).to.equal(nil)
|
||||||
|
expect(e).to.equal(7)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Promise:expect", function()
|
||||||
|
it("should throw the correct values", function()
|
||||||
|
local rejectionValue = {}
|
||||||
|
local promise = Promise.reject(rejectionValue)
|
||||||
|
|
||||||
|
local success, value = pcall(function()
|
||||||
|
promise:expect()
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(success).to.equal(false)
|
||||||
|
expect(value).to.equal(rejectionValue)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Promise:now", function()
|
||||||
|
it("should resolve if the Promise is resolved", function()
|
||||||
|
local success, value = Promise.resolve("foo"):now():_unwrap()
|
||||||
|
|
||||||
|
expect(success).to.equal(true)
|
||||||
|
expect(value).to.equal("foo")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should reject if the Promise is not resolved", function()
|
||||||
|
local success, value = Promise.new(function() end):now():_unwrap()
|
||||||
|
|
||||||
|
expect(success).to.equal(false)
|
||||||
|
expect(Promise.Error.isKind(value, "NotResolvedInTime")).to.equal(true)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should reject with a custom rejection value", function()
|
||||||
|
local success, value = Promise.new(function() end):now("foo"):_unwrap()
|
||||||
|
|
||||||
|
expect(success).to.equal(false)
|
||||||
|
expect(value).to.equal("foo")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Promise.each", function()
|
||||||
|
it("should iterate", function()
|
||||||
|
local ok, result = Promise.each({
|
||||||
|
"foo", "bar", "baz", "qux"
|
||||||
|
}, function(...)
|
||||||
|
return {...}
|
||||||
|
end):_unwrap()
|
||||||
|
|
||||||
|
expect(ok).to.equal(true)
|
||||||
|
expect(result[1][1]).to.equal("foo")
|
||||||
|
expect(result[1][2]).to.equal(1)
|
||||||
|
expect(result[2][1]).to.equal("bar")
|
||||||
|
expect(result[2][2]).to.equal(2)
|
||||||
|
expect(result[3][1]).to.equal("baz")
|
||||||
|
expect(result[3][2]).to.equal(3)
|
||||||
|
expect(result[4][1]).to.equal("qux")
|
||||||
|
expect(result[4][2]).to.equal(4)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should iterate serially", function()
|
||||||
|
local resolves = {}
|
||||||
|
local callCounts = {}
|
||||||
|
|
||||||
|
local promise = Promise.each({
|
||||||
|
"foo", "bar", "baz"
|
||||||
|
}, function(value, index)
|
||||||
|
callCounts[index] = (callCounts[index] or 0) + 1
|
||||||
|
|
||||||
|
return Promise.new(function(resolve)
|
||||||
|
table.insert(resolves, function()
|
||||||
|
resolve(value:upper())
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
expect(#resolves).to.equal(1)
|
||||||
|
expect(callCounts[1]).to.equal(1)
|
||||||
|
expect(callCounts[2]).to.never.be.ok()
|
||||||
|
|
||||||
|
table.remove(resolves, 1)()
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
expect(#resolves).to.equal(1)
|
||||||
|
expect(callCounts[1]).to.equal(1)
|
||||||
|
expect(callCounts[2]).to.equal(1)
|
||||||
|
expect(callCounts[3]).to.never.be.ok()
|
||||||
|
|
||||||
|
table.remove(resolves, 1)()
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
expect(callCounts[1]).to.equal(1)
|
||||||
|
expect(callCounts[2]).to.equal(1)
|
||||||
|
expect(callCounts[3]).to.equal(1)
|
||||||
|
|
||||||
|
table.remove(resolves, 1)()
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||||
|
expect(type(promise._values[1])).to.equal("table")
|
||||||
|
expect(type(promise._values[2])).to.equal("nil")
|
||||||
|
|
||||||
|
local result = promise._values[1]
|
||||||
|
|
||||||
|
expect(result[1]).to.equal("FOO")
|
||||||
|
expect(result[2]).to.equal("BAR")
|
||||||
|
expect(result[3]).to.equal("BAZ")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should reject with the value if the predicate promise rejects", function()
|
||||||
|
local promise = Promise.each({1, 2, 3}, function()
|
||||||
|
return Promise.reject("foobar")
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
expect(promise._values[1]).to.equal("foobar")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should allow Promises to be in the list and wait when it gets to them", function()
|
||||||
|
local innerResolve
|
||||||
|
local innerPromise = Promise.new(function(resolve)
|
||||||
|
innerResolve = resolve
|
||||||
|
end)
|
||||||
|
|
||||||
|
local promise = Promise.each({
|
||||||
|
innerPromise
|
||||||
|
}, function(value)
|
||||||
|
return value * 2
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
|
||||||
|
innerResolve(2)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||||
|
expect(promise._values[1][1]).to.equal(4)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should reject with the value if a Promise from the list rejects", function()
|
||||||
|
local called = false
|
||||||
|
local promise = Promise.each({1, 2, Promise.reject("foobar")}, function(value)
|
||||||
|
called = true
|
||||||
|
return "never"
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
expect(promise._values[1]).to.equal("foobar")
|
||||||
|
expect(called).to.equal(false)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should reject immediately if there's a cancelled Promise in the list initially", function()
|
||||||
|
local cancelled = Promise.new(function() end)
|
||||||
|
cancelled:cancel()
|
||||||
|
|
||||||
|
local called = false
|
||||||
|
local promise = Promise.each({1, 2, cancelled}, function()
|
||||||
|
called = true
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
expect(called).to.equal(false)
|
||||||
|
expect(promise._values[1].kind).to.equal(Promise.Error.Kind.AlreadyCancelled)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should stop iteration if Promise.each is cancelled", function()
|
||||||
|
local callCounts = {}
|
||||||
|
|
||||||
|
local promise = Promise.each({
|
||||||
|
"foo", "bar", "baz"
|
||||||
|
}, function(value, index)
|
||||||
|
callCounts[index] = (callCounts[index] or 0) + 1
|
||||||
|
|
||||||
|
return Promise.new(function()
|
||||||
|
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
expect(callCounts[1]).to.equal(1)
|
||||||
|
expect(callCounts[2]).to.never.be.ok()
|
||||||
|
|
||||||
|
promise:cancel()
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||||
|
expect(callCounts[1]).to.equal(1)
|
||||||
|
expect(callCounts[2]).to.never.be.ok()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should cancel the Promise returned from the predicate if Promise.each is cancelled", function()
|
||||||
|
local innerPromise
|
||||||
|
|
||||||
|
local promise = Promise.each({
|
||||||
|
"foo", "bar", "baz"
|
||||||
|
}, function(value, index)
|
||||||
|
innerPromise = Promise.new(function()
|
||||||
|
end)
|
||||||
|
return innerPromise
|
||||||
|
end)
|
||||||
|
|
||||||
|
promise:cancel()
|
||||||
|
|
||||||
|
expect(innerPromise:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should cancel Promises in the list if Promise.each is cancelled", function()
|
||||||
|
local innerPromise = Promise.new(function() end)
|
||||||
|
|
||||||
|
local promise = Promise.each({innerPromise}, function() end)
|
||||||
|
|
||||||
|
promise:cancel()
|
||||||
|
|
||||||
|
expect(innerPromise:getStatus()).to.equal(Promise.Status.Cancelled)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Promise.retry", function()
|
||||||
|
it("should retry N times", function()
|
||||||
|
local counter = 0
|
||||||
|
|
||||||
|
local promise = Promise.retry(function(parameter)
|
||||||
|
expect(parameter).to.equal("foo")
|
||||||
|
|
||||||
|
counter = counter + 1
|
||||||
|
|
||||||
|
if counter == 5 then
|
||||||
|
return Promise.resolve("ok")
|
||||||
|
end
|
||||||
|
|
||||||
|
return Promise.reject("fail")
|
||||||
|
end, 5, "foo")
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||||
|
expect(promise._values[1]).to.equal("ok")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should reject if threshold is exceeded", function()
|
||||||
|
local promise = Promise.retry(function()
|
||||||
|
return Promise.reject("fail")
|
||||||
|
end, 5)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Rejected)
|
||||||
|
expect(promise._values[1]).to.equal("fail")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe("Promise.fromEvent", function()
|
||||||
|
it("should convert a Promise into an event", function()
|
||||||
|
local event = Instance.new("BindableEvent")
|
||||||
|
|
||||||
|
local promise = Promise.fromEvent(event.Event)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
|
||||||
|
event:Fire("foo")
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||||
|
expect(promise._values[1]).to.equal("foo")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should convert a Promise into an event with the predicate", function()
|
||||||
|
local event = Instance.new("BindableEvent")
|
||||||
|
|
||||||
|
local promise = Promise.fromEvent(event.Event, function(param)
|
||||||
|
return param == "foo"
|
||||||
|
end)
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
|
||||||
|
event:Fire("bar")
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Started)
|
||||||
|
|
||||||
|
event:Fire("foo")
|
||||||
|
|
||||||
|
expect(promise:getStatus()).to.equal(Promise.Status.Resolved)
|
||||||
|
expect(promise._values[1]).to.equal("foo")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end
|
end
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 3b295421315081168db63c00bbb4e6c070ac7634
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3c42175897f7133d10fdc269c040df98a55906d6
|
Subproject commit dc2f2fa1e1f3c2aa9c0d7d92e78d6530c682fac9
|
59
package-lock.json
generated
59
package-lock.json
generated
|
@ -9526,13 +9526,64 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vuepress-plugin-api-docs-generator": {
|
"vuepress-plugin-api-docs-generator": {
|
||||||
"version": "1.0.17",
|
"version": "1.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/vuepress-plugin-api-docs-generator/-/vuepress-plugin-api-docs-generator-1.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/vuepress-plugin-api-docs-generator/-/vuepress-plugin-api-docs-generator-1.0.18.tgz",
|
||||||
"integrity": "sha512-KOeexJgcXOykjp3Barj7lFNDGmMGt+FMRE3Le4Y+QkrBvPrFwjAWdoHpPpjQtSrXwTK7I9eVqmfqUKXxdaf2dg==",
|
"integrity": "sha512-fQauXyRcj5gAdQbAE3IIGHSZ9tsrLmykbjFgCq5Xd4LTZAsDOqWjWxfbFG5BChmePkCq+GjFlckEjGxAixAmBg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@vuepress/plugin-register-components": "^1.4.1",
|
"@vuepress/plugin-register-components": "^1.4.1",
|
||||||
"node-balanced": "0.0.14",
|
"node-balanced": "0.0.14",
|
||||||
"slugify": "^1.3.4"
|
"slugify": "^1.3.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vuepress/plugin-register-components": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vuepress/plugin-register-components/-/plugin-register-components-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-6yI4J/tMhOASSLmlP+5p4ccljlWuNBRsyYSKiD5jWAV181oMmN32LtuoCggXBhSvQUgn2grxyjmYw+tcSV5KGQ==",
|
||||||
|
"requires": {
|
||||||
|
"@vuepress/shared-utils": "1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@vuepress/shared-utils": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vuepress/shared-utils/-/shared-utils-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-FBUHFhvR7vk6glQy/qUntBz8bVeWiNYZ2/G16EKaerKKn15xAiD7tUFCQ3L/KjtQJ8TV38GK47UEXh7UTcRwQg==",
|
||||||
|
"requires": {
|
||||||
|
"chalk": "^2.3.2",
|
||||||
|
"diacritics": "^1.3.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"fs-extra": "^7.0.1",
|
||||||
|
"globby": "^9.2.0",
|
||||||
|
"gray-matter": "^4.0.1",
|
||||||
|
"hash-sum": "^1.0.2",
|
||||||
|
"semver": "^6.0.0",
|
||||||
|
"upath": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"globby": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/glob": "^7.1.1",
|
||||||
|
"array-union": "^1.0.2",
|
||||||
|
"dir-glob": "^2.2.2",
|
||||||
|
"fast-glob": "^2.2.6",
|
||||||
|
"glob": "^7.1.3",
|
||||||
|
"ignore": "^4.0.3",
|
||||||
|
"pify": "^4.0.1",
|
||||||
|
"slash": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pify": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vuepress-plugin-container": {
|
"vuepress-plugin-container": {
|
||||||
|
@ -10031,4 +10082,4 @@
|
||||||
"integrity": "sha1-4Se9nmb9hGvl6rSME5SIL3wOT5g="
|
"integrity": "sha1-4Se9nmb9hGvl6rSME5SIL3wOT5g="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,8 +8,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gh-pages": "^2.1.1",
|
"gh-pages": "^2.1.1",
|
||||||
"vuepress": "^1.4.1",
|
"vuepress-plugin-api-docs-generator": "^1.0.18",
|
||||||
"vuepress-plugin-api-docs-generator": "^1.0.17"
|
"vuepress": "^1.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -27,4 +27,4 @@
|
||||||
"url": "https://github.com/evaera/roblox-lua-promise/issues"
|
"url": "https://github.com/evaera/roblox-lua-promise/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/evaera/roblox-lua-promise#readme"
|
"homepage": "https://github.com/evaera/roblox-lua-promise#readme"
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "roblox-lua-promise"
|
name = "roblox-lua-promise"
|
||||||
version = "2.4.1"
|
version = "3.0.0-rc.1"
|
||||||
author = "evaera"
|
author = "evaera"
|
||||||
content_root = "lib"
|
content_root = "lib"
|
||||||
|
|
||||||
|
|
3
runTests.server.lua
Normal file
3
runTests.server.lua
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
require(script.Parent.TestEZ).TestBootstrap:run({
|
||||||
|
game.ServerScriptService.Lib
|
||||||
|
})
|
69
spec.lua
69
spec.lua
|
@ -1,69 +0,0 @@
|
||||||
--[[
|
|
||||||
Loads our library and all of its dependencies, then runs tests using TestEZ.
|
|
||||||
]]
|
|
||||||
|
|
||||||
-- If you add any dependencies, add them to this table so they'll be loaded!
|
|
||||||
local LOAD_MODULES = {
|
|
||||||
{"lib", "Library"},
|
|
||||||
{"modules/testez/lib", "TestEZ"},
|
|
||||||
}
|
|
||||||
|
|
||||||
-- This makes sure we can load Lemur and other libraries that depend on init.lua
|
|
||||||
package.path = package.path .. ";?/init.lua"
|
|
||||||
|
|
||||||
-- If this fails, make sure you've run `lua bin/install-dependencies.lua` first!
|
|
||||||
local lemur = require("modules.lemur")
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Collapses ModuleScripts named 'init' into their parent folders.
|
|
||||||
|
|
||||||
This is the same result as the collapsing mechanism from Rojo.
|
|
||||||
]]
|
|
||||||
local function collapse(root)
|
|
||||||
local init = root:FindFirstChild("init")
|
|
||||||
if init then
|
|
||||||
init.Name = root.Name
|
|
||||||
init.Parent = root.Parent
|
|
||||||
|
|
||||||
for _, child in ipairs(root:GetChildren()) do
|
|
||||||
child.Parent = init
|
|
||||||
end
|
|
||||||
|
|
||||||
root:Destroy()
|
|
||||||
root = init
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, child in ipairs(root:GetChildren()) do
|
|
||||||
if child:IsA("Folder") then
|
|
||||||
collapse(child)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return root
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Create a virtual Roblox tree
|
|
||||||
local habitat = lemur.Habitat.new()
|
|
||||||
|
|
||||||
-- We'll put all of our library code and dependencies here
|
|
||||||
local Root = lemur.Instance.new("Folder")
|
|
||||||
Root.Name = "Root"
|
|
||||||
|
|
||||||
-- Load all of the modules specified above
|
|
||||||
for _, module in ipairs(LOAD_MODULES) do
|
|
||||||
local container = lemur.Instance.new("Folder", Root)
|
|
||||||
container.Name = module[2]
|
|
||||||
habitat:loadFromFs(module[1], container)
|
|
||||||
end
|
|
||||||
|
|
||||||
collapse(Root)
|
|
||||||
|
|
||||||
-- Load TestEZ and run our tests
|
|
||||||
local TestEZ = habitat:require(Root.TestEZ)
|
|
||||||
|
|
||||||
local results = TestEZ.TestBootstrap:run(Root.Library, TestEZ.Reporters.TextReporter)
|
|
||||||
|
|
||||||
-- Did something go wrong?
|
|
||||||
if results.failureCount > 0 then
|
|
||||||
os.exit(1)
|
|
||||||
end
|
|
Loading…
Reference in a new issue