TradeSkillMaster/Core/Service/Accounting/Sync.lua

288 lines
9.8 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local AccountingSync = TSM.Accounting:NewPackage("Sync")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local Log = TSM.Include("Util.Log")
local TempTable = TSM.Include("Util.TempTable")
local Theme = TSM.Include("Util.Theme")
local Sync = TSM.Include("Service.Sync")
local private = {
accountLookup = {},
accountStatus = {},
pendingChunks = {},
dataTemp = {},
}
local CHANGE_NOTIFICATION_DELAY = 5
local RETRY_DELAY = 5
-- ============================================================================
-- Module Functions
-- ============================================================================
function AccountingSync.OnInitialize()
Sync.RegisterConnectionChangedCallback(private.ConnectionChangedHandler)
Sync.RegisterRPC("ACCOUNTING_GET_PLAYER_HASH", private.RPCGetPlayerHash)
Sync.RegisterRPC("ACCOUNTING_GET_PLAYER_CHUNKS", private.RPCGetPlayerChunks)
Sync.RegisterRPC("ACCOUNTING_GET_PLAYER_DATA", private.RPCGetData)
Sync.RegisterRPC("ACCOUNTING_CHANGE_NOTIFICATION", private.RPCChangeNotification)
end
function AccountingSync.GetStatus(account)
local status = private.accountStatus[account]
if not status then
return Theme.GetFeedbackColor("RED"):ColorText(L["Not Connected"])
elseif status == "GET_PLAYER_HASH" or status == "GET_PLAYER_CHUNKS" or status == "GET_PLAYER_DATA" or status == "RETRY" then
return Theme.GetFeedbackColor("YELLOW"):ColorText(L["Updating"])
elseif status == "SYNCED" then
return Theme.GetFeedbackColor("GREEN"):ColorText(L["Up to date"])
else
error("Invalid status: "..tostring(status))
end
end
function AccountingSync.OnTransactionsChanged()
Delay.AfterTime("ACCOUNTING_SYNC_CHANGE", CHANGE_NOTIFICATION_DELAY, private.NotifyChange)
end
-- ============================================================================
-- RPC Functions and Result Handlers
-- ============================================================================
function private.GetPlayerHash(player)
local account = private.accountLookup[player]
private.accountStatus[account] = "GET_PLAYER_HASH"
TSM.Accounting.Transactions.PrepareSyncHashes(player)
Sync.CallRPC("ACCOUNTING_GET_PLAYER_HASH", player, private.RPCGetPlayerHashResultHandler)
end
function private.RPCGetPlayerHash()
local player = UnitName("player")
return player, TSM.Accounting.Transactions.GetSyncHash(player)
end
function private.RPCGetPlayerHashResultHandler(player, hash)
local account = player and private.accountLookup[player]
if not account then
-- request timed out, so try again
Log.Warn("Getting player hash timed out")
private.QueueRetriesByStatus("GET_PLAYER_HASH")
return
elseif not hash then
-- the hash isn't ready yet, so try again
Log.Warn("Sync player hash not ready yet")
private.QueueRetryByPlayer(player)
return
end
if private.accountStatus[account] == "RETRY" then
-- There is a race condition where if we tried to issue GET_PLAYER_HASH for two players and one times out,
-- we would also queue a retry for the other one, so handle that here.
private.accountStatus[account] = "GET_PLAYER_HASH"
end
assert(private.accountStatus[account] == "GET_PLAYER_HASH")
local currentHash = TSM.Accounting.Transactions.GetSyncHash(player)
if not currentHash then
-- don't have our hash yet, so try again
Log.Warn("Current player hash not ready yet")
private.QueueRetryByPlayer(player)
return
end
if hash ~= currentHash then
Log.Info("Need updated transactions data from %s (%s, %s)", player, hash, currentHash)
private.GetPlayerChunks(player)
else
Log.Info("Transactions data for %s already up to date (%s, %s)", player, hash, currentHash)
private.accountStatus[account] = "SYNCED"
end
end
function private.GetPlayerChunks(player)
local account = private.accountLookup[player]
private.accountStatus[account] = "GET_PLAYER_CHUNKS"
Sync.CallRPC("ACCOUNTING_GET_PLAYER_CHUNKS", player, private.RPCGetPlayerChunksResultHandler)
end
function private.RPCGetPlayerChunks()
local player = UnitName("player")
return player, TSM.Accounting.Transactions.GetSyncHashByDay(player)
end
function private.RPCGetPlayerChunksResultHandler(player, chunks)
local account = player and private.accountLookup[player]
if not account then
-- request timed out, so try again from the start
Log.Warn("Getting chunks timed out")
private.QueueRetriesByStatus("GET_PLAYER_CHUNKS")
return
elseif not chunks then
-- the hashes have been invalidated, so try again from the start
Log.Warn("Sync player chunks not ready yet")
private.QueueRetryByPlayer(player)
return
end
assert(private.accountStatus[account] == "GET_PLAYER_CHUNKS")
local currentChunks = TSM.Accounting.Transactions.GetSyncHashByDay(player)
if not currentChunks then
-- our hashes have been invalidated, so try again from the start
Log.Warn("Local hashes are invalid")
private.QueueRetryByPlayer(player)
return
end
for day in pairs(currentChunks) do
if not chunks[day] then
-- remove day which no longer exists
TSM.Accounting.Transactions.RemovePlayerDay(player, day)
end
end
-- queue up all the pending chunks
private.pendingChunks[player] = private.pendingChunks[player] or TempTable.Acquire()
wipe(private.pendingChunks[player])
for day, hash in pairs(chunks) do
if currentChunks[day] ~= hash then
tinsert(private.pendingChunks[player], day)
end
end
local requestDay = private.GetNextPendingChunk(player)
if requestDay then
Log.Info("Requesting transactions data (%s, %s, %s, %s)", player, requestDay, tostring(currentChunks[requestDay]), chunks[requestDay])
private.GetPlayerData(player, requestDay)
else
Log.Info("All chunks are up to date (%s)", player)
private.accountStatus[account] = "SYNCED"
end
end
function private.GetPlayerData(player, requestDay)
local account = private.accountLookup[player]
private.accountStatus[account] = "GET_PLAYER_DATA"
Sync.CallRPC("ACCOUNTING_GET_PLAYER_DATA", player, private.RPCGetDataResultHandler, requestDay)
end
function private.RPCGetData(day)
local player = UnitName("player")
wipe(private.dataTemp)
TSM.Accounting.Transactions.GetSyncData(player, day, private.dataTemp)
return player, day, private.dataTemp
end
function private.RPCGetDataResultHandler(player, day, data)
local account = player and private.accountLookup[player]
if not account then
-- request timed out, so try again from the start
Log.Warn("Getting transactions data timed out")
private.QueueRetriesByStatus("GET_PLAYER_DATA")
return
elseif #data % 9 ~= 0 then
-- invalid data - just silently give up
Log.Warn("Got invalid transactions data")
return
end
assert(private.accountStatus[account] == "GET_PLAYER_DATA")
Log.Info("Received transactions data (%s, %s, %s)", player, day, #data)
TSM.Accounting.Transactions.HandleSyncedData(player, day, data)
local requestDay = private.GetNextPendingChunk(player)
if requestDay then
-- request the next chunk
Log.Info("Requesting transactions data (%s, %s)", player, requestDay)
private.GetPlayerData(player, requestDay)
else
-- request chunks again to check for other chunks we need to sync
private.GetPlayerChunks(player)
end
end
function private.RPCChangeNotification(player)
if private.accountStatus[private.accountLookup[player]] == "SYNCED" then
-- request the player hash
Log.Info("Got change notification - requesting player hash")
private.GetPlayerHash(player)
else
Log.Info("Got change notification - dropping (%s)", tostring(private.accountStatus[private.accountLookup[player]]))
end
end
function private.RPCChangeNotificationResultHandler()
-- nop
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ConnectionChangedHandler(account, player, connected)
if connected then
private.accountLookup[player] = account
private.GetPlayerHash(player)
else
private.accountLookup[player] = nil
private.accountStatus[account] = nil
if private.pendingChunks[player] then
TempTable.Release(private.pendingChunks[player])
private.pendingChunks[player] = nil
end
end
end
function private.GetNextPendingChunk(player)
if not private.pendingChunks[player] then
return nil
end
local result = tremove(private.pendingChunks[player])
if not result then
TempTable.Release(private.pendingChunks[player])
private.pendingChunks[player] = nil
end
return result
end
function private.QueueRetriesByStatus(statusFilter)
for player, account in pairs(private.accountLookup) do
if private.accountStatus[account] == statusFilter then
private.QueueRetryByPlayer(player)
end
end
end
function private.QueueRetryByPlayer(player)
local account = private.accountLookup[player]
Log.Info("Retrying (%s, %s, %s)", player, account, private.accountStatus[account])
private.accountStatus[account] = "RETRY"
Delay.AfterTime(RETRY_DELAY, private.RetryGetPlayerHashRPC)
end
function private.RetryGetPlayerHashRPC()
for player, account in pairs(private.accountLookup) do
if private.accountStatus[account] == "RETRY" then
private.GetPlayerHash(player)
end
end
end
function private.NotifyChange()
for player, account in pairs(private.accountLookup) do
if private.accountStatus[account] == "SYNCED" then
-- notify the other account that our data has changed and request the other account's latest hash ourselves
private.GetPlayerHash(player)
Sync.CallRPC("ACCOUNTING_CHANGE_NOTIFICATION", player, private.RPCChangeNotificationResultHandler, UnitName("player"))
end
end
end