Improve error messages for derived sources

There seemed to be confusion over reactive-scoping (likely due to
incomplete docs), so I've added more explicit error messages when
deriving in non-reactive scopes to warn the user to use `root()` or
`mount()`.
This commit is contained in:
aaron 2023-09-18 09:40:54 +01:00
parent d13f66a1e1
commit c4a0180a2f
9 changed files with 31 additions and 42 deletions

View file

@ -1,12 +1,11 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw)
local trace = require(script.Parent.trace) local trace = require(script.Parent.trace)
local flags = require(script.Parent.flags) local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
local create_node = graph.create_node local create_node = graph.create_node
local get_scope = graph.get_scope local get_owning_scope = graph.get_owning_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
@ -33,10 +32,7 @@ function create_binding<T>(updater: (T) -> T, binding: T)
end end
local owner = get_scope() local owner = get_owning_scope()
if not owner then
throw("cannot bind property in non-reactive scope")
end; assert(owner)
local node = create_node(binding, updater) local node = create_node(binding, updater)

View file

@ -7,8 +7,9 @@ local add_cleanup = graph.add_cleanup
local function cleanup(callback: () -> ()) local function cleanup(callback: () -> ())
local scope = get_scope() local scope = get_scope()
if not scope then
throw("cannot cleanup in a non-reactive scope") if not scope then
throw "cannot cleanup in a non-reactive scope"
end; assert(scope) end; assert(scope)
add_cleanup(scope, callback) add_cleanup(scope, callback)

View file

@ -1,18 +1,14 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
local create_node = graph.create_node local create_node = graph.create_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
local track = graph.track local track = graph.track
local get_scope = graph.get_scope local get_owning_scope = graph.get_owning_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local function derive<T>(source: () -> T): () -> T local function derive<T>(source: () -> T): () -> T
local owner = get_scope() local owner = get_owning_scope()
if not owner then
throw("cannot derive in non-reactive scope")
end; assert(owner)
local node = create_node(false :: any, source) local node = create_node(false :: any, source)

View file

@ -1,17 +1,13 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
local create_node = graph.create_node local create_node = graph.create_node
local get_scope = graph.get_scope local get_owning_scope = graph.get_owning_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
local function effect<T>(callback: (T) -> T, initial_value: T) local function effect<T>(callback: (T) -> T, initial_value: T)
local owner = get_scope() local owner = get_owning_scope()
if not owner then
throw("cannot create effect in non-reactive scope")
end; assert(owner)
local node = create_node(initial_value, callback) local node = create_node(initial_value, callback)

View file

@ -45,6 +45,17 @@ local function get_scope(): Node<unknown>?
return scopes[scopes.n] return scopes[scopes.n]
end end
local function get_owning_scope(): Node<unknown>
local scope = get_scope()
if not scope then
local caller_name = debug.info(2, "n")
return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
elseif scope.effect then
throw("owning scope is not stable; are you trying to derive a new source from within a side-effect?")
end
return scope
end
local function add_child<T>(parent: StartNode<any>, child: Node<any>) local function add_child<T>(parent: StartNode<any>, child: Node<any>)
table.insert(parent, child) table.insert(parent, child)
table.insert(child.parents, parent) table.insert(child.parents, parent)
@ -149,6 +160,7 @@ local function evaluate_node<T>(node: Node<T>)
return cur_value ~= new_value -- node has changed value return cur_value ~= new_value -- node has changed value
end end
-- todo: case where owner is set from an untrack call within an effectful node, children clearing
local function update_from<T>(node: StartNode<T>, n0: number) local function update_from<T>(node: StartNode<T>, n0: number)
if not node[1] then return end if not node[1] then return end
@ -169,7 +181,7 @@ local function update_from<T>(node: StartNode<T>, n0: number)
-- evaluate all queued children -- evaluate all queued children
for i = n0 + 1, n do for i = n0 + 1, n do
local child = update_queue[i] local child = update_queue[i] -- todo: error: index boolean
if not child.effect then continue end if not child.effect then continue end
if evaluate_node(child) then if evaluate_node(child) then
@ -213,6 +225,7 @@ return table.freeze {
close_scope = close_scope, close_scope = close_scope,
evaluate_node = evaluate_node, evaluate_node = evaluate_node,
get_scope = get_scope, get_scope = get_scope,
get_owning_scope = get_owning_scope,
add_cleanup = add_cleanup, add_cleanup = add_cleanup,
set_owner = set_owner, set_owner = set_owner,
destroy = destroy, destroy = destroy,

View file

@ -10,7 +10,7 @@ local create_start_node = graph.create_start_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
local track = graph.track local track = graph.track
local update = graph.update local update = graph.update
local get_scope = graph.get_scope local get_owning_scope = graph.get_owning_scope
local open_scope = graph.open_scope local open_scope = graph.open_scope
local close_scope = graph.close_scope local close_scope = graph.close_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
@ -28,10 +28,7 @@ local function check_primitives(t: {})
end end
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO } local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
local owner = get_scope() local owner = get_owning_scope()
if not owner then
throw("cannot derive in non-reactive scope")
end; assert(owner)
local subowner = create_node(false, false) local subowner = create_node(false, false)
set_owner(subowner, owner) set_owner(subowner, owner)
@ -125,10 +122,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
end end
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO } local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
local owner = get_scope() local owner = get_owning_scope()
if not owner then
throw("cannot derive in non-reactive scope")
end; assert(owner)
local subowner = create_node(false, false) local subowner = create_node(false, false)
set_owner(subowner, owner) set_owner(subowner, owner)

View file

@ -27,7 +27,7 @@ type Node<T> = graph.Node<T>
type StartNode<T> = graph.StartNode<T> type StartNode<T> = graph.StartNode<T>
local create_node = graph.create_node local create_node = graph.create_node
local create_start_node = graph.create_start_node local create_start_node = graph.create_start_node
local get_scope = graph.get_scope local get_owning_scope = graph.get_owning_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local update = graph.update local update = graph.update
local set_owner = graph.set_owner local set_owner = graph.set_owner
@ -150,10 +150,7 @@ local springs: { [SpringData<any>]: StartNode<any> } = {}
setmetatable(springs, { __mode = "v" }) setmetatable(springs, { __mode = "v" })
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
local owner = get_scope() local owner = get_owning_scope()
if not owner then
throw("cannot derive in non-reactive scope")
end; assert(owner)
-- https://en.wikipedia.org/wiki/Damping -- https://en.wikipedia.org/wiki/Damping

View file

@ -9,19 +9,16 @@ local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
local track = graph.track local track = graph.track
local destroy = graph.destroy local destroy = graph.destroy
local get_scope = graph.get_scope local get_owning_scope = graph.get_owning_scope
local open_scope = graph.open_scope local open_scope = graph.open_scope
local close_scope = graph.close_scope local close_scope = graph.close_scope
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U? local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U?
return function(map) local owner = get_owning_scope()
local owner = get_scope()
if not owner then
throw("cannot switch in non-reactive scope")
end; assert(owner)
return function(map)
local last_scope: Node<false>? local last_scope: Node<false>?
local last_component: (() -> U)? local last_component: (() -> U)?

View file

@ -464,7 +464,6 @@ TEST("derive()", wrap_root(function()
local a = source(0) local a = source(0)
local destroy = vide.mount(function() local destroy = vide.mount(function()
local _b = derive(function() local _b = derive(function()
cleanup(function() cleanup(function()
count += 1 count += 1