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) -> booleanPlayer 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
endWhat 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:
| method | what it pushes |
|---|---|
Edit, Reserve, Confirm, Release, Bump, Reset | the key it wrote |
Transfer | both keys |
Grant | both keys |
Tx | every 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.