تخطَّ إلى المحتوى

NexusData

m
m/NexusData عام
نجمة 0
230 lines · 7.8 KB
Raw تنزيل
Store.lua 230 lines
local DataStoreService = game:GetService("DataStoreService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")

local Util = require(script.Parent.Util)
local Profile = require(script.Parent.Profile)
local Config = require(script.Parent.Config)

local Store = {}
Store.__index = Store

local function makeKey(userId)
    return "Player_" .. tostring(userId)
end

function Store.new(name, template, options)
    assert(type(name) == "string" and #name > 0, "store name must be a non-empty string")
    assert(type(template) == "table", "template must be a table")
    options = options or {}

    local self = setmetatable({
        Name = name,
        Template = Util.DeepCopy(template),
        SchemaVersion = options.SchemaVersion or 1,
        Options = options,
        Config = options.Config or Config,
        Profiles = {},
        _closing = false,
        _store = DataStoreService:GetDataStore(name, options.Scope),
        ProfileLoaded = Instance.new("BindableEvent"),
        ProfileReleased = Instance.new("BindableEvent"),
    }, Store)

    return self
end

function Store:_retry(operation, attempts)
    local lastError
    for attempt = 1, attempts do
        local ok, result = pcall(operation)
        if ok then return true, result end
        lastError = result
        if attempt < attempts then
            task.wait(Util.Backoff(attempt, self.Config.RetryBaseDelay, self.Config.RetryMaxDelay))
        end
    end
    return false, lastError
end

function Store:LoadProfileAsync(userId)
    assert(type(userId) == "number", "userId must be a number")
    if self._closing then return nil, "store is closing" end
    if self.Profiles[userId] then return self.Profiles[userId], nil end

    local key = makeKey(userId)
    local sessionId = game.JobId .. ":" .. HttpService:GenerateGUID(false)
    local loadedData, keyInfo
    local success, result = self:_retry(function()
        return self._store:UpdateAsync(key, function(oldData, oldKeyInfo)
            local data = oldData or Util.DeepCopy(self.Template)
            local meta = data.__NexusData
            if type(meta) ~= "table" then meta = {} end

            local lock = meta.Session
            if lock and lock.SessionId ~= sessionId then
                local lastSeen = tonumber(lock.Timestamp) or 0
                if os.time() - lastSeen < self.Config.SessionLockTimeout then
                    return nil
                end
            end

            meta.Session = {
                JobId = game.JobId,
                SessionId = sessionId,
                Timestamp = os.time(),
            }
            data.__NexusData = meta
            loadedData = data
            keyInfo = oldKeyInfo
            return data
        end)
    end, self.Config.LoadAttempts)

    if not success then return nil, result end
    if result == nil or loadedData == nil then
        return nil, "profile is locked by another server or unavailable"
    end

    loadedData.__NexusData = loadedData.__NexusData or {}
    local profile = Profile.new(self, userId, key, loadedData, sessionId, keyInfo)
    profile:Reconcile()
    self.Profiles[userId] = profile
    self.ProfileLoaded:Fire(profile)
    return profile, nil
end

function Store:SaveProfile(userId)
    local profile = self.Profiles[userId]
    if not profile or not profile.IsActive then return false, "profile is not active" end

    local snapshot = Util.DeepCopy(profile.Data)
    local success, result = self:_retry(function()
        return self._store:UpdateAsync(profile.Key, function(current)
            if type(current) ~= "table" then return nil end
            local meta = current.__NexusData
            local lock = type(meta) == "table" and meta.Session
            if type(lock) ~= "table" or lock.SessionId ~= profile.SessionId then
                return nil
            end

            snapshot.__NexusData = snapshot.__NexusData or {}
            snapshot.__NexusData.Session = {
                JobId = game.JobId,
                SessionId = profile.SessionId,
                Timestamp = os.time(),
            }
            return snapshot
        end)
    end, self.Config.SaveAttempts)

    if not success or result == nil then
        return false, result or "session ownership lost; save cancelled"
    end

    profile.IsDirty = false
    profile._lastSaved = os.clock()
    return true
end

function Store:ReleaseProfile(userId)
    local profile = self.Profiles[userId]
    if not profile then return true end
    if profile._released then return true end

    local saved, err = self:SaveProfile(userId)
    if not saved then
        warn(("[NexusData] Failed final save for %s: %s"):format(tostring(userId), tostring(err)))
    end

    local releaseSuccess, releaseResult = self:_retry(function()
        return self._store:UpdateAsync(profile.Key, function(current)
            if type(current) ~= "table" then return nil end
            local meta = current.__NexusData
            local lock = type(meta) == "table" and meta.Session
            if type(lock) ~= "table" or lock.SessionId ~= profile.SessionId then
                return nil
            end
            meta.Session = nil
            current.__NexusData = meta
            return current
        end)
    end, self.Config.SaveAttempts)

    profile.IsActive = false
    profile._released = true
    self.Profiles[userId] = nil
    self.ProfileReleased:Fire(userId, saved, err)
    if not releaseSuccess then
        warn(("[NexusData] Failed to release session for %s: %s"):format(tostring(userId), tostring(releaseResult)))
    end
    return saved, err
end

function Store:_refreshSession(profile)
    local success, result = self:_retry(function()
        return self._store:UpdateAsync(profile.Key, function(current)
            if type(current) ~= "table" then return nil end

            local meta = current.__NexusData
            local lock = type(meta) == "table" and meta.Session
            if type(lock) ~= "table" or lock.SessionId ~= profile.SessionId then
                return nil
            end

            lock.Timestamp = os.time()
            current.__NexusData = meta
            return current
        end)
    end, self.Config.SaveAttempts)

    if not success or result == nil then
        return false, result or "session ownership lost"
    end

    return true
end

function Store:StartAutosave()
    if self._autosaveStarted then return end
    self._autosaveStarted = true
    task.spawn(function()
        while not self._closing do
            task.wait(self.Config.AutosaveInterval)
            for userId, profile in pairs(self.Profiles) do
                if profile.IsActive then
                    -- Refresh the session timestamp even when the profile has no changes.
                    -- Otherwise a quiet profile could look abandoned to another server.
                    if profile.IsDirty then
                        local ok, err = self:SaveProfile(userId)
                        if not ok then
                            warn(("[NexusData] Autosave failed for %s: %s"):format(tostring(userId), tostring(err)))
                        end
                    else
                        local ok, err = self:_refreshSession(profile)
                        if not ok then
                            warn(("[NexusData] Session refresh failed for %s: %s"):format(tostring(userId), tostring(err)))
                        end
                    end
                end
            end
        end
    end)
end

function Store:Close()
    if self._closing then return end
    self._closing = true
    local deadline = os.clock() + self.Config.BindToCloseTimeout
    for userId in pairs(self.Profiles) do
        if os.clock() >= deadline then break end
        self:ReleaseProfile(userId)
    end
end

function Store:GetProfile(userId)
    return self.Profiles[userId]
end

return Store

تأكيد