mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: add deep freeze flag, misc refactors
This commit is contained in:
parent
708bc55de7
commit
77c98ca94a
22 changed files with 185 additions and 337 deletions
|
|
@ -1,4 +1,6 @@
|
||||||
local CharmSync = require("../packages/charm-sync/src")
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
|
local CharmSync = require(ReplicatedStorage.packages["charm-sync"].src)
|
||||||
|
|
||||||
local patch = CharmSync.patch
|
local patch = CharmSync.patch
|
||||||
|
|
||||||
|
|
|
||||||
40
packages/charm/src/__tests__/core/atom.spec.luau
Normal file
40
packages/charm/src/__tests__/core/atom.spec.luau
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
local core = require("../../core")
|
||||||
|
local suite = require("../../../../../test/suite")
|
||||||
|
|
||||||
|
local atom = core.atom
|
||||||
|
local effect = core.effect
|
||||||
|
local test = suite.test
|
||||||
|
|
||||||
|
test("should accept equality function", function()
|
||||||
|
local calls = {}
|
||||||
|
local src = atom(0, function(current, incoming)
|
||||||
|
return current == incoming or incoming % 2 == 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
table.insert(calls, src())
|
||||||
|
end)
|
||||||
|
|
||||||
|
src(2)
|
||||||
|
src(3)
|
||||||
|
assert(src() == 2, "should skip odd value")
|
||||||
|
|
||||||
|
src(4)
|
||||||
|
src(5)
|
||||||
|
assert(src() == 4, "should skip odd value")
|
||||||
|
assert(table.concat(calls) == "024", `should only call on even updates, got {table.concat(calls)}`)
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should enforce immutability", function()
|
||||||
|
local src = atom({ nested = {} })
|
||||||
|
assert(not pcall(function()
|
||||||
|
src().nested.a = 1
|
||||||
|
end), "should not allow mutation of initial value")
|
||||||
|
|
||||||
|
src({ nested = { a = 1 } })
|
||||||
|
assert(not pcall(function()
|
||||||
|
src().nested.a = 2
|
||||||
|
end), "should not allow mutation of new value")
|
||||||
|
end)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
local core = require("../../core")
|
local core = require("../../core")
|
||||||
local suite = require("../../../../../test/suite")
|
local suite = require("../../../../../test/suite")
|
||||||
|
|
||||||
local computed = core.computed
|
|
||||||
local atom = core.atom
|
local atom = core.atom
|
||||||
|
local computed = core.computed
|
||||||
|
local effect = core.effect
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
test("should correctly propagate changes through computed core", function()
|
test("should correctly propagate changes through computed core", function()
|
||||||
|
|
@ -80,4 +81,27 @@ test("should not update if the atom value is reverted", function()
|
||||||
assert(times == 1, "computed ran when it should not")
|
assert(times == 1, "computed ran when it should not")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should accept equality function", function()
|
||||||
|
local calls = {}
|
||||||
|
local src = atom(0)
|
||||||
|
local c1 = computed(function()
|
||||||
|
return src()
|
||||||
|
end, function(current, incoming)
|
||||||
|
return current == incoming or incoming % 2 == 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
table.insert(calls, c1())
|
||||||
|
end)
|
||||||
|
|
||||||
|
src(2)
|
||||||
|
src(3)
|
||||||
|
assert(c1() == 2, "should skip odd value")
|
||||||
|
|
||||||
|
src(4)
|
||||||
|
src(5)
|
||||||
|
assert(c1() == 4, "should skip odd value")
|
||||||
|
assert(table.concat(calls) == "024", `should only call on even updates, got {table.concat(calls)}`)
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,11 @@ local effectScope = core.effectScope
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
--- Creates a weak reference to a value.
|
--- Creates a weak reference to a value.
|
||||||
--- @return Function to dereference the value.
|
local function weakRef<T>(value: T): { value: T? }
|
||||||
local function weakRef<T>(value: T): () -> T?
|
return setmetatable({ value = value }, { __mode = "v" }) :: any
|
||||||
local ref = setmetatable({ value = value }, { __mode = "v" })
|
|
||||||
return function()
|
|
||||||
return ref.value
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Triggers garbage collection by allocating excessive memory.
|
--- Triggers garbage collection by creating a large number of tables.
|
||||||
local function gc()
|
local function gc()
|
||||||
for _ = 1, 1000 do
|
for _ = 1, 1000 do
|
||||||
local _ = table.create(100, 0)
|
local _ = table.create(100, 0)
|
||||||
|
|
@ -24,14 +20,14 @@ local function gc()
|
||||||
end
|
end
|
||||||
|
|
||||||
--- In alien-signals, a computed value unlinks from dependencies when its last
|
--- 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
|
--- parent scope is disposed. However, calling a computed value outside of a
|
||||||
--- it to dependencies, but the dependencies hold strong references to the
|
--- scope still links it to dependencies, which act as strong references.
|
||||||
--- computed value, so it won't GC until called in a scope and disposed.
|
--- Computed values called in this way won't GC until unlinked from a scope.
|
||||||
---
|
---
|
||||||
--- This function releases a computed value by linking it to a scope and then
|
--- 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
|
--- immediately closing it, causing alien-signals to unlink the computed value
|
||||||
--- and allow it to be garbage collected.
|
--- from its dependencies and allow it to be garbage collected.
|
||||||
local function release(getter: () -> ())
|
local function unlink(getter: () -> ())
|
||||||
effectScope(function()
|
effectScope(function()
|
||||||
getter()
|
getter()
|
||||||
end)()
|
end)()
|
||||||
|
|
@ -39,18 +35,17 @@ end
|
||||||
|
|
||||||
test("should release computed cache", function()
|
test("should release computed cache", function()
|
||||||
local srcRef = weakRef({})
|
local srcRef = weakRef({})
|
||||||
local src = atom(srcRef())
|
local src = atom(srcRef.value)
|
||||||
|
|
||||||
local c = computed(function()
|
local c = computed(function()
|
||||||
return src()
|
return src()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
c() -- cache src value
|
c() -- cache src value
|
||||||
src(nil) -- release value
|
src(nil) -- release value
|
||||||
c = release(c) -- release computed
|
c = unlink(c) -- release computed
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
assert(srcRef() == nil, "should release computed cache")
|
assert(srcRef.value == nil, "should release computed cache")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should not release effect for atom", function()
|
test("should not release effect for atom", function()
|
||||||
|
|
@ -61,7 +56,6 @@ test("should not release effect for atom", function()
|
||||||
effectCalls += 1
|
effectCalls += 1
|
||||||
src()
|
src()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(effectCalls == 1, "should call effect once")
|
assert(effectCalls == 1, "should call effect once")
|
||||||
|
|
||||||
gc()
|
gc()
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
local core = require("../../core")
|
|
||||||
local suite = require("../../../../../test/suite")
|
|
||||||
|
|
||||||
local computed = core.computed
|
|
||||||
local effect = core.effect
|
|
||||||
local setCurrentSub = core.setCurrentSub
|
|
||||||
local atom = core.atom
|
|
||||||
local test = suite.test
|
|
||||||
|
|
||||||
type ReactionOptions<T = unknown> = {
|
|
||||||
fireImmediately: boolean?,
|
|
||||||
equals: ((a: T, b: T?) -> boolean)?,
|
|
||||||
onError: ((error: unknown) -> ())?,
|
|
||||||
scheduler: ((fn: () -> ()) -> ())?,
|
|
||||||
once: boolean?,
|
|
||||||
}
|
|
||||||
|
|
||||||
local function untracked<T>(callback: () -> T): T
|
|
||||||
local currentSub = setCurrentSub(nil)
|
|
||||||
local success, result = pcall(callback)
|
|
||||||
setCurrentSub(currentSub)
|
|
||||||
if not success then
|
|
||||||
error(result)
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reaction<T>(
|
|
||||||
dataFn: () -> T,
|
|
||||||
effectFn: (newValue: T, oldValue: T?) -> (),
|
|
||||||
options: ReactionOptions<T>?
|
|
||||||
): () -> ()
|
|
||||||
local options: ReactionOptions<T> = options or {}
|
|
||||||
local scheduler: (() -> ()) -> () = options.scheduler or function(fn)
|
|
||||||
return fn()
|
|
||||||
end
|
|
||||||
local equals: (T, T?) -> boolean = options.equals or function(a, b)
|
|
||||||
return a == b
|
|
||||||
end
|
|
||||||
local onError = options.onError
|
|
||||||
local once = options.once or false
|
|
||||||
local fireImmediately = options.fireImmediately or false
|
|
||||||
|
|
||||||
local prevValue: T?
|
|
||||||
local version = 0
|
|
||||||
|
|
||||||
local tracked = computed(function()
|
|
||||||
local success, result = pcall(dataFn)
|
|
||||||
if not success then
|
|
||||||
untracked(function()
|
|
||||||
if onError then
|
|
||||||
onError(result)
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end)
|
|
||||||
return prevValue
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end)
|
|
||||||
|
|
||||||
local dispose
|
|
||||||
dispose = effect(function()
|
|
||||||
local current = tracked()
|
|
||||||
if not fireImmediately and version == 0 then
|
|
||||||
prevValue = current
|
|
||||||
end
|
|
||||||
version += 1
|
|
||||||
if equals(current :: T, prevValue) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local oldValue = prevValue
|
|
||||||
prevValue = current
|
|
||||||
untracked(function()
|
|
||||||
scheduler(function()
|
|
||||||
local success = pcall(effectFn, current :: T, oldValue)
|
|
||||||
if not success and onError then
|
|
||||||
onError(error)
|
|
||||||
end
|
|
||||||
if once then
|
|
||||||
if (fireImmediately and version > 1) or (not fireImmediately and version > 0) then
|
|
||||||
dispose()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
return nil
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return dispose
|
|
||||||
end
|
|
||||||
|
|
||||||
test("#48", function()
|
|
||||||
local source = atom(0)
|
|
||||||
local disposeInner: () -> ()
|
|
||||||
|
|
||||||
reaction(function()
|
|
||||||
return source()
|
|
||||||
end, function(val)
|
|
||||||
if val == 1 then
|
|
||||||
disposeInner = reaction(function()
|
|
||||||
return source()
|
|
||||||
end, function() end)
|
|
||||||
elseif val == 2 then
|
|
||||||
disposeInner()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
source(1)
|
|
||||||
source(2)
|
|
||||||
source(3)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
local core = require("../../core")
|
|
||||||
local suite = require("../../../../../test/suite")
|
|
||||||
|
|
||||||
local computed = core.computed
|
|
||||||
local setCurrentSub = core.setCurrentSub
|
|
||||||
local atom = core.atom
|
|
||||||
local test = suite.test
|
|
||||||
|
|
||||||
test("should pause tracking", function()
|
|
||||||
local src = atom(0)
|
|
||||||
local c = computed(function()
|
|
||||||
local currentSub = setCurrentSub(nil)
|
|
||||||
local value = src()
|
|
||||||
setCurrentSub(currentSub)
|
|
||||||
return value
|
|
||||||
end)
|
|
||||||
assert(c() == 0, "should not track src")
|
|
||||||
|
|
||||||
src(1)
|
|
||||||
assert(c() == 0, "should not track src")
|
|
||||||
end)
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
@ -98,4 +98,21 @@ test("should track inner subs", function()
|
||||||
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
|
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should skip redundant updates", function()
|
||||||
|
local order = {}
|
||||||
|
local a = atom(0)
|
||||||
|
|
||||||
|
listen(function(): "even" | "odd"
|
||||||
|
return if a() % 2 == 0 then "even" else "odd"
|
||||||
|
end, function(value)
|
||||||
|
table.insert(order, value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a(1)
|
||||||
|
a(3)
|
||||||
|
a(4)
|
||||||
|
a(6)
|
||||||
|
assert(table.concat(order) == "evenoddeven", `bad order: {table.concat(order)}`)
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
local Charm = require("../")
|
|
||||||
local suite = require("../../../../test/suite")
|
|
||||||
|
|
||||||
local atom = Charm.atom
|
|
||||||
local computed = Charm.computed
|
|
||||||
local effect = Charm.effect
|
|
||||||
local onCleanup = Charm.onCleanup
|
|
||||||
local peek = Charm.peek
|
|
||||||
local test = suite.test
|
|
||||||
|
|
||||||
test("should not run detached", function()
|
|
||||||
local a = atom(1)
|
|
||||||
local spyCalls = 0
|
|
||||||
|
|
||||||
effect(function()
|
|
||||||
a()
|
|
||||||
peek(function()
|
|
||||||
onCleanup(function()
|
|
||||||
spyCalls += 1
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert(spyCalls == 0, "should not be called yet")
|
|
||||||
a(2)
|
|
||||||
assert(spyCalls == 1, "should be called once")
|
|
||||||
a(3)
|
|
||||||
assert(spyCalls == 2, "should be called twice")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("should block tracking inside effects", function()
|
|
||||||
local a = atom(1)
|
|
||||||
local b = atom(2)
|
|
||||||
local spyCalls = 0
|
|
||||||
|
|
||||||
effect(function()
|
|
||||||
peek(function()
|
|
||||||
spyCalls += 1
|
|
||||||
return a() + b()
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
assert(spyCalls == 1, "should be called once")
|
|
||||||
|
|
||||||
a(10)
|
|
||||||
b(20)
|
|
||||||
assert(spyCalls == 1, "should be called once")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("should block tracking even when run inside effect run inside peek", function()
|
|
||||||
local s = atom(1)
|
|
||||||
local spyCalls = 0
|
|
||||||
|
|
||||||
peek(function()
|
|
||||||
effect(function()
|
|
||||||
peek(function()
|
|
||||||
spyCalls += 1
|
|
||||||
return s()
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
assert(spyCalls == 1, "should be called once")
|
|
||||||
|
|
||||||
s(2)
|
|
||||||
assert(spyCalls == 1, "should be called once")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("should not cause atom assignments throw", function()
|
|
||||||
local a = atom(1)
|
|
||||||
local aChangedTime = atom(0)
|
|
||||||
|
|
||||||
local dispose = effect(function()
|
|
||||||
a()
|
|
||||||
peek(function()
|
|
||||||
aChangedTime(aChangedTime() + 1)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
a(2)
|
|
||||||
assert(aChangedTime() == 2, "aChangedTime should be 2")
|
|
||||||
a(3)
|
|
||||||
assert(aChangedTime() == 3, "aChangedTime should be 3")
|
|
||||||
|
|
||||||
dispose()
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("should block tracking inside computed atoms", function()
|
|
||||||
local a = atom(1)
|
|
||||||
local b = atom(2)
|
|
||||||
local spyCalls = 0
|
|
||||||
local c = computed(function()
|
|
||||||
return peek(function()
|
|
||||||
spyCalls += 1
|
|
||||||
return a() + b()
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert(spyCalls == 0, "should not be called yet")
|
|
||||||
assert(c() == 3, "c should be 3")
|
|
||||||
a(10)
|
|
||||||
c()
|
|
||||||
b(20)
|
|
||||||
c()
|
|
||||||
assert(spyCalls == 1, "should be called once")
|
|
||||||
assert(c() == 3, "c should be 3")
|
|
||||||
end)
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
local suite = require("../../../../test/suite")
|
local suite = require("../../../../test/suite")
|
||||||
|
|
||||||
suite.describe("core", function()
|
suite.describe("core", function()
|
||||||
|
suite.describe("atom", function()
|
||||||
|
require("./core/atom.spec")
|
||||||
|
end)
|
||||||
suite.describe("computed", function()
|
suite.describe("computed", function()
|
||||||
require("./core/computed.spec")
|
require("./core/computed.spec")
|
||||||
end)
|
end)
|
||||||
|
|
@ -10,18 +13,12 @@ suite.describe("core", function()
|
||||||
suite.describe("effectScope", function()
|
suite.describe("effectScope", function()
|
||||||
require("./core/effectScope.spec")
|
require("./core/effectScope.spec")
|
||||||
end)
|
end)
|
||||||
suite.describe("issue_48", function()
|
|
||||||
require("./core/issue_48.spec")
|
|
||||||
end)
|
|
||||||
suite.describe("gc", function()
|
suite.describe("gc", function()
|
||||||
require("./core/gc.spec")
|
require("./core/gc.spec")
|
||||||
end)
|
end)
|
||||||
suite.describe("topology", function()
|
suite.describe("topology", function()
|
||||||
require("./core/topology.spec")
|
require("./core/topology.spec")
|
||||||
end)
|
end)
|
||||||
suite.describe("untrack", function()
|
|
||||||
require("./core/untrack.spec")
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
suite.describe("listen", function()
|
suite.describe("listen", function()
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,6 @@ test("should track inner subs", function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(table.concat(order) == "", "should not run immediately")
|
|
||||||
|
|
||||||
a(a() + 1)
|
a(a() + 1)
|
||||||
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||||
|
|
||||||
|
|
@ -118,4 +116,21 @@ test("should track inner subs", function()
|
||||||
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
|
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
test("should skip redundant updates", function()
|
||||||
|
local order = {}
|
||||||
|
local a = atom(0)
|
||||||
|
|
||||||
|
subscribe(function(): "even" | "odd"
|
||||||
|
return if a() % 2 == 0 then "even" else "odd"
|
||||||
|
end, function(value)
|
||||||
|
table.insert(order, value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
a(1)
|
||||||
|
a(3)
|
||||||
|
a(4)
|
||||||
|
a(6)
|
||||||
|
assert(table.concat(order) == "oddeven", `bad order: {table.concat(order)}`)
|
||||||
|
end)
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,28 @@ local atom = Charm.atom
|
||||||
local computed = Charm.computed
|
local computed = Charm.computed
|
||||||
local effect = Charm.effect
|
local effect = Charm.effect
|
||||||
local untracked = Charm.untracked
|
local untracked = Charm.untracked
|
||||||
local onCleanup = Charm.onCleanup
|
|
||||||
local test = suite.test
|
local test = suite.test
|
||||||
|
|
||||||
test("should run detached", function()
|
test("should not run detached", function()
|
||||||
local a = atom(1)
|
local a = atom(1)
|
||||||
local spyCalls = 0
|
local spyCalls = 0
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
a()
|
a()
|
||||||
untracked(function()
|
untracked(function()
|
||||||
onCleanup(function()
|
effect(function()
|
||||||
spyCalls += 1
|
return function()
|
||||||
end, true)
|
spyCalls += 1
|
||||||
|
end
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(spyCalls == 0, "should not be called")
|
assert(spyCalls == 0, "should not be called yet")
|
||||||
a(2)
|
a(2)
|
||||||
assert(spyCalls == 0, "rerun should not call cleanup")
|
assert(spyCalls == 1, "should be called once")
|
||||||
a(3)
|
a(3)
|
||||||
assert(spyCalls == 0, "rerun should not call cleanup")
|
assert(spyCalls == 2, "should be called twice")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should block tracking inside effects", function()
|
test("should block tracking inside effects", function()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
-- Adapted from Alien Signals
|
||||||
|
-- https://github.com/stackblitz/alien-signals
|
||||||
|
-- https://github.com/Nicell/alien-signals-luau
|
||||||
|
|
||||||
local config = require("./flags")
|
local config = require("./flags")
|
||||||
local system = require("./system")
|
local system = require("./system")
|
||||||
local utils = require("./utils")
|
local utils = require("./utils")
|
||||||
|
|
@ -8,6 +12,7 @@ export type Link = system.Link
|
||||||
|
|
||||||
local ReactiveFlags = system.ReactiveFlags
|
local ReactiveFlags = system.ReactiveFlags
|
||||||
local createReactiveSystem = system.createReactiveSystem
|
local createReactiveSystem = system.createReactiveSystem
|
||||||
|
local deepFreeze = utils.deepFreeze
|
||||||
local noYield = utils.noYield
|
local noYield = utils.noYield
|
||||||
|
|
||||||
local NONE = 0b0000000
|
local NONE = 0b0000000
|
||||||
|
|
@ -211,6 +216,9 @@ end
|
||||||
local function writeAtom<T>(atom: Atom<T>, oldValue: T, newValue: T): T
|
local function writeAtom<T>(atom: Atom<T>, oldValue: T, newValue: T): T
|
||||||
local equals = atom.equals
|
local equals = atom.equals
|
||||||
if if equals then not equals(oldValue, newValue :: T) else oldValue ~= newValue then
|
if if equals then not equals(oldValue, newValue :: T) else oldValue ~= newValue then
|
||||||
|
if config.frozen then
|
||||||
|
deepFreeze(newValue)
|
||||||
|
end
|
||||||
atom.value = newValue
|
atom.value = newValue
|
||||||
atom.flags = bit32.bor(MUTABLE, DIRTY)
|
atom.flags = bit32.bor(MUTABLE, DIRTY)
|
||||||
local subs = atom.subs
|
local subs = atom.subs
|
||||||
|
|
@ -323,6 +331,9 @@ local function unwatched(node: Atom | Computed | Effect | EffectScope)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function atom<T>(initialValue: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T
|
local function atom<T>(initialValue: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T
|
||||||
|
if config.frozen then
|
||||||
|
deepFreeze(initialValue)
|
||||||
|
end
|
||||||
return atomGetterSetter({
|
return atomGetterSetter({
|
||||||
previousValue = initialValue,
|
previousValue = initialValue,
|
||||||
value = initialValue,
|
value = initialValue,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@ local function inlineTest(): string
|
||||||
return debug.info(1, "n")
|
return debug.info(1, "n")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local isO2 = inlineTest() ~= "inlineTest"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
-- Enable strict mode in O2
|
--- Enforces synchronous, non-yielding behavior in atoms and effects.
|
||||||
strict = inlineTest() == "inlineTest",
|
--- Enabled if the optimize level is below 2, which is the case in studio.
|
||||||
|
strict = not isO2,
|
||||||
|
--- Enforces immutability of atoms by deep-freezing table values. Disabled
|
||||||
|
--- by default, but can be enabled to detect accidental mutations.
|
||||||
|
frozen = false,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
packages/charm/src/index.d.ts
vendored
13
packages/charm/src/index.d.ts
vendored
|
|
@ -12,6 +12,7 @@ export type Equals<T> = (current: T, incoming: T) => boolean;
|
||||||
|
|
||||||
export const flags: {
|
export const flags: {
|
||||||
strict: boolean;
|
strict: boolean;
|
||||||
|
frozen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function atom<T>(value: T, equals?: Equals<T>): Atom<T>;
|
export function atom<T>(value: T, equals?: Equals<T>): Atom<T>;
|
||||||
|
|
@ -63,6 +64,14 @@ export function batched<Args extends any[], Result>(
|
||||||
...args: Args
|
...args: Args
|
||||||
): (...args: Args) => Result;
|
): (...args: Args) => Result;
|
||||||
|
|
||||||
export function peek<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
|
||||||
|
|
||||||
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use `batched()` instead.
|
||||||
|
*/
|
||||||
|
export const batch: typeof batched;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use `untracked()` instead.
|
||||||
|
*/
|
||||||
|
export const peek: typeof untracked;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ local flags = require("@self/flags")
|
||||||
local listen = require("@self/listen")
|
local listen = require("@self/listen")
|
||||||
local mapped = require("@self/mapped")
|
local mapped = require("@self/mapped")
|
||||||
local observe = require("@self/observe")
|
local observe = require("@self/observe")
|
||||||
local peek = require("@self/peek")
|
|
||||||
local subscribe = require("@self/subscribe")
|
local subscribe = require("@self/subscribe")
|
||||||
local types = require("@self/types")
|
local types = require("@self/types")
|
||||||
local untracked = require("@self/untracked")
|
local untracked = require("@self/untracked")
|
||||||
|
|
@ -12,6 +11,11 @@ local untracked = require("@self/untracked")
|
||||||
export type Atom<T = any> = types.Atom<T>
|
export type Atom<T = any> = types.Atom<T>
|
||||||
export type Selector<T = any> = types.Selector<T>
|
export type Selector<T = any> = types.Selector<T>
|
||||||
|
|
||||||
|
--- @deprecated Function `batch()` is deprecated, use `batched()` instead.
|
||||||
|
local batch = batched
|
||||||
|
--- @deprecated Function `peek()` is deprecated, use `untracked()` instead.
|
||||||
|
local peek = untracked
|
||||||
|
|
||||||
return {
|
return {
|
||||||
core = core,
|
core = core,
|
||||||
flags = flags,
|
flags = flags,
|
||||||
|
|
@ -24,7 +28,9 @@ return {
|
||||||
listen = listen,
|
listen = listen,
|
||||||
mapped = mapped,
|
mapped = mapped,
|
||||||
observe = observe,
|
observe = observe,
|
||||||
peek = peek,
|
|
||||||
subscribe = subscribe,
|
subscribe = subscribe,
|
||||||
untracked = untracked,
|
untracked = untracked,
|
||||||
|
-- Temporary aliases for backwards compatibility
|
||||||
|
batch = batch,
|
||||||
|
peek = peek,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
local peek = require("./peek")
|
local untracked = require("./untracked")
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
|
@ -19,7 +19,7 @@ local function listen<T>(selector: () -> T, listener: (state: T, previous: T?, d
|
||||||
dispose = effect(function()
|
dispose = effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = tracked()
|
current = tracked()
|
||||||
peek(listener, current, previous, dispose)
|
untracked(listener, current, previous, dispose)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if disposed then
|
if disposed then
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
local core = require("./core")
|
|
||||||
|
|
||||||
local setCurrentTracking = core.setCurrentTracking
|
|
||||||
|
|
||||||
local function handlePeek(prevTracking: boolean, ok: boolean, ...)
|
|
||||||
setCurrentTracking(prevTracking)
|
|
||||||
if ok then
|
|
||||||
return ...
|
|
||||||
else
|
|
||||||
error((...))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
|
||||||
return handlePeek(setCurrentTracking(false), pcall(callback, ...))
|
|
||||||
end
|
|
||||||
|
|
||||||
return peek
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
local peek = require("./peek")
|
local untracked = require("./untracked")
|
||||||
|
|
||||||
local computed = core.computed
|
local computed = core.computed
|
||||||
local effect = core.effect
|
local effect = core.effect
|
||||||
|
|
@ -18,7 +18,7 @@ local function subscribe<T>(selector: () -> T, listener: (state: T, lastState: T
|
||||||
if mounted then
|
if mounted then
|
||||||
mounted = false
|
mounted = false
|
||||||
else
|
else
|
||||||
peek(listener, current, previous)
|
untracked(listener, current, previous)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,6 @@
|
||||||
--[[
|
-- Adapted from Alien Signals
|
||||||
alien-signals 2.0.1 implementation adopted from
|
-- https://github.com/stackblitz/alien-signals
|
||||||
https://github.com/Nicell/alien-signals-luau
|
-- https://github.com/Nicell/alien-signals-luau
|
||||||
https://github.com/stackblitz/alien-signals
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024-present Johnson Chu, Nicholas Winans
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
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 = {
|
export type ReactiveNode = {
|
||||||
deps: Link?,
|
deps: Link?,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
local core = require("./core")
|
local core = require("./core")
|
||||||
|
|
||||||
local setCurrentSub = core.setCurrentSub
|
local setCurrentTracking = core.setCurrentTracking
|
||||||
|
|
||||||
local function handleUntracked(prevSub: core.ReactiveNode?, ok: boolean, ...)
|
local function handleUntracked(prevTracking: boolean, ok: boolean, ...)
|
||||||
setCurrentSub(prevSub)
|
setCurrentTracking(prevTracking)
|
||||||
if ok then
|
if ok then
|
||||||
return ...
|
return ...
|
||||||
else
|
else
|
||||||
|
|
@ -12,7 +12,7 @@ local function handleUntracked(prevSub: core.ReactiveNode?, ok: boolean, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
|
||||||
return handleUntracked(setCurrentSub(nil), pcall(callback, ...))
|
return handleUntracked(setCurrentTracking(false), pcall(callback, ...))
|
||||||
end
|
end
|
||||||
|
|
||||||
return untracked
|
return untracked
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,16 @@ local function noYield<Args..., Results...>(callback: (Args...) -> Results..., .
|
||||||
return handleNoYield(thread, coroutine.resume(thread, ...))
|
return handleNoYield(thread, coroutine.resume(thread, ...))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function deepFreeze(object: any)
|
||||||
|
if type(object) == "table" and not table.isfrozen(object) then
|
||||||
|
table.freeze(object)
|
||||||
|
for _, value in object do
|
||||||
|
deepFreeze(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
noYield = noYield :: typeof(pcall),
|
noYield = noYield :: typeof(pcall),
|
||||||
|
deepFreeze = deepFreeze,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ local Charm = require("../packages/charm/src")
|
||||||
local suite = require("./suite")
|
local suite = require("./suite")
|
||||||
|
|
||||||
Charm.flags.strict = true
|
Charm.flags.strict = true
|
||||||
|
Charm.flags.frozen = true
|
||||||
|
|
||||||
suite.describe("charm", function()
|
suite.describe("charm", function()
|
||||||
require("../packages/charm/src/__tests__/spec")
|
require("../packages/charm/src/__tests__/spec")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue