DataKeep - A promise based, auto-saving DataStore module

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.

1 Like

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

That works, thank you!

I am getting this warning, but I wont worry about it for now.

Type Error: (27,23) Table type '{| SetBanned: (any, any) -> (...any), SetCloth: (any, any, any) -> (...any), ... 8 more ... |}' not compatible with type '{| Mutate: (any, string, any) -> (...any), onDataChange: (any, string, any) -> (...any) |}' because the former is missing fields 'Mutate', and 'onDataChange'

What module is this from? This is just a typing warning

1 Like

Sorry for the late response, it’s from the StatManager Module.

I’m wondering if that warning is related to an issue that has popped up, where data isn’t saved anymore. I’ll use the WriteLib functions, and Data.EquippedGun get’s changed to the correct value (same with all other WriteLib functions), but then the correct value isn’t saved to the datastore. It get’s reset to the previous value, not the default.

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)

KeepService.Wrapper = require(game.ReplicatedStorage.WriteLib) -- The code giving a typing warning.

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

WriteLib is the exact same as before.

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
			print(keep.Data.EquippedGun)
			keep:SetEquippedGun(Gun)
			print(keep.Data.EquippedGun)
			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

(accidentally skipped 2.1.0)

version 2.2.1: 12/17/2023

API Breaking Change

Fixed

  • ViewKeep :Save not working, replaced with :Overwrite()
  • #12

Added

  • ViewKeep :Overwrite()
1 Like

Updated to version 2.2.1 (as listed on GitHub), still having the same issue. I also tested in a live client, along with studio, and still had the same issue. I’m getting no errors, which is confusing me more. I don’t know what in specific I’m doing wrong. Sorry for asking for help so much btw :sweat_smile:

BTW, the link for 2.1.1 is leading to a 404 for me, so I’ll link the latest version:

Version 2.2.1 GitHub

Is anything coming through the error signal?

1 Like

Error signal? What’s that? I checked my output, there were no errors, checked the dashboard error report for the live test, and there was also nothing.

My theory is that it’s a result of changing the wrapper to be my writelib, but if that’s the case, I don’t know how to change data values. Changing them without the writelib also didn’t work. I have the settings set so that studio let api requests work.

Sorry, seems I missed that in documentation. Try

KeepStore.IssueSignal:Connect(function(err)
      warn(err)
end)

added that to documentation

1 Like