ProfileStore

repository·main·Indexed 43 days ago

https://github.com/madstudioroblox/profilestore

A Roblox DataStore wrapper that simplifies player data caching, auto-saving, and session locking to prevent data duplication and corruption. It provides a session-based API for managing player-specific profiles, including support for mock testing in Roblox Studio, version querying for rollbacks, and critical messaging via MessageAsync().

Tokens
9.7K
Snippets
18
Records
36
Agent score
54%

What's inside ProfileStore

  1. Overview of ProfileStore

    main

    ProfileStore is a Roblox DataStore wrapper designed to streamline player data management. It provides built-in support for auto-saving and session locking to prevent data corruption and item "duping" (duplication) in games with trading.

    Key characteristics:

    • Player-Data Oriented: Optimized for use cases where each player has a single profile representing their progress.
    • Session Locking: Prevents multiple game servers from accessing the same data simultaneously, gracefully handling ownership transfers between servers.
    • API Style: Methods follow Roblox API conventions. Methods containing the Async keyword (e.g., :StartSessionAsync()) are yielding functions that wait until a result is ready.
  2. Understand ProfileStore Roblox API usage and limits

    main

    ProfileStore is built on top of Roblox DataStores and MessagingService. Because Roblox imposes limits on how many API requests can be made in a specific timeframe, it is important to understand how ProfileStore's methods consume these limits.

    Key behaviors include:

    • Exponential Backoff: During DataStore outages, ProfileStore may repeat requests using exponential backoff to manage errors.
    • Session Conflict Resolution: ProfileStore uses MessagingService to resolve session conflicts between multiple servers more quickly than DataStores alone.
    • API Consumption: Most ProfileStore operations translate into one or more Roblox UpdateAsync, SetAsync, GetAsync, or PublishAsync calls. High-frequency operations or session conflicts (e.g., after a server crash) will result in higher API usage.
  3. Handle session termination with Profile.OnSessionEnd

    main

    The Profile.OnSessionEnd signal fires when a profile session is no longer active. This happens when:

    • Profile:EndSession() is called.
    • Another server starts a session for the same profile (session theft).
    • The server is shutting down.

    Best Practices:

    • Do NOT use this for final data changes. Use Profile.OnLastSave for that. Use OnLastSave to ensure data is actually written.
    • Security: If a session is stolen by another server, you may want to kick the player from the current server to prevent data corruption or exploitation.
    • Post-Signal State: After this signal fires, no further changes to Profile.Data should be made.
    Profile.OnSessionEnd:Connect(function()
      player:Kick(`Your data has been loaded on another server - please rejoin`)
    end)
  4. How ProfileStore works

    main

    ProfileStore is a Roblox DataStore wrapper designed primarily for player-oriented data (e.g., game progress). It manages data through profiles which are accessed via sessions.

    Core Mechanics

    • Caching & Session Locking: ProfileStore loads and caches data from a DataStore key on a single server. It uses session locking to prevent other servers from accessing the same data simultaneously, which prevents item "duping" in games with trading.
    • Data Access: Once a session is active, you interact with a table via Profile.Data. Changes made to this table are automatically saved to the DataStore during the next auto-save or when the session is manually ended.
    • Concurrency: It uses MessagingService to resolve session lock conflicts between servers more efficiently than older methods, allowing for slower auto-save intervals (defaulting to 300 seconds) without sacrificing responsiveness to external server requests.

    Important Constraints

    • Not for Global State: ProfileStore is not designed for in-game leaderboards or any form of global state.
    • Not for High-Frequency Multi-Server Writes: While it can be used for non-player data, its session locking mechanism is optimized for single-server ownership and is not ideal for data that requires rapid, simultaneous writes from multiple game servers.
  5. How ProfileStore manages data sessions

    main

    ProfileStore operates using two primary abstractions: Profiles and Sessions.

    1. Profiles: These are the units of data saved to a DataStore key. A profile contains a Data table.
    2. Sessions: To interact with a profile, you must start a session.

    Once a session is active, you gain access to a Profile.Data table. Any changes made to this table will be persisted to the DataStore during the next auto-save cycle or when the session is manually ended.

    Warning: ProfileStore is not intended for in-game leaderboards or global state management. Its session-locking mechanism is optimized for player-specific data and is not ideal for high-frequency writes from multiple servers to a single shared key.

  6. Handle profile saving lifecycle with OnSave and OnLastSave

    main

    ProfileStore provides signals to hook into the saving process:

    • Profile.OnSave: Fired right before any save operation (auto-save, manual :Save(), or final session save). Use this for logic that must run before data hits the DataStore. Avoid yielding here.
    • Profile.OnLastSave: Fired right before the final save of a session. It provides a reason string:
      • "Manual": Called via Profile:EndSession().
      • "External": Another server took over the session.
      • "Shutdown": The server is shutting down.

    Use Case: Logout Penalties You can use OnLastSave to penalize players for closing the game improperly (e.g., while in combat), provided the reason is not "Shutdown".

    local InCombat = false
    
    Profile.OnLastSave:Connect(function(reason)
      if reason ~= "Shutdown" then
        -- Penalize player for improper logout
        if InCombat == true then
          Profile.Data.Coins -= 100
        end
      end
    end)
  7. Enable API access for Roblox Studio testing

    main

    By default, data saved with ProfileStore in Roblox Studio will not persist. To allow data to persist during testing, you must enable studio access to API services in your game settings.

    Warning: When studio access to API services is enabled, ProfileStore will write to the live DataStore keys of the game you are editing. This can lead to accidental changes to your live game data. To avoid this, use ProfileStore.Mock during development.

  8. Use ProfileStore.Mock for testing in Roblox Studio

    main

    To avoid saving data to live DataStore keys while testing in Roblox Studio, you can use ProfileStore.Mock. This creates a reflection of the ProfileStore methods but operates on a separate, temporary "fake" DataStore that is wiped when the game server shuts down. ProfileStore and ProfileStore.Mock are isolated; using the same key in both will result in different profiles.

    Use this pattern to automatically switch to mock mode when running in Studio:

    local RunService = game:GetService("RunService")
    local PlayerStore = ProfileStore.New("PlayerData", {})
    if RunService:IsStudio() == true then
      PlayerStore = PlayerStore.Mock
    end
  9. Implement Developer Products using the official Roblox way

    main

    This method follows the standard Roblox MarketplaceService.ProcessReceipt pattern while integrating ProfileStore.

    Key Steps:

    1. Define a productFunctions table where keys are Product IDs and values are functions that modify the player's profile.Data.
    2. Inside the product handler, call profile:Save() immediately after modifying data to secure the purchase against server crashes.
    3. In the processReceipt callback, retrieve the player's profile from your local Profiles table.
    4. Ensure the profile is active using profile:IsActive() == true before granting the item.
    5. Return Enum.ProductPurchaseDecision.PurchaseGranted on success, or Enum.ProductPurchaseDecision.NotProcessedYet if the profile is unavailable, allowing Roblox to retry the transaction later.
    local Profiles: {[player]: typeof(PlayerStore:StartSessionAsync())} = {} -- Reference to your Profiles table
    
    local MarketplaceService = game:GetService("MarketplaceService")
    local Players = game:GetService("Players")
    
    local productFunctions = {}
    
    -- Example: product ID 456456 awards 100 cash
    productFunctions[456456] = function(receipt, player, profile)
        profile.Data.Cash += 100
        -- Perform an instant save to secure the purchase
        profile:Save()
    end
    
    local function processReceipt(receiptInfo)
        local userId = receiptInfo.PlayerId
        local productId = receiptInfo.ProductId
    
        local player = Players:GetPlayerByUserId(userId)
        if player then
            local profile = Profiles[player]
    
            -- Wait for profile to be available if player just joined
            while profile == nil and player.Parent == Players do
                profile = Profiles[player]
                if profile ~= nil then
                    break
                end
                task.wait()
            end
    
            if profile ~= nil and profile:IsActive() == true then
                local handler = productFunctions[productId]
                local success, result = pcall(handler, receiptInfo, player, profile)
                if success then
                    return Enum.ProductPurchaseDecision.PurchaseGranted
                else
                    warn(`Failed to process receipt:`, receiptInfo, result)
                end
            end
        end
    
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end
    
    MarketplaceService.ProcessReceipt = processReceipt
  10. Implement Developer Products by caching PurchaseIds

    main

    For critical purchases (like currency or permanent perks), you can use a more robust method that yields the ProcessReceipt callback until the purchase is confirmed to be saved in the DataStore. This prevents loss of items in the rare event of a server crash occurring between the data modification and the automatic save.

    How it works:

    1. Cache PurchaseIds: Store a list of PurchaseIds in your profile.Data (e.g., profile.Data.PurchaseIdCache).
    2. Yield the Callback: Use a helper function (like PurchaseIdCheckAsync) that yields inside ProcessReceipt.
    3. Verify via LastSavedData: The helper function periodically calls profile:Save() and checks profile.LastSavedData to see if the PurchaseId has been successfully written to the underlying DataStore.
    4. Grant and Confirm: Only return Enum.ProductPurchaseDecision.PurchaseGranted once the PurchaseId is found in profile.LastSavedData.

    Note: This requires maintaining a PurchaseIdCache within your profile data schema.

    -- Implementation snippet for caching PurchaseIds
    function PurchaseIdCheckAsync(profile, purchase_id, grant_product): Enum.ProductPurchaseDecision
        if profile:IsActive() == true then
            local purchase_id_cache = profile.Data.PurchaseIdCache
            if purchase_id_cache == nil then
                purchase_id_cache = {}
                profile.Data.PurchaseIdCache = purchase_id_cache
            end
    
            -- Granting product if not already received in this session
            if table.find(purchase_id_cache, purchase_id) == nil then
                local success, result = pcall(grant_product)
                if success ~= true then
                    return Enum.ProductPurchaseDecision.NotProcessedYet
                end
                table.insert(purchase_id_cache, purchase_id)
            end
    
            -- Wait until the purchase is confirmed to be saved to the DataStore
            local function is_purchase_saved()
                local saved_cache = profile.LastSavedData.PurchaseIdCache
                return if saved_cache ~= nil then table.find(saved_cache, purchase_id) ~= nil else false
            end
    
            if is_purchase_saved() == true then
                return Enum.ProductPurchaseDecision.PurchaseGranted
            end
    
            while profile:IsActive() == true do
                local last_saved_data = profile.LastSavedData
                profile:Save()
    
                if profile.LastSavedData == last_saved_data then
                    profile.OnAfterSave:Wait()
                end
    
                if is_purchase_saved() == true then
                    return Enum.ProductPurchaseDecision.PurchaseGranted
                end
    
                if profile:IsActive() == true then
                    task.wait(10)
                end
            end
        end
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end
  11. Migrating from ProfileService to ProfileStore

    main

    ProfileStore is the successor to ProfileService. While they share similar session-locking mechanisms, there are several key differences to note during a migration:

    Key Improvements

    • Auto-save Period: Increased from 30 seconds to 300 seconds to reduce DataStore calls and improve scalability.
    • Conflict Resolution: Uses MessagingService for faster, more responsive session lock handling.
    • Data Verification: MetaTags have been replaced by Profile.LastSavedData. This field is updated with the version of Profile.Data that was successfully saved to the DataStore.
    • Messaging: GlobalUpdates is replaced by the simpler and faster ProfileStore:MessageAsync().
    • Signals: New signals Profile.OnSave, Profile.OnLastSave, and Profile.OnAfterSave are available to react to DataStore requests.

    Compatibility Warning

    • Data Compatibility: ProfileStore profiles are backwards-compatible with ProfileService. You can load ProfileService data using the same keys in ProfileStore.
    • Risk: If you start using ProfileStore:MessageAsync() on a profile, the older ProfileService module might encounter issues loading that same profile.
    • Recommendation: Test migrations thoroughly in Roblox Studio with API access enabled before deploying to live servers.
  12. Install ProfileStore

    main

    ProfileStore is a ModuleScript that must be run on the server-side only, as it interacts with Roblox DataStores. You can install it using one of two methods:

    Option 1: Roblox Library

    1. Get the library model from the Roblox Creator Store.
    2. Place the ProfileStore ModuleScript inside ServerScriptService (or another preferred server-side location).

    Option 2: GitHub

    Clone or download the source from the ProfileStore repository.