mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-25 04:54:07 +00:00
Switch to external Promise library, plugin is broken right now
This commit is contained in:
parent
892378e0da
commit
0c52c519f0
4 changed files with 4 additions and 569 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -13,3 +13,6 @@
|
||||||
[submodule "plugin/modules/lemur"]
|
[submodule "plugin/modules/lemur"]
|
||||||
path = plugin/modules/lemur
|
path = plugin/modules/lemur
|
||||||
url = https://github.com/LPGhatguy/lemur.git
|
url = https://github.com/LPGhatguy/lemur.git
|
||||||
|
[submodule "plugin/modules/promise"]
|
||||||
|
path = plugin/modules/promise
|
||||||
|
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
||||||
|
|
|
||||||
1
plugin/modules/promise
Submodule
1
plugin/modules/promise
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 7fb09d103f4a680cbd61d5348ec9c5d50244a8e0
|
||||||
|
|
@ -1,307 +0,0 @@
|
||||||
--[[
|
|
||||||
An implementation of Promises similar to Promise/A+.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local PROMISE_DEBUG = false
|
|
||||||
|
|
||||||
-- If promise debugging is on, use a version of pcall that warns on failure.
|
|
||||||
-- This is useful for finding errors that happen within Promise itself.
|
|
||||||
local wpcall
|
|
||||||
if PROMISE_DEBUG then
|
|
||||||
wpcall = function(f, ...)
|
|
||||||
local result = { pcall(f, ...) }
|
|
||||||
|
|
||||||
if not result[1] then
|
|
||||||
warn(result[2])
|
|
||||||
end
|
|
||||||
|
|
||||||
return unpack(result)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
wpcall = pcall
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Creates a function that invokes a callback with correct error handling and
|
|
||||||
resolution mechanisms.
|
|
||||||
]]
|
|
||||||
local function createAdvancer(callback, resolve, reject)
|
|
||||||
return function(...)
|
|
||||||
local result = { wpcall(callback, ...) }
|
|
||||||
local ok = table.remove(result, 1)
|
|
||||||
|
|
||||||
if ok then
|
|
||||||
resolve(unpack(result))
|
|
||||||
else
|
|
||||||
reject(unpack(result))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function isEmpty(t)
|
|
||||||
return next(t) == nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local Promise = {}
|
|
||||||
Promise.__index = Promise
|
|
||||||
|
|
||||||
Promise.Status = {
|
|
||||||
Started = "Started",
|
|
||||||
Resolved = "Resolved",
|
|
||||||
Rejected = "Rejected",
|
|
||||||
}
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Constructs a new Promise with the given initializing callback.
|
|
||||||
|
|
||||||
This is generally only called when directly wrapping a non-promise API into
|
|
||||||
a promise-based version.
|
|
||||||
|
|
||||||
The callback will receive 'resolve' and 'reject' methods, used to start
|
|
||||||
invoking the promise chain.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
local function get(url)
|
|
||||||
return Promise.new(function(resolve, reject)
|
|
||||||
spawn(function()
|
|
||||||
resolve(HttpService:GetAsync(url))
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
get("https://google.com")
|
|
||||||
:andThen(function(stuff)
|
|
||||||
print("Got some stuff!", stuff)
|
|
||||||
end)
|
|
||||||
]]
|
|
||||||
function Promise.new(callback)
|
|
||||||
local promise = {
|
|
||||||
-- Used to locate where a promise was created
|
|
||||||
_source = debug.traceback(),
|
|
||||||
|
|
||||||
-- A tag to identify us as a promise
|
|
||||||
_type = "Promise",
|
|
||||||
|
|
||||||
_status = Promise.Status.Started,
|
|
||||||
|
|
||||||
-- A table containing a list of all results, whether success or failure.
|
|
||||||
-- Only valid if _status is set to something besides Started
|
|
||||||
_value = nil,
|
|
||||||
|
|
||||||
-- If an error occurs with no observers, this will be set.
|
|
||||||
_unhandledRejection = false,
|
|
||||||
|
|
||||||
-- Queues representing functions we should invoke when we update!
|
|
||||||
_queuedResolve = {},
|
|
||||||
_queuedReject = {},
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(promise, Promise)
|
|
||||||
|
|
||||||
local function resolve(...)
|
|
||||||
promise:_resolve(...)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reject(...)
|
|
||||||
promise:_reject(...)
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, err = wpcall(callback, resolve, reject)
|
|
||||||
|
|
||||||
if not ok and promise._status == Promise.Status.Started then
|
|
||||||
reject(err)
|
|
||||||
end
|
|
||||||
|
|
||||||
return promise
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a promise that represents the immediately resolved value.
|
|
||||||
]]
|
|
||||||
function Promise.resolve(value)
|
|
||||||
return Promise.new(function(resolve)
|
|
||||||
resolve(value)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Create a promise that represents the immediately rejected value.
|
|
||||||
]]
|
|
||||||
function Promise.reject(value)
|
|
||||||
return Promise.new(function(_, reject)
|
|
||||||
reject(value)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Returns a new promise that:
|
|
||||||
* is resolved when all input promises resolve
|
|
||||||
* is rejected if ANY input promises reject
|
|
||||||
]]
|
|
||||||
function Promise.all(...)
|
|
||||||
error("unimplemented", 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Is the given object a Promise instance?
|
|
||||||
]]
|
|
||||||
function Promise.is(object)
|
|
||||||
if type(object) ~= "table" then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
return object._type == "Promise"
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Creates a new promise that receives the result of this promise.
|
|
||||||
|
|
||||||
The given callbacks are invoked depending on that result.
|
|
||||||
]]
|
|
||||||
function Promise:andThen(successHandler, failureHandler)
|
|
||||||
self._unhandledRejection = false
|
|
||||||
|
|
||||||
-- Create a new promise to follow this part of the chain
|
|
||||||
return Promise.new(function(resolve, reject)
|
|
||||||
-- Our default callbacks just pass values onto the next promise.
|
|
||||||
-- This lets success and failure cascade correctly!
|
|
||||||
|
|
||||||
local successCallback = resolve
|
|
||||||
if successHandler then
|
|
||||||
successCallback = createAdvancer(successHandler, resolve, reject)
|
|
||||||
end
|
|
||||||
|
|
||||||
local failureCallback = reject
|
|
||||||
if failureHandler then
|
|
||||||
failureCallback = createAdvancer(failureHandler, resolve, reject)
|
|
||||||
end
|
|
||||||
|
|
||||||
if self._status == Promise.Status.Started then
|
|
||||||
-- If we haven't resolved yet, put ourselves into the queue
|
|
||||||
table.insert(self._queuedResolve, successCallback)
|
|
||||||
table.insert(self._queuedReject, failureCallback)
|
|
||||||
elseif self._status == Promise.Status.Resolved then
|
|
||||||
-- This promise has already resolved! Trigger success immediately.
|
|
||||||
successCallback(unpack(self._value))
|
|
||||||
elseif self._status == Promise.Status.Rejected then
|
|
||||||
-- This promise died a terrible death! Trigger failure immediately.
|
|
||||||
failureCallback(unpack(self._value))
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Used to catch any errors that may have occurred in the promise.
|
|
||||||
]]
|
|
||||||
function Promise:catch(failureCallback)
|
|
||||||
return self:andThen(nil, failureCallback)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Yield until the promise is completed.
|
|
||||||
|
|
||||||
This matches the execution model of normal Roblox functions.
|
|
||||||
]]
|
|
||||||
function Promise:await()
|
|
||||||
self._unhandledRejection = false
|
|
||||||
|
|
||||||
if self._status == Promise.Status.Started then
|
|
||||||
local result
|
|
||||||
local bindable = Instance.new("BindableEvent")
|
|
||||||
|
|
||||||
self:andThen(function(...)
|
|
||||||
result = {...}
|
|
||||||
bindable:Fire(true)
|
|
||||||
end, function(...)
|
|
||||||
result = {...}
|
|
||||||
bindable:Fire(false)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local ok = bindable.Event:Wait()
|
|
||||||
bindable:Destroy()
|
|
||||||
|
|
||||||
return ok, unpack(result)
|
|
||||||
elseif self._status == Promise.Status.Resolved then
|
|
||||||
return true, unpack(self._value)
|
|
||||||
elseif self._status == Promise.Status.Rejected then
|
|
||||||
return false, unpack(self._value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Promise:_resolve(...)
|
|
||||||
if self._status ~= Promise.Status.Started then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If the resolved value was a Promise, we chain onto it!
|
|
||||||
if Promise.is((...)) then
|
|
||||||
-- Without this warning, arguments sometimes mysteriously disappear
|
|
||||||
if select("#", ...) > 1 then
|
|
||||||
local message = (
|
|
||||||
"When returning a Promise from andThen, extra arguments are " ..
|
|
||||||
"discarded! See:\n\n%s"
|
|
||||||
):format(
|
|
||||||
self._source
|
|
||||||
)
|
|
||||||
warn(message)
|
|
||||||
end
|
|
||||||
|
|
||||||
(...):andThen(function(...)
|
|
||||||
self:_resolve(...)
|
|
||||||
end, function(...)
|
|
||||||
self:_reject(...)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
self._status = Promise.Status.Resolved
|
|
||||||
self._value = {...}
|
|
||||||
|
|
||||||
-- We assume that these callbacks will not throw errors.
|
|
||||||
for _, callback in ipairs(self._queuedResolve) do
|
|
||||||
callback(...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Promise:_reject(...)
|
|
||||||
if self._status ~= Promise.Status.Started then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
self._status = Promise.Status.Rejected
|
|
||||||
self._value = {...}
|
|
||||||
|
|
||||||
-- If there are any rejection handlers, call those!
|
|
||||||
if not isEmpty(self._queuedReject) then
|
|
||||||
-- We assume that these callbacks will not throw errors.
|
|
||||||
for _, callback in ipairs(self._queuedReject) do
|
|
||||||
callback(...)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- At this point, no one was able to observe the error.
|
|
||||||
-- An error handler might still be attached if the error occurred
|
|
||||||
-- synchronously. We'll wait one tick, and if there are still no
|
|
||||||
-- observers, then we should put a message in the console.
|
|
||||||
|
|
||||||
self._unhandledRejection = true
|
|
||||||
local err = tostring((...))
|
|
||||||
|
|
||||||
spawn(function()
|
|
||||||
-- Someone observed the error, hooray!
|
|
||||||
if not self._unhandledRejection then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Build a reasonable message
|
|
||||||
local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
|
|
||||||
err,
|
|
||||||
self._source
|
|
||||||
)
|
|
||||||
warn(message)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Promise
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
return function()
|
|
||||||
local Promise = require(script.Parent.Promise)
|
|
||||||
|
|
||||||
describe("Promise.new", function()
|
|
||||||
it("should instantiate with a callback", function()
|
|
||||||
local promise = Promise.new(function() end)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should invoke the given callback with resolve and reject", function()
|
|
||||||
local callCount = 0
|
|
||||||
local resolveArg
|
|
||||||
local rejectArg
|
|
||||||
|
|
||||||
local promise = Promise.new(function(resolve, reject)
|
|
||||||
callCount = callCount + 1
|
|
||||||
resolveArg = resolve
|
|
||||||
rejectArg = reject
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
|
|
||||||
expect(callCount).to.equal(1)
|
|
||||||
expect(resolveArg).to.be.a("function")
|
|
||||||
expect(rejectArg).to.be.a("function")
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Started)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should resolve promises on resolve()", function()
|
|
||||||
local callCount = 0
|
|
||||||
|
|
||||||
local promise = Promise.new(function(resolve)
|
|
||||||
callCount = callCount + 1
|
|
||||||
resolve()
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(callCount).to.equal(1)
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should reject promises on reject()", function()
|
|
||||||
local callCount = 0
|
|
||||||
|
|
||||||
local promise = Promise.new(function(resolve, reject)
|
|
||||||
callCount = callCount + 1
|
|
||||||
reject()
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(callCount).to.equal(1)
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should reject on error in callback", function()
|
|
||||||
local callCount = 0
|
|
||||||
|
|
||||||
local promise = Promise.new(function()
|
|
||||||
callCount = callCount + 1
|
|
||||||
error("hahah")
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(callCount).to.equal(1)
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
|
||||||
expect(promise._value[1]:find("hahah")).to.be.ok()
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("Promise.resolve", function()
|
|
||||||
it("should immediately resolve with a value", function()
|
|
||||||
local promise = Promise.resolve(5)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
|
||||||
expect(promise._value[1]).to.equal(5)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should chain onto passed promises", function()
|
|
||||||
local promise = Promise.resolve(Promise.new(function(_, reject)
|
|
||||||
reject(7)
|
|
||||||
end))
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
|
||||||
expect(promise._value[1]).to.equal(7)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("Promise.reject", function()
|
|
||||||
it("should immediately reject with a value", function()
|
|
||||||
local promise = Promise.reject(6)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
|
||||||
expect(promise._value[1]).to.equal(6)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should pass a promise as-is as an error", function()
|
|
||||||
local innerPromise = Promise.new(function(resolve)
|
|
||||||
resolve(6)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local promise = Promise.reject(innerPromise)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
|
||||||
expect(promise._value[1]).to.equal(innerPromise)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe("Promise:andThen", function()
|
|
||||||
it("should chain onto resolved promises", function()
|
|
||||||
local args
|
|
||||||
local argsLength
|
|
||||||
local callCount = 0
|
|
||||||
local badCallCount = 0
|
|
||||||
|
|
||||||
local promise = Promise.resolve(5)
|
|
||||||
|
|
||||||
local chained = promise
|
|
||||||
:andThen(function(...)
|
|
||||||
args = {...}
|
|
||||||
argsLength = select("#", ...)
|
|
||||||
callCount = callCount + 1
|
|
||||||
end, function()
|
|
||||||
badCallCount = badCallCount + 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(badCallCount).to.equal(0)
|
|
||||||
|
|
||||||
expect(callCount).to.equal(1)
|
|
||||||
expect(argsLength).to.equal(1)
|
|
||||||
expect(args[1]).to.equal(5)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
|
||||||
expect(promise._value[1]).to.equal(5)
|
|
||||||
|
|
||||||
expect(chained).to.be.ok()
|
|
||||||
expect(chained).never.to.equal(promise)
|
|
||||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
|
||||||
expect(#chained._value).to.equal(0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should chain onto rejected promises", function()
|
|
||||||
local args
|
|
||||||
local argsLength
|
|
||||||
local callCount = 0
|
|
||||||
local badCallCount = 0
|
|
||||||
|
|
||||||
local promise = Promise.reject(5)
|
|
||||||
|
|
||||||
local chained = promise
|
|
||||||
:andThen(function(...)
|
|
||||||
badCallCount = badCallCount + 1
|
|
||||||
end, function(...)
|
|
||||||
args = {...}
|
|
||||||
argsLength = select("#", ...)
|
|
||||||
callCount = callCount + 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(badCallCount).to.equal(0)
|
|
||||||
|
|
||||||
expect(callCount).to.equal(1)
|
|
||||||
expect(argsLength).to.equal(1)
|
|
||||||
expect(args[1]).to.equal(5)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
|
||||||
expect(promise._value[1]).to.equal(5)
|
|
||||||
|
|
||||||
expect(chained).to.be.ok()
|
|
||||||
expect(chained).never.to.equal(promise)
|
|
||||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
|
||||||
expect(#chained._value).to.equal(0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should chain onto asynchronously resolved promises", function()
|
|
||||||
local args
|
|
||||||
local argsLength
|
|
||||||
local callCount = 0
|
|
||||||
local badCallCount = 0
|
|
||||||
|
|
||||||
local startResolution
|
|
||||||
local promise = Promise.new(function(resolve)
|
|
||||||
startResolution = resolve
|
|
||||||
end)
|
|
||||||
|
|
||||||
local chained = promise
|
|
||||||
:andThen(function(...)
|
|
||||||
args = {...}
|
|
||||||
argsLength = select("#", ...)
|
|
||||||
callCount = callCount + 1
|
|
||||||
end, function()
|
|
||||||
badCallCount = badCallCount + 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(callCount).to.equal(0)
|
|
||||||
expect(badCallCount).to.equal(0)
|
|
||||||
|
|
||||||
startResolution(6)
|
|
||||||
|
|
||||||
expect(badCallCount).to.equal(0)
|
|
||||||
|
|
||||||
expect(callCount).to.equal(1)
|
|
||||||
expect(argsLength).to.equal(1)
|
|
||||||
expect(args[1]).to.equal(6)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
|
||||||
expect(promise._value[1]).to.equal(6)
|
|
||||||
|
|
||||||
expect(chained).to.be.ok()
|
|
||||||
expect(chained).never.to.equal(promise)
|
|
||||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
|
||||||
expect(#chained._value).to.equal(0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should chain onto asynchronously rejected promises", function()
|
|
||||||
local args
|
|
||||||
local argsLength
|
|
||||||
local callCount = 0
|
|
||||||
local badCallCount = 0
|
|
||||||
|
|
||||||
local startResolution
|
|
||||||
local promise = Promise.new(function(_, reject)
|
|
||||||
startResolution = reject
|
|
||||||
end)
|
|
||||||
|
|
||||||
local chained = promise
|
|
||||||
:andThen(function()
|
|
||||||
badCallCount = badCallCount + 1
|
|
||||||
end, function(...)
|
|
||||||
args = {...}
|
|
||||||
argsLength = select("#", ...)
|
|
||||||
callCount = callCount + 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
expect(callCount).to.equal(0)
|
|
||||||
expect(badCallCount).to.equal(0)
|
|
||||||
|
|
||||||
startResolution(6)
|
|
||||||
|
|
||||||
expect(badCallCount).to.equal(0)
|
|
||||||
|
|
||||||
expect(callCount).to.equal(1)
|
|
||||||
expect(argsLength).to.equal(1)
|
|
||||||
expect(args[1]).to.equal(6)
|
|
||||||
|
|
||||||
expect(promise).to.be.ok()
|
|
||||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
|
||||||
expect(promise._value[1]).to.equal(6)
|
|
||||||
|
|
||||||
expect(chained).to.be.ok()
|
|
||||||
expect(chained).never.to.equal(promise)
|
|
||||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
|
||||||
expect(#chained._value).to.equal(0)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue