This commit is contained in:
Aaron Smith 2023-09-14 18:15:54 +01:00
parent bdd725659e
commit d77fe0f92f
34 changed files with 1215 additions and 566 deletions

View file

@ -26,6 +26,7 @@ export default defineConfig({
items: [ items: [
{ text: "Reactivity: Core", link: "/api/reactivity-core" }, { text: "Reactivity: Core", link: "/api/reactivity-core" },
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" }, { text: "Reactivity: Utility", link: "/api/reactivity-utility" },
{ text: "Reactivity: Control Flow", link: "/api/reactivity-flow" },
{ text: "Element Creation", link: "/api/creation" }, { text: "Element Creation", link: "/api/creation" },
{ text: "Animation", link: "/api/animation" }, { text: "Animation", link: "/api/animation" },
{ text: "Strict Mode", link: "/api/strict-mode" }, { text: "Strict Mode", link: "/api/strict-mode" },
@ -41,16 +42,26 @@ export default defineConfig({
{ text: "Element Creation", link: "/tut/crash-course/2-creation" }, { text: "Element Creation", link: "/tut/crash-course/2-creation" },
{ text: "Components", link: "/tut/crash-course/3-components" }, { text: "Components", link: "/tut/crash-course/3-components" },
{ text: "Source", link: "/tut/crash-course/4-source" }, { text: "Source", link: "/tut/crash-course/4-source" },
{ text: "Derived Source", link: "/tut/crash-course/5-derived-source" }, { text: "Effect", link: "/tut/crash-course/5-effect" },
{ text: "Table Source", link: "/tut/crash-course/6-table-source" }, { text: "Derived Source", link: "/tut/crash-course/6-derived-source" },
{ text: "Nested Properties", link: "/tut/crash-course/7-nested-properties" }, { text: "Cleanup", link: "/tut/crash-course/7-cleanup" },
{ text: "Actions", link: "/tut/crash-course/8-actions" }, { text: "Control Flow", link: "/tut/crash-course/8-control-flow" },
{ text: "Property Nesting", link: "/tut/crash-course/9-property-nesting" },
{ text: "Actions", link: "/tut/crash-course/10-actions" },
] ]
}, },
{ {
text: "Tutorials", text: "Control Flow WIP",
items: [ items: [
{ text: "Crash Course", link: "/tut/crash-course/index" }, { text: "switch", link: "/tut/control-flow/switch.md" },
{ text: "indexes", link: "/tut/control-flow/indexes.md" },
{ text: "values", link: "/tut/control-flow/values.md" },
]
},
{
text: "Advanced Reactivity WIP",
items: [
{ text: "reactive-scopes", link: "/tut/reactive-scoping.md"}
] ]
} }
], ],

View file

@ -2,6 +2,38 @@
<br/> <br/>
## mount()
Runs a function and applies its result to a target instance.
- **Type**
```lua
function mount<T>(component: () -> T, target: Instance?): () -> ()
```
- **Details**
The result of the function is applies to the target in the same way
properties are using `create()`.
The function is ran in a new reactive scope, just like
[root()](reactivity-core.md#root).
Returns a function that when called will destroy the reactive scope.
- **Example**
```lua
local function App()
return create "ScreenGui" {
create "TextLabel" { Text = "Vide" }
}
end
mount(App, game.StarterGui)
```
## create() ## create()
Creates a new UI element, applying any given properties. Creates a new UI element, applying any given properties.
@ -28,19 +60,18 @@ Creates a new UI element, applying any given properties.
- **Property setting rules** - **Property setting rules**
- If a table index is a string: - If a table index is a string:
- If its value is a table then it will attempt to perform aggregate
initialization.
- If its value is a function then it will either bind that property to - If its value is a function then it will either bind that property to
the function or connect it if the property type is a `RBXScriptSignal`. the function or connect it if the property type is a `RBXScriptSignal`.
- If the value is not a function then the property will be set to that - If the value is not a function then the property will be set to that
value. value.
- If a table index is a number: - If a table index is a number:
- If its value is an action then that action will be queued to run after
properties are set.
- If its value is a table then that table will be recursively - If its value is a table then that table will be recursively
- processed just like the outer table. processed just like the outer table.
- If its value is a function then it will parent and bind any instances - If its value is a function then it will bind the instances children to
returned by that function as children. that function.
- If its value is an instance then it will be parented to the instance. - If its value is an instance then it will be parented to the instance.
- **Example** - **Example**
@ -54,7 +85,7 @@ Creates a new UI element, applying any given properties.
} }
``` ```
A component using property nesting/grouping. A component using property nesting.
```lua ```lua
type Layout = { type Layout = {

View file

@ -22,6 +22,10 @@ Creates and runs a function in a new reactive scope.
Also returns a function to destroy the root, which will run any cleanups Also returns a function to destroy the root, which will run any cleanups
and allow derived sources created to garbage collect. and allow derived sources created to garbage collect.
::: warning
`fn()` cannot yield.
:::
## source() ## source()
Creates a new source with the given value. Creates a new source with the given value.
@ -52,42 +56,37 @@ Creates a new source with the given value.
## effect() ## effect()
Runs a callback on source update. Runs a side-effect on source update.
- **Type** - **Type**
```lua ```lua
function effect(source: () -> ()): Uneffect function effect(callback: () -> ())
type Uneffect = () -> ()
``` ```
- **Details** - **Details**
The source callback is ran immediately to determine what states are The callback is ran immediately.
referenced.
Any time a source referenced in the callback is changed, the callback will Any time a source referenced in the callback is changed, the callback will
be reran. be reran.
Also returns a function that when called, stops the effecter immediately.
::: warning ::: warning
`source()` cannot yield. `callback()` cannot yield.
::: :::
- **Example** - **Example**
```lua ```lua
local state = source(1) local num = source(1)
effect(function() effect(function()
print(state()) print(num())
end) end)
-- prints 1 -- prints 1
state(state() + 1) num(num() + 1)
-- prints 2 -- prints 2
``` ```
@ -130,150 +129,4 @@ Derives a new source from existing sources.
text() -- "count: 1" text() -- "count: 1"
``` ```
## indexes()
Maps each index in a table source to an object.
- **Type**
```lua
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each index in the
source table. The first argument is a source containing the index's value
and the second argument is just the index.
Anytime a new index is added, the transform function will be called again
for that new index.
Anytime an existing index value changes, the transform function is not rerun,
instead the source value for that index will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
- **Example**
The intended purpose of this function is to map each index in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
return item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
LayoutOrder = i
}
end)
```
## values()
Maps each value in a table source to an object.
- **Type**
```lua
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: VI, index: () -> KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each value in the
source table. The first argument is the index's value and
the second argument is a source containing the index.
Anytime a new value is added, the transform function will be called again
for that new value.
Anytime an existing value's index changes, the transform function is not
rerun, instead the source index for that value will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
::: warning
Having primitive values in the source table can cause unexpected behavior,
as duplicate primitives can result in multiple index sources being bound
to the same UI element.
:::
- **Example**
The intended purpose of this function is to map each value in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(items, function(item, i)
return ItemDisplay {
Name = item.Name
Image = "rbxassetid://" .. item.icon,
LayoutOrder = i
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- 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.
e.g.
- List of character or weapon stats.
In most cases, both functions will appear to have the same behavior.
The main difference is performance, picking the right function to use can
result in less property updates and less re-renders.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View file

@ -0,0 +1,186 @@
# Reactivity API: Control Flow
<br/>
## switch()
Changes object based on a source and a mapping table.
- **Type**
```lua
function switch<K, V>(source: () -> K): (map: Map<K, () -> V>) -> V?
```
- **Details**
The mapped function is ran in a new reactive scope that is destroyed when
the source changes and maps to a different function.
::: warning
Mapped functions cannot yield.
:::
- **Example**
```lua
local logged = source(false)
local button = switch(logged) {
[true] = function()
return Button { Text = "Log out", Toggle = logged }
end,
[false] = function()
return Button { Text = "Log in", Toggle = logged }
end
}
```
## indexes()
Maps each index in a table source to an object.
- **Type**
```lua
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each index in the
source table. The first argument is a source containing the index's value
and the second argument is just the index.
Anytime a new index is added, the transform function will be called again
for that new index.
Anytime an existing index value changes, the transform function is not rerun,
instead the source value for that index will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
- **Example**
The intended purpose of this function is to map each index in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
return item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
LayoutOrder = i
}
end)
```
## values()
Maps each value in a table source to an object.
- **Type**
```lua
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: VI, index: () -> KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each value in the
source table. The first argument is the index's value and
the second argument is a source containing the index.
Anytime a new value is added, the transform function will be called again
for that new value.
Anytime an existing value's index changes, the transform function is not
rerun, instead the source index for that value will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
::: warning
Having primitive values in the source table can cause unexpected behavior,
as duplicate primitives can result in multiple index sources being bound
to the same UI element.
:::
- **Example**
The intended purpose of this function is to map each value in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(items, function(item, i)
return ItemDisplay {
Name = item.Name
Image = "rbxassetid://" .. item.icon,
LayoutOrder = i
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- 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.
e.g.
- List of character or weapon stats.
In most cases, both functions will appear to have the same behavior.
The main difference is performance, picking the right function to use can
result in less property updates and less re-renders.
--------------------------------------------------------------------------------

View file

@ -69,3 +69,5 @@ Runs a given function where any sources read will not track its reactive scope.
a(1) a(1)
print(sum()) -- 2 print(sum()) -- 2
``` ```
--------------------------------------------------------------------------------

View file

@ -12,14 +12,14 @@ and identifying improper usage.
Currently, strict mode will: Currently, strict mode will:
1. Run derived sources twice a source updates. 1. Run derived sources twice a source updates.
2. Run effecters twice when a source updates. 2. Run effects twice when a source updates.
3. Throw an error if yields occur where they are not allowed. 3. Throw an error if yields occur where they are not allowed.
4. Checks for `indexes()` and `values()` returning primitive values. 4. Checks for `indexes()` and `values()` returning primitive values.
5. Checks for duplicate nested properties at same depth. 5. Checks for duplicate nested properties at same depth.
6. Better error reporting and stack traces. 6. Better error reporting and stack traces.
7. Checks for multiple `cleanup()` calls in the same function scope. 7. Checks for multiple `cleanup()` calls in the same function scope.
By rerunning sources and effecters, any side-effects are made more apparent. By rerunning sources and effects, any side-effects are made more apparent.
This also helps ensure that cleanups are being handled correctly. This also helps ensure that cleanups are being handled correctly.
Accidental yielding within reactive scopes can break Vide's reactive graph, Accidental yielding within reactive scopes can break Vide's reactive graph,

View file

@ -0,0 +1,66 @@
# Control Flow
Vide has specific functions for dealing with sources that store a table value.
Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
UI element, Vide provides functions `indexes()` and `values()` to do this for
you.
`indexes()` maps each *index* in a table to a UI element.
```lua
local names = source { "a", "b", "c" }
local elements = indexes(names, function(name, i)
return create "TextLabel" {
Text = function()
return "Name: " .. name()
end,
LayoutOrder = i
}
end)
```
What happens here is the given callback is only ever ran *once* for each index
in the table. The callback receives two arguments, a *source* containing the
index's value and then the index itself.
Anytime the value at a corresponding index changes, the source for that index
value is updated, causing the UI element depending on it to update too.
`values()` behaves similarly, except it maps each *value* in a table to a UI
element.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
local elements = values(items, function(item, i)
return create "ImageLabel" {
Image = "rbxassetid://" .. item.Icon,
LayoutOrder = i
}
end)
```
The callback is again only ever ran *once* for each value in the table. The
callback receives two arguments, a value in the table and then a *source*
containing the value's corresponding index.
Any time a value in a table changes index, the source for that value is updated,
causing the UI element position to change.
In certain cases `values()` can cause less recalculation and rerenders than
`indexes()` like when items are re-arranged and shifted within a table.
It is important that each value in a table is unique when using `values()`,
and for this reason always using `indexes()` if a table contains primitive
values.
Both `indexes()` and `values()` return an array of all mapped UI elements.

View file

@ -0,0 +1,66 @@
# Control Flow
Vide has specific functions for dealing with sources that store a table value.
Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
UI element, Vide provides functions `indexes()` and `values()` to do this for
you.
`indexes()` maps each *index* in a table to a UI element.
```lua
local names = source { "a", "b", "c" }
local elements = indexes(names, function(name, i)
return create "TextLabel" {
Text = function()
return "Name: " .. name()
end,
LayoutOrder = i
}
end)
```
What happens here is the given callback is only ever ran *once* for each index
in the table. The callback receives two arguments, a *source* containing the
index's value and then the index itself.
Anytime the value at a corresponding index changes, the source for that index
value is updated, causing the UI element depending on it to update too.
`values()` behaves similarly, except it maps each *value* in a table to a UI
element.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
local elements = values(items, function(item, i)
return create "ImageLabel" {
Image = "rbxassetid://" .. item.Icon,
LayoutOrder = i
}
end)
```
The callback is again only ever ran *once* for each value in the table. The
callback receives two arguments, a value in the table and then a *source*
containing the value's corresponding index.
Any time a value in a table changes index, the source for that value is updated,
causing the UI element position to change.
In certain cases `values()` can cause less recalculation and rerenders than
`indexes()` like when items are re-arranged and shifted within a table.
It is important that each value in a table is unique when using `values()`,
and for this reason always using `indexes()` if a table contains primitive
values.
Both `indexes()` and `values()` return an array of all mapped UI elements.

View file

@ -0,0 +1,66 @@
# Control Flow
Vide has specific functions for dealing with sources that store a table value.
Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
UI element, Vide provides functions `indexes()` and `values()` to do this for
you.
`indexes()` maps each *index* in a table to a UI element.
```lua
local names = source { "a", "b", "c" }
local elements = indexes(names, function(name, i)
return create "TextLabel" {
Text = function()
return "Name: " .. name()
end,
LayoutOrder = i
}
end)
```
What happens here is the given callback is only ever ran *once* for each index
in the table. The callback receives two arguments, a *source* containing the
index's value and then the index itself.
Anytime the value at a corresponding index changes, the source for that index
value is updated, causing the UI element depending on it to update too.
`values()` behaves similarly, except it maps each *value* in a table to a UI
element.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
local elements = values(items, function(item, i)
return create "ImageLabel" {
Image = "rbxassetid://" .. item.Icon,
LayoutOrder = i
}
end)
```
The callback is again only ever ran *once* for each value in the table. The
callback receives two arguments, a value in the table and then a *source*
containing the value's corresponding index.
Any time a value in a table changes index, the source for that value is updated,
causing the UI element position to change.
In certain cases `values()` can cause less recalculation and rerenders than
`indexes()` like when items are re-arranged and shifted within a table.
It is important that each value in a table is unique when using `values()`,
and for this reason always using `indexes()` if a table contains primitive
values.
Both `indexes()` and `values()` return an array of all mapped UI elements.

View file

@ -19,11 +19,11 @@ Some of the main focuses behind Vide's design choices:
- Reducing the amount of imports needed for usage by leveraging Luau's syntax - Reducing the amount of imports needed for usage by leveraging Luau's syntax
and semantics. and semantics.
- Being completely typecheckable. - Being completely typecheckable.
- Flexibility, particularly with integrating other libraries and allowing users - Flexibility with integrating other libraries and allowing users to use their
to use their own patterns. own patterns.
- Independence from instance lifetimes. - Independence from instance lifetimes.
- A powerful reactive system that can surgically update properties as a result - A powerful reactive system that can update specific properties as a result of
of state changes. state changes, updates are immediate with no diffing needed.
## Structure Of A Vide App ## Structure Of A Vide App

View file

@ -40,3 +40,6 @@ the updating of the Text property anytime the count is changed.
All observable changes to the user are considered to be side-effects of the All observable changes to the user are considered to be side-effects of the
reactive system. reactive system.
You should not update other sources using an effect. Improper usage can lead to
unecessary updates and infinite loops.

View file

@ -1,66 +1,95 @@
# Control Flow # Control Flow
Vide has specific functions for dealing with sources that store a table value. Eventually you will need a way to dynamically create and destroy UI elements
resulting from state changes. Vide provides functions to help you do this,
known as *control flow* functions.
These functions return a new source, which holds the instances to be displayed.
These sources can be assigned as children, meaning the displayed children
will update when the input source updates.
One of these functions is `switch()`, used to conditionally show one of a set of
components.
```lua
local vide = require(vide)
local source = vide.source
local switch = vide.switch
local function ToggleButton(p: {
Text: string,
Toggle: (boolean) -> boolean
})
return create "TextButton" {
Size = UDim2.fromOffset(300, 300),
Text = p.Text,
Activated = function()
p.Toggle(not p.Toggle())
end
}
end
local loggedIn = source(false)
local function LoginMenu()
return Frame {
switch(loggedIn) {
[true] = function()
return ToggleButton { Text = "Log out", Toggle = loggedIn }
end,
[false] = function()
return ToggleButton { Text = "Log in", Toggle = loggedIn }
end
}
}
end
mount(function() return create "ScreenGui" { LoginMenu {} } end, game.StarterGui)
```
Above is an example of using a switch to create a login menu. Each time
`loggedIn` toggles, the current button will be destroyed, and a new button
created, which the text to represent the current action, to log in or log out.
Another control flow function, `indexes()`, is used to create elements from an
input table.
Often, you will have a table of values that will be displayed in a similar Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding manner. Rather than manually looping over each value to generate a corresponding
UI element, Vide provides functions `indexes()` and `values()` to do this for UI element, `indexes()` can autmatically run a transform function for each
you. index and value, generating a UI element.
`indexes()` maps each *index* in a table to a UI element.
```lua ```lua
local names = source { "a", "b", "c" } local todoList = {
"Finish the crash course",
"Star vide's GitHub"
}
local elements = indexes(names, function(name, i) local elements = indexes(todoList, function(todo, i)
return create "TextLabel" { return create "TextLabel" {
Text = function() Text = function()
return "Name: " .. name() return i .. ": " .. todo()
end, end,
LayoutOrder = i LayoutOrder = i
} }
end) end)
```
What happens here is the given callback is only ever ran *once* for each index mount(function()
in the table. The callback receives two arguments, a *source* containing the return create "ScreenGui" {
index's value and then the index itself. create "UIListLayout" {}, elements
Anytime the value at a corresponding index changes, the source for that index
value is updated, causing the UI element depending on it to update too.
`values()` behaves similarly, except it maps each *value* in a table to a UI
element.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
local elements = values(items, function(item, i)
return create "ImageLabel" {
Image = "rbxassetid://" .. item.Icon,
LayoutOrder = i
} }
end) end, game.StarterGui)
``` ```
The callback is again only ever ran *once* for each value in the table. The For each unique index in the passed table, the transform function will be called
callback receives two arguments, a value in the table and then a *source* with 1. a source containing the value of the index, 2. the index itself.
containing the value's corresponding index.
Any time a value in a table changes index, the source for that value is updated, When the value at an index is changed, the function is not reran. Instead, the
causing the UI element position to change. given source is updated instead.
In certain cases `values()` can cause less recalculation and rerenders than `indexes()` is said to map each *index* in a table to a UI element, each index
`indexes()` like when items are re-arranged and shifted within a table. has a single corresponding element.
It is important that each value in a table is unique when using `values()`, An element is only destroyed if the value of an index is set to `nil`.
and for this reason always using `indexes()` if a table contains primitive
values.
Both `indexes()` and `values()` return an array of all mapped UI elements.

View file

@ -0,0 +1,61 @@
# Reactive Scoping
This is a brief document designed to give the user more insight into how Vide's
reactive graph works.
Each time you create and derive sources, a new node representing that source is
created and added to the reactive graph. Each node stores a value and a
side-effect function. Each node also keeps track of its parents and children,
as well as any cleanups registered.
Any time a node is updated, Vide will traverse and update that node's children,
its children's children, etc, until all nodes descending from that node has been
updated. Traversal will stop at a node if that node's cached value does not
change after an update.
For every node that is updated, a scope is opened for that node. These scopes
are referred to as "reactive scopes". Any source read from within a node's scope
will that node as a child. This is similar to cleanups, anytime a cleanup is
registered, it is added to the node of the currently active scope.
The way Vide tracks reactive scopes, is by using a stack of nodes. The current
active reactive scope is the node at the top of this stack.
When destroying a node, its descendents are traversed and also destroyed.
When being destroyed, a node's connections (parents and children) are cleared,
and any pending cleanup functions are ran.
The purpose of `root()` (which is called internally by `mount()`) is to setup
the root node which will track any node created or derived inside its scope, or
any cleanups registered. Without it, nodes could be garbage collected without a
chance to run pending cleanups which can cause memory leakage.
Control flow functions in Vide are special, as they can dynamically create and
destroy new root scopes.
It is the combination of the above which allows us to write components like so:
```lua
local function Counter()
local count = source(0)
local connection = stepped:Connect(function() count(count() + 1) end)
cleanup(function() connection:Disconnect() end)
effect(function() print(count()) end)
return create "TextLabel" { Text = count }
end
```
Vide doesn't recognise this as a "component", that is a user abstraction. Vide
just sees this as a function that creates nodes in the reactive graph.
Whenever the reactive scope that calls this function is destroyed, like by a
control flow function, the registered cleanup will be called, and the effect
(which is just a node on the reactive graph) is destroyed. The returned instance
and the bound `count` source is just considered to be a side-effect, and with
the reactive scope from which the side-effects stem from destroyed, the instance
can be garbage collected - everything is nicely cleaned up.
> todo: add graphics

View file

@ -3,7 +3,7 @@ type Action = {
callback: (Instance) -> () callback: (Instance) -> ()
} }
local ActionMT = {} local ActionMT = table.freeze {}
local function is_action(v: any) local function is_action(v: any)
return getmetatable(v) == ActionMT return getmetatable(v) == ActionMT

View file

@ -1,6 +1,7 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw) local throw = require(script.Parent.throw)
local trace = require(script.Parent.trace)
local flags = require(script.Parent.flags) local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T> type Node<T> = graph.Node<T>
@ -9,34 +10,11 @@ local get_scope = graph.get_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
-- todo: replace with throw's method
local root do
local src = debug.info(1, "s")
root = string.sub(src, 1, #src - 5)
end
local function traceback(skips: number) -- ensures trace begins outside of any vide library file
local s = 1
repeat
s += 1
local path = debug.info(s, "s")
local found = not string.find(path, root)
if found then
skips -= 1
end
until found and skips < 0
return debug.traceback(nil, s)
end
function create_binding<T>(updater: (T) -> T, binding: T) function create_binding<T>(updater: (T) -> T, binding: T)
if flags.strict then if flags.strict then
-- track bind creation trace -- track bind creation trace
local fn = updater local fn = updater
local bind_trace = traceback(0) local bind_trace = debug.traceback(nil, trace()-1)
updater = function(...) updater = function(...)
local ok, result = xpcall(fn, function(err: string) local ok, result = xpcall(fn, function(err: string)
return err return err

View file

@ -5,12 +5,13 @@ local graph = require(script.Parent.graph)
local get_scope = graph.get_scope local get_scope = graph.get_scope
local add_cleanup = graph.add_cleanup local add_cleanup = graph.add_cleanup
local function cleanup(fn: () -> ()) local function cleanup(callback: () -> ())
local scope = get_scope() local scope = get_scope()
if not scope then throw("cannot cleanup in a non-reactive scope") end if not scope then
assert(scope) throw("cannot cleanup in a non-reactive scope")
end; assert(scope)
add_cleanup(scope, fn) add_cleanup(scope, callback)
end end
return cleanup return cleanup

View file

@ -7,11 +7,11 @@ local defaults = require(script.Parent.defaults)
local apply = require(script.Parent.apply) local apply = require(script.Parent.apply)
local memoize = require(script.Parent.memoize) local memoize = require(script.Parent.memoize)
local function create_instance(class_name: string) local function create_instance(class: string)
local ok, instance: Instance = pcall(Instance.new, class_name :: any) local ok, instance: Instance = pcall(Instance.new, class :: any)
if not ok then throw(`invalid class name, could not create instance of class { class_name }`) end if not ok then throw(`invalid class name, could not create instance of class { class }`) end
local default: { [string]: unknown }? = defaults[class_name] local default: { [string]: unknown }? = defaults[class]
if default then if default then
for i, v in next, default do for i, v in next, default do
(instance :: any)[i] = v (instance :: any)[i] = v

View file

@ -1,10 +1,11 @@
local Enum = game and Enum or require "test/mock".Enum :: never local Enum = game and Enum or require "test/mock".Enum :: never
local Color3 = game and Color3 or require "test/mock".Color3 :: never local Color3 = game and Color3 or require "test/mock".Color3 :: never
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
return { return {
Part = { Part = {
Material = Enum.Material.SmoothPlastic, Material = Enum.Material.SmoothPlastic,
--Size = Vector3.new(1, 1, 1), Size = Vector3.new(1, 1, 1),
Anchored = true Anchored = true
}, },

View file

@ -8,13 +8,13 @@ local track = graph.track
local get_scope = graph.get_scope local get_scope = graph.get_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local function derive<T>(fn: () -> T): () -> T local function derive<T>(source: () -> T): () -> T
local owner = get_scope() local owner = get_scope()
if not owner then if not owner then
throw("cannot derive in non-reactive scope") throw("cannot derive in non-reactive scope")
end; assert(owner) end; assert(owner)
local node = create_node(false :: any, fn) local node = create_node(false :: any, source)
set_owner(node, owner) set_owner(node, owner)
evaluate_node(node) evaluate_node(node)

View file

@ -7,16 +7,16 @@ local get_scope = graph.get_scope
local evaluate_node = graph.evaluate_node local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner local set_owner = graph.set_owner
local function effect<T>(effect: (T) -> T, initial_value: T) local function effect<T>(callback: (T) -> T, initial_value: T)
local owner = get_scope() local owner = get_scope()
if not owner then if not owner then
throw("cannot create effect in non-reactive scope") throw("cannot create effect in non-reactive scope")
end; assert(owner) end; assert(owner)
local node = create_node(initial_value, effect) local node = create_node(initial_value, callback)
set_owner(node, owner) set_owner(node, owner)
evaluate_node(node) evaluate_node(node)
end end
return effect :: (<T>(effect: (T) -> T, initial_value: T) -> ()) & ((effect: () -> ()) -> ()) return effect :: (<T>(callback: (T) -> T, initial_value: T) -> ()) & ((callback: () -> ()) -> ())

View file

@ -16,10 +16,11 @@ export type Node<T> = {
[number]: Node<T> [number]: Node<T>
} }
-- reactive scope stack
local scopes = { n = 0 } :: { [number]: Node<any>, n: number } local scopes = { n = 0 } :: { [number]: Node<any>, n: number }
-- runs a given callback in a context that Luau does not allow yielding in -- runs a given callback in a context that Luau does not allow yielding in
local check_for_yield: <T...>(fn: (T...) -> (boolean, string?), T...) -> () do local check_for_yield: <T...>(fn: (T...) -> (), T...) -> (boolean, string?) do
local t = { __mode = "kv" } local t = { __mode = "kv" }
setmetatable(t, t) setmetatable(t, t)
@ -40,10 +41,6 @@ local check_for_yield: <T...>(fn: (T...) -> (boolean, string?), T...) -> () do
end end
end end
local function get_stack_scope(offset: number): Node<unknown>?
return scopes[scopes.n - offset]
end
local function get_scope(): Node<unknown>? local function get_scope(): Node<unknown>?
return scopes[scopes.n] return scopes[scopes.n]
end end
@ -120,8 +117,18 @@ end
local function evaluate_node<T>(node: Node<T>) local function evaluate_node<T>(node: Node<T>)
local cur_value = node.cache local cur_value = node.cache
run_cleanups(node) -- todo: move in scope? if flags.strict then
run_cleanups(node)
open_scope(node)
local ok, err = check_for_yield(node.effect :: (T) -> T, cur_value)
close_scope()
if not ok then throw(err :: string) end
end
run_cleanups(node) -- todo: move in scope?
open_scope(node) open_scope(node)
local ok, new_value = pcall(node.effect :: (T) -> T, cur_value) local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
@ -150,6 +157,7 @@ local function update<T>(node: StartNode<T>)
table.clear(update_queue) table.clear(update_queue)
end end
-- unparent all children and queue for eval
do do
local child = node[1] local child = node[1]
while child do -- todo: case where child in owner context while child do -- todo: case where child in owner context
@ -162,6 +170,7 @@ local function update<T>(node: StartNode<T>)
end end
end end
-- evaluate all queued children
for i = n0 + 1, n do for i = n0 + 1, n do
local child = update_queue[i] local child = update_queue[i]
if not child.effect then continue end if not child.effect then continue end
@ -178,36 +187,33 @@ end
local function track<T>(node: StartNode<T>) local function track<T>(node: StartNode<T>)
local scope = get_scope() local scope = get_scope()
if scope and scope.effect then -- todo if scope and scope.effect then -- do not track nodes with no effect
add_child(node, scope) add_child(node, scope)
end end
end end
local function create_node<T>(value: T, effect: false | (T) -> T): Node<T> local function create_node<T>(value: T, effect: false | (T) -> T): Node<T>
local node: Node<T> = { return {
cache = value, cache = value,
effect = effect, effect = effect,
cleanups = false :: false, cleanups = false,
parents = {}, parents = {},
} }
return node
end
local function get_children<T>(node: Node<T>): { Node<unknown> }
return { unpack(node) } :: { Node<any> }
end end
local function create_start_node<T>(value: T): StartNode<T> local function create_start_node<T>(value: T): StartNode<T>
return { cache = value } return { cache = value }
end end
local function get_children<T>(node: Node<T>): { Node<unknown> }
return { unpack(node) } :: { Node<any> }
end
return table.freeze { return table.freeze {
open_scope = open_scope, open_scope = open_scope,
close_scope = close_scope, close_scope = close_scope,
evaluate_node = evaluate_node, evaluate_node = evaluate_node,
get_scope = get_scope, get_scope = get_scope,
get_stack_scope = get_stack_scope,
add_cleanup = add_cleanup, add_cleanup = add_cleanup,
set_owner = set_owner, set_owner = set_owner,
destroy = destroy, destroy = destroy,
@ -217,5 +223,6 @@ return table.freeze {
add_child = add_child, add_child = add_child,
create_node = create_node, create_node = create_node,
create_start_node = create_start_node, create_start_node = create_start_node,
get_children = get_children get_children = get_children,
scopes = scopes
} }

View file

@ -1,7 +1,5 @@
if not game then script = require "test/relative-string" end if not game then script = require "test/relative-string" end
-- todo: more testing needed regarding `cleanup()` usage
local throw = require(script.Parent.throw) local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags) local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph) local graph = require(script.Parent.graph)
@ -71,7 +69,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
local cv = input_cache[i] local cv = input_cache[i]
if cv ~= v then if cv ~= v then
if cv == nil then if cv == nil then -- create new scope and run transform
local scope = create_node(false, false) local scope = create_node(false, false)
scopes[i] = scope :: Node<any> scopes[i] = scope :: Node<any>
@ -87,20 +85,22 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
close_scope() close_scope()
if not ok then error(result, 0) end if not ok then
close_scope() -- subowner scope
error(result, 0)
end
input_nodes[i] = node input_nodes[i] = node
input_cache[i] = v
output_cache[i] = result output_cache[i] = result
else else -- update source
input_nodes[i].cache = v input_nodes[i].cache = v
update(input_nodes[i]) update(input_nodes[i])
input_cache[i] = v
end end
input_cache[i] = v
end end
end end
-- todo: handle early error
close_scope() close_scope()
local output_array = table.create(#scopes) local output_array = table.create(#scopes)
@ -118,7 +118,6 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
evaluate_node(node) evaluate_node(node)
return function() return function()
track(node) track(node)
return node.cache return node.cache
@ -161,7 +160,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
local cv = cur_input_cache[v] local cv = cur_input_cache[v]
if cv == nil then if cv == nil then -- create new scope and run transform
local scope = create_node(false, false) local scope = create_node(false, false)
scopes[v] = scope :: Node<any> scopes[v] = scope :: Node<any>
@ -177,20 +176,23 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
close_scope() close_scope()
if not ok then error(result, 0) end if not ok then
close_scope() -- subowner scope
error(result, 0)
end
input_nodes[v] = node input_nodes[v] = node
output_cache[v] = result output_cache[v] = result
else else -- update source
if cv ~= i then if cv ~= i then
input_nodes[v].cache = i input_nodes[v].cache = i
update(input_nodes[v]) update(input_nodes[v])
end end
cur_input_cache[v] = nil cur_input_cache[v] = nil
end end
end end
-- todo: handle early error
close_scope() close_scope()
-- remove old values -- remove old values

View file

@ -31,4 +31,4 @@ local function root<T>(fn: () -> T): (T, () -> ())
end end
end end
return root return root :: (<T>(fn: () -> T) -> (T, () -> ())) & ((fn: () -> ()) -> (nil, () -> ()))

View file

@ -9,8 +9,6 @@ local update = graph.update
export type Source<T> = (() -> T) & ((T) -> T) export type Source<T> = (() -> T) & ((T) -> T)
local function source<T>(initial_value: T): Source<T> local function source<T>(initial_value: T): Source<T>
--assert(get_scope())
local node = create_start_node(initial_value) local node = create_start_node(initial_value)
return function(...): T return function(...): T

View file

@ -28,8 +28,7 @@ type StartNode<T> = graph.StartNode<T>
local create_node = graph.create_node local create_node = graph.create_node
local create_start_node = graph.create_start_node local create_start_node = graph.create_start_node
local get_scope = graph.get_scope local get_scope = graph.get_scope
local open_scope = graph.open_scope local evaluate_node = graph.evaluate_node
local close_scope = graph.close_scope
local update = graph.update local update = graph.update
local set_owner = graph.set_owner local set_owner = graph.set_owner
local track = graph.track local track = graph.track
@ -43,6 +42,8 @@ local function Vec3(x: number?, y: number?, z: number?)
return Vector3.new(x, y, z) return Vector3.new(x, y, z)
end end
local ZERO = Vec3(0, 0, 0)
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3 type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
type SpringData<T> = { type SpringData<T> = {
@ -67,21 +68,20 @@ type Vec6ToType<T> = (Vec3, Vec3) -> T
local type_to_vec6 = { local type_to_vec6 = {
number = function(v) number = function(v)
return Vec3(v, 0, 0), Vec3() return Vec3(v, 0, 0), ZERO
end :: TypeToVec6<number>, end :: TypeToVec6<number>,
CFrame = function(v) CFrame = function(v)
-- todo: proper rotation tween
return v.Position, Vec3(v:ToEulerAnglesXYZ()) return v.Position, Vec3(v:ToEulerAnglesXYZ())
end :: TypeToVec6<CFrame>, end :: TypeToVec6<CFrame>,
Color3 = function(v) Color3 = function(v)
-- todo: hsv -- todo: hsv, oklab?
return Vec3(v.R, v.G, v.B), Vec3() return Vec3(v.R, v.G, v.B), ZERO
end :: TypeToVec6<Color3>, end :: TypeToVec6<Color3>,
UDim = function(v) UDim = function(v)
return Vec3(v.Scale, v.Offset, 0), Vec3() return Vec3(v.Scale, v.Offset, 0), ZERO
end :: TypeToVec6<UDim>, end :: TypeToVec6<UDim>,
UDim2 = function(v) UDim2 = function(v)
@ -89,11 +89,11 @@ local type_to_vec6 = {
end :: TypeToVec6<UDim2>, end :: TypeToVec6<UDim2>,
Vector2 = function(v) Vector2 = function(v)
return Vec3(v.X, v.Y, 0), Vec3() return Vec3(v.X, v.Y, 0), ZERO
end :: TypeToVec6<Vector2>, end :: TypeToVec6<Vector2>,
Vector3 = function(v) Vector3 = function(v)
return v, Vec3() return v, ZERO
end :: TypeToVec6<Vector3>, end :: TypeToVec6<Vector3>,
Rect = function(v) Rect = function(v)
@ -151,25 +151,11 @@ setmetatable(springs, { __mode = "v" })
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
local owner = get_scope() local owner = get_scope()
if not owner then throw("cannot derive in non-reactive scope") end if not owner then
assert(owner) throw("cannot derive in non-reactive scope")
end; assert(owner)
local updater = create_node(false)
updater.effect = true :: any -- todo
set_owner(updater, owner)
open_scope(updater)
local initial_value = source()
close_scope()
local vtype = typeof(initial_value)
local x1_123, x1_456 = type_to_vec6[vtype](initial_value)
-- https://en.wikipedia.org/wiki/Damping -- https://en.wikipedia.org/wiki/Damping
-- todo: calculate damped freq at 10tau instead of natural freq
local w_n = 2*math.pi / (period or 1) local w_n = 2*math.pi / (period or 1)
local z = damping_ratio or 1 local z = damping_ratio or 1
@ -182,27 +168,38 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
k = k, k = k,
c = c, c = c,
x0_123 = x1_123, x0_123 = ZERO,
x1_123 = x1_123, x1_123 = ZERO,
v_123 = Vec3(), v_123 = ZERO,
x0_456 = x1_456, x0_456 = ZERO,
x1_456 = x1_456, x1_456 = ZERO,
v_456 = Vec3(), v_456 = ZERO,
source_value = initial_value, source_value = false :: any,
} }
local output = create_start_node(initial_value) local output = create_start_node(false :: any)
updater.effect = function() local function updater_effect()
local v = source() local value = source()
data.x1_123, data.x1_456 = type_to_vec6[typeof(v)](v) data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
data.source_value = v data.source_value = value
springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs
return false return value
end end
local updater = create_node(false :: any, updater_effect)
set_owner(updater, owner)
evaluate_node(updater)
-- set initial position to goal
data.x0_123, data.x0_456 = data.x1_123, data.x1_456
-- set output to goal
output.cache = data.source_value
return function() return function()
track(output) track(output)
return output.cache return output.cache

View file

@ -22,22 +22,27 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
throw("cannot switch in non-reactive scope") throw("cannot switch in non-reactive scope")
end; assert(owner) end; assert(owner)
local scope: Node<false>? local last_scope: Node<false>?
local last_component: (() -> U)? local last_component: (() -> U)?
local function update(): U? local function update(cached): U?
local component = map[source()] local component = map[source()]
if component == last_component then return nil end if component == last_component then return cached end
last_component = component last_component = component
if scope then if last_scope then
destroy(scope :: Node<any>) destroy(last_scope :: Node<any>)
last_scope = nil
end end
if component == nil then return nil end if component == nil then return nil end
if type(component) ~= "function" then
throw("map must map a value to a function")
end
local new_scope = create_node(false, false) local new_scope = create_node(false, false)
scope = new_scope :: Node<any> last_scope = new_scope :: Node<any>
set_owner(new_scope, owner) set_owner(new_scope, owner)
open_scope(new_scope) open_scope(new_scope)
@ -51,7 +56,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
return result return result
end end
local node = create_node(nil, update :: () -> any) local node = create_node(nil :: any, update)
set_owner(node, owner) set_owner(node, owner)
evaluate_node(node) evaluate_node(node)

View file

@ -1,32 +1,9 @@
-- returns path to file as an array with each directory if not game then script = require "test/relative-string" end
-- 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 ".") local trace = require(script.Parent.trace)
end
-- get directory of vide root local function throw(msg): any
local root do error(msg, trace()-1)
local path = get_path(debug.info(1, "s"))
root = path[#path - 1]
end
-- throws an error, ensuring stack trace begins at the first callsite outside
-- of all vide library files
local function throw(msg: string)
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
error(msg, stack)
end end
return throw return throw

29
src/trace.luau Normal file
View file

@ -0,0 +1,29 @@
-- 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

View file

@ -7,17 +7,21 @@ local get_scope = graph.get_scope
local function untrack<T>(source: () -> T): T local function untrack<T>(source: () -> T): T
local scope = get_scope() local scope = get_scope()
if not scope then throw("cannot untrack in non-reactive scope") end if not scope then
assert(scope) throw("cannot untrack in non-reactive scope")
end; assert(scope)
-- sources are only tracked if the node in scope has an effect
local effect = scope.effect local effect = scope.effect
scope.effect = false scope.effect = false
local v = source() local ok, result = pcall(source)
scope.effect = effect :: () -> () scope.effect = effect :: () -> ()
return v if not ok then error(result, 0) end
return result
end end
return untrack return untrack

View file

@ -1,14 +1,19 @@
local BENCH, START = require("test/testkit").benchmark() local BENCH, START = require("test/testkit").benchmark()
local vide = require "src/init" local vide = require "src/init"
local root = vide.root
local source = vide.source
local derive = vide.derive
local indexes = vide.indexes
local values = vide.values
local cleanup = vide.cleanup
local create = vide.create
local N = 2^18 -- 262144 local N = 2^18 -- 262144
-- todo: wide and deep graph benchmarks -- todo: wide and deep graph benchmarks
BENCH("create source", function() BENCH("create source", function()
local source = vide.source
local cache = table.create(N) local cache = table.create(N)
for i = 1, START(N) do for i = 1, START(N) do
@ -17,7 +22,7 @@ BENCH("create source", function()
end) end)
BENCH("get value", function() BENCH("get value", function()
local src = vide.source(1) local src = source(1)
for i = 1, START(N) do for i = 1, START(N) do
src() src()
@ -25,7 +30,7 @@ BENCH("get value", function()
end) end)
BENCH("set value", function() BENCH("set value", function()
local src = vide.source(1) local src = source(1)
for i = 1, START(N) do for i = 1, START(N) do
src(i) src(i)
@ -33,12 +38,10 @@ BENCH("set value", function()
end) end)
BENCH("derive 1 source", function() BENCH("derive 1 source", function()
local derive = vide.derive
local cache = table.create(N) local cache = table.create(N)
local src = vide.source(1) local src = source(1)
vide.root(function() root(function()
for i = 1, START(N) do for i = 1, START(N) do
cache[i] = derive(function() cache[i] = derive(function()
return src() return src()
@ -49,8 +52,6 @@ BENCH("derive 1 source", function()
end) end)
BENCH("derive 4 sources", function() BENCH("derive 4 sources", function()
local derive = vide.derive
local cache = table.create(N) local cache = table.create(N)
local src = vide.source(1) local src = vide.source(1)
local src2 = vide.source(2) local src2 = vide.source(2)
@ -68,11 +69,11 @@ BENCH("derive 4 sources", function()
end) end)
end) end)
BENCH("set derived value", function() BENCH("update 1->1 graph", function()
local src = vide.source(1) local src = source(1)
vide.root(function() root(function()
local _derived = vide.derive(function() return src() end) local _derived = derive(function() return src() end)
for i = 1, START(N) do for i = 1, START(N) do
src(i) src(i)
@ -82,9 +83,104 @@ BENCH("set derived value", function()
end) end)
end) end)
BENCH("update 1->1 graph with cleanup", function()
local src = source(1)
root(function()
derive(function()
cleanup(function() end)
return src()
end)
for i = 1, START(N) do
src(i)
end
return nil
end)
end)
BENCH("update 1->1000 graph", function()
local src = source(-1)
root(function()
for i = 1, 1000 do
derive(function() return src() end)
end
src(0)
for i = 1, START(10) do
src(i)
end
end)
end)
BENCH("update 1->1->1->1...1000 graph", function()
local src = source(-1)
root(function()
local last = src
for i = 1, 1000 do
local l = last
last = derive(function() return l() end)
end
src(0)
for i = 1, START(10) do
src(i)
end
end)
end)
-- todo: repeat with batching
BENCH("update 1000->1 graph", function()
local srcs = {}
for i = 1, 1000 do
srcs[i] = source(0)
end
root(function()
derive(function()
for i = 1, 1000 do
srcs[i]()
end
return false
end)
for i = 1, START(1) do
for idx = 1, 1000 do
srcs[idx](i)
end
end
end)
end)
-- todo: optimize, repeat with batching
BENCH("update 1000x 1->1 common extern. graph", function()
local ext = source(-1)
root(function()
local srcs = {}
for i = 1, 1000 do
srcs[i] = source(0)
derive(function() return srcs[i]() + ext() end)
end
ext(0)
for i = 1, START(10) do
for idx = 1, 1000 do
srcs[idx](i)
end
end
end)
end)
BENCH("apply 0 properties", function() BENCH("apply 0 properties", function()
local apply = require "src/apply" local apply = require "src/apply"
local instance = vide.create("Frame") {} local instance = create("Frame") {}
for i = 1, START(N) do for i = 1, START(N) do
apply(instance, {}) apply(instance, {})
@ -93,7 +189,7 @@ end)
BENCH("apply 8 properties", function() BENCH("apply 8 properties", function()
local apply = require "src/apply" local apply = require "src/apply"
local instance = vide.create("Frame") {} local instance = create("Frame") {}
for i = 1, START(N) do for i = 1, START(N) do
apply(instance, { apply(instance, {
@ -112,10 +208,10 @@ end)
BENCH("bind property", function() BENCH("bind property", function()
local apply = require "src/apply" local apply = require "src/apply"
local instance = vide.create("Frame") {} local instance = create("Frame") {}
local src = vide.source(1) local src = source(1)
vide.root(function() root(function()
for i = 1, START(N) do for i = 1, START(N) do
apply(instance, { apply(instance, {
Text = src Text = src
@ -129,10 +225,10 @@ end)
BENCH("update binding", function() BENCH("update binding", function()
local apply = require "src/apply" local apply = require "src/apply"
local instance = vide.create("Frame") {} local instance = create("Frame") {}
local src = vide.source(1) local src = source(1)
vide.root(function() root(function()
apply(instance, { apply(instance, {
Text = src Text = src
}) })
@ -154,12 +250,12 @@ BENCH("indexes() all new", function()
data[i] = i data[i] = i
end end
local src = vide.source(data) local src = source(data)
vide.root(function() root(function()
START(N) START(N)
local _list = vide.indexes(src, function(v, i) local _list = indexes(src, function(v, i)
return {} return {}
end) end)
@ -174,10 +270,10 @@ BENCH("indexes() no change", function()
data[i] = i data[i] = i
end end
local src = vide.source(data) local src = source(data)
vide.root(function() root(function()
local _list = vide.indexes(src, function(v, i) local _list = indexes(src, function(v, i)
return {} return {}
end) end)
@ -196,11 +292,11 @@ BENCH("indexes() all change", function()
data[i] = i data[i] = i
end end
local src = vide.source(data) local src = source(data)
vide.root(function() root(function()
local _list = vide.indexes(src, function(v, i) local _list = indexes(src, function(v, i)
return {} return {}
end) end)
@ -225,10 +321,10 @@ BENCH("indexes() all remove", function()
data[i] = i data[i] = i
end end
local src = vide.source(data) local src = source(data)
vide.root(function() root(function()
local _list = vide.indexes(src, function(v, i) local _list = indexes(src, function(v, i)
return {} return {}
end) end)
@ -249,12 +345,12 @@ BENCH("values() all new", function()
data[i] = {} data[i] = {}
end end
local src = vide.source(data) local src = source(data)
vide.root(function() root(function()
START(N) START(N)
local _list = vide.values(src, function(v, i) local _list = values(src, function(v, i)
return {} return {}
end) end)
@ -269,10 +365,10 @@ BENCH("values() no change", function()
data[i] = {} data[i] = {}
end end
local src = vide.source(data) local src = source(data)
vide.root(function() root(function()
local _list = vide.values(src, function(v, i) local _list = values(src, function(v, i)
return {} return {}
end) end)
@ -293,10 +389,10 @@ BENCH("values() all change", function()
data[i] = {} data[i] = {}
end end
local src = vide.source(data) local src = source(data)
vide.root(function() root(function()
local _list = vide.values(src, function(v, i) local _list = values(src, function(v, i)
return {} return {}
end) end)
@ -322,10 +418,10 @@ BENCH("values() all remove", function()
data[i] = {} data[i] = {}
end end
local src = vide.source(data) local src = source(data)
vide.root(function() root(function()
local _list = vide.values(src, function(v, i) local _list = values(src, function(v, i)
return {} return {}
end) end)
@ -342,9 +438,9 @@ end)
N *= 1024 N *= 1024
BENCH("register new cleanup", function() BENCH("register new cleanup", function()
local cleanup = vide.cleanup local cleanup = cleanup
vide.root(function() root(function()
local cleaner = function() end local cleaner = function() end
local callers = {} local callers = {}
@ -368,7 +464,6 @@ do
-- the purpose of the two following benchmarks is to measure the overhead of -- the purpose of the two following benchmarks is to measure the overhead of
-- aggregate construction -- aggregate construction
BENCH("set explicit mock vector2", function() BENCH("set explicit mock vector2", function()
local create = vide.create
local apply = require "src/apply" local apply = require "src/apply"
local Vector2 = require "test/mock".Vector2 local Vector2 = require "test/mock".Vector2
@ -384,7 +479,6 @@ do
end) end)
BENCH("set aggregate mock vector2", function() BENCH("set aggregate mock vector2", function()
local create = vide.create
local apply = require "src/apply" local apply = require "src/apply"
local Vector2 = require "test/mock".Vector2 local Vector2 = require "test/mock".Vector2
@ -400,4 +494,47 @@ do
end) end)
end end
-- innacurate due to no Vector3 in vanilla Luau
-- mock vector is 200x slower than native vector
-- BENCH("spring update", function()
-- local root, source, spring = vide.root, vide.source, vide.spring
-- local src = source(0)
-- root(function()
-- for i = 1, N do
-- spring(src)
-- end
-- START(N)
-- src(1)
-- return nil
-- end)
-- end)
-- N /= 1024
-- BENCH("spring step", function()
-- local root, source, spring = vide.root, vide.source, vide.spring
-- local src = source(0)
-- root(function()
-- for i = 1, N do
-- spring(src)
-- end
-- src(1)
-- START(N)
-- vide.step(1/60)
-- return nil
-- end)
-- end)
return nil return nil

View file

@ -1,5 +1,6 @@
local function dir(directory: string) local function dir(directory: string)
return setmetatable({} :: { [string]: any }, { __index = function(_, path) return directory .. path end }) return setmetatable({} :: { [string]: any },
{ __index = function(_, path) return directory .. path end })
end end
local script = dir "src/" local script = dir "src/"

View file

@ -65,7 +65,7 @@ local function main()
until false until false
end end
main() vide.root(main)

View file

@ -33,7 +33,6 @@ local NIL = nil :: any
TEST("graph", function() TEST("graph", function()
local create_node = graph.create_node local create_node = graph.create_node
local create_start_node = graph.create_start_node
local track = graph.track local track = graph.track
local update = graph.update local update = graph.update
local add_child = graph.add_child local add_child = graph.add_child
@ -45,13 +44,11 @@ TEST("graph", function()
local destroy = graph.destroy local destroy = graph.destroy
local function node<T>(v: T?) local function node<T>(v: T?)
local n = create_node(v or false) return create_node(v or false, function(x) return not x end)
n.effect = function() end
return n
end end
local function scope() local function scope()
return create_node(false) return create_node(false, false)
end end
local function cleanup(fn: () -> ()) local function cleanup(fn: () -> ())
@ -82,17 +79,18 @@ TEST("graph", function()
local count = 0 local count = 0
local function effect() local function effect(x)
track(a) track(a)
track(b) track(b)
count += 1 count += 1
return not x
end end
c.effect = effect c.effect = effect
open_scope(c) open_scope(c)
effect() effect(c.cache)
close_scope() close_scope()
@ -107,9 +105,9 @@ TEST("graph", function()
local a, b, c, d = node(), node(), node(), node() local a, b, c, d = node(), node(), node(), node()
local b_cnt, c_cnt, d_cnt = 0, 0, 0 local b_cnt, c_cnt, d_cnt = 0, 0, 0
function b.effect() b_cnt += 1 end function b.effect(x) b_cnt += 1; return not x end
function c.effect() c_cnt += 1 end function c.effect(x) c_cnt += 1; return not x end
function d.effect() d_cnt += 1 end function d.effect(x) d_cnt += 1; return not x end
open_scope(b); track(a); close_scope() open_scope(b); track(a); close_scope()
open_scope(c); track(a); close_scope() open_scope(c); track(a); close_scope()
@ -125,9 +123,10 @@ TEST("graph", function()
do CASE "duplicate child on rerun" do CASE "duplicate child on rerun"
local a, b, c = node(), node(), node() local a, b, c = node(), node(), node()
function c.effect() function c.effect(x)
track(a) track(a)
track(b) track(b)
return not x
end end
open_scope(c); assert(c.effect)(NIL); close_scope() open_scope(c); assert(c.effect)(NIL); close_scope()
@ -165,7 +164,6 @@ TEST("graph", function()
do open_scope(root) do open_scope(root)
clean "root" clean "root"
items_updated = node() items_updated = node()
items_updated.effect = function() end
track(items_updated) -- should not track(items_updated) -- should not
add_child(root, items_updated) add_child(root, items_updated)
@ -177,7 +175,6 @@ TEST("graph", function()
do open_scope(scope1) do open_scope(scope1)
clean "scope1" clean "scope1"
bind1 = node() bind1 = node()
bind1.effect = function() end
add_child(scope1, bind1) add_child(scope1, bind1)
do open_scope(bind1) do open_scope(bind1)
@ -190,7 +187,6 @@ TEST("graph", function()
do open_scope(scope2) do open_scope(scope2)
clean "scope2" clean "scope2"
bind2 = node() bind2 = node()
bind2.effect = function() end
add_child(scope2, bind2) add_child(scope2, bind2)
do open_scope(bind2) do open_scope(bind2)
clean "bind2" clean "bind2"
@ -252,13 +248,47 @@ TEST("graph", function()
end end
do CASE "nodes garbage collection" do CASE "nodes garbage collection"
local wref = weak { create_node(1) } local wref = weak { node(1) }
destroy(wref[1]) destroy(wref[1])
gc() gc()
CHECK(not wref[1]) CHECK(not wref[1])
end end
end) end)
TEST("mount()", function()
local mount = vide.mount
local create = vide.create
local source = vide.source
local cleanup = vide.cleanup
local screen = create "ScreenGui" {}
local text = source "foo"
local count = 0
local unmount = mount(function()
cleanup(function()
count += 1
end)
return create "TextLabel" {
Name = "TextLabel",
Text = text
}
end, screen)
local label = screen:FindFirstChild "TextLabel" :: TextLabel
CHECK(label)
CHECK(label.Text == "foo")
text "bar"
CHECK(label.Text == "bar")
CHECK(count == 0)
unmount()
CHECK(count == 1)
end)
TEST("source()", wrap_root(function() TEST("source()", wrap_root(function()
local source = vide.source local source = vide.source
local effect = vide.effect local effect = vide.effect
@ -422,7 +452,7 @@ TEST("derive()", wrap_root(function()
local _, destroy = root(function() local _, destroy = root(function()
local b = derive(function() local _b = derive(function()
cleanup(function() cleanup(function()
count += 1 count += 1
end) end)
@ -534,13 +564,13 @@ TEST("cleanup()", wrap_root(function()
end end
do CASE "cleanup on rerun" do CASE "cleanup on rerun"
local state = source(1) local src = source(1)
local effected = 0 local effected = 0
local cleaned = 0 local cleaned = 0
effect(function() effect(function()
state() src()
effected += 1 effected += 1
cleanup(function() cleanup(function()
cleaned += 1 cleaned += 1
@ -550,27 +580,27 @@ TEST("cleanup()", wrap_root(function()
CHECK(effected == 1) CHECK(effected == 1)
CHECK(cleaned == 0) CHECK(cleaned == 0)
state(2) src(2)
CHECK(effected == 2) CHECK(effected == 2)
CHECK(cleaned == 1) CHECK(cleaned == 1)
end end
do CASE "multiple cleanup" do CASE "multiple cleanup"
local state = source(1) local src = source(1)
local queue = {} local queue = {}
effect(function() effect(function()
state() src()
cleanup(function() table.insert(queue, 1) end) cleanup(function() table.insert(queue, 1) end)
cleanup(function() table.insert(queue, 2) end) cleanup(function() table.insert(queue, 2) end)
end) end)
CHECK(testkit.seq(queue, {})) CHECK(testkit.seq(queue, {}))
state(2) src(2)
CHECK(testkit.seq(queue, { 1, 2 })) CHECK(testkit.seq(queue, { 1, 2 }))
state(3) src(3)
CHECK(testkit.seq(queue, { 1, 2, 1, 2 })) CHECK(testkit.seq(queue, { 1, 2, 1, 2 }))
end end
end)) end))
@ -660,7 +690,6 @@ TEST("create()", wrap_root(function()
CHECK(frame:FindFirstChild "C") CHECK(frame:FindFirstChild "C")
CHECK(frame:FindFirstChild "D") CHECK(frame:FindFirstChild "D")
CHECK(frame:FindFirstChild "E") CHECK(frame:FindFirstChild "E")
CHECK(frame:FindFirstChild "F") CHECK(frame:FindFirstChild "F")
CHECK(frame:FindFirstChild "G") CHECK(frame:FindFirstChild "G")
end end
@ -706,7 +735,7 @@ TEST("create()", wrap_root(function()
CHECK(count == 1) CHECK(count == 1)
end end
do CASE "bind same state to multiple instance properties" do CASE "bind same source to multiple instance properties"
local src = source "1" local src = source "1"
local text = create "TextBox" { local text = create "TextBox" {
@ -807,11 +836,9 @@ TEST("create()", wrap_root(function()
end)) end))
TEST("switch()", wrap_root(function() TEST("switch()", wrap_root(function()
local create = vide.create
local source = vide.source local source = vide.source
local switch = vide.switch local switch = vide.switch
local effect = vide.effect local effect = vide.effect
local derive = vide.derive
local cleanup = vide.cleanup local cleanup = vide.cleanup
do CASE "update on source change" do CASE "update on source change"
@ -828,15 +855,42 @@ TEST("switch()", wrap_root(function()
CHECK(count == 1) CHECK(count == 1)
CHECK(output() == 1) CHECK(output() == 1)
input(false) input(false)
CHECK(output() == 0) CHECK(output() == 0)
CHECK(count == 2) CHECK(count == 2)
input(false) input(false)
CHECK(output() == 0)
CHECK(count == 2) CHECK(count == 2)
input(nil)
input(NIL)
CHECK(output() == nil) CHECK(output() == nil)
end end
do CASE "same component different map"
local input = source(0)
local function component()
return {}
end
local output = switch(input) {
[1] = component,
[2] = component
}
CHECK(output() == nil)
input(1)
local instance = output()
CHECK(instance)
input(2)
CHECK(output() == instance)
end
do CASE "scoped switch" do CASE "scoped switch"
local input = source(true) local input = source(true)
@ -865,11 +919,30 @@ TEST("switch()", wrap_root(function()
input(true) input(true)
CHECK(switch1_count == 1) CHECK(switch1_count == 1)
CHECK(switch0_count == 1) CHECK(switch0_count == 1)
input(nil) input(NIL)
CHECK(switch1_count == 2) CHECK(switch1_count == 2)
CHECK(switch0_count == 1) CHECK(switch0_count == 1)
CHECK(owner_count == 0) CHECK(owner_count == 0)
end end
do CASE "reactive stack resets after error"
local scopes = require "src/graph".scopes
local input = source(1)
local n0 = scopes.n
local ok = pcall(function()
switch(input) {
error :: any
}
end)
CHECK(not ok)
local n1 = scopes.n
CHECK(n0 == n1)
end
end)) end))
TEST("indexes()", wrap_root(function() TEST("indexes()", wrap_root(function()
@ -893,10 +966,10 @@ TEST("indexes()", wrap_root(function()
do CASE "cache result" do CASE "cache result"
local input = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local runcount = table.create(3, 0) local count = table.create(3, 0)
local output = indexes(input, function(v, i) local output = indexes(input, function(v, i)
runcount[i] += 1 count[i] += 1
return v return v
end) end)
@ -906,9 +979,9 @@ TEST("indexes()", wrap_root(function()
CHECK(output()[2]() == 2) CHECK(output()[2]() == 2)
CHECK(output()[3]() == 4) CHECK(output()[3]() == 4)
CHECK(runcount[1] == 1) CHECK(count[1] == 1)
CHECK(runcount[2] == 1) CHECK(count[2] == 1)
CHECK(runcount[3] == 1) CHECK(count[3] == 1)
end end
do CASE "removal reflected" do CASE "removal reflected"
@ -989,6 +1062,27 @@ TEST("indexes()", wrap_root(function()
CHECK(count[2] == 0) CHECK(count[2] == 0)
CHECK(count[3] == 0) CHECK(count[3] == 0)
end end
do CASE "reactive stack resets after error"
local scopes = require "src/graph".scopes
local input = source { 1 }
local n0 = scopes.n
local ok = pcall(function()
indexes(input, function()
error("")
return NIL
end)
end)
CHECK(not ok)
local n1 = scopes.n
CHECK(n0 == n1)
end
end)) end))
TEST("values()", wrap_root(function() TEST("values()", wrap_root(function()
@ -1012,10 +1106,10 @@ TEST("values()", wrap_root(function()
do CASE "cache result" do CASE "cache result"
local input = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local runcount = table.create(3, 0) local count = table.create(3, 0)
local output = values(input, function(v, i) local output = values(input, function(v, i)
runcount[v] += 1 count[v] += 1
return i return i
end) end)
@ -1025,9 +1119,9 @@ TEST("values()", wrap_root(function()
CHECK(output()[2]() == 3) CHECK(output()[2]() == 3)
CHECK(output()[3]() == 2) CHECK(output()[3]() == 2)
CHECK(runcount[1] == 1) CHECK(count[1] == 1)
CHECK(runcount[2] == 1) CHECK(count[2] == 1)
CHECK(runcount[3] == 1) CHECK(count[3] == 1)
end end
do CASE "removal reflected" do CASE "removal reflected"
@ -1094,6 +1188,27 @@ TEST("values()", wrap_root(function()
CHECK(count[2] == 0) CHECK(count[2] == 0)
CHECK(count[3] == 0) CHECK(count[3] == 0)
end end
do CASE "reactive stack resets after error"
local scopes = require "src/graph".scopes
local input = source { 1 }
local n0 = scopes.n
local ok = pcall(function()
values(input, function()
error("")
return NIL
end)
end)
CHECK(not ok)
local n1 = scopes.n
CHECK(n0 == n1)
end
end)) end))
TEST("spring()", wrap_root(function() TEST("spring()", wrap_root(function()
@ -1226,7 +1341,7 @@ TEST("untrack()", wrap_root(function()
CHECK(a() == untrack(a)) CHECK(a() == untrack(a))
end end
do CASE "derived state" do CASE "derived source"
local a = source(0) local a = source(0)
local b = source(0) local b = source(0)
local c = source(0) local c = source(0)
@ -1368,127 +1483,156 @@ TEST("actions", function()
end end
end) end)
-- TEST("strict", function() TEST("changed()", wrap_root(function()
-- vide.strict = true local root = vide.root
local create = vide.create
local source = vide.source
local changed = vide.changed
-- local create = vide.create do CASE "outputs"
-- local source = vide.source local output = source(nil)
-- local derive = vide.derive
-- local effect = vide.effect
-- local indexes, values = vide.indexes, vide.values
-- local cleanup = vide.cleanup
-- -- do CASE "error on derived callback yield" local text = create "TextLabel" {
-- -- local state = source(1) Text = "a",
changed("Text", output)
}
-- -- local ok = pcall(function() --CHECK(output() == "a")
-- -- local _derived = derive(function() text.Text = "b"
-- -- coroutine.yield() CHECK(output() == "b")
-- -- return state() end
-- -- end)
-- -- end)
-- -- CHECK(not ok) do CASE "connection disconnected"
-- -- end local text, destroy = root(function()
local output = source(nil)
-- -- do CASE "error on effecter callback yield" return create "TextLabel" {
-- -- local state = source(1) Text = "a",
changed("Text", output)
}
end)
-- -- local ok = pcall(function() destroy() -- changed() should of disconnect connection
-- -- local _derived = effect(function()
-- -- coroutine.yield()
-- -- state()
-- -- end)
-- -- end)
-- -- CHECK(not ok) -- check if instance can gc
-- -- end local wref = weak { text }
text = NIL
gc()
CHECK(not wref[1])
end
end))
-- do CASE "run derived callback twice" TEST("strict", wrap_root(function()
-- local state = source(1) vide.strict = true
-- local runcount = 0
-- local _ = derive(function() local create = vide.create
-- runcount += 1 local source = vide.source
-- return state() local derive = vide.derive
-- end) local effect = vide.effect
local indexes, values = vide.indexes, vide.values
-- CHECK(runcount == 2) do CASE "error on derived callback yield"
-- state(2) local src = source(1)
-- CHECK(runcount == 4)
-- end
-- do CASE "run effecter callback twice" local ok = pcall(function()
-- local state = source(1) local _derived = derive(function()
-- local runcount = 0 coroutine.yield()
return src()
end)
end)
-- effect(function() CHECK(not ok)
-- runcount += 1 end
-- state()
-- end)
-- CHECK(runcount == 2) do CASE "error on effecter callback yield"
-- state(2) local src = source(1)
-- CHECK(runcount == 4)
-- end
-- do CASE "indexes() error if primitive" local ok = pcall(function()
-- local state = source { 1 } effect(function()
coroutine.yield()
src()
end)
end)
-- local ok = pcall(function() CHECK(not ok)
-- indexes(state, function() return 1 end) end
-- end)
-- CHECK(not ok) do CASE "run derived callback twice"
-- end local src = source(1)
local count = 0
-- do CASE "values() error if duplicate" local _ = derive(function()
-- local state = source { 1, 2, 1 } count += 1
return src()
end)
-- local ok = pcall(function() CHECK(count == 2)
-- values(state, function() return {} end) src(2)
-- end) CHECK(count == 4)
end
-- CHECK(not ok) do CASE "run effect callback twice"
-- end local src = source(1)
local count = 0
-- do CASE "duplicate properties" effect(function()
-- local ok = pcall(function() count += 1
-- create "TextLabel" { src()
-- { end)
-- Name = "foo"
-- },
-- {
-- Name = "bar"
-- }
-- }
-- end)
-- CHECK(not ok) CHECK(count == 2)
src(2)
CHECK(count == 4)
end
-- ok = pcall(function() do CASE "indexes() error if primitive"
-- create "TextLabel" { local src = source { 1 }
-- {
-- Name = "foo",
-- {
-- Name = "bar"
-- }
-- }
-- }
-- end)
-- CHECK(ok) local ok = pcall(function()
-- end indexes(src, function() return 1 end)
end)
-- do CASE "multiple cleanup per scope" CHECK(not ok)
-- local ok = pcall(function() end
-- cleanup(function() end)
-- cleanup(function() end)
-- end)
-- CHECK(not ok) do CASE "values() error if duplicate"
-- end local src = source { 1, 2, 1 }
-- end)
local ok = pcall(function()
values(src, function() return {} end)
end)
CHECK(not ok)
end
do CASE "duplicate properties"
local ok = pcall(function()
create "TextLabel" {
{
Name = "foo"
},
{
Name = "bar"
}
}
end)
CHECK(not ok)
ok = pcall(function()
create "TextLabel" {
{
Name = "foo",
{
Name = "bar"
}
}
}
end)
CHECK(ok)
end
end))
local ok = FINISH() local ok = FINISH()
if not ok then error("Tests failed", 0) end if not ok then error("Tests failed", 0) end

View file

@ -1,19 +1,13 @@
# todo # todo
- better error reporting and stack traces in strict mode
- auto-enable of strict mode depending on compiler optimizaton level - auto-enable of strict mode depending on compiler optimizaton level
- investigate if weak table iteration can be invalidated
- property binding optimization - property binding optimization
- would no longer allow `cleanup()` usage in binding scopes - would no longer allow `cleanup()` usage in binding scopes
- solution to nested reactivity, see: SolidJS stores - solution to nested reactivity, see: SolidJS stores
- SolidJS control flow components
- equality checking of derived sources
- investigate performance of wide graphs - investigate performance of wide graphs
- optimize child removal - optimize child removal
- implement from solid: - implement from solid:
- Show
- Switch
- Dynamic
- Portal - Portal
- batch - batch
- optimize `indexes()` double-diffing - optimize `indexes()` double-diffing
- define behavior of deriving a source within a derived source