-
Notifications
You must be signed in to change notification settings - Fork 0
API DataStoreHandler
Low-level persistence surface for RBXStateMachine.
Important
Audit scope (2026-05): This page was verified against:
FSM/Orchestrator/Core/DataStore/Entity/DataStoreEntity.luauFSM/Orchestrator/Core/DataStore/FSM/RequestFSM.luauFSM/Orchestrator/Core/DataStore/RetryBehavior.luauFSM/Orchestrator/Core/Types.luauFSM/Orchestrator/Core/Settings.luau
The audited source contains a concrete runtime implementation named DataStoreEntity plus retry orchestration (RequestFSM + RetryBehavior). Types.luau also declares a richer DataStoreHandler / DataStore API (get, caching, ordered stores, IncrementAsync, etc.), but no implementation for that wrapper exists under Core/DataStore/ in the audited tree. This page separates implemented behavior from type-only contract surface.
At runtime, persistence requests flow through a server-side system entity created by Orchestrator:
Orchestrator.CreateEntity({
EntityClass = "DataStoreEntity",
EntityId = "DataStoreSystem",
Context = { Orchestrator = Orchestrator },
})EntityPersistence and any custom server code talk to that entity. Each request is wrapped in a short-lived RequestFSM, which:
- attempts the DataStore call,
- retries with exponential backoff on failure,
- resolves via callbacks,
- rethrows the final error to the caller if all retries fail.
There is no read cache, no 7-second write throttle, no ordered-store wrapper, and no DataStoreHandler.get(...) implementation in the audited runtime source.
| Concern | Source | What it owns |
|---|---|---|
| Runtime facade | Core/DataStore/Entity/DataStoreEntity.luau |
Actual GetAsync / SetAsync / UpdateAsync / RemoveAsync calls |
| Retry FSM | Core/DataStore/FSM/RequestFSM.luau |
Request lifecycle states (Queued, Processing, Retrying, Success, Failed) |
| Retry policy | Core/DataStore/RetryBehavior.luau |
pcall, retry budget, jittered exponential backoff |
| Public type contract | Core/Types.luau |
Declared DataStore / DataStoreHandler types |
| Settings | Core/Settings.luau |
DataStoreName, KeyPrefix, PersistenceEnabled, ResetAccessCountIntervalInSeconds
|
[Server code / EntityPersistence]
β
βββ [DataStoreSystem : DataStoreEntity]
β
βββ [_ExecuteRequest(requestFn, maxRetries)]
β
βββ [CreateStateMachine RequestFSM]
β
βββ [RetryBehavior]
ββ pcall success βββ [Success state]
β βββ [OnSuccess callback]
β βββ [Return result to caller]
ββ pcall failure + retries left βββ [Retrying state]
β βββΊ back to [RetryBehavior]
ββ pcall failure + exhausted βββ [Failed state]
βββ [OnFailure callback]
βββ [Raise final error to caller]
Returns a tiny context table used when the DataStoreSystem entity is created.
-
Reads:
params.Context.Orchestrator -
Returns:
-
Orchestrator(any?)
-
local context = DataStoreEntity:GetContext({
Context = { Orchestrator = Orchestrator },
})
-- { Orchestrator = Orchestrator }No-op hook. DataStoreEntity is a service facade, not a visual entity.
- Behavior: does nothing.
-
Edge case: mutating
ActiveRequests/RateLimitBudgetdoes not trigger any extra side effects here.
Seeds diagnostic counters.
-
Writes:
self.ActiveRequests = 0self.RateLimitBudget = 60
Note
RateLimitBudget is only initialized to 60 as a placeholder/visual field. The audited runtime does not implement an actual token bucket or access-counter reset loop in Core/DataStore/.
Performs DataStoreService:GetDataStore(datastoreName):GetAsync(key) through _ExecuteRequest(...).
-
Parameters:
-
datastoreName(string): Roblox DataStore name. -
key(string): concrete DataStore key. -
maxRetries(number?): retry budget override.nilfalls back to3inside_ExecuteRequest.
-
-
Returns: the raw value from Roblox DataStore (
any), or throws on unrecoverable failure. -
Failure mode: errors are retried by
RequestFSM, then rethrown.
local dataStoreSystem = Orchestrator.GetEntity("DataStoreSystem")
local ok, result = pcall(function()
return dataStoreSystem:GetAsync("EntityPersistence", "Entity:Player_123", 5)
end)
if ok then
print("Loaded raw payload:", result)
else
warn("GetAsync failed:", result)
endDataStoreEntity:SetAsync(datastoreName: string, key: string, value: any, maxRetries: number?) -> any
Performs DataStore:SetAsync(key, value) through _ExecuteRequest(...).
-
Parameters:
-
datastoreName(string) -
key(string) -
value(any): already-serialized payload in most framework usage. -
maxRetries(number?)
-
-
Returns: Roblox native
SetAsyncresult, or throws. -
Behavior:
- No throttle layer is added.
- No cache invalidation layer exists because no cache exists.
local dataStoreSystem = Orchestrator.GetEntity("DataStoreSystem")
local ok, err = pcall(function()
dataStoreSystem:SetAsync(
"EntityPersistence",
"Entity:Player_123",
'{"version":1,"updatedAt":1700000000,"data":{"Coins":10},"meta":{}}',
3
)
end)
if not ok then
warn("SetAsync failed:", err)
endDataStoreEntity:UpdateAsync(datastoreName: string, key: string, transform: (any) -> any, maxRetries: number?) -> any
Performs Roblox UpdateAsync through _ExecuteRequest(...).
-
Parameters:
-
datastoreName(string) -
key(string) -
transform((any) -> any): called by Roblox with the current stored value. -
maxRetries(number?)
-
-
Returns: Roblox native
UpdateAsyncresult, or throws. -
Important:
-
transformmay run multiple times under contention. - Keep
transformpure and deterministic.
-
local HttpService = game:GetService("HttpService")
local dataStoreSystem = Orchestrator.GetEntity("DataStoreSystem")
local ok, result = pcall(function()
return dataStoreSystem:UpdateAsync("EntityPersistence", "Entity:Player_123", function(oldRaw)
local payload
if type(oldRaw) == "string" then
payload = HttpService:JSONDecode(oldRaw)
else
payload = oldRaw or { data = {}, meta = {} }
end
payload.data = payload.data or {}
payload.data.Coins = (payload.data.Coins or 0) + 25
payload.updatedAt = os.time()
payload.version = 1
return HttpService:JSONEncode(payload)
end, 5)
end)Performs DataStore:RemoveAsync(key) through _ExecuteRequest(...).
-
Parameters:
-
datastoreName(string) -
key(string) -
maxRetries(number?)
-
- Returns: Roblox native removed value, or throws.
local dataStoreSystem = Orchestrator.GetEntity("DataStoreSystem")
local ok, removedOrErr = pcall(function()
return dataStoreSystem:RemoveAsync("EntityPersistence", "Entity:Player_123", 3)
end)Internal helper used by all four public request methods.
-
Parameters:
-
requestFn(() -> any): closure that performs the raw DataStore call. -
maxRetries(number?): retry cap; defaults to3.
-
-
Behavior:
- asserts
self.Orchestratorexists, - increments
ActiveRequests, - creates a
BindableEvent, - spawns
RequestFSM, - waits until
OnSuccessorOnFailurefires, - decrements
ActiveRequests, - returns the result or throws the final error.
- asserts
-
Failure modes:
- missing
Orchestratorcontext β immediate error, - failed
RequestFSMcreation β immediate error, - exhausted retries β rethrows
LastError.
- missing
Builds the behavior tree used by RequestFSM.
| Field | Type | Meaning |
|---|---|---|
RequestFunction |
() -> any |
Raw DataStore call wrapped in pcall
|
MaxRetries |
number? |
Retry cap; defaults to 3
|
RetryCount |
number? |
Current retry attempt count |
FSM |
any |
Owning RequestFSM instance |
Result |
any |
Stored successful value |
LastError |
any |
Stored last failure |
OnSuccess |
(any) -> () |
Completion callback |
OnFailure |
(any) -> () |
Failure callback |
local jitter = math.random() * 0.5 + 0.75 -- 0.75x .. 1.25x
local delay = math.min(math.pow(2, RetryCount) * jitter, 30)- First retry waits roughly
1.5sto2.5s. - Delay is capped at
30s. -
RetryCountincrements before the delay is computed.
| State | Purpose | Transition |
|---|---|---|
Queued |
Initial state | immediately enters Processing
|
Processing |
Runs RetryBehavior
|
goes to Success, Retrying, or Failed
|
Retrying |
Wait span already elapsed | immediately re-enters Processing
|
Success |
Resolves OnSuccess(Result)
|
terminal |
Failed |
Resolves OnFailure(LastError)
|
terminal |
[*]
β
βΌ
ββββββββββ
β Queued β
ββββββ¬ββββ
βΌ
ββββββββββββββ
β Processing β
βββ¬βββββββββ¬ββ
β ββ RequestFunction failed and retries exhausted βββ ββββββββββ
β β β Failed β
β β ββββββββββ
ββ RequestFunction succeeded ββββββββββββββββββββββββββββββββ ββββββββββ
β β Successβ
β ββββββββββ
ββ RequestFunction failed and retries remain ββββββββββββββββ ββββββββββββ
β Retrying β
ββββββ¬ββββββ
β
βββ [Processing]
The following signatures are declared, but no matching implementation was found in Core/DataStore/ during this audit.
export type DataStoreHandler = {
ResetAccessCount: () -> (),
get: (databaseName: string, scope: string?, orderedBool: boolean?) -> DataStore,
}export type RetryConfig = {
enabled: boolean?,
retries: number?,
baseDelay: number?,
jitter: number?,
}
export type DataStore = {
CachingTime: number,
RetryConfig: RetryConfig,
SetRetryConfig: (self: DataStore, config: RetryConfig?) -> (),
EnableRetry: (self: DataStore, enabled: boolean) -> (),
SetCachingTime: (self: DataStore, TimeInSeconds: number) -> (),
GetDBName: (self: DataStore) -> string,
GetAsync: (self: DataStore, Key: string) -> (boolean, any, string?),
SetAsync: (self: DataStore, Key: string, value: any) -> (boolean, nil, string?),
UpdateAsync: (self: DataStore, Key: string, transformFunction: (any) -> any) -> (boolean, any, string?),
IncrementAsync: (self: DataStore, Key: string, delta: number) -> (boolean, number?, string?),
RemoveAsync: (self: DataStore, Key: string) -> (boolean, any, string?),
GetSortedAsync: ((self: DataStore, ascending: boolean, pagesize: number, minValue: number?, maxValue: number?) -> (boolean, any, string?))?,
OnUpdate: any,
}| Feature | Declared in types | Backed by audited runtime source? |
|---|---|---|
DataStoreHandler.get(...) |
β | β |
ResetAccessCount() |
β | β |
CachingTime / SetCachingTime()
|
β | β |
EnableRetry() / SetRetryConfig()
|
β | β |
IncrementAsync() |
β | β |
GetSortedAsync() |
β | β |
GetAsync() / SetAsync() / UpdateAsync() / RemoveAsync()
|
β | β
(DataStoreEntity, but with different runtime signature) |
Caution
EntityPersistence does not call the type-only DataStore:GetAsync(key) form. It calls the runtime facade form: DataStoreReference:GetAsync(datastoreName, key).
| Surface | Failure | What happens |
|---|---|---|
DataStoreEntity:_ExecuteRequest |
self.Orchestrator missing |
throws "DataStoreEntity missing Orchestrator context"
|
CreateStateMachine inside _ExecuteRequest
|
returns nil / errors |
decrements ActiveRequests, destroys bindable, rethrows |
| Raw DataStore call | transient error | retried by RetryBehavior
|
| Raw DataStore call | still failing after retries |
Failed state fires OnFailure; _ExecuteRequest throws final error |
| Calling from client code |
DataStoreService inaccessible |
request function errors; retry loop eventually fails |
Missing method on DataStoreReference
|
e.g. no RemoveAsync
|
caller errors immediately |
-
No cache layer exists. Every successful
GetAsyncgoes straight to Roblox DataStore. -
No write throttle exists. If you need per-key throttling, implement it above
DataStoreEntity. -
ActiveRequestsis live, not cumulative. It increments before the FSM starts and decrements only when success/failure callbacks run. -
UpdateAsynctransform works on the raw stored value. In framework persistence that raw value is usually a JSON string, not a decoded table. -
ResetAccessCountIntervalInSecondsexists in settings but is not consumed here. The setting is currently documentation/config debt unless another subsystem uses it externally. -
Retry is always available at the request-FSM layer. There is no extra toggle in
Core/DataStore/; the caller chooses the retry count viamaxRetries.
local dataStoreSystem = Orchestrator.GetEntity("DataStoreSystem")
if not dataStoreSystem then
warn("DataStoreSystem not registered")
return
end
local ok, rawOrErr = pcall(function()
return dataStoreSystem:GetAsync("EntityPersistence", "Entity:Player_123", 5)
end)
if ok then
print("Raw payload:", rawOrErr)
else
warn("Read failed:", rawOrErr)
endlocal HttpService = game:GetService("HttpService")
local dataStoreSystem = Orchestrator.GetEntity("DataStoreSystem")
local ok, resultOrErr = pcall(function()
return dataStoreSystem:UpdateAsync("EntityPersistence", "Entity:Player_123", function(oldRaw)
local payload
if type(oldRaw) == "string" then
payload = HttpService:JSONDecode(oldRaw)
elseif type(oldRaw) == "table" then
payload = oldRaw
else
payload = { version = 1, updatedAt = os.time(), data = {}, meta = {} }
end
payload.data = payload.data or {}
payload.data.TotalSessions = (payload.data.TotalSessions or 0) + 1
payload.updatedAt = os.time()
return HttpService:JSONEncode(payload)
end, 5)
end)
if not ok then
warn("Update failed:", resultOrErr)
endlocal dataStoreSystem = Orchestrator.GetEntity("DataStoreSystem")
local ok, removedOrErr = pcall(function()
return dataStoreSystem:RemoveAsync("EntityPersistence", "Entity:ObsoleteDoor_44", 3)
end)
if ok then
print("Removed old payload:", removedOrErr)
else
warn("Delete failed:", removedOrErr)
end-
API: EntityPersistence β high-level controller that uses
DataStoreSystem -
Versioning β entity versions,
_versionHistory, and persistence envelope versioning - Replication Pipeline β how persistent entities later replicate runtime deltas
-
API: Settings β
DataStore.*settings
Quick Links: Home Β· Quick Start Β· API Reference Β· Architecture Β· Examples Β· Glossary
Copyright: Β© 2026 RBXStateMachine contributors Β· Repository Β· License information