diff --git a/Basalt/libraries/reactivePrimitives.lua b/Basalt/libraries/reactivePrimitives.lua new file mode 100644 index 0000000..adae52e --- /dev/null +++ b/Basalt/libraries/reactivePrimitives.lua @@ -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 diff --git a/Basalt/plugins/reactive.lua b/Basalt/plugins/reactiveXml.lua similarity index 67% rename from Basalt/plugins/reactive.lua rename to Basalt/plugins/reactiveXml.lua index fb10fae..9c2dc81 100644 --- a/Basalt/plugins/reactive.lua +++ b/Basalt/plugins/reactiveXml.lua @@ -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