mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: improve logic
This commit is contained in:
parent
210e948225
commit
72f936591e
48 changed files with 434 additions and 349 deletions
|
|
@ -2,20 +2,22 @@ local ServerScriptService = game:GetService("ServerScriptService")
|
|||
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
|
||||
local describe = suite.describe
|
||||
|
||||
local function spec(name: string)
|
||||
describe(name, function()
|
||||
return require(`./{name}.spec`) :: any
|
||||
suite.describe("charm-sync", function()
|
||||
suite.describe("client", function()
|
||||
require("./client.spec")
|
||||
end)
|
||||
suite.describe("server", function()
|
||||
require("./server.spec")
|
||||
end)
|
||||
suite.describe("flatten", function()
|
||||
require("./flatten.spec")
|
||||
end)
|
||||
suite.describe("patch", function()
|
||||
require("./patch.spec")
|
||||
end)
|
||||
suite.describe("validate", function()
|
||||
require("./validate.spec")
|
||||
end)
|
||||
end
|
||||
|
||||
describe("charm-sync", function()
|
||||
spec("client")
|
||||
spec("server")
|
||||
spec("flatten")
|
||||
spec("patch")
|
||||
spec("validate")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -2,51 +2,46 @@ local signals = require("./signals")
|
|||
local types = require("./types")
|
||||
|
||||
type Atom<T> = types.Atom<T>
|
||||
type Getter<T> = types.Getter<T>
|
||||
type Setter<T> = types.Setter<T>
|
||||
type Update<T> = types.Update<T>
|
||||
|
||||
local signal = signals.signal
|
||||
|
||||
local function atomGetterSetterEquals<T>(
|
||||
source: () -> T,
|
||||
setSource: (((T) -> T) | T) -> T,
|
||||
equals: (T, T) -> boolean
|
||||
): Atom<T>
|
||||
return function(...: ((T) -> T) | T): T
|
||||
type Equals<T> = (current: T, incoming: T) -> boolean
|
||||
|
||||
local function createAtomWithEqualityCheck<T>(getter: Getter<T>, setter: Setter<T>, equals: Equals<T>): Atom<T>
|
||||
local function atom(...: Update<T>): T
|
||||
if select("#", ...) == 0 then
|
||||
return source()
|
||||
return getter()
|
||||
end
|
||||
|
||||
local nextState = (...)
|
||||
|
||||
if type(nextState) == "function" then
|
||||
return setSource(function(currentState): T
|
||||
local nextState = nextState(currentState)
|
||||
return if equals(currentState, nextState) then currentState else nextState
|
||||
end)
|
||||
else
|
||||
return setSource(function(currentState): T
|
||||
return if equals(currentState, nextState) then currentState else nextState
|
||||
end)
|
||||
end
|
||||
return setter(function(currentState): T
|
||||
local nextState = if type(nextState) == "function" then nextState(currentState) else nextState
|
||||
return if equals(currentState, nextState) then currentState else nextState
|
||||
end)
|
||||
end
|
||||
|
||||
return atom
|
||||
end
|
||||
|
||||
local function atomGetterSetter<T>(source: () -> T, setSource: (((T) -> T) | T) -> T): Atom<T>
|
||||
return function(...: ((T) -> T) | T): T
|
||||
if select("#", ...) == 0 then
|
||||
return source()
|
||||
else
|
||||
return setSource((...))
|
||||
end
|
||||
local function createAtom<T>(getter: Getter<T>, setter: Setter<T>): Atom<T>
|
||||
local function atom(...: Update<T>): T
|
||||
return if select("#", ...) == 0 then getter() else setter((...))
|
||||
end
|
||||
|
||||
return atom
|
||||
end
|
||||
|
||||
local function atom<T>(state: T, equals: ((previous: T, current: T) -> boolean)?): Atom<T>
|
||||
local source, setSource = signal(state)
|
||||
local function atom<T>(state: T, equals: Equals<T>?): Atom<T>
|
||||
local getter, setter = signal(state)
|
||||
|
||||
if equals then
|
||||
return atomGetterSetterEquals(source, setSource, equals)
|
||||
if equals == nil then
|
||||
return createAtom(getter, setter)
|
||||
else
|
||||
return atomGetterSetter(source, setSource)
|
||||
return createAtomWithEqualityCheck(getter, setter, equals)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@ local signals = require("./signals")
|
|||
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
|
||||
type Cleanup = () -> ()
|
||||
|
||||
local function effect(callback: (dispose: () -> ()) -> ...Cleanup?): () -> ()
|
||||
local disposeImmediately = false
|
||||
local function effect(callback: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
|
||||
local disposed = false
|
||||
|
||||
local function dispose()
|
||||
disposeImmediately = true
|
||||
disposed = true
|
||||
end
|
||||
|
||||
dispose = signals.effect(function()
|
||||
|
|
@ -18,7 +16,7 @@ local function effect(callback: (dispose: () -> ()) -> ...Cleanup?): () -> ()
|
|||
end
|
||||
end)
|
||||
|
||||
if disposeImmediately then
|
||||
if disposed then
|
||||
dispose()
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ local signals = require("./signals")
|
|||
|
||||
local onScopeDispose = signals.onScopeDispose
|
||||
|
||||
type Cleanup = () -> ()
|
||||
|
||||
local function effectScope(callback: () -> ...Cleanup?, detached: boolean?): () -> ()
|
||||
local function effectScope(callback: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
||||
return signals.effectScope(function()
|
||||
local cleanup = callback()
|
||||
if cleanup then
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
local lib = require(script.lib)
|
||||
|
||||
export type Atom<T = any> = lib.Atom<T>
|
||||
export type Getter<T = any> = lib.Getter<T>
|
||||
export type Selector<T = any> = lib.Selector<T>
|
||||
export type Setter<T = any> = lib.Setter<T>
|
||||
export type Update<T = any> = lib.Update<T>
|
||||
|
||||
return lib
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ local types = require("./types")
|
|||
local untracked = require("./untracked")
|
||||
|
||||
export type Atom<T = any> = types.Atom<T>
|
||||
export type Getter<T = any> = types.Getter<T>
|
||||
export type Selector<T = any> = types.Selector<T>
|
||||
export type Setter<T = any> = types.Setter<T>
|
||||
export type Update<T = any> = types.Update<T>
|
||||
|
||||
local computed = signals.computed
|
||||
local effectScope = signals.effectScope
|
||||
|
|
|
|||
|
|
@ -1,32 +1,27 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
|
||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||
local disposeImmediately = false
|
||||
local tracked = computed(function()
|
||||
return selector()
|
||||
end)
|
||||
|
||||
local current: T
|
||||
local disposed = false
|
||||
|
||||
local function dispose()
|
||||
disposeImmediately = true
|
||||
disposed = true
|
||||
end
|
||||
|
||||
dispose = effect(function()
|
||||
local previous: T? = current
|
||||
current = selector()
|
||||
|
||||
if current ~= previous then
|
||||
local currentSub = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(listener, current, previous, dispose)
|
||||
setCurrentSub(currentSub)
|
||||
|
||||
if not success then
|
||||
error(`listen() encountered an error: {result}`)
|
||||
end
|
||||
end
|
||||
local previous = current
|
||||
current = tracked()
|
||||
listener(current, previous, dispose)
|
||||
end)
|
||||
|
||||
if disposeImmediately then
|
||||
if disposed then
|
||||
dispose()
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
local noYield = require("./noYield")
|
||||
local signals = require("./signals")
|
||||
|
||||
local computed = signals.computed
|
||||
|
|
@ -17,14 +18,18 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
|
|||
|
||||
local function map(value: VI, key: KI): (VO, KO)
|
||||
local currentSub = setCurrentSub(nil)
|
||||
local success, valueOut, keyOut = pcall(mapper, value, key)
|
||||
local success, valueOut, keyOut = noYield(mapper, value, key)
|
||||
setCurrentSub(currentSub)
|
||||
|
||||
if not success then
|
||||
error(`mapped() transform function failed: {valueOut}`)
|
||||
end
|
||||
|
||||
return valueOut, if keyOut ~= nil then keyOut else key :: any
|
||||
if keyOut == nil then
|
||||
keyOut = key :: any
|
||||
end
|
||||
|
||||
return valueOut, keyOut
|
||||
end
|
||||
|
||||
return computed(function(previousOutput)
|
||||
|
|
@ -53,9 +58,11 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
|
|||
|
||||
local valueOut, keyOut = map(value, key)
|
||||
|
||||
currentOutput = ensureCanMutateOutput(currentOutput, previousOutput)
|
||||
currentOutput[keyOut] = valueOut
|
||||
keyCache[key] = keyOut
|
||||
if currentOutput[keyOut] ~= valueOut then
|
||||
currentOutput = ensureCanMutateOutput(currentOutput, previousOutput)
|
||||
currentOutput[keyOut] = valueOut
|
||||
keyCache[key] = keyOut
|
||||
end
|
||||
end
|
||||
|
||||
return currentOutput
|
||||
|
|
|
|||
25
pkgs/charm/src/noYield.luau
Normal file
25
pkgs/charm/src/noYield.luau
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
local function handleNoYield(thread: thread, ok: boolean, ...)
|
||||
if not ok then
|
||||
local err = (...)
|
||||
if typeof(err) == "string" then
|
||||
return false, debug.traceback(thread, err)
|
||||
else
|
||||
return false, tostring(err)
|
||||
end
|
||||
end
|
||||
|
||||
if coroutine.status(thread) ~= "dead" then
|
||||
return false, debug.traceback(thread, "Attempted to yield!")
|
||||
end
|
||||
|
||||
return true, ...
|
||||
end
|
||||
|
||||
local function noYield<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...)
|
||||
local thread = coroutine.create(callback)
|
||||
return handleNoYield(thread, coroutine.resume(thread, ...))
|
||||
end
|
||||
|
||||
-- To eliminate the overhead of noYield returning callback(...) in debug mode,
|
||||
-- replace it with pcall to avoid wrapping the callback in another function.
|
||||
return if _G.__DEV__ then noYield else pcall :: never
|
||||
|
|
@ -2,23 +2,19 @@ local signals = require("./signals")
|
|||
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local getCurrentSub = signals.getCurrentSub
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local onScopeDispose = signals.onScopeDispose
|
||||
local getCurrentSub = signals.getCurrentSub
|
||||
|
||||
type Cleanup = () -> ()
|
||||
|
||||
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...Cleanup?): () -> ()
|
||||
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
||||
local scopes: { [K]: () -> () } = {}
|
||||
local currentItems: { [K]: V } = {}
|
||||
|
||||
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
|
||||
for key in previousItems do
|
||||
if currentItems[key] == nil then
|
||||
local dispose = scopes[key]
|
||||
if currentItems[key] == nil and scopes[key] then
|
||||
scopes[key]()
|
||||
scopes[key] = nil
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -46,13 +42,7 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
|
|||
currentItems = selector()
|
||||
|
||||
if currentItems ~= previousItems then
|
||||
local currentSub = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(updateScopes, currentItems, previousItems)
|
||||
setCurrentSub(currentSub)
|
||||
|
||||
if not success then
|
||||
error(`observe() encountered an error: {result}`, 2)
|
||||
end
|
||||
updateScopes(currentItems, previousItems)
|
||||
end
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
--[=[
|
||||
--[[
|
||||
alien-signals 2.0.1 implementation adopted from
|
||||
https://github.com/Nicell/alien-signals-luau
|
||||
https://github.com/stackblitz/alien-signals
|
||||
|
|
@ -24,8 +24,9 @@
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]=]
|
||||
]]
|
||||
|
||||
local noYield = require("./noYield")
|
||||
local system = require("./system")
|
||||
|
||||
export type ReactiveFlags = system.ReactiveFlags
|
||||
|
|
@ -43,12 +44,12 @@ local PENDING = 0b0100000
|
|||
local QUEUED = 0b1000000
|
||||
|
||||
type EffectScope = ReactiveNode & {
|
||||
cleanup: (() -> ())?,
|
||||
cleanups: { () -> () },
|
||||
}
|
||||
|
||||
type Effect = ReactiveNode & {
|
||||
fn: () -> (),
|
||||
cleanup: (() -> ())?,
|
||||
cleanups: { () -> () },
|
||||
}
|
||||
|
||||
type Computed<T = any> = ReactiveNode & {
|
||||
|
|
@ -99,42 +100,28 @@ local function setCurrentScope(scope: Effect | EffectScope?)
|
|||
return prevScope
|
||||
end
|
||||
|
||||
local function addCleanup(e: Effect | EffectScope, cleanup: () -> ())
|
||||
local prevCleanup: (() -> ())? = e.cleanup
|
||||
local nextCleanup = cleanup
|
||||
|
||||
if prevCleanup then
|
||||
function cleanup()
|
||||
prevCleanup()
|
||||
nextCleanup()
|
||||
end
|
||||
end
|
||||
|
||||
(e :: Effect).cleanup = cleanup
|
||||
end
|
||||
|
||||
local function onEffectCleanup(cleanup: () -> ())
|
||||
if activeSub then
|
||||
addCleanup(activeSub :: Effect, cleanup)
|
||||
table.insert((activeSub :: Effect).cleanups, cleanup)
|
||||
end
|
||||
end
|
||||
|
||||
local function onScopeDispose(cleanup: () -> ())
|
||||
if activeScope then
|
||||
addCleanup(activeScope, cleanup)
|
||||
table.insert(activeScope.cleanups, cleanup)
|
||||
end
|
||||
end
|
||||
|
||||
local function cleanupEffect(e: Effect | EffectScope)
|
||||
if e.cleanup then
|
||||
local prev = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(e.cleanup)
|
||||
setCurrentSub(prev);
|
||||
(e :: Effect).cleanup = nil
|
||||
for _, cleanup in e.cleanups :: { () -> () } do
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local success, result: unknown = noYield(cleanup)
|
||||
setCurrentSub(prevSub)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
end
|
||||
table.clear(e.cleanups)
|
||||
end
|
||||
|
||||
local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
|
||||
|
|
@ -142,7 +129,7 @@ local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
|
|||
cleanupEffect(e)
|
||||
local prev = setCurrentSub(e)
|
||||
startTracking(e)
|
||||
local success, result: unknown = pcall((e :: Effect).fn)
|
||||
local success, result: unknown = noYield((e :: Effect).fn)
|
||||
setCurrentSub(prev)
|
||||
endTracking(e)
|
||||
if not success then
|
||||
|
|
@ -208,7 +195,7 @@ local function updateComputed(c: Computed): boolean
|
|||
local prevSub = setCurrentSub(c)
|
||||
startTracking(c)
|
||||
local oldValue = c.value
|
||||
local success, result = pcall(c.getter, oldValue)
|
||||
local success, result = noYield(c.getter, oldValue)
|
||||
setCurrentSub(prevSub)
|
||||
endTracking(c)
|
||||
if not success then
|
||||
|
|
@ -254,7 +241,7 @@ local function signalGetterSetter<T>(this: Signal<T>): (() -> T, (value: ((T) ->
|
|||
local newValue = newValue :: T
|
||||
if type(newValue) == "function" then
|
||||
local prev = setCurrentSub(nil)
|
||||
local success, result = pcall(newValue, value)
|
||||
local success, result = noYield(newValue, value)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
|
|
@ -336,6 +323,7 @@ end
|
|||
local function effect(fn: () -> ()): () -> ()
|
||||
local e: Effect = {
|
||||
fn = fn,
|
||||
cleanups = {},
|
||||
flags = WATCHING,
|
||||
}
|
||||
if activeSub then
|
||||
|
|
@ -344,7 +332,7 @@ local function effect(fn: () -> ()): () -> ()
|
|||
link(e, activeScope)
|
||||
end
|
||||
local prev = setCurrentSub(e)
|
||||
local success, result: unknown = pcall(fn)
|
||||
local success, result: unknown = noYield(fn)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
|
|
@ -354,6 +342,7 @@ end
|
|||
|
||||
local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
||||
local e: EffectScope = {
|
||||
cleanups = {},
|
||||
flags = NONE,
|
||||
}
|
||||
if activeScope and not detached then
|
||||
|
|
@ -361,7 +350,7 @@ local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
|||
end
|
||||
local prevSub = setCurrentSub(nil)
|
||||
local prevScope = setCurrentScope(e)
|
||||
local success, result: unknown = pcall(fn)
|
||||
local success, result: unknown = noYield(fn)
|
||||
setCurrentScope(prevScope)
|
||||
setCurrentSub(prevSub)
|
||||
if not success then
|
||||
|
|
@ -401,7 +390,7 @@ local function unwatched(node: Signal | Computed | Effect | EffectScope)
|
|||
toRemove = unlink(toRemove, node)
|
||||
until not toRemove
|
||||
end
|
||||
elseif (node :: Signal).previousValue == nil then
|
||||
elseif (node :: Effect).cleanups ~= nil then
|
||||
effectStop(node :: Effect)()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,26 +1,21 @@
|
|||
local signals = require("./signals")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
|
||||
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
||||
local current: T
|
||||
local mounted = true
|
||||
local tracked = computed(function()
|
||||
return selector()
|
||||
end)
|
||||
|
||||
local current = tracked()
|
||||
|
||||
return effect(function()
|
||||
local previous = current
|
||||
current = selector()
|
||||
current = tracked()
|
||||
|
||||
if mounted then
|
||||
mounted = false
|
||||
elseif current ~= previous then
|
||||
local currentSub = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(listener, current, previous)
|
||||
setCurrentSub(currentSub)
|
||||
|
||||
if not success then
|
||||
error(`subscribe() encountered an error: {result}`)
|
||||
end
|
||||
if current ~= previous then
|
||||
listener(current, previous)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
--[=[
|
||||
--[[
|
||||
alien-signals 2.0.1 implementation adopted from
|
||||
https://github.com/Nicell/alien-signals-luau
|
||||
https://github.com/stackblitz/alien-signals
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]=]
|
||||
]]
|
||||
|
||||
export type ReactiveNode = {
|
||||
deps: Link?,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
export type Getter<T = any> = () -> T
|
||||
export type Setter<T = any> = (update: Update<T>) -> T
|
||||
export type Update<T = any> = T | (previous: T) -> T
|
||||
|
||||
export type Selector<T = any> = () -> T
|
||||
|
||||
export type Atom<T = any> = ((((T) -> T) | T) -> T) & (() -> T)
|
||||
export type Atom<T = any> = Setter<T> & Getter<T>
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
local noYield = require("./noYield")
|
||||
local signals = require("./signals")
|
||||
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
|
||||
local function untracked<T, Args...>(callback: ((Args...) -> T) | (Args...) -> (), ...: Args...): T
|
||||
local currentSub = setCurrentSub(nil)
|
||||
local success, result = pcall(callback, ...)
|
||||
local success, result = noYield(callback, ...)
|
||||
setCurrentSub(currentSub)
|
||||
|
||||
if not success then
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
local Charm = require("../src/lib")
|
||||
local suite = require("@tests/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local effectScope = Charm.effectScope
|
||||
local test = suite.test
|
||||
|
||||
--- Creates a weak reference to a value.
|
||||
--- @return Function to dereference the value.
|
||||
local function weakRef<T>(value: T): () -> T?
|
||||
local ref = setmetatable({ value = value }, { __mode = "v" })
|
||||
return function()
|
||||
return ref.value
|
||||
end
|
||||
end
|
||||
|
||||
--- Triggers garbage collection by allocating excessive memory.
|
||||
local function gc()
|
||||
for _ = 1, 1000 do
|
||||
local _ = table.create(100, 0)
|
||||
end
|
||||
end
|
||||
|
||||
--- In alien-signals, a computed value unlinks from dependencies when its last
|
||||
--- parent scope is disposed. Calling a computed value outside of a scope links
|
||||
--- it to dependencies, but the dependencies hold strong references to the
|
||||
--- computed value, so it won't GC until called in a scope and disposed.
|
||||
---
|
||||
--- This function releases a computed value by linking it to a scope and then
|
||||
--- immediately closing it, causing alien-signals to unlink the computed value
|
||||
--- and allow it to be garbage collected.
|
||||
local function release(getter: () -> ())
|
||||
effectScope(function()
|
||||
getter()
|
||||
end)()
|
||||
end
|
||||
|
||||
test("should release computed cache", function()
|
||||
local srcRef = weakRef({})
|
||||
local src = atom(srcRef())
|
||||
|
||||
local c = computed(function()
|
||||
return src()
|
||||
end)
|
||||
|
||||
c() -- cache src value
|
||||
src(nil) -- release value
|
||||
c = release(c) -- release computed
|
||||
|
||||
gc()
|
||||
assert(srcRef() == nil, "should release computed cache")
|
||||
end)
|
||||
|
||||
test("should not release effect for atom", function()
|
||||
local effectCalls = 0
|
||||
local src = atom(0)
|
||||
|
||||
effect(function()
|
||||
effectCalls += 1
|
||||
src()
|
||||
end)
|
||||
|
||||
assert(effectCalls == 1, "should call effect once")
|
||||
|
||||
gc()
|
||||
src(src() + 1)
|
||||
assert(effectCalls == 2, "should call effect again")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require("../src/lib")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local listen = Charm.listen
|
||||
|
|
@ -26,4 +26,34 @@ test("should listen to selector", function()
|
|||
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||
end)
|
||||
|
||||
test("should track inner subs", function()
|
||||
local order = {}
|
||||
local a = atom(1)
|
||||
|
||||
local unsub = listen(a, function()
|
||||
table.insert(order, 1)
|
||||
listen(a, function()
|
||||
table.insert(order, 2)
|
||||
end)
|
||||
listen(a, function()
|
||||
table.insert(order, 3)
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(table.concat(order) == "123", `bad mount order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
a(a() + 1)
|
||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
a(a() + 1)
|
||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
unsub()
|
||||
a(a() + 1)
|
||||
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require("../src/lib")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local mapped = Charm.mapped
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local Charm = require("../src/lib")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local signal = signals.signal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
local Charm = require("@pkgs/charm/src/lib")
|
||||
local suite = require("@tests/suite")
|
||||
local signals = require("../../src/signals")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local effectScope = Charm.effectScope
|
||||
local signal = signals.signal
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local test = suite.test
|
||||
|
||||
--- Creates a weak reference to a value.
|
||||
|
|
@ -39,23 +39,23 @@ end
|
|||
|
||||
test("should release computed cache", function()
|
||||
local srcRef = weakRef({})
|
||||
local src = atom(srcRef())
|
||||
local src, setSrc = signal(srcRef())
|
||||
|
||||
local c = computed(function()
|
||||
return src()
|
||||
end)
|
||||
|
||||
c() -- cache src value
|
||||
src(nil) -- release value
|
||||
setSrc(nil) -- release value
|
||||
c = release(c) -- release computed
|
||||
|
||||
gc()
|
||||
assert(srcRef() == nil, "should release computed cache")
|
||||
end)
|
||||
|
||||
test("should not release effect for atom", function()
|
||||
test("should not release effect for signal", function()
|
||||
local effectCalls = 0
|
||||
local src = atom(0)
|
||||
local src, setSrc = signal(0)
|
||||
|
||||
effect(function()
|
||||
effectCalls += 1
|
||||
|
|
@ -65,7 +65,7 @@ test("should not release effect for atom", function()
|
|||
assert(effectCalls == 1, "should call effect once")
|
||||
|
||||
gc()
|
||||
src(src() + 1)
|
||||
setSrc(src() + 1)
|
||||
assert(effectCalls == 2, "should call effect again")
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
|
|
|
|||
|
|
@ -1,21 +1,27 @@
|
|||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local describe = suite.describe
|
||||
|
||||
local function spec(name: string)
|
||||
describe(name, function()
|
||||
return require(`./{name}.spec`) :: any
|
||||
suite.describe("signals", function()
|
||||
suite.describe("computed", function()
|
||||
require("./computed.spec")
|
||||
end)
|
||||
suite.describe("effect", function()
|
||||
require("./effect.spec")
|
||||
end)
|
||||
suite.describe("effectScope", function()
|
||||
require("./effectScope.spec")
|
||||
end)
|
||||
suite.describe("issue_48", function()
|
||||
require("./issue_48.spec")
|
||||
end)
|
||||
suite.describe("gc", function()
|
||||
require("./gc.spec")
|
||||
end)
|
||||
suite.describe("topology", function()
|
||||
require("./topology.spec")
|
||||
end)
|
||||
suite.describe("untrack", function()
|
||||
require("./untrack.spec")
|
||||
end)
|
||||
end
|
||||
|
||||
describe("signals", function()
|
||||
spec("computed")
|
||||
spec("effect")
|
||||
spec("effectScope")
|
||||
spec("issue_48")
|
||||
spec("gc")
|
||||
spec("topology")
|
||||
spec("untrack")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
local computed = signals.computed
|
||||
local setCurrentSub = signals.setCurrentSub
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
local suite = require("@tests/suite")
|
||||
|
||||
local describe = suite.describe
|
||||
|
||||
local function spec(name: string)
|
||||
describe(name, function()
|
||||
return require(`./{name}.spec`) :: any
|
||||
end)
|
||||
end
|
||||
local suite = require("@test/suite")
|
||||
|
||||
require("./signals/spec")
|
||||
|
||||
spec("gc")
|
||||
spec("listen")
|
||||
spec("mapped")
|
||||
spec("observe")
|
||||
spec("subscribe")
|
||||
suite.describe("charm", function()
|
||||
suite.describe("listen", function()
|
||||
require("./listen.spec")
|
||||
end)
|
||||
suite.describe("mapped", function()
|
||||
require("./mapped.spec")
|
||||
end)
|
||||
suite.describe("observe", function()
|
||||
require("./observe.spec")
|
||||
end)
|
||||
suite.describe("subscribe", function()
|
||||
require("./subscribe.spec")
|
||||
end)
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
local Charm = require("../src/lib")
|
||||
local suite = require("@tests/suite")
|
||||
local suite = require("@test/suite")
|
||||
|
||||
type Atom<T> = Charm.Atom<T>
|
||||
|
||||
local atom = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local subscribe = Charm.subscribe
|
||||
local listen = Charm.listen
|
||||
local test = suite.test
|
||||
|
||||
test("should subscribe to selector", function()
|
||||
|
|
@ -55,4 +56,33 @@ test("should ensure correct execution order", function()
|
|||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should track inner subs", function()
|
||||
local order = {}
|
||||
local a = atom(1)
|
||||
|
||||
local unsub = subscribe(a, function()
|
||||
table.insert(order, 1)
|
||||
listen(a, function()
|
||||
table.insert(order, 2)
|
||||
end)
|
||||
listen(a, function()
|
||||
table.insert(order, 3)
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(table.concat(order) == "", "should not run immediately")
|
||||
|
||||
a(a() + 1)
|
||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
a(a() + 1)
|
||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
unsub()
|
||||
a(a() + 1)
|
||||
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,56 @@
|
|||
local Charm = require(script.Parent.Charm)
|
||||
local React = require(script.Parent.React)
|
||||
|
||||
local listen = Charm.listen
|
||||
local effect = Charm.effect
|
||||
local useBinding = React.useBinding
|
||||
local useEffect = React.useEffect
|
||||
local useState = React.useState
|
||||
|
||||
--[=[
|
||||
A hook that subscribes to changes in the given atom or selector. The
|
||||
component is re-rendered whenever the state changes.
|
||||
|
||||
If the `dependencies` array is provided, the subscription to the atom or
|
||||
selector is re-created whenever the dependencies change. Otherwise, the
|
||||
subscription is created once when the component is mounted.
|
||||
|
||||
@param callback The atom or selector to subscribe to.
|
||||
@param dependencies An array of values that the subscription depends on.
|
||||
@return The current state.
|
||||
]=]
|
||||
local function useAtom<State>(callback: () -> State, dependencies: { any }?): State
|
||||
local state, setState = useState(callback)
|
||||
--- A hook that subscribes to changes in the given atom or selector. The
|
||||
--- component is re-rendered whenever the state changes.
|
||||
---
|
||||
--- If the `dependencies` array is provided, the effect is re-created whenever
|
||||
--- the dependencies change. Otherwise, the subscription is created once when
|
||||
--- the component is mounted.
|
||||
---
|
||||
--- @param selector The atom or selector to subscribe to.
|
||||
--- @param dependencies An array of values that the subscription depends on.
|
||||
--- @return The current state.
|
||||
local function useAtom<State>(selector: () -> State, dependencies: { any }?): State
|
||||
local state, setState = useState(selector)
|
||||
|
||||
useEffect(function()
|
||||
return listen(callback, setState)
|
||||
return effect(function()
|
||||
setState(selector())
|
||||
end)
|
||||
end, dependencies or {})
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
--- A hook that subscribes to changes in the given atom or selector. Returns
|
||||
--- a binding that updates when the state changes.
|
||||
---
|
||||
--- If the `dependencies` array is provided, the effect is re-created whenever
|
||||
--- the dependencies change. Otherwise, the subscription is created once when
|
||||
--- the component is mounted.
|
||||
---
|
||||
--- @param selector The atom or selector to subscribe to.
|
||||
--- @param dependencies An array of values that the subscription depends on.
|
||||
--- @return A binding that stores the current state.
|
||||
local function useAtomBinding<State>(selector: () -> State, dependencies: { any }?)
|
||||
local binding, setBinding = useBinding(selector)
|
||||
|
||||
useEffect(function()
|
||||
return effect(function()
|
||||
setBinding(selector())
|
||||
end)
|
||||
end, dependencies or {})
|
||||
|
||||
return binding
|
||||
end
|
||||
|
||||
return {
|
||||
useAtom = useAtom,
|
||||
useAtomBinding = useAtomBinding,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,13 @@ local ServerScriptService = game:GetService("ServerScriptService")
|
|||
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
|
||||
local describe = suite.describe
|
||||
|
||||
local function spec(name: string)
|
||||
describe(name, function()
|
||||
return require(`./{name}.spec`) :: any
|
||||
suite.describe("react-charm", function()
|
||||
suite.describe("useAtom", function()
|
||||
require("./useAtom.spec")
|
||||
end)
|
||||
suite.describe("useAtomBinding", function()
|
||||
require("./useAtomBinding.spec")
|
||||
end)
|
||||
end
|
||||
|
||||
describe("react-charm", function()
|
||||
spec("useAtom")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
50
pkgs/react-charm/tests/useAtomBinding.spec.luau
Normal file
50
pkgs/react-charm/tests/useAtomBinding.spec.luau
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
|
||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||
local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm)
|
||||
local renderHook = require(script.Parent.helpers.renderHook)
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
|
||||
local atom = Charm.atom
|
||||
local useAtomBinding = ReactCharm.useAtomBinding
|
||||
local test = suite.test
|
||||
|
||||
test("tracks atom state", function()
|
||||
local state = atom(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtomBinding(state)
|
||||
end)
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
state(1)
|
||||
assert(hook.result:getValue() == 1, "should return 1")
|
||||
end)
|
||||
|
||||
test("accepts a selector", function()
|
||||
local state = atom(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtomBinding(function()
|
||||
return 2 * state()
|
||||
end)
|
||||
end)
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
state(1)
|
||||
assert(hook.result:getValue() == 2, "should return 2")
|
||||
end)
|
||||
|
||||
test("tracks dependencies", function()
|
||||
local state = atom(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtomBinding(state, { state })
|
||||
end)
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
state(1)
|
||||
assert(hook.result:getValue() == 1, "should return 1")
|
||||
state = atom(0)
|
||||
hook.rerender()
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
state(1)
|
||||
assert(hook.result:getValue() == 1, "should return 1")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
@ -5,15 +5,14 @@ local subscribe = Charm.subscribe
|
|||
local source = Vide.source
|
||||
local cleanup = Vide.cleanup
|
||||
|
||||
--[=[
|
||||
Subscribes to the state of an atom and returns a Vide source.
|
||||
|
||||
@param callback The atom or selector to subscribe to.
|
||||
@return The reactive source.
|
||||
]=]
|
||||
local function useAtom<State>(callback: () -> State): () -> State
|
||||
local state = source(callback())
|
||||
cleanup(subscribe(callback, state))
|
||||
--- Subscribes to the state of an atom or selector. Returns a source that
|
||||
--- updates when the state changes.
|
||||
---
|
||||
--- @param selector The atom or selector to subscribe to.
|
||||
--- @return The reactive source.
|
||||
local function useAtom<State>(selector: () -> State): () -> State
|
||||
local state = source(selector())
|
||||
cleanup(subscribe(selector, state))
|
||||
return state
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,10 @@ local ServerScriptService = game:GetService("ServerScriptService")
|
|||
|
||||
local suite = require(ServerScriptService.Tests.suite)
|
||||
|
||||
local describe = suite.describe
|
||||
|
||||
local function spec(name: string)
|
||||
describe(name, function()
|
||||
return require(`./{name}.spec`) :: any
|
||||
suite.describe("vide-charm", function()
|
||||
suite.describe("useAtom", function()
|
||||
require("./useAtom.spec")
|
||||
end)
|
||||
end
|
||||
|
||||
describe("vide-charm", function()
|
||||
spec("useAtom")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue