Promises#
A promise is a value that stands for work that finishes later: an http answer, a door that is still
opening, a player who has not clicked yet. You attach what happens next to it, and you can cancel it
or give it a deadline. It comes with Moud as require("@moud/promise").
Making one#
A door that slides open over a second is work that finishes later. Wrapped in a promise, whoever opens it can wait for it, chain on it or cancel it:
local Promise = require("@moud/promise")
local function open(door: Part)
return Promise.new(function(resolve, reject, onCancel)
local slide = door:tween({ cframe = door.cframe * cframe(0, 4, 0) }, { time = 1 })
onCancel(function()
slide:cancel()
end)
slide.completed:wait()
resolve()
end)
end- The function runs straight away inside its own thread, so it can wait (
task.wait,signal:wait,http.get) withouttask.spawnaround it. resolve(...)settles the promise with values,reject(...)with a reason. An error inside the function rejects it with the error. Only the first of these counts.onCancel(fn)runsfnif the promise is cancelled before it settles, to stop what the promise started.
The other ways to make one:
Promise.resolve(...) |
already resolved with these values |
Promise.reject(reason) |
already rejected |
Promise.try(fn, ...) |
runs fn(...) in a thread, resolves with what it returns |
Promise.delay(seconds) |
resolves after that many seconds of game time |
Promise.fromSignal(signal, predicate?) |
resolves with the next firing, or the next one predicate accepts |
Promise.is(value) |
true for a promise |
What happens next#
A player's save is the usual thing to wait for. Opening a session can take a moment under a fleet, and fails when another server still holds it, so the server gives it ten seconds before it lets the player in or turns them away:
-- server
local Promise = require("@moud/promise")
local saves = store("players")
local sessions = {}
game.players.joined:connect(function(player)
Promise.try(function()
return saves:session(player.id)
end)
:timeout(10, "your save took too long to load")
:andThen(function(session)
sessions[player] = session
session.data.coins = session.data.coins or 0
player:spawn(vec3(0, 66, 0))
end)
:catch(function(why)
warn(player.name, why)
player:kick("Your save could not be loaded. Try again in a minute.")
end)
end)
game.players.leaving:connect(function(player)
local session = sessions[player]
sessions[player] = nil
if session then
session:release()
end
end)andThen(onResolved, onRejected?)hands back a new promise for what the function returns. A returned promise is waited for, so steps that wait chain without nesting.catch(fn)isandThen(nil, fn). A rejection skips everyandThenuntil one has a rejection handler, so the session error and the timeout both end up in the samecatch.finally(fn)runs once the promise settles, however it settles, with"resolved","rejected"or"cancelled".- Handlers run in their own thread, so they can wait too.
A rejection that nothing handles is printed as a warning on the next step.
Waiting for one#
Inside a thread you can wait for the result instead of chaining. The same door, opened from a pressure plate that closes it again after three seconds:
-- server
local door = game.world:find("Door") :: Part
local plate = game.world:find("Plate") :: Part
local busy = false
plate.touched:connect(function()
if busy then
return
end
busy = true
task.spawn(function()
if open(door):await() then
task.wait(3)
door.cframe = door.cframe * cframe(0, -4, 0)
end
busy = false
end)
end)await()yields until the promise settles and hands backtrue, ...valuesorfalse, reason.expect()hands back the values, or raises the reason as an error.status()is"started","resolved","rejected"or"cancelled".
Cancelling and timeouts#
A vote at the end of a round asks each player for a map and waits fifteen seconds at most. A player who does not answer gets the first map:
-- server
local vote = game.world:add("Remote", { name = "vote", accepts = "string" })
local function ask(player: Player)
vote:fireClient(player, "choose")
return Promise.fromSignal(vote.onServerPlayer, function(from)
return from == player
end)
:andThen(function(_, map)
return map
end)
:timeout(15)
:catch(function()
return "castle"
end)
endcancel()stops a promise that has not settled. ItsonCancelhooks run, the thread running its function is stopped, and every promise chained from it is cancelled too.- A promise is also cancelled when everything chained from it is cancelled, so cancelling the last step of a chain stops the work at the start of it.
timeout(seconds, reason?)hands back a promise that rejects withreason(or"timed out after N seconds") if this one has not settled in time, and cancels this one when it does.
Several at once#
Promise.all(list) |
resolves with a list of every first value once all resolve, rejects with the first rejection and cancels the rest |
Promise.allSettled(list) |
resolves once all settle, with { status, value } for each |
Promise.race(list) |
settles like the first one to settle and cancels the rest |
The vote above asks everyone at once and counts the answers when the last one is in. Every ask
falls back to "castle", so none of them rejects and all always resolves. A real vote would also
check the map name against the list on offer, see Checking player input:
-- server
local asks = {}
for _, player in game.players:list() do
table.insert(asks, ask(player))
end
Promise.all(asks):andThen(function(maps)
local counts = {}
local best = "castle"
for _, map in maps do
counts[map] = (counts[map] or 0) + 1
if counts[map] > (counts[best] or 0) then
best = map
end
end
print("next map:", best)
end)