evaera / roblox-lua-promise Goto Github PK
View Code? Open in Web Editor NEWPromise implementation for Roblox
Home Page: https://eryn.io/roblox-lua-promise/
License: MIT License
Promise implementation for Roblox
Home Page: https://eryn.io/roblox-lua-promise/
License: MIT License
Promise.promisify should reject the promise if the function errors instead even after a yield, so it needs to be pcalled on top of using coroutine.wrap.
Promise.is()
will treat any object with an "andThen" function as a Promise object. This causes the library to call the object's andThen
function even if it is not actually a Promise.
This behavior is unexpected.
The library currently checks for:
assert(
successHandler == nil or type(successHandler) == "function",
string.format(ERROR_NON_FUNCTION, "Promise:andThen")
)
assert(
failureHandler == nil or type(failureHandler) == "function",
string.format(ERROR_NON_FUNCTION, "Promise:andThen")
)
which will fail for callable tables that use the __call metamethod, so this code fails the assertion above:
local wrapped = {}
setmetatable(wrapped, {
__call = _wrapped,
})
wrapped.cancel = _cancel
Promise.new():andThen(wrapped, wrapped)
a helper method that could be used in the library might look like:
local function isCallable(value)
if typeof(value) == "function" then
return true
end
if typeof(value) == "table" then
local mt = getmetatable(value)
if mt and rawget(mt, "__call") then
return true
end
end
return false
end
Attempting to use Wally Publish
always returns a server error regardless of the .toml.
Error:
Error: 500 Internal Server Error
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>500 Server Error</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Server Error</h1>
<h2>The server encountered an error and could not complete your request.<p>Please try again in 30 seconds.</h2>
<h2></h2>
</body></html>
Project I'm trying to upload: https://github.com/prooheckcp/RoactMotion
https://eryn.io/roblox-lua-promise/docs/Tour
Under 'Creating a promise', it should be:
local function myFunction()
instead of
local myFunction()
Promise.all but iterates serially
This error happens when a promise is finished and I'm pretty sure its around the ":finally" things.
I think the cause of this bug is because coroutine.close
is not a valid function.
This bug is not too bad though.
Log:
ServerScriptService.Script:2680: invalid argument #1 to 'defer' (function or thread expected)
This will prevent the thread of a cancelled promise from ever resuming. We'll still keep the existing cancellation hook API around so people can cancel work without needing to yield though
expect
can no longer be called with multiple values.
https://github.com/LPGhatguy/roblox-lua-promise/blob/master/lib/init.spec.lua#L73-L78
Because their values are inaccessible anyways.
I encountered a use case where I need to do an array reduce operation with a callback that may return a promise. When the reduce callback returns the first promise, the reduction will continue after each promise resolves.
If that's a feature you would like for this library, I can submit a pull request for it!
it returns a promise
roblox-lua-promise/lib/init.lua
Lines 219 to 221 in 9db7364
If these are not cleared when a promise completes, this can lead to unnecessary prevention of GC. An :andThen
or :finally
can include callbacks with upvalue references. Since promises keep strong references to these callbacks, the callbacks and their upvalue references will never be collected.
Here's a realistic example:
local gameReadyPromise = getGameReadyPromise() -- this promise exists forever and completes when all modules have loaded
-- ...
game.Players.PlayerAdded:Connect(function(player)
gameReadyPromise:andThen(function()
print(player.Name, "has joined the game!")
-- load the player into the game
end)
end)
If any players join the game before gameReadyPromise completes then their player objects can never be garbage collected ultimately due to references originating in _queuedResolve
.
Here's a simple test:
local function testGc(callback)
local gcTester = {}
local weakRef = setmetatable({gcTester}, {__mode = "v"})
callback(gcTester)
gcTester = nil
wait(10)
return #weakRef == 0 and "GCed" or "Not GCed"
end
local persistentPromise = Promise.delay(0)
local didGetGced = testGc(function(upvalueGcTester)
persistentPromise:andThen(function()
local thisUsesUpvalue = upvalueGcTester
end)
persistentPromise:await()
-- the above callback will *never* be called again, so ideally the callback
-- and upvalueGcTester would be garbage collected, but they won't be:
end)
print("Did upvalueGcTester get collected?:", didGetGced)
This can be solved by clearing out or replacing _queuedResolve
, _queuedReject
, and _queuedFinally
when the promise completes (such as in _finalize
).
Because of coroutines!
Promises have an accessible, yet considered private by convention, _source
field to them. It be great if the library could 'stabilize' it by removing the underscore or adding a method like getSource
to the API.
A use case for that could be a test runner that keeps track of running promises and want to print out the source of each one to locate them.
Currently, Promise.defer
uses RunService.Heartbeat
, which should be replaced with task.defer
instead. The current implementation of Promise.defer
results in the Promise being resumed in the next frame, rather than the next invocation point.
Same as promise:andThen(function() return value end)
If there is an unresolved Promise in the list passed to Promise.fold
, it always returns the initial value.
Semantic versioning would help us know when the API has changed in a breaking way and prevent us from relying on the head of the master branch to always be 100% stable. These could be released as versions via Github Releases.
There's a few examples within the post that mention Promise.async
instead of its successor Promise.defer
:
Promise.fromEvent(RunService.Heartbeat, function(resolve)
-- function runs multiple times
if condition then
resolve() -- automatically disconnects
end
end)
Currently, Promise.all will give the error Non-promise value passed into Promise.all at index i
if you attempt to pass in non-promise values:
roblox-lua-promise/lib/init.lua
Lines 491 to 497 in 0c09558
This is a bit different from the javascript Promise.all implementation, which will resolve non-promise values:
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// Expected output: Array [3, 42, "foo"]
While javascript is not always the best place for inspiration, I think this is a convenient feature. You may have some data in a list that can be resolved synchronously, while you may have other data is resolved asynchronously.
For instance, you may want to fetch some data from the network, while other data may already be in a cache. It may not make sense to add the overhead of creating a Promise object for each cached item, which can be resolved synchronously, especially if you're looking up thousands of keys.
local cache = {
a = 5
}
local function fetch(key)
if cache[key] then
return cache[key]
end
return networkFetch(key):andThen(function(result)
cache[key] = result
return result
end)
end
local keys = { "a", "b", "c" }
local results = map(keys, fetch)
Promise.all(results):andThen(...)
I think it would be great to align to the javascript implementation here, but let me know what you think! As far as implementation goes, you could add a check if the value is not a promise in the resolve loop, and add immediately call resolveOne here: https://github.com/evaera/roblox-lua-promise/blob/master/lib/init.lua#L546-L549
Thanks for taking the time to consider this proposal!
local h = Promise.new(function(resolve)
wait(3)
resolve()
end)
local andthen1 = h:andThen(function()
print("this should NOT print, but it does today")
end)
local andthen2 = h:andThen(function()
print("this should print")
end)
andthen1:cancel()
x = Promise.new(executor)
, with executor function executor
,
and then chain onto it with andThen
.
In effect, calling x:andThen(cb)
is registering a callback (cb
) to be run when x
resolves. it returns a new promise, y
, whose executor you do not control; it's controlled by the promise library, and it is what calls cb
internally.
This is different, because the code inside cb
is not related to promise y
in the same way that the code in executor
is related to promise x
I think this is confusing behavior, so we can just change it in a new version
I'm trying to find a solution how to use Promise.fromEvent so
Promise.fromEvent(robloxEvent)
:tap(function ()
print("event occurred")
end)
Will be triggered each time the event fires, is there known guide how to do this? or walkaround ?
Thanks ๐
I'm not really sure if this is intended behavior or not, but I'm creating a new promise to fetch data from the datastore and calling :await() on that promise. Then I'm intentionally rejecting the promise just to see what gets returned.
I see the success returning false and the error returned in my output log as expected, but then I also get a warning saying I have an unhandled promise rejection. I thought calling :await() essentially stops this from happening since self._unhandledRejection gets set to false?
Here's how I'm creating and using the promise
function DSEventQueue:AddEvent(method, ...)
print("[DSEventQueue::AddEvent] Adding event")
local args = { ... }
return Promise.new(function(resolve, reject, onCancel)
-- insert the datastore event into the queue
table.insert(self.events, {
method = method,
args = args,
resolve = resolve,
reject = reject
})
if #self.events == 1 then
connectHeartbeatTick(self)
end
end)
end
local promise = getDSEventQueue:AddEvent(self.datastore.GetAsync, self.datastore, self.key)
local retValues = table.pack(promise:await())
local success = table.remove(retValues, 1)
Inside the heartbeat tick
dsEventQueue.connection = RunService.Heartbeat:Connect(function(dt)
local budget = DataStoreService:GetRequestBudgetForRequestType(dsEventQueue.requestType)
if #dsEventQueue.events == 0 then
dsEventQueue.connection:Disconnect()
dsEventQueue.connection = nil
return
end
if tick() - dsEventQueue.processTime >= dsEventQueue.rate and budget > 0 then
dsEventQueue.processTime = tick()
local event = table.remove(dsEventQueue.events, 1)
local method = event.method
local args = event.args
local resolve = event.resolve
local reject = event.reject
local retValues = table.pack(pcall(method, table.unpack(args)))
local success = table.remove(retValues, 1)
if success then
resolve(table.unpack(retValues))
else
reject(table.unpack(retValues))
end
end
end)
Output log
I'm basically just grabbing the value of a key whose length is larger than 50 characters which results in an error from the datastore
Returns a chained Promise which is resolved if the Promise is resolved, or rejected if the Promise is not
Somewhat related to #59. Right now, finally
acts as a consumer of the root promise. This means if another consumer of the root promise (that is, a sibling of the finally
promise) is cancelled, the root itself will not be cancelled because it still has a second consumer (the finally
promise itself).
If we make it so that finally
forwards rejection values and doesn't reset the state (#59), then we should probably also make it so that finally
does not count as a proper consumer. This way attaching a finally
to a chain is more or less transparent and doesn't affect the logistics of cancellation.
An example where this causes problems:
local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))
local Promise = require("Promise")
local function wrapPromise(x)
return Promise.resolve(x)
end
local test = function()
-- 'a' always has the state 'Started'.
local a = Promise.delay(2)
a:finally(function()
print("Test - this should be called on cancel.")
end)
return a
end
local prom = test()
wait()
prom:cancel() -- This will print 'test' to the console immediately.
local prom2 = wrapPromise(test())
wait()
prom2:cancel() -- This will print 'test' to the console after **2 seconds**, as the promise can't be cancelled.
So, I don't know if I'm making some sort of mistake somewhere or if this is a roblx issue, but for some reason, when I try to use this module the roblox auto-filling doesn't work. What I mean by this is that normally, when you have a module and you require it and then try to call a module function (eg: module.FunctionName()) roblox will give you a list of the functions in that module so it's easier for you to script, I don't know if I explained this correctly, but moving on. Basically, my issue is that when I try to use the promise module, roblox seems to not see that it is a module and simply say "error type" in that little "Auto-filling window"; I don't know why this is happening and it's pretty problematic for me since I've never used promises before and I'm just starting to learn how to use them, if I'm gonna have to memorize how every function is written without ever having used the promise library/module it's gonna be very hard.
Just for clarification, the module DOES work, it's just that without auto-filling and very little knowledge on the module using it is pretty hard.
Promise.some(array<Promise>, amount)
Resolves when at least amount
Promises have resolved with an array of the resolved values in the order that they resolved.
Rejects if enough input Promises rejected that the promise can no longer resolve. Still pending Promises become cancelled if they have no more consumers.
Promise.any
Promise.some
with 1 amount
, except resolved value is value directly instead of an array with one element. This is different than Promise.race, because Promise.race rejects if any input rejects, but Promise.any will only reject if all input promises reject.
Description of the bug: The following error is thrown when yielding in the chain andThen after the resolution of Promise.delay: Promise:726: attempt to index nil with 'next'
.
Frequency of the error (in the this artificial example posted below): 100%
Reproduction steps
Run the following server script:
local ServerStorage = game:GetService("ServerStorage")
local Promise = require(ServerStorage.src.Promise)
Promise.delay(5):andThen(function ()
wait(2)
end)
function Promise.retry(callback, times, ...)
local args, length = {...}, select("#", ...)
return callback(...):catch(function(...)
if times > 0 then
return retry(callback, times - 1, unpack(args, 1, length))
else
return Promise.reject(...)
end
end)
end
Shorthand for:
Promise.race(
Promise.delay(seconds):andThen(function() return Promise.reject("Timed out.") end),
promise
)
Super low-priority issue that serves as merely an inconvenience- I've completely transitioned into using strict typings for all of my projects (sadly, I haven't had the opportunity to take up TypeScript just yet). It would be super helpful if we could have a few types implemented for Promises (namely, a Promise type) so I won't have to use "any" everywhere in my code.
If an error occurs during execution of a Promise which has :expect()
called on it then the only error reported is Error occurred, no output from Lua.
The Promise library collects the correct information about this error but doesn't report it correctly. This appears to be due to expectHelper attempting to call error(..., 3)
. The error function can only accept a string as it's first argument and if given a table gives you that error shown earlier.
They should cancel the internal newPromises
An alternative to wait()
, but instead using consistent timing with its own scheduler. Cancelling should remove it from the queued task list.
Should resolve with the amount of time waited.
This is a utility that is a shorthand for:
Promise.resolve():andThen(callback)
. It begins a Promise chain such that any errors will turn into rejections.
if an error is thrown inside a finally handler of a promise that has been timed out, the promise is cancelled and the Timeout Error does not propagate to the subsequent catch handlers. This leads to unexpected silent failures that gives the appearance the promise is yielding forever in cases where timeout rejection is expected
Promises.resolve():andThen(function(...)
return Promises.delay(10):finally(function()
error("Something went wrong") -- This will silently Error
end)
end)
:timeout(1)
:catch(function() -- Silent failure
warn("The Promise was Rejected :( ") -- This won't run
end)
Promises.resolve():andThen(function(...)
return Promises.delay(10):finally(function()
warn("Did some work here")
end)
end)
:timeout(1)
:catch(function() -- This will run!
warn("The Promise was Rejected :( ") -- Promise was rejected because of Timeout
end)
In the second example, if there's no error in the finally block, the catch handler is correctly invoked due to the :timeout. The expected behavior is for the catch handler to always be triggered by the :timeout, even if there is an error in the finally block.
Currently :finally()
resets the promise state, but this makes certain patterns hard. In JavaScript, promise.finally
does not reset the success state, so we should probably mirror this behavior.
https://discord.com/channels/385151591524597761/402843211707449354/847622059064557638
It would be nice if the library allowed to set a callback for any promise rejections that are not handled, similar to how browsers fire the unhandledrejection
event.
I'm not sure what's the best way to expose that functionality though, maybe a static function to assign on Promise? That would allow a single function to run.
Promise.onUnhandledRejection = function(rejection)
...
end
Maybe a bindable/custom event? (bindable would lose metatables on the rejection values)
Promise.onUnhandledRejection:Connect(function(rejection)
...
end)
Let me know what are your thoughts about this ๐
๐
local w = require(script.Promise)
w.async(function(resolve, reject)
wait(.2)
print('foo')
resolve('bar')
end):andThen(function(data)
wait(.2)
print(data)
return w.async(function(resolve, reject)
wait(.2)
resolve(1)
end)
end):andThen(function(data)
print(data)
end)
Shows
foo nil bar
Expected
foo bar 1
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.