Lazy reactive evaluation, performance optimisations #89
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 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
|
||||
Reference in New Issue
Block a user