diff --git a/src/__tests__/batch.spec.luau b/src/__tests__/batch.spec.luau index a362350..4929bed 100644 --- a/src/__tests__/batch.spec.luau +++ b/src/__tests__/batch.spec.luau @@ -1,4 +1,5 @@ local atom = require(script.Parent.Parent.atom) +local derive = require(script.Parent.Parent.derive) local effect = require(script.Parent.Parent.effect) local store = require(script.Parent.Parent.store) local subscribe = require(script.Parent.Parent.subscribe) @@ -35,4 +36,26 @@ return function() end) expect(value).to.equal(1) end) + + it("batches derived atoms", function() + local source = atom(1) + local updates = 0 + local computed = 0 + local double = derive(function() + computed += 1 + return source() * 2 + end) + computed = 0 -- derived atoms will run once initially + subscribe(double, function() + updates += 1 + end) + store.batch(function() + source(2) + source(3) + source(4) + end) + expect(updates).to.equal(1) + expect(computed).to.equal(1) + expect(double()).to.equal(8) + end) end diff --git a/src/__tests__/benchmarks/atom.bench.luau b/src/__tests__/benchmarks/atom.bench.luau index 4f44024..9d058a3 100644 --- a/src/__tests__/benchmarks/atom.bench.luau +++ b/src/__tests__/benchmarks/atom.bench.luau @@ -5,6 +5,8 @@ local Package = game:GetService("ReplicatedStorage").Packages.charm local atom = require(Package.atom) local derive = require(Package.derive) +local effect = require(Package.effect) +local subscribe = require(Package.subscribe) local function parameterGenerator() return atom({ count = 0 }) @@ -26,14 +28,13 @@ local benchmarks = { end end, - derived_memo = function(_, source: any) - derive(function() + derived_nested = function(_, source: any) + local double = derive(function() return { count = 2 * source().count } - end, { - equals = function(previous, current) - return previous.count == current.count - end, - }) + end) + derive(function() + return { count = 2 * double().count } + end) for _ = 1, 500 do source({ count = math.random(1, 2) }) end diff --git a/src/__tests__/derive.spec.luau b/src/__tests__/derive.spec.luau index 49acedf..7dae523 100644 --- a/src/__tests__/derive.spec.luau +++ b/src/__tests__/derive.spec.luau @@ -1,6 +1,8 @@ local atom = require(script.Parent.Parent.atom) local derive = require(script.Parent.Parent.derive) local store = require(script.Parent.Parent.store) +local collect = require(script.Parent.Parent.utils.collect) +local count = require(script.Parent.Parent.utils.count) return function() it("memoizes the result", function() @@ -35,34 +37,34 @@ return function() expect(quadruple()).to.equal(8) end) - it("garbage collects", function() - -- large values coerce the garbage collector to run - local source = atom(string.rep("x", 1e5)) - local max = 0 + it("collects derived atoms", function() + local source = atom(0) - local function listenersDecreased() - local count = 0 - for _ in next, store.listeners[source] do - count += 1 - end - max = math.max(max, count) - return count < max - end + derive(function() + return source() + end) - for index = 1, 500 do - derive(function() - return string.upper(source()) + collect() + + assert(count(store.listeners[source]) == 0, "derived atom did not gc") + end) + + it("collects nested derived atoms", function() + local source = atom(0) + + do + -- Declare this in another scope to ensure no references are kept + local derived = derive(function() + return source() + end) + + derive(function() + return derived() * 2 end) - if index % 50 == 0 then - -- derived atoms should be collected when memory is stressed - source(source() .. "x") - task.wait() - end - if listenersDecreased() then - return - end end - error("None of the derived atoms were collected") + collect() + + assert(count(store.listeners[source]) == 0, "nested derived atom did not gc") end) end diff --git a/src/__tests__/subscribe.spec.luau b/src/__tests__/subscribe.spec.luau index cf6d379..5973d35 100644 --- a/src/__tests__/subscribe.spec.luau +++ b/src/__tests__/subscribe.spec.luau @@ -1,6 +1,9 @@ local atom = require(script.Parent.Parent.atom) local derive = require(script.Parent.Parent.derive) +local store = require(script.Parent.Parent.store) local subscribe = require(script.Parent.Parent.subscribe) +local collect = require(script.Parent.Parent.utils.collect) +local count = require(script.Parent.Parent.utils.count) return function() it("runs on state change", function() @@ -27,25 +30,41 @@ return function() local source = atom(1) local updates = 0 - for _ = 1, 1000 do - -- The idea here is to test whether subscribe() prevents every derived - -- atom in a chain from being garbage collected. If any are GCed, then - -- the subscription will stop updating and the test will fail. - local derived = derive(function() - return source() - end) - subscribe( - derive(function() - return derived() * 2 - end), - function() - updates += 1 - end - ) + -- Create a new scope to encapsulate variables with a limited lifetime, like + -- 'unsubscribe' and 'derived', to ensure they are garbage collected. + -- + -- The idea here is to test whether subscribe() prevents every derived + -- atom in a chain from being garbage collected. If any are GCed, then + -- the subscription will stop updating and the test will fail. + do + local unsubscribe + + do + local derived = derive(function() + return source() + end) + + unsubscribe = subscribe( + derive(function() + return derived() * 2 + end), + function() + updates += 1 + end + ) + end + + collect() + source(source() + 1) + + assert(updates == 1, "subscription stopped updating") + assert(count(store.listeners[source]) == 1, "derived atom was garbage collected") + + unsubscribe() end - source(source() + 1) - task.wait() - expect(updates).to.equal(1000) + collect() + + assert(count(store.listeners[source]) == 0, "derived atom did not collect") end) end diff --git a/src/utils/collect.luau b/src/utils/collect.luau new file mode 100644 index 0000000..6a9515b --- /dev/null +++ b/src/utils/collect.luau @@ -0,0 +1,8 @@ +local function collect() + for _ = 1, 1e4 do + local _ = table.create(1e3) + end + task.wait() +end + +return collect diff --git a/src/utils/count.luau b/src/utils/count.luau new file mode 100644 index 0000000..821c395 --- /dev/null +++ b/src/utils/count.luau @@ -0,0 +1,9 @@ +local function count(object: any): number + local count = 0 + for _ in next, object do + count += 1 + end + return count +end + +return count