refactor: start charm-sync rewrite

This commit is contained in:
littensy 2025-09-29 18:11:39 -07:00
parent 4a07297a13
commit 079abaacc1
18 changed files with 468 additions and 560 deletions

View file

@ -1,19 +1,22 @@
local Charm = require("../../Charm")
local CharmSync = require("../")
local client = require("../client")
local suite = require("../../../../test/suite")
local atom = Charm.atom
local subscribe = Charm.subscribe
local client = CharmSync.client
local test = suite.test
local afterEach = suite.afterEach
afterEach(function()
client.clearAtoms()
end)
test("should sync state payload", function()
local a = atom(1)
local client = client({ atoms = { a = a } })
client:sync({
type = "init",
data = { a = 2 },
client.registerAtoms({ a = a })
client.applyPayloads({
{ type = "init", data = { a = 2 } },
})
assert(a() == 2, "should set a to the initial value")
@ -22,18 +25,16 @@ end)
test("should sync state patch", function()
local a = atom({ b = 1, c = 2 })
local previous = a()
local client = client({ atoms = { a = a } })
client:sync({
type = "init",
data = { a = a() },
client.registerAtoms({ a = a })
client.applyPayloads({
{ type = "init", data = { a = a() } },
})
assert(a() == previous, "should set a to the initial value")
client:sync({
type = "patch",
data = { a = { b = 3 } },
client.applyPayloads({
{ type = "patch", data = { a = { b = 3 } } },
})
assert(a() ~= previous, "should apply patch")
@ -43,19 +44,17 @@ end)
test("should fix sparse array patches", function()
local a = atom({ 1 })
local client = client({ atoms = { a = a } })
client:sync({
type = "init",
data = { a = a() },
client.registerAtoms({ a = a })
client.applyPayloads({
{ type = "init", data = { a = a() } },
})
-- This patch would be the result of a diff between { 1 } and { 1, 2 } that
-- was JSON-serialized and deserialized. The client should be able to catch
-- this and convert the string key back to a number.
client:sync({
type = "patch",
data = { a = { ["2"] = 2 } },
client.applyPayloads({
{ type = "patch", data = { a = { ["2"] = 2 } } },
})
assert(a()["2" :: any] == nil, "should convert serialized arrays to use numbers")
@ -64,24 +63,21 @@ end)
test("should apply multiple patches", function()
local a = atom(1)
local client = client({ atoms = { a = a } })
local calls = {}
client.registerAtoms({ a = a })
subscribe(a, function(value)
table.insert(calls, value)
end)
client:sync({
type = "init",
data = { a = a() },
client.applyPayloads({
{ type = "init", data = { a = a() } },
})
client:sync({
type = "patch",
data = { a = 2 },
}, {
type = "patch",
data = { a = 3 },
client.applyPayloads({
{ type = "patch", data = { a = 2 } },
{ type = "patch", data = { a = 3 } },
})
assert(a() == 3, "should apply the last patch")
@ -91,9 +87,10 @@ end)
test("should batch patches", function()
local a = atom(0)
local b = atom(0)
local client = client({ atoms = { a = a, b = b } })
local states = {}
client.registerAtoms({ a = a, b = b })
subscribe(a, function()
states.b = b()
end)
@ -102,14 +99,12 @@ test("should batch patches", function()
states.a = a()
end)
client:sync({
type = "init",
data = { a = a(), b = b() },
client.applyPayloads({
{ type = "init", data = { a = a(), b = b() } },
})
client:sync({
type = "patch",
data = { a = 1, b = 2 },
client.applyPayloads({
{ type = "patch", data = { a = 1, b = 2 } },
})
assert(a() == 1, "should set a to the new value")

View file

@ -1,9 +1,8 @@
local Charm = require("../../Charm")
local CharmSync = require("../")
local flatten = require("../flatten")
local suite = require("../../../../test/suite")
local atom = Charm.atom
local flatten = CharmSync.flatten
local test = suite.test
test("should flatten atoms", function()

View file

@ -1,15 +1,33 @@
local HttpService = game:GetService("HttpService")
local CharmSync = require("../")
local config = require("../config")
local patch = require("../patch")
local suite = require("../../../../test/suite")
local patch = CharmSync.patch
local test = suite.test
local afterEach = suite.afterEach
local function serdes(value: { [any]: any })
return HttpService:JSONDecode(HttpService:JSONEncode(value))
local function mockArraySerdes(object: { [any]: any }): { [any]: any }
if object[1] then
local copy = {}
for index = 1, #object do
copy[index] = object[index]
end
return copy
elseif table.maxn(object) ~= #object then
local copy = {}
for key, value in object do
copy[tostring(key)] = value
end
return copy
else
return object
end
end
afterEach(function()
config.autoSerialize = true
config.validatePatches = true
end)
test("returns a patch", function()
local prevState = { foo = "bar", baz = "qux" }
local nextState = { foo = "bar", baz = "quux" }
@ -60,7 +78,7 @@ test("preserves array density after serdes", function()
local prevState = { "a", "b", "c", "d", "e" }
local nextState = { "A", "b", "c", "D" }
local patches = serdes(patch.diff(prevState, nextState))
local patches = mockArraySerdes(patch.diff(prevState, nextState))
local applied = patch.apply(prevState, patches)
for index in next, prevState do
@ -72,27 +90,11 @@ test("preserves array density after serdes", function()
end
end)
test("preserves large gaps between keys", function()
local prevState = table.create(10000, "foo")
local nextState = table.move(prevState, 2, 9998, 2, { [1] = "bar", [9999] = "bar" })
local start = os.clock()
local patches = serdes(patch.diff(prevState, nextState))
local applied = patch.apply(prevState, patches)
assert(os.clock() - start < 10e-3, `Performance is not acceptable ({(os.clock() - start) * 1e3}ms)`)
for index in next, prevState do
assert(applied[index] == nextState[index], `expected {applied[index]} to be {nextState[index]}`)
end
end)
test("cannot preserve sparse arrays", function()
local prevState = {}
local nextState = { [2] = "foo" }
local patches = serdes(patch.diff(prevState, nextState))
local patches = mockArraySerdes(patch.diff(prevState, nextState))
local applied = patch.apply(prevState, patches)
-- Not enough information is available to determine whether 'patches'
@ -102,24 +104,30 @@ test("cannot preserve sparse arrays", function()
end)
test("optionally fixes sparse arrays", function()
for index in next, patch.diff({ 1, 1, 1, 1, 1 }, { 2, 1, 1, 2 }, true) do
config.autoSerialize = true
for index in next, patch.diff({ 1, 1, 1, 1, 1 }, { 2, 1, 1, 2 }) do
assert(type(index) == "string", `expected {type(index)} {index} to be string`)
end
for index in next, patch.diff({ 1, 1, 1, 1, 1 }, { 2, 1, 1, 2 }, false) do
config.autoSerialize = false
for index in next, patch.diff({ 1, 1, 1, 1, 1 }, { 2, 1, 1, 2 }) do
assert(type(index) == "number", `expected {type(index)} {index} to be number`)
end
end)
test("optionally validates state", function()
local success = pcall(function()
patch.diff({}, { [0.1] = true }, true)
config.validatePatches = true
patch.diff({}, { [0.1] = true })
end)
assert(not success, "should not allow non-integer keys")
success = pcall(function()
patch.diff({}, { [0.1] = true }, false)
config.validatePatches = false
patch.diff({}, { [0.1] = true })
end)
assert(success, "should skip validation if not requested")

View file

@ -1,99 +0,0 @@
local Charm = require("../../Charm")
local CharmSync = require("../")
local suite = require("../../../../test/suite")
type SyncPayload = CharmSync.SyncPayload
local atom = Charm.atom
local server = CharmSync.server
local patch = CharmSync.patch
local test = suite.test
local target: Player = {} :: any
test("sends state to new players", function()
local a = atom(1)
local server = server({ atoms = { a = a }, interval = -1 })
local player: Player, payload: SyncPayload
local disconnect = server:connect(function(...)
player, payload = ...
end)
server:hydrate(target)
assert(player == target, "should send to the target player")
assert(payload.type == "init", "should be an init payload")
assert(payload.data.a == 1, "should send the initial state")
disconnect()
end)
test("sends state patches", function()
local a = atom({ b = 1, c = 2 } :: { [string]: number? })
local server = server({ atoms = { a = a }, interval = -1 })
local player: Player, payload: SyncPayload
local disconnect = server:connect(function(...)
player, payload = ...
end)
a({ b = 1, c = 5 })
server:_sendPatch(target)
assert(player == target, "should send to the target player")
assert(payload.type == "patch", "should be a patch payload")
assert(payload.data.a.b == nil, "should not send unchanged values")
assert(payload.data.a.c == 5, "should send changed values")
a({ b = 1, d = 1 })
server:_sendPatch(target)
assert(payload.data.a.b == nil, "should not send unchanged values")
assert(patch.isNone(payload.data.a.c) == true, "should send None for removed values")
assert(payload.data.a.d == 1, "should send new values")
disconnect()
end)
test("preserves history", function()
local a = atom(1)
local b = atom(1)
local payloads: { SyncPayload }
local server = server({
atoms = { a = a, b = b },
interval = -1,
preserveHistory = true,
})
local disconnect = server:connect(function(_, ...)
payloads = { ... }
end)
a(2)
b(2)
server:_sendPatch(target)
assert(#payloads == 1, "should only have one payload")
assert(payloads[1].type == "patch", "should be a patch payload")
assert(payloads[1].data.a == 2, "should send the new value of a")
assert(payloads[1].data.b == 2, "should send the new value of b")
a(3)
a(4)
b(3)
server:_sendPatch(target)
assert(#payloads == 2, "should have two payloads")
assert(payloads[1].type == "patch", "1 should be a patch payload")
assert(payloads[1].data.a == 3, "1 should send the new value of a")
assert(payloads[1].data.b == nil, "1 should not send unchanged values")
assert(payloads[2].type == "patch", "2 should be a patch payload")
assert(payloads[2].data.a == 4, "2 should send the new value of a")
assert(payloads[2].data.b == 3, "2 should send the new value of b")
disconnect()
end)
return {}

View file

@ -4,10 +4,6 @@ suite.describe("client", function()
require("./client.spec")
end)
suite.describe("server", function()
require("./server.spec")
end)
suite.describe("flatten", function()
require("./flatten.spec")
end)

View file

@ -1,9 +1,8 @@
local Charm = require("../../Charm")
local CharmSync = require("../")
local suite = require("../../../../test/suite")
local validate = require("../validate")
local atom = Charm.atom
local validate = CharmSync.validate
local test = suite.test
test("validates inputs", function()