mirror of
https://github.com/littensy/charm.git
synced 2026-10-22 12:41:37 +00:00
perf: optimize mapped, add benches
This commit is contained in:
parent
d56672384a
commit
ada1b80b87
12 changed files with 385 additions and 161 deletions
48
benchmarks/bench.luau
Normal file
48
benchmarks/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 < 1 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, elapsed in times do
|
||||||
|
table.insert(results, `{name}: \t{string.format("%.2f", elapsed / 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,
|
||||||
|
}
|
||||||
99
benchmarks/mapped.bench.luau
Normal file
99
benchmarks/mapped.bench.luau
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
--!optimize 2
|
||||||
|
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
|
local Charm = require(ReplicatedStorage.Packages.Charm)
|
||||||
|
|
||||||
|
local atom = Charm.atom
|
||||||
|
local computed = Charm.computed
|
||||||
|
local mapped = Charm.mapped
|
||||||
|
local pauseTracking = Charm.signals.pauseTracking
|
||||||
|
local resumeTracking = Charm.signals.resumeTracking
|
||||||
|
|
||||||
|
type Entry = { number }
|
||||||
|
|
||||||
|
local count = 512
|
||||||
|
local initialState: { Entry } = table.create(count)
|
||||||
|
|
||||||
|
for i = 1, count do
|
||||||
|
initialState[i] = table.create(32, i)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mapper(value: Entry): Entry
|
||||||
|
return table.clone(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function simpleMapped(source: () -> { Entry }): () -> { Entry }
|
||||||
|
return computed(function()
|
||||||
|
local state = source()
|
||||||
|
local nextState = {}
|
||||||
|
|
||||||
|
for index, value in state do
|
||||||
|
pauseTracking()
|
||||||
|
local success, valueOut = pcall(mapper, value)
|
||||||
|
resumeTracking()
|
||||||
|
|
||||||
|
if success then
|
||||||
|
nextState[index] = valueOut
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return state
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updatePercentage(percent: number): ({ Entry }) -> { Entry }
|
||||||
|
return function(state)
|
||||||
|
local nextState = table.clone(state)
|
||||||
|
for index, value in state do
|
||||||
|
if math.random() < percent then
|
||||||
|
nextState[index] = table.clone(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nextState
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function test(
|
||||||
|
percent: number,
|
||||||
|
profiler: any,
|
||||||
|
mapped: (src: () -> { Entry }, ...(Entry) -> Entry) -> () -> { Entry }
|
||||||
|
)
|
||||||
|
local src = atom(initialState)
|
||||||
|
local out = mapped(src, mapper)
|
||||||
|
|
||||||
|
profiler.Begin("init")
|
||||||
|
out()
|
||||||
|
profiler.End()
|
||||||
|
|
||||||
|
profiler.Begin("setup")
|
||||||
|
src(updatePercentage(percent))
|
||||||
|
profiler.End()
|
||||||
|
|
||||||
|
profiler.Begin("update")
|
||||||
|
out()
|
||||||
|
profiler.End()
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
Functions = {
|
||||||
|
["mapped 10%"] = function(profiler)
|
||||||
|
test(0.1, profiler, mapped)
|
||||||
|
end,
|
||||||
|
["mapped 50%"] = function(profiler)
|
||||||
|
test(0.5, profiler, mapped)
|
||||||
|
end,
|
||||||
|
["mapped 100%"] = function(profiler)
|
||||||
|
test(1, profiler, mapped)
|
||||||
|
end,
|
||||||
|
["control 10%"] = function(profiler)
|
||||||
|
test(0.1, profiler, simpleMapped)
|
||||||
|
end,
|
||||||
|
["control 50%"] = function(profiler)
|
||||||
|
test(0.5, profiler, simpleMapped)
|
||||||
|
end,
|
||||||
|
["control 100%"] = function(profiler)
|
||||||
|
test(1, profiler, simpleMapped)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
80
benchmarks/mapped.luau
Normal file
80
benchmarks/mapped.luau
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
local Charm = require("@pkgs/charm/src/lib")
|
||||||
|
local bench = require("./bench")
|
||||||
|
|
||||||
|
local atom = Charm.atom
|
||||||
|
local computed = Charm.computed
|
||||||
|
local mapped = Charm.mapped
|
||||||
|
local pauseTracking = Charm.signals.pauseTracking
|
||||||
|
local resumeTracking = Charm.signals.resumeTracking
|
||||||
|
|
||||||
|
type Entry = { number }
|
||||||
|
|
||||||
|
local count = 512
|
||||||
|
local initialState: { Entry } = table.create(count)
|
||||||
|
|
||||||
|
for i = 1, count do
|
||||||
|
initialState[i] = table.create(64, i)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mapper(value: Entry, key: number): (Entry, number)
|
||||||
|
return table.clone(value), key + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function simpleMapped(source: () -> { Entry }, mapper: (Entry, number) -> (Entry, number)): () -> { Entry }
|
||||||
|
return computed(function()
|
||||||
|
local state = source()
|
||||||
|
local nextState = {}
|
||||||
|
|
||||||
|
for index, value in state do
|
||||||
|
pauseTracking()
|
||||||
|
local success, valueOut, keyOut = pcall(mapper, value, index)
|
||||||
|
resumeTracking()
|
||||||
|
|
||||||
|
if success then
|
||||||
|
nextState[keyOut] = valueOut
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return state
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updatePercentage(percent: number): ({ Entry }) -> { Entry }
|
||||||
|
return function(state)
|
||||||
|
local nextState = table.clone(state)
|
||||||
|
for index, value in state do
|
||||||
|
if math.random() < percent then
|
||||||
|
nextState[index] = table.clone(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nextState
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, percent in { 0.1, 0.5, 1 } do
|
||||||
|
bench.run(`mapped {percent}`, function()
|
||||||
|
local src = atom(initialState)
|
||||||
|
local out = mapped(src, mapper)
|
||||||
|
|
||||||
|
bench.profilebegin("init")
|
||||||
|
out()
|
||||||
|
bench.profileend()
|
||||||
|
src(updatePercentage(percent))
|
||||||
|
bench.profilebegin("update")
|
||||||
|
out()
|
||||||
|
bench.profileend()
|
||||||
|
end)
|
||||||
|
|
||||||
|
bench.run(`control {percent}`, function()
|
||||||
|
local src = atom(initialState)
|
||||||
|
local out = simpleMapped(src, mapper)
|
||||||
|
|
||||||
|
bench.profilebegin("init")
|
||||||
|
out()
|
||||||
|
bench.profileend()
|
||||||
|
src(updatePercentage(percent))
|
||||||
|
bench.profilebegin("update")
|
||||||
|
out()
|
||||||
|
bench.profileend()
|
||||||
|
end)
|
||||||
|
end
|
||||||
27
benchmarks/propagate.luau
Normal file
27
benchmarks/propagate.luau
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
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
|
||||||
|
|
@ -22,9 +22,7 @@
|
||||||
"VideCharm": { "$path": "pkgs/vide-charm/tests" }
|
"VideCharm": { "$path": "pkgs/vide-charm/tests" }
|
||||||
},
|
},
|
||||||
"Benchmarks": {
|
"Benchmarks": {
|
||||||
"$className": "Folder",
|
"$path": "benchmarks"
|
||||||
"Charm": { "$path": "pkgs/charm/benchmarks" },
|
|
||||||
"CharmSync": { "$path": "pkgs/charm-sync/benchmarks" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
|
|
||||||
local Charm = require(ReplicatedStorage.Packages.Charm)
|
|
||||||
|
|
||||||
local atom = Charm.atom
|
|
||||||
local computed = Charm.computed
|
|
||||||
local mapped = Charm.mapped
|
|
||||||
|
|
||||||
type Person = {
|
|
||||||
name: string,
|
|
||||||
age: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
local personCount = 1000
|
|
||||||
local initialState: { Person } = table.create(personCount)
|
|
||||||
|
|
||||||
for i = 1, personCount do
|
|
||||||
initialState[i] = {
|
|
||||||
name = "Person " .. i,
|
|
||||||
age = math.random(1, 100),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function mapper(person: Person): Person
|
|
||||||
return {
|
|
||||||
name = person.name .. "!",
|
|
||||||
age = person.age + 1,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function simpleMapped(source: () -> { Person }): () -> { Person }
|
|
||||||
return computed(function()
|
|
||||||
local state = table.clone(source())
|
|
||||||
for index, person in state do
|
|
||||||
state[index] = mapper(person)
|
|
||||||
end
|
|
||||||
return state
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function updatePercentageOfPeople(percent: number): ({ Person }) -> { Person }
|
|
||||||
return function(state)
|
|
||||||
local nextState = table.clone(state)
|
|
||||||
for index, person in state do
|
|
||||||
if math.random() < percent then
|
|
||||||
nextState[index] = table.clone(person)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nextState
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
Functions = {
|
|
||||||
["mapped 50%"] = function(profiler)
|
|
||||||
profiler.Begin("init")
|
|
||||||
local src = atom(initialState)
|
|
||||||
local out = mapped(src, mapper)
|
|
||||||
out()
|
|
||||||
profiler.End()
|
|
||||||
|
|
||||||
profiler.Begin("update")
|
|
||||||
src(updatePercentageOfPeople(0.5))
|
|
||||||
profiler.End()
|
|
||||||
end,
|
|
||||||
|
|
||||||
["simple 50%"] = function(profiler)
|
|
||||||
profiler.Begin("init")
|
|
||||||
local src = atom(initialState)
|
|
||||||
local out = simpleMapped(src)
|
|
||||||
out()
|
|
||||||
profiler.End()
|
|
||||||
|
|
||||||
profiler.Begin("update")
|
|
||||||
src(updatePercentageOfPeople(0.5))
|
|
||||||
profiler.End()
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
local Charm = require("@pkgs/charm/src/lib")
|
|
||||||
|
|
||||||
local atom = Charm.atom
|
|
||||||
local computed = Charm.computed
|
|
||||||
local effect = Charm.effect
|
|
||||||
local listen = Charm.listen
|
|
||||||
|
|
||||||
local signal = Charm.signals.signal
|
|
||||||
local computedSignal = Charm.signals.computed
|
|
||||||
local effectSignal = Charm.signals.effect
|
|
||||||
|
|
||||||
local w = 10
|
|
||||||
local h = 10
|
|
||||||
|
|
||||||
local function bench(name: string, test: () -> ())
|
|
||||||
local start = os.clock()
|
|
||||||
local iter = 0
|
|
||||||
while os.clock() - start < 2 do
|
|
||||||
iter += 1
|
|
||||||
test()
|
|
||||||
end
|
|
||||||
local endTime = os.clock()
|
|
||||||
local elapsed = endTime - start
|
|
||||||
print(`{name}: {elapsed / iter * 1000 * 1000} us/iter`)
|
|
||||||
end
|
|
||||||
|
|
||||||
bench(`listen {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
|
|
||||||
listen(last, function() end)
|
|
||||||
end
|
|
||||||
src(src() + 1)
|
|
||||||
end)
|
|
||||||
|
|
||||||
bench(`effect {w}*{h}`, function()
|
|
||||||
local src = atom(1)
|
|
||||||
for _ = 1, w do
|
|
||||||
local last = src
|
|
||||||
for _ = 1, h do
|
|
||||||
local prev = last
|
|
||||||
last = computed(function()
|
|
||||||
return prev() + 1
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
effect(function()
|
|
||||||
last()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
src(src() + 1)
|
|
||||||
end)
|
|
||||||
|
|
||||||
bench(`signals {w}*{h}`, function()
|
|
||||||
local src, setSrc = signal(1)
|
|
||||||
for _ = 1, w do
|
|
||||||
local last = src
|
|
||||||
for _ = 1, h do
|
|
||||||
local prev = last
|
|
||||||
last = computedSignal(function()
|
|
||||||
return prev() + 1
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
effectSignal(function()
|
|
||||||
last()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
setSrc(src() + 1)
|
|
||||||
end)
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
--!optimize 2
|
||||||
|
|
||||||
local signals = require("./signals")
|
local signals = require("./signals")
|
||||||
|
|
||||||
local computed = signals.computed
|
local computed = signals.computed
|
||||||
|
|
@ -8,12 +10,12 @@ type Mapped =
|
||||||
& (<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
|
& (<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
|
||||||
& (<K, VI, VO>(selector: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO })
|
& (<K, VI, VO>(selector: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO })
|
||||||
|
|
||||||
local function ensureCanMutateOutput<KO, VO>(output: { [KO]: VO }, previous: { [KO]: VO }?): { [KO]: VO }
|
local function ensureCanMutateOutput<KO, VO>(output: { [KO]: VO }, previous: { [KO]: VO }): { [KO]: VO }
|
||||||
return if output == previous then table.clone(output) else output
|
return if output == previous then table.clone(output) else output
|
||||||
end
|
end
|
||||||
|
|
||||||
local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO }
|
local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO }
|
||||||
local currentData: { [KI]: VI } = {}
|
local currentData: { [KI]: VI }
|
||||||
local keyCache: { [KI]: KO } = {}
|
local keyCache: { [KI]: KO } = {}
|
||||||
|
|
||||||
local function map(value: VI, key: KI): (VO, KO)
|
local function map(value: VI, key: KI): (VO, KO)
|
||||||
|
|
@ -29,10 +31,26 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
|
||||||
end
|
end
|
||||||
|
|
||||||
return computed(function(previousOutput)
|
return computed(function(previousOutput)
|
||||||
local output: { [KO]: VO } = previousOutput or {}
|
|
||||||
local previousData = currentData
|
local previousData = currentData
|
||||||
currentData = selector()
|
currentData = selector()
|
||||||
|
|
||||||
|
-- Only perform additions if this is the first run
|
||||||
|
if not previousOutput then
|
||||||
|
local output: { [KO]: VO } = {}
|
||||||
|
|
||||||
|
for key, value in currentData do
|
||||||
|
local valueOut, keyOut = map(value, key)
|
||||||
|
if valueOut ~= nil then
|
||||||
|
output[keyOut] = valueOut
|
||||||
|
keyCache[key] = keyOut
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
local output = previousOutput
|
||||||
|
|
||||||
-- Check equality since the selector might not be a computed value
|
-- Check equality since the selector might not be a computed value
|
||||||
if currentData == previousData then
|
if currentData == previousData then
|
||||||
return output
|
return output
|
||||||
|
|
@ -42,7 +60,6 @@ local function mapped<KI, VI, KO, VO>(selector: () -> { [KI]: VI }, mapper: (ite
|
||||||
for key in previousData do
|
for key in previousData do
|
||||||
if currentData[key] == nil then
|
if currentData[key] == nil then
|
||||||
local keyOut = keyCache[key]
|
local keyOut = keyCache[key]
|
||||||
|
|
||||||
if keyOut then
|
if keyOut then
|
||||||
output = ensureCanMutateOutput(output, previousOutput)
|
output = ensureCanMutateOutput(output, previousOutput)
|
||||||
output[keyOut] = nil
|
output[keyOut] = nil
|
||||||
|
|
|
||||||
107
pkgs/charm/tests/mapped.spec.luau
Normal file
107
pkgs/charm/tests/mapped.spec.luau
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
local Charm = require("../src/lib")
|
||||||
|
local suite = require("@tests/suite")
|
||||||
|
|
||||||
|
local atom = Charm.atom
|
||||||
|
local mapped = Charm.mapped
|
||||||
|
local test = suite.test
|
||||||
|
|
||||||
|
test("should map atom", function()
|
||||||
|
local src = atom({ 1, 2, 3 })
|
||||||
|
local map = mapped(src, function(value: number)
|
||||||
|
return value + 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(table.concat(map()) == "234", table.concat(map()))
|
||||||
|
src({ 4, 5, 6 })
|
||||||
|
assert(table.concat(map()) == "567", table.concat(map()))
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should not map unchanged values", function()
|
||||||
|
local calls = 0
|
||||||
|
|
||||||
|
local src = atom({ 1, 2, 3 })
|
||||||
|
local map = mapped(src, function(value: number)
|
||||||
|
calls += 1
|
||||||
|
return value + 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(table.concat(map()) == "234", table.concat(map()))
|
||||||
|
assert(calls == 3, "should map 3 values")
|
||||||
|
|
||||||
|
src({ 1, 3, 3 })
|
||||||
|
assert(table.concat(map()) == "244", table.concat(map()))
|
||||||
|
assert(calls == 4, "should map 1 change")
|
||||||
|
|
||||||
|
src({})
|
||||||
|
assert(#map() == 0, "should be empty")
|
||||||
|
assert(calls == 4, "should not map for removed values")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should not update if no values change", function()
|
||||||
|
local calls = 0
|
||||||
|
|
||||||
|
local src = atom({ 1, 2, 3 })
|
||||||
|
local map = mapped(src, function(value: number)
|
||||||
|
calls += 1
|
||||||
|
return value + 1
|
||||||
|
end)
|
||||||
|
|
||||||
|
local prev = map()
|
||||||
|
assert(calls == 3, "should map 3 values")
|
||||||
|
assert(table.concat(map()) == "234", table.concat(map()))
|
||||||
|
|
||||||
|
src({ 4, 5, 6, 7 })
|
||||||
|
src({ 1, 2, 3 })
|
||||||
|
assert(calls == 3, "should not map unchanged values")
|
||||||
|
assert(table.concat(map()) == "234", table.concat(map()))
|
||||||
|
assert(prev == map(), "should not create new table")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should filter values", function()
|
||||||
|
local calls = 0
|
||||||
|
|
||||||
|
local src = atom({ a = 1, b = 2, c = 3 })
|
||||||
|
local map = mapped(src, function(value: number)
|
||||||
|
calls += 1
|
||||||
|
return if value < 0 then nil else value
|
||||||
|
end)
|
||||||
|
|
||||||
|
local state = map()
|
||||||
|
assert(calls == 3, "should map 3 values")
|
||||||
|
assert(state.a and state.b and state.c, "should map all values")
|
||||||
|
|
||||||
|
calls = 0
|
||||||
|
src({ a = -1, b = 2, c = 3, d = 4 })
|
||||||
|
state = map()
|
||||||
|
assert(calls == 2, "should map 2 values")
|
||||||
|
assert(state.a == nil, "should remove a")
|
||||||
|
assert(state.b and state.c, "should keep b and c")
|
||||||
|
assert(state.d == 4, "should add d")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("should map keys", function()
|
||||||
|
local calls = 0
|
||||||
|
|
||||||
|
local src = atom({ a = 1, b = 2, c = 3 })
|
||||||
|
local map = mapped(src, function(value: number, key: string)
|
||||||
|
calls += 1
|
||||||
|
return if value < 0 then nil else value, string.upper(key)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local state = map()
|
||||||
|
assert(calls == 3, "should map 3 values")
|
||||||
|
assert(not (state.a and state.b and state.c), "should discard old keys")
|
||||||
|
assert(state.A == 1, "should map a")
|
||||||
|
assert(state.B == 2, "should map b")
|
||||||
|
assert(state.C == 3, "should map c")
|
||||||
|
|
||||||
|
calls = 0
|
||||||
|
src({ a = -1, b = 2, c = 3, d = 4 })
|
||||||
|
state = map()
|
||||||
|
assert(calls == 2, "should map 2 values")
|
||||||
|
assert(state.A == nil and state.a == nil, "should remove a")
|
||||||
|
assert(state.B and state.C, "should keep b and c")
|
||||||
|
assert(state.D == 4, "should add d")
|
||||||
|
end)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
@ -16,8 +16,9 @@ end
|
||||||
|
|
||||||
collect("signals")
|
collect("signals")
|
||||||
spec("gc")
|
spec("gc")
|
||||||
spec("subscribe")
|
|
||||||
spec("listen")
|
spec("listen")
|
||||||
|
spec("mapped")
|
||||||
spec("observe")
|
spec("observe")
|
||||||
|
spec("subscribe")
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue