mirror of
https://github.com/littensy/charm.git
synced 2026-10-21 11:44:08 +00:00
refactor!: design overhaul (#1)
* refactor!: use atom functions * docs: reformat api reference * docs: showcase atom functions * docs: remove unused import * refactor: finalize updates * chore: misc edits
This commit is contained in:
parent
4d4979daa9
commit
33ecae2f50
39 changed files with 577 additions and 729 deletions
|
|
@ -1,4 +1,5 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
|
||||
return function()
|
||||
it("updates state", function()
|
||||
|
|
@ -14,10 +15,10 @@ return function()
|
|||
it("calls listeners", function()
|
||||
local source = atom(1)
|
||||
local value
|
||||
local function listener(state)
|
||||
value = state
|
||||
local function listener()
|
||||
value = source()
|
||||
end
|
||||
source.listeners[listener] = true
|
||||
store.listeners[source][listener] = true
|
||||
source(2)
|
||||
expect(value).to.equal(2)
|
||||
end)
|
||||
|
|
@ -31,9 +32,9 @@ return function()
|
|||
local function listener()
|
||||
inner += 1
|
||||
end
|
||||
source.listeners[listener] = true
|
||||
store.listeners[source][listener] = true
|
||||
end
|
||||
source.listeners[listener] = true
|
||||
store.listeners[source][listener] = true
|
||||
source(2)
|
||||
expect(outer).to.equal(1)
|
||||
expect(inner).to.equal(0)
|
||||
|
|
@ -42,44 +43,17 @@ return function()
|
|||
expect(inner).to.equal(1)
|
||||
end)
|
||||
|
||||
it("can be readonly", function()
|
||||
local readonly = atom(1):readonly()
|
||||
|
||||
expect(function()
|
||||
(readonly :: any)(2)
|
||||
end).to.throw()
|
||||
|
||||
local writable = readonly:writable()
|
||||
|
||||
writable(2)
|
||||
expect(writable()).to.equal(2)
|
||||
end)
|
||||
|
||||
it("calls the setter", function()
|
||||
local source = atom(1):writable(function(_, value: number)
|
||||
return value + 1
|
||||
end)
|
||||
source(1)
|
||||
expect(source()).to.equal(2)
|
||||
end)
|
||||
|
||||
it("cancels if setter returns nil", function()
|
||||
local source = atom(1):writable(function(value: number)
|
||||
return if value == 2 then nil else value
|
||||
end)
|
||||
source(2)
|
||||
expect(source()).to.equal(1)
|
||||
source(3)
|
||||
expect(source()).to.equal(3)
|
||||
end)
|
||||
|
||||
it("can be memoized", function()
|
||||
local source = atom(1):memo(function(prev, next)
|
||||
return next < prev
|
||||
end)
|
||||
source(2)
|
||||
expect(source()).to.equal(2)
|
||||
source(1)
|
||||
expect(source()).to.equal(2)
|
||||
it("takes a custom comparator", function()
|
||||
local max = atom(1, {
|
||||
equals = function(prev, next)
|
||||
return next < prev
|
||||
end,
|
||||
})
|
||||
max(2)
|
||||
expect(max()).to.equal(2)
|
||||
max(1)
|
||||
expect(max()).to.equal(2)
|
||||
max(3)
|
||||
expect(max()).to.equal(3)
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,48 +6,41 @@ local Package = game:GetService("ReplicatedStorage").Packages.charm
|
|||
local atom = require(Package.atom)
|
||||
local derive = require(Package.derive)
|
||||
|
||||
return {
|
||||
ParameterGenerator = function()
|
||||
return atom({ count = 0 })
|
||||
local function parameterGenerator()
|
||||
return atom({ count = 0 })
|
||||
end
|
||||
|
||||
local benchmarks = {
|
||||
atom = function(_, source: any)
|
||||
for _ = 1, 500 do
|
||||
source({ count = math.random(1, 2) })
|
||||
end
|
||||
end,
|
||||
|
||||
Functions = {
|
||||
atom = function(_, source: any)
|
||||
for _ = 1, 500 do
|
||||
source({ count = math.random(1, 2) })
|
||||
end
|
||||
end,
|
||||
derived = function(_, source: any)
|
||||
derive(function()
|
||||
return { count = 2 * source().count }
|
||||
end)
|
||||
for _ = 1, 500 do
|
||||
source({ count = math.random(1, 2) })
|
||||
end
|
||||
end,
|
||||
|
||||
derived = function(_, source: any)
|
||||
derive(source, function(state)
|
||||
return { count = 2 * state.count }
|
||||
end)
|
||||
for _ = 1, 500 do
|
||||
source({ count = math.random(1, 2) })
|
||||
end
|
||||
end,
|
||||
|
||||
derived_memo = function(_, source: any)
|
||||
source:memo(function(previous, current)
|
||||
derived_memo = function(_, source: any)
|
||||
derive(function()
|
||||
return { count = 2 * source().count }
|
||||
end, {
|
||||
equals = function(previous, current)
|
||||
return previous.count == current.count
|
||||
end)
|
||||
derive(source, function(state)
|
||||
return { count = 2 * state.count }
|
||||
end)
|
||||
for _ = 1, 500 do
|
||||
source({ count = math.random(1, 2) })
|
||||
end
|
||||
end,
|
||||
|
||||
derived_writable = function(_, source: any)
|
||||
local derived = derive(source, function(state)
|
||||
return { count = 2 * state.count }
|
||||
end):writable(function(update)
|
||||
source({ count = 0.5 * update.count })
|
||||
end)
|
||||
for _ = 1, 500 do
|
||||
derived({ count = 2 * math.random(1, 2) })
|
||||
end
|
||||
end,
|
||||
},
|
||||
end,
|
||||
})
|
||||
for _ = 1, 500 do
|
||||
source({ count = math.random(1, 2) })
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
return {
|
||||
ParameterGenerator = parameterGenerator,
|
||||
Functions = benchmarks,
|
||||
}
|
||||
|
|
|
|||
22
src/__tests__/capture.spec.luau
Normal file
22
src/__tests__/capture.spec.luau
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local capture = require(script.Parent.Parent.capture)
|
||||
|
||||
return function()
|
||||
it("captures atoms from a function", function()
|
||||
local a, b, c = atom(1), atom(1), atom(2)
|
||||
local captured, result = capture(function()
|
||||
return a() + b()
|
||||
end)
|
||||
expect(captured[a]).to.be.ok()
|
||||
expect(captured[b]).to.be.ok()
|
||||
expect(captured[c]).to.never.be.ok()
|
||||
expect(result).to.equal(2)
|
||||
end)
|
||||
|
||||
it("captures an atom by itself", function()
|
||||
local a = atom(1)
|
||||
local captured, result = capture(a)
|
||||
expect(captured[a]).to.be.ok()
|
||||
expect(result).to.equal(1)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,64 +1,40 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local derive = require(script.Parent.Parent.derive)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
|
||||
return function()
|
||||
it("derives from dependencies", function()
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
local sum = derive(a, b, function(a: number, b: number)
|
||||
return a + b
|
||||
end)
|
||||
expect(sum()).to.equal(3)
|
||||
a(2)
|
||||
expect(sum()).to.equal(4)
|
||||
b(3)
|
||||
expect(sum()).to.equal(5)
|
||||
expect(function()
|
||||
(sum :: any)(2)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("memoizes the result", function()
|
||||
local calls = 0
|
||||
local source = atom(1)
|
||||
local double = derive(source, function(value: number)
|
||||
local a = atom(1)
|
||||
local b = atom(2)
|
||||
local double = derive(function()
|
||||
calls += 1
|
||||
return value * 2
|
||||
return a() * b()
|
||||
end)
|
||||
expect(calls).to.equal(1)
|
||||
source(1)
|
||||
a(1)
|
||||
expect(calls).to.equal(1)
|
||||
source(2)
|
||||
a(2)
|
||||
expect(calls).to.equal(2)
|
||||
b(3)
|
||||
expect(calls).to.equal(3)
|
||||
double()
|
||||
expect(calls).to.equal(2)
|
||||
expect(calls).to.equal(3)
|
||||
end)
|
||||
|
||||
it("can be a dependency", function()
|
||||
it("can be nested", function()
|
||||
local source = atom(1)
|
||||
local double = derive(source, function(value: number)
|
||||
return value * 2
|
||||
local double = derive(function()
|
||||
return source() * 2
|
||||
end)
|
||||
local quadruple = derive(double, function(value: number)
|
||||
return value * 2
|
||||
local quadruple = derive(function()
|
||||
return double() * 2
|
||||
end)
|
||||
expect(quadruple()).to.equal(4)
|
||||
source(2)
|
||||
expect(quadruple()).to.equal(8)
|
||||
end)
|
||||
|
||||
it("allows custom setter", function()
|
||||
local source = atom(1)
|
||||
local double = derive(source, function(value: number)
|
||||
return value * 2
|
||||
end):writable(function(update: number)
|
||||
source(update / 2)
|
||||
end)
|
||||
double(4)
|
||||
expect(source()).to.equal(2)
|
||||
expect(double()).to.equal(4)
|
||||
end)
|
||||
|
||||
it("garbage collects", function()
|
||||
-- large values coerce the garbage collector to run
|
||||
local source = atom(string.rep("x", 1e5))
|
||||
|
|
@ -66,7 +42,7 @@ return function()
|
|||
|
||||
local function listenersDecreased()
|
||||
local count = 0
|
||||
for _ in source.listeners do
|
||||
for _ in pairs(store.listeners[source]) do
|
||||
count += 1
|
||||
end
|
||||
max = math.max(max, count)
|
||||
|
|
@ -74,11 +50,11 @@ return function()
|
|||
end
|
||||
|
||||
for index = 1, 500 do
|
||||
derive(source, function(value)
|
||||
return string.upper(value)
|
||||
derive(function()
|
||||
return string.upper(source())
|
||||
end)
|
||||
if index % 50 == 0 then
|
||||
-- derived atoms check if they are collected when they are updated
|
||||
-- derived atoms should be collected when memory is stressed
|
||||
source(source() .. "x")
|
||||
task.wait()
|
||||
end
|
||||
|
|
@ -89,14 +65,4 @@ return function()
|
|||
|
||||
error("None of the derived atoms were collected")
|
||||
end)
|
||||
|
||||
it("unmounts from parent atom", function()
|
||||
local source = atom(1)
|
||||
local derived = derive(source, function(value)
|
||||
return value
|
||||
end)
|
||||
expect((next(source.listeners))).to.be.ok()
|
||||
derived:unmount()
|
||||
expect((next(source.listeners))).to.never.be.ok()
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
30
src/__tests__/effect.spec.luau
Normal file
30
src/__tests__/effect.spec.luau
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local effect = require(script.Parent.Parent.effect)
|
||||
|
||||
return function()
|
||||
it("runs on state change", function()
|
||||
local a = atom(0)
|
||||
local b = atom(0)
|
||||
local current
|
||||
local unsubscribe = effect(function()
|
||||
current = a() + b()
|
||||
end)
|
||||
|
||||
expect(current).to.equal(0)
|
||||
|
||||
for index = 1, 3 do
|
||||
a(index)
|
||||
expect(current).to.equal(index)
|
||||
end
|
||||
|
||||
for index = 1, 3 do
|
||||
b(index)
|
||||
expect(current).to.equal(a() + index)
|
||||
end
|
||||
|
||||
unsubscribe()
|
||||
a(4)
|
||||
|
||||
expect(current).to.equal(6)
|
||||
end)
|
||||
end
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
local reference = require(script.Parent.Parent.utils.reference)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
|
||||
return function()
|
||||
local previous = _G.__DEV__
|
||||
|
|
@ -12,6 +12,6 @@ return function()
|
|||
end)
|
||||
|
||||
afterEach(function()
|
||||
reference.clear()
|
||||
table.clear(store.listeners :: any)
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local mapAtom = require(script.Parent.Parent.mapAtom)
|
||||
local map = require(script.Parent.Parent.map)
|
||||
local observe = require(script.Parent.Parent.observe)
|
||||
local store = require(script.Parent.Parent.store)
|
||||
|
||||
return function()
|
||||
it("returns mapped atoms", function()
|
||||
local itemsAtom = atom({ "a", "b", "c" })
|
||||
local mappedAtom = mapAtom(itemsAtom, function(value, key)
|
||||
local mappedAtom = map(itemsAtom, function(value, key)
|
||||
return string.upper(value), key
|
||||
end)
|
||||
local updates = 0
|
||||
|
||||
mappedAtom.listeners[function()
|
||||
store.listeners[mappedAtom][function()
|
||||
updates += 1
|
||||
end] = true
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ return function()
|
|||
|
||||
it("uses key extractor", function()
|
||||
local itemsAtom = atom({ { key = 1 }, { key = 2 }, { key = 3 } })
|
||||
local mappedAtom = mapAtom(itemsAtom, function(item)
|
||||
local mappedAtom = map(itemsAtom, function(item)
|
||||
return item, item.key
|
||||
end)
|
||||
|
||||
|
|
@ -60,14 +61,14 @@ return function()
|
|||
{ key = 3, value = "c" },
|
||||
})
|
||||
|
||||
local mappedAtom = mapAtom(itemsAtom, function(item)
|
||||
local mappedAtom = map(itemsAtom, function(item)
|
||||
return item, item.key
|
||||
end)
|
||||
|
||||
local added = 0
|
||||
local deleted = 0
|
||||
|
||||
local _, flush = observe(mappedAtom, function(item, key)
|
||||
observe(mappedAtom, function(item, key)
|
||||
expect(item).to.equal(itemsAtom()[key])
|
||||
added += 1
|
||||
|
||||
|
|
@ -81,25 +82,13 @@ return function()
|
|||
expect(deleted).to.equal(0)
|
||||
|
||||
itemsAtom({ itemsAtom()[1], itemsAtom()[3] })
|
||||
flush()
|
||||
|
||||
expect(added).to.equal(3)
|
||||
expect(deleted).to.equal(1)
|
||||
|
||||
itemsAtom({})
|
||||
flush()
|
||||
|
||||
expect(added).to.equal(3)
|
||||
expect(deleted).to.equal(3)
|
||||
end)
|
||||
|
||||
it("unmounts from parent atom", function()
|
||||
local itemsAtom = atom({})
|
||||
local mappedAtom = mapAtom(itemsAtom, function(item, key)
|
||||
return item, key
|
||||
end)
|
||||
expect((next(itemsAtom.listeners))).to.be.ok()
|
||||
mappedAtom:unmount()
|
||||
expect((next(itemsAtom.listeners))).to.never.be.ok()
|
||||
end)
|
||||
end
|
||||
|
|
@ -6,7 +6,7 @@ return function()
|
|||
local itemsAtom = atom({ "a", "b", "c" })
|
||||
local state = {}
|
||||
|
||||
local cleanup, flush = observe(itemsAtom, function(item, key)
|
||||
local cleanup = observe(itemsAtom, function(item, key)
|
||||
state[key] = item
|
||||
return function()
|
||||
state[key] = nil
|
||||
|
|
@ -14,14 +14,13 @@ return function()
|
|||
end)
|
||||
|
||||
for key = 1, 3 do
|
||||
expect(state[key]).to.equal(itemsAtom:get()[key])
|
||||
expect(state[key]).to.equal(itemsAtom()[key])
|
||||
end
|
||||
|
||||
itemsAtom:set({ [4] = "d", [5] = "e", [6] = "f" })
|
||||
flush()
|
||||
itemsAtom({ [4] = "d", [5] = "e", [6] = "f" })
|
||||
|
||||
for key = 1, 6 do
|
||||
expect(state[key]).to.equal(itemsAtom:get()[key])
|
||||
expect(state[key]).to.equal(itemsAtom()[key])
|
||||
end
|
||||
|
||||
cleanup()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ _G.__ROACT_17_MOCK_SCHEDULER__ = true
|
|||
|
||||
local Package = script:FindFirstAncestor("__tests__").Parent
|
||||
|
||||
local React = require(Package.modules.React)
|
||||
local React = require(Package.modules.React)()
|
||||
local ReactRoblox = require(Package.modules.ReactRoblox)
|
||||
|
||||
type RenderedHook<Props, Result> = {
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ return function()
|
|||
end)
|
||||
|
||||
it("memoizes the function", function()
|
||||
local hook = renderHook(function(source)
|
||||
return useSetAtom(source)
|
||||
end, atom(0))
|
||||
local hook = renderHook(function(props)
|
||||
return useSetAtom(props.source)
|
||||
end, { source = atom(0) })
|
||||
local fn = hook.result
|
||||
hook.rerender(atom(1))
|
||||
hook.rerender({ source = atom(1) })
|
||||
expect(hook.result).to.never.equal(fn)
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,62 +6,23 @@ return function()
|
|||
it("runs on state change", function()
|
||||
local current, previous
|
||||
local source = atom(0)
|
||||
local unsubscribe, flush = subscribe(source, function(...)
|
||||
local unsubscribe = subscribe(source, function(...)
|
||||
current, previous = ...
|
||||
end)
|
||||
|
||||
for index = 1, 3 do
|
||||
source(index)
|
||||
flush()
|
||||
expect(current).to.equal(index)
|
||||
expect(previous).to.equal(index - 1)
|
||||
end
|
||||
|
||||
unsubscribe()
|
||||
source(4)
|
||||
flush()
|
||||
|
||||
expect(current).to.equal(3)
|
||||
expect(previous).to.equal(2)
|
||||
end)
|
||||
|
||||
it("batches updates", function()
|
||||
local current, previous
|
||||
local source = atom(0)
|
||||
local _, flush = subscribe(source, function(...)
|
||||
current, previous = ...
|
||||
end)
|
||||
|
||||
source(1)
|
||||
source(2)
|
||||
flush()
|
||||
|
||||
expect(current).to.equal(2)
|
||||
expect(previous).to.equal(0)
|
||||
end)
|
||||
|
||||
it("checks equality", function()
|
||||
local current, previous
|
||||
local source = atom(0):memo(function(previous, current)
|
||||
return current < previous
|
||||
end)
|
||||
local _, flush = subscribe(source, function(...)
|
||||
current, previous = ...
|
||||
end)
|
||||
|
||||
source(1)
|
||||
flush()
|
||||
|
||||
expect(current).to.equal(1)
|
||||
expect(previous).to.equal(0)
|
||||
|
||||
source(0)
|
||||
flush()
|
||||
|
||||
expect(current).to.equal(1)
|
||||
expect(previous).to.equal(0)
|
||||
end)
|
||||
|
||||
it("prevents garbage collect", function()
|
||||
local source = atom(1)
|
||||
local updates = 0
|
||||
|
|
@ -70,15 +31,13 @@ return function()
|
|||
-- The idea here is to test whether subscribe() prevents every derived
|
||||
-- atom in a chain from being garbage collected. If any are GCed, then
|
||||
-- the subscription will stop updating and the test will fail.
|
||||
local derived = derive(function()
|
||||
return source()
|
||||
end)
|
||||
subscribe(
|
||||
derive(
|
||||
derive(source, function(x)
|
||||
return x
|
||||
end),
|
||||
function(x)
|
||||
return 2 * x
|
||||
end
|
||||
),
|
||||
derive(function()
|
||||
return derived() * 2
|
||||
end),
|
||||
function()
|
||||
updates += 1
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
local atom = require(script.Parent.Parent.Parent.atom)
|
||||
local sync = require(script.Parent.Parent.Parent.sync)
|
||||
local types = require(script.Parent.Parent.Parent.types)
|
||||
|
||||
return function()
|
||||
local target: Player = {} :: any
|
||||
|
|
@ -7,9 +8,9 @@ return function()
|
|||
it("sends state to new players", function()
|
||||
local a = atom(1)
|
||||
local server = sync.server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: sync.SyncPayload
|
||||
local player: Player, payload: types.SyncPayload
|
||||
|
||||
server:onSync(function(...)
|
||||
local disconnect = server:connect(function(...)
|
||||
player, payload = ...
|
||||
end)
|
||||
|
||||
|
|
@ -18,14 +19,16 @@ return function()
|
|||
expect(player).to.equal(target)
|
||||
expect(payload.type).to.equal("set")
|
||||
expect(payload.data.a).to.equal(1)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
|
||||
it("sends state patches", function()
|
||||
local a = atom({ b = 1, c = 2 })
|
||||
local server = sync.server({ atoms = { a = a }, interval = -1 })
|
||||
local player: Player, payload: sync.SyncPayload
|
||||
local player: Player, payload: types.SyncPayload
|
||||
|
||||
server:onSync(function(...)
|
||||
local disconnect = server:connect(function(...)
|
||||
player, payload = ...
|
||||
end)
|
||||
|
||||
|
|
@ -36,5 +39,7 @@ return function()
|
|||
expect(payload.type).to.equal("patch")
|
||||
expect(payload.data.a.b).to.never.be.ok()
|
||||
expect(payload.data.a.c).to.equal(5)
|
||||
|
||||
disconnect()
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
23
src/__tests__/unwrap.spec.luau
Normal file
23
src/__tests__/unwrap.spec.luau
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
local capture = require(script.Parent.Parent.capture)
|
||||
local unwrap = require(script.Parent.Parent.unwrap)
|
||||
|
||||
return function()
|
||||
it("calls an atom without capturing it", function()
|
||||
local a, b, c = atom(1), atom(1), atom(1)
|
||||
local captured, result = capture(function()
|
||||
return a() + unwrap(b) + c()
|
||||
end)
|
||||
expect(captured[a]).to.be.ok()
|
||||
expect(captured[b]).to.never.be.ok()
|
||||
expect(captured[c]).to.be.ok()
|
||||
expect(result).to.equal(3)
|
||||
end)
|
||||
|
||||
it("passes through arguments", function()
|
||||
expect(unwrap(1)).to.equal(1)
|
||||
expect(unwrap(function()
|
||||
return 1
|
||||
end)).to.equal(1)
|
||||
end)
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue