Ledger
Reference

Observer

What Session:Observe gives you back.

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

An observer is a stream of values you can subscribe to. Ledger pushes the new state onto one every time a change goes through, and it hands you that same stream from Session:Observe().

Store:Stale() is the other stream Ledger gives you. It carries the keys this server has changed rather than a state table. Everything on this page works on both.

Subscribe

Observer:Subscribe(Listener: (T) -> ()) -> Connection

The connection has a Connected boolean and a Disconnect method:

local Connection = Session:Observe():Subscribe(function(State)
	print(State.Gold)
end)

Connection:Disconnect()

It fires on every accepted change, including ones that came from another server and turned up when a transfer or transaction settled. It does not fire for a refused op, since nothing changed.

It does not fire on subscribe either. If you need the current value first, call Session:Get() yourself.

Listeners run inline

Your listener is called on the thread doing the write, not on a fresh one. Two consequences.

It must not yield. No task.wait, no :Wait(). Ledger runs it inside a guard that errors if it does. If you need to yield, hand the work to task.spawn and get out.

A listener that throws is caught and warned, and the rest of the listeners still run. One broken HUD update won't stop the others.

Listeners are called over a snapshot of the list, so subscribing or disconnecting from inside a listener is safe and takes effect on the next push rather than partway through this one.

Listeners that may yield

Store:Stale() is the one stream that does not work this way. Ledger calls each of its listeners on a fresh thread, so they can yield. A Flush or a PublishAsync inside one is fine.

Ledger warns when a listener runs for 10 seconds without returning. Every pushed key holds a thread until its listener returns, so one that never returns costs a thread per write.

Map

Observer:Map(Transform: (T) -> U) -> Observer<U>
Session:Observe():Map(function(State)
	return State.Gold
end):Subscribe(function(Gold)
	GoldLabel.Text = tostring(Gold)
end)

Filter

Observer:Filter(Predicate: (T) -> boolean) -> Observer<T>
Session:Observe():Filter(function(State)
	return State.Gold == 0
end):Subscribe(ShowBrokeMessage)

Changed

Observer:Changed(Equals: ((T, T) -> boolean)?) -> Observer<T>

Drops a value when it's the same as the one before it. Without an Equals it compares with ==.

Calling Changed() straight on Session:Observe() filters nothing. Every fold builds a new state table, so == is comparing identity and no two pushes are ever equal.

Compare the thing you actually care about, either with a comparer:

Session:Observe():Changed(function(Was, Now)
	return Was.Gold == Now.Gold
end):Subscribe(UpdateGoldLabel)

or by mapping down to it first, which is usually what you meant:

Session:Observe()
	:Map(function(State) return State.Gold end)
	:Changed()
	:Subscribe(UpdateGoldLabel)

Use

Observer:Use(Middleware: (Value: T, Emit: (U) -> ()) -> ()) -> Observer<U>

The general form the other three are built on. Emit as many times as you like, or not at all:

Session:Observe():Use(function(State, Emit)
	for _, Item in State.Items do
		Emit(Item)
	end
end):Subscribe(print)

Chains are lazy

Map, Filter, Changed and Use don't do anything until something subscribes to the end of the chain. The first subscriber wires it up to the source, and the last one to disconnect tears it back down.

So building a chain and never subscribing costs nothing, and a chain whose subscribers have all gone stops pulling from the session on its own.

Cleaning up

Nothing disconnects your listeners for you. The session does not clear its observers when the player leaves, it only stops pushing to them, so they stay connected and never fire again.

In practice that's fine. Ledger drops the session when the player leaves, and if you didn't keep the connection anywhere it gets collected along with it.

If you did store connections somewhere long lived, disconnect them yourself:

local Connections: { [Player]: any } = {}

Players.PlayerAdded:Connect(function(Player)
	Store:Load(Player)
	Connections[Player] = Store:Expect(Player):Observe():Subscribe(function(State)
		UpdateHud(Player, State)
	end)
end)

Players.PlayerRemoving:Connect(function(Player)
	local Connection = Connections[Player]
	if Connection then
		Connection:Disconnect()
		Connections[Player] = nil
	end
	Store:Unload(Player)
end)

Destroy

Observer:Destroy() -> ()

Drops every listener and runs the teardown.

Don't call this on Session:Observe(). It hands back the session's own stream rather than a copy, so destroying it kills change notifications for everything else watching that player. Destroy your own chains if you want, never the source.

On this page