Data module, could it be better?

Hey! I made a data module using profile service and others. However, I am not sure if it could be condensed, have added type notations, or more promises. Any help would be appreciated. Pardon the length of the code.

type Receipt = {
    PlayerId: number,
    PurchaseId: number,
    ProductId: number
}

local Players = game:GetService"Players"
local ReplicatedStorage = game:GetService"ReplicatedStorage"
local MarketPlaceService = game:GetService"MarketplaceService"

local Knit = require(ReplicatedStorage.Packages.Knit)
local Promise = require(ReplicatedStorage.Packages.Promise)
local Structure = require(script.Parent.Structure)
local ProfileService = require(ReplicatedStorage.Packages.ProfileService)
local ReplicaService = require(ReplicatedStorage.Packages.ReplicaService)

local Profiles, Replicas = {}, {}
local ProfileClassToken = ReplicaService.NewClassToken("Data")
local ProfileStore = ProfileService.GetProfileStore("Data", Structure.UserData)

local DataService = Knit.CreateService {
    Name = "DataService"
}

local function PlayerAdded(Player: Player)
    local StartTime = tick()
    local Profile = ProfileStore:LoadProfileAsync("Player_" .. Player.UserId)

    if Profile then
        Profile:AddUserId(Player.UserId)
        Profile:Reconcile()
        Profile:ListenToRelease(function()
            Profiles[Player] = nil
            Replicas[Player]:Destroy()
            Replicas[Player] = nil
            Player:Kick("DataService: Profile was released.")
        end)

        if Player:IsDescendantOf(Players) then
            Profiles[Player] = Profile

            local Replica = ReplicaService.NewReplica {
                ClassToken = ProfileClassToken, 
                Tags = {["Player"] = Player},
                Data = Profile.Data,
                Replication = "All"
            }

            Replicas[Player] = Replica
            warn(Player.Name .. "'s profile has been loaded. ".. "(" .. string.sub(tostring(tick() - StartTime), 1, 5) .. ")")
        else
            Profile:Release()
        end
    else
        Player:Kick("DataService: Profile is nil.")
    end
end

local function PlayerProfileAsync(Player: Player): {}
    local Profile = Profiles[Player]

    while Profile == nil and Player:IsDescendantOf(Players) do
        task.wait()
        Profile = Profiles[Player]     
    end

    return Profile
end

local function GrantProduct(Player: Player, ID: number)
    local Profile = Profiles[Player]
    local ProductFunction = Structure.Products[ID]

    if ProductFunction ~= nil then
        ProductFunction(Profile)
    else
        warn("DataService: ProductId " .. tostring(ID) .. " has not been defined in the 'Products' table.")
    end
end

function PurchaseAsync(Profile, ID: number, Callback): EnumItem
    if not Profile:IsActive() then
        return Enum.ProductPurchaseDecision.NotProcessedYet
    else
        local MetaData = Profile.MetaData
        local PurchaseIDs = MetaData.MetaTags.ProfilePurchaseIds

        if PurchaseIDs == nil then
            PurchaseIDs = {}
            MetaData.MetaTags.ProfilePurchaseIds = PurchaseIDs
        end

        if table.find(PurchaseIDs, ID) == nil then
            while #PurchaseIDs >= Structure.PurchaseIDLog do
                table.remove(PurchaseIDs, 1)
            end

            table.insert(PurchaseIDs, ID)
            task.spawn(Callback)
        end

        local Result = nil

        local function LatestMetaTag()
            local SavedPurchaseIds = MetaData.MetaTagsLatest.ProfilePurchaseIds

            if SavedPurchaseIds ~= nil and table.find(SavedPurchaseIds, ID) ~= nil then
                Result = Enum.ProductPurchaseDecision.PurchaseGranted            
            end
        end

        LatestMetaTag()

        local MetaTagsConnection = Profile.MetaTagsUpdated:Connect(function()
            LatestMetaTag()

            if not Profile:IsActive() and Result == nil then
                Result = Enum.ProductPurchaseDecision.NotProcessedYet
            end
        end)

        while Result == nil do
            task.wait()
        end

        MetaTagsConnection:Disconnect()

        return Result
    end
end

local function ProcessReceipt(Info: Receipt): EnumItem
    local Player = Players:GetPlayerByUserId(Info.PlayerId)

    if Player == nil then
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end

    local Profile = PlayerProfileAsync(Player)

    if Profile ~= nil then
        return PurchaseAsync(
            Profile,
            Info.PurchaseId,
            function()
                GrantProduct(Player, Info.ProductId)
            end
        )
    else
        return Enum.ProductPurchaseDecision.NotProcessedYet
    end
end

function DataService:GetReplica(Player: Player)
    return Promise.new(function(Resolve, Reject)
        if typeof(Player) == "Instance" and not Player:IsDescendantOf(Players) then
            error("DataService: Value passed is not a player.")
        end

        if not Profiles[Player] or not Replicas[Player] then
            repeat
                if Player then
                    task.wait()
                else
                    Reject("DataService: Player has left the game.")
                end
            until Profiles[Player] and Replicas[Player]
        end

        local Profile, Replica = Profiles[Player], Replicas[Player]

        if Profile and Profile:IsActive() then
            if Replica and Replica:IsActive() then
                Resolve(Replica)
            else
                Reject("DataService: Replica did not exist or was not active.")
            end
        else 
            Reject("DataService: Profile did not exist or was not active.")
        end
    end)
end

function DataService:KnitInit()
    for _, Player in Players:GetPlayers() do
        task.spawn(PlayerAdded, Player)
    end

    MarketPlaceService.ProcessReceipt = ProcessReceipt

    Players.PlayerAdded:Connect(PlayerAdded)

    Players.PlayerRemoving:Connect(function(Player)
        if Profiles[Player] then
            Profiles[Player]:Release()
        end
    end)
end

return DataService
1 Like

It is good, very simplified, but it’s still a wrapper for a datastore wrapper, so it won’t cause more simplicity, it’s just your own function names

But it still is very nice especially the receipt handler, I like it