Future
What every method that yields hands you back.
Anything in Ledger that touches the datastore gives you a Future rather than yielding on the spot.
local Job = Store:Peek(UserId) -- already running
local State = Job:Wait() -- yields hereThe important part is that the work starts when you call the method, not when you call Wait. The
callback is spawned straight away on its own thread. Wait only parks your thread until it's done.
Running two at once
Because they're eager, starting several and waiting afterwards runs them together:
local A = Store:Peek(FirstUserId)
local B = Store:Peek(SecondUserId)
print(A:Wait().Gold + B:Wait().Gold)Both reads are already running while you sit on the first Wait. Writing it as
Store:Peek(First):Wait() + Store:Peek(Second):Wait() costs you two round trips instead of one.
Wait
Job:Wait(Timeout: number?) -> T...Yields until the callback finishes and gives you whatever it returned. If it's already finished,
Wait returns immediately without yielding at all, so don't lean on it as a way to give up a frame.
Waiting more than once is fine, and so is waiting from several threads.
Wait returns nothing if the callback errored or the timeout ran out. Not false, not nil
as a deliberate answer, just no values at all, which lands in your locals as nil.
Every Ledger method answers with a reason rather than throwing, so in
normal use you don't hit this. A failed Peek gives you (nil, Unresolved), not nothing at all:
local State, Why = Store:Peek(UserId):Wait()
if State == nil then
warn(`could not read that profile: {Why}`)
return
endA timeout still bites, and so does any Future you build yourself. In both cases the missing first
return is nil, which is falsy, so if Ok then and if State == nil then both do the right thing.
You lose Why though. It comes back nil as well, so a timeout reads exactly like a failure.
Happened answers false for both, so it cannot tell them apart either.
Timeout
local Ok, Why = Store:Edit(UserId, "GrantItem", { Item = "Sword" }):Wait(10)After 10 seconds your thread resumes with no values. The work carries on in the background, it isn't
cancelled, and if it finishes later the result is still there for a second Wait.
Don't put a timeout on a write. (nil, nil) looks the same as a refusal, so a caller reads a
timeout as "it didn't happen" and asks again under a new name, which moves the money twice. The
timeout doesn't cancel the write either. Call Wait() with no timeout to get the real answer, and
let the reason tell you what happened.
Happened
Job:Happened(Wait: boolean?) -> booleanWhether the callback ran to completion without erroring.
Called while the job is still going it gives you false straight away rather than waiting. So the
plain form is for after you already waited:
local Job = Store:Peek(UserId)
local State = Job:Wait()
if not Job:Happened() then
warn("that read failed")
endPass true and it waits for the answer first. That's the one to use when you only care whether it
worked and never wanted the value:
local Job = Store:ClearDelivered(UserId)
-- ... do other things while it runs ...
if not Job:Happened(true) then
warn("that housekeeping pass failed, the next one picks it up")
endHappened(true) yields exactly like Wait does, so don't reach for it somewhere that can't yield.
This is the only way to tell an empty result apart from a failure.
Happened answers whether the callback ran, not whether what you asked for worked. A refused
Edit gives you a future that ran perfectly well and answered (false, Refused), so
Happened() is true. Read the boolean for the outcome, Happened for whether there is an
outcome at all.
It has one rough edge. A job that's still running and a job that failed both report
false, so straight after a Wait that timed out you get false from a job that's doing fine and
will finish a moment later. If you used a timeout, use Happened(true). It waits for the job before
it answers, so the answer means something.
Errors don't propagate
A callback that throws is caught. Ledger warns with Future callback errored: and the message, the
future settles as not happened, and Wait gives you nothing. It will not rethrow into your thread,
so a pcall around :Wait() catches nothing useful.
If you want a failed read to throw in your own code, check Happened and raise it yourself.
Fire and forget
You don't have to wait at all. The work still runs.
Store:ClearDelivered(UserId) -- no :Wait(), still happensReasonable for maintenance calls. Not reasonable for anything you're about to act on, since you have no idea whether it worked.