Ledger
Reference

Types

What Ledger exports for strict mode.

Everything here is exported off the top level module, so Ledger.Op, Ledger.Store and so on.

Op

type Op = {
	Id: string,
	Kind: string,
	[any]: any,
}

One change. Id and Kind are Ledger's, everything else is yours.

Id, Kind and OnceAt are reserved. Passing any of them in Fields warns and gets overwritten.

Once is yours to set, and it's what makes the op apply at most one time on that key. See Once.

Every method that can fail answers (value?, Reason?) or (boolean, Reason?). Nothing in Ledger throws into a Future, so Wait never comes back empty on you. See Reasons.

Reducer

type Reducer<S> = (State: S, Op: Op) -> S?

See Writing a reducer.

Reason

type Reason =
	"Refused" | "Busy" | "Spent" | "Unresolved" | "Closed"
	| "Backlog" | "Full" | "Invalid" | "Behind"

Compare against Ledger.Reason.Refused and friends rather than the literals. See Reasons.

KeyLike

type KeyLike = number | string

A UserId on a player store, a key string on an entity store. Ledger checks which one the store wants and throws with what it expected.

KeysMode

type KeysMode = "Player" | "String"

Config

type Config<D> = {
	read Name: string,
	read Reducer: (State: D, Op: Op) -> unknown,
	read Default: D,
	read Balance: string?,
	read Migrations: { Migration }?,
	read Keys: KeysMode?,
	read OnLoadFailed: ((Player: Player, Why: Reason) -> boolean)?,
}

What Ledger.New takes. See Ledger.New.

Migration

type Migration =
	((State: any) -> any)
	| { Apply: (State: any) -> any, Compatible: boolean? }

A plain function, or a table when you need Compatible. See Migrations.

TxLeg

type TxLeg = {
	Store: Store<any>?,
	UserId: number?,
	Key: string?,
	Kind: string,
	Fields: { [any]: any }?,
}

One leg of a transaction. Store defaults to the one you called Tx on. Use UserId for a player store and Key for a string keyed one.

HoldOptions

type HoldOptions = {
	Hold: number?,
	To: KeyLike?,
}

The options table Store:Reserve takes last. Hold is how long to keep the units, in seconds, and defaults to 15 minutes. To names the key the units are going to, which is what Grant needs. See Reservations.

HistoryEntry

type HistoryEntry = {
	Version: string,
	At: number,
	Deleted: boolean,
}

At is Unix seconds. Version is what you hand to PeekVersion.

Future

What every method that touches the datastore hands back. The work starts at the call, :Wait() parks your thread until it's done.

local Job = Store:Peek(UserId)
DoSomethingElse()
local State = Job:Wait()

See Future for timeouts, error handling, and why a failed read gives you nil.

Observer

What Session:Observe() and Store:Stale() hand back. Subscribe for every change that goes through, and chain with Map, Filter and Changed.

Session:Observe():Subscribe(function(State)
	UpdateHud(Player, State)
end)

See Observer.

Typing your own state

Write the state type yourself and let the store carry it:

export type Profile = {
	Gold: number,
	Items: { string },
}

local function Reducer(State: Profile, Op: Ledger.Op): Profile?
	-- ...
end

local Store: Ledger.Store<Profile> = Ledger.New({
	Name = "PlayerData",
	Default = { Gold = 100, Items = {} } :: Profile,
	Reducer = Reducer,
})

Session:Get() then gives you a Profile, and the reserved fields stay out of your type. They're there at runtime, your reducer passes them through with table.clone, and you don't have to declare them.

On this page