This commit is contained in:
Aaron Smith 2023-09-11 18:20:21 +01:00
parent 52ea2e4690
commit 5b61a9df10
13 changed files with 57 additions and 39 deletions

View file

@ -2,6 +2,26 @@
<br/> <br/>
## root()
Creates and runs a function in a new reactive scope.
- **Type**
```lua
function root<T>(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() ## source()
Creates a new source with the given value. Creates a new source with the given value.
@ -14,10 +34,10 @@ Creates a new source with the given value.
- **Details** - **Details**
Calling the returned source with no arguments will return its stored value, Calling the returned source with no argument will return its stored value,
calling with arguments will set a new 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. to that source to be tracked and anything depending on it to update.
- **Example** - **Example**

View file

@ -2,7 +2,7 @@
## cleanup() ## cleanup()
Runs a callback anytime a function scope is re-ran. Runs a callback anytime a reactive scope is re-ran.
- **Type** - **Type**
@ -10,19 +10,6 @@ Runs a callback anytime a function scope is re-ran.
function cleanup(callback: () -> ()) 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** - **Example**
```lua ```lua
@ -53,7 +40,7 @@ Runs a callback anytime a function scope is re-ran.
## untrack() ## 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** - **Type**

View file

@ -3,7 +3,7 @@
This is a brief tutorial designed to give you a quick run through the usage of This is a brief tutorial designed to give you a quick run through the usage of
Vide. 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? ## Why Vide?
@ -21,5 +21,6 @@ Some of the main focuses behind Vide's design choices:
- Being completely typecheckable. - Being completely typecheckable.
- Flexibility, particularly with integrating other libraries and allowing users - Flexibility, particularly with integrating other libraries and allowing users
to use their own patterns. to use their own patterns.
- A powerful reactive system that does not interfere with the lifetime of - Independence from instance lifetimes.
instances. - A powerful reactive system that can surgically update properties as a result
of state changes.

View file

@ -138,7 +138,7 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
-- finally set parent if any -- finally set parent if any
if parent then if parent then
if type(parent) == "function" then if type(parent) == "function" then
bind.parent(instance, parent :: () -> ()) bind.parent(instance, parent :: () -> Instance)
else else
instance.Parent = parent :: Instance instance.Parent = parent :: Instance
end end

View file

@ -51,6 +51,7 @@ function create_binding<T>(updater: (T) -> T, binding_data: T)
binding.effect = updater binding.effect = updater
local owner = get_scope() local owner = get_scope()
if not owner then throw("cannot bind property in non-reactive scope") end
assert(owner) assert(owner)
set_owner(binding, owner) set_owner(binding, owner)

View file

@ -1,15 +1,16 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
local flags = require(script.Parent.flags)
local throw = require(script.Parent.throw) local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
local get_scope = graph.get_scope local get_scope = graph.get_scope
local add_cleanup = graph.add_cleanup local add_cleanup = graph.add_cleanup
local function cleanup(fn: () -> ()) local function cleanup(fn: () -> ())
local node = get_scope() local scope = get_scope()
if node == nil then throw("cannot call cleanup() in a non-reactive scope") end if not scope then throw("cannot cleanup in a non-reactive scope") end
add_cleanup(node, fn) assert(scope)
add_cleanup(scope, fn)
end end
return cleanup return cleanup

View file

@ -1,5 +1,6 @@
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
@ -8,8 +9,10 @@ local get_scope = graph.get_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 function derive<T>(fn: () -> T): () -> T local function derive<T>(fn: () -> T): () -> T
local owner = get_scope() local owner = get_scope()
if not owner then throw("cannot derive in non-reactive scope") end
assert(owner) assert(owner)
local node = create_node((false :: any) :: T) local node = create_node((false :: any) :: T)

View file

@ -5,7 +5,7 @@ if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw) local throw = require(script.Parent.throw)
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 Scope = graph.Scope 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
@ -28,9 +28,12 @@ local function check_primitives(t: {})
end end
end end
-- todo: verify destruction of subscopes when owner scope is destroyed
-- todo: optimize output array -- todo: optimize output array
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_scope()
if not owner then throw("cannot derive in non-reactive scope") end
assert(owner) assert(owner)
local input_cache = {} :: Map<K, VI> local input_cache = {} :: Map<K, VI>
@ -38,7 +41,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
local input_nodes = {} :: Map<K, StartNode<VI>> local input_nodes = {} :: Map<K, StartNode<VI>>
local remove_queue = {} :: { K } local remove_queue = {} :: { K }
local scopes = {} :: Map<K, Scope> local scopes = {} :: Map<K, Node<unknown>>
local function update_children(data) local function update_children(data)
-- queue removed values -- queue removed values
@ -69,7 +72,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
if cv ~= v then if cv ~= v then
if cv == nil then if cv == nil then
local scope = create_node(false) local scope = create_node(false)
scopes[i] = scope scopes[i] = scope :: Node<any>
set_owner(scope, owner) set_owner(scope, owner)
open_scope(scope) open_scope(scope)
@ -122,6 +125,7 @@ end
-- todo: optimize output array -- todo: optimize output array
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_scope()
if not owner then throw("cannot derive in non-reactive scope") end
assert(owner) assert(owner)
local cur_input_cache_up = {} :: Map<VI, K> local cur_input_cache_up = {} :: Map<VI, K>
@ -130,7 +134,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
local output_cache = {} :: Map<VI, VO> local output_cache = {} :: Map<VI, VO>
local input_nodes = {} :: Map<VI, StartNode<K>> local input_nodes = {} :: Map<VI, StartNode<K>>
local scopes = {} :: Map<VI, Scope> local scopes = {} :: Map<VI, Node<unknown>>
local function update_children(data: Map<K, VI>) local function update_children(data: Map<K, VI>)
local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up local cur_input_cache, new_input_cache = cur_input_cache_up, new_input_cache_up
@ -155,7 +159,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
if cv == nil then if cv == nil then
local scope = create_node(false) local scope = create_node(false)
scopes[v] = scope scopes[v] = scope :: Node<any>
set_owner(scope, owner) set_owner(scope, owner)
open_scope(scope) open_scope(scope)

View file

@ -14,7 +14,6 @@ local destroy = graph.destroy
local refs = {} local refs = {}
local function root<T>(fn: () -> T): (T, () -> ()) local function root<T>(fn: () -> T): (T, () -> ())
--assert(not get_scope())
local node = create_node(false) local node = create_node(false)
open_scope(node) open_scope(node)

View file

@ -151,6 +151,7 @@ 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_scope()
if not owner then throw("cannot derive in non-reactive scope") end
assert(owner) assert(owner)
local updater = create_node(false) local updater = create_node(false)
@ -199,6 +200,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
data.x1_123, data.x1_456 = type_to_vec6[typeof(v)](v) data.x1_123, data.x1_456 = type_to_vec6[typeof(v)](v)
data.source_value = v data.source_value = v
springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs
return false
end end
return function() return function()

View file

@ -1,15 +1,13 @@
if not game then script = require "test/relative-string" end 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) 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 get_scope = graph.get_scope local get_scope = graph.get_scope
local open_scope = graph.open_scope
local close_scope = graph.close_scope
local function untrack<T>(source: () -> T): T local function untrack<T>(source: () -> T): T
local scope = get_scope() local scope = get_scope()
if not scope then throw("cannot untrack in non-reactive scope") end
assert(scope) assert(scope)
local effect = scope.effect local effect = scope.effect

View file

@ -1,5 +1,6 @@
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_scope = graph.get_scope
@ -9,9 +10,10 @@ local set_owner = graph.set_owner
local function watch<T>(effect: (T) -> T, initial_value: T) local function watch<T>(effect: (T) -> T, initial_value: T)
local owner = get_scope() local owner = get_scope()
if not owner then throw("cannot watch in non-reactive scope") end
assert(owner) assert(owner)
local node = create_node(false) local node = create_node(initial_value)
node.effect = effect node.effect = effect
set_owner(node, owner) set_owner(node, owner)

View file

@ -1,5 +1,5 @@
local testkit = require("test/testkit") 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 mock = require "test/mock"
local Instance, Signal = mock.Instance, mock.Signal local Instance, Signal = mock.Instance, mock.Signal
@ -29,7 +29,7 @@ local function wrap_root(fn: () -> ())
end end
end end
local NIL = NIL local NIL = nil
TEST("graph", function() TEST("graph", function()
local create_node = graph.create_node local create_node = graph.create_node