2013/Libraries/Fusion/State/ForPairs.luau

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