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
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
run: sh bin/check.sh
|
run: sh bin/check.sh
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: lute tests/init.luau
|
run: lute test/init.luau
|
||||||
|
|
||||||
- name: Build models
|
- name: Build models
|
||||||
run: sh bin/build.sh
|
run: sh bin/build.sh
|
||||||
|
|
|
||||||
2
.luaurc
2
.luaurc
|
|
@ -4,7 +4,7 @@
|
||||||
"LocalShadow": false
|
"LocalShadow": false
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"tests": "tests",
|
"test": "test",
|
||||||
"pkgs": "pkgs"
|
"pkgs": "pkgs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img width="150" height="150" src="https://raw.githubusercontent.com/littensy/charm/main/images/logo.png" alt="Logo">
|
<img width="150" height="150" src="https://raw.githubusercontent.com/littensy/charm/main/assets/logo.png" alt="Logo">
|
||||||
</p>
|
</p>
|
||||||
<h1 align="center"><b>Charm</b></h1>
|
<h1 align="center"><b>Charm</b></h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 255 KiB |
42
benches/propagate.luau
Normal file
42
benches/propagate.luau
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
local Charm = require("@pkgs/charm/src/lib")
|
||||||
|
local bench = require("./bench")
|
||||||
|
|
||||||
|
local signals = Charm.signals
|
||||||
|
|
||||||
|
for _, w in { 10, 100 } do
|
||||||
|
for _, h in { 10, 100 } do
|
||||||
|
bench.run(`charm {w}*{h}`, function()
|
||||||
|
local src = Charm.atom(1)
|
||||||
|
for _ = 1, w do
|
||||||
|
local last = src
|
||||||
|
for _ = 1, h do
|
||||||
|
local prev = last
|
||||||
|
last = Charm.computed(function()
|
||||||
|
return prev() + 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
Charm.effect(function()
|
||||||
|
last()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
src(src() + 1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
bench.run(`alien-signals {w}*{h}`, function()
|
||||||
|
local src, setSrc = signals.signal(1)
|
||||||
|
for _ = 1, w do
|
||||||
|
local last = src
|
||||||
|
for _ = 1, h do
|
||||||
|
local prev = last
|
||||||
|
last = signals.computed(function()
|
||||||
|
return prev() + 1
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
signals.effect(function()
|
||||||
|
last()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
setSrc(src() + 1)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
local Charm = require("@pkgs/charm/src/lib")
|
|
||||||
local bench = require("./bench")
|
|
||||||
|
|
||||||
local atom = Charm.atom
|
|
||||||
local computed = Charm.computed
|
|
||||||
local effect = Charm.effect
|
|
||||||
|
|
||||||
for _, w in { 1, 10, 100 } do
|
|
||||||
for _, h in { 1, 10, 100 } do
|
|
||||||
bench.run(`propagate {w}*{h}`, function()
|
|
||||||
local src = atom(1)
|
|
||||||
for _ = 1, w do
|
|
||||||
local last = src
|
|
||||||
for _ = 1, h do
|
|
||||||
local prev = last
|
|
||||||
last = computed(function()
|
|
||||||
return prev() + 1
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
effect(function()
|
|
||||||
last()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
src(src() + 1)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -7,10 +7,10 @@ luau-lsp analyze \
|
||||||
--flag:LuauFixIndexerSubtypingOrdering=true \
|
--flag:LuauFixIndexerSubtypingOrdering=true \
|
||||||
--sourcemap=sourcemap.json \
|
--sourcemap=sourcemap.json \
|
||||||
--ignore="**/_Index/**" \
|
--ignore="**/_Index/**" \
|
||||||
pkgs tests
|
pkgs test
|
||||||
|
|
||||||
selene pkgs tests
|
selene pkgs test
|
||||||
|
|
||||||
stylua --check pkgs tests
|
stylua --check pkgs test
|
||||||
|
|
||||||
rm bin/roblox.d.luau
|
rm bin/roblox.d.luau
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@
|
||||||
"ServerScriptService": {
|
"ServerScriptService": {
|
||||||
"$className": "ServerScriptService",
|
"$className": "ServerScriptService",
|
||||||
"Tests": {
|
"Tests": {
|
||||||
"$path": "tests",
|
"$path": "test",
|
||||||
"Charm": { "$path": "pkgs/charm/tests" },
|
"Charm": { "$path": "pkgs/charm/tests" },
|
||||||
"CharmSync": { "$path": "pkgs/charm-sync/tests" },
|
"CharmSync": { "$path": "pkgs/charm-sync/tests" },
|
||||||
"ReactCharm": { "$path": "pkgs/react-charm/tests" },
|
"ReactCharm": { "$path": "pkgs/react-charm/tests" },
|
||||||
"VideCharm": { "$path": "pkgs/vide-charm/tests" }
|
"VideCharm": { "$path": "pkgs/vide-charm/tests" }
|
||||||
},
|
},
|
||||||
"Benchmarks": {
|
"Benchmarks": {
|
||||||
"$path": "benchmarks"
|
"$path": "benches"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,22 @@ local ServerScriptService = game:GetService("ServerScriptService")
|
||||||
|
|
||||||
local suite = require(ServerScriptService.Tests.suite)
|
local suite = require(ServerScriptService.Tests.suite)
|
||||||
|
|
||||||
local describe = suite.describe
|
suite.describe("charm-sync", function()
|
||||||
|
suite.describe("client", function()
|
||||||
local function spec(name: string)
|
require("./client.spec")
|
||||||
describe(name, function()
|
end)
|
||||||
return require(`./{name}.spec`) :: any
|
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)
|
||||||
end
|
|
||||||
|
|
||||||
describe("charm-sync", function()
|
|
||||||
spec("client")
|
|
||||||
spec("server")
|
|
||||||
spec("flatten")
|
|
||||||
spec("patch")
|
|
||||||
spec("validate")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,46 @@ local signals = require("./signals")
|
||||||
local types = require("./types")
|
local types = require("./types")
|
||||||
|
|
||||||
type Atom<T> = types.Atom<T>
|
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 signal = signals.signal
|
||||||
|
|
||||||
local function atomGetterSetterEquals<T>(
|
type Equals<T> = (current: T, incoming: T) -> boolean
|
||||||
source: () -> T,
|
|
||||||
setSource: (((T) -> T) | T) -> T,
|
local function createAtomWithEqualityCheck<T>(getter: Getter<T>, setter: Setter<T>, equals: Equals<T>): Atom<T>
|
||||||
equals: (T, T) -> boolean
|
local function atom(...: Update<T>): T
|
||||||
): Atom<T>
|
|
||||||
return function(...: ((T) -> T) | T): T
|
|
||||||
if select("#", ...) == 0 then
|
if select("#", ...) == 0 then
|
||||||
return source()
|
return getter()
|
||||||
end
|
end
|
||||||
|
|
||||||
local nextState = (...)
|
local nextState = (...)
|
||||||
|
|
||||||
if type(nextState) == "function" then
|
return setter(function(currentState): T
|
||||||
return setSource(function(currentState): T
|
local nextState = if type(nextState) == "function" then nextState(currentState) else nextState
|
||||||
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
|
return if equals(currentState, nextState) then currentState else nextState
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
return atom
|
||||||
end
|
end
|
||||||
|
|
||||||
local function atomGetterSetter<T>(source: () -> T, setSource: (((T) -> T) | T) -> T): Atom<T>
|
local function createAtom<T>(getter: Getter<T>, setter: Setter<T>): Atom<T>
|
||||||
return function(...: ((T) -> T) | T): T
|
local function atom(...: Update<T>): T
|
||||||
if select("#", ...) == 0 then
|
return if select("#", ...) == 0 then getter() else setter((...))
|
||||||
return source()
|
end
|
||||||
|
|
||||||
|
return atom
|
||||||
|
end
|
||||||
|
|
||||||
|
local function atom<T>(state: T, equals: Equals<T>?): Atom<T>
|
||||||
|
local getter, setter = signal(state)
|
||||||
|
|
||||||
|
if equals == nil then
|
||||||
|
return createAtom(getter, setter)
|
||||||
else
|
else
|
||||||
return setSource((...))
|
return createAtomWithEqualityCheck(getter, setter, equals)
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function atom<T>(state: T, equals: ((previous: T, current: T) -> boolean)?): Atom<T>
|
|
||||||
local source, setSource = signal(state)
|
|
||||||
|
|
||||||
if equals then
|
|
||||||
return atomGetterSetterEquals(source, setSource, equals)
|
|
||||||
else
|
|
||||||
return atomGetterSetter(source, setSource)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@ local signals = require("./signals")
|
||||||
|
|
||||||
local onEffectCleanup = signals.onEffectCleanup
|
local onEffectCleanup = signals.onEffectCleanup
|
||||||
|
|
||||||
type Cleanup = () -> ()
|
local function effect(callback: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
|
||||||
|
local disposed = false
|
||||||
local function effect(callback: (dispose: () -> ()) -> ...Cleanup?): () -> ()
|
|
||||||
local disposeImmediately = false
|
|
||||||
|
|
||||||
local function dispose()
|
local function dispose()
|
||||||
disposeImmediately = true
|
disposed = true
|
||||||
end
|
end
|
||||||
|
|
||||||
dispose = signals.effect(function()
|
dispose = signals.effect(function()
|
||||||
|
|
@ -18,7 +16,7 @@ local function effect(callback: (dispose: () -> ()) -> ...Cleanup?): () -> ()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if disposeImmediately then
|
if disposed then
|
||||||
dispose()
|
dispose()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@ local signals = require("./signals")
|
||||||
|
|
||||||
local onScopeDispose = signals.onScopeDispose
|
local onScopeDispose = signals.onScopeDispose
|
||||||
|
|
||||||
type Cleanup = () -> ()
|
local function effectScope(callback: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
||||||
|
|
||||||
local function effectScope(callback: () -> ...Cleanup?, detached: boolean?): () -> ()
|
|
||||||
return signals.effectScope(function()
|
return signals.effectScope(function()
|
||||||
local cleanup = callback()
|
local cleanup = callback()
|
||||||
if cleanup then
|
if cleanup then
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
local lib = require(script.lib)
|
local lib = require(script.lib)
|
||||||
|
|
||||||
export type Atom<T = any> = lib.Atom<T>
|
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 Selector<T = any> = lib.Selector<T>
|
||||||
|
export type Setter<T = any> = lib.Setter<T>
|
||||||
|
export type Update<T = any> = lib.Update<T>
|
||||||
|
|
||||||
return lib
|
return lib
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,10 @@ local types = require("./types")
|
||||||
local untracked = require("./untracked")
|
local untracked = require("./untracked")
|
||||||
|
|
||||||
export type Atom<T = any> = types.Atom<T>
|
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 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 computed = signals.computed
|
||||||
local effectScope = signals.effectScope
|
local effectScope = signals.effectScope
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,27 @@
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
|
local computed = signals.computed
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local setCurrentSub = signals.setCurrentSub
|
|
||||||
|
|
||||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
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 current: T
|
||||||
|
local disposed = false
|
||||||
|
|
||||||
local function dispose()
|
local function dispose()
|
||||||
disposeImmediately = true
|
disposed = true
|
||||||
end
|
end
|
||||||
|
|
||||||
dispose = effect(function()
|
dispose = effect(function()
|
||||||
local previous: T? = current
|
local previous = current
|
||||||
current = selector()
|
current = tracked()
|
||||||
|
listener(current, previous, dispose)
|
||||||
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
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if disposeImmediately then
|
if disposed then
|
||||||
dispose()
|
dispose()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
local noYield = require("./noYield")
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
local computed = signals.computed
|
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 function map(value: VI, key: KI): (VO, KO)
|
||||||
local currentSub = setCurrentSub(nil)
|
local currentSub = setCurrentSub(nil)
|
||||||
local success, valueOut, keyOut = pcall(mapper, value, key)
|
local success, valueOut, keyOut = noYield(mapper, value, key)
|
||||||
setCurrentSub(currentSub)
|
setCurrentSub(currentSub)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(`mapped() transform function failed: {valueOut}`)
|
error(`mapped() transform function failed: {valueOut}`)
|
||||||
end
|
end
|
||||||
|
|
||||||
return valueOut, if keyOut ~= nil then keyOut else key :: any
|
if keyOut == nil then
|
||||||
|
keyOut = key :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
return valueOut, keyOut
|
||||||
end
|
end
|
||||||
|
|
||||||
return computed(function(previousOutput)
|
return computed(function(previousOutput)
|
||||||
|
|
@ -53,10 +58,12 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
|
||||||
|
|
||||||
local valueOut, keyOut = map(value, key)
|
local valueOut, keyOut = map(value, key)
|
||||||
|
|
||||||
|
if currentOutput[keyOut] ~= valueOut then
|
||||||
currentOutput = ensureCanMutateOutput(currentOutput, previousOutput)
|
currentOutput = ensureCanMutateOutput(currentOutput, previousOutput)
|
||||||
currentOutput[keyOut] = valueOut
|
currentOutput[keyOut] = valueOut
|
||||||
keyCache[key] = keyOut
|
keyCache[key] = keyOut
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return currentOutput
|
return currentOutput
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
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 effect = signals.effect
|
||||||
local effectScope = signals.effectScope
|
local effectScope = signals.effectScope
|
||||||
local getCurrentSub = signals.getCurrentSub
|
|
||||||
local setCurrentSub = signals.setCurrentSub
|
|
||||||
local onEffectCleanup = signals.onEffectCleanup
|
local onEffectCleanup = signals.onEffectCleanup
|
||||||
local onScopeDispose = signals.onScopeDispose
|
local onScopeDispose = signals.onScopeDispose
|
||||||
|
local getCurrentSub = signals.getCurrentSub
|
||||||
|
|
||||||
type Cleanup = () -> ()
|
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
||||||
|
|
||||||
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...Cleanup?): () -> ()
|
|
||||||
local scopes: { [K]: () -> () } = {}
|
local scopes: { [K]: () -> () } = {}
|
||||||
local currentItems: { [K]: V } = {}
|
local currentItems: { [K]: V } = {}
|
||||||
|
|
||||||
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
|
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
|
||||||
for key in previousItems do
|
for key in previousItems do
|
||||||
if currentItems[key] == nil then
|
if currentItems[key] == nil and scopes[key] then
|
||||||
local dispose = scopes[key]
|
scopes[key]()
|
||||||
scopes[key] = nil
|
scopes[key] = nil
|
||||||
dispose()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -46,13 +42,7 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
|
||||||
currentItems = selector()
|
currentItems = selector()
|
||||||
|
|
||||||
if currentItems ~= previousItems then
|
if currentItems ~= previousItems then
|
||||||
local currentSub = setCurrentSub(nil)
|
updateScopes(currentItems, previousItems)
|
||||||
local success, result: unknown = pcall(updateScopes, currentItems, previousItems)
|
|
||||||
setCurrentSub(currentSub)
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
error(`observe() encountered an error: {result}`, 2)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
--[=[
|
--[[
|
||||||
alien-signals 2.0.1 implementation adopted from
|
alien-signals 2.0.1 implementation adopted from
|
||||||
https://github.com/Nicell/alien-signals-luau
|
https://github.com/Nicell/alien-signals-luau
|
||||||
https://github.com/stackblitz/alien-signals
|
https://github.com/stackblitz/alien-signals
|
||||||
|
|
@ -24,8 +24,9 @@
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
]=]
|
]]
|
||||||
|
|
||||||
|
local noYield = require("./noYield")
|
||||||
local system = require("./system")
|
local system = require("./system")
|
||||||
|
|
||||||
export type ReactiveFlags = system.ReactiveFlags
|
export type ReactiveFlags = system.ReactiveFlags
|
||||||
|
|
@ -43,12 +44,12 @@ local PENDING = 0b0100000
|
||||||
local QUEUED = 0b1000000
|
local QUEUED = 0b1000000
|
||||||
|
|
||||||
type EffectScope = ReactiveNode & {
|
type EffectScope = ReactiveNode & {
|
||||||
cleanup: (() -> ())?,
|
cleanups: { () -> () },
|
||||||
}
|
}
|
||||||
|
|
||||||
type Effect = ReactiveNode & {
|
type Effect = ReactiveNode & {
|
||||||
fn: () -> (),
|
fn: () -> (),
|
||||||
cleanup: (() -> ())?,
|
cleanups: { () -> () },
|
||||||
}
|
}
|
||||||
|
|
||||||
type Computed<T = any> = ReactiveNode & {
|
type Computed<T = any> = ReactiveNode & {
|
||||||
|
|
@ -99,42 +100,28 @@ local function setCurrentScope(scope: Effect | EffectScope?)
|
||||||
return prevScope
|
return prevScope
|
||||||
end
|
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: () -> ())
|
local function onEffectCleanup(cleanup: () -> ())
|
||||||
if activeSub then
|
if activeSub then
|
||||||
addCleanup(activeSub :: Effect, cleanup)
|
table.insert((activeSub :: Effect).cleanups, cleanup)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function onScopeDispose(cleanup: () -> ())
|
local function onScopeDispose(cleanup: () -> ())
|
||||||
if activeScope then
|
if activeScope then
|
||||||
addCleanup(activeScope, cleanup)
|
table.insert(activeScope.cleanups, cleanup)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function cleanupEffect(e: Effect | EffectScope)
|
local function cleanupEffect(e: Effect | EffectScope)
|
||||||
if e.cleanup then
|
for _, cleanup in e.cleanups :: { () -> () } do
|
||||||
local prev = setCurrentSub(nil)
|
local prevSub = setCurrentSub(nil)
|
||||||
local success, result: unknown = pcall(e.cleanup)
|
local success, result: unknown = noYield(cleanup)
|
||||||
setCurrentSub(prev);
|
setCurrentSub(prevSub)
|
||||||
(e :: Effect).cleanup = nil
|
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
table.clear(e.cleanups)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
|
local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
|
||||||
|
|
@ -142,7 +129,7 @@ local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
|
||||||
cleanupEffect(e)
|
cleanupEffect(e)
|
||||||
local prev = setCurrentSub(e)
|
local prev = setCurrentSub(e)
|
||||||
startTracking(e)
|
startTracking(e)
|
||||||
local success, result: unknown = pcall((e :: Effect).fn)
|
local success, result: unknown = noYield((e :: Effect).fn)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
endTracking(e)
|
endTracking(e)
|
||||||
if not success then
|
if not success then
|
||||||
|
|
@ -208,7 +195,7 @@ local function updateComputed(c: Computed): boolean
|
||||||
local prevSub = setCurrentSub(c)
|
local prevSub = setCurrentSub(c)
|
||||||
startTracking(c)
|
startTracking(c)
|
||||||
local oldValue = c.value
|
local oldValue = c.value
|
||||||
local success, result = pcall(c.getter, oldValue)
|
local success, result = noYield(c.getter, oldValue)
|
||||||
setCurrentSub(prevSub)
|
setCurrentSub(prevSub)
|
||||||
endTracking(c)
|
endTracking(c)
|
||||||
if not success then
|
if not success then
|
||||||
|
|
@ -254,7 +241,7 @@ local function signalGetterSetter<T>(this: Signal<T>): (() -> T, (value: ((T) ->
|
||||||
local newValue = newValue :: T
|
local newValue = newValue :: T
|
||||||
if type(newValue) == "function" then
|
if type(newValue) == "function" then
|
||||||
local prev = setCurrentSub(nil)
|
local prev = setCurrentSub(nil)
|
||||||
local success, result = pcall(newValue, value)
|
local success, result = noYield(newValue, value)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
|
|
@ -336,6 +323,7 @@ end
|
||||||
local function effect(fn: () -> ()): () -> ()
|
local function effect(fn: () -> ()): () -> ()
|
||||||
local e: Effect = {
|
local e: Effect = {
|
||||||
fn = fn,
|
fn = fn,
|
||||||
|
cleanups = {},
|
||||||
flags = WATCHING,
|
flags = WATCHING,
|
||||||
}
|
}
|
||||||
if activeSub then
|
if activeSub then
|
||||||
|
|
@ -344,7 +332,7 @@ local function effect(fn: () -> ()): () -> ()
|
||||||
link(e, activeScope)
|
link(e, activeScope)
|
||||||
end
|
end
|
||||||
local prev = setCurrentSub(e)
|
local prev = setCurrentSub(e)
|
||||||
local success, result: unknown = pcall(fn)
|
local success, result: unknown = noYield(fn)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
|
|
@ -354,6 +342,7 @@ end
|
||||||
|
|
||||||
local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
||||||
local e: EffectScope = {
|
local e: EffectScope = {
|
||||||
|
cleanups = {},
|
||||||
flags = NONE,
|
flags = NONE,
|
||||||
}
|
}
|
||||||
if activeScope and not detached then
|
if activeScope and not detached then
|
||||||
|
|
@ -361,7 +350,7 @@ local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
||||||
end
|
end
|
||||||
local prevSub = setCurrentSub(nil)
|
local prevSub = setCurrentSub(nil)
|
||||||
local prevScope = setCurrentScope(e)
|
local prevScope = setCurrentScope(e)
|
||||||
local success, result: unknown = pcall(fn)
|
local success, result: unknown = noYield(fn)
|
||||||
setCurrentScope(prevScope)
|
setCurrentScope(prevScope)
|
||||||
setCurrentSub(prevSub)
|
setCurrentSub(prevSub)
|
||||||
if not success then
|
if not success then
|
||||||
|
|
@ -401,7 +390,7 @@ local function unwatched(node: Signal | Computed | Effect | EffectScope)
|
||||||
toRemove = unlink(toRemove, node)
|
toRemove = unlink(toRemove, node)
|
||||||
until not toRemove
|
until not toRemove
|
||||||
end
|
end
|
||||||
elseif (node :: Signal).previousValue == nil then
|
elseif (node :: Effect).cleanups ~= nil then
|
||||||
effectStop(node :: Effect)()
|
effectStop(node :: Effect)()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,21 @@
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
|
local computed = signals.computed
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local setCurrentSub = signals.setCurrentSub
|
|
||||||
|
|
||||||
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
|
||||||
local current: T
|
local tracked = computed(function()
|
||||||
local mounted = true
|
return selector()
|
||||||
|
end)
|
||||||
|
|
||||||
|
local current = tracked()
|
||||||
|
|
||||||
return effect(function()
|
return effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = selector()
|
current = tracked()
|
||||||
|
|
||||||
if mounted then
|
if current ~= previous then
|
||||||
mounted = false
|
listener(current, previous)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
--[=[
|
--[[
|
||||||
alien-signals 2.0.1 implementation adopted from
|
alien-signals 2.0.1 implementation adopted from
|
||||||
https://github.com/Nicell/alien-signals-luau
|
https://github.com/Nicell/alien-signals-luau
|
||||||
https://github.com/stackblitz/alien-signals
|
https://github.com/stackblitz/alien-signals
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
]=]
|
]]
|
||||||
|
|
||||||
export type ReactiveNode = {
|
export type ReactiveNode = {
|
||||||
deps: Link?,
|
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 Selector<T = any> = () -> T
|
||||||
|
|
||||||
export type Atom<T = any> = ((((T) -> T) | T) -> T) & (() -> T)
|
export type Atom<T = any> = Setter<T> & Getter<T>
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
local noYield = require("./noYield")
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
local setCurrentSub = signals.setCurrentSub
|
local setCurrentSub = signals.setCurrentSub
|
||||||
|
|
||||||
local function untracked<T, Args...>(callback: ((Args...) -> T) | (Args...) -> (), ...: Args...): T
|
local function untracked<T, Args...>(callback: ((Args...) -> T) | (Args...) -> (), ...: Args...): T
|
||||||
local currentSub = setCurrentSub(nil)
|
local currentSub = setCurrentSub(nil)
|
||||||
local success, result = pcall(callback, ...)
|
local success, result = noYield(callback, ...)
|
||||||
setCurrentSub(currentSub)
|
setCurrentSub(currentSub)
|
||||||
|
|
||||||
if not success then
|
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 Charm = require("../src/lib")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local atom = Charm.atom
|
local atom = Charm.atom
|
||||||
local listen = Charm.listen
|
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")
|
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||||
end)
|
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 {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local Charm = require("../src/lib")
|
local Charm = require("../src/lib")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local atom = Charm.atom
|
local atom = Charm.atom
|
||||||
local mapped = Charm.mapped
|
local mapped = Charm.mapped
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local Charm = require("../src/lib")
|
local Charm = require("../src/lib")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local atom = Charm.atom
|
local atom = Charm.atom
|
||||||
local computed = Charm.computed
|
local computed = Charm.computed
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local signals = require("../../src/signals")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local signal = signals.signal
|
local signal = signals.signal
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local signals = require("../../src/signals")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local signals = require("../../src/signals")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local effectScope = signals.effectScope
|
local effectScope = signals.effectScope
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
local Charm = require("@pkgs/charm/src/lib")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local atom = Charm.atom
|
local signal = signals.signal
|
||||||
local computed = Charm.computed
|
local computed = signals.computed
|
||||||
local effect = Charm.effect
|
local effect = signals.effect
|
||||||
local effectScope = Charm.effectScope
|
local effectScope = signals.effectScope
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
--- Creates a weak reference to a value.
|
--- Creates a weak reference to a value.
|
||||||
|
|
@ -39,23 +39,23 @@ end
|
||||||
|
|
||||||
test("should release computed cache", function()
|
test("should release computed cache", function()
|
||||||
local srcRef = weakRef({})
|
local srcRef = weakRef({})
|
||||||
local src = atom(srcRef())
|
local src, setSrc = signal(srcRef())
|
||||||
|
|
||||||
local c = computed(function()
|
local c = computed(function()
|
||||||
return src()
|
return src()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
c() -- cache src value
|
c() -- cache src value
|
||||||
src(nil) -- release value
|
setSrc(nil) -- release value
|
||||||
c = release(c) -- release computed
|
c = release(c) -- release computed
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
assert(srcRef() == nil, "should release computed cache")
|
assert(srcRef() == nil, "should release computed cache")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should not release effect for atom", function()
|
test("should not release effect for signal", function()
|
||||||
local effectCalls = 0
|
local effectCalls = 0
|
||||||
local src = atom(0)
|
local src, setSrc = signal(0)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
effectCalls += 1
|
effectCalls += 1
|
||||||
|
|
@ -65,7 +65,7 @@ test("should not release effect for atom", function()
|
||||||
assert(effectCalls == 1, "should call effect once")
|
assert(effectCalls == 1, "should call effect once")
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
src(src() + 1)
|
setSrc(src() + 1)
|
||||||
assert(effectCalls == 2, "should call effect again")
|
assert(effectCalls == 2, "should call effect again")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local signals = require("../../src/signals")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,27 @@
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local describe = suite.describe
|
suite.describe("signals", function()
|
||||||
|
suite.describe("computed", function()
|
||||||
local function spec(name: string)
|
require("./computed.spec")
|
||||||
describe(name, function()
|
end)
|
||||||
return require(`./{name}.spec`) :: any
|
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)
|
||||||
end
|
|
||||||
|
|
||||||
describe("signals", function()
|
|
||||||
spec("computed")
|
|
||||||
spec("effect")
|
|
||||||
spec("effectScope")
|
|
||||||
spec("issue_48")
|
|
||||||
spec("gc")
|
|
||||||
spec("topology")
|
|
||||||
spec("untrack")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local signals = require("../../src/signals")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local signals = require("../../src/signals")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local setCurrentSub = signals.setCurrentSub
|
local setCurrentSub = signals.setCurrentSub
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
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
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
require("./signals/spec")
|
require("./signals/spec")
|
||||||
|
|
||||||
spec("gc")
|
suite.describe("charm", function()
|
||||||
spec("listen")
|
suite.describe("listen", function()
|
||||||
spec("mapped")
|
require("./listen.spec")
|
||||||
spec("observe")
|
end)
|
||||||
spec("subscribe")
|
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 {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
local Charm = require("../src/lib")
|
local Charm = require("../src/lib")
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
type Atom<T> = Charm.Atom<T>
|
type Atom<T> = Charm.Atom<T>
|
||||||
|
|
||||||
local atom = Charm.atom
|
local atom = Charm.atom
|
||||||
local computed = Charm.computed
|
local computed = Charm.computed
|
||||||
local subscribe = Charm.subscribe
|
local subscribe = Charm.subscribe
|
||||||
|
local listen = Charm.listen
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
test("should subscribe to selector", function()
|
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)}`)
|
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||||
end)
|
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 {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,56 @@
|
||||||
local Charm = require(script.Parent.Charm)
|
local Charm = require(script.Parent.Charm)
|
||||||
local React = require(script.Parent.React)
|
local React = require(script.Parent.React)
|
||||||
|
|
||||||
local listen = Charm.listen
|
local effect = Charm.effect
|
||||||
|
local useBinding = React.useBinding
|
||||||
local useEffect = React.useEffect
|
local useEffect = React.useEffect
|
||||||
local useState = React.useState
|
local useState = React.useState
|
||||||
|
|
||||||
--[=[
|
--- A hook that subscribes to changes in the given atom or selector. The
|
||||||
A hook that subscribes to changes in the given atom or selector. The
|
--- component is re-rendered whenever the state changes.
|
||||||
component is re-rendered whenever the state changes.
|
---
|
||||||
|
--- If the `dependencies` array is provided, the effect is re-created whenever
|
||||||
If the `dependencies` array is provided, the subscription to the atom or
|
--- the dependencies change. Otherwise, the subscription is created once when
|
||||||
selector is re-created whenever the dependencies change. Otherwise, the
|
--- the component is mounted.
|
||||||
subscription is created once when the component is mounted.
|
---
|
||||||
|
--- @param selector The atom or selector to subscribe to.
|
||||||
@param callback The atom or selector to subscribe to.
|
--- @param dependencies An array of values that the subscription depends on.
|
||||||
@param dependencies An array of values that the subscription depends on.
|
--- @return The current state.
|
||||||
@return The current state.
|
local function useAtom<State>(selector: () -> State, dependencies: { any }?): State
|
||||||
]=]
|
local state, setState = useState(selector)
|
||||||
local function useAtom<State>(callback: () -> State, dependencies: { any }?): State
|
|
||||||
local state, setState = useState(callback)
|
|
||||||
|
|
||||||
useEffect(function()
|
useEffect(function()
|
||||||
return listen(callback, setState)
|
return effect(function()
|
||||||
|
setState(selector())
|
||||||
|
end)
|
||||||
end, dependencies or {})
|
end, dependencies or {})
|
||||||
|
|
||||||
return state
|
return state
|
||||||
end
|
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 {
|
return {
|
||||||
useAtom = useAtom,
|
useAtom = useAtom,
|
||||||
|
useAtomBinding = useAtomBinding,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,13 @@ local ServerScriptService = game:GetService("ServerScriptService")
|
||||||
|
|
||||||
local suite = require(ServerScriptService.Tests.suite)
|
local suite = require(ServerScriptService.Tests.suite)
|
||||||
|
|
||||||
local describe = suite.describe
|
suite.describe("react-charm", function()
|
||||||
|
suite.describe("useAtom", function()
|
||||||
local function spec(name: string)
|
require("./useAtom.spec")
|
||||||
describe(name, function()
|
end)
|
||||||
return require(`./{name}.spec`) :: any
|
suite.describe("useAtomBinding", function()
|
||||||
|
require("./useAtomBinding.spec")
|
||||||
end)
|
end)
|
||||||
end
|
|
||||||
|
|
||||||
describe("react-charm", function()
|
|
||||||
spec("useAtom")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
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 source = Vide.source
|
||||||
local cleanup = Vide.cleanup
|
local cleanup = Vide.cleanup
|
||||||
|
|
||||||
--[=[
|
--- Subscribes to the state of an atom or selector. Returns a source that
|
||||||
Subscribes to the state of an atom and returns a Vide source.
|
--- updates when the state changes.
|
||||||
|
---
|
||||||
@param callback The atom or selector to subscribe to.
|
--- @param selector The atom or selector to subscribe to.
|
||||||
@return The reactive source.
|
--- @return The reactive source.
|
||||||
]=]
|
local function useAtom<State>(selector: () -> State): () -> State
|
||||||
local function useAtom<State>(callback: () -> State): () -> State
|
local state = source(selector())
|
||||||
local state = source(callback())
|
cleanup(subscribe(selector, state))
|
||||||
cleanup(subscribe(callback, state))
|
|
||||||
return state
|
return state
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,10 @@ local ServerScriptService = game:GetService("ServerScriptService")
|
||||||
|
|
||||||
local suite = require(ServerScriptService.Tests.suite)
|
local suite = require(ServerScriptService.Tests.suite)
|
||||||
|
|
||||||
local describe = suite.describe
|
suite.describe("vide-charm", function()
|
||||||
|
suite.describe("useAtom", function()
|
||||||
local function spec(name: string)
|
require("./useAtom.spec")
|
||||||
describe(name, function()
|
|
||||||
return require(`./{name}.spec`) :: any
|
|
||||||
end)
|
end)
|
||||||
end
|
|
||||||
|
|
||||||
describe("vide-charm", function()
|
|
||||||
spec("useAtom")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
local suite = require("@tests/suite")
|
local suite = require("@test/suite")
|
||||||
|
|
||||||
if not table.isfrozen(_G) then
|
if not table.isfrozen(_G) then
|
||||||
_G.__DEV__ = true
|
_G.__DEV__ = true
|
||||||
|
|
@ -42,13 +42,13 @@ local color = {
|
||||||
return if options.colors then `\27[90;1m{text}\27[0m` else text
|
return if options.colors then `\27[90;1m{text}\27[0m` else text
|
||||||
end,
|
end,
|
||||||
green = function(text: string)
|
green = function(text: string)
|
||||||
return if options.colors then `\27[32;1m{text}\27[0m` else text
|
return if options.colors then `\27[32;1m{text}\27[0m` else `\u{1F7E2} {text}`
|
||||||
end,
|
end,
|
||||||
red = function(text: string)
|
red = function(text: string)
|
||||||
return if options.colors then `\27[31;1m{text}\27[0m` else text
|
return if options.colors then `\27[31;1m{text}\27[0m` else `\u{1F534} {text}`
|
||||||
end,
|
end,
|
||||||
blue = function(text: string)
|
blue = function(text: string)
|
||||||
return if options.colors then `\27[34;1m{text}\27[0m` else text
|
return if options.colors then `\27[34;1m{text}\27[0m` else `\u{1F535} {text}`
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,27 +405,33 @@ local function finish(config: Config?)
|
||||||
report()
|
report()
|
||||||
end
|
end
|
||||||
|
|
||||||
local testImpl = setmetatable({
|
local testCtor = setmetatable({
|
||||||
skip = testSkip,
|
skip = testSkip,
|
||||||
only = testOnly,
|
only = testOnly,
|
||||||
}, {
|
}, {
|
||||||
__call = function(_, name: string, run: () -> ())
|
__call = function(_, ...)
|
||||||
return test(name, run)
|
return test(...)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
local describeImpl = setmetatable({
|
local describeCtor = setmetatable({
|
||||||
skip = describeSkip,
|
skip = describeSkip,
|
||||||
only = describeOnly,
|
only = describeOnly,
|
||||||
}, {
|
}, {
|
||||||
__call = function(_, name: string, run: () -> ())
|
__call = function(_, ...)
|
||||||
return describe(name, run)
|
return describe(...)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
test = testImpl,
|
test = testCtor :: ((name: string, callback: () -> ()) -> ()) & {
|
||||||
describe = describeImpl,
|
skip: (name: string, callback: () -> ()) -> (),
|
||||||
|
only: (name: string, callback: () -> ()) -> (),
|
||||||
|
},
|
||||||
|
describe = describeCtor :: ((name: string, callback: () -> ()) -> ()) & {
|
||||||
|
skip: (name: string, callback: () -> ()) -> (),
|
||||||
|
only: (name: string, callback: () -> ()) -> (),
|
||||||
|
},
|
||||||
skip = skip,
|
skip = skip,
|
||||||
only = only,
|
only = only,
|
||||||
finish = finish,
|
finish = finish,
|
||||||
Loading…
Add table
Add a link
Reference in a new issue