mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor!: migrate to monorepo (#23)
* refactor!: create monorepo * chore: prepare release, set up workflows * refactor: rename `modules` to `src` * docs: add docs for all packages * refactor: rename root project file * chore: bump deps * build: rename `lint`, add `pack` script * revert: delete pack script This will not be useful for development.
This commit is contained in:
parent
b038bf1f70
commit
08c7e690d5
81 changed files with 917 additions and 570 deletions
121
tests/charm-sync/client.spec.luau
Normal file
121
tests/charm-sync/client.spec.luau
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local subscribe = Charm.subscribe
|
||||
local CharmSync = require(ReplicatedStorage.DevPackages.CharmSync)
|
||||
local client = CharmSync.client
|
||||
|
||||
return function()
|
||||
it("syncs state payload", function()
|
||||
local a = atom(1)
|
||||
local client = client({ atoms = { a = a } })
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
data = { a = 2 },
|
||||
})
|
||||
|
||||
expect(a()).to.equal(2)
|
||||
end)
|
||||
|
||||
it("syncs 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() },
|
||||
})
|
||||
|
||||
expect(a()).to.equal(previous)
|
||||
|
||||
client:sync({
|
||||
type = "patch",
|
||||
data = { a = { b = 3 } },
|
||||
})
|
||||
|
||||
expect(a()).to.never.equal(previous)
|
||||
expect(a().b).to.equal(3)
|
||||
expect(a().c).to.equal(2)
|
||||
end)
|
||||
|
||||
it("fixes sparse array patches", function()
|
||||
local a = atom({ 1 })
|
||||
local client = client({ atoms = { a = a } })
|
||||
|
||||
client:sync({
|
||||
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 } },
|
||||
})
|
||||
|
||||
expect(a()["2" :: any]).to.never.be.ok()
|
||||
expect(a()[2]).to.equal(2)
|
||||
end)
|
||||
|
||||
it("applies multiple patches", function()
|
||||
local a = atom(1)
|
||||
local client = client({ atoms = { a = a } })
|
||||
local calls = {}
|
||||
|
||||
subscribe(a, function(value)
|
||||
table.insert(calls, value)
|
||||
end)
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
data = { a = a() },
|
||||
})
|
||||
|
||||
client:sync({
|
||||
type = "patch",
|
||||
data = { a = 2 },
|
||||
}, {
|
||||
type = "patch",
|
||||
data = { a = 3 },
|
||||
})
|
||||
|
||||
expect(a()).to.equal(3)
|
||||
expect(calls[1]).to.equal(2)
|
||||
expect(calls[2]).to.equal(3)
|
||||
end)
|
||||
|
||||
it("batches patches", function()
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local client = client({ atoms = { a = a, b = b } })
|
||||
local states = {}
|
||||
|
||||
subscribe(a, function()
|
||||
states.b = b()
|
||||
end)
|
||||
|
||||
subscribe(b, function()
|
||||
states.a = a()
|
||||
end)
|
||||
|
||||
client:sync({
|
||||
type = "init",
|
||||
data = { a = a(), b = b() },
|
||||
})
|
||||
|
||||
client:sync({
|
||||
type = "patch",
|
||||
data = { a = 1, b = 2 },
|
||||
})
|
||||
|
||||
expect(a()).to.equal(1)
|
||||
expect(b()).to.equal(2)
|
||||
expect(states.a).to.equal(1)
|
||||
expect(states.b).to.equal(2)
|
||||
end)
|
||||
end
|
||||
19
tests/charm-sync/init.spec.luau
Normal file
19
tests/charm-sync/init.spec.luau
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local store = require(ReplicatedStorage.DevPackages.Charm.store)
|
||||
|
||||
return function()
|
||||
beforeAll(function()
|
||||
_G.__DEV__ = true
|
||||
end)
|
||||
|
||||
afterAll(function()
|
||||
_G.__DEV__ = nil
|
||||
end)
|
||||
|
||||
afterEach(function()
|
||||
table.clear(store.listeners :: any)
|
||||
table.clear(store.capturing.stack)
|
||||
store.capturing.index = 0
|
||||
end)
|
||||
end
|
||||
101
tests/charm-sync/patch.spec.luau
Normal file
101
tests/charm-sync/patch.spec.luau
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
local HttpService = game:GetService("HttpService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local patch = require(ReplicatedStorage.DevPackages.CharmSync.patch)
|
||||
|
||||
return function()
|
||||
local function serdes(value: { [any]: any })
|
||||
return HttpService:JSONDecode(HttpService:JSONEncode(value))
|
||||
end
|
||||
|
||||
it("returns a patch", function()
|
||||
local prevState = { foo = "bar", baz = "qux" }
|
||||
local nextState = { foo = "bar", baz = "quux" }
|
||||
local patches = patch.diff(prevState, nextState)
|
||||
|
||||
expect(patches).to.be.ok()
|
||||
expect(patches.baz).to.equal("quux")
|
||||
expect(patches.foo).to.never.be.ok()
|
||||
end)
|
||||
|
||||
it("applies a patch", function()
|
||||
local prevState = { foo = "bar", baz = "qux" }
|
||||
local patches = { baz = "quux", qaz = "qux" }
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
expect(applied).to.be.ok()
|
||||
expect(applied.baz).to.equal("quux")
|
||||
expect(applied.foo).to.equal("bar")
|
||||
expect(applied.qaz).to.equal("qux")
|
||||
end)
|
||||
|
||||
it("allows nested patches", function()
|
||||
local prevState = { foo = { bar = "baz" } }
|
||||
local nextState = { foo = { bar = "qux" } }
|
||||
local patches = patch.diff(prevState, nextState)
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
expect(applied.foo.bar).to.equal("qux")
|
||||
end)
|
||||
|
||||
it("preserves array density when applying", function()
|
||||
local prevState = { "a", "b", "c", "d" }
|
||||
local nextState = { ["2"] = "B", ["4"] = "D" }
|
||||
local applied = patch.apply(prevState, nextState)
|
||||
|
||||
expect(applied[1]).to.equal("a")
|
||||
expect(applied[2]).to.equal("B")
|
||||
expect(applied[3]).to.equal("c")
|
||||
expect(applied[4]).to.equal("D")
|
||||
|
||||
expect(applied["2"]).to.never.be.ok()
|
||||
expect(applied["4"]).to.never.be.ok()
|
||||
end)
|
||||
|
||||
it("preserves array density after serdes", function()
|
||||
-- This particular change results in a patch that cannot be serialized
|
||||
-- without losing information. Charm resolves this issue internally.
|
||||
local prevState = { "a", "b", "c", "d", "e" }
|
||||
local nextState = { "A", "b", "c", "D" }
|
||||
|
||||
local patches = serdes(patch.diff(prevState, nextState))
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
for index in next, prevState do
|
||||
expect(applied[index]).to.equal(nextState[index])
|
||||
end
|
||||
|
||||
for index in next, applied do
|
||||
expect(applied[index]).to.equal(nextState[index])
|
||||
end
|
||||
end)
|
||||
|
||||
it("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
|
||||
expect(applied[index]).to.equal(nextState[index])
|
||||
end
|
||||
end)
|
||||
|
||||
it("cannot preserve sparse arrays", function()
|
||||
local prevState = {}
|
||||
local nextState = { [2] = "foo" }
|
||||
|
||||
local patches = serdes(patch.diff(prevState, nextState))
|
||||
local applied = patch.apply(prevState, patches)
|
||||
|
||||
-- Not enough information is available to determine whether 'patches'
|
||||
-- should keep the string keys added during remote event serialization.
|
||||
expect(applied["2"]).to.be.ok()
|
||||
expect(applied[2]).to.never.be.ok()
|
||||
end)
|
||||
end
|
||||
97
tests/charm-sync/server.spec.luau
Normal file
97
tests/charm-sync/server.spec.luau
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local CharmSync = require(ReplicatedStorage.DevPackages.CharmSync)
|
||||
type SyncPayload = CharmSync.SyncPayload
|
||||
local server = CharmSync.server
|
||||
local patch = require(ReplicatedStorage.DevPackages.CharmSync.patch)
|
||||
|
||||
return function()
|
||||
local target: Player = {} :: any
|
||||
|
||||
it("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)
|
||||
|
||||
expect(player).to.equal(target)
|
||||
expect(payload.type).to.equal("init")
|
||||
expect(payload.data.a).to.equal(1)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
|
||||
it("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)
|
||||
|
||||
expect(player).to.equal(target)
|
||||
expect(payload.type).to.equal("patch")
|
||||
expect(payload.data.a.b).to.never.be.ok()
|
||||
expect(payload.data.a.c).to.equal(5)
|
||||
|
||||
a({ b = 1, d = 1 })
|
||||
server:_sendPatch(target)
|
||||
|
||||
expect(payload.data.a.b).to.never.be.ok()
|
||||
expect(payload.data.a.c).to.equal(patch.NONE)
|
||||
expect(payload.data.a.d).to.equal(1)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
|
||||
it("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)
|
||||
|
||||
expect(#payloads).to.equal(1)
|
||||
expect(payloads[1].type).to.equal("patch")
|
||||
expect(payloads[1].data.a).to.equal(2)
|
||||
expect(payloads[1].data.b).to.equal(2)
|
||||
|
||||
a(3)
|
||||
a(4)
|
||||
b(3)
|
||||
server:_sendPatch(target)
|
||||
|
||||
expect(#payloads).to.equal(2)
|
||||
expect(payloads[1].type).to.equal("patch")
|
||||
expect(payloads[1].data.a).to.equal(3)
|
||||
expect(payloads[1].data.b).to.never.be.ok()
|
||||
expect(payloads[2].type).to.equal("patch")
|
||||
expect(payloads[2].data.a).to.equal(4)
|
||||
expect(payloads[2].data.b).to.equal(3)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
end
|
||||
36
tests/charm-sync/validate.spec.luau
Normal file
36
tests/charm-sync/validate.spec.luau
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Charm = require(ReplicatedStorage.DevPackages.Charm)
|
||||
local atom = Charm.atom
|
||||
local validate = require(ReplicatedStorage.DevPackages.CharmSync.validate)
|
||||
|
||||
return function()
|
||||
it("validates inputs", function()
|
||||
local cases: { [string]: { value: any, expected: any } } = {
|
||||
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 },
|
||||
|
||||
nested_atom = { value = atom(0), expected = false },
|
||||
}
|
||||
|
||||
for key, case in next, cases do
|
||||
local success, result = pcall(function()
|
||||
validate(case.value, key)
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue