forked from rojo-rbx/rojo
* 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
148 lines
4.1 KiB
Lua
148 lines
4.1 KiB
Lua
--[[
|
|
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 |