Bundles, reactions and derived fields
Facets add behavior to the data itself, so it happens on every write without the code that made the write knowing about it.
Bundles
A bundle owns a group of fields that must stay consistent, like experience and level. Its normalize function runs whenever any of its fields change, in the same transaction.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Geode = require(ReplicatedStorage.Packages.Geode)
local levels = Geode.bundle({ "xp", "level" }, function(tx)
local xp = tx:get("xp") :: number
local level = tx:get("level") :: number
while xp >= level * 100 do
xp -= level * 100
level += 1
end
tx:set("xp", xp)
tx:set("level", level)
end)
local PlayerData = Geode.players({
name = "PlayerData",
template = { xp = 0, level = 1, coins = 0 },
bundles = { levels },
})
Reactions
A reaction is a one-way effect: when its dependencies change, it writes somewhere else in the same transaction. Reactions declare what they write, so cycles are caught when the store is made instead of looping in a live server. They run in dependency order, and a reaction that writes outside its declaration fails the transaction.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Geode = require(ReplicatedStorage.Packages.Geode)
local levelReward = Geode.react({ "level" }, function(tx)
if tx:get("level") ~= tx:getBefore("level") then
tx:increment("coins", 50)
end
end, { writes = { "coins" } })
local PlayerData = Geode.players({
name = "PlayerData",
template = { level = 1, coins = 0 },
reactions = { levelReward },
})
Derived fields
A derived field is computed from other fields and never saved. It reads like normal data, can be observed and can be replicated.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Geode = require(ReplicatedStorage.Packages.Geode)
local power = Geode.derive("power", { "level", "inventory" }, function(state)
return state.level * 10 + #state.inventory
end)
local Profiles = Geode.Profile.store({
name = "Profiles",
template = { level = 1, inventory = {} },
derived = { power },
})
local result = Profiles:load("player_1")
if result.ok then
print(result.profile:get("power"))
result.profile:release()
end
Derived values are cached and only recomputed when one of their dependencies changes.