Moud

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:

luau
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) without task.spawn around 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) runs fn if 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:

luau
-- 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) is andThen(nil, fn). A rejection skips every andThen until one has a rejection handler, so the session error and the timeout both end up in the same catch.
  • 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:

luau
-- 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 back true, ...values or false, 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:

luau
-- 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)
end
  • cancel() stops a promise that has not settled. Its onCancel hooks 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 with reason (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:

luau
-- 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)