New Reactive system (#89)

Simplify

Move out shared code

Wrap render functions and event functions in transactions

Rename
This commit is contained in:
Sabine Lim
2023-05-26 02:48:54 +10:00
committed by GitHub
parent 555ab6217c
commit f7e55c9f52
2 changed files with 177 additions and 74 deletions

View File

@@ -0,0 +1,165 @@
local NodeStatus = {
CURRENT = 0,
STALE = 1,
MAYBE_STALE = 2
}
local Node = {}
Node.new = function()
return {
fn = nil,
value = nil,
status = NodeStatus.STALE,
parents = {},
children = {},
cleanup = function(self)
for _, parentNode in ipairs(self.parents) do
for index, childNode in ipairs(parentNode.children) do
if (childNode == self) then
table.remove(parentNode.children, index)
break
end
end
end
self.parents = {}
end
}
end
local ReactiveState = {
listeningNode = nil,
sourceNodes = {},
effectNodes = {},
transaction = false
}
local Reactive = {}
Reactive.pushUpdates = function()
for _, sourceNode in ipairs(ReactiveState.sourceNodes) do
Reactive.pushSourceNodeUpdate(sourceNode)
end
Reactive.pullUpdates()
end
Reactive.pushSourceNodeUpdate = function(sourceNode)
if (sourceNode.status == NodeStatus.CURRENT) then
return
end
Reactive.pushNodeUpdate(sourceNode)
for _, childNode in ipairs(sourceNode.children) do
childNode.status = NodeStatus.STALE
end
sourceNode.status = NodeStatus.CURRENT
end
Reactive.pushNodeUpdate = function(node)
if (node == nil) then
return
end
node.status = NodeStatus.MAYBE_STALE
for _, childNode in ipairs(node.children) do
Reactive.pushNodeUpdate(childNode)
end
end
Reactive.pullUpdates = function()
for _, effectNode in ipairs(ReactiveState.effectNodes) do
Reactive.pullNodeUpdates(effectNode)
end
end
Reactive.pullNodeUpdates = function(node)
if (node.status == NodeStatus.CURRENT) then
return
end
if (node.status == NodeStatus.MAYBE_STALE) then
for _, parentNode in ipairs(node.parents) do
Reactive.pullNodeUpdates(parentNode)
end
end
if (node.status == NodeStatus.STALE) then
node:cleanup()
local prevListeningNode = ReactiveState.listeningNode
ReactiveState.listeningNode = node
local oldValue = node.value
node.value = node.fn()
ReactiveState.listeningNode = prevListeningNode
for _, childNode in ipairs(node.children) do
if (oldValue == node.value) then
childNode.status = NodeStatus.CURRENT
else
childNode.status = NodeStatus.STALE
end
end
end
node.status = NodeStatus.CURRENT
end
Reactive.subscribe = function(node)
local listeningNode = ReactiveState.listeningNode
if (listeningNode ~= nil) then
table.insert(node.children, listeningNode)
table.insert(listeningNode.parents, node)
end
end
Reactive.observable = function(initialValue)
local node = Node.new()
node.value = initialValue
node.status = NodeStatus.CURRENT
local get = function()
Reactive.subscribe(node)
return node.value
end
local set = function(newValue)
if (node.value == newValue) then
return
end
node.value = newValue
node.status = ReactiveState.STALE
if (not ReactiveState.transaction) then
Reactive.pushUpdates()
end
end
table.insert(ReactiveState.sourceNodes, node)
return get, set
end
Reactive.derived = function(fn)
local node = Node.new()
node.fn = fn
return function()
if (node.status ~= NodeStatus.CURRENT) then
Reactive.pullNodeUpdates(node)
end
Reactive.subscribe(node)
return node.value
end
end
Reactive.effect = function(fn)
local node = Node.new()
node.fn = fn
table.insert(ReactiveState.effectNodes, node)
Reactive.pushUpdates()
end
Reactive.transaction = function(fn)
ReactiveState.transaction = true
fn()
ReactiveState.transaction = false
Reactive.pushUpdates()
end
Reactive.untracked = function(fn)
local prevListeningNode = ReactiveState.listeningNode
ReactiveState.listeningNode = nil
local value = fn()
ReactiveState.listeningNode = prevListeningNode
return value
end
return Reactive

View File

@@ -1,72 +1,6 @@
local Reactive = require("reactivePrimitives")
local XMLParser = require("xmlParser")
local Reactive = {}
Reactive.currentEffect = nil
Reactive.observable = function(initialValue)
local value = initialValue
local observerEffects = {}
local get = function()
if (Reactive.currentEffect ~= nil) then
table.insert(observerEffects, Reactive.currentEffect)
table.insert(Reactive.currentEffect.dependencies, observerEffects)
end
return value
end
local set = function(newValue)
value = newValue
local observerEffectsCopy = {}
for index, effect in ipairs(observerEffects) do
observerEffectsCopy[index] = effect
end
for _, effect in ipairs(observerEffectsCopy) do
effect.execute()
end
end
return get, set
end
Reactive.untracked = function(getter)
local parentEffect = Reactive.currentEffect
Reactive.currentEffect = nil
local value = getter()
Reactive.currentEffect = parentEffect
return value
end
Reactive.effect = function(effectFn)
local effect = {dependencies = {}}
local execute = function()
Reactive.clearEffectDependencies(effect)
local parentEffect = Reactive.currentEffect
Reactive.currentEffect = effect
effectFn()
Reactive.currentEffect = parentEffect
end
effect.execute = execute
effect.execute()
end
Reactive.derived = function(computeFn)
local getValue, setValue = Reactive.observable();
Reactive.effect(function()
setValue(computeFn())
end)
return getValue;
end
Reactive.clearEffectDependencies = function(effect)
for _, dependency in ipairs(effect.dependencies) do
for index, backlink in ipairs(dependency) do
if (backlink == effect) then
table.remove(dependency, index)
end
end
end
effect.dependencies = {};
end
local Layout = {
fromXML = function(text)
local nodes = XMLParser.parseText(text)
@@ -91,7 +25,9 @@ end
local registerFunctionEvent = function(object, event, script, env)
event(object, function(...)
local success, msg = pcall(load(script, nil, "t", env))
local success, msg = pcall(function()
Reactive.transaction(load(script, nil, "t", env))
end)
if not success then
error("XML Error: "..msg)
end
@@ -116,11 +52,10 @@ return {
if (attribute:sub(1, 2) == "on") then
registerFunctionEvent(object, object[attribute], expression .. "()", env)
else
local update = function()
Reactive.effect(function()
local value = load("return " .. expression, nil, "t", env)()
object:setProperty(attribute, value)
end
basalt.effect(update)
end)
end
end
for _, child in ipairs(node.children) do
@@ -134,9 +69,10 @@ return {
local object = {
observable = Reactive.observable,
untracked = Reactive.untracked,
effect = Reactive.effect,
derived = Reactive.derived,
effect = Reactive.effect,
transaction = Reactive.transaction,
untracked = Reactive.untracked,
layout = function(path)
if (not fs.exists(path)) then
@@ -163,7 +99,9 @@ return {
end
})
if (layout.script ~= nil) then
executeScript(layout.script, env)
Reactive.transaction(function()
executeScript(layout.script, env)
end)
end
local objects = {}
for _, node in ipairs(layout.nodes) do