-
Notifications
You must be signed in to change notification settings - Fork 0
API EntityPersistence
High-level controller that serializes entity state into a versioned JSON envelope and stores it through the DataStoreSystem entity.
Important
Audit scope (2026-05): This page was verified against:
FSM/Orchestrator/Core/Factory/PersistenceManager/EntityPersistence.luauFSM/Orchestrator/Core/Factory/PersistenceManager/init.luauFSM/Orchestrator/Core/Factory/BaseEntity.luauFSM/Orchestrator/Core/DataStore/Entity/DataStoreEntity.luauFSM/Orchestrator/Core/Settings.luau
EntityPersistence talks to a runtime DataStore facade whose actual call shape is DataStoreReference:GetAsync(datastoreName, key, maxRetries?), not the type-only DataStore:GetAsync(key) form described in Types.luau.
EntityPersistence sits between BaseEntity serialization and Roblox DataStores.
-
Savecallsentity:Serialize(), wraps the result in a JSON envelope, and writes it. -
Loadreads the raw payload, decodes it, and callsentity:Deserialize(data). -
Updateperforms an atomicUpdateAsyncmutation on the storeddatafield. -
Deleteremoves the key.
Most code should not instantiate the controller manually. Use the singleton facade exposed through Factory.PersistenceManager:
PersistenceManager.LoadState(entity, key)PersistenceManager.SaveState(entity)
That wrapper lazily resolves the server-created DataStoreSystem entity and stamps _IsPersistent / _PersistenceKey into entity context.
type RuntimeDataStoreReference = {
GetAsync: (self: any, datastoreName: string, key: string, maxRetries: number?) -> any,
SetAsync: (self: any, datastoreName: string, key: string, value: any, maxRetries: number?) -> any,
UpdateAsync: (self: any, datastoreName: string, key: string, transform: (any) -> any, maxRetries: number?) -> any,
RemoveAsync: (self: any, datastoreName: string, key: string, maxRetries: number?) -> any,
}
type PersistenceConfig = {
DataStoreName: string,
KeyPrefix: string?,
DataStoreReference: RuntimeDataStoreReference,
}{
"version": 1,
"updatedAt": 1706240000,
"data": { "...persisted fields...": "..." },
"meta": { "...caller metadata...": "..." }
}Note
The envelope version is hard-coded to 1 by encodePayload(...). It is not the runtime entity version used by replication (entity.Version, _v, _versionHistory). See Versioning.
Load path
Caller PersistenceManager EntityPersistence Entity DataStoreSystem DataStoreService
β β β β β β
ββ LoadState(entity, key) βββ β β β β
β ββ Load(entity, key) βββ β β β
β β ββ GetAsync(DataStoreName, KeyPrefix:key) βββ β
β β β β ββ GetAsync(key) βββ
β β β β ββ raw payload / nil ββ
β β ββ raw payload / error βββββββββββββ β
β β β decodePayload(raw) β β
β β ββ Deserialize(data) βββββββββββββββ β
β ββ (ok, data, err) ββββ β β β
Save path
Caller PersistenceManager EntityPersistence Entity DataStoreSystem DataStoreService
β β β β β β
ββ SaveState(entity) βββ β β β β
β ββ Save(entity, key) βββ β β β
β β ββ Serialize() βββββββββββββββββββββ β
β β β encodePayload(data, meta) β β
β β ββ SetAsync(DataStoreName, KeyPrefix:key, json) βββ β
β β β β ββ SetAsync(key, json) βββ
β β β β ββ result / error ββββββ
β β ββ result / error ββββββββββββββββββ β
β ββ (ok, err) ββββββββββ β β β
Initializes module-level logging.
- Must run before
EntityPersistence.new(...). - Idempotent.
- Called automatically by
PersistenceManager.Initialize(Util).
Constructs a controller bound to one DataStore name, one key prefix, and one concrete runtime facade.
-
Parameters:
-
config.DataStoreName(string) -
config.KeyPrefix(string?) -
config.DataStoreReference(RuntimeDataStoreReference)
-
-
Asserts:
- module initialized,
-
DataStoreNamepresent, -
DataStoreReferencepresent.
-
Returns: controller with
Save,Load,Update,Delete.
local controller = EntityPersistence.new({
DataStoreName = "EntityPersistence",
KeyPrefix = "Entity",
DataStoreReference = Orchestrator.GetEntity("DataStoreSystem"),
})Serializes the entity and writes a JSON envelope to makeKey(KeyPrefix, key).
- Verifies
entity.Serializeexists. - Calls
entity:Serialize(). - Calls
encodePayload(data, metadata). - Calls:
DataStore:SetAsync(config.DataStoreName, makeKey(config.KeyPrefix, key), payload)
- Returns
(true, nil)on success.
| Error | Trigger |
|---|---|
EntityMissingSerialize |
entity missing or has no Serialize method |
SerializeFailed |
HttpService:JSONEncode(...) threw |
DatastoreFail |
SetAsync missing or the runtime call threw |
-
metadatadefaults to{}inside the envelope. - The actual return value from Roblox
SetAsyncis ignored; only thrown errors matter. - There is no framework throttle here.
local ok, err = controller:Save(playerEntity, "Player_123", {
ServerJobId = game.JobId,
SavedBy = "PlayerRemoving",
})
if not ok then
warn("Save failed:", err)
endReads, decodes, and optionally deserializes the stored payload.
- Calls:
DataStore:GetAsync(config.DataStoreName, makeKey(config.KeyPrefix, key))
- If the raw value is
nil, returns(true, nil, nil). - Runs
decodePayload(raw)insidepcall. - If
entity.Deserializeexists, callsentity:Deserialize(data). - Returns
(true, data, nil)on success.
| Error | Trigger |
|---|---|
DatastoreFail |
runtime GetAsync threw |
DecodeFailed: <detail> |
malformed JSON / bad payload shape caused decodePayload to throw |
-
rawmay be either a JSON string or a table.decodePayloadaccepts both. - If
decodePayloadsees a table, it returns that table directly asdataandnilasmeta. -
metais not forwarded toentity:Deserialize(...). - For
BaseEntitysubclasses,Deserializewrites through__newindex, so loaded values land inPending+_cache, not immediately into committedData.
Caution
PersistenceManager.LoadState(...) does not call entity:UpdateEntity() after Deserialize. For mutable BaseEntity instances, loaded values become visible through proxy reads immediately, but they do not produce a version bump or committed Data snapshot until a later UpdateEntity().
local ok, data, err = controller:Load(playerEntity, "Player_123")
if not ok then
warn("Load failed:", err)
elseif data == nil then
print("First-time player; using schema defaults")
endcontroller:Update(key: string, mutateFn: (data: { [string]: any }) -> { [string]: any }) -> (boolean, string?)
Performs an atomic DataStore update against the envelope's data field.
EntityPersistence passes a transform into DataStore:UpdateAsync(...) that:
- starts with
current = {}andmeta = {}, - if
oldexists, attemptsdecodePayload(old)insidepcall, - if decoding succeeds, uses decoded
dataandmeta, - computes
nextData = mutateFn(current) or current, - writes
encodePayload(nextData, meta)back.
| Error | Trigger |
|---|---|
DatastoreFail |
runtime UpdateAsync threw or returned nil
|
-
mutateFnis executed inside RobloxUpdateAsync; it may run multiple times. - Returning
nilorfalsefrommutateFndoes not abort the write; the code falls back tocurrentbecause ofor current. - If an existing payload is corrupt, decode failure is swallowed inside
Update, and the mutation proceeds from{}+{}. - Existing
metais preserved across updates when decoding succeeds. -
updatedAtis rewritten byencodePayload(...); envelopeversionstays1.
local ok, err = controller:Update("Player_123", function(data)
data.TotalSessions = (data.TotalSessions or 0) + 1
data.LastSeenAt = os.time()
return data
end)Removes the composed key from the DataStore.
DataStore:RemoveAsync(config.DataStoreName, makeKey(config.KeyPrefix, key))- Returns
(false, "DatastoreFail")ifRemoveAsyncis missing or throws. - Returns
(true, nil)otherwise.
- There is no distinction between βdeleted existing keyβ and βkey did not existβ.
- The removed value is ignored.
local ok, err = controller:Delete("Player_123")- Server-only.
- Initializes
EntityPersistence. - Stores
_core,_settings, and_logger. - Does not construct the controller immediately.
GetController() does this on first use:
local DataStoreSystem = PersistenceManager._core.Factory.Registry.GetEntity("DataStoreSystem")
PersistenceManager.Controller = EntityPersistence.new({
DataStoreName = Settings.DataStore.DataStoreName,
KeyPrefix = Settings.DataStore.KeyPrefix,
DataStoreReference = DataStoreSystem,
})If DataStoreSystem is missing, the manager logs:
DataStoreSystem not found. Persistence disabled.
and returns nil.
- Asserts:
- initialized,
- server-only,
-
Settings.DataStore.PersistenceEnabled == true.
- Sets context:
_PersistenceKey = key_IsPersistent = true
- Delegates to
controller:Load(entity, key). - Logs success or error.
- Same assertions as
LoadState. - Returns early unless
entity._privateProperties.Context._IsPersistentis truthy. - Uses
_PersistenceKeyif present, otherwiseContext.EntityId. - Delegates to
controller:Save(entity, key).
Factory.CreateEntity(...) hooks persistence in two places when params.Persistent is truthy:
-
On create
Factory.PersistenceManager.LoadState(entity, params.PersistenceKey or entityId)
-
On every
StateUpdatedeventtask.defer(function() Factory.PersistenceManager.SaveState(entity) end)
-
On destroy
Factory.PersistenceManager.SaveState(entity)
| Field | Type | Written by | Notes |
|---|---|---|---|
version |
number |
encodePayload |
Always 1
|
updatedAt |
number |
encodePayload |
os.time() at write time |
data |
{ [string]: any } |
entity:Serialize() / Update
|
Persisted entity data |
meta |
{ [string]: any } |
caller metadata / preserved by Update
|
Optional auxiliary data |
local function makeKey(prefix: string?, key: string): string
if prefix and prefix ~= "" then
return prefix .. ":" .. key
end
return key
endExamples:
KeyPrefix |
key |
Final DataStore key |
|---|---|---|
"Entity" |
"Player_123" |
"Entity:Player_123" |
"Door" |
"FrontGate" |
"Door:FrontGate" |
nil |
"RawKey" |
"RawKey" |
| Surface | Failure | Result |
|---|---|---|
EntityPersistence.new |
not initialized / missing config | assertion failure |
Save |
no Serialize method |
(false, "EntityMissingSerialize") |
Save |
JSON encode error | (false, "SerializeFailed") |
Save / Load / Update / Delete
|
runtime DataStore error | (false, "DatastoreFail") |
Load |
bad stored JSON | (false, nil, "DecodeFailed: ...") |
PersistenceManager.LoadState/SaveState |
client-side call | assertion failure |
PersistenceManager.LoadState/SaveState |
persistence disabled in settings | assertion failure |
PersistenceManager.GetController |
DataStoreSystem missing |
logs warning, no-op |
-
Load of a missing key is success, not failure.
(true, nil, nil)means βnew saveβ. -
Envelope
versionis not runtime entity version. It never increments beyond1in audited source. -
metais preserved byUpdate, but ignored byLoad.entity:Deserialize(...)only receivesdata. -
Updatecan overwrite corrupt payloads silently. Decode failures insideUpdatefall back to{}instead of erroring. -
Deletecannot confirm the key existed. It only reports whetherRemoveAsyncthrew. -
Auto-save happens on every
StateUpdatedforPersistententities. If your entity mutates extremely often, you are responsible for controlling update frequency above this layer.
local dataStoreSystem = Orchestrator.GetEntity("DataStoreSystem")
if not dataStoreSystem then
error("DataStoreSystem not available")
end
local controller = EntityPersistence.new({
DataStoreName = "EntityPersistence",
KeyPrefix = "Entity",
DataStoreReference = dataStoreSystem,
})
local ok, err = controller:Save(playerEntity, "Player_123", {
SavedAtServer = game.JobId,
})local ok, data, err = controller:Load(playerEntity, "Player_123")
if not ok then
warn(err)
return
end
if data and playerEntity.UpdateEntity then
-- Commit deserialized Pending values into Data/version history.
playerEntity:UpdateEntity()
endlocal entity = Orchestrator.CreateEntity({
EntityClass = "DoorEntity",
EntityId = "Door_Lobby_01",
Persistent = true,
PersistenceKey = "Door_Lobby_01",
Context = {
Instance = workspace.Doors.Lobby_01,
OwnerId = "server",
},
})What happens:
-
Factory.CreateEntity(...)creates the entity. -
PersistenceManager.LoadState(...)loads persisted data. - Every later
entity:UpdateEntity()triggersStateUpdated. - The factory listener defers
PersistenceManager.SaveState(entity). - Destroy also triggers a final save.
local playerIds = { "123", "456", "789" }
for _, id in ipairs(playerIds) do
local ok, err = controller:Update("Player_" .. id, function(data)
if type(data.Coins) == "number" and data.Coins < 0 then
data.Coins = 0
end
return data
end)
if not ok then
warn("Patch failed for", id, err)
end
end-
API: DataStoreHandler β actual
DataStoreSystemruntime surface and retry FSM - Versioning β runtime entity versions vs persistence envelope version
- Pooling Strategy β pooled entities and persistence interaction on reuse
-
API: BaseEntity β
Serialize,Deserialize,UpdateEntity
Quick Links: Home Β· Quick Start Β· API Reference Β· Architecture Β· Examples Β· Glossary
Copyright: Β© 2026 RBXStateMachine contributors Β· Repository Β· License information