diff --git a/docs/api/reactivity-core.md b/docs/api/reactivity-core.md index 7f9fe28..02390b7 100644 --- a/docs/api/reactivity-core.md +++ b/docs/api/reactivity-core.md @@ -2,6 +2,26 @@
+## root() + +Creates and runs a function in a new reactive scope. + +- **Type** + + ```lua + function root(fn: () -> T): (T, () -> ()) + ``` + +- **Details** + + Creates a new root reactive scope, where creation and derivations of sources + can be tracked and properly disposed of. + + Returns the result of the given function. + + Also returns a function to destroy the root, which will run any cleanups + and allow derived sources created to garbage collect. + ## source() Creates a new source with the given value. @@ -14,10 +34,10 @@ Creates a new source with the given value. - **Details** - Calling the returned source with no arguments will return its stored value, - calling with arguments will set a new value. + Calling the returned source with no argument will return its stored value, + calling with an argument will set a new value. - Reading from the source from within any reactive scope will cause changes + Reading from the source from within a reactive scope will cause changes to that source to be tracked and anything depending on it to update. - **Example** diff --git a/docs/api/reactivity-utility.md b/docs/api/reactivity-utility.md index 4597364..82b9fd5 100644 --- a/docs/api/reactivity-utility.md +++ b/docs/api/reactivity-utility.md @@ -2,7 +2,7 @@ ## cleanup() -Runs a callback anytime a function scope is re-ran. +Runs a callback anytime a reactive scope is re-ran. - **Type** @@ -10,19 +10,6 @@ Runs a callback anytime a function scope is re-ran. function cleanup(callback: () -> ()) ``` -- **Details** - - The primary purpose of this function is to provide a means of cleaning up - side effects caused by source updates and `watch()` updates. - - The stack is inspected to find the function that calls `cleanup()`. The - callback passed is called anytime the caller is re-ran, and when the caller - finally garbage collects. - - ::: warning - Only one `cleanup()` call is allowed per function scope. - ::: - - **Example** ```lua @@ -53,7 +40,7 @@ Runs a callback anytime a function scope is re-ran. ## untrack() -Gets the value of a source without reactively tracking it. +Runs a given function where any sources read will not track its reactive scope. - **Type** diff --git a/docs/tut/crash-course/1-introduction.md b/docs/tut/crash-course/1-introduction.md index 0197384..7376579 100644 --- a/docs/tut/crash-course/1-introduction.md +++ b/docs/tut/crash-course/1-introduction.md @@ -3,7 +3,7 @@ This is a brief tutorial designed to give you a quick run through the usage of Vide. -Vide is largely inspired by other UI libraries such as Solid and Fusion. +Vide is heavily inspired by [Solid](https://www.solidjs.com/). ## Why Vide? @@ -21,5 +21,6 @@ Some of the main focuses behind Vide's design choices: - Being completely typecheckable. - Flexibility, particularly with integrating other libraries and allowing users to use their own patterns. -- A powerful reactive system that does not interfere with the lifetime of - instances. +- Independence from instance lifetimes. +- A powerful reactive system that can surgically update properties as a result + of state changes. diff --git a/src/apply.luau b/src/apply.luau index 8ef87ed..960ff8b 100644 --- a/src/apply.luau +++ b/src/apply.luau @@ -138,7 +138,7 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown -- finally set parent if any if parent then if type(parent) == "function" then - bind.parent(instance, parent :: () -> ()) + bind.parent(instance, parent :: () -> Instance) else instance.Parent = parent :: Instance end diff --git a/src/bind.luau b/src/bind.luau index fe1605d..914ba45 100644 --- a/src/bind.luau +++ b/src/bind.luau @@ -51,6 +51,7 @@ function create_binding(updater: (T) -> T, binding_data: T) binding.effect = updater local owner = get_scope() + if not owner then throw("cannot bind property in non-reactive scope") end assert(owner) set_owner(binding, owner) diff --git a/src/cleanup.luau b/src/cleanup.luau index 3aec998..2d8fc73 100644 --- a/src/cleanup.luau +++ b/src/cleanup.luau @@ -1,15 +1,16 @@ if not game then script = require "test/relative-string" end -local flags = require(script.Parent.flags) local throw = require(script.Parent.throw) local graph = require(script.Parent.graph) local get_scope = graph.get_scope local add_cleanup = graph.add_cleanup local function cleanup(fn: () -> ()) - local node = get_scope() - if node == nil then throw("cannot call cleanup() in a non-reactive scope") end - add_cleanup(node, fn) + local scope = get_scope() + if not scope then throw("cannot cleanup in a non-reactive scope") end + assert(scope) + + add_cleanup(scope, fn) end return cleanup diff --git a/src/derive.luau b/src/derive.luau index 3b7972f..62a7096 100644 --- a/src/derive.luau +++ b/src/derive.luau @@ -1,5 +1,6 @@ if not game then script = require "test/relative-string" end +local throw = require(script.Parent.throw) local graph = require(script.Parent.graph) local create_node = graph.create_node local set_owner = graph.set_owner @@ -8,8 +9,10 @@ local get_scope = graph.get_scope local open_scope = graph.open_scope local close_scope = graph.close_scope + local function derive(fn: () -> T): () -> T local owner = get_scope() + if not owner then throw("cannot derive in non-reactive scope") end assert(owner) local node = create_node((false :: any) :: T) diff --git a/src/maps.luau b/src/maps.luau index b8eedfb..ffbfa7e 100644 --- a/src/maps.luau +++ b/src/maps.luau @@ -5,7 +5,7 @@ if not game then script = require "test/relative-string" end local throw = require(script.Parent.throw) local flags = require(script.Parent.flags) local graph = require(script.Parent.graph) -type Scope = graph.Scope +type Node = graph.Node type StartNode = graph.StartNode local create_node = graph.create_node local create_start_node = graph.create_start_node @@ -28,9 +28,12 @@ local function check_primitives(t: {}) end end +-- todo: verify destruction of subscopes when owner scope is destroyed + -- todo: optimize output array local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO): () -> { VO } local owner = get_scope() + if not owner then throw("cannot derive in non-reactive scope") end assert(owner) local input_cache = {} :: Map @@ -38,7 +41,7 @@ local function indexes(input: () -> Map, transform: (() -> VI, local input_nodes = {} :: Map> local remove_queue = {} :: { K } - local scopes = {} :: Map + local scopes = {} :: Map> local function update_children(data) -- queue removed values @@ -69,7 +72,7 @@ local function indexes(input: () -> Map, transform: (() -> VI, if cv ~= v then if cv == nil then local scope = create_node(false) - scopes[i] = scope + scopes[i] = scope :: Node set_owner(scope, owner) open_scope(scope) @@ -122,6 +125,7 @@ end -- todo: optimize output array local function values(input: () -> Map, transform: (VI, () -> K) -> VO): () -> { VO } local owner = get_scope() + if not owner then throw("cannot derive in non-reactive scope") end assert(owner) local cur_input_cache_up = {} :: Map @@ -130,7 +134,7 @@ local function values(input: () -> Map, transform: (VI, () -> local output_cache = {} :: Map local input_nodes = {} :: Map> - local scopes = {} :: Map + local scopes = {} :: Map> local function update_children(data: Map) local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up @@ -155,7 +159,7 @@ local function values(input: () -> Map, transform: (VI, () -> if cv == nil then local scope = create_node(false) - scopes[v] = scope + scopes[v] = scope :: Node set_owner(scope, owner) open_scope(scope) diff --git a/src/root.luau b/src/root.luau index d795d4d..b211518 100644 --- a/src/root.luau +++ b/src/root.luau @@ -14,7 +14,6 @@ local destroy = graph.destroy local refs = {} local function root(fn: () -> T): (T, () -> ()) - --assert(not get_scope()) local node = create_node(false) open_scope(node) diff --git a/src/spring.luau b/src/spring.luau index 9b4e35f..ee76e10 100644 --- a/src/spring.luau +++ b/src/spring.luau @@ -151,6 +151,7 @@ setmetatable(springs, { __mode = "v" }) local function spring(source: () -> T, period: number?, damping_ratio: number?): () -> T local owner = get_scope() + if not owner then throw("cannot derive in non-reactive scope") end assert(owner) local updater = create_node(false) @@ -199,6 +200,7 @@ local function spring(source: () -> T, period: number?, damping_ratio: number data.x1_123, data.x1_456 = type_to_vec6[typeof(v)](v) data.source_value = v springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs + return false end return function() diff --git a/src/untrack.luau b/src/untrack.luau index 0dbde8a..abdfb78 100644 --- a/src/untrack.luau +++ b/src/untrack.luau @@ -1,15 +1,13 @@ if not game then script = require "test/relative-string" end -local create = require(script.Parent.create) +local throw = require(script.Parent.throw) local graph = require(script.Parent.graph) type Node = graph.Node -local create_node = graph.create_node local get_scope = graph.get_scope -local open_scope = graph.open_scope -local close_scope = graph.close_scope local function untrack(source: () -> T): T local scope = get_scope() + if not scope then throw("cannot untrack in non-reactive scope") end assert(scope) local effect = scope.effect diff --git a/src/watch.luau b/src/watch.luau index 9213631..c5ee5df 100644 --- a/src/watch.luau +++ b/src/watch.luau @@ -1,5 +1,6 @@ if not game then script = require "test/relative-string" end +local throw = require(script.Parent.throw) local graph = require(script.Parent.graph) local create_node = graph.create_node local get_scope = graph.get_scope @@ -9,9 +10,10 @@ local set_owner = graph.set_owner local function watch(effect: (T) -> T, initial_value: T) local owner = get_scope() + if not owner then throw("cannot watch in non-reactive scope") end assert(owner) - local node = create_node(false) + local node = create_node(initial_value) node.effect = effect set_owner(node, owner) diff --git a/test/tests.luau b/test/tests.luau index 574bdc6..9c7fbe9 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,5 +1,5 @@ local testkit = require("test/testkit") -local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() +local TEST, CASE, CHECK, FINISH = testkit.test() local mock = require "test/mock" local Instance, Signal = mock.Instance, mock.Signal @@ -29,7 +29,7 @@ local function wrap_root(fn: () -> ()) end end -local NIL = NIL +local NIL = nil TEST("graph", function() local create_node = graph.create_node