diff --git a/README.md b/README.md index 2f447fc..ee858a4 100644 --- a/README.md +++ b/README.md @@ -47,55 +47,37 @@ Charm = "littensy/charm@VERSION" ## 📚 Documentation -### Core +### `atom(state)` -#### `atom(state)` +### `derive(atom, comparator?)` -#### `derive(...atoms, combiner)` +### `subscribe(atom, listener)` -#### `subscribe(atom, listener)` +### `effect(callback)` -#### `observe(atom, observer)` +### `observe(atom, observer)` -#### `mapAtom(atom, mapper)` +### `mapAtom(atom, mapper)` -### Atom +## 📚 React API -#### `Atom(state?)` +### `useAtom(atom, selector?)` -#### `Atom.get(selector?)` +### `useAtomState(atom)` -#### `Atom.set(state)` +### `useSetAtom(atom)` -#### `Atom.memo(areEqual)` +## 📚 Sync API -#### `Atom.readonly()` +### `sync.client(options)` -#### `Atom.writable(onSet?)` +### `sync.server(options)` -#### `Atom.named(label)` +### `Client.sync(payload)` -#### `Atom.unmount()` +### `Server.onSync(callback)` -### React - -#### `useAtom(atom, selector?)` - -#### `useAtomState(atom)` - -#### `useSetAtom(atom)` - -### Sync - -#### `sync.client(options)` - -#### `Client.sync(payload)` - -#### `sync.server(options)` - -#### `Server.onSync(callback)` - -#### `Server.hydrate(player)` +### `Server.hydrate(player)` ## 🚀 Examples @@ -105,7 +87,7 @@ Charm = "littensy/charm@VERSION" import { atom, derive, subscribe } from "@rbxts/charm"; const counterAtom = atom(0); -const doubleCounterAtom = derive(counterAtom, (counter) => counter * 2); +const doubleCounterAtom = derive(() => counterAtom() * 2); subscribe(doubleCounterAtom, (value) => { print(value); @@ -137,12 +119,6 @@ function Counter() { } ``` -## 🙋 Concerns - -In Jotai, `atom()` returns an [atom config](https://jotai.org/docs/core/atom), and its state is managed entirely by the Provider. However, in Charm, `atom()` returns something closer to a store, and its state is managed by the atom itself. - -Charm does this for simplicity in use cases outside of React, but this might go against Jotai's focus on pure functional programming. More insight is needed to determine if this is a good idea. - ---
diff --git a/src/__tests__/atom.spec.luau b/src/__tests__/atom.spec.luau
index c41fa63..78cfd1b 100644
--- a/src/__tests__/atom.spec.luau
+++ b/src/__tests__/atom.spec.luau
@@ -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)
@@ -41,45 +42,4 @@ return function()
expect(outer).to.equal(2)
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)
- end)
end
diff --git a/src/__tests__/benchmarks/atom.bench.luau b/src/__tests__/benchmarks/atom.bench.luau
index 2bca275..7746117 100644
--- a/src/__tests__/benchmarks/atom.bench.luau
+++ b/src/__tests__/benchmarks/atom.bench.luau
@@ -6,48 +6,39 @@ 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)
- 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,
- },
+ derived_memo = function(_, source: any)
+ derive(function()
+ return { count = 2 * source().count }
+ end, function(previous, current)
+ return previous.count == current.count
+ end)
+ for _ = 1, 500 do
+ source({ count = math.random(1, 2) })
+ end
+ end,
+}
+
+return {
+ ParameterGenerator = parameterGenerator,
+ Functions = benchmarks,
}
diff --git a/src/__tests__/derive.spec.luau b/src/__tests__/derive.spec.luau
index 00b60a2..b7c65f0 100644
--- a/src/__tests__/derive.spec.luau
+++ b/src/__tests__/derive.spec.luau
@@ -1,64 +1,53 @@
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("takes a custom comparator", function()
local source = atom(1)
- local double = derive(source, function(value: number)
- return value * 2
+ local derived = derive(function()
+ return source()
+ end, function(prev, next)
+ return next < prev
end)
- local quadruple = derive(double, function(value: number)
- return value * 2
+ source(2)
+ expect(derived()).to.equal(2)
+ source(1)
+ expect(derived()).to.equal(2)
+ end)
+
+ it("can be nested", function()
+ local source = atom(1)
+ local double = derive(function()
+ return source() * 2
+ end)
+ 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 +55,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 +63,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 +78,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
diff --git a/src/__tests__/effect.spec.luau b/src/__tests__/effect.spec.luau
new file mode 100644
index 0000000..d1b0337
--- /dev/null
+++ b/src/__tests__/effect.spec.luau
@@ -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
diff --git a/src/__tests__/init.spec.luau b/src/__tests__/init.spec.luau
index c37cf45..a7878dc 100644
--- a/src/__tests__/init.spec.luau
+++ b/src/__tests__/init.spec.luau
@@ -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
diff --git a/src/__tests__/mapAtom.spec.luau b/src/__tests__/mapAtom.spec.luau
index 05ec132..4b68b5c 100644
--- a/src/__tests__/mapAtom.spec.luau
+++ b/src/__tests__/mapAtom.spec.luau
@@ -1,6 +1,7 @@
local atom = require(script.Parent.Parent.atom)
local mapAtom = require(script.Parent.Parent.mapAtom)
local observe = require(script.Parent.Parent.observe)
+local store = require(script.Parent.Parent.store)
return function()
it("returns mapped atoms", function()
@@ -10,7 +11,7 @@ return function()
end)
local updates = 0
- mappedAtom.listeners[function()
+ store.listeners[mappedAtom][function()
updates += 1
end] = true
@@ -67,7 +68,7 @@ return function()
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
diff --git a/src/__tests__/observe.spec.luau b/src/__tests__/observe.spec.luau
index 79e8953..a4e6b63 100644
--- a/src/__tests__/observe.spec.luau
+++ b/src/__tests__/observe.spec.luau
@@ -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()
diff --git a/src/__tests__/react/helpers/renderHook.luau b/src/__tests__/react/helpers/renderHook.luau
index 1714a66..020b2ae 100644
--- a/src/__tests__/react/helpers/renderHook.luau
+++ b/src/__tests__/react/helpers/renderHook.luau
@@ -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