You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
6.1 KiB

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local TSM = select(2, ...) ---@type TSM
local State = TSM.Init("Util.ReactiveClasses.State") ---@class Util.ReactiveClasses.State
local Publisher = TSM.Include("Util.ReactiveClasses.Publisher")
local Table = TSM.Include("Util.Table")
local private = {
nextPublisherId = 1,
stateContext = {},
debugLinesTemp = {},
}
-- ============================================================================
-- State Methods
-- ============================================================================
local STATE_METHODS = {} ---@class ReactiveState
---Creates a new publisher which gets published values from the state.
---@return ReactivePublisher @The publisher object
function STATE_METHODS:Publisher()
local context = private.stateContext[self]
local publisher = Publisher.Get()
publisher:_Acquire(self)
assert(not context.publishers[publisher])
context.publishers[publisher] = private.nextPublisherId
private.nextPublisherId = private.nextPublisherId + 1
tinsert(context.publishers, publisher)
return publisher
end
---Creates a new publisher for a specific key of the state.
---@param key string The key to create a publisher for (ignoring duplicate values)
---@return ReactivePublisher @The publisher object
function STATE_METHODS:PublisherForKeyChange(key)
local context = private.stateContext[self]
assert(context.schema:_HasKey(key))
return self:Publisher()
:MapWithKey(key)
:IgnoreDuplicates()
end
---Resets the state to its default value
function STATE_METHODS:ResetToDefault()
local context = private.stateContext[self]
wipe(context.data)
context.schema:_ApplyDefaults(context.data)
self:_HandleDataChanged()
end
---Automatically stores any new publishers in the specified table
---@param tbl table The table to store new publishers in
function STATE_METHODS:SetAutoStore(tbl)
local context = private.stateContext[self]
context.autoStore = tbl
end
function STATE_METHODS:_GetData()
return private.stateContext[self].data
end
function STATE_METHODS:_HandlePublisherEvent(publisher, event, arg)
local context = private.stateContext[self]
if event == "OnHandled" then
if context.autoStore then
publisher:StoreIn(context.autoStore)
end
elseif event == "OnCommit" then
assert(arg, "State publishers must be optimized")
-- Send the initial value
publisher:_HandleData(context.data)
elseif event == "OnCancel" then
assert(context.publishers[publisher])
context.publishers[publisher] = nil
assert(Table.RemoveByValue(context.publishers, publisher) == 1)
else
error("Unknown event: "..tostring(event))
end
end
function STATE_METHODS:_HandleDataChanged(key)
local context = private.stateContext[self]
if context.handlingDataChange then
-- We are already in the middle of processing another event, so queue this one up
tinsert(context.dataChangeQueue, key)
assert(#context.dataChangeQueue < 50)
return
end
context.handlingDataChange = true
self:_CallPublishersHandleData(key)
-- Process queued keys
while #context.dataChangeQueue > 0 do
local queuedKey = tremove(context.dataChangeQueue, 1)
self:_CallPublishersHandleData(queuedKey)
end
context.handlingDataChange = false
end
function STATE_METHODS:_CallPublishersHandleData(key)
local context = private.stateContext[self]
-- The list of publishers can change as a result of calling _HandleData() so copy them to a
-- temp table and verify they are still subscribed before calling them.
assert(not next(context.dataChangeTemp))
local maxId = 0
for _, publisher in ipairs(context.publishers) do
tinsert(context.dataChangeTemp, publisher)
maxId = max(maxId, context.publishers[publisher])
end
local data = context.data
while #context.dataChangeTemp > 0 do
local publisher = tremove(context.dataChangeTemp, 1)
local id = context.publishers[publisher]
if id and id <= maxId then
publisher:_HandleData(data, key)
end
end
end
-- ============================================================================
-- State Metatable
-- ============================================================================
local STATE_MT = {
__index = function(self, key)
if STATE_METHODS[key] then
return STATE_METHODS[key]
end
local context = private.stateContext[self]
if not context.schema:_HasKey(key) then
error("Invalid key: "..tostring(key))
end
return context.data[key]
end,
__newindex = function(self, key, value)
local context = private.stateContext[self]
local data = context.data
if data[key] == value then
return
end
context.schema:_ValidateValueForKey(key, value)
data[key] = value
self:_HandleDataChanged(key)
end,
__metatable = false,
}
-- ============================================================================
-- Module Methods
-- ============================================================================
---Creates a new state object.
---@return ReactiveState @The state
function State.Create(schema)
local state = setmetatable({}, STATE_MT)
local data = {}
schema:_ApplyDefaults(data)
private.stateContext[state] = {
schema = schema,
data = data,
publishers = {},
autoStore = nil,
handlingDataChange = false,
dataChangeQueue = {},
dataChangeTemp = {},
}
return state
end
function State.GetDebugInfo(state)
local context = private.stateContext[state]
if not context then
return nil
end
assert(not next(private.debugLinesTemp))
for key, fieldType in context.schema:_FieldIterator() do
local value = context.data[key]
if value ~= nil then
if fieldType == "string" then
tinsert(private.debugLinesTemp, format("%s = \"%s\"", key, value))
else
tinsert(private.debugLinesTemp, format("%s = %s", key, tostring(value)))
end
end
end
local result = table.concat(private.debugLinesTemp, "\n")
wipe(private.debugLinesTemp)
return result
end