mirror of
https://github.com/littensy/charm.git
synced 2026-10-21 11:44:08 +00:00
refactor: start charm-sync rewrite
This commit is contained in:
parent
4a07297a13
commit
079abaacc1
18 changed files with 468 additions and 560 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue