mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: embrace scope cleanup, restructure code
This commit is contained in:
parent
3eba56e298
commit
8d385356cd
81 changed files with 687 additions and 626 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: sh bin/test.sh
|
run: lute tests/init.luau
|
||||||
|
|
||||||
- name: Build models
|
- name: Build models
|
||||||
run: sh bin/build.sh
|
run: sh bin/build.sh
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
/.vscode
|
/.vscode
|
||||||
/build
|
/build
|
||||||
/roblox_packages
|
/roblox_packages
|
||||||
/packages/**/wally.lock
|
/pkgs/**/wally.lock
|
||||||
/sourcemap.json
|
/sourcemap.json
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
*.rbxm
|
*.rbxm
|
||||||
|
|
|
||||||
4
.luaurc
4
.luaurc
|
|
@ -2,5 +2,9 @@
|
||||||
"languageMode": "strict",
|
"languageMode": "strict",
|
||||||
"lint": {
|
"lint": {
|
||||||
"LocalShadow": false
|
"LocalShadow": false
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"tests": "tests",
|
||||||
|
"pkgs": "packages"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
mkdir build
|
mkdir build
|
||||||
rojo build packages/charm -o build/charm.rbxm
|
rojo build pkgs/charm -o build/charm.rbxm
|
||||||
rojo build packages/charm-sync -o build/charm-sync.rbxm
|
rojo build pkgs/charm-sync -o build/charm-sync.rbxm
|
||||||
rojo build packages/react-charm -o build/react-charm.rbxm
|
rojo build pkgs/react-charm -o build/react-charm.rbxm
|
||||||
rojo build packages/vide-charm -o build/vide-charm.rbxm
|
rojo build pkgs/vide-charm -o build/vide-charm.rbxm
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
lute tests/spec.server.luau
|
|
||||||
|
|
@ -6,15 +6,26 @@
|
||||||
"$className": "ReplicatedStorage",
|
"$className": "ReplicatedStorage",
|
||||||
"Packages": {
|
"Packages": {
|
||||||
"$path": "roblox_packages",
|
"$path": "roblox_packages",
|
||||||
"Charm": { "$path": "packages/charm" },
|
"Charm": { "$path": "pkgs/charm" },
|
||||||
"CharmSync": { "$path": "packages/charm-sync" },
|
"CharmSync": { "$path": "pkgs/charm-sync" },
|
||||||
"ReactCharm": { "$path": "packages/react-charm" },
|
"ReactCharm": { "$path": "pkgs/react-charm" },
|
||||||
"VideCharm": { "$path": "packages/vide-charm" }
|
"VideCharm": { "$path": "pkgs/vide-charm" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ServerScriptService": {
|
"ServerScriptService": {
|
||||||
"$className": "ServerScriptService",
|
"$className": "ServerScriptService",
|
||||||
"Tests": { "$path": "tests" }
|
"Tests": {
|
||||||
|
"$path": "tests",
|
||||||
|
"Charm": { "$path": "pkgs/charm/__tests__" },
|
||||||
|
"CharmSync": { "$path": "pkgs/charm-sync/__tests__" },
|
||||||
|
"ReactCharm": { "$path": "pkgs/react-charm/__tests__" },
|
||||||
|
"VideCharm": { "$path": "pkgs/vide-charm/__tests__" }
|
||||||
|
},
|
||||||
|
"Benchmarks": {
|
||||||
|
"$className": "Folder",
|
||||||
|
"Charm": { "$path": "pkgs/charm/__benchmarks__" },
|
||||||
|
"CharmSync": { "$path": "pkgs/charm-sync/__benchmarks__" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
--!nocheck
|
|
||||||
return require(script.Parent.charm.src)
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
local signals = require("./signals")
|
|
||||||
local types = require("./types")
|
|
||||||
|
|
||||||
type Atom<T> = types.Atom<T>
|
|
||||||
type AtomOptions<T> = types.AtomOptions<T>
|
|
||||||
|
|
||||||
local signal = signals.signal
|
|
||||||
local pauseTracking = signals.pauseTracking
|
|
||||||
local resumeTracking = signals.resumeTracking
|
|
||||||
|
|
||||||
local function atomOper<T>(source: (...T) -> T, equals: ((T, T) -> boolean)?, ...: ((T) -> T) | T): T
|
|
||||||
if select("#", ...) == 0 then
|
|
||||||
return source()
|
|
||||||
end
|
|
||||||
|
|
||||||
local transform = (...)
|
|
||||||
local currentState = (source :: any).value
|
|
||||||
local nextState: T
|
|
||||||
|
|
||||||
-- If the transform is a function, call it with the current state and pause
|
|
||||||
-- tracking to avoid adding signals called by the transform.
|
|
||||||
if type(transform) == "function" then
|
|
||||||
pauseTracking()
|
|
||||||
local success, result = pcall(transform, currentState)
|
|
||||||
resumeTracking()
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
error(result, 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
nextState = result
|
|
||||||
else
|
|
||||||
nextState = transform
|
|
||||||
end
|
|
||||||
|
|
||||||
if equals and equals(currentState, nextState) then
|
|
||||||
return nextState
|
|
||||||
end
|
|
||||||
|
|
||||||
source(nextState)
|
|
||||||
|
|
||||||
return nextState
|
|
||||||
end
|
|
||||||
|
|
||||||
local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
|
|
||||||
local source = signal(state)
|
|
||||||
local equals = options and options.equals
|
|
||||||
|
|
||||||
return function(...)
|
|
||||||
return atomOper(source, equals, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return atom
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
local signals = require("./signals")
|
|
||||||
|
|
||||||
local effectImpl = signals.effect
|
|
||||||
|
|
||||||
local function effect(callback: (dispose: () -> ()) -> ...() -> ()?): () -> ()
|
|
||||||
local disposed = false
|
|
||||||
local dispose: () -> ()
|
|
||||||
|
|
||||||
local function disposeSafe()
|
|
||||||
disposed = true
|
|
||||||
if dispose then
|
|
||||||
dispose()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
dispose = effectImpl(function()
|
|
||||||
return callback(disposeSafe)
|
|
||||||
end)
|
|
||||||
|
|
||||||
if disposed then
|
|
||||||
dispose()
|
|
||||||
end
|
|
||||||
|
|
||||||
return disposeSafe
|
|
||||||
end
|
|
||||||
|
|
||||||
return effect
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
local listen = require("./listen")
|
|
||||||
|
|
||||||
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...() -> ()?): () -> ()
|
|
||||||
local cleanups: { [K]: () -> ()? } = {}
|
|
||||||
|
|
||||||
local unsub = listen(selector, function(currentItems, previousItems)
|
|
||||||
if not previousItems then
|
|
||||||
for key, value in currentItems do
|
|
||||||
cleanups[key] = observer(value, key)
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
for key in previousItems do
|
|
||||||
local cleanup = cleanups[key]
|
|
||||||
if currentItems[key] == nil and cleanup then
|
|
||||||
cleanups[key] = nil
|
|
||||||
cleanup()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for key, value in currentItems do
|
|
||||||
if previousItems[key] == nil then
|
|
||||||
cleanups[key] = observer(value, key)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
return function()
|
|
||||||
unsub()
|
|
||||||
for _, cleanup in cleanups do
|
|
||||||
(cleanup :: () -> ())()
|
|
||||||
end
|
|
||||||
table.clear(cleanups)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return observe
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
--!nocheck
|
|
||||||
return require(script.Parent.charm.src)
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
--!nocheck
|
|
||||||
return require(script.Parent.react)
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
--!nocheck
|
|
||||||
return require(script.Parent.charm.src)
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
--!nocheck
|
|
||||||
return require(script.Parent.vide.src)
|
|
||||||
|
|
@ -24,14 +24,18 @@ local function update(state)
|
||||||
state.users = table.clone(state.users)
|
state.users = table.clone(state.users)
|
||||||
|
|
||||||
for user = 1, USERS do
|
for user = 1, USERS do
|
||||||
if math.random() > 0.5 then
|
local rng = math.random()
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
state.users[user] = {
|
if rng < 1 / 3 then
|
||||||
coins = math.random(1, 2),
|
continue
|
||||||
gems = math.random(1, 2),
|
elseif rng < 2 / 3 then
|
||||||
}
|
state.users[user] = nil
|
||||||
|
else
|
||||||
|
state.users[user] = {
|
||||||
|
coins = math.random(1, 2),
|
||||||
|
gems = math.random(1, 2),
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return state
|
return state
|
||||||
19
pkgs/charm-sync/__tests__/spec.luau
Normal file
19
pkgs/charm-sync/__tests__/spec.luau
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
spec("client")
|
||||||
|
spec("server")
|
||||||
|
spec("flatten")
|
||||||
|
spec("patch")
|
||||||
|
spec("validate")
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
@ -6,17 +6,12 @@ local computed = Charm.computed
|
||||||
local effect = Charm.effect
|
local effect = Charm.effect
|
||||||
local atom = Charm.atom
|
local atom = Charm.atom
|
||||||
|
|
||||||
local cleanup: () -> ()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
AfterEach = function()
|
|
||||||
cleanup()
|
|
||||||
end,
|
|
||||||
Functions = {
|
Functions = {
|
||||||
atom = function()
|
atom = function()
|
||||||
local state = atom(0)
|
local state = atom(0)
|
||||||
|
|
||||||
cleanup = effect(function()
|
effect(function()
|
||||||
state()
|
state()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -32,7 +27,7 @@ return {
|
||||||
return 2 * state()
|
return 2 * state()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
cleanup = effect(function()
|
effect(function()
|
||||||
double()
|
double()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
63
pkgs/charm/__benchmarks__/propagate.bench.luau
Normal file
63
pkgs/charm/__benchmarks__/propagate.bench.luau
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
|
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||||
|
|
||||||
|
local atom = Charm.atom
|
||||||
|
local computed = Charm.computed
|
||||||
|
local effect = Charm.effect
|
||||||
|
local listen = Charm.listen
|
||||||
|
|
||||||
|
local function propagate(w: number, h: number)
|
||||||
|
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
|
||||||
|
|
||||||
|
local function propagateListen(w: number, h: number)
|
||||||
|
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
|
||||||
|
listen(last, function() end)
|
||||||
|
end
|
||||||
|
src(src() + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
Functions = {
|
||||||
|
["effect 1*100"] = function()
|
||||||
|
propagate(1, 100)
|
||||||
|
end,
|
||||||
|
["effect 100*1"] = function()
|
||||||
|
propagate(100, 1)
|
||||||
|
end,
|
||||||
|
["effect 10*10"] = function()
|
||||||
|
propagate(10, 10)
|
||||||
|
end,
|
||||||
|
["listen 1*100"] = function()
|
||||||
|
propagateListen(1, 100)
|
||||||
|
end,
|
||||||
|
["listen 100*1"] = function()
|
||||||
|
propagateListen(100, 1)
|
||||||
|
end,
|
||||||
|
["listen 10*10"] = function()
|
||||||
|
propagateListen(10, 10)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
local charm = require("../charm")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("../suite")
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
local signals = charm.signals
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local signal = signals.signal
|
local signal = signals.signal
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
test("should correctly propagate changes through computed signals", function()
|
test("should correctly propagate changes through computed signals", function()
|
||||||
local src = signal(0)
|
local src, setSrc = signal(0)
|
||||||
local c1 = computed(function()
|
local c1 = computed(function()
|
||||||
return src() % 2
|
return src() % 2
|
||||||
end)
|
end)
|
||||||
|
|
@ -19,15 +18,15 @@ test("should correctly propagate changes through computed signals", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
c3()
|
c3()
|
||||||
src(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
|
setSrc(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
|
||||||
c2() -- c1 -> none, c2 -> none
|
c2() -- c1 -> none, c2 -> none
|
||||||
src(3) -- c1 -> dirty, c2 -> toCheckDirty
|
setSrc(3) -- c1 -> dirty, c2 -> toCheckDirty
|
||||||
|
|
||||||
assert(c3() == 1, "bad final value")
|
assert(c3() == 1, "bad final value")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should propagate updated source value through chained computations", function()
|
test("should propagate updated source value through chained computations", function()
|
||||||
local src = signal(0)
|
local src, setSrc = signal(0)
|
||||||
local a = computed(function()
|
local a = computed(function()
|
||||||
return src()
|
return src()
|
||||||
end)
|
end)
|
||||||
|
|
@ -42,12 +41,12 @@ test("should propagate updated source value through chained computations", funct
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(d() == 0, "bad initial value")
|
assert(d() == 0, "bad initial value")
|
||||||
src(2)
|
setSrc(2)
|
||||||
assert(d() == 2, "bad updated value")
|
assert(d() == 2, "bad updated value")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should handle flags are indirectly updated during checkDirty", function()
|
test("should handle flags are indirectly updated during checkDirty", function()
|
||||||
local a = signal(false)
|
local a, setA = signal(false)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -61,22 +60,22 @@ test("should handle flags are indirectly updated during checkDirty", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(d() == false, "bad initial value")
|
assert(d() == false, "bad initial value")
|
||||||
a(true)
|
setA(true)
|
||||||
assert(d() == true, "bad updated value")
|
assert(d() == true, "bad updated value")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should not update if the signal value is reverted", function()
|
test("should not update if the signal value is reverted", function()
|
||||||
local times = 0
|
local times = 0
|
||||||
|
|
||||||
local src = signal(0)
|
local src, setSrc = signal(0)
|
||||||
local c1 = computed(function()
|
local c1 = computed(function()
|
||||||
times += 1
|
times += 1
|
||||||
return src()
|
return src()
|
||||||
end)
|
end)
|
||||||
c1()
|
c1()
|
||||||
assert(times == 1, "computed did not run")
|
assert(times == 1, "computed did not run")
|
||||||
src(1)
|
setSrc(1)
|
||||||
src(0)
|
setSrc(0)
|
||||||
c1()
|
c1()
|
||||||
assert(times == 1, "computed ran when it should not")
|
assert(times == 1, "computed ran when it should not")
|
||||||
end)
|
end)
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
local charm = require("../charm")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("../suite")
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
local signals = charm.signals
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local effectScope = signals.effectScope
|
local effectScope = signals.effectScope
|
||||||
local endBatch = signals.endBatch
|
local endBatch = signals.endBatch
|
||||||
local pauseTracking = signals.pauseTracking
|
local pauseTracking = signals.pauseTracking
|
||||||
local resumeTracking = signals.resumeTracking
|
local resumeTracking = signals.resumeTracking
|
||||||
|
local onEffectCleanup = signals.onEffectCleanup
|
||||||
local signal = signals.signal
|
local signal = signals.signal
|
||||||
local startBatch = signals.startBatch
|
local startBatch = signals.startBatch
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
@ -15,7 +15,7 @@ local test = suite.test
|
||||||
test("should clear subscriptions when untracked by all subscribers", function()
|
test("should clear subscriptions when untracked by all subscribers", function()
|
||||||
local bRunTimes = 0
|
local bRunTimes = 0
|
||||||
|
|
||||||
local a = signal(1)
|
local a, setA = signal(1)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
bRunTimes += 1
|
bRunTimes += 1
|
||||||
return a() * 2
|
return a() * 2
|
||||||
|
|
@ -25,15 +25,15 @@ test("should clear subscriptions when untracked by all subscribers", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(bRunTimes == 1, "should run initially")
|
assert(bRunTimes == 1, "should run initially")
|
||||||
a(2)
|
setA(2)
|
||||||
assert(bRunTimes == 2, "should run after change")
|
assert(bRunTimes == 2, "should run after change")
|
||||||
stopEffect()
|
stopEffect()
|
||||||
a(3)
|
setA(3)
|
||||||
assert(bRunTimes == 2, "should not run after stop")
|
assert(bRunTimes == 2, "should not run after stop")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should not run untracked inner effect", function()
|
test("should not run untracked inner effect", function()
|
||||||
local a = signal(3)
|
local a, setA = signal(3)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a() > 0
|
return a() > 0
|
||||||
end)
|
end)
|
||||||
|
|
@ -48,14 +48,14 @@ test("should not run untracked inner effect", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
a(2)
|
setA(2)
|
||||||
a(1)
|
setA(1)
|
||||||
a(0)
|
setA(0)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should run outer effect first", function()
|
test("should run outer effect first", function()
|
||||||
local a = signal(1)
|
local a, setA = signal(1)
|
||||||
local b = signal(1)
|
local b, setB = signal(1)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
if a() > 0 then
|
if a() > 0 then
|
||||||
|
|
@ -69,13 +69,13 @@ test("should run outer effect first", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
startBatch()
|
startBatch()
|
||||||
b(0)
|
setB(0)
|
||||||
a(0)
|
setA(0)
|
||||||
endBatch()
|
endBatch()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should not trigger inner effect when resolve maybe dirty", function()
|
test("should not trigger inner effect when resolve maybe dirty", function()
|
||||||
local a = signal(0)
|
local a, setA = signal(0)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a() % 2
|
return a() % 2
|
||||||
end)
|
end)
|
||||||
|
|
@ -92,12 +92,12 @@ test("should not trigger inner effect when resolve maybe dirty", function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
a(2)
|
setA(2)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should trigger inner effects in sequence", function()
|
test("should trigger inner effects in sequence", function()
|
||||||
local a = signal(0)
|
local a, setA = signal(0)
|
||||||
local b = signal(0)
|
local b, setB = signal(0)
|
||||||
local c = computed(function()
|
local c = computed(function()
|
||||||
return a() - b()
|
return a() - b()
|
||||||
end)
|
end)
|
||||||
|
|
@ -121,16 +121,16 @@ test("should trigger inner effects in sequence", function()
|
||||||
table.clear(order)
|
table.clear(order)
|
||||||
|
|
||||||
startBatch()
|
startBatch()
|
||||||
b(1)
|
setB(1)
|
||||||
a(1)
|
setA(1)
|
||||||
endBatch()
|
endBatch()
|
||||||
|
|
||||||
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should trigger inner effects in sequence in effect scope", function()
|
test("should trigger inner effects in sequence in effect scope", function()
|
||||||
local a = signal(0)
|
local a, setA = signal(0)
|
||||||
local b = signal(0)
|
local b, setB = signal(0)
|
||||||
local order: { string } = {}
|
local order: { string } = {}
|
||||||
|
|
||||||
effectScope(function()
|
effectScope(function()
|
||||||
|
|
@ -149,8 +149,8 @@ test("should trigger inner effects in sequence in effect scope", function()
|
||||||
table.clear(order)
|
table.clear(order)
|
||||||
|
|
||||||
startBatch()
|
startBatch()
|
||||||
b(1)
|
setB(1)
|
||||||
a(1)
|
setA(1)
|
||||||
endBatch()
|
endBatch()
|
||||||
|
|
||||||
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
||||||
|
|
@ -170,12 +170,12 @@ test("should custom effect support batch", function()
|
||||||
|
|
||||||
local logs: { string } = {}
|
local logs: { string } = {}
|
||||||
local a = signal(0)
|
local a = signal(0)
|
||||||
local b = signal(0)
|
local b, setB = signal(0)
|
||||||
|
|
||||||
local aa = computed(function()
|
local aa = computed(function()
|
||||||
table.insert(logs, "aa-0")
|
table.insert(logs, "aa-0")
|
||||||
if a() == 0 then
|
if a() == 0 then
|
||||||
b(1)
|
setB(1)
|
||||||
end
|
end
|
||||||
table.insert(logs, "aa-1")
|
table.insert(logs, "aa-1")
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -197,8 +197,8 @@ test("should custom effect support batch", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should duplicate subscribers do not affect the notify order", function()
|
test("should duplicate subscribers do not affect the notify order", function()
|
||||||
local src1 = signal(0)
|
local src1, setSrc1 = signal(0)
|
||||||
local src2 = signal(0)
|
local src2, setSrc2 = signal(0)
|
||||||
local order: { string } = {}
|
local order: { string } = {}
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
|
|
@ -216,17 +216,17 @@ test("should duplicate subscribers do not affect the notify order", function()
|
||||||
table.insert(order, "b")
|
table.insert(order, "b")
|
||||||
src1()
|
src1()
|
||||||
end)
|
end)
|
||||||
src2(1) -- src1.subs: a -> b -> a
|
setSrc2(1) -- src1.subs: a -> b -> a
|
||||||
|
|
||||||
table.clear(order)
|
table.clear(order)
|
||||||
src1(src1() + 1)
|
setSrc1(src1() + 1)
|
||||||
|
|
||||||
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should handle side effect with inner effects", function()
|
test("should handle side effect with inner effects", function()
|
||||||
local a = signal(0)
|
local a, setA = signal(0)
|
||||||
local b = signal(0)
|
local b, setB = signal(0)
|
||||||
local order: { string } = {}
|
local order: { string } = {}
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
|
|
@ -241,14 +241,14 @@ test("should handle side effect with inner effects", function()
|
||||||
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
||||||
|
|
||||||
table.clear(order)
|
table.clear(order)
|
||||||
b(1)
|
setB(1)
|
||||||
a(1)
|
setA(1)
|
||||||
assert(table.concat(order) == "ba", `wrong order: {table.concat(order)}`)
|
assert(table.concat(order) == "ba", `wrong order: {table.concat(order)}`)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should handle flags are indirectly updated during checkDirty", function()
|
test("should handle flags are indirectly updated during checkDirty", function()
|
||||||
local a = signal(false)
|
local a, setA = signal(false)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -268,71 +268,57 @@ test("should handle flags are indirectly updated during checkDirty", function()
|
||||||
triggers += 1
|
triggers += 1
|
||||||
end)
|
end)
|
||||||
assert(triggers == 1, "did not run initially")
|
assert(triggers == 1, "did not run initially")
|
||||||
a(true)
|
setA(true)
|
||||||
assert(triggers == 2, "did not update indirectly")
|
assert(triggers == 2, "did not update indirectly")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should run effect cleanup", function()
|
test("should run cleanups in sequence", function()
|
||||||
local cleanups = 0
|
local order = {}
|
||||||
|
|
||||||
local a = signal(false)
|
local a, setA = signal(false)
|
||||||
local cleanup = effect(function()
|
local stopEffect = effect(function()
|
||||||
a()
|
a()
|
||||||
return function()
|
onEffectCleanup(function()
|
||||||
cleanups += 1
|
table.insert(order, "a")
|
||||||
end
|
end)
|
||||||
end)
|
onEffectCleanup(function()
|
||||||
|
table.insert(order, "b")
|
||||||
assert(cleanups == 0, "should not cleanup immediately")
|
|
||||||
a(true)
|
|
||||||
assert(cleanups == 1, "should cleanup once")
|
|
||||||
cleanup()
|
|
||||||
assert(cleanups == 2, "should cleanup after effect stop")
|
|
||||||
a(false)
|
|
||||||
assert(cleanups == 2, "should not cleanup after update")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("should run inner effect cleanup in scope", function()
|
|
||||||
local cleanups = 0
|
|
||||||
|
|
||||||
local a = signal(false)
|
|
||||||
local stopScope = effectScope(function()
|
|
||||||
effect(function()
|
|
||||||
a()
|
|
||||||
return function()
|
|
||||||
cleanups += 1
|
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(cleanups == 0, "should not cleanup immediately")
|
assert(table.concat(order) == "", "should not cleanup immediately")
|
||||||
a(true)
|
|
||||||
assert(cleanups == 1, "should cleanup once")
|
setA(true)
|
||||||
stopScope()
|
assert(table.concat(order) == "ab", `bad update: {table.concat(order)}`)
|
||||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
|
||||||
a(false)
|
table.clear(order)
|
||||||
assert(cleanups == 2, "should not cleanup after update")
|
stopEffect()
|
||||||
|
assert(table.concat(order) == "ab", `bad dispose: {table.concat(order)}`)
|
||||||
|
|
||||||
|
table.clear(order)
|
||||||
|
setA(false)
|
||||||
|
assert(table.concat(order) == "", "should not cleanup after update")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should run inner effect cleanup", function()
|
test("should run inner effect cleanup", function()
|
||||||
local cleanups = 0
|
local cleanups = 0
|
||||||
|
|
||||||
local a = signal(false)
|
local a, setA = signal(false)
|
||||||
local stopScope = effect(function()
|
local stopEffect = effect(function()
|
||||||
a()
|
a()
|
||||||
effect(function()
|
effect(function()
|
||||||
return function()
|
onEffectCleanup(function()
|
||||||
cleanups += 1
|
cleanups += 1
|
||||||
end
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(cleanups == 0, "should not cleanup immediately")
|
assert(cleanups == 0, "should not cleanup immediately")
|
||||||
a(true)
|
setA(true)
|
||||||
assert(cleanups == 1, "should cleanup after outer update")
|
assert(cleanups == 1, "should cleanup after outer update")
|
||||||
stopScope()
|
stopEffect()
|
||||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
assert(cleanups == 2, "should cleanup after outer dispose")
|
||||||
a(false)
|
setA(false)
|
||||||
assert(cleanups == 2, "should not cleanup after update")
|
assert(cleanups == 2, "should not cleanup after update")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
local charm = require("../charm")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("../suite")
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
local signals = charm.signals
|
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local effectScope = signals.effectScope
|
local effectScope = signals.effectScope
|
||||||
local signal = signals.signal
|
local signal = signals.signal
|
||||||
|
local onEffectCleanup = signals.onEffectCleanup
|
||||||
|
local onScopeDispose = signals.onScopeDispose
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
test("should not trigger after stop", function()
|
test("should not trigger after stop", function()
|
||||||
local count = signal(1)
|
local count, setCount = signal(1)
|
||||||
|
|
||||||
local triggers = 0
|
local triggers = 0
|
||||||
|
|
||||||
|
|
@ -19,42 +20,26 @@ test("should not trigger after stop", function()
|
||||||
end)
|
end)
|
||||||
assert(triggers == 1, "did not run initially")
|
assert(triggers == 1, "did not run initially")
|
||||||
|
|
||||||
count(2)
|
setCount(2)
|
||||||
assert(triggers == 2, "did not run after update")
|
assert(triggers == 2, "did not run after update")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
count(3)
|
setCount(3)
|
||||||
assert(triggers == 3, "did not run after update outside scope")
|
assert(triggers == 3, "did not run after update outside scope")
|
||||||
stopScope()
|
stopScope()
|
||||||
count(4)
|
setCount(4)
|
||||||
assert(triggers == 3, "ran after stop")
|
assert(triggers == 3, "ran after stop")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should run effect cleanup", function()
|
test("should run indirect scope cleanup", function()
|
||||||
local cleanups = 0
|
|
||||||
|
|
||||||
local cleanup = effectScope(function()
|
|
||||||
return function()
|
|
||||||
cleanups += 1
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert(cleanups == 0, "should not cleanup immediately")
|
|
||||||
cleanup()
|
|
||||||
assert(cleanups == 1, "should cleanup after effect stop")
|
|
||||||
cleanup()
|
|
||||||
assert(cleanups == 1, "should skip redundant cleanup")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("should run indirect effect cleanup", function()
|
|
||||||
local cleanups = 0
|
local cleanups = 0
|
||||||
|
|
||||||
local cleanup
|
local cleanup
|
||||||
local stopScope = effectScope(function()
|
local stopScope = effectScope(function()
|
||||||
cleanup = effectScope(function()
|
cleanup = effectScope(function()
|
||||||
return function()
|
onScopeDispose(function()
|
||||||
cleanups += 1
|
cleanups += 1
|
||||||
end
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -65,4 +50,26 @@ test("should run indirect effect cleanup", function()
|
||||||
assert(cleanups == 1, "should skip redundant dispose")
|
assert(cleanups == 1, "should skip redundant dispose")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should run indirect effect cleanup", function()
|
||||||
|
local cleanups = 0
|
||||||
|
|
||||||
|
local a, setA = signal(false)
|
||||||
|
local stopScope = effectScope(function()
|
||||||
|
effect(function()
|
||||||
|
a()
|
||||||
|
onEffectCleanup(function()
|
||||||
|
cleanups += 1
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(cleanups == 0, "should not cleanup immediately")
|
||||||
|
setA(true)
|
||||||
|
assert(cleanups == 1, "should cleanup once")
|
||||||
|
stopScope()
|
||||||
|
assert(cleanups == 2, "should cleanup after outer dispose")
|
||||||
|
setA(false)
|
||||||
|
assert(cleanups == 2, "should not cleanup after update")
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
local charm = require("../charm")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("../suite")
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
local signals = charm.signals
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local pauseTracking = signals.pauseTracking
|
local pauseTracking = signals.pauseTracking
|
||||||
|
|
@ -92,7 +91,7 @@ local function reaction<T>(
|
||||||
end
|
end
|
||||||
|
|
||||||
test("#48", function()
|
test("#48", function()
|
||||||
local source = signal(0)
|
local source, setSource = signal(0)
|
||||||
local disposeInner: () -> ()
|
local disposeInner: () -> ()
|
||||||
|
|
||||||
reaction(function()
|
reaction(function()
|
||||||
|
|
@ -107,9 +106,9 @@ test("#48", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
source(1)
|
setSource(1)
|
||||||
source(2)
|
setSource(2)
|
||||||
source(3)
|
setSource(3)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
18
pkgs/charm/__tests__/signals/spec.luau
Normal file
18
pkgs/charm/__tests__/signals/spec.luau
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
|
local describe = suite.describe
|
||||||
|
|
||||||
|
local function spec(name: string)
|
||||||
|
describe(name, function()
|
||||||
|
return require(`./{name}.spec`) :: any
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
spec("computed")
|
||||||
|
spec("effect")
|
||||||
|
spec("effectScope")
|
||||||
|
spec("issue_48")
|
||||||
|
spec("topology")
|
||||||
|
spec("untrack")
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
local charm = require("../charm")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("../suite")
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
local signals = charm.signals
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local effect = signals.effect
|
local effect = signals.effect
|
||||||
local signal = signals.signal
|
local signal = signals.signal
|
||||||
|
|
@ -44,7 +43,7 @@ describe("graph updates", function()
|
||||||
-- C
|
-- C
|
||||||
-- |
|
-- |
|
||||||
-- D
|
-- D
|
||||||
local a = signal(2)
|
local a, setA = signal(2)
|
||||||
|
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a() - 1
|
return a() - 1
|
||||||
|
|
@ -65,7 +64,7 @@ describe("graph updates", function()
|
||||||
assert(computeCalls == 1, "compute should be called once")
|
assert(computeCalls == 1, "compute should be called once")
|
||||||
computeCalls = 0
|
computeCalls = 0
|
||||||
|
|
||||||
a(4)
|
setA(4)
|
||||||
d()
|
d()
|
||||||
assert(computeCalls == 1, "compute should drop update")
|
assert(computeCalls == 1, "compute should drop update")
|
||||||
end)
|
end)
|
||||||
|
|
@ -79,7 +78,7 @@ describe("graph updates", function()
|
||||||
-- \ /
|
-- \ /
|
||||||
-- D
|
-- D
|
||||||
|
|
||||||
local a = signal("a")
|
local a, setA = signal("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -97,7 +96,7 @@ describe("graph updates", function()
|
||||||
assert(d() == "a a", "d should be 'a a'")
|
assert(d() == "a a", "d should be 'a a'")
|
||||||
assert(spyCalls == 1, "compute should be called once")
|
assert(spyCalls == 1, "compute should be called once")
|
||||||
|
|
||||||
a("aa")
|
setA("aa")
|
||||||
assert(d() == "aa aa", "d should be 'aa aa'")
|
assert(d() == "aa aa", "d should be 'aa aa'")
|
||||||
assert(spyCalls == 2, "compute should be called only twice")
|
assert(spyCalls == 2, "compute should be called only twice")
|
||||||
end)
|
end)
|
||||||
|
|
@ -112,7 +111,7 @@ describe("graph updates", function()
|
||||||
-- |
|
-- |
|
||||||
-- E
|
-- E
|
||||||
|
|
||||||
local a = signal("a")
|
local a, setA = signal("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -134,7 +133,7 @@ describe("graph updates", function()
|
||||||
assert(e() == "a a", "e should be 'a a'")
|
assert(e() == "a a", "e should be 'a a'")
|
||||||
assert(spyCalls == 1, "compute should be called once")
|
assert(spyCalls == 1, "compute should be called once")
|
||||||
|
|
||||||
a("aa")
|
setA("aa")
|
||||||
assert(e() == "aa aa", "e should be 'aa aa'")
|
assert(e() == "aa aa", "e should be 'aa aa'")
|
||||||
assert(spyCalls == 2, "compute should be called only twice")
|
assert(spyCalls == 2, "compute should be called only twice")
|
||||||
end)
|
end)
|
||||||
|
|
@ -142,7 +141,7 @@ describe("graph updates", function()
|
||||||
test("should bail out if result is the same", function()
|
test("should bail out if result is the same", function()
|
||||||
-- Bail out if value of "B" never changes
|
-- Bail out if value of "B" never changes
|
||||||
-- A->B->C
|
-- A->B->C
|
||||||
local a = signal("a")
|
local a, setA = signal("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
a()
|
a()
|
||||||
return "foo"
|
return "foo"
|
||||||
|
|
@ -158,7 +157,7 @@ describe("graph updates", function()
|
||||||
assert(c() == "foo", "c should be 'foo'")
|
assert(c() == "foo", "c should be 'foo'")
|
||||||
assert(spyCalls == 1, "compute should be called once")
|
assert(spyCalls == 1, "compute should be called once")
|
||||||
|
|
||||||
a("aa")
|
setA("aa")
|
||||||
assert(c() == "foo", "c should be 'foo'")
|
assert(c() == "foo", "c should be 'foo'")
|
||||||
assert(spyCalls == 1, "compute should bail out")
|
assert(spyCalls == 1, "compute should bail out")
|
||||||
end)
|
end)
|
||||||
|
|
@ -174,7 +173,7 @@ describe("graph updates", function()
|
||||||
-- E
|
-- E
|
||||||
-- / \
|
-- / \
|
||||||
-- F G
|
-- F G
|
||||||
local a = signal("a")
|
local a, setA = signal("a")
|
||||||
|
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
|
|
@ -223,7 +222,7 @@ describe("graph updates", function()
|
||||||
fSpyCalls, fSpyTime = 0, 0
|
fSpyCalls, fSpyTime = 0, 0
|
||||||
gSpyCalls, gSpyTime = 0, 0
|
gSpyCalls, gSpyTime = 0, 0
|
||||||
|
|
||||||
a("b")
|
setA("b")
|
||||||
|
|
||||||
assert(e() == "b b", "e should be 'b b'")
|
assert(e() == "b b", "e should be 'b b'")
|
||||||
assert(eSpyCalls == 1, "e should run once after update")
|
assert(eSpyCalls == 1, "e should run once after update")
|
||||||
|
|
@ -238,7 +237,7 @@ describe("graph updates", function()
|
||||||
fSpyCalls, fSpyTime = 0, 0
|
fSpyCalls, fSpyTime = 0, 0
|
||||||
gSpyCalls, gSpyTime = 0, 0
|
gSpyCalls, gSpyTime = 0, 0
|
||||||
|
|
||||||
a("c")
|
setA("c")
|
||||||
|
|
||||||
assert(e() == "c c", "e should be 'c c'")
|
assert(e() == "c c", "e should be 'c c'")
|
||||||
assert(eSpyCalls == 1, "e should run once after update")
|
assert(eSpyCalls == 1, "e should run once after update")
|
||||||
|
|
@ -259,7 +258,7 @@ describe("graph updates", function()
|
||||||
-- *A
|
-- *A
|
||||||
-- / \
|
-- / \
|
||||||
-- *B C <- we don't listen to C
|
-- *B C <- we don't listen to C
|
||||||
local a = signal("a")
|
local a, setA = signal("a")
|
||||||
|
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
|
|
@ -274,7 +273,7 @@ describe("graph updates", function()
|
||||||
assert(b() == "a", "b should be 'a'")
|
assert(b() == "a", "b should be 'a'")
|
||||||
assert(spyCalls == 0, "compute should not run")
|
assert(spyCalls == 0, "compute should not run")
|
||||||
|
|
||||||
a("aa")
|
setA("aa")
|
||||||
assert(b() == "aa", "b should be 'aa'")
|
assert(b() == "aa", "b should be 'aa'")
|
||||||
assert(spyCalls == 0, "compute should not run")
|
assert(spyCalls == 0, "compute should not run")
|
||||||
end)
|
end)
|
||||||
|
|
@ -288,7 +287,7 @@ describe("graph updates", function()
|
||||||
-- *B D <- we don't listen to C
|
-- *B D <- we don't listen to C
|
||||||
-- |
|
-- |
|
||||||
-- *C
|
-- *C
|
||||||
local a = signal("a")
|
local a, setA = signal("a")
|
||||||
local spyBCalls = 0
|
local spyBCalls = 0
|
||||||
local spyB = function()
|
local spyB = function()
|
||||||
spyBCalls += 1
|
spyBCalls += 1
|
||||||
|
|
@ -319,7 +318,7 @@ describe("graph updates", function()
|
||||||
spyCCalls = 0
|
spyCCalls = 0
|
||||||
unsub()
|
unsub()
|
||||||
|
|
||||||
a("aa")
|
setA("aa")
|
||||||
|
|
||||||
assert(spyBCalls == 0, "b should not run")
|
assert(spyBCalls == 0, "b should not run")
|
||||||
assert(spyCCalls == 0, "c should not run")
|
assert(spyCCalls == 0, "c should not run")
|
||||||
|
|
@ -336,7 +335,7 @@ describe("graph updates", function()
|
||||||
-- B *C <- returns same value every time
|
-- B *C <- returns same value every time
|
||||||
-- \ /
|
-- \ /
|
||||||
-- D
|
-- D
|
||||||
local a = signal("a")
|
local a, setA = signal("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -354,7 +353,7 @@ describe("graph updates", function()
|
||||||
assert(d() == "a c", "d should be 'a c'")
|
assert(d() == "a c", "d should be 'a c'")
|
||||||
spyResult = nil
|
spyResult = nil
|
||||||
|
|
||||||
a("aa")
|
setA("aa")
|
||||||
d()
|
d()
|
||||||
assert(spyResult == "aa c", "d should have returned 'aa c'")
|
assert(spyResult == "aa c", "d should have returned 'aa c'")
|
||||||
end)
|
end)
|
||||||
|
|
@ -368,7 +367,7 @@ describe("graph updates", function()
|
||||||
-- B *C *D
|
-- B *C *D
|
||||||
-- \ | /
|
-- \ | /
|
||||||
-- E
|
-- E
|
||||||
local a = signal("a")
|
local a, setA = signal("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -390,13 +389,13 @@ describe("graph updates", function()
|
||||||
assert(e() == "a c d", "e should be 'a c d'")
|
assert(e() == "a c d", "e should be 'a c d'")
|
||||||
spyResult = nil
|
spyResult = nil
|
||||||
|
|
||||||
a("aa")
|
setA("aa")
|
||||||
e()
|
e()
|
||||||
assert(spyResult == "aa c d", "e should have returned 'aa c d'")
|
assert(spyResult == "aa c d", "e should have returned 'aa c d'")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should support lazy branches", function()
|
test("should support lazy branches", function()
|
||||||
local a = signal(0)
|
local a, setA = signal(0)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
return a()
|
return a()
|
||||||
end)
|
end)
|
||||||
|
|
@ -405,10 +404,10 @@ describe("graph updates", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(c() == 0, "c should be 0")
|
assert(c() == 0, "c should be 0")
|
||||||
a(1)
|
setA(1)
|
||||||
assert(c() == 1, "c should be 1")
|
assert(c() == 1, "c should be 1")
|
||||||
|
|
||||||
a(0)
|
setA(0)
|
||||||
assert(c() == 0, "c should be 0 again")
|
assert(c() == 0, "c should be 0 again")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -420,7 +419,7 @@ describe("graph updates", function()
|
||||||
-- *B *C
|
-- *B *C
|
||||||
-- \ /
|
-- \ /
|
||||||
-- D
|
-- D
|
||||||
local a = signal("a")
|
local a, setA = signal("a")
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
a()
|
a()
|
||||||
return "b"
|
return "b"
|
||||||
|
|
@ -439,14 +438,14 @@ describe("graph updates", function()
|
||||||
assert(d() == "b c", "d should be 'b c'")
|
assert(d() == "b c", "d should be 'b c'")
|
||||||
spyCalls = 0
|
spyCalls = 0
|
||||||
|
|
||||||
a("aa")
|
setA("aa")
|
||||||
assert(spyCalls == 0, "d should not run")
|
assert(spyCalls == 0, "d should not run")
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("error handling", function()
|
describe("error handling", function()
|
||||||
test("should keep graph consistent on errors during activation", function()
|
test("should keep graph consistent on errors during activation", function()
|
||||||
local a = signal(0)
|
local a, setA = signal(0)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
error("fail")
|
error("fail")
|
||||||
end)
|
end)
|
||||||
|
|
@ -460,12 +459,12 @@ describe("error handling", function()
|
||||||
assert(not success, "should throw")
|
assert(not success, "should throw")
|
||||||
assert(string.match(result, "fail"), "should throw fail")
|
assert(string.match(result, "fail"), "should throw fail")
|
||||||
|
|
||||||
a(1)
|
setA(1)
|
||||||
assert(c() == 1, "c should recover and return 1")
|
assert(c() == 1, "c should recover and return 1")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should keep graph consistent on errors in computeds", function()
|
test("should keep graph consistent on errors in computeds", function()
|
||||||
local a = signal(0)
|
local a, setA = signal(0)
|
||||||
local b = computed(function()
|
local b = computed(function()
|
||||||
if a() == 1 then
|
if a() == 1 then
|
||||||
error("fail")
|
error("fail")
|
||||||
|
|
@ -478,14 +477,14 @@ describe("error handling", function()
|
||||||
|
|
||||||
assert(c() == 0, "c should be 0")
|
assert(c() == 0, "c should be 0")
|
||||||
|
|
||||||
a(1)
|
setA(1)
|
||||||
local success, result = pcall(function()
|
local success, result = pcall(function()
|
||||||
b()
|
b()
|
||||||
end)
|
end)
|
||||||
assert(not success, "should throw")
|
assert(not success, "should throw")
|
||||||
assert(string.match(result, "fail"), "should throw fail")
|
assert(string.match(result, "fail"), "should throw fail")
|
||||||
|
|
||||||
a(2)
|
setA(2)
|
||||||
assert(c() == 2, "c should recover and return 2")
|
assert(c() == 2, "c should recover and return 2")
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
local charm = require("../charm")
|
local signals = require("../../src/signals")
|
||||||
local suite = require("../suite")
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
local signals = charm.signals
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local pauseTracking = signals.pauseTracking
|
local pauseTracking = signals.pauseTracking
|
||||||
local resumeTracking = signals.resumeTracking
|
local resumeTracking = signals.resumeTracking
|
||||||
|
|
@ -9,7 +8,7 @@ local signal = signals.signal
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
test("should pause tracking", function()
|
test("should pause tracking", function()
|
||||||
local src = signal(0)
|
local src, setSrc = signal(0)
|
||||||
local c = computed(function()
|
local c = computed(function()
|
||||||
pauseTracking()
|
pauseTracking()
|
||||||
local value = src()
|
local value = src()
|
||||||
|
|
@ -18,7 +17,7 @@ test("should pause tracking", function()
|
||||||
end)
|
end)
|
||||||
assert(c() == 0, "should not track src")
|
assert(c() == 0, "should not track src")
|
||||||
|
|
||||||
src(1)
|
setSrc(1)
|
||||||
assert(c() == 0, "should not track src")
|
assert(c() == 0, "should not track src")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
20
pkgs/charm/__tests__/spec.luau
Normal file
20
pkgs/charm/__tests__/spec.luau
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
|
local describe = suite.describe
|
||||||
|
|
||||||
|
local function collect(name: string)
|
||||||
|
describe(name, function()
|
||||||
|
return require(`./{name}/spec`) :: any
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function spec(name: string)
|
||||||
|
describe(name, function()
|
||||||
|
return require(`./{name}.spec`) :: any
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
collect("signals")
|
||||||
|
spec("subscribe")
|
||||||
|
|
||||||
|
return {}
|
||||||
28
pkgs/charm/__tests__/subscribe.spec.luau
Normal file
28
pkgs/charm/__tests__/subscribe.spec.luau
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
local atom = require("../src/atom")
|
||||||
|
local subscribe = require("../src/subscribe")
|
||||||
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
|
local test = suite.test
|
||||||
|
|
||||||
|
test("should subscribe to atom", function()
|
||||||
|
local source = atom(0)
|
||||||
|
local current, previous
|
||||||
|
|
||||||
|
local unsub = subscribe(source, function(a, b)
|
||||||
|
current = a
|
||||||
|
previous = b
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(not current and not previous, "should not run initially")
|
||||||
|
source(1)
|
||||||
|
assert(current == 1, "should run with new value")
|
||||||
|
assert(previous == 0, "should run with old value")
|
||||||
|
source(2)
|
||||||
|
assert(current == 2, "should run with new value 2")
|
||||||
|
assert(previous == 1, "should run with old value 2")
|
||||||
|
unsub()
|
||||||
|
source(3)
|
||||||
|
assert(current == 2, "should not run after unsubscribing")
|
||||||
|
end)
|
||||||
|
|
||||||
|
return {}
|
||||||
54
pkgs/charm/src/atom.luau
Normal file
54
pkgs/charm/src/atom.luau
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
local signals = require("./signals")
|
||||||
|
local types = require("./types")
|
||||||
|
|
||||||
|
type Atom<T> = types.Atom<T>
|
||||||
|
type AtomOptions<T> = types.AtomOptions<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
|
||||||
|
if select("#", ...) == 0 then
|
||||||
|
return source()
|
||||||
|
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
|
||||||
|
end
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
return atom
|
||||||
27
pkgs/charm/src/effect.luau
Normal file
27
pkgs/charm/src/effect.luau
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
local signals = require("./signals")
|
||||||
|
|
||||||
|
local _effect = signals.effect
|
||||||
|
local onEffectCleanup = signals.onEffectCleanup
|
||||||
|
|
||||||
|
local function effect(callback: (dispose: () -> ()) -> ...() -> ()?): () -> ()
|
||||||
|
local disposeImmediately = false
|
||||||
|
|
||||||
|
local function dispose()
|
||||||
|
disposeImmediately = true
|
||||||
|
end
|
||||||
|
|
||||||
|
dispose = _effect(function()
|
||||||
|
local cleanup = callback(dispose)
|
||||||
|
if cleanup then
|
||||||
|
onEffectCleanup(cleanup)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
if disposeImmediately then
|
||||||
|
dispose()
|
||||||
|
end
|
||||||
|
|
||||||
|
return dispose
|
||||||
|
end
|
||||||
|
|
||||||
|
return effect
|
||||||
15
pkgs/charm/src/effectScope.luau
Normal file
15
pkgs/charm/src/effectScope.luau
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
local signals = require("./signals")
|
||||||
|
|
||||||
|
local _effectScope = signals.effectScope
|
||||||
|
local onScopeDispose = signals.onScopeDispose
|
||||||
|
|
||||||
|
local function effectScope(callback: () -> ...() -> ()?): () -> ()
|
||||||
|
return _effectScope(function()
|
||||||
|
local cleanup = callback()
|
||||||
|
if cleanup then
|
||||||
|
onScopeDispose(cleanup)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return effectScope
|
||||||
|
|
@ -15,6 +15,8 @@ export type Selector<T = any> = types.Selector<T>
|
||||||
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
local effectScope = signals.effectScope
|
local effectScope = signals.effectScope
|
||||||
|
local onEffectCleanup = signals.onEffectCleanup
|
||||||
|
local onScopeDispose = signals.onScopeDispose
|
||||||
|
|
||||||
return {
|
return {
|
||||||
atom = atom,
|
atom = atom,
|
||||||
|
|
@ -27,5 +29,7 @@ return {
|
||||||
observe = observe,
|
observe = observe,
|
||||||
subscribe = subscribe,
|
subscribe = subscribe,
|
||||||
untracked = untracked,
|
untracked = untracked,
|
||||||
|
onEffectCleanup = onEffectCleanup,
|
||||||
|
onScopeDispose = onScopeDispose,
|
||||||
signals = signals,
|
signals = signals,
|
||||||
}
|
}
|
||||||
|
|
@ -6,40 +6,31 @@ local pauseTracking = signals.pauseTracking
|
||||||
local resumeTracking = signals.resumeTracking
|
local resumeTracking = signals.resumeTracking
|
||||||
|
|
||||||
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 disposed = false
|
local disposeImmediately = false
|
||||||
local mounted = true
|
|
||||||
local current: T?
|
local current: T?
|
||||||
local dispose: () -> ()
|
|
||||||
|
|
||||||
local function disposeSafe()
|
local function dispose()
|
||||||
disposed = true
|
disposeImmediately = true
|
||||||
if dispose then
|
|
||||||
dispose()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
dispose = effect(function()
|
dispose = effect(function()
|
||||||
local value = selector()
|
local value = selector()
|
||||||
|
|
||||||
if mounted then
|
if value ~= current then
|
||||||
mounted = false
|
local previous = current
|
||||||
elseif value == current then
|
current = value
|
||||||
return
|
|
||||||
|
pauseTracking()
|
||||||
|
spawn(listener, value, previous, dispose)
|
||||||
|
resumeTracking()
|
||||||
end
|
end
|
||||||
|
|
||||||
local previous = current
|
|
||||||
current = value
|
|
||||||
|
|
||||||
pauseTracking()
|
|
||||||
spawn(listener, value, previous, disposeSafe)
|
|
||||||
resumeTracking()
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if disposed then
|
if disposeImmediately then
|
||||||
dispose()
|
dispose()
|
||||||
end
|
end
|
||||||
|
|
||||||
return disposeSafe
|
return dispose
|
||||||
end
|
end
|
||||||
|
|
||||||
return listen
|
return listen
|
||||||
|
|
@ -19,7 +19,7 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, transform: (
|
||||||
resumeTracking()
|
resumeTracking()
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(`observe() transform function failed: {valueOut}`)
|
error(`mapped() transform function failed: {valueOut}`)
|
||||||
end
|
end
|
||||||
|
|
||||||
return valueOut, keyOut
|
return valueOut, keyOut
|
||||||
|
|
@ -47,6 +47,10 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, transform: (
|
||||||
return mappedItems
|
return mappedItems
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if previousItems == currentItems then
|
||||||
|
return previousMappedItems
|
||||||
|
end
|
||||||
|
|
||||||
local mappedItems = table.clone(previousMappedItems)
|
local mappedItems = table.clone(previousMappedItems)
|
||||||
|
|
||||||
-- Update and remove mapped keys if the source values changed
|
-- Update and remove mapped keys if the source values changed
|
||||||
60
pkgs/charm/src/observe.luau
Normal file
60
pkgs/charm/src/observe.luau
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
local signals = require("./signals")
|
||||||
|
|
||||||
|
local computed = signals.computed
|
||||||
|
local effect = signals.effect
|
||||||
|
local getCurrentSub = signals.getCurrentSub
|
||||||
|
local onEffectCleanup = signals.onEffectCleanup
|
||||||
|
local onScopeDispose = signals.onScopeDispose
|
||||||
|
|
||||||
|
local function noop() end
|
||||||
|
|
||||||
|
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...() -> ()?): () -> ()
|
||||||
|
local tracked = computed(function()
|
||||||
|
return selector()
|
||||||
|
end)
|
||||||
|
|
||||||
|
local cleanups: { [K]: () -> () } = {}
|
||||||
|
local currentItems: { [K]: V } = {}
|
||||||
|
|
||||||
|
local function update(currentItems: { [K]: V }, previousItems: { [K]: V })
|
||||||
|
for key in previousItems do
|
||||||
|
local cleanup = cleanups[key]
|
||||||
|
if currentItems[key] == nil and cleanup then
|
||||||
|
cleanups[key] = nil
|
||||||
|
cleanup()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for key, value in currentItems do
|
||||||
|
if previousItems[key] == nil then
|
||||||
|
cleanups[key] = observer(value, key) or noop
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function teardown()
|
||||||
|
for _, cleanup in cleanups do
|
||||||
|
cleanup()
|
||||||
|
end
|
||||||
|
table.clear(cleanups)
|
||||||
|
end
|
||||||
|
|
||||||
|
local disposeEffect = effect(function()
|
||||||
|
local previousItems = currentItems
|
||||||
|
currentItems = tracked()
|
||||||
|
update(currentItems, previousItems)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if getCurrentSub() then
|
||||||
|
onEffectCleanup(teardown)
|
||||||
|
else
|
||||||
|
onScopeDispose(teardown)
|
||||||
|
end
|
||||||
|
|
||||||
|
return function()
|
||||||
|
disposeEffect()
|
||||||
|
teardown()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return observe
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
--[=[
|
--[=[
|
||||||
alien-signals 2.0.1 implementation with effect cleanups, 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
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ type EffectScope = ReactiveNode & {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Effect = ReactiveNode & {
|
type Effect = ReactiveNode & {
|
||||||
fn: () -> (() -> ())?,
|
fn: () -> (),
|
||||||
cleanup: (() -> ())?,
|
cleanup: (() -> ())?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,26 +100,36 @@ local function setCurrentScope(scope: EffectScope?)
|
||||||
return prevScope
|
return prevScope
|
||||||
end
|
end
|
||||||
|
|
||||||
local function pauseTracking()
|
local function addCleanup(e: EffectScope, cleanup: () -> ())
|
||||||
pauseStackCount += 1
|
local prevCleanup = e.cleanup
|
||||||
pauseStack[pauseStackCount] = activeSub
|
if prevCleanup then
|
||||||
activeSub = nil
|
e.cleanup = function()
|
||||||
|
prevCleanup()
|
||||||
|
cleanup()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
e.cleanup = cleanup
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function resumeTracking()
|
local function onEffectCleanup(cleanup: () -> ())
|
||||||
activeSub = pauseStack[pauseStackCount]
|
if activeSub then
|
||||||
pauseStack[pauseStackCount] = nil
|
addCleanup(activeSub :: Effect, cleanup)
|
||||||
pauseStackCount -= 1
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function cleanup(e: ReactiveNode)
|
local function onScopeDispose(cleanup: () -> ())
|
||||||
local e = e :: EffectScope
|
if activeScope then
|
||||||
local cleanup = e.cleanup
|
addCleanup(activeScope, cleanup)
|
||||||
if cleanup then
|
end
|
||||||
e.cleanup = nil
|
end
|
||||||
|
|
||||||
|
local function cleanupEffect(e: EffectScope)
|
||||||
|
if e.cleanup then
|
||||||
local prev = setCurrentSub(nil)
|
local prev = setCurrentSub(nil)
|
||||||
local success, result: unknown = pcall(cleanup)
|
local success, result: unknown = pcall(e.cleanup)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
|
e.cleanup = nil
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
|
|
@ -128,16 +138,15 @@ end
|
||||||
|
|
||||||
local function run(e: EffectScope, flags: ReactiveFlags): ()
|
local function run(e: EffectScope, flags: ReactiveFlags): ()
|
||||||
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(e.deps :: Link, e)) then
|
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(e.deps :: Link, e)) then
|
||||||
cleanup(e)
|
cleanupEffect(e)
|
||||||
local prev = setCurrentSub(e)
|
local prev = setCurrentSub(e)
|
||||||
startTracking(e)
|
startTracking(e)
|
||||||
local success, result = pcall((e :: Effect).fn)
|
local success, result: unknown = pcall((e :: Effect).fn)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
endTracking(e)
|
endTracking(e)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
e.cleanup = result
|
|
||||||
return
|
return
|
||||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||||
e.flags = bit32.band(flags, bit32.bnot(PENDING))
|
e.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||||
|
|
@ -182,6 +191,18 @@ local function endBatch()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function pauseTracking()
|
||||||
|
pauseStackCount += 1
|
||||||
|
pauseStack[pauseStackCount] = activeSub
|
||||||
|
activeSub = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resumeTracking()
|
||||||
|
activeSub = pauseStack[pauseStackCount]
|
||||||
|
pauseStack[pauseStackCount] = nil
|
||||||
|
pauseStackCount -= 1
|
||||||
|
end
|
||||||
|
|
||||||
local function updateComputed(c: Computed): boolean
|
local function updateComputed(c: Computed): boolean
|
||||||
local prevSub = setCurrentSub(c)
|
local prevSub = setCurrentSub(c)
|
||||||
startTracking(c)
|
startTracking(c)
|
||||||
|
|
@ -210,22 +231,8 @@ local function updateSignal(s: Signal, value: any): boolean
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function signalOper<T>(this: Signal<T>, ...: T): T?
|
local function signalGetterSetter<T>(this: Signal<T>): (() -> T, (value: ((T) -> T) | T) -> T)
|
||||||
if select("#", ...) > 0 then
|
local function getter(): T
|
||||||
local newValue = (...)
|
|
||||||
if this.value ~= newValue then
|
|
||||||
this.value = newValue
|
|
||||||
this.flags = bit32.bor(MUTABLE, DIRTY)
|
|
||||||
local subs = this.subs
|
|
||||||
if subs then
|
|
||||||
propagate(subs)
|
|
||||||
if batchDepth == 0 then
|
|
||||||
flush()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return
|
|
||||||
else
|
|
||||||
local value = this.value
|
local value = this.value
|
||||||
if bit32.band(this.flags, DIRTY) ~= 0 then
|
if bit32.band(this.flags, DIRTY) ~= 0 then
|
||||||
if updateSignal(this, value) then
|
if updateSignal(this, value) then
|
||||||
|
|
@ -240,64 +247,93 @@ local function signalOper<T>(this: Signal<T>, ...: T): T?
|
||||||
end
|
end
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
local function computedOper<T>(this: Computed<T>): T
|
local function setter(newValue: ((T) -> T) | T): T
|
||||||
local flags = this.flags
|
local value = this.value
|
||||||
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(this.deps :: Link, this)) then
|
local newValue = newValue :: T
|
||||||
if updateComputed(this) then
|
if type(newValue) == "function" then
|
||||||
|
local prev = setCurrentSub(nil)
|
||||||
|
local success, result = pcall(newValue, value)
|
||||||
|
setCurrentSub(prev)
|
||||||
|
if not success then
|
||||||
|
error(result)
|
||||||
|
end
|
||||||
|
newValue = result
|
||||||
|
end
|
||||||
|
if value ~= newValue then
|
||||||
|
this.value = newValue
|
||||||
|
this.flags = bit32.bor(MUTABLE, DIRTY)
|
||||||
local subs = this.subs
|
local subs = this.subs
|
||||||
if subs then
|
if subs then
|
||||||
shallowPropagate(subs)
|
propagate(subs)
|
||||||
|
if batchDepth == 0 then
|
||||||
|
flush()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
return newValue
|
||||||
this.flags = bit32.band(flags, bit32.bnot(PENDING))
|
|
||||||
end
|
end
|
||||||
if activeSub then
|
|
||||||
link(this, activeSub)
|
return getter, setter
|
||||||
elseif activeScope then
|
|
||||||
link(this, activeScope)
|
|
||||||
end
|
|
||||||
return this.value :: T
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function effectOper(this: EffectScope): ()
|
local function computedGetter<T>(this: Computed<T>): () -> T
|
||||||
local dep = this.deps
|
return function()
|
||||||
while dep do
|
local flags = this.flags
|
||||||
dep = unlink(dep, this)
|
if
|
||||||
|
bit32.band(flags, DIRTY) ~= 0
|
||||||
|
or (bit32.band(flags, PENDING) ~= 0 and checkDirty(this.deps :: Link, this))
|
||||||
|
then
|
||||||
|
if updateComputed(this) then
|
||||||
|
local subs = this.subs
|
||||||
|
if subs then
|
||||||
|
shallowPropagate(subs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||||
|
this.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||||
|
end
|
||||||
|
if activeSub then
|
||||||
|
link(this, activeSub)
|
||||||
|
elseif activeScope then
|
||||||
|
link(this, activeScope)
|
||||||
|
end
|
||||||
|
return this.value :: T
|
||||||
end
|
end
|
||||||
local sub = this.subs
|
|
||||||
while sub do
|
|
||||||
unlink(sub)
|
|
||||||
sub = this.subs
|
|
||||||
end
|
|
||||||
this.flags = NONE
|
|
||||||
cleanup(this)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local signalMetatable = { __call = signalOper }
|
local function effectStop(this: EffectScope): () -> ()
|
||||||
local computedMetatable = { __call = computedOper }
|
return function()
|
||||||
local effectMetatable = { __call = effectOper }
|
local dep = this.deps
|
||||||
|
while dep do
|
||||||
|
dep = unlink(dep, this)
|
||||||
|
end
|
||||||
|
local sub = this.subs
|
||||||
|
while sub do
|
||||||
|
unlink(sub)
|
||||||
|
sub = this.subs
|
||||||
|
end
|
||||||
|
this.flags = NONE
|
||||||
|
cleanupEffect(this)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function signal<T>(initialValue: T): ((T) -> ()) & (() -> T)
|
local function signal<T>(initialValue: T): (() -> T, (value: ((T) -> T) | T) -> T)
|
||||||
local this: Signal<T> = {
|
return signalGetterSetter({
|
||||||
previousValue = initialValue,
|
previousValue = initialValue,
|
||||||
value = initialValue,
|
value = initialValue,
|
||||||
flags = MUTABLE,
|
flags = MUTABLE,
|
||||||
}
|
})
|
||||||
return setmetatable(this, signalMetatable) :: any
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
||||||
local this: Computed<T> = {
|
return computedGetter({
|
||||||
flags = bit32.bor(MUTABLE, DIRTY),
|
flags = bit32.bor(MUTABLE, DIRTY),
|
||||||
getter = getter,
|
getter = getter,
|
||||||
}
|
})
|
||||||
return setmetatable(this, computedMetatable) :: any
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function effect(fn: () -> ...() -> ()?): () -> ()
|
local function effect(fn: () -> ()): () -> ()
|
||||||
local e: Effect = {
|
local e: Effect = {
|
||||||
fn = fn,
|
fn = fn,
|
||||||
flags = WATCHING,
|
flags = WATCHING,
|
||||||
|
|
@ -308,16 +344,15 @@ local function effect(fn: () -> ...() -> ()?): () -> ()
|
||||||
link(e, activeScope)
|
link(e, activeScope)
|
||||||
end
|
end
|
||||||
local prev = setCurrentSub(e)
|
local prev = setCurrentSub(e)
|
||||||
local success, result = pcall(fn)
|
local success, result: unknown = pcall(fn)
|
||||||
setCurrentSub(prev)
|
setCurrentSub(prev)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
e.cleanup = result
|
return effectStop(e)
|
||||||
return setmetatable(e, effectMetatable) :: any
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function effectScope(fn: () -> ...() -> ()?): () -> ()
|
local function effectScope(fn: () -> ()): () -> ()
|
||||||
local e: EffectScope = {
|
local e: EffectScope = {
|
||||||
flags = NONE,
|
flags = NONE,
|
||||||
}
|
}
|
||||||
|
|
@ -325,13 +360,12 @@ local function effectScope(fn: () -> ...() -> ()?): () -> ()
|
||||||
link(e, activeScope)
|
link(e, activeScope)
|
||||||
end
|
end
|
||||||
local prev = setCurrentScope(e)
|
local prev = setCurrentScope(e)
|
||||||
local success, result = pcall(fn)
|
local success, result: unknown = pcall(fn)
|
||||||
setCurrentScope(prev)
|
setCurrentScope(prev)
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
e.cleanup = result
|
return effectStop(e)
|
||||||
return setmetatable(e, effectMetatable) :: any
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function notify(e: ReactiveNode)
|
local function notify(e: ReactiveNode)
|
||||||
|
|
@ -343,7 +377,7 @@ local function notify(e: ReactiveNode)
|
||||||
notify(subs.sub :: Effect | EffectScope)
|
notify(subs.sub :: Effect | EffectScope)
|
||||||
else
|
else
|
||||||
queuedEffectsLength += 1
|
queuedEffectsLength += 1
|
||||||
queuedEffects[queuedEffectsLength] = e :: Effect | EffectScope
|
queuedEffects[queuedEffectsLength] = e :: Effect
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -364,7 +398,7 @@ local function unwatched(signal: ReactiveNode)
|
||||||
until not toRemove
|
until not toRemove
|
||||||
signal.flags = bit32.bor(signal.flags, DIRTY)
|
signal.flags = bit32.bor(signal.flags, DIRTY)
|
||||||
end
|
end
|
||||||
cleanup(signal)
|
cleanupEffect(signal :: EffectScope)
|
||||||
end
|
end
|
||||||
|
|
||||||
local reactiveSystem = createReactiveSystem({
|
local reactiveSystem = createReactiveSystem({
|
||||||
|
|
@ -393,6 +427,8 @@ return {
|
||||||
endBatch = endBatch,
|
endBatch = endBatch,
|
||||||
pauseTracking = pauseTracking,
|
pauseTracking = pauseTracking,
|
||||||
resumeTracking = resumeTracking,
|
resumeTracking = resumeTracking,
|
||||||
|
onEffectCleanup = onEffectCleanup,
|
||||||
|
onScopeDispose = onScopeDispose,
|
||||||
signal = signal,
|
signal = signal,
|
||||||
computed = computed,
|
computed = computed,
|
||||||
effect = effect,
|
effect = effect,
|
||||||
|
|
@ -8,9 +8,8 @@ local resumeTracking = signals.resumeTracking
|
||||||
local function subscribe<T>(selector: () -> T, listener: (T, T) -> ()): () -> ()
|
local function subscribe<T>(selector: () -> T, listener: (T, T) -> ()): () -> ()
|
||||||
local current: T
|
local current: T
|
||||||
local mounted = true
|
local mounted = true
|
||||||
local dispose: () -> ()
|
|
||||||
|
|
||||||
dispose = effect(function()
|
return effect(function()
|
||||||
local value = selector()
|
local value = selector()
|
||||||
|
|
||||||
if mounted then
|
if mounted then
|
||||||
|
|
@ -25,10 +24,6 @@ local function subscribe<T>(selector: () -> T, listener: (T, T) -> ()): () -> ()
|
||||||
resumeTracking()
|
resumeTracking()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return function()
|
|
||||||
dispose()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return subscribe
|
return subscribe
|
||||||
15
pkgs/react-charm/__tests__/spec.luau
Normal file
15
pkgs/react-charm/__tests__/spec.luau
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
spec("useAtom")
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
@ -3,7 +3,7 @@ local ServerScriptService = game:GetService("ServerScriptService")
|
||||||
|
|
||||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||||
local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm)
|
local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm)
|
||||||
local renderHook = require(ServerScriptService.Tests.utils.renderHook)
|
local renderHook = require(script.Parent.helpers.renderHook)
|
||||||
local suite = require(ServerScriptService.Tests.suite)
|
local suite = require(ServerScriptService.Tests.suite)
|
||||||
|
|
||||||
local atom = Charm.atom
|
local atom = Charm.atom
|
||||||
15
pkgs/vide-charm/__tests__/spec.luau
Normal file
15
pkgs/vide-charm/__tests__/spec.luau
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
spec("useAtom")
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
|
|
||||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
|
||||||
|
|
||||||
local signals = Charm.signals
|
|
||||||
|
|
||||||
local function propagate(
|
|
||||||
signal: <T>(T) -> (T?) -> T,
|
|
||||||
computed: <T>(() -> T) -> () -> T,
|
|
||||||
effect: (() -> ()) -> (),
|
|
||||||
w: number,
|
|
||||||
h: number
|
|
||||||
)
|
|
||||||
local src = signal(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
|
|
||||||
|
|
||||||
return {
|
|
||||||
Functions = {
|
|
||||||
charm = function()
|
|
||||||
propagate(Charm.atom, Charm.computed, Charm.effect, 10, 10)
|
|
||||||
end,
|
|
||||||
alien_signals = function()
|
|
||||||
propagate(signals.signal, signals.computed, signals.effect, 10, 10)
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
local Charm = require("../charm")
|
|
||||||
|
|
||||||
local atom = Charm.atom
|
|
||||||
local computed = Charm.computed
|
|
||||||
local effect = Charm.effect
|
|
||||||
|
|
||||||
local signal = Charm.signals.signal
|
|
||||||
local computedSignal = Charm.signals.computed
|
|
||||||
local effectSignal = Charm.signals.effect
|
|
||||||
|
|
||||||
local w = 10
|
|
||||||
local h = 10
|
|
||||||
|
|
||||||
local function bench(name: string, test: () -> ())
|
|
||||||
local start = os.clock()
|
|
||||||
local iter = 0
|
|
||||||
for _ = 1, 15000 do
|
|
||||||
iter += 1
|
|
||||||
test()
|
|
||||||
end
|
|
||||||
local endTime = os.clock()
|
|
||||||
local elapsed = endTime - start
|
|
||||||
print(`{name}: {elapsed / iter * 1000 * 1000} us/iter`)
|
|
||||||
end
|
|
||||||
|
|
||||||
bench(`charm {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)
|
|
||||||
|
|
||||||
bench(`alien-signals {w} * {h}`, function()
|
|
||||||
local src = signal(1)
|
|
||||||
for _ = 1, w do
|
|
||||||
local last = src
|
|
||||||
for _ = 1, h do
|
|
||||||
local prev = last
|
|
||||||
last = computedSignal(function()
|
|
||||||
return prev() + 1
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
effectSignal(function()
|
|
||||||
last()
|
|
||||||
return nil
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
src(src() + 1)
|
|
||||||
end)
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
local suite = require("../suite")
|
|
||||||
|
|
||||||
local describe = suite.describe
|
|
||||||
|
|
||||||
local function test(name: string)
|
|
||||||
describe(name, function()
|
|
||||||
return require(`./{name}.spec`) :: any
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test("client")
|
|
||||||
test("server")
|
|
||||||
test("flatten")
|
|
||||||
test("patch")
|
|
||||||
test("validate")
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
return if game
|
|
||||||
then require(game:GetService("ReplicatedStorage").Packages.Charm)
|
|
||||||
else require("../packages/charm/src/lib")
|
|
||||||
14
tests/init.luau
Normal file
14
tests/init.luau
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
|
local describe = suite.describe
|
||||||
|
local finish = suite.finish
|
||||||
|
|
||||||
|
if not table.isfrozen(_G) then
|
||||||
|
_G.__DEV__ = true
|
||||||
|
end
|
||||||
|
|
||||||
|
describe("charm", function()
|
||||||
|
require("@pkgs/charm/__tests__/spec")
|
||||||
|
end)
|
||||||
|
|
||||||
|
finish()
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
local suite = require("../suite")
|
|
||||||
|
|
||||||
local describe = suite.describe
|
|
||||||
|
|
||||||
local function test(name: string)
|
|
||||||
describe(name, function()
|
|
||||||
return require(`./{name}.spec`) :: any
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test("useAtom")
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
local suite = require("../suite")
|
|
||||||
|
|
||||||
local describe = suite.describe
|
|
||||||
|
|
||||||
local function test(name: string)
|
|
||||||
describe(name, function()
|
|
||||||
return require(`./{name}.spec`) :: any
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test("computed")
|
|
||||||
test("effect")
|
|
||||||
test("effectScope")
|
|
||||||
test("issue_48")
|
|
||||||
test("topology")
|
|
||||||
test("untrack")
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
@ -3,19 +3,18 @@ local suite = require("./suite")
|
||||||
local describe = suite.describe
|
local describe = suite.describe
|
||||||
local finish = suite.finish
|
local finish = suite.finish
|
||||||
|
|
||||||
local function test(name: string)
|
_G.__DEV__ = true
|
||||||
describe(name, function()
|
|
||||||
return require(`./{name}/spec`) :: any
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test("signals")
|
describe("charm-sync", function()
|
||||||
|
require(script.Parent.CharmSync.spec)
|
||||||
|
end)
|
||||||
|
|
||||||
if game then
|
describe("react-charm", function()
|
||||||
_G.__DEV__ = true
|
require(script.Parent.ReactCharm.spec)
|
||||||
test("charm-sync")
|
end)
|
||||||
test("react-charm")
|
|
||||||
test("vide-charm")
|
describe("vide-charm", function()
|
||||||
end
|
require(script.Parent.VideCharm.spec)
|
||||||
|
end)
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
--[=[
|
|
||||||
Trigger a garbage collection cycle by allocating a large amount of junk memory
|
|
||||||
and then waiting for the next frame.
|
|
||||||
]=]
|
|
||||||
local function collect()
|
|
||||||
for _ = 1, 1e4 do
|
|
||||||
local _ = table.create(1e3)
|
|
||||||
end
|
|
||||||
task.wait()
|
|
||||||
end
|
|
||||||
|
|
||||||
return collect
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
--[=[
|
|
||||||
Counts the number of elements in a table.
|
|
||||||
|
|
||||||
@param object The table to count.
|
|
||||||
@return The number of elements in the table.
|
|
||||||
]=]
|
|
||||||
local function count(object: any): number
|
|
||||||
local count = 0
|
|
||||||
for _ in next, object do
|
|
||||||
count += 1
|
|
||||||
end
|
|
||||||
return count
|
|
||||||
end
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
local suite = require("../suite")
|
|
||||||
|
|
||||||
local describe = suite.describe
|
|
||||||
|
|
||||||
local function test(name: string)
|
|
||||||
describe(name, function()
|
|
||||||
return require(`./{name}.spec`) :: any
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
test("useAtom")
|
|
||||||
|
|
||||||
return {}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue