Ledger
Player data as a ledger, not a document. You never write state. You write down the change you want, a function you own decides if it's allowed, and state is what falls out of replaying those changes.
local Store = Ledger.New({
Name = "PlayerData",
Default = { Gold = 100 },
Reducer = function(State, Op)
if Op.Kind == "Earn" then
if type(Op.Amount) ~= "number" or Op.Amount <= 0 then
return nil
end
return { Gold = State.Gold + Op.Amount }
end
if Op.Kind == "SpendGold" then
if type(Op.Amount) ~= "number" or Op.Amount > State.Gold then
return nil -- refused, on every server, forever
end
return { Gold = State.Gold - Op.Amount }
end
return nil -- an op this build has never heard of
end,
})
Store:Load(Player)
Store:Expect(Player):Apply("SpendGold", { Amount = 25 })No session locks
Two servers can write the same key at once. The fold decides who wins and every server gets the same answer. Nothing to lease, nothing to wait out after a crash.
A reducer you own
State is a pure fold over a log of ops. Your reducer decides if each one is allowed, so the bad state is unreachable instead of being caught after the fact.
Money that moves properly
Transfers move a balance through an escrow, deduped by id, and they fix themselves after a crash. Transactions commit two to four keys, all or nothing.
Once, forever
Name an op after a receipt or an order and it applies one time on that key, across compaction, rejoins, and two servers racing the same replay.