Skip to main content

PlayerDataStoreManager

This item only works when running on the server. Server

DataStore manager for player that automatically saves on player leave and game close.

TIP

Consider using PlayerDataStoreService instead, which wraps one PlayerDataStoreManager.

This will ensure that the datastores are reused between different services and other things integrating with Nevermore.

local serviceBag = ServiceBag.new()
local playerDataStoreService = serviceBag:GetService(require("PlayerDataStoreService"))

serviceBag:Init()
serviceBag:Start()

local topMaid = Maid.new()

local function handlePlayer(player: Player)
	local maid = Maid.new()

	local playerMoneyValue = Instance.new("IntValue")
	playerMoneyValue.Name = "Money"
	playerMoneyValue.Value = 0
	playerMoneyValue.Parent = player

	maid:GivePromise(playerDataStoreService:PromiseDataStore(Players)):Then(function(dataStore)
		maid:GivePromise(dataStore:Load("money", 0))
			:Then(function(money)
				playerMoneyValue.Value = money
				maid:GiveTask(dataStore:StoreOnValueChange("money", playerMoneyValue))
			end)
	end)

	topMaid[player] = maid
end
Players.PlayerAdded:Connect(handlePlayer)
Players.PlayerRemoving:Connect(function(player)
	topMaid[player] = nil
end)
for _, player in Players:GetPlayers() do
	task.spawn(handlePlayer, player)
end

Functions

new

PlayerDataStoreManager.new(
serviceBagServiceBag.ServiceBag,
robloxDataStoreDataStore,
keyGenerator(player) → string,--

Function that takes in a player, and outputs a key

skipBindingToCloseboolean?
) → PlayerDataStoreManager

Constructs a new PlayerDataStoreManager.

Unless skipBindingToClose is true, this resolves BindToCloseService from the serviceBag to save on game close, so that service must be registered before the serviceBag starts.

DisableSaveOnCloseStudio

PlayerDataStoreManager.DisableSaveOnCloseStudio(selfPlayerDataStoreManager) → ()

For if you want to disable saving in studio for faster close time!

SetLoadRetryOptions

PlayerDataStoreManager.SetLoadRetryOptions(
optionsRetryOptions
) → ()

Overrides the load retry backoff on every datastore this manager creates. See DataStore.SetLoadRetryOptions.

This is the knob that decides how long a player waits on a lock held by a dead server: the ladder runs, and only once it is exhausted is the lock stolen unconditionally. Defaults to ~49s.

INFO

Must be set before the first datastore is created.

SetAutoSaveTimeSeconds

PlayerDataStoreManager.SetAutoSaveTimeSeconds(
autoSaveTimeSecondsnumber?
) → ()

Sets the autosave interval on every datastore this manager creates. See DataStore.SetAutoSaveTimeSeconds. Passing nil disables syncing entirely.

INFO

Must be set before the first datastore is created.

SetSessionMessagingCloseDelaySeconds

PlayerDataStoreManager.SetSessionMessagingCloseDelaySeconds(
secondsnumber
) → ()

Sets the post-graceful-close replication delay on every datastore this manager creates. See DataStore.SetSessionMessagingCloseDelaySeconds.

INFO

Must be set before the first datastore is created.

AddRemovingCallback

PlayerDataStoreManager.AddRemovingCallback(
callbackfunction--

May return a promise

) → ()

Adds a callback to be called before save on removal

RemovePlayerDataStore

PlayerDataStoreManager.RemovePlayerDataStore(
playerOrUserIdPlayer | PlayerUserId
) → ()

Callable to allow manual GC so things can properly clean up. This can be used to pre-emptively cleanup players.

PromiseDataStoreHandle

PlayerDataStoreManager.PromiseDataStoreHandle(
playerOrUserIdPlayer | number
) → Promise<PlayerDataStoreHandle>

Gets the datastore for a player as a counted handle, opening a session if none is live.

Prefer this over PlayerDataStoreManager.PromiseDataStore for anything acting on a player who may not be in this server. Opening their store takes the session lock, which kicks them from wherever they were and keeps them from rejoining until it is dropped -- and destroying the handle is what drops it.

Handles are counted, so several systems can hold the same player's store at once and the session survives until the last handle is destroyed.

NOTE

The join/leave path deliberately does not run through handles. Making a player's presence just another reference would be tidier, but removal is reached from several directions already -- a stolen session, a close request, a failed lock, PlayerRemoving, server shutdown -- and a handle leaked on any of them would hold a player's save open instead of closing it, which is worse than the asymmetry. So a handle never removes a store belonging to a player who is in this server; their own path owns that.

PromiseSessionClosed

PlayerDataStoreManager.PromiseSessionClosed(
playerOrUserIdPlayer | number
) → Promise<()>

Resolves once any removal in flight for this player has saved and closed their session, and immediately when there is nothing being removed.

Destroying the last handle for an absent player starts the save-and-close; it does not wait for it. Tooling that reports back to an operator waits here first, so it says the lock is released only once the write that releases it has actually landed.

GetDataStore

PlayerDataStoreManager.GetDataStore(
playerOrUserIdPlayer | PlayerUserId
) → DataStore?

Gets the datastore for a player. If it does not exist, it will create one.

TIP

Returns nil if the player is in the process of being removed.

PromiseDataStore

PlayerDataStoreManager.PromiseDataStore(
playerOrUserIdPlayer | PlayerUserId
) → Promise<DataStore>

Gets the datastore for a player, waiting for any in-progress removal/save first. Use this in async flows to safely support fast leave/rejoin behavior.

PromiseReadSessionLock

PlayerDataStoreManager.PromiseReadSessionLock(
playerOrUserIdPlayer | number
) → Promise<LockData?>

Reads the session lock on a player's key without opening a session on it.

This is the read side of the tooling path: it answers "who holds this key, and how stale is that claim", whether or not the player is in this server. Resolves nil when the key is unlocked or absent. Reads the stored key, so for a player in this server it reflects their last save rather than unsaved in-memory state.

PromiseUnlockSession

PlayerDataStoreManager.PromiseUnlockSession(
playerOrUserIdPlayer | number
) → Promise<LockData?>--

the lock that was cleared, or nil if it was already unlocked

Clears the session lock on a player's key with a raw write, releasing a claim left behind by a server that died without closing its session.

WARNING

This is a soft lock. A loading session steals it anyway once its retry ladder is exhausted (see PlayerDataStoreManager.SetLoadRetryOptions) -- clearing it early only saves the player that wait.

DANGER

Permitted against a session this server holds, which desynchronizes that session from the key -- its next save either re-writes the lock or reads this as a theft and kicks the player. That is a debug/stress-test capability, not a normal one.

PromiseLockSession

PlayerDataStoreManager.PromiseLockSession(
playerOrUserIdPlayer | number
) → Promise<LockData?>--

the lock that was replaced, or nil if it was unlocked

Claims a player's key with a raw write, under a session this server will never answer for. Parks the key so an inspection is not racing a live server.

WARNING

This is a soft lock, and holds only for as long as a loading session's retry ladder. It is not a way to keep a player out of their data.

DANGER

Permitted against a session this server holds, with the same desynchronizing effect described on PlayerDataStoreManager.PromiseUnlockSession.

PromiseAllSaves

PlayerDataStoreManager.PromiseAllSaves(selfPlayerDataStoreManager) → Promise

Removes all player data stores, and returns a promise that resolves when all pending saves are saved.

On a closing server Roblox fires PlayerRemoving for every player, so a removal is usually already in flight by the time this runs. Those removals do the real save-and-close themselves; this waits for them rather than starting anything of its own.

Show raw api
{
    "functions": [
        {
            "name": "new",
            "desc": "Constructs a new PlayerDataStoreManager.\n\nUnless `skipBindingToClose` is true, this resolves [BindToCloseService] from the serviceBag to\nsave on game close, so that service must be registered before the serviceBag starts.",
            "params": [
                {
                    "name": "serviceBag",
                    "desc": "",
                    "lua_type": "ServiceBag.ServiceBag"
                },
                {
                    "name": "robloxDataStore",
                    "desc": "",
                    "lua_type": "DataStore"
                },
                {
                    "name": "keyGenerator",
                    "desc": "Function that takes in a player, and outputs a key",
                    "lua_type": "(player) -> string"
                },
                {
                    "name": "skipBindingToClose",
                    "desc": "",
                    "lua_type": "boolean?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 114,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "DisableSaveOnCloseStudio",
            "desc": "For if you want to disable saving in studio for faster close time!",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 166,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "SetLoadRetryOptions",
            "desc": "Overrides the load retry backoff on every datastore this manager creates. See\n[DataStore.SetLoadRetryOptions].\n\nThis is the knob that decides how long a player waits on a lock held by a dead server: the ladder\nruns, and only once it is exhausted is the lock stolen unconditionally. Defaults to ~49s.\n\n:::info\nMust be set before the first datastore is created.\n:::",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "options",
                    "desc": "",
                    "lua_type": "RetryOptions"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 185,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "SetAutoSaveTimeSeconds",
            "desc": "Sets the autosave interval on every datastore this manager creates. See\n[DataStore.SetAutoSaveTimeSeconds]. Passing nil disables syncing entirely.\n\n:::info\nMust be set before the first datastore is created.\n:::",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "autoSaveTimeSeconds",
                    "desc": "",
                    "lua_type": "number?"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 205,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "SetSessionMessagingCloseDelaySeconds",
            "desc": "Sets the post-graceful-close replication delay on every datastore this manager creates. See\n[DataStore.SetSessionMessagingCloseDelaySeconds].\n\n:::info\nMust be set before the first datastore is created.\n:::",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "seconds",
                    "desc": "",
                    "lua_type": "number"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 223,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "AddRemovingCallback",
            "desc": "Adds a callback to be called before save on removal",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "callback",
                    "desc": "May return a promise",
                    "lua_type": "function"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 234,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "RemovePlayerDataStore",
            "desc": "Callable to allow manual GC so things can properly clean up.\nThis can be used to pre-emptively cleanup players.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "playerOrUserId",
                    "desc": "",
                    "lua_type": "Player | PlayerUserId\n"
                }
            ],
            "returns": [],
            "function_type": "static",
            "source": {
                "line": 243,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "PromiseDataStoreHandle",
            "desc": "Gets the datastore for a player as a counted handle, opening a session if none is live.\n\nPrefer this over [PlayerDataStoreManager.PromiseDataStore] for anything acting on a player who may\nnot be in this server. Opening their store takes the session lock, which kicks them from wherever\nthey were and keeps them from rejoining until it is dropped -- and destroying the handle is what\ndrops it.\n\nHandles are counted, so several systems can hold the same player's store at once and the session\nsurvives until the last handle is destroyed.\n\n:::note\nThe join/leave path deliberately does *not* run through handles. Making a player's presence just\nanother reference would be tidier, but removal is reached from several directions already -- a\nstolen session, a close request, a failed lock, PlayerRemoving, server shutdown -- and a handle\nleaked on any of them would hold a player's save open instead of closing it, which is worse than\nthe asymmetry. So a handle never removes a store belonging to a player who is in this server;\ntheir own path owns that.\n:::",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "playerOrUserId",
                    "desc": "",
                    "lua_type": "Player | number"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Promise<PlayerDataStoreHandle>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 275,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "PromiseSessionClosed",
            "desc": "Resolves once any removal in flight for this player has saved and closed their session, and\nimmediately when there is nothing being removed.\n\nDestroying the last handle for an absent player *starts* the save-and-close; it does not wait for\nit. Tooling that reports back to an operator waits here first, so it says the lock is released\nonly once the write that releases it has actually landed.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "playerOrUserId",
                    "desc": "",
                    "lua_type": "Player | number"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Promise<()>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 330,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "GetDataStore",
            "desc": "Gets the datastore for a player. If it does not exist, it will create one.\n\n:::tip\nReturns nil if the player is in the process of being removed.\n:::",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "playerOrUserId",
                    "desc": "",
                    "lua_type": "Player | PlayerUserId\n"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "DataStore?"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 348,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "PromiseDataStore",
            "desc": "Gets the datastore for a player, waiting for any in-progress removal/save first.\nUse this in async flows to safely support fast leave/rejoin behavior.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "playerOrUserId",
                    "desc": "",
                    "lua_type": "Player | PlayerUserId\n"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Promise<DataStore>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 372,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "PromiseReadSessionLock",
            "desc": "Reads the session lock on a player's key without opening a session on it.\n\nThis is the read side of the tooling path: it answers \"who holds this key, and how stale is that\nclaim\", whether or not the player is in this server. Resolves nil when the key is unlocked or\nabsent. Reads the stored key, so for a player in this server it reflects their last save rather\nthan unsaved in-memory state.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "playerOrUserId",
                    "desc": "",
                    "lua_type": "Player | number"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Promise<LockData?>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 452,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "PromiseUnlockSession",
            "desc": "Clears the session lock on a player's key with a raw write, releasing a claim left behind by a\nserver that died without closing its session.\n\n:::warning\nThis is a soft lock. A loading session steals it anyway once its retry ladder is exhausted (see\n[PlayerDataStoreManager.SetLoadRetryOptions]) -- clearing it early only saves the player that wait.\n:::\n\n:::danger\nPermitted against a session this server holds, which desynchronizes that session from the key --\nits next save either re-writes the lock or reads this as a theft and kicks the player. That is a\ndebug/stress-test capability, not a normal one.\n:::",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "playerOrUserId",
                    "desc": "",
                    "lua_type": "Player | number"
                }
            ],
            "returns": [
                {
                    "desc": "the lock that was cleared, or nil if it was already unlocked",
                    "lua_type": "Promise<LockData?>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 481,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "PromiseLockSession",
            "desc": "Claims a player's key with a raw write, under a session this server will never answer for. Parks\nthe key so an inspection is not racing a live server.\n\n:::warning\nThis is a soft lock, and holds only for as long as a loading session's retry ladder. It is not a\nway to keep a player out of their data.\n:::\n\n:::danger\nPermitted against a session this server holds, with the same desynchronizing effect described on\n[PlayerDataStoreManager.PromiseUnlockSession].\n:::",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                },
                {
                    "name": "playerOrUserId",
                    "desc": "",
                    "lua_type": "Player | number"
                }
            ],
            "returns": [
                {
                    "desc": "the lock that was replaced, or nil if it was unlocked",
                    "lua_type": "Promise<LockData?>"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 505,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        },
        {
            "name": "PromiseAllSaves",
            "desc": "Removes all player data stores, and returns a promise that\nresolves when all pending saves are saved.\n\nOn a closing server Roblox fires PlayerRemoving for every player, so a removal is usually already\nin flight by the time this runs. Those removals do the real save-and-close themselves; this waits\nfor them rather than starting anything of its own.",
            "params": [
                {
                    "name": "self",
                    "desc": "",
                    "lua_type": "PlayerDataStoreManager"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "Promise"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 561,
                "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
            }
        }
    ],
    "properties": [],
    "types": [],
    "name": "PlayerDataStoreManager",
    "desc": "DataStore manager for player that automatically saves on player leave and game close.\n\n:::tip\nConsider using [PlayerDataStoreService] instead, which wraps one PlayerDataStoreManager.\n:::\n\nThis will ensure that the datastores are reused between different services and other things integrating\nwith Nevermore.\n\n```lua\nlocal serviceBag = ServiceBag.new()\nlocal playerDataStoreService = serviceBag:GetService(require(\"PlayerDataStoreService\"))\n\nserviceBag:Init()\nserviceBag:Start()\n\nlocal topMaid = Maid.new()\n\nlocal function handlePlayer(player: Player)\n\tlocal maid = Maid.new()\n\n\tlocal playerMoneyValue = Instance.new(\"IntValue\")\n\tplayerMoneyValue.Name = \"Money\"\n\tplayerMoneyValue.Value = 0\n\tplayerMoneyValue.Parent = player\n\n\tmaid:GivePromise(playerDataStoreService:PromiseDataStore(Players)):Then(function(dataStore)\n\t\tmaid:GivePromise(dataStore:Load(\"money\", 0))\n\t\t\t:Then(function(money)\n\t\t\t\tplayerMoneyValue.Value = money\n\t\t\t\tmaid:GiveTask(dataStore:StoreOnValueChange(\"money\", playerMoneyValue))\n\t\t\tend)\n\tend)\n\n\ttopMaid[player] = maid\nend\nPlayers.PlayerAdded:Connect(handlePlayer)\nPlayers.PlayerRemoving:Connect(function(player)\n\ttopMaid[player] = nil\nend)\nfor _, player in Players:GetPlayers() do\n\ttask.spawn(handlePlayer, player)\nend\n```",
    "realm": [
        "Server"
    ],
    "source": {
        "line": 51,
        "path": "src/datastore/src/Server/PlayerDataStoreManager.lua"
    }
}