Apply and Commit
One is instant and local, the other is durable before it answers.
Both of them write an op. The difference is when you find out it stuck.
Apply | Commit | |
|---|---|---|
| Gives you | (boolean, Reason?) | Future<boolean, Reason?> |
| Yields | no | yes, on :Wait() |
| Durable when it answers | no | yes |
| Costs a datastore request | no | yes |
| Sees other servers' writes | no | yes |
Apply
Apply runs your reducer against live state, updates it, tells observers, and queues the op. It
never touches the datastore. The op goes out on the next autosave, which runs every 30 seconds, or
on the next Flush, Commit or Unload.
local Ok, Why = Session:Apply("SpendGold", { Amount = 25 })Use it by default, for whatever the player is doing right now. Spending, picking things up, progression, stats.
The answer is local. It knows what this server has, and not what another server appended a second ago. Normally a player writes from one server only, so that's fine.
When the answer changes under you
Apply returns true straight away, and the op only reaches the log on the next save. If another
server wrote to that key in between, the fold can refuse your op when it finally lands, after you
already told the player it worked.
You only see it when two different ops compete for the same rule. The same op twice looks fine, since both servers reach the same number either way. Two different ops don't:
-- 100 gold. server A spends 80, server B spends 30, neither knows about the other
A:Apply("SpendGold", { Amount = 80 }) -- A shows 20
B:Apply("SpendGold", { Amount = 30 }) -- B shows 70
-- both ops reach the log. A's lands first, so B's would take the balance negative
-- and the reducer refuses it. B's player watches 70 become 20Nothing is lost or double spent, and every server agrees once it settles. The player on B still saw a number that was never true.
Render from state, not from the return value. The correction then arrives on its own through
Observe. Use Commit where a wrong number for a moment is worse than
waiting.
Commit
Commit pushes everything queued, appends the op, waits for the datastore to take it, refolds from
what came back, and only then answers.
local Ok, Why = Session:Commit("GrantReward", { Item = "Sword" }):Wait()
if Ok then
GiveTheSword()
endUse it when you're about to do something you can't take back. Handing out a purchase, calling a
webhook, telling another service the thing happened. true means the op is in the log, your reducer
took it, and every server will agree from here on.
The fold runs against the real record, so Commit sees other servers' writes. A stuck
transaction leg on the key can still change the answer afterwards. When
that is possible, Commit says Unresolved.
CommitOp
CommitOp takes an op table you built yourself, so you can put a Once name on it or reuse an id
across a retry:
local Ok, Why = Session:CommitOp({
Id = HttpService:GenerateGUID(false),
Kind = "GrantReward",
Item = "Sword",
Once = `order:{OrderId}`,
}):Wait()Id and Kind are required. Once is optional, and it makes the op apply at most one time on that
key. See Once.
Which one
Apply for gameplay, Commit for side effects.
If the player wouldn't notice a rollback, Apply. If someone would file a ticket about it,
Commit.
Flush
Session:Flush() pushes queued ops without adding one. Autosave calls it. You rarely need it
yourself, but it's there for the moment before you do something risky:
Session:Flush():Wait()Many ops, one request
A flush writes the whole queue in one request. The number of ops does not change this. Use it when one action of a player must change more than one part of their data.
| call | requests |
|---|---|
Session:Apply | none |
Session:Flush | one, for the whole queue |
Session:Commit | one, or two when ops are already queued |
Store:Edit | one, and it takes one op |
Session:Apply("Coins", { Amount = 500 })
Session:Apply("Sword")
Session:Flush():Wait()Two ops, and one request. Three Commit calls cost three requests. Each Commit is durable before
it answers. A Commit also writes the queued ops first, if there are any.
The ops go in together. Each op is still folded on its own. If the reducer refuses one op, the other ops stay applied. Write one op when a grant must be all or nothing. Let the reducer make each change for that one op. One op cannot apply in part.