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:
richard 2024-03-25 19:53:28 -07:00 • committed by GitHub
parent 4d4979daa9
commit 33ecae2f50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 577 additions and 729 deletions

View file

@ -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

View file

@ -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,
}

View 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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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> = {

View file

@ -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

View file

@ -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

View file

@ -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

View 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