mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Refactor
This commit is contained in:
parent
67e87110c7
commit
f2de9b0e63
25 changed files with 315 additions and 364 deletions
|
|
@ -43,9 +43,9 @@ export default withMermaid({
|
|||
{ text: "Components", link: "/tut/crash-course/3-components" },
|
||||
{ text: "Sources", link: "/tut/crash-course/4-source" },
|
||||
{ text: "Effects", link: "/tut/crash-course/5-effect" },
|
||||
{ text: "Root Scopes", link: "/tut/crash-course/6-root" },
|
||||
{ text: "Scopes", link: "/tut/crash-course/6-scope" },
|
||||
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
|
||||
{ text: "Property Binding", link: "/tut/crash-course/8-property-binding" },
|
||||
{ text: "Property Binding", link: "/tut/crash-course/8-implicit-effect" },
|
||||
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
|
||||
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
|
||||
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## mount()
|
||||
|
||||
Runs a function in a new reactive scope and optionally applies its result to a
|
||||
Runs a function in a new stable scope and optionally applies its result to a
|
||||
target instance.
|
||||
|
||||
- **Type**
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
<br/>
|
||||
|
||||
:::warning
|
||||
Yielding is not allowed in any reactive scope. Strict mode can check for this.
|
||||
Yielding is not allowed in any stable or reactive scope. Strict mode will check
|
||||
for this.
|
||||
:::
|
||||
|
||||
## root()
|
||||
|
||||
Creates and runs a function in a new reactive scope.
|
||||
Creates and runs a function in a new stable scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -20,8 +21,8 @@ Creates and runs a function in a new reactive scope.
|
|||
|
||||
Returns the result of the given function.
|
||||
|
||||
Creates a new root reactive scope, where creation and derivations of sources
|
||||
can be tracked and properly disposed of.
|
||||
Creates a new stable scope, where creation of effects can be tracked and
|
||||
properly disposed of.
|
||||
|
||||
A function to destroy the root is passed into the callback, which will run
|
||||
any cleanups and allow derived sources created to garbage collect.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# Nested Reactive Scopes
|
||||
# Nested Scopes
|
||||
|
||||
Nesting reactive scopes gives you finer control over the reactive graph, but
|
||||
needs more work to do. The built-in control flow functions try to cover the
|
||||
Nesting scopes gives you finer control over the reactive graph, but needs more work to do. The built-in control flow functions try to cover the
|
||||
most common cases, but they do not cover all of them.
|
||||
|
||||
This tutorial will demonstrate how to implement a `show()` control flow function
|
||||
|
|
@ -21,7 +20,7 @@ local function Counter()
|
|||
}
|
||||
end
|
||||
|
||||
mount(function()
|
||||
root(function()
|
||||
local toggled = source(true)
|
||||
|
||||
show(toggled, Button)
|
||||
|
|
@ -57,7 +56,7 @@ Above is the reactive graph for `show()`. It creates a new effect depending on
|
|||
`toggle` where anytime `toggle` is truthy, it will create a new `Counter`. The
|
||||
`show` effect calls `Counter`, which creates a new reactive scope to update its
|
||||
text whenever `count` changes. As per the rules of reactive scopes, a reactive
|
||||
scope rerunning will destroy any reactive scope created within it. So the text
|
||||
scope rerunning will destroy any scopes created within it. So the text
|
||||
effect's reactive scope is destroyed whenever the show effect is rerun.
|
||||
|
||||
The same can be achieved without the use of `show()`:
|
||||
|
|
@ -82,7 +81,10 @@ mount(function()
|
|||
|
||||
effect(function()
|
||||
if toggled() then
|
||||
local destroy = mount(Button)
|
||||
local destroy = root(function(destroy)
|
||||
Counter()
|
||||
return destroy
|
||||
end)
|
||||
cleanup(destroy)
|
||||
end
|
||||
end)
|
||||
|
|
@ -116,11 +118,14 @@ subgraph mount
|
|||
end
|
||||
```
|
||||
|
||||
This is another way to achieve the same. Here we use `mount()` within the effect
|
||||
to manually create and destroy a new reactive scope whenever the effect reruns.
|
||||
This is another way to achieve the same. Here we use `root()` within the effect
|
||||
to manually create and destroy a new stable scope whenever the effect reruns.
|
||||
|
||||
Alternatively, instead of using `mount()`, a new reactive scope can be created
|
||||
directly within the effect:
|
||||
The reason for creating a stable scope is to prevent the effect from tracking
|
||||
any sources that may be read inside the `Counter()` call. Otherwise, the effect
|
||||
may be rerun needlessly and recreate the counter.
|
||||
|
||||
Alternatively, instead of using `root()`:
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
|
|
@ -174,7 +179,9 @@ end
|
|||
```
|
||||
|
||||
Without the use of `untrack()`, an error would occur, since Vide does not allow
|
||||
the creation of reactive scopes inside reactive scopes that are tracking. The
|
||||
the creation of reactive scopes inside reactive scopes. `untrack()` creates a
|
||||
stable scope inside the reactive scope, and we can create another reactive scope
|
||||
inside that stable scope. The
|
||||
reason for this, is because if the `Counter` component reads from a source
|
||||
internally, that can cause the reactive scope calling `Counter()` to track that
|
||||
source, causing unintentional reruns. As a guard against this, you are forced to
|
||||
|
|
@ -184,5 +191,3 @@ The final result is the same as using the `show()` component. An effect is
|
|||
created which creates the counter, which creates its own reactive scope. The
|
||||
effect rerunning causes the counter's internal reactive scope to be destroyed,
|
||||
making sure everything is cleaned up.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Sometimes you may need to do some cleanup when destroying a component or after
|
||||
a side-effect from a source update. Vide provides a function `cleanup()` which
|
||||
is used to queue a cleanup callback for the next time a reactive scope is rerun
|
||||
or destroyed.
|
||||
or destroyed, or when a stable scope is destroyed.
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
|
|
@ -31,14 +31,18 @@ local function Timer()
|
|||
}
|
||||
end
|
||||
|
||||
local unmount = mount(Timer)
|
||||
local instance, destroy = root(function(destroy)
|
||||
local instance = Timer()
|
||||
return instance, destroy
|
||||
end)
|
||||
|
||||
unmount() -- all queued cleanups are ran, heartbeat connection disconnected
|
||||
wait(5)
|
||||
|
||||
destroy() -- all queued cleanups are ran, heartbeat connection disconnected
|
||||
```
|
||||
|
||||
In the above example, this allows us to disconnect the heartbeat connection
|
||||
when the reactive scope responsible for creating the timer component is
|
||||
destroyed, such as when it is unmounted.
|
||||
when the scope responsible for creating the timer component is destroyed.
|
||||
|
||||
::: tip
|
||||
Roblox instances do not need to be explicitly destroyed for their
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ known as *control flow* functions.
|
|||
|
||||
These functions return new sources, which hold the instances to be displayed.
|
||||
|
||||
Control flow functions run their components in a new reactive scope, which can
|
||||
be destroyed independently of the reactive scope that called the control flow
|
||||
Control flow functions run their components in a new stable scope, which can
|
||||
be destroyed independently of the stable scope that called the control flow
|
||||
function. This means parts of your app can be independently created and
|
||||
destroyed.
|
||||
|
||||
|
|
@ -93,9 +93,9 @@ subgraph root["root scope"]
|
|||
end
|
||||
```
|
||||
|
||||
A `switch()` call creates a new effect and a new scope as seen in the above
|
||||
graph. Whenever `menu` updates, it causes the `switch` effect to run, which
|
||||
will destroy and recreate the switch scope with the new component.
|
||||
A `switch()` call creates a new effect and a new stable scope as seen in the
|
||||
above graph. Whenever `menu` updates, it causes the `switch` effect to run,
|
||||
which will destroy and recreate the switch scope with the new component.
|
||||
|
||||
This will also destroy the internal effect that the button uses to highlight
|
||||
itself when it is hovered, each time the switch is rerun.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ want this.
|
|||
|
||||
Strict mode will run derived sources and effects twice each time they update.
|
||||
This is to help ensure that derived source computations are pure, and that any
|
||||
cleanups made in derived sources or effects are done correctly.
|
||||
cleanups made in derived sources or effects are done properly.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
|
|
|
|||
|
|
@ -22,52 +22,52 @@ Anything that happens in response to a source update.
|
|||
|
||||
Created with `effect()`.
|
||||
|
||||
## Reactive Scope
|
||||
## Stable Scope
|
||||
|
||||
A scope created by certain functions such as:
|
||||
One of the two types of Vide scopes.
|
||||
|
||||
Created by:
|
||||
|
||||
- `root()`
|
||||
- `untrack()`
|
||||
- `switch()`
|
||||
- `indexes()`
|
||||
|
||||
Stable scopes do not track sources and never rerun.
|
||||
|
||||
New stable or reactive scopes can be created within a stable scope.
|
||||
|
||||
## Reactive Scope
|
||||
|
||||
Created by:
|
||||
|
||||
- `effect()`
|
||||
- `derive()`
|
||||
|
||||
Reactive scopes can:
|
||||
Reactive scopes do track sources and will rerun when those sources update.
|
||||
|
||||
- track sources that are read from within.
|
||||
- rerun when a tracked source updates.
|
||||
- track new reactive scopes created from within.
|
||||
New reactive scopes cannot be created within a reactive scope, but stable scopes
|
||||
can.
|
||||
|
||||
## Scope Owners
|
||||
|
||||
A reactive scope created within another reactive scope is *owned* by the other
|
||||
reactive scope, with the exception of the reactive scope created by `root()`.
|
||||
A scope created within another scope is *owned* by the other scope, with the
|
||||
exception of the scope created by `root()`.
|
||||
|
||||
When a reactive scope is rerun or destroyed, all reactive scopes owned by it are
|
||||
automatically destroyed.
|
||||
When a scope is rerun or destroyed, all scopes owned by it are automatically
|
||||
destroyed.
|
||||
|
||||
`root()`, which `mount()` uses internally, creates a reactive scope with no
|
||||
owner, since it must be destroyed manually using a destructor
|
||||
returned.
|
||||
`root()` creates a stable scope with no owner, instead it is destroyed manually.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Arbitrary code to run whenever a reactive scope is rerun or destroyed.
|
||||
Arbitrary code to run whenever a stable or reactive scope is rerun or destroyed.
|
||||
|
||||
Queue a function to run using `cleanup()`.
|
||||
|
||||
## Tracking
|
||||
|
||||
Sources read from within a reactive scope will be tracked. This can be disabled
|
||||
using `untrack()`, which will make reactive scopes temporarily ignore sources
|
||||
read.
|
||||
|
||||
The reactive scope created by `root()` is non-tracking by default.
|
||||
|
||||
As a guard against misusage, a reactive scope cannot be created within a
|
||||
reactive scope, unless it is made non-tracking using `untrack()`.
|
||||
|
||||
## Reactive Graph
|
||||
|
||||
The combination of reactive scopes can viewed graphically, called a
|
||||
The combination of stable and reactive scopes can viewed graphically, called a
|
||||
*reactive graph*. This can be a more intuitive way to think of the
|
||||
relationships between effects and the sources they depend on.
|
||||
|
||||
|
|
@ -114,10 +114,10 @@ count --> text
|
|||
Notes:
|
||||
|
||||
- Since `count` is a source, not an effect, it can exist
|
||||
outside of a root reactive scope.
|
||||
outside of scopes.
|
||||
- An update to `count` will cause `text` to rerun, which
|
||||
then causes `effect` to rerun.
|
||||
- When the root reactive scope is destroyed, `text` and
|
||||
- When the root scope is destroyed, `text` and
|
||||
`effect` will be destroyed alongside it, since they are
|
||||
owned by it. `count` will be untouched and future updates
|
||||
to `count` will have no effect.
|
||||
|
|
|
|||
|
|
@ -39,10 +39,3 @@ return create "ScreenGui" {
|
|||
Assign a value to a string key to set a property, and assign a value to a
|
||||
number key to set a child. Events can be connected to by assigning a function
|
||||
to a string key.
|
||||
|
||||
::: warning
|
||||
When creating an instance with no properties, it is important to not forget to
|
||||
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
|
||||
To be clear, `create "Frame"` returns a *function* which is a constructor for
|
||||
that class, not an instance of that class.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -58,20 +58,8 @@ local function App()
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
App().Parent = game.StarterGui
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Above is a simple example of a button component being used across files.
|
||||
|
||||
A single parameter `props` is used to pass properties to the component.
|
||||
|
||||
You can only modify the component in ways that you allow in the component,
|
||||
through the `props` parameter.
|
||||
|
||||
To create a new button all you must do is call the `Button` function, passing in
|
||||
values. This saves having to create and set every property each time. Also, when
|
||||
updating the button component in future, any changes to the button file will be
|
||||
seen anywhere the button is used in your app.
|
||||
|
|
|
|||
|
|
@ -1,43 +1,65 @@
|
|||
# Root Reactive Scopes
|
||||
# Scopes
|
||||
|
||||
Vide operates on the concept of scopes. Vide scopes come in two flavors:
|
||||
stable and reactive.
|
||||
|
||||
The three main rules for scopes are:
|
||||
|
||||
- Stable scopes never rerun.
|
||||
- Reactive scopes will rerun on source updates.
|
||||
- A reactive scope cannot be created within another reactive scope.
|
||||
|
||||
Reactive scopes cannot be created on their own - they must be created within
|
||||
another reactive scope so that it can be tracked and later destroyed when it is
|
||||
a stable scope so that it can be tracked and later destroyed when it is
|
||||
no longer needed.
|
||||
|
||||
This is the purpose of `mount()`, which creates an initial "root", or
|
||||
"top-level" reactive scope, which all other reactive scopes, such as
|
||||
ones created by `effect()`, can stem from.
|
||||
This is the purpose of `root()`, which creates an initial stable scope, which
|
||||
all other reactive scopes, such as ones created by `effect()`, can stem from.
|
||||
|
||||
When this root reactive scope is destroyed, it will ensure all other reactive
|
||||
scopes created within it are also destroyed, ensuring everything is cleaned up
|
||||
properly.
|
||||
When this root reactive scope is destroyed, it will destroy any effects created
|
||||
within it, ensuring everything is cleaned up properly.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function App()
|
||||
local function setup()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
setup() -- will error since effect() was not called within a stable scope
|
||||
|
||||
App() -- will error since effect() was not called within a reactive scope
|
||||
|
||||
vide.mount(App) -- works!
|
||||
|
||||
local count = vide.root(setup) -- runs
|
||||
count(1) -- prints "1"
|
||||
```
|
||||
|
||||
Mounting returns a function that when called will destroy its reactive scope,
|
||||
along with any other reactive scopes created inside it.
|
||||
The scope created by `root()` can be destroyed by calling the function it passes
|
||||
into the given function.
|
||||
|
||||
```lua
|
||||
local unmount = mount(App)
|
||||
local function setup(destroy)
|
||||
local count = source(0)
|
||||
|
||||
unmount()
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
|
||||
return count, destroy
|
||||
end
|
||||
|
||||
local count, destroy = root(setup)
|
||||
|
||||
count(1) -- prints "1"
|
||||
|
||||
destroy()
|
||||
|
||||
count(2) -- effect is destroyed; no longer prints
|
||||
```
|
||||
|
||||
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
||||
|
|
@ -65,7 +87,7 @@ subgraph root
|
|||
end
|
||||
```
|
||||
|
||||
When the root reactive scope created by `mount()` is destroyed, the `effect`
|
||||
When the root reactive scope created by `root()` is destroyed, the `effect`
|
||||
scope will also be destroyed since it was created within it.
|
||||
|
||||
This is important because you may have an effect that updates the property of a
|
||||
|
|
@ -75,6 +97,6 @@ instance to be garbage collected.
|
|||
|
||||
You don't need to worry about ensuring all your effects are created within a
|
||||
root reactive scope, since you should be creating all your UI and corresponding
|
||||
effects within a top-level `mount()` call that puts all your UI together. So it
|
||||
effects within a top-level `root()` call that puts all your UI together. So it
|
||||
is safe to assume that any effect you create will be created under this top
|
||||
level scope. Vide will prevent you from accidently doing otherwise anyways.
|
||||
|
|
@ -27,8 +27,6 @@ local function Counter()
|
|||
|
||||
return instance
|
||||
end
|
||||
|
||||
mount(Counter, game.StarterGui)
|
||||
```
|
||||
|
||||
Above is an example of a counter component, that when clicked, will increment
|
||||
|
|
@ -37,9 +35,6 @@ its internal count, and automatically update its text to reflect that count.
|
|||
Each instance of `Counter()` will maintain its own independent count, since the
|
||||
count source is created inside the component.
|
||||
|
||||
We use `mount()` to create the counter within a reactive scope, which also takes
|
||||
a second argument to parent the counter to another instance.
|
||||
|
||||
## External State
|
||||
|
||||
External sources can also be passed into components for them to use.
|
||||
|
|
@ -72,4 +67,4 @@ count(1) -- the Counter component will update to display this count
|
|||
|
||||
Sources can be created internally or passed in from externally, there are no
|
||||
restrictions on how they are used as long as the effect using it is created
|
||||
within a reactive scope.
|
||||
within a stable scope.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Property Binding
|
||||
# Implicit Effects
|
||||
|
||||
Explicitly creating effects to update properties can be tedious. Vide provides a
|
||||
way to *implicitly* create an effect to update properties.
|
||||
|
|
@ -31,12 +31,7 @@ source used within is updated.
|
|||
Just like effects, the function is ran immediately in a reactive scope to set
|
||||
the property initially and determine what sources are being used.
|
||||
|
||||
This allows you as the programmer to not need to manually update UI as the state
|
||||
of your program changes. You just define how data sources map to UI, and Vide's
|
||||
reactive system will automatically update any properties depending on those
|
||||
sources.
|
||||
|
||||
## Children Binding
|
||||
## Children
|
||||
|
||||
Children can also be set in a similar manner. A source passed as a child (passed
|
||||
with a number key instead of string key) can return an instance or an array of
|
||||
|
|
@ -36,7 +36,7 @@ source(1) -- prints "ran" x2
|
|||
```
|
||||
|
||||
To avoid this, you can use `derive()` to derive a new source instead. This will
|
||||
run a callback in a new reactive scope only when a dependent source has updated.
|
||||
run a function in a new reactive scope only when a dependent source has updated.
|
||||
Reading this derived source multiple times will just return a cached result from
|
||||
when it last updated.
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ effect(function() text() end)
|
|||
source(1) -- prints "ran" x1
|
||||
```
|
||||
|
||||
`derive()` must also be called within a reactive scope, just like `effect()`.
|
||||
`derive()` must also be called within a stable scope, just like `effect()`.
|
||||
|
||||
If the recalculated value is the same as the old value, the derived source will
|
||||
not rerun the effects using it.
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ local flags = require(script.Parent.flags)
|
|||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
|
||||
function create_binding<T>(updater: (T) -> T, binding: T)
|
||||
if flags.strict then
|
||||
|
|
@ -31,12 +30,7 @@ function create_binding<T>(updater: (T) -> T, binding: T)
|
|||
end
|
||||
end
|
||||
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(binding, updater)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
evaluate_node(create_node(assert_stable_scope(), updater, binding))
|
||||
end
|
||||
|
||||
type PropertyBinding = {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ local typeof = game and typeof or require "test/mock".typeof :: never
|
|||
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 push_cleanup = graph.push_cleanup
|
||||
|
||||
local function helper(obj: any)
|
||||
return
|
||||
|
|
@ -21,13 +21,13 @@ local function cleanup(value: unknown)
|
|||
local scope = get_scope()
|
||||
|
||||
if not scope then
|
||||
throw "cannot cleanup in a non-reactive scope"
|
||||
throw "cannot cleanup outside a stable or reactive scope"
|
||||
end; assert(scope)
|
||||
|
||||
if type(value) == "function" then
|
||||
add_cleanup(scope, value :: () -> ())
|
||||
push_cleanup(scope, value :: () -> ())
|
||||
else
|
||||
add_cleanup(scope, helper(value))
|
||||
push_cleanup(scope, helper(value))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,21 +2,17 @@ if not game then script = require "test/relative-string" end
|
|||
|
||||
local graph = require(script.Parent.graph)
|
||||
local create_node = graph.create_node
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
|
||||
local function derive<T>(source: () -> T): () -> T
|
||||
local owner = assert_owning_scope()
|
||||
local node = create_node(assert_stable_scope(), source, false :: any)
|
||||
|
||||
local node = create_node(false :: any, source)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
track(node)
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,16 +2,12 @@ if not game then script = require "test/relative-string" end
|
|||
|
||||
local graph = require(script.Parent.graph)
|
||||
local create_node = graph.create_node
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
|
||||
local function effect<T>(callback: (T) -> T, initial_value: T)
|
||||
local owner = assert_owning_scope()
|
||||
local node = create_node(assert_stable_scope(), callback, initial_value)
|
||||
|
||||
local node = create_node(initial_value, callback)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
end
|
||||
|
||||
|
|
|
|||
104
src/graph.luau
104
src/graph.luau
|
|
@ -3,7 +3,7 @@ if not game then script = require "test/relative-string" end
|
|||
local throw = require(script.Parent.throw)
|
||||
local flags = require(script.Parent.flags)
|
||||
|
||||
export type StartNode<T> = {
|
||||
export type SourceNode<T> = {
|
||||
cache: T,
|
||||
[number]: Node<T>
|
||||
}
|
||||
|
|
@ -16,12 +16,11 @@ export type Node<T> = {
|
|||
owned: { Node<T> } | false,
|
||||
owner: Node<T> | false,
|
||||
|
||||
parents: { StartNode<T> },
|
||||
parents: { SourceNode<T> },
|
||||
[number]: Node<T> -- children
|
||||
}
|
||||
|
||||
-- reactive scope stack
|
||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number }
|
||||
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } -- scopes stack
|
||||
|
||||
local function ycall<T, U>(fn: (T) -> U, arg: T): (boolean, string|U)
|
||||
local thread = coroutine.create(pcall)
|
||||
|
|
@ -40,46 +39,37 @@ local function get_scope(): Node<unknown>?
|
|||
return scopes[scopes.n]
|
||||
end
|
||||
|
||||
local function assert_owning_scope(): Node<unknown>
|
||||
local function assert_stable_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 a non-reactive scope`)
|
||||
return throw(`cannot use {caller_name}() outside a stable or reactive scope`)
|
||||
elseif scope.effect then
|
||||
throw("cannot create new reactive scope in a tracking reactive scope")
|
||||
throw("cannot create a new reactive scope inside another reactive scope")
|
||||
end
|
||||
|
||||
return scope
|
||||
end
|
||||
|
||||
local function add_child<T>(parent: StartNode<any>, child: Node<any>)
|
||||
local function push_child<T>(parent: SourceNode<any>, child: Node<any>)
|
||||
table.insert(parent, child)
|
||||
table.insert(child.parents, parent)
|
||||
end
|
||||
|
||||
local function set_owner(node: Node<any>, owner: Node<any>)
|
||||
node.owner = owner
|
||||
if owner.owned then
|
||||
table.insert(owner.owned, node)
|
||||
else
|
||||
owner.owned = { node }
|
||||
end
|
||||
end
|
||||
|
||||
local function open_scope<T>(node: Node<T>)
|
||||
local function push_scope<T>(node: Node<T>)
|
||||
local n = scopes.n + 1
|
||||
scopes.n = n
|
||||
scopes[n] = node
|
||||
end
|
||||
|
||||
local function close_scope()
|
||||
local function pop_scope()
|
||||
local n = scopes.n
|
||||
scopes.n = n - 1
|
||||
scopes[n] = nil
|
||||
end
|
||||
|
||||
local function add_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
||||
local function push_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
||||
if node.cleanups then
|
||||
table.insert(node.cleanups, cleanup)
|
||||
else
|
||||
|
|
@ -87,34 +77,35 @@ local function add_cleanup<T>(node: Node<T>, cleanup: () -> ())
|
|||
end
|
||||
end
|
||||
|
||||
local function run_cleanups<T>(node: Node<T>)
|
||||
local function flush_cleanups<T>(node: Node<T>)
|
||||
if node.cleanups then
|
||||
for _, fn in next, node.cleanups do
|
||||
local ok, err: string? = pcall(fn)
|
||||
if not ok then throw(`cleanup error: {err}`) end
|
||||
end
|
||||
|
||||
table.clear(node.cleanups)
|
||||
end
|
||||
end
|
||||
|
||||
local function find_and_swap_pop<T>(t: { T }, v: T)
|
||||
local idx = table.find(t, v) :: number
|
||||
local i = table.find(t, v) :: number
|
||||
local n = #t
|
||||
t[idx] = t[n]
|
||||
t[i] = t[n]
|
||||
t[n] = nil
|
||||
end
|
||||
|
||||
local function unparent<T>(node: Node<T>)
|
||||
local parents = node.parents
|
||||
|
||||
for i, parent in next, parents do
|
||||
for i, parent in parents do
|
||||
find_and_swap_pop(parent, node)
|
||||
parents[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function destroy<T>(node: Node<T>)
|
||||
run_cleanups(node)
|
||||
flush_cleanups(node)
|
||||
unparent(node)
|
||||
|
||||
if node.owner then
|
||||
|
|
@ -141,28 +132,28 @@ local function evaluate_node<T>(node: Node<T>)
|
|||
local cur_value = node.cache
|
||||
|
||||
if flags.strict then
|
||||
run_cleanups(node)
|
||||
flush_cleanups(node)
|
||||
destroy_owned(node)
|
||||
|
||||
open_scope(node)
|
||||
push_scope(node)
|
||||
|
||||
local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
if not ok then throw(new_value :: string) end
|
||||
|
||||
node.cache = new_value :: T
|
||||
end
|
||||
|
||||
run_cleanups(node)
|
||||
flush_cleanups(node)
|
||||
destroy_owned(node)
|
||||
|
||||
open_scope(node)
|
||||
push_scope(node)
|
||||
|
||||
local ok, new_value = pcall(node.effect :: (T) -> T, node.cache)
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
table.clear(update_queue)
|
||||
|
|
@ -175,7 +166,7 @@ local function evaluate_node<T>(node: Node<T>)
|
|||
return cur_value ~= new_value
|
||||
end
|
||||
|
||||
local function queue_children<T>(node: StartNode<T>)
|
||||
local function queue_children_for_update<T>(node: SourceNode<T>)
|
||||
local i = update_queue.n
|
||||
while node[1] do
|
||||
i += 1
|
||||
|
|
@ -198,7 +189,7 @@ local function flush_update_queue()
|
|||
--assert(node.effect)
|
||||
|
||||
if node.owner and evaluate_node(node) then
|
||||
queue_children(node)
|
||||
queue_children_for_update(node)
|
||||
end
|
||||
|
||||
update_queue[i] = false :: any
|
||||
|
|
@ -210,9 +201,9 @@ local function flush_update_queue()
|
|||
_flushing = false
|
||||
end
|
||||
|
||||
local function update<T>(root: StartNode<T>)
|
||||
local function update_descendants<T>(root: SourceNode<T>)
|
||||
local n0 = update_queue.n
|
||||
queue_children(root)
|
||||
queue_children_for_update(root)
|
||||
|
||||
if flags.batch then return end
|
||||
|
||||
|
|
@ -223,7 +214,7 @@ local function update<T>(root: StartNode<T>)
|
|||
|
||||
-- check if node is still owned in case destroyed after queued
|
||||
if node.owner and evaluate_node(node) then
|
||||
queue_children(node)
|
||||
queue_children_for_update(node)
|
||||
end
|
||||
|
||||
update_queue[i] = false :: any -- false instead of nil to avoid sparse
|
||||
|
|
@ -233,27 +224,37 @@ local function update<T>(root: StartNode<T>)
|
|||
update_queue.n = n0
|
||||
end
|
||||
|
||||
local function track<T>(node: StartNode<T>)
|
||||
local function push_child_to_scope<T>(node: SourceNode<T>)
|
||||
local scope = get_scope()
|
||||
if scope and scope.effect then -- do not track nodes with no effect
|
||||
add_child(node, scope)
|
||||
push_child(node, scope)
|
||||
end
|
||||
end
|
||||
|
||||
local function create_node<T>(value: T, effect: false | (T) -> T): Node<T>
|
||||
return {
|
||||
local function create_node<T>(owner: false | Node<any>, effect: false | (T) -> T, value: T): Node<T>
|
||||
local node: Node<T> = {
|
||||
cache = value,
|
||||
effect = effect,
|
||||
cleanups = false,
|
||||
|
||||
owner = false,
|
||||
owner = owner,
|
||||
owned = false,
|
||||
|
||||
parents = {},
|
||||
}
|
||||
|
||||
if owner then
|
||||
if owner.owned then
|
||||
table.insert(owner.owned, node)
|
||||
else
|
||||
owner.owned = { node }
|
||||
end
|
||||
end
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
local function create_start_node<T>(value: T): StartNode<T>
|
||||
local function create_source_node<T>(value: T): SourceNode<T>
|
||||
return { cache = value }
|
||||
end
|
||||
|
||||
|
|
@ -262,20 +263,19 @@ local function get_children<T>(node: Node<T>): { Node<unknown> }
|
|||
end
|
||||
|
||||
return table.freeze {
|
||||
open_scope = open_scope,
|
||||
close_scope = close_scope,
|
||||
push_scope = push_scope,
|
||||
pop_scope = pop_scope,
|
||||
evaluate_node = evaluate_node,
|
||||
get_scope = get_scope,
|
||||
assert_owning_scope = assert_owning_scope,
|
||||
add_cleanup = add_cleanup,
|
||||
set_owner = set_owner,
|
||||
assert_stable_scope = assert_stable_scope,
|
||||
push_cleanup = push_cleanup,
|
||||
destroy = destroy,
|
||||
run_cleanups = run_cleanups,
|
||||
track = track,
|
||||
update = update,
|
||||
add_child = add_child,
|
||||
flush_cleanups = flush_cleanups,
|
||||
push_child_to_scope = push_child_to_scope,
|
||||
update_descendants = update_descendants,
|
||||
push_child = push_child,
|
||||
create_node = create_node,
|
||||
create_start_node = create_start_node,
|
||||
create_source_node = create_source_node,
|
||||
get_children = get_children,
|
||||
flush_update_queue = flush_update_queue,
|
||||
scopes = scopes
|
||||
|
|
|
|||
|
|
@ -4,15 +4,14 @@ local throw = require(script.Parent.throw)
|
|||
local flags = require(script.Parent.flags)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
type StartNode<T> = graph.StartNode<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
local create_start_node = graph.create_start_node
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local update = graph.update
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
local create_source_node = graph.create_source_node
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local update_descendants = graph.update_descendants
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local push_scope = graph.push_scope
|
||||
local pop_scope = graph.pop_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local destroy = graph.destroy
|
||||
|
||||
|
|
@ -28,14 +27,12 @@ local function check_primitives(t: {})
|
|||
end
|
||||
|
||||
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local subowner = create_node(false, false)
|
||||
set_owner(subowner, owner)
|
||||
local owner = assert_stable_scope()
|
||||
local subowner = create_node(owner, false, false)
|
||||
|
||||
local input_cache = {} :: Map<K, VI>
|
||||
local output_cache = {} :: Map<K, VO>
|
||||
local input_nodes = {} :: Map<K, StartNode<VI>>
|
||||
local input_nodes = {} :: Map<K, SourceNode<VI>>
|
||||
local remove_queue = {} :: { K }
|
||||
local scopes = {} :: Map<K, Node<unknown>>
|
||||
|
||||
|
|
@ -59,7 +56,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
|
||||
table.clear(remove_queue)
|
||||
|
||||
open_scope(subowner)
|
||||
push_scope(subowner)
|
||||
|
||||
-- process new or changed values
|
||||
for i, v in next, data do
|
||||
|
|
@ -67,23 +64,22 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
|
||||
if cv ~= v then
|
||||
if cv == nil then -- create new scope and run transform
|
||||
local scope = create_node(false, false)
|
||||
local scope = create_node(subowner, false, false)
|
||||
scopes[i] = scope :: Node<any>
|
||||
|
||||
local node = create_start_node(v)
|
||||
local node = create_source_node(v)
|
||||
|
||||
set_owner(scope, subowner)
|
||||
open_scope(scope)
|
||||
push_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, function()
|
||||
track(node)
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end, i)
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
close_scope() -- subowner scope
|
||||
pop_scope() -- subowner scope
|
||||
error(result, 0)
|
||||
end
|
||||
|
||||
|
|
@ -91,14 +87,14 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
output_cache[i] = result
|
||||
else -- update source
|
||||
input_nodes[i].cache = v
|
||||
update(input_nodes[i])
|
||||
update_descendants(input_nodes[i])
|
||||
end
|
||||
|
||||
input_cache[i] = v
|
||||
end
|
||||
end
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
local output_array = table.create(#scopes)
|
||||
for _, v in next, output_cache do
|
||||
|
|
@ -109,29 +105,26 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
|
|||
return output_array
|
||||
end
|
||||
|
||||
local node = create_node(false :: any, function()
|
||||
local node = create_node(owner, function()
|
||||
return update_children(input())
|
||||
end)
|
||||
set_owner(node, owner)
|
||||
end, false :: any)
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
track(node)
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
||||
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local subowner = create_node(false, false)
|
||||
set_owner(subowner, owner)
|
||||
local owner = assert_stable_scope()
|
||||
local subowner = create_node(owner, false, false)
|
||||
|
||||
local cur_input_cache_up = {} :: Map<VI, K>
|
||||
local new_input_cache_up = {} :: Map<VI, K>
|
||||
local output_cache = {} :: Map<VI, VO>
|
||||
local input_nodes = {} :: Map<VI, StartNode<K>>
|
||||
local input_nodes = {} :: Map<VI, SourceNode<K>>
|
||||
local scopes = {} :: Map<VI, Node<unknown>>
|
||||
|
||||
local function update_children(data: Map<K, VI>)
|
||||
|
|
@ -147,7 +140,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
end
|
||||
end
|
||||
|
||||
open_scope(subowner)
|
||||
push_scope(subowner)
|
||||
|
||||
-- process data
|
||||
for i, v in next, data do
|
||||
|
|
@ -156,23 +149,22 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
local cv = cur_input_cache[v]
|
||||
|
||||
if cv == nil then -- create new scope and run transform
|
||||
local scope = create_node(false, false)
|
||||
local scope = create_node(subowner, false, false)
|
||||
scopes[v] = scope :: Node<any>
|
||||
|
||||
local node = create_start_node(i)
|
||||
local node = create_source_node(i)
|
||||
|
||||
set_owner(scope, subowner)
|
||||
open_scope(scope)
|
||||
push_scope(scope)
|
||||
|
||||
local ok, result = pcall(transform, v, function()
|
||||
track(node)
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end)
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
close_scope() -- subowner scope
|
||||
pop_scope() -- subowner scope
|
||||
error(result, 0)
|
||||
end
|
||||
|
||||
|
|
@ -181,14 +173,14 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
else -- update source
|
||||
if cv ~= i then
|
||||
input_nodes[v].cache = i
|
||||
update(input_nodes[v])
|
||||
update_descendants(input_nodes[v])
|
||||
end
|
||||
|
||||
cur_input_cache[v] = nil
|
||||
end
|
||||
end
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
-- remove old values
|
||||
for v in next, cur_input_cache do
|
||||
|
|
@ -212,15 +204,14 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
|
|||
return output_array
|
||||
end
|
||||
|
||||
local node = create_node(false :: any, function()
|
||||
local node = create_node(owner, function()
|
||||
return update_children(input())
|
||||
end)
|
||||
set_owner(node, owner)
|
||||
end, false :: any)
|
||||
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
track(node)
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ local throw = require(script.Parent.throw)
|
|||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_node = graph.create_node
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
local push_scope = graph.push_scope
|
||||
local pop_scope = graph.pop_scope
|
||||
local destroy = graph.destroy
|
||||
|
||||
local refs = {}
|
||||
|
||||
local function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
||||
local node = create_node(false, false)
|
||||
local node = create_node(false, false, false)
|
||||
|
||||
refs[node] = true -- prevent gc of root node
|
||||
|
||||
|
|
@ -21,11 +21,11 @@ local function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
|||
destroy(node)
|
||||
end
|
||||
|
||||
open_scope(node)
|
||||
push_scope(node)
|
||||
|
||||
local result = { pcall(fn, destroy) }
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
if not result[1] then
|
||||
refs[node] = nil
|
||||
|
|
|
|||
|
|
@ -2,18 +2,18 @@ if not game then script = require "test/relative-string" end
|
|||
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
local create_start_node = graph.create_start_node
|
||||
local track = graph.track
|
||||
local update = graph.update
|
||||
local create_source_node = graph.create_source_node
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local update_descendants = graph.update_descendants
|
||||
|
||||
export type Source<T> = (() -> T) & ((value: T) -> T)
|
||||
|
||||
local function source<T>(initial_value: T): Source<T>
|
||||
local node = create_start_node(initial_value)
|
||||
local node = create_source_node(initial_value)
|
||||
|
||||
return function(...): T
|
||||
if select("#", ...) == 0 then -- no args were given
|
||||
track(node)
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ local function source<T>(initial_value: T): Source<T>
|
|||
end
|
||||
|
||||
node.cache = v
|
||||
update(node)
|
||||
update_descendants(node)
|
||||
return v
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,14 +24,13 @@ Unsupported datatypes:
|
|||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
type StartNode<T> = graph.StartNode<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
local create_start_node = graph.create_start_node
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local create_source_node = graph.create_source_node
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local update = graph.update
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local update_descendants = graph.update_descendants
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
|
||||
local UPDATE_RATE = 120
|
||||
local TOLERANCE = 0.0001
|
||||
|
|
@ -146,11 +145,11 @@ setmetatable(vec6_to_type, invalid_type)
|
|||
|
||||
-- maps spring data to its corresponding output node
|
||||
-- lifetime of spring data is tied to output node
|
||||
local springs: { [SpringData<any>]: StartNode<any> } = {}
|
||||
local springs: { [SpringData<any>]: SourceNode<any> } = {}
|
||||
setmetatable(springs, { __mode = "v" })
|
||||
|
||||
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
|
||||
local owner = assert_owning_scope()
|
||||
local owner = assert_stable_scope()
|
||||
|
||||
-- https://en.wikipedia.org/wiki/Damping
|
||||
|
||||
|
|
@ -182,7 +181,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
source_value = false :: any,
|
||||
}
|
||||
|
||||
local output = create_start_node(false :: any)
|
||||
local output = create_source_node(false :: any)
|
||||
|
||||
local function updater_effect()
|
||||
local value = source()
|
||||
|
|
@ -192,9 +191,8 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
return value
|
||||
end
|
||||
|
||||
local updater = create_node(false :: any, updater_effect)
|
||||
local updater = create_node(owner, updater_effect, false :: any)
|
||||
|
||||
set_owner(updater, owner)
|
||||
evaluate_node(updater)
|
||||
|
||||
-- set initial position to goal
|
||||
|
|
@ -204,7 +202,7 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
|
|||
output.cache = data.source_value
|
||||
|
||||
return function()
|
||||
track(output)
|
||||
push_child_to_scope(output)
|
||||
return output.cache
|
||||
end
|
||||
end
|
||||
|
|
@ -269,7 +267,7 @@ local function update_spring_sources()
|
|||
output.cache = vec6_to_type[typeof(data.source_value)](x0_123, x0_456)
|
||||
end
|
||||
|
||||
update(output)
|
||||
update_descendants(output)
|
||||
end
|
||||
|
||||
for _, data in next, remove_queue do
|
||||
|
|
|
|||
|
|
@ -3,20 +3,19 @@ if not game then script = require "test/relative-string" end
|
|||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
type Node<T> = graph.Node<T>
|
||||
type StartNode<T> = graph.StartNode<T>
|
||||
type SourceNode<T> = graph.SourceNode<T>
|
||||
local create_node = graph.create_node
|
||||
local evaluate_node = graph.evaluate_node
|
||||
local set_owner = graph.set_owner
|
||||
local track = graph.track
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local destroy = graph.destroy
|
||||
local assert_owning_scope = graph.assert_owning_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
local assert_stable_scope = graph.assert_stable_scope
|
||||
local push_scope = graph.push_scope
|
||||
local pop_scope = graph.pop_scope
|
||||
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> () -> U?
|
||||
local owner = assert_owning_scope()
|
||||
local owner = assert_stable_scope()
|
||||
|
||||
return function(map)
|
||||
local last_scope: Node<false>?
|
||||
|
|
@ -38,28 +37,26 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
|
|||
throw "map must map a value to a function"
|
||||
end
|
||||
|
||||
local new_scope = create_node(false, false)
|
||||
local new_scope = create_node(owner, false, false)
|
||||
last_scope = new_scope :: Node<any>
|
||||
|
||||
set_owner(new_scope, owner)
|
||||
open_scope(new_scope)
|
||||
push_scope(new_scope)
|
||||
|
||||
local ok, result = pcall(component)
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
if not ok then error(result, 0) end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local node = create_node(nil :: U?, update)
|
||||
local node = create_node(owner, update, nil)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
|
||||
return function()
|
||||
track(node)
|
||||
push_child_to_scope(node)
|
||||
return node.cache
|
||||
end
|
||||
end
|
||||
|
|
|
|||
156
test/tests.luau
156
test/tests.luau
|
|
@ -35,28 +35,27 @@ vide.strict = false
|
|||
|
||||
TEST("graph", function()
|
||||
local create_node = graph.create_node
|
||||
local track = graph.track
|
||||
local update = graph.update
|
||||
local add_child = graph.add_child
|
||||
local push_child_to_scope = graph.push_child_to_scope
|
||||
local update_descendants = graph.update_descendants
|
||||
local push_child = graph.push_child
|
||||
local get_scope = graph.get_scope
|
||||
local open_scope = graph.open_scope
|
||||
local close_scope = graph.close_scope
|
||||
local set_owner = graph.set_owner
|
||||
local push_scope = graph.push_scope
|
||||
local pop_scope = graph.pop_scope
|
||||
local get_children = graph.get_children
|
||||
local add_cleanup = graph.add_cleanup
|
||||
local push_cleanup = graph.push_cleanup
|
||||
local destroy = graph.destroy
|
||||
|
||||
local function node<T>(v: T?)
|
||||
return create_node(v or false, function(x) return not x end)
|
||||
local function node<T>(owner: Node<any>?, v: T?)
|
||||
return create_node(owner or false, function(x) return not x end, v or false :: any)
|
||||
end
|
||||
|
||||
local function scope()
|
||||
return create_node(false, false)
|
||||
local function scope(owner: Node<any>?)
|
||||
return create_node(owner or false, false, false)
|
||||
end
|
||||
|
||||
local function cleanup(fn: () -> ())
|
||||
local node = assert(get_scope())
|
||||
add_cleanup(node, fn)
|
||||
push_cleanup(node, fn)
|
||||
end
|
||||
|
||||
do CASE "link nodes"
|
||||
|
|
@ -64,12 +63,12 @@ TEST("graph", function()
|
|||
local b = node()
|
||||
local c = node()
|
||||
|
||||
open_scope(c)
|
||||
push_scope(c)
|
||||
|
||||
track(a)
|
||||
track(b)
|
||||
push_child_to_scope(a)
|
||||
push_child_to_scope(b)
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
CHECK(get_children(a)[1] == c)
|
||||
CHECK(get_children(b)[1] == c)
|
||||
|
|
@ -78,33 +77,30 @@ TEST("graph", function()
|
|||
do CASE "rerun linked nodes"
|
||||
local root = node()
|
||||
local a = node()
|
||||
local b = node()
|
||||
local c = node()
|
||||
|
||||
set_owner(b, root)
|
||||
set_owner(c, root)
|
||||
local b = node(root)
|
||||
local c = node(root)
|
||||
|
||||
local count = 0
|
||||
|
||||
local function effect(x)
|
||||
track(a)
|
||||
track(b)
|
||||
push_child_to_scope(a)
|
||||
push_child_to_scope(b)
|
||||
count += 1
|
||||
return not x
|
||||
end
|
||||
|
||||
c.effect = effect
|
||||
|
||||
open_scope(c)
|
||||
push_scope(c)
|
||||
|
||||
effect(c.cache)
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
CHECK(count == 1)
|
||||
update(a)
|
||||
update_descendants(a)
|
||||
CHECK(count == 2)
|
||||
update(b)
|
||||
update_descendants(b)
|
||||
CHECK(count == 3)
|
||||
end
|
||||
|
||||
|
|
@ -112,22 +108,18 @@ TEST("graph", function()
|
|||
-- a -> b -> d
|
||||
-- -> c
|
||||
local root = node()
|
||||
local a, b, c, d = node(), node(), node(), node()
|
||||
|
||||
set_owner(b, root)
|
||||
set_owner(c, root)
|
||||
set_owner(d, root)
|
||||
local a, b, c, d = node(), node(root), node(root), node(root)
|
||||
|
||||
local b_cnt, c_cnt, d_cnt = 0, 0, 0
|
||||
function b.effect(x) b_cnt += 1; return not x end
|
||||
function c.effect(x) c_cnt += 1; return not x end
|
||||
function d.effect(x) d_cnt += 1; return not x end
|
||||
|
||||
open_scope(b); track(a); close_scope()
|
||||
open_scope(c); track(a); close_scope()
|
||||
open_scope(d); track(b); track(c); close_scope()
|
||||
push_scope(b); push_child_to_scope(a); pop_scope()
|
||||
push_scope(c); push_child_to_scope(a); pop_scope()
|
||||
push_scope(d); push_child_to_scope(b); push_child_to_scope(c); pop_scope()
|
||||
|
||||
update(a)
|
||||
update_descendants(a)
|
||||
|
||||
CHECK(b_cnt == 1)
|
||||
CHECK(c_cnt == 1)
|
||||
|
|
@ -136,21 +128,17 @@ TEST("graph", function()
|
|||
|
||||
do CASE "duplicate child on rerun"
|
||||
local root = node()
|
||||
local a, b, c = node(), node(), node()
|
||||
|
||||
set_owner(a, root)
|
||||
set_owner(b, root)
|
||||
set_owner(c, root)
|
||||
local a, b, c = node(root), node(root), node(root)
|
||||
|
||||
function c.effect(x)
|
||||
track(a)
|
||||
track(b)
|
||||
push_child_to_scope(a)
|
||||
push_child_to_scope(b)
|
||||
return not x
|
||||
end
|
||||
|
||||
open_scope(c); assert(type(c.effect) == "function" and c.effect)(NIL); close_scope()
|
||||
push_scope(c); assert(type(c.effect) == "function" and c.effect)(NIL); pop_scope()
|
||||
|
||||
update(a)
|
||||
update_descendants(a)
|
||||
|
||||
CHECK(#get_children(a) == 1)
|
||||
CHECK(#get_children(b) == 1)
|
||||
|
|
@ -159,13 +147,13 @@ TEST("graph", function()
|
|||
do CASE "case 1"
|
||||
-- construct graph
|
||||
|
||||
local items = node { "a", "b" }
|
||||
local selected = node "a"
|
||||
local items = node(nil, { "a", "b" })
|
||||
local selected = node(nil, "a")
|
||||
|
||||
local root = scope()
|
||||
|
||||
local scope1 = scope()
|
||||
local scope2 = scope()
|
||||
local scope1 = scope(root)
|
||||
local scope2 = scope(root)
|
||||
|
||||
local items_updated
|
||||
|
||||
|
|
@ -180,41 +168,36 @@ TEST("graph", function()
|
|||
end)
|
||||
end
|
||||
|
||||
do open_scope(root)
|
||||
do push_scope(root)
|
||||
clean "root"
|
||||
items_updated = node()
|
||||
track(items_updated) -- should not
|
||||
items_updated = node(root)
|
||||
push_child_to_scope(items_updated) -- should not
|
||||
|
||||
set_owner(items_updated, root)
|
||||
do open_scope(items_updated)
|
||||
track(items)
|
||||
do push_scope(items_updated)
|
||||
push_child_to_scope(items)
|
||||
|
||||
do open_scope(root)
|
||||
set_owner(scope1, root)
|
||||
do open_scope(scope1)
|
||||
do push_scope(root)
|
||||
do push_scope(scope1)
|
||||
clean "scope1"
|
||||
bind1 = node()
|
||||
bind1 = node(scope1)
|
||||
|
||||
set_owner(bind1, scope1)
|
||||
do open_scope(bind1)
|
||||
do push_scope(bind1)
|
||||
clean "bind1"
|
||||
track(selected)
|
||||
close_scope() end
|
||||
close_scope() end
|
||||
push_child_to_scope(selected)
|
||||
pop_scope() end
|
||||
pop_scope() end
|
||||
|
||||
set_owner(scope2, root)
|
||||
do open_scope(scope2)
|
||||
do push_scope(scope2)
|
||||
clean "scope2"
|
||||
bind2 = node()
|
||||
set_owner(bind2, scope2)
|
||||
do open_scope(bind2)
|
||||
bind2 = node(scope2)
|
||||
do push_scope(bind2)
|
||||
clean "bind2"
|
||||
track(selected)
|
||||
close_scope() end
|
||||
close_scope() end
|
||||
close_scope() end
|
||||
close_scope() end
|
||||
close_scope() end
|
||||
push_child_to_scope(selected)
|
||||
pop_scope() end
|
||||
pop_scope() end
|
||||
pop_scope() end
|
||||
pop_scope() end
|
||||
pop_scope() end
|
||||
|
||||
|
||||
-- verify graph
|
||||
|
|
@ -267,7 +250,7 @@ TEST("graph", function()
|
|||
end
|
||||
|
||||
do CASE "nodes garbage collection"
|
||||
local wref = weak { node(1) }
|
||||
local wref = weak { node(nil, 1) }
|
||||
destroy(wref[1])
|
||||
gc()
|
||||
CHECK(not wref[1])
|
||||
|
|
@ -294,30 +277,23 @@ TEST("graph", function()
|
|||
^
|
||||
|
||||
depth=1
|
||||
_, _ <- attempt to update nothing
|
||||
_, _ <- attempt to update_descendants nothing
|
||||
^
|
||||
|
||||
]]
|
||||
|
||||
local a, b, c, d, e, f = node(), node(), node(), node(), node(), node()
|
||||
|
||||
local root = node()
|
||||
set_owner(a, root)
|
||||
set_owner(b, root)
|
||||
set_owner(c, root)
|
||||
set_owner(d, root)
|
||||
set_owner(e, root)
|
||||
set_owner(f, root)
|
||||
local a, b, c, d, e, f = node(root), node(root), node(root), node(root), node(root), node(root)
|
||||
|
||||
function b.effect(x)
|
||||
update(d)
|
||||
update_descendants(d)
|
||||
return not x
|
||||
end
|
||||
|
||||
add_child(a, b); add_child(a, c)
|
||||
add_child(d, e); add_child(d, f)
|
||||
push_child(a, b); push_child(a, c)
|
||||
push_child(d, e); push_child(d, f)
|
||||
|
||||
update(a)
|
||||
update_descendants(a)
|
||||
|
||||
CHECK(true)
|
||||
end
|
||||
|
|
@ -1924,7 +1900,7 @@ TEST("read()", wrap_root(function()
|
|||
CHECK(read(src) == 1)
|
||||
end
|
||||
|
||||
do CASE "track source"
|
||||
do CASE "push_child_to_scope source"
|
||||
local src = source(0)
|
||||
|
||||
local count = 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue