forked from rojo-rbx/rojo
Break apart plugin reconciler (#332)
* Start splitting apart reconciler, with tests * Reify children in reify * Baseline hydrate implementation * Remove debug print * Scaffold out diff implementation, just supporting name changes * invariant -> error in decodeValue * Flesh out diff and add getProperty * Clear out top-level reconciler interface, start updating code that touches it * Address review feedback * Add (experimental) Selene configuration * Add emptiness checks to PatchSet, remove unimplement invert method * Improve descendant destruction behavior in InstanceMap * Track instanceId on all reify errors * Base implementation of applyPatch, returning partial patches on failure * Change reify to accept InstanceMap and insert instances into it * Start testing applyPatch for removals * Add test for applyPatch adding instances successfully and not * Add , which is just error with formatting * Correctly use new diff and applyPatch APIs * Improve applyPatch logging and fix field name typo * Better debug output when reify fails * Print out unapplied patch in debug mode * Don't write properties if their values are not different. This was exposed trying to sync the Rojo plugin, which has a gigantic ModuleScript in it with the reflection database. This workaround was present in some form in many versions of Rojo, and I guess we still need it. This time, I actually documented why it's here so that I don't forget for the umpteenth time... * Add placeholder test that needs to happen still * Introduce easier plugin testing, write applyPatch properties test * Delete legacy get/setCanonicalProperty files * Fix trying to remove numbers instead of instances * Change applyPatch to return partial patches instead of binary success * Work towards being able to decode and apply refs * Add helpers for PatchSet assertions * Apply refs in reify, test all cases * Improve diagnostics when patches fail to apply * Stop logging when destroying untracked instances, it's ok * Remove read before setting property in applyPatch * Fix diff thinking all properties are changed
This commit is contained in:
committed by
GitHub
parent
50f0a2bd2e
commit
f66860bdfe
148
plugin/src/Reconciler/diff.lua
Normal file
148
plugin/src/Reconciler/diff.lua
Normal file
@@ -0,0 +1,148 @@
|
||||
--[[
|
||||
Defines the process for diffing a virtual DOM and the real DOM to compute a
|
||||
patch that can be later applied.
|
||||
]]
|
||||
|
||||
local Log = require(script.Parent.Parent.Parent.Log)
|
||||
local invariant = require(script.Parent.Parent.invariant)
|
||||
local getProperty = require(script.Parent.getProperty)
|
||||
local Error = require(script.Parent.Error)
|
||||
local decodeValue = require(script.Parent.decodeValue)
|
||||
|
||||
local function isEmpty(table)
|
||||
return next(table) == nil
|
||||
end
|
||||
|
||||
local function shouldDeleteUnknownInstances(virtualInstance)
|
||||
if virtualInstance.Metadata ~= nil then
|
||||
return not virtualInstance.Metadata.ignoreUnknownInstances
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function diff(instanceMap, virtualInstances, rootId)
|
||||
local patch = {
|
||||
removed = {},
|
||||
added = {},
|
||||
updated = {},
|
||||
}
|
||||
|
||||
-- Add a virtual instance and all of its descendants to the patch, marked as
|
||||
-- being added.
|
||||
local function markIdAdded(id)
|
||||
local virtualInstance = virtualInstances[id]
|
||||
patch.added[id] = virtualInstance
|
||||
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
markIdAdded(childId)
|
||||
end
|
||||
end
|
||||
|
||||
-- Internal recursive kernel for diffing an instance with the given ID.
|
||||
local function diffInternal(id)
|
||||
local virtualInstance = virtualInstances[id]
|
||||
local instance = instanceMap.fromIds[id]
|
||||
|
||||
if virtualInstance == nil then
|
||||
invariant("Cannot diff an instance not present in virtualInstances\nID: {}", id)
|
||||
end
|
||||
|
||||
if instance == nil then
|
||||
invariant("Cannot diff an instance not present in InstanceMap\nID: {}", id)
|
||||
end
|
||||
|
||||
if virtualInstance.ClassName ~= instance.ClassName then
|
||||
error("unimplemented: support changing ClassName")
|
||||
end
|
||||
|
||||
local changedName = nil
|
||||
if virtualInstance.Name ~= instance.Name then
|
||||
changedName = virtualInstance.Name
|
||||
end
|
||||
|
||||
local changedProperties = {}
|
||||
for propertyName, virtualValue in pairs(virtualInstance.Properties) do
|
||||
local ok, existingValueOrErr = getProperty(instance, propertyName)
|
||||
|
||||
if ok then
|
||||
local existingValue = existingValueOrErr
|
||||
local ok, decodedValue = decodeValue(virtualValue, instanceMap)
|
||||
|
||||
if ok then
|
||||
if existingValue ~= decodedValue then
|
||||
changedProperties[propertyName] = virtualValue
|
||||
end
|
||||
else
|
||||
Log.warn("Failed to decode property of type {}", virtualValue.Type)
|
||||
end
|
||||
else
|
||||
local err = existingValueOrErr
|
||||
|
||||
if err.kind == Error.UnknownProperty then
|
||||
Log.trace("Skipping unknown property {}.{}", err.details.className, err.details.propertyName)
|
||||
elseif err.kind == Error.UnreadableProperty then
|
||||
Log.trace("Skipping unreadable property {}.{}", err.details.className, err.details.propertyName)
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if changedName ~= nil or not isEmpty(changedProperties) then
|
||||
table.insert(patch.updated, {
|
||||
id = id,
|
||||
changedName = changedName,
|
||||
changedClassName = nil,
|
||||
changedProperties = changedProperties,
|
||||
changedMetadata = nil,
|
||||
})
|
||||
end
|
||||
|
||||
-- Traverse the list of children in the DOM. Any instance that has no
|
||||
-- corresponding virtual instance should be removed. Any instance that
|
||||
-- does have a corresponding virtual instance is recursively diffed.
|
||||
for _, childInstance in ipairs(instance:GetChildren()) do
|
||||
local childId = instanceMap.fromInstances[childInstance]
|
||||
|
||||
if childId == nil then
|
||||
-- This is an existing instance not present in the virtual DOM.
|
||||
-- We can mark it for deletion unless the user has asked us not
|
||||
-- to delete unknown stuff.
|
||||
if shouldDeleteUnknownInstances(virtualInstance) then
|
||||
table.insert(patch.removed, childInstance)
|
||||
end
|
||||
else
|
||||
local ok, err = diffInternal(childId)
|
||||
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Traverse the list of children in the virtual DOM. Any virtual
|
||||
-- instance that has no corresponding real instance should be created.
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
local childInstance = instanceMap.fromIds[childId]
|
||||
|
||||
if childInstance == nil then
|
||||
-- This instance is present in the virtual DOM, but doesn't
|
||||
-- exist in the real DOM.
|
||||
markIdAdded(childId)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local ok, err = diffInternal(rootId)
|
||||
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
|
||||
return true, patch
|
||||
end
|
||||
|
||||
return diff
|
||||
Reference in New Issue
Block a user