DataKeep - A promise based, auto-saving DataStore module

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

version 3.0.0: 12/22/2023

API Breaking Change

Added

  • Documented IssueSignal, CriticalState, & CriticalStateSignal

  • Added ‘Saving’ signal to Keep

  • Mockstore didyield detection for more accurate development testing

Changes

  • DataKeep.Wrapper is now: DataStore.Wrapper (independant wrappers supported)

  • OnRelease → Releasing (signal) to address #13

  • #13 Releasing, and Saving signals now pass a promise to track the state

Fixed

  • Wrapper type warning
1 Like

I’m getting errors again, which is a VERY good thing! VERY good update.

You should probably change the WriteLib documentation, as it still says DataKeep.WriteLib = require(path_to_WriteLib). That might confuse people a lot.

Btw, I’m assuming that DataStore in your post is the DataKeep.GetStore(“PlayerData”, defaultData), is that the case or not?

Yes. It is now .GetStore().Wrapper

Updated the docs.

1 Like

version 3.0.1: 12/22/2023

Super small update, just fixed a wrapper error with the new API that I accidentally left in

Fixed

  • Default Wrapper error relating to new API

  • Docs

1 Like

Sorry to bother you, just wanted to let you know that the RBXM file wasn’t included in that release. I’ll just copy the code in the changelog to my copy of 3.0.0 to fix it

1 Like

Done some testing, and it’s weird. It can create a new keep and save that just fine, and it can load a keep just fine, but saving later changes to the keep doesn’t seem to work. LoadCount gets set to 2 ingame, but stays as 1 in the datastore.

I can test that with the Data Editor plugin, using it to delete my pre-existing keep, and changing some data values in the keep.

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.Releasing:Connect(function() -- don't have to clean up, it cleans up internally.
			player:Kick("Session Release")
		end)
		
		keep.Saving:Connect(function()
			print("Saving keep")
		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

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

KeepStore.CriticalStateSignal:Connect(function()
	warn("Critical State!")
end)

return DataManager

Output:


All though this is nicely done and better than using profile service, I would recommend; that if you have not created your own cached session locking based datastore you should do that. It will help you learn more about the workings of the API when you come across issues.

How to create a session locker efficiently:

Use updateasync so you can get data and set data at the same time. You can set a boolean in the database to see if there is an existing session. If there is an active session retry every second. If after 20 seconds the session is still locked the server might have crashed and just load their most recent data. It is also important to use promises and listen for errors to retry.

This must be with mock store detection or something-- haven’t seen people experiencing this

can you attach a Roblox place file?

1 Like

I don’t know much about promises, can you tell me how I can use KeepStore:andThen(function(store) inside Promise.new ?

Everything in this module returns a promise (or a Promise.new) so from there you can interact as you would with Promise.new

DataKeep.GetStore("PlayerData", defaultData):andThen(function(store)
       -- interact with store
end)
2 Likes

Issue was fixed by changing the SaveInterval in src to 5. I don’t know if there’s a way to have it save data when releasing the keep, but saving on an interval of 5 will make saving more consistent.

For future developers reading this:

You need to be releasing on leave and should resolve it.

1 Like