341 lines
9.3 KiB
Plaintext
341 lines
9.3 KiB
Plaintext
--!strict
|
|
|
|
--[[
|
|
Constructs a new ForPairs object which maps pairs 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 logErrorNonFatal = require "../Logging/logErrorNonFatal"
|
|
local logError = require "../Logging/logError"
|
|
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 keys meeting any of the following criteria:
|
|
|
|
- they were not previously present
|
|
- their associated value has changed
|
|
- a dependency used during generation of this value has changed
|
|
|
|
It will recalculate those key/value pairs, storing information about any
|
|
dependencies used in the processor callback during value generation, and
|
|
save the new key/value pair to the output array. If it is overwriting an
|
|
older key/value pair, that older pair will be passed to the destructor
|
|
for cleanup.
|
|
|
|
Finally, this function will find keys that are no longer present, and remove
|
|
their key/value pairs from the output table and pass them to the destructor.
|
|
]]
|
|
function class:update(): boolean
|
|
local inputIsState = self._inputIsState
|
|
local newInputTable = peek(self._inputTable)
|
|
local oldInputTable = self._oldInputTable
|
|
|
|
local keyIOMap = self._keyIOMap
|
|
local meta = self._meta
|
|
|
|
local didChange = false
|
|
|
|
-- 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
|
|
|
|
-- clean out output table
|
|
self._oldOutputTable, self._outputTable =
|
|
self._outputTable, self._oldOutputTable
|
|
|
|
local oldOutputTable = self._oldOutputTable
|
|
local newOutputTable = self._outputTable
|
|
-- table.clear(newOutputTable)
|
|
for i, _ in pairs(newOutputTable) do
|
|
newOutputTable[i] = nil
|
|
end
|
|
|
|
-- Step 1: find key/value pairs that changed or were not previously present
|
|
|
|
for newInKey, newInValue in pairs(newInputTable) do
|
|
-- get or create key data
|
|
local keyData = self._keyData[newInKey]
|
|
|
|
if keyData == nil then
|
|
keyData = {
|
|
dependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
|
|
oldDependencySet = setmetatable({}, WEAK_KEYS_METATABLE),
|
|
dependencyValues = setmetatable({}, WEAK_KEYS_METATABLE),
|
|
}
|
|
self._keyData[newInKey] = keyData
|
|
end
|
|
|
|
-- check if the pair is new or changed
|
|
local shouldRecalculate = oldInputTable[newInKey] ~= newInValue
|
|
|
|
-- check if the pair's dependencies have changed
|
|
if shouldRecalculate == false then
|
|
for dependency, oldValue in pairs(keyData.dependencyValues) do
|
|
if oldValue ~= peek(dependency) then
|
|
shouldRecalculate = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- recalculate the output pair if necessary
|
|
if shouldRecalculate then
|
|
keyData.oldDependencySet, keyData.dependencySet =
|
|
keyData.dependencySet, keyData.oldDependencySet
|
|
-- table.clear(keyData.dependencySet)
|
|
for i, _ in pairs(keyData.dependencySet) do
|
|
keyData.dependencySet[i] = nil
|
|
end
|
|
|
|
local use = makeUseCallback(keyData.dependencySet)
|
|
-- local processOK, newOutKey, newOutValue, newMetaValue =
|
|
-- xpcall(self._processor, parseError, use, newInKey, newInValue)
|
|
local processOK, newOutKey, newOutValue, newMetaValue =
|
|
pcall(self._processor, use, newInKey, newInValue)
|
|
|
|
if processOK then
|
|
if
|
|
self._destructor == nil
|
|
and (
|
|
needsDestruction(newOutKey)
|
|
or needsDestruction(newOutValue)
|
|
or needsDestruction(newMetaValue)
|
|
)
|
|
then
|
|
logWarn "destructorNeededForPairs"
|
|
end
|
|
|
|
-- if this key was already written to on this run-through, throw a fatal error.
|
|
if newOutputTable[newOutKey] ~= nil then
|
|
-- figure out which key/value pair previously wrote to this key
|
|
local previousNewKey, previousNewValue
|
|
for inKey, outKey in pairs(keyIOMap) do
|
|
if outKey == newOutKey then
|
|
previousNewValue = newInputTable[inKey]
|
|
if previousNewValue ~= nil then
|
|
previousNewKey = inKey
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if previousNewKey ~= nil then
|
|
logError(
|
|
"forPairsKeyCollision",
|
|
nil,
|
|
tostring(newOutKey),
|
|
tostring(previousNewKey),
|
|
tostring(previousNewValue),
|
|
tostring(newInKey),
|
|
tostring(newInValue)
|
|
)
|
|
end
|
|
end
|
|
|
|
local oldOutValue = oldOutputTable[newOutKey]
|
|
|
|
if oldOutValue ~= newOutValue then
|
|
local oldMetaValue = meta[newOutKey]
|
|
if oldOutValue ~= nil then
|
|
local destructOK, err = pcall(
|
|
self._destructor or cleanup,
|
|
newOutKey,
|
|
oldOutValue,
|
|
oldMetaValue
|
|
)
|
|
if not destructOK then
|
|
logErrorNonFatal(
|
|
"forPairsDestructorError",
|
|
parseError(err)
|
|
)
|
|
end
|
|
end
|
|
|
|
oldOutputTable[newOutKey] = nil
|
|
end
|
|
|
|
-- update the stored data for this key/value pair
|
|
oldInputTable[newInKey] = newInValue
|
|
keyIOMap[newInKey] = newOutKey
|
|
meta[newOutKey] = newMetaValue
|
|
newOutputTable[newOutKey] = newOutValue
|
|
|
|
-- if we had to recalculate the output, then we did change
|
|
didChange = true
|
|
else
|
|
-- restore old dependencies, because the new dependencies may be corrupt
|
|
keyData.oldDependencySet, keyData.dependencySet =
|
|
keyData.dependencySet, keyData.oldDependencySet
|
|
|
|
logErrorNonFatal(
|
|
"forPairsProcessorError",
|
|
parseError(newOutKey)
|
|
)
|
|
end
|
|
else
|
|
local storedOutKey = keyIOMap[newInKey]
|
|
|
|
-- check for key collision
|
|
if newOutputTable[storedOutKey] ~= nil then
|
|
-- figure out which key/value pair previously wrote to this key
|
|
local previousNewKey, previousNewValue
|
|
for inKey, outKey in pairs(keyIOMap) do
|
|
if storedOutKey == outKey then
|
|
previousNewValue = newInputTable[inKey]
|
|
|
|
if previousNewValue ~= nil then
|
|
previousNewKey = inKey
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if previousNewKey ~= nil then
|
|
logError(
|
|
"forPairsKeyCollision",
|
|
nil,
|
|
tostring(storedOutKey),
|
|
tostring(previousNewKey),
|
|
tostring(previousNewValue),
|
|
tostring(newInKey),
|
|
tostring(newInValue)
|
|
)
|
|
end
|
|
end
|
|
|
|
-- copy the stored key/value pair into the new output table
|
|
newOutputTable[storedOutKey] = oldOutputTable[storedOutKey]
|
|
end
|
|
|
|
-- save dependency values and add to main dependency set
|
|
for dependency in pairs(keyData.dependencySet) do
|
|
keyData.dependencyValues[dependency] = peek(dependency)
|
|
|
|
self.dependencySet[dependency] = true
|
|
dependency.dependentSet[self] = true
|
|
end
|
|
end
|
|
|
|
-- STEP 2: find keys that were removed
|
|
for oldOutKey, oldOutValue in pairs(oldOutputTable) do
|
|
-- check if this key/value pair is in the new output table
|
|
if newOutputTable[oldOutKey] ~= oldOutValue then
|
|
-- clean up the old output pair
|
|
local oldMetaValue = meta[oldOutKey]
|
|
if oldOutValue ~= nil then
|
|
local destructOK, err = pcall(
|
|
self._destructor or cleanup,
|
|
oldOutKey,
|
|
oldOutValue,
|
|
oldMetaValue
|
|
)
|
|
if not destructOK then
|
|
logErrorNonFatal("forPairsDestructorError", parseError(err))
|
|
end
|
|
end
|
|
|
|
-- check if the key was completely removed from the output table
|
|
if newOutputTable[oldOutKey] == nil then
|
|
meta[oldOutKey] = nil
|
|
self._keyData[oldOutKey] = nil
|
|
end
|
|
|
|
didChange = true
|
|
end
|
|
end
|
|
|
|
for key in pairs(oldInputTable) do
|
|
if newInputTable[key] == nil then
|
|
oldInputTable[key] = nil
|
|
keyIOMap[key] = nil
|
|
end
|
|
end
|
|
|
|
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 ForPairs<KI, VI, KO, VO, M>(
|
|
inputTable: PubTypes.CanBeState<{ [KI]: VI }>,
|
|
processor: (KI, VI) -> (KO, VO, M?),
|
|
destructor: (KO, VO, M?) -> ()?
|
|
): Types.ForPairs<KI, VI, KO, VO, M>
|
|
local inputIsState = isState(inputTable)
|
|
|
|
local self = setmetatable({
|
|
type = "State",
|
|
kind = "ForPairs",
|
|
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,
|
|
_oldInputTable = {},
|
|
_outputTable = {},
|
|
_oldOutputTable = {},
|
|
_keyIOMap = {},
|
|
_keyData = {},
|
|
_meta = {},
|
|
}, CLASS_METATABLE)
|
|
|
|
self:update()
|
|
|
|
return self
|
|
end
|
|
|
|
return ForPairs
|