DataKeep - A promise based, auto-saving DataStore module

version 1.2.2: 11/21/2023

Fixed

  • ‘cannot resume dead coroutine’ error on release

  • Fixed UpdatedAsync accidentally yielding causing a failure to save

Improved

  • OnRelease fires quicker
  • Finished issue signal implementation and code reused

Sorry for lack of new features and mostly just bug fixes. A new feature is coming.

1 Like

Don’t apologize for focusing on bug fixes! I’d rather have a stable datastore system with less features, than have an unstable one with more features.

So, I’m a bit confused.

I’m following the example setup exactly, but getting errors.

Example:

Script:

KeepStore:andThen(function(store)
	KeepStore = store
	Players.PlayerAdded:Connect(onPlayerJoin)
end)

Error:

  13:31:43.145  ServerScriptService.StatStuff:126: attempt to call missing method 'andThen' of table  -  Server - StatStuff:126
  13:31:43.145  Stack Begin  -  Studio
  13:31:43.145  Script 'ServerScriptService.StatStuff', Line 126  -  Studio - StatStuff:126
  13:31:43.145  Stack End  -  Studio

What am I doing wrong?

Can I be provided with more context? I am using the exact basic usage in the test place and it works fine

1 Like
local Players = game:GetService("Players")
local KeepStore = require(game.ReplicatedStorage.datakeep)

local DefaultData = {
	"Player",
	{
		Lazercards = 0;
		OwnedGuns = {"StarterPistol"};
		EquippedGun = "StarterPistol";
		Victories = 0;
		Cosmeticards = 0;
		ShirtID = 6536782130;
		PantsID = 129459077;
		R = .75;
		G = .75;
		B = .75;
		OwnedShirts = {6536782130};
		OwnedPants = {129459077};
	}
}

local LoadedKeeps = {}

local function onPlayerJoin(player)
	KeepStore:LoadKeep("Player_" .. player.UserId):andThen(function(keep)
		if keep == nil then
			player:Kick("Data locked") -- will never happen, when no releaseHandler is passed it default steals from the locked session
		end

		keep:Reconcile()
		keep:AddUserId(player.UserId) -- help with GDPR requests

		keep.OnRelease:Connect(function() -- don't have to clean up, it cleans up internally.
			player:Kick("Session Release")
		end)

		if not player:IsDescendantOf(Players) then
			keep:Release()
			return
		end

		print(`Loaded {player.Name}'s Keep!`)

		local leaderstats = Instance.new("Folder")
		leaderstats.Name = "leaderstats"
		leaderstats.Parent = player

		local OwnedGuns = Instance.new("Folder")
		OwnedGuns.Name = "OwnedGuns"
		OwnedGuns.Parent = player

		local OwnedShirts = Instance.new("Folder")
		OwnedShirts.Name = "OwnedShirts"
		OwnedShirts.Parent = player

		local SkinRGB = Instance.new("Folder")
		OwnedShirts.Name = "SkinRGB"
		OwnedShirts.Parent = player

		local OwnedPants = Instance.new("Folder")
		OwnedPants.Name = "OwnedPants"
		OwnedPants.Parent = player

		local Clothing = Instance.new("Folder")
		Clothing.Name = "clothing"
		Clothing.Parent = player

		local EquippedGun = Instance.new("StringValue")
		EquippedGun.Name = "EquippedGun"
		EquippedGun.Parent = player
		EquippedGun.Value = keep.Data.EquippedGun

		local Lazercards = Instance.new("IntValue")
		Lazercards.Name = "Lazercards"
		Lazercards.Parent = leaderstats
		Lazercards.Value = keep.Data.Lazercards

		local Cosmeticards = Instance.new("IntValue")
		Cosmeticards.Name = "Cosmeticards"
		Cosmeticards.Parent = leaderstats
		Cosmeticards.Value = keep.Data.Cosmeticards

		local Tags = Instance.new("IntValue")
		Tags.Name = "Tags"
		Tags.Parent = leaderstats
		Tags.Value = keep.Data.Tags

		local Victories = Instance.new("IntValue")
		Victories.Name = "Victories"
		Victories.Parent = leaderstats
		Victories.Value = keep.Data.Victories

		local Shirt = Instance.new("NumberValue")
		Shirt.Name = "Shirt"
		Shirt.Parent = Clothing
		Shirt.Value = keep.Data.Shirt

		local Pants = Instance.new("NumberValue")
		Pants.Name = "Pants"
		Pants.Parent = Clothing
		Pants.Value = keep.Data.Pants

		local R = Instance.new("IntValue")
		R.Name = "R"
		R.Parent = SkinRGB
		R.Value = keep.Data.R

		local G = Instance.new("IntValue")
		G.Name = "G"
		G.Parent = SkinRGB
		G.Value = keep.Data.G

		local B = Instance.new("IntValue")
		B.Name = "B"
		B.Parent = SkinRGB
		B.Value = keep.Data.B
	end)
end

Players.PlayerRemoving:Connect(function(player)
	local keep = LoadedKeeps[player]
	if keep then return end
	keep:Release()
end)

KeepStore:andThen(function(store)
	KeepStore = store
	Players.PlayerAdded:Connect(onPlayerJoin)
end)

(ik my instance generation is crap)

having a issue, where in game it says
Datastoreservice: UserIdLimitExceeded: UserID size exceeds limit of 4 API: UpdateAsync Datatstore: Inv

is there a way to add a cooldown on the autosaving?

This isn’t a problem with auto saving. You are adding too many userIDs (:AddUserId)

2 Likes

KeepStore is what you are requiring but you need it to be DataKeep.GetStore() try this:

local KeepService = require(game.ReplicatedStorage.datakeep)

...

local KeepStore = KeepService.GetStore("PlayerData"):awaitValue()

(continue)
1 Like

version 1.3.0 & 1.3.1: 11/27/2023

(version skip due to small patch)

Added

You can now use DataKeep with your favorite compression libraries/methods!

See new docs on the function here: Store | DataKeep

1 Like

Autosaving?!? Wait did I miss something, that sounds amazing!

It automatically saves and splits the saves up evenly between the keeps

1 Like

version 2.0.0: 11/28/2023

API Breaking Change for a better design.

:AttachSave → :PreSave & :PreLoad

Allows for more control and is not limited to just compression/decompression but transforming the data however you want.

1 Like

I just realized, the script should be a ModuleScript, shouldn’t it. This whole time I’ve put it in a ServerScript, not thinking about it.

Question, is there an easy way to get a specific player’s keep? Reason being, I want to modify some keeps using the Data Editor plugin.

Edit: Got it myself, here’s how you do it using Free Datastore Editor plugin! [21K installs] [OPEN-SOURCE]

  1. Where it says Datastore, type PlayerData, and press the enter key.
    image

  2. Type Player_[UserID], and press the enter key.
    image

  3. Boom, you’re in! Edit your data as you please. ONLY EDIT THINGS UNDER THE DATA DROPDOWN, OR ELSE YOU MIGHT BREAK THINGS.

2 Likes

version 2.1.0: 11/30/2023

Added

  • Store.validate() for validating data before saving ex: type guards
1 Like

How do you do WriteLibs? I checked the source code, and found 0 mention of them, and when I do KeepService.WriteLib, it tells me “Cannot add property ‘WriteLib’ to table ‘Store’”.

StatManager

local Players = game:GetService("Players")
local KeepService = require(game.ReplicatedStorage.datakeep)

KeepService.WriteLib = require(game.ReplicatedStorage.WriteLib)

local DefaultData = {
	Player = {
		Lang = "English";
		Lazercards = 0;
		OwnedGuns = {"StarterPistol"};
		EquippedGun = "StarterPistol";
		Victories = 0;
		Cosmeticards = 0;
		ShirtID = 6536782130;
		PantsID = 129459077;
		R = .75;
		G = .75;
		B = .75;
		OwnedShirts = {6536782130};
		OwnedPants = {129459077};
		Banned = false,
	}
}

local KeepStore = KeepService.GetStore("PlayerData", DefaultData)

local LoadedKeeps = {}

local function onPlayerJoin(player)
	KeepStore:LoadKeep("Player_" .. player.UserId):andThen(function(keep)
		if keep == nil then
			player:Kick("Data locked") -- will never happen, when no releaseHandler is passed it default steals from the locked session
		end

		keep:Reconcile()
		keep:AddUserId(player.UserId) -- help with GDPR requests

		keep.OnRelease:Connect(function() -- don't have to clean up, it cleans up internally.
			player:Kick("Session Release")
		end)

		if not player:IsDescendantOf(Players) then
			keep:Release()
			return
		end

		local PlayerData = keep.Data
		
		for i, v in pairs(DefaultData.Player) do
			if PlayerData[i] == nil then
				PlayerData[i] = v
			end
		end
		
		print(keep)
		print(PlayerData)

		print(`Loaded {player.Name}'s Keep!`)

		LoadedKeeps[player] = keep

		local leaderstats = Instance.new("Folder")
		leaderstats.Name = "leaderstats"
		leaderstats.Parent = player

		local Lazercards = Instance.new("IntValue")
		Lazercards.Name = "Lazercards"
		Lazercards.Parent = leaderstats
		Lazercards.Value = PlayerData.Lazercards

		local Cosmeticards = Instance.new("IntValue")
		Cosmeticards.Name = "Cosmeticards"
		Cosmeticards.Parent = leaderstats
		Cosmeticards.Value = PlayerData.Cosmeticards

		local Tags = Instance.new("IntValue")
		Tags.Name = "Tags"
		Tags.Parent = leaderstats
		Tags.Value = PlayerData.Tags

		local Victories = Instance.new("IntValue")
		Victories.Name = "Victories"
		Victories.Parent = leaderstats
		Victories.Value = PlayerData.Victories
	end)
end

Players.PlayerRemoving:Connect(function(player)
	local keep = LoadedKeeps[player]
	if keep then return end
	keep:Release()
end)

KeepStore:andThen(function(store)
	KeepStore = store
	Players.PlayerAdded:Connect(onPlayerJoin)
end)

local DataManager = {}

function DataManager:Get(player)
	local keep = LoadedKeeps[player]
	
	if keep then
		local PlayerData = keep.Data
		return PlayerData
	end
end

return DataManager

The WriteLib

return {
	SetLazercards = function(keep, amount)
		keep.Data.Lazercards = amount
	end,
	
	SetCosmeticards = function(keep, amount)
		keep.Data.Cosmeticards = amount
	end,
	
	SetEquippedGun = function(keep, gun)
		keep.Data.EquippedGun = gun
	end,
	
	SetOwnedGuns = function(keep, Table)
		keep.Data.OwnedGuns = Table
	end,
	
	SetLang = function(keep, lang)
		keep.Data.Lang = lang
	end,
	
	SetVictories = function(keep, amount)
		keep.Data.Victories = amount
	end,
	
	SetBanned = function(keep, bool)
		keep.Data.Banned = bool
	end,
	
	SetCloth = function(keep, clothing, item)
		if clothing == "Shirt" then
			keep.Data.EquippedShirt = item
		elseif clothing == "Pants" then
			keep.Data.EquippedPants = item
		end
	end,
	
	SetSkinRGB = function(keep, color, value)
		if color == "R" then
			keep.Data.R = value
		elseif color == "G" then
			keep.Data.G = value
		else
			keep.Data.B = value
		end
	end,
	
	SetOwnedClothes = function(keep, clothing, Table)
		if clothing == "Shirt" then
			keep.Data.OwnedShirts = Table
		elseif clothing == "Pants" then
			keep.Data.OwnedPants = Table
		end
		
	end,
}

Any idea why it won’t work?

Hey! Try:

KeepStore.Wrapper = require(game.ReplicatedStorage.WriteLib)
1 Like

Tried that, and while I didn’t get the warning, I got errors instead.

StatManager

local Players = game:GetService("Players")
local KeepService = require(game.ReplicatedStorage.datakeep)



local DefaultData = {
	Player = {
		Lang = "English";
		Lazercards = 0;
		OwnedGuns = {"StarterPistol"};
		EquippedGun = "StarterPistol";
		Victories = 0;
		Cosmeticards = 0;
		ShirtID = 6536782130;
		PantsID = 129459077;
		R = .75;
		G = .75;
		B = .75;
		OwnedShirts = {6536782130};
		OwnedPants = {129459077};
		Banned = false,
	}
}

local KeepStore = KeepService.GetStore("PlayerData", DefaultData)

KeepStore.Wrapper = require(game.ReplicatedStorage.WriteLib)

local LoadedKeeps = {}

local function onPlayerJoin(player)
	KeepStore:LoadKeep("Player_" .. player.UserId):andThen(function(keep)
		if keep == nil then
			player:Kick("Data locked") -- will never happen, when no releaseHandler is passed it default steals from the locked session
		end

		keep:Reconcile()
		keep:AddUserId(player.UserId) -- help with GDPR requests

		keep.OnRelease:Connect(function() -- don't have to clean up, it cleans up internally.
			player:Kick("Session Release")
		end)

		if not player:IsDescendantOf(Players) then
			keep:Release()
			return
		end

		local PlayerData = keep.Data
		
		for i, v in pairs(DefaultData.Player) do
			if PlayerData[i] == nil then
				PlayerData[i] = v
			end
		end
		
		print(keep)
		print(PlayerData)

		print(`Loaded {player.Name}'s Keep!`)

		LoadedKeeps[player] = keep

		local leaderstats = Instance.new("Folder")
		leaderstats.Name = "leaderstats"
		leaderstats.Parent = player

		local Lazercards = Instance.new("IntValue")
		Lazercards.Name = "Lazercards"
		Lazercards.Parent = leaderstats
		Lazercards.Value = PlayerData.Lazercards

		local Cosmeticards = Instance.new("IntValue")
		Cosmeticards.Name = "Cosmeticards"
		Cosmeticards.Parent = leaderstats
		Cosmeticards.Value = PlayerData.Cosmeticards

		local Tags = Instance.new("IntValue")
		Tags.Name = "Tags"
		Tags.Parent = leaderstats
		Tags.Value = PlayerData.Tags

		local Victories = Instance.new("IntValue")
		Victories.Name = "Victories"
		Victories.Parent = leaderstats
		Victories.Value = PlayerData.Victories
	end)
end

Players.PlayerRemoving:Connect(function(player)
	local keep = LoadedKeeps[player]
	if keep then return end
	keep:Release()
end)

KeepStore:andThen(function(store)
	KeepStore = store
	Players.PlayerAdded:Connect(onPlayerJoin)
end)

local DataManager = {}

function DataManager:Get(player)
	local keep = LoadedKeeps[player]
	
	if keep then
		local PlayerData = keep
		return PlayerData
	end
end

return DataManager

GunManager

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local StatManager = require(ReplicatedStorage.StatManager)
local GunStats = require(ReplicatedStorage.GunStats)

local ReplicatedEvents = ReplicatedStorage.Events.Replicated

local BuyGun = ReplicatedEvents.BuyGun
local GetGuns = ReplicatedEvents.GetGuns
local GetGun = ReplicatedEvents.GetGun
local SetGun = ReplicatedEvents.SetGun

local Guns = ReplicatedStorage:WaitForChild("Guns")

BuyGun.OnServerInvoke = function(Player, Gun)
	local keep = StatManager:Get(Player)
	print(keep.Data)
	if keep.Data then
		local MockTable = keep.Data.OwnedGuns
		if keep.Data.Lazercards >= GunStats.Guns[Gun]["Price"] then
			keep:SetLazercards(keep.Data.Lazercards - GunStats.Guns[Gun]["Price"])
			table.insert(MockTable, Gun)
			keep:SetOwnedGuns(MockTable)
			return true
		else
			return false
		end
	else 
		return false
	end
end

SetGun.OnServerInvoke = function(Player, Gun)
	local keep = StatManager:Get(Player)
	--print(keep.Data)
	if keep.Data then
		if table.find(keep.Data.OwnedGuns, Gun) then
			keep:SetEquippedGun(Gun)
			return true
		else
			return false
		end
	else 
		return false
	end
end

GetGuns.OnServerInvoke = function(Player, Type)
	local keep = StatManager:Get(Player)
	print(keep.Data.OwnedGuns)
	print(Type)
	local GunsToReturn = {}
	pcall(function()
		if keep.Data then
			if Type == "Owned" then
				GunsToReturn = keep.Data.OwnedGuns
			elseif Type == "Store" then
				print("Getting store guns")
				for i, v in pairs(Guns:GetChildren()) do
					print("Checking "..v.Name)
					if not table.find(keep.Data.OwnedGuns, v.Name) then
						print(v.Name)
						table.insert(GunsToReturn, v.Name)
					end
				end
			end
		end
	end)
	if #GunsToReturn > 0 then
		return GunsToReturn
	else
		return false
	end
end

GetGun.OnServerInvoke = function(Player)
	local keep = StatManager:Get(Player)
	print(keep.Data.EquippedGun)

	local Item


	pcall(function()
		if keep.Data then
			Item = Guns:WaitForChild(keep.Data.EquippedGun)
		end
	end)
	if Item ~= nil then
		return Item.Name
	else
		return false
	end
end

So sorry, it is not keepStore.Wrapper, but DataKeep.Wrapper

Should this be moved under keepstore to allow each keepstore a custom writelib?

1 Like

Thanks!

Maybe, that could be a cool idea! Maybe you should do a poll, see how people in general feel about it