2013/Libraries/Fusion/State/Computed.luau

126 lines
3.4 KiB
Plaintext

--!strict
--[[
Constructs and returns objects which can be used to model derived reactive
state.
]]
local Types = require "../Types"
-- Logging
local logError = require "../Logging/logError"
local logErrorNonFatal = require "../Logging/logErrorNonFatal"
local logWarn = require "../Logging/logWarn"
local parseError = require "../Logging/parseError"
-- Utility
local isSimilar = require "../Utility/isSimilar"
local needsDestruction = require "../Utility/needsDestruction"
-- State
local makeUseCallback = require "../State/makeUseCallback"
local class = {}
local CLASS_METATABLE = { __index = class }
local WEAK_KEYS_METATABLE = { __mode = "k" }
--[[
Recalculates this Computed's cached value and dependencies.
Returns true if it changed, or false if it's identical.
]]
function class:update(): boolean
-- remove this object from its dependencies' dependent sets
for dependency in pairs(self.dependencySet) do
dependency.dependentSet[self] = nil
end
-- we need to create a new, empty dependency set to capture dependencies
-- into, but in case there's an error, we want to restore our old set of
-- dependencies. by using this table-swapping solution, we can avoid the
-- overhead of allocating new tables each update.
self._oldDependencySet, self.dependencySet =
self.dependencySet, self._oldDependencySet
-- table.clear(self.dependencySet)
for i, _ in pairs(self.dependencySet) do
self.dependencySet[i] = nil
end
local use = makeUseCallback(self.dependencySet)
-- local ok, newValue, newMetaValue =
-- xpcall(self._processor, parseError, use)
local ok, newValue, newMetaValue = pcall(self._processor, use)
if ok then
if self._destructor == nil and needsDestruction(newValue) then
logWarn "destructorNeededComputed"
end
if newMetaValue ~= nil then
logWarn "multiReturnComputed"
end
local oldValue = self._value
local similar = isSimilar(oldValue, newValue)
if self._destructor ~= nil then
self._destructor(oldValue)
end
self._value = newValue
-- add this object to the dependencies' dependent sets
for dependency in pairs(self.dependencySet) do
dependency.dependentSet[self] = true
end
return not similar
else
-- this needs to be non-fatal, because otherwise it'd disrupt the
-- update process
logErrorNonFatal("computedCallbackError", parseError(newValue))
-- restore old dependencies, because the new dependencies may be corrupt
self._oldDependencySet, self.dependencySet =
self.dependencySet, self._oldDependencySet
-- restore this object in the dependencies' dependent sets
for dependency in pairs(self.dependencySet) do
dependency.dependentSet[self] = true
end
return false
end
end
--[[
Returns the interior value of this state object.
]]
function class:_peek(): any
return self._value
end
function class:get()
logError "stateGetWasRemoved"
end
local function Computed<T>(
processor: () -> T,
destructor: ((T) -> ())?
): Types.Computed<T>
local dependencySet = {}
local self = setmetatable({
type = "State",
kind = "Computed",
dependencySet = 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,
_value = nil,
}, CLASS_METATABLE)
self:update()
return self
end
return Computed