perf: optimize sparse array preservation

This commit is contained in:
Richard 2024-09-26 11:20:27 -07:00
parent 03847bc3f2
commit e32c62753e

View file

@ -12,76 +12,49 @@ end
--[=[ --[=[
JSON serialization can drop the last values of a sparse array. For example, JSON serialization can drop the last values of a sparse array. For example,
`{ [1] = 1, [3] = 3 }` becomes `[1]` in some cases. This loss of data can `{ [1] = 1, [3] = 3 }` becomes `[1]` in some cases. Ambiguity in the
cause hard-to-debug issues down the line, so we need to ensure that all serialization of sparse arrays can be resolved by converting them to
arrays are dense. dictionaries with string keys.
This function coerces the input object into being interpreted as a dense The `apply` function will convert these dictionaries back into arrays as
array, so that the above result would instead be `[1, null, 3]`. long as the target array is not sparse. This function ensures that the diff
function produces a patch that can be correctly applied.
]=] ]=]
local function coerceDenseArray(array: { any }) local function stringifySparseArray(object: { any }): { [string | number]: any }
local maxn = -1 local maxn = table.maxn(object)
for index in next, array do if maxn == 0 or maxn == #object then
if type(index) ~= "number" then -- The object is either not an array or a dense array
return array return object
end
if index > 2 ^ 16 then
-- The object is too large to be considered a dense array
if _G.__DEV__ then
warn(
"Charm encountered an array that may not be synced reliably. This can happen for any of the following reasons:",
"\n1. The array is too large (more than 2^16 elements).",
"\n2. The array has large gaps between elements (e.g. { [1] = foo, [100000] = bar }).",
"\n3. The array is keyed by large numbers (e.g. { [1234] = foo, [5678] = bar }).",
"\nTo resolve this issue, consider using string keys instead of an array.",
`\n\tat index {index} of array`,
array
)
end
return array
end
maxn = math.max(maxn, index)
end end
if maxn == -1 or maxn == #array then local dictionary = {}
-- If the table is already dense, return it as-is
return array for index, value in next, object do
dictionary[tostring(index)] = value
end end
local result = table.create(maxn) return dictionary
table.move(array, 1, maxn, 1, result)
return result
end end
local function diff(prevState: { [any]: any }, nextState: { [any]: any }) local function diff(prevState: { [any]: any }, nextState: { [any]: any })
local patches = table.clone(nextState) local patches = table.clone(nextState)
local numeric = true
for key, previous in next, prevState do for key, previous in next, prevState do
local next = nextState[key] local current = nextState[key]
if numeric and type(key) ~= "number" then if previous == current then
numeric = false
end
if previous == next then
patches[key] = nil patches[key] = nil
elseif next == nil then elseif current == nil then
patches[key] = NONE patches[key] = NONE
elseif type(previous) == "table" and type(next) == "table" then elseif type(previous) == "table" and type(current) == "table" then
patches[key] = diff(previous, next) patches[key] = diff(previous, current)
end end
end end
if numeric then -- Coerce sparse array patches into dictionaries
-- Coerce sparse array patches into being interpreted as dense arrays if prevState[1] ~= nil or nextState[1] ~= nil then
patches = coerceDenseArray(patches) patches = stringifySparseArray(patches)
end end
if _G.__DEV__ then if _G.__DEV__ then