mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
1806339bac
41 changed files with 543 additions and 895 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
|||
*.luau linguist-language=Lua
|
||||
2
.github/workflows/unit-test.yml
vendored
2
.github/workflows/unit-test.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
uses: robinraju/release-downloader@v1.6
|
||||
with:
|
||||
repository: Roblox/luau
|
||||
latest: true
|
||||
tag: "0.620"
|
||||
fileName: luau-ubuntu.zip
|
||||
out-file-path: bin
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Error stack traces being lost.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## [0.2.0] - 2023-11-22
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -43,16 +43,15 @@ 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: "Implicit Effects", 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" },
|
||||
{ text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" },
|
||||
{ text: "Actions", link: "/tut/crash-course/13-actions" },
|
||||
{ text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" },
|
||||
{ text: "Concepts Summary", link: "/tut/crash-course/15-concepts" }
|
||||
{ text: "Actions", link: "/tut/crash-course/12-actions" },
|
||||
{ text: "Strict Mode", link: "/tut/crash-course/13-strict-mode" },
|
||||
{ text: "Concepts Summary", link: "/tut/crash-course/14-concepts" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ Returns a new source with a value always moving torwards the input source value.
|
|||
|
||||
- **Details**
|
||||
|
||||
The output source value is updated every step based on the input source
|
||||
value.
|
||||
An effect is created to update the new source every frame based on the input
|
||||
source value.
|
||||
|
||||
The movement is physically simulated according to a
|
||||
[spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
|
||||
|
|
|
|||
|
|
@ -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**
|
||||
|
|
@ -18,10 +18,10 @@ target instance.
|
|||
The result of the function is applied to a target in the same way
|
||||
properties are using `create()`.
|
||||
|
||||
The function is ran in a new reactive scope, just like
|
||||
The function is ran in a new stable scope, just like
|
||||
[root()](reactivity-core.md#root).
|
||||
|
||||
Returns a function that when called will destroy the reactive scope.
|
||||
Returns a function that when called will destroy the stable scope.
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ A wrapper for `action()` to listen for property changes.
|
|||
Will run the given callback any time the property is changed, as well as
|
||||
when the action is initially run.
|
||||
|
||||
The changed connection is disconnected when the reactive scope the action is
|
||||
ran in is destroyed.
|
||||
The changed connection is disconnected when the scope the action is ran in
|
||||
is destroyed.
|
||||
|
||||
Runs with an action priority of 1.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -41,11 +42,6 @@ Creates a new source with the given 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 a reactive scope will cause changes
|
||||
to that source to be tracked and anything depending on it to update.
|
||||
|
||||
Sources can be created outside of reactive scopes.
|
||||
|
||||
- **Example**
|
||||
|
||||
```lua
|
||||
|
|
@ -68,10 +64,10 @@ Runs a side-effect in a new reactive scope on source update.
|
|||
|
||||
- **Details**
|
||||
|
||||
Any time a source referenced in the callback is changed, the callback will
|
||||
Any time a source referenced in the callback is updated, the callback will
|
||||
be reran.
|
||||
|
||||
The callback is ran to initially ran on first call to find dependent sources.
|
||||
The callback is ran once immediately.
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
@ -107,7 +103,7 @@ Derives a new source in a new reactive scope from existing sources.
|
|||
Anytime its value is recalculated it is also cached, subsequent calls will
|
||||
retun this cached value until it recalculates again.
|
||||
|
||||
The callback is ran to initially ran on first call to find dependent sources.
|
||||
The callback is ran once immediately.
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ Shows one of two components depending on an input source.
|
|||
Returns a source holding an instance of the currently shown component.
|
||||
|
||||
When the input source changes from a falsey to a truthy value, the
|
||||
component will be reran under a new reactive scope. If it changes from a
|
||||
truthy to falsey value, the reactive scope the component was created in will
|
||||
component will be reran under a new stable scope. If it changes from a
|
||||
truthy to falsey value, the stable scope the component was created in will
|
||||
be destroyed, and the returned source will output `nil`, or a fallback
|
||||
component if given.
|
||||
|
||||
The fallback component is also ran under a new reactive scope, and destroyed
|
||||
The fallback component is also ran under a new stable scope, and destroyed
|
||||
when the input source switches back to truthy.
|
||||
|
||||
## switch()
|
||||
|
|
@ -41,10 +41,10 @@ Shows one of a set of components depending on an input source and a mapping tabl
|
|||
Returns a source holding an instance of the currently shown component.
|
||||
|
||||
When the input source changes, the new value will be used to lookup a given
|
||||
mapping table to get a component, which will be ran under a new reactive
|
||||
scope. If the input source changes, the reactive scope the component was
|
||||
mapping table to get a component, which will be ran under a new stable
|
||||
scope. If the input source changes, the stable scope the component was
|
||||
created in will be destroyed, and a new component created under a new
|
||||
reactive scope. If no component is found for an input value, the switch will
|
||||
stable scope. If no component is found for an input value, the switch will
|
||||
output `nil`.
|
||||
|
||||
- **Example**
|
||||
|
|
@ -82,9 +82,9 @@ Maps each index in a table source to an object.
|
|||
When the input source changes, each *index* in the new table is compared with
|
||||
the last input table.
|
||||
|
||||
- For any new index, the `transform` function is ran under a new reactive
|
||||
- For any new index, the `transform` function is ran under a new stable
|
||||
scope to produce a new instance.
|
||||
- For any removed index, the reactive scope for that index is destroyed.
|
||||
- For any removed index, the stable scope for that index is destroyed.
|
||||
- Unchanged indexes are untouched.
|
||||
|
||||
The transform function is called only ever *once* for each index in the
|
||||
|
|
@ -142,9 +142,9 @@ Maps each value in a table source to an object.
|
|||
When the input source changes, each *value* in the new table is compared with
|
||||
the last input table. Similar to `indexes()` but for values instead of indexes.
|
||||
|
||||
- For any new value, the `transform` function is ran under a new reactive
|
||||
- For any new value, the `transform` function is ran under a new stable
|
||||
scope to produce a new instance.
|
||||
- For any removed value, the reactive scope for that value is destroyed.
|
||||
- For any removed value, the stable scope for that value is destroyed.
|
||||
- Unchanged values are untouched.
|
||||
|
||||
The transform function is only ever called *once* for each value in the
|
||||
|
|
@ -203,7 +203,7 @@ Maps each value in a table source to an object.
|
|||
- Toast notifications.
|
||||
|
||||
`indexes()` should be used in other cases, especially when your source table
|
||||
has primitive value. It maps an index to a UI element.
|
||||
has primitive values. It maps an index to a UI element.
|
||||
|
||||
e.g.
|
||||
- List of character or weapon stats.
|
||||
|
|
@ -213,6 +213,6 @@ Maps each value in a table source to an object.
|
|||
result in less property updates and less re-renders. One case to note is
|
||||
that `values()` works nicely when animating re-ordering of instances, since
|
||||
the value is not destroyed when indexes are changed, and the source index
|
||||
can easily be put through a spring.
|
||||
can be used to animate a change in position for the UI element.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## cleanup()
|
||||
|
||||
Runs a callback anytime a reactive scope is reran or destroyed.
|
||||
Runs a callback anytime a scope is reran or destroyed.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -31,8 +31,7 @@ Runs a callback anytime a reactive scope is reran or destroyed.
|
|||
|
||||
## untrack()
|
||||
|
||||
Runs a given function where any sources read will not be tracked by a reactive
|
||||
scope.
|
||||
Runs a given function in a new stable scope.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
@ -42,8 +41,8 @@ scope.
|
|||
|
||||
- **Details**
|
||||
|
||||
Updates made to a source passed to `untrack()` will not cause updates to
|
||||
anything depending on that source.
|
||||
Can be used inside a reactive scope to read from sources you do not want
|
||||
tracked by the reactive scope.
|
||||
|
||||
- **Example**
|
||||
|
||||
|
|
@ -76,7 +75,7 @@ read can still be tracked inside a reactive scope.
|
|||
## batch()
|
||||
|
||||
Runs a given function where any source updates made within the function do not
|
||||
trigger effects until after the function runs.
|
||||
trigger effects until after the function finishes running.
|
||||
|
||||
- **Type**
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ ensure that derived source computations are pure, and that any
|
|||
cleanups made in derived sources or effects are done correctly.
|
||||
|
||||
Accidental yielding within reactive scopes can break Vide's reactive graph,
|
||||
which strict mode can catch.
|
||||
which strict mode will catch.
|
||||
|
||||
As well as additional safety checks, Vide will dedicate extra resources to
|
||||
recording and better emitting stack traces where errors occur, particularly
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,22 +4,12 @@ This is a tutorial that introduces the concepts and usage of Vide.
|
|||
|
||||
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
|
||||
|
||||
This tutorial assumes familiarity with Luau and Roblox UI.
|
||||
|
||||
## Why Vide?
|
||||
|
||||
Creating UI is complicated, slow, and tedious.
|
||||
|
||||
Vide tries to simplify and speed up this process by providing a declarative and
|
||||
reactive of style programming, which lets you focus more on designing the UI
|
||||
itself and not having to manually update or reparent UI instances.
|
||||
Vide provides a reactive and declarative API to simplify managing UI.
|
||||
|
||||
Some of the main focuses behind Vide's design choices:
|
||||
|
||||
- Minimal syntax.
|
||||
- Minimal syntax
|
||||
- Complete typechecking
|
||||
- Independence from instances.
|
||||
|
||||
As with most declarative libraries, there is an initial learning curve to
|
||||
understand the concepts and usage. This tutorial tries to comprehensively
|
||||
cover these concepts and usage, more so than you need just to use it.
|
||||
- Independence from instances
|
||||
|
|
|
|||
|
|
@ -2,44 +2,33 @@
|
|||
|
||||
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.
|
||||
is used to queue a callback for the next time a reactive scope is rerun or
|
||||
destroyed, or when a stable scope is destroyed.
|
||||
|
||||
```lua
|
||||
local mount = vide.mount
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local cleanup = vide.cleanup
|
||||
local effect = vide.effect
|
||||
|
||||
local function Timer()
|
||||
local count = source(0)
|
||||
|
||||
local con = game:GetService("RunService").Heartbeat:Connect(function(dt)
|
||||
count(count() + dt)
|
||||
|
||||
local destroy = root(function(destroy)
|
||||
effect(function()
|
||||
local x = count()
|
||||
cleanup(function() print(x) end)
|
||||
end)
|
||||
|
||||
cleanup(function()
|
||||
con:Disconnect()
|
||||
cleanup(function() print "root destroyed" end)
|
||||
|
||||
return destroy
|
||||
end)
|
||||
|
||||
return create "TextButton" {
|
||||
Position = UDim2.fromOffset(300, 300),
|
||||
Size = UDim2.fromOffset(200, 50),
|
||||
|
||||
Text = function()
|
||||
return "seconds: " .. math.floor(count())
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local unmount = mount(Timer)
|
||||
|
||||
unmount() -- all queued cleanups are ran, heartbeat connection disconnected
|
||||
count(1) -- prints "0"
|
||||
count(2) -- prints "1"
|
||||
destroy() -- prints "2" and "root destroyed"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
::: tip
|
||||
Roblox instances do not need to be explicitly destroyed for their
|
||||
memory to be freed, they only need to be parented to `nil`. So there is no
|
||||
|
|
|
|||
|
|
@ -5,119 +5,26 @@ resulting from source updates. Vide provides functions to help you do this,
|
|||
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
|
||||
function. This means parts of your app can be independently created and
|
||||
destroyed.
|
||||
|
||||
## switch()
|
||||
|
||||
`switch()` condtionally displays one instance at a time. It uses a table to map
|
||||
a source value to a component.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local switch = vide.switch
|
||||
|
||||
local function Button(props: {
|
||||
Text: string,
|
||||
Activated: () -> ()
|
||||
})
|
||||
local hovered = source(false)
|
||||
|
||||
return create "TextButton" {
|
||||
Text = props.Text,
|
||||
Activated = props.Activated,
|
||||
|
||||
TextColor3 = function()
|
||||
return hovered() and Color3.new(1, 1, 1) or Color3.new(.7, .7, .7)
|
||||
end,
|
||||
|
||||
MouseEnter = function() hovered(true) end,
|
||||
MouseLeave = function() hovered(false) end
|
||||
}
|
||||
end
|
||||
|
||||
local function JoinMenu()
|
||||
local joined = source(false)
|
||||
|
||||
local function JoinButton()
|
||||
return Button {
|
||||
Text = "Join",
|
||||
Activated = function() joined(true) end
|
||||
}
|
||||
end
|
||||
|
||||
local function LeaveButton()
|
||||
return Button {
|
||||
Text = "Leave"
|
||||
Activated = function() joined(false) end
|
||||
}
|
||||
end
|
||||
|
||||
return create "Frame" {
|
||||
switch(joined) {
|
||||
[true] = LeaveButton,
|
||||
[false] = JoinButton
|
||||
}
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#1C1C1F"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root["root scope"]
|
||||
direction LR
|
||||
joined --> switch -.- subroot
|
||||
|
||||
subgraph subroot["switch scope"]
|
||||
direction LR
|
||||
effect["TextColor3 effect"]
|
||||
end
|
||||
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.
|
||||
|
||||
This will also destroy the internal effect that the button uses to highlight
|
||||
itself when it is hovered, each time the switch is rerun.
|
||||
The new sources can be used in `create()` to update the children of a container
|
||||
instance.
|
||||
|
||||
## indexes()
|
||||
|
||||
Often, you will have a table of values with each value displayed in a similar
|
||||
manner. Rather than manually looping over each value to generate a corresponding
|
||||
UI element, `indexes()` allows you to create elements each corresponding to a
|
||||
table index, to display the value at that index.
|
||||
`indexes()` *maps* each table index to a new UI element that can
|
||||
update to display the current value at that index. Each table index is given a
|
||||
single corresponding UI element.
|
||||
|
||||
```lua
|
||||
local todoList = source {
|
||||
local list = source {
|
||||
"finish the crash course",
|
||||
"star vide's GitHub"
|
||||
"star Vide's GitHub"
|
||||
}
|
||||
|
||||
local function TodoList(props: { list: () -> Array<string> })
|
||||
return create "Frame" {
|
||||
create "UIListLayout" {},
|
||||
|
||||
indexes(todoList, function(todo, i)
|
||||
indexes(list, function(todo, i)
|
||||
return create "TextLabel" {
|
||||
Text = function()
|
||||
return i .. ": " .. todo()
|
||||
|
|
@ -129,13 +36,13 @@ local function TodoList(props: { list: () -> Array<string> })
|
|||
}
|
||||
end
|
||||
|
||||
TodoList { list = todoList }
|
||||
TodoList { list = list }
|
||||
```
|
||||
|
||||
For each index in the given source table, the given function will be called
|
||||
with:
|
||||
For each index in the given source table, the given function to `indexes()` will
|
||||
be run in a new stable scope with:
|
||||
|
||||
1. a source containing the value of the index
|
||||
1. a source containing the value at the index
|
||||
2. the index itself
|
||||
|
||||
When the value at an index is changed, the function is not reran. Instead, the
|
||||
|
|
@ -143,12 +50,9 @@ given source for that index is updated.
|
|||
|
||||
Any time the input source table is updated, the given function will be ran for
|
||||
any newly added indexes, while any removed indexes (indexes now with a `nil`
|
||||
value), will have its corresponding reactive scope destroyed to clean up that
|
||||
element.
|
||||
value), will have its corresponding stable scope destroyed.
|
||||
|
||||
|
||||
`indexes()` is said to *map* each table index to a new UI element that can
|
||||
update to display the current value at that index. Each table index is given a
|
||||
single corresponding UI element.
|
||||
|
||||
The reactive graph for the above example:
|
||||
|
||||
|
|
@ -183,8 +87,8 @@ subgraph root ["root scope"]
|
|||
end
|
||||
```
|
||||
|
||||
One thing to note regarding table sources, is that when you edit a table in a
|
||||
source, you must set that table again to actually update the source.
|
||||
When you edit a table in a source, you must set that table again to actually
|
||||
update the source.
|
||||
|
||||
```lua
|
||||
local src = source { 1, 2 }
|
||||
|
|
@ -192,11 +96,3 @@ local data = src()
|
|||
table.insert(data, 3) -- no effects will run
|
||||
src(data) -- effects will run
|
||||
```
|
||||
|
||||
Together, these control flow functions cover the majority of cases where you
|
||||
need to dynamically create and destroy parts of your UI.
|
||||
|
||||
If you need to do something that these control flow functions cannot, you can
|
||||
always use `mount()` within an effect to dynamically create and destroy
|
||||
components on your own terms. Just remember to use `cleanup()` to unmount when
|
||||
the effect reruns.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ action used to listen for property changes:
|
|||
|
||||
```lua
|
||||
local action = vide.action
|
||||
local effect = vide.effect
|
||||
local cleanup = vide.cleanup
|
||||
|
||||
local function changed(prop: string, callback: (new) -> ())
|
||||
|
|
@ -44,9 +45,11 @@ local instance = create "TextBox" {
|
|||
changed("Text", output)
|
||||
}
|
||||
|
||||
instance.Text = "foo"
|
||||
effect(function()
|
||||
print(output())
|
||||
end)
|
||||
|
||||
print(output()) -- "foo"
|
||||
instance.Text = "foo" -- "foo" will be printed from the effect
|
||||
```
|
||||
|
||||
The source `output` will be updated with the new property value any time it is
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
# Nested Properties
|
||||
|
||||
Often when creating components from existing components, you can find yourself
|
||||
repetitively passing through properties such as size or position.
|
||||
|
||||
Example below:
|
||||
|
||||
```lua
|
||||
function Background(props: {
|
||||
Color: Color3,
|
||||
AnchorPoint: UDim2,
|
||||
Position: UDim2,
|
||||
Size: UDim2
|
||||
})
|
||||
return create "Frame" {
|
||||
Color = props.Color
|
||||
AnchorPoint = props.AnchorPoint,
|
||||
Position = props.Position,
|
||||
Size = props.Size
|
||||
}
|
||||
end
|
||||
|
||||
function Menu(props: {
|
||||
Color = props.Color
|
||||
AnchorPoint: UDim2,
|
||||
Position: UDim2,
|
||||
Size: UDim2
|
||||
})
|
||||
return Background {
|
||||
Color = props.Color,
|
||||
AnchorPoint = props.AnchorPoint,
|
||||
Position = props.Position,
|
||||
Size = props.Size
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
One way this can be avoided is by using *property nesting*. In Vide, passing a
|
||||
table value inside `props` has special semantics. Any key with a table value is
|
||||
not assigned like a property, instead the table is iterated and processed just
|
||||
like the outer table is. Any properties in the nested table will be assigned
|
||||
to the instance just the same.
|
||||
|
||||
Below is an example of how you can use this to pass groups of similar properties
|
||||
together such as position and size, while also using typechecking.
|
||||
|
||||
```lua
|
||||
type Layout = {
|
||||
Layout = {
|
||||
Position: UDim2?,
|
||||
Size: UDim2?,
|
||||
AnchorPoint: Vector2?
|
||||
}
|
||||
}
|
||||
|
||||
function Background(props: Layout & { Color: Color3 })
|
||||
return create "Frame" {
|
||||
Color = props.Color,
|
||||
props.Layout
|
||||
}
|
||||
end
|
||||
|
||||
function Menu(props: Layout & { Color: Color3 })
|
||||
return Background {
|
||||
Color = props.Color,
|
||||
Layout = props.Layout
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
Here we created a nested group with the key `Layout` that can accept
|
||||
layout-related properties. Any name could be chosen for the key.
|
||||
This allows us to write much more concise syntax that is also typecheckable.
|
||||
|
||||
In another example we use a key named `Children` to pass arrays of instances to
|
||||
be parented.
|
||||
|
||||
```lua
|
||||
type Children = {
|
||||
-- also can optionally pass a source that returns an array of children too
|
||||
Children = Array<Instance> | () -> Array<Instance>
|
||||
}
|
||||
|
||||
local function List(props: Children & Layout)
|
||||
return create "Frame" {
|
||||
props.Children,
|
||||
props.Layout,
|
||||
create "UIListLayout" {}
|
||||
}
|
||||
end
|
||||
|
||||
List {
|
||||
Layout = {
|
||||
Position = UDim2.new()
|
||||
},
|
||||
|
||||
Children = {
|
||||
create "TextLabel" { Text = "1" },
|
||||
create "TextLabel" { Text = "2" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Deeper nested properties are guaranteed to be set after shallower nested
|
||||
properties, this can be used to create overridable default properties.
|
||||
|
||||
```lua
|
||||
local function List(props: Children & Layout)
|
||||
return create "Frame" {
|
||||
props.Children,
|
||||
props.Layout,
|
||||
|
||||
-- can be overriden by `props.Layout`
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Position = UDim2.fromScale(0.5, 0),
|
||||
|
||||
create "UIListLayout" {}
|
||||
}
|
||||
end
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -2,11 +2,8 @@
|
|||
|
||||
Instances are created using `create()`.
|
||||
|
||||
`create()` returns a constructor for a class which then takes a table of
|
||||
properties to assign when creating a new instance for that class.
|
||||
|
||||
Luau allows us to omit parentheses `()` when calling functions with string or
|
||||
table literals which is recommended to use for brevity.
|
||||
Parentheses `()` can be omitted when calling functions with string or
|
||||
table literals for brevity.
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
|
|
@ -39,10 +36,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.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@ end
|
|||
return Button
|
||||
```
|
||||
|
||||
```lua [App.luau]
|
||||
```lua [Menu.luau]
|
||||
local create = vide.create
|
||||
|
||||
local Button = require(Button)
|
||||
|
||||
local function App()
|
||||
local function Menu()
|
||||
return create "ScreenGui" {
|
||||
Button {
|
||||
Position = UDim2.fromOffset(200, 200),
|
||||
|
|
@ -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,7 +1,7 @@
|
|||
# Sources
|
||||
|
||||
Sources are special objects that store a single value. They are the core of
|
||||
Vide's reactivity. They are called sources because they act as sources of data.
|
||||
Sources are special objects that store a single value and are the core of
|
||||
Vide's reactivity.
|
||||
|
||||
A source can be created using `source()`.
|
||||
|
||||
|
|
@ -20,8 +20,7 @@ by calling it with no arguments.
|
|||
count(count() + 1) -- increment count by 1
|
||||
```
|
||||
|
||||
Sources can be *derived* by wrapping them in functions. A wrapped source
|
||||
effectively becomes a new source.
|
||||
Sources can be *derived* by wrapping them in functions.
|
||||
|
||||
```lua
|
||||
local count = source(0)
|
||||
|
|
@ -35,7 +34,5 @@ count(1)
|
|||
print(text()) -- "count: 1"
|
||||
```
|
||||
|
||||
Sources on their own aren't very special, the above can be achieved with plain
|
||||
variables. The real use for sources become apparent when used in combination
|
||||
with *effects*. Similar to a signal and connection, a source and effect allows
|
||||
you to do things like automatically updating UI when a source is updated.
|
||||
While the above can be achieved with plain variables, the use for sources will
|
||||
be obvious in the next part.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# Effects
|
||||
|
||||
Effects are functions that are ran in response to source updates. They are
|
||||
called effects because they cause *side-effects* when reacting to source
|
||||
updates.
|
||||
A source and effect is analogous to a signal and connection.
|
||||
|
||||
Effects are created using `effect()`.
|
||||
|
||||
|
|
@ -21,11 +20,10 @@ count(1)
|
|||
-- "count: 1" printed
|
||||
```
|
||||
|
||||
The callback given to `effect()` is ran immediately in a *reactive scope*. Any
|
||||
source read from inside a reactive scope will be tracked, so when any of those
|
||||
sources update, the effect will be reran too.
|
||||
Any source read inside an effect is tracked and will rerun the effect when
|
||||
that source is updated.
|
||||
|
||||
Reactive scopes also track derived sources, it doesn't matter how deeply nested
|
||||
Derived sources are also tracked, it doesn't matter how deeply nested
|
||||
inside a function a source is.
|
||||
|
||||
```lua
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
# Root Reactive Scopes
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function App()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
App() -- will error since effect() was not called within a reactive scope
|
||||
|
||||
vide.mount(App) -- works!
|
||||
|
||||
```
|
||||
|
||||
Mounting returns a function that when called will destroy its reactive scope,
|
||||
along with any other reactive scopes created inside it.
|
||||
|
||||
```lua
|
||||
local unmount = mount(App)
|
||||
|
||||
unmount()
|
||||
```
|
||||
|
||||
Vide's reactivity can be represented graphically, as a *reactive graph*.
|
||||
|
||||
The reactive graph for the above example looks like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root
|
||||
direction LR
|
||||
count --> effect
|
||||
end
|
||||
```
|
||||
|
||||
When the root reactive scope created by `mount()` 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
|
||||
UI instance, meaning the effect is referencing and holding that instance in
|
||||
memory. The effect being destroyed will remove this reference, allowing the
|
||||
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
|
||||
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.
|
||||
105
docs/tut/crash-course/6-scope.md
Normal file
105
docs/tut/crash-course/6-scope.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Scopes
|
||||
|
||||
Just like how a signal's connection may need to be disconnected, a source's
|
||||
effect also may need to be disconnected.
|
||||
|
||||
But the disconnecting of many signals and connections is tedious and verbose.
|
||||
Vide instead operates on the concept of scopes which provides a much cleaner
|
||||
API, given that you follow a few rules.
|
||||
|
||||
Scopes come in two flavors; stable and reactive.
|
||||
|
||||
- All scopes must be created within another scope with the exception of `root()`
|
||||
- Stable scopes never rerun
|
||||
- Reactive scopes can rerun
|
||||
- A reactive scope cannot be created within another reactive scope
|
||||
|
||||
`effect()` creates a reactive scope.
|
||||
`root()` creates a stable scope.
|
||||
|
||||
Whenever a scope is destroyed, any scope created within that scope is also
|
||||
destroyed, and so on. This is why all scopes must be created within another
|
||||
scope, except `root()` which is used to create the initial scope that you can
|
||||
manually destroy.
|
||||
|
||||
```lua
|
||||
local root = vide.root
|
||||
local source = vide.source
|
||||
local effect = vide.effect
|
||||
|
||||
local function setup()
|
||||
local count = source(0)
|
||||
|
||||
effect(function()
|
||||
print(count())
|
||||
end)
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
setup() -- will error since effect() tries to create a reactive scope outside of a stable scope
|
||||
|
||||
local count = root(setup) -- ok since effect() was called within a stable scope
|
||||
count(1) -- prints "1"
|
||||
```
|
||||
|
||||
The scope created by `root()` can be destroyed by calling the function it passes
|
||||
into the given function.
|
||||
|
||||
```lua
|
||||
local function setup(destroy)
|
||||
local count = source(0)
|
||||
|
||||
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*.
|
||||
|
||||
The reactive graph for the above example looks like so:
|
||||
|
||||
```mermaid
|
||||
%%{init: {
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#1B1B1F",
|
||||
"primaryTextColor": "#fff",
|
||||
"primaryBorderColor": "#1B1B1F",
|
||||
"lineColor": "#79B8FF",
|
||||
"tertiaryColor": "#161618",
|
||||
"tertiaryBorderColor": "#161618"
|
||||
}
|
||||
}}%%
|
||||
|
||||
graph
|
||||
|
||||
subgraph root
|
||||
direction LR
|
||||
count --> effect
|
||||
end
|
||||
```
|
||||
|
||||
When the stable `root()` is destroyed, the reactive `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
|
||||
UI instance, meaning the effect is referencing and holding that instance in
|
||||
memory. The effect being destroyed will remove this reference, allowing the
|
||||
instance to be garbage collected.
|
||||
|
||||
You don't need to worry about ensuring all your effects are created within a
|
||||
stable scope, since you should be creating all your UI and effects within a
|
||||
single top-level `root()` call that puts all your UI together, making it safe to
|
||||
assume any effect created will be created under this stable scope.
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
# Stateful Components
|
||||
|
||||
A stateful component is a component that stores some data internally.
|
||||
|
||||
Stateful components in Vide are created using sources and effects - sources to
|
||||
store the data, and effects to display the data.
|
||||
|
||||
|
|
@ -27,8 +25,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 +33,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 +65,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,7 +1,7 @@
|
|||
# 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.
|
||||
Explicitly creating effects to update properties is tedious. You can
|
||||
*implicitly* create an effect to update properties instead.
|
||||
|
||||
```lua
|
||||
local create = vide.create
|
||||
|
|
@ -25,23 +25,14 @@ end
|
|||
This example is equivalent to the example seen on the previous page.
|
||||
|
||||
Instead of explicitly creating an effect, assigning a (non-event) property a
|
||||
function will implicitly create an effect to update that property anytime a
|
||||
source used within is updated.
|
||||
function will implicitly create an effect to update that property.
|
||||
|
||||
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
|
||||
instances. Vide will automatically unparent removed instances and parent new
|
||||
instances when that source's stored instances change.
|
||||
instances. An effect is automatically created to unparent removed instances and
|
||||
parent new instances on source update.
|
||||
|
||||
```lua
|
||||
local items = source {
|
||||
|
|
@ -62,5 +53,5 @@ items {
|
|||
create "TextLabel" { Text = "C" }
|
||||
}
|
||||
|
||||
-- this will automatically unparent the text label "A", and parent the labels "B" and "C".
|
||||
-- this will automatically unparent the text label "A", and parent the labels "B" and "C"
|
||||
```
|
||||
|
|
@ -36,14 +36,13 @@ 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.
|
||||
Reading this derived source multiple times will just return a cached result from
|
||||
when it last updated.
|
||||
run a function in a reactive scope only when a source used inside updated.
|
||||
Reading this derived source multiple times will just return a cached result.
|
||||
|
||||
```lua
|
||||
local source = vide.source
|
||||
local derive = vide.derive
|
||||
local effect = vide.effect
|
||||
local derive = vide.derive
|
||||
|
||||
local count = source(0)
|
||||
|
||||
|
|
@ -58,7 +57,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.
|
||||
|
|
@ -87,6 +86,7 @@ end
|
|||
```
|
||||
|
||||
Deriving a source in this manner is similar to creating an effect to update
|
||||
another source. You should never manually do this using an effect however,
|
||||
improper usage could accidently create infinite loops in the reactive graph.
|
||||
Always favour deriving when you need one source to update based on another.
|
||||
another source. You should never manually do this using an effect however.
|
||||
Improper usage could accidently create infinite loops in the reactive graph.
|
||||
Always favour deriving when you need one source to update based on another
|
||||
source.
|
||||
|
|
|
|||
|
|
@ -1,42 +1,13 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local trace = require(script.Parent.trace)
|
||||
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
|
||||
-- track bind creation trace
|
||||
local fn = updater
|
||||
local bind_trace = debug.traceback(nil, trace()-1)
|
||||
updater = function(...)
|
||||
local ok, result = xpcall(fn, function(err: string)
|
||||
return err
|
||||
end, ...)
|
||||
|
||||
if not ok then
|
||||
local btype =
|
||||
if (binding :: any).property then (binding :: any).property
|
||||
elseif (binding :: any).parent then "Parent"
|
||||
else "children"
|
||||
error(`PROPERTY BINDING ERROR: Property {btype}\n{result}\nBIND CREATION TRACE:\n{bind_trace}`, 0)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
local owner = assert_owning_scope()
|
||||
|
||||
local node = create_node(binding, updater)
|
||||
|
||||
set_owner(node, owner)
|
||||
evaluate_node(node)
|
||||
function create_implicit_effect<T>(updater: (T) -> T, binding: T)
|
||||
evaluate_node(create_node(assert_stable_scope(), updater, binding))
|
||||
end
|
||||
|
||||
type PropertyBinding = {
|
||||
|
|
@ -45,7 +16,7 @@ type PropertyBinding = {
|
|||
source: () -> unknown
|
||||
}
|
||||
|
||||
local function update_property(p: PropertyBinding)
|
||||
local function update_property_effect(p: PropertyBinding)
|
||||
(p.instance :: any)[p.property] = p.source()
|
||||
return p
|
||||
end
|
||||
|
|
@ -55,7 +26,7 @@ type ParentBinding = {
|
|||
parent: () -> Instance
|
||||
}
|
||||
|
||||
local function update_parent(p: ParentBinding)
|
||||
local function update_parent_effect(p: ParentBinding)
|
||||
p.instance.Parent = p.parent()
|
||||
return p
|
||||
end
|
||||
|
|
@ -67,7 +38,7 @@ type ChildrenBinding = {
|
|||
children: () -> Instance | { Instance }
|
||||
}
|
||||
|
||||
local function update_children(p: ChildrenBinding)
|
||||
local function update_children_effect(p: ChildrenBinding)
|
||||
local cur_children_set: { [Instance]: true } = p.cur_children_set -- cache of all children parented before update
|
||||
local new_child_set: { [Instance]: true } = p.new_children_set -- cache of all children parented after update
|
||||
|
||||
|
|
@ -100,7 +71,7 @@ end
|
|||
|
||||
return {
|
||||
property = function(instance, property, source)
|
||||
return create_binding(update_property, {
|
||||
return create_implicit_effect(update_property_effect, {
|
||||
instance = instance,
|
||||
property = property,
|
||||
source = source
|
||||
|
|
@ -108,14 +79,14 @@ return {
|
|||
end,
|
||||
|
||||
parent = function(instance, parent)
|
||||
return create_binding(update_parent, {
|
||||
return create_implicit_effect(update_parent_effect, {
|
||||
instance = instance,
|
||||
parent = parent
|
||||
})
|
||||
end,
|
||||
|
||||
children = function(instance, children)
|
||||
return create_binding(update_children, {
|
||||
return create_implicit_effect(update_children_effect, {
|
||||
instance = instance,
|
||||
cur_children_set = {},
|
||||
new_children_set = {},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
150
src/graph.luau
150
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,21 +16,21 @@ 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)
|
||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, arg)
|
||||
local thread = coroutine.create(xpcall)
|
||||
local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local resume_ok, run_ok, result = coroutine.resume(thread, fn, efn, arg)
|
||||
|
||||
assert(resume_ok)
|
||||
|
||||
if coroutine.status(thread) ~= "dead" then
|
||||
return false, "attempt to yield in reactive scope"
|
||||
return false, debug.traceback(thread, "attempt to yield in reactive scope")
|
||||
end
|
||||
|
||||
return run_ok, result
|
||||
|
|
@ -40,46 +40,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 +78,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
|
||||
|
|
@ -138,44 +130,51 @@ end
|
|||
local update_queue = { n = 0 } :: { n: number, [number]: Node<any> }
|
||||
|
||||
local function evaluate_node<T>(node: Node<T>)
|
||||
if flags.strict then
|
||||
local initial_value = node.cache
|
||||
|
||||
for i = 1, 2 do
|
||||
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()
|
||||
|
||||
if not ok then throw(new_value :: string) end
|
||||
|
||||
node.cache = new_value :: T
|
||||
end
|
||||
|
||||
run_cleanups(node)
|
||||
destroy_owned(node)
|
||||
|
||||
open_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)
|
||||
update_queue.n = 0
|
||||
throw(`side-effect error from source update\n{new_value}`)
|
||||
throw(`effect stacktrace:\n{new_value :: string}`)
|
||||
end
|
||||
|
||||
node.cache = new_value :: T
|
||||
end
|
||||
|
||||
return initial_value ~= node.cache
|
||||
else
|
||||
local cur_value = node.cache
|
||||
|
||||
flush_cleanups(node)
|
||||
destroy_owned(node)
|
||||
|
||||
push_scope(node)
|
||||
local ok, new_value = pcall(node.effect :: (T) -> T, node.cache)
|
||||
pop_scope()
|
||||
|
||||
if not ok then
|
||||
table.clear(update_queue)
|
||||
update_queue.n = 0
|
||||
throw(`effect stacktrace:\n{new_value}\n`)
|
||||
end
|
||||
|
||||
node.cache = new_value
|
||||
|
||||
return cur_value ~= new_value
|
||||
end
|
||||
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 +197,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 +209,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 +222,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 +232,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
|
||||
|
||||
local function create_start_node<T>(value: T): StartNode<T>
|
||||
return node
|
||||
end
|
||||
|
||||
local function create_source_node<T>(value: T): SourceNode<T>
|
||||
return { cache = value }
|
||||
end
|
||||
|
||||
|
|
@ -262,20 +271,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
|
||||
|
||||
|
|
@ -22,20 +21,18 @@ local function check_primitives(t: {})
|
|||
if not flags.strict then return end
|
||||
|
||||
for _, v in next, t do
|
||||
if type(v) == "table" or type(v) == "userdata" then continue end
|
||||
if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end
|
||||
throw("table source map cannot return primitives")
|
||||
end
|
||||
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,15 +21,16 @@ local function root<T...>(fn: (destroy: () -> ()) -> T...): T...
|
|||
destroy(node)
|
||||
end
|
||||
|
||||
open_scope(node)
|
||||
push_scope(node)
|
||||
|
||||
local result = { pcall(fn, destroy) }
|
||||
local function efn(err: string) return debug.traceback(err, 3) end
|
||||
local result = { xpcall(fn, efn, destroy) }
|
||||
|
||||
close_scope()
|
||||
pop_scope()
|
||||
|
||||
if not result[1] then
|
||||
refs[node] = nil
|
||||
throw(`mount error\n{result[2]}`)
|
||||
throw(`error while running root():\n\n{result[2]}`)
|
||||
end
|
||||
|
||||
return unpack(result :: any, 2)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -111,7 +110,7 @@ local vec6_to_type = {
|
|||
end :: Vec6ToType<CFrame>,
|
||||
|
||||
Color3 = function(v)
|
||||
return Color3.new(v.X, v.Y, v.Z)
|
||||
return Color3.new(math.clamp(v.X, 0, 1), math.clamp(v.Y, 0, 1), math.clamp(v.Z, 0, 1))
|
||||
end :: Vec6ToType<Color3>,
|
||||
|
||||
UDim = function(v)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local trace = require(script.Parent.trace)
|
||||
|
||||
local function throw(msg): any
|
||||
error(msg, trace() - 1)
|
||||
local function VIDE_ASSERT(msg): any
|
||||
error(msg, 0)
|
||||
end
|
||||
|
||||
return throw
|
||||
return VIDE_ASSERT
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
-- returns path to file as an array with each directory
|
||||
-- accounts for Roblox and Luau contexts
|
||||
local function get_path(s)
|
||||
if string.sub(s, #s - 4, #s) == ".luau" then
|
||||
s = string.sub(s, 1, #s - 5)
|
||||
end
|
||||
|
||||
return string.split(s, string.match(s, "%w+/") and "/" or ".")
|
||||
end
|
||||
|
||||
-- get directory of vide root
|
||||
local root do
|
||||
local path = get_path(debug.info(1, "s"))
|
||||
root = path[#path - 1]
|
||||
end
|
||||
|
||||
-- finds the first stack depth outside of any vide library function
|
||||
return function(): number
|
||||
local stack = 1
|
||||
|
||||
local path = get_path(debug.info(stack, "s"))
|
||||
|
||||
while path[#path] == root or path[#path - 1] == root do
|
||||
stack += 1
|
||||
path = get_path(debug.info(stack, "s"))
|
||||
end
|
||||
|
||||
return stack
|
||||
end
|
||||
|
|
@ -26,7 +26,6 @@ end
|
|||
|
||||
local N = 2^18 -- 262144
|
||||
|
||||
|
||||
TITLE "sources"
|
||||
|
||||
BENCH("create source", function()
|
||||
|
|
@ -449,27 +448,6 @@ end)
|
|||
|
||||
N *= 1024
|
||||
|
||||
TITLE "cleanup"
|
||||
|
||||
ROOT_BENCH("register new cleanup", function()
|
||||
local cleanup = cleanup
|
||||
|
||||
local cleaner = function() end
|
||||
|
||||
local callers = {}
|
||||
|
||||
for i = 1, N do
|
||||
callers[i] = function(fn, v)
|
||||
fn(v)
|
||||
return i -- return unique upvalue to ensure unique closure
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, START(N) do
|
||||
callers[i](cleanup, cleaner)
|
||||
end
|
||||
end)
|
||||
|
||||
TITLE "aggregate"
|
||||
|
||||
do
|
||||
|
|
|
|||
181
test/tests.luau
181
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
|
||||
|
|
@ -235,8 +218,8 @@ TEST("graph", function()
|
|||
do
|
||||
local c = get_children(selected)
|
||||
CHECK(#c == 2)
|
||||
CHECK(table.find(c, bind1))
|
||||
CHECK(table.find(c, bind2))
|
||||
CHECK(table.find(c, bind1 :: any))
|
||||
CHECK(table.find(c, bind2 :: any))
|
||||
end
|
||||
|
||||
do
|
||||
|
|
@ -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
|
||||
|
|
@ -2285,6 +2261,27 @@ TEST("strict", wrap_root(function()
|
|||
src(not src())
|
||||
CHECK(count == 4)
|
||||
end
|
||||
|
||||
do CASE "effect using derived source"
|
||||
local input = source(true)
|
||||
|
||||
local output = derive(function()
|
||||
return input()
|
||||
end)
|
||||
|
||||
local count = 0
|
||||
|
||||
effect(function()
|
||||
output()
|
||||
count += 1
|
||||
end)
|
||||
|
||||
CHECK(count == 2)
|
||||
|
||||
input(false)
|
||||
|
||||
CHECK(count == 4)
|
||||
end
|
||||
end))
|
||||
|
||||
local ok = FINISH()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue