2013/Libraries/Fusion/State/ForKeys.luau

275 lines
7.4 KiB
Plaintext

--!strict
--[[
Constructs a new ForKeys state object which maps keys of an array using
a `processor` function.
Optionally, a `destructor` function can be specified for cleaning up
calculated keys. If omitted, the default cleanup function will be used instead.
Optionally, a `meta` value can be returned in the processor function as the
second value to pass data from the processor to the destructor.
]]
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
- a dependency used during generation of this value has changed
It will recalculate those key pairs, storing information about any
dependencies used in the processor callback during output key generation,
and save the new key to the output array with the same value. If it is
overwriting an older value, that older value will be passed to the
destructor for cleanup.
Finally, this function will find keys that are no longer present, and remove
their output keys 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 outputTable = self._outputTable
local keyOIMap = self._keyOIMap
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
-- STEP 1: find keys that changed or were not previously present
for newInKey, value 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 key is new
local shouldRecalculate = oldInputTable[newInKey] == nil
-- check if the key'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 key 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, newMetaValue =
-- xpcall(self._processor, parseError, use, newInKey)
local processOK, newOutKey, newMetaValue =
pcall(self._processor, use, newInKey)
if processOK then
if
self._destructor == nil
and (
needsDestruction(newOutKey)
or needsDestruction(newMetaValue)
)
then
logWarn "destructorNeededForKeys"
end
local oldInKey = keyOIMap[newOutKey]
local oldOutKey = keyIOMap[newInKey]
-- check for key collision
if oldInKey ~= newInKey and newInputTable[oldInKey] ~= nil then
logError(
"forKeysKeyCollision",
nil,
tostring(newOutKey),
tostring(oldInKey),
tostring(newOutKey)
)
end
-- check for a changed output key
if
oldOutKey ~= newOutKey
and keyOIMap[oldOutKey] == newInKey
then
-- clean up the old calculated value
local oldMetaValue = meta[oldOutKey]
local destructOK, err = pcall(
self._destructor or cleanup,
oldOutKey,
oldMetaValue
)
if not destructOK then
logErrorNonFatal(
"forKeysDestructorError",
parseError(err)
)
end
keyOIMap[oldOutKey] = nil
outputTable[oldOutKey] = nil
meta[oldOutKey] = nil
end
-- update the stored data for this key
oldInputTable[newInKey] = value
meta[newOutKey] = newMetaValue
keyOIMap[newOutKey] = newInKey
keyIOMap[newInKey] = newOutKey
outputTable[newOutKey] = value
-- 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("forKeysProcessorError", parseError(newOutKey))
end
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 outputKey, inputKey in pairs(keyOIMap) do
if newInputTable[inputKey] == nil then
-- clean up the old calculated value
local oldMetaValue = meta[outputKey]
local destructOK, err =
pcall(self._destructor or cleanup, outputKey, oldMetaValue)
if not destructOK then
logErrorNonFatal("forKeysDestructorError", parseError(err))
end
-- remove data
oldInputTable[inputKey] = nil
meta[outputKey] = nil
keyOIMap[outputKey] = nil
keyIOMap[inputKey] = nil
outputTable[outputKey] = nil
self._keyData[inputKey] = nil
-- if we removed a key, then the table/state changed
didChange = true
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 ForKeys<KI, KO, M>(
inputTable: PubTypes.CanBeState<{ [KI]: any }>,
processor: (KI) -> (KO, M?),
destructor: (KO, M?) -> ()?
): Types.ForKeys<KI, KO, M>
local inputIsState = isState(inputTable)
local self = setmetatable({
type = "State",
kind = "ForKeys",
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 = {},
_keyOIMap = {},
_keyIOMap = {},
_keyData = {},
_meta = {},
}, CLASS_METATABLE)
self:update()
return self
end
return ForKeys