Skip to content

API DataStoreHandler

iKryptonic edited this page May 1, 2026 · 11 revisions

API: DataStoreHandler

Low-level persistence surface for RBXStateMachine.

Important

Audit scope (2026-05): This page was verified against:

  • FSM/Orchestrator/Core/DataStore/Entity/DataStoreEntity.luau
  • FSM/Orchestrator/Core/DataStore/FSM/RequestFSM.luau
  • FSM/Orchestrator/Core/DataStore/RetryBehavior.luau
  • FSM/Orchestrator/Core/Types.luau
  • FSM/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.


🎯 Overview

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:

  1. attempts the DataStore call,
  2. retries with exponential backoff on failure,
  3. resolves via callbacks,
  4. 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.


πŸ—ΊοΈ Source Map

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

πŸ”„ Runtime Request Flow

[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]

βœ… Implemented Runtime API

DataStoreEntity:GetContext(params: any) -> { Orchestrator: any? }

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 }

DataStoreEntity:ApplyChanges(changes: { [string]: any }) -> ()

No-op hook. DataStoreEntity is a service facade, not a visual entity.

  • Behavior: does nothing.
  • Edge case: mutating ActiveRequests / RateLimitBudget does not trigger any extra side effects here.

DataStoreEntity:Initialize() -> ()

Seeds diagnostic counters.

  • Writes:
    • self.ActiveRequests = 0
    • self.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/.


DataStoreEntity:GetAsync(datastoreName: string, key: string, maxRetries: number?) -> any

Performs DataStoreService:GetDataStore(datastoreName):GetAsync(key) through _ExecuteRequest(...).

  • Parameters:
    • datastoreName (string): Roblox DataStore name.
    • key (string): concrete DataStore key.
    • maxRetries (number?): retry budget override. nil falls back to 3 inside _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)
end

DataStoreEntity: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 SetAsync result, 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)
end

DataStoreEntity: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 UpdateAsync result, or throws.
  • Important:
    • transform may run multiple times under contention.
    • Keep transform pure 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)

DataStoreEntity:RemoveAsync(datastoreName: string, key: string, maxRetries: number?) -> any

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)

DataStoreEntity:_ExecuteRequest(requestFn: () -> any, maxRetries: number?) -> any

Internal helper used by all four public request methods.

  • Parameters:
    • requestFn (() -> any): closure that performs the raw DataStore call.
    • maxRetries (number?): retry cap; defaults to 3.
  • Behavior:
    1. asserts self.Orchestrator exists,
    2. increments ActiveRequests,
    3. creates a BindableEvent,
    4. spawns RequestFSM,
    5. waits until OnSuccess or OnFailure fires,
    6. decrements ActiveRequests,
    7. returns the result or throws the final error.
  • Failure modes:
    • missing Orchestrator context β†’ immediate error,
    • failed RequestFSM creation β†’ immediate error,
    • exhausted retries β†’ rethrows LastError.

πŸ€– Retry Orchestration

RetryBehavior.Create(Orchestrator: any) -> (context: any) -> string

Builds the behavior tree used by RequestFSM.

Retry context fields used by the tree

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

Backoff formula

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.5s to 2.5s.
  • Delay is capped at 30s.
  • RetryCount increments before the delay is computed.

RequestFSM states

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]

πŸ“˜ Type-Only Contract Declared in Types.luau

The following signatures are declared, but no matching implementation was found in Core/DataStore/ during this audit.

Declared DataStoreHandler

export type DataStoreHandler = {
    ResetAccessCount: () -> (),
    get: (databaseName: string, scope: string?, orderedBool: boolean?) -> DataStore,
}

Declared 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,
}

What this means in practice

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).


⚠️ Failure Modes

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

🧩 Edge Cases

  1. No cache layer exists. Every successful GetAsync goes straight to Roblox DataStore.
  2. No write throttle exists. If you need per-key throttling, implement it above DataStoreEntity.
  3. ActiveRequests is live, not cumulative. It increments before the FSM starts and decrements only when success/failure callbacks run.
  4. UpdateAsync transform works on the raw stored value. In framework persistence that raw value is usually a JSON string, not a decoded table.
  5. ResetAccessCountIntervalInSeconds exists in settings but is not consumed here. The setting is currently documentation/config debt unless another subsystem uses it externally.
  6. Retry is always available at the request-FSM layer. There is no extra toggle in Core/DataStore/; the caller chooses the retry count via maxRetries.

πŸš€ Real Code Examples

Example 1: Read the raw persistence envelope

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)
end

Example 2: Atomic JSON patch with UpdateAsync

local 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)
end

Example 3: Delete a key with retries

local 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

πŸ”— See Also

🏠 Home


πŸš€ Getting Started


πŸ—οΈ Architecture


πŸ”§ API Reference


πŸ“š Guides


βš™οΈ Advanced


πŸ› οΈ Development


πŸ“– Reference

Clone this wiki locally