Greetings, I am currently working on a Datastore module, there are currently no problems whatsoever however I’m looking to improve the architecture of my program. Here’s the main module:
--|| Services ||--
local DataStoreService = game:GetService("DataStoreService");
local RunService = game:GetService("RunService");
local Players = game.Players;
local Datastore = {};
Datastore.__index = Datastore;
local Cache = {};
local StarterData = require(script.StarterData);
local Configs = require(script.Configurations);
--|| Local Functions
local Datatypes = {
["number"] = "NumberValue",
["string"] = "StringValue",
["boolean"] = "BoolValue",
}
local function GetIndexCount(Table)
local Count = 0;
for _, _ in next, Table do
Count += 1;
end
return Count;
end
local function Copy(Table)
local NewTable = {}
for Index, Value in next, Table do
if typeof(Value) == "table" then
NewTable[Index] = Copy(Value)
else
NewTable[Index] = Value
end
end
return NewTable
end
local function GetSession()
return {
JobId = game.JobId;
Stamp = os.time();
};
end
local function GetTimeStamp()
local Info = os.date("!*t")
local TimeStamp = "DATE: "..Info.year.."/"..Info.month.."/"..Info.day.." | TIME: "..(Info.hour > 12 and Info.hour - 12 or Info.hour)..":"..(Info.min < 10 and "0"..Info.min or Info.min)..":"..(Info.sec < 10 and "0"..Info.sec or Info.sec).." "..((Info.hour < 12 or Info.hour == 24) and "AM" or "PM")
return TimeStamp
end
local function GetDataStore(Identifier)
return DataStoreService:GetDataStore(tostring(Identifier));
end
local function GetOrderedDatastore(Identifier)
return DataStoreService:GetOrderedDataStore(tostring(Identifier))
end
local function CreateDataTree(Parent, Data)
for Index, Value in next, Data do
local Datatype = type(Value);
if Datatype == "table" then
local Folder = Instance.new("Folder");
Folder.Name = Index;
CreateDataTree(Folder, Value);
Folder.Parent = Parent;
else
local Object = Instance.new(Datatypes[Datatype]);
Object.Value = Value;
Object.Name = Index;
Object.Parent = Parent;
end
end
end
local function UpdateDataTree(TreeFolder, Data)
--| This iterates over the tables to ensure that the data exists
for Index, Value in next, Data do -- you are unable to change a "category" into anything but a table, if this line errors you are tryin to change a category table into a string
local Object = TreeFolder:FindFirstChild(Index);
local Datatype = type(Value);
if Datatype == "table" then
if not Object then
local Folder = Instance.new("Folder");
Folder.Name = Index;
UpdateDataTree(Folder, Value);
Folder.Parent = TreeFolder;
else
UpdateDataTree(Object, Value);
end
else
if not Object then
local Object = Instance.new(Datatypes[Datatype]);
Object.Value = Value;
Object.Name = Index;
Object.Parent = TreeFolder;
else
Object.Value = Value;
end
end
end
--| This removes any old data
local Objects = TreeFolder:GetChildren()
for i = 1, #Objects do
local Object = Objects[i];
if Data[Object.Name] == nil and (tonumber(Object.Name) and Data[tonumber(Object.Name)] == nil or tonumber(Object.Name) == nil) then
Object:Destroy();
end
end
end
local function RecursiveUpdate(Tbl1, Tbl2)
for Index, Value in next, Tbl2 do
if Tbl1[Index] then
if type(Value) == "table" and type(Tbl1[Index]) == "table" then
RecursiveUpdate(Tbl1[Index], Value);
else
Tbl1[Index] = Value
end
else
Tbl1[Index] = Value
end
end
for Index, Value in next, Tbl1 do
if Tbl2[Index] == nil then
Tbl1[Index] = nil;
end
end
end
--|| Public Functions
function Datastore.new(UserId)
local Player = Players:GetPlayerByUserId(UserId);
local Data = {
LastBackup = os.clock();
Identifier = UserId;
UpdateCallbacks = {};
BeforeSaveCallbacks = {};
};
local PlayerData = Datastore:Load(UserId);
if PlayerData then
Data.Data = PlayerData;
print("[DATASTORE LITE]: "..UserId.."'s PlayerData was loaded.");
local Bindable, Fired = Instance.new("BindableEvent"), false;
local Connection
Connection = Player.AncestryChanged:Connect(function(_, Parent)
if Player:IsDescendantOf(game) then return end; -- In case the player is reparented elsewhere
if Parent == nil then
Connection:Disconnect(); -- Shall only run once
--| Run Callbacks Before Saving
for i = 1, #Data.BeforeSaveCallbacks do
local Callback = Data.BeforeSaveCallbacks[i];
Callback(Data.Data);
end
Datastore:Save(UserId);
Bindable:Fire();
Cache[UserId] = nil;
end
end)
--| Trigger AncestryChanged
if RunService:IsStudio() and Configs.StudioSave or not Configs.StudioSave and not RunService:IsStudio() then
game:BindToClose(function()
if not Fired then
coroutine.resume(coroutine.create(function()
Player.Parent = nil;
end))
Bindable.Event:Wait(); -- Will yield until player data is saved.
Bindable:Destroy();
end;
end)
end
--| Creates cache for OnUpdate connections
for Index, Value in next, PlayerData do
Data.UpdateCallbacks[Index] = {};
if StarterData[Index] and StarterData[Index].CreateObjects then
local Folder = Instance.new("Folder");
Folder.Name = Index;
Folder.Parent = Player;
CreateDataTree(Folder, Value)
end
end
else
if Player then
Player:Kick("We were unable to load your data, try rejoining or contact the owner if this issue persists.")
print("[DATASTORE LIST]: Failed to load "..UserId.."'s Data, Player was kicked.")
return nil;
end
end
local Object = setmetatable(Data, Datastore);
Cache[UserId] = Object;
for Index, Value in next, PlayerData do
if StarterData[Index] and StarterData[Index].CreateObjects then
local Folder = Player:FindFirstChild(Index);
if Folder then
Datastore:OnUpdate(UserId, Index, function(Old, New)
UpdateDataTree(Folder, New);
end)
end
end
end
return Cache[UserId];
end
function Datastore:OnUpdate(UserId, Index, Callback)
local Bindable = Instance.new("BindableEvent");
Bindable.Event:Connect(Callback);
Cache[UserId].UpdateCallbacks[Index][#Cache[UserId].UpdateCallbacks[Index] + 1] = Bindable;
end
function Datastore:BeforeSave(UserId, Callback)
if Cache[UserId] then
Cache[UserId].BeforeSaveCallbacks[#Cache[UserId].BeforeSaveCallbacks + 1] = Callback;
else
warn("[DATASTORE LITE]: Could not find "..UserId.." in local cache")
end
end
function Datastore:Backup(UserId)
local UserId = UserId
local PointerDatastore = GetOrderedDatastore(UserId);
local BackupDatastore = GetDataStore(UserId.." Backup");
local Data = Datastore:Get(UserId);
local Timestamp = GetTimeStamp();
local Time = os.time();
local Success, Error = pcall(function()
PointerDatastore:SetAsync(Timestamp, Time);
end)
if not Success then
warn("[DATASTORE LITE]: Failed to set pointer data for; ", Error)
return
else
Success, Error = pcall(function()
BackupDatastore:SetAsync(Time, Data);
end)
if not Success then
warn("[DATASTORE LITE]: Failed to set backup data for; ", Error)
else
print("[DATASTORE LITE]: Backup Data was saved for "..UserId)
end
end
end
function Datastore:Load(UserId)
local PlayerDatastore = GetDataStore(UserId..Configs.DataVersion);
local Success, Error, Attempts, Data = nil, nil, 0, nil;
local InSession = false;
while (Players:GetPlayerByUserId(UserId)
and Attempts < Configs.Attempts)
and (not Success or InSession) do -- If you did not retrieve data properly or still in session
Attempts += 1;
Success, Error = pcall(function()
PlayerDatastore:UpdateAsync("Data", function(OldData)
--| Assume Player Is New
if OldData == nil then
InSession = false;
local NewData = {};
for Index, Value in next, StarterData do
NewData[Index] = Copy(Value.Data);
end
--| Set Session
NewData.SessionData = GetSession();
Data = NewData;
return NewData;
elseif OldData
and (OldData.SessionData == nil
or os.time() - OldData.SessionData.Stamp >= 5) then
InSession = false;
--| Add New Data
for Index, Value in next, StarterData do
if OldData[Index] == nil then
if Value.Data then
OldData[Index] = Copy(Value.Data);
else
warn("[DATASTORE LITE]: When attempting to make a copy of "..Index.." data, the Data property was not located in ", Value)
end
end
end
--| Set Session
OldData.SessionData = GetSession();
Data = OldData;
return OldData;
elseif OldData and (OldData.SessionData and os.time() - OldData.SessionData.Stamp < 10) then
InSession = true;
end
end)
end)
if (not Success or InSession) and Attempts < Configs.Attempts then
wait(6);
end
end
return Data;
end
function Datastore:Save(UserId)
if RunService:IsStudio() and not Configs.StudioSave then warn("[DATASTORE LITE]: Attempted to save user data in ROBLOX STUDIO when StudioSave property is set to false") return end;
local PlayerDatastore = GetDataStore(UserId..Configs.DataVersion);
local Success, Error, Attempts = nil, nil, 0
while not Success and Attempts < Configs.Attempts do
Attempts += 1;
Success, Error = pcall(function()
PlayerDatastore:UpdateAsync("Data", function(OldData)
if OldData.SessionData == nil or
OldData.SessionData.JobId == game.JobId then
return Cache[UserId].Data;
end
end)
end)
if Error and Attempts < Configs.Attempts then
warn("[DATASTORE LITE]: Datastore failed to save "..Attempts.." times with; "..Error)
wait(10);
else
print("[DATASTORE LITE]: "..UserId.."'s Data was saved successfully")
end
end
if Error then
warn("[DATASTORE LITE]: "..UserId.."'s Data failed to save.")
end
Datastore:Backup(UserId);
end
function Datastore:Set(Player, List, Value)
if not Players:FindFirstChild(Player.Name) then return end;
local Data = Datastore:Get(Player.UserId);
local Index = List[1];
local OldDirectory = Index and Copy(Cache[Player.UserId].Data[Index]);
local Parent, IndexedValue, LastIndex = Data, Data, nil;
for i = 1, #List do
local Index = List[i];
Parent = IndexedValue;
IndexedValue = Parent[Index];
LastIndex = Index;
end
if type(LastIndex) == "number" and Value == nil then
table.remove(Parent, LastIndex);
else
if type(Parent[LastIndex]) == "table" and type(Value) == "table" then
RecursiveUpdate(Parent[LastIndex], Value);
else
Parent[LastIndex] = Value;
end
end
if Index then
local Bindables = Cache[Player.UserId].UpdateCallbacks[Index]
for i = 1, #Bindables do
local Bindable = Bindables[i];
Bindable:Fire(OldDirectory, Cache[Player.UserId].Data[Index]);
end
end
end
function Datastore:Get(UserId)
--[[
[Description]: Yields until data is available, if player leaves while yielding will return nil
]]
if Cache[UserId] then
return Cache[UserId].Data;
else
while Players:GetPlayerByUserId(UserId) and Cache[UserId] == nil do
RunService.Stepped:Wait();
end
if Cache[UserId] then
return Cache[UserId].Data;
end
end
end
function Datastore:GetAsync(UserId)
local Data
local Success, Error = pcall(function()
Data = GetDataStore(UserId..Configs.DataVersion):GetAsync("Data");
end)
return Data;
end
--| Automatic Backups
coroutine.resume(coroutine.create(function()
while true do
local Players = Players:GetPlayers()
for i = 1, #Players do
local Player = Players[i];
local Data = Cache[Player.UserId];
if Data then
if os.clock() - Data.LastBackup >= Configs.BackupInterval then
Data.LastBackup = os.clock();
coroutine.resume(coroutine.create(function()
Datastore:Backup(Player.UserId);
end))
end
end
end
wait(1)
end
end))
return Datastore
If you wish to have a in-depth view of the model, the model is available right here:
As of right now, the code will yield if a individual is “in session” however looking at the Datastore limits, I was wondering if it’d be safe to call GetAsync at a faster rate (in the scenario there is a session, I want to load the data as soon as possible). Micro-optimisations are appreciated but not what I’m looking for.