Johhny's Custom DataStore

What is Johhny's Custom DataStore?


It is a wrapper that's designed to make using DataStores more convenient, by:

  1. Removing the need to use pcall or xpcall, as the module uses them internally
  2. Calls DataStore methods in a new thread, so that they no longer yield the code below
  3. Adds the ability to set a default value, so that you'll no longer have to worry about data for first time players being nil
  4. Forces Studio to wait 1 second before closing the server

How does it work?


It does so by creating a custom DataStore object, whose methods handle the inconveniences for you. When requiring the module, it returns a frozen table which contains a fetcher and a constructor for the custom DataStore:

.new()

Arguments:

  1. name - The DataStore's name - Required - Value Type = string
  2. default_value - The default data/value that new players will get - Optional - Value Type = any
  3. Note: The default value of "default_value" is nil, to match the original behavior of DataStores
  4. options - The DataStoreOptions Instance - Optional - Value Type = DataStoreOptions or nil

This function returns a CustomDataStore object

.waitFor()

Arguments:

  1. name - The DataStore's name - Required - Value Type = string
  2. Note: "waitFor" does yield the code below it until a custom DataStore with the same name is first created using the "new" constructor

    If a custom DataStore with the same name isn't created within 5 seconds, a warning will show up in the output

    TLDR: Please do not use this function as a constructor, only to fetch a custom DataStore that was already created using "new"

This function returns a CustomDataStore object

How do the custom DataStore's methods work?


Please note that unlike the original DataStore methods with are indexed using a colon :, the custom DataStore's methods will need to be indexed using a dot .

.Get()

Arguments:

  1. key - The DataStore key, which you'd like to retrieve data from - Required - Value Type = string
  2. onError - The function to be called if Get fails - Required - Value Type = function
  3. onSuccess - The function to be called if Get works successfully - Required - Value Type = function
  4. options - The DataStoreGetOptions Instance - Optional - Value Type = DataStoreGetOptions or nil

This method returns nothing

.Set()

Arguments:

  1. key - The DataStore key, which you'd like to save data to - Required - Value Type = string
  2. data - The data to save - Required - Value Type = any but not nil
  3. onError - The function to be called if Set fails - Required - Value Type = function
  4. onSuccess - The function to be called if Set works successfully - Optional - Value Type = function or nil
  5. options - The DataStoreSetOptions Instance - Optional - Value Type = DataStoreSetOptions or nil

This method returns nothing

.Remove()

Arguments:

  1. key - The DataStore key, which you'd like to remove its data - Required - Value Type = string
  2. onError - The function to be called if Remove fails - Required - Value Type = function
  3. onSuccess - The function to be called if Remove works successfully - Optional - Value Type = function or nil

This method returns nothing

Usage Examples


Leaderstats:

--!strict
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")

local DataStore = require(ServerStorage.Modules.DataStore)

local data = {
	Coins = 100,
	Gems = 10
}

local dataStore = DataStore.new("ExampleDataStore", data)

Players.PlayerAdded:Connect(function(player: Player)
	dataStore.Get(""..player.UserId, warn, function(data: typeof(data))
		local leaderstats = Instance.new("Folder")
		leaderstats.Name = "leaderstats"
		leaderstats.Parent = player

		local coins = Instance.new("IntValue")
		coins.Name = "Coins"
		coins.Value = data.Coins
		coins.Parent = leaderstats

		local gems = Instance.new("IntValue")
		gems.Name = "Gems"
		gems.Value = data.Gems
		gems.Parent = leaderstats
	end)
end)
Killbrick:
--!strict
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")

local DataStore = require(ServerStorage.Modules.DataStore)

local dataStore = DataStore.waitFor("ExampleDataStore")

local debounce = false

script.Parent.Touched:Connect(function(otherPart)
	if debounce then return end

	local model = otherPart:FindFirstAncestorOfClass("Model")

	if model then
		local player = Players:GetPlayerFromCharacter(model)

		if player then
			debounce = true

			dataStore.Get(""..player.UserId, warn, function(data: typeof(dataStore.Default))
				data.Coins += 10

				dataStore.Set(""..player.UserId, data, warn, function()
					player.leaderstats.Coins.Value = data.Coins
				end)
			end)

			task.wait(1)

			debounce = false
		end
	end
end)
Place file where you can test the examples above (Please make sure to publish the place, and enable Studio access to API services before testing!):

CustomDataStoreDemoArea.rbxl (55.5 KB)

Module source and link


Source code
--!strict
local DataStoreService = game:GetService("DataStoreService")
local RunService = game:GetService("RunService")

local EMPTY_ARRAY = {}

export type func = (...any) -> ...any

export type CustomDataStore = {
	DataStore: DataStore,
	Default: any,

	Get: (key: string, onError: func, onSuccess: func, options: DataStoreGetOptions?) -> (),
	Remove: (key: string, onError: func, onSuccess: func?) -> (),
	Set: (key: string, data: any, onError: func, onSuccess: func?, options: DataStoreSetOptions?) -> ()
}

local dataStores: {[string]: CustomDataStore} = {}

local function get(dataStore: DataStore, key: string, onError: func, onSuccess: func, default: any, options: DataStoreGetOptions?)
	local success, data = pcall(dataStore.GetAsync, dataStore, key, options)

	if success then
		onSuccess(data or default)
	else
		onError(data)
	end
end

local function remove(dataStore: DataStore, key: string, onError: func, onSuccess: func?)
	local success, result = pcall(dataStore.RemoveAsync, dataStore, key)

	if success then
		if onSuccess then onSuccess(result) end
	else
		onError(result)
	end
end

local function set(dataStore: DataStore, key: string, data: any, onError: func, onSuccess: func?, options: DataStoreSetOptions?)
	local success, result = pcall(dataStore.SetAsync, dataStore, key, data, EMPTY_ARRAY, options)

	if success then
		if onSuccess then onSuccess(result) end
	else
		onError(result)
	end
end

if RunService:IsStudio() then
	game:BindToClose(function()
		task.wait(1)
	end)
end

return table.freeze{
	new = function(name: string, default_value: any, options: DataStoreOptions?): CustomDataStore
		if typeof(name) ~= "string" then error("DataStore.new - Name must be a string value", 2) end

		if dataStores[name] then return dataStores[name] end

		local CustomDataStore = {
			DataStore = DataStoreService:GetDataStore(name, options),
			Default = default_value
		}

		function CustomDataStore.Get(key: string, onError: func, onSuccess: func, options: DataStoreGetOptions?)
			if typeof(key) ~= "string" then error("DataStore.Get - Key must be a string value", 2) end
			if typeof(onError) ~= "function" then error("DataStore.Get - OnError must be a function", 2) end
			if typeof(onSuccess) ~= "function" then error("DataStore.Get - OnSuccess must be a function", 2) end

			task.spawn(get, CustomDataStore.DataStore, key, onError, onSuccess, CustomDataStore.Default, options)
		end

		function CustomDataStore.Remove(key: string, onError: func, onSuccess: func?)
			if typeof(key) ~= "string" then error("DataStore.Remove - Key must be a string value", 2) end
			if typeof(onError) ~= "function" then error("DataStore.Remove - OnError must be a function", 2) end
			if typeof(onSuccess) ~= "nil" and typeof(onSuccess) ~= "function" then error("DataStore.Remove - OnSuccess must be nil or a function", 2) end

			task.spawn(remove, CustomDataStore.DataStore, key, onError, onSuccess)
		end

		function CustomDataStore.Set(key: string, data: any, onError: func, onSuccess: func?, options: DataStoreSetOptions?)
			if typeof(key) ~= "string" then error("DataStore.Set - Key must be a string value", 2) end
			if typeof(data) == "nil" then error('DataStore.Set - The value of "data" cannot be nil', 2) end
			if typeof(onError) ~= "function" then error("DataStore.Set - OnError must be a function", 2) end
			if typeof(onSuccess) ~= "nil" and typeof(onSuccess) ~= "function" then error("DataStore.Set - OnSuccess must be nil or a function", 2) end

			task.spawn(set, CustomDataStore.DataStore, key, data, onError, onSuccess, options)
		end

		dataStores[name] = table.freeze(CustomDataStore)

		return dataStores[name]
	end,

	waitFor = function(name: string): CustomDataStore
		if typeof(name) ~= "string" then error("DataStore.waitFor - Name must be a string value", 2) end

		local x = 0

		local warned

		while true do
			x += task.wait()

			if dataStores[name] then return dataStores[name] end

			if warned or x < 5 then continue end
			warned = true

			warn(`DataStore.waitFor - DataStore "{name}" is taking longer than 5 seconds to be found`)
		end
	end
}

I hope you enjoy using this module :grin::+1:

4 Likes

Is this meant to be more beneficial than the established datastore modules or is this just you sharing what you personally like using?

2 Likes

Its not intended to directly compete with other DataStore modules when it comes to providing unique features or such, the people who will enjoy using it the most are those that prefer to use the default DataStore API, but would like a way to hide the pcalls and if statements that doing so requires, and people who are new to using DataStores and would like an easier point of entry

2 Likes

I guess tostring() just doesn’t exist.
Other than that, how is this in anyway better than just using profile service?

2 Likes

It’s faster to do it that way, which is why I did it like that :smile:
Correction: tostring seems to be faster now

My aim isn’t to be better than other DataStore modules, just to provide a convenient way of interacting with DataStores. If the other modules available provide functionality that my module doesn’t provide and which you enjoy using, then by all means continue to use them :slight_smile::+1:

didnt know that, is there any document that you based that conclusion off of?

2 Likes

It was a while since I ran the test, seems as though using tostring is actually faster now:

local function test(func)
	local s = os.clock()
	for _ = 1, 99_999_999 do func() end
	print(os.clock() - s)
end

test(function()
	return ""..123456789
end)
-- Result = 11.286000000000001

test(function()
	return tostring(123456789)
end)
-- Result = 7.100000000000001

I stand corrected, goes to show how important it is to retest things after a while

2 Likes