Ledger
Reference

Store

Everything on the object Ledger.New gives you back.

A store owns one datastore name, every key under it, and every session live on this server.

Methods marked player only throw on a store with Keys = "String".

Sessions

Load

Store:Load(Player: Player) -> ()

Player only. Yields while it reads and folds the record, then starts a 30 second autosave. Kicks the player if the load fails.

Calling it twice for the same player warns and does nothing. If they leave mid load, the session is released instead of being left around.

Unload

Store:Unload(Player: Player) -> ()

Player only. Cancels the autosave, pushes queued ops, yields until they're durable.

Get

Store:Get(Player: Player) -> Session<D>?

Player only. The session, or nil if they aren't loaded.

Expect

Store:Expect(Player: Player) -> Session<D>

Player only. The session, or throws. For code that already knows they're loaded.

IsLoaded

Store:IsLoaded(Player: Player) -> boolean

Player only.

WaitForLoaded

Store:WaitForLoaded(Player: Player) -> Session<D>?

Player only. Yields until the session exists. Gives nil if the player left first. This is the one for ProcessReceipt.

Read

Store:Read(Player: Player) -> D?

Player only. The state table, or nil. Shorthand for Get then Get().

Reading any key

Peek

Store:Peek(Key: KeyLike) -> Future<D?, Reason?>

Reads the record and folds it. Works on anyone, online here, elsewhere, or offline. No cache, so it costs a request every time.

A key nobody has written folds to your Default, so nil always means the read failed and the reason says why. Behind means a newer server wrote it, anything else is worth another go.

DidApply

Store:DidApply(Key: KeyLike, Id: string) -> Future<boolean?, Reason?>

Whether a Once name ever applied on that key.

nil means it could not read the record, which is not the same as false. Compare against true rather than trusting truthiness, or a failed read reads as "never granted" and you grant twice.

History

Store:History(Key: KeyLike, Limit: number?) -> Future<{ HistoryEntry }?, Reason?>

Up to 30 days of versions, newest first. Limit is clamped to 1 through 100 and defaults to 25. nil means the listing failed.

PeekVersion

Store:PeekVersion(Key: KeyLike, Version: string) -> Future<D?, Reason?>

Folds an old version. Read only, there's no restore. See Recovery.

Writing any key

Edit

Store:Edit(Key: KeyLike, Kind: string, Fields: { [any]: any }?) -> Future<boolean, Reason?>

Appends one op to any key and waits for the answer. Works whether or not the target is online.

If the key has a live session on this server, Edit still goes through the log, so the session picks it up on its next fold.

Can answer Unresolved when a stuck transaction leg would change the verdict. An edit that leg can't affect answers straight away.

Transfer

Store:Transfer(From: KeyLike, To: KeyLike, Amount: number, Id: string?) -> Future<boolean, Reason?>

Moves Amount of the Balance field from one key to another. Needs the store to name a Balance.

From and To have to be different. Amount has to be positive and finite. Id is 1 to 64 characters when given, and giving one makes a retry safe.

A delivery into a key that was erased answers Held. The money left the sender and waits with them until recovery gives it back.

See Transfers.

Reserve

Store:Reserve(Key: KeyLike, Field: string, Amount: number, Id: string, Options: {
	Hold: number?,
	To: KeyLike?
}?) -> Future<boolean, Reason?>

Sets Amount of a numeric field aside on one key, under the name Id. The field drops by that much straight away, so nothing else can spend it. Field has to name a number field your Default declares. Id is 1 to 64 characters.

Asking again under a name already set aside answers true and sets nothing aside a second time. Asking for more than the field holds answers Refused.

Hold is how long to keep it, in seconds, and defaults to 15 minutes. It lives on the reservation itself, so two reservations on one key can be held for different lengths.

To names the key the units are going to, which is what Grant needs. Leave it out for a reservation you will Confirm on the same key.

A reservation nobody takes is given back once its hold runs out, either by the next Reserve on that key or by the recovery sweep.

See Reservations.

Confirm

Store:Confirm(Key: KeyLike, Id: string) -> Future<boolean, Reason?>

Spends what Id set aside. The units are gone and the field stays where it is.

Answers false if nothing is set aside under that name, so a confirm after a release cannot spend the same units twice. A Grant that already moved the units answers false as well, because they were not spent here.

Answers Held while a Grant is handing those units over. They are not on this key to spend, and only recovery can say where they ended up.

Release

Store:Release(Key: KeyLike, Id: string) -> Future<boolean, Reason?>

Gives back what Id set aside. The field returns to where it was.

Answers Held while a Grant is handing those units to another key. They are not here to give back, and the grant may already have delivered them. Recovery settles it either way, so leave it alone rather than calling again.

Grant

Store:Grant(Key: KeyLike, Id: string) -> Future<boolean, Reason?>

Hands what Id set aside to the key it was reserved To. The units are added there and the hold on this key is settled.

Needs the reservation to name a To. One that doesn't answers false, and Confirm is what you want instead.

Answers Held if the receiving key will not take the units, because it was erased or because the field there is no longer a number. The units stay set aside with the sender until recovery gives them back.

Safe to call again. The receiving key remembers the name, so a second Grant adds nothing and answers false.

A server that dies part way through leaves the units on both keys. Recovery reads the receiver before it gives anything back, so a reservation already handed over is settled rather than returned.

Bump

Store:Bump(Name: string, Field: string, Amount: number) -> Future<boolean, Reason?>

String keyed stores only. Adds Amount to a total spread over 16 keys, named <Name>#0 to <Name>#15. Each server writes its own shard, so servers do not queue behind each other on one key.

Amount has to be positive. A total is spread over keys that cannot see each other, so nothing can be taken back out of one. Anything with a limit belongs on a single key, where Reserve can hold it.

Total

Store:Total(Name: string, Field: string) -> Future<number?, Reason?>

Reads all 16 shards and adds them up. Costs 16 requests, so read it on a timer rather than per frame.

It answers what has been added, not what the keys hold. Every shard starts at whatever your Default says the field is, and there are 16 of them, so that baseline is taken back off. A tally nobody has added to reads 0 whatever the Default is.

Tx

Store:Tx(Id: string, Legs: { TxLeg }) -> Future<boolean, Reason?>

Commits 2 to 4 legs all or nothing. Id comes first and is required, 1 to 50 characters, and has to be stable across retries. Running the same id again answers true and moves nothing.

A leg's shape throws, so a bad key, a duplicate key, a Once on a leg. A leg's Fields answer Invalid instead, the same as Edit, and nothing is prepared when they do.

See Transactions.

Reset

Store:Reset(Key: KeyLike) -> Future<boolean, Reason?>

Puts the key back to Default, keeping _Received and _Held. A record written at a version this server doesn't know answers Behind rather than Refused, because nothing turned the write down, this server just can't read what's there.

A key with a transaction parked on it answers Busy. Resetting it would throw away a leg the transaction still counts as committed. Settle it with Resettle first.

Inspect

Store:Inspect(Key: KeyLike) -> Future<Record?, Reason?>

The record itself rather than the state it folds to: the snapshot, the ops not yet compacted into it, the applied ids, and the version. Peek answers what the player has, Inspect answers what is on the key.

local Record = Store:Inspect(UserId):Wait()
if Record then
	print(`{#Record.Ops} op(s) waiting on top of the snapshot`)
	for _, Op in Record.Ops do
		print(Op.Id, Op.Kind)
	end
end

What it hands back is frozen, like everything else Ledger gives you. To change a key, use Edit, Reset or Erase.

Erase

Store:Erase(Key: KeyLike) -> Future<boolean, Reason?>

Throws the record away and leaves a tombstone. Hands over money this key was sending out first. For 8 days the tombstone refuses anything sent to the key, so money still being sent returns to whoever sent it. See Erase.

The tombstone holds for the full 8 days even if the key is written to again. A session on another server knows nothing about the erase and keeps saving, and those writes no longer cancel the tombstone. Ledger warns when one arrives, because it means the player is still live somewhere. Get them off every server before erasing them.

A key with a transaction parked on it answers Busy, the same as Reset.

false means the record is still there. Check it before you tell anyone their data is gone.

Watching writes

Stale

Store:Stale() -> Observer<string>

A stream of the keys this server has changed. Ledger pushes a key onto it once the write has gone through, so you can refresh a session on that key at once.

Store:Stale():Subscribe(function(Key)
	print(`{Key} changed`)
end)

These writes push a key:

methodwhat it pushes
Edit, Reserve, Confirm, Release, Bump, Resetthe key it wrote
Transferboth keys
Grantboth keys
Txevery leg key, onto that leg's own store

Nothing else pushes. A write the reducer refuses pushes nothing, and so does a read, an Erase and every maintenance method.

Session:Apply and Session:Commit push nothing either. That session already holds the change, so it has nothing to catch up on.

The key is a string. A player store keys on the UserId, so call tonumber on it before you look the player up.

Listeners on this stream run on their own thread and may yield, which the ones on Session:Observe() may not. See Observer.

Use it to flush a session the moment another part of your game writes to its key. See Transactions.

Maintenance

Resettle

Store:Resettle(Key: KeyLike) -> Future<boolean, Reason?>

Settles any transaction leg parked on the key and finishes any transfer set aside on it. true means nothing is left unfinished. Busy means a leg is still waiting on a decision, so try again later.

The recovery sweep calls this for you every minute. It is here for the case the sweep warns about, where it is already following its limit of keys or has given up on one after five goes.

RecoverTransfers

Store:RecoverTransfers(Key: KeyLike) -> Future<boolean, Reason?>

Forces stranded transfers on one key to finish or refund. The sweeper already does this, so it's for support tools.

ClearDelivered

Store:ClearDelivered(Key: KeyLike) -> Future<boolean, Reason?>

Drops delivered transfer ids older than 30 days from the key. Happens on its own.

Destroy

Store:Destroy() -> ()

Saves every live session, frees the name, and takes the store out of the registry. Every method throws afterwards.

On this page