From 72f936591ee10dab332c3d054ddd66020e0d42c6 Mon Sep 17 00:00:00 2001
From: littensy <56808540+littensy@users.noreply.github.com>
Date: Mon, 12 May 2025 02:15:49 -0700
Subject: [PATCH] refactor: improve logic
---
.github/workflows/ci.yml | 2 +-
.luaurc | 2 +-
README.md | 2 +-
{images => assets}/logo.png | Bin
{benchmarks => benches}/bench.luau | 0
{benchmarks => benches}/mapped.luau | 0
{benchmarks => benches}/patch.bench.luau | 0
benches/propagate.luau | 42 ++++++++++
benchmarks/propagate.luau | 27 -------
bin/check.sh | 6 +-
default.project.json | 4 +-
pkgs/charm-sync/tests/spec.luau | 28 +++----
pkgs/charm/src/atom.luau | 53 ++++++-------
pkgs/charm/src/effect.luau | 10 +--
pkgs/charm/src/effectScope.luau | 4 +-
pkgs/charm/src/init.luau | 3 +
pkgs/charm/src/lib.luau | 3 +
pkgs/charm/src/listen.luau | 27 +++----
pkgs/charm/src/mapped.luau | 17 +++--
pkgs/charm/src/noYield.luau | 25 ++++++
pkgs/charm/src/observe.luau | 20 ++---
pkgs/charm/src/signals.luau | 51 +++++--------
pkgs/charm/src/subscribe.luau | 23 +++---
pkgs/charm/src/system.luau | 4 +-
pkgs/charm/src/types.luau | 6 +-
pkgs/charm/src/untracked.luau | 3 +-
pkgs/charm/tests/gc.spec.luau | 72 ------------------
pkgs/charm/tests/listen.spec.luau | 32 +++++++-
pkgs/charm/tests/mapped.spec.luau | 2 +-
pkgs/charm/tests/observe.spec.luau | 2 +-
pkgs/charm/tests/signals/computed.spec.luau | 2 +-
pkgs/charm/tests/signals/effect.spec.luau | 2 +-
.../charm/tests/signals/effectScope.spec.luau | 2 +-
pkgs/charm/tests/signals/gc.spec.luau | 22 +++---
pkgs/charm/tests/signals/issue_48.spec.luau | 2 +-
pkgs/charm/tests/signals/spec.luau | 38 +++++----
pkgs/charm/tests/signals/topology.spec.luau | 2 +-
pkgs/charm/tests/signals/untrack.spec.luau | 2 +-
pkgs/charm/tests/spec.luau | 29 +++----
pkgs/charm/tests/subscribe.spec.luau | 32 +++++++-
pkgs/react-charm/src/init.luau | 56 ++++++++++----
pkgs/react-charm/tests/spec.luau | 15 ++--
.../tests/useAtomBinding.spec.luau | 50 ++++++++++++
pkgs/vide-charm/src/init.luau | 17 ++---
pkgs/vide-charm/tests/spec.luau | 12 +--
{tests => test}/init.luau | 2 +-
{tests => test}/spec.server.luau | 0
{tests => test}/suite.luau | 28 ++++---
48 files changed, 434 insertions(+), 349 deletions(-)
rename {images => assets}/logo.png (100%)
rename {benchmarks => benches}/bench.luau (100%)
rename {benchmarks => benches}/mapped.luau (100%)
rename {benchmarks => benches}/patch.bench.luau (100%)
create mode 100644 benches/propagate.luau
delete mode 100644 benchmarks/propagate.luau
create mode 100644 pkgs/charm/src/noYield.luau
delete mode 100644 pkgs/charm/tests/gc.spec.luau
create mode 100644 pkgs/react-charm/tests/useAtomBinding.spec.luau
rename {tests => test}/init.luau (79%)
rename {tests => test}/spec.server.luau (100%)
rename {tests => test}/suite.luau (92%)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4b357e8..3e92647 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,7 +16,7 @@ jobs:
run: sh bin/check.sh
- name: Run tests
- run: lute tests/init.luau
+ run: lute test/init.luau
- name: Build models
run: sh bin/build.sh
diff --git a/.luaurc b/.luaurc
index 7199120..a3678db 100644
--- a/.luaurc
+++ b/.luaurc
@@ -4,7 +4,7 @@
"LocalShadow": false
},
"aliases": {
- "tests": "tests",
+ "test": "test",
"pkgs": "pkgs"
}
}
diff --git a/README.md b/README.md
index f832d4d..e3a421f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
Charm
diff --git a/images/logo.png b/assets/logo.png
similarity index 100%
rename from images/logo.png
rename to assets/logo.png
diff --git a/benchmarks/bench.luau b/benches/bench.luau
similarity index 100%
rename from benchmarks/bench.luau
rename to benches/bench.luau
diff --git a/benchmarks/mapped.luau b/benches/mapped.luau
similarity index 100%
rename from benchmarks/mapped.luau
rename to benches/mapped.luau
diff --git a/benchmarks/patch.bench.luau b/benches/patch.bench.luau
similarity index 100%
rename from benchmarks/patch.bench.luau
rename to benches/patch.bench.luau
diff --git a/benches/propagate.luau b/benches/propagate.luau
new file mode 100644
index 0000000..27b1aed
--- /dev/null
+++ b/benches/propagate.luau
@@ -0,0 +1,42 @@
+local Charm = require("@pkgs/charm/src/lib")
+local bench = require("./bench")
+
+local signals = Charm.signals
+
+for _, w in { 10, 100 } do
+ for _, h in { 10, 100 } do
+ bench.run(`charm {w}*{h}`, function()
+ local src = Charm.atom(1)
+ for _ = 1, w do
+ local last = src
+ for _ = 1, h do
+ local prev = last
+ last = Charm.computed(function()
+ return prev() + 1
+ end)
+ end
+ Charm.effect(function()
+ last()
+ end)
+ end
+ src(src() + 1)
+ end)
+
+ bench.run(`alien-signals {w}*{h}`, function()
+ local src, setSrc = signals.signal(1)
+ for _ = 1, w do
+ local last = src
+ for _ = 1, h do
+ local prev = last
+ last = signals.computed(function()
+ return prev() + 1
+ end)
+ end
+ signals.effect(function()
+ last()
+ end)
+ end
+ setSrc(src() + 1)
+ end)
+ end
+end
diff --git a/benchmarks/propagate.luau b/benchmarks/propagate.luau
deleted file mode 100644
index a08e8b1..0000000
--- a/benchmarks/propagate.luau
+++ /dev/null
@@ -1,27 +0,0 @@
-local Charm = require("@pkgs/charm/src/lib")
-local bench = require("./bench")
-
-local atom = Charm.atom
-local computed = Charm.computed
-local effect = Charm.effect
-
-for _, w in { 1, 10, 100 } do
- for _, h in { 1, 10, 100 } do
- bench.run(`propagate {w}*{h}`, function()
- local src = atom(1)
- for _ = 1, w do
- local last = src
- for _ = 1, h do
- local prev = last
- last = computed(function()
- return prev() + 1
- end)
- end
- effect(function()
- last()
- end)
- end
- src(src() + 1)
- end)
- end
-end
diff --git a/bin/check.sh b/bin/check.sh
index 8cf8dba..f3cccd4 100644
--- a/bin/check.sh
+++ b/bin/check.sh
@@ -7,10 +7,10 @@ luau-lsp analyze \
--flag:LuauFixIndexerSubtypingOrdering=true \
--sourcemap=sourcemap.json \
--ignore="**/_Index/**" \
- pkgs tests
+ pkgs test
-selene pkgs tests
+selene pkgs test
-stylua --check pkgs tests
+stylua --check pkgs test
rm bin/roblox.d.luau
diff --git a/default.project.json b/default.project.json
index 904e9cd..1348524 100644
--- a/default.project.json
+++ b/default.project.json
@@ -15,14 +15,14 @@
"ServerScriptService": {
"$className": "ServerScriptService",
"Tests": {
- "$path": "tests",
+ "$path": "test",
"Charm": { "$path": "pkgs/charm/tests" },
"CharmSync": { "$path": "pkgs/charm-sync/tests" },
"ReactCharm": { "$path": "pkgs/react-charm/tests" },
"VideCharm": { "$path": "pkgs/vide-charm/tests" }
},
"Benchmarks": {
- "$path": "benchmarks"
+ "$path": "benches"
}
}
}
diff --git a/pkgs/charm-sync/tests/spec.luau b/pkgs/charm-sync/tests/spec.luau
index 39ee48c..725714b 100644
--- a/pkgs/charm-sync/tests/spec.luau
+++ b/pkgs/charm-sync/tests/spec.luau
@@ -2,20 +2,22 @@ local ServerScriptService = game:GetService("ServerScriptService")
local suite = require(ServerScriptService.Tests.suite)
-local describe = suite.describe
-
-local function spec(name: string)
- describe(name, function()
- return require(`./{name}.spec`) :: any
+suite.describe("charm-sync", function()
+ suite.describe("client", function()
+ require("./client.spec")
+ end)
+ suite.describe("server", function()
+ require("./server.spec")
+ end)
+ suite.describe("flatten", function()
+ require("./flatten.spec")
+ end)
+ suite.describe("patch", function()
+ require("./patch.spec")
+ end)
+ suite.describe("validate", function()
+ require("./validate.spec")
end)
-end
-
-describe("charm-sync", function()
- spec("client")
- spec("server")
- spec("flatten")
- spec("patch")
- spec("validate")
end)
return {}
diff --git a/pkgs/charm/src/atom.luau b/pkgs/charm/src/atom.luau
index 7e7eccb..ec70d79 100644
--- a/pkgs/charm/src/atom.luau
+++ b/pkgs/charm/src/atom.luau
@@ -2,51 +2,46 @@ local signals = require("./signals")
local types = require("./types")
type Atom = types.Atom
+type Getter = types.Getter
+type Setter = types.Setter
+type Update = types.Update
local signal = signals.signal
-local function atomGetterSetterEquals(
- source: () -> T,
- setSource: (((T) -> T) | T) -> T,
- equals: (T, T) -> boolean
-): Atom
- return function(...: ((T) -> T) | T): T
+type Equals = (current: T, incoming: T) -> boolean
+
+local function createAtomWithEqualityCheck(getter: Getter, setter: Setter, equals: Equals): Atom
+ local function atom(...: Update): T
if select("#", ...) == 0 then
- return source()
+ return getter()
end
local nextState = (...)
- if type(nextState) == "function" then
- return setSource(function(currentState): T
- local nextState = nextState(currentState)
- return if equals(currentState, nextState) then currentState else nextState
- end)
- else
- return setSource(function(currentState): T
- return if equals(currentState, nextState) then currentState else nextState
- end)
- end
+ return setter(function(currentState): T
+ local nextState = if type(nextState) == "function" then nextState(currentState) else nextState
+ return if equals(currentState, nextState) then currentState else nextState
+ end)
end
+
+ return atom
end
-local function atomGetterSetter(source: () -> T, setSource: (((T) -> T) | T) -> T): Atom
- return function(...: ((T) -> T) | T): T
- if select("#", ...) == 0 then
- return source()
- else
- return setSource((...))
- end
+local function createAtom(getter: Getter, setter: Setter): Atom
+ local function atom(...: Update): T
+ return if select("#", ...) == 0 then getter() else setter((...))
end
+
+ return atom
end
-local function atom(state: T, equals: ((previous: T, current: T) -> boolean)?): Atom
- local source, setSource = signal(state)
+local function atom(state: T, equals: Equals?): Atom
+ local getter, setter = signal(state)
- if equals then
- return atomGetterSetterEquals(source, setSource, equals)
+ if equals == nil then
+ return createAtom(getter, setter)
else
- return atomGetterSetter(source, setSource)
+ return createAtomWithEqualityCheck(getter, setter, equals)
end
end
diff --git a/pkgs/charm/src/effect.luau b/pkgs/charm/src/effect.luau
index 572891e..2eb0802 100644
--- a/pkgs/charm/src/effect.luau
+++ b/pkgs/charm/src/effect.luau
@@ -2,13 +2,11 @@ local signals = require("./signals")
local onEffectCleanup = signals.onEffectCleanup
-type Cleanup = () -> ()
-
-local function effect(callback: (dispose: () -> ()) -> ...Cleanup?): () -> ()
- local disposeImmediately = false
+local function effect(callback: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
+ local disposed = false
local function dispose()
- disposeImmediately = true
+ disposed = true
end
dispose = signals.effect(function()
@@ -18,7 +16,7 @@ local function effect(callback: (dispose: () -> ()) -> ...Cleanup?): () -> ()
end
end)
- if disposeImmediately then
+ if disposed then
dispose()
end
diff --git a/pkgs/charm/src/effectScope.luau b/pkgs/charm/src/effectScope.luau
index 7327d9a..7a40ee2 100644
--- a/pkgs/charm/src/effectScope.luau
+++ b/pkgs/charm/src/effectScope.luau
@@ -2,9 +2,7 @@ local signals = require("./signals")
local onScopeDispose = signals.onScopeDispose
-type Cleanup = () -> ()
-
-local function effectScope(callback: () -> ...Cleanup?, detached: boolean?): () -> ()
+local function effectScope(callback: () -> ...(() -> ())?, detached: boolean?): () -> ()
return signals.effectScope(function()
local cleanup = callback()
if cleanup then
diff --git a/pkgs/charm/src/init.luau b/pkgs/charm/src/init.luau
index c3dec01..bc510ce 100644
--- a/pkgs/charm/src/init.luau
+++ b/pkgs/charm/src/init.luau
@@ -1,6 +1,9 @@
local lib = require(script.lib)
export type Atom = lib.Atom
+export type Getter = lib.Getter
export type Selector = lib.Selector
+export type Setter = lib.Setter
+export type Update = lib.Update
return lib
diff --git a/pkgs/charm/src/lib.luau b/pkgs/charm/src/lib.luau
index 479af84..bae8330 100644
--- a/pkgs/charm/src/lib.luau
+++ b/pkgs/charm/src/lib.luau
@@ -10,7 +10,10 @@ local types = require("./types")
local untracked = require("./untracked")
export type Atom = types.Atom
+export type Getter = types.Getter
export type Selector = types.Selector
+export type Setter = types.Setter
+export type Update = types.Update
local computed = signals.computed
local effectScope = signals.effectScope
diff --git a/pkgs/charm/src/listen.luau b/pkgs/charm/src/listen.luau
index c72b061..816c165 100644
--- a/pkgs/charm/src/listen.luau
+++ b/pkgs/charm/src/listen.luau
@@ -1,32 +1,27 @@
local signals = require("./signals")
+local computed = signals.computed
local effect = signals.effect
-local setCurrentSub = signals.setCurrentSub
local function listen(selector: () -> T, listener: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
- local disposeImmediately = false
+ local tracked = computed(function()
+ return selector()
+ end)
+
local current: T
+ local disposed = false
local function dispose()
- disposeImmediately = true
+ disposed = true
end
dispose = effect(function()
- local previous: T? = current
- current = selector()
-
- if current ~= previous then
- local currentSub = setCurrentSub(nil)
- local success, result: unknown = pcall(listener, current, previous, dispose)
- setCurrentSub(currentSub)
-
- if not success then
- error(`listen() encountered an error: {result}`)
- end
- end
+ local previous = current
+ current = tracked()
+ listener(current, previous, dispose)
end)
- if disposeImmediately then
+ if disposed then
dispose()
end
diff --git a/pkgs/charm/src/mapped.luau b/pkgs/charm/src/mapped.luau
index fa4f7f7..847968c 100644
--- a/pkgs/charm/src/mapped.luau
+++ b/pkgs/charm/src/mapped.luau
@@ -1,3 +1,4 @@
+local noYield = require("./noYield")
local signals = require("./signals")
local computed = signals.computed
@@ -17,14 +18,18 @@ local function mapped(selector: () -> { [KI]: VI }, mapper: (ite
local function map(value: VI, key: KI): (VO, KO)
local currentSub = setCurrentSub(nil)
- local success, valueOut, keyOut = pcall(mapper, value, key)
+ local success, valueOut, keyOut = noYield(mapper, value, key)
setCurrentSub(currentSub)
if not success then
error(`mapped() transform function failed: {valueOut}`)
end
- return valueOut, if keyOut ~= nil then keyOut else key :: any
+ if keyOut == nil then
+ keyOut = key :: any
+ end
+
+ return valueOut, keyOut
end
return computed(function(previousOutput)
@@ -53,9 +58,11 @@ local function mapped(selector: () -> { [KI]: VI }, mapper: (ite
local valueOut, keyOut = map(value, key)
- currentOutput = ensureCanMutateOutput(currentOutput, previousOutput)
- currentOutput[keyOut] = valueOut
- keyCache[key] = keyOut
+ if currentOutput[keyOut] ~= valueOut then
+ currentOutput = ensureCanMutateOutput(currentOutput, previousOutput)
+ currentOutput[keyOut] = valueOut
+ keyCache[key] = keyOut
+ end
end
return currentOutput
diff --git a/pkgs/charm/src/noYield.luau b/pkgs/charm/src/noYield.luau
new file mode 100644
index 0000000..2d381b1
--- /dev/null
+++ b/pkgs/charm/src/noYield.luau
@@ -0,0 +1,25 @@
+local function handleNoYield(thread: thread, ok: boolean, ...)
+ if not ok then
+ local err = (...)
+ if typeof(err) == "string" then
+ return false, debug.traceback(thread, err)
+ else
+ return false, tostring(err)
+ end
+ end
+
+ if coroutine.status(thread) ~= "dead" then
+ return false, debug.traceback(thread, "Attempted to yield!")
+ end
+
+ return true, ...
+end
+
+local function noYield(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...)
+ local thread = coroutine.create(callback)
+ return handleNoYield(thread, coroutine.resume(thread, ...))
+end
+
+-- To eliminate the overhead of noYield returning callback(...) in debug mode,
+-- replace it with pcall to avoid wrapping the callback in another function.
+return if _G.__DEV__ then noYield else pcall :: never
diff --git a/pkgs/charm/src/observe.luau b/pkgs/charm/src/observe.luau
index 828ea0f..108b969 100644
--- a/pkgs/charm/src/observe.luau
+++ b/pkgs/charm/src/observe.luau
@@ -2,23 +2,19 @@ local signals = require("./signals")
local effect = signals.effect
local effectScope = signals.effectScope
-local getCurrentSub = signals.getCurrentSub
-local setCurrentSub = signals.setCurrentSub
local onEffectCleanup = signals.onEffectCleanup
local onScopeDispose = signals.onScopeDispose
+local getCurrentSub = signals.getCurrentSub
-type Cleanup = () -> ()
-
-local function observe(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...Cleanup?): () -> ()
+local function observe(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
local scopes: { [K]: () -> () } = {}
local currentItems: { [K]: V } = {}
local function updateScopes(currentItems: { [K]: V }, previousItems: { [K]: V })
for key in previousItems do
- if currentItems[key] == nil then
- local dispose = scopes[key]
+ if currentItems[key] == nil and scopes[key] then
+ scopes[key]()
scopes[key] = nil
- dispose()
end
end
@@ -46,13 +42,7 @@ local function observe(selector: () -> { [K]: V }, observer: (item: V, key
currentItems = selector()
if currentItems ~= previousItems then
- local currentSub = setCurrentSub(nil)
- local success, result: unknown = pcall(updateScopes, currentItems, previousItems)
- setCurrentSub(currentSub)
-
- if not success then
- error(`observe() encountered an error: {result}`, 2)
- end
+ updateScopes(currentItems, previousItems)
end
end)
diff --git a/pkgs/charm/src/signals.luau b/pkgs/charm/src/signals.luau
index bb27e45..a1f4ae0 100644
--- a/pkgs/charm/src/signals.luau
+++ b/pkgs/charm/src/signals.luau
@@ -1,4 +1,4 @@
---[=[
+--[[
alien-signals 2.0.1 implementation adopted from
https://github.com/Nicell/alien-signals-luau
https://github.com/stackblitz/alien-signals
@@ -24,8 +24,9 @@
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-]=]
+]]
+local noYield = require("./noYield")
local system = require("./system")
export type ReactiveFlags = system.ReactiveFlags
@@ -43,12 +44,12 @@ local PENDING = 0b0100000
local QUEUED = 0b1000000
type EffectScope = ReactiveNode & {
- cleanup: (() -> ())?,
+ cleanups: { () -> () },
}
type Effect = ReactiveNode & {
fn: () -> (),
- cleanup: (() -> ())?,
+ cleanups: { () -> () },
}
type Computed = ReactiveNode & {
@@ -99,42 +100,28 @@ local function setCurrentScope(scope: Effect | EffectScope?)
return prevScope
end
-local function addCleanup(e: Effect | EffectScope, cleanup: () -> ())
- local prevCleanup: (() -> ())? = e.cleanup
- local nextCleanup = cleanup
-
- if prevCleanup then
- function cleanup()
- prevCleanup()
- nextCleanup()
- end
- end
-
- (e :: Effect).cleanup = cleanup
-end
-
local function onEffectCleanup(cleanup: () -> ())
if activeSub then
- addCleanup(activeSub :: Effect, cleanup)
+ table.insert((activeSub :: Effect).cleanups, cleanup)
end
end
local function onScopeDispose(cleanup: () -> ())
if activeScope then
- addCleanup(activeScope, cleanup)
+ table.insert(activeScope.cleanups, cleanup)
end
end
local function cleanupEffect(e: Effect | EffectScope)
- if e.cleanup then
- local prev = setCurrentSub(nil)
- local success, result: unknown = pcall(e.cleanup)
- setCurrentSub(prev);
- (e :: Effect).cleanup = nil
+ for _, cleanup in e.cleanups :: { () -> () } do
+ local prevSub = setCurrentSub(nil)
+ local success, result: unknown = noYield(cleanup)
+ setCurrentSub(prevSub)
if not success then
error(result)
end
end
+ table.clear(e.cleanups)
end
local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
@@ -142,7 +129,7 @@ local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
cleanupEffect(e)
local prev = setCurrentSub(e)
startTracking(e)
- local success, result: unknown = pcall((e :: Effect).fn)
+ local success, result: unknown = noYield((e :: Effect).fn)
setCurrentSub(prev)
endTracking(e)
if not success then
@@ -208,7 +195,7 @@ local function updateComputed(c: Computed): boolean
local prevSub = setCurrentSub(c)
startTracking(c)
local oldValue = c.value
- local success, result = pcall(c.getter, oldValue)
+ local success, result = noYield(c.getter, oldValue)
setCurrentSub(prevSub)
endTracking(c)
if not success then
@@ -254,7 +241,7 @@ local function signalGetterSetter(this: Signal): (() -> T, (value: ((T) ->
local newValue = newValue :: T
if type(newValue) == "function" then
local prev = setCurrentSub(nil)
- local success, result = pcall(newValue, value)
+ local success, result = noYield(newValue, value)
setCurrentSub(prev)
if not success then
error(result)
@@ -336,6 +323,7 @@ end
local function effect(fn: () -> ()): () -> ()
local e: Effect = {
fn = fn,
+ cleanups = {},
flags = WATCHING,
}
if activeSub then
@@ -344,7 +332,7 @@ local function effect(fn: () -> ()): () -> ()
link(e, activeScope)
end
local prev = setCurrentSub(e)
- local success, result: unknown = pcall(fn)
+ local success, result: unknown = noYield(fn)
setCurrentSub(prev)
if not success then
error(result)
@@ -354,6 +342,7 @@ end
local function effectScope(fn: () -> (), detached: boolean?): () -> ()
local e: EffectScope = {
+ cleanups = {},
flags = NONE,
}
if activeScope and not detached then
@@ -361,7 +350,7 @@ local function effectScope(fn: () -> (), detached: boolean?): () -> ()
end
local prevSub = setCurrentSub(nil)
local prevScope = setCurrentScope(e)
- local success, result: unknown = pcall(fn)
+ local success, result: unknown = noYield(fn)
setCurrentScope(prevScope)
setCurrentSub(prevSub)
if not success then
@@ -401,7 +390,7 @@ local function unwatched(node: Signal | Computed | Effect | EffectScope)
toRemove = unlink(toRemove, node)
until not toRemove
end
- elseif (node :: Signal).previousValue == nil then
+ elseif (node :: Effect).cleanups ~= nil then
effectStop(node :: Effect)()
end
end
diff --git a/pkgs/charm/src/subscribe.luau b/pkgs/charm/src/subscribe.luau
index 01c4a98..6f5b84d 100644
--- a/pkgs/charm/src/subscribe.luau
+++ b/pkgs/charm/src/subscribe.luau
@@ -1,26 +1,21 @@
local signals = require("./signals")
+local computed = signals.computed
local effect = signals.effect
-local setCurrentSub = signals.setCurrentSub
local function subscribe(selector: () -> T, listener: (state: T, lastState: T) -> ()): () -> ()
- local current: T
- local mounted = true
+ local tracked = computed(function()
+ return selector()
+ end)
+
+ local current = tracked()
return effect(function()
local previous = current
- current = selector()
+ current = tracked()
- if mounted then
- mounted = false
- elseif current ~= previous then
- local currentSub = setCurrentSub(nil)
- local success, result: unknown = pcall(listener, current, previous)
- setCurrentSub(currentSub)
-
- if not success then
- error(`subscribe() encountered an error: {result}`)
- end
+ if current ~= previous then
+ listener(current, previous)
end
end)
end
diff --git a/pkgs/charm/src/system.luau b/pkgs/charm/src/system.luau
index 79d3a48..0c6334b 100644
--- a/pkgs/charm/src/system.luau
+++ b/pkgs/charm/src/system.luau
@@ -1,4 +1,4 @@
---[=[
+--[[
alien-signals 2.0.1 implementation adopted from
https://github.com/Nicell/alien-signals-luau
https://github.com/stackblitz/alien-signals
@@ -24,7 +24,7 @@
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-]=]
+]]
export type ReactiveNode = {
deps: Link?,
diff --git a/pkgs/charm/src/types.luau b/pkgs/charm/src/types.luau
index 20aa1f5..b237054 100644
--- a/pkgs/charm/src/types.luau
+++ b/pkgs/charm/src/types.luau
@@ -1,5 +1,9 @@
+export type Getter = () -> T
+export type Setter = (update: Update) -> T
+export type Update = T | (previous: T) -> T
+
export type Selector = () -> T
-export type Atom = ((((T) -> T) | T) -> T) & (() -> T)
+export type Atom = Setter & Getter
return {}
diff --git a/pkgs/charm/src/untracked.luau b/pkgs/charm/src/untracked.luau
index ac77e85..4330920 100644
--- a/pkgs/charm/src/untracked.luau
+++ b/pkgs/charm/src/untracked.luau
@@ -1,10 +1,11 @@
+local noYield = require("./noYield")
local signals = require("./signals")
local setCurrentSub = signals.setCurrentSub
local function untracked(callback: ((Args...) -> T) | (Args...) -> (), ...: Args...): T
local currentSub = setCurrentSub(nil)
- local success, result = pcall(callback, ...)
+ local success, result = noYield(callback, ...)
setCurrentSub(currentSub)
if not success then
diff --git a/pkgs/charm/tests/gc.spec.luau b/pkgs/charm/tests/gc.spec.luau
deleted file mode 100644
index 033a66f..0000000
--- a/pkgs/charm/tests/gc.spec.luau
+++ /dev/null
@@ -1,72 +0,0 @@
-local Charm = require("../src/lib")
-local suite = require("@tests/suite")
-
-local atom = Charm.atom
-local computed = Charm.computed
-local effect = Charm.effect
-local effectScope = Charm.effectScope
-local test = suite.test
-
---- Creates a weak reference to a value.
---- @return Function to dereference the value.
-local function weakRef(value: T): () -> T?
- local ref = setmetatable({ value = value }, { __mode = "v" })
- return function()
- return ref.value
- end
-end
-
---- Triggers garbage collection by allocating excessive memory.
-local function gc()
- for _ = 1, 1000 do
- local _ = table.create(100, 0)
- end
-end
-
---- In alien-signals, a computed value unlinks from dependencies when its last
---- parent scope is disposed. Calling a computed value outside of a scope links
---- it to dependencies, but the dependencies hold strong references to the
---- computed value, so it won't GC until called in a scope and disposed.
----
---- This function releases a computed value by linking it to a scope and then
---- immediately closing it, causing alien-signals to unlink the computed value
---- and allow it to be garbage collected.
-local function release(getter: () -> ())
- effectScope(function()
- getter()
- end)()
-end
-
-test("should release computed cache", function()
- local srcRef = weakRef({})
- local src = atom(srcRef())
-
- local c = computed(function()
- return src()
- end)
-
- c() -- cache src value
- src(nil) -- release value
- c = release(c) -- release computed
-
- gc()
- assert(srcRef() == nil, "should release computed cache")
-end)
-
-test("should not release effect for atom", function()
- local effectCalls = 0
- local src = atom(0)
-
- effect(function()
- effectCalls += 1
- src()
- end)
-
- assert(effectCalls == 1, "should call effect once")
-
- gc()
- src(src() + 1)
- assert(effectCalls == 2, "should call effect again")
-end)
-
-return {}
diff --git a/pkgs/charm/tests/listen.spec.luau b/pkgs/charm/tests/listen.spec.luau
index 15d7457..a244a45 100644
--- a/pkgs/charm/tests/listen.spec.luau
+++ b/pkgs/charm/tests/listen.spec.luau
@@ -1,5 +1,5 @@
local Charm = require("../src/lib")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
local atom = Charm.atom
local listen = Charm.listen
@@ -26,4 +26,34 @@ test("should listen to selector", function()
assert(current == 2 and previous == 1, "should not run after unsub")
end)
+test("should track inner subs", function()
+ local order = {}
+ local a = atom(1)
+
+ local unsub = listen(a, function()
+ table.insert(order, 1)
+ listen(a, function()
+ table.insert(order, 2)
+ end)
+ listen(a, function()
+ table.insert(order, 3)
+ end)
+ end)
+
+ assert(table.concat(order) == "123", `bad mount order: {table.concat(order)}`)
+
+ table.clear(order)
+ a(a() + 1)
+ assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
+
+ table.clear(order)
+ a(a() + 1)
+ assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
+
+ table.clear(order)
+ unsub()
+ a(a() + 1)
+ assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
+end)
+
return {}
diff --git a/pkgs/charm/tests/mapped.spec.luau b/pkgs/charm/tests/mapped.spec.luau
index 7bc4a0c..6d623bc 100644
--- a/pkgs/charm/tests/mapped.spec.luau
+++ b/pkgs/charm/tests/mapped.spec.luau
@@ -1,5 +1,5 @@
local Charm = require("../src/lib")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
local atom = Charm.atom
local mapped = Charm.mapped
diff --git a/pkgs/charm/tests/observe.spec.luau b/pkgs/charm/tests/observe.spec.luau
index 57da889..ddceca9 100644
--- a/pkgs/charm/tests/observe.spec.luau
+++ b/pkgs/charm/tests/observe.spec.luau
@@ -1,5 +1,5 @@
local Charm = require("../src/lib")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
local atom = Charm.atom
local computed = Charm.computed
diff --git a/pkgs/charm/tests/signals/computed.spec.luau b/pkgs/charm/tests/signals/computed.spec.luau
index 3a4721a..5c74738 100644
--- a/pkgs/charm/tests/signals/computed.spec.luau
+++ b/pkgs/charm/tests/signals/computed.spec.luau
@@ -1,5 +1,5 @@
local signals = require("../../src/signals")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
local computed = signals.computed
local signal = signals.signal
diff --git a/pkgs/charm/tests/signals/effect.spec.luau b/pkgs/charm/tests/signals/effect.spec.luau
index a0d70a2..9fad3ce 100644
--- a/pkgs/charm/tests/signals/effect.spec.luau
+++ b/pkgs/charm/tests/signals/effect.spec.luau
@@ -1,5 +1,5 @@
local signals = require("../../src/signals")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
local computed = signals.computed
local effect = signals.effect
diff --git a/pkgs/charm/tests/signals/effectScope.spec.luau b/pkgs/charm/tests/signals/effectScope.spec.luau
index 711f785..e48dcbe 100644
--- a/pkgs/charm/tests/signals/effectScope.spec.luau
+++ b/pkgs/charm/tests/signals/effectScope.spec.luau
@@ -1,5 +1,5 @@
local signals = require("../../src/signals")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
local effect = signals.effect
local effectScope = signals.effectScope
diff --git a/pkgs/charm/tests/signals/gc.spec.luau b/pkgs/charm/tests/signals/gc.spec.luau
index e08d265..2179894 100644
--- a/pkgs/charm/tests/signals/gc.spec.luau
+++ b/pkgs/charm/tests/signals/gc.spec.luau
@@ -1,10 +1,10 @@
-local Charm = require("@pkgs/charm/src/lib")
-local suite = require("@tests/suite")
+local signals = require("../../src/signals")
+local suite = require("@test/suite")
-local atom = Charm.atom
-local computed = Charm.computed
-local effect = Charm.effect
-local effectScope = Charm.effectScope
+local signal = signals.signal
+local computed = signals.computed
+local effect = signals.effect
+local effectScope = signals.effectScope
local test = suite.test
--- Creates a weak reference to a value.
@@ -39,23 +39,23 @@ end
test("should release computed cache", function()
local srcRef = weakRef({})
- local src = atom(srcRef())
+ local src, setSrc = signal(srcRef())
local c = computed(function()
return src()
end)
c() -- cache src value
- src(nil) -- release value
+ setSrc(nil) -- release value
c = release(c) -- release computed
gc()
assert(srcRef() == nil, "should release computed cache")
end)
-test("should not release effect for atom", function()
+test("should not release effect for signal", function()
local effectCalls = 0
- local src = atom(0)
+ local src, setSrc = signal(0)
effect(function()
effectCalls += 1
@@ -65,7 +65,7 @@ test("should not release effect for atom", function()
assert(effectCalls == 1, "should call effect once")
gc()
- src(src() + 1)
+ setSrc(src() + 1)
assert(effectCalls == 2, "should call effect again")
end)
diff --git a/pkgs/charm/tests/signals/issue_48.spec.luau b/pkgs/charm/tests/signals/issue_48.spec.luau
index 5089a84..a60b79d 100644
--- a/pkgs/charm/tests/signals/issue_48.spec.luau
+++ b/pkgs/charm/tests/signals/issue_48.spec.luau
@@ -1,5 +1,5 @@
local signals = require("../../src/signals")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
local computed = signals.computed
local effect = signals.effect
diff --git a/pkgs/charm/tests/signals/spec.luau b/pkgs/charm/tests/signals/spec.luau
index 15c69f5..7d5bdd7 100644
--- a/pkgs/charm/tests/signals/spec.luau
+++ b/pkgs/charm/tests/signals/spec.luau
@@ -1,21 +1,27 @@
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
-local describe = suite.describe
-
-local function spec(name: string)
- describe(name, function()
- return require(`./{name}.spec`) :: any
+suite.describe("signals", function()
+ suite.describe("computed", function()
+ require("./computed.spec")
+ end)
+ suite.describe("effect", function()
+ require("./effect.spec")
+ end)
+ suite.describe("effectScope", function()
+ require("./effectScope.spec")
+ end)
+ suite.describe("issue_48", function()
+ require("./issue_48.spec")
+ end)
+ suite.describe("gc", function()
+ require("./gc.spec")
+ end)
+ suite.describe("topology", function()
+ require("./topology.spec")
+ end)
+ suite.describe("untrack", function()
+ require("./untrack.spec")
end)
-end
-
-describe("signals", function()
- spec("computed")
- spec("effect")
- spec("effectScope")
- spec("issue_48")
- spec("gc")
- spec("topology")
- spec("untrack")
end)
return {}
diff --git a/pkgs/charm/tests/signals/topology.spec.luau b/pkgs/charm/tests/signals/topology.spec.luau
index 070b30c..7fa961d 100644
--- a/pkgs/charm/tests/signals/topology.spec.luau
+++ b/pkgs/charm/tests/signals/topology.spec.luau
@@ -1,5 +1,5 @@
local signals = require("../../src/signals")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
local computed = signals.computed
local effect = signals.effect
diff --git a/pkgs/charm/tests/signals/untrack.spec.luau b/pkgs/charm/tests/signals/untrack.spec.luau
index 56a28ae..f8f738f 100644
--- a/pkgs/charm/tests/signals/untrack.spec.luau
+++ b/pkgs/charm/tests/signals/untrack.spec.luau
@@ -1,5 +1,5 @@
local signals = require("../../src/signals")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
local computed = signals.computed
local setCurrentSub = signals.setCurrentSub
diff --git a/pkgs/charm/tests/spec.luau b/pkgs/charm/tests/spec.luau
index f86a562..40c00f1 100644
--- a/pkgs/charm/tests/spec.luau
+++ b/pkgs/charm/tests/spec.luau
@@ -1,19 +1,20 @@
-local suite = require("@tests/suite")
-
-local describe = suite.describe
-
-local function spec(name: string)
- describe(name, function()
- return require(`./{name}.spec`) :: any
- end)
-end
+local suite = require("@test/suite")
require("./signals/spec")
-spec("gc")
-spec("listen")
-spec("mapped")
-spec("observe")
-spec("subscribe")
+suite.describe("charm", function()
+ suite.describe("listen", function()
+ require("./listen.spec")
+ end)
+ suite.describe("mapped", function()
+ require("./mapped.spec")
+ end)
+ suite.describe("observe", function()
+ require("./observe.spec")
+ end)
+ suite.describe("subscribe", function()
+ require("./subscribe.spec")
+ end)
+end)
return {}
diff --git a/pkgs/charm/tests/subscribe.spec.luau b/pkgs/charm/tests/subscribe.spec.luau
index c2d5bd5..c5a3907 100644
--- a/pkgs/charm/tests/subscribe.spec.luau
+++ b/pkgs/charm/tests/subscribe.spec.luau
@@ -1,11 +1,12 @@
local Charm = require("../src/lib")
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
type Atom = Charm.Atom
local atom = Charm.atom
local computed = Charm.computed
local subscribe = Charm.subscribe
+local listen = Charm.listen
local test = suite.test
test("should subscribe to selector", function()
@@ -55,4 +56,33 @@ test("should ensure correct execution order", function()
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
end)
+test("should track inner subs", function()
+ local order = {}
+ local a = atom(1)
+
+ local unsub = subscribe(a, function()
+ table.insert(order, 1)
+ listen(a, function()
+ table.insert(order, 2)
+ end)
+ listen(a, function()
+ table.insert(order, 3)
+ end)
+ end)
+
+ assert(table.concat(order) == "", "should not run immediately")
+
+ a(a() + 1)
+ assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
+
+ table.clear(order)
+ a(a() + 1)
+ assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
+
+ table.clear(order)
+ unsub()
+ a(a() + 1)
+ assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
+end)
+
return {}
diff --git a/pkgs/react-charm/src/init.luau b/pkgs/react-charm/src/init.luau
index 9d13132..9293a5f 100644
--- a/pkgs/react-charm/src/init.luau
+++ b/pkgs/react-charm/src/init.luau
@@ -1,32 +1,56 @@
local Charm = require(script.Parent.Charm)
local React = require(script.Parent.React)
-local listen = Charm.listen
+local effect = Charm.effect
+local useBinding = React.useBinding
local useEffect = React.useEffect
local useState = React.useState
---[=[
- A hook that subscribes to changes in the given atom or selector. The
- component is re-rendered whenever the state changes.
-
- If the `dependencies` array is provided, the subscription to the atom or
- selector is re-created whenever the dependencies change. Otherwise, the
- subscription is created once when the component is mounted.
-
- @param callback The atom or selector to subscribe to.
- @param dependencies An array of values that the subscription depends on.
- @return The current state.
-]=]
-local function useAtom(callback: () -> State, dependencies: { any }?): State
- local state, setState = useState(callback)
+--- A hook that subscribes to changes in the given atom or selector. The
+--- component is re-rendered whenever the state changes.
+---
+--- If the `dependencies` array is provided, the effect is re-created whenever
+--- the dependencies change. Otherwise, the subscription is created once when
+--- the component is mounted.
+---
+--- @param selector The atom or selector to subscribe to.
+--- @param dependencies An array of values that the subscription depends on.
+--- @return The current state.
+local function useAtom(selector: () -> State, dependencies: { any }?): State
+ local state, setState = useState(selector)
useEffect(function()
- return listen(callback, setState)
+ return effect(function()
+ setState(selector())
+ end)
end, dependencies or {})
return state
end
+--- A hook that subscribes to changes in the given atom or selector. Returns
+--- a binding that updates when the state changes.
+---
+--- If the `dependencies` array is provided, the effect is re-created whenever
+--- the dependencies change. Otherwise, the subscription is created once when
+--- the component is mounted.
+---
+--- @param selector The atom or selector to subscribe to.
+--- @param dependencies An array of values that the subscription depends on.
+--- @return A binding that stores the current state.
+local function useAtomBinding(selector: () -> State, dependencies: { any }?)
+ local binding, setBinding = useBinding(selector)
+
+ useEffect(function()
+ return effect(function()
+ setBinding(selector())
+ end)
+ end, dependencies or {})
+
+ return binding
+end
+
return {
useAtom = useAtom,
+ useAtomBinding = useAtomBinding,
}
diff --git a/pkgs/react-charm/tests/spec.luau b/pkgs/react-charm/tests/spec.luau
index 2ce9682..dabaab7 100644
--- a/pkgs/react-charm/tests/spec.luau
+++ b/pkgs/react-charm/tests/spec.luau
@@ -2,16 +2,13 @@ local ServerScriptService = game:GetService("ServerScriptService")
local suite = require(ServerScriptService.Tests.suite)
-local describe = suite.describe
-
-local function spec(name: string)
- describe(name, function()
- return require(`./{name}.spec`) :: any
+suite.describe("react-charm", function()
+ suite.describe("useAtom", function()
+ require("./useAtom.spec")
+ end)
+ suite.describe("useAtomBinding", function()
+ require("./useAtomBinding.spec")
end)
-end
-
-describe("react-charm", function()
- spec("useAtom")
end)
return {}
diff --git a/pkgs/react-charm/tests/useAtomBinding.spec.luau b/pkgs/react-charm/tests/useAtomBinding.spec.luau
new file mode 100644
index 0000000..28dcad2
--- /dev/null
+++ b/pkgs/react-charm/tests/useAtomBinding.spec.luau
@@ -0,0 +1,50 @@
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local ServerScriptService = game:GetService("ServerScriptService")
+
+local Charm = require(ReplicatedStorage.Packages.Charm)
+local ReactCharm = require(ReplicatedStorage.Packages.ReactCharm)
+local renderHook = require(script.Parent.helpers.renderHook)
+local suite = require(ServerScriptService.Tests.suite)
+
+local atom = Charm.atom
+local useAtomBinding = ReactCharm.useAtomBinding
+local test = suite.test
+
+test("tracks atom state", function()
+ local state = atom(0)
+ local hook = renderHook(function()
+ return useAtomBinding(state)
+ end)
+ assert(hook.result:getValue() == 0, "should return 0")
+ state(1)
+ assert(hook.result:getValue() == 1, "should return 1")
+end)
+
+test("accepts a selector", function()
+ local state = atom(0)
+ local hook = renderHook(function()
+ return useAtomBinding(function()
+ return 2 * state()
+ end)
+ end)
+ assert(hook.result:getValue() == 0, "should return 0")
+ state(1)
+ assert(hook.result:getValue() == 2, "should return 2")
+end)
+
+test("tracks dependencies", function()
+ local state = atom(0)
+ local hook = renderHook(function()
+ return useAtomBinding(state, { state })
+ end)
+ assert(hook.result:getValue() == 0, "should return 0")
+ state(1)
+ assert(hook.result:getValue() == 1, "should return 1")
+ state = atom(0)
+ hook.rerender()
+ assert(hook.result:getValue() == 0, "should return 0")
+ state(1)
+ assert(hook.result:getValue() == 1, "should return 1")
+end)
+
+return {}
diff --git a/pkgs/vide-charm/src/init.luau b/pkgs/vide-charm/src/init.luau
index ef7bb25..805a634 100644
--- a/pkgs/vide-charm/src/init.luau
+++ b/pkgs/vide-charm/src/init.luau
@@ -5,15 +5,14 @@ local subscribe = Charm.subscribe
local source = Vide.source
local cleanup = Vide.cleanup
---[=[
- Subscribes to the state of an atom and returns a Vide source.
-
- @param callback The atom or selector to subscribe to.
- @return The reactive source.
-]=]
-local function useAtom(callback: () -> State): () -> State
- local state = source(callback())
- cleanup(subscribe(callback, state))
+--- Subscribes to the state of an atom or selector. Returns a source that
+--- updates when the state changes.
+---
+--- @param selector The atom or selector to subscribe to.
+--- @return The reactive source.
+local function useAtom(selector: () -> State): () -> State
+ local state = source(selector())
+ cleanup(subscribe(selector, state))
return state
end
diff --git a/pkgs/vide-charm/tests/spec.luau b/pkgs/vide-charm/tests/spec.luau
index 2bde270..4583308 100644
--- a/pkgs/vide-charm/tests/spec.luau
+++ b/pkgs/vide-charm/tests/spec.luau
@@ -2,16 +2,10 @@ local ServerScriptService = game:GetService("ServerScriptService")
local suite = require(ServerScriptService.Tests.suite)
-local describe = suite.describe
-
-local function spec(name: string)
- describe(name, function()
- return require(`./{name}.spec`) :: any
+suite.describe("vide-charm", function()
+ suite.describe("useAtom", function()
+ require("./useAtom.spec")
end)
-end
-
-describe("vide-charm", function()
- spec("useAtom")
end)
return {}
diff --git a/tests/init.luau b/test/init.luau
similarity index 79%
rename from tests/init.luau
rename to test/init.luau
index 0b17cbc..467958e 100644
--- a/tests/init.luau
+++ b/test/init.luau
@@ -1,4 +1,4 @@
-local suite = require("@tests/suite")
+local suite = require("@test/suite")
if not table.isfrozen(_G) then
_G.__DEV__ = true
diff --git a/tests/spec.server.luau b/test/spec.server.luau
similarity index 100%
rename from tests/spec.server.luau
rename to test/spec.server.luau
diff --git a/tests/suite.luau b/test/suite.luau
similarity index 92%
rename from tests/suite.luau
rename to test/suite.luau
index e23b9d4..516db20 100644
--- a/tests/suite.luau
+++ b/test/suite.luau
@@ -42,13 +42,13 @@ local color = {
return if options.colors then `\27[90;1m{text}\27[0m` else text
end,
green = function(text: string)
- return if options.colors then `\27[32;1m{text}\27[0m` else text
+ return if options.colors then `\27[32;1m{text}\27[0m` else `\u{1F7E2} {text}`
end,
red = function(text: string)
- return if options.colors then `\27[31;1m{text}\27[0m` else text
+ return if options.colors then `\27[31;1m{text}\27[0m` else `\u{1F534} {text}`
end,
blue = function(text: string)
- return if options.colors then `\27[34;1m{text}\27[0m` else text
+ return if options.colors then `\27[34;1m{text}\27[0m` else `\u{1F535} {text}`
end,
}
@@ -405,27 +405,33 @@ local function finish(config: Config?)
report()
end
-local testImpl = setmetatable({
+local testCtor = setmetatable({
skip = testSkip,
only = testOnly,
}, {
- __call = function(_, name: string, run: () -> ())
- return test(name, run)
+ __call = function(_, ...)
+ return test(...)
end,
})
-local describeImpl = setmetatable({
+local describeCtor = setmetatable({
skip = describeSkip,
only = describeOnly,
}, {
- __call = function(_, name: string, run: () -> ())
- return describe(name, run)
+ __call = function(_, ...)
+ return describe(...)
end,
})
return {
- test = testImpl,
- describe = describeImpl,
+ test = testCtor :: ((name: string, callback: () -> ()) -> ()) & {
+ skip: (name: string, callback: () -> ()) -> (),
+ only: (name: string, callback: () -> ()) -> (),
+ },
+ describe = describeCtor :: ((name: string, callback: () -> ()) -> ()) & {
+ skip: (name: string, callback: () -> ()) -> (),
+ only: (name: string, callback: () -> ()) -> (),
+ },
skip = skip,
only = only,
finish = finish,