fix: skip nested captures (#22)

* fix: skip nested captures

* test: update atom bench
This commit is contained in:
richard 2024-09-28 17:17:54 -07:00 • committed by GitHub
parent 69e5f78463
commit 7c323e5739
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 71 additions and 44 deletions

View file

@ -1,7 +1,7 @@
-- This file is for use by Benchmarker -- This file is for use by Benchmarker
-- https://boatbomber.itch.io/benchmarker -- https://boatbomber.itch.io/benchmarker
local Package = game:GetService("ReplicatedStorage").Packages.charm local Package = game:GetService("ReplicatedStorage").DevPackages.Charm
local atom = require(Package.atom) local atom = require(Package.atom)
local computed = require(Package.computed) local computed = require(Package.computed)
@ -11,19 +11,19 @@ local function parameterGenerator()
end end
local benchmarks = { local benchmarks = {
atom = function(_, source: any) atom = function(_, state: any)
for _ = 1, 500 do for _ = 1, 500 do
source(math.random(1, 2)) state(math.random(1, 2))
end end
end, end,
computed = function(_, source: any) computed = function(_, state: any)
computed(function() computed(function()
return 2 * source() return 2 * state()
end) end)
for _ = 1, 500 do for _ = 1, 500 do
source(math.random(1, 2)) state(math.random(1, 2))
end end
end, end,
} }

View file

@ -1,5 +1,6 @@
local atom = require(script.Parent.Parent.atom) local atom = require(script.Parent.Parent.atom)
local store = require(script.Parent.Parent.store) local store = require(script.Parent.Parent.store)
local subscribe = require(script.Parent.Parent.subscribe)
return function() return function()
it("gracefully handles yielding", function() it("gracefully handles yielding", function()
@ -29,7 +30,7 @@ return function()
expect(result).to.equal(1) expect(result).to.equal(1)
end) end)
it("allows nested captures", function() it("skips atoms in inner capture calls", function()
local a = atom(1) local a = atom(1)
local b = atom(1) local b = atom(1)
local inner local inner
@ -41,8 +42,20 @@ return function()
end) end)
assert(outer[a], "outer did not capture a") assert(outer[a], "outer did not capture a")
assert(outer[b], "outer did not capture b") assert(outer[b] == nil, "outer captured b")
assert(inner[a] == nil, "inner captured a") assert(inner[a] == nil, "inner captured a")
assert(inner[b], "inner did not capture b") assert(inner[b], "inner did not capture b")
end) end)
it("should not capture notified dependents", function()
local state = atom(1)
subscribe(state, function() end)
local deps = store.capture(function()
return state(2)
end)
expect(deps[state]).to.never.be.ok()
end)
end end

View file

@ -13,5 +13,7 @@ return function()
afterEach(function() afterEach(function()
table.clear(store.listeners :: any) table.clear(store.listeners :: any)
table.clear(store.capturing.stack)
store.capturing.index = 0
end) end)
end end

View file

@ -15,8 +15,10 @@ local function atom<T>(state: T, options: AtomOptions<T>?): Atom<T>
local function atom(...) local function atom(...)
if select("#", ...) == 0 then if select("#", ...) == 0 then
for set in next, store.capturing do local index = store.capturing.index
set[atom] = true
if index > 0 then
store.capturing.stack[index][atom] = true
end end
return state return state

View file

@ -4,10 +4,15 @@ type Set<T> = types.Set<T>
type WeakMap<K, V> = types.WeakMap<K, V> type WeakMap<K, V> = types.WeakMap<K, V>
local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" }) local listeners: WeakMap<Atom<any>, WeakMap<() -> (), unknown>> = setmetatable({}, { __mode = "k" })
local capturing: Set<Set<Atom<any>>> = {}
local batched: Set<() -> ()> = {} local batched: Set<() -> ()> = {}
local batching = false local batching = false
local capturing = {
stack = {} :: { Set<Atom<any>> },
index = 0,
}
--[=[ --[=[
Calls the given function and returns the result. If the function yields or Calls the given function and returns the result. If the function yields or
throws an error, the thread is closed and an error is thrown. Regardless of throws an error, the thread is closed and an error is thrown. Regardless of
@ -29,7 +34,9 @@ local function try<T, U...>(callback: (U...) -> T | any, finally: (() -> ())?, .
if coroutine.status(thread) == "suspended" then if coroutine.status(thread) == "suspended" then
local source, line, name = debug.info(callback, "sln") local source, line, name = debug.info(callback, "sln")
coroutine.close(thread) coroutine.close(thread)
error( error(
"Yielding is not allowed in atom functions. Consider wrapping this code in a Promise or task.defer instead." "Yielding is not allowed in atom functions. Consider wrapping this code in a Promise or task.defer instead."
.. `\nFunction defined at: {source}:{line}` .. `\nFunction defined at: {source}:{line}`
@ -37,6 +44,7 @@ local function try<T, U...>(callback: (U...) -> T | any, finally: (() -> ())?, .
) )
elseif not success then elseif not success then
local source, line, name = debug.info(callback, "sln") local source, line, name = debug.info(callback, "sln")
error( error(
"An error occurred while running an atom function" "An error occurred while running an atom function"
.. `\nFunction defined at: {source}:{line}` .. `\nFunction defined at: {source}:{line}`
@ -53,6 +61,7 @@ local function try<T, U...>(callback: (U...) -> T | any, finally: (() -> ())?, .
end end
local success, result = pcall(callback, ...) local success, result = pcall(callback, ...)
finally() finally()
assert(success, result) assert(success, result)
@ -87,6 +96,34 @@ local function notify(atom: Atom<any>)
end end
end end
--[=[
Returns the result of the function without subscribing to changes. If a
non-function value is provided, it is returned as is.
@param molecule The atom or molecule to get the state of.
@param args Arguments to pass to the molecule.
@return The current state.
]=]
local function peek<T, U...>(callback: ((U...) -> T) | T, ...: U...): T
if type(callback) ~= "function" then
return callback
end
if capturing.index == 0 then
return callback(...)
end
capturing.index += 1
capturing.stack[capturing.index] = {}
local result = try(callback, function()
capturing.stack[capturing.index] = nil
capturing.index -= 1
end, ...)
return result
end
--[=[ --[=[
Captures all atoms that are read during the function call and returns them Captures all atoms that are read during the function call and returns them
along with the result of the function. Useful for tracking dependencies. along with the result of the function. Useful for tracking dependencies.
@ -96,14 +133,17 @@ end
]=] ]=]
local function capture<T>(molecule: () -> T): (Set<Atom<any>>, T) local function capture<T>(molecule: () -> T): (Set<Atom<any>>, T)
if listeners[molecule] then if listeners[molecule] then
return { [molecule] = true }, molecule() return { [molecule] = true }, peek(molecule)
end end
local dependencies: Set<Atom<any>> = {} local dependencies: Set<Atom<any>> = {}
capturing[dependencies] = true
capturing.index += 1
capturing.stack[capturing.index] = dependencies
local result = try(molecule, function() local result = try(molecule, function()
capturing[dependencies] = nil capturing.stack[capturing.index] = nil
capturing.index -= 1
end) end)
return dependencies, result return dependencies, result
@ -133,36 +173,6 @@ local function batch(callback: () -> ())
table.clear(batched) table.clear(batched)
end end
--[=[
Returns the result of the function without subscribing to changes. If a
non-function value is provided, it is returned as is.
@param molecule The atom or molecule to get the state of.
@param args Arguments to pass to the molecule.
@return The current state.
]=]
local function peek<T, U...>(callback: ((U...) -> T) | T, ...: U...): T
if type(callback) ~= "function" then
return callback
end
if not next(capturing) then
return callback(...)
end
local snapshot = table.clone(capturing)
table.clear(capturing)
local result = try(callback, function()
for set in next, snapshot do
capturing[set] = true
end
end, ...)
return result
end
--[=[ --[=[
Subscribes the listener to the changes of the given atoms. Subscribes the listener to the changes of the given atoms.