mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat!: use alien-signals for reactivity (#50)
* feat: use alien-signals * refactor: remove pnpm, fix tests * fix: `effectScope` should set scope * fix: check previous value before calling listener * refactor: improve imports * refactor: exclude re-exports from wally * refactor: embrace scope cleanup, restructure code * fix: update pkgs dir references * ci: simplify pesde install * build: install dev deps with wally * feat: add detached, add more tests * test: simplify mapped test update * chore: rephrase doc comment * perf: optimize subscribe and mapped * refactor: avoid ambiguous optional function type * refactor: add assertion error messages * perf: optimize mapped, add benches * chore: delete redundant benchmarks * perf: simplify mapped logic * test: prettify test library * test: try different gray * test: color brackets * refactor: sync 2.0.4 * refactor: use setCurrentSub instead of pauseTracking * test: improve test suite * test: separate parent from root * refactor: iterate over previous data in mapped * refactor: improve logic * style: use `---` doc comments * refactor: improve logic * chore: housekeeping * ci: add pnpm step * ci: init submodules * test: use lune * refactor: improve structure and alien-signals integration * ci: update test file path * feat: add peek and roblox-ts types * feat: add deep freeze flag, misc refactors * refactor: reduce type casting, housekeeping * refactor: rewrite untracked, update alien-signals * chore: update eslint, housekeeping * refactor: start charm-sync rewrite * fix: send init payload even if atom did not update * test: add all sync tests * feat: prepare for alpha release * feat: add peek, make untracked escape scope * fix: wrap hooks in untracked, update types * feat: add flag for inner effect tracking * refactor: add docs, remove some modules * refactor: align with alien-signals 3.0 * feat: add signal api * refactor: update charm sync api * feat: update alien-signals * refactor: remove unnecessary features, update docs * feat: finalize sync docs * docs: update sync section * docs: simplify examples * docs: capitalize headers * docs: add migration tips * docs: fix whitespace * docs: rephrase computed arg change * refactor: rename batch, misc fixes & perf updates * refactor: simplify charm sync validation * feat: update alien-signals, add recursion flag * feat: add `recursive()` * fix: allow dispose in observers * feat: add `trigger()`, refactor source * docs: update readme * chore: prepare release candidate * test: make some tests more reliable * docs: fix typo * feat: add `reactive()` * chore: bump versions * docs: reword reactive docs * docs: fix typo * feat: add `toRaw()` * fix: check for nil diffs * chore: bump versions * perf: do not create proxies for iteration * fix: make `toRaw` track all calls * fix: check none before patching mutable tables * chore: bump charm-sync version * docs: add fishing game example project * feat: sync signals that return reactive proxy * fix: refine reactive proxy logic * fix: prevent early reactive gc, alien-signals 3.1.1 * fix: wrap iterated values in `reactive()` * fix: loosen client signal types * chore: bump release candidate * fix: remove unsynced signals immediately * test: use lute for tests * chore: bump versions * ci: set up lute * refactor: improve source readability * ci: upgrade lute * build: loosen workspace version requirements * chore: bump deps * fix: update signal overload for setter (#58) * fix: add missing recursed check * test: add important vue reactivity tests * chore: fix react tests, run stylua * chore: update vide * docs: improve intro and explain getters * docs: adjust ordering by importance * docs: move listen nesting note * build: fix react peer dep for ci * chore: update lockfile * ci: use javascript eslint config * ci: ignore eslint config file * refactor: move test directories * feat: allow recursion in effects by default * release: charm v0.11.0-rc.4 * refactor: remove recursive() * refactor: reorganize repo, create deep-charm * docs: rephrase some sections * feat: export getter and setter types * docs: rephrase intro again * feat!: run listen and subscribe callback in untracked * refactor!: remove peek * docs: note peek in migration guide * release: charm v0.11.0-rc.5 * release: charm-sync v0.4.0-rc.7 * release: react-charm v0.4.0-rc.4 * fix: remove old keys when updating key in `mapped()` * refactor: shorten error traceback, housekeeping * refactor: keep error level in some cases * fix: check if cleanup functions disposed effect * refactor!: run effects in batched * ci: check eslint and prettier * perf: run observe fn in untracked * perf: call mapper as-is in `mapped()` * refactor: error for effect cleanup errors * refactor: use new type solver * ci: disable new solver flag until 0.714 * chore: housekeeping * refactor: improve errors in nonstrict, optimize noyield * chore: housekeeping * refactor: satisfy new solver * refactor: improve error traceback * docs: link roblox signals * chore: housekeeping * perf: remove `clearPending` inline operation * refactor: check type of cleanup * ci: remove selene * fix: always return pending value in signal setter * test: add multi benchmark * release: charm v0.11.0-rc.6 * test: add cleanup order tests * chore: bump deps * docs: add warning for deep-charm and note for `observe()` --------- Co-authored-by: safazi <safazics@gmail.com>
This commit is contained in:
parent
d0b7cff943
commit
573533058f
148 changed files with 9222 additions and 4384 deletions
48
benches/bench.luau
Normal file
48
benches/bench.luau
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
local profiles: { [string]: number } = {}
|
||||
local times: { [string]: number } = {}
|
||||
local stack: { string } = {}
|
||||
|
||||
local function run(name: string, test: () -> ())
|
||||
local start = os.clock()
|
||||
local iter = 0
|
||||
|
||||
table.clear(profiles)
|
||||
table.clear(times)
|
||||
table.clear(stack)
|
||||
|
||||
while os.clock() - start < 2 do
|
||||
iter += 1
|
||||
test()
|
||||
end
|
||||
|
||||
local elapsed = os.clock() - start
|
||||
local results = {}
|
||||
|
||||
table.insert(results, `{name}: \t{string.format("%.2f", elapsed / iter * 1000 * 1000)} us/iter`)
|
||||
|
||||
for name, duration in times do
|
||||
table.insert(results, `{name}: \t{string.format("%.2f", duration / iter * 1000 * 1000)} us/iter`)
|
||||
end
|
||||
|
||||
print(table.concat(results, " | "))
|
||||
end
|
||||
|
||||
local function profilebegin(name: string)
|
||||
profiles[name] = os.clock()
|
||||
table.insert(stack, name)
|
||||
end
|
||||
|
||||
local function profileend()
|
||||
local name = table.remove(stack)
|
||||
|
||||
if name then
|
||||
times[name] = (times[name] or 0) + os.clock() - profiles[name]
|
||||
profiles[name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
run = run,
|
||||
profilebegin = profilebegin,
|
||||
profileend = profileend,
|
||||
}
|
||||
84
benches/mapped.luau
Normal file
84
benches/mapped.luau
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
local Charm = require("../packages/charm/src")
|
||||
local bench = require("./bench")
|
||||
|
||||
local computed = Charm.computed
|
||||
local mapped = Charm.mapped
|
||||
local signal = Charm.signal
|
||||
|
||||
type Key = number
|
||||
type Value = number
|
||||
|
||||
local count = 512
|
||||
local initialState: { [Key]: Value } = table.create(count, 0)
|
||||
|
||||
local function expensiveOperation()
|
||||
for _ = 1, 100 do
|
||||
continue
|
||||
end
|
||||
end
|
||||
|
||||
local function simpleMapped(
|
||||
source: () -> { [Key]: Value },
|
||||
mapper: (Value, Key) -> (Value, Key?)
|
||||
): () -> { [Key]: Value }
|
||||
return computed(function()
|
||||
local state = source()
|
||||
local nextState = {}
|
||||
|
||||
for keyIn, valueIn in state do
|
||||
local valueOut, keyOut = mapper(valueIn, keyIn)
|
||||
nextState[if keyOut == nil then keyIn else keyOut] = valueOut
|
||||
end
|
||||
|
||||
return nextState
|
||||
end)
|
||||
end
|
||||
|
||||
local function updateRandomly(updateChance: number): ({ [Key]: Value }) -> { [Key]: Value }
|
||||
return function(state)
|
||||
local nextState = table.clone(state)
|
||||
for key in state do
|
||||
if math.random() < updateChance then
|
||||
nextState[key] += 1
|
||||
end
|
||||
end
|
||||
return nextState
|
||||
end
|
||||
end
|
||||
|
||||
for _, updateChance in { 0.1, 0.5, 1 } do
|
||||
local function mapper(value: Value, key: Key): (Value, Key?)
|
||||
expensiveOperation()
|
||||
return value + 1, key + 1
|
||||
end
|
||||
|
||||
bench.run(`mapped {updateChance}`, function()
|
||||
local src, setSrc = signal(initialState)
|
||||
local out = mapped(src, mapper)
|
||||
|
||||
bench.profilebegin("init")
|
||||
out()
|
||||
bench.profileend()
|
||||
|
||||
setSrc(updateRandomly(updateChance))
|
||||
|
||||
bench.profilebegin("update")
|
||||
out()
|
||||
bench.profileend()
|
||||
end)
|
||||
|
||||
bench.run(`control {updateChance}`, function()
|
||||
local src, setSrc = signal(initialState)
|
||||
local out = simpleMapped(src, mapper)
|
||||
|
||||
bench.profilebegin("init")
|
||||
out()
|
||||
bench.profileend()
|
||||
|
||||
setSrc(updateRandomly(updateChance))
|
||||
|
||||
bench.profilebegin("update")
|
||||
out()
|
||||
bench.profileend()
|
||||
end)
|
||||
end
|
||||
188
benches/multi.luau
Normal file
188
benches/multi.luau
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
local Charm = require("../packages/charm/src")
|
||||
local bench = require("./bench")
|
||||
|
||||
type Atom<T> = Charm.Atom<T>
|
||||
|
||||
local signal = Charm.atom
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
|
||||
local function busy(): number
|
||||
local a = 0
|
||||
for i = 1, 100 do
|
||||
a += 1
|
||||
end
|
||||
return a
|
||||
end
|
||||
|
||||
do
|
||||
local head = signal(0)
|
||||
local computed1 = computed(function()
|
||||
return head()
|
||||
end)
|
||||
local computed2 = computed(function()
|
||||
computed1()
|
||||
return 0
|
||||
end)
|
||||
local computed3 = computed(function()
|
||||
busy() -- heavy computation
|
||||
return computed2() + 1
|
||||
end)
|
||||
local computed4 = computed(function()
|
||||
return computed3() + 2
|
||||
end)
|
||||
local computed5 = computed(function()
|
||||
return computed4() + 3
|
||||
end)
|
||||
effect(function()
|
||||
computed5()
|
||||
busy() -- heavy side effect
|
||||
end)
|
||||
|
||||
bench.run("avoidablePropagation", function()
|
||||
head(1)
|
||||
assert(computed5() == 6)
|
||||
for i = 0, 999 do
|
||||
head(i)
|
||||
end
|
||||
assert(computed5() == 6)
|
||||
end)
|
||||
end
|
||||
|
||||
do
|
||||
local head = signal(0)
|
||||
local double = computed(function()
|
||||
return head() * 2
|
||||
end)
|
||||
local inverse = computed(function()
|
||||
return -head()
|
||||
end)
|
||||
local current = computed(function()
|
||||
local result = 0
|
||||
for i = 0, 19 do
|
||||
result += if head() % 2 == 1 then double() else inverse()
|
||||
end
|
||||
return result
|
||||
end)
|
||||
|
||||
local callCounter = 0
|
||||
effect(function()
|
||||
current()
|
||||
callCounter += 1
|
||||
end)
|
||||
|
||||
bench.run("unstable", function()
|
||||
head(1)
|
||||
assert(current() == 40)
|
||||
callCounter = 0
|
||||
for i = 0, 99 do
|
||||
head(i)
|
||||
end
|
||||
assert(callCounter == 100)
|
||||
end)
|
||||
end
|
||||
|
||||
do
|
||||
local head = signal(0)
|
||||
local double = computed(function()
|
||||
return head() * 2
|
||||
end)
|
||||
local inverse = computed(function()
|
||||
return -head()
|
||||
end)
|
||||
local current = computed(function()
|
||||
local result = 0
|
||||
for i = 0, 19 do
|
||||
result += if head() % 2 == 1 and i % 2 == 1 then double() else inverse()
|
||||
end
|
||||
return result
|
||||
end)
|
||||
|
||||
local callCounter = 0
|
||||
effect(function()
|
||||
current()
|
||||
callCounter += 1
|
||||
end)
|
||||
|
||||
bench.run("unstable 50%", function()
|
||||
head(1)
|
||||
assert(current() == 10)
|
||||
callCounter = 0
|
||||
for i = 0, 99 do
|
||||
head(i)
|
||||
end
|
||||
assert(callCounter == 100)
|
||||
end)
|
||||
end
|
||||
|
||||
do
|
||||
local head = signal(0)
|
||||
local last: () -> number = head
|
||||
local callCounter = 0
|
||||
|
||||
for i = 0, 49 do
|
||||
local current = computed(function()
|
||||
return head() + i
|
||||
end)
|
||||
local current2 = computed(function()
|
||||
return current() + 1
|
||||
end)
|
||||
effect(function()
|
||||
current2()
|
||||
callCounter += 1
|
||||
end)
|
||||
last = current2
|
||||
end
|
||||
|
||||
bench.run("broadPropagation", function()
|
||||
head(1)
|
||||
callCounter = 0
|
||||
for i = 0, 49 do
|
||||
head(i)
|
||||
assert(last() == i + 50)
|
||||
end
|
||||
assert(callCounter == 50 * 50)
|
||||
end)
|
||||
end
|
||||
|
||||
do
|
||||
local heads = {}
|
||||
for index = 0, 99 do
|
||||
heads[index] = signal(0)
|
||||
end
|
||||
|
||||
local mux = computed(function()
|
||||
local result = {}
|
||||
for index = 0, 99 do
|
||||
result[index] = heads[index]()
|
||||
end
|
||||
return result
|
||||
end)
|
||||
|
||||
local splited = {}
|
||||
for index = 0, 99 do
|
||||
local x = computed(function()
|
||||
return mux()[index]
|
||||
end)
|
||||
splited[index] = computed(function()
|
||||
return x() + 1
|
||||
end)
|
||||
end
|
||||
|
||||
for _, x in splited do
|
||||
effect(function()
|
||||
x()
|
||||
end)
|
||||
end
|
||||
|
||||
bench.run("mux", function()
|
||||
for i = 0, 9 do
|
||||
heads[i](i)
|
||||
assert(splited[i]() == i + 1)
|
||||
end
|
||||
for i = 0, 9 do
|
||||
heads[i](i * 2)
|
||||
assert(splited[i]() == i * 2 + 1)
|
||||
end
|
||||
end)
|
||||
end
|
||||
47
benches/propagate.luau
Normal file
47
benches/propagate.luau
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
local Charm = require("../packages/charm/src")
|
||||
local bench = require("./bench")
|
||||
|
||||
local computed = Charm.computed
|
||||
local effect = Charm.effect
|
||||
local listen = Charm.listen
|
||||
local signal = Charm.signal
|
||||
|
||||
for _, w in { 10, 100 } do
|
||||
for _, h in { 10, 100 } do
|
||||
bench.run(`propagate {w}*{h}`, function()
|
||||
local src, setSrc = 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
|
||||
setSrc(src() + 1)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
for _, w in { 10, 100 } do
|
||||
for _, h in { 10, 100 } do
|
||||
bench.run(`propagate (listen) {w}*{h}`, function()
|
||||
local src, setSrc = signal(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
|
||||
setSrc(src() + 1)
|
||||
end)
|
||||
end
|
||||
end
|
||||
88
benches/reactive.luau
Normal file
88
benches/reactive.luau
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
local Charm = require("../packages/charm/src")
|
||||
local DeepCharm = require("../packages/deep-charm/src")
|
||||
local bench = require("./bench")
|
||||
|
||||
local effect = Charm.effect
|
||||
local reactive = DeepCharm.reactive
|
||||
|
||||
do
|
||||
local value = reactive({ a = 1 })
|
||||
bench.run("track reactive property", function()
|
||||
effect(function()
|
||||
local _ = value.a
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
do
|
||||
local value = reactive({ a = 1 })
|
||||
bench.run("write reactive property", function()
|
||||
value.a += 1
|
||||
end)
|
||||
end
|
||||
|
||||
for _, amount in { 10, 100, 1000 } do
|
||||
print("---")
|
||||
|
||||
do
|
||||
local rawArray = table.create(amount, 1)
|
||||
local array = reactive(rawArray)
|
||||
|
||||
bench.run(`track for loop, {amount} elements`, function()
|
||||
local sum = 0
|
||||
effect(function()
|
||||
for i = 1, #array do
|
||||
sum += array[i]
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
do
|
||||
local rawArray = table.create(amount, 1)
|
||||
local array = reactive(rawArray)
|
||||
|
||||
bench.run(`track iteration, {amount} elements`, function()
|
||||
local sum = 0
|
||||
effect(function()
|
||||
for _, x in array do
|
||||
sum += x
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
do
|
||||
local rawArray = table.create(amount, 1)
|
||||
local array = reactive(rawArray)
|
||||
|
||||
effect(function()
|
||||
local sum = 0
|
||||
for _, x in array do
|
||||
sum += x
|
||||
end
|
||||
end)
|
||||
|
||||
bench.run(`track index mutation (1st only), {amount} elements`, function()
|
||||
array[1] += 1
|
||||
end)
|
||||
end
|
||||
|
||||
do
|
||||
local rawArray = table.create(amount, 1)
|
||||
local array = reactive(rawArray)
|
||||
|
||||
effect(function()
|
||||
local sum = 0
|
||||
for _, x in array do
|
||||
sum += x
|
||||
end
|
||||
end)
|
||||
|
||||
bench.run(`track index mutation (all), {amount} elements`, function()
|
||||
for i = 1, #array do
|
||||
array[i] += 1
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue