mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
- Updated versions of several dependencies in `Cargo.lock`, including `async-compression` (0.4.40), `aws-lc-rs` (1.16.0), `bumpalo` (3.20.1), and `compression-codecs` (0.4.37), ensuring compatibility and access to the latest features and fixes. - Refactored the `fire` method in `SyncbackSignal` to return a boolean indicating whether a syncback was successfully initiated, preventing concurrent syncbacks. - Enhanced error handling in the API syncback process to return a conflict response if a syncback is already in progress. - Improved the `serve.rs` file by replacing `forget` with `drop` for better memory management and added checks to ensure service chunk integrity during live syncback operations. - Added new tests for edge cases and resilience in live syncback functionality, ensuring robustness against malformed data and rapid sequential requests. These changes aim to enhance the reliability and performance of the syncback feature while keeping dependencies up to date.
656 lines
16 KiB
Lua
656 lines
16 KiB
Lua
--!strict
|
|
--!native
|
|
--!optimize 2
|
|
--[[
|
|
Instance matching algorithm for the plugin.
|
|
|
|
Pairs server virtual instances to Studio instances during hydration
|
|
by minimizing total reconciler changes. matchChildren is fundamentally
|
|
computeSubtreeCost -- it finds the cost-minimizing pairing of children
|
|
and returns the total cost. The match assignments are a bonus output.
|
|
|
|
Two-function mutual recursion:
|
|
matchChildren: given two child sets, find optimal pairing + totalCost
|
|
computePairCost: given one virtual + one studio instance, compute
|
|
total cost including subtree (calls matchChildren for children)
|
|
|
|
A MatchingSession holds caches keyed by instance identity so that
|
|
recursive scoring results are reused when hydrate recurses into the
|
|
same children that were already scored.
|
|
]]
|
|
|
|
local Packages = script.Parent.Parent.Parent.Packages
|
|
local RbxDom = require(Packages.RbxDom)
|
|
|
|
local trueEquals = require(script.Parent.trueEquals)
|
|
|
|
local UNMATCHED_PENALTY = 10000
|
|
local MAX_SCORING_DEPTH = 3
|
|
|
|
-- ================================================================
|
|
-- Types
|
|
-- ================================================================
|
|
|
|
type MatchPair = {
|
|
virtualId: string,
|
|
studioInstance: Instance,
|
|
}
|
|
|
|
type MatchResult = {
|
|
matched: { MatchPair },
|
|
unmatchedVirtual: { string },
|
|
unmatchedStudio: { Instance },
|
|
totalCost: number,
|
|
}
|
|
|
|
type MatchingSession = {
|
|
matchCache: { [string]: { [Instance]: MatchResult } },
|
|
costCache: { [string]: { [Instance]: number } },
|
|
vCacheStore: { [string]: VCache },
|
|
}
|
|
|
|
type RefIdentity = {
|
|
name: string,
|
|
className: string,
|
|
}
|
|
|
|
type VCache = {
|
|
props: { [string]: any },
|
|
extraProps: { string },
|
|
tags: { [string]: boolean },
|
|
attrs: { [string]: any },
|
|
refs: { [string]: RefIdentity },
|
|
childCount: number,
|
|
validChildren: { string },
|
|
}
|
|
|
|
type SCache = {
|
|
instance: Instance,
|
|
props: { [string]: any },
|
|
tags: { [string]: boolean },
|
|
attrs: { [string]: any },
|
|
refs: { [string]: RefIdentity },
|
|
children: { Instance },
|
|
childCount: number,
|
|
}
|
|
|
|
type VirtualInstance = {
|
|
Name: string,
|
|
ClassName: string,
|
|
Children: { string },
|
|
Properties: { [string]: any }?,
|
|
}
|
|
|
|
type VirtualInstances = { [string]: VirtualInstance }
|
|
|
|
type ClassComparisonKeys = {
|
|
propNames: { string },
|
|
propNameSet: { [string]: boolean },
|
|
defaults: { [string]: any },
|
|
}
|
|
|
|
type ScoredPair = {
|
|
vi: number,
|
|
si: number,
|
|
cost: number,
|
|
idx: number,
|
|
}
|
|
|
|
-- ================================================================
|
|
-- Session constructor
|
|
-- ================================================================
|
|
|
|
local function newSession(): MatchingSession
|
|
return {
|
|
matchCache = {},
|
|
costCache = {},
|
|
vCacheStore = {},
|
|
}
|
|
end
|
|
|
|
-- ================================================================
|
|
-- Utilities
|
|
-- ================================================================
|
|
|
|
local function rawIndex(inst: any, key: string): any
|
|
return inst[key]
|
|
end
|
|
|
|
local function safeGet(inst: Instance, key: string): (boolean, any)
|
|
return pcall(rawIndex, inst :: any, key)
|
|
end
|
|
|
|
-- ================================================================
|
|
-- Pre-computation helpers (called once per instance per group)
|
|
-- ================================================================
|
|
|
|
local function cacheVirtual(
|
|
vInst: VirtualInstance,
|
|
classKeys: ClassComparisonKeys,
|
|
virtualInstances: VirtualInstances
|
|
): VCache
|
|
local decoded: { [string]: any } = {}
|
|
local extraProps: { string } = {}
|
|
local refs: { [string]: RefIdentity } = {}
|
|
local vProps = vInst.Properties
|
|
|
|
if vProps then
|
|
for propName, encodedValue in pairs(vProps) do
|
|
if propName == "Tags" or propName == "Attributes" then
|
|
continue
|
|
end
|
|
|
|
local ty = next(encodedValue)
|
|
if ty == "Ref" then
|
|
local targetId = encodedValue.Ref
|
|
if targetId then
|
|
local target = virtualInstances[targetId]
|
|
if target then
|
|
refs[propName] = { name = target.Name, className = target.ClassName }
|
|
end
|
|
end
|
|
continue
|
|
end
|
|
|
|
local pcallOk, decodeOk, value = pcall(RbxDom.EncodedValue.decode, encodedValue)
|
|
if pcallOk and decodeOk and value ~= nil then
|
|
decoded[propName] = value
|
|
if not classKeys.propNameSet[propName] then
|
|
table.insert(extraProps, propName)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local decodedTags: { [string]: boolean } = {}
|
|
if vProps and vProps.Tags then
|
|
local ok, tags = RbxDom.EncodedValue.decode(vProps.Tags)
|
|
if ok and type(tags) == "table" then
|
|
for _, tag in ipairs(tags) do
|
|
decodedTags[tag] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local decodedAttrs: { [string]: any } = {}
|
|
if vProps and vProps.Attributes then
|
|
local ok, attrs = RbxDom.EncodedValue.decode(vProps.Attributes)
|
|
if ok and type(attrs) == "table" then
|
|
decodedAttrs = attrs
|
|
end
|
|
end
|
|
|
|
local validChildren: { string } = {}
|
|
if vInst.Children then
|
|
for _, childId in ipairs(vInst.Children) do
|
|
if virtualInstances[childId] then
|
|
table.insert(validChildren, childId)
|
|
end
|
|
end
|
|
end
|
|
|
|
return {
|
|
props = decoded,
|
|
extraProps = extraProps,
|
|
tags = decodedTags,
|
|
attrs = decodedAttrs,
|
|
refs = refs,
|
|
childCount = if vInst.Children then #vInst.Children else 0,
|
|
validChildren = validChildren,
|
|
}
|
|
end
|
|
|
|
local function cacheStudio(
|
|
studioInstance: Instance,
|
|
classKeys: ClassComparisonKeys,
|
|
extraPropNames: { string },
|
|
refPropNames: { string }
|
|
): SCache
|
|
local props: { [string]: any } = {}
|
|
|
|
for _, propName in ipairs(classKeys.propNames) do
|
|
local ok, value = safeGet(studioInstance, propName)
|
|
if ok then
|
|
props[propName] = value
|
|
end
|
|
end
|
|
|
|
for _, propName in ipairs(extraPropNames) do
|
|
if props[propName] ~= nil then
|
|
continue
|
|
end
|
|
local ok, value = safeGet(studioInstance, propName)
|
|
if ok then
|
|
props[propName] = value
|
|
end
|
|
end
|
|
|
|
local refs: { [string]: RefIdentity } = {}
|
|
for _, propName in ipairs(refPropNames) do
|
|
local ok, value = safeGet(studioInstance, propName)
|
|
if ok and typeof(value) == "Instance" then
|
|
refs[propName] = { name = (value :: Instance).Name, className = (value :: Instance).ClassName }
|
|
end
|
|
end
|
|
|
|
local tags: { [string]: boolean } = {}
|
|
for _, tag in ipairs(studioInstance:GetTags()) do
|
|
tags[tag] = true
|
|
end
|
|
|
|
local children = studioInstance:GetChildren()
|
|
|
|
return {
|
|
instance = studioInstance,
|
|
props = props,
|
|
tags = tags,
|
|
attrs = studioInstance:GetAttributes(),
|
|
refs = refs,
|
|
children = children,
|
|
childCount = #children,
|
|
}
|
|
end
|
|
|
|
-- ================================================================
|
|
-- Hot-path scoring (ZERO decode, ZERO reflection lookup)
|
|
-- ================================================================
|
|
|
|
local function countOwnDiffs(vCache: VCache, sCache: SCache, classKeys: ClassComparisonKeys): number
|
|
local cost = 0
|
|
local vProps = vCache.props
|
|
local sProps = sCache.props
|
|
local defaults = classKeys.defaults
|
|
|
|
for _, propName in ipairs(classKeys.propNames) do
|
|
local vVal = vProps[propName]
|
|
if vVal == nil then
|
|
vVal = defaults[propName]
|
|
end
|
|
|
|
local sVal = sProps[propName]
|
|
if vVal ~= sVal and not trueEquals(vVal, sVal) then
|
|
cost += 1
|
|
end
|
|
end
|
|
|
|
for _, propName in ipairs(vCache.extraProps) do
|
|
local vVal = vProps[propName]
|
|
local sVal = sProps[propName]
|
|
if vVal ~= sVal and not trueEquals(vVal, sVal) then
|
|
cost += 1
|
|
end
|
|
end
|
|
|
|
local vTags = vCache.tags
|
|
local sTags = sCache.tags
|
|
for tag, _ in pairs(vTags) do
|
|
if not sTags[tag] then
|
|
cost += 1
|
|
end
|
|
end
|
|
for tag, _ in pairs(sTags) do
|
|
if not vTags[tag] then
|
|
cost += 1
|
|
end
|
|
end
|
|
|
|
local vAttrs = vCache.attrs
|
|
local sAttrs = sCache.attrs
|
|
for key, vVal in pairs(vAttrs) do
|
|
local sVal = sAttrs[key]
|
|
if vVal ~= sVal and not trueEquals(vVal, sVal) then
|
|
cost += 1
|
|
end
|
|
end
|
|
for key, _ in pairs(sAttrs) do
|
|
if vAttrs[key] == nil then
|
|
cost += 1
|
|
end
|
|
end
|
|
|
|
local vRefs = vCache.refs
|
|
local sRefs = sCache.refs
|
|
for propName, vRef in pairs(vRefs) do
|
|
local sRef = sRefs[propName]
|
|
if sRef then
|
|
if vRef.name ~= sRef.name or vRef.className ~= sRef.className then
|
|
cost += UNMATCHED_PENALTY
|
|
end
|
|
else
|
|
cost += UNMATCHED_PENALTY
|
|
end
|
|
end
|
|
for propName, _ in pairs(sRefs) do
|
|
if not vRefs[propName] then
|
|
cost += UNMATCHED_PENALTY
|
|
end
|
|
end
|
|
|
|
if vCache.childCount ~= sCache.childCount then
|
|
cost += 1
|
|
end
|
|
|
|
return cost
|
|
end
|
|
|
|
-- ================================================================
|
|
-- Core: 2-function mutual recursion
|
|
-- ================================================================
|
|
|
|
local matchChildren -- forward declare
|
|
|
|
local function computePairCost(
|
|
session: MatchingSession,
|
|
virtualId: string,
|
|
studioInstance: Instance,
|
|
virtualInstances: VirtualInstances,
|
|
bestSoFar: number,
|
|
depth: number
|
|
): number
|
|
local vc = session.costCache[virtualId]
|
|
if vc then
|
|
local cached = vc[studioInstance]
|
|
if cached ~= nil then
|
|
return cached
|
|
end
|
|
end
|
|
|
|
local vInst = virtualInstances[virtualId]
|
|
if not vInst then
|
|
return UNMATCHED_PENALTY
|
|
end
|
|
|
|
local classKeys = RbxDom.getClassComparisonKeys(vInst.ClassName)
|
|
local vCache = session.vCacheStore[virtualId]
|
|
if not vCache then
|
|
vCache = cacheVirtual(vInst, classKeys, virtualInstances)
|
|
session.vCacheStore[virtualId] = vCache
|
|
end
|
|
local refPropNames: { string } = {}
|
|
for propName, _ in pairs(vCache.refs) do
|
|
table.insert(refPropNames, propName)
|
|
end
|
|
local sCache = cacheStudio(studioInstance, classKeys, vCache.extraProps, refPropNames)
|
|
|
|
local cost = countOwnDiffs(vCache, sCache, classKeys)
|
|
if cost >= bestSoFar or depth >= MAX_SCORING_DEPTH then
|
|
return cost
|
|
end
|
|
|
|
local validVChildren = vCache.validChildren
|
|
local studioKids = sCache.children
|
|
|
|
if #validVChildren > 0 or #studioKids > 0 then
|
|
if #validVChildren == 0 then
|
|
cost += #studioKids * UNMATCHED_PENALTY
|
|
elseif #studioKids == 0 then
|
|
cost += #validVChildren * UNMATCHED_PENALTY
|
|
else
|
|
local childResult = matchChildren(
|
|
session,
|
|
validVChildren,
|
|
studioKids,
|
|
virtualInstances,
|
|
virtualId,
|
|
studioInstance,
|
|
depth + 1
|
|
)
|
|
cost += childResult.totalCost
|
|
end
|
|
end
|
|
|
|
if cost < bestSoFar then
|
|
if not session.costCache[virtualId] then
|
|
session.costCache[virtualId] = {}
|
|
end
|
|
session.costCache[virtualId][studioInstance] = cost
|
|
end
|
|
|
|
return cost
|
|
end
|
|
|
|
matchChildren = function(
|
|
session: MatchingSession,
|
|
virtualChildren: { string },
|
|
studioChildren: { Instance },
|
|
virtualInstances: VirtualInstances,
|
|
parentVirtualId: string?,
|
|
parentStudioInstance: Instance?,
|
|
_depth: number?
|
|
): MatchResult
|
|
local depth = _depth or 0
|
|
if parentVirtualId and parentStudioInstance then
|
|
local pc = session.matchCache[parentVirtualId]
|
|
if pc then
|
|
local cached = pc[parentStudioInstance]
|
|
if cached then
|
|
return cached
|
|
end
|
|
end
|
|
end
|
|
|
|
local matched: { MatchPair } = {}
|
|
local matchedCosts: { number? } = {}
|
|
local matchedV: { [number]: boolean } = {}
|
|
local matchedS: { [number]: boolean } = {}
|
|
|
|
-- ============================================================
|
|
-- Group by (Name, ClassName) once
|
|
-- ============================================================
|
|
local vByKey: { [string]: { number } } = {}
|
|
for i, id in ipairs(virtualChildren) do
|
|
local vInst = virtualInstances[id]
|
|
if vInst then
|
|
local key = vInst.Name .. "\0" .. vInst.ClassName
|
|
local group = vByKey[key]
|
|
if not group then
|
|
group = {}
|
|
vByKey[key] = group
|
|
end
|
|
table.insert(group, i)
|
|
end
|
|
end
|
|
|
|
local sByKey: { [string]: { number } } = {}
|
|
for i, inst in ipairs(studioChildren) do
|
|
local key = inst.Name .. "\0" .. inst.ClassName
|
|
local group = sByKey[key]
|
|
if not group then
|
|
group = {}
|
|
sByKey[key] = group
|
|
end
|
|
table.insert(group, i)
|
|
end
|
|
|
|
-- ============================================================
|
|
-- 1:1 instant match + ambiguous scoring in single pass
|
|
-- ============================================================
|
|
for key, vIndices in pairs(vByKey) do
|
|
local sIndices = sByKey[key]
|
|
if not sIndices then
|
|
continue
|
|
end
|
|
|
|
if #vIndices == 1 and #sIndices == 1 then
|
|
local vi, si = vIndices[1], sIndices[1]
|
|
table.insert(matched, {
|
|
virtualId = virtualChildren[vi],
|
|
studioInstance = studioChildren[si],
|
|
})
|
|
table.insert(matchedCosts, nil)
|
|
matchedV[vi] = true
|
|
matchedS[si] = true
|
|
continue
|
|
end
|
|
|
|
local firstVInst = virtualInstances[virtualChildren[vIndices[1]]]
|
|
if not firstVInst then
|
|
continue
|
|
end
|
|
local classKeys = RbxDom.getClassComparisonKeys(firstVInst.ClassName)
|
|
|
|
local vCaches: { [number]: VCache } = {}
|
|
local allExtraProps: { [string]: boolean } = {}
|
|
local allRefProps: { [string]: boolean } = {}
|
|
for _, vi in ipairs(vIndices) do
|
|
local vid = virtualChildren[vi]
|
|
local vInst = virtualInstances[vid]
|
|
if not vInst then
|
|
continue
|
|
end
|
|
local vCache = session.vCacheStore[vid]
|
|
if not vCache then
|
|
vCache = cacheVirtual(vInst, classKeys, virtualInstances)
|
|
session.vCacheStore[vid] = vCache
|
|
end
|
|
vCaches[vi] = vCache
|
|
for _, propName in ipairs(vCache.extraProps) do
|
|
allExtraProps[propName] = true
|
|
end
|
|
for propName, _ in pairs(vCache.refs) do
|
|
allRefProps[propName] = true
|
|
end
|
|
end
|
|
|
|
local extraPropNamesArray: { string } = {}
|
|
for propName, _ in pairs(allExtraProps) do
|
|
table.insert(extraPropNamesArray, propName)
|
|
end
|
|
|
|
local refPropNamesArray: { string } = {}
|
|
for propName, _ in pairs(allRefProps) do
|
|
table.insert(refPropNamesArray, propName)
|
|
end
|
|
|
|
local sCaches: { [number]: SCache } = {}
|
|
for _, si in ipairs(sIndices) do
|
|
sCaches[si] = cacheStudio(studioChildren[si], classKeys, extraPropNamesArray, refPropNamesArray)
|
|
end
|
|
|
|
local scoredPairs: { ScoredPair } = {}
|
|
local pairIdx = 0
|
|
local bestSoFar = math.huge
|
|
|
|
for _, vi in ipairs(vIndices) do
|
|
local vCache = vCaches[vi]
|
|
if not vCache then
|
|
continue
|
|
end
|
|
|
|
for _, si in ipairs(sIndices) do
|
|
local sCache = sCaches[si]
|
|
if not sCache then
|
|
continue
|
|
end
|
|
|
|
pairIdx += 1
|
|
local cost = countOwnDiffs(vCache, sCache, classKeys)
|
|
|
|
if cost < bestSoFar and depth < MAX_SCORING_DEPTH then
|
|
local validVChildren = vCache.validChildren
|
|
local studioKids = sCache.children
|
|
|
|
if #validVChildren > 0 or #studioKids > 0 then
|
|
if #validVChildren == 0 then
|
|
cost += #studioKids * UNMATCHED_PENALTY
|
|
elseif #studioKids == 0 then
|
|
cost += #validVChildren * UNMATCHED_PENALTY
|
|
else
|
|
local childResult = matchChildren(
|
|
session,
|
|
validVChildren,
|
|
studioKids,
|
|
virtualInstances,
|
|
virtualChildren[vi],
|
|
sCache.instance,
|
|
depth + 1
|
|
)
|
|
cost += childResult.totalCost
|
|
end
|
|
end
|
|
end
|
|
|
|
table.insert(scoredPairs, { vi = vi, si = si, cost = cost, idx = pairIdx })
|
|
if cost < bestSoFar then
|
|
bestSoFar = cost
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(scoredPairs, function(a: ScoredPair, b: ScoredPair): boolean
|
|
if a.cost ~= b.cost then
|
|
return a.cost < b.cost
|
|
end
|
|
return a.idx < b.idx
|
|
end)
|
|
|
|
for _, pair in ipairs(scoredPairs) do
|
|
if matchedV[pair.vi] or matchedS[pair.si] then
|
|
continue
|
|
end
|
|
table.insert(matched, {
|
|
virtualId = virtualChildren[pair.vi],
|
|
studioInstance = studioChildren[pair.si],
|
|
})
|
|
table.insert(matchedCosts, pair.cost)
|
|
matchedV[pair.vi] = true
|
|
matchedS[pair.si] = true
|
|
end
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Build unmatched lists + compute totalCost
|
|
-- ============================================================
|
|
local unmatchedVirtual: { string } = {}
|
|
for i, id in ipairs(virtualChildren) do
|
|
if not matchedV[i] then
|
|
table.insert(unmatchedVirtual, id)
|
|
end
|
|
end
|
|
|
|
local unmatchedStudio: { Instance } = {}
|
|
for i, inst in ipairs(studioChildren) do
|
|
if not matchedS[i] then
|
|
table.insert(unmatchedStudio, inst)
|
|
end
|
|
end
|
|
|
|
local totalCost = 0
|
|
for i, pair in ipairs(matched) do
|
|
local precomputed = matchedCosts[i]
|
|
if precomputed ~= nil then
|
|
totalCost += precomputed
|
|
else
|
|
totalCost += computePairCost(
|
|
session,
|
|
pair.virtualId,
|
|
pair.studioInstance,
|
|
virtualInstances,
|
|
math.huge,
|
|
depth
|
|
)
|
|
end
|
|
end
|
|
totalCost += (#unmatchedVirtual + #unmatchedStudio) * UNMATCHED_PENALTY
|
|
|
|
local result = {
|
|
matched = matched,
|
|
unmatchedVirtual = unmatchedVirtual,
|
|
unmatchedStudio = unmatchedStudio,
|
|
totalCost = totalCost,
|
|
}
|
|
|
|
if parentVirtualId and parentStudioInstance then
|
|
if not session.matchCache[parentVirtualId] then
|
|
session.matchCache[parentVirtualId] = {}
|
|
end
|
|
session.matchCache[parentVirtualId][parentStudioInstance] = result
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
return {
|
|
newSession = newSession,
|
|
matchChildren = matchChildren,
|
|
}
|