mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: add sync validation
This commit is contained in:
parent
23dc1ad855
commit
d8d0c2a1f8
5 changed files with 166 additions and 0 deletions
11
src/__tests__/sync/init.spec.luau
Normal file
11
src/__tests__/sync/init.spec.luau
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
return function()
|
||||
local previous = _G.__DEV__
|
||||
|
||||
beforeAll(function()
|
||||
_G.__DEV__ = true
|
||||
end)
|
||||
|
||||
afterAll(function()
|
||||
_G.__DEV__ = previous
|
||||
end)
|
||||
end
|
||||
30
src/__tests__/sync/validate.spec.luau
Normal file
30
src/__tests__/sync/validate.spec.luau
Normal 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
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
local validate = require(script.Parent.validate)
|
||||
|
||||
local NONE = { __none = "__none" }
|
||||
|
||||
local function diff(previousState: { [any]: any }, nextState: { [any]: any })
|
||||
|
|
@ -15,6 +17,15 @@ local function diff(previousState: { [any]: any }, nextState: { [any]: any })
|
|||
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
|
||||
end
|
||||
|
||||
|
|
|
|||
71
src/sync/validate.luau
Normal file
71
src/sync/validate.luau
Normal 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
43
src/test.luau
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue