New Reactive system (#89)
Simplify Move out shared code Wrap render functions and event functions in transactions Rename
This commit is contained in:
165
Basalt/libraries/reactivePrimitives.lua
Normal file
165
Basalt/libraries/reactivePrimitives.lua
Normal 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
|
||||||
@@ -1,72 +1,6 @@
|
|||||||
|
local Reactive = require("reactivePrimitives")
|
||||||
local XMLParser = require("xmlParser")
|
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 = {
|
local Layout = {
|
||||||
fromXML = function(text)
|
fromXML = function(text)
|
||||||
local nodes = XMLParser.parseText(text)
|
local nodes = XMLParser.parseText(text)
|
||||||
@@ -91,7 +25,9 @@ end
|
|||||||
|
|
||||||
local registerFunctionEvent = function(object, event, script, env)
|
local registerFunctionEvent = function(object, event, script, env)
|
||||||
event(object, function(...)
|
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
|
if not success then
|
||||||
error("XML Error: "..msg)
|
error("XML Error: "..msg)
|
||||||
end
|
end
|
||||||
@@ -116,11 +52,10 @@ return {
|
|||||||
if (attribute:sub(1, 2) == "on") then
|
if (attribute:sub(1, 2) == "on") then
|
||||||
registerFunctionEvent(object, object[attribute], expression .. "()", env)
|
registerFunctionEvent(object, object[attribute], expression .. "()", env)
|
||||||
else
|
else
|
||||||
local update = function()
|
Reactive.effect(function()
|
||||||
local value = load("return " .. expression, nil, "t", env)()
|
local value = load("return " .. expression, nil, "t", env)()
|
||||||
object:setProperty(attribute, value)
|
object:setProperty(attribute, value)
|
||||||
end
|
end)
|
||||||
basalt.effect(update)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for _, child in ipairs(node.children) do
|
for _, child in ipairs(node.children) do
|
||||||
@@ -134,9 +69,10 @@ return {
|
|||||||
|
|
||||||
local object = {
|
local object = {
|
||||||
observable = Reactive.observable,
|
observable = Reactive.observable,
|
||||||
untracked = Reactive.untracked,
|
|
||||||
effect = Reactive.effect,
|
|
||||||
derived = Reactive.derived,
|
derived = Reactive.derived,
|
||||||
|
effect = Reactive.effect,
|
||||||
|
transaction = Reactive.transaction,
|
||||||
|
untracked = Reactive.untracked,
|
||||||
|
|
||||||
layout = function(path)
|
layout = function(path)
|
||||||
if (not fs.exists(path)) then
|
if (not fs.exists(path)) then
|
||||||
@@ -163,7 +99,9 @@ return {
|
|||||||
end
|
end
|
||||||
})
|
})
|
||||||
if (layout.script ~= nil) then
|
if (layout.script ~= nil) then
|
||||||
executeScript(layout.script, env)
|
Reactive.transaction(function()
|
||||||
|
executeScript(layout.script, env)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
local objects = {}
|
local objects = {}
|
||||||
for _, node in ipairs(layout.nodes) do
|
for _, node in ipairs(layout.nodes) do
|
||||||
Reference in New Issue
Block a user