275 lines
7.4 KiB
Plaintext
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
|