mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
This commit is contained in:
parent
bdd725659e
commit
d77fe0f92f
34 changed files with 1215 additions and 566 deletions
|
|
@ -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"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
66
docs/tut/control-flow/indexes.md
Normal file
66
docs/tut/control-flow/indexes.md
Normal 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.
|
||||||
66
docs/tut/control-flow/switch.md
Normal file
66
docs/tut/control-flow/switch.md
Normal 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.
|
||||||
66
docs/tut/control-flow/values.md
Normal file
66
docs/tut/control-flow/values.md
Normal 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.
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
|
||||||
61
docs/tut/reactive-scoping.md
Normal file
61
docs/tut/reactive-scoping.md
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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: () -> ()) -> ())
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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, () -> ()))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
29
src/trace.luau
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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/"
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ local function main()
|
||||||
until false
|
until false
|
||||||
end
|
end
|
||||||
|
|
||||||
main()
|
vide.root(main)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
416
test/tests.luau
416
test/tests.luau
|
|
@ -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
|
||||||
|
|
|
||||||
8
todo.md
8
todo.md
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue