From 4b877ce8ff3e8b09529c9b0d231009ef7afe2eb1 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Thu, 21 Sep 2023 10:29:14 +0100
Subject: [PATCH 001/116] Update docs
---
docs/api/reactivity-core.md | 31 +++++++------------
docs/api/reactivity-utility.md | 8 +++--
docs/tut/crash-course/5-effect.md | 2 +-
docs/tut/crash-course/6-stateful-component.md | 4 +--
4 files changed, 20 insertions(+), 25 deletions(-)
diff --git a/docs/api/reactivity-core.md b/docs/api/reactivity-core.md
index 0e4a5d0..133bc4a 100644
--- a/docs/api/reactivity-core.md
+++ b/docs/api/reactivity-core.md
@@ -2,6 +2,10 @@
+:::warning
+Yielding is not allowed in any reactive scope. Strict mode can check for this.
+:::
+
## root()
Creates and runs a function in a new reactive scope.
@@ -14,18 +18,14 @@ Creates and runs a function in a new reactive scope.
- **Details**
+ Returns the result of the given function.
+
Creates a new root reactive scope, where creation and derivations of sources
can be tracked and properly disposed of.
- Returns the result of the given function.
-
A function to destroy the root is passed into the callback, which will run
any cleanups and allow derived sources created to garbage collect.
- ::: warning
- `fn()` cannot yield.
- :::
-
## source()
Creates a new source with the given value.
@@ -44,6 +44,8 @@ Creates a new source with the given value.
Reading from the source from within a reactive scope will cause changes
to that source to be tracked and anything depending on it to update.
+ Sources can be created outside of reactive scopes.
+
- **Example**
```lua
@@ -56,7 +58,7 @@ Creates a new source with the given value.
## effect()
-Runs a side-effect on source update.
+Runs a side-effect in a new reactive scope on source update.
- **Type**
@@ -66,14 +68,10 @@ Runs a side-effect on source update.
- **Details**
- The callback is ran immediately.
-
Any time a source referenced in the callback is changed, the callback will
be reran.
- ::: warning
- `callback()` cannot yield.
- :::
+ The callback is ran to initially ran on first call to find dependent sources.
- **Example**
@@ -93,7 +91,7 @@ Runs a side-effect on source update.
## derive()
-Derives a new source from existing sources.
+Derives a new source in a new reactive scope from existing sources.
- **Type**
@@ -109,12 +107,7 @@ Derives a new source from existing sources.
Anytime its value is recalculated it is also cached, subsequent calls will
retun this cached value until it recalculates again.
- Takes a callback that is immediately run to determine what sources are being
- referenced.
-
- ::: warning
- `source()` cannot yield.
- :::
+ The callback is ran to initially ran on first call to find dependent sources.
- **Example**
diff --git a/docs/api/reactivity-utility.md b/docs/api/reactivity-utility.md
index 9219b29..d5292e5 100644
--- a/docs/api/reactivity-utility.md
+++ b/docs/api/reactivity-utility.md
@@ -2,7 +2,7 @@
## cleanup()
-Runs a callback anytime a reactive scope is re-ran.
+Runs a callback anytime a reactive scope is reran or destroyed.
- **Type**
@@ -26,7 +26,8 @@ Runs a callback anytime a reactive scope is re-ran.
## untrack()
-Runs a given function where any sources read will not track its reactive scope.
+Runs a given function where any sources read will not be tracked by a reactive
+scope.
- **Type**
@@ -58,7 +59,8 @@ Runs a given function where any sources read will not track its reactive scope.
## read()
-Utility used to read a value that is either a primitive or a source.
+Utility used to read a value that is either a primitive or a source. Sources
+read can still be tracked inside a reactive-scope.
- **Type**
diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md
index 3891652..c875de9 100644
--- a/docs/tut/crash-course/5-effect.md
+++ b/docs/tut/crash-course/5-effect.md
@@ -1,7 +1,7 @@
# Effect
Effects are functions that are ran in response to source updates. They are
-alled effects because they cause *side-effects* when reacting to source updates.
+called effects because they cause *side-effects* when reacting to source updates.
Effects are created using `effect()`.
diff --git a/docs/tut/crash-course/6-stateful-component.md b/docs/tut/crash-course/6-stateful-component.md
index ba167e2..ce9d8d6 100644
--- a/docs/tut/crash-course/6-stateful-component.md
+++ b/docs/tut/crash-course/6-stateful-component.md
@@ -23,7 +23,7 @@ local function Counter()
instance.Text = "count: " .. count()
end)
- return count
+ return instance
end
```
@@ -49,7 +49,7 @@ local function Counter(props: { count: () -> number })
instance.Text = "count: " .. count()
end)
- return count
+ return instance
end
local count = source(0)
From 0e8bfef9050656f2f6425a7fcd8339f98cefedf3 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Thu, 21 Sep 2023 10:40:05 +0100
Subject: [PATCH 002/116] Fix typos in creation docs
---
docs/api/creation.md | 35 +++++++++++++++++++----------------
1 file changed, 19 insertions(+), 16 deletions(-)
diff --git a/docs/api/creation.md b/docs/api/creation.md
index f5b4c1c..f3f51c8 100644
--- a/docs/api/creation.md
+++ b/docs/api/creation.md
@@ -4,7 +4,8 @@
## mount()
-Runs a function and applies its result to a target instance.
+Runs a function in a new reactive scope and optionally applies its result to a
+target instance.
- **Type**
@@ -14,7 +15,7 @@ Runs a function and applies its result to a target instance.
- **Details**
- The result of the function is applies to the target in the same way
+ The result of the function is applied to a target in the same way
properties are using `create()`.
The function is ran in a new reactive scope, just like
@@ -60,19 +61,16 @@ Creates a new UI element, applying any given properties.
- **Property setting rules**
- - If a table index is a string:
- - 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`.
- - If the value is not a function then the property will be set to that
- value.
- - 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
- processed just like the outer table.
- - If its value is a function then it will bind the instances children to
- that function.
- - If its value is an instance then it will be parented to the instance.
+ - **index is string:**
+ - **value is function:**
+ - **property is event:** connect function as callback
+ - **property is not event:** create effect to update property
+ - **value is not function:** set property to value
+ - **index is number:**
+ - **value is action:** run action
+ - **value is table:** recurse table
+ - **value is functon:** create effect to update children
+ - **value is instance:** set instance as child
- **Example**
@@ -138,9 +136,14 @@ instances.
```lua
local function changed(property: string, callback: (new) -> ())
return action(function(instance)
- instance:GetPropertyChangedSignal("property"):Connect(function()
+ local con - instance:GetPropertyChangedSignal(property):Connect(function()
callback(instance[property])
end)
+
+ -- disconnect on reactive scope destruction to allow gc of instance
+ cleanup(function()
+ con:Disconnect()
+ end)
end)
end
From 62716a074db2f4837aa9dbbc56ab5a957263d3cd Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Thu, 21 Sep 2023 14:58:44 +0100
Subject: [PATCH 003/116] Update benchmarks
---
test/benchmark.luau | 81 ++++++++++++++++++++++++++++-----------------
1 file changed, 50 insertions(+), 31 deletions(-)
diff --git a/test/benchmark.luau b/test/benchmark.luau
index ca9c436..f3c39b6 100644
--- a/test/benchmark.luau
+++ b/test/benchmark.luau
@@ -16,18 +16,19 @@ local function TITLE(name: string)
print(testkit.color.white(name))
end
-local N = 2^18 -- 262144
-
-local function WRAP_BENCH(name: string, fn: () -> ())
+local function ROOT_BENCH(name: string, fn: () -> ())
vide.root(function(destroy)
BENCH(name, fn)
return destroy
end)()
end
+local N = 2^18 -- 262144
+
+
TITLE "sources"
-WRAP_BENCH("create source", function()
+BENCH("create source", function()
local cache = table.create(N)
for i = 1, START(N) do
@@ -35,7 +36,7 @@ WRAP_BENCH("create source", function()
end
end)
-WRAP_BENCH("get value", function()
+BENCH("get value", function()
local src = source(1)
for i = 1, START(N) do
@@ -43,7 +44,7 @@ WRAP_BENCH("get value", function()
end
end)
-WRAP_BENCH("set value", function()
+BENCH("set value", function()
local src = source(1)
for i = 1, START(N) do
@@ -51,7 +52,7 @@ WRAP_BENCH("set value", function()
end
end)
-WRAP_BENCH("derive 1 source", function()
+ROOT_BENCH("derive 1 source", function()
local cache = table.create(N)
local src = source(1)
@@ -62,7 +63,7 @@ WRAP_BENCH("derive 1 source", function()
end
end)
-WRAP_BENCH("derive 4 sources", function()
+ROOT_BENCH("derive 4 sources", function()
local cache = table.create(N)
local src = vide.source(1)
local src2 = vide.source(2)
@@ -78,7 +79,7 @@ end)
TITLE "graphs"
-WRAP_BENCH("update 1->1 graph", function()
+ROOT_BENCH("update 1->1 graph", function()
local src = source(1)
local _derived = derive(function() return src() end)
@@ -88,7 +89,7 @@ WRAP_BENCH("update 1->1 graph", function()
end
end)
-WRAP_BENCH("update 1->1 graph with cleanup", function()
+ROOT_BENCH("update 1->1 graph with cleanup", function()
local src = source(1)
derive(function()
@@ -101,7 +102,7 @@ WRAP_BENCH("update 1->1 graph with cleanup", function()
end
end)
-WRAP_BENCH("update 1->1000 graph", function()
+ROOT_BENCH("update 1->1000 graph", function()
local src = source(-1)
for i = 1, 1000 do
@@ -115,7 +116,7 @@ WRAP_BENCH("update 1->1000 graph", function()
end
end)
-WRAP_BENCH("update 1->1->1->1...1000 graph", function()
+ROOT_BENCH("update 1->1->1->1...1000 graph", function()
local src = source(-1)
local last = src
@@ -132,7 +133,7 @@ WRAP_BENCH("update 1->1->1->1...1000 graph", function()
end)
-- todo: repeat with batching
-WRAP_BENCH("update 1000->1 graph", function()
+ROOT_BENCH("update 1000->1 graph", function()
local srcs = {}
for i = 1, 1000 do
srcs[i] = source(0)
@@ -153,7 +154,7 @@ WRAP_BENCH("update 1000->1 graph", function()
end)
-- todo: optimize, repeat with batching
-WRAP_BENCH("update 1000x 1->1 common extern. graph", function()
+ROOT_BENCH("update 1000x 1->1 common extern. graph", function()
local ext = source(-1)
local srcs = {}
@@ -173,7 +174,7 @@ end)
TITLE "property apply"
-WRAP_BENCH("apply 0 properties", function()
+ROOT_BENCH("apply 0 properties", function()
local apply = require "src/apply"
local instance = create("Frame") {}
@@ -182,7 +183,7 @@ WRAP_BENCH("apply 0 properties", function()
end
end)
-WRAP_BENCH("apply 8 properties", function()
+ROOT_BENCH("apply 8 properties", function()
local apply = require "src/apply"
local instance = create("Frame") {}
@@ -200,7 +201,7 @@ WRAP_BENCH("apply 8 properties", function()
end
end)
-WRAP_BENCH("bind property", function()
+ROOT_BENCH("bind property", function()
local apply = require "src/apply"
local instance = create("Frame") {}
@@ -215,7 +216,7 @@ WRAP_BENCH("bind property", function()
return nil
end)
-WRAP_BENCH("update binding", function()
+ROOT_BENCH("update binding", function()
local apply = require "src/apply"
local instance = create("Frame") {}
@@ -232,11 +233,29 @@ WRAP_BENCH("update binding", function()
return nil
end)
+TITLE "switch()"
+
+ROOT_BENCH("switch()", function()
+ local M = 2^8
+
+ local map = {}
+ for i = 1, M do
+ map[i] = function() return i end
+ end
+
+ local input = source(0)
+ vide.switch(input)(map)
+
+ for i = 1, START(N) do
+ input(bit32.band(i, M - 1) + 1) -- i % m + 1
+ end
+end)
+
TITLE "indexes()"
N /= 1024
-WRAP_BENCH("indexes() all new", function()
+ROOT_BENCH("indexes() all new", function()
local data = {}
for i = 1, N do
@@ -254,7 +273,7 @@ WRAP_BENCH("indexes() all new", function()
return nil
end)
-WRAP_BENCH("indexes() no change", function()
+ROOT_BENCH("indexes() no change", function()
local data = {}
for i = 1, N do
@@ -274,7 +293,7 @@ WRAP_BENCH("indexes() no change", function()
return nil
end)
-WRAP_BENCH("indexes() all change", function()
+ROOT_BENCH("indexes() all change", function()
local data = {}
for i = 1, N do
@@ -298,7 +317,7 @@ WRAP_BENCH("indexes() all change", function()
src(data)
end)
-WRAP_BENCH("indexes() all remove", function()
+ROOT_BENCH("indexes() all remove", function()
local data = {}
for i = 1, N do
@@ -322,7 +341,7 @@ end)
TITLE "values()"
-WRAP_BENCH("values() all new", function()
+ROOT_BENCH("values() all new", function()
local data = {}
for i = 1, N do
@@ -340,7 +359,7 @@ WRAP_BENCH("values() all new", function()
return nil
end)
-WRAP_BENCH("values() no change", function()
+ROOT_BENCH("values() no change", function()
local data = {}
for i = 1, N do
@@ -360,7 +379,7 @@ WRAP_BENCH("values() no change", function()
src(data)
end)
-WRAP_BENCH("values() all change", function()
+ROOT_BENCH("values() all change", function()
local data = {}
for i = 1, N do
@@ -385,7 +404,7 @@ WRAP_BENCH("values() all change", function()
src(data)
end)
-WRAP_BENCH("values() all remove", function()
+ROOT_BENCH("values() all remove", function()
local data = {}
for i = 1, N do
@@ -409,7 +428,7 @@ N *= 1024
TITLE "cleanup"
-WRAP_BENCH("register new cleanup", function()
+ROOT_BENCH("register new cleanup", function()
local cleanup = cleanup
local cleaner = function() end
@@ -433,7 +452,7 @@ TITLE "aggregate"
do
-- the purpose of the two following benchmarks is to measure the overhead of
-- aggregate construction
- WRAP_BENCH("set explicit mock vector2", function()
+ ROOT_BENCH("set explicit mock vector2", function()
local apply = require "src/apply"
local Vector2 = require "test/mock".Vector2
@@ -448,7 +467,7 @@ do
end
end)
- WRAP_BENCH("set aggregate mock vector2", function()
+ ROOT_BENCH("set aggregate mock vector2", function()
local apply = require "src/apply"
local Vector2 = require "test/mock".Vector2
@@ -467,7 +486,7 @@ end
-- innacurate due to no Vector3 in vanilla Luau
-- mock vector is 200x slower than native vector
--- WRAP_BENCH("spring update", function()
+-- ROOT_BENCH("spring update", function()
-- local root, source, spring = vide.root, vide.source, vide.spring
-- local src = source(0)
@@ -487,7 +506,7 @@ end
-- N /= 1024
--- WRAP_BENCH("spring step", function()
+-- ROOT_BENCH("spring step", function()
-- local root, source, spring = vide.root, vide.source, vide.spring
-- local src = source(0)
From 3c45defeac786131aa7333eb7982abf8d28101d7 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Thu, 21 Sep 2023 19:14:55 +0100
Subject: [PATCH 004/116] Fix typos
---
docs/api/reactivity-utility.md | 2 +-
docs/tut/crash-course/4-source.md | 2 +-
docs/tut/crash-course/6-stateful-component.md | 2 +-
docs/tut/crash-course/7-property-binding.md | 5 +++--
docs/tut/crash-course/8-cleanup.md | 2 +-
5 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/docs/api/reactivity-utility.md b/docs/api/reactivity-utility.md
index d5292e5..9c7980a 100644
--- a/docs/api/reactivity-utility.md
+++ b/docs/api/reactivity-utility.md
@@ -60,7 +60,7 @@ scope.
## read()
Utility used to read a value that is either a primitive or a source. Sources
-read can still be tracked inside a reactive-scope.
+read can still be tracked inside a reactive scope.
- **Type**
diff --git a/docs/tut/crash-course/4-source.md b/docs/tut/crash-course/4-source.md
index 1583c59..b7f7d04 100644
--- a/docs/tut/crash-course/4-source.md
+++ b/docs/tut/crash-course/4-source.md
@@ -1,4 +1,4 @@
-# Source
+# Sources
*Sources* in Vide are special objects that store a single value. They are the
core of reactivity in Vide. Each source represents a source of data, and they
diff --git a/docs/tut/crash-course/6-stateful-component.md b/docs/tut/crash-course/6-stateful-component.md
index ce9d8d6..3d16279 100644
--- a/docs/tut/crash-course/6-stateful-component.md
+++ b/docs/tut/crash-course/6-stateful-component.md
@@ -1,4 +1,4 @@
-# Stateful Component
+# Stateful Components
A stateful component is a component that stores and displays some data.
diff --git a/docs/tut/crash-course/7-property-binding.md b/docs/tut/crash-course/7-property-binding.md
index b3840cc..9488fba 100644
--- a/docs/tut/crash-course/7-property-binding.md
+++ b/docs/tut/crash-course/7-property-binding.md
@@ -3,7 +3,8 @@
Explicitly creating effects to update properties can become verbose when there
are a lot of properties to update. Vide provides a way to *implicitly* create
an effect to update properties on source update. This is also known as
-*property binding*, as a property is binded to reflect some data.
+*property binding*, since changes to a source will automatically update the
+property.
```lua
local create = vide.create
@@ -29,7 +30,7 @@ Instead of explicitly creating an effect, assigning a (non-event) property
a function will implicitly create a side-effect to update that property anytime
a dependent source is updated.
-Just like effects, the function is ran immediately in a reactive-scope to set
+Just like effects, the function is ran immediately in a reactive scope to set
the property initially and determine what sources are being depended on.
This allows you as the programmer to not need to manually update UI as the state
diff --git a/docs/tut/crash-course/8-cleanup.md b/docs/tut/crash-course/8-cleanup.md
index 3a04463..16d611d 100644
--- a/docs/tut/crash-course/8-cleanup.md
+++ b/docs/tut/crash-course/8-cleanup.md
@@ -43,6 +43,6 @@ covered next.
On a related note: the reason why `mount()` is used to create your app, is so
that any top-level components that need to be cleaned up, can be cleaned up
-when the app is later unmounted, since `mount()` runs in a reactive-scope to
+when the app is later unmounted, since `mount()` runs in a reactive scope to
track `cleanup()` calls. Vide's entire reactive system is independent from the
life-time of instances; instances are just a side-effect of the reactive system.
From e9742e88fdbd46a0e5c52afd8eeb77b2cb426137 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Thu, 21 Sep 2023 19:15:32 +0100
Subject: [PATCH 005/116] Add mermaid diagrams to docs
---
docs/.vitepress/config.ts | 5 +-
docs/package.json | 3 +-
docs/tut/advanced/reactive-scoping.md | 116 +++++++++++++++++++++++---
docs/tut/crash-course/5-effect.md | 25 +++++-
4 files changed, 131 insertions(+), 18 deletions(-)
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 18e36cc..dec9146 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -1,7 +1,8 @@
-import { defineConfig } from "vitepress"
+//import { defineConfig } from "vitepress"
+import { withMermaid } from "vitepress-plugin-mermaid";
// https://vitepress.dev/reference/site-config
-export default defineConfig({
+export default withMermaid({
title: "Vide",
titleTemplate: ":title - A reactive UI library for Luau",
description: "A reactive UI library for Luau.",
diff --git a/docs/package.json b/docs/package.json
index 921ee39..e156bea 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -8,6 +8,7 @@
},
"devDependencies": {
- "vitepress": "^1.0.0-rc.4"
+ "vitepress": "^1.0.0-rc.4",
+ "vitepress-plugin-mermaid": "^2.0.14"
}
}
diff --git a/docs/tut/advanced/reactive-scoping.md b/docs/tut/advanced/reactive-scoping.md
index 5b983ab..99a09f4 100644
--- a/docs/tut/advanced/reactive-scoping.md
+++ b/docs/tut/advanced/reactive-scoping.md
@@ -1,13 +1,54 @@
# Reactive Scoping
This is a brief document designed to give the user more insight into how Vide's
-reactive graph works.
+reactive system works.
+
+Vide's reactivity can be pictured as a graph, where each source, derived source,
+and effect is a node on that graph. For example:
+
+```lua
+
+root(function()
+ local forename = source "quan"
+ local surname = source "xi"
+
+ local name = derive(function()
+ return forename() .. " " .. surname()
+ end)
+
+ effect(function()
+ print("new name: " .. name())
+ end)
+end)
+```
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.
+This code will produce a graph that looks like so:
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#161618"
+ }
+}}%%
+
+flowchart
+ subgraph root
+ forename & surname --> name
+ name --> effect
+ end
+```
+
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
@@ -30,32 +71,85 @@ 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.
+Nodes created by `source()` can actually exist outside of root nodes, since
+they do not have direct side-effects or cleanups, they do not have to be
+explicitly destroyed.
+
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 function Counter(props: { text: string })
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 }
+ return create "TextLabel" {
+ Text = function()
+ return props.text() .. ": " .. count()
+ end
+ }
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.
+```lua
+root(function()
+ local counters = { "A", "B" }
-> todo: add graphics
+ indexes(counters, function(name)
+ return Counter { text = name }
+ end)
+end)
+```
+
+This code produces a graph like so:
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#161618"
+ }
+}}%%
+
+flowchart LR
+ subgraph root
+ counters --> indexes
+ end
+
+ subgraph root1[subroot 1]
+ n1[name] --> p1[prop binding]
+ end
+
+ subgraph root2[subroot 2]
+ n2[name] --> p2[prop binding]
+ end
+
+ indexes .-> root1 & root2
+```
+
+This shows how the `indexes()` control flow function creates and manages new
+root scopes. The function creates an effect seen as `indexes` in the graph,
+which manages the new roots `subroot 1` and `subroot 2`, as well as the sources
+`name` for which one exists for each index value in the input table.
+
+When the input table changes, `indexes()` can automatically destroy and create
+subroots based on the changed indexes. Destroyed nodes run any cleanups made, in
+this case it is the cleanups to disconnect the counters connection. The same
+applies to all other control flow functions.
+
+Whenever the root reactive scope is destroyed, all its children, `counters` and
+`indexes` will be destroyed too, which means that `indexes` children, the
+subroots, will also be destroyed. Everything is nicely cleaned up.
diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md
index c875de9..f75d21a 100644
--- a/docs/tut/crash-course/5-effect.md
+++ b/docs/tut/crash-course/5-effect.md
@@ -1,4 +1,4 @@
-# Effect
+# Effects
Effects are functions that are ran in response to source updates. They are
called effects because they cause *side-effects* when reacting to source updates.
@@ -20,9 +20,9 @@ count(1)
-- "count: 1" printed
```
-The callback given to `effect()` is ran in a *reactive-scope*. Any source read
+The callback given to `effect()` is ran in a *reactive scope*. Any source read
from inside a reactive scope will be tracked, so that if any of those sources
-update, the effect will be re-ran too.
+update, the effect will be reran too.
The callback is first ran immediately inside the `effect()` call to initially
figure out what sources are being used.
@@ -56,9 +56,26 @@ All observable changes to the user are considered to be side-effects of pure
computations.
Sources, derived sources, and effects form what is called a *reactive graph*.
-In the above example a graph `count -> doubled -> effect` is formed. Anywhere
+In the above example, the following graph is formed. Anywhere
an update occures, everything further down the graph is updated.
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#161618"
+ }
+}}%%
+
+flowchart LR
+ count --> doubled --> effect
+```
+
You should not update other sources using an effect. Improper usage can lead to
a cyclic loop in the graph, causing an infinite loop when it tries to update.
Sources should be derived instead.
From 15026b5ad361e3b3ff83caae3893186959175966 Mon Sep 17 00:00:00 2001
From: aaron <83140718+centau@users.noreply.github.com>
Date: Fri, 22 Sep 2023 16:57:14 +0100
Subject: [PATCH 006/116] Update todo
---
todo.md | 25 +++++++++++++++++++++++--
1 file changed, 23 insertions(+), 2 deletions(-)
diff --git a/todo.md b/todo.md
index 5b9fc47..a3f26a8 100644
--- a/todo.md
+++ b/todo.md
@@ -8,6 +8,27 @@
- Portal
- batch
- optimize `indexes()` double-diffing
-- define behavior of deriving a source within a derived source
-- define behavior of node destruction under a root that has a child in another root
- improve crash course, some sections feel like information dumps
+- reactive scopes
+ - define behavior of creating a reactive scope within a non-root reactive scope
+ - i.e. an effect within an effect
+ - error on attempt for now
+ - define behavior of destruction of a node with a child, where the node and
+ its child are in separate, non-nested root reactive scopes.
+ - should destruction of the parent also destroy the child?
+ - or should destruction of the parent silently disconnect the child, without
+ invoking the child's cleanups?
+
+ ```mermaid
+ graph LR
+
+ subgraph root1
+ parent
+ end
+
+ subgraph root2
+ child
+ end
+
+ parent --> child
+ ```
From c0c2166edf6849dbae775dc96a259555e50abf5e Mon Sep 17 00:00:00 2001
From: aaron <83140718+centau@users.noreply.github.com>
Date: Sun, 24 Sep 2023 01:49:16 +0100
Subject: [PATCH 007/116] Update testkit
---
test/testkit.luau | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/test/testkit.luau b/test/testkit.luau
index 544c7cc..31876eb 100644
--- a/test/testkit.luau
+++ b/test/testkit.luau
@@ -1,6 +1,6 @@
--------------------------------------------------------------------------------
-- testkit.luau
--- v0.7.1
+-- v0.7.2
--------------------------------------------------------------------------------
local color = {
@@ -323,7 +323,7 @@ end
local function print2(v: unknown)
type Buffer = { n: number, [number]: string }
- type Cyclic = { [{}]: true }
+ type Cyclic = { n: number, [{}]: number }
-- overkill concatenationless string buffer
local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
@@ -347,16 +347,19 @@ local function print2(v: unknown)
else -- is table
local tabbed_indent = indent .. TAB
- str.n += 1
-
if cyclic[value] then
- str[str.n] = color.gray "*cyclic reference*"
+ str.n += 1
+ str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
return
else
- cyclic[value] = true
+ cyclic.n += 1
+ cyclic[value] = cyclic.n
end
- str[str.n] = "{\n"
+ str.n += 3
+ str[str.n - 2] = "{ "
+ str[str.n - 1] = color.gray(tostring(cyclic[value]))
+ str[str.n - 0] = "\n"
local i, v = next(value, nil)
while v ~= nil do
@@ -393,7 +396,7 @@ local function print2(v: unknown)
end
local str = { n = 0 }
- local cyclic = {}
+ local cyclic = { n = 0 }
tos(v, 0, str, cyclic)
print(table.concat(str))
end
@@ -455,7 +458,7 @@ return {
return BENCH, START
end,
- print2 = print2,
+ print = print2,
seq = shallow_eq,
deq = deep_eq,
From 38342b38059021ac28021f830a58b85ac705dcff Mon Sep 17 00:00:00 2001
From: aaron <83140718+centau@users.noreply.github.com>
Date: Mon, 25 Sep 2023 11:44:24 +0100
Subject: [PATCH 008/116] Disallow creation of nested tracking scopes
---
src/graph.luau | 31 ++---
src/maps.luau | 10 +-
src/root.luau | 2 +-
src/switch.luau | 2 +-
src/untrack.luau | 2 +-
test/tests.luau | 318 +++++++++++++++++++++++++++++++++++------------
6 files changed, 256 insertions(+), 109 deletions(-)
diff --git a/src/graph.luau b/src/graph.luau
index f911f7b..de6f7e1 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -10,7 +10,7 @@ export type StartNode = {
export type Node = {
cache: T,
- effect: ((T) -> T) | false,
+ effect: ((T) -> T) | "owner" | "untracked",
cleanups: { () -> () } | false,
parents: { owner: StartNode?, [number]: StartNode },
[number]: Node
@@ -50,8 +50,8 @@ local function get_owning_scope(): Node
if not scope then
local caller_name = debug.info(2, "n")
return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
- elseif scope.effect then
- throw("owning scope is not stable; are you trying to derive a new source from within a side-effect?")
+ elseif scope.effect ~= "owner" then
+ throw("reactive scope is not an owning scope; new effects cannot be created in side-effects")
end
return scope
end
@@ -117,8 +117,6 @@ local function destroy(node: Node)
run_cleanups(node)
unparent(node)
- node.effect = false
-
if node.parents.owner then
remove_child(node.parents.owner, node)
node.parents.owner = nil
@@ -168,24 +166,13 @@ local function update_from(node: StartNode, n0: number)
-- unparent all children and queue for eval
do
- local i = 1
- local child = node[i]
+ local child = node[1]
while child do
+ --assert(child.parents.owner)
unparent(child)
-
n += 1
update_queue[n] = child
-
- local next_child = node[i]
-
- -- children who have this parent as an owner will not be unparented
- -- if such a child is encountered then skip it
- if next_child == child then
- i += 1
- next_child = node[i]
- end
-
- child = next_child
+ child = node[1]
end
end
@@ -194,7 +181,7 @@ local function update_from(node: StartNode, n0: number)
-- evaluate all queued children
for i = n0 + 1, n do
local child = update_queue[i]
- if not child.effect then continue end
+ assert(type(child.effect) == "function")
if evaluate_node(child) then
update_from(child, n)
@@ -212,12 +199,12 @@ end
local function track(node: StartNode)
local scope = get_scope()
- if scope and scope.effect then -- do not track nodes with no effect
+ if scope and type(scope.effect) == "function" then -- do not track nodes with no effect
add_child(node, scope)
end
end
-local function create_node(value: T, effect: false | (T) -> T): Node
+local function create_node(value: T, effect: "owner" | (T) -> T): Node
return {
cache = value,
effect = effect,
diff --git a/src/maps.luau b/src/maps.luau
index 1649039..d18357c 100644
--- a/src/maps.luau
+++ b/src/maps.luau
@@ -30,7 +30,7 @@ end
local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO): () -> { VO }
local owner = get_owning_scope()
- local subowner = create_node(false, false)
+ local subowner = create_node(false, "owner")
set_owner(subowner, owner)
local input_cache = {} :: Map
@@ -67,7 +67,7 @@ local function indexes(input: () -> Map, transform: (() -> VI,
if cv ~= v then
if cv == nil then -- create new scope and run transform
- local scope = create_node(false, false)
+ local scope = create_node(false, "owner")
scopes[i] = scope :: Node
local node = create_start_node(v)
@@ -112,6 +112,7 @@ local function indexes(input: () -> Map, transform: (() -> VI,
local node = create_node(false :: any, function()
return update_children(input())
end)
+ set_owner(node, owner)
evaluate_node(node)
@@ -124,7 +125,7 @@ end
local function values(input: () -> Map, transform: (VI, () -> K) -> VO): () -> { VO }
local owner = get_owning_scope()
- local subowner = create_node(false, false)
+ local subowner = create_node(false, "owner")
set_owner(subowner, owner)
local cur_input_cache_up = {} :: Map
@@ -155,7 +156,7 @@ local function values(input: () -> Map, transform: (VI, () ->
local cv = cur_input_cache[v]
if cv == nil then -- create new scope and run transform
- local scope = create_node(false, false)
+ local scope = create_node(false, "owner")
scopes[v] = scope :: Node
local node = create_start_node(i)
@@ -214,6 +215,7 @@ local function values(input: () -> Map, transform: (VI, () ->
local node = create_node(false :: any, function()
return update_children(input())
end)
+ set_owner(node, owner)
evaluate_node(node)
diff --git a/src/root.luau b/src/root.luau
index 50a1c1c..5f79bd7 100644
--- a/src/root.luau
+++ b/src/root.luau
@@ -11,7 +11,7 @@ local destroy = graph.destroy
local refs = {}
local function root(fn: (destroy: () -> ()) -> T...): T...
- local node = create_node(false, false)
+ local node = create_node(false, "owner")
refs[node] = true -- prevent gc of root node
diff --git a/src/switch.luau b/src/switch.luau
index 421d583..e3efbd6 100644
--- a/src/switch.luau
+++ b/src/switch.luau
@@ -38,7 +38,7 @@ local function switch(source: () -> T): (map: Map U)?)>) -> ()
throw("map must map a value to a function")
end
- local new_scope = create_node(false, false)
+ local new_scope = create_node(false, "owner")
last_scope = new_scope :: Node
set_owner(new_scope, owner)
diff --git a/src/untrack.luau b/src/untrack.luau
index 230674d..32246d2 100644
--- a/src/untrack.luau
+++ b/src/untrack.luau
@@ -13,7 +13,7 @@ local function untrack(source: () -> T): T
-- sources are only tracked if the node in scope has an effect
local effect = scope.effect
- scope.effect = false
+ scope.effect = "untracked"
local ok, result = pcall(source)
diff --git a/test/tests.luau b/test/tests.luau
index d9600ef..74a2e3e 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -41,6 +41,7 @@ TEST("graph", function()
local get_scope = graph.get_scope
local open_scope = graph.open_scope
local close_scope = graph.close_scope
+ local set_owner = graph.set_owner
local get_children = graph.get_children
local add_cleanup = graph.add_cleanup
local destroy = graph.destroy
@@ -50,7 +51,7 @@ TEST("graph", function()
end
local function scope()
- return create_node(false, false)
+ return create_node(false, "owner")
end
local function cleanup(fn: () -> ())
@@ -75,10 +76,14 @@ TEST("graph", function()
end
do CASE "rerun linked nodes"
+ local root = node()
local a = node()
local b = node()
local c = node()
+ set_owner(b, root)
+ set_owner(c, root)
+
local count = 0
local function effect(x)
@@ -104,8 +109,15 @@ TEST("graph", function()
end
do CASE "diamond graph"
+ -- a -> b -> d
+ -- -> c
+ local root = node()
local a, b, c, d = node(), node(), node(), node()
+ set_owner(b, root)
+ set_owner(c, root)
+ set_owner(d, root)
+
local b_cnt, c_cnt, d_cnt = 0, 0, 0
function b.effect(x) b_cnt += 1; return not x end
function c.effect(x) c_cnt += 1; return not x end
@@ -122,16 +134,52 @@ TEST("graph", function()
CHECK(d_cnt == 1)
end
+ do CASE "diamond graph 2"
+ -- todo: include cached value from parent nodes to confirm update order
+ -- a -> b -> c -> e
+ -- -> d
+ local root = node()
+ local a, b, c, d, e = node(), node(), node(), node(), node()
+
+ set_owner(b, root)
+ set_owner(c, root)
+ set_owner(d, root)
+ set_owner(e, root)
+
+ local b_cnt, c_cnt, d_cnt, e_cnt = 0, 0, 0, 0
+ function b.effect(x) b_cnt += 1; return not x end
+ function c.effect(x) c_cnt += 1; return not x end
+ function d.effect(x) d_cnt += 1; return not x end
+ function e.effect(x) e_cnt += 1; return not x end
+
+ open_scope(b); track(a); close_scope()
+ open_scope(c); track(b); close_scope()
+ open_scope(d); track(a); close_scope()
+ open_scope(e); track(c); track(d); close_scope()
+
+ update(a)
+
+ CHECK(b_cnt == 1)
+ CHECK(c_cnt == 1)
+ CHECK(d_cnt == 1)
+ CHECK(e_cnt == 1)
+ end
+
do CASE "duplicate child on rerun"
+ local root = node()
local a, b, c = node(), node(), node()
+ set_owner(a, root)
+ set_owner(b, root)
+ set_owner(c, root)
+
function c.effect(x)
track(a)
track(b)
return not x
end
- open_scope(c); assert(c.effect)(NIL); close_scope()
+ open_scope(c); assert(type(c.effect) == "function" and c.effect)(NIL); close_scope()
update(a)
@@ -168,28 +216,28 @@ TEST("graph", function()
items_updated = node()
track(items_updated) -- should not
- add_child(root, items_updated)
+ set_owner(items_updated, root)
do open_scope(items_updated)
track(items)
do open_scope(root)
- add_child(root, scope1)
+ set_owner(scope1, root)
do open_scope(scope1)
clean "scope1"
bind1 = node()
- add_child(scope1, bind1)
+ set_owner(bind1, scope1)
do open_scope(bind1)
clean "bind1"
track(selected)
close_scope() end
close_scope() end
- add_child(root, scope2)
+ set_owner(scope2, root)
do open_scope(scope2)
clean "scope2"
bind2 = node()
- add_child(scope2, bind2)
+ set_owner(bind2, scope2)
do open_scope(bind2)
clean "bind2"
track(selected)
@@ -284,6 +332,14 @@ TEST("graph", function()
local a, b, c, d, e, f = node(), node(), node(), node(), node(), node()
+ local root = node()
+ set_owner(a, root)
+ set_owner(b, root)
+ set_owner(c, root)
+ set_owner(d, root)
+ set_owner(e, root)
+ set_owner(f, root)
+
function b.effect(x)
update(d)
return not x
@@ -418,7 +474,6 @@ TEST("derive()", wrap_root(function()
local derive = vide.derive
local effect = vide.effect
local cleanup = vide.cleanup
- local untrack = vide.untrack
do CASE "derive new value on source change"
local a = source(1)
@@ -524,38 +579,43 @@ TEST("derive()", wrap_root(function()
CHECK(count == 2)
end
- do CASE "child with parent as owner not lost"
- local num = source(0)
+ -- do CASE "behavior of effect within an effect"
+ -- local num = source(1)
- local cleaned = {}
+ -- local ran = table.create(100, 0)
+ -- local cleaned = table.create(100, 0)
- local destroy = vide.mount(function()
- local owner = derive(function()
- local i = num()
+ -- local destroy = vide.mount(function()
+ -- local owner = derive(function()
+ -- local i = num()
- return untrack(function()
- return derive(function()
- cleanup(function()
- cleaned[i] = true
- end)
- return i
- end)
- end)
- end)
+ -- return untrack(function()
+ -- return derive(function()
+ -- ran[i] += 1
+ -- cleanup(function()
+ -- cleaned[i] += 1
+ -- end)
+ -- return i
+ -- end)
+ -- end)
+ -- end)
- local child1 = owner()
- num(1)
- local child2 = owner()
+ -- local child1 = owner()
+ -- num(2)
+ -- CHECK(cleaned[1] == 1)
+ -- local child2 = owner()
- CHECK(child1() == 0)
- CHECK(child2() == 1)
- end)
+ -- CHECK(child1() == 1)
+ -- CHECK(child2() == 2)
+ -- end)
- destroy()
+ -- destroy()
- CHECK(cleaned[0])
- CHECK(cleaned[1])
- end
+ -- CHECK(ran[1] == 1)
+ -- CHECK(ran[2] == 1)
+ -- CHECK(cleaned[1] == 1)
+ -- CHECK(cleaned[2] == 1)
+ -- end
do CASE "garbage collection"
-- check that `b` does not allow gc of `a`
@@ -1070,9 +1130,13 @@ TEST("indexes()", wrap_root(function()
local count = table.create(3, 0)
- local output = indexes(input, function(v, i)
- count[i] += 1
- return v
+ local output = vide.root(function()
+ local output = indexes(input, function(v, i)
+ count[i] += 1
+ return v
+ end)
+
+ return output
end)
input { 1, 2, 4 }
@@ -1453,11 +1517,8 @@ TEST("spring()", wrap_root(function()
end))
TEST("untrack()", wrap_root(function()
- local root = vide.root
local source = vide.source
- local derive = vide.derive
local effect = vide.effect
- local cleanup = vide.cleanup
local untrack = vide.untrack
do CASE "does not register dependency"
@@ -1506,56 +1567,69 @@ TEST("untrack()", wrap_root(function()
CHECK(count == 2)
end
- do CASE "outer scope"
- local outer_count = 0
- local inner_count = 0
- local cleaned_count = 0
+ -- do CASE "outer scope"
+ -- local outer_count = 0
+ -- local inner_count = 0
+ -- local cleaned_count = 0
- local input = source(0)
+ -- local input = source(0)
- local output, destroy = root(function(destroy)
- local output = derive(function()
- outer_count += 1
+ -- local output, destroy = root(function(destroy)
+ -- local output = derive(function()
+ -- outer_count += 1
- return untrack(function()
- return derive(function()
- inner_count += 1
+ -- return untrack(function()
+ -- return derive(function()
+ -- inner_count += 1
- cleanup(function()
- cleaned_count += 1
- end)
+ -- cleanup(function()
+ -- cleaned_count += 1
+ -- end)
- return tostring(input())
- end)
+ -- return tostring(input())
+ -- end)
+ -- end)
+ -- end)
+
+ -- return output, destroy
+ -- end)
+
+ -- CHECK(outer_count == 1)
+ -- CHECK(inner_count == 1)
+ -- CHECK(cleaned_count == 0)
+
+ -- local output2 = output()
+
+ -- CHECK(output2() == "0")
+
+ -- input(1)
+
+ -- CHECK(outer_count == 1)
+ -- CHECK(inner_count == 2)
+ -- CHECK(cleaned_count == 1)
+
+ -- local output3 = output()
+ -- CHECK(output2() == "1")
+ -- CHECK(output3() == "1")
+
+ -- CHECK(output2 == output3)
+
+ -- destroy()
+
+ -- CHECK(cleaned_count == 2)
+ -- end
+
+ do CASE "cannot create effect within untrack()"
+ local ok = pcall(function()
+ effect(function()
+ untrack(function()
+ effect(function() end)
+ return nil
end)
end)
-
- return output, destroy
end)
- CHECK(outer_count == 1)
- CHECK(inner_count == 1)
- CHECK(cleaned_count == 0)
-
- local output2 = output()
-
- CHECK(output2() == "0")
-
- input(1)
-
- CHECK(outer_count == 1)
- CHECK(inner_count == 2)
- CHECK(cleaned_count == 1)
-
- local output3 = output()
- CHECK(output2() == "1")
- CHECK(output3() == "1")
-
- CHECK(output2 == output3)
-
- destroy()
-
- CHECK(cleaned_count == 2)
+ CHECK(not ok)
end
end))
@@ -1690,6 +1764,90 @@ TEST("read()", wrap_root(function()
end
end))
+TEST("nested effects cases", function()
+ -- local vide = require "src/init"
+ -- local source = vide.source
+ -- local effect = vide.effect
+ -- local untrack = vide.untrack
+ -- local cleanup = vide.cleanup
+ -- local root = vide.root
+
+ -- local ran = 0
+ -- local cleaned = 0
+
+ -- local function Count()
+ -- local count = source(0)
+
+ -- effect(function()
+ -- count()
+ -- ran += 1
+ -- cleanup(function() cleaned += 1 end)
+ -- end)
+
+ -- return nil
+ -- end
+
+ -- local function App(destroy)
+ -- local name = source "a"
+
+ -- effect(function()
+ -- name()
+ -- untrack(Count)
+ -- end)
+
+ -- CHECK(ran == 1)
+ -- CHECK(cleaned == 0)
+
+ -- name "b"
+
+ -- CHECK(ran == 2)
+ -- CHECK(cleaned == 1)
+ -- print(cleaned)
+ -- end
+
+ -- root(App)
+
+ local vide = require "src/init"
+ local source = vide.source
+ local effect = vide.effect
+ local untrack = vide.untrack
+ local cleanup = vide.cleanup
+ local root = vide.root
+
+ local ran = 0
+ local cleaned = 0
+
+ local function Count()
+ local count = source(0)
+
+ effect(function()
+ count()
+ ran += 1
+ cleanup(function() cleaned += 1 end)
+ end)
+
+ return nil
+ end
+
+ local function App(destroy)
+ local name = source "a"
+
+ effect(function()
+ name()
+ untrack(Count)
+ end)
+
+ CHECK(ran == 1)
+ CHECK(cleaned == 0)
+ end
+
+ local ok = pcall(function()
+ root(App)
+ end)
+
+ CHECK(not ok)
+end)
+
vide.strict = true
TEST("strict", wrap_root(function()
From c68d0a6c18cb93e14afc125f6b637fab8b57916c Mon Sep 17 00:00:00 2001
From: aaron <83140718+centau@users.noreply.github.com>
Date: Mon, 25 Sep 2023 23:37:51 +0100
Subject: [PATCH 009/116] Allow creation of nested tracking scopes
It turns out that handling destruction of a nested tracking scope was
not as difficult as I thought and didn't need much changes
---
CHANGELOG.md | 4 ++
src/graph.luau | 69 +++++++++++++-----
src/maps.luau | 8 +--
src/root.luau | 2 +-
src/switch.luau | 2 +-
src/untrack.luau | 2 +-
test/tests.luau | 178 ++++++++++++++++++-----------------------------
7 files changed, 128 insertions(+), 137 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2127a86..c42c5ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
+### Changed
+
+- Reactive scopes created within reactive scopes are now destroyed on rerun.
+
---
## [0.1.0] - 2023-09-20
diff --git a/src/graph.luau b/src/graph.luau
index de6f7e1..6917462 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -10,10 +10,14 @@ export type StartNode = {
export type Node = {
cache: T,
- effect: ((T) -> T) | "owner" | "untracked",
+ effect: ((T) -> T) | false,
cleanups: { () -> () } | false,
- parents: { owner: StartNode?, [number]: StartNode },
- [number]: Node
+
+ owned: { Node } | false,
+ owner: Node | false,
+
+ parents: { StartNode },
+ [number]: Node -- children
}
-- reactive scope stack
@@ -50,7 +54,7 @@ local function get_owning_scope(): Node
if not scope then
local caller_name = debug.info(2, "n")
return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
- elseif scope.effect ~= "owner" then
+ elseif scope.effect then
throw("reactive scope is not an owning scope; new effects cannot be created in side-effects")
end
return scope
@@ -62,8 +66,12 @@ local function add_child(parent: StartNode, child: Node)
end
local function set_owner(node: Node, owner: Node)
- node.parents.owner = owner
- table.insert(owner, node)
+ node.owner = owner
+ if owner.owned then
+ table.insert(owner.owned, node)
+ else
+ owner.owned = { node }
+ end
end
local function open_scope(node: Node)
@@ -96,18 +104,29 @@ local function run_cleanups(node: Node)
end
end
+local function find_and_swap_pop(t: { T }, v: T)
+ local idx = table.find(t, v)
+ assert(idx, "value not found")
+ local n = #t
+ t[idx] = t[n]
+ t[n] = nil
+end
+
local function remove_child(parent: StartNode, child: Node)
- local idx = table.find(parent, child)
- assert(idx, "child not found")
- local n = #parent
- parent[idx] = parent[n]
- parent[n] = nil
+ find_and_swap_pop(parent, child)
+end
+
+local function remove_owner(node: Node)
+ local owner = node.owner :: Node
+ if node.owner and owner.owned then
+ find_and_swap_pop(owner.owned, node)
+ end
end
local function unparent(node: Node)
local parents = node.parents
- for i, parent in ipairs(parents) do
+ for i, parent in next, parents do
remove_child(parent, node)
parents[i] = nil
end
@@ -116,15 +135,21 @@ end
local function destroy(node: Node)
run_cleanups(node)
unparent(node)
+ remove_owner(node)
- if node.parents.owner then
- remove_child(node.parents.owner, node)
- node.parents.owner = nil
+ if node.owned then
+ local owned = node.owned
+ while owned[1] do destroy(owned[1]) end
end
-
while node[1] do destroy(node[1]) end
end
+local function destroy_owned(node: Node)
+ if node.owned then
+ while node.owned[1] do destroy(node.owned[1]) end
+ end
+end
+
local update_queue = { n = 0 } :: { n: number, [number]: Node }
local function evaluate_node(node: Node)
@@ -132,6 +157,8 @@ local function evaluate_node(node: Node)
if flags.strict then
run_cleanups(node)
+ destroy_owned(node)
+
open_scope(node)
local ok, err = check_for_yield(node.effect :: (T) -> T, cur_value)
@@ -141,7 +168,9 @@ local function evaluate_node(node: Node)
if not ok then throw(err :: string) end
end
- run_cleanups(node) -- todo: move in scope?
+ run_cleanups(node)
+ destroy_owned(node)
+
open_scope(node)
local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
@@ -204,11 +233,15 @@ local function track(node: StartNode)
end
end
-local function create_node(value: T, effect: "owner" | (T) -> T): Node
+local function create_node(value: T, effect: false | (T) -> T): Node
return {
cache = value,
effect = effect,
cleanups = false,
+
+ owner = false,
+ owned = false,
+
parents = {},
}
end
diff --git a/src/maps.luau b/src/maps.luau
index d18357c..1034e9a 100644
--- a/src/maps.luau
+++ b/src/maps.luau
@@ -30,7 +30,7 @@ end
local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO): () -> { VO }
local owner = get_owning_scope()
- local subowner = create_node(false, "owner")
+ local subowner = create_node(false, false)
set_owner(subowner, owner)
local input_cache = {} :: Map
@@ -67,7 +67,7 @@ local function indexes(input: () -> Map, transform: (() -> VI,
if cv ~= v then
if cv == nil then -- create new scope and run transform
- local scope = create_node(false, "owner")
+ local scope = create_node(false, false)
scopes[i] = scope :: Node
local node = create_start_node(v)
@@ -125,7 +125,7 @@ end
local function values(input: () -> Map, transform: (VI, () -> K) -> VO): () -> { VO }
local owner = get_owning_scope()
- local subowner = create_node(false, "owner")
+ local subowner = create_node(false, false)
set_owner(subowner, owner)
local cur_input_cache_up = {} :: Map
@@ -156,7 +156,7 @@ local function values(input: () -> Map, transform: (VI, () ->
local cv = cur_input_cache[v]
if cv == nil then -- create new scope and run transform
- local scope = create_node(false, "owner")
+ local scope = create_node(false, false)
scopes[v] = scope :: Node
local node = create_start_node(i)
diff --git a/src/root.luau b/src/root.luau
index 5f79bd7..50a1c1c 100644
--- a/src/root.luau
+++ b/src/root.luau
@@ -11,7 +11,7 @@ local destroy = graph.destroy
local refs = {}
local function root(fn: (destroy: () -> ()) -> T...): T...
- local node = create_node(false, "owner")
+ local node = create_node(false, false)
refs[node] = true -- prevent gc of root node
diff --git a/src/switch.luau b/src/switch.luau
index e3efbd6..421d583 100644
--- a/src/switch.luau
+++ b/src/switch.luau
@@ -38,7 +38,7 @@ local function switch(source: () -> T): (map: Map U)?)>) -> ()
throw("map must map a value to a function")
end
- local new_scope = create_node(false, "owner")
+ local new_scope = create_node(false, false)
last_scope = new_scope :: Node
set_owner(new_scope, owner)
diff --git a/src/untrack.luau b/src/untrack.luau
index 32246d2..230674d 100644
--- a/src/untrack.luau
+++ b/src/untrack.luau
@@ -13,7 +13,7 @@ local function untrack(source: () -> T): T
-- sources are only tracked if the node in scope has an effect
local effect = scope.effect
- scope.effect = "untracked"
+ scope.effect = false
local ok, result = pcall(source)
diff --git a/test/tests.luau b/test/tests.luau
index 74a2e3e..08c3cff 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -51,7 +51,7 @@ TEST("graph", function()
end
local function scope()
- return create_node(false, "owner")
+ return create_node(false, false)
end
local function cleanup(fn: () -> ())
@@ -257,10 +257,10 @@ TEST("graph", function()
do
local c = get_children(root)
- CHECK(#c == 3)
- CHECK(table.find(c, items_updated))
- CHECK(table.find(c, scope1 :: Node))
- CHECK(table.find(c, scope2 :: Node))
+ CHECK(#c == 0)
+ -- CHECK(table.find(c, items_updated))
+ -- CHECK(table.find(c, scope1 :: Node))
+ -- CHECK(table.find(c, scope2 :: Node))
end
do
@@ -272,19 +272,19 @@ TEST("graph", function()
do
local c = get_children(scope1)
- CHECK(#c == 1)
- CHECK(table.find(c, bind1))
+ CHECK(#c == 0)
+ --CHECK(table.find(c, bind1))
end
do
local c = get_children(scope2)
- CHECK(#c == 1)
- CHECK(table.find(c, bind2))
+ CHECK(#c == 0)
+ --CHECK(table.find(c, bind2))
end
-- destroy
- CHECK(table.find(get_children(root), scope1 :: Node))
+ --CHECK(table.find(get_children(root), scope1 :: Node))
destroy(scope1)
CHECK(cleaned.scope1)
@@ -293,7 +293,7 @@ TEST("graph", function()
bind1 = NIL
bind2 = NIL
gc()
- CHECK(#get_children(root) == 2)
+ CHECK(#get_children(root) == 0)
CHECK(#get_children(selected) == 1)
end
@@ -1519,7 +1519,10 @@ end))
TEST("untrack()", wrap_root(function()
local source = vide.source
local effect = vide.effect
+ local derive = vide.derive
local untrack = vide.untrack
+ local cleanup = vide.cleanup
+ local root = vide.root
do CASE "does not register dependency"
local a = source(0)
@@ -1567,69 +1570,56 @@ TEST("untrack()", wrap_root(function()
CHECK(count == 2)
end
- -- do CASE "outer scope"
- -- local outer_count = 0
- -- local inner_count = 0
- -- local cleaned_count = 0
+ do CASE "outer scope"
+ local outer_count = 0
+ local inner_count = 0
+ local cleaned_count = 0
- -- local input = source(0)
+ local input = source(0)
- -- local output, destroy = root(function(destroy)
- -- local output = derive(function()
- -- outer_count += 1
+ local output, destroy = root(function(destroy)
+ local output = derive(function()
+ outer_count += 1
- -- return untrack(function()
- -- return derive(function()
- -- inner_count += 1
+ return untrack(function()
+ return derive(function()
+ inner_count += 1
- -- cleanup(function()
- -- cleaned_count += 1
- -- end)
+ cleanup(function()
+ cleaned_count += 1
+ end)
- -- return tostring(input())
- -- end)
- -- end)
- -- end)
-
- -- return output, destroy
- -- end)
-
- -- CHECK(outer_count == 1)
- -- CHECK(inner_count == 1)
- -- CHECK(cleaned_count == 0)
-
- -- local output2 = output()
-
- -- CHECK(output2() == "0")
-
- -- input(1)
-
- -- CHECK(outer_count == 1)
- -- CHECK(inner_count == 2)
- -- CHECK(cleaned_count == 1)
-
- -- local output3 = output()
- -- CHECK(output2() == "1")
- -- CHECK(output3() == "1")
-
- -- CHECK(output2 == output3)
-
- -- destroy()
-
- -- CHECK(cleaned_count == 2)
- -- end
-
- do CASE "cannot create effect within untrack()"
- local ok = pcall(function()
- effect(function()
- untrack(function()
- effect(function() end)
- return nil
+ return tostring(input())
+ end)
end)
end)
+
+ return output, destroy
end)
- CHECK(not ok)
+ CHECK(outer_count == 1)
+ CHECK(inner_count == 1)
+ CHECK(cleaned_count == 0)
+
+ local output2 = output()
+
+ CHECK(output2() == "0")
+
+ input(1)
+
+ CHECK(outer_count == 1)
+ CHECK(inner_count == 2)
+ CHECK(cleaned_count == 1)
+
+ local output3 = output()
+ CHECK(output2() == "1")
+ CHECK(output3() == "1")
+
+ CHECK(output2 == output3)
+
+ destroy()
+
+ CHECK(cleaned_count == 2)
end
end))
@@ -1765,48 +1755,6 @@ TEST("read()", wrap_root(function()
end))
TEST("nested effects cases", function()
- -- local vide = require "src/init"
- -- local source = vide.source
- -- local effect = vide.effect
- -- local untrack = vide.untrack
- -- local cleanup = vide.cleanup
- -- local root = vide.root
-
- -- local ran = 0
- -- local cleaned = 0
-
- -- local function Count()
- -- local count = source(0)
-
- -- effect(function()
- -- count()
- -- ran += 1
- -- cleanup(function() cleaned += 1 end)
- -- end)
-
- -- return nil
- -- end
-
- -- local function App(destroy)
- -- local name = source "a"
-
- -- effect(function()
- -- name()
- -- untrack(Count)
- -- end)
-
- -- CHECK(ran == 1)
- -- CHECK(cleaned == 0)
-
- -- name "b"
-
- -- CHECK(ran == 2)
- -- CHECK(cleaned == 1)
- -- print(cleaned)
- -- end
-
- -- root(App)
-
local vide = require "src/init"
local source = vide.source
local effect = vide.effect
@@ -1839,13 +1787,19 @@ TEST("nested effects cases", function()
CHECK(ran == 1)
CHECK(cleaned == 0)
+
+ name "b"
+
+ CHECK(ran == 2)
+ CHECK(cleaned == 1)
+
+ destroy()
+
+ CHECK(ran == 2)
+ CHECK(cleaned == 2)
end
- local ok = pcall(function()
- root(App)
- end)
-
- CHECK(not ok)
+ root(App)
end)
vide.strict = true
From 71bbaa2092f0e76c501b87428c44e71a090135a3 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Tue, 26 Sep 2023 13:27:38 +0100
Subject: [PATCH 010/116] Update reactive scoping docs
---
docs/tut/advanced/reactive-scoping.md | 149 +++++++++++++++++++++-----
test/tests.luau | 73 +++++++++++--
todo.md | 24 +----
3 files changed, 189 insertions(+), 57 deletions(-)
diff --git a/docs/tut/advanced/reactive-scoping.md b/docs/tut/advanced/reactive-scoping.md
index 99a09f4..f75525e 100644
--- a/docs/tut/advanced/reactive-scoping.md
+++ b/docs/tut/advanced/reactive-scoping.md
@@ -3,11 +3,34 @@
This is a brief document designed to give the user more insight into how Vide's
reactive system works.
-Vide's reactivity can be pictured as a graph, where each source, derived source,
-and effect is a node on that graph. For example:
+## Graph Basics
+
+Vide's reactivity can be represented as a graph, where each source, derived
+source, and effect is a node on that graph. The term "*reactive scope*" is just
+an abstraction used to refer to these nodes. Each node is a reactive scope.
+
+Each node stores a cached value, a side-effect function, cleanup functions,
+its parents and children, and its owner and owned.
+
+Whenever a node is updated it will:
+
+1. destroy its owned nodes
+2. run its cleanups
+3. rerun its side-effect and update its cached value
+4. if its cached value changes, update its children recursively.
+
+There is a difference between children nodes and owned nodes:
+
+- children nodes are updated when a parent is updated.
+- owned nodes are destroyed when a parent is updated.
+- both children and owned are destroyed when a parent is destroyed.
+
+Nodes created by `root()` generally have no children, and only tracks owned.
+Nodes created by `derive()` generally have no owned, and only tracks children.
+
+## Basic Example
```lua
-
root(function()
local forename = source "quan"
local surname = source "xi"
@@ -22,11 +45,6 @@ root(function()
end)
```
-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.
-
This code will produce a graph that looks like so:
```mermaid
@@ -49,32 +67,35 @@ flowchart
end
```
+Nodes connected by arrows represent parent and children connections.
+Nodes within other nodes represent owner and owned connections.
+
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 the side-effect for a node is being reran when a node is updated, any
+other nodes read within that side-effect are set as parents of the node
+currently being reran. As those nodes are read, we know that the current node
+depends on them, so any time those nodes are updated, they will update dependent
+nodes since they will be stored as children.
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.
+When being destroyed, a node's connections (parents and children, owner and
+owned) 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
+the root node which will track any node created 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.
Nodes created by `source()` can actually exist outside of root nodes, since
they do not have direct side-effects or cleanups, they do not have to be
explicitly destroyed.
+## Control-flow Graph Example
+
Control flow functions in Vide are special, as they can dynamically create and
destroy new root scopes.
@@ -120,21 +141,21 @@ This code produces a graph like so:
"primaryBorderColor": "#1B1B1F",
"lineColor": "#79B8FF",
"tertiaryColor": "#161618",
- "tertiaryBorderColor": "#161618"
+ "tertiaryBorderColor": "#fff"
}
}}%%
flowchart LR
subgraph root
counters --> indexes
- end
- subgraph root1[subroot 1]
- n1[name] --> p1[prop binding]
- end
+ subgraph root1[subroot 1]
+ n1[name] --> p1[prop binding]
+ end
- subgraph root2[subroot 2]
- n2[name] --> p2[prop binding]
+ subgraph root2[subroot 2]
+ n2[name] --> p2[prop binding]
+ end
end
indexes .-> root1 & root2
@@ -153,3 +174,79 @@ applies to all other control flow functions.
Whenever the root reactive scope is destroyed, all its children, `counters` and
`indexes` will be destroyed too, which means that `indexes` children, the
subroots, will also be destroyed. Everything is nicely cleaned up.
+
+## Custom Control-flow Example
+
+Below is a simple example of the `show()` control-flow function.
+
+Each time `visible` changes, `show()` will destroy the current reactive scope
+and rerun its function in a new one.
+
+```lua
+local visible = source(true)
+local count = source(0)
+
+root(function()
+ show(visible, function()
+ return create "TextLabel" { Text = count }
+ end)
+end)
+```
+
+The above code produces a graph like so:
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#fff"
+ }
+}}%%
+
+flowchart LR
+ subgraph root
+ show
+
+ subgraph subroot[show subroot]
+ p1[prop binding]
+ end
+ end
+
+ visible --> show
+ count --> p1
+ show .-> subroot
+```
+
+This can be recreated without the `show()` control-flow function, with the
+following code:
+
+```lua
+local visible = source(true)
+local count = source(0)
+
+root(function()
+ local output = derive(function()
+ visible()
+
+ -- untrack so any source read from within this scope
+ -- will not cause the outer `derive()` call to rerun,
+ -- we only want `derive()` to rerun when `visible` changes
+ return untrack(function()
+ local label = create "TextLabel" {}
+
+ effect(function()
+ label.Text = count()
+ end)
+
+ return label
+ end)
+ end)
+end)
+```
+
+Both of the above code samples will produce the same visible result.
diff --git a/test/tests.luau b/test/tests.luau
index 08c3cff..8a11df1 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -981,19 +981,76 @@ TEST("create()", wrap_root(function()
end))
TEST("show()", wrap_root(function()
- -- uses switch() internally, more extensive testing of scoping not needed
+ local untrack = vide.untrack
+ local cleanup = vide.cleanup
local source = vide.source
+ local effect = vide.effect
local show = vide.show
+ local root = vide.root
- local value = source("truey" :: unknown)
- local function one() return 1 end
- local function two() return 2 end
+ do CASE "main"
+ -- uses switch() internally, more extensive testing of scoping not needed
+ local value = source("truey" :: unknown)
+ local function one() return 1 end
+ local function two() return 2 end
- local output = show(value, one, two)
+ local output = show(value, one, two)
- CHECK(output() == 1)
- value(nil)
- CHECK(output() == 2)
+ CHECK(output() == 1)
+ value(nil)
+ CHECK(output() == 2)
+ end
+
+ do CASE "alt"
+ local visible = vide.source(true)
+ local count = vide.source(0)
+
+ local outer = 0
+ local inner = 0
+ local destroyed = 0
+
+ root(function()
+ effect(function()
+ visible()
+ outer += 1
+
+ untrack(function()
+ effect(function()
+ count()
+
+ inner += 1
+
+ cleanup(function()
+ destroyed += 1
+ end)
+ end)
+ return nil
+ end)
+ end)
+ end)
+
+ CHECK(outer == 1)
+ CHECK(inner == 1)
+ CHECK(destroyed == 0)
+
+ count(count() + 1)
+ CHECK(outer == 1)
+ CHECK(inner == 2)
+ CHECK(destroyed == 1)
+
+ visible(false)
+ CHECK(outer == 2)
+ CHECK(inner == 3)
+ CHECK(destroyed == 2)
+
+ count(count() + 1)
+ CHECK(outer == 2)
+ CHECK(inner == 4)
+ CHECK(destroyed == 3)
+
+
+
+ end
end))
TEST("switch()", wrap_root(function()
diff --git a/todo.md b/todo.md
index a3f26a8..8daf7a7 100644
--- a/todo.md
+++ b/todo.md
@@ -9,26 +9,4 @@
- batch
- optimize `indexes()` double-diffing
- improve crash course, some sections feel like information dumps
-- reactive scopes
- - define behavior of creating a reactive scope within a non-root reactive scope
- - i.e. an effect within an effect
- - error on attempt for now
- - define behavior of destruction of a node with a child, where the node and
- its child are in separate, non-nested root reactive scopes.
- - should destruction of the parent also destroy the child?
- - or should destruction of the parent silently disconnect the child, without
- invoking the child's cleanups?
-
- ```mermaid
- graph LR
-
- subgraph root1
- parent
- end
-
- subgraph root2
- child
- end
-
- parent --> child
- ```
+- cleanup source and tests
From d1f86a3f9e85d6a114905aa001ffd477e15ecbb9 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Tue, 26 Sep 2023 14:11:33 +0100
Subject: [PATCH 011/116] Allow `untrack()` in non-reactive scopes
Closes #17
---
CHANGELOG.md | 1 +
src/untrack.luau | 23 ++++++++++++-----------
2 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c42c5ca..6d6423e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- Reactive scopes created within reactive scopes are now destroyed on rerun.
+- `untrack()` can be called outside of reactive scopes.
---
diff --git a/src/untrack.luau b/src/untrack.luau
index 230674d..d9ca120 100644
--- a/src/untrack.luau
+++ b/src/untrack.luau
@@ -7,21 +7,22 @@ local get_scope = graph.get_scope
local function untrack(source: () -> T): T
local scope = get_scope()
- if not scope then
- throw("cannot untrack in non-reactive scope")
- end; assert(scope)
+
+ if scope then
+ -- sources are only tracked if the node in scope has an effect
+ local effect = scope.effect
+ scope.effect = false
- -- sources are only tracked if the node in scope has an effect
- local effect = scope.effect
- scope.effect = false
+ local ok, result = pcall(source)
- local ok, result = pcall(source)
+ scope.effect = effect :: () -> ()
- scope.effect = effect :: () -> ()
+ if not ok then error(result, 0) end
- if not ok then error(result, 0) end
-
- return result
+ return result
+ else
+ return source()
+ end
end
return untrack
From 32fa44f4c5ff5a1b1f7a8d4084946a579c5d5fd2 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Tue, 26 Sep 2023 14:34:27 +0100
Subject: [PATCH 012/116] Cleanup helper for instances
Closes #19
---
src/cleanup.luau | 30 +++++++++++++++++++++++++++---
src/untrack.luau | 1 -
test/tests.luau | 20 ++++++++++++++++++++
3 files changed, 47 insertions(+), 4 deletions(-)
diff --git a/src/cleanup.luau b/src/cleanup.luau
index 0bfb6c1..803be03 100644
--- a/src/cleanup.luau
+++ b/src/cleanup.luau
@@ -1,19 +1,43 @@
if not game then script = require "test/relative-string" end
+local typeof = game and typeof or require "test/mock".typeof :: never
local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph)
local get_scope = graph.get_scope
local add_cleanup = graph.add_cleanup
-local function cleanup(callback: () -> ())
+local function helper(obj: any)
+ return
+ if typeof(obj) == "RBXScriptConnection" then function() obj:Disconnect() end
+ elseif typeof(obj) == "Instance" then function() obj:Destroy() end
+ elseif obj.destroy then function() obj:destroy() end
+ elseif obj.disconnect then function() obj:disconnect() end
+ elseif obj.Destroy then function() obj:Destroy() end
+ elseif obj.Disconnect then function() obj:Disconnect() end
+ else throw("cannot cleanup given object")
+end
+
+local function cleanup(value: unknown)
local scope = get_scope()
if not scope then
throw "cannot cleanup in a non-reactive scope"
end; assert(scope)
- add_cleanup(scope, callback)
+ if type(value) == "function" then
+ add_cleanup(scope, value :: () -> ())
+ else
+ add_cleanup(scope, helper(value))
+ end
end
-return cleanup
+type Destroyable = { destroy: (any) -> () } | { Destroy: (any) -> () }
+type Disconnectable = { disconnect: (any) -> () } | { Disconnect: (any) -> () }
+
+return cleanup ::
+ ( (callback: () -> ()) -> () ) &
+ ( (instance: Destroyable) -> () ) &
+ ( (connection: Disconnectable) -> () ) &
+ ( (instance: Instance) -> () ) &
+ ( (connection: RBXScriptConnection) -> () )
diff --git a/src/untrack.luau b/src/untrack.luau
index d9ca120..86cdb7b 100644
--- a/src/untrack.luau
+++ b/src/untrack.luau
@@ -1,6 +1,5 @@
if not game then script = require "test/relative-string" end
-local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph)
type Node = graph.Node
local get_scope = graph.get_scope
diff --git a/test/tests.luau b/test/tests.luau
index 8a11df1..73ea0b7 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -691,6 +691,7 @@ TEST("effect()", wrap_root(function()
end))
TEST("cleanup()", wrap_root(function()
+ local root = vide.root
local source = vide.source
local effect = vide.effect
local cleanup = vide.cleanup
@@ -749,6 +750,25 @@ TEST("cleanup()", wrap_root(function()
src(3)
CHECK(testkit.seq(queue, { 1, 2, 1, 2 }))
end
+
+ do CASE "cleanup objects"
+ local ran = {}
+
+ root(function(destroy)
+ effect(function()
+ cleanup { disconnect = function() ran.disconnect = true end }
+ cleanup { Disconnect = function() ran.Disconnect = true end }
+ cleanup { destroy = function() ran.destroy = true end }
+ cleanup { Destroy = function() ran.Destroy = true end }
+ destroy()
+ end)
+ end)
+
+ CHECK(ran.disconnect)
+ CHECK(ran.Disconnect)
+ CHECK(ran.destroy)
+ CHECK(ran.Destroy)
+ end
end))
TEST("create()", wrap_root(function()
From c3bc53c40a312f67d32894d925392072cd1ea708 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Tue, 26 Sep 2023 14:37:48 +0100
Subject: [PATCH 013/116] Fix typo in docs
Fixes #18
---
docs/tut/crash-course/10-property-nesting.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/tut/crash-course/10-property-nesting.md b/docs/tut/crash-course/10-property-nesting.md
index 2aabb11..ea2ae43 100644
--- a/docs/tut/crash-course/10-property-nesting.md
+++ b/docs/tut/crash-course/10-property-nesting.md
@@ -27,7 +27,7 @@ function Menu(props: {
Size: UDim2
})
return Background {
- Color = props.COlor,
+ Color = props.Color,
AnchorPoint = props.AnchorPoint,
Position = props.Position,
Size = props.Size
From fd2888afabc7691cb7655c8c0ae219f2eb912c75 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Tue, 26 Sep 2023 18:24:30 +0100
Subject: [PATCH 014/116] Update docs
---
docs/tut/crash-course/4-source.md | 9 ++++-----
docs/tut/crash-course/5-effect.md | 3 ++-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/docs/tut/crash-course/4-source.md b/docs/tut/crash-course/4-source.md
index b7f7d04..4419b72 100644
--- a/docs/tut/crash-course/4-source.md
+++ b/docs/tut/crash-course/4-source.md
@@ -36,8 +36,7 @@ count(1)
print(text()) -- "count: 1"
```
-You may be wondering why we are using sources instead of plain variables to do
-this. The reason is that Vide has an entire reactive system based on sources.
-You can write functions to automatically run each time a source is updated. This
-can be to update properties, create new instances, print to the terminal, etc.
-How this is done will be covered next.
+Derived sources should be pure functions. This is where the same output is
+always produced for the same input. As well as making source updates more
+predictable, knowing that updates are pure allows Vide to use optimizations such
+as caching, to avoid updating derived sources if their inputs are the same.
diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md
index f75d21a..406ccd2 100644
--- a/docs/tut/crash-course/5-effect.md
+++ b/docs/tut/crash-course/5-effect.md
@@ -1,7 +1,8 @@
# Effects
Effects are functions that are ran in response to source updates. They are
-called effects because they cause *side-effects* when reacting to source updates.
+called effects because they cause *side-effects* when reacting to source updates
+which are pure.
Effects are created using `effect()`.
From d07c705b64c25416b1ff0080cd27b30c2dc2acb7 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Wed, 27 Sep 2023 10:02:09 +0100
Subject: [PATCH 015/116] Fix `switch()` not working in strict mode
Fixes #20
---
src/graph.luau | 35 ++++++++++++++---------------------
src/switch.luau | 2 +-
test/tests.luau | 34 +++++++++++++++++++++++++++++++---
3 files changed, 46 insertions(+), 25 deletions(-)
diff --git a/src/graph.luau b/src/graph.luau
index 6917462..bb42fbb 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -23,26 +23,17 @@ export type Node = {
-- reactive scope stack
local scopes = { n = 0 } :: { [number]: Node, n: number }
--- runs a given callback in a context that Luau does not allow yielding in
-local check_for_yield: (fn: (T...) -> (), T...) -> (boolean, string?) do
- local t = { __mode = "kv" }
- setmetatable(t, t)
+local function ycall(fn: (T) -> U, arg: T): (boolean, string|U)
+ local thread = coroutine.create(pcall)
+ local resume_ok, run_ok, result = coroutine.resume(thread, fn, arg)
- check_for_yield = function(fn, ...: any)
- local args = { ... }
-
- t.__unm = function(_)
- fn(unpack(args))
- end
-
- local ok, err: string? = pcall(function()
- local _ = -t
- end)
-
- return ok, if err == "attempt to yield across metamethod/C-call boundary"
- or err == "thread is not yieldable" then "yield occured"
- else err
+ assert(resume_ok)
+
+ if coroutine.status(thread) ~= "dead" then
+ return false, "attempt to yield in reactive scope"
end
+
+ return run_ok, result
end
local function get_scope(): Node?
@@ -161,11 +152,13 @@ local function evaluate_node(node: Node)
open_scope(node)
- local ok, err = check_for_yield(node.effect :: (T) -> T, cur_value)
+ local ok, new_value = ycall(node.effect :: (T) -> T, cur_value)
close_scope()
- if not ok then throw(err :: string) end
+ if not ok then throw(new_value :: string) end
+
+ node.cache = new_value :: T
end
run_cleanups(node)
@@ -173,7 +166,7 @@ local function evaluate_node(node: Node)
open_scope(node)
- local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
+ local ok, new_value = pcall(node.effect :: (T) -> T, node.cache)
close_scope()
diff --git a/src/switch.luau b/src/switch.luau
index 421d583..1ddc8e6 100644
--- a/src/switch.luau
+++ b/src/switch.luau
@@ -53,7 +53,7 @@ local function switch(source: () -> T): (map: Map U)?)>) -> ()
return result
end
- local node = create_node(nil :: any, update)
+ local node = create_node(nil :: U?, update)
set_owner(node, owner)
evaluate_node(node)
diff --git a/test/tests.luau b/test/tests.luau
index 73ea0b7..eb5581c 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -1067,9 +1067,6 @@ TEST("show()", wrap_root(function()
CHECK(outer == 2)
CHECK(inner == 4)
CHECK(destroyed == 3)
-
-
-
end
end))
@@ -1181,6 +1178,22 @@ TEST("switch()", wrap_root(function()
CHECK(n0 == n1)
end
+
+ do CASE "strict"
+ vide.strict = true
+
+ local input = source(0)
+ local output = switch(input) {
+ [0] = function() return 0 end,
+ [1] = function() return 1 end,
+ }
+
+ CHECK(output() == 0)
+ input(1)
+ CHECK(output() == 1)
+
+ vide.strict = false
+ end
end))
TEST("indexes()", wrap_root(function()
@@ -1364,6 +1377,21 @@ TEST("indexes()", wrap_root(function()
CHECK(updated[3] == 2)
CHECK(updated[4] == 2)
end
+
+ do CASE "strict"
+ vide.strict = true
+
+ local input = source{1}
+ local output = indexes(input, function(v)
+ return { v }
+ end)
+
+ CHECK(output()[1][1]() == 1)
+ input{2}
+ CHECK(output()[1][1]() == 2)
+
+ vide.strict = false
+ end
end))
TEST("values()", wrap_root(function()
From 2ae65a0194fa8fea0a3eed6e3e6e57ed5d2e1943 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Wed, 27 Sep 2023 10:24:14 +0100
Subject: [PATCH 016/116] Update tests
Added a case to confirm the behavior of strict mode's double evaluation
and node cached values.
---
test/tests.luau | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/test/tests.luau b/test/tests.luau
index eb5581c..7e95cca 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -2019,6 +2019,22 @@ TEST("strict", wrap_root(function()
CHECK(ok)
end
+
+ do CASE "effect counter"
+ local src = source(true)
+
+ local count = 0
+
+ effect(function(x: number)
+ src()
+ count = x + 1
+ return count
+ end, count)
+
+ CHECK(count == 2)
+ src(not src())
+ CHECK(count == 4)
+ end
end))
local ok = FINISH()
From bbda0426dacc253174b043e4db0afca974d73987 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Wed, 27 Sep 2023 10:31:52 +0100
Subject: [PATCH 017/116] Update changelog
---
CHANGELOG.md | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d6423e..3900dad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,14 +4,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+--------------------------------------------------------------------------------
+
## Unreleased
+### Added
+
+- `cleanup()` accepts objects with a `Destroy()` or `Disconnect()` interface.
+- `read()` as a utility to read sources or passthrough a non-source value.
+
### Changed
- Reactive scopes created within reactive scopes are now destroyed on rerun.
- `untrack()` can be called outside of reactive scopes.
----
+### Fixed
+
+- `show()` and `switch()` not updating when in strict mode.
+
+--------------------------------------------------------------------------------
## [0.1.0] - 2023-09-20
From 3959f5119e406a77054bf91f7f20649f3bfc1145 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Wed, 27 Sep 2023 10:33:38 +0100
Subject: [PATCH 018/116] Update `cleanup()` docs
---
docs/api/reactivity-utility.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/docs/api/reactivity-utility.md b/docs/api/reactivity-utility.md
index 9c7980a..a16e251 100644
--- a/docs/api/reactivity-utility.md
+++ b/docs/api/reactivity-utility.md
@@ -8,6 +8,11 @@ Runs a callback anytime a reactive scope is reran or destroyed.
```lua
function cleanup(callback: () -> ())
+ function cleanup(obj: Destroyable)
+ function cleanup(obj: Disconnectable)
+
+ type Destroyable = { destroy: () -> () }
+ type Disconnectable = { disconnect: () -> () }
```
- **Example**
From 9b6be14441c18fad6df2a2b9a9f94f571737fa2c Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Wed, 27 Sep 2023 16:44:39 +0100
Subject: [PATCH 019/116] Update docs
---
docs/.vitepress/config.ts | 16 ++--
docs/tut/advanced/reactive-scoping.md | 7 +-
docs/tut/crash-course/1-introduction.md | 11 ++-
.../{8-cleanup.md => 10-cleanup.md} | 32 ++++++-
.../{9-control-flow.md => 11-control-flow.md} | 94 +++++++++++++++++-
...erty-nesting.md => 12-property-nesting.md} | 0
.../{11-actions.md => 13-actions.md} | 0
.../{12-strict-mode.md => 14-strict-mode.md} | 0
docs/tut/crash-course/4-source.md | 12 ++-
docs/tut/crash-course/5-effect.md | 41 ++------
docs/tut/crash-course/6-root.md | 49 ++++++++++
...l-component.md => 7-stateful-component.md} | 3 +
...perty-binding.md => 8-property-binding.md} | 8 +-
docs/tut/crash-course/9-derived-source.md | 95 +++++++++++++++++++
14 files changed, 308 insertions(+), 60 deletions(-)
rename docs/tut/crash-course/{8-cleanup.md => 10-cleanup.md} (64%)
rename docs/tut/crash-course/{9-control-flow.md => 11-control-flow.md} (67%)
rename docs/tut/crash-course/{10-property-nesting.md => 12-property-nesting.md} (100%)
rename docs/tut/crash-course/{11-actions.md => 13-actions.md} (100%)
rename docs/tut/crash-course/{12-strict-mode.md => 14-strict-mode.md} (100%)
create mode 100644 docs/tut/crash-course/6-root.md
rename docs/tut/crash-course/{6-stateful-component.md => 7-stateful-component.md} (95%)
rename docs/tut/crash-course/{7-property-binding.md => 8-property-binding.md} (87%)
create mode 100644 docs/tut/crash-course/9-derived-source.md
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index dec9146..7c820ff 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -43,13 +43,15 @@ export default withMermaid({
{ text: "Components", link: "/tut/crash-course/3-components" },
{ text: "Sources", link: "/tut/crash-course/4-source" },
{ text: "Effects", link: "/tut/crash-course/5-effect" },
- { text: "Stateful Components", link: "/tut/crash-course/6-stateful-component" },
- { text: "Property Binding", link: "/tut/crash-course/7-property-binding" },
- { text: "Cleanup", link: "/tut/crash-course/8-cleanup" },
- { text: "Control Flow", link: "/tut/crash-course/9-control-flow" },
- { text: "Property Nesting", link: "/tut/crash-course/10-property-nesting" },
- { text: "Actions", link: "/tut/crash-course/11-actions" },
- { text: "Strict Mode", link: "/tut/crash-course/12-strict-mode" },
+ { text: "Root Scopes", link: "/tut/crash-course/6-root" },
+ { text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
+ { text: "Property Binding", link: "/tut/crash-course/8-property-binding" },
+ { text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
+ { text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
+ { text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
+ { text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" },
+ { text: "Actions", link: "/tut/crash-course/13-actions" },
+ { text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" }
]
},
{
diff --git a/docs/tut/advanced/reactive-scoping.md b/docs/tut/advanced/reactive-scoping.md
index f75525e..c9ed084 100644
--- a/docs/tut/advanced/reactive-scoping.md
+++ b/docs/tut/advanced/reactive-scoping.md
@@ -204,22 +204,23 @@ The above code produces a graph like so:
"primaryBorderColor": "#1B1B1F",
"lineColor": "#79B8FF",
"tertiaryColor": "#161618",
- "tertiaryBorderColor": "#fff"
+ "tertiaryBorderColor": "#1B1B1F"
}
}}%%
flowchart LR
subgraph root
+ direction LR
show
- subgraph subroot[show subroot]
+ subgraph subroot["show() subroot"]
p1[prop binding]
end
end
visible --> show
count --> p1
- show .-> subroot
+ show -.- subroot
```
This can be recreated without the `show()` control-flow function, with the
diff --git a/docs/tut/crash-course/1-introduction.md b/docs/tut/crash-course/1-introduction.md
index 371950a..ccf739a 100644
--- a/docs/tut/crash-course/1-introduction.md
+++ b/docs/tut/crash-course/1-introduction.md
@@ -15,11 +15,10 @@ worrying about manually updating UI instances.
Some of the main focuses behind Vide's design choices:
-- Concise syntax to reduce verbosity as much as possible.
+- Concise syntax.
- Being completely typecheckable.
- Independence from instance lifetimes.
-- A powerful reactive system that can update specific properties as a result of
- state changes, updates are immediate with no diffing needed.
+- Real reactivity.
## Structure Of A Vide App
@@ -33,8 +32,10 @@ called *components*.
```lua
local function App()
- return create "ScreenGui" {
- create "TextLabel" { Text = "hi" }
+ return {
+ PlayerStats(),
+ Inventory(),
+ Settings()
}
end
diff --git a/docs/tut/crash-course/8-cleanup.md b/docs/tut/crash-course/10-cleanup.md
similarity index 64%
rename from docs/tut/crash-course/8-cleanup.md
rename to docs/tut/crash-course/10-cleanup.md
index 16d611d..df4614d 100644
--- a/docs/tut/crash-course/8-cleanup.md
+++ b/docs/tut/crash-course/10-cleanup.md
@@ -41,8 +41,30 @@ when the timer component is destroyed, whether that is from unmounting the app
or if it is dynamically created by a control-flow function, which will be
covered next.
-On a related note: the reason why `mount()` is used to create your app, is so
-that any top-level components that need to be cleaned up, can be cleaned up
-when the app is later unmounted, since `mount()` runs in a reactive scope to
-track `cleanup()` calls. Vide's entire reactive system is independent from the
-life-time of instances; instances are just a side-effect of the reactive system.
+This is another reason why `mount()` is used at the top level of your app, so
+that any registered cleanups created by your app components can be ran when
+they are destroyed.
+
+The reactive graph for the above example:
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#161618"
+ }
+}}%%
+
+flowchart
+
+subgraph root
+ direction LR
+ cleanup([cleanup]) ~~~ count
+ count --> bind[text binding]
+end
+```
diff --git a/docs/tut/crash-course/9-control-flow.md b/docs/tut/crash-course/11-control-flow.md
similarity index 67%
rename from docs/tut/crash-course/9-control-flow.md
rename to docs/tut/crash-course/11-control-flow.md
index afeabfc..dcb0bbf 100644
--- a/docs/tut/crash-course/9-control-flow.md
+++ b/docs/tut/crash-course/11-control-flow.md
@@ -63,6 +63,37 @@ local function JoinMenu()
end
```
+The reactive graph for the above example:
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#1C1C1F"
+ }
+}}%%
+
+flowchart
+
+subgraph root ["mount() scope"]
+ direction LR
+ joined --> show -.- subroot
+
+ subgraph subroot ["show() scope"]
+ direction LR
+ Button
+ end
+end
+```
+
+The dotted line indicates that the new reactive scope isn't actually connected
+to the `show` on the graph, it is only managed internally through code.
+
## switch()
Similar to `show()`, `switch()`, also condtionally displays one instance at a
@@ -113,6 +144,34 @@ switch(menu) {
}
```
+The reactive graph for the above example:
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#1C1C1F"
+ }
+}}%%
+
+flowchart
+
+subgraph root ["mount() scope"]
+ direction LR
+ joined --> show -.- subroot
+
+ subgraph subroot ["switch() scope"]
+ direction LR
+ Button
+ end
+end
+```
+
## indexes()
Often, you will have a table of values that will be displayed in a similar
@@ -121,7 +180,7 @@ UI element, `indexes()` allows you to create an instance for each table index,
to display the value at that index.
```lua
-local todoList = {
+local todoList = source {
"finish the crash course",
"star vide's GitHub"
}
@@ -153,5 +212,38 @@ given source for that index is updated.
An element is only destroyed if the value of an index is set to `nil`.
+The reactive graph for the above example:
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#1C1C1F"
+ }
+}}%%
+
+flowchart
+
+subgraph root ["mount() scope"]
+ direction LR
+ todoList --> indexes -.- subroot1 & subroot2
+
+ subgraph subroot1 ["indexes() scope 1"]
+ direction LR
+ value1[todo] --> prop1["prop binding"]
+ end
+
+ subgraph subroot2 ["indexes() scope 2"]
+ direction LR
+ value2[todo] --> prop2[prop binding]
+ end
+end
+```
+
Together, these control flow functions cover the majority of cases where you
need to dynamically create and destroy parts of your UI.
diff --git a/docs/tut/crash-course/10-property-nesting.md b/docs/tut/crash-course/12-property-nesting.md
similarity index 100%
rename from docs/tut/crash-course/10-property-nesting.md
rename to docs/tut/crash-course/12-property-nesting.md
diff --git a/docs/tut/crash-course/11-actions.md b/docs/tut/crash-course/13-actions.md
similarity index 100%
rename from docs/tut/crash-course/11-actions.md
rename to docs/tut/crash-course/13-actions.md
diff --git a/docs/tut/crash-course/12-strict-mode.md b/docs/tut/crash-course/14-strict-mode.md
similarity index 100%
rename from docs/tut/crash-course/12-strict-mode.md
rename to docs/tut/crash-course/14-strict-mode.md
diff --git a/docs/tut/crash-course/4-source.md b/docs/tut/crash-course/4-source.md
index 4419b72..f8f9e86 100644
--- a/docs/tut/crash-course/4-source.md
+++ b/docs/tut/crash-course/4-source.md
@@ -24,6 +24,9 @@ count(count() + 1) -- increment count by 1
Sources can be *derived* by wrapping them in functions. A wrapped source
effectively becomes a new source.
+Derived sources should be pure functions. This is where the same output is
+always produced for the same input no matter how many times it is reran.
+
```lua
local count = source(0)
@@ -36,7 +39,8 @@ count(1)
print(text()) -- "count: 1"
```
-Derived sources should be pure functions. This is where the same output is
-always produced for the same input. As well as making source updates more
-predictable, knowing that updates are pure allows Vide to use optimizations such
-as caching, to avoid updating derived sources if their inputs are the same.
+Sources on their own aren't very special, the above can be achieved with plain
+variables. The real use for sources become apparent when used in combination
+with Vide's *reactive scopes*. When a source is read from within a reactive
+scope, it can automatically rerun the scope that reads it when the source is
+updated in the future.
diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md
index 406ccd2..22cd199 100644
--- a/docs/tut/crash-course/5-effect.md
+++ b/docs/tut/crash-course/5-effect.md
@@ -1,8 +1,8 @@
# Effects
Effects are functions that are ran in response to source updates. They are
-called effects because they cause *side-effects* when reacting to source updates
-which are pure.
+called effects because they cause *side-effects* when reacting to source
+updates.
Effects are created using `effect()`.
@@ -26,10 +26,10 @@ from inside a reactive scope will be tracked, so that if any of those sources
update, the effect will be reran too.
The callback is first ran immediately inside the `effect()` call to initially
-figure out what sources are being used.
+track sources used.
-Effects also work with derived sources, it doesn't matter how deeply nested a
-source is.
+Effects also work with derived sources, it doesn't matter how deeply nested
+inside a function a source is.
```lua
local source = vide.source
@@ -50,15 +50,7 @@ count(2)
-- "doubled count: 4" printed
```
-Derived sources should be a *pure computation*. A pure computation is one where
-the same input will always produce the same output.
-
-All observable changes to the user are considered to be side-effects of pure
-computations.
-
-Sources, derived sources, and effects form what is called a *reactive graph*.
-In the above example, the following graph is formed. Anywhere
-an update occures, everything further down the graph is updated.
+The reactive graph for the above example:
```mermaid
%%{init: {
@@ -69,25 +61,12 @@ an update occures, everything further down the graph is updated.
"primaryBorderColor": "#1B1B1F",
"lineColor": "#79B8FF",
"tertiaryColor": "#161618",
- "tertiaryBorderColor": "#161618"
+ "tertiaryBorderColor": "#fff"
}
}}%%
flowchart LR
- count --> doubled --> effect
+
+count --> effect
+
```
-
-You should not update other sources using an effect. Improper usage can lead to
-a cyclic loop in the graph, causing an infinite loop when it tries to update.
-Sources should be derived instead.
-
-## Root Reactive Scopes
-
-Effects must be created within another reactive scope. This is so that the
-effect itself can be tracked and later freed when the parent reactive scope is
-destroyed, such as from unmounting an app. The example code above will not
-actually work unless it is ran inside a root reactive scope, such as one created
-by `vide.mount(function)`. This generally isn't a concern since you can assume
-that all your components will be created within a single `mount()` call, which
-happens only once at the top level, where you put together your UI and parent it
-to a ScreenGUI.
diff --git a/docs/tut/crash-course/6-root.md b/docs/tut/crash-course/6-root.md
new file mode 100644
index 0000000..ff45184
--- /dev/null
+++ b/docs/tut/crash-course/6-root.md
@@ -0,0 +1,49 @@
+# Root Reactive Scopes
+
+Any reactive scopes created, such as one from `effect()`, must be done so within
+a "root" reactive scope. This is the main purpose of `mount()`, which you use
+once at the top level to create your app as shown in the first introduction.
+
+This is so that when the app is unmounted, it can clean up any reactive scopes
+created within it, since reactive scopes track any reactive scopes created
+within them.
+
+```lua
+local source = vide.source
+local effect = vide.effect
+
+local function App()
+ local count = source(0)
+
+ effect(function()
+ print(count())
+ end)
+end
+
+vide.mount(App) -- works!
+
+App() -- will error since effect() was not called within a reactive scope
+```
+
+The reactive graph for the above example:
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#161618"
+ }
+}}%%
+
+flowchart
+
+subgraph root
+ direction LR
+ count --> effect
+end
+```
diff --git a/docs/tut/crash-course/6-stateful-component.md b/docs/tut/crash-course/7-stateful-component.md
similarity index 95%
rename from docs/tut/crash-course/6-stateful-component.md
rename to docs/tut/crash-course/7-stateful-component.md
index 3d16279..246e050 100644
--- a/docs/tut/crash-course/6-stateful-component.md
+++ b/docs/tut/crash-course/7-stateful-component.md
@@ -30,6 +30,9 @@ end
Above is an example of a counter component, that when clicked, will increment
its internal count, and automatically update its text to reflect that count.
+Making a property update based on a source is also called *property
+binding*.
+
Each instance of `Counter()` will maintain its own independent count, since the
count source is created inside the scope of the component.
diff --git a/docs/tut/crash-course/7-property-binding.md b/docs/tut/crash-course/8-property-binding.md
similarity index 87%
rename from docs/tut/crash-course/7-property-binding.md
rename to docs/tut/crash-course/8-property-binding.md
index 9488fba..c8985c9 100644
--- a/docs/tut/crash-course/7-property-binding.md
+++ b/docs/tut/crash-course/8-property-binding.md
@@ -2,9 +2,7 @@
Explicitly creating effects to update properties can become verbose when there
are a lot of properties to update. Vide provides a way to *implicitly* create
-an effect to update properties on source update. This is also known as
-*property binding*, since changes to a source will automatically update the
-property.
+an effect to update properties on source update.
```lua
local create = vide.create
@@ -40,7 +38,9 @@ that are updated.
## Children Binding
-Children can also be set in a similar manner.
+Children can also be set in a similar manner. Sources bound to properties can
+return an instance or an array of instances. Vide will automatically unparent
+removed instances and parent new instances.
```lua
local items = source {
diff --git a/docs/tut/crash-course/9-derived-source.md b/docs/tut/crash-course/9-derived-source.md
new file mode 100644
index 0000000..52574eb
--- /dev/null
+++ b/docs/tut/crash-course/9-derived-source.md
@@ -0,0 +1,95 @@
+# Derived Sources
+
+We have seen the basic way to derive a source:
+
+```lua
+local count = source(0)
+
+local text = function()
+ return "count: " .. tostring(count())
+end
+
+print(text()) -- "count: 0"
+count(1)
+print(text()) -- "count: 1"
+```
+
+However, in some cases where this source could be used by multiple effects at
+the same time, the function wrapping the source will needlessly rerun to convert
+the count into a string for each effect using it.
+
+```lua
+local source = vide.source
+local effect = vide.effect
+
+local count = source(0)
+
+local text = function()
+ print "ran"
+ return "count: " .. tostring(count())
+end
+
+effect(function()
+ text() -- prints "ran"
+end)
+
+effect(function()
+ text() -- prints "ran" again
+end)
+```
+
+To avoid this, you can use `derive()` to derive a new source instead. This will
+run a callback in a new reactive scope only when a dependent source has updated.
+Reading this derived source multiple times will just return a cached result from
+when it last updated.
+
+```lua
+local source = vide.source
+local derive = vide.derive
+local effect = vide.effect
+
+local count = source(0)
+
+local text = derive(function()
+ print "ran"
+ return "count: " .. tostring(count())
+end)
+
+effect(function()
+ text() -- prints "ran"
+end)
+
+effect(function()
+ text() -- does not print
+end)
+```
+
+Deriving a source in this manner is similar to creating an effect to update
+another source. You should never manually do this using an effect however,
+improper usage could accidently create infinite loops in the reactive graph.
+Always favour deriving when you need one source to update based on another.
+
+`derive()` must also be used within a root reactive scope, just like `effect()`.
+
+The reactive graph for the above example:
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#161618"
+ }
+}}%%
+
+flowchart
+
+subgraph root
+ direction LR
+ count --> text --> effect1 & effect2
+end
+```
From 46937c69790b6ffdcf008a34eea143400a590f0f Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Thu, 28 Sep 2023 16:39:25 +0100
Subject: [PATCH 020/116] Update docs
---
docs/api/strict-mode.md | 5 +-
docs/tut/advanced/reactive-scoping.md | 6 +-
docs/tut/crash-course/1-introduction.md | 2 +
docs/tut/crash-course/10-cleanup.md | 8 +--
docs/tut/crash-course/11-control-flow.md | 67 +++++++++++++------
docs/tut/crash-course/12-property-nesting.md | 2 +-
docs/tut/crash-course/13-actions.md | 15 ++---
docs/tut/crash-course/14-strict-mode.md | 8 ++-
docs/tut/crash-course/2-creation.md | 15 +----
docs/tut/crash-course/3-components.md | 37 ++++++----
docs/tut/crash-course/4-source.md | 7 +-
docs/tut/crash-course/5-effect.md | 31 ++-------
docs/tut/crash-course/6-root.md | 29 ++++++--
docs/tut/crash-course/7-stateful-component.md | 10 ++-
docs/tut/crash-course/8-property-binding.md | 14 ++--
docs/tut/crash-course/9-derived-source.md | 17 +++--
16 files changed, 157 insertions(+), 116 deletions(-)
diff --git a/docs/api/strict-mode.md b/docs/api/strict-mode.md
index 35de4d8..958672e 100644
--- a/docs/api/strict-mode.md
+++ b/docs/api/strict-mode.md
@@ -22,8 +22,9 @@ Currently, strict mode will:
6. Checks for duplicate nested properties at same depth.
7. Better error reporting and stack traces + creation traces of property bindings.
-By rerunning sources and effects, any side-effects are made more apparent.
-This also helps ensure that cleanups are being handled correctly.
+By rerunning derived sources and effects twice each time they update,it helps
+ensure that derived source computations are pure, and that any
+cleanups made in derived sources or effects are done correctly.
Accidental yielding within reactive scopes can break Vide's reactive graph,
which strict mode can catch.
diff --git a/docs/tut/advanced/reactive-scoping.md b/docs/tut/advanced/reactive-scoping.md
index c9ed084..20872cf 100644
--- a/docs/tut/advanced/reactive-scoping.md
+++ b/docs/tut/advanced/reactive-scoping.md
@@ -60,7 +60,7 @@ This code will produce a graph that looks like so:
}
}}%%
-flowchart
+graph
subgraph root
forename & surname --> name
name --> effect
@@ -145,7 +145,7 @@ This code produces a graph like so:
}
}}%%
-flowchart LR
+graph LR
subgraph root
counters --> indexes
@@ -208,7 +208,7 @@ The above code produces a graph like so:
}
}}%%
-flowchart LR
+graph LR
subgraph root
direction LR
show
diff --git a/docs/tut/crash-course/1-introduction.md b/docs/tut/crash-course/1-introduction.md
index ccf739a..604f7ae 100644
--- a/docs/tut/crash-course/1-introduction.md
+++ b/docs/tut/crash-course/1-introduction.md
@@ -5,6 +5,8 @@ Vide.
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
+This tutorial assumes familiarity with Luau and Roblox GUI.
+
## Why Vide?
Creating UI is a slow and tedious process. The purpose of Vide is to make UI
diff --git a/docs/tut/crash-course/10-cleanup.md b/docs/tut/crash-course/10-cleanup.md
index df4614d..4f62233 100644
--- a/docs/tut/crash-course/10-cleanup.md
+++ b/docs/tut/crash-course/10-cleanup.md
@@ -6,7 +6,7 @@ is used to register a cleanup callback for the next time the reactive scope
it is called in re-runs.
```lua
-locla mount = vide.mount
+local mount = vide.mount
local source = vide.source
local cleanup = vide.cleanup
@@ -60,11 +60,11 @@ The reactive graph for the above example:
}
}}%%
-flowchart
+graph
-subgraph root
+subgraph mount
direction LR
cleanup([cleanup]) ~~~ count
- count --> bind[text binding]
+ count --> bind["effect (text binding)"]
end
```
diff --git a/docs/tut/crash-course/11-control-flow.md b/docs/tut/crash-course/11-control-flow.md
index dcb0bbf..e5430b8 100644
--- a/docs/tut/crash-course/11-control-flow.md
+++ b/docs/tut/crash-course/11-control-flow.md
@@ -11,7 +11,7 @@ will update when the input source updates.
Control flow functions are special, because they run their components in a new
reactive scope, which can be destroyed independently of the reactive scope that
called the control flow function itself. This means that parts of your app can
-be independently created then destroyed and cleaned.
+be independently created then destroyed.
## show()
@@ -78,21 +78,24 @@ The reactive graph for the above example:
}
}}%%
-flowchart
+graph
-subgraph root ["mount() scope"]
+subgraph root["mount() scope"]
direction LR
joined --> show -.- subroot
- subgraph subroot ["show() scope"]
+ subgraph subroot["show() scope"]
direction LR
Button
end
end
```
-The dotted line indicates that the new reactive scope isn't actually connected
-to the `show` on the graph, it is only managed internally through code.
+`show()` will implicitly create an effect depending on `joined`, which can be
+seen as `show` on the graph. This effect manages, and can create or destroy
+a separate reactive scope seen as `show() scope` on the graph. The dotted line
+indicates that it isn't actually connected, only indirectly managed through
+code.
## switch()
@@ -135,11 +138,11 @@ The switch can map any value to any component.
```lua
type ActiveMenu = "none" | "inventory" | "shop" | "settings"
-local menu = source "none"
+local menu = source "inventory"
switch(menu) {
inventory = InventoryMenu,
- shop = ShopMenu.
+ shop = ShopMenu,
settings = SettingsMenu
}
```
@@ -159,25 +162,25 @@ The reactive graph for the above example:
}
}}%%
-flowchart
+graph
-subgraph root ["mount() scope"]
+subgraph root["mount() scope"]
direction LR
- joined --> show -.- subroot
+ menu --> switch -.- subroot
- subgraph subroot ["switch() scope"]
+ subgraph subroot["switch() scope"]
direction LR
- Button
+ Menu
end
end
```
## indexes()
-Often, you will have a table of values that will be displayed in a similar
+Often, you will have a table of values with each value displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
-UI element, `indexes()` allows you to create an instance for each table index,
-to display the value at that index.
+UI element, `indexes()` allows you to create elements for each table index, to
+display the value at that index.
```lua
local todoList = source {
@@ -204,13 +207,22 @@ end
TodoList { list = todoList }
```
-For each unique index in the passed table, the transform function will be called
-with 1. a source containing the value of the index, 2. the index itself.
+For each index in the given source table, the given function will be called
+with:
+
+1. a source containing the value of the index
+2. the index itself
When the value at an index is changed, the function is not reran. Instead, the
given source for that index is updated.
-An element is only destroyed if the value of an index is set to `nil`.
+Any time the input source table is updated, the given function will be ran for
+any newly added indexes, while any removed indexes (indexes now with a `nil`
+value), will have its corresponding reactive scope destroyed to clean up that
+element.
+
+`indexes()` is said to *map* each table index to a new UI element that can
+update to display the current value at that index.
The reactive graph for the above example:
@@ -227,7 +239,7 @@ The reactive graph for the above example:
}
}}%%
-flowchart
+graph
subgraph root ["mount() scope"]
direction LR
@@ -245,5 +257,20 @@ subgraph root ["mount() scope"]
end
```
+One thing to note regarding table sources, is that when you edit a table in a
+source, you must set that table again to actually update the source.
+
+```lua
+local src = source { 1, 2 }
+local data = src()
+table.insert(data, 3) -- no effects will run
+src(data) -- effects will run
+```
+
Together, these control flow functions cover the majority of cases where you
need to dynamically create and destroy parts of your UI.
+
+If you need to do something that these control flow functions cannot, you can
+always use `mount()` within an effect to dynamically create and destroy
+components on your own terms. Just remember to use `cleanup()` to unmount when
+the effect reruns.
diff --git a/docs/tut/crash-course/12-property-nesting.md b/docs/tut/crash-course/12-property-nesting.md
index ea2ae43..d8c44ce 100644
--- a/docs/tut/crash-course/12-property-nesting.md
+++ b/docs/tut/crash-course/12-property-nesting.md
@@ -77,7 +77,7 @@ be parented.
```lua
type Children = {
- -- allows us to also optionally pass a source that returns an array of children instead
+ -- also can optionally pass a source that returns an array of children too
Children = Array | () -> Array
}
diff --git a/docs/tut/crash-course/13-actions.md b/docs/tut/crash-course/13-actions.md
index 79ca7c7..06b431d 100644
--- a/docs/tut/crash-course/13-actions.md
+++ b/docs/tut/crash-course/13-actions.md
@@ -1,8 +1,7 @@
# Actions
Actions in Vide are special callbacks that you can pass along with properties,
-which will be called when those properties are being processed with the instance
-being assigned to, allowing you to run custom code.
+to run some code on an instance receiving them.
```lua
local action = vide.action
@@ -20,24 +19,22 @@ create "TextLabel" {
-- will print "test"
```
-Actions can be wrapped with functions to re-use specific behaviors. Below is
-an example of an action used to listen for property changes:
+Actions can be wrapped with functions for reuse. Below is an example of an
+action used to listen for property changes:
```lua
local action = vide.action
local cleanup = vide.cleanup
-local function changed(property: string, callback: (new) -> ())
+local function changed(prop: string, callback: (new) -> ())
return action(function(instance)
- local con = instance:GetPropertyChangedSignal(property):Connect(function()
+ local connection = instance:GetPropertyChangedSignal(prop):Connect(function()
callback(instance[property])
end)
-- remember to clean up the connection when the reactive scope the action
-- is ran in is destroyed, so the instance can be garbage collected
- cleanup(function()
- con:Disconnect()
- end)
+ cleanup(connection)
end)
end
diff --git a/docs/tut/crash-course/14-strict-mode.md b/docs/tut/crash-course/14-strict-mode.md
index 3cb09e6..ca8465a 100644
--- a/docs/tut/crash-course/14-strict-mode.md
+++ b/docs/tut/crash-course/14-strict-mode.md
@@ -5,9 +5,13 @@ be set with `vide.strict = true` once when you first require Vide. Strict mode
will add extra safety checks and emit better error traces, particularly when
errors occur in property bindings.
+Strict mode is automatically enabled when Vide is required in O0 or O1
+optimization (default studio level). You can `vide.strict = false` if you do not
+want this.
+
Strict mode will run derived sources and effects twice each time they update.
-This is to help identify improper cleanup of side-effects and ensure that pure
-computations are actually pure.
+This is to help ensure that derived source computations are pure, and that any
+cleanups made in derived sources or effects are done correctly.
```lua
local source = vide.source
diff --git a/docs/tut/crash-course/2-creation.md b/docs/tut/crash-course/2-creation.md
index 68e3007..e66f987 100644
--- a/docs/tut/crash-course/2-creation.md
+++ b/docs/tut/crash-course/2-creation.md
@@ -45,19 +45,10 @@ Assign a value to a string key to set a property, and assign a value to a
number key to set a child. Events can be connected to by assigning a function
to a string key.
-You can also use a shorthand to create datatypes instead of explicitly typing
-out the class name and constructor. The table will be unpacked into the `.new()`
-constructor of the property's type.
-
-```lua
-create "Frame" {
- AnchorPoint = { 0.5, 1 },
- UDim2 = { 0.5, 0, 0.5, 0 }
-}
-```
-
+::: warning
When creating an instance with no properties, it is important to not forget to
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
To be clear, `create "Frame"` returns a *function* which is a constructor for
that class, not an instance of that class. This would result in you attempting
-to parent a function instead of an instance which is not the correct behavior.
+to parent a function instead of an instance which is not correct.
+:::
diff --git a/docs/tut/crash-course/3-components.md b/docs/tut/crash-course/3-components.md
index 3050cf8..a9f38d9 100644
--- a/docs/tut/crash-course/3-components.md
+++ b/docs/tut/crash-course/3-components.md
@@ -1,9 +1,11 @@
# Components
-Components are custom-made reusable pieces of UI made from other pieces of UI.
+A component is a function that creates and returns a piece of UI.
-By using components you can make your application more modular and better
-organized.
+This is a way to separate your app into small chunks that you can reuse and put
+together.
+
+::: code-group
```lua [Button.luau]
local create = vide.create
@@ -15,11 +17,14 @@ local function Button(props: {
})
return create "TextButton" {
BackgroundColor3 = Color3.fromRGB(50, 50, 50),
+ TextColor3 = Color3.fromRGB(255, 255, 255),
Size = UDim2.fromOffset(200, 150),
Position = props.Position,
Text = props.Text,
- Activated = props.Activated
+ Activated = props.Activated,
+
+ create "UICorner" {}
}
end
@@ -36,10 +41,17 @@ local function App()
return create "ScreenGui" {
Button {
Position = UDim2.fromOffset(200, 200),
- Text = "click me!",
-
+ Text = "back",
Activated = function()
- print "clicked"
+ print "go to previous page"
+ end
+ },
+
+ Button {
+ Position = UDim2.fromOffset(400, 200),
+ Text = "next",
+ Activated = function()
+ print "go to next page"
end
}
}
@@ -48,8 +60,9 @@ end
mount(App, game.StarterGui)
```
-Above is a simple example of a button component with a set color and size,
-being reused across files.
+:::
+
+Above is a simple example of a button component being used across files.
A single parameter `props` is used to pass properties to the component.
@@ -57,8 +70,8 @@ Components allow you to *encapsulate* behavior. You can only modify the
component in ways that you allow in the component, through the `props` parameter.
To create a new button all you must do is call the `Button` function, passing in
-values through props. This saves having to create and set every property each
-time. Also, when updating the button component in future, any changes to the
-button file will be seen anywhere the button is used in your app.
+values. This saves having to create and set every property each time. Also, when
+updating the button component in future, any changes to the button file will be
+seen anywhere the button is used in your app.
This can be extended to much more complicated UI.
diff --git a/docs/tut/crash-course/4-source.md b/docs/tut/crash-course/4-source.md
index f8f9e86..f5a848a 100644
--- a/docs/tut/crash-course/4-source.md
+++ b/docs/tut/crash-course/4-source.md
@@ -1,10 +1,9 @@
# Sources
-*Sources* in Vide are special objects that store a single value. They are the
-core of reactivity in Vide. Each source represents a source of data, and they
-can be composed and derived to create new sources of data.
+Sources are special objects that store a single value. They are the core of
+Vide's reactivity. They are called sources because they act as sources of data.
-A source in Vide can be created using `source()`.
+A source can be created using `source()`.
```lua
local source = vide.source
diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md
index 22cd199..9f0dd6b 100644
--- a/docs/tut/crash-course/5-effect.md
+++ b/docs/tut/crash-course/5-effect.md
@@ -21,12 +21,9 @@ count(1)
-- "count: 1" printed
```
-The callback given to `effect()` is ran in a *reactive scope*. Any source read
-from inside a reactive scope will be tracked, so that if any of those sources
-update, the effect will be reran too.
-
-The callback is first ran immediately inside the `effect()` call to initially
-track sources used.
+The callback given to `effect()` is initially ran immediately in a
+*reactive scope*. Any source read from inside a reactive scope will be tracked,
+so that if any of those sources update, the effect will be reran too.
Effects also work with derived sources, it doesn't matter how deeply nested
inside a function a source is.
@@ -50,23 +47,5 @@ count(2)
-- "doubled count: 4" printed
```
-The reactive graph for the above example:
-
-```mermaid
-%%{init: {
- "theme": "base",
- "themeVariables": {
- "primaryColor": "#1B1B1F",
- "primaryTextColor": "#fff",
- "primaryBorderColor": "#1B1B1F",
- "lineColor": "#79B8FF",
- "tertiaryColor": "#161618",
- "tertiaryBorderColor": "#fff"
- }
-}}%%
-
-flowchart LR
-
-count --> effect
-
-```
+If a source is updated with the same value it already had, it will not rerun
+effects depending on it.
diff --git a/docs/tut/crash-course/6-root.md b/docs/tut/crash-course/6-root.md
index ff45184..2b2d7ad 100644
--- a/docs/tut/crash-course/6-root.md
+++ b/docs/tut/crash-course/6-root.md
@@ -1,7 +1,7 @@
# Root Reactive Scopes
-Any reactive scopes created, such as one from `effect()`, must be done so within
-a "root" reactive scope. This is the main purpose of `mount()`, which you use
+Any reactive scopes created, such as by `effect()`, must be done so within a
+"root" reactive scope. This is the main purpose of `mount()`, which you use
once at the top level to create your app as shown in the first introduction.
This is so that when the app is unmounted, it can clean up any reactive scopes
@@ -25,7 +25,18 @@ vide.mount(App) -- works!
App() -- will error since effect() was not called within a reactive scope
```
-The reactive graph for the above example:
+Mounting returns a function that when called will destroy any reactive scopes
+created during the `mount()` call.
+
+```lua
+local unmount = mount(App)
+
+unmount()
+```
+
+Vide's reactivity can be represented graphically, as a *reactive graph*.
+
+The reactive graph for the above example looks like so:
```mermaid
%%{init: {
@@ -40,10 +51,18 @@ The reactive graph for the above example:
}
}}%%
-flowchart
+graph
-subgraph root
+subgraph root["mount"]
direction LR
count --> effect
end
```
+
+When the `mount` scope is destroyed, the `effect` scope will also be destroyed
+since it was created within it.
+
+You don't need to worry about ensuring all your effects are created within a
+root scope, since you should be creating all your UI and corresponding effects
+within a top-level `mount()` call that puts all your UI together. So it is safe
+to assume that any effect you create will be created under this top level scope.
diff --git a/docs/tut/crash-course/7-stateful-component.md b/docs/tut/crash-course/7-stateful-component.md
index 246e050..8de8209 100644
--- a/docs/tut/crash-course/7-stateful-component.md
+++ b/docs/tut/crash-course/7-stateful-component.md
@@ -5,6 +5,8 @@ A stateful component is a component that stores and displays some data.
Stateful components in Vide are created using sources and effects - sources to
store the data, and effects to display the data.
+## Internal State
+
```lua
local create = vide.create
local source = vide.source
@@ -30,12 +32,14 @@ end
Above is an example of a counter component, that when clicked, will increment
its internal count, and automatically update its text to reflect that count.
-Making a property update based on a source is also called *property
+Making a property update based on a source is also referred to as *property
binding*.
Each instance of `Counter()` will maintain its own independent count, since the
count source is created inside the scope of the component.
+## External State
+
External sources can also be passed into components for them to use.
```lua
@@ -65,5 +69,5 @@ count(1) -- the Counter component will update to display this count
```
Sources can be created internally or passed in from externally, there are no
-restrictions on how they are used as long as the effect is created within a
-reactive scope so that it can be tracked.
+restrictions on how they are used as long as the effect using it is created
+within a reactive scope so that it can be cleaned up later.
diff --git a/docs/tut/crash-course/8-property-binding.md b/docs/tut/crash-course/8-property-binding.md
index c8985c9..8304565 100644
--- a/docs/tut/crash-course/8-property-binding.md
+++ b/docs/tut/crash-course/8-property-binding.md
@@ -20,6 +20,7 @@ local function Counter()
count(count() + 1)
end
}
+end
```
This example is equivalent to the example seen on the previous page.
@@ -32,15 +33,16 @@ Just like effects, the function is ran immediately in a reactive scope to set
the property initially and determine what sources are being depended on.
This allows you as the programmer to not need to manually update UI as the state
-of your program changes. You just define how the data maps to UI, and Vide's
-reactive system will automatically update any properties depending on sources
-that are updated.
+of your program changes. You just define how the data sources map to UI, and
+Vide's reactive system will automatically update any properties depending on
+those sources that were updated.
## Children Binding
-Children can also be set in a similar manner. Sources bound to properties can
-return an instance or an array of instances. Vide will automatically unparent
-removed instances and parent new instances.
+Children can also be set in a similar manner. A source passed as a child (passed
+with a number key instead of string key) can return an instance or an array of
+instances. Vide will automatically unparent removed instances and parent new
+instances when that source's stored instances change.
```lua
local items = source {
diff --git a/docs/tut/crash-course/9-derived-source.md b/docs/tut/crash-course/9-derived-source.md
index 52574eb..17e3b77 100644
--- a/docs/tut/crash-course/9-derived-source.md
+++ b/docs/tut/crash-course/9-derived-source.md
@@ -60,17 +60,15 @@ effect(function()
end)
effect(function()
- text() -- does not print
+ text() -- does not print, returns cached value
end)
```
-Deriving a source in this manner is similar to creating an effect to update
-another source. You should never manually do this using an effect however,
-improper usage could accidently create infinite loops in the reactive graph.
-Always favour deriving when you need one source to update based on another.
-
`derive()` must also be used within a root reactive scope, just like `effect()`.
+If the recalculated value is the same as the old value, the derived source will
+not rerun the effects using it.
+
The reactive graph for the above example:
```mermaid
@@ -86,10 +84,15 @@ The reactive graph for the above example:
}
}}%%
-flowchart
+graph
subgraph root
direction LR
count --> text --> effect1 & effect2
end
```
+
+Deriving a source in this manner is similar to creating an effect to update
+another source. You should never manually do this using an effect however,
+improper usage could accidently create infinite loops in the reactive graph.
+Always favour deriving when you need one source to update based on another.
From c03fb0577933a3eec6e579b7ff7ae54d49838d3d Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Thu, 28 Sep 2023 16:46:32 +0100
Subject: [PATCH 021/116] Make `changed()` run initially
---
CHANGELOG.md | 1 +
docs/api/creation.md | 20 ++++++++++++++++++++
src/changed.luau | 2 ++
test/tests.luau | 2 +-
4 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3900dad..2a576af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Reactive scopes created within reactive scopes are now destroyed on rerun.
- `untrack()` can be called outside of reactive scopes.
+- `changed()` will also run its callback with the initial property value.
### Fixed
diff --git a/docs/api/creation.md b/docs/api/creation.md
index f3f51c8..b4576b8 100644
--- a/docs/api/creation.md
+++ b/docs/api/creation.md
@@ -154,3 +154,23 @@ instances.
changed("Text", output)
}
```
+
+## changed()
+
+A wrapper for `action()` to listen for property changes.
+
+- **Type**
+
+ ```lua
+ function changed(property: string, callback: (...unknown) -> ()): Action
+ ```
+
+- **Details**
+
+ Will run the given callback any time the property is changed, as well as
+ when the action is initially run.
+
+ The changed connection is disconnected when the reactive scope the action is
+ ran in is destroyed.
+
+ Runs with an action priority of 1.
diff --git a/src/changed.luau b/src/changed.luau
index 5d439c6..519a554 100644
--- a/src/changed.luau
+++ b/src/changed.luau
@@ -12,6 +12,8 @@ local function changed(property: string, callback: (T) -> ())
cleanup(function()
con:Disconnect()
end)
+
+ callback((instance :: any)[property])
end)
end
diff --git a/test/tests.luau b/test/tests.luau
index 7e95cca..a82712b 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -1806,7 +1806,7 @@ TEST("changed()", wrap_root(function()
changed("Text", output)
}
- --CHECK(output() == "a")
+ CHECK(output() == "a")
text.Text = "b"
CHECK(output() == "b")
end
From 7128b9a58b7b1e162fa74b6445720c221acf51eb Mon Sep 17 00:00:00 2001
From: aaron <83140718+centau@users.noreply.github.com>
Date: Sat, 30 Sep 2023 01:32:14 +0100
Subject: [PATCH 022/116] Bump version to `v0.1.1`
---
CHANGELOG.md | 6 ++++++
src/init.luau | 2 +-
wally.toml | 4 ++--
3 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a576af..558b398 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
+-
+
+--------------------------------------------------------------------------------
+
+## [0.1.1] - 2023-09-30
+
### Added
- `cleanup()` accepts objects with a `Destroy()` or `Disconnect()` interface.
diff --git a/src/init.luau b/src/init.luau
index 62b6281..687f417 100644
--- a/src/init.luau
+++ b/src/init.luau
@@ -1,6 +1,6 @@
--------------------------------------------------------------------------------
-- vide.luau
--- v0.1.0
+-- v0.1.1
--------------------------------------------------------------------------------
if not game then script = require "test/relative-string" end
diff --git a/wally.toml b/wally.toml
index f118dc8..d4d48ed 100644
--- a/wally.toml
+++ b/wally.toml
@@ -2,7 +2,7 @@
name = "centau/vide"
description = "A reactive Luau library for creating UI. "
license = "MIT"
-version = "0.1.0"
+version = "0.1.1"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
include = ["default.project.json", "LICENSE", "src"]
@@ -16,4 +16,4 @@ exclude = [
"CHANGELOG.md",
"README.md",
"todo.md"
-]
\ No newline at end of file
+]
From da85cbbac85f7b0040812ab3ab4c62ee1eae467d Mon Sep 17 00:00:00 2001
From: Alo <70312917+Aloroid@users.noreply.github.com>
Date: Sat, 7 Oct 2023 00:16:01 +0200
Subject: [PATCH 023/116] Fix docs typo
minor spelling mistake
---
docs/api/creation.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/api/creation.md b/docs/api/creation.md
index b4576b8..8e67478 100644
--- a/docs/api/creation.md
+++ b/docs/api/creation.md
@@ -69,7 +69,7 @@ Creates a new UI element, applying any given properties.
- **index is number:**
- **value is action:** run action
- **value is table:** recurse table
- - **value is functon:** create effect to update children
+ - **value is function:** create effect to update children
- **value is instance:** set instance as child
- **Example**
From 8218e4702fc525e5491ea68d0479458cd867cd17 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Wed, 25 Oct 2023 15:49:35 +0100
Subject: [PATCH 024/116] Fix diamond graphs
---
src/graph.luau | 83 +++++++++++++++++++++++++++++++++---------------
test/tests.luau | 84 +++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 140 insertions(+), 27 deletions(-)
diff --git a/src/graph.luau b/src/graph.luau
index bb42fbb..0c0bf21 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -181,47 +181,80 @@ local function evaluate_node(node: Node)
return cur_value ~= new_value -- node has changed value
end
-local function update_from(node: StartNode, n0: number)
- if not node[1] then return end
+-- local function update_from(node: StartNode, n0: number)
+-- if not node[1] then return end
- local n = n0
+-- local n = n0
- -- unparent all children and queue for eval
- do
- local child = node[1]
- while child do
- --assert(child.parents.owner)
- unparent(child)
- n += 1
- update_queue[n] = child
- child = node[1]
- end
+-- -- unparent all children and queue for eval
+-- do
+-- local child = node[1]
+-- while child do
+-- --assert(child.parents.owner)
+-- unparent(child)
+-- n += 1
+-- update_queue[n] = child
+-- child = node[1]
+-- end
+-- end
+
+-- update_queue.n = n
+
+-- -- evaluate all queued children
+-- for i = n0 + 1, n do
+-- local child = update_queue[i]
+-- assert(type(child.effect) == "function")
+
+-- if evaluate_node(child) then
+-- update_from(child, n)
+-- end
+
+-- update_queue[i] = false :: any -- false instead of nil to avoid sparse
+-- end
+
+-- update_queue.n = n0
+-- end
+
+-- local function update(node: StartNode)
+-- update_from(node, update_queue.n)
+-- end
+
+local function queue_children(node: StartNode)
+ local i = update_queue.n
+ local child = node[1]
+ while child do
+ --assert(child.parents.owner)
+ unparent(child)
+ i += 1
+ update_queue[i] = child
+ child = node[1]
end
+ update_queue.n = i
+end
- update_queue.n = n
+local function update(root: StartNode)
+ local n0 = update_queue.n
+ queue_children(root)
- -- evaluate all queued children
- for i = n0 + 1, n do
- local child = update_queue[i]
- assert(type(child.effect) == "function")
+ local i = n0 + 1
+ while i <= update_queue.n do
+ local node = update_queue[i]
+ assert(node.effect)
- if evaluate_node(child) then
- update_from(child, n)
+ if evaluate_node(node) then
+ queue_children(node)
end
update_queue[i] = false :: any -- false instead of nil to avoid sparse
+ i += 1
end
update_queue.n = n0
end
-local function update(node: StartNode)
- update_from(node, update_queue.n)
-end
-
local function track(node: StartNode)
local scope = get_scope()
- if scope and type(scope.effect) == "function" then -- do not track nodes with no effect
+ if scope and scope.effect then -- do not track nodes with no effect
add_child(node, scope)
end
end
diff --git a/test/tests.luau b/test/tests.luau
index a82712b..0c72f90 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -1,5 +1,5 @@
local testkit = require("test/testkit")
-local TEST, CASE, CHECK, FINISH = testkit.test()
+local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
local mock = require "test/mock"
local Instance, Signal = mock.Instance, mock.Signal
@@ -33,6 +33,8 @@ local NIL = nil :: any
vide.strict = false
+--SKIP "graph edge cases"
+
TEST("graph", function()
local create_node = graph.create_node
local track = graph.track
@@ -1907,7 +1909,85 @@ TEST("nested effects cases", function()
root(App)
end)
-vide.strict = true
+TEST("graph edge cases", wrap_root(function()
+ local source = vide.source
+ local derive = vide.derive
+ local effect = vide.effect
+
+ do CASE "diamond A,B,C,D"
+ --[[
+
+ a > b > d
+ > c >
+
+ ]]
+
+ local a = source(0)
+
+ local b = derive(function() return (a() % 2 == 0) and 1 or 0 end)
+ local c = derive(function() return a() * 2 end)
+ local d = derive(function() return b() + c() end)
+
+ local count = { b = 0, c = 0, d = 0 }
+ effect(function() b(); count.b += 1 end)
+ effect(function() c(); count.c += 1 end)
+ effect(function() d(); count.d += 1 end)
+
+ a(1)
+ CHECK(count.b == 2)
+ CHECK(count.c == 2)
+ CHECK(count.d == 2)
+ CHECK(d() == 2)
+
+ a(3)
+ CHECK(count.b == 2)
+ CHECK(count.c == 3)
+ CHECK(count.d == 3)
+ CHECK(d() == 6)
+ end
+
+ do CASE "diamond A,B,C,D,E"
+ --[[
+
+ a > b > > e
+ > c > d >
+
+ ]]
+
+ local a = source(0)
+
+ local b = derive(function() print "ran b"; return (a() % 2 == 0) and 1 or 0 end)
+ local c = derive(function() print "ran c"; return a() * 2 end)
+ local d = derive(function() print "ran d"; return c() * 2 end)
+ local e = derive(function() print "ran e"; return b() + c() end)
+
+ local count = { b = 0, c = 0, d = 0, e = 0 }
+ effect(function() b(); count.b += 1 end)
+ effect(function() c(); count.c += 1 end)
+ effect(function() d(); count.d += 1 end)
+ effect(function() d(); count.e += 1 end)
+
+ a(1)
+
+ print(e())
+ CHECK(count.b == 2)
+ CHECK(count.c == 2)
+ CHECK(count.d == 2)
+ CHECK(count.e == 2)
+ CHECK(e() == 4) -- todo: solve e evaluating before d
+
+ a(3)
+ CHECK(count.b == 2)
+ CHECK(count.c == 3)
+ CHECK(count.d == 3)
+ CHECK(count.e == 3)
+ CHECK(e() == 12)
+ end
+
+ do CASE "repeated read"
+
+ end
+end))
TEST("strict", wrap_root(function()
vide.strict = true
From 15255855c3bad0f0af75fdf84297cdbc005a8ead Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Wed, 25 Oct 2023 16:58:29 +0100
Subject: [PATCH 025/116] Add repeated read test
---
src/graph.luau | 38 --------------------------------------
test/tests.luau | 9 ++++++++-
2 files changed, 8 insertions(+), 39 deletions(-)
diff --git a/src/graph.luau b/src/graph.luau
index 0c0bf21..ab0be0b 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -181,44 +181,6 @@ local function evaluate_node(node: Node)
return cur_value ~= new_value -- node has changed value
end
--- local function update_from(node: StartNode, n0: number)
--- if not node[1] then return end
-
--- local n = n0
-
--- -- unparent all children and queue for eval
--- do
--- local child = node[1]
--- while child do
--- --assert(child.parents.owner)
--- unparent(child)
--- n += 1
--- update_queue[n] = child
--- child = node[1]
--- end
--- end
-
--- update_queue.n = n
-
--- -- evaluate all queued children
--- for i = n0 + 1, n do
--- local child = update_queue[i]
--- assert(type(child.effect) == "function")
-
--- if evaluate_node(child) then
--- update_from(child, n)
--- end
-
--- update_queue[i] = false :: any -- false instead of nil to avoid sparse
--- end
-
--- update_queue.n = n0
--- end
-
--- local function update(node: StartNode)
--- update_from(node, update_queue.n)
--- end
-
local function queue_children(node: StartNode)
local i = update_queue.n
local child = node[1]
diff --git a/test/tests.luau b/test/tests.luau
index 0c72f90..b431a90 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -1969,7 +1969,6 @@ TEST("graph edge cases", wrap_root(function()
a(1)
- print(e())
CHECK(count.b == 2)
CHECK(count.c == 2)
CHECK(count.d == 2)
@@ -1985,7 +1984,15 @@ TEST("graph edge cases", wrap_root(function()
end
do CASE "repeated read"
+ local a = source(0)
+ local b = derive(function() return a() + a() end)
+ local count = 0
+ effect(function() b(); count += 1 end)
+
+ a(1)
+ CHECK(b() == 2)
+ CHECK(count == 2)
end
end))
From aca08709b6ecaee21dbb0586d659e95eb20fd800 Mon Sep 17 00:00:00 2001
From: aaron <83140718+centau@users.noreply.github.com>
Date: Wed, 25 Oct 2023 20:02:40 +0100
Subject: [PATCH 026/116] Update tests
---
test/benchmark.luau | 7 ++++---
test/tests.luau | 31 -------------------------------
2 files changed, 4 insertions(+), 34 deletions(-)
diff --git a/test/benchmark.luau b/test/benchmark.luau
index f3c39b6..aac9071 100644
--- a/test/benchmark.luau
+++ b/test/benchmark.luau
@@ -132,22 +132,23 @@ ROOT_BENCH("update 1->1->1->1...1000 graph", function()
end
end)
+-- todo: crashes at 1k
-- todo: repeat with batching
ROOT_BENCH("update 1000->1 graph", function()
local srcs = {}
- for i = 1, 1000 do
+ for i = 1, 800 do
srcs[i] = source(0)
end
derive(function()
- for i = 1, 1000 do
+ for i = 1, 800 do
srcs[i]()
end
return false
end)
for i = 1, START(1) do
- for idx = 1, 1000 do
+ for idx = 1, 800 do
srcs[idx](i)
end
end
diff --git a/test/tests.luau b/test/tests.luau
index b431a90..0f22371 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -136,37 +136,6 @@ TEST("graph", function()
CHECK(d_cnt == 1)
end
- do CASE "diamond graph 2"
- -- todo: include cached value from parent nodes to confirm update order
- -- a -> b -> c -> e
- -- -> d
- local root = node()
- local a, b, c, d, e = node(), node(), node(), node(), node()
-
- set_owner(b, root)
- set_owner(c, root)
- set_owner(d, root)
- set_owner(e, root)
-
- local b_cnt, c_cnt, d_cnt, e_cnt = 0, 0, 0, 0
- function b.effect(x) b_cnt += 1; return not x end
- function c.effect(x) c_cnt += 1; return not x end
- function d.effect(x) d_cnt += 1; return not x end
- function e.effect(x) e_cnt += 1; return not x end
-
- open_scope(b); track(a); close_scope()
- open_scope(c); track(b); close_scope()
- open_scope(d); track(a); close_scope()
- open_scope(e); track(c); track(d); close_scope()
-
- update(a)
-
- CHECK(b_cnt == 1)
- CHECK(c_cnt == 1)
- CHECK(d_cnt == 1)
- CHECK(e_cnt == 1)
- end
-
do CASE "duplicate child on rerun"
local root = node()
local a, b, c = node(), node(), node()
From 7d82fe353e1d0865f5ef267e02b5b2056e3e61e3 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Fri, 27 Oct 2023 15:47:43 +0100
Subject: [PATCH 027/116] Fix error in test
Also misc changes
---
src/graph.luau | 33 ++++++++++++++++-----------------
src/spring.luau | 3 ++-
test/tests.luau | 24 ++++++++++++------------
todo.md | 10 ++++------
4 files changed, 34 insertions(+), 36 deletions(-)
diff --git a/src/graph.luau b/src/graph.luau
index ab0be0b..d316e78 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -42,12 +42,14 @@ end
local function get_owning_scope(): Node
local scope = get_scope()
+
if not scope then
local caller_name = debug.info(2, "n")
return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
elseif scope.effect then
- throw("reactive scope is not an owning scope; new effects cannot be created in side-effects")
+ throw("cannot create new reactive scope inside of a tracking scope") -- todo: allow this?
end
+
return scope
end
@@ -96,8 +98,8 @@ local function run_cleanups(node: Node)
end
local function find_and_swap_pop(t: { T }, v: T)
- local idx = table.find(t, v)
- assert(idx, "value not found")
+ local idx = table.find(t, v) :: number
+ --assert(idx, "value not found")
local n = #t
t[idx] = t[n]
t[n] = nil
@@ -107,10 +109,9 @@ local function remove_child(parent: StartNode, child: Node)
find_and_swap_pop(parent, child)
end
-local function remove_owner(node: Node)
- local owner = node.owner :: Node
- if node.owner and owner.owned then
- find_and_swap_pop(owner.owned, node)
+local function disown(node: Node)
+ if node.owner then
+ find_and_swap_pop(node.owner.owned :: { Node }, node)
end
end
@@ -126,7 +127,7 @@ end
local function destroy(node: Node)
run_cleanups(node)
unparent(node)
- remove_owner(node)
+ disown(node)
if node.owned then
local owned = node.owned
@@ -137,7 +138,8 @@ end
local function destroy_owned(node: Node)
if node.owned then
- while node.owned[1] do destroy(node.owned[1]) end
+ local owned = node.owned
+ while owned[1] do destroy(owned[1]) end
end
end
@@ -178,18 +180,15 @@ local function evaluate_node(node: Node)
node.cache = new_value
- return cur_value ~= new_value -- node has changed value
+ return cur_value ~= new_value
end
local function queue_children(node: StartNode)
local i = update_queue.n
- local child = node[1]
- while child do
- --assert(child.parents.owner)
- unparent(child)
+ while node[1] do
i += 1
- update_queue[i] = child
- child = node[1]
+ update_queue[i] = node[1]
+ unparent(node[1])
end
update_queue.n = i
end
@@ -201,7 +200,7 @@ local function update(root: StartNode)
local i = n0 + 1
while i <= update_queue.n do
local node = update_queue[i]
- assert(node.effect)
+ --assert(node.effect)
if evaluate_node(node) then
queue_children(node)
diff --git a/src/spring.luau b/src/spring.luau
index 66dc896..b627a74 100644
--- a/src/spring.luau
+++ b/src/spring.luau
@@ -161,7 +161,8 @@ local function spring(source: () -> T, period: number?, damping_ratio: number
local c_c = 2*w_n
local c = z * c_c
- -- todo: is there a solution to this other than upping step frequency?
+ -- todo: is there a solution other than reducing step size?
+ -- todo: this does not catch all solver exploding cases
if c > UPDATE_RATE*2 then -- solver will explode if this is true
throw("spring damping too high, consider reducing damping or increasing period")
end
diff --git a/test/tests.luau b/test/tests.luau
index 0f22371..5c51e5f 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -1,5 +1,5 @@
local testkit = require("test/testkit")
-local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
+local TEST, CASE, CHECK, FINISH = testkit.test()
local mock = require "test/mock"
local Instance, Signal = mock.Instance, mock.Signal
@@ -33,8 +33,6 @@ local NIL = nil :: any
vide.strict = false
---SKIP "graph edge cases"
-
TEST("graph", function()
local create_node = graph.create_node
local track = graph.track
@@ -1918,37 +1916,39 @@ TEST("graph edge cases", wrap_root(function()
do CASE "diamond A,B,C,D,E"
--[[
- a > b > > e
+ a > b > e
> c > d >
]]
local a = source(0)
- local b = derive(function() print "ran b"; return (a() % 2 == 0) and 1 or 0 end)
- local c = derive(function() print "ran c"; return a() * 2 end)
- local d = derive(function() print "ran d"; return c() * 2 end)
- local e = derive(function() print "ran e"; return b() + c() end)
+ local b = derive(function() return (a() % 2 == 0) and 1 or 0 end)
+ local c = derive(function() return a() * 2 end)
+ local d = derive(function() return c() * 2 end)
+ local e = derive(function() return b() + d() end)
local count = { b = 0, c = 0, d = 0, e = 0 }
effect(function() b(); count.b += 1 end)
effect(function() c(); count.c += 1 end)
effect(function() d(); count.d += 1 end)
- effect(function() d(); count.e += 1 end)
+ effect(function() e(); count.e += 1 end)
+
+ CHECK(e() == 1)
a(1)
CHECK(count.b == 2)
CHECK(count.c == 2)
CHECK(count.d == 2)
- CHECK(count.e == 2)
- CHECK(e() == 4) -- todo: solve e evaluating before d
+ CHECK(count.e == 3) -- todo: redundant re-eval
+ CHECK(e() == 4)
a(3)
CHECK(count.b == 2)
CHECK(count.c == 3)
CHECK(count.d == 3)
- CHECK(count.e == 3)
+ CHECK(count.e == 4)
CHECK(e() == 12)
end
diff --git a/todo.md b/todo.md
index 8daf7a7..1256cdd 100644
--- a/todo.md
+++ b/todo.md
@@ -1,12 +1,10 @@
# todo
-- property binding optimization
- - would no longer allow `cleanup()` usage in binding scopes
-- solution to nested reactivity, see: SolidJS stores
- optimize wide graph updating
- implement from solid:
- - Portal
- - batch
+ - stores
+ - portals
+ - batch()
- optimize `indexes()` double-diffing
-- improve crash course, some sections feel like information dumps
- cleanup source and tests
+- prevent redundant re-eval of nodes in a complex diamond graph
From ec998ccbc8ddd229a94039c76268de7286843580 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Fri, 27 Oct 2023 16:47:49 +0100
Subject: [PATCH 028/116] Implement batched updates
No docs yet, more testing needed.
---
src/batch.luau | 23 +++++++++++++++++++++++
src/flags.luau | 2 +-
src/graph.luau | 23 +++++++++++++++++++++++
src/init.luau | 4 +++-
test/benchmark.luau | 38 ++++++++++++++++++++++++++++++--------
test/tests.luau | 43 +++++++++++++++++++++++++++++++++++++++++++
6 files changed, 123 insertions(+), 10 deletions(-)
create mode 100644 src/batch.luau
diff --git a/src/batch.luau b/src/batch.luau
new file mode 100644
index 0000000..3c08127
--- /dev/null
+++ b/src/batch.luau
@@ -0,0 +1,23 @@
+if not game then script = require "test/relative-string" end
+
+local flags = require(script.Parent.flags)
+local throw = require(script.Parent.throw)
+local graph = require(script.Parent.graph)
+
+local function batch(setter: () -> ())
+ local already_batching = flags.batch
+
+ flags.batch = true
+
+ local ok, err: string? = pcall(setter)
+
+ flags.batch = false
+
+ if not ok then throw(`error occured while batching updates: {err}`) end
+
+ if not already_batching then -- todo: flush anyways?
+ graph.flush_update_queue()
+ end
+end
+
+return batch
diff --git a/src/flags.luau b/src/flags.luau
index 1b9f80e..cc2d2f8 100644
--- a/src/flags.luau
+++ b/src/flags.luau
@@ -4,4 +4,4 @@ end
local is_O2 = inline_test() ~= "inline_test"
-return { strict = not is_O2 }
+return { strict = not is_O2, batch = false }
diff --git a/src/graph.luau b/src/graph.luau
index d316e78..e32f3d0 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -193,10 +193,32 @@ local function queue_children(node: StartNode)
update_queue.n = i
end
+local function flush_update_queue()
+ -- todo: test with recursive batch sets
+ local n0 = 0
+
+ local i = n0 + 1
+ while i <= update_queue.n do
+ local node = update_queue[i]
+ --assert(node.effect)
+
+ if evaluate_node(node) then
+ queue_children(node)
+ end
+
+ update_queue[i] = false :: any
+ i += 1
+ end
+
+ update_queue.n = n0
+end
+
local function update(root: StartNode)
local n0 = update_queue.n
queue_children(root)
+ if flags.batch then return end
+
local i = n0 + 1
while i <= update_queue.n do
local node = update_queue[i]
@@ -257,5 +279,6 @@ return table.freeze {
create_node = create_node,
create_start_node = create_start_node,
get_children = get_children,
+ flush_update_queue = flush_update_queue,
scopes = scopes
}
diff --git a/src/init.luau b/src/init.luau
index 687f417..cfc49d0 100644
--- a/src/init.luau
+++ b/src/init.luau
@@ -11,9 +11,10 @@ local create = require(script.create)
local apply = require(script.apply)
local source = require(script.source)
local effect = require(script.effect)
+local derive = require(script.derive)
local cleanup = require(script.cleanup)
local untrack = require(script.untrack)
-local derive = require(script.derive)
+local batch = require(script.batch)
local switch = require(script.switch)
local show = require(script.show)
local indexes, values = require(script.maps)()
@@ -59,6 +60,7 @@ local vide = {
-- util
cleanup = cleanup,
untrack = untrack,
+ batch = batch,
read = function(value: T | () -> T): T
return if type(value) == "function" then value() else value
end,
diff --git a/test/benchmark.luau b/test/benchmark.luau
index aac9071..bee8c68 100644
--- a/test/benchmark.luau
+++ b/test/benchmark.luau
@@ -6,6 +6,7 @@ local source = vide.source
local derive = vide.derive
local indexes = vide.indexes
local values = vide.values
+local batch = vide.batch
local cleanup = vide.cleanup
local create = vide.create
@@ -132,30 +133,51 @@ ROOT_BENCH("update 1->1->1->1...1000 graph", function()
end
end)
--- todo: crashes at 1k
--- todo: repeat with batching
-ROOT_BENCH("update 1000->1 graph", function()
+-- todo: why does it hang at 1k? it didn't before
+ROOT_BENCH("update 500->1 graph", function()
local srcs = {}
- for i = 1, 800 do
+ for i = 1, 500 do
srcs[i] = source(0)
end
derive(function()
- for i = 1, 800 do
+ for i = 1, 500 do
srcs[i]()
end
return false
end)
for i = 1, START(1) do
- for idx = 1, 800 do
+ for idx = 1, 500 do
srcs[idx](i)
end
end
end)
--- todo: optimize, repeat with batching
-ROOT_BENCH("update 1000x 1->1 common extern. graph", function()
+ROOT_BENCH("update 1000->1 graph (batched)", function()
+ local srcs = {}
+ for i = 1, 1000 do
+ srcs[i] = source(0)
+ end
+
+ derive(function()
+ for i = 1, 1000 do
+ srcs[i]()
+ end
+ return false
+ end)
+
+ for i = 1, START(1) do
+ batch(function()
+ for idx = 1, 1000 do
+ srcs[idx](i)
+ end
+ end)
+ end
+end)
+
+-- todo: optimize this case
+ROOT_BENCH("update 1000 1->1 common extern. graph", function()
local ext = source(-1)
local srcs = {}
diff --git a/test/tests.luau b/test/tests.luau
index 5c51e5f..16fa50d 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -1800,6 +1800,49 @@ TEST("changed()", wrap_root(function()
end
end))
+TEST("batch()", wrap_root(function()
+ local source = vide.source
+ local derive = vide.derive
+ local batch = vide.batch
+
+ do CASE "child evaluation halted"
+ local a = source(0)
+
+ local count = { b = 0, b2 = 0, c = 0 }
+
+ local b = derive(function()
+ count.b += 1
+ return a() + 1
+ end)
+
+ local b2 = derive(function()
+ count.b2 += 1
+ return a() + 2
+ end)
+
+ local c = derive(function()
+ count.c += 1
+ return b() + b2()
+ end)
+
+ batch(function()
+ a(1)
+ CHECK(count.b == 1)
+ CHECK(count.b2 == 1)
+ CHECK(count.c == 1)
+ end)
+
+ CHECK(count.b == 2)
+ CHECK(count.b2 == 2)
+ CHECK(count.c == 2)
+
+ CHECK(b() == 2)
+ CHECK(c() == 5)
+ end
+
+ -- todo: test batch call in recursive set
+end))
+
TEST("read()", wrap_root(function()
local source = vide.source
local effect = vide.effect
From 65fe3fcf47547cbfe4960d276b59601e1f57245e Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Thu, 16 Nov 2023 18:01:45 +0000
Subject: [PATCH 029/116] Fix some graph edge cases
---
CHANGELOG.md | 14 ++++++++-
src/graph.luau | 25 ++++++---------
test/tests.luau | 84 +++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 106 insertions(+), 17 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 558b398..5ea3ffa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
--
+### Added
+
+- Batched updates with `batch()`.
+
+### Changed
+
+- Improved graph updating algorithm.
+- Graph nodes no longer destroy children; only owned.
+
+### Fixed
+
+- Graph edge case where a destroyed node can be readded if it was queued for
+ evaluation before being destroyed.
--------------------------------------------------------------------------------
diff --git a/src/graph.luau b/src/graph.luau
index e32f3d0..9288a82 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -99,27 +99,16 @@ end
local function find_and_swap_pop(t: { T }, v: T)
local idx = table.find(t, v) :: number
- --assert(idx, "value not found")
local n = #t
t[idx] = t[n]
t[n] = nil
end
-local function remove_child(parent: StartNode, child: Node)
- find_and_swap_pop(parent, child)
-end
-
-local function disown(node: Node)
- if node.owner then
- find_and_swap_pop(node.owner.owned :: { Node }, node)
- end
-end
-
local function unparent(node: Node)
local parents = node.parents
for i, parent in next, parents do
- remove_child(parent, node)
+ find_and_swap_pop(parent, node)
parents[i] = nil
end
end
@@ -127,13 +116,16 @@ end
local function destroy(node: Node)
run_cleanups(node)
unparent(node)
- disown(node)
+
+ if node.owner then
+ find_and_swap_pop(node.owner.owned :: { Node }, node)
+ node.owner = false
+ end
if node.owned then
local owned = node.owned
while owned[1] do destroy(owned[1]) end
end
- while node[1] do destroy(node[1]) end
end
local function destroy_owned(node: Node)
@@ -202,7 +194,7 @@ local function flush_update_queue()
local node = update_queue[i]
--assert(node.effect)
- if evaluate_node(node) then
+ if node.owner and evaluate_node(node) then
queue_children(node)
end
@@ -224,7 +216,8 @@ local function update(root: StartNode)
local node = update_queue[i]
--assert(node.effect)
- if evaluate_node(node) then
+ -- check if node is still owned in case destroyed after queued
+ if node.owner and evaluate_node(node) then
queue_children(node)
end
diff --git a/test/tests.luau b/test/tests.luau
index 16fa50d..9cd6afe 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -1923,6 +1923,7 @@ TEST("graph edge cases", wrap_root(function()
local source = vide.source
local derive = vide.derive
local effect = vide.effect
+ local root = vide.root
do CASE "diamond A,B,C,D"
--[[
@@ -2006,6 +2007,89 @@ TEST("graph edge cases", wrap_root(function()
CHECK(b() == 2)
CHECK(count == 2)
end
+
+ do CASE "do not destroy children"
+ local parent = source(0)
+
+ local
+ destroy,
+ parent_to_destroy,
+ update_parent_to_destroy
+ = root(function(destroy)
+ local src = source(0)
+ return
+ destroy,
+ derive(function() return src() end),
+ src
+ end)
+
+ local count = 0
+
+ effect(function()
+ count += 1
+ parent()
+ parent_to_destroy()
+ end)
+
+ parent(parent() + 1)
+ CHECK(count == 2)
+ update_parent_to_destroy(1)
+ CHECK(count == 3)
+
+ destroy()
+
+ update_parent_to_destroy(2)
+ CHECK(count == 3)
+
+ parent(parent() + 1)
+ CHECK(count == 4)
+ end
+
+ do CASE "double destroy"
+ -- issue:
+ -- parent evaluates
+ -- child A queued
+ -- child B queued
+ -- child A destroys child B
+ -- child B reevaluates due to already being queued
+ -- parent destroys, destroys child B - uh oh
+
+ local
+ destroy_parent,
+ parent,
+ update_parent
+ = root(function(destroy)
+ local src = source(0)
+ return
+ destroy,
+ derive(function() return src() end),
+ src
+ end)
+
+ local destroy_child, _child_B = function() end, nil
+
+ local count_A = 0
+
+ -- child_A
+ effect(function()
+ count_A += 1
+ parent()
+ destroy_child()
+ end)
+
+ local count_B = 0
+ destroy_child, _child_B = root(function(destroy)
+ return
+ destroy,
+ derive(function() count_B += 1; return parent() end)
+ end)
+
+ update_parent(parent() + 1)
+ CHECK(count_A == 2)
+ CHECK(count_B == 1) -- child B should not run again
+ destroy_parent() -- should not error
+ CHECK(true)
+ end
end))
TEST("strict", wrap_root(function()
From 8eb5f96c5b47406798a2f13b62aee84eb98821dd Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Mon, 20 Nov 2023 12:27:56 +0000
Subject: [PATCH 030/116] Fix recursive `batch()` use
---
docs/api/reactivity-utility.md | 18 ++++++++++++++
src/batch.luau | 14 ++++++-----
src/graph.luau | 7 +++++-
test/tests.luau | 44 ++++++++++++++++++++++++++++++++--
todo.md | 8 +------
5 files changed, 75 insertions(+), 16 deletions(-)
diff --git a/docs/api/reactivity-utility.md b/docs/api/reactivity-utility.md
index a16e251..ee82067 100644
--- a/docs/api/reactivity-utility.md
+++ b/docs/api/reactivity-utility.md
@@ -73,4 +73,22 @@ read can still be tracked inside a reactive scope.
function read(value: T | () -> T): T
```
+## batch()
+
+Runs a given function where any source updates made within the function do not
+trigger effects until after the function runs.
+
+- **Type**
+
+ ```lua
+ function batch(fn: () -> ())
+ ```
+
+- **Details**
+
+ Improves performance when an effect depends on multiple sources, and those
+ sources need to be updated. Updating those sources inside a batch call will
+ only cause the effect to run once after the batch call ends instead of after
+ each time a source is updated.
+
--------------------------------------------------------------------------------
diff --git a/src/batch.luau b/src/batch.luau
index 3c08127..1951789 100644
--- a/src/batch.luau
+++ b/src/batch.luau
@@ -11,13 +11,15 @@ local function batch(setter: () -> ())
local ok, err: string? = pcall(setter)
- flags.batch = false
-
- if not ok then throw(`error occured while batching updates: {err}`) end
-
- if not already_batching then -- todo: flush anyways?
- graph.flush_update_queue()
+ if not already_batching then
+ flags.batch = false
+
+ if not already_batching then
+ graph.flush_update_queue()
+ end
end
+
+ if not ok then throw(`error occured while batching updates: {err}`) end
end
return batch
diff --git a/src/graph.luau b/src/graph.luau
index 9288a82..66587a8 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -185,8 +185,11 @@ local function queue_children(node: StartNode)
update_queue.n = i
end
+local _flushing = false
local function flush_update_queue()
- -- todo: test with recursive batch sets
+ assert(not flushing, "recursive queue flush occured") -- todo
+ _flushing = true
+
local n0 = 0
local i = n0 + 1
@@ -203,6 +206,8 @@ local function flush_update_queue()
end
update_queue.n = n0
+
+ _flushing = false
end
local function update(root: StartNode)
diff --git a/test/tests.luau b/test/tests.luau
index 9cd6afe..94c7bb4 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -1805,7 +1805,7 @@ TEST("batch()", wrap_root(function()
local derive = vide.derive
local batch = vide.batch
- do CASE "child evaluation halted"
+ do CASE "evaluation deferred"
local a = source(0)
local count = { b = 0, b2 = 0, c = 0 }
@@ -1840,7 +1840,47 @@ TEST("batch()", wrap_root(function()
CHECK(c() == 5)
end
- -- todo: test batch call in recursive set
+ do CASE "recursive call"
+ local a1 = source(0)
+ local a2 = source(0)
+ local a3 = source(0)
+
+ local count = { b1 = 0, b2 = 0, b3 = 0 }
+
+ local b1 = derive(function()
+ count.b1 += 1
+ return a1() + 1
+ end)
+
+ local b2 = derive(function()
+ count.b2 += 1
+ return a2() + 1
+ end)
+
+ local b3 = derive(function()
+ count.b3 += 1
+ return a3() + 1
+ end)
+
+ batch(function()
+ a1(1)
+ batch(function()
+ a2(2)
+ end)
+ a3(3)
+ CHECK(count.b1 == 1)
+ CHECK(count.b2 == 1)
+ CHECK(count.b3 == 1)
+ end)
+
+ CHECK(count.b1 == 2)
+ CHECK(count.b2 == 2)
+ CHECK(count.b3 == 2)
+
+ CHECK(b1() == 2)
+ CHECK(b2() == 3)
+ CHECK(b3() == 4)
+ end
end))
TEST("read()", wrap_root(function()
diff --git a/todo.md b/todo.md
index 1256cdd..6f254bd 100644
--- a/todo.md
+++ b/todo.md
@@ -1,10 +1,4 @@
# todo
-- optimize wide graph updating
-- implement from solid:
- - stores
- - portals
- - batch()
-- optimize `indexes()` double-diffing
-- cleanup source and tests
+- improve error traces
- prevent redundant re-eval of nodes in a complex diamond graph
From c288cb92c4881608e38585ddfc472a8959c15bea Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Mon, 20 Nov 2023 16:53:30 +0000
Subject: [PATCH 031/116] Minor refactors
Also fixed potential bug with `create()` being called recursively if a
property binding passed as a property to `create()` also calls
`create()`
---
src/action.luau | 6 +-
src/apply.luau | 148 ++++++++++++++++++++++++++---------------------
src/bind.luau | 5 +-
src/create.luau | 50 +++++++++-------
src/derive.luau | 4 +-
src/effect.luau | 4 +-
src/graph.luau | 6 +-
src/init.luau | 5 +-
src/maps.luau | 6 +-
src/memoize.luau | 17 ------
src/read.luau | 7 +++
src/source.luau | 2 +-
src/spring.luau | 4 +-
src/switch.luau | 6 +-
src/throw.luau | 2 +-
test/tests.luau | 27 +++++++++
16 files changed, 169 insertions(+), 130 deletions(-)
delete mode 100644 src/memoize.luau
create mode 100644 src/read.luau
diff --git a/src/action.luau b/src/action.luau
index f40bf3a..8cc4987 100644
--- a/src/action.luau
+++ b/src/action.luau
@@ -10,14 +10,14 @@ local function is_action(v: any)
end
local function action(callback: (Instance) -> (), priority: number?): Action
- local t = {
+ local a = {
priority = priority or 1,
callback = callback
}
- setmetatable(t :: any, ActionMT)
+ setmetatable(a :: any, ActionMT)
- return table.freeze(t)
+ return table.freeze(a)
end
return function()
diff --git a/src/apply.luau b/src/apply.luau
index 1cc533a..13e8cab 100644
--- a/src/apply.luau
+++ b/src/apply.luau
@@ -13,41 +13,58 @@ type Node = graph.Node
type Array = { V }
type Map = { [K]: V }
--- buffer of event -> callback to connect after properties are set
-local event_buffer = {} :: Map ()>
+local free_caches: {
+ -- event listeners to connect after properties are set
+ events: Map<
+ string, -- event name
+ () -> () -- listener
+ >,
--- buffer of priority -> callback to run after events are connected
-local action_buffers = {} :: Map ()>>
+ -- actions to run after events are connected
+ actions: Map<
+ number, -- priority
+ Array<(Instance) -> ()> -- action callbacks
+ >,
--- lazily create buffers on nil index
-setmetatable(action_buffers :: any, {
- __index = function(_, i: number)
- action_buffers[i] = {}
- return action_buffers[i]
+ -- cache to detect duplicate property setting at same nesting depth
+ nested_debug: Map<
+ number, -- depth
+ Map -- set of property names
+ >,
+
+ -- use stack instead of recursive function to process nesting layers one at time
+ -- deeper-nested properties take precedence over shallower-nested ones
+ -- each nested layer occupies two indexes: 1. table ref 2. nested depth
+ -- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
+ nested_stack: { {} | number }
+}?
+
+local function borrow_caches(): typeof(assert(free_caches))
+ if free_caches then
+ local caches = free_caches :: typeof(assert(free_caches))
+ free_caches = nil
+ return caches
+ else
+ return {
+ events = {},
+ actions = setmetatable({} :: any, { -- lazy init
+ __index = function(self, i) self[i] = {}; return self[i] end
+ }),
+ nested_debug = setmetatable({} :: any, {
+ __index = function(self, i: number) self[i] = {}; return self[i] end
+ }),
+ nested_stack = {}
+ }
end
-})
+end
--- cache in strict mode to detect duplicate property set at same nesting level
-local nested_debug_cache = {} :: Map>
+local function return_caches(caches: typeof(free_caches) )
+ free_caches = caches
+end
-setmetatable(nested_debug_cache :: any, {
- __index = function(_, i: number)
- nested_debug_cache[i] = {}
- return nested_debug_cache[i]
- end
-})
-
--- use stack instead of recursive function to process nested layers one at time
--- deeper-nested properties take precedence over shallower-nested ones
--- each nested layer occupies two indexes: 1. table ref 2. nested depth
--- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 }
-local nested_stack = {} :: { {} | number }
-
--- todo: solution without manual updating of this table
-- map of datatype names to class default constructor for aggregate init
local aggregates = {}
-
-for i, v in next, {
+for name, class in {
CFrame = CFrame,
Color3 = Color3,
UDim = UDim,
@@ -55,27 +72,39 @@ for i, v in next, {
Vector2 = Vector2,
Vector3 = Vector3,
Rect = Rect
-} do
- aggregates[i] = v.new
+} :: Map do
+ aggregates[name] = class.new
end
--- processes a potentially nested table of values to assign to an instance
-local function process_props(instance: Instance, properties: Map)
+-- applies table of nested properties to an instance using full vide semantics
+local function apply(instance: T & Instance, properties: { [unknown]: unknown }): T
+ if not properties then
+ throw("attempt to call a constructor returned by create() with no properties")
+ end
+
local strict = flags.strict
- table.clear(nested_stack)
- if strict then table.clear(nested_debug_cache) end
+ -- queue parent assignment if any for last
+ local parent: unknown = properties.Parent
+ local caches = borrow_caches()
+ local events = caches.events
+ local actions = caches.actions
+ local nested_debug = caches.nested_debug
+ local nested_stack = caches.nested_stack
+
+ -- process all properties
local depth = 1
-
repeat
for property, value in properties do
+ if property == "Parent" then continue end
+
if type(property) == "string" then
- if strict then -- check for duplicate prop assignment at nesting layer
- if nested_debug_cache[depth][property] then
+ if strict then -- check for duplicate prop assignment at nesting depth
+ if nested_debug[depth][property] then
throw(`duplicate property {property} at depth {depth}`)
end
- nested_debug_cache[depth][property] = true
+ nested_debug[depth][property] = true
end
if type(value) == "table" then -- attempt aggregate init
@@ -86,7 +115,7 @@ local function process_props(instance: Instance, properties: Map () -- add event to buffer
+ events[property] = value :: () -> () -- add event to buffer
else
bind.property(instance, property, value :: () -> ()) -- bind property
end
@@ -98,7 +127,7 @@ local function process_props(instance: Instance, properties: Map Instance | Array) -- bind children
elseif type(value) == "table" then
if is_action(value) then
- table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
+ table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer
else
table.insert(nested_stack, value :: {})
table.insert(nested_stack, depth + 1) -- push table to stack for later processing
@@ -109,40 +138,17 @@ local function process_props(instance: Instance, properties: Map(instance: T & Instance, properties: { [unknown]: unknown }): T
- if not properties then
- throw("no properties given, did you forget to call the constructor returned by create()?")
+ for event, listener in next, events do
+ (instance :: any)[event]:Connect(listener)
end
- -- queue parent assignment if any for last
- local parent: unknown = properties.Parent
- if parent then properties.Parent = nil end
-
- -- reset buffers
- table.clear(event_buffer)
- for _, buffer in next, action_buffers do
- table.clear(buffer)
- end
-
- -- process all properties for immediate setting or buffering
- process_props(instance, properties)
-
- -- connect buffered events
- for event, fn in next, event_buffer do
- (instance :: any)[event]:Connect(fn)
- end
-
- -- run buffered actions
- for _, buffer in next, action_buffers do
- for _, callback in next, buffer do
+ for _, queued in next, actions do
+ for _, callback in next, queued do
callback(instance)
end
end
@@ -156,6 +162,14 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown
end
end
+ -- clear caches
+ table.clear(events)
+ for _, queued in next, actions do table.clear(queued) end
+ if strict then table.clear(nested_debug) end
+ table.clear(nested_stack)
+
+ return_caches(caches)
+
return instance
end
diff --git a/src/bind.luau b/src/bind.luau
index c614590..4a204f1 100644
--- a/src/bind.luau
+++ b/src/bind.luau
@@ -5,7 +5,7 @@ local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph)
type Node = graph.Node
local create_node = graph.create_node
-local get_owning_scope = graph.get_owning_scope
+local assert_owning_scope = graph.assert_owning_scope
local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner
@@ -31,8 +31,7 @@ function create_binding(updater: (T) -> T, binding: T)
end
end
-
- local owner = get_owning_scope()
+ local owner = assert_owning_scope()
local node = create_node(binding, updater)
diff --git a/src/create.luau b/src/create.luau
index 0ce889e..7adfc68 100644
--- a/src/create.luau
+++ b/src/create.luau
@@ -5,41 +5,51 @@ local Instance = game and Instance or require "test/mock".Instance :: never
local throw = require(script.Parent.throw)
local defaults = require(script.Parent.defaults)
local apply = require(script.Parent.apply)
-local memoize = require(script.Parent.memoize)
+
+local ctor_cache = {} :: { [string]: () -> Instance }
+
+setmetatable(ctor_cache :: any, {
+ __index = function(self, class)
+ local ok, instance: Instance = pcall(Instance.new, class :: any)
+ if not ok then throw(`invalid class name, could not create instance of class { class }`) end
+
+ local default: { [string]: unknown }? = defaults[class]
+ if default then
+ for i, v in next, default do
+ (instance :: any)[i] = v
+ end
+ end
+
+ local function ctor(properties: Props): Instance
+ return apply(instance:Clone(), properties)
+ end
+
+ self[class] = ctor
+ return ctor
+ end
+})
local function create_instance(class: string)
- local ok, instance: Instance = pcall(Instance.new, class :: any)
- if not ok then throw(`invalid class name, could not create instance of class { class }`) end
-
- local default: { [string]: unknown }? = defaults[class]
- if default then
- for i, v in next, default do
- (instance :: any)[i] = v
- end
- end
-
- return function(properties: { [any]: unknown }): Instance
- return apply(instance:Clone(), properties)
- end
-end; create_instance = memoize(create_instance) -- always return same constructor for given class
+ return ctor_cache[class]
+end
local function clone_instance(instance: Instance)
- return function(properties: { [any]: unknown }): Instance
+ return function(properties: Props): Instance
local clone = instance:Clone()
- if not clone then error("Attempt to clone a non-archivable instance", 3) end
+ if not clone then throw "attempt to clone a non-archivable instance" end
return apply(clone, properties)
end
end
-local function create(class_or_instance: string|Instance)
+local function create(class_or_instance: string|Instance): (Props) -> Instance
if type(class_or_instance) == "string" then
return create_instance(class_or_instance)
elseif typeof(class_or_instance) == "Instance" then
return clone_instance(class_or_instance)
else
- throw("bad argument #1, expected string or instance, got "..typeof(class_or_instance))
+ throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance))
+ return nil :: never
end
- return nil :: never
end
type Props = { [any]: any }
diff --git a/src/derive.luau b/src/derive.luau
index 49094d5..863fd98 100644
--- a/src/derive.luau
+++ b/src/derive.luau
@@ -4,11 +4,11 @@ local graph = require(script.Parent.graph)
local create_node = graph.create_node
local set_owner = graph.set_owner
local track = graph.track
-local get_owning_scope = graph.get_owning_scope
+local assert_owning_scope = graph.assert_owning_scope
local evaluate_node = graph.evaluate_node
local function derive(source: () -> T): () -> T
- local owner = get_owning_scope()
+ local owner = assert_owning_scope()
local node = create_node(false :: any, source)
diff --git a/src/effect.luau b/src/effect.luau
index 43b12ab..bbe1669 100644
--- a/src/effect.luau
+++ b/src/effect.luau
@@ -2,12 +2,12 @@ if not game then script = require "test/relative-string" end
local graph = require(script.Parent.graph)
local create_node = graph.create_node
-local get_owning_scope = graph.get_owning_scope
+local assert_owning_scope = graph.assert_owning_scope
local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner
local function effect(callback: (T) -> T, initial_value: T)
- local owner = get_owning_scope()
+ local owner = assert_owning_scope()
local node = create_node(initial_value, callback)
diff --git a/src/graph.luau b/src/graph.luau
index 66587a8..ecda1ab 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -40,7 +40,7 @@ local function get_scope(): Node?
return scopes[scopes.n]
end
-local function get_owning_scope(): Node
+local function assert_owning_scope(): Node
local scope = get_scope()
if not scope then
@@ -187,7 +187,7 @@ end
local _flushing = false
local function flush_update_queue()
- assert(not flushing, "recursive queue flush occured") -- todo
+ assert(not _flushing, "recursive queue flush occured") -- todo
_flushing = true
local n0 = 0
@@ -266,7 +266,7 @@ return table.freeze {
close_scope = close_scope,
evaluate_node = evaluate_node,
get_scope = get_scope,
- get_owning_scope = get_owning_scope,
+ assert_owning_scope = assert_owning_scope,
add_cleanup = add_cleanup,
set_owner = set_owner,
destroy = destroy,
diff --git a/src/init.luau b/src/init.luau
index cfc49d0..17812c3 100644
--- a/src/init.luau
+++ b/src/init.luau
@@ -14,6 +14,7 @@ local effect = require(script.effect)
local derive = require(script.derive)
local cleanup = require(script.cleanup)
local untrack = require(script.untrack)
+local read = require(script.read)
local batch = require(script.batch)
local switch = require(script.switch)
local show = require(script.show)
@@ -60,10 +61,8 @@ local vide = {
-- util
cleanup = cleanup,
untrack = untrack,
+ read = read,
batch = batch,
- read = function(value: T | () -> T): T
- return if type(value) == "function" then value() else value
- end,
-- animations
spring = spring,
diff --git a/src/maps.luau b/src/maps.luau
index 1034e9a..ef36209 100644
--- a/src/maps.luau
+++ b/src/maps.luau
@@ -10,7 +10,7 @@ local create_start_node = graph.create_start_node
local set_owner = graph.set_owner
local track = graph.track
local update = graph.update
-local get_owning_scope = graph.get_owning_scope
+local assert_owning_scope = graph.assert_owning_scope
local open_scope = graph.open_scope
local close_scope = graph.close_scope
local evaluate_node = graph.evaluate_node
@@ -28,7 +28,7 @@ local function check_primitives(t: {})
end
local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO): () -> { VO }
- local owner = get_owning_scope()
+ local owner = assert_owning_scope()
local subowner = create_node(false, false)
set_owner(subowner, owner)
@@ -123,7 +123,7 @@ local function indexes(input: () -> Map, transform: (() -> VI,
end
local function values(input: () -> Map, transform: (VI, () -> K) -> VO): () -> { VO }
- local owner = get_owning_scope()
+ local owner = assert_owning_scope()
local subowner = create_node(false, false)
set_owner(subowner, owner)
diff --git a/src/memoize.luau b/src/memoize.luau
deleted file mode 100644
index cf83427..0000000
--- a/src/memoize.luau
+++ /dev/null
@@ -1,17 +0,0 @@
-local function memoize(f: (X) -> Y): (X) -> Y
- local cache: { [X]: Y? } = {}
-
- return function(x: X): Y
- local y = cache[x]
-
- if not y then
- y = f(x)
- cache[x] = y
- end
-
- return y :: Y
- end
-end
-
-return memoize
-
diff --git a/src/read.luau b/src/read.luau
new file mode 100644
index 0000000..d3a2fb7
--- /dev/null
+++ b/src/read.luau
@@ -0,0 +1,7 @@
+if not game then script = require "test/relative-string" end
+
+local function read(value: T | () -> T): T
+ return if type(value) == "function" then value() else value
+end
+
+return read
diff --git a/src/source.luau b/src/source.luau
index f1307fc..dd633bb 100644
--- a/src/source.luau
+++ b/src/source.luau
@@ -6,7 +6,7 @@ local create_start_node = graph.create_start_node
local track = graph.track
local update = graph.update
-export type Source = (() -> T) & ((T) -> T)
+export type Source = (() -> T) & ((value: T) -> T)
local function source(initial_value: T): Source
local node = create_start_node(initial_value)
diff --git a/src/spring.luau b/src/spring.luau
index b627a74..91f17b1 100644
--- a/src/spring.luau
+++ b/src/spring.luau
@@ -27,7 +27,7 @@ type Node = graph.Node
type StartNode = graph.StartNode
local create_node = graph.create_node
local create_start_node = graph.create_start_node
-local get_owning_scope = graph.get_owning_scope
+local assert_owning_scope = graph.assert_owning_scope
local evaluate_node = graph.evaluate_node
local update = graph.update
local set_owner = graph.set_owner
@@ -150,7 +150,7 @@ local springs: { [SpringData]: StartNode } = {}
setmetatable(springs, { __mode = "v" })
local function spring(source: () -> T, period: number?, damping_ratio: number?): () -> T
- local owner = get_owning_scope()
+ local owner = assert_owning_scope()
-- https://en.wikipedia.org/wiki/Damping
diff --git a/src/switch.luau b/src/switch.luau
index 1ddc8e6..12fd376 100644
--- a/src/switch.luau
+++ b/src/switch.luau
@@ -9,14 +9,14 @@ local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner
local track = graph.track
local destroy = graph.destroy
-local get_owning_scope = graph.get_owning_scope
+local assert_owning_scope = graph.assert_owning_scope
local open_scope = graph.open_scope
local close_scope = graph.close_scope
type Map = { [K]: V }
local function switch(source: () -> T): (map: Map U)?)>) -> () -> U?
- local owner = get_owning_scope()
+ local owner = assert_owning_scope()
return function(map)
local last_scope: Node?
@@ -35,7 +35,7 @@ local function switch(source: () -> T): (map: Map U)?)>) -> ()
if component == nil then return nil end
if type(component) ~= "function" then
- throw("map must map a value to a function")
+ throw "map must map a value to a function"
end
local new_scope = create_node(false, false)
diff --git a/src/throw.luau b/src/throw.luau
index d3ea687..70b7973 100644
--- a/src/throw.luau
+++ b/src/throw.luau
@@ -3,7 +3,7 @@ if not game then script = require "test/relative-string" end
local trace = require(script.Parent.trace)
local function throw(msg): any
- error(msg, trace()-1)
+ error(msg, trace() - 1)
end
return throw
diff --git a/test/tests.luau b/test/tests.luau
index 94c7bb4..c38c6a7 100644
--- a/test/tests.luau
+++ b/test/tests.luau
@@ -942,6 +942,33 @@ TEST("create()", wrap_root(function()
CHECK(not wref[1])
end
+ do CASE "recursive create"
+ local set_test_to_true = vide.action(function(self) (self :: any).test = true end)
+
+ local f2
+
+ local to_apply = {
+ { a = 1 },
+ set_test_to_true,
+ b = function() f2 = create "Frame" { a = 2 } end,
+ } :: { [number|string]: unknown }
+
+ -- do -- confirm iteration order
+ -- local t = {}
+ -- for i in to_apply do
+ -- table.insert(t, i)
+ -- end
+ -- assert(t[1] == "a")
+ -- end
+
+ local f = create "Frame" (to_apply)
+
+ CHECK((f :: any).a == 1)
+ CHECK((f :: any).test == true )
+
+ CHECK((f2 :: any).a == 2)
+ end
+
do CASE "garbage collection test"
local wref
From 338c66ed57162063c87b5a61b73950685ea5338e Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Tue, 21 Nov 2023 18:48:47 +0000
Subject: [PATCH 032/116] Try improve crash course
---
docs/.vitepress/config.ts | 3 +-
docs/tut/crash-course/1-introduction.md | 46 ++----
docs/tut/crash-course/10-cleanup.md | 26 +++-
docs/tut/crash-course/15-concepts.md | 132 ++++++++++++++++++
docs/tut/crash-course/2-creation.md | 39 +++---
docs/tut/crash-course/3-components.md | 14 +-
docs/tut/crash-course/4-source.md | 8 +-
docs/tut/crash-course/6-root.md | 16 ++-
docs/tut/crash-course/7-stateful-component.md | 7 +-
docs/tut/crash-course/8-property-binding.md | 14 +-
docs/tut/crash-course/9-derived-source.md | 20 +--
11 files changed, 223 insertions(+), 102 deletions(-)
create mode 100644 docs/tut/crash-course/15-concepts.md
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 7c820ff..0a3500d 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -51,7 +51,8 @@ export default withMermaid({
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
{ text: "Property Nesting", link: "/tut/crash-course/12-property-nesting" },
{ text: "Actions", link: "/tut/crash-course/13-actions" },
- { text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" }
+ { text: "Strict Mode", link: "/tut/crash-course/14-strict-mode" },
+ { text: "Concepts Summary", link: "/tut/crash-course/15-concepts" }
]
},
{
diff --git a/docs/tut/crash-course/1-introduction.md b/docs/tut/crash-course/1-introduction.md
index 604f7ae..261ce83 100644
--- a/docs/tut/crash-course/1-introduction.md
+++ b/docs/tut/crash-course/1-introduction.md
@@ -1,45 +1,25 @@
# Introduction
-This is a brief tutorial designed to give you a quick run through the usage of
-Vide.
+This is a tutorial that introduces the concepts and usage of Vide.
Vide is heavily inspired by [Solid](https://www.solidjs.com/).
-This tutorial assumes familiarity with Luau and Roblox GUI.
+This tutorial assumes familiarity with Luau and Roblox UI.
## Why Vide?
-Creating UI is a slow and tedious process. The purpose of Vide is to make UI
-declarative and concise, making it faster to create and more importantly easier
-to maintain. Vide achieves this using a reactive style of programming which
-allows you to focus on the flow of data through your application without
-worrying about manually updating UI instances.
+Creating UI is complicated, slow, and tedious.
+
+Vide tries to simplify and speed up this process by providing a declarative and
+reactive of style programming, which lets you focus more on designing the UI
+itself and not having to manually update or reparent UI instances.
Some of the main focuses behind Vide's design choices:
-- Concise syntax.
-- Being completely typecheckable.
-- Independence from instance lifetimes.
-- Real reactivity.
+- Minimal syntax.
+- Complete typechecking
+- Independence from instances.
-## Structure Of A Vide App
-
-The entry point for all Vide apps is the `mount()` function. This function
-sets up Vide's reactivity system. It takes and calls a function that should
-create your entire app, and will apply its result to a target.
-
-In Vide, your app should be composed of functions, each function creates a
-specific part of your app, and can be reused if needed. These functions are
-called *components*.
-
-```lua
-local function App()
- return {
- PlayerStats(),
- Inventory(),
- Settings()
- }
-end
-
-mount(App, game.StarterGui)
-```
+As with most declarative libraries, there is an initial learning curve to
+understand the concepts and usage. This tutorial tries to comprehensively
+cover these concepts and usage, more so than you need just to use it.
diff --git a/docs/tut/crash-course/10-cleanup.md b/docs/tut/crash-course/10-cleanup.md
index 4f62233..b9361ec 100644
--- a/docs/tut/crash-course/10-cleanup.md
+++ b/docs/tut/crash-course/10-cleanup.md
@@ -2,8 +2,7 @@
Sometimes you may need to do some cleanup when destroying a component or after
a side-effect from a source update. Vide provides a function `cleanup()` which
-is used to register a cleanup callback for the next time the reactive scope
-it is called in re-runs.
+is used to queue a cleanup callback for the next time a reactive scope re-runs.
```lua
local mount = vide.mount
@@ -33,18 +32,33 @@ end
local unmount = mount(Timer)
-unmount() -- all registered cleanups are ran, heartbeat connection stopped
+unmount() -- all queued cleanups are ran, heartbeat connection stopped
```
In the above example, this allows us to disconnect the heartbeat connection
-when the timer component is destroyed, whether that is from unmounting the app
-or if it is dynamically created by a control-flow function, which will be
-covered next.
+when the reactive scope responsible for creating the timer component is
+destroyed, such as when it is unmounted.
+
+Vide does not see "components", it only sees reactive scopes and how they are
+linked together. Components are just a user pattern that creates UI instances
+alongside effects. In other words, instances are just a side-effect of the
+reactive graph. When a reactive scope is created, you create a corresponding
+instance to display that data, when that reactive scope is destroyed, any
+cleanups queued will be ran and take care of anything that needs to be, such
+as disconnecting connections.
This is another reason why `mount()` is used at the top level of your app, so
that any registered cleanups created by your app components can be ran when
they are destroyed.
+Side note: Roblox instances do not need to be explicitly destroyed for their
+memory to be freed, they only need to be parented to `nil`. So there is no
+need to use `cleanup()` to destroy instances. However, be wary of connecting
+a function that references an instance to an event from the same instance,
+this causes the instance to reference itself and never be freed. In such a case
+you would need to use `cleanup()` to disconnect this connection or to explicitly
+destroy the instance.
+
The reactive graph for the above example:
```mermaid
diff --git a/docs/tut/crash-course/15-concepts.md b/docs/tut/crash-course/15-concepts.md
new file mode 100644
index 0000000..2c2c313
--- /dev/null
+++ b/docs/tut/crash-course/15-concepts.md
@@ -0,0 +1,132 @@
+# Concepts Summary
+
+A summary of all the concepts covered during the crash course.
+
+## Source
+
+A source of data.
+
+Stores a single value that can be updated by the user.
+
+## Effect
+
+Anything that happens in reponse to a source update.
+
+Vide has built-in functions to create effects such as
+
+- `effect()` - runs arbitrary user code on source update
+- `derive()` - updates a derived source on source update
+
+## Reactive Scope
+
+A scope created by certain Vide functions where source updates can be tracked,
+and cleanups queued.
+
+When a source used inside a reactive scope is updated, the reactive scope will
+rerun.
+
+Reactive scopes are created by functions such as
+
+- `root()`
+- `effect()`
+- `derive()`
+
+## Owner
+
+A reactive scope created within an outer reactive scope, is *owned* by the outer
+reactive scope.
+
+When a reactive scope is re-ran or destroyed, all reactive scopes owned by it
+are also destroyed.
+
+Vide does not let you create reactive scopes without owners.
+
+## Root Reactive Scope
+
+A top-level reactive scope. These scopes are an exception to the owner rule.
+
+Created by `root()`, which `mount()` uses internally.
+
+A root reactive scope can be created on its own. It allows other reactive scopes
+to be created with an owner.
+
+Root reactive scopes must be destroyed manually by the user, a function to do
+this is given by `root()`.
+
+A root reactive scope can be created within another reactive scope and it will
+not automatically be owned by that scope.
+
+## Cleanup
+
+Cleans up the result from an effect.
+
+Unneeded in most cases, a cleanup is arbitrary code that can be ran before
+a reactive scope is rerun or destroyed, so that the result from the previous
+run can be cleaned up. A cleanup can be queued by using `cleanup()` within
+a reactive scope.
+
+## Tracking
+
+Reactive scopes are tracking by default, meaning sources read from within scope
+will be tracked.
+
+A reactive scope can be made temporarily non-tracking within `untrack()`, so
+that any source used will be ignored. The only function that creates a
+nontracking reactive scope by default is `root()`.
+
+## Reactive Graph
+
+The combination of reactive scopes can viewed graphically, called a
+*reactive graph*. This can be a more intuitive way to think of the
+relationships between effects and the sources they depend on.
+
+### Code
+
+```lua
+local count = source(0)
+
+root(function()
+ local text = derive(function()
+ return "count: " .. text()
+ end)
+
+ effect(function()
+ print(text())
+ end)
+end)
+```
+
+### Graph resulting from code
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#1C1C1F"
+ }
+}}%%
+
+graph LR
+
+subgraph root
+ text --> effect
+end
+
+count --> text
+```
+
+Notes:
+
+- Since `count` is a source, not an effect, it can exist
+ outside of a root reactive scope.
+- An update to `count` will cause `text` to rerun, which
+ then causes `effect` to rerun.
+- When the root reactive scope is destroyed, `text` and
+ `effect` will be destroyed alongside it, since they are
+ owned by it. `count` will be untouched and future updates
+ to `count` will have no effect.
diff --git a/docs/tut/crash-course/2-creation.md b/docs/tut/crash-course/2-creation.md
index e66f987..c1cafc0 100644
--- a/docs/tut/crash-course/2-creation.md
+++ b/docs/tut/crash-course/2-creation.md
@@ -9,36 +9,31 @@ Luau allows us to omit parentheses `()` when calling functions with string or
table literals which Vide takes advantage of for brevity.
```lua
-local mount = vide.mount
local create = vide.create
-local function App()
- return create "ScreenGui" {
- create "Frame" {
- AnchorPoint = Vector2.new(0.5, 0.5),
- Position = UDim2.fromScale(0.5, 0.5),
- Size = UDim2.fromScale(0.4, 0.7),
+return create "ScreenGui" {
+ create "Frame" {
+ AnchorPoint = Vector2.new(0.5, 0.5),
+ Position = UDim2.fromScale(0.5, 0.5),
+ Size = UDim2.fromScale(0.4, 0.7),
- create "TextLabel" {
- Text = "hi"
- },
+ create "TextLabel" {
+ Text = "hi"
+ },
- create "TextLabel" {
- Text = "bye"
- },
+ create "TextLabel" {
+ Text = "bye"
+ },
- create "TextButton" {
- Text = "click me",
+ create "TextButton" {
+ Text = "click me",
- Activated = function()
- print "clicked!"
- end
- }
+ Activated = function()
+ print "clicked!"
+ end
}
}
-end
-
-mount(App, game.StarterGui)
+}
```
Assign a value to a string key to set a property, and assign a value to a
diff --git a/docs/tut/crash-course/3-components.md b/docs/tut/crash-course/3-components.md
index a9f38d9..f6b3ad4 100644
--- a/docs/tut/crash-course/3-components.md
+++ b/docs/tut/crash-course/3-components.md
@@ -1,8 +1,11 @@
# Components
+Vide encourages separating different parts of your UI into functions called
+*components*.
+
A component is a function that creates and returns a piece of UI.
-This is a way to separate your app into small chunks that you can reuse and put
+This is a way to separate your UI into small chunks that you can reuse and put
together.
::: code-group
@@ -66,12 +69,15 @@ Above is a simple example of a button component being used across files.
A single parameter `props` is used to pass properties to the component.
-Components allow you to *encapsulate* behavior. You can only modify the
-component in ways that you allow in the component, through the `props` parameter.
+You can only modify the component in ways that you allow in the component,
+through the `props` parameter.
To create a new button all you must do is call the `Button` function, passing in
values. This saves having to create and set every property each time. Also, when
updating the button component in future, any changes to the button file will be
seen anywhere the button is used in your app.
-This can be extended to much more complicated UI.
+The `mount()` function is used to set up Vide's reactivity system when creating
+your UI. It only needs to be called once at the top-level with the function that
+puts together your entire app. It also parents the returned instance to another
+a target instance for you.
diff --git a/docs/tut/crash-course/4-source.md b/docs/tut/crash-course/4-source.md
index f5a848a..c356b6b 100644
--- a/docs/tut/crash-course/4-source.md
+++ b/docs/tut/crash-course/4-source.md
@@ -23,9 +23,6 @@ count(count() + 1) -- increment count by 1
Sources can be *derived* by wrapping them in functions. A wrapped source
effectively becomes a new source.
-Derived sources should be pure functions. This is where the same output is
-always produced for the same input no matter how many times it is reran.
-
```lua
local count = source(0)
@@ -40,6 +37,5 @@ print(text()) -- "count: 1"
Sources on their own aren't very special, the above can be achieved with plain
variables. The real use for sources become apparent when used in combination
-with Vide's *reactive scopes*. When a source is read from within a reactive
-scope, it can automatically rerun the scope that reads it when the source is
-updated in the future.
+with *effects*. Similar to a signal and connection, a source and effect allows
+you to do things like automatically updating UI when a source is updated.
diff --git a/docs/tut/crash-course/6-root.md b/docs/tut/crash-course/6-root.md
index 2b2d7ad..abeec2f 100644
--- a/docs/tut/crash-course/6-root.md
+++ b/docs/tut/crash-course/6-root.md
@@ -2,9 +2,9 @@
Any reactive scopes created, such as by `effect()`, must be done so within a
"root" reactive scope. This is the main purpose of `mount()`, which you use
-once at the top level to create your app as shown in the first introduction.
+once at the top level to create your UI.
-This is so that when the app is unmounted, it can clean up any reactive scopes
+This is so that if you want to destroy your UI, it can stop any reactive scopes
created within it, since reactive scopes track any reactive scopes created
within them.
@@ -53,16 +53,22 @@ The reactive graph for the above example looks like so:
graph
-subgraph root["mount"]
+subgraph root
direction LR
count --> effect
end
```
-When the `mount` scope is destroyed, the `effect` scope will also be destroyed
-since it was created within it.
+When the root reactive scope created by `mount()` is destroyed, the `effect`
+scope will also be destroyed since it was created within it.
+
+This is important because you may have an effect that updates the property of a
+UI instance, meaning the effect is referencing and holding that instance in
+memory. The effect being destroyed will remove this reference, allowing the
+instance to be garbage collected.
You don't need to worry about ensuring all your effects are created within a
root scope, since you should be creating all your UI and corresponding effects
within a top-level `mount()` call that puts all your UI together. So it is safe
to assume that any effect you create will be created under this top level scope.
+Vide will prevent you from accidently doing otherwise anyways.
diff --git a/docs/tut/crash-course/7-stateful-component.md b/docs/tut/crash-course/7-stateful-component.md
index 8de8209..12f16a2 100644
--- a/docs/tut/crash-course/7-stateful-component.md
+++ b/docs/tut/crash-course/7-stateful-component.md
@@ -1,6 +1,6 @@
# Stateful Components
-A stateful component is a component that stores and displays some data.
+A stateful component is a component that can update in reponse to data.
Stateful components in Vide are created using sources and effects - sources to
store the data, and effects to display the data.
@@ -32,9 +32,6 @@ end
Above is an example of a counter component, that when clicked, will increment
its internal count, and automatically update its text to reflect that count.
-Making a property update based on a source is also referred to as *property
-binding*.
-
Each instance of `Counter()` will maintain its own independent count, since the
count source is created inside the scope of the component.
@@ -70,4 +67,4 @@ count(1) -- the Counter component will update to display this count
Sources can be created internally or passed in from externally, there are no
restrictions on how they are used as long as the effect using it is created
-within a reactive scope so that it can be cleaned up later.
+within a reactive scope.
diff --git a/docs/tut/crash-course/8-property-binding.md b/docs/tut/crash-course/8-property-binding.md
index 8304565..a981b8a 100644
--- a/docs/tut/crash-course/8-property-binding.md
+++ b/docs/tut/crash-course/8-property-binding.md
@@ -25,17 +25,17 @@ end
This example is equivalent to the example seen on the previous page.
-Instead of explicitly creating an effect, assigning a (non-event) property
-a function will implicitly create a side-effect to update that property anytime
-a dependent source is updated.
+Instead of explicitly creating an effect, assigning a (non-event) property a
+function will implicitly create an effect to update that property anytime a
+source used within is updated.
Just like effects, the function is ran immediately in a reactive scope to set
-the property initially and determine what sources are being depended on.
+the property initially and determine what sources are being used.
This allows you as the programmer to not need to manually update UI as the state
-of your program changes. You just define how the data sources map to UI, and
-Vide's reactive system will automatically update any properties depending on
-those sources that were updated.
+of your program changes. You just define how data sources map to UI, and Vide's
+reactive system will automatically update any properties depending on those
+sources.
## Children Binding
diff --git a/docs/tut/crash-course/9-derived-source.md b/docs/tut/crash-course/9-derived-source.md
index 17e3b77..d3b7156 100644
--- a/docs/tut/crash-course/9-derived-source.md
+++ b/docs/tut/crash-course/9-derived-source.md
@@ -29,13 +29,10 @@ local text = function()
return "count: " .. tostring(count())
end
-effect(function()
- text() -- prints "ran"
-end)
+effect(function() text() end)
+effect(function() text() end)
-effect(function()
- text() -- prints "ran" again
-end)
+source(1) -- prints "ran" x2
```
To avoid this, you can use `derive()` to derive a new source instead. This will
@@ -55,16 +52,13 @@ local text = derive(function()
return "count: " .. tostring(count())
end)
-effect(function()
- text() -- prints "ran"
-end)
+effect(function() text() end)
+effect(function() text() end)
-effect(function()
- text() -- does not print, returns cached value
-end)
+source(1) -- prints "ran" x1
```
-`derive()` must also be used within a root reactive scope, just like `effect()`.
+`derive()` must also be called within a reactive scope, just like `effect()`.
If the recalculated value is the same as the old value, the derived source will
not rerun the effects using it.
From c527a62ab1dc04be03cabc7e69ae2d401471972c Mon Sep 17 00:00:00 2001
From: aaron <83140718+centau@users.noreply.github.com>
Date: Wed, 22 Nov 2023 10:14:00 +0000
Subject: [PATCH 033/116] Bump version to `0.2.0`
---
CHANGELOG.md | 7 ++++---
src/init.luau | 2 +-
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ea3ffa..83d16e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
--------------------------------------------------------------------------------
-## Unreleased
+## [0.2.0] - 2023-11-22
### Added
@@ -15,12 +15,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- Improved graph updating algorithm.
-- Graph nodes no longer destroy children; only owned.
+- Graph nodes when destroyed no longer destroy children; only owned.
### Fixed
- Graph edge case where a destroyed node can be readded if it was queued for
- evaluation before being destroyed.
+ rerun before being destroyed.
+- Some properties not being applied when `create()` is used recursively.
--------------------------------------------------------------------------------
diff --git a/src/init.luau b/src/init.luau
index 17812c3..840f7f9 100644
--- a/src/init.luau
+++ b/src/init.luau
@@ -1,6 +1,6 @@
--------------------------------------------------------------------------------
-- vide.luau
--- v0.1.1
+-- v0.2.0
--------------------------------------------------------------------------------
if not game then script = require "test/relative-string" end
From 50244c2bd8bf21f8bf031970cfa9d7a99dc6c727 Mon Sep 17 00:00:00 2001
From: aaron <83140718+centau@users.noreply.github.com>
Date: Wed, 22 Nov 2023 10:34:02 +0000
Subject: [PATCH 034/116] Update wally version
---
wally.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/wally.toml b/wally.toml
index d4d48ed..b1387e5 100644
--- a/wally.toml
+++ b/wally.toml
@@ -2,7 +2,7 @@
name = "centau/vide"
description = "A reactive Luau library for creating UI. "
license = "MIT"
-version = "0.1.1"
+version = "0.2.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
include = ["default.project.json", "LICENSE", "src"]
From 3aed45212a1bb96ce1d8ba18649fc6482d02f94f Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Fri, 24 Nov 2023 11:09:32 +0000
Subject: [PATCH 035/116] Fix site mermaid rendering
Vitepress rc 26 breaks mermaid plugin - use rc 25.
---
docs/package.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/package.json b/docs/package.json
index e156bea..5dce993 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -8,7 +8,7 @@
},
"devDependencies": {
- "vitepress": "^1.0.0-rc.4",
- "vitepress-plugin-mermaid": "^2.0.14"
+ "vitepress": "1.0.0-rc.25",
+ "vitepress-plugin-mermaid": "2.0.14"
}
}
From d682161c06229f9cb280041fe817680456ad9784 Mon Sep 17 00:00:00 2001
From: Aaron Smith <83140718+centau@users.noreply.github.com>
Date: Thu, 7 Dec 2023 17:26:01 +0000
Subject: [PATCH 036/116] Update docs
---
docs/.vitepress/config.ts | 2 +-
docs/tut/advanced/nested-scoping.md | 188 +++++++++++++
docs/tut/advanced/reactive-scoping.md | 253 ------------------
docs/tut/crash-course/10-cleanup.md | 45 +---
docs/tut/crash-course/11-control-flow.md | 166 ++++--------
docs/tut/crash-course/15-concepts.md | 75 +++---
docs/tut/crash-course/2-creation.md | 5 +-
docs/tut/crash-course/3-components.md | 8 +-
docs/tut/crash-course/5-effect.md | 8 +-
docs/tut/crash-course/6-root.md | 32 ++-
docs/tut/crash-course/7-stateful-component.md | 9 +-
docs/tut/crash-course/8-property-binding.md | 13 +-
src/graph.luau | 4 +-
13 files changed, 315 insertions(+), 493 deletions(-)
create mode 100644 docs/tut/advanced/nested-scoping.md
delete mode 100644 docs/tut/advanced/reactive-scoping.md
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 0a3500d..87dada1 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -58,7 +58,7 @@ export default withMermaid({
{
text: "Advanced Reactivity",
items: [
- { text: "Reactive Scopes", link: "/tut/advanced/reactive-scoping.md"}
+ { text: "Nested Scopes", link: "/tut/advanced/nested-scoping.md"}
]
}
],
diff --git a/docs/tut/advanced/nested-scoping.md b/docs/tut/advanced/nested-scoping.md
new file mode 100644
index 0000000..17c95d2
--- /dev/null
+++ b/docs/tut/advanced/nested-scoping.md
@@ -0,0 +1,188 @@
+# Nested Reactive Scopes
+
+Nesting reactive scopes gives you finer control over the reactive graph, but
+needs more work to do. The built-in control flow functions try to cover the
+most common cases, but they do not cover all of them.
+
+This tutorial will demonstrate how to implement a `show()` control flow function
+using just sources and effects.
+
+```lua
+local mount = vide.mount
+local source = vide.source
+local show = vide.show
+
+local function Counter()
+ local count = source(0)
+
+ return create "TextButton" {
+ Text = count,
+ Activated = function() count(count() + 1) end
+ }
+end
+
+mount(function()
+ local toggled = source(true)
+
+ show(toggled, Button)
+end)
+```
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#1C1C1F"
+ }
+}}%%
+
+graph
+
+subgraph mount
+ direction LR
+ toggle --> show
+
+ subgraph show[show effect]
+ text[Text effect]
+ end
+end
+```
+
+Above is the reactive graph for `show()`. It creates a new effect depending on
+`toggle` where anytime `toggle` is truthy, it will create a new `Counter`. The
+`show` effect calls `Counter`, which creates a new reactive scope to update its
+text whenever `count` changes. As per the rules of reactive scopes, a reactive
+scope rerunning will destroy any reactive scope created within it. So the text
+effect's reactive scope is destroyed whenever the show effect is rerun.
+
+The same can be achieved without the use of `show()`:
+
+```lua
+local mount = vide.mount
+local source = vide.source
+local effect = vide.effect
+local cleanup = vide.cleanup
+
+local function Counter()
+ local count = source(0)
+
+ return create "TextButton" {
+ Text = count,
+ Activated = function() count(count() + 1) end
+ }
+end
+
+mount(function()
+ local toggled = source(true)
+
+ effect(function()
+ if toggled() then
+ local destroy = mount(Button)
+ cleanup(destroy)
+ end
+ end)
+end)
+```
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#1C1C1F"
+ }
+}}%%
+
+graph
+
+subgraph mount
+ direction LR
+ toggle --> effect
+
+ subgraph effect
+ subgraph mount2[inner mount]
+ text[Text effect]
+ end
+ end
+end
+```
+
+This is another way to achieve the same. Here we use `mount()` within the effect
+to manually create and destroy a new reactive scope whenever the effect reruns.
+
+Alternatively, instead of using `mount()`, a new reactive scope can be created
+directly within the effect:
+
+```lua
+local mount = vide.mount
+local source = vide.source
+local effect = vide.effect
+local untrack = vide.untrack
+
+local function Counter()
+ local count = source(0)
+
+ return create "TextButton" {
+ Text = count,
+ Activated = function() count(count() + 1) end
+ }
+end
+
+mount(function()
+ local toggled = source(true)
+
+ effect(function()
+ if toggled() then
+ untrack(Button)
+ end
+ end)
+end)
+```
+
+```mermaid
+%%{init: {
+ "theme": "base",
+ "themeVariables": {
+ "primaryColor": "#1B1B1F",
+ "primaryTextColor": "#fff",
+ "primaryBorderColor": "#1B1B1F",
+ "lineColor": "#79B8FF",
+ "tertiaryColor": "#161618",
+ "tertiaryBorderColor": "#1C1C1F"
+ }
+}}%%
+
+graph
+
+subgraph mount
+ direction LR
+ toggle --> effect
+
+ subgraph effect
+ text[Text effect]
+ end
+end
+```
+
+Without the use of `untrack()`, an error would occur, since Vide does not allow
+the creation of reactive scopes inside reactive scopes that are tracking. The
+reason for this, is because if the `Counter` component reads from a source
+internally, that can cause the reactive scope calling `Counter()` to track that
+source, causing unintentional reruns. As a guard against this, you are forced to
+use `untrack()` to create nested reactive scopes.
+
+The final result is the same as using the `show()` component. An effect is
+created which creates the counter, which creates its own reactive scope. The
+effect rerunning causes the counter's internal reactive scope to be destroyed,
+making sure everything is cleaned up.
+
+
diff --git a/docs/tut/advanced/reactive-scoping.md b/docs/tut/advanced/reactive-scoping.md
deleted file mode 100644
index 20872cf..0000000
--- a/docs/tut/advanced/reactive-scoping.md
+++ /dev/null
@@ -1,253 +0,0 @@
-# Reactive Scoping
-
-This is a brief document designed to give the user more insight into how Vide's
-reactive system works.
-
-## Graph Basics
-
-Vide's reactivity can be represented as a graph, where each source, derived
-source, and effect is a node on that graph. The term "*reactive scope*" is just
-an abstraction used to refer to these nodes. Each node is a reactive scope.
-
-Each node stores a cached value, a side-effect function, cleanup functions,
-its parents and children, and its owner and owned.
-
-Whenever a node is updated it will:
-
-1. destroy its owned nodes
-2. run its cleanups
-3. rerun its side-effect and update its cached value
-4. if its cached value changes, update its children recursively.
-
-There is a difference between children nodes and owned nodes:
-
-- children nodes are updated when a parent is updated.
-- owned nodes are destroyed when a parent is updated.
-- both children and owned are destroyed when a parent is destroyed.
-
-Nodes created by `root()` generally have no children, and only tracks owned.
-Nodes created by `derive()` generally have no owned, and only tracks children.
-
-## Basic Example
-
-```lua
-root(function()
- local forename = source "quan"
- local surname = source "xi"
-
- local name = derive(function()
- return forename() .. " " .. surname()
- end)
-
- effect(function()
- print("new name: " .. name())
- end)
-end)
-```
-
-This code will produce a graph that looks like so:
-
-```mermaid
-%%{init: {
- "theme": "base",
- "themeVariables": {
- "primaryColor": "#1B1B1F",
- "primaryTextColor": "#fff",
- "primaryBorderColor": "#1B1B1F",
- "lineColor": "#79B8FF",
- "tertiaryColor": "#161618",
- "tertiaryBorderColor": "#161618"
- }
-}}%%
-
-graph
- subgraph root
- forename & surname --> name
- name --> effect
- end
-```
-
-Nodes connected by arrows represent parent and children connections.
-Nodes within other nodes represent owner and owned connections.
-
-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.
-
-When the side-effect for a node is being reran when a node is updated, any
-other nodes read within that side-effect are set as parents of the node
-currently being reran. As those nodes are read, we know that the current node
-depends on them, so any time those nodes are updated, they will update dependent
-nodes since they will be stored as children.
-
-When destroying a node, its descendents are traversed and also destroyed.
-When being destroyed, a node's connections (parents and children, owner and
-owned) 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 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.
-
-Nodes created by `source()` can actually exist outside of root nodes, since
-they do not have direct side-effects or cleanups, they do not have to be
-explicitly destroyed.
-
-## Control-flow Graph Example
-
-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(props: { text: string })
- local count = source(0)
-
- local connection = stepped:Connect(function() count(count() + 1) end)
-
- cleanup(function() connection:Disconnect() end)
-
- return create "TextLabel" {
- Text = function()
- return props.text() .. ": " .. count()
- end
- }
-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.
-
-```lua
-root(function()
- local counters = { "A", "B" }
-
- indexes(counters, function(name)
- return Counter { text = name }
- end)
-end)
-```
-
-This code produces a graph like so:
-
-```mermaid
-%%{init: {
- "theme": "base",
- "themeVariables": {
- "primaryColor": "#1B1B1F",
- "primaryTextColor": "#fff",
- "primaryBorderColor": "#1B1B1F",
- "lineColor": "#79B8FF",
- "tertiaryColor": "#161618",
- "tertiaryBorderColor": "#fff"
- }
-}}%%
-
-graph LR
- subgraph root
- counters --> indexes
-
- subgraph root1[subroot 1]
- n1[name] --> p1[prop binding]
- end
-
- subgraph root2[subroot 2]
- n2[name] --> p2[prop binding]
- end
- end
-
- indexes .-> root1 & root2
-```
-
-This shows how the `indexes()` control flow function creates and manages new
-root scopes. The function creates an effect seen as `indexes` in the graph,
-which manages the new roots `subroot 1` and `subroot 2`, as well as the sources
-`name` for which one exists for each index value in the input table.
-
-When the input table changes, `indexes()` can automatically destroy and create
-subroots based on the changed indexes. Destroyed nodes run any cleanups made, in
-this case it is the cleanups to disconnect the counters connection. The same
-applies to all other control flow functions.
-
-Whenever the root reactive scope is destroyed, all its children, `counters` and
-`indexes` will be destroyed too, which means that `indexes` children, the
-subroots, will also be destroyed. Everything is nicely cleaned up.
-
-## Custom Control-flow Example
-
-Below is a simple example of the `show()` control-flow function.
-
-Each time `visible` changes, `show()` will destroy the current reactive scope
-and rerun its function in a new one.
-
-```lua
-local visible = source(true)
-local count = source(0)
-
-root(function()
- show(visible, function()
- return create "TextLabel" { Text = count }
- end)
-end)
-```
-
-The above code produces a graph like so:
-
-```mermaid
-%%{init: {
- "theme": "base",
- "themeVariables": {
- "primaryColor": "#1B1B1F",
- "primaryTextColor": "#fff",
- "primaryBorderColor": "#1B1B1F",
- "lineColor": "#79B8FF",
- "tertiaryColor": "#161618",
- "tertiaryBorderColor": "#1B1B1F"
- }
-}}%%
-
-graph LR
- subgraph root
- direction LR
- show
-
- subgraph subroot["show() subroot"]
- p1[prop binding]
- end
- end
-
- visible --> show
- count --> p1
- show -.- subroot
-```
-
-This can be recreated without the `show()` control-flow function, with the
-following code:
-
-```lua
-local visible = source(true)
-local count = source(0)
-
-root(function()
- local output = derive(function()
- visible()
-
- -- untrack so any source read from within this scope
- -- will not cause the outer `derive()` call to rerun,
- -- we only want `derive()` to rerun when `visible` changes
- return untrack(function()
- local label = create "TextLabel" {}
-
- effect(function()
- label.Text = count()
- end)
-
- return label
- end)
- end)
-end)
-```
-
-Both of the above code samples will produce the same visible result.
diff --git a/docs/tut/crash-course/10-cleanup.md b/docs/tut/crash-course/10-cleanup.md
index b9361ec..bf632f5 100644
--- a/docs/tut/crash-course/10-cleanup.md
+++ b/docs/tut/crash-course/10-cleanup.md
@@ -2,7 +2,8 @@
Sometimes you may need to do some cleanup when destroying a component or after
a side-effect from a source update. Vide provides a function `cleanup()` which
-is used to queue a cleanup callback for the next time a reactive scope re-runs.
+is used to queue a cleanup callback for the next time a reactive scope is rerun
+or destroyed.
```lua
local mount = vide.mount
@@ -32,53 +33,19 @@ end
local unmount = mount(Timer)
-unmount() -- all queued cleanups are ran, heartbeat connection stopped
+unmount() -- all queued cleanups are ran, heartbeat connection disconnected
```
In the above example, this allows us to disconnect the heartbeat connection
when the reactive scope responsible for creating the timer component is
destroyed, such as when it is unmounted.
-Vide does not see "components", it only sees reactive scopes and how they are
-linked together. Components are just a user pattern that creates UI instances
-alongside effects. In other words, instances are just a side-effect of the
-reactive graph. When a reactive scope is created, you create a corresponding
-instance to display that data, when that reactive scope is destroyed, any
-cleanups queued will be ran and take care of anything that needs to be, such
-as disconnecting connections.
-
-This is another reason why `mount()` is used at the top level of your app, so
-that any registered cleanups created by your app components can be ran when
-they are destroyed.
-
-Side note: Roblox instances do not need to be explicitly destroyed for their
+::: tip
+Roblox instances do not need to be explicitly destroyed for their
memory to be freed, they only need to be parented to `nil`. So there is no
need to use `cleanup()` to destroy instances. However, be wary of connecting
a function that references an instance to an event from the same instance,
this causes the instance to reference itself and never be freed. In such a case
you would need to use `cleanup()` to disconnect this connection or to explicitly
destroy the instance.
-
-The reactive graph for the above example:
-
-```mermaid
-%%{init: {
- "theme": "base",
- "themeVariables": {
- "primaryColor": "#1B1B1F",
- "primaryTextColor": "#fff",
- "primaryBorderColor": "#1B1B1F",
- "lineColor": "#79B8FF",
- "tertiaryColor": "#161618",
- "tertiaryBorderColor": "#161618"
- }
-}}%%
-
-graph
-
-subgraph mount
- direction LR
- cleanup([cleanup]) ~~~ count
- count --> bind["effect (text binding)"]
-end
-```
+:::
diff --git a/docs/tut/crash-course/11-control-flow.md b/docs/tut/crash-course/11-control-flow.md
index e5430b8..91f95ac 100644
--- a/docs/tut/crash-course/11-control-flow.md
+++ b/docs/tut/crash-course/11-control-flow.md
@@ -1,123 +1,57 @@
# Control Flow
-Eventually you will need a way to dynamically create and destroy UI elements
+Eventually you may need a way to dynamically create and destroy UI elements
resulting from source updates. Vide provides functions to help you do this,
known as *control flow* functions.
These functions return new sources, which hold the instances to be displayed.
-These sources can be assigned as children, meaning the displayed children
-will update when the input source updates.
-Control flow functions are special, because they run their components in a new
-reactive scope, which can be destroyed independently of the reactive scope that
-called the control flow function itself. This means that parts of your app can
-be independently created then destroyed.
-
-## show()
-
-The most basic control flow function is `show()`, which is used to conditionally
-show a component.
-
-```lua
-local source = vide.source
-local show = vide.show
-
-local function JoinMenu()
- local joined = source(false)
-
- local function JoinButton()
- return Button {
- Activated = function() joined(true) end
- }
- end
-
- return create "Frame" {
- show(function() return not joined() end, JoinButton)
- }
-end
-```
-
-This will make a button to join if you have not joined already.
-
-You can also pass a third argument, a fallback to show if the condition is falsey.
-
-```lua
-local function JoinMenu()
- local joined = source(false)
-
- local function JoinButton()
- return Button {
- Activated = function() joined(true) end
- }
- end
-
- local function LeaveButton()
- return Button {
- Activated = function() joined(false) end
- }
- end
-
- return create "Frame" {
- show(joined, LeaveButton, JoinButton)
- }
-end
-```
-
-The reactive graph for the above example:
-
-```mermaid
-%%{init: {
- "theme": "base",
- "themeVariables": {
- "primaryColor": "#1B1B1F",
- "primaryTextColor": "#fff",
- "primaryBorderColor": "#1B1B1F",
- "lineColor": "#79B8FF",
- "tertiaryColor": "#161618",
- "tertiaryBorderColor": "#1C1C1F"
- }
-}}%%
-
-graph
-
-subgraph root["mount() scope"]
- direction LR
- joined --> show -.- subroot
-
- subgraph subroot["show() scope"]
- direction LR
- Button
- end
-end
-```
-
-`show()` will implicitly create an effect depending on `joined`, which can be
-seen as `show` on the graph. This effect manages, and can create or destroy
-a separate reactive scope seen as `show() scope` on the graph. The dotted line
-indicates that it isn't actually connected, only indirectly managed through
-code.
+Control flow functions run their components in a new reactive scope, which can
+be destroyed independently of the reactive scope that called the control flow
+function. This means parts of your app can be independently created and
+destroyed.
## switch()
-Similar to `show()`, `switch()`, also condtionally displays one instance at a
-time. It is more flexible since it can show one of many components, based on a
-table used to map a source value to a component.
+`switch()` condtionally displays one instance at a time. It uses a table to map
+a source value to a component.
```lua
local source = vide.source
local switch = vide.switch
+local function Button(props: {
+ Text: string,
+ Activated: () -> ()
+})
+ local hovered = source(false)
+
+ return create "TextButton" {
+ Text = props.Text,
+ Activated = props.Activated,
+
+ TextColor3 = function()
+ return hovered() and Color3.new(1, 1, 1) or Color3.new(.7, .7, .7)
+ end,
+
+ MouseEnter = function() hovered(true) end,
+ MouseLeave = function() hovered(false) end
+ }
+end
+
local function JoinMenu()
local joined = source(false)
local function JoinButton()
return Button {
+ Text = "Join",
Activated = function() joined(true) end
}
end
local function LeaveButton()
return Button {
+ Text = "Leave"
Activated = function() joined(false) end
}
end
@@ -131,22 +65,6 @@ local function JoinMenu()
end
```
-This example is equivalent to the previous one.
-
-The switch can map any value to any component.
-
-```lua
-type ActiveMenu = "none" | "inventory" | "shop" | "settings"
-
-local menu = source "inventory"
-
-switch(menu) {
- inventory = InventoryMenu,
- shop = ShopMenu,
- settings = SettingsMenu
-}
-```
-
The reactive graph for the above example:
```mermaid
@@ -164,23 +82,30 @@ The reactive graph for the above example:
graph
-subgraph root["mount() scope"]
+subgraph root["root scope"]
direction LR
- menu --> switch -.- subroot
+ joined --> switch -.- subroot
- subgraph subroot["switch() scope"]
+ subgraph subroot["switch scope"]
direction LR
- Menu
+ effect["TextColor3 effect"]
end
end
```
+A `switch()` call creates a new effect and a new scope as seen in the above
+graph. Whenever `menu` updates, it causes the `switch` effect to run, which
+will destroy and recreate the switch scope with the new component.
+
+This will also destroy the internal effect that the button uses to highlight
+itself when it is hovered, each time the switch is rerun.
+
## indexes()
Often, you will have a table of values with each value displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
-UI element, `indexes()` allows you to create elements for each table index, to
-display the value at that index.
+UI element, `indexes()` allows you to create elements each corresponding to a
+table index, to display the value at that index.
```lua
local todoList = source {
@@ -222,7 +147,8 @@ value), will have its corresponding reactive scope destroyed to clean up that
element.
`indexes()` is said to *map* each table index to a new UI element that can
-update to display the current value at that index.
+update to display the current value at that index. Each table index is given a
+single corresponding UI element.
The reactive graph for the above example:
@@ -241,16 +167,16 @@ The reactive graph for the above example:
graph
-subgraph root ["mount() scope"]
+subgraph root ["root scope"]
direction LR
todoList --> indexes -.- subroot1 & subroot2
- subgraph subroot1 ["indexes() scope 1"]
+ subgraph subroot1 ["indexes scope 1"]
direction LR
value1[todo] --> prop1["prop binding"]
end
- subgraph subroot2 ["indexes() scope 2"]
+ subgraph subroot2 ["indexes scope 2"]
direction LR
value2[todo] --> prop2[prop binding]
end
diff --git a/docs/tut/crash-course/15-concepts.md b/docs/tut/crash-course/15-concepts.md
index 2c2c313..62ee35d 100644
--- a/docs/tut/crash-course/15-concepts.md
+++ b/docs/tut/crash-course/15-concepts.md
@@ -6,73 +6,64 @@ A summary of all the concepts covered during the crash course.
A source of data.
-Stores a single value that can be updated by the user.
+Stores a single value that can be updated.
+
+Created with `source()`.
+
+# Derived Source
+
+A new source composed of other sources.
+
+Created with a plain function or with `derive()`.
## Effect
-Anything that happens in reponse to a source update.
+Anything that happens in response to a source update.
-Vide has built-in functions to create effects such as
-
-- `effect()` - runs arbitrary user code on source update
-- `derive()` - updates a derived source on source update
+Created with `effect()`.
## Reactive Scope
-A scope created by certain Vide functions where source updates can be tracked,
-and cleanups queued.
-
-When a source used inside a reactive scope is updated, the reactive scope will
-rerun.
-
-Reactive scopes are created by functions such as
+A scope created by certain functions such as:
- `root()`
- `effect()`
- `derive()`
-## Owner
+Reactive scopes can:
-A reactive scope created within an outer reactive scope, is *owned* by the outer
-reactive scope.
+- track sources that are read from within.
+- rerun when a tracked source updates.
+- track new reactive scopes created from within.
-When a reactive scope is re-ran or destroyed, all reactive scopes owned by it
-are also destroyed.
+## Scope Owners
-Vide does not let you create reactive scopes without owners.
+A reactive scope created within another reactive scope is *owned* by the other
+reactive scope, with the exception of the reactive scope created by `root()`.
-## Root Reactive Scope
+When a reactive scope is rerun or destroyed, all reactive scopes owned by it are
+automatically destroyed.
-A top-level reactive scope. These scopes are an exception to the owner rule.
-
-Created by `root()`, which `mount()` uses internally.
-
-A root reactive scope can be created on its own. It allows other reactive scopes
-to be created with an owner.
-
-Root reactive scopes must be destroyed manually by the user, a function to do
-this is given by `root()`.
-
-A root reactive scope can be created within another reactive scope and it will
-not automatically be owned by that scope.
+`root()`, which `mount()` uses internally, creates a reactive scope with no
+owner, since it must be destroyed manually using a destructor
+returned.
## Cleanup
-Cleans up the result from an effect.
+Arbitrary code to run whenever a reactive scope is rerun or destroyed.
-Unneeded in most cases, a cleanup is arbitrary code that can be ran before
-a reactive scope is rerun or destroyed, so that the result from the previous
-run can be cleaned up. A cleanup can be queued by using `cleanup()` within
-a reactive scope.
+Queue a function to run using `cleanup()`.
## Tracking
-Reactive scopes are tracking by default, meaning sources read from within scope
-will be tracked.
+Sources read from within a reactive scope will be tracked. This can be disabled
+using `untrack()`, which will make reactive scopes temporarily ignore sources
+read.
-A reactive scope can be made temporarily non-tracking within `untrack()`, so
-that any source used will be ignored. The only function that creates a
-nontracking reactive scope by default is `root()`.
+The reactive scope created by `root()` is non-tracking by default.
+
+As a guard against misusage, a reactive scope cannot be created within a
+reactive scope, unless it is made non-tracking using `untrack()`.
## Reactive Graph
diff --git a/docs/tut/crash-course/2-creation.md b/docs/tut/crash-course/2-creation.md
index c1cafc0..96f9dd2 100644
--- a/docs/tut/crash-course/2-creation.md
+++ b/docs/tut/crash-course/2-creation.md
@@ -6,7 +6,7 @@ Instances are created using `create()`.
properties to assign when creating a new instance for that class.
Luau allows us to omit parentheses `()` when calling functions with string or
-table literals which Vide takes advantage of for brevity.
+table literals which is recommended to use for brevity.
```lua
local create = vide.create
@@ -44,6 +44,5 @@ to a string key.
When creating an instance with no properties, it is important to not forget to
actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
To be clear, `create "Frame"` returns a *function* which is a constructor for
-that class, not an instance of that class. This would result in you attempting
-to parent a function instead of an instance which is not correct.
+that class, not an instance of that class.
:::
diff --git a/docs/tut/crash-course/3-components.md b/docs/tut/crash-course/3-components.md
index f6b3ad4..aa81916 100644
--- a/docs/tut/crash-course/3-components.md
+++ b/docs/tut/crash-course/3-components.md
@@ -35,7 +35,6 @@ return Button
```
```lua [App.luau]
-local mount = vide.mount
local create = vide.create
local Button = require(Button)
@@ -60,7 +59,7 @@ local function App()
}
end
-mount(App, game.StarterGui)
+App().Parent = game.StarterGui
```
:::
@@ -76,8 +75,3 @@ To create a new button all you must do is call the `Button` function, passing in
values. This saves having to create and set every property each time. Also, when
updating the button component in future, any changes to the button file will be
seen anywhere the button is used in your app.
-
-The `mount()` function is used to set up Vide's reactivity system when creating
-your UI. It only needs to be called once at the top-level with the function that
-puts together your entire app. It also parents the returned instance to another
-a target instance for you.
diff --git a/docs/tut/crash-course/5-effect.md b/docs/tut/crash-course/5-effect.md
index 9f0dd6b..275a2e6 100644
--- a/docs/tut/crash-course/5-effect.md
+++ b/docs/tut/crash-course/5-effect.md
@@ -21,11 +21,11 @@ count(1)
-- "count: 1" printed
```
-The callback given to `effect()` is initially ran immediately in a
-*reactive scope*. Any source read from inside a reactive scope will be tracked,
-so that if any of those sources update, the effect will be reran too.
+The callback given to `effect()` is ran immediately in a *reactive scope*. Any
+source read from inside a reactive scope will be tracked, so when any of those
+sources update, the effect will be reran too.
-Effects also work with derived sources, it doesn't matter how deeply nested
+Reactive scopes also track derived sources, it doesn't matter how deeply nested
inside a function a source is.
```lua
diff --git a/docs/tut/crash-course/6-root.md b/docs/tut/crash-course/6-root.md
index abeec2f..4cb47da 100644
--- a/docs/tut/crash-course/6-root.md
+++ b/docs/tut/crash-course/6-root.md
@@ -1,12 +1,16 @@
# Root Reactive Scopes
-Any reactive scopes created, such as by `effect()`, must be done so within a
-"root" reactive scope. This is the main purpose of `mount()`, which you use
-once at the top level to create your UI.
+Reactive scopes cannot be created on their own - they must be created within
+another reactive scope so that it can be tracked and later destroyed when it is
+no longer needed.
-This is so that if you want to destroy your UI, it can stop any reactive scopes
-created within it, since reactive scopes track any reactive scopes created
-within them.
+This is the purpose of `mount()`, which creates an initial "root", or
+"top-level" reactive scope, which all other reactive scopes, such as
+ones created by `effect()`, can stem from.
+
+When this root reactive scope is destroyed, it will ensure all other reactive
+scopes created within it are also destroyed, ensuring everything is cleaned up
+properly.
```lua
local source = vide.source
@@ -20,13 +24,15 @@ local function App()
end)
end
-vide.mount(App) -- works!
App() -- will error since effect() was not called within a reactive scope
+
+vide.mount(App) -- works!
+
```
-Mounting returns a function that when called will destroy any reactive scopes
-created during the `mount()` call.
+Mounting returns a function that when called will destroy its reactive scope,
+along with any other reactive scopes created inside it.
```lua
local unmount = mount(App)
@@ -68,7 +74,7 @@ memory. The effect being destroyed will remove this reference, allowing the
instance to be garbage collected.
You don't need to worry about ensuring all your effects are created within a
-root scope, since you should be creating all your UI and corresponding effects
-within a top-level `mount()` call that puts all your UI together. So it is safe
-to assume that any effect you create will be created under this top level scope.
-Vide will prevent you from accidently doing otherwise anyways.
+root reactive scope, since you should be creating all your UI and corresponding
+effects within a top-level `mount()` call that puts all your UI together. So it
+is safe to assume that any effect you create will be created under this top
+level scope. Vide will prevent you from accidently doing otherwise anyways.
diff --git a/docs/tut/crash-course/7-stateful-component.md b/docs/tut/crash-course/7-stateful-component.md
index 12f16a2..85139db 100644
--- a/docs/tut/crash-course/7-stateful-component.md
+++ b/docs/tut/crash-course/7-stateful-component.md
@@ -1,6 +1,6 @@
# Stateful Components
-A stateful component is a component that can update in reponse to data.
+A stateful component is a component that stores some data internally.
Stateful components in Vide are created using sources and effects - sources to
store the data, and effects to display the data.
@@ -27,13 +27,18 @@ local function Counter()
return instance
end
+
+mount(Counter, game.StarterGui)
```
Above is an example of a counter component, that when clicked, will increment
its internal count, and automatically update its text to reflect that count.
Each instance of `Counter()` will maintain its own independent count, since the
-count source is created inside the scope of the component.
+count source is created inside the component.
+
+We use `mount()` to create the counter within a reactive scope, which also takes
+a second argument to parent the counter to another instance.
## External State
diff --git a/docs/tut/crash-course/8-property-binding.md b/docs/tut/crash-course/8-property-binding.md
index a981b8a..1fe18fc 100644
--- a/docs/tut/crash-course/8-property-binding.md
+++ b/docs/tut/crash-course/8-property-binding.md
@@ -1,8 +1,7 @@
# Property Binding
-Explicitly creating effects to update properties can become verbose when there
-are a lot of properties to update. Vide provides a way to *implicitly* create
-an effect to update properties on source update.
+Explicitly creating effects to update properties can be tedious. Vide provides a
+way to *implicitly* create an effect to update properties.
```lua
local create = vide.create
@@ -12,12 +11,12 @@ local function Counter()
local count = source(0)
return create "TextButton" {
- Text = function()
- return "count: " .. count()
- end,
-
Activated = function()
count(count() + 1)
+ end,
+
+ Text = function()
+ return "count: " .. count()
end
}
end
diff --git a/src/graph.luau b/src/graph.luau
index ecda1ab..2908e3d 100644
--- a/src/graph.luau
+++ b/src/graph.luau
@@ -45,9 +45,9 @@ local function assert_owning_scope(): Node
if not scope then
local caller_name = debug.info(2, "n")
- return throw(`cannot use {caller_name}() in non-reactive scope, must be used within a root() or mount() callback`)
+ return throw(`cannot use {caller_name}() in a non-reactive scope`)
elseif scope.effect then
- throw("cannot create new reactive scope inside of a tracking scope") -- todo: allow this?
+ throw("cannot create new reactive scope in a tracking reactive scope")
end
return scope
From 3d324cc30bed0b334a1a6d35d580e6c3717cad02 Mon Sep 17 00:00:00 2001
From: ReturnedTrue <58662983+ReturnedTrue@users.noreply.github.com>
Date: Mon, 19 Feb 2024 21:06:06 +0000
Subject: [PATCH 037/116] Update strict-mode.md (#24)
---
docs/api/strict-mode.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/api/strict-mode.md b/docs/api/strict-mode.md
index 958672e..a7fca48 100644
--- a/docs/api/strict-mode.md
+++ b/docs/api/strict-mode.md
@@ -33,6 +33,6 @@ As well as additional safety checks, Vide will dedicate extra resources to
recording and better emitting stack traces where errors occur, particularly
when binding properties to sources.
-It is recommend to develop UI with strict mode and to disable it when pushing to
+It is recommended to develop UI with strict mode and to disable it when pushing to
production. In Roblox, production code compiles at O2 by default, so you don't
need to worry about disabling strict mode unless you have manually enabled it.
From e1d69afa20754ab184a8f83286228b3ef2bf6261 Mon Sep 17 00:00:00 2001
From: richard <56808540+littensy@users.noreply.github.com>
Date: Sun, 21 Apr 2024 05:57:29 -0700
Subject: [PATCH 038/116] Fix springs passing negative Color3 values (#25)
* Fix springs passing negative Color3 values
* Clamp color values between 0 and 1
---
src/spring.luau | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/spring.luau b/src/spring.luau
index 91f17b1..cc92d9a 100644
--- a/src/spring.luau
+++ b/src/spring.luau
@@ -111,7 +111,7 @@ local vec6_to_type = {
end :: Vec6ToType,
Color3 = function(v)
- return Color3.new(v.X, v.Y, v.Z)
+ return Color3.new(math.clamp(v.X, 0, 1), math.clamp(v.Y, 0, 1), math.clamp(v.Z, 0, 1))
end :: Vec6ToType,
UDim = function(v)
From 67e87110c7f293871e75f236b293f8fc172963e6 Mon Sep 17 00:00:00 2001
From: Someon1e <142684596+Someon1e@users.noreply.github.com>
Date: Sun, 23 Jun 2024 18:40:24 +0100
Subject: [PATCH 039/116] Delete .gitattributes (#26)
---
.gitattributes | 1 -
1 file changed, 1 deletion(-)
delete mode 100644 .gitattributes
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 876fbd3..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-*.luau linguist-language=Lua
From f2de9b0e63d7581a3924e5254ad37e7da4625f17 Mon Sep 17 00:00:00 2001
From: aaron <83140718+centau@users.noreply.github.com>
Date: Thu, 20 Jun 2024 17:36:25 +0100
Subject: [PATCH 040/116] Refactor
---
docs/.vitepress/config.ts | 4 +-
docs/api/creation.md | 2 +-
docs/api/reactivity-core.md | 9 +-
docs/tut/advanced/nested-scoping.md | 31 ++--
docs/tut/crash-course/10-cleanup.md | 14 +-
docs/tut/crash-course/11-control-flow.md | 10 +-
docs/tut/crash-course/14-strict-mode.md | 2 +-
docs/tut/crash-course/15-concepts.md | 56 +++----
docs/tut/crash-course/2-creation.md | 7 -
docs/tut/crash-course/3-components.md | 12 --
.../crash-course/{6-root.md => 6-scope.md} | 60 ++++---
docs/tut/crash-course/7-stateful-component.md | 7 +-
...operty-binding.md => 8-implicit-effect.md} | 9 +-
docs/tut/crash-course/9-derived-source.md | 4 +-
src/bind.luau | 10 +-
src/cleanup.luau | 8 +-
src/derive.luau | 12 +-
src/effect.luau | 8 +-
src/graph.luau | 104 ++++++------
src/maps.luau | 83 +++++-----
src/root.luau | 10 +-
src/source.luau | 12 +-
src/spring.luau | 24 ++-
src/switch.luau | 25 ++-
test/tests.luau | 156 ++++++++----------
25 files changed, 315 insertions(+), 364 deletions(-)
rename docs/tut/crash-course/{6-root.md => 6-scope.md} (51%)
rename docs/tut/crash-course/{8-property-binding.md => 8-implicit-effect.md} (85%)
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 87dada1..51b477a 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -43,9 +43,9 @@ export default withMermaid({
{ text: "Components", link: "/tut/crash-course/3-components" },
{ text: "Sources", link: "/tut/crash-course/4-source" },
{ text: "Effects", link: "/tut/crash-course/5-effect" },
- { text: "Root Scopes", link: "/tut/crash-course/6-root" },
+ { text: "Scopes", link: "/tut/crash-course/6-scope" },
{ text: "Stateful Components", link: "/tut/crash-course/7-stateful-component" },
- { text: "Property Binding", link: "/tut/crash-course/8-property-binding" },
+ { text: "Property Binding", link: "/tut/crash-course/8-implicit-effect" },
{ text: "Derived Sources", link: "/tut/crash-course/9-derived-source" },
{ text: "Cleanup", link: "/tut/crash-course/10-cleanup" },
{ text: "Control Flow", link: "/tut/crash-course/11-control-flow" },
diff --git a/docs/api/creation.md b/docs/api/creation.md
index 8e67478..46d54c3 100644
--- a/docs/api/creation.md
+++ b/docs/api/creation.md
@@ -4,7 +4,7 @@
## mount()
-Runs a function in a new reactive scope and optionally applies its result to a
+Runs a function in a new stable scope and optionally applies its result to a
target instance.
- **Type**
diff --git a/docs/api/reactivity-core.md b/docs/api/reactivity-core.md
index 133bc4a..652c80a 100644
--- a/docs/api/reactivity-core.md
+++ b/docs/api/reactivity-core.md
@@ -3,12 +3,13 @@
:::warning
-Yielding is not allowed in any reactive scope. Strict mode can check for this.
+Yielding is not allowed in any stable or reactive scope. Strict mode will check
+for this.
:::
## root()
-Creates and runs a function in a new reactive scope.
+Creates and runs a function in a new stable scope.
- **Type**
@@ -20,8 +21,8 @@ Creates and runs a function in a new reactive scope.
Returns the result of the given function.
- Creates a new root reactive scope, where creation and derivations of sources
- can be tracked and properly disposed of.
+ Creates a new stable scope, where creation of effects can be tracked and
+ properly disposed of.
A function to destroy the root is passed into the callback, which will run
any cleanups and allow derived sources created to garbage collect.
diff --git a/docs/tut/advanced/nested-scoping.md b/docs/tut/advanced/nested-scoping.md
index 17c95d2..fb31c15 100644
--- a/docs/tut/advanced/nested-scoping.md
+++ b/docs/tut/advanced/nested-scoping.md
@@ -1,7 +1,6 @@
-# Nested Reactive Scopes
+# Nested Scopes
-Nesting reactive scopes gives you finer control over the reactive graph, but
-needs more work to do. The built-in control flow functions try to cover the
+Nesting scopes gives you finer control over the reactive graph, but needs more work to do. The built-in control flow functions try to cover the
most common cases, but they do not cover all of them.
This tutorial will demonstrate how to implement a `show()` control flow function
@@ -21,7 +20,7 @@ local function Counter()
}
end
-mount(function()
+root(function()
local toggled = source(true)
show(toggled, Button)
@@ -57,7 +56,7 @@ Above is the reactive graph for `show()`. It creates a new effect depending on
`toggle` where anytime `toggle` is truthy, it will create a new `Counter`. The
`show` effect calls `Counter`, which creates a new reactive scope to update its
text whenever `count` changes. As per the rules of reactive scopes, a reactive
-scope rerunning will destroy any reactive scope created within it. So the text
+scope rerunning will destroy any scopes created within it. So the text
effect's reactive scope is destroyed whenever the show effect is rerun.
The same can be achieved without the use of `show()`:
@@ -82,7 +81,10 @@ mount(function()
effect(function()
if toggled() then
- local destroy = mount(Button)
+ local destroy = root(function(destroy)
+ Counter()
+ return destroy
+ end)
cleanup(destroy)
end
end)
@@ -116,11 +118,14 @@ subgraph mount
end
```
-This is another way to achieve the same. Here we use `mount()` within the effect
-to manually create and destroy a new reactive scope whenever the effect reruns.
+This is another way to achieve the same. Here we use `root()` within the effect
+to manually create and destroy a new stable scope whenever the effect reruns.
-Alternatively, instead of using `mount()`, a new reactive scope can be created
-directly within the effect:
+The reason for creating a stable scope is to prevent the effect from tracking
+any sources that may be read inside the `Counter()` call. Otherwise, the effect
+may be rerun needlessly and recreate the counter.
+
+Alternatively, instead of using `root()`:
```lua
local mount = vide.mount
@@ -174,7 +179,9 @@ end
```
Without the use of `untrack()`, an error would occur, since Vide does not allow
-the creation of reactive scopes inside reactive scopes that are tracking. The
+the creation of reactive scopes inside reactive scopes. `untrack()` creates a
+stable scope inside the reactive scope, and we can create another reactive scope
+inside that stable scope. The
reason for this, is because if the `Counter` component reads from a source
internally, that can cause the reactive scope calling `Counter()` to track that
source, causing unintentional reruns. As a guard against this, you are forced to
@@ -184,5 +191,3 @@ The final result is the same as using the `show()` component. An effect is
created which creates the counter, which creates its own reactive scope. The
effect rerunning causes the counter's internal reactive scope to be destroyed,
making sure everything is cleaned up.
-
-
diff --git a/docs/tut/crash-course/10-cleanup.md b/docs/tut/crash-course/10-cleanup.md
index bf632f5..474f1f2 100644
--- a/docs/tut/crash-course/10-cleanup.md
+++ b/docs/tut/crash-course/10-cleanup.md
@@ -3,7 +3,7 @@
Sometimes you may need to do some cleanup when destroying a component or after
a side-effect from a source update. Vide provides a function `cleanup()` which
is used to queue a cleanup callback for the next time a reactive scope is rerun
-or destroyed.
+or destroyed, or when a stable scope is destroyed.
```lua
local mount = vide.mount
@@ -31,14 +31,18 @@ local function Timer()
}
end
-local unmount = mount(Timer)
+local instance, destroy = root(function(destroy)
+ local instance = Timer()
+ return instance, destroy
+end)
-unmount() -- all queued cleanups are ran, heartbeat connection disconnected
+wait(5)
+
+destroy() -- all queued cleanups are ran, heartbeat connection disconnected
```
In the above example, this allows us to disconnect the heartbeat connection
-when the reactive scope responsible for creating the timer component is
-destroyed, such as when it is unmounted.
+when the scope responsible for creating the timer component is destroyed.
::: tip
Roblox instances do not need to be explicitly destroyed for their
diff --git a/docs/tut/crash-course/11-control-flow.md b/docs/tut/crash-course/11-control-flow.md
index 91f95ac..246d65b 100644
--- a/docs/tut/crash-course/11-control-flow.md
+++ b/docs/tut/crash-course/11-control-flow.md
@@ -6,8 +6,8 @@ known as *control flow* functions.
These functions return new sources, which hold the instances to be displayed.
-Control flow functions run their components in a new reactive scope, which can
-be destroyed independently of the reactive scope that called the control flow
+Control flow functions run their components in a new stable scope, which can
+be destroyed independently of the stable scope that called the control flow
function. This means parts of your app can be independently created and
destroyed.
@@ -93,9 +93,9 @@ subgraph root["root scope"]
end
```
-A `switch()` call creates a new effect and a new scope as seen in the above
-graph. Whenever `menu` updates, it causes the `switch` effect to run, which
-will destroy and recreate the switch scope with the new component.
+A `switch()` call creates a new effect and a new stable scope as seen in the
+above graph. Whenever `menu` updates, it causes the `switch` effect to run,
+which will destroy and recreate the switch scope with the new component.
This will also destroy the internal effect that the button uses to highlight
itself when it is hovered, each time the switch is rerun.
diff --git a/docs/tut/crash-course/14-strict-mode.md b/docs/tut/crash-course/14-strict-mode.md
index ca8465a..6efa1c3 100644
--- a/docs/tut/crash-course/14-strict-mode.md
+++ b/docs/tut/crash-course/14-strict-mode.md
@@ -11,7 +11,7 @@ want this.
Strict mode will run derived sources and effects twice each time they update.
This is to help ensure that derived source computations are pure, and that any
-cleanups made in derived sources or effects are done correctly.
+cleanups made in derived sources or effects are done properly.
```lua
local source = vide.source
diff --git a/docs/tut/crash-course/15-concepts.md b/docs/tut/crash-course/15-concepts.md
index 62ee35d..0b9dc0c 100644
--- a/docs/tut/crash-course/15-concepts.md
+++ b/docs/tut/crash-course/15-concepts.md
@@ -22,52 +22,52 @@ Anything that happens in response to a source update.
Created with `effect()`.
-## Reactive Scope
+## Stable Scope
-A scope created by certain functions such as:
+One of the two types of Vide scopes.
+
+Created by:
- `root()`
+- `untrack()`
+- `switch()`
+- `indexes()`
+
+Stable scopes do not track sources and never rerun.
+
+New stable or reactive scopes can be created within a stable scope.
+
+## Reactive Scope
+
+Created by:
+
- `effect()`
- `derive()`
-Reactive scopes can:
+Reactive scopes do track sources and will rerun when those sources update.
-- track sources that are read from within.
-- rerun when a tracked source updates.
-- track new reactive scopes created from within.
+New reactive scopes cannot be created within a reactive scope, but stable scopes
+can.
## Scope Owners
-A reactive scope created within another reactive scope is *owned* by the other
-reactive scope, with the exception of the reactive scope created by `root()`.
+A scope created within another scope is *owned* by the other scope, with the
+exception of the scope created by `root()`.
-When a reactive scope is rerun or destroyed, all reactive scopes owned by it are
-automatically destroyed.
+When a scope is rerun or destroyed, all scopes owned by it are automatically
+destroyed.
-`root()`, which `mount()` uses internally, creates a reactive scope with no
-owner, since it must be destroyed manually using a destructor
-returned.
+`root()` creates a stable scope with no owner, instead it is destroyed manually.
## Cleanup
-Arbitrary code to run whenever a reactive scope is rerun or destroyed.
+Arbitrary code to run whenever a stable or reactive scope is rerun or destroyed.
Queue a function to run using `cleanup()`.
-## Tracking
-
-Sources read from within a reactive scope will be tracked. This can be disabled
-using `untrack()`, which will make reactive scopes temporarily ignore sources
-read.
-
-The reactive scope created by `root()` is non-tracking by default.
-
-As a guard against misusage, a reactive scope cannot be created within a
-reactive scope, unless it is made non-tracking using `untrack()`.
-
## Reactive Graph
-The combination of reactive scopes can viewed graphically, called a
+The combination of stable and reactive scopes can viewed graphically, called a
*reactive graph*. This can be a more intuitive way to think of the
relationships between effects and the sources they depend on.
@@ -114,10 +114,10 @@ count --> text
Notes:
- Since `count` is a source, not an effect, it can exist
- outside of a root reactive scope.
+ outside of scopes.
- An update to `count` will cause `text` to rerun, which
then causes `effect` to rerun.
-- When the root reactive scope is destroyed, `text` and
+- When the root scope is destroyed, `text` and
`effect` will be destroyed alongside it, since they are
owned by it. `count` will be untouched and future updates
to `count` will have no effect.
diff --git a/docs/tut/crash-course/2-creation.md b/docs/tut/crash-course/2-creation.md
index 96f9dd2..3cc4fd5 100644
--- a/docs/tut/crash-course/2-creation.md
+++ b/docs/tut/crash-course/2-creation.md
@@ -39,10 +39,3 @@ return create "ScreenGui" {
Assign a value to a string key to set a property, and assign a value to a
number key to set a child. Events can be connected to by assigning a function
to a string key.
-
-::: warning
-When creating an instance with no properties, it is important to not forget to
-actually call the constructor: `create "Frame" {}` and not `create "Frame"`.
-To be clear, `create "Frame"` returns a *function* which is a constructor for
-that class, not an instance of that class.
-:::
diff --git a/docs/tut/crash-course/3-components.md b/docs/tut/crash-course/3-components.md
index aa81916..e3cc730 100644
--- a/docs/tut/crash-course/3-components.md
+++ b/docs/tut/crash-course/3-components.md
@@ -58,20 +58,8 @@ local function App()
}
}
end
-
-App().Parent = game.StarterGui
```
:::
-Above is a simple example of a button component being used across files.
-
A single parameter `props` is used to pass properties to the component.
-
-You can only modify the component in ways that you allow in the component,
-through the `props` parameter.
-
-To create a new button all you must do is call the `Button` function, passing in
-values. This saves having to create and set every property each time. Also, when
-updating the button component in future, any changes to the button file will be
-seen anywhere the button is used in your app.
diff --git a/docs/tut/crash-course/6-root.md b/docs/tut/crash-course/6-scope.md
similarity index 51%
rename from docs/tut/crash-course/6-root.md
rename to docs/tut/crash-course/6-scope.md
index 4cb47da..6148364 100644
--- a/docs/tut/crash-course/6-root.md
+++ b/docs/tut/crash-course/6-scope.md
@@ -1,43 +1,65 @@
-# Root Reactive Scopes
+# Scopes
+
+Vide operates on the concept of scopes. Vide scopes come in two flavors:
+stable and reactive.
+
+The three main rules for scopes are:
+
+- Stable scopes never rerun.
+- Reactive scopes will rerun on source updates.
+- A reactive scope cannot be created within another reactive scope.
Reactive scopes cannot be created on their own - they must be created within
-another reactive scope so that it can be tracked and later destroyed when it is
+a stable scope so that it can be tracked and later destroyed when it is
no longer needed.
-This is the purpose of `mount()`, which creates an initial "root", or
-"top-level" reactive scope, which all other reactive scopes, such as
-ones created by `effect()`, can stem from.
+This is the purpose of `root()`, which creates an initial stable scope, which
+all other reactive scopes, such as ones created by `effect()`, can stem from.
-When this root reactive scope is destroyed, it will ensure all other reactive
-scopes created within it are also destroyed, ensuring everything is cleaned up
-properly.
+When this root reactive scope is destroyed, it will destroy any effects created
+within it, ensuring everything is cleaned up properly.
```lua
local source = vide.source
local effect = vide.effect
-local function App()
+local function setup()
local count = source(0)
effect(function()
print(count())
end)
+
+ return count
end
+setup() -- will error since effect() was not called within a stable scope
-App() -- will error since effect() was not called within a reactive scope
-
-vide.mount(App) -- works!
-
+local count = vide.root(setup) -- runs
+count(1) -- prints "1"
```
-Mounting returns a function that when called will destroy its reactive scope,
-along with any other reactive scopes created inside it.
+The scope created by `root()` can be destroyed by calling the function it passes
+into the given function.
```lua
-local unmount = mount(App)
+local function setup(destroy)
+ local count = source(0)
-unmount()
+ effect(function()
+ print(count())
+ end)
+
+ return count, destroy
+end
+
+local count, destroy = root(setup)
+
+count(1) -- prints "1"
+
+destroy()
+
+count(2) -- effect is destroyed; no longer prints
```
Vide's reactivity can be represented graphically, as a *reactive graph*.
@@ -65,7 +87,7 @@ subgraph root
end
```
-When the root reactive scope created by `mount()` is destroyed, the `effect`
+When the root reactive scope created by `root()` is destroyed, the `effect`
scope will also be destroyed since it was created within it.
This is important because you may have an effect that updates the property of a
@@ -75,6 +97,6 @@ instance to be garbage collected.
You don't need to worry about ensuring all your effects are created within a
root reactive scope, since you should be creating all your UI and corresponding
-effects within a top-level `mount()` call that puts all your UI together. So it
+effects within a top-level `root()` call that puts all your UI together. So it
is safe to assume that any effect you create will be created under this top
level scope. Vide will prevent you from accidently doing otherwise anyways.
diff --git a/docs/tut/crash-course/7-stateful-component.md b/docs/tut/crash-course/7-stateful-component.md
index 85139db..427d0ee 100644
--- a/docs/tut/crash-course/7-stateful-component.md
+++ b/docs/tut/crash-course/7-stateful-component.md
@@ -27,8 +27,6 @@ local function Counter()
return instance
end
-
-mount(Counter, game.StarterGui)
```
Above is an example of a counter component, that when clicked, will increment
@@ -37,9 +35,6 @@ its internal count, and automatically update its text to reflect that count.
Each instance of `Counter()` will maintain its own independent count, since the
count source is created inside the component.
-We use `mount()` to create the counter within a reactive scope, which also takes
-a second argument to parent the counter to another instance.
-
## External State
External sources can also be passed into components for them to use.
@@ -72,4 +67,4 @@ count(1) -- the Counter component will update to display this count
Sources can be created internally or passed in from externally, there are no
restrictions on how they are used as long as the effect using it is created
-within a reactive scope.
+within a stable scope.
diff --git a/docs/tut/crash-course/8-property-binding.md b/docs/tut/crash-course/8-implicit-effect.md
similarity index 85%
rename from docs/tut/crash-course/8-property-binding.md
rename to docs/tut/crash-course/8-implicit-effect.md
index 1fe18fc..60d7823 100644
--- a/docs/tut/crash-course/8-property-binding.md
+++ b/docs/tut/crash-course/8-implicit-effect.md
@@ -1,4 +1,4 @@
-# Property Binding
+# Implicit Effects
Explicitly creating effects to update properties can be tedious. Vide provides a
way to *implicitly* create an effect to update properties.
@@ -31,12 +31,7 @@ source used within is updated.
Just like effects, the function is ran immediately in a reactive scope to set
the property initially and determine what sources are being used.
-This allows you as the programmer to not need to manually update UI as the state
-of your program changes. You just define how data sources map to UI, and Vide's
-reactive system will automatically update any properties depending on those
-sources.
-
-## Children Binding
+## Children
Children can also be set in a similar manner. A source passed as a child (passed
with a number key instead of string key) can return an instance or an array of
diff --git a/docs/tut/crash-course/9-derived-source.md b/docs/tut/crash-course/9-derived-source.md
index d3b7156..c1e3669 100644
--- a/docs/tut/crash-course/9-derived-source.md
+++ b/docs/tut/crash-course/9-derived-source.md
@@ -36,7 +36,7 @@ source(1) -- prints "ran" x2
```
To avoid this, you can use `derive()` to derive a new source instead. This will
-run a callback in a new reactive scope only when a dependent source has updated.
+run a function in a new reactive scope only when a dependent source has updated.
Reading this derived source multiple times will just return a cached result from
when it last updated.
@@ -58,7 +58,7 @@ effect(function() text() end)
source(1) -- prints "ran" x1
```
-`derive()` must also be called within a reactive scope, just like `effect()`.
+`derive()` must also be called within a stable scope, just like `effect()`.
If the recalculated value is the same as the old value, the derived source will
not rerun the effects using it.
diff --git a/src/bind.luau b/src/bind.luau
index 4a204f1..c6e8ab7 100644
--- a/src/bind.luau
+++ b/src/bind.luau
@@ -5,9 +5,8 @@ local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph)
type Node = graph.Node
local create_node = graph.create_node
-local assert_owning_scope = graph.assert_owning_scope
+local assert_stable_scope = graph.assert_stable_scope
local evaluate_node = graph.evaluate_node
-local set_owner = graph.set_owner
function create_binding(updater: (T) -> T, binding: T)
if flags.strict then
@@ -31,12 +30,7 @@ function create_binding