diff --git a/ProfileService.lua b/ProfileService.lua index 47bcb4c..8c76b9d 100644 --- a/ProfileService.lua +++ b/ProfileService.lua @@ -206,12 +206,12 @@ local SETTINGS = { RobloxWriteCooldown = 7, -- Seconds between successive DataStore calls for the same key ForceLoadMaxSteps = 8, -- Steps taken before ForceLoad request steals the active session for a profile AssumeDeadSessionLock = 30 * 60, -- (seconds) If a profile hasn't been updated for 30 minutes, assume the session lock is dead - -- As of writing, os.time() is not completely reliable, so we can only assume session locks are dead after a significant amount of time. - + -- As of writing, os.time() is not completely reliable, so we can only assume session locks are dead after a significant amount of time. + IssueCountForCriticalState = 5, -- Issues to collect to announce critical state IssueLast = 120, -- Seconds CriticalStateLast = 120, -- Seconds - + MetaTagsUpdatedValues = { -- Technical stuff - do not alter ProfileCreateTime = true, SessionLoadCount = true, @@ -219,30 +219,28 @@ local SETTINGS = { ForceLoadSession = true, LastUpdate = true, }, - } local Madwork -- Standalone Madwork reference for portable version of ProfileService do - local MadworkScriptSignal = {} local FreeRunnerThread = nil - + local function AcquireRunnerThreadAndCallEventHandler(fn, ...) local acquired_runner_thread = FreeRunnerThread FreeRunnerThread = nil fn(...) FreeRunnerThread = acquired_runner_thread end - + local function RunEventHandlerInFreeThread(...) AcquireRunnerThreadAndCallEventHandler(...) while true do AcquireRunnerThreadAndCallEventHandler(coroutine.yield()) end end - + -- ScriptConnection object: local ScriptConnection = { @@ -259,7 +257,6 @@ do ScriptConnection.__index = ScriptConnection function ScriptConnection:Disconnect() - if self._is_connected == false then return end @@ -286,9 +283,8 @@ do task.spawn(FreeRunnerThread, self._disconnect_listener, self._disconnect_param) self._disconnect_listener = nil end - end - + -- ScriptSignal object: local ScriptSignal = { @@ -300,7 +296,6 @@ do ScriptSignal.__index = ScriptSignal function ScriptSignal:Connect(listener, disconnect_listener, disconnect_param) --> [ScriptConnection] - local script_connection = { _listener = listener, _script_signal = self, @@ -316,7 +311,6 @@ do self._listener_count += 1 return script_connection - end function ScriptSignal:GetListenerCount() --> [number] @@ -361,7 +355,7 @@ do end -- Madwork framework namespace: - + Madwork = { NewScriptSignal = MadworkScriptSignal.NewScriptSignal, ConnectToOnClose = function(task, run_in_studio_mode) @@ -370,9 +364,11 @@ do end end, } - end +export type ScriptSignal = typeof(Madwork.NewScriptSignal()) +export type ScriptConnection = typeof(Madwork.NewScriptSignal().Connect(function() end)) + ----- Service Table ----- local ProfileService = { @@ -401,7 +397,6 @@ local ProfileService = { _user_mock_data_store = {}, _use_mock_data_store = false, - } --[[ @@ -488,7 +483,7 @@ local CustomWriteQueue = { ----- Utils ----- -local function DeepCopyTable(t) +local function DeepCopyTable(t: { [T]: V }): { [T]: V } local copy = {} for key, value in pairs(t) do if type(value) == "table" then @@ -500,7 +495,7 @@ local function DeepCopyTable(t) return copy end -local function ReconcileTable(target, template) +local function ReconcileTable(target: { [T]: V }, template: { [T]: V }) for k, v in pairs(template) do if type(k) == "string" then -- Only string keys will be reconciled if target[k] == nil then @@ -518,16 +513,16 @@ end ----- Private functions ----- -local function IdentifyProfile(store_name, store_scope, key) +local function IdentifyProfile(store_name: string, store_scope: string, key: string): string return string.format( - "[Store:\"%s\";%sKey:\"%s\"]", + '[Store:"%s";%sKey:"%s"]', store_name, - store_scope ~= nil and string.format("Scope:\"%s\";", store_scope) or "", + store_scope ~= nil and string.format('Scope:"%s";', store_scope) or "", key ) end -local function CustomWriteQueueCleanup(store, key) +local function CustomWriteQueueCleanup(store: string, key: string) if CustomWriteQueue[store] ~= nil then CustomWriteQueue[store][key] = nil if next(CustomWriteQueue[store]) == nil then @@ -536,37 +531,32 @@ local function CustomWriteQueueCleanup(store, key) end end -local function CustomWriteQueueMarkForCleanup(store, key) +local function CustomWriteQueueMarkForCleanup(store: string, key: string) if CustomWriteQueue[store] ~= nil then if CustomWriteQueue[store][key] ~= nil then - local queue_data = CustomWriteQueue[store][key] local queue = queue_data.Queue if queue_data.CleanupJob == nil then - queue_data.CleanupJob = RunService.Heartbeat:Connect(function() if os.clock() - queue_data.LastWrite > SETTINGS.RobloxWriteCooldown and #queue == 0 then queue_data.CleanupJob:Disconnect() CustomWriteQueueCleanup(store, key) end end) - end - elseif next(CustomWriteQueue[store]) == nil then CustomWriteQueue[store] = nil end end end -local function CustomWriteQueueAsync(callback, store, key) --> ... -- Passed return from callback - +local function CustomWriteQueueAsync(callback: () -> T..., store: string, key: string) --> ... -- Passed return from callback if CustomWriteQueue[store] == nil then CustomWriteQueue[store] = {} end if CustomWriteQueue[store][key] == nil then - CustomWriteQueue[store][key] = {LastWrite = 0, Queue = {}, CleanupJob = nil} + CustomWriteQueue[store][key] = { LastWrite = 0, Queue = {}, CleanupJob = nil } end local queue_data = CustomWriteQueue[store][key] @@ -595,10 +585,9 @@ local function CustomWriteQueueAsync(callback, store, key) --> ... -- Passed ret task.wait() end end - end -local function IsCustomWriteQueueEmptyFor(store, key) --> is_empty [bool] +local function IsCustomWriteQueueEmptyFor(store: string, key: string): boolean --> is_empty [bool] local lookup = CustomWriteQueue[store] if lookup ~= nil then lookup = lookup[key] @@ -613,25 +602,30 @@ local function WaitForLiveAccessCheck() -- This function was created to prevent end end -local function WaitForPendingProfileStore(profile_store) +local function WaitForPendingProfileStore(profile_store: ProfileStore) while profile_store._is_pending == true do task.wait() end end -local function RegisterIssue(error_message, store_name, store_scope, profile_key) -- Called when a DataStore API call errors - warn("[ProfileService]: DataStore API error " .. IdentifyProfile(store_name, store_scope, profile_key) .. " - \"" .. tostring(error_message) .. "\"") +local function RegisterIssue(error_message: string?, store_name: string, store_scope: string, profile_key: string) -- Called when a DataStore API call errors + warn( + "[ProfileService]: DataStore API error " + .. IdentifyProfile(store_name, store_scope, profile_key) + .. ' - "' + .. tostring(error_message) + .. '"' + ) table.insert(IssueQueue, os.clock()) -- Adding issue time to queue ProfileService.IssueSignal:Fire(tostring(error_message), store_name, profile_key) end -local function RegisterCorruption(store_name, store_scope, profile_key) -- Called when a corrupted profile is loaded +local function RegisterCorruption(store_name: string, store_scope: string, profile_key: string) -- Called when a corrupted profile is loaded warn("[ProfileService]: Resolved profile corruption " .. IdentifyProfile(store_name, store_scope, profile_key)) ProfileService.CorruptionSignal:Fire(store_name, profile_key) end -local function NewMockDataStoreKeyInfo(params) - +local function NewMockDataStoreKeyInfo(params: DataStoreKeyInfo): MockDataStoreKeyInfo local version_id_string = tostring(params.VersionId or 0) local meta_data = params.MetaData or {} local user_ids = params.UserIds or {} @@ -639,9 +633,14 @@ local function NewMockDataStoreKeyInfo(params) return { CreatedTime = params.CreatedTime, UpdatedTime = params.UpdatedTime, - Version = string.rep("0", 16) .. "." - .. string.rep("0", 10 - string.len(version_id_string)) .. version_id_string - .. "." .. string.rep("0", 16) .. "." .. "01", + Version = string.rep("0", 16) + .. "." + .. string.rep("0", 10 - string.len(version_id_string)) + .. version_id_string + .. "." + .. string.rep("0", 16) + .. "." + .. "01", GetMetadata = function() return DeepCopyTable(meta_data) @@ -651,11 +650,15 @@ local function NewMockDataStoreKeyInfo(params) return DeepCopyTable(user_ids) end, } - end -local function MockUpdateAsync(mock_data_store, profile_store_name, key, transform_function, is_get_call) --> loaded_data, key_info - +local function MockUpdateAsync( + mock_data_store: { [string]: MockDataStoreKeyInfo }, + profile_store_name: string, + key: string, + transform_function: (T?, MockDataStoreKeyInfo) -> T, + is_get_call: boolean? +) --> loaded_data, key_info local profile_store = mock_data_store[profile_store_name] if profile_store == nil then @@ -699,28 +702,26 @@ local function MockUpdateAsync(mock_data_store, profile_store_name, key, transfo return DeepCopyTable(transform), mock_entry ~= nil and NewMockDataStoreKeyInfo(mock_entry) or nil end - end -local function IsThisSession(session_tag) +local function IsThisSession(session_tag: SessionTag) return session_tag[1] == PlaceId and session_tag[2] == JobId end ---[[ -update_settings = { - ExistingProfileHandle = function(latest_data), - MissingProfileHandle = function(latest_data), - EditProfile = function(lastest_data), -} ---]] -local function StandardProfileUpdateAsyncDataStore(profile_store, profile_key, update_settings, is_user_mock, is_get_call, version) --> loaded_data, key_info +local function StandardProfileUpdateAsyncDataStore( + profile_store: ProfileStore, + profile_key: string, + update_settings: UpdateSettings, + is_user_mock: boolean, + is_get_call: boolean, + version: string +): ({ [string]: any }?, (DataStoreKeyInfo | MockDataStoreKeyInfo)?) local loaded_data, key_info local success, error_message = pcall(function() local transform_function = function(latest_data) - local missing_profile = false local data_corrupted = false - local global_updates_data = {0, {}} + local global_updates_data = { 0, {} } if latest_data == nil then missing_profile = true @@ -731,20 +732,22 @@ local function StandardProfileUpdateAsyncDataStore(profile_store, profile_key, u if type(latest_data) == "table" then -- Case #1: Profile was loaded - if type(latest_data.Data) == "table" + if + type(latest_data.Data) == "table" and type(latest_data.MetaData) == "table" - and type(latest_data.GlobalUpdates) == "table" then - + and type(latest_data.GlobalUpdates) == "table" + then latest_data.WasCorrupted = false -- Must be set to false if set previously global_updates_data = latest_data.GlobalUpdates if update_settings.ExistingProfileHandle ~= nil then update_settings.ExistingProfileHandle(latest_data) end -- Case #2: Profile was not loaded but GlobalUpdate data exists - elseif latest_data.Data == nil + elseif + latest_data.Data == nil and latest_data.MetaData == nil - and type(latest_data.GlobalUpdates) == "table" then - + and type(latest_data.GlobalUpdates) == "table" + then latest_data.WasCorrupted = false -- Must be set to false if set previously global_updates_data = latest_data.GlobalUpdates or global_updates_data missing_profile = true @@ -779,10 +782,22 @@ local function StandardProfileUpdateAsyncDataStore(profile_store, profile_key, u return latest_data, latest_data.UserIds, latest_data.RobloxMetaData end if is_user_mock == true then -- Used when the profile is accessed through ProfileStore.Mock - loaded_data, key_info = MockUpdateAsync(UserMockDataStore, profile_store._profile_store_lookup, profile_key, transform_function, is_get_call) + loaded_data, key_info = MockUpdateAsync( + UserMockDataStore, + profile_store._profile_store_lookup, + profile_key, + transform_function, + is_get_call + ) task.wait() -- Simulate API call yield elseif UseMockDataStore == true then -- Used when API access is disabled - loaded_data, key_info = MockUpdateAsync(MockDataStore, profile_store._profile_store_lookup, profile_key, transform_function, is_get_call) + loaded_data, key_info = MockUpdateAsync( + MockDataStore, + profile_store._profile_store_lookup, + profile_key, + transform_function, + is_get_call + ) task.wait() -- Simulate API call yield else loaded_data, key_info = CustomWriteQueueAsync( @@ -790,11 +805,19 @@ local function StandardProfileUpdateAsyncDataStore(profile_store, profile_key, u if is_get_call == true then local get_data, get_key_info if version ~= nil then - local success, error_message = pcall(function() - get_data, get_key_info = profile_store._global_data_store:GetVersionAsync(profile_key, version) + local success2, error_message2 = pcall(function() + get_data, get_key_info = + profile_store._global_data_store:GetVersionAsync(profile_key, version) end) - if success == false and type(error_message) == "string" and string.find(error_message, "not valid") ~= nil then - warn("[ProfileService]: Passed version argument is not valid; Traceback:\n" .. debug.traceback()) + if + success2 == false + and type(error_message2) == "string" + and string.find(error_message2, "not valid") ~= nil + then + warn( + "[ProfileService]: Passed version argument is not valid; Traceback:\n" + .. debug.traceback() + ) end else get_data, get_key_info = profile_store._global_data_store:GetAsync(profile_key) @@ -813,11 +836,7 @@ local function StandardProfileUpdateAsyncDataStore(profile_store, profile_key, u if success == true and type(loaded_data) == "table" then -- Corruption handling: if loaded_data.WasCorrupted == true and is_get_call ~= true then - RegisterCorruption( - profile_store._profile_store_name, - profile_store._profile_store_scope, - profile_key - ) + RegisterCorruption(profile_store._profile_store_name, profile_store._profile_store_scope, profile_key) end -- Return loaded_data: return loaded_data, key_info @@ -833,7 +852,7 @@ local function StandardProfileUpdateAsyncDataStore(profile_store, profile_key, u end end -local function RemoveProfileFromAutoSave(profile) +local function RemoveProfileFromAutoSave(profile: GenericProfile) local auto_save_index = table.find(AutoSaveList, profile) if auto_save_index ~= nil then table.remove(AutoSaveList, auto_save_index) @@ -846,7 +865,7 @@ local function RemoveProfileFromAutoSave(profile) end end -local function AddProfileToAutoSave(profile) -- Notice: Makes sure this profile isn't auto-saved too soon +local function AddProfileToAutoSave(profile: GenericProfile) -- Notice: Makes sure this profile isn't auto-saved too soon -- Add at AutoSaveIndex and move AutoSaveIndex right: table.insert(AutoSaveList, AutoSaveIndex, profile) if #AutoSaveList > 1 then @@ -857,11 +876,12 @@ local function AddProfileToAutoSave(profile) -- Notice: Makes sure this profile end end -local function ReleaseProfileInternally(profile) +local function ReleaseProfileInternally(profile: GenericProfile) -- 1) Remove profile object from ProfileService references: -- -- Clear reference in ProfileStore: local profile_store = profile._profile_store - local loaded_profiles = profile._is_user_mock == true and profile_store._mock_loaded_profiles or profile_store._loaded_profiles + local loaded_profiles = profile._is_user_mock == true and profile_store._mock_loaded_profiles + or profile_store._loaded_profiles loaded_profiles[profile._profile_key] = nil if next(profile_store._loaded_profiles) == nil and next(profile_store._mock_loaded_profiles) == nil then -- ProfileStore has turned inactive local index = table.find(ActiveProfileStores, profile_store) @@ -882,7 +902,11 @@ local function ReleaseProfileInternally(profile) profile._release_listeners:Fire(place_id, game_job_id) end -local function CheckForNewGlobalUpdates(profile, old_global_updates_data, new_global_updates_data) +local function CheckForNewGlobalUpdates( + profile: GenericProfile, + old_global_updates_data: GlobalUpdatesData, + new_global_updates_data: GlobalUpdatesData +) local global_updates_object = profile.GlobalUpdates -- [GlobalUpdates] local pending_update_lock = global_updates_object._pending_update_lock -- {update_id, ...} local pending_update_clear = global_updates_object._pending_update_clear -- {update_id, ...} @@ -898,7 +922,11 @@ local function CheckForNewGlobalUpdates(profile, old_global_updates_data, new_gl end -- A global update is new when it didn't exist before or its version_id or update_locked state changed: local is_new = false - if old_global_update == nil or new_global_update[2] > old_global_update[2] or new_global_update[3] ~= old_global_update[3] then + if + old_global_update == nil + or new_global_update[2] > old_global_update[2] + or new_global_update[3] ~= old_global_update[3] + then is_new = true end if is_new == true then @@ -930,24 +958,20 @@ local function CheckForNewGlobalUpdates(profile, old_global_updates_data, new_gl if is_pending_clear == false then -- Trigger new locked update listeners: - global_updates_object._new_locked_update_listeners:FireUntil( - function() - -- Check if listener marked the update to be cleared: - -- Normally there should be only one listener per profile for new locked global updates, but - -- in case several listeners are connected we will not trigger more listeners after one listener - -- marks the locked global update to be cleared. - return table.find(pending_update_clear, new_global_update[1]) == nil - end, - new_global_update[1], new_global_update[4] - ) - + global_updates_object._new_locked_update_listeners:FireUntil(function() + -- Check if listener marked the update to be cleared: + -- Normally there should be only one listener per profile for new locked global updates, but + -- in case several listeners are connected we will not trigger more listeners after one listener + -- marks the locked global update to be cleared. + return table.find(pending_update_clear, new_global_update[1]) == nil + end, new_global_update[1], new_global_update[4]) end end end end end -local function SaveProfileAsync(profile, release_from_session, is_overwriting) +local function SaveProfileAsync(profile: GenericProfile, release_from_session: boolean, is_overwriting: boolean) if type(profile.Data) ~= "table" then RegisterCorruption( profile._profile_store._profile_store_name, @@ -967,14 +991,11 @@ local function SaveProfileAsync(profile, release_from_session, is_overwriting) if release_from_session ~= true then repeat_save_flag = false end - local loaded_data, key_info = StandardProfileUpdateAsyncDataStore( - profile._profile_store, - profile._profile_key, - { + local loaded_data, key_info = + StandardProfileUpdateAsyncDataStore(profile._profile_store, profile._profile_key, { ExistingProfileHandle = nil, MissingProfileHandle = nil, EditProfile = function(latest_data) - local session_owns_profile = false local force_load_pending = false @@ -985,7 +1006,8 @@ local function SaveProfileAsync(profile, release_from_session, is_overwriting) local session_load_count = latest_data.MetaData.SessionLoadCount if type(active_session) == "table" then - session_owns_profile = IsThisSession(active_session) and session_load_count == last_session_load_count + session_owns_profile = IsThisSession(active_session) + and session_load_count == last_session_load_count end if type(force_load_session) == "table" then force_load_pending = not IsThisSession(force_load_session) @@ -995,7 +1017,6 @@ local function SaveProfileAsync(profile, release_from_session, is_overwriting) end if session_owns_profile == true then -- We may only edit the profile if this session has ownership of the profile - if is_overwriting ~= true then -- 2) Manage global updates: -- local latest_global_updates_data = latest_data.GlobalUpdates -- {update_index, {{update_id, version_id, update_locked, update_data}, ...}} @@ -1016,7 +1037,10 @@ local function SaveProfileAsync(profile, release_from_session, is_overwriting) -- Locked update clearing: for _, clear_id in ipairs(pending_update_clear) do for i = 1, #latest_global_updates_list do - if latest_global_updates_list[i][1] == clear_id and latest_global_updates_list[i][3] == true then + if + latest_global_updates_list[i][1] == clear_id + and latest_global_updates_list[i][3] == true + then table.remove(latest_global_updates_list, i) break end @@ -1041,12 +1065,9 @@ local function SaveProfileAsync(profile, release_from_session, is_overwriting) latest_data.MetaData.ForceLoadSession = nil latest_data.GlobalUpdates = profile.GlobalUpdates._updates_latest end - end end, - }, - profile._is_user_mock - ) + }, profile._is_user_mock) if loaded_data ~= nil and key_info ~= nil then if is_overwriting == true then break @@ -1122,7 +1143,7 @@ local GlobalUpdates = { _update_handler_mode = true / nil, -- [bool / nil] --]] -} +} :: GlobalUpdates GlobalUpdates.__index = GlobalUpdates -- ALWAYS PUBLIC: @@ -1140,7 +1161,7 @@ function GlobalUpdates:GetActiveUpdates() --> [table] {{update_id, update_data}, end end if is_pending_lock == false then - table.insert(query_list, {global_update[1], global_update[4]}) + table.insert(query_list, { global_update[1], global_update[4] }) end end end @@ -1161,7 +1182,7 @@ function GlobalUpdates:GetLockedUpdates() --> [table] {{update_id, update_data}, end end if is_pending_clear == false then - table.insert(query_list, {global_update[1], global_update[4]}) + table.insert(query_list, { global_update[1], global_update[4] }) end end end @@ -1169,7 +1190,7 @@ function GlobalUpdates:GetLockedUpdates() --> [table] {{update_id, update_data}, end -- ONLY WHEN FROM "Profile.GlobalUpdates": -function GlobalUpdates:ListenToNewActiveUpdate(listener) --> [ScriptConnection] listener(update_id, update_data) +function GlobalUpdates:ListenToNewActiveUpdate(listener: (number, table) -> nil) --> [ScriptConnection] listener(update_id, update_data) if type(listener) ~= "function" then error("[ProfileService]: Only a function can be set as listener in GlobalUpdates:ListenToNewActiveUpdate()") end @@ -1187,7 +1208,7 @@ function GlobalUpdates:ListenToNewActiveUpdate(listener) --> [ScriptConnection] return self._new_active_update_listeners:Connect(listener) end -function GlobalUpdates:ListenToNewLockedUpdate(listener) --> [ScriptConnection] listener(update_id, update_data) +function GlobalUpdates:ListenToNewLockedUpdate(listener: (number, table) -> nil) --> [ScriptConnection] listener(update_id, update_data) if type(listener) ~= "function" then error("[ProfileService]: Only a function can be set as listener in GlobalUpdates:ListenToNewLockedUpdate()") end @@ -1205,7 +1226,7 @@ function GlobalUpdates:ListenToNewLockedUpdate(listener) --> [ScriptConnection] return self._new_locked_update_listeners:Connect(listener) end -function GlobalUpdates:LockActiveUpdate(update_id) +function GlobalUpdates:LockActiveUpdate(update_id: number) if type(update_id) ~= "number" then error("[ProfileService]: Invalid update_id") end @@ -1241,7 +1262,7 @@ function GlobalUpdates:LockActiveUpdate(update_id) end end -function GlobalUpdates:ClearLockedUpdate(update_id) +function GlobalUpdates:ClearLockedUpdate(update_id: number) if type(update_id) ~= "number" then error("[ProfileService]: Invalid update_id") end @@ -1278,24 +1299,28 @@ function GlobalUpdates:ClearLockedUpdate(update_id) end -- EXPOSED TO "update_handler" DURING ProfileStore:GlobalUpdateProfileAsync() CALL -function GlobalUpdates:AddActiveUpdate(update_data) +function GlobalUpdates:AddActiveUpdate(update_data: table) if type(update_data) ~= "table" then error("[ProfileService]: Invalid update_data") end if self._new_active_update_listeners ~= nil then - error("[ProfileService]: Can't add active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()") + error( + "[ProfileService]: Can't add active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()" + ) elseif self._update_handler_mode ~= true then - error("[ProfileService]: Can't add active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()") + error( + "[ProfileService]: Can't add active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()" + ) end -- self._updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}} local updates_latest = self._updates_latest local update_index = updates_latest[1] + 1 -- Incrementing global update index updates_latest[1] = update_index -- Add new active global update: - table.insert(updates_latest[2], {update_index, 1, false, update_data}) + table.insert(updates_latest[2], { update_index, 1, false, update_data }) end -function GlobalUpdates:ChangeActiveUpdate(update_id, update_data) +function GlobalUpdates:ChangeActiveUpdate(update_id: number, update_data: table) if type(update_id) ~= "number" then error("[ProfileService]: Invalid update_id") end @@ -1303,9 +1328,13 @@ function GlobalUpdates:ChangeActiveUpdate(update_id, update_data) error("[ProfileService]: Invalid update_data") end if self._new_active_update_listeners ~= nil then - error("[ProfileService]: Can't change active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()") + error( + "[ProfileService]: Can't change active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()" + ) elseif self._update_handler_mode ~= true then - error("[ProfileService]: Can't change active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()") + error( + "[ProfileService]: Can't change active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()" + ) end -- self._updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}} local updates_latest = self._updates_latest @@ -1327,14 +1356,18 @@ function GlobalUpdates:ChangeActiveUpdate(update_id, update_data) end end -function GlobalUpdates:ClearActiveUpdate(update_id) +function GlobalUpdates:ClearActiveUpdate(update_id: number) if type(update_id) ~= "number" then error("[ProfileService]: Invalid update_id argument") end if self._new_active_update_listeners ~= nil then - error("[ProfileService]: Can't clear active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()") + error( + "[ProfileService]: Can't clear active global updates in loaded Profile; Use ProfileStore:GlobalUpdateProfileAsync()" + ) elseif self._update_handler_mode ~= true then - error("[ProfileService]: Can't clear active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()") + error( + "[ProfileService]: Can't clear active global updates in view mode; Use ProfileStore:GlobalUpdateProfileAsync()" + ) end -- self._updates_latest = {}, -- [table] {update_index, {{update_id, version_id, update_locked, update_data}, ...}} local updates_latest = self._updates_latest @@ -1379,15 +1412,16 @@ local Profile = { _is_user_mock = false, -- ProfileStore.Mock _mock_key_info = {}, --]] -} +} :: GenericProfile Profile.__index = Profile -function Profile:IsActive() --> [bool] - local loaded_profiles = self._is_user_mock == true and self._profile_store._mock_loaded_profiles or self._profile_store._loaded_profiles +function Profile:IsActive(): boolean --> [bool] + local loaded_profiles = self._is_user_mock == true and self._profile_store._mock_loaded_profiles + or self._profile_store._loaded_profiles return loaded_profiles[self._profile_key] == self end -function Profile:GetMetaTag(tag_name) --> value +function Profile:GetMetaTag(tag_name: string): any --> value local meta_data = self.MetaData if meta_data == nil then return nil @@ -1396,7 +1430,7 @@ function Profile:GetMetaTag(tag_name) --> value return self.MetaData.MetaTags[tag_name] end -function Profile:SetMetaTag(tag_name, value) +function Profile:SetMetaTag(tag_name: string, value: any) if type(tag_name) ~= "string" then error("[ProfileService]: tag_name must be a string") elseif string.len(tag_name) == 0 then @@ -1409,12 +1443,12 @@ function Profile:Reconcile() ReconcileTable(self.Data, self._profile_store._profile_template) end -function Profile:ListenToRelease(listener) --> [ScriptConnection] (place_id / nil, game_job_id / nil) +function Profile:ListenToRelease(listener: (number?, string?) -> nil) --> [ScriptConnection] (place_id / nil, game_job_id / nil) if type(listener) ~= "function" then error("[ProfileService]: Only a function can be set as listener in Profile:ListenToRelease()") end if self._view_mode == true then - return {Disconnect = function() end} + return { Disconnect = function() end } end if self:IsActive() == false then -- Call release listener immediately if profile is expired @@ -1426,7 +1460,7 @@ function Profile:ListenToRelease(listener) --> [ScriptConnection] (place_id / ni game_job_id = active_session[2] end listener(place_id, game_job_id) - return {Disconnect = function() end} + return { Disconnect = function() end } else return self._release_listeners:Connect(listener) end @@ -1437,8 +1471,12 @@ function Profile:Save() error("[ProfileService]: Can't save Profile in view mode - Should you be calling :OverwriteAsync() instead?") end if self:IsActive() == false then - warn("[ProfileService]: Attempted saving an inactive profile " - .. self:Identify() .. "; Traceback:\n" .. debug.traceback()) + warn( + "[ProfileService]: Attempted saving an inactive profile " + .. self:Identify() + .. "; Traceback:\n" + .. debug.traceback() + ) return end -- Reject save request if a save is already pending in the queue - this will prevent the user from @@ -1461,26 +1499,29 @@ function Profile:Release() end end -function Profile:ListenToHopReady(listener) --> [ScriptConnection] () +function Profile:ListenToHopReady(listener: () -> ()) --> [ScriptConnection] () if type(listener) ~= "function" then error("[ProfileService]: Only a function can be set as listener in Profile:ListenToHopReady()") end if self._view_mode == true then - return {Disconnect = function() end} + return { Disconnect = function() end } end if self._hop_ready == true then task.spawn(listener) - return {Disconnect = function() end} + return { Disconnect = function() end } else return self._hop_ready_listeners:Connect(listener) end end -function Profile:AddUserId(user_id) -- Associates user_id with profile (GDPR compliance) - +function Profile:AddUserId(user_id: number) -- Associates user_id with profile (GDPR compliance) if type(user_id) ~= "number" or user_id % 1 ~= 0 then - warn("[ProfileService]: Invalid UserId argument for :AddUserId() (" - .. tostring(user_id) .. "); Traceback:\n" .. debug.traceback()) + warn( + "[ProfileService]: Invalid UserId argument for :AddUserId() (" + .. tostring(user_id) + .. "); Traceback:\n" + .. debug.traceback() + ) return end @@ -1491,23 +1532,24 @@ function Profile:AddUserId(user_id) -- Associates user_id with profile (GDPR com if table.find(self.UserIds, user_id) == nil then table.insert(self.UserIds, user_id) end - end -function Profile:RemoveUserId(user_id) -- Unassociates user_id with profile (safe function) - +function Profile:RemoveUserId(user_id: number) -- Unassociates user_id with profile (safe function) if type(user_id) ~= "number" or user_id % 1 ~= 0 then - warn("[ProfileService]: Invalid UserId argument for :RemoveUserId() (" - .. tostring(user_id) .. "); Traceback:\n" .. debug.traceback()) + warn( + "[ProfileService]: Invalid UserId argument for :RemoveUserId() (" + .. tostring(user_id) + .. "); Traceback:\n" + .. debug.traceback() + ) return end - + local index = table.find(self.UserIds, user_id) if index ~= nil then table.remove(self.UserIds, index) end - end function Profile:Identify() --> [string] @@ -1519,29 +1561,25 @@ function Profile:Identify() --> [string] end function Profile:ClearGlobalUpdates() -- Clears all global updates data from a profile payload - if self._view_mode ~= true then error("[ProfileService]: :ClearGlobalUpdates() can only be used in view mode") end local global_updates_object = { - _updates_latest = {0, {}}, + _updates_latest = { 0, {} }, _profile = self, } setmetatable(global_updates_object, GlobalUpdates) self.GlobalUpdates = global_updates_object - end function Profile:OverwriteAsync() -- Saves the profile to the DataStore and removes the session lock - if self._view_mode ~= true then error("[ProfileService]: :OverwriteAsync() can only be used in view mode") end SaveProfileAsync(self, nil, true) - end -- ProfileVersionQuery object: @@ -1561,7 +1599,7 @@ local ProfileVersionQuery = { _is_query_yielded = false, _query_queue = {}, --]] -} +} :: ProfileVersionQuery ProfileVersionQuery.__index = ProfileVersionQuery function ProfileVersionQuery:_MoveQueue() @@ -1574,8 +1612,7 @@ function ProfileVersionQuery:_MoveQueue() end end -function ProfileVersionQuery:NextAsync(_is_stacking) --> [Profile] or nil - +function ProfileVersionQuery:NextAsync(_is_stacking: boolean?): GenericProfile? --> [Profile] or nil if self._profile_store == nil then return nil end @@ -1584,7 +1621,6 @@ function ProfileVersionQuery:NextAsync(_is_stacking) --> [Profile] or nil local is_finished = false local function query_job() - if self._query_failure == true then is_finished = true return @@ -1593,13 +1629,12 @@ function ProfileVersionQuery:NextAsync(_is_stacking) --> [Profile] or nil -- First "next" call loads version pages: if self._query_pages == nil then - self._is_query_yielded = true task.spawn(function() profile = self:NextAsync(true) is_finished = true end) - + local list_success, error_message = pcall(function() self._query_pages = self._profile_store._global_data_store:ListVersionsAsync( self._profile_key, @@ -1619,14 +1654,13 @@ function ProfileVersionQuery:NextAsync(_is_stacking) --> [Profile] or nil self:_MoveQueue() return - end local current_page = self._query_pages:GetCurrentPage() local next_item = current_page[self._query_index + 1] -- No more entries: - + if self._query_pages.IsFinished == true and next_item == nil then is_finished = true return @@ -1635,7 +1669,6 @@ function ProfileVersionQuery:NextAsync(_is_stacking) --> [Profile] or nil -- Load next page when this page is over: if next_item == nil then - self._is_query_yielded = true task.spawn(function() profile = self:NextAsync(true) @@ -1655,7 +1688,6 @@ function ProfileVersionQuery:NextAsync(_is_stacking) --> [Profile] or nil self:_MoveQueue() return - end -- Next page item: @@ -1663,7 +1695,6 @@ function ProfileVersionQuery:NextAsync(_is_stacking) --> [Profile] or nil self._query_index += 1 profile = self._profile_store:ViewProfileAsync(self._profile_key, next_item.Version) is_finished = true - end if self._is_query_yielded == false then @@ -1681,7 +1712,6 @@ function ProfileVersionQuery:NextAsync(_is_stacking) --> [Profile] or nil end return profile - end -- ProfileStore object: @@ -1703,22 +1733,31 @@ local ProfileStore = { _mock_loaded_profiles = {[profile_key] = Profile, ...}, _mock_profile_load_jobs = {[profile_key] = {load_id, loaded_data}, ...}, --]] -} +} :: ProfileStore ProfileStore.__index = ProfileStore -function ProfileStore:LoadProfileAsync(profile_key, not_released_handler, _use_mock) --> [Profile / nil] not_released_handler(place_id, game_job_id) - +function ProfileStore:LoadProfileAsync( + profile_key: string, + not_released_handler: ((number, string) -> "Repeat" | "Cancel" | "ForceLoad" | "Steal") | "Steal" | "ForceLoad" | nil, + _use_mock: boolean +): GenericProfile? --> [Profile / nil] not_released_handler(place_id, game_job_id) not_released_handler = not_released_handler or "ForceLoad" if self._profile_template == nil then - error("[ProfileService]: Profile template not set - ProfileStore:LoadProfileAsync() locked for this ProfileStore") + error( + "[ProfileService]: Profile template not set - ProfileStore:LoadProfileAsync() locked for this ProfileStore" + ) end if type(profile_key) ~= "string" then error("[ProfileService]: profile_key must be a string") elseif string.len(profile_key) == 0 then error("[ProfileService]: Invalid profile_key") end - if type(not_released_handler) ~= "function" and not_released_handler ~= "ForceLoad" and not_released_handler ~= "Steal" then + if + type(not_released_handler) ~= "function" + and not_released_handler ~= "ForceLoad" + and not_released_handler ~= "Steal" + then error("[ProfileService]: Invalid not_released_handler") end @@ -1733,9 +1772,14 @@ function ProfileStore:LoadProfileAsync(profile_key, not_released_handler, _use_m -- Check if profile with profile_key isn't already loaded in this session: for _, profile_store in ipairs(ActiveProfileStores) do if profile_store._profile_store_lookup == self._profile_store_lookup then - local loaded_profiles = is_user_mock == true and profile_store._mock_loaded_profiles or profile_store._loaded_profiles + local loaded_profiles = is_user_mock == true and profile_store._mock_loaded_profiles + or profile_store._loaded_profiles if loaded_profiles[profile_key] ~= nil then - error("[ProfileService]: Profile " .. IdentifyProfile(self._profile_store_name, self._profile_store_scope, profile_key) .. " is already loaded in this session") + error( + "[ProfileService]: Profile " + .. IdentifyProfile(self._profile_store_name, self._profile_store_scope, profile_key) + .. " is already loaded in this session" + ) -- Are you using Profile:Release() properly? end end @@ -1770,70 +1814,65 @@ function ProfileStore:LoadProfileAsync(profile_key, not_released_handler, _use_m return nil end else - profile_load_job = {load_id, nil} + profile_load_job = { load_id, nil } profile_load_jobs[profile_key] = profile_load_job - profile_load_job[2] = table.pack(StandardProfileUpdateAsyncDataStore( - self, - profile_key, - { - ExistingProfileHandle = function(latest_data) - if ProfileService.ServiceLocked == false then - local active_session = latest_data.MetaData.ActiveSession - local force_load_session = latest_data.MetaData.ForceLoadSession - -- IsThisSession(active_session) - if active_session == nil then - latest_data.MetaData.ActiveSession = {PlaceId, JobId} - latest_data.MetaData.ForceLoadSession = nil - elseif type(active_session) == "table" then - if IsThisSession(active_session) == false then - local last_update = latest_data.MetaData.LastUpdate - if last_update ~= nil then - if os.time() - last_update > SETTINGS.AssumeDeadSessionLock then - latest_data.MetaData.ActiveSession = {PlaceId, JobId} - latest_data.MetaData.ForceLoadSession = nil - return - end + profile_load_job[2] = table.pack(StandardProfileUpdateAsyncDataStore(self, profile_key, { + ExistingProfileHandle = function(latest_data) + if ProfileService.ServiceLocked == false then + local active_session = latest_data.MetaData.ActiveSession + local force_load_session = latest_data.MetaData.ForceLoadSession + -- IsThisSession(active_session) + if active_session == nil then + latest_data.MetaData.ActiveSession = { PlaceId, JobId } + latest_data.MetaData.ForceLoadSession = nil + elseif type(active_session) == "table" then + if IsThisSession(active_session) == false then + local last_update = latest_data.MetaData.LastUpdate + if last_update ~= nil then + if os.time() - last_update > SETTINGS.AssumeDeadSessionLock then + latest_data.MetaData.ActiveSession = { PlaceId, JobId } + latest_data.MetaData.ForceLoadSession = nil + return end - if steal_session == true or aggressive_steal == true then - local force_load_uninterrupted = false - if force_load_session ~= nil then - force_load_uninterrupted = IsThisSession(force_load_session) - end - if force_load_uninterrupted == true or aggressive_steal == true then - latest_data.MetaData.ActiveSession = {PlaceId, JobId} - latest_data.MetaData.ForceLoadSession = nil - end - elseif request_force_load == true then - latest_data.MetaData.ForceLoadSession = {PlaceId, JobId} + end + if steal_session == true or aggressive_steal == true then + local force_load_uninterrupted = false + if force_load_session ~= nil then + force_load_uninterrupted = IsThisSession(force_load_session) + end + if force_load_uninterrupted == true or aggressive_steal == true then + latest_data.MetaData.ActiveSession = { PlaceId, JobId } + latest_data.MetaData.ForceLoadSession = nil end - else - latest_data.MetaData.ForceLoadSession = nil + elseif request_force_load == true then + latest_data.MetaData.ForceLoadSession = { PlaceId, JobId } end + else + latest_data.MetaData.ForceLoadSession = nil end end - end, - MissingProfileHandle = function(latest_data) - latest_data.Data = DeepCopyTable(self._profile_template) - latest_data.MetaData = { - ProfileCreateTime = os.time(), - SessionLoadCount = 0, - ActiveSession = {PlaceId, JobId}, - ForceLoadSession = nil, - MetaTags = {}, - } - end, - EditProfile = function(latest_data) - if ProfileService.ServiceLocked == false then - local active_session = latest_data.MetaData.ActiveSession - if active_session ~= nil and IsThisSession(active_session) == true then - latest_data.MetaData.SessionLoadCount = latest_data.MetaData.SessionLoadCount + 1 - latest_data.MetaData.LastUpdate = os.time() - end + end + end, + MissingProfileHandle = function(latest_data) + latest_data.Data = DeepCopyTable(self._profile_template) + latest_data.MetaData = { + ProfileCreateTime = os.time(), + SessionLoadCount = 0, + ActiveSession = { PlaceId, JobId }, + ForceLoadSession = nil, + MetaTags = {}, + } + end, + EditProfile = function(latest_data) + if ProfileService.ServiceLocked == false then + local active_session = latest_data.MetaData.ActiveSession + if active_session ~= nil and IsThisSession(active_session) == true then + latest_data.MetaData.SessionLoadCount = latest_data.MetaData.SessionLoadCount + 1 + latest_data.MetaData.LastUpdate = os.time() end - end, - }, - is_user_mock - )) + end + end, + }, is_user_mock)) if profile_load_job[1] == load_id then -- Load job hasn't been yoinked loaded_data, key_info = table.unpack(profile_load_job[2]) profile_load_jobs[profile_key] = nil @@ -1946,9 +1985,15 @@ function ProfileStore:LoadProfileAsync(profile_key, not_released_handler, _use_m task.wait() -- Overload prevention else error( - "[ProfileService]: Invalid return from not_released_handler (\"" .. tostring(handler_result) .. "\")(" .. type(handler_result) .. ");" .. - "\n" .. IdentifyProfile(self._profile_store_name, self._profile_store_scope, profile_key) .. - " Traceback:\n" .. debug.traceback() + '[ProfileService]: Invalid return from not_released_handler ("' + .. tostring(handler_result) + .. '")(' + .. type(handler_result) + .. ");" + .. "\n" + .. IdentifyProfile(self._profile_store_name, self._profile_store_scope, profile_key) + .. " Traceback:\n" + .. debug.traceback() ) end end @@ -1965,7 +2010,11 @@ function ProfileStore:LoadProfileAsync(profile_key, not_released_handler, _use_m return nil -- If loop breaks return nothing end -function ProfileStore:GlobalUpdateProfileAsync(profile_key, update_handler, _use_mock) --> [GlobalUpdates / nil] (update_handler(GlobalUpdates)) +function ProfileStore:GlobalUpdateProfileAsync( + profile_key: string, + update_handler: (GlobalUpdates) -> nil, + _use_mock: boolean +): GlobalUpdates? --> [GlobalUpdates / nil] (update_handler(GlobalUpdates)) if type(profile_key) ~= "string" or string.len(profile_key) == 0 then error("[ProfileService]: Invalid profile_key") end @@ -1981,24 +2030,19 @@ function ProfileStore:GlobalUpdateProfileAsync(profile_key, update_handler, _use while ProfileService.ServiceLocked == false do -- Updating profile: - local loaded_data = StandardProfileUpdateAsyncDataStore( - self, - profile_key, - { - ExistingProfileHandle = nil, - MissingProfileHandle = nil, - EditProfile = function(latest_data) - -- Running update_handler: - local global_updates_object = { - _updates_latest = latest_data.GlobalUpdates, - _update_handler_mode = true, - } - setmetatable(global_updates_object, GlobalUpdates) - update_handler(global_updates_object) - end, - }, - _use_mock == UseMockTag - ) + local loaded_data = StandardProfileUpdateAsyncDataStore(self, profile_key, { + ExistingProfileHandle = nil, + MissingProfileHandle = nil, + EditProfile = function(latest_data) + -- Running update_handler: + local global_updates_object = { + _updates_latest = latest_data.GlobalUpdates, + _update_handler_mode = true, + } + setmetatable(global_updates_object, GlobalUpdates) + update_handler(global_updates_object) + end, + }, _use_mock == UseMockTag) CustomWriteQueueMarkForCleanup(self._profile_store_lookup, profile_key) -- Handling loaded_data: if loaded_data ~= nil then @@ -2015,7 +2059,11 @@ function ProfileStore:GlobalUpdateProfileAsync(profile_key, update_handler, _use return nil -- Return nothing (Update unsuccessful) end -function ProfileStore:ViewProfileAsync(profile_key, version, _use_mock) --> [Profile / nil] +function ProfileStore:ViewProfileAsync( + profile_key: string, + version: string, + _use_mock: boolean +): GenericProfile? --> [Profile / nil] if type(profile_key) ~= "string" or string.len(profile_key) == 0 then error("[ProfileService]: Invalid profile_key") end @@ -2095,7 +2143,13 @@ function ProfileStore:ViewProfileAsync(profile_key, version, _use_mock) --> [Pro return nil -- If loop breaks return nothing end -function ProfileStore:ProfileVersionQuery(profile_key, sort_direction, min_date, max_date, _use_mock) --> [ProfileVersionQuery] +function ProfileStore:ProfileVersionQuery( + profile_key: string, + sort_direction: Enum.SortDirection?, + min_date: number?, + max_date: number?, + _use_mock: boolean? +): ProfileVersionQuery? --> [ProfileVersionQuery] if type(profile_key) ~= "string" or string.len(profile_key) == 0 then error("[ProfileService]: Invalid profile_key") end @@ -2111,8 +2165,10 @@ function ProfileStore:ProfileVersionQuery(profile_key, sort_direction, min_date, end -- Type check: - if sort_direction ~= nil and (typeof(sort_direction) ~= "EnumItem" - or sort_direction.EnumType ~= Enum.SortDirection) then + if + sort_direction ~= nil + and (typeof(sort_direction) ~= "EnumItem" or sort_direction.EnumType ~= Enum.SortDirection) + then error("[ProfileService]: Invalid sort_direction (" .. tostring(sort_direction) .. ")") end @@ -2144,10 +2200,9 @@ function ProfileStore:ProfileVersionQuery(profile_key, sort_direction, min_date, setmetatable(profile_version_query, ProfileVersionQuery) return profile_version_query - end -function ProfileStore:WipeProfileAsync(profile_key, _use_mock) --> is_wipe_successful [bool] +function ProfileStore:WipeProfileAsync(profile_key: string, _use_mock: boolean): boolean --> is_wipe_successful [bool] if type(profile_key) ~= "string" or string.len(profile_key) == 0 then error("[ProfileService]: Invalid profile_key") end @@ -2187,8 +2242,10 @@ end -- New ProfileStore: -function ProfileService.GetProfileStore(profile_store_index, profile_template) --> [ProfileStore] - +function ProfileService.GetProfileStore( + profile_store_index: ProfileStoreIndex, + profile_template: table +): ProfileStore? --> [ProfileStore] local profile_store_name local profile_store_scope = nil @@ -2206,13 +2263,15 @@ function ProfileService.GetProfileStore(profile_store_index, profile_template) - -- Type checking: if profile_store_name == nil or type(profile_store_name) ~= "string" then - error("[ProfileService]: Missing or invalid \"Name\" parameter") + error('[ProfileService]: Missing or invalid "Name" parameter') elseif string.len(profile_store_name) == 0 then error("[ProfileService]: ProfileStore name cannot be an empty string") end - if profile_store_scope ~= nil and (type(profile_store_scope) ~= "string" or string.len(profile_store_scope) == 0) then - error("[ProfileService]: Invalid \"Scope\" parameter") + if + profile_store_scope ~= nil and (type(profile_store_scope) ~= "string" or string.len(profile_store_scope) == 0) + then + error('[ProfileService]: Invalid "Scope" parameter') end if type(profile_template) ~= "table" then @@ -2232,11 +2291,17 @@ function ProfileService.GetProfileStore(profile_store_index, profile_template) - return profile_store:ViewProfileAsync(profile_key, version, UseMockTag) end, FindProfileVersionAsync = function(_, profile_key, sort_direction, min_date, max_date) - return profile_store:FindProfileVersionAsync(profile_key, sort_direction, min_date, max_date, UseMockTag) + return profile_store:FindProfileVersionAsync( + profile_key, + sort_direction, + min_date, + max_date, + UseMockTag + ) end, WipeProfileAsync = function(_, profile_key) return profile_store:WipeProfileAsync(profile_key, UseMockTag) - end + end, }, _profile_store_name = profile_store_name, @@ -2258,7 +2323,8 @@ function ProfileService.GetProfileStore(profile_store_index, profile_template) - task.spawn(function() WaitForLiveAccessCheck() if UseMockDataStore == false then - profile_store._global_data_store = DataStoreService:GetDataStore(profile_store_name, profile_store_scope) + profile_store._global_data_store = + DataStoreService:GetDataStore(profile_store_name, profile_store_scope) end profile_store._is_pending = false end) @@ -2271,12 +2337,10 @@ function ProfileService.GetProfileStore(profile_store_index, profile_template) - return profile_store end -function ProfileService.IsLive() --> [bool] -- (CAN YIELD!!!) - +function ProfileService.IsLive(): boolean --> [bool] -- (CAN YIELD!!!) WaitForLiveAccessCheck() return UseMockDataStore == false - end ----- Initialize ----- @@ -2292,11 +2356,14 @@ if IsStudio == true then if no_internet_access == true then warn("[ProfileService]: No internet access - check your network connection") end - if status == false and - (string.find(message, "403", 1, true) ~= nil or -- Cannot write to DataStore from studio if API access is not enabled - string.find(message, "must publish", 1, true) ~= nil or -- Game must be published to access live keys - no_internet_access == true) then -- No internet access - + if + status == false + and ( + string.find(message, "403", 1, true) ~= nil -- Cannot write to DataStore from studio if API access is not enabled + or string.find(message, "must publish", 1, true) ~= nil -- Game must be published to access live keys + or no_internet_access == true + ) + then -- No internet access UseMockDataStore = true ProfileService._use_mock_data_store = true print("[ProfileService]: Roblox API services unavailable - data will not be saved") @@ -2387,10 +2454,7 @@ task.spawn(function() -- 1) Release all active profiles: -- -- Clone AutoSaveList to a new table because AutoSaveList changes when profiles are released: local on_close_save_job_count = 0 - local active_profiles = {} - for index, profile in ipairs(AutoSaveList) do - active_profiles[index] = profile - end + local active_profiles = table.clone(AutoSaveList) -- Release the profiles; Releasing profiles can trigger listeners that release other profiles, so check active state: for _, profile in ipairs(active_profiles) do if profile:IsActive() == true then @@ -2411,4 +2475,97 @@ task.spawn(function() ) end) -return ProfileService \ No newline at end of file +export type ProfileStore = typeof(ProfileStore) & { + Mock: { [string]: any }, + + _profile_store_name: string, + _profile_store_scope: string?, + _profile_store_lookup: string, + + _profile_template: { [string]: any }, + _global_data_store: DataStore, + + _loaded_profiles: { [string]: GenericProfile }, + _profile_load_jobs: { [string]: { number | any } | any }, + + _mock_loaded_profiles: { [string]: GenericProfile }, + _mock_profile_load_jobs: { [string]: { number | any } | any }, +} + +export type Profile = typeof(Profile) & { + Data: T, + MetaData: { [string]: any }, + GlobalUpdates: GlobalUpdates, + + _profile_store: ProfileStore, + _profile_key: string, + + _release_listeners: ScriptSignal?, + _hop_ready_listeners: ScriptSignal?, + _hop_ready: boolean, + + _view_mode: boolean?, + _load_timestamp: number, + + _is_user_mock: boolean, + _mock_key_info: { [string]: any }, +} + +export type GenericProfile = Profile<{ [string]: any }> + +export type ProfileVersionQuery = typeof(ProfileVersionQuery) & { + _profile_store: ProfileStore, + _profile_key: string, + _sort_direction: Enum.SortDirection, + _min_date: number?, + _max_date: number?, + + _query_pages: DataStoreVersionPages, + _query_index: number, + _query_failure: boolean, + + _is_query_yielded: boolean, + _query_queue: { [number]: { [string]: any } }, +} + +export type GlobalUpdates = typeof(GlobalUpdates) & { + _updates_latest: { [number]: { [number]: { [string]: any } } }, + _pending_update_lock: { [number]: boolean }?, + _pending_update_clear: { [number]: boolean }?, + + _new_active_update_listeners: ScriptSignal?, + _new_locked_update_listeners: ScriptSignal?, + + _profile: GenericProfile?, + + _update_handler_mode: boolean?, +} + +type DataValue = { + Data: { [string]: any }, + MetaData: { [string]: any }, + GlobalUpdates: GlobalUpdates, +} + +type GlobalUpdatesData = { + [any]: any, +} + +type UpdateSettings = { + ExistingProfileHandle: (DataValue) -> nil, + MissingProfileHandle: (DataValue) -> nil, + EditProfile: (DataValue) -> nil, +} + +type ProfileStoreIndex = string | { Name: string, Scope: string? } +type SessionTag = { number | string } + +type MockDataStoreKeyInfo = { + CreatedTime: number?, + UpdatedTime: number?, + Version: string, + GetMetadata: () -> { [string]: any }, + GetUserIds: () -> { [string]: boolean }, +} + +return ProfileService