mirror of
https://github.com/littensy/charm.git
synced 2026-10-23 05:46:27 +00:00
refactor!: use atom functions
This commit is contained in:
parent
4d4979daa9
commit
5be8e27296
29 changed files with 362 additions and 574 deletions
58
README.md
58
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.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ local Package = game:GetService("ReplicatedStorage").Packages.charm
|
|||
local atom = require(Package.atom)
|
||||
local derive = require(Package.derive)
|
||||
|
||||
return {
|
||||
ParameterGenerator = function()
|
||||
local function parameterGenerator()
|
||||
return atom({ count = 0 })
|
||||
end,
|
||||
end
|
||||
|
||||
Functions = {
|
||||
local benchmarks = {
|
||||
atom = function(_, source: any)
|
||||
for _ = 1, 500 do
|
||||
source({ count = math.random(1, 2) })
|
||||
|
|
@ -19,8 +18,8 @@ return {
|
|||
end,
|
||||
|
||||
derived = function(_, source: any)
|
||||
derive(source, function(state)
|
||||
return { count = 2 * state.count }
|
||||
derive(function()
|
||||
return { count = 2 * source().count }
|
||||
end)
|
||||
for _ = 1, 500 do
|
||||
source({ count = math.random(1, 2) })
|
||||
|
|
@ -28,26 +27,18 @@ return {
|
|||
end,
|
||||
|
||||
derived_memo = function(_, source: any)
|
||||
source:memo(function(previous, current)
|
||||
derive(function()
|
||||
return { count = 2 * source().count }
|
||||
end, 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,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
ParameterGenerator = parameterGenerator,
|
||||
Functions = benchmarks,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
derive(function()
|
||||
return derived() * 2
|
||||
end),
|
||||
function(x)
|
||||
return 2 * x
|
||||
end
|
||||
),
|
||||
function()
|
||||
updates += 1
|
||||
end
|
||||
|
|
|
|||
127
src/atom.luau
127
src/atom.luau
|
|
@ -1,126 +1,31 @@
|
|||
local reference = require(script.Parent.utils.reference)
|
||||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
|
||||
type AtomInternal<T> = {
|
||||
setForced: (self: AtomInternal<T>, state: T | ((T) -> T)) -> (),
|
||||
listeners: { [(T) -> ()]: true },
|
||||
debugTraceback: string,
|
||||
debugLabel: string,
|
||||
owners: { [any]: true },
|
||||
}
|
||||
|
||||
export type Atom<T> = (() -> T) & ((state: T | ((T) -> T)) -> ()) & {
|
||||
get: ((self: Atom<T>) -> T) & (<Result>(self: Atom<T>, selector: (T) -> Result) -> Result),
|
||||
set: (self: Atom<T>, state: T | ((T) -> T)) -> (),
|
||||
memo: (self: Atom<T>, areEqual: (previous: T, current: T) -> boolean) -> Atom<T>,
|
||||
writable: (self: Atom<T>, onSet: ((update: T, previous: T) -> T?) & (update: T, previous: T) -> ()) -> Atom<T>,
|
||||
readonly: (self: Atom<T>) -> ReadonlyAtom<T>,
|
||||
named: (self: Atom<T>, label: string) -> Atom<T>,
|
||||
unmount: (self: Atom<T>) -> (),
|
||||
} & AtomInternal<T>
|
||||
|
||||
export type ReadonlyAtom<T> = (() -> T) & {
|
||||
get: ((self: ReadonlyAtom<T>) -> T) & (<Result>(self: ReadonlyAtom<T>, selector: (T) -> Result) -> Result),
|
||||
memo: (self: ReadonlyAtom<T>, areEqual: (previous: T, current: T) -> boolean) -> ReadonlyAtom<T>,
|
||||
writable: ((self: Atom<T>) -> Atom<T>)
|
||||
& ((self: Atom<T>, onSet: ((update: T, previous: T) -> T?) & (update: T, previous: T) -> ()) -> Atom<T>),
|
||||
readonly: (self: ReadonlyAtom<T>) -> ReadonlyAtom<T>,
|
||||
named: (self: ReadonlyAtom<T>, label: string) -> ReadonlyAtom<T>,
|
||||
unmount: (self: ReadonlyAtom<T>) -> (),
|
||||
} & AtomInternal<T>
|
||||
|
||||
local atomCount = 0
|
||||
|
||||
local function atom<State>(state: State): Atom<State>
|
||||
atomCount += 1
|
||||
|
||||
local atom = {
|
||||
listeners = {},
|
||||
debugTraceback = debug.traceback(nil, 2),
|
||||
debugLabel = `atom{atomCount}`,
|
||||
owners = {},
|
||||
} :: Atom<State>
|
||||
|
||||
local equals: ((State, State) -> boolean)?
|
||||
local setter: ((State, State) -> State?)?
|
||||
|
||||
function atom:get<Result>(selector: ((State) -> Result)?): any
|
||||
return if selector then selector(state) else state
|
||||
local function atom<T>(state: T): Atom<T>
|
||||
local function atom(...)
|
||||
if select("#", ...) == 0 then
|
||||
if store.shouldCapture() then
|
||||
store.captured[atom] = true
|
||||
end
|
||||
|
||||
function atom:set(update: State | ((State) -> State))
|
||||
local nextState = if type(update) == "function" then update(state) else update
|
||||
|
||||
if setter then
|
||||
local result = setter(nextState, state)
|
||||
|
||||
if result ~= nil then
|
||||
nextState = result
|
||||
else
|
||||
return
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
if not (if equals then equals(state, nextState) else state == nextState) then
|
||||
local nextState = if type(...) == "function" then (...)(state) else ...
|
||||
|
||||
if state ~= nextState then
|
||||
state = nextState
|
||||
|
||||
for listener in table.clone(self.listeners) do
|
||||
listener(state)
|
||||
end
|
||||
for listener in pairs(table.clone(store.listeners[atom])) do
|
||||
listener()
|
||||
end
|
||||
end
|
||||
|
||||
function atom:setForced(update: State | ((State) -> State))
|
||||
local nextState = if type(update) == "function" then update(state) else update
|
||||
|
||||
if not (if equals then equals(state, nextState) else state == nextState) then
|
||||
state = nextState
|
||||
|
||||
for listener in table.clone(self.listeners) do
|
||||
listener(state)
|
||||
end
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
function atom:memo(areEqual: (State, State) -> boolean): Atom<State>
|
||||
equals = areEqual
|
||||
return self
|
||||
end
|
||||
|
||||
function atom:writable(onSet: ((State, State) -> State?)?): Atom<State>
|
||||
setter = onSet
|
||||
return self
|
||||
end
|
||||
|
||||
function atom:readonly(): ReadonlyAtom<State>
|
||||
function setter(_, _)
|
||||
if _G.__DEV__ then
|
||||
error(`Attempt to set read-only atom '{self.debugLabel}' created at: {self.debugTraceback}`, 3)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function atom:named(name: string): Atom<State>
|
||||
self.debugLabel = name
|
||||
return self
|
||||
end
|
||||
|
||||
function atom:unmount()
|
||||
table.clear(self.listeners)
|
||||
table.clear(self.owners)
|
||||
reference.release(self)
|
||||
end
|
||||
|
||||
setmetatable(atom :: AtomInternal<State>, {
|
||||
__type = "Atom",
|
||||
__call = function(self, ...)
|
||||
if select("#", ...) > 0 then
|
||||
return self:set((...))
|
||||
else
|
||||
return self:get()
|
||||
end
|
||||
end,
|
||||
})
|
||||
store.listeners[atom] = setmetatable({}, { __mode = "v" })
|
||||
|
||||
return atom
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,65 +1,32 @@
|
|||
local atom = require(script.Parent.atom)
|
||||
type Atom<T> = atom.Atom<T>
|
||||
type ReadonlyAtom<T> = atom.ReadonlyAtom<T>
|
||||
local reference = require(script.Parent.utils.reference)
|
||||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
|
||||
type Derive =
|
||||
(<A, T>(Atom<A>, (A) -> T) -> ReadonlyAtom<T>)
|
||||
& (<A, B, T>(Atom<A>, Atom<B>, (A, B) -> T) -> ReadonlyAtom<T>)
|
||||
& (<A, B, C, T>(Atom<A>, Atom<B>, Atom<C>, (A, B, C) -> T) -> ReadonlyAtom<T>)
|
||||
& (<A, B, C, D, T>(Atom<A>, Atom<B>, Atom<C>, Atom<D>, (A, B, C, D) -> T) -> ReadonlyAtom<T>)
|
||||
& (<A, B, C, D, E, T>(Atom<A>, Atom<B>, Atom<C>, Atom<D>, Atom<E>, (A, B, C, D, E) -> T) -> ReadonlyAtom<T>)
|
||||
& (<T>(...(Atom<any> | ((...any) -> T))) -> ReadonlyAtom<T>)
|
||||
local function derive<T>(source: ReadonlyAtom<T>, comparator: ((old: T, new: T) -> boolean)?): ReadonlyAtom<T>
|
||||
local atoms, state = store.capture(source)
|
||||
local derived = atom(state)
|
||||
local derivedRef = setmetatable({ current = derived }, { __mode = "v" })
|
||||
|
||||
local function derive<Result>(...): ReadonlyAtom<Result>
|
||||
local atoms: { Atom<unknown> } = { ... }
|
||||
local combiner: (...unknown) -> Result = table.remove(atoms) :: any
|
||||
local size = 0
|
||||
local dependencies: { unknown } = {}
|
||||
local listeners: { [() -> ()]: boolean } = {}
|
||||
|
||||
for index, atom in atoms do
|
||||
dependencies[index] = atom:get()
|
||||
size += 1
|
||||
local function listener()
|
||||
if not derivedRef.current then
|
||||
return
|
||||
end
|
||||
|
||||
local derivedAtom = atom(combiner(table.unpack(dependencies, 1, size))):readonly()
|
||||
local derivedAtomUnmount = derivedAtom.unmount
|
||||
local getDerivedAtom = reference.weak(derivedAtom)
|
||||
local nextState = source()
|
||||
|
||||
function derivedAtom:unmount()
|
||||
derivedAtomUnmount(self)
|
||||
|
||||
for listener in listeners do
|
||||
for _, atom in atoms do
|
||||
atom.listeners[listener] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for index, atom in atoms do
|
||||
local function listener(state: unknown)
|
||||
-- Remove the listener if the derived atom has been garbage collected,
|
||||
-- typically because no subscriptions to it exist.
|
||||
local derivedAtom = getDerivedAtom()
|
||||
|
||||
if derivedAtom then
|
||||
dependencies[index] = state
|
||||
derivedAtom:setForced(combiner(table.unpack(dependencies, 1, size)))
|
||||
else
|
||||
atom.listeners[listener] = nil
|
||||
if state ~= nextState and not (comparator and comparator(state, nextState)) then
|
||||
state = nextState
|
||||
derivedRef.current(nextState)
|
||||
end
|
||||
end
|
||||
|
||||
-- Tie the lifetime of the derived atom to the lifetime of its dependencies
|
||||
-- without creating a strong reference cycle. As long as an atom is in use,
|
||||
-- its derived atoms will be in use as well.
|
||||
derivedAtom.owners[atom] = true
|
||||
atom.listeners[listener] = true
|
||||
listeners[listener] = true
|
||||
for atom in atoms do
|
||||
store.listeners[atom][listener] = derived
|
||||
end
|
||||
|
||||
return derivedAtom
|
||||
return derived
|
||||
end
|
||||
|
||||
return derive :: Derive
|
||||
return derive
|
||||
|
|
|
|||
23
src/effect.luau
Normal file
23
src/effect.luau
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
|
||||
local function effect(source: ReadonlyAtom<nil>): () -> ()
|
||||
local atoms = store.capture(source)
|
||||
|
||||
local function listener()
|
||||
source()
|
||||
end
|
||||
|
||||
for atom in atoms do
|
||||
store.listeners[atom][listener] = true
|
||||
end
|
||||
|
||||
return function()
|
||||
for atom in atoms do
|
||||
store.listeners[atom][listener] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return effect
|
||||
73
src/index.d.ts
vendored
73
src/index.d.ts
vendored
|
|
@ -2,91 +2,74 @@ export = Charm;
|
|||
export as namespace Charm;
|
||||
|
||||
declare namespace Charm {
|
||||
interface InternalAtom<State> {
|
||||
/** @hidden */
|
||||
readonly listeners: Set<(value: State) => void>;
|
||||
/** @hidden */
|
||||
readonly debugTraceback: string;
|
||||
/** @hidden */
|
||||
readonly debugLabel: string;
|
||||
/** @hidden */
|
||||
setForced(state: State | ((current: State) => State)): void;
|
||||
interface Atom<T> extends ReadonlyAtom<T> {
|
||||
readonly __nominal: unique symbol;
|
||||
(state: T | ((state: T) => T)): T;
|
||||
}
|
||||
|
||||
interface BaseAtom<State> extends InternalAtom<State> {
|
||||
get(): State;
|
||||
get<Result>(selector: (state: State) => Result): Result;
|
||||
set(state: State | ((current: State) => State)): void;
|
||||
memo(areEqual: (previous: State, current: State) => boolean): this;
|
||||
writable(onSet?: (update: State, previous: State) => State | void): Atom<State>;
|
||||
readonly(): ReadonlyAtom<State>;
|
||||
named(label: string): this;
|
||||
unmount(): void;
|
||||
interface ReadonlyAtom<T> {
|
||||
(): T;
|
||||
}
|
||||
|
||||
interface Atom<State> extends BaseAtom<State> {
|
||||
(): State;
|
||||
(state: State | ((previous: State) => State)): void;
|
||||
}
|
||||
|
||||
interface ReadonlyAtom<State> extends BaseAtom<State> {
|
||||
(): State;
|
||||
set: never;
|
||||
}
|
||||
|
||||
type AtomArrayToStates<Atoms extends Atom<any>[]> = {
|
||||
[Key in keyof Atoms]: Atoms[Key] extends Atom<infer State> ? State : never;
|
||||
type AtomArrayToStates<Atoms extends ReadonlyAtom<any>[]> = {
|
||||
[Key in keyof Atoms]: Atoms[Key] extends ReadonlyAtom<infer State> ? State : never;
|
||||
};
|
||||
|
||||
function atom<State>(state: State): Atom<State>;
|
||||
|
||||
function isAtom(value: unknown): value is Atom<any>;
|
||||
|
||||
function derive<Atoms extends Atom<any>[], Result>(
|
||||
...args: [...atoms: Atoms, combiner: (...atoms: AtomArrayToStates<Atoms>) => Result]
|
||||
): ReadonlyAtom<Result>;
|
||||
function derive<State>(
|
||||
atom: ReadonlyAtom<State>,
|
||||
comparator?: (previous: State, current: State) => boolean,
|
||||
): ReadonlyAtom<State>;
|
||||
|
||||
function subscribe<State>(atom: Atom<State>, callback: (state: State) => void): () => void;
|
||||
function subscribe<State>(
|
||||
atom: ReadonlyAtom<State>,
|
||||
callback: (state: State, previousState: State) => void,
|
||||
): () => void;
|
||||
|
||||
function effect(callback: () => void): () => void;
|
||||
|
||||
function observe<K, V>(
|
||||
atom: Atom<Map<K, V> | ReadonlyMap<K, V>>,
|
||||
atom: ReadonlyAtom<Map<K, V> | ReadonlyMap<K, V>>,
|
||||
factory: (value: V, key: K) => (() => void) | void,
|
||||
): () => void;
|
||||
|
||||
function observe(atom: Atom<any>, factory: (value: unknown, key: unknown) => (() => void) | void): () => void;
|
||||
function observe(atom: ReadonlyAtom<any>, factory: (value: unknown, key: unknown) => (() => void) | void): () => void;
|
||||
|
||||
function mapAtom<V0, K1, V1>(
|
||||
atom: Atom<readonly V0[]> | Atom<V0[]>,
|
||||
atom: ReadonlyAtom<readonly V0[]> | ReadonlyAtom<V0[]>,
|
||||
mapper: (value: V0, index: number) => LuaTuple<[V1 | undefined, K1]>,
|
||||
): ReadonlyAtom<ReadonlyMap<K1, V1>>;
|
||||
|
||||
function mapAtom<V0, V1>(
|
||||
atom: Atom<readonly V0[]> | Atom<V0[]>,
|
||||
atom: ReadonlyAtom<readonly V0[]> | ReadonlyAtom<V0[]>,
|
||||
mapper: (value: V0, index: number) => V1,
|
||||
): ReadonlyAtom<readonly V1[]>;
|
||||
|
||||
function mapAtom<K0, V0, K1 = K0, V1 = V0>(
|
||||
atom: Atom<ReadonlyMap<K0, V0>> | Atom<Map<K0, V0>>,
|
||||
atom: ReadonlyAtom<ReadonlyMap<K0, V0>> | ReadonlyAtom<Map<K0, V0>>,
|
||||
mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1,
|
||||
): ReadonlyAtom<ReadonlyMap<K1, V1>>;
|
||||
|
||||
function mapAtom<K0 extends string | number | symbol, V0, K1, V1>(
|
||||
atom: Atom<{ readonly [K in K0]: V0 }>,
|
||||
atom: ReadonlyAtom<{ readonly [K in K0]: V0 }>,
|
||||
mapper: (value: V0, key: K0) => LuaTuple<[V1 | undefined, K1]> | V1,
|
||||
): ReadonlyAtom<ReadonlyMap<K1, V1>>;
|
||||
|
||||
function useAtomState<State>(atom: Atom<State>): State;
|
||||
function useAtomState<State>(atom: ReadonlyAtom<State>): State;
|
||||
|
||||
function useAtomState<State, Result>(atom: Atom<State>, selector: (state: State) => Result): Result;
|
||||
function useAtomState<State, Result>(atom: ReadonlyAtom<State>, selector: (state: State) => Result): Result;
|
||||
|
||||
function useSetAtom<State>(atom: Atom<State>): (state: State | ((previous: State) => State)) => void;
|
||||
function useSetAtom<State>(atom: ReadonlyAtom<State>): (state: State | ((previous: State) => State)) => void;
|
||||
|
||||
function useAtom<State>(
|
||||
atom: Atom<State>,
|
||||
atom: ReadonlyAtom<State>,
|
||||
): LuaTuple<[state: State, setState: (state: State | ((previous: State) => State)) => void]>;
|
||||
|
||||
function useAtom<State, Result>(
|
||||
atom: Atom<State>,
|
||||
atom: ReadonlyAtom<State>,
|
||||
selector: (state: State) => Result,
|
||||
): LuaTuple<[state: Result, setState: (state: Result | ((previous: Result) => Result)) => void]>;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ _G[script.modules.Promise] = _G[script]
|
|||
|
||||
local atom = require(script.atom)
|
||||
local derive = require(script.derive)
|
||||
local effect = require(script.effect)
|
||||
local isAtom = require(script.isAtom)
|
||||
local mapAtom = require(script.mapAtom)
|
||||
local observe = require(script.observe)
|
||||
|
|
@ -10,16 +11,18 @@ local useAtomState = require(script.react.useAtomState)
|
|||
local useSetAtom = require(script.react.useSetAtom)
|
||||
local subscribe = require(script.subscribe)
|
||||
local sync = require(script.sync)
|
||||
local types = require(script.types)
|
||||
|
||||
export type Atom<State> = atom.Atom<State>
|
||||
export type ReadonlyAtom<State> = atom.ReadonlyAtom<State>
|
||||
export type Atom<State> = types.Atom<State>
|
||||
export type ReadonlyAtom<State> = types.ReadonlyAtom<State>
|
||||
|
||||
return {
|
||||
atom = atom,
|
||||
derive = derive,
|
||||
effect = effect,
|
||||
isAtom = isAtom,
|
||||
observe = observe,
|
||||
mapAtom = mapAtom,
|
||||
observe = observe,
|
||||
subscribe = subscribe,
|
||||
sync = sync,
|
||||
useAtom = useAtom,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
local store = require(script.Parent.store)
|
||||
|
||||
local function isAtom(value: any)
|
||||
return type(value) == "table" and getmetatable(value) and getmetatable(value).__type == "Atom" or false
|
||||
return type(value) == "function" and store.listeners[value] :: any ~= nil
|
||||
end
|
||||
|
||||
return isAtom
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
local atom = require(script.Parent.atom)
|
||||
type Atom<T> = atom.Atom<T>
|
||||
type ReadonlyAtom<T> = atom.ReadonlyAtom<T>
|
||||
local reference = require(script.Parent.utils.reference)
|
||||
local subscribe = require(script.Parent.subscribe)
|
||||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
|
||||
local function mapAtom<K0, V0, K1, V1>(itemsAtom: Atom<{ [K0]: V0 }>, mapper: (V0, K0) -> (V1?, K1?)): ReadonlyAtom<{ [K1]: V1 }>
|
||||
local mappedAtom: ReadonlyAtom<{ [K1]: V1 }> = atom({}):readonly()
|
||||
local mappedAtomUnmount = mappedAtom.unmount
|
||||
local getMappedAtom = reference.weak(mappedAtom)
|
||||
local mappedAtom = atom({})
|
||||
local mappedAtomRef = setmetatable({ current = mappedAtom }, { __mode = "v" })
|
||||
local previousMappedItems: { [K1]: V1 } = {}
|
||||
local unsubscribe
|
||||
|
||||
local function listener(items)
|
||||
local mappedAtom = getMappedAtom()
|
||||
|
||||
if not mappedAtom then
|
||||
itemsAtom.listeners[listener] = nil
|
||||
return
|
||||
local function listener(items: { [K0]: V0 })
|
||||
if not mappedAtomRef.current then
|
||||
return unsubscribe()
|
||||
end
|
||||
|
||||
local mappedItems = table.clone(mappedAtom:get())
|
||||
local mappedItems = table.clone(mappedAtomRef.current())
|
||||
local mappedKeys = {}
|
||||
|
||||
for key, item in items do
|
||||
|
|
@ -39,16 +37,11 @@ local function mapAtom<K0, V0, K1, V1>(itemsAtom: Atom<{ [K0]: V0 }>, mapper: (V
|
|||
end
|
||||
|
||||
previousMappedItems = mappedItems
|
||||
mappedAtom:setForced(mappedItems)
|
||||
mappedAtom(mappedItems)
|
||||
end
|
||||
|
||||
function mappedAtom:unmount()
|
||||
mappedAtomUnmount(self)
|
||||
itemsAtom.listeners[listener] = nil
|
||||
end
|
||||
|
||||
itemsAtom.listeners[listener] = true
|
||||
listener(itemsAtom:get())
|
||||
unsubscribe = subscribe(itemsAtom, listener)
|
||||
listener(itemsAtom())
|
||||
|
||||
return mappedAtom
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,4 +5,11 @@ local Packages = if Package.Parent:FindFirstChild("React")
|
|||
|
||||
type React = typeof(require(Package.Parent.React))
|
||||
|
||||
return (if Packages then require(Packages.React) else nil) :: React
|
||||
local React: React
|
||||
|
||||
return function()
|
||||
if not React then
|
||||
React = Packages and require(Packages.React) :: React
|
||||
end
|
||||
return React
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
local atom = require(script.Parent.atom)
|
||||
type Atom<T> = atom.Atom<T>
|
||||
local subscribe = require(script.Parent.subscribe)
|
||||
local effect = require(script.Parent.effect)
|
||||
local types = require(script.Parent.types)
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
|
||||
local function observe<Item, Key>(atom: Atom<{ [Key]: Item }>, factory: (value: Item, key: Key) -> () -> ()): (() -> (), () -> ())
|
||||
local connections: { [Key]: () -> () } = {}
|
||||
local function observe<V, K>(source: ReadonlyAtom<{ [K]: V }>, factory: (value: V, key: K) -> () -> ()): () -> ()
|
||||
local connections: { [K]: () -> () } = {}
|
||||
|
||||
local unsubscribe = effect(function()
|
||||
local state = source()
|
||||
|
||||
local function update(state: { [Key]: Item })
|
||||
for key, disconnect in connections do
|
||||
if state[key] == nil then
|
||||
connections[key] = nil
|
||||
|
|
@ -18,9 +20,7 @@ local function observe<Item, Key>(atom: Atom<{ [Key]: Item }>, factory: (value:
|
|||
connections[key] = factory(value, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local unsubscribe, flush = subscribe(atom, update)
|
||||
end)
|
||||
|
||||
local function cleanup()
|
||||
unsubscribe()
|
||||
|
|
@ -29,9 +29,7 @@ local function observe<Item, Key>(atom: Atom<{ [Key]: Item }>, factory: (value:
|
|||
end
|
||||
end
|
||||
|
||||
update(atom:get())
|
||||
|
||||
return cleanup, flush
|
||||
return cleanup
|
||||
end
|
||||
|
||||
return observe
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
type Atom<T> = atom.Atom<T>
|
||||
local types = require(script.Parent.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
|
||||
local useAtomState = require(script.Parent.useAtomState)
|
||||
local useSetAtom = require(script.Parent.useSetAtom)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
local React = require(script.Parent.Parent.modules.React)
|
||||
local requireReact = require(script.Parent.Parent.modules.React)
|
||||
local subscribe = require(script.Parent.Parent.subscribe)
|
||||
|
||||
local atom = require(script.Parent.Parent.atom)
|
||||
type Atom<T> = atom.Atom<T>
|
||||
local types = require(script.Parent.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
|
||||
type UseAtomState =
|
||||
(<State>(atom: Atom<State>) -> State)
|
||||
|
|
@ -9,24 +10,20 @@ type UseAtomState =
|
|||
& (<State, Result>(atom: Atom<State>, selector: (State) -> Result) -> Result)
|
||||
|
||||
local function useAtomState<State>(atom: Atom<State>, selector: ((State) -> State)?): State
|
||||
local React = requireReact()
|
||||
|
||||
local state, setState = React.useState(function()
|
||||
return atom:get(selector :: (State) -> State)
|
||||
return if selector then atom(selector) else atom()
|
||||
end)
|
||||
|
||||
local latestSelector = React.useRef(selector)
|
||||
latestSelector.current = selector
|
||||
|
||||
React.useEffect(function()
|
||||
local function listener(state: State)
|
||||
return subscribe(atom, function(state)
|
||||
local selector = latestSelector.current
|
||||
setState(if selector then selector(state) else state)
|
||||
end
|
||||
|
||||
atom.listeners[listener] = true
|
||||
|
||||
return function()
|
||||
atom.listeners[listener] = nil
|
||||
end
|
||||
end)
|
||||
end, { atom })
|
||||
|
||||
return state
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
local React = require(script.Parent.Parent.modules.React)
|
||||
local requireReact = require(script.Parent.Parent.modules.React)
|
||||
|
||||
local atom = require(script.Parent.Parent.atom)
|
||||
type Atom<T> = atom.Atom<T>
|
||||
local types = require(script.Parent.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
|
||||
export type SetState<T> = (T | ((T) -> T)) -> ()
|
||||
|
||||
local function useSetAtom<State>(atom: Atom<State>): SetState<State>
|
||||
return React.useCallback(function(value)
|
||||
atom:set(value)
|
||||
return requireReact().useCallback(function(value)
|
||||
atom(value)
|
||||
end, { atom })
|
||||
end
|
||||
|
||||
|
|
|
|||
38
src/store.luau
Normal file
38
src/store.luau
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
local types = require(script.Parent.types)
|
||||
type Atom<T> = types.Atom<T>
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
type WeakMap<K, V> = types.WeakMap<K, V>
|
||||
|
||||
local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" })
|
||||
local captured: { [Atom<any>]: true } = {}
|
||||
local capturing = false
|
||||
|
||||
local function capture<T>(source: ReadonlyAtom<T>): ({ [Atom<any>]: true }, T)
|
||||
if listeners[source] then
|
||||
return { [source] = true }, source()
|
||||
end
|
||||
|
||||
capturing = true
|
||||
local success, result = pcall(source)
|
||||
capturing = false
|
||||
|
||||
local dependencies = table.clone(captured)
|
||||
table.clear(captured)
|
||||
|
||||
if not success then
|
||||
warn("An error occurred while calling an atom:", result)
|
||||
end
|
||||
|
||||
return dependencies, result
|
||||
end
|
||||
|
||||
local function shouldCapture()
|
||||
return capturing
|
||||
end
|
||||
|
||||
return {
|
||||
listeners = listeners,
|
||||
captured = captured,
|
||||
capture = capture,
|
||||
shouldCapture = shouldCapture,
|
||||
}
|
||||
|
|
@ -1,45 +1,25 @@
|
|||
local atom = require(script.Parent.atom)
|
||||
type Atom<T> = atom.Atom<T>
|
||||
local reference = require(script.Parent.utils.reference)
|
||||
local store = require(script.Parent.store)
|
||||
local types = require(script.Parent.types)
|
||||
type ReadonlyAtom<T> = types.ReadonlyAtom<T>
|
||||
|
||||
local function subscribe<State>(atom: Atom<State>, callback: (state: State, previous: State) -> ()): (() -> (), () -> ())
|
||||
local pending: thread? = nil
|
||||
local state = atom:get()
|
||||
local function subscribe<T>(source: ReadonlyAtom<T>, callback: (state: T, previous: T) -> ()): () -> ()
|
||||
local atoms, state = store.capture(source)
|
||||
|
||||
local function emit()
|
||||
local function listener()
|
||||
local lastState = state
|
||||
state = atom:get()
|
||||
state = source()
|
||||
callback(state, lastState)
|
||||
end
|
||||
|
||||
local function update()
|
||||
pending = pending or task.defer(function()
|
||||
pending = nil
|
||||
emit()
|
||||
end)
|
||||
for atom in atoms do
|
||||
store.listeners[atom][listener] = true
|
||||
end
|
||||
|
||||
local function flush()
|
||||
if pending then
|
||||
pending = task.cancel(pending)
|
||||
emit()
|
||||
return function()
|
||||
for atom in atoms do
|
||||
store.listeners[atom][listener] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function unsubscribe()
|
||||
reference.release(atom)
|
||||
atom.listeners[update] = nil
|
||||
|
||||
if pending then
|
||||
pending = task.cancel(pending)
|
||||
end
|
||||
end
|
||||
|
||||
-- Ensure the atom is held in memory while the subscription is active
|
||||
reference.hold(atom)
|
||||
atom.listeners[update] = true
|
||||
|
||||
return unsubscribe, flush
|
||||
end
|
||||
|
||||
return subscribe
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
local atom = require(script.Parent.Parent.atom)
|
||||
type Atom<T> = atom.Atom<T>
|
||||
local atomTypes = require(script.Parent.Parent.types)
|
||||
type Atom<T> = atomTypes.Atom<T>
|
||||
local patch = require(script.Parent.patch)
|
||||
local types = require(script.Parent.types)
|
||||
type SyncPayload = types.SyncPayload
|
||||
|
||||
type ClientSyncOptions = {
|
||||
atoms: { [string]: Atom<any> },
|
||||
}
|
||||
|
||||
type ClientSyncer = {
|
||||
sync: (self: ClientSyncer, payload: types.SyncPayload) -> (),
|
||||
sync: (self: ClientSyncer, payload: SyncPayload) -> (),
|
||||
}
|
||||
|
||||
local function client(options: ClientSyncOptions): ClientSyncer
|
||||
|
|
@ -18,9 +19,9 @@ local function client(options: ClientSyncOptions): ClientSyncer
|
|||
function self:sync(payload)
|
||||
for key, state in payload.data do
|
||||
if payload.type == "patch" then
|
||||
atoms[key]:set(patch.apply(atoms[key]:get(), state))
|
||||
atoms[key](patch.apply(atoms[key](), state))
|
||||
else
|
||||
atoms[key]:set(state)
|
||||
atoms[key](state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ local validate = require(script.Parent.validate)
|
|||
local NONE = { __none = "__none" }
|
||||
|
||||
local function diff(previousState: { [any]: any }, nextState: { [any]: any })
|
||||
local patch = table.clone(nextState)
|
||||
local patches = table.clone(nextState)
|
||||
|
||||
for key, previous in previousState do
|
||||
local next = nextState[key]
|
||||
|
||||
if previous == next then
|
||||
patch[key] = nil
|
||||
patches[key] = nil
|
||||
elseif next == nil then
|
||||
patch[key] = NONE
|
||||
patches[key] = NONE
|
||||
elseif type(previous) == "table" and type(next) == "table" then
|
||||
patch[key] = diff(previous, next)
|
||||
patches[key] = diff(previous, next)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -23,8 +23,10 @@ local function diff(previousState: { [any]: any }, nextState: { [any]: any })
|
|||
validate(value, key)
|
||||
end
|
||||
for key, value in nextState do
|
||||
if previousState[key] ~= value then
|
||||
validate(value, key)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if not success then
|
||||
|
|
@ -32,13 +34,13 @@ local function diff(previousState: { [any]: any }, nextState: { [any]: any })
|
|||
end
|
||||
end
|
||||
|
||||
return patch
|
||||
return patches
|
||||
end
|
||||
|
||||
local function apply(state: { [any]: any }, patchState: { [any]: any })
|
||||
local function apply(state: { [any]: any }, patches: { [any]: any })
|
||||
local nextState = table.clone(state)
|
||||
|
||||
for key, patch in patchState do
|
||||
for key, patch in patches do
|
||||
local current = nextState[key]
|
||||
|
||||
if type(patch) == "table" and patch.__none == "__none" then
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
local Players = game:GetService("Players")
|
||||
|
||||
local atom = require(script.Parent.Parent.atom)
|
||||
type Atom<T> = atom.Atom<T>
|
||||
local atomTypes = require(script.Parent.Parent.types)
|
||||
type Atom<T> = atomTypes.Atom<T>
|
||||
local setInterval = require(script.Parent.Parent.utils.setInterval)
|
||||
local patch = require(script.Parent.patch)
|
||||
local types = require(script.Parent.types)
|
||||
type SyncPayload = types.SyncPayload
|
||||
|
||||
type ServerSyncOptions = {
|
||||
atoms: { [string]: Atom<any> },
|
||||
|
|
@ -12,7 +13,7 @@ type ServerSyncOptions = {
|
|||
}
|
||||
|
||||
type ServerSyncer = {
|
||||
onSync: (self: ServerSyncer, callback: (player: Player, payload: types.SyncPayload) -> ()) -> () -> (),
|
||||
onSync: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (),
|
||||
flush: (self: ServerSyncer, player: Player?) -> (),
|
||||
hydrate: (self: ServerSyncer, player: Player) -> (),
|
||||
}
|
||||
|
|
@ -21,13 +22,13 @@ local function server(options: ServerSyncOptions): ServerSyncer
|
|||
local atoms = options.atoms
|
||||
local interval = options.interval or 0
|
||||
local self = {} :: ServerSyncer
|
||||
local sync: (player: Player, payload: types.SyncPayload) -> ()
|
||||
local sync: (player: Player, payload: SyncPayload) -> ()
|
||||
local state: {}
|
||||
|
||||
local function getState()
|
||||
local state = {}
|
||||
for key, atom in atoms do
|
||||
state[key] = atom:get()
|
||||
state[key] = atom()
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
|
@ -47,7 +48,7 @@ local function server(options: ServerSyncOptions): ServerSyncer
|
|||
state = current
|
||||
|
||||
if current ~= previous then
|
||||
local payload: types.SyncPayload = {
|
||||
local payload: SyncPayload = {
|
||||
type = "patch",
|
||||
data = patch.diff(previous, current),
|
||||
}
|
||||
|
|
|
|||
5
src/types.luau
Normal file
5
src/types.luau
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export type Atom<T> = (() -> T) & ((T | (T) -> T) -> T)
|
||||
export type ReadonlyAtom<T> = () -> T
|
||||
export type WeakMap<K, V> = typeof(setmetatable({} :: { [K]: V }, { __mode = "k" }))
|
||||
|
||||
return nil
|
||||
Loading…
Add table
Add a link
Reference in a new issue