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
|
||||
|
||||
- name: Run tests
|
||||
run: sh bin/test.sh
|
||||
run: lute tests/init.luau
|
||||
|
||||
- name: Build models
|
||||
run: sh bin/build.sh
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,7 +1,7 @@
|
|||
/.vscode
|
||||
/build
|
||||
/roblox_packages
|
||||
/packages/**/wally.lock
|
||||
/pkgs/**/wally.lock
|
||||
/sourcemap.json
|
||||
*.tsbuildinfo
|
||||
*.rbxm
|
||||
|
|
|
|||
4
.luaurc
4
.luaurc
|
|
@ -2,5 +2,9 @@
|
|||
"languageMode": "strict",
|
||||
"lint": {
|
||||
"LocalShadow": false
|
||||
},
|
||||
"aliases": {
|
||||
"tests": "tests",
|
||||
"pkgs": "packages"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
mkdir build
|
||||
rojo build packages/charm -o build/charm.rbxm
|
||||
rojo build packages/charm-sync -o build/charm-sync.rbxm
|
||||
rojo build packages/react-charm -o build/react-charm.rbxm
|
||||
rojo build packages/vide-charm -o build/vide-charm.rbxm
|
||||
rojo build pkgs/charm -o build/charm.rbxm
|
||||
rojo build pkgs/charm-sync -o build/charm-sync.rbxm
|
||||
rojo build pkgs/react-charm -o build/react-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",
|
||||
"Packages": {
|
||||
"$path": "roblox_packages",
|
||||
"Charm": { "$path": "packages/charm" },
|
||||
"CharmSync": { "$path": "packages/charm-sync" },
|
||||
"ReactCharm": { "$path": "packages/react-charm" },
|
||||
"VideCharm": { "$path": "packages/vide-charm" }
|
||||
"Charm": { "$path": "pkgs/charm" },
|
||||
"CharmSync": { "$path": "pkgs/charm-sync" },
|
||||
"ReactCharm": { "$path": "pkgs/react-charm" },
|
||||
"VideCharm": { "$path": "pkgs/vide-charm" }
|
||||
}
|
||||
},
|
||||
"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)
|
||||
|
||||
for user = 1, USERS do
|
||||
if math.random() > 0.5 then
|
||||
continue
|
||||
end
|
||||
local rng = math.random()
|
||||
|
||||
state.users[user] = {
|
||||
coins = math.random(1, 2),
|
||||
gems = math.random(1, 2),
|
||||
}
|
||||
if rng < 1 / 3 then
|
||||
continue
|
||||
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
|
||||
|
||||
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 atom = Charm.atom
|
||||
|
||||
local cleanup: () -> ()
|
||||
|
||||
return {
|
||||
AfterEach = function()
|
||||
cleanup()
|
||||
end,
|
||||
Functions = {
|
||||
atom = function()
|
||||
local state = atom(0)
|
||||
|
||||
cleanup = effect(function()
|
||||
effect(function()
|
||||
state()
|
||||
end)
|
||||
|
||||
|
|
@ -32,7 +27,7 @@ return {
|
|||
return 2 * state()
|
||||
end)
|
||||
|
||||
cleanup = effect(function()
|
||||
effect(function()
|
||||
double()
|
||||
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 suite = require("../suite")
|
||||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
|
||||
local signals = charm.signals
|
||||
local computed = signals.computed
|
||||
local signal = signals.signal
|
||||
local test = suite.test
|
||||
|
||||
test("should correctly propagate changes through computed signals", function()
|
||||
local src = signal(0)
|
||||
local src, setSrc = signal(0)
|
||||
local c1 = computed(function()
|
||||
return src() % 2
|
||||
end)
|
||||
|
|
@ -19,15 +18,15 @@ test("should correctly propagate changes through computed signals", function()
|
|||
end)
|
||||
|
||||
c3()
|
||||
src(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
|
||||
setSrc(1) -- c1 -> dirty, c2 -> toCheckDirty, c3 -> toCheckDirty
|
||||
c2() -- c1 -> none, c2 -> none
|
||||
src(3) -- c1 -> dirty, c2 -> toCheckDirty
|
||||
setSrc(3) -- c1 -> dirty, c2 -> toCheckDirty
|
||||
|
||||
assert(c3() == 1, "bad final value")
|
||||
end)
|
||||
|
||||
test("should propagate updated source value through chained computations", function()
|
||||
local src = signal(0)
|
||||
local src, setSrc = signal(0)
|
||||
local a = computed(function()
|
||||
return src()
|
||||
end)
|
||||
|
|
@ -42,12 +41,12 @@ test("should propagate updated source value through chained computations", funct
|
|||
end)
|
||||
|
||||
assert(d() == 0, "bad initial value")
|
||||
src(2)
|
||||
setSrc(2)
|
||||
assert(d() == 2, "bad updated value")
|
||||
end)
|
||||
|
||||
test("should handle flags are indirectly updated during checkDirty", function()
|
||||
local a = signal(false)
|
||||
local a, setA = signal(false)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -61,22 +60,22 @@ test("should handle flags are indirectly updated during checkDirty", function()
|
|||
end)
|
||||
|
||||
assert(d() == false, "bad initial value")
|
||||
a(true)
|
||||
setA(true)
|
||||
assert(d() == true, "bad updated value")
|
||||
end)
|
||||
|
||||
test("should not update if the signal value is reverted", function()
|
||||
local times = 0
|
||||
|
||||
local src = signal(0)
|
||||
local src, setSrc = signal(0)
|
||||
local c1 = computed(function()
|
||||
times += 1
|
||||
return src()
|
||||
end)
|
||||
c1()
|
||||
assert(times == 1, "computed did not run")
|
||||
src(1)
|
||||
src(0)
|
||||
setSrc(1)
|
||||
setSrc(0)
|
||||
c1()
|
||||
assert(times == 1, "computed ran when it should not")
|
||||
end)
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
local charm = require("../charm")
|
||||
local suite = require("../suite")
|
||||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
|
||||
local signals = charm.signals
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local endBatch = signals.endBatch
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local signal = signals.signal
|
||||
local startBatch = signals.startBatch
|
||||
local test = suite.test
|
||||
|
|
@ -15,7 +15,7 @@ local test = suite.test
|
|||
test("should clear subscriptions when untracked by all subscribers", function()
|
||||
local bRunTimes = 0
|
||||
|
||||
local a = signal(1)
|
||||
local a, setA = signal(1)
|
||||
local b = computed(function()
|
||||
bRunTimes += 1
|
||||
return a() * 2
|
||||
|
|
@ -25,15 +25,15 @@ test("should clear subscriptions when untracked by all subscribers", function()
|
|||
end)
|
||||
|
||||
assert(bRunTimes == 1, "should run initially")
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(bRunTimes == 2, "should run after change")
|
||||
stopEffect()
|
||||
a(3)
|
||||
setA(3)
|
||||
assert(bRunTimes == 2, "should not run after stop")
|
||||
end)
|
||||
|
||||
test("should not run untracked inner effect", function()
|
||||
local a = signal(3)
|
||||
local a, setA = signal(3)
|
||||
local b = computed(function()
|
||||
return a() > 0
|
||||
end)
|
||||
|
|
@ -48,14 +48,14 @@ test("should not run untracked inner effect", function()
|
|||
end
|
||||
end)
|
||||
|
||||
a(2)
|
||||
a(1)
|
||||
a(0)
|
||||
setA(2)
|
||||
setA(1)
|
||||
setA(0)
|
||||
end)
|
||||
|
||||
test("should run outer effect first", function()
|
||||
local a = signal(1)
|
||||
local b = signal(1)
|
||||
local a, setA = signal(1)
|
||||
local b, setB = signal(1)
|
||||
|
||||
effect(function()
|
||||
if a() > 0 then
|
||||
|
|
@ -69,13 +69,13 @@ test("should run outer effect first", function()
|
|||
end)
|
||||
|
||||
startBatch()
|
||||
b(0)
|
||||
a(0)
|
||||
setB(0)
|
||||
setA(0)
|
||||
endBatch()
|
||||
end)
|
||||
|
||||
test("should not trigger inner effect when resolve maybe dirty", function()
|
||||
local a = signal(0)
|
||||
local a, setA = signal(0)
|
||||
local b = computed(function()
|
||||
return a() % 2
|
||||
end)
|
||||
|
|
@ -92,12 +92,12 @@ test("should not trigger inner effect when resolve maybe dirty", function()
|
|||
end)
|
||||
end)
|
||||
|
||||
a(2)
|
||||
setA(2)
|
||||
end)
|
||||
|
||||
test("should trigger inner effects in sequence", function()
|
||||
local a = signal(0)
|
||||
local b = signal(0)
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local c = computed(function()
|
||||
return a() - b()
|
||||
end)
|
||||
|
|
@ -121,16 +121,16 @@ test("should trigger inner effects in sequence", function()
|
|||
table.clear(order)
|
||||
|
||||
startBatch()
|
||||
b(1)
|
||||
a(1)
|
||||
setB(1)
|
||||
setA(1)
|
||||
endBatch()
|
||||
|
||||
assert(table.concat(order, ", ") == "first inner, last inner", `wrong order: {table.concat(order, ", ")}`)
|
||||
end)
|
||||
|
||||
test("should trigger inner effects in sequence in effect scope", function()
|
||||
local a = signal(0)
|
||||
local b = signal(0)
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effectScope(function()
|
||||
|
|
@ -149,8 +149,8 @@ test("should trigger inner effects in sequence in effect scope", function()
|
|||
table.clear(order)
|
||||
|
||||
startBatch()
|
||||
b(1)
|
||||
a(1)
|
||||
setB(1)
|
||||
setA(1)
|
||||
endBatch()
|
||||
|
||||
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 a = signal(0)
|
||||
local b = signal(0)
|
||||
local b, setB = signal(0)
|
||||
|
||||
local aa = computed(function()
|
||||
table.insert(logs, "aa-0")
|
||||
if a() == 0 then
|
||||
b(1)
|
||||
setB(1)
|
||||
end
|
||||
table.insert(logs, "aa-1")
|
||||
return nil
|
||||
|
|
@ -197,8 +197,8 @@ test("should custom effect support batch", function()
|
|||
end)
|
||||
|
||||
test("should duplicate subscribers do not affect the notify order", function()
|
||||
local src1 = signal(0)
|
||||
local src2 = signal(0)
|
||||
local src1, setSrc1 = signal(0)
|
||||
local src2, setSrc2 = signal(0)
|
||||
local order: { string } = {}
|
||||
|
||||
effect(function()
|
||||
|
|
@ -216,17 +216,17 @@ test("should duplicate subscribers do not affect the notify order", function()
|
|||
table.insert(order, "b")
|
||||
src1()
|
||||
end)
|
||||
src2(1) -- src1.subs: a -> b -> a
|
||||
setSrc2(1) -- src1.subs: a -> b -> a
|
||||
|
||||
table.clear(order)
|
||||
src1(src1() + 1)
|
||||
setSrc1(src1() + 1)
|
||||
|
||||
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should handle side effect with inner effects", function()
|
||||
local a = signal(0)
|
||||
local b = signal(0)
|
||||
local a, setA = signal(0)
|
||||
local b, setB = signal(0)
|
||||
local order: { string } = {}
|
||||
|
||||
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)}`)
|
||||
|
||||
table.clear(order)
|
||||
b(1)
|
||||
a(1)
|
||||
setB(1)
|
||||
setA(1)
|
||||
assert(table.concat(order) == "ba", `wrong order: {table.concat(order)}`)
|
||||
end)
|
||||
end)
|
||||
|
||||
test("should handle flags are indirectly updated during checkDirty", function()
|
||||
local a = signal(false)
|
||||
local a, setA = signal(false)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -268,71 +268,57 @@ test("should handle flags are indirectly updated during checkDirty", function()
|
|||
triggers += 1
|
||||
end)
|
||||
assert(triggers == 1, "did not run initially")
|
||||
a(true)
|
||||
setA(true)
|
||||
assert(triggers == 2, "did not update indirectly")
|
||||
end)
|
||||
|
||||
test("should run effect cleanup", function()
|
||||
local cleanups = 0
|
||||
test("should run cleanups in sequence", function()
|
||||
local order = {}
|
||||
|
||||
local a = signal(false)
|
||||
local cleanup = effect(function()
|
||||
local a, setA = signal(false)
|
||||
local stopEffect = effect(function()
|
||||
a()
|
||||
return function()
|
||||
cleanups += 1
|
||||
end
|
||||
end)
|
||||
|
||||
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
|
||||
onEffectCleanup(function()
|
||||
table.insert(order, "a")
|
||||
end)
|
||||
onEffectCleanup(function()
|
||||
table.insert(order, "b")
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
a(true)
|
||||
assert(cleanups == 1, "should cleanup once")
|
||||
stopScope()
|
||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
||||
a(false)
|
||||
assert(cleanups == 2, "should not cleanup after update")
|
||||
assert(table.concat(order) == "", "should not cleanup immediately")
|
||||
|
||||
setA(true)
|
||||
assert(table.concat(order) == "ab", `bad update: {table.concat(order)}`)
|
||||
|
||||
table.clear(order)
|
||||
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)
|
||||
|
||||
test("should run inner effect cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local a = signal(false)
|
||||
local stopScope = effect(function()
|
||||
local a, setA = signal(false)
|
||||
local stopEffect = effect(function()
|
||||
a()
|
||||
effect(function()
|
||||
return function()
|
||||
onEffectCleanup(function()
|
||||
cleanups += 1
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
assert(cleanups == 0, "should not cleanup immediately")
|
||||
a(true)
|
||||
setA(true)
|
||||
assert(cleanups == 1, "should cleanup after outer update")
|
||||
stopScope()
|
||||
stopEffect()
|
||||
assert(cleanups == 2, "should cleanup after outer dispose")
|
||||
a(false)
|
||||
setA(false)
|
||||
assert(cleanups == 2, "should not cleanup after update")
|
||||
end)
|
||||
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
local charm = require("../charm")
|
||||
local suite = require("../suite")
|
||||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
|
||||
local signals = charm.signals
|
||||
local effect = signals.effect
|
||||
local effectScope = signals.effectScope
|
||||
local signal = signals.signal
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local onScopeDispose = signals.onScopeDispose
|
||||
local test = suite.test
|
||||
|
||||
test("should not trigger after stop", function()
|
||||
local count = signal(1)
|
||||
local count, setCount = signal(1)
|
||||
|
||||
local triggers = 0
|
||||
|
||||
|
|
@ -19,42 +20,26 @@ test("should not trigger after stop", function()
|
|||
end)
|
||||
assert(triggers == 1, "did not run initially")
|
||||
|
||||
count(2)
|
||||
setCount(2)
|
||||
assert(triggers == 2, "did not run after update")
|
||||
end)
|
||||
|
||||
count(3)
|
||||
setCount(3)
|
||||
assert(triggers == 3, "did not run after update outside scope")
|
||||
stopScope()
|
||||
count(4)
|
||||
setCount(4)
|
||||
assert(triggers == 3, "ran after stop")
|
||||
end)
|
||||
|
||||
test("should run effect 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()
|
||||
test("should run indirect scope cleanup", function()
|
||||
local cleanups = 0
|
||||
|
||||
local cleanup
|
||||
local stopScope = effectScope(function()
|
||||
cleanup = effectScope(function()
|
||||
return function()
|
||||
onScopeDispose(function()
|
||||
cleanups += 1
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
|
@ -65,4 +50,26 @@ test("should run indirect effect cleanup", function()
|
|||
assert(cleanups == 1, "should skip redundant dispose")
|
||||
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 {}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
local charm = require("../charm")
|
||||
local suite = require("../suite")
|
||||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
|
||||
local signals = charm.signals
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local pauseTracking = signals.pauseTracking
|
||||
|
|
@ -92,7 +91,7 @@ local function reaction<T>(
|
|||
end
|
||||
|
||||
test("#48", function()
|
||||
local source = signal(0)
|
||||
local source, setSource = signal(0)
|
||||
local disposeInner: () -> ()
|
||||
|
||||
reaction(function()
|
||||
|
|
@ -107,9 +106,9 @@ test("#48", function()
|
|||
end
|
||||
end)
|
||||
|
||||
source(1)
|
||||
source(2)
|
||||
source(3)
|
||||
setSource(1)
|
||||
setSource(2)
|
||||
setSource(3)
|
||||
end)
|
||||
|
||||
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 suite = require("../suite")
|
||||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
|
||||
local signals = charm.signals
|
||||
local computed = signals.computed
|
||||
local effect = signals.effect
|
||||
local signal = signals.signal
|
||||
|
|
@ -44,7 +43,7 @@ describe("graph updates", function()
|
|||
-- C
|
||||
-- |
|
||||
-- D
|
||||
local a = signal(2)
|
||||
local a, setA = signal(2)
|
||||
|
||||
local b = computed(function()
|
||||
return a() - 1
|
||||
|
|
@ -65,7 +64,7 @@ describe("graph updates", function()
|
|||
assert(computeCalls == 1, "compute should be called once")
|
||||
computeCalls = 0
|
||||
|
||||
a(4)
|
||||
setA(4)
|
||||
d()
|
||||
assert(computeCalls == 1, "compute should drop update")
|
||||
end)
|
||||
|
|
@ -79,7 +78,7 @@ describe("graph updates", function()
|
|||
-- \ /
|
||||
-- D
|
||||
|
||||
local a = signal("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -97,7 +96,7 @@ describe("graph updates", function()
|
|||
assert(d() == "a a", "d should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
assert(d() == "aa aa", "d should be 'aa aa'")
|
||||
assert(spyCalls == 2, "compute should be called only twice")
|
||||
end)
|
||||
|
|
@ -112,7 +111,7 @@ describe("graph updates", function()
|
|||
-- |
|
||||
-- E
|
||||
|
||||
local a = signal("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -134,7 +133,7 @@ describe("graph updates", function()
|
|||
assert(e() == "a a", "e should be 'a a'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
assert(e() == "aa aa", "e should be 'aa aa'")
|
||||
assert(spyCalls == 2, "compute should be called only twice")
|
||||
end)
|
||||
|
|
@ -142,7 +141,7 @@ describe("graph updates", function()
|
|||
test("should bail out if result is the same", function()
|
||||
-- Bail out if value of "B" never changes
|
||||
-- A->B->C
|
||||
local a = signal("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "foo"
|
||||
|
|
@ -158,7 +157,7 @@ describe("graph updates", function()
|
|||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should be called once")
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
assert(c() == "foo", "c should be 'foo'")
|
||||
assert(spyCalls == 1, "compute should bail out")
|
||||
end)
|
||||
|
|
@ -174,7 +173,7 @@ describe("graph updates", function()
|
|||
-- E
|
||||
-- / \
|
||||
-- F G
|
||||
local a = signal("a")
|
||||
local a, setA = signal("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
|
|
@ -223,7 +222,7 @@ describe("graph updates", function()
|
|||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
a("b")
|
||||
setA("b")
|
||||
|
||||
assert(e() == "b b", "e should be 'b b'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
|
@ -238,7 +237,7 @@ describe("graph updates", function()
|
|||
fSpyCalls, fSpyTime = 0, 0
|
||||
gSpyCalls, gSpyTime = 0, 0
|
||||
|
||||
a("c")
|
||||
setA("c")
|
||||
|
||||
assert(e() == "c c", "e should be 'c c'")
|
||||
assert(eSpyCalls == 1, "e should run once after update")
|
||||
|
|
@ -259,7 +258,7 @@ describe("graph updates", function()
|
|||
-- *A
|
||||
-- / \
|
||||
-- *B C <- we don't listen to C
|
||||
local a = signal("a")
|
||||
local a, setA = signal("a")
|
||||
|
||||
local b = computed(function()
|
||||
return a()
|
||||
|
|
@ -274,7 +273,7 @@ describe("graph updates", function()
|
|||
assert(b() == "a", "b should be 'a'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
assert(b() == "aa", "b should be 'aa'")
|
||||
assert(spyCalls == 0, "compute should not run")
|
||||
end)
|
||||
|
|
@ -288,7 +287,7 @@ describe("graph updates", function()
|
|||
-- *B D <- we don't listen to C
|
||||
-- |
|
||||
-- *C
|
||||
local a = signal("a")
|
||||
local a, setA = signal("a")
|
||||
local spyBCalls = 0
|
||||
local spyB = function()
|
||||
spyBCalls += 1
|
||||
|
|
@ -319,7 +318,7 @@ describe("graph updates", function()
|
|||
spyCCalls = 0
|
||||
unsub()
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
|
||||
assert(spyBCalls == 0, "b 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
|
||||
-- \ /
|
||||
-- D
|
||||
local a = signal("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -354,7 +353,7 @@ describe("graph updates", function()
|
|||
assert(d() == "a c", "d should be 'a c'")
|
||||
spyResult = nil
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
d()
|
||||
assert(spyResult == "aa c", "d should have returned 'aa c'")
|
||||
end)
|
||||
|
|
@ -368,7 +367,7 @@ describe("graph updates", function()
|
|||
-- B *C *D
|
||||
-- \ | /
|
||||
-- E
|
||||
local a = signal("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -390,13 +389,13 @@ describe("graph updates", function()
|
|||
assert(e() == "a c d", "e should be 'a c d'")
|
||||
spyResult = nil
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
e()
|
||||
assert(spyResult == "aa c d", "e should have returned 'aa c d'")
|
||||
end)
|
||||
|
||||
test("should support lazy branches", function()
|
||||
local a = signal(0)
|
||||
local a, setA = signal(0)
|
||||
local b = computed(function()
|
||||
return a()
|
||||
end)
|
||||
|
|
@ -405,10 +404,10 @@ describe("graph updates", function()
|
|||
end)
|
||||
|
||||
assert(c() == 0, "c should be 0")
|
||||
a(1)
|
||||
setA(1)
|
||||
assert(c() == 1, "c should be 1")
|
||||
|
||||
a(0)
|
||||
setA(0)
|
||||
assert(c() == 0, "c should be 0 again")
|
||||
end)
|
||||
|
||||
|
|
@ -420,7 +419,7 @@ describe("graph updates", function()
|
|||
-- *B *C
|
||||
-- \ /
|
||||
-- D
|
||||
local a = signal("a")
|
||||
local a, setA = signal("a")
|
||||
local b = computed(function()
|
||||
a()
|
||||
return "b"
|
||||
|
|
@ -439,14 +438,14 @@ describe("graph updates", function()
|
|||
assert(d() == "b c", "d should be 'b c'")
|
||||
spyCalls = 0
|
||||
|
||||
a("aa")
|
||||
setA("aa")
|
||||
assert(spyCalls == 0, "d should not run")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("error handling", function()
|
||||
test("should keep graph consistent on errors during activation", function()
|
||||
local a = signal(0)
|
||||
local a, setA = signal(0)
|
||||
local b = computed(function()
|
||||
error("fail")
|
||||
end)
|
||||
|
|
@ -460,12 +459,12 @@ describe("error handling", function()
|
|||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
a(1)
|
||||
setA(1)
|
||||
assert(c() == 1, "c should recover and return 1")
|
||||
end)
|
||||
|
||||
test("should keep graph consistent on errors in computeds", function()
|
||||
local a = signal(0)
|
||||
local a, setA = signal(0)
|
||||
local b = computed(function()
|
||||
if a() == 1 then
|
||||
error("fail")
|
||||
|
|
@ -478,14 +477,14 @@ describe("error handling", function()
|
|||
|
||||
assert(c() == 0, "c should be 0")
|
||||
|
||||
a(1)
|
||||
setA(1)
|
||||
local success, result = pcall(function()
|
||||
b()
|
||||
end)
|
||||
assert(not success, "should throw")
|
||||
assert(string.match(result, "fail"), "should throw fail")
|
||||
|
||||
a(2)
|
||||
setA(2)
|
||||
assert(c() == 2, "c should recover and return 2")
|
||||
end)
|
||||
end)
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
local charm = require("../charm")
|
||||
local suite = require("../suite")
|
||||
local signals = require("../../src/signals")
|
||||
local suite = require("@tests/suite")
|
||||
|
||||
local signals = charm.signals
|
||||
local computed = signals.computed
|
||||
local pauseTracking = signals.pauseTracking
|
||||
local resumeTracking = signals.resumeTracking
|
||||
|
|
@ -9,7 +8,7 @@ local signal = signals.signal
|
|||
local test = suite.test
|
||||
|
||||
test("should pause tracking", function()
|
||||
local src = signal(0)
|
||||
local src, setSrc = signal(0)
|
||||
local c = computed(function()
|
||||
pauseTracking()
|
||||
local value = src()
|
||||
|
|
@ -18,7 +17,7 @@ test("should pause tracking", function()
|
|||
end)
|
||||
assert(c() == 0, "should not track src")
|
||||
|
||||
src(1)
|
||||
setSrc(1)
|
||||
assert(c() == 0, "should not track src")
|
||||
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 effectScope = signals.effectScope
|
||||
local onEffectCleanup = signals.onEffectCleanup
|
||||
local onScopeDispose = signals.onScopeDispose
|
||||
|
||||
return {
|
||||
atom = atom,
|
||||
|
|
@ -27,5 +29,7 @@ return {
|
|||
observe = observe,
|
||||
subscribe = subscribe,
|
||||
untracked = untracked,
|
||||
onEffectCleanup = onEffectCleanup,
|
||||
onScopeDispose = onScopeDispose,
|
||||
signals = signals,
|
||||
}
|
||||
|
|
@ -6,40 +6,31 @@ local pauseTracking = signals.pauseTracking
|
|||
local resumeTracking = signals.resumeTracking
|
||||
|
||||
local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||
local disposed = false
|
||||
local mounted = true
|
||||
local disposeImmediately = false
|
||||
local current: T?
|
||||
local dispose: () -> ()
|
||||
|
||||
local function disposeSafe()
|
||||
disposed = true
|
||||
if dispose then
|
||||
dispose()
|
||||
end
|
||||
local function dispose()
|
||||
disposeImmediately = true
|
||||
end
|
||||
|
||||
dispose = effect(function()
|
||||
local value = selector()
|
||||
|
||||
if mounted then
|
||||
mounted = false
|
||||
elseif value == current then
|
||||
return
|
||||
if value ~= current then
|
||||
local previous = current
|
||||
current = value
|
||||
|
||||
pauseTracking()
|
||||
spawn(listener, value, previous, dispose)
|
||||
resumeTracking()
|
||||
end
|
||||
|
||||
local previous = current
|
||||
current = value
|
||||
|
||||
pauseTracking()
|
||||
spawn(listener, value, previous, disposeSafe)
|
||||
resumeTracking()
|
||||
end)
|
||||
|
||||
if disposed then
|
||||
if disposeImmediately then
|
||||
dispose()
|
||||
end
|
||||
|
||||
return disposeSafe
|
||||
return dispose
|
||||
end
|
||||
|
||||
return listen
|
||||
|
|
@ -19,7 +19,7 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, transform: (
|
|||
resumeTracking()
|
||||
|
||||
if not success then
|
||||
error(`observe() transform function failed: {valueOut}`)
|
||||
error(`mapped() transform function failed: {valueOut}`)
|
||||
end
|
||||
|
||||
return valueOut, keyOut
|
||||
|
|
@ -47,6 +47,10 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, transform: (
|
|||
return mappedItems
|
||||
end
|
||||
|
||||
if previousItems == currentItems then
|
||||
return previousMappedItems
|
||||
end
|
||||
|
||||
local mappedItems = table.clone(previousMappedItems)
|
||||
|
||||
-- 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/stackblitz/alien-signals
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ type EffectScope = ReactiveNode & {
|
|||
}
|
||||
|
||||
type Effect = ReactiveNode & {
|
||||
fn: () -> (() -> ())?,
|
||||
fn: () -> (),
|
||||
cleanup: (() -> ())?,
|
||||
}
|
||||
|
||||
|
|
@ -100,26 +100,36 @@ local function setCurrentScope(scope: EffectScope?)
|
|||
return prevScope
|
||||
end
|
||||
|
||||
local function pauseTracking()
|
||||
pauseStackCount += 1
|
||||
pauseStack[pauseStackCount] = activeSub
|
||||
activeSub = nil
|
||||
local function addCleanup(e: EffectScope, cleanup: () -> ())
|
||||
local prevCleanup = e.cleanup
|
||||
if prevCleanup then
|
||||
e.cleanup = function()
|
||||
prevCleanup()
|
||||
cleanup()
|
||||
end
|
||||
else
|
||||
e.cleanup = cleanup
|
||||
end
|
||||
end
|
||||
|
||||
local function resumeTracking()
|
||||
activeSub = pauseStack[pauseStackCount]
|
||||
pauseStack[pauseStackCount] = nil
|
||||
pauseStackCount -= 1
|
||||
local function onEffectCleanup(cleanup: () -> ())
|
||||
if activeSub then
|
||||
addCleanup(activeSub :: Effect, cleanup)
|
||||
end
|
||||
end
|
||||
|
||||
local function cleanup(e: ReactiveNode)
|
||||
local e = e :: EffectScope
|
||||
local cleanup = e.cleanup
|
||||
if cleanup then
|
||||
e.cleanup = nil
|
||||
local function onScopeDispose(cleanup: () -> ())
|
||||
if activeScope then
|
||||
addCleanup(activeScope, cleanup)
|
||||
end
|
||||
end
|
||||
|
||||
local function cleanupEffect(e: EffectScope)
|
||||
if e.cleanup then
|
||||
local prev = setCurrentSub(nil)
|
||||
local success, result: unknown = pcall(cleanup)
|
||||
local success, result: unknown = pcall(e.cleanup)
|
||||
setCurrentSub(prev)
|
||||
e.cleanup = nil
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
|
|
@ -128,16 +138,15 @@ end
|
|||
|
||||
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
|
||||
cleanup(e)
|
||||
cleanupEffect(e)
|
||||
local prev = setCurrentSub(e)
|
||||
startTracking(e)
|
||||
local success, result = pcall((e :: Effect).fn)
|
||||
local success, result: unknown = pcall((e :: Effect).fn)
|
||||
setCurrentSub(prev)
|
||||
endTracking(e)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
e.cleanup = result
|
||||
return
|
||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||
e.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
|
|
@ -182,6 +191,18 @@ local function endBatch()
|
|||
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 prevSub = setCurrentSub(c)
|
||||
startTracking(c)
|
||||
|
|
@ -210,22 +231,8 @@ local function updateSignal(s: Signal, value: any): boolean
|
|||
end
|
||||
end
|
||||
|
||||
local function signalOper<T>(this: Signal<T>, ...: T): T?
|
||||
if select("#", ...) > 0 then
|
||||
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 function signalGetterSetter<T>(this: Signal<T>): (() -> T, (value: ((T) -> T) | T) -> T)
|
||||
local function getter(): T
|
||||
local value = this.value
|
||||
if bit32.band(this.flags, DIRTY) ~= 0 then
|
||||
if updateSignal(this, value) then
|
||||
|
|
@ -240,64 +247,93 @@ local function signalOper<T>(this: Signal<T>, ...: T): T?
|
|||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
local function computedOper<T>(this: Computed<T>): T
|
||||
local flags = this.flags
|
||||
if bit32.band(flags, DIRTY) ~= 0 or (bit32.band(flags, PENDING) ~= 0 and checkDirty(this.deps :: Link, this)) then
|
||||
if updateComputed(this) then
|
||||
local function setter(newValue: ((T) -> T) | T): T
|
||||
local value = this.value
|
||||
local newValue = newValue :: T
|
||||
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
|
||||
if subs then
|
||||
shallowPropagate(subs)
|
||||
propagate(subs)
|
||||
if batchDepth == 0 then
|
||||
flush()
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif bit32.band(flags, PENDING) ~= 0 then
|
||||
this.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||
return newValue
|
||||
end
|
||||
if activeSub then
|
||||
link(this, activeSub)
|
||||
elseif activeScope then
|
||||
link(this, activeScope)
|
||||
end
|
||||
return this.value :: T
|
||||
|
||||
return getter, setter
|
||||
end
|
||||
|
||||
local function effectOper(this: EffectScope): ()
|
||||
local dep = this.deps
|
||||
while dep do
|
||||
dep = unlink(dep, this)
|
||||
local function computedGetter<T>(this: Computed<T>): () -> T
|
||||
return function()
|
||||
local flags = this.flags
|
||||
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
|
||||
local sub = this.subs
|
||||
while sub do
|
||||
unlink(sub)
|
||||
sub = this.subs
|
||||
end
|
||||
this.flags = NONE
|
||||
cleanup(this)
|
||||
end
|
||||
|
||||
local signalMetatable = { __call = signalOper }
|
||||
local computedMetatable = { __call = computedOper }
|
||||
local effectMetatable = { __call = effectOper }
|
||||
local function effectStop(this: EffectScope): () -> ()
|
||||
return function()
|
||||
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 this: Signal<T> = {
|
||||
local function signal<T>(initialValue: T): (() -> T, (value: ((T) -> T) | T) -> T)
|
||||
return signalGetterSetter({
|
||||
previousValue = initialValue,
|
||||
value = initialValue,
|
||||
flags = MUTABLE,
|
||||
}
|
||||
return setmetatable(this, signalMetatable) :: any
|
||||
})
|
||||
end
|
||||
|
||||
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
||||
local this: Computed<T> = {
|
||||
return computedGetter({
|
||||
flags = bit32.bor(MUTABLE, DIRTY),
|
||||
getter = getter,
|
||||
}
|
||||
return setmetatable(this, computedMetatable) :: any
|
||||
})
|
||||
end
|
||||
|
||||
local function effect(fn: () -> ...() -> ()?): () -> ()
|
||||
local function effect(fn: () -> ()): () -> ()
|
||||
local e: Effect = {
|
||||
fn = fn,
|
||||
flags = WATCHING,
|
||||
|
|
@ -308,16 +344,15 @@ local function effect(fn: () -> ...() -> ()?): () -> ()
|
|||
link(e, activeScope)
|
||||
end
|
||||
local prev = setCurrentSub(e)
|
||||
local success, result = pcall(fn)
|
||||
local success, result: unknown = pcall(fn)
|
||||
setCurrentSub(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
e.cleanup = result
|
||||
return setmetatable(e, effectMetatable) :: any
|
||||
return effectStop(e)
|
||||
end
|
||||
|
||||
local function effectScope(fn: () -> ...() -> ()?): () -> ()
|
||||
local function effectScope(fn: () -> ()): () -> ()
|
||||
local e: EffectScope = {
|
||||
flags = NONE,
|
||||
}
|
||||
|
|
@ -325,13 +360,12 @@ local function effectScope(fn: () -> ...() -> ()?): () -> ()
|
|||
link(e, activeScope)
|
||||
end
|
||||
local prev = setCurrentScope(e)
|
||||
local success, result = pcall(fn)
|
||||
local success, result: unknown = pcall(fn)
|
||||
setCurrentScope(prev)
|
||||
if not success then
|
||||
error(result)
|
||||
end
|
||||
e.cleanup = result
|
||||
return setmetatable(e, effectMetatable) :: any
|
||||
return effectStop(e)
|
||||
end
|
||||
|
||||
local function notify(e: ReactiveNode)
|
||||
|
|
@ -343,7 +377,7 @@ local function notify(e: ReactiveNode)
|
|||
notify(subs.sub :: Effect | EffectScope)
|
||||
else
|
||||
queuedEffectsLength += 1
|
||||
queuedEffects[queuedEffectsLength] = e :: Effect | EffectScope
|
||||
queuedEffects[queuedEffectsLength] = e :: Effect
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -364,7 +398,7 @@ local function unwatched(signal: ReactiveNode)
|
|||
until not toRemove
|
||||
signal.flags = bit32.bor(signal.flags, DIRTY)
|
||||
end
|
||||
cleanup(signal)
|
||||
cleanupEffect(signal :: EffectScope)
|
||||
end
|
||||
|
||||
local reactiveSystem = createReactiveSystem({
|
||||
|
|
@ -393,6 +427,8 @@ return {
|
|||
endBatch = endBatch,
|
||||
pauseTracking = pauseTracking,
|
||||
resumeTracking = resumeTracking,
|
||||
onEffectCleanup = onEffectCleanup,
|
||||
onScopeDispose = onScopeDispose,
|
||||
signal = signal,
|
||||
computed = computed,
|
||||
effect = effect,
|
||||
|
|
@ -8,9 +8,8 @@ local resumeTracking = signals.resumeTracking
|
|||
local function subscribe<T>(selector: () -> T, listener: (T, T) -> ()): () -> ()
|
||||
local current: T
|
||||
local mounted = true
|
||||
local dispose: () -> ()
|
||||
|
||||
dispose = effect(function()
|
||||
return effect(function()
|
||||
local value = selector()
|
||||
|
||||
if mounted then
|
||||
|
|
@ -25,10 +24,6 @@ local function subscribe<T>(selector: () -> T, listener: (T, T) -> ()): () -> ()
|
|||
resumeTracking()
|
||||
end
|
||||
end)
|
||||
|
||||
return function()
|
||||
dispose()
|
||||
end
|
||||
end
|
||||
|
||||
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 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 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 finish = suite.finish
|
||||
|
||||
local function test(name: string)
|
||||
describe(name, function()
|
||||
return require(`./{name}/spec`) :: any
|
||||
end)
|
||||
end
|
||||
_G.__DEV__ = true
|
||||
|
||||
test("signals")
|
||||
describe("charm-sync", function()
|
||||
require(script.Parent.CharmSync.spec)
|
||||
end)
|
||||
|
||||
if game then
|
||||
_G.__DEV__ = true
|
||||
test("charm-sync")
|
||||
test("react-charm")
|
||||
test("vide-charm")
|
||||
end
|
||||
describe("react-charm", function()
|
||||
require(script.Parent.ReactCharm.spec)
|
||||
end)
|
||||
|
||||
describe("vide-charm", function()
|
||||
require(script.Parent.VideCharm.spec)
|
||||
end)
|
||||
|
||||
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