Advertisement
DevSarim

ProfileStore

Apr 23rd, 2025
661
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 65.19 KB | Source Code | 0 0
  1. --[[
  2. MAD STUDIO (by loleris)
  3.  
  4. -[ProfileStore]---------------------------------------
  5.  
  6.     Periodic DataStore saving solution with session locking
  7.    
  8.     WARNINGS FOR "Profile.Data" VALUES:
  9.         ! Do not create numeric tables with gaps - attempting to store such tables will result in an error.
  10.         ! Do not create mixed tables (some values indexed by number and others by a string key)
  11.             - only numerically indexed  data will be stored.
  12.         ! Do not index tables by anything other than numbers and strings.
  13.         ! Do not reference Roblox Instances
  14.         ! Do not reference userdata (Vector3, Color3, CFrame...) - Serialize userdata before referencing
  15.         ! Do not reference functions
  16.        
  17.     Members:
  18.    
  19.         ProfileStore.IsClosing          [bool]
  20.             -- Set to true after a game:BindToClose() trigger
  21.            
  22.         ProfileStore.IsCriticalState    [bool]
  23.             -- Set to true when ProfileStore experiences too many consecutive errors
  24.        
  25.         ProfileStore.OnError            [Signal] (message, store_name, profile_key)
  26.             -- Most ProfileStore errors will be caught and passed to this signal
  27.            
  28.         ProfileStore.OnOverwrite        [Signal] (store_name, profile_key)
  29.             -- Triggered when a DataStore key was likely used to store data that wasn't
  30.             a ProfileStore profile or the ProfileStore structure was invalidly manually
  31.             altered for that DataStore key
  32.            
  33.         ProfileStore.OnCriticalToggle   [Signal] (is_critical)
  34.             -- Triggered when ProfileStore experiences too many consecutive errors
  35.        
  36.         ProfileStore.DataStoreState     [string] ("NotReady", "NoInternet", "NoAccess", "Access")
  37.             -- This value resembles ProfileStore's access to the DataStore; The value starts
  38.             as "NotReady" and will eventually change to one of the other 3 possible values.
  39.    
  40.     Functions:
  41.    
  42.         ProfileStore.New(store_name, template?) --> [ProfileStore]
  43.             store_name   [string] -- DataStore name
  44.             template     [table] or nil -- Profiles will default to given table (hard-copy) when no data was saved previously
  45.            
  46.         ProfileStore.SetConstant(name, value)
  47.             name    [string]
  48.             value   [number]
  49.                
  50.     Members [ProfileStore]:
  51.    
  52.         ProfileStore.Mock   [ProfileStore]
  53.             -- Reflection of ProfileStore methods, but the methods will now query a mock
  54.             DataStore with no relation to the real DataStore
  55.            
  56.         ProfileStore.Name   [string]
  57.        
  58.     Methods [ProfileStore]:
  59.    
  60.         ProfileStore:StartSessionAsync(profile_key, params?) --> [Profile] or nil
  61.             profile_key [string] -- DataStore key
  62.             params      nil or [table]: -- Custom params; E.g. {Steal = true}
  63.                 {
  64.                     Steal = true, -- Pass this to disregard an existing session lock
  65.                     Cancel = fn() -> (boolean), -- Pass this to create a request cancel condition.
  66.                         -- If the cancel function returns true, ProfileStore will stop trying to
  67.                         -- start the session and return nil
  68.                 }
  69.            
  70.         ProfileStore:MessageAsync(profile_key, message) --> is_success [bool]
  71.             profile_key [string] -- DataStore key
  72.             message     [table] -- Data to be messaged to the profile
  73.            
  74.         ProfileStore:GetAsync(profile_key, version?) --> [Profile] or nil
  75.             -- Reads a profile without starting a session - will not autosave
  76.             profile_key   [string] -- DataStore key
  77.             version       nil or [string] -- DataStore key version
  78.  
  79.         ProfileStore:VersionQuery(profile_key, sort_direction?, min_date?, max_date?) --> [VersionQuery]
  80.             profile_key      [string]
  81.             sort_direction   nil or [Enum.SortDirection]
  82.             min_date         nil or [DateTime]
  83.             max_date         nil or [DateTime]
  84.            
  85.         ProfileStore:RemoveAsync(profile_key) --> is_success [bool]
  86.             -- Completely removes profile data from the DataStore / mock DataStore with no way to recover it.
  87.  
  88.     Methods [VersionQuery]:
  89.  
  90.         VersionQuery:NextAsync() --> [Profile] or nil -- (Yields)
  91.             -- Returned profile is similar to profiles returned by ProfileStore:GetAsync()
  92.        
  93.     Members [Profile]:
  94.    
  95.         Profile.Data               [table]
  96.             -- When the profile is active changes to this table are guaranteed to be saved
  97.         Profile.LastSavedData      [table] (Read-only)
  98.             -- Last snapshot of "Profile.Data" that has been successfully saved to the DataStore;
  99.             Useful for proper developer product purchase receipt handling
  100.        
  101.         Profile.FirstSessionTime   [number] (Read-only)
  102.             -- os.time() timestamp of the first profile session
  103.            
  104.         Profile.SessionLoadCount   [number] (Read-only) -- Amount of times a session was started for this profile
  105.            
  106.         Profile.Session            [table] (Read-only) {PlaceId = number, JobId = string} / nil
  107.             -- Set to a table if this profile is in use by a server; nil if released
  108.  
  109.         Profile.RobloxMetaData     [table] -- Writable table that gets saved automatically and once the profile is released
  110.         Profile.UserIds            [table] -- (Read-only) -- {user_id [number], ...} -- User ids associated with this profile
  111.  
  112.         Profile.KeyInfo            [DataStoreKeyInfo] -- Changes before OnAfterSave signal
  113.        
  114.         Profile.OnSave             [Signal] ()
  115.             -- Triggered right before changes to Profile.Data are saved to the DataStore
  116.            
  117.         Profile.OnLastSave         [Signal] (reason [string]: "Manual", "External", "Shutdown")
  118.             -- Triggered right before changes to Profile.Data are saved to the DataStore
  119.             for the last time; A reason is provided for the last save:
  120.                 - "Manual"   - Profile:EndSession() was called
  121.                 - "Shutdown" - The server that has ownership of this profile is shutting down
  122.                 - "External" - Another server has started a session for this profile
  123.             Note that this event will not trigger for when a profile session is ended by
  124.             another server trying to take ownership of the session - this is impossible to
  125.             do without compromising on ProfileStore's speed.
  126.            
  127.         Profile.OnSessionEnd       [Signal] ()
  128.             -- Triggered when the profile session is terminated on this server
  129.        
  130.         Profile.OnAfterSave        [Signal] (last_saved_data)
  131.             -- Triggered after a successful save
  132.             last_saved_data [table] -- Profile.LastSavedData
  133.            
  134.         Profile.ProfileStore       [ProfileStore] -- ProfileStore object this profile belongs to
  135.         Profile.Key                [string] -- DataStore key
  136.        
  137.     Methods [Profile]:
  138.    
  139.         Profile:IsActive() --> [bool] -- If "true" is returned, changes to Profile.Data are guaranteed to save;
  140.             This guarantee is only valid until code yields (e.g. task.wait() is used).
  141.            
  142.         Profile:Reconcile() -- Fills in missing (nil) [string_key] = [value] pairs to the Profile.Data structure
  143.             from the "template" argument that was passed to "ProfileStore.New()"
  144.            
  145.         Profile:EndSession() -- Call after the server has finished working with this profile
  146.             e.g., after the player leaves (Profile object will become inactive)
  147.  
  148.         Profile:AddUserId(user_id) -- Associates user_id with profile (GDPR compliance)
  149.             user_id   [number]
  150.  
  151.         Profile:RemoveUserId(user_id) -- Removes user_id association with profile (safe function)
  152.             user_id   [number]
  153.            
  154.         Profile:MessageHandler(fn) -- Sets a message handler for this profile
  155.             fn [function] (message [table], processed [function]())
  156.             -- The handler function receives a message table and a callback function;
  157.             The callback function is to be called when a message has been processed
  158.             - this will discard the message from the profile message cache; If the
  159.             callback function is not called, other message handlers will also be triggered
  160.             with unprocessed message data.
  161.            
  162.         Profile:Save() -- If the profile session is still active makes an UpdateAsync call
  163.             to the DataStore to immediately save profile data
  164.  
  165.         Profile:SetAsync() -- Forcefully saves changes to the profile; Only for profiles
  166.             loaded with ProfileStore:GetAsync() or ProfileStore:VersionQuery()
  167.        
  168. --]]
  169.  
  170. local AUTO_SAVE_PERIOD = 300 -- (Seconds) Time between when changes to a profile are saved to the DataStore
  171. local LOAD_REPEAT_PERIOD = 10 -- (Seconds) Time between successive profile reads when handling a session conflict
  172. local FIRST_LOAD_REPEAT = 5 -- (Seconds) Time between first and second profile read when handling a session conflict
  173. local SESSION_STEAL = 40 -- (Seconds) Time until a session conflict is resolved with the waiting server stealing the session
  174. local ASSUME_DEAD = 630 -- (Seconds) If a profile hasn't had updates for this long, quickly assume an active session belongs to a crashed server
  175. local START_SESSION_TIMEOUT = 120 -- (Seconds) If a session can't be started for a profile for this long, stop repeating calls to the DataStore
  176.  
  177. local CRITICAL_STATE_ERROR_COUNT = 5 -- Assume critical state if this many issues happen in a short amount of time
  178. local CRITICAL_STATE_ERROR_EXPIRE = 120 -- (Seconds) Individual issue expiration
  179. local CRITICAL_STATE_EXPIRE = 120 -- (Seconds) Critical state expiration
  180.  
  181. local MAX_MESSAGE_QUEUE = 1000 -- Max messages saved in a profile that were sent using "ProfileStore:MessageAsync()"
  182.  
  183. ----- Dependencies -----
  184.  
  185. -- local Util = require(game.ReplicatedStorage.Shared.Util)
  186. -- local Signal = Util.Signal
  187.  
  188. local Signal do
  189.  
  190.     local FreeRunnerThread
  191.  
  192.     --[[
  193.         Yield-safe coroutine reusing by stravant;
  194.         Sources:
  195.         https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063
  196.         https://gist.github.com/stravant/b75a322e0919d60dde8a0316d1f09d2f
  197.     --]]
  198.  
  199.     local function AcquireRunnerThreadAndCallEventHandler(fn, ...)
  200.         local acquired_runner_thread = FreeRunnerThread
  201.         FreeRunnerThread = nil
  202.         fn(...)
  203.         -- The handler finished running, this runner thread is free again.
  204.         FreeRunnerThread = acquired_runner_thread
  205.     end
  206.  
  207.     local function RunEventHandlerInFreeThread(...)
  208.         AcquireRunnerThreadAndCallEventHandler(...)
  209.         while true do
  210.             AcquireRunnerThreadAndCallEventHandler(coroutine.yield())
  211.         end
  212.     end
  213.  
  214.     local Connection = {}
  215.     Connection.__index = Connection
  216.  
  217.     local SignalClass = {}
  218.     SignalClass.__index = SignalClass
  219.  
  220.     function Connection:Disconnect()
  221.  
  222.         if self.is_connected == false then
  223.             return
  224.         end
  225.  
  226.         local signal = self.signal
  227.         self.is_connected = false
  228.         signal.listener_count -= 1
  229.  
  230.         if signal.head == self then
  231.             signal.head = self.next
  232.         else
  233.             local prev = signal.head
  234.             while prev ~= nil and prev.next ~= self do
  235.                 prev = prev.next
  236.             end
  237.             if prev ~= nil then
  238.                 prev.next = self.next
  239.             end
  240.         end
  241.  
  242.     end
  243.  
  244.     function SignalClass.New()
  245.  
  246.         local self = {
  247.             head = nil,
  248.             listener_count = 0,
  249.         }
  250.         setmetatable(self, SignalClass)
  251.  
  252.         return self
  253.  
  254.     end
  255.  
  256.     function SignalClass:Connect(listener: (...any) -> ())
  257.  
  258.         if type(listener) ~= "function" then
  259.             error(`[{script.Name}]: \"listener\" must be a function; Received {typeof(listener)}`)
  260.         end
  261.  
  262.         local connection = {
  263.             listener = listener,
  264.             signal = self,
  265.             next = self.head,
  266.             is_connected = true,
  267.         }
  268.         setmetatable(connection, Connection)
  269.  
  270.         self.head = connection
  271.         self.listener_count += 1
  272.  
  273.         return connection
  274.  
  275.     end
  276.  
  277.     function SignalClass:GetListenerCount(): number
  278.         return self.listener_count
  279.     end
  280.  
  281.     function SignalClass:Fire(...)
  282.         local item = self.head
  283.         while item ~= nil do
  284.             if item.is_connected == true then
  285.                 if not FreeRunnerThread then
  286.                     FreeRunnerThread = coroutine.create(RunEventHandlerInFreeThread)
  287.                 end
  288.                 task.spawn(FreeRunnerThread, item.listener, ...)
  289.             end
  290.             item = item.next
  291.         end
  292.     end
  293.  
  294.     function SignalClass:Wait()
  295.         local co = coroutine.running()
  296.         local connection
  297.         connection = self:Connect(function(...)
  298.             connection:Disconnect()
  299.             task.spawn(co, ...)
  300.         end)
  301.         return coroutine.yield()
  302.     end
  303.  
  304.     Signal = table.freeze({
  305.         New = SignalClass.New,
  306.     })
  307.  
  308. end
  309.  
  310. ----- Private -----
  311.  
  312. local ActiveSessionCheck = {} -- {[session_token] = profile, ...}
  313. local AutoSaveList = {} -- {profile, ...} -- Loaded profile table which will be circularly auto-saved
  314. local IssueQueue = {} -- {issue_time, ...}
  315.  
  316. local DataStoreService = game:GetService("DataStoreService")
  317. local MessagingService = game:GetService("MessagingService")
  318. local HttpService = game:GetService("HttpService")
  319. local RunService = game:GetService("RunService")
  320.  
  321. local PlaceId = game.PlaceId
  322. local JobId = game.JobId
  323.  
  324. local AutoSaveIndex = 1 -- Next profile to auto save
  325. local LastAutoSave = os.clock()
  326.  
  327. local LoadIndex = 0
  328.  
  329. local ActiveProfileLoadJobs = 0 -- Number of active threads that are loading in profiles
  330. local ActiveProfileSaveJobs = 0 -- Number of active threads that are saving profiles
  331.  
  332. local CriticalStateStart = 0 -- os.clock()
  333.  
  334. local IsStudio = RunService:IsStudio()
  335. local DataStoreState: "NotReady" | "NoInternet" | "NoAccess" | "Access" = "NotReady"
  336.  
  337. local MockStore = {}
  338. local UserMockStore = {}
  339. local MockFlag = false
  340.  
  341. local OnError = Signal.New() -- (message, store_name, profile_key)
  342. local OnOverwrite = Signal.New() -- (store_name, profile_key)
  343.  
  344. local UpdateQueue = { -- For stability sake, we won't do UpdateAsync calls for the same key until all previous calls finish
  345.     --[[
  346.         [session_token] = {
  347.             coroutine, ...
  348.         },
  349.         ...
  350.     --]]
  351. }
  352.  
  353. local function WaitInUpdateQueue(session_token) --> next_in_queue()
  354.  
  355.     local is_first = false
  356.  
  357.     if UpdateQueue[session_token] == nil then
  358.         is_first = true
  359.         UpdateQueue[session_token] = {}
  360.     end
  361.  
  362.     local queue = UpdateQueue[session_token]
  363.  
  364.     if is_first == false then
  365.         table.insert(queue, coroutine.running())
  366.         coroutine.yield()
  367.     end
  368.  
  369.     return function()
  370.         local next_co = table.remove(queue, 1)
  371.         if next_co ~= nil then
  372.             coroutine.resume(next_co)
  373.         else
  374.             UpdateQueue[session_token] = nil
  375.         end
  376.     end
  377.  
  378. end
  379.  
  380. local function SessionToken(store_name, profile_key, is_mock)
  381.  
  382.     local session_token = "L_" -- Live
  383.  
  384.     if is_mock == true then
  385.         session_token = "U_" -- User mock
  386.     elseif DataStoreState ~= "Access" then
  387.         session_token = "M_" -- Mock, cause no DataStore access
  388.     end
  389.  
  390.     session_token ..= store_name .. "\0" .. profile_key
  391.  
  392.     return session_token
  393.  
  394. end
  395.  
  396. local function DeepCopyTable(t)
  397.     local copy = {}
  398.     for key, value in pairs(t) do
  399.         if type(value) == "table" then
  400.             copy[key] = DeepCopyTable(value)
  401.         else
  402.             copy[key] = value
  403.         end
  404.     end
  405.     return copy
  406. end
  407.  
  408. local function ReconcileTable(target, template)
  409.     for k, v in pairs(template) do
  410.         if type(k) == "string" then -- Only string keys will be reconciled
  411.             if target[k] == nil then
  412.                 if type(v) == "table" then
  413.                     target[k] = DeepCopyTable(v)
  414.                 else
  415.                     target[k] = v
  416.                 end
  417.             elseif type(target[k]) == "table" and type(v) == "table" then
  418.                 ReconcileTable(target[k], v)
  419.             end
  420.         end
  421.     end
  422. end
  423.  
  424. local function RegisterError(error_message, store_name, profile_key) -- Called when a DataStore API call errors
  425.     warn(`[{script.Name}]: DataStore API error (STORE:{store_name}; KEY:{profile_key}) - {tostring(error_message)}`)
  426.     table.insert(IssueQueue, os.clock()) -- Adding issue time to queue
  427.     OnError:Fire(tostring(error_message), store_name, profile_key)
  428. end
  429.  
  430. local function RegisterOverwrite(store_name, profile_key) -- Called when a corrupted profile is loaded
  431.     warn(`[{script.Name}]: Invalid profile was overwritten (STORE:{store_name}; KEY:{profile_key})`)
  432.     OnOverwrite:Fire(store_name, profile_key)
  433. end
  434.  
  435. local function NewMockDataStoreKeyInfo(params)
  436.  
  437.     local version_id_string = tostring(params.VersionId or 0)
  438.     local meta_data = params.MetaData or {}
  439.     local user_ids = params.UserIds or {}
  440.  
  441.     return {
  442.         CreatedTime = params.CreatedTime,
  443.         UpdatedTime = params.UpdatedTime,
  444.         Version = string.rep("0", 16) .. "."
  445.             .. string.rep("0", 10 - string.len(version_id_string)) .. version_id_string
  446.             .. "." .. string.rep("0", 16) .. "." .. "01",
  447.  
  448.         GetMetadata = function()
  449.             return DeepCopyTable(meta_data)
  450.         end,
  451.  
  452.         GetUserIds = function()
  453.             return DeepCopyTable(user_ids)
  454.         end,
  455.     }
  456.  
  457. end
  458.  
  459. local function MockUpdateAsync(mock_data_store, profile_store_name, key, transform_function, is_get_call) --> loaded_data, key_info
  460.  
  461.     local profile_store = mock_data_store[profile_store_name]
  462.  
  463.     if profile_store == nil then
  464.         profile_store = {}
  465.         mock_data_store[profile_store_name] = profile_store
  466.     end
  467.  
  468.     local epoch_time = math.floor(os.time() * 1000)
  469.     local mock_entry = profile_store[key]
  470.     local mock_entry_was_nil = false
  471.  
  472.     if mock_entry == nil then
  473.         mock_entry_was_nil = true
  474.         if is_get_call ~= true then
  475.             mock_entry = {
  476.                 Data = nil,
  477.                 CreatedTime = epoch_time,
  478.                 UpdatedTime = epoch_time,
  479.                 VersionId = 0,
  480.                 UserIds = {},
  481.                 MetaData = {},
  482.             }
  483.             profile_store[key] = mock_entry
  484.         end
  485.     end
  486.  
  487.     local mock_key_info = mock_entry_was_nil == false and NewMockDataStoreKeyInfo(mock_entry) or nil
  488.  
  489.     local transform, user_ids, roblox_meta_data = transform_function(mock_entry and mock_entry.Data, mock_key_info)
  490.  
  491.     if transform == nil then
  492.         return nil
  493.     else
  494.         if mock_entry ~= nil and is_get_call ~= true then
  495.             mock_entry.Data = DeepCopyTable(transform)
  496.             mock_entry.UserIds = DeepCopyTable(user_ids or {})
  497.             mock_entry.MetaData = DeepCopyTable(roblox_meta_data or {})
  498.             mock_entry.VersionId += 1
  499.             mock_entry.UpdatedTime = epoch_time
  500.         end
  501.  
  502.         return DeepCopyTable(transform), mock_entry ~= nil and NewMockDataStoreKeyInfo(mock_entry) or nil
  503.     end
  504.  
  505. end
  506.  
  507. local function UpdateAsync(profile_store, profile_key, transform_params, is_user_mock, is_get_call, version) --> loaded_data, key_info
  508.     --transform_params = {
  509.     --  ExistingProfileHandle = function(latest_data),
  510.     --  MissingProfileHandle = function(latest_data),
  511.     --  EditProfile = function(latest_data),
  512.     --}
  513.  
  514.     local loaded_data, key_info
  515.  
  516.     local next_in_queue = WaitInUpdateQueue(SessionToken(profile_store.Name, profile_key, is_user_mock))
  517.  
  518.     local success = true
  519.  
  520.     local success, error_message = pcall(function()
  521.         local transform_function = function(latest_data)
  522.  
  523.             local missing_profile = false
  524.             local overwritten = false
  525.             local global_updates = {0, {}}
  526.  
  527.             if latest_data == nil then
  528.  
  529.                 missing_profile = true
  530.  
  531.             elseif type(latest_data) ~= "table" then
  532.  
  533.                 missing_profile = true
  534.                 overwritten = true
  535.  
  536.             else
  537.  
  538.                 if type(latest_data.Data) == "table" and type(latest_data.MetaData) == "table" and type(latest_data.GlobalUpdates) == "table" then
  539.  
  540.                     -- Regular profile structure detected:
  541.  
  542.                     latest_data.WasOverwritten = false -- Must be set to false if set previously
  543.                     global_updates = latest_data.GlobalUpdates
  544.  
  545.                     if transform_params.ExistingProfileHandle ~= nil then
  546.                         transform_params.ExistingProfileHandle(latest_data)
  547.                     end
  548.  
  549.                 elseif latest_data.Data == nil and latest_data.MetaData == nil and type(latest_data.GlobalUpdates) == "table" then
  550.  
  551.                     -- Regular structure not detected, but GlobalUpdate data exists:
  552.  
  553.                     latest_data.WasOverwritten = false -- Must be set to false if set previously
  554.                     global_updates = latest_data.GlobalUpdates or global_updates
  555.                     missing_profile = true
  556.  
  557.                 else
  558.  
  559.                     missing_profile = true
  560.                     overwritten = true
  561.  
  562.                 end
  563.  
  564.             end
  565.  
  566.             -- Profile was not created or corrupted and no GlobalUpdate data exists:
  567.             if missing_profile == true then
  568.                 latest_data = {
  569.                     -- Data = nil,
  570.                     -- MetaData = nil,
  571.                     GlobalUpdates = global_updates,
  572.                 }
  573.                 if transform_params.MissingProfileHandle ~= nil then
  574.                     transform_params.MissingProfileHandle(latest_data)
  575.                 end
  576.             end
  577.  
  578.             -- Editing profile:
  579.             if transform_params.EditProfile ~= nil then
  580.                 transform_params.EditProfile(latest_data)
  581.             end
  582.  
  583.             -- Invalid data handling (Silently override with empty profile)
  584.             if overwritten == true then
  585.                 latest_data.WasOverwritten = true -- Temporary tag that will be removed on first save
  586.             end
  587.  
  588.             return latest_data, latest_data.UserIds, latest_data.RobloxMetaData
  589.         end
  590.  
  591.         if is_user_mock == true then -- Used when the profile is accessed through ProfileStore.Mock
  592.  
  593.             loaded_data, key_info = MockUpdateAsync(UserMockStore, profile_store.Name, profile_key, transform_function, is_get_call)
  594.             task.wait() -- Simulate API call yield
  595.  
  596.         elseif DataStoreState ~= "Access" then -- Used when API access is disabled
  597.  
  598.             loaded_data, key_info = MockUpdateAsync(MockStore, profile_store.Name, profile_key, transform_function, is_get_call)
  599.             task.wait() -- Simulate API call yield
  600.  
  601.         else
  602.  
  603.             if is_get_call == true then
  604.  
  605.                 if version ~= nil then
  606.  
  607.                     local success, error_message = pcall(function()
  608.                         loaded_data, key_info = profile_store.data_store:GetVersionAsync(profile_key, version)
  609.                     end)
  610.  
  611.                     if success == false and type(error_message) == "string" and string.find(error_message, "not valid") ~= nil then
  612.                         warn(`[{script.Name}]: Passed version argument is not valid; Traceback:\n` .. debug.traceback())
  613.                     end
  614.  
  615.                 else
  616.  
  617.                     loaded_data, key_info = profile_store.data_store:GetAsync(profile_key)
  618.  
  619.                 end
  620.  
  621.                 loaded_data = transform_function(loaded_data)
  622.  
  623.             else
  624.  
  625.                 loaded_data, key_info = profile_store.data_store:UpdateAsync(profile_key, transform_function)
  626.  
  627.             end
  628.  
  629.         end
  630.  
  631.     end)
  632.  
  633.     next_in_queue()
  634.  
  635.     if success == true and type(loaded_data) == "table" then
  636.         -- Invalid data handling:
  637.         if loaded_data.WasOverwritten == true and is_get_call ~= true then
  638.             RegisterOverwrite(
  639.                 profile_store.Name,
  640.                 profile_key
  641.             )
  642.         end
  643.         -- Return loaded_data:
  644.         return loaded_data, key_info
  645.     else
  646.         -- Error handling:
  647.         RegisterError(
  648.             error_message or "Undefined error",
  649.             profile_store.Name,
  650.             profile_key
  651.         )
  652.         -- Return nothing:
  653.         return nil
  654.     end
  655.  
  656. end
  657.  
  658. local function IsThisSession(session_tag)
  659.     return session_tag[1] == PlaceId and session_tag[2] == JobId
  660. end
  661.  
  662. local function ReadMockFlag(): boolean
  663.     local is_mock = MockFlag
  664.     MockFlag = false
  665.     return is_mock
  666. end
  667.  
  668. local function WaitForStoreReady(profile_store)
  669.     while profile_store.is_ready == false do
  670.         task.wait()
  671.     end
  672. end
  673.  
  674. local function AddProfileToAutoSave(profile)
  675.  
  676.     ActiveSessionCheck[profile.session_token] = profile
  677.  
  678.     -- Add at AutoSaveIndex and move AutoSaveIndex right:
  679.  
  680.     table.insert(AutoSaveList, AutoSaveIndex, profile)
  681.  
  682.     if #AutoSaveList > 1 then
  683.         AutoSaveIndex = AutoSaveIndex + 1
  684.     elseif #AutoSaveList == 1 then
  685.         -- First profile created - make sure it doesn't get immediately auto saved:
  686.         LastAutoSave = os.clock()
  687.     end
  688.  
  689. end
  690.  
  691. local function RemoveProfileFromAutoSave(profile)
  692.  
  693.     ActiveSessionCheck[profile.session_token] = nil
  694.  
  695.     local auto_save_index = table.find(AutoSaveList, profile)
  696.  
  697.     if auto_save_index ~= nil then
  698.         table.remove(AutoSaveList, auto_save_index)
  699.         if auto_save_index < AutoSaveIndex then
  700.             AutoSaveIndex = AutoSaveIndex - 1 -- Table contents were moved left before AutoSaveIndex so move AutoSaveIndex left as well
  701.         end
  702.         if AutoSaveList[AutoSaveIndex] == nil then -- AutoSaveIndex was at the end of the AutoSaveList - reset to 1
  703.             AutoSaveIndex = 1
  704.         end
  705.     end
  706.  
  707. end
  708.  
  709. local function SaveProfileAsync(profile, is_ending_session, is_overwriting, last_save_reason)
  710.  
  711.     if type(profile.Data) ~= "table" then
  712.         error(`[{script.Name}]: Developer code likely set "Profile.Data" to a non-table value! (STORE:{profile.ProfileStore.Name}; KEY:{profile.Key})`)
  713.     end
  714.  
  715.     profile.OnSave:Fire()
  716.     if is_ending_session == true then
  717.         profile.OnLastSave:Fire(last_save_reason or "Manual")
  718.     end
  719.  
  720.     if is_ending_session == true and is_overwriting ~= true then
  721.         if profile.roblox_message_subscription ~= nil then
  722.             profile.roblox_message_subscription:Disconnect()
  723.         end
  724.         RemoveProfileFromAutoSave(profile)
  725.         profile.OnSessionEnd:Fire()
  726.     end
  727.  
  728.     ActiveProfileSaveJobs = ActiveProfileSaveJobs + 1
  729.  
  730.     -- Compare "SessionLoadCount" when writing to profile to prevent a rare case of repeat last save when the profile is loaded on the same server again
  731.  
  732.     local repeat_save_flag = true -- Released Profile save calls have to repeat until they succeed
  733.     local exp_backoff = 1
  734.  
  735.     while repeat_save_flag == true do
  736.  
  737.         if is_ending_session ~= true then
  738.             repeat_save_flag = false
  739.         end
  740.  
  741.         local loaded_data, key_info = UpdateAsync(
  742.             profile.ProfileStore,
  743.             profile.Key,
  744.             {
  745.                 ExistingProfileHandle = nil,
  746.                 MissingProfileHandle = nil,
  747.                 EditProfile = function(latest_data)
  748.  
  749.                     -- Check if this session still owns the profile:
  750.  
  751.                     local session_owns_profile = false
  752.  
  753.                     if is_overwriting ~= true then
  754.  
  755.                         local active_session = latest_data.MetaData.ActiveSession
  756.                         local session_load_count = latest_data.MetaData.SessionLoadCount
  757.  
  758.                         if type(active_session) == "table" then
  759.                             session_owns_profile = IsThisSession(active_session) and session_load_count == profile.load_index
  760.                         end
  761.  
  762.                     else
  763.                         session_owns_profile = true
  764.                     end
  765.  
  766.                     -- We may only edit the profile if this server has ownership of the profile:
  767.  
  768.                     if session_owns_profile == true then
  769.  
  770.                         -- Clear processed updates (messages):
  771.  
  772.                         local locked_updates = profile.locked_global_updates -- [index] = true, ...
  773.                         local active_updates = latest_data.GlobalUpdates[2]
  774.                         -- ProfileService module format: {{update_id, version_id, update_locked, update_data}, ...}
  775.                         -- ProfileStore module format: {{update_id, update_data}, ...}
  776.  
  777.                         if next(locked_updates) ~= nil then
  778.                             local i = 1
  779.                             while i <= #active_updates do
  780.                                 local update = active_updates[i]
  781.                                 if locked_updates[update[1]] == true then
  782.                                     table.remove(active_updates, i)
  783.                                 else
  784.                                     i += 1
  785.                                 end
  786.                             end
  787.                         end
  788.  
  789.                         -- Save profile data:
  790.  
  791.                         latest_data.Data = profile.Data
  792.                         latest_data.RobloxMetaData = profile.RobloxMetaData
  793.                         latest_data.UserIds = profile.UserIds
  794.  
  795.                         if is_overwriting ~= true then
  796.  
  797.                             latest_data.MetaData.LastUpdate = os.time()
  798.  
  799.                             if is_ending_session == true then
  800.                                 latest_data.MetaData.ActiveSession = nil
  801.                             end
  802.  
  803.                         else
  804.  
  805.                             latest_data.MetaData.ActiveSession = nil
  806.                             latest_data.MetaData.ForceLoadSession = nil
  807.  
  808.                         end
  809.  
  810.                     end
  811.  
  812.                 end,
  813.             },
  814.             profile.is_mock
  815.         )
  816.  
  817.         if loaded_data ~= nil and key_info ~= nil then
  818.  
  819.             if is_overwriting == true then
  820.                 break
  821.             end
  822.  
  823.             repeat_save_flag = false
  824.  
  825.             local active_session = loaded_data.MetaData.ActiveSession
  826.             local session_load_count = loaded_data.MetaData.SessionLoadCount
  827.             local session_owns_profile = false
  828.  
  829.             if type(active_session) == "table" then
  830.                 session_owns_profile = IsThisSession(active_session) and session_load_count == profile.load_index
  831.             end
  832.  
  833.             local force_load_session = loaded_data.MetaData.ForceLoadSession
  834.             local force_load_pending = false
  835.             if type(force_load_session) == "table" then
  836.                 force_load_pending = not IsThisSession(force_load_session)
  837.             end
  838.  
  839.             local is_active = profile:IsActive()
  840.  
  841.             -- If another server is trying to start a session for this profile - end the session:
  842.  
  843.             if force_load_pending == true and session_owns_profile == true then
  844.                 if is_active == true then
  845.                     SaveProfileAsync(profile, true, false, "External")
  846.                 end
  847.                 break
  848.             end
  849.  
  850.             -- Clearing processed update list / Detecting new updates:
  851.  
  852.             local locked_updates = profile.locked_global_updates -- [index] = true, ...
  853.             local received_updates = profile.received_global_updates -- [index] = true, ...
  854.             local active_updates = loaded_data.GlobalUpdates[2]
  855.  
  856.             local new_updates = {} -- {}, ...
  857.             local still_pending = {} -- [index] = true, ...
  858.  
  859.             for _, update in ipairs(active_updates) do
  860.                 if locked_updates[update[1]] == true then
  861.                     still_pending[update[1]] = true
  862.                 elseif received_updates[update[1]] ~= true then
  863.                     received_updates[update[1]] = true
  864.                     table.insert(new_updates, update)
  865.                 end
  866.             end
  867.  
  868.             for index in pairs(locked_updates) do
  869.                 if still_pending[index] ~= true then
  870.                     locked_updates[index] = nil
  871.                 end
  872.             end
  873.  
  874.             -- Updating profile values:
  875.  
  876.             profile.KeyInfo = key_info
  877.             profile.LastSavedData = loaded_data.Data
  878.             profile.global_updates = loaded_data.GlobalUpdates and loaded_data.GlobalUpdates[2] or {}
  879.  
  880.             if session_owns_profile == true then
  881.                 if is_active == true and is_ending_session ~= true then
  882.  
  883.                     -- Processing new global updates (messages):
  884.  
  885.                     for _, update in ipairs(new_updates) do
  886.  
  887.                         local index = update[1]
  888.                         local update_data = update[#update] -- Backwards compatibility with ProfileService
  889.  
  890.                         for _, handler in ipairs(profile.message_handlers) do
  891.  
  892.                             local is_processed = false
  893.                             local processed_callback = function()
  894.                                 is_processed = true
  895.                                 locked_updates[index] = true
  896.                             end
  897.  
  898.                             local send_update_data = DeepCopyTable(update_data)
  899.  
  900.                             task.spawn(handler, send_update_data, processed_callback)
  901.  
  902.                             if is_processed == true then
  903.                                 break
  904.                             end
  905.  
  906.                         end
  907.  
  908.                     end
  909.  
  910.                 end
  911.             else
  912.  
  913.                 if profile.roblox_message_subscription ~= nil then
  914.                     profile.roblox_message_subscription:Disconnect()
  915.                 end
  916.  
  917.                 if is_active == true then
  918.                     RemoveProfileFromAutoSave(profile)
  919.                     profile.OnSessionEnd:Fire()
  920.                 end
  921.  
  922.             end
  923.  
  924.             profile.OnAfterSave:Fire(profile.LastSavedData)
  925.  
  926.         elseif repeat_save_flag == true then
  927.  
  928.             -- DataStore call likely resulted in an error; Repeat the DataStore call shortly
  929.             task.wait(exp_backoff)
  930.             exp_backoff = math.min(if last_save_reason == "Shutdown" then 8 else 20, exp_backoff * 2)
  931.  
  932.         end
  933.  
  934.     end
  935.  
  936.     ActiveProfileSaveJobs = ActiveProfileSaveJobs - 1
  937.  
  938. end
  939.  
  940. ----- Public -----
  941.  
  942. --[[
  943.     Saved profile structure:
  944.    
  945.     {
  946.         Data = {},
  947.        
  948.         MetaData = {
  949.             ProfileCreateTime = 0,
  950.             SessionLoadCount = 0,
  951.             ActiveSession = {place_id, game_job_id, unique_session_id} / nil,
  952.             ForceLoadSession = {place_id, game_job_id} / nil,
  953.             LastUpdate = 0, -- os.time()
  954.             MetaTags = {}, -- Backwards compatibility with ProfileService
  955.         },
  956.        
  957.         RobloxMetaData = {},
  958.         UserIds = {},
  959.        
  960.         GlobalUpdates = {
  961.             update_index,
  962.             {
  963.                 {update_index, data}, ...
  964.             },
  965.         },
  966.     }
  967.  
  968. --]]
  969.  
  970. export type JSONAcceptable = { JSONAcceptable } | { [string]: JSONAcceptable } | number | string | boolean | buffer
  971.  
  972. export type Profile<T> = {
  973.     Data: T & JSONAcceptable,
  974.     LastSavedData: T & JSONAcceptable,
  975.     FirstSessionTime: number,
  976.     SessionLoadCount: number,
  977.     Session: {PlaceId: number, JobId: string}?,
  978.     RobloxMetaData: JSONAcceptable,
  979.     UserIds: {number},
  980.     KeyInfo: DataStoreKeyInfo,
  981.     OnSave: {Connect: (self: any, listener: () -> ()) -> ({Disconnect: (self: any) -> ()})},
  982.     OnLastSave: {Connect: (self: any, listener: (reason: "Manual" | "External" | "Shutdown") -> ()) -> ({Disconnect: (self: any) -> ()})},
  983.     OnSessionEnd: {Connect: (self: any, listener: () -> ()) -> ({Disconnect: (self: any) -> ()})},
  984.     OnAfterSave: {Connect: (self: any, listener: (last_saved_data: T & JSONAcceptable) -> ()) -> ({Disconnect: (self: any) -> ()})},
  985.     ProfileStore: JSONAcceptable,
  986.     Key: string,
  987.  
  988.     IsActive: (self: any) -> (boolean),
  989.     Reconcile: (self: any) -> (),
  990.     EndSession: (self: any) -> (),
  991.     AddUserId: (self: any, user_id: number) -> (),
  992.     RemoveUserId: (self: any, user_id: number) -> (),
  993.     MessageHandler: (self: any, fn: (message: JSONAcceptable, processed: () -> ()) -> ()) -> (),
  994.     Save: (self: any) -> (),
  995.     SetAsync: (self: any) -> (),
  996. }
  997.  
  998. export type VersionQuery<T> = {
  999.     NextAsync: (self: any) -> (Profile<T>?),
  1000. }
  1001.  
  1002. type ProfileStoreStandard<T> = {
  1003.     Name: string,
  1004.     StartSessionAsync: (self: any, profile_key: string, params: {Steal: boolean?}) -> (Profile<T>?),
  1005.     MessageAsync: (self: any, profile_key: string, message: JSONAcceptable) -> (boolean),
  1006.     GetAsync: (self: any, profile_key: string, version: string?) -> (Profile<T>?),
  1007.     VersionQuery: (self: any, profile_key: string, sort_direction: Enum.SortDirection?, min_date: DateTime | number | nil, max_date: DateTime | number | nil) -> (VersionQuery<T>),
  1008.     RemoveAsync: (self: any, profile_key: string) -> (boolean),
  1009. }
  1010.  
  1011. export type ProfileStore<T> = {
  1012.     Mock: ProfileStoreStandard<T>,
  1013. } & ProfileStoreStandard<T>
  1014.  
  1015. type ConstantName = "AUTO_SAVE_PERIOD" | "LOAD_REPEAT_PERIOD" | "FIRST_LOAD_REPEAT" | "SESSION_STEAL"
  1016. | "ASSUME_DEAD" | "START_SESSION_TIMEOUT" | "CRITICAL_STATE_ERROR_COUNT" | "CRITICAL_STATE_ERROR_EXPIRE"
  1017. | "CRITICAL_STATE_EXPIRE" | "MAX_MESSAGE_QUEUE"
  1018.  
  1019. export type ProfileStoreModule = {
  1020.     IsClosing: boolean,
  1021.     IsCriticalState: boolean,
  1022.     OnError: {Connect: (self: any, listener: (message: string, store_name: string, profile_key: string) -> ()) -> ({Disconnect: (self: any) -> ()})},
  1023.     OnOverwrite: {Connect: (self: any, listener: (store_name: string, profile_key: string) -> ()) -> ({Disconnect: (self: any) -> ()})},
  1024.     OnCriticalToggle: {Connect: (self: any, listener: (is_critical: boolean) -> ()) -> ({Disconnect: (self: any) -> ()})},
  1025.     DataStoreState: "NotReady" | "NoInternet" | "NoAccess" | "Access",
  1026.     New: <T>(store_name: string, template: (T & JSONAcceptable)?) -> (ProfileStore<T>),
  1027.     SetConstant: (name: ConstantName, value: number) -> ()
  1028. }
  1029.  
  1030. local Profile = {}
  1031. Profile.__index = Profile
  1032.  
  1033. function Profile.New(raw_data, key_info, profile_store, key, is_mock, session_token)
  1034.  
  1035.     local data = raw_data.Data or {}
  1036.     local session = raw_data.MetaData and raw_data.MetaData.ActiveSession or nil
  1037.  
  1038.     local global_updates = raw_data.GlobalUpdates and raw_data.GlobalUpdates[2] or {}
  1039.     local received_global_updates = {}
  1040.  
  1041.     for _, update in ipairs(global_updates) do
  1042.         received_global_updates[update[1]] = true
  1043.     end
  1044.  
  1045.     local self = {
  1046.  
  1047.         Data = data,
  1048.         LastSavedData = DeepCopyTable(data),
  1049.  
  1050.         FirstSessionTime = raw_data.MetaData and raw_data.MetaData.ProfileCreateTime or 0,
  1051.         SessionLoadCount = raw_data.MetaData and raw_data.MetaData.SessionLoadCount or 0,
  1052.         Session = session and {PlaceId = session[1], JobId = session[2]},
  1053.  
  1054.         RobloxMetaData = raw_data.RobloxMetaData or {},
  1055.         UserIds = raw_data.UserIds or {},
  1056.         KeyInfo = key_info,
  1057.  
  1058.         OnAfterSave = Signal.New(),
  1059.         OnSave = Signal.New(),
  1060.         OnLastSave = Signal.New(),
  1061.         OnSessionEnd = Signal.New(),
  1062.  
  1063.         ProfileStore = profile_store,
  1064.         Key = key,
  1065.  
  1066.         load_timestamp = os.clock(),
  1067.         is_mock = is_mock,
  1068.         session_token = session_token or "",
  1069.         load_index = raw_data.MetaData and raw_data.MetaData.SessionLoadCount or 0,
  1070.         locked_global_updates = {},
  1071.         received_global_updates = received_global_updates,
  1072.         message_handlers = {},
  1073.         global_updates = global_updates,
  1074.  
  1075.     }
  1076.     setmetatable(self, Profile)
  1077.  
  1078.     return self
  1079.  
  1080. end
  1081.  
  1082. function Profile:IsActive()
  1083.     return ActiveSessionCheck[self.session_token] == self
  1084. end
  1085.  
  1086. function Profile:Reconcile()
  1087.     ReconcileTable(self.Data, self.ProfileStore.template)
  1088. end
  1089.  
  1090. function Profile:EndSession()
  1091.     if self:IsActive() == true then
  1092.         task.spawn(SaveProfileAsync, self, true, nil, "Manual") -- Call save function in a new thread with release_from_session = true
  1093.     end
  1094. end
  1095.  
  1096. function Profile:AddUserId(user_id) -- Associates user_id with profile (GDPR compliance)
  1097.  
  1098.     if type(user_id) ~= "number" or user_id % 1 ~= 0 then
  1099.         warn(`[{script.Name}]: Invalid UserId argument for :AddUserId() ({tostring(user_id)}); Traceback:\n` .. debug.traceback())
  1100.         return
  1101.     end
  1102.  
  1103.     if user_id < 0 and self.is_mock ~= true and DataStoreState == "Access" then
  1104.         return -- Avoid giving real Roblox APIs negative UserId's
  1105.     end
  1106.  
  1107.     if table.find(self.UserIds, user_id) == nil then
  1108.         table.insert(self.UserIds, user_id)
  1109.     end
  1110.  
  1111. end
  1112.  
  1113. function Profile:RemoveUserId(user_id) -- Removes user_id association with profile (safe function)
  1114.  
  1115.     if type(user_id) ~= "number" or user_id % 1 ~= 0 then
  1116.         warn(`[{script.Name}]: Invalid UserId argument for :RemoveUserId() ({tostring(user_id)}); Traceback:\n` .. debug.traceback())
  1117.         return
  1118.     end
  1119.  
  1120.     local index = table.find(self.UserIds, user_id)
  1121.  
  1122.     if index ~= nil then
  1123.         table.remove(self.UserIds, index)
  1124.     end
  1125.  
  1126. end
  1127.  
  1128. function Profile:SetAsync() -- Saves the profile to the DataStore and removes the session lock
  1129.  
  1130.     if self.view_mode ~= true then
  1131.         error(`[{script.Name}]: :SetAsync() can only be used in view mode`)
  1132.     end
  1133.  
  1134.     SaveProfileAsync(self, nil, true)
  1135.  
  1136. end
  1137.  
  1138. function Profile:MessageHandler(fn)
  1139.  
  1140.     if type(fn) ~= "function" then
  1141.         error(`[{script.Name}]: fn argument is not a function`)
  1142.     end
  1143.  
  1144.     if self.view_mode ~= true and self:IsActive() ~= true then
  1145.         return -- Don't process messages if the profile session was ended
  1146.     end
  1147.  
  1148.     local locked_updates = self.locked_global_updates
  1149.     table.insert(self.message_handlers, fn)
  1150.  
  1151.     for _, update in ipairs(self.global_updates) do
  1152.  
  1153.         local index = update[1]
  1154.         local update_data = update[#update] -- Backwards compatibility with ProfileService
  1155.  
  1156.         if locked_updates[index] ~= true then
  1157.  
  1158.             local processed_callback = function()
  1159.                 locked_updates[index] = true
  1160.             end
  1161.  
  1162.             local send_update_data = DeepCopyTable(update_data)
  1163.  
  1164.             task.spawn(fn, send_update_data, processed_callback)
  1165.  
  1166.         end
  1167.  
  1168.     end
  1169.  
  1170. end
  1171.  
  1172. function Profile:Save()
  1173.  
  1174.     if self.view_mode == true then
  1175.         error(`[{script.Name}]: Can't save profile in view mode; Should you be calling :SetAsync() instead?`)
  1176.     end
  1177.  
  1178.     if self:IsActive() == false then
  1179.         warn(`[{script.Name}]: Attempted saving an inactive profile (STORE:{self.ProfileStore.Name}; KEY:{self.Key});`
  1180.             .. ` Traceback:\n` .. debug.traceback())
  1181.         return
  1182.     end
  1183.  
  1184.     -- Move the profile right behind the auto save index to delay the next auto save for it:
  1185.     RemoveProfileFromAutoSave(self)
  1186.     AddProfileToAutoSave(self)
  1187.  
  1188.     -- Perform save in new thread:
  1189.     task.spawn(SaveProfileAsync, self)
  1190.  
  1191. end
  1192.  
  1193. local ProfileStore: ProfileStoreModule = {
  1194.  
  1195.     IsClosing = false,
  1196.     IsCriticalState = false,
  1197.     OnError = OnError, -- (message, store_name, profile_key)
  1198.     OnOverwrite = OnOverwrite, -- (store_name, profile_key)
  1199.     OnCriticalToggle = Signal.New(), -- (is_critical)
  1200.     DataStoreState = "NotReady", -- ("NotReady", "NoInternet", "NoAccess", "Access")
  1201.  
  1202. }
  1203. ProfileStore.__index = ProfileStore
  1204.  
  1205. function ProfileStore.SetConstant(name, value)
  1206.  
  1207.     if type(value) ~= "number" then
  1208.         error(`[{script.Name}]: Invalid value type`)
  1209.     end
  1210.  
  1211.     if name == "AUTO_SAVE_PERIOD" then
  1212.         AUTO_SAVE_PERIOD = value
  1213.     elseif name == "LOAD_REPEAT_PERIOD" then
  1214.         LOAD_REPEAT_PERIOD = value
  1215.     elseif name == "FIRST_LOAD_REPEAT" then
  1216.         FIRST_LOAD_REPEAT = value
  1217.     elseif name == "SESSION_STEAL" then
  1218.         SESSION_STEAL = value
  1219.     elseif name == "ASSUME_DEAD" then
  1220.         ASSUME_DEAD = value
  1221.     elseif name == "START_SESSION_TIMEOUT" then
  1222.         START_SESSION_TIMEOUT = value
  1223.     elseif name == "CRITICAL_STATE_ERROR_COUNT" then
  1224.         CRITICAL_STATE_ERROR_COUNT = value
  1225.     elseif name == "CRITICAL_STATE_ERROR_EXPIRE" then
  1226.         CRITICAL_STATE_ERROR_EXPIRE = value
  1227.     elseif name == "CRITICAL_STATE_EXPIRE" then
  1228.         CRITICAL_STATE_EXPIRE = value
  1229.     elseif name == "MAX_MESSAGE_QUEUE" then
  1230.         MAX_MESSAGE_QUEUE = value
  1231.     else
  1232.         error(`[{script.Name}]: Invalid constant name was provided`)
  1233.     end
  1234.  
  1235. end
  1236.  
  1237. function ProfileStore.Test()
  1238.     return {
  1239.         ActiveSessionCheck = ActiveSessionCheck,
  1240.         AutoSaveList = AutoSaveList,
  1241.         ActiveProfileLoadJobs = ActiveProfileLoadJobs,
  1242.         ActiveProfileSaveJobs = ActiveProfileSaveJobs,
  1243.         MockStore = MockStore,
  1244.         UserMockStore = UserMockStore,
  1245.         UpdateQueue = UpdateQueue,
  1246.     }
  1247. end
  1248.  
  1249. function ProfileStore.New(store_name, template)
  1250.  
  1251.     template = template or {}
  1252.  
  1253.     if type(store_name) ~= "string" then
  1254.         error(`[{script.Name}]: Invalid or missing "store_name"`)
  1255.     elseif string.len(store_name) == 0 then
  1256.         error(`[{script.Name}]: store_name cannot be an empty string`)
  1257.     elseif string.len(store_name) > 50 then
  1258.         error(`[{script.Name}]: store_name is too long`)
  1259.     end
  1260.  
  1261.     if type(template) ~= "table" then
  1262.         error(`[{script.Name}]: Invalid template argument`)
  1263.     end
  1264.  
  1265.     local self
  1266.     self = {
  1267.  
  1268.         Mock = {
  1269.  
  1270.             Name = store_name,
  1271.  
  1272.             StartSessionAsync = function(_, profile_key)
  1273.                 MockFlag = true
  1274.                 return self:StartSessionAsync(profile_key)
  1275.             end,
  1276.             MessageAsync = function(_, profile_key, message)
  1277.                 MockFlag = true
  1278.                 return self:MessageAsync(profile_key, message)
  1279.             end,
  1280.             GetAsync = function(_, profile_key, version)
  1281.                 MockFlag = true
  1282.                 return self:GetAsync(profile_key, version)
  1283.             end,
  1284.             VersionQuery = function(_, profile_key, sort_direction, min_date, max_date)
  1285.                 MockFlag = true
  1286.                 return self:VersionQuery(profile_key, sort_direction, min_date, max_date)
  1287.             end,
  1288.             RemoveAsync = function(_, profile_key)
  1289.                 MockFlag = true
  1290.                 return self:RemoveAsync(profile_key)
  1291.             end
  1292.         },
  1293.  
  1294.         Name = store_name,
  1295.  
  1296.         template = template,
  1297.         data_store = nil,
  1298.         load_jobs = {},
  1299.         mock_load_jobs = {},
  1300.         is_ready = true,
  1301.  
  1302.     }
  1303.     setmetatable(self, ProfileStore)
  1304.  
  1305.     local options = Instance.new("DataStoreOptions")
  1306.     options:SetExperimentalFeatures({v2 = true})
  1307.  
  1308.     if DataStoreState == "NotReady" then
  1309.  
  1310.         -- The module is not sure whether DataStores are accessible yet:
  1311.  
  1312.         self.is_ready = false
  1313.  
  1314.         task.spawn(function()
  1315.  
  1316.             repeat task.wait() until DataStoreState ~= "NotReady"
  1317.  
  1318.             if DataStoreState == "Access" then
  1319.                 self.data_store = DataStoreService:GetDataStore(store_name, nil, options)
  1320.             end
  1321.  
  1322.             self.is_ready = true
  1323.  
  1324.         end)
  1325.  
  1326.     elseif DataStoreState == "Access" then
  1327.  
  1328.         self.data_store = DataStoreService:GetDataStore(store_name, nil, options)
  1329.  
  1330.     end
  1331.  
  1332.     return self
  1333.  
  1334. end
  1335.  
  1336. local function RobloxMessageSubscription(profile, unique_session_id)
  1337.  
  1338.     local last_roblox_message = 0
  1339.  
  1340.     local roblox_message_subscription = MessagingService:SubscribeAsync("PS_" .. unique_session_id, function(message)
  1341.         if type(message.Data) == "table" and message.Data.LoadCount == profile.SessionLoadCount then
  1342.             -- High reaction rate, based on numPlayers × 10 DataStore budget as of writing
  1343.             if os.clock() - last_roblox_message > 6 then
  1344.                 last_roblox_message = os.clock()
  1345.                 if profile:IsActive() == true then
  1346.                     if message.Data.EndSession == true then
  1347.                         SaveProfileAsync(profile, true, false, "External")
  1348.                     else
  1349.                         profile:Save()
  1350.                     end
  1351.                 end
  1352.             end
  1353.         end
  1354.     end)
  1355.  
  1356.     if profile:IsActive() == true then
  1357.         profile.roblox_message_subscription = roblox_message_subscription
  1358.     else
  1359.         roblox_message_subscription:Disconnect()
  1360.     end
  1361.  
  1362. end
  1363.  
  1364. function ProfileStore:StartSessionAsync(profile_key, params)
  1365.  
  1366.     local is_mock = ReadMockFlag()
  1367.  
  1368.     if type(profile_key) ~= "string" then
  1369.         error(`[{script.Name}]: profile_key must be a string`)
  1370.     elseif string.len(profile_key) == 0 then
  1371.         error(`[{script.Name}]: Invalid profile_key`)
  1372.     elseif string.len(profile_key) > 50 then
  1373.         error(`[{script.Name}]: profile_key is too long`)
  1374.     end
  1375.  
  1376.     if params ~= nil and type(params) ~= "table" then
  1377.         error(`[{script.Name}]: Invalid params`)
  1378.     end
  1379.  
  1380.     params = params or {}
  1381.  
  1382.     if ProfileStore.IsClosing == true then
  1383.         return nil
  1384.     end
  1385.  
  1386.     WaitForStoreReady(self)
  1387.  
  1388.     local session_token = SessionToken(self.Name, profile_key, is_mock)
  1389.  
  1390.     if ActiveSessionCheck[session_token] ~= nil then
  1391.         error(`[{script.Name}]: Profile (STORE:{self.Name}; KEY:{profile_key}) is already loaded in this session`)
  1392.     end
  1393.  
  1394.     ActiveProfileLoadJobs = ActiveProfileLoadJobs + 1
  1395.  
  1396.     local is_user_cancel = false
  1397.  
  1398.     local function cancel_condition()
  1399.         if is_user_cancel == false then
  1400.             if params.Cancel ~= nil then
  1401.                 is_user_cancel = params.Cancel() == true
  1402.             end
  1403.             return is_user_cancel
  1404.         end
  1405.         return true
  1406.     end
  1407.  
  1408.     local user_steal = params.Steal == true
  1409.  
  1410.     local force_load_steps = 0 -- Session conflict handling values
  1411.     local request_force_load = true
  1412.     local steal_session = false
  1413.  
  1414.     local start = os.clock()
  1415.     local exp_backoff = 1
  1416.  
  1417.     while ProfileStore.IsClosing == false and cancel_condition() == false do
  1418.  
  1419.         -- Load profile:
  1420.  
  1421.         -- SPECIAL CASE - If StartSessionAsync is called for the same key again before another StartSessionAsync finishes,
  1422.         -- grab the DataStore return for the new call. The early call will return nil. This is supposed to retain
  1423.         -- expected and efficient behavior in cases where a player would quickly rejoin the same server.
  1424.  
  1425.         LoadIndex += 1
  1426.         local load_id = LoadIndex
  1427.         local profile_load_jobs = is_mock == true and self.mock_load_jobs or self.load_jobs
  1428.         local profile_load_job = profile_load_jobs[profile_key] -- {load_id, {loaded_data, key_info} or nil}
  1429.  
  1430.         local loaded_data, key_info
  1431.         local unique_session_id = HttpService:GenerateGUID(false)
  1432.  
  1433.         if profile_load_job ~= nil then
  1434.  
  1435.             profile_load_job[1] = load_id -- Steal load job
  1436.             while profile_load_job[2] == nil do -- Wait for job to finish
  1437.                 task.wait()
  1438.             end
  1439.             if profile_load_job[1] == load_id then -- Load job hasn't been double-stolen
  1440.                 loaded_data, key_info = table.unpack(profile_load_job[2])
  1441.                 profile_load_jobs[profile_key] = nil
  1442.             else
  1443.                 ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
  1444.                 return nil
  1445.             end
  1446.  
  1447.         else
  1448.  
  1449.             profile_load_job = {load_id, nil}
  1450.             profile_load_jobs[profile_key] = profile_load_job
  1451.  
  1452.             profile_load_job[2] = table.pack(UpdateAsync(
  1453.                 self,
  1454.                 profile_key,
  1455.                 {
  1456.                     ExistingProfileHandle = function(latest_data)
  1457.  
  1458.                         if ProfileStore.IsClosing == true or cancel_condition() == true then
  1459.                             return
  1460.                         end
  1461.  
  1462.                         local active_session = latest_data.MetaData.ActiveSession
  1463.                         local force_load_session = latest_data.MetaData.ForceLoadSession
  1464.  
  1465.                         if active_session == nil then
  1466.                             latest_data.MetaData.ActiveSession = {PlaceId, JobId, unique_session_id}
  1467.                             latest_data.MetaData.ForceLoadSession = nil
  1468.                         elseif type(active_session) == "table" then
  1469.                             if IsThisSession(active_session) == false then
  1470.                                 local last_update = latest_data.MetaData.LastUpdate
  1471.                                 if last_update ~= nil then
  1472.                                     if os.time() - last_update > ASSUME_DEAD then
  1473.                                         latest_data.MetaData.ActiveSession = {PlaceId, JobId, unique_session_id}
  1474.                                         latest_data.MetaData.ForceLoadSession = nil
  1475.                                         return
  1476.                                     end
  1477.                                 end
  1478.                                 if steal_session == true or user_steal == true then
  1479.                                     local force_load_interrupted = if force_load_session ~= nil then not IsThisSession(force_load_session) else true
  1480.                                     if force_load_interrupted == false or user_steal == true then
  1481.                                         latest_data.MetaData.ActiveSession = {PlaceId, JobId, unique_session_id}
  1482.                                         latest_data.MetaData.ForceLoadSession = nil
  1483.                                     end
  1484.                                 elseif request_force_load == true then
  1485.                                     latest_data.MetaData.ForceLoadSession = {PlaceId, JobId}
  1486.                                 end
  1487.                             else
  1488.                                 latest_data.MetaData.ForceLoadSession = nil
  1489.                             end
  1490.                         end
  1491.  
  1492.                     end,
  1493.                     MissingProfileHandle = function(latest_data)
  1494.  
  1495.                         local is_cancel = ProfileStore.IsClosing == true or cancel_condition() == true
  1496.  
  1497.                         latest_data.Data = DeepCopyTable(self.template)
  1498.                         latest_data.MetaData = {
  1499.                             ProfileCreateTime = os.time(),
  1500.                             SessionLoadCount = 0,
  1501.                             ActiveSession = if is_cancel == false then {PlaceId, JobId, unique_session_id} else nil,
  1502.                             ForceLoadSession = nil,
  1503.                             MetaTags = {}, -- Backwards compatibility with ProfileService
  1504.                         }
  1505.  
  1506.                     end,
  1507.                     EditProfile = function(latest_data)
  1508.  
  1509.                         if ProfileStore.IsClosing == true or cancel_condition() == true then
  1510.                             return
  1511.                         end
  1512.  
  1513.                         local active_session = latest_data.MetaData.ActiveSession
  1514.                         if active_session ~= nil and IsThisSession(active_session) == true then
  1515.                             latest_data.MetaData.SessionLoadCount = latest_data.MetaData.SessionLoadCount + 1
  1516.                             latest_data.MetaData.LastUpdate = os.time()
  1517.                         end
  1518.  
  1519.                     end,
  1520.                 },
  1521.                 is_mock
  1522.                 ))
  1523.             if profile_load_job[1] == load_id then -- Load job hasn't been stolen
  1524.                 loaded_data, key_info = table.unpack(profile_load_job[2])
  1525.                 profile_load_jobs[profile_key] = nil
  1526.             else
  1527.                 ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
  1528.                 return nil -- Load job stolen
  1529.             end
  1530.         end
  1531.  
  1532.         -- Handle load_data:
  1533.  
  1534.         if loaded_data ~= nil and key_info ~= nil then
  1535.             local active_session = loaded_data.MetaData.ActiveSession
  1536.             if type(active_session) == "table" then
  1537.  
  1538.                 if IsThisSession(active_session) == true then
  1539.  
  1540.                     -- Profile is now taken by this session:
  1541.  
  1542.                     local profile = Profile.New(loaded_data, key_info, self, profile_key, is_mock, session_token)
  1543.                     AddProfileToAutoSave(profile)
  1544.  
  1545.                     if is_mock ~= true and DataStoreState == "Access" then
  1546.  
  1547.                         -- Use MessagingService to quickly detect session conflicts and resolve them quickly:
  1548.                         task.spawn(RobloxMessageSubscription, profile, unique_session_id) -- Blocking prevention
  1549.  
  1550.                     end
  1551.  
  1552.                     if ProfileStore.IsClosing == true or cancel_condition() == true then
  1553.                         -- The server has initiated a shutdown by the time this profile was loaded
  1554.                         SaveProfileAsync(profile, true) -- Release profile and yield until the DataStore call is finished
  1555.                         profile = nil -- Don't return the profile object
  1556.                     end
  1557.  
  1558.                     ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
  1559.                     return profile
  1560.  
  1561.                 else
  1562.  
  1563.                     if ProfileStore.IsClosing == true or cancel_condition() == true then
  1564.                         ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
  1565.                         return nil
  1566.                     end
  1567.  
  1568.                     -- Profile is taken by some other session:
  1569.  
  1570.                     local force_load_session = loaded_data.MetaData.ForceLoadSession
  1571.                     local force_load_interrupted = if force_load_session ~= nil then not IsThisSession(force_load_session) else true
  1572.  
  1573.                     if force_load_interrupted == false then
  1574.  
  1575.                         if request_force_load == false then
  1576.                             force_load_steps = force_load_steps + 1
  1577.                             if force_load_steps >= math.ceil(SESSION_STEAL / LOAD_REPEAT_PERIOD) then
  1578.                                 steal_session = true
  1579.                             end
  1580.                         end
  1581.  
  1582.                         -- Request the remote server to end its session:
  1583.                         if type(active_session[3]) == "string" then
  1584.                             local session_load_count = loaded_data.MetaData.SessionLoadCount or 0
  1585.                             task.spawn(MessagingService.PublishAsync, MessagingService, "PS_" .. active_session[3], {LoadCount = session_load_count, EndSession = true})
  1586.                         end
  1587.  
  1588.                         -- Attempt to load the profile again after a delay
  1589.                         local wait_until = os.clock() + if request_force_load == true then FIRST_LOAD_REPEAT else LOAD_REPEAT_PERIOD
  1590.                         repeat task.wait() until os.clock() >= wait_until or ProfileStore.IsClosing == true
  1591.  
  1592.                     else
  1593.                         -- Another session tried to load this profile:
  1594.                         ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
  1595.                         return nil
  1596.                     end
  1597.  
  1598.                     request_force_load = false -- Only request a force load once
  1599.  
  1600.                 end
  1601.  
  1602.             else
  1603.                 ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
  1604.                 return nil -- In this scenario it is likely that this server started shutting down
  1605.             end
  1606.         else
  1607.  
  1608.             -- A DataStore call has likely ended in an error:
  1609.  
  1610.             local default_timeout = false
  1611.  
  1612.             if params.Cancel == nil then
  1613.                 default_timeout = os.clock() - start >= START_SESSION_TIMEOUT
  1614.             end
  1615.  
  1616.             if default_timeout == true or ProfileStore.IsClosing == true or cancel_condition() == true then
  1617.                 ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
  1618.                 return nil
  1619.             end
  1620.  
  1621.             task.wait(exp_backoff)  -- Repeat the call shortly
  1622.             exp_backoff = math.min(20, exp_backoff * 2)
  1623.  
  1624.         end
  1625.  
  1626.     end
  1627.  
  1628.     ActiveProfileLoadJobs = ActiveProfileLoadJobs - 1
  1629.     return nil -- Game started shutting down or the request was cancelled - don't return the profile
  1630.  
  1631. end
  1632.  
  1633. function ProfileStore:MessageAsync(profile_key, message)
  1634.  
  1635.     local is_mock = ReadMockFlag()
  1636.  
  1637.     if type(profile_key) ~= "string" then
  1638.         error(`[{script.Name}]: profile_key must be a string`)
  1639.     elseif string.len(profile_key) == 0 then
  1640.         error(`[{script.Name}]: Invalid profile_key`)
  1641.     elseif string.len(profile_key) > 50 then
  1642.         error(`[{script.Name}]: profile_key is too long`)
  1643.     end
  1644.  
  1645.     if type(message) ~= "table" then
  1646.         error(`[{script.Name}]: message must be a table`)
  1647.     end
  1648.  
  1649.     if ProfileStore.IsClosing == true then
  1650.         return false
  1651.     end
  1652.  
  1653.     WaitForStoreReady(self)
  1654.  
  1655.     local exp_backoff = 1
  1656.  
  1657.     while ProfileStore.IsClosing == false do
  1658.  
  1659.         -- Updating profile:
  1660.  
  1661.         local loaded_data = UpdateAsync(
  1662.             self,
  1663.             profile_key,
  1664.             {
  1665.                 ExistingProfileHandle = nil,
  1666.                 MissingProfileHandle = nil,
  1667.                 EditProfile = function(latest_data)
  1668.  
  1669.                     local global_updates = latest_data.GlobalUpdates
  1670.                     local update_list = global_updates[2]
  1671.                     --{
  1672.                     --  update_index,
  1673.                     --  {
  1674.                     --      {update_index, data}, ...
  1675.                     --  },
  1676.                     --},
  1677.  
  1678.                     global_updates[1] += 1
  1679.                     table.insert(update_list, {global_updates[1], message})
  1680.  
  1681.                     -- Clearing queue if above limit:
  1682.  
  1683.                     while #update_list > MAX_MESSAGE_QUEUE do
  1684.                         table.remove(update_list, 1)
  1685.                     end
  1686.  
  1687.                 end,
  1688.             },
  1689.             is_mock
  1690.         )
  1691.  
  1692.         if loaded_data ~= nil then
  1693.  
  1694.             local session_token = SessionToken(self.Name, profile_key, is_mock)
  1695.  
  1696.             local profile = ActiveSessionCheck[session_token]
  1697.  
  1698.             if profile ~= nil then
  1699.  
  1700.                 -- The message was sent to a profile that is active in this server:
  1701.                 profile:Save()
  1702.  
  1703.             else
  1704.  
  1705.                 local meta_data = loaded_data.MetaData or {}
  1706.                 local active_session = meta_data.ActiveSession
  1707.                 local session_load_count = meta_data.SessionLoadCount or 0
  1708.  
  1709.                 if type(active_session) == "table" and type(active_session[3]) == "string" then
  1710.                     -- Request the remote server to auto-save sooner and receive the message:
  1711.                     task.spawn(MessagingService.PublishAsync, MessagingService, "PS_" .. active_session[3], {LoadCount = session_load_count})
  1712.                 end
  1713.  
  1714.             end
  1715.  
  1716.             return true
  1717.  
  1718.         else
  1719.  
  1720.             task.wait(exp_backoff) -- A DataStore call has likely ended in an error - repeat the call shortly
  1721.             exp_backoff = math.min(20, exp_backoff * 2)
  1722.  
  1723.         end
  1724.  
  1725.     end
  1726.  
  1727.     return false
  1728.  
  1729. end
  1730.  
  1731. function ProfileStore:GetAsync(profile_key, version)
  1732.  
  1733.     local is_mock = ReadMockFlag()
  1734.  
  1735.     if type(profile_key) ~= "string" then
  1736.         error(`[{script.Name}]: profile_key must be a string`)
  1737.     elseif string.len(profile_key) == 0 then
  1738.         error(`[{script.Name}]: Invalid profile_key`)
  1739.     elseif string.len(profile_key) > 50 then
  1740.         error(`[{script.Name}]: profile_key is too long`)
  1741.     end
  1742.  
  1743.     if ProfileStore.IsClosing == true then
  1744.         return nil
  1745.     end
  1746.  
  1747.     WaitForStoreReady(self)
  1748.  
  1749.     if version ~= nil and (is_mock or DataStoreState ~= "Access") then
  1750.         return nil -- No version support in mock mode
  1751.     end
  1752.  
  1753.     local exp_backoff = 1
  1754.  
  1755.     while ProfileStore.IsClosing == false do
  1756.  
  1757.         -- Load profile:
  1758.  
  1759.         local loaded_data, key_info = UpdateAsync(
  1760.             self,
  1761.             profile_key,
  1762.             {
  1763.                 ExistingProfileHandle = nil,
  1764.                 MissingProfileHandle = function(latest_data)
  1765.  
  1766.                     latest_data.Data = DeepCopyTable(self.template)
  1767.                     latest_data.MetaData = {
  1768.                         ProfileCreateTime = os.time(),
  1769.                         SessionLoadCount = 0,
  1770.                         ActiveSession = nil,
  1771.                         ForceLoadSession = nil,
  1772.                         MetaTags = {}, -- Backwards compatibility with ProfileService
  1773.                     }
  1774.  
  1775.                 end,
  1776.                 EditProfile = nil,
  1777.             },
  1778.             is_mock,
  1779.             true, -- Use :GetAsync()
  1780.             version -- DataStore key version
  1781.         )
  1782.  
  1783.         -- Handle load_data:
  1784.  
  1785.         if loaded_data ~= nil then
  1786.  
  1787.             if key_info == nil then
  1788.                 return nil -- Load was successful, but the key was empty - return no profile object
  1789.             end
  1790.  
  1791.             local profile = Profile.New(loaded_data, key_info, self, profile_key, is_mock)
  1792.             profile.view_mode = true
  1793.  
  1794.             return profile
  1795.  
  1796.         else
  1797.  
  1798.             task.wait(exp_backoff) -- A DataStore call has likely ended in an error - repeat the call shortly
  1799.             exp_backoff = math.min(20, exp_backoff * 2)
  1800.  
  1801.         end
  1802.  
  1803.     end
  1804.  
  1805.     return nil -- Game started shutting down - don't return the profile
  1806.  
  1807. end
  1808.  
  1809. function ProfileStore:RemoveAsync(profile_key)
  1810.  
  1811.     local is_mock = ReadMockFlag()
  1812.  
  1813.     if type(profile_key) ~= "string" or string.len(profile_key) == 0 then
  1814.         error(`[{script.Name}]: Invalid profile_key`)
  1815.     end
  1816.  
  1817.     if ProfileStore.IsClosing == true then
  1818.         return false
  1819.     end
  1820.  
  1821.     WaitForStoreReady(self)
  1822.  
  1823.     local wipe_status = false
  1824.  
  1825.     local next_in_queue = WaitInUpdateQueue(SessionToken(self.Name, profile_key, is_mock))
  1826.  
  1827.     if is_mock == true then -- Used when the profile is accessed through ProfileStore.Mock
  1828.  
  1829.         local mock_data_store = UserMockStore[self.Name]
  1830.  
  1831.         if mock_data_store ~= nil then
  1832.             mock_data_store[profile_key] = nil
  1833.             if next(mock_data_store) == nil then
  1834.                 UserMockStore[self.Name] = nil
  1835.             end
  1836.         end
  1837.  
  1838.         wipe_status = true
  1839.         task.wait() -- Simulate API call yield
  1840.  
  1841.     elseif DataStoreState ~= "Access" then -- Used when API access is disabled
  1842.  
  1843.         local mock_data_store = MockStore[self.Name]
  1844.  
  1845.         if mock_data_store ~= nil then
  1846.             mock_data_store[profile_key] = nil
  1847.             if next(mock_data_store) == nil then
  1848.                 MockStore[self.Name] = nil
  1849.             end
  1850.         end
  1851.  
  1852.         wipe_status = true
  1853.         task.wait() -- Simulate API call yield
  1854.  
  1855.     else -- Live DataStore
  1856.  
  1857.         wipe_status = pcall(function()
  1858.             self.data_store:RemoveAsync(profile_key)
  1859.         end)
  1860.  
  1861.     end
  1862.  
  1863.     next_in_queue()
  1864.  
  1865.     return wipe_status
  1866.  
  1867. end
  1868.  
  1869. local ProfileVersionQuery = {}
  1870. ProfileVersionQuery.__index = ProfileVersionQuery
  1871.  
  1872. function ProfileVersionQuery.New(profile_store, profile_key, sort_direction, min_date, max_date, is_mock)
  1873.  
  1874.     local self = {
  1875.         profile_store = profile_store,
  1876.         profile_key = profile_key,
  1877.         sort_direction = sort_direction,
  1878.         min_date = min_date,
  1879.         max_date = max_date,
  1880.  
  1881.         query_pages = nil,
  1882.         query_index = 0,
  1883.         query_failure = false,
  1884.  
  1885.         is_query_yielded = false,
  1886.         query_queue = {},
  1887.  
  1888.         is_mock = is_mock,
  1889.     }
  1890.     setmetatable(self, ProfileVersionQuery)
  1891.  
  1892.     return self
  1893.  
  1894. end
  1895.  
  1896. function MoveVersionQueryQueue(self) -- Hidden ProfileVersionQuery method
  1897.     while #self.query_queue > 0 do
  1898.  
  1899.         local queue_entry = table.remove(self.query_queue, 1)
  1900.  
  1901.         task.spawn(queue_entry)
  1902.  
  1903.         if self.is_query_yielded == true then
  1904.             break
  1905.         end
  1906.  
  1907.     end
  1908. end
  1909.  
  1910. local VersionQueryNextAsyncStackingFlag = false
  1911. local WarnAboutVersionQueryOnce = false
  1912.  
  1913. function ProfileVersionQuery:NextAsync()
  1914.  
  1915.     local is_stacking = VersionQueryNextAsyncStackingFlag == true
  1916.     VersionQueryNextAsyncStackingFlag = false
  1917.  
  1918.     WaitForStoreReady(self.profile_store)
  1919.  
  1920.     if ProfileStore.IsClosing == true then
  1921.         return nil -- Silently fail :NextAsync() requests
  1922.     end
  1923.  
  1924.     if self.is_mock == true or DataStoreState ~= "Access" then
  1925.         if IsStudio == true and WarnAboutVersionQueryOnce == false then
  1926.             WarnAboutVersionQueryOnce = true
  1927.             warn(`[{script.Name}]: :VersionQuery() is not supported in mock mode!`)
  1928.         end
  1929.         return nil -- Silently fail :NextAsync() requests
  1930.     end
  1931.  
  1932.     local profile
  1933.     local is_finished = false
  1934.  
  1935.     local function query_job()
  1936.  
  1937.         if self.query_failure == true then
  1938.             is_finished = true
  1939.             return
  1940.         end
  1941.  
  1942.         -- First "next" call loads version pages:
  1943.  
  1944.         if self.query_pages == nil then
  1945.  
  1946.             self.is_query_yielded = true
  1947.  
  1948.             task.spawn(function()
  1949.                 VersionQueryNextAsyncStackingFlag = true
  1950.                 profile = self:NextAsync()
  1951.                 is_finished = true
  1952.             end)
  1953.  
  1954.             local list_success, error_message = pcall(function()
  1955.                 self.query_pages = self.profile_store.data_store:ListVersionsAsync(
  1956.                     self.profile_key,
  1957.                     self.sort_direction,
  1958.                     self.min_date,
  1959.                     self.max_date
  1960.                 )
  1961.                 self.query_index = 0
  1962.             end)
  1963.  
  1964.             if list_success == false or self.query_pages == nil then
  1965.                 warn(`[{script.Name}]: Version query fail - {tostring(error_message)}`)
  1966.                 self.query_failure = true
  1967.             end
  1968.  
  1969.             self.is_query_yielded = false
  1970.  
  1971.             MoveVersionQueryQueue(self)
  1972.  
  1973.             return
  1974.  
  1975.         end
  1976.  
  1977.         local current_page = self.query_pages:GetCurrentPage()
  1978.         local next_item = current_page[self.query_index + 1]
  1979.  
  1980.         -- No more entries:
  1981.  
  1982.         if self.query_pages.IsFinished == true and next_item == nil then
  1983.             is_finished = true
  1984.             return
  1985.         end
  1986.  
  1987.         -- Load next page when this page is over:
  1988.  
  1989.         if next_item == nil then
  1990.  
  1991.             self.is_query_yielded = true
  1992.             task.spawn(function()
  1993.                 VersionQueryNextAsyncStackingFlag = true
  1994.                 profile = self:NextAsync()
  1995.                 is_finished = true
  1996.             end)
  1997.  
  1998.             local success, error_message = pcall(function()
  1999.                 self.query_pages:AdvanceToNextPageAsync()
  2000.                 self.query_index = 0
  2001.             end)
  2002.  
  2003.             if success == false or #self.query_pages:GetCurrentPage() == 0 then
  2004.                 self.query_failure = true
  2005.             end
  2006.  
  2007.             self.is_query_yielded = false
  2008.             MoveVersionQueryQueue(self)
  2009.  
  2010.             return
  2011.  
  2012.         end
  2013.  
  2014.         -- Next page item:
  2015.  
  2016.         self.query_index += 1
  2017.         profile = self.profile_store:GetAsync(self.profile_key, next_item.Version)
  2018.         is_finished = true
  2019.  
  2020.     end
  2021.  
  2022.     if self.is_query_yielded == false then
  2023.         query_job()
  2024.     else
  2025.         if is_stacking == true then
  2026.             table.insert(self.query_queue, 1, query_job)
  2027.         else
  2028.             table.insert(self.query_queue, query_job)
  2029.         end
  2030.     end
  2031.  
  2032.     while is_finished == false do
  2033.         task.wait()
  2034.     end
  2035.  
  2036.     return profile
  2037.  
  2038. end
  2039.  
  2040. function ProfileStore:VersionQuery(profile_key, sort_direction, min_date, max_date)
  2041.  
  2042.     local is_mock = ReadMockFlag()
  2043.  
  2044.     if type(profile_key) ~= "string" or string.len(profile_key) == 0 then
  2045.         error(`[{script.Name}]: Invalid profile_key`)
  2046.     end
  2047.  
  2048.     -- Type check:
  2049.  
  2050.     if sort_direction ~= nil and (typeof(sort_direction) ~= "EnumItem"
  2051.         or sort_direction.EnumType ~= Enum.SortDirection) then
  2052.         error(`[{script.Name}]: Invalid sort_direction ({tostring(sort_direction)})`)
  2053.     end
  2054.  
  2055.     if min_date ~= nil and typeof(min_date) ~= "DateTime" and typeof(min_date) ~= "number" then
  2056.         error(`[{script.Name}]: Invalid min_date ({tostring(min_date)})`)
  2057.     end
  2058.  
  2059.     if max_date ~= nil and typeof(max_date) ~= "DateTime" and typeof(max_date) ~= "number" then
  2060.         error(`[{script.Name}]: Invalid max_date ({tostring(max_date)})`)
  2061.     end
  2062.  
  2063.     min_date = typeof(min_date) == "DateTime" and min_date.UnixTimestampMillis or min_date
  2064.     max_date = typeof(max_date) == "DateTime" and max_date.UnixTimestampMillis or max_date
  2065.  
  2066.     return ProfileVersionQuery.New(self, profile_key, sort_direction, min_date, max_date, is_mock)
  2067.  
  2068. end
  2069.  
  2070. -- DataStore API access check:
  2071.  
  2072. if IsStudio == true then
  2073.  
  2074.     task.spawn(function()
  2075.  
  2076.         local new_state = "NoAccess"
  2077.  
  2078.         local status, message = pcall(function()
  2079.             -- This will error if current instance has no Studio API access:
  2080.             DataStoreService:GetDataStore("____PS"):SetAsync("____PS", os.time())
  2081.         end)
  2082.  
  2083.         local no_internet_access = status == false and string.find(message, "ConnectFail", 1, true) ~= nil
  2084.  
  2085.         if no_internet_access == true then
  2086.             warn(`[{script.Name}]: No internet access - check your network connection`)
  2087.         end
  2088.  
  2089.         if status == false and
  2090.             (string.find(message, "403", 1, true) ~= nil or -- Cannot write to DataStore from studio if API access is not enabled
  2091.                 string.find(message, "must publish", 1, true) ~= nil or -- Game must be published to access live keys
  2092.                 no_internet_access == true) then -- No internet access
  2093.  
  2094.             new_state = if no_internet_access == true then "NoInternet" else "NoAccess"
  2095.             print(`[{script.Name}]: Roblox API services unavailable - data will not be saved`)
  2096.         else
  2097.             new_state = "Access"
  2098.             print(`[{script.Name}]: Roblox API services available - data will be saved`)
  2099.         end
  2100.  
  2101.         DataStoreState = new_state
  2102.         ProfileStore.DataStoreState = new_state
  2103.  
  2104.     end)
  2105.  
  2106. else
  2107.  
  2108.     DataStoreState = "Access"
  2109.     ProfileStore.DataStoreState = "Access"
  2110.  
  2111. end
  2112.  
  2113. -- Update loop:
  2114.  
  2115. RunService.Heartbeat:Connect(function()
  2116.  
  2117.     -- Auto saving:
  2118.  
  2119.     local auto_save_list_length = #AutoSaveList
  2120.     if auto_save_list_length > 0 then
  2121.         local auto_save_index_speed = AUTO_SAVE_PERIOD / auto_save_list_length
  2122.         local os_clock = os.clock()
  2123.         while os_clock - LastAutoSave > auto_save_index_speed do
  2124.             LastAutoSave = LastAutoSave + auto_save_index_speed
  2125.             local profile = AutoSaveList[AutoSaveIndex]
  2126.             if os_clock - profile.load_timestamp < AUTO_SAVE_PERIOD / 2 then
  2127.                 -- This profile is freshly loaded - auto saving immediately is not necessary:
  2128.                 profile = nil
  2129.                 for _ = 1, auto_save_list_length - 1 do
  2130.                     -- Move auto save index to the right:
  2131.                     AutoSaveIndex = AutoSaveIndex + 1
  2132.                     if AutoSaveIndex > auto_save_list_length then
  2133.                         AutoSaveIndex = 1
  2134.                     end
  2135.                     profile = AutoSaveList[AutoSaveIndex]
  2136.                     if os_clock - profile.load_timestamp >= AUTO_SAVE_PERIOD / 2 then
  2137.                         break
  2138.                     else
  2139.                         profile = nil
  2140.                     end
  2141.                 end
  2142.             end
  2143.             -- Move auto save index to the right:
  2144.             AutoSaveIndex = AutoSaveIndex + 1
  2145.             if AutoSaveIndex > auto_save_list_length then
  2146.                 AutoSaveIndex = 1
  2147.             end
  2148.             -- Perform save call:
  2149.             if profile ~= nil then
  2150.                 task.spawn(SaveProfileAsync, profile) -- Auto save profile in new thread
  2151.             end
  2152.         end
  2153.     end
  2154.  
  2155.     -- Critical state handling:
  2156.  
  2157.     if ProfileStore.IsCriticalState == false then
  2158.         if #IssueQueue >= CRITICAL_STATE_ERROR_COUNT then
  2159.             ProfileStore.IsCriticalState = true
  2160.             ProfileStore.OnCriticalToggle:Fire(true)
  2161.             CriticalStateStart = os.clock()
  2162.             warn(`[{script.Name}]: Entered critical state`)
  2163.         end
  2164.     else
  2165.         if #IssueQueue >= CRITICAL_STATE_ERROR_COUNT then
  2166.             CriticalStateStart = os.clock()
  2167.         elseif os.clock() - CriticalStateStart > CRITICAL_STATE_EXPIRE then
  2168.             ProfileStore.IsCriticalState = false
  2169.             ProfileStore.OnCriticalToggle:Fire(false)
  2170.             warn(`[{script.Name}]: Critical state ended`)
  2171.         end
  2172.     end
  2173.  
  2174.     -- Issue queue:
  2175.  
  2176.     while true do
  2177.         local issue_time = IssueQueue[1]
  2178.         if issue_time == nil then
  2179.             break
  2180.         elseif os.clock() - issue_time > CRITICAL_STATE_ERROR_EXPIRE then
  2181.             table.remove(IssueQueue, 1)
  2182.         else
  2183.             break
  2184.         end
  2185.     end
  2186.  
  2187. end)
  2188.  
  2189. -- Release all loaded profiles when the server is shutting down:
  2190.  
  2191. task.spawn(function()
  2192.  
  2193.     while DataStoreState == "NotReady" do
  2194.         task.wait()
  2195.     end
  2196.  
  2197.     if DataStoreState ~= "Access" then
  2198.  
  2199.         game:BindToClose(function()
  2200.             ProfileStore.IsClosing = true
  2201.             task.wait() -- Mock shutdown delay
  2202.         end)
  2203.  
  2204.         return -- Don't wait for profiles to properly save in mock mode so studio could end the simulation faster
  2205.  
  2206.     end
  2207.  
  2208.     game:BindToClose(function()
  2209.  
  2210.         ProfileStore.IsClosing = true
  2211.  
  2212.         -- Release all active profiles:
  2213.         -- (Clone AutoSaveList to a new table because AutoSaveList changes when profiles are released)
  2214.  
  2215.         local on_close_save_job_count = 0
  2216.         local active_profiles = {}
  2217.         for index, profile in ipairs(AutoSaveList) do
  2218.             active_profiles[index] = profile
  2219.         end
  2220.  
  2221.         -- Release the profiles; Releasing profiles can trigger listeners that release other profiles, so check active state:
  2222.         for _, profile in ipairs(active_profiles) do
  2223.             if profile:IsActive() == true then
  2224.                 on_close_save_job_count = on_close_save_job_count + 1
  2225.                 task.spawn(function() -- Save profile on new thread
  2226.                     SaveProfileAsync(profile, true, nil, "Shutdown")
  2227.                     on_close_save_job_count = on_close_save_job_count - 1
  2228.                 end)
  2229.             end
  2230.         end
  2231.  
  2232.         -- Yield until all active profile jobs are finished:
  2233.         while on_close_save_job_count > 0 or ActiveProfileLoadJobs > 0 or ActiveProfileSaveJobs > 0 do
  2234.             task.wait()
  2235.         end
  2236.  
  2237.         return -- We're done!
  2238.  
  2239.     end)
  2240.  
  2241. end)
  2242.  
  2243. return ProfileStore
Tags: Roblox luau
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement