273 lines
7.7 KiB
Plaintext
273 lines
7.7 KiB
Plaintext
--!strict
|
|
|
|
--[[
|
|
Constructs a new ForValues object which maps values of a table using
|
|
a `processor` function.
|
|
|
|
Optionally, a `destructor` function can be specified for cleaning up values.
|
|
If omitted, the default cleanup function will be used instead.
|
|
|
|
Additionally, a `meta` table/value can optionally be returned to pass data created
|
|
when running the processor to the destructor when the created object is cleaned up.
|
|
]]
|
|
local PubTypes = require "../PubTypes"
|
|
local Types = require "../Types"
|
|
-- Logging
|
|
local parseError = require "../Logging/parseError"
|
|
local logError = require "../Logging/logError"
|
|
local logErrorNonFatal = require "../Logging/logErrorNonFatal"
|
|
local logWarn = require "../Logging/logWarn"
|
|
-- Utility
|
|
local cleanup = require "../Utility/cleanup"
|
|
local needsDestruction = require "../Utility/needsDestruction"
|
|
-- State
|
|
local peek = require "../State/peek"
|
|
local makeUseCallback = require "../State/makeUseCallback"
|
|
local isState = require "../State/isState"
|
|
|
|
local class = {}
|
|
|
|
local CLASS_METATABLE = { __index = class }
|
|
local WEAK_KEYS_METATABLE = { __mode = "k" }
|
|
|
|
--[[
|
|
Called when the original table is changed.
|
|
|
|
This will firstly find any values meeting any of the following criteria:
|
|
|
|
- they were not previously present
|
|
- a dependency used during generation of this value has changed
|
|
|
|
It will recalculate those values, storing information about any dependencies
|
|
used in the processor callback during value generation, and save the new value
|
|
to the output array with the same key. If it is overwriting an older value,
|
|
that older value will be passed to the destructor for cleanup.
|
|
|
|
Finally, this function will find values that are no longer present, and remove
|
|
their values from the output table and pass them to the destructor. You can re-use
|
|
the same value multiple times and this will function will update them as little as
|
|
possible; reusing the same values where possible.
|
|
]]
|
|
function class:update(): boolean
|
|
local inputIsState = self._inputIsState
|
|
local inputTable = peek(self._inputTable)
|
|
local outputValues = {}
|
|
|
|
local didChange = false
|
|
|
|
-- clean out value cache
|
|
self._oldValueCache, self._valueCache =
|
|
self._valueCache, self._oldValueCache
|
|
local newValueCache = self._valueCache
|
|
local oldValueCache = self._oldValueCache
|
|
-- table.clear(newValueCache)
|
|
for i, _ in pairs(newValueCache) do
|
|
newValueCache[i] = nil
|
|
end
|
|
|
|
-- clean out main dependency set
|
|
for dependency in pairs(self.dependencySet) do
|
|
dependency.dependentSet[self] = nil
|
|
end
|
|
self._oldDependencySet, self.dependencySet =
|
|
self.dependencySet, self._oldDependencySet
|
|
-- table.clear(self.dependencySet)
|
|
for i, _ in pairs(self.dependencySet) do
|
|
self.dependencySet[i] = nil
|
|
end
|
|
|
|
-- if the input table is a state object, add it as a dependency
|
|
if inputIsState then
|
|
self._inputTable.dependentSet[self] = true
|
|
self.dependencySet[self._inputTable] = true
|
|
end
|
|
|
|
-- STEP 1: find values that changed or were not previously present
|
|
for inKey, inValue in pairs(inputTable) do
|
|
-- check if the value is new or changed
|
|
local oldCachedValues = oldValueCache[inValue]
|
|
local shouldRecalculate = oldCachedValues == nil
|
|
|
|
-- get a cached value and its dependency/meta data if available
|
|
local value, valueData, meta
|
|
|
|
if type(oldCachedValues) == "table" and #oldCachedValues > 0 then
|
|
local valueInfo = table.remove(oldCachedValues, #oldCachedValues)
|
|
value = valueInfo.value
|
|
valueData = valueInfo.valueData
|
|
meta = valueInfo.meta
|
|
|
|
if #oldCachedValues <= 0 then
|
|
oldValueCache[inValue] = nil
|
|
end
|
|
elseif oldCachedValues ~= nil then
|
|
oldValueCache[inValue] = nil
|
|
shouldRecalculate = true
|
|
end
|
|
|
|
if valueData == nil then
|
|
valueData = {
|
|
dependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
|
|
oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
|
|
dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE),
|
|
}
|
|
end
|
|
|
|
-- check if the value's dependencies have changed
|
|
if shouldRecalculate == false then
|
|
for dependency, oldValue in pairs(valueData.dependencyValues) do
|
|
if oldValue ~= peek(dependency) then
|
|
shouldRecalculate = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- recalculate the output value if necessary
|
|
if shouldRecalculate then
|
|
valueData.oldDependencySet, valueData.dependencySet =
|
|
valueData.dependencySet, valueData.oldDependencySet
|
|
-- table.clear(valueData.dependencySet)
|
|
for i, _ in pairs(valueData.dependencySet) do
|
|
valueData.dependencySet[i] = nil
|
|
end
|
|
|
|
local use = makeUseCallback(valueData.dependencySet)
|
|
-- local processOK, newOutValue, newMetaValue =
|
|
-- xpcall(self._processor, parseError, use, inValue)
|
|
local processOK, newOutValue, newMetaValue =
|
|
pcall(self._processor, use, inValue)
|
|
|
|
if processOK then
|
|
if
|
|
self._destructor == nil
|
|
and (
|
|
needsDestruction(newOutValue)
|
|
or needsDestruction(newMetaValue)
|
|
)
|
|
then
|
|
logWarn "destructorNeededForValues"
|
|
end
|
|
|
|
-- pass the old value to the destructor if it exists
|
|
if value ~= nil then
|
|
local destructOK, err =
|
|
pcall(self._destructor or cleanup, value, meta)
|
|
if not destructOK then
|
|
logErrorNonFatal(
|
|
"forValuesDestructorError",
|
|
parseError(err)
|
|
)
|
|
end
|
|
end
|
|
|
|
-- store the new value and meta data
|
|
value = newOutValue
|
|
meta = newMetaValue
|
|
didChange = true
|
|
else
|
|
-- restore old dependencies, because the new dependencies may be corrupt
|
|
valueData.oldDependencySet, valueData.dependencySet =
|
|
valueData.dependencySet, valueData.oldDependencySet
|
|
logErrorNonFatal(
|
|
"forValuesProcessorError",
|
|
parseError(newOutValue)
|
|
)
|
|
end
|
|
end
|
|
|
|
-- store the value and its dependency/meta data
|
|
local newCachedValues = newValueCache[inValue]
|
|
if newCachedValues == nil then
|
|
newCachedValues = {}
|
|
newValueCache[inValue] = newCachedValues
|
|
end
|
|
|
|
table.insert(newCachedValues, {
|
|
value = value,
|
|
valueData = valueData,
|
|
meta = meta,
|
|
})
|
|
|
|
outputValues[inKey] = value
|
|
|
|
-- save dependency values and add to main dependency set
|
|
for dependency in pairs(valueData.dependencySet) do
|
|
valueData.dependencyValues[dependency] = peek(dependency)
|
|
|
|
self.dependencySet[dependency] = true
|
|
dependency.dependentSet[self] = true
|
|
end
|
|
end
|
|
|
|
-- STEP 2: find values that were removed
|
|
-- for tables of data, we just need to check if it's still in the cache
|
|
for _oldInValue, oldCachedValueInfo in pairs(oldValueCache) do
|
|
for _, valueInfo in ipairs(oldCachedValueInfo) do
|
|
local oldValue = valueInfo.value
|
|
local oldMetaValue = valueInfo.meta
|
|
|
|
local destructOK, err =
|
|
pcall(self._destructor or cleanup, oldValue, oldMetaValue)
|
|
if not destructOK then
|
|
logErrorNonFatal("forValuesDestructorError", parseError(err))
|
|
end
|
|
|
|
didChange = true
|
|
end
|
|
|
|
-- table.clear(oldCachedValueInfo)
|
|
for i, _ in pairs(oldCachedValueInfo) do
|
|
oldCachedValueInfo[i] = nil
|
|
end
|
|
end
|
|
|
|
self._outputTable = outputValues
|
|
|
|
return didChange
|
|
end
|
|
|
|
--[[
|
|
Returns the interior value of this state object.
|
|
]]
|
|
function class:_peek(): any
|
|
return self._outputTable
|
|
end
|
|
|
|
function class:get()
|
|
logError "stateGetWasRemoved"
|
|
end
|
|
|
|
local function ForValues<VI, VO, M>(
|
|
inputTable: PubTypes.CanBeState<{ [any]: VI }>,
|
|
processor: (VI) -> (VO, M?),
|
|
destructor: (VO, M?) -> ()?
|
|
): Types.ForValues<VI, VO, M>
|
|
local inputIsState = isState(inputTable)
|
|
|
|
local self = setmetatable({
|
|
type = "State",
|
|
kind = "ForValues",
|
|
dependencySet = {},
|
|
-- if we held strong references to the dependents, then they wouldn't be
|
|
-- able to get garbage collected when they fall out of scope
|
|
dependentSet = setmetatable({}, WEAK_KEYS_METATABLE),
|
|
_oldDependencySet = {},
|
|
|
|
_processor = processor,
|
|
_destructor = destructor,
|
|
_inputIsState = inputIsState,
|
|
|
|
_inputTable = inputTable,
|
|
_outputTable = {},
|
|
_valueCache = {},
|
|
_oldValueCache = {},
|
|
}, CLASS_METATABLE)
|
|
|
|
self:update()
|
|
|
|
return self
|
|
end
|
|
|
|
return ForValues
|