feat: add sync validation

This commit is contained in:
Richard 2024-02-28 21:24:24 -08:00
parent 23dc1ad855
commit d8d0c2a1f8
5 changed files with 166 additions and 0 deletions

View file

@ -0,0 +1,11 @@
return function()
local previous = _G.__DEV__
beforeAll(function()
_G.__DEV__ = true
end)
afterAll(function()
_G.__DEV__ = previous
end)
end

View file

@ -0,0 +1,30 @@
local validate = require(script.Parent.Parent.Parent.sync.validate)
return function()
it("validates inputs", function()
local cases = {
array = { value = { 1, 2, 3 }, expected = true },
array_sparse = { value = { 1, nil :: any, 3 }, expected = true },
array_sparse_invalid = { value = { [1] = 0, [3] = 0 }, expected = false },
map_decimal_key = { value = { [1.5] = 0 }, expected = false },
map_mixed_key = { value = { [1] = 0, a = 0 }, expected = false },
number = { value = 1, expected = true },
number_infinity = { value = math.huge, expected = false },
number_nan = { value = 0 / 0, expected = false },
}
for key, case in pairs(cases) do
local success, result = pcall(function()
validate(case.value, key, error)
end)
if case.expected and not success then
error(`Expected {key} to be valid, threw {result}`)
elseif not case.expected and success then
error(`Expected {key} to be invalid, but it was valid`)
end
end
end)
end

View file

@ -1,3 +1,5 @@
local validate = require(script.Parent.validate)
local NONE = { __none = "__none" } local NONE = { __none = "__none" }
local function diff(previousState: { [any]: any }, nextState: { [any]: any }) local function diff(previousState: { [any]: any }, nextState: { [any]: any })
@ -15,6 +17,15 @@ local function diff(previousState: { [any]: any }, nextState: { [any]: any })
end end
end end
if _G.__DEV__ then
for key, value in previousState do
validate(value, key, warn)
end
for key, value in nextState do
validate(value, key, warn)
end
end
return patch return patch
end end

71
src/sync/validate.luau Normal file
View file

@ -0,0 +1,71 @@
local isAtom = require(script.Parent.Parent.isAtom)
local SAFE_KEYS = { string = true, number = true }
local SAFE_VALUES = { string = true, number = true, boolean = true, table = true }
local function isUnsafeTable(object: { [any]: any })
local keyType = nil
local objectSize = 0
-- All keys must have the same type
for key in object do
local currentType = type(key)
if not keyType and SAFE_KEYS[currentType] then
keyType = currentType
elseif keyType ~= currentType then
return true
end
objectSize += 1
end
-- If there are more keys than the length of the array, it's an array with
-- non-sequential keys.
if objectSize > #object and keyType == "number" then
return true
end
return false
end
local function validate(value: any, key: any, log: (string) -> ())
local typeOfKey = type(key)
local typeOfValue = type(value)
if not SAFE_KEYS[typeOfKey] then
return log(`Invalid key type '{typeOfKey}' at key '{key}'`)
elseif not SAFE_VALUES[typeOfValue] then
return log(`Invalid value type '{typeOfValue}' at key '{key}'`)
elseif typeOfValue == "table" then
if getmetatable(value) ~= nil then
return log(`Cannot sync tables with metatables! Got {value} at key '{key}'`)
elseif isAtom(value) then
return log(`Cannot sync nested atoms! Got atom '{value.debugLabel}' at key '{key}'`)
elseif isUnsafeTable(value) then
return log(
`Cannot sync tables unsupported by remote events! The value has the key '{key}'.\n\n`
.. "This can be for the following reasons:\n"
.. "1. The object is an array with non-sequential keys\n"
.. "2. The object is a dictionary with mixed key types (e.g. string and number)\n\n"
.. "Read more: https://create.roblox.com/docs/scripting/events/remote#argument-limitations"
)
end
end
if typeOfValue == "number" then
if value ~= value then
return log(`Cannot sync NaN at key '{key}'`)
elseif value == math.huge or value == -math.huge then
return log(`Cannot sync infinity at key '{key}'`)
end
elseif typeOfKey == "number" then
if key == math.huge or key == -math.huge then
return log("Cannot sync infinity as key")
elseif math.floor(key) ~= key then
return log("Cannot sync non-integer number as key")
end
end
end
return validate

43
src/test.luau Normal file
View file

@ -0,0 +1,43 @@
local Players = game:GetService("Players")
local Charm = require(script.Parent)
local remotes: any = nil
local todoCount = 3
local todos = Charm.atom({
{ id = 1, text = "Learn about Charm" },
{ id = 2, text = "Build an app" },
{ id = 3, text = "Profit" },
})
local function addTodo(text)
todoCount += 1
todos(function(state)
local newState = table.clone(state)
table.insert(newState, { id = todoCount, text = text })
return newState
end)
end
local client = Charm.sync.client({
atoms = { todos = todos },
})
remotes.init:fire()
remotes.sync:connect(function(payload)
client:sync(payload)
end)
local server = Charm.sync.server({
atoms = { todos = todos },
})
server:onSync(function(player, payload)
print(player.Name, "synced", payload)
end)
Players.PlayerAdded:Connect(function(player)
server:hydrate(player)
end)