This commit is contained in:
Nitwel 2024-04-09 15:30:23 +02:00
parent 586ea20d1a
commit 6690b9b195
7 changed files with 628 additions and 0 deletions

201
app/addons/rdot/Rdot.gd Normal file
View File

@ -0,0 +1,201 @@
class_name R
static func state(value: Variant, options: Dictionary={}):
if value is Dictionary:
return store(value)
return State.new(value, options)
static func store(value: Dictionary):
return RdotStore.new(value)
static func computed(computation: Callable, options: Dictionary={}):
return Computed.new(computation, options)
## DIY overloading of
## bind(target, prop, value)
## bind(target, prop, value, watch_signal)
## bind(target, prop, store, key, watch_signal)
static func bind(target, prop, value, arg1=null, arg2=null):
if value is RdotStore:
return _bind_store(target, prop, value, arg1, arg2)
if value is State or value is Computed:
return _bind_state(target, prop, value, arg1)
assert(false, "Invalid arguments to bind, value must be a R.State or a RdotStore")
static func _bind_store(target, prop, store: RdotStore, key, watch_signal=null):
store._access_property(key)
return _bind_state(target, prop, store._proxied_value[key], watch_signal)
static func _bind_state(target, prop, value, watch_signal=null):
var graph = RdotGraph.getInstance()
var watch_c = func(new_value):
value.value = new_value
var c = computed(func(_arg):
var oldValue=target.get(prop)
if oldValue != value.value:
target.set(prop, value.value)
)
if watch_signal:
watch_signal.connect(watch_c)
graph.watcher.watch([c])
c.do_get()
return func():
graph.watcher.unwatch([c])
if watch_signal:
watch_signal.disconnect(watch_c)
static func effect(callback: Callable):
var graph = RdotGraph.getInstance()
var deconstructor := Callable()
var c = computed(func(_arg):
if !deconstructor.is_null():
deconstructor.call()
var result=callback.call(_arg)
if result is Callable:
deconstructor=result
)
graph.watcher.watch([c])
c.do_get()
return func():
if !deconstructor.is_null():
deconstructor.call()
graph.watcher.unwatch([c])
class State:
var node: RdotState
var value = null:
get:
return do_get()
set(value):
do_set(value)
func _init(value: Variant, options: Dictionary={}):
var ref = RdotState.createSignal(value)
var node = ref[1]
self.node = node
node.wrapper = self
if !options.is_empty():
var equals = options.get("equals")
if !equals.is_null():
node.equals = equals
func do_get():
return RdotState.signalGetFn.call(self.node)
func do_set(value: Variant):
var graph = RdotGraph.getInstance()
assert(graph.isInNotificationPhase() == false, "Writes to signals not permitted during Watcher callback")
var ref = self.node
RdotState.signalSetFn.call(ref, value)
class Computed:
var node: RdotComputed
var value = null:
get:
return do_get()
set(value):
pass
func _init(computation: Callable, options: Dictionary={}):
var ref = RdotComputed.createdComputed(computation)
var node = ref[1]
node.consumerAllowSignalWrites = true
self.node = node
node.wrapper = self
if options:
var equals = options.get("equals")
if !equals.is_null():
node.equals = equals
func do_get():
return RdotComputed.computedGet(node)
class Watcher:
var node: RdotNode
func _init(notify: Callable):
var node = RdotNode.new()
node.wrapper = self
node.consumerMarkedDirty = notify
node.consumerIsAlwaysLive = true
node.consumerAllowSignalWrites = false
node.producerNode = []
self.node = node
## signals: Array[RState | RComputed]
func _assertSignals(signals: Array):
for s in signals:
assert(s is State or s is Computed, "Watcher expects signals to be RState or RComputed")
func watch(signals:=[]):
_assertSignals(signals)
var graph = RdotGraph.getInstance()
var node = self.node
node.dirty = false
var prev = graph.setActiveConsumer(node)
for s in signals:
graph.producerAccessed(s.node)
graph.setActiveConsumer(prev)
func unwatch(signals: Array):
_assertSignals(signals)
var graph = RdotGraph.getInstance()
var node = self.node
graph.assertConsumerNode(node)
var indicesToShift = []
for i in range(node.producerNode.size()):
if signals.has(node.producerNode[i].wrapper):
graph.producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i])
indicesToShift.append(i)
for idx in indicesToShift:
var lastIdx = node.producerNode.size() - 1
node.producerNode[idx] = node.producerNode[lastIdx]
node.producerIndexOfThis[idx] = node.producerIndexOfThis[lastIdx]
node.producerNode.pop_back()
node.producerIndexOfThis.pop_back()
node.nextProducerIndex -= 1
if idx < node.producerNode.size():
var idxConsumer = node.producerNode[idx]
var producer = node.producerNode[idx]
graph.assertProducerNode(producer)
producer.liveConsumerIndexOfThis[idxConsumer] = idx
## Returns Array[RComputed]
func getPending() -> Array:
var node = self.node
return node.producerNode.filter(func(n): return n.dirty).map(func(n): return n.wrapper)

13
app/addons/rdot/array.gd Normal file
View File

@ -0,0 +1,13 @@
class_name RdotArray
static func do_set(array: Array, index: int, value):
if index >= array.size():
array.resize(index + 1)
array[index] = value
static func do_get(array: Array, index: int):
if index >= array.size():
return null
return array[index]

View File

@ -0,0 +1,64 @@
extends RdotNode
class_name RdotComputed
enum State {
SET = 0,
UNSET = 1,
COMPUTING = 2,
ERRORED = 3
}
var value: Variant = null
var state: State = State.UNSET
var error = null
var computation := Callable()
var equal := func(this, a, b): return a == b
static func computedGet(node: RdotComputed) -> Variant:
var graph := RdotGraph.getInstance()
graph.producerUpdateValueVersion(node)
graph.producerAccessed(node)
assert(node.state != State.ERRORED, "Error in computed.")
return node.value
static func createdComputed(computation: Callable):
var node = RdotComputed.new()
node.computation = computation
var computed = func():
return computedGet(node)
return [computed, node]
func producerMustRecompute(node: RdotNode) -> bool:
return node.state == State.UNSET or node.state == State.COMPUTING
func _init():
self.dirty = true
self.producerRecomputeValue = func(node: RdotNode):
assert(node.state != State.COMPUTING, "Detected cycle in computations.")
var graph := RdotGraph.getInstance()
var oldState = node.state
var oldValue = node.value
node.state = State.COMPUTING
var prevConsumer = graph.consumerBeforeComputation(node)
var newValue = node.computation.call(node.wrapper)
var oldOk = oldState != State.UNSET&&oldState != State.ERRORED
var wasEqual = oldOk&&node.equal.call(node.wrapper, oldValue, newValue)
graph.consumerAfterComputation(node, prevConsumer)
if wasEqual:
node.value = oldValue
node.state = oldState
return
node.value = newValue
node.state = State.SET
node.version += 1

225
app/addons/rdot/graph.gd Normal file
View File

@ -0,0 +1,225 @@
extends RefCounted
class_name RdotGraph
static var instance: RdotGraph = null
static func getInstance() -> RdotGraph:
if instance == null:
instance = RdotGraph.new()
return instance
var activeConsumer: RdotNode = null
var inNotificationPhase := false
var epoch := 1
var postSignalSetFn := Callable()
var watcherPending := false
var watcher = R.Watcher.new(func(_arg):
if watcherPending:
return
watcherPending=true
var endOfFrame=func():
watcherPending=false
for s in watcher.getPending():
s.do_get()
watcher.watch()
endOfFrame.call_deferred()
)
func setActiveConsumer(consumer: RdotNode) -> RdotNode:
var prev = activeConsumer
activeConsumer = consumer
return prev
func getActiveConsumer() -> RdotNode:
return activeConsumer
func isInNotificationPhase() -> bool:
return inNotificationPhase
func producerAccessed(node: RdotNode):
assert(inNotificationPhase == false, "Signal read during notification phase")
if activeConsumer == null:
return
if activeConsumer.consumerOnSignalRead.is_null() == false:
activeConsumer.consumerOnSignalRead.call(node)
var idx = activeConsumer.nextProducerIndex;
activeConsumer.nextProducerIndex += 1
assertConsumerNode(activeConsumer)
if idx < activeConsumer.producerNode.size()&&activeConsumer.producerNode[idx] != node:
if consumerIsLive(activeConsumer):
var staleProducer = activeConsumer.producerNode[idx]
producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx])
if RdotArray.do_get(activeConsumer.producerNode, idx) != node:
RdotArray.do_set(activeConsumer.producerNode, idx, node)
RdotArray.do_set(activeConsumer.producerIndexOfThis, idx, producerAddLiveConsumer(node, activeConsumer, idx) if consumerIsLive(activeConsumer) else 0)
RdotArray.do_set(activeConsumer.producerLastReadVersion, idx, node.version)
func producerIncrementEpoch():
epoch += 1
func producerUpdateValueVersion(node: RdotNode):
if consumerIsLive(node)&&!node.dirty:
return
if !node.dirty&&node.lastCleanEpoch == epoch:
return
if !node.producerMustRecompute(node)&&!consumerPollProducersForChange(node):
node.dirty = false;
node.lastCleanEpoch = epoch
return
if node.producerRecomputeValue.is_null() == false:
node.producerRecomputeValue.call(node)
node.dirty = false
node.lastCleanEpoch = epoch
func producerNotifyConsumers(node: RdotNode):
if node.liveConsumerNode == null:
return
var prev = inNotificationPhase
inNotificationPhase = true
for consumer in node.liveConsumerNode:
if !consumer.dirty:
consumerMarkDirty(consumer)
inNotificationPhase = prev
func producerUpdatesAllowed() -> bool:
return activeConsumer == null||activeConsumer.consumerAllowSignalWrites != false
func consumerMarkDirty(node: RdotNode):
node.dirty = true
producerNotifyConsumers(node)
if node.consumerMarkedDirty.is_null() == false:
node.consumerMarkedDirty.call(node)
func consumerBeforeComputation(node: RdotNode) -> RdotNode:
if node:
node.nextProducerIndex = 0
return setActiveConsumer(node)
func consumerAfterComputation(node: RdotNode, prevConsumer: RdotNode):
setActiveConsumer(prevConsumer)
if node == null||node.producerNode == null||node.producerIndexOfThis == null||node.producerLastReadVersion == null:
return
if consumerIsLive(node):
for i in range(node.nextProducerIndex, node.producerNode.size()):
producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i])
while node.producerNode.size() > node.nextProducerIndex:
node.producerNode.pop_back()
node.producerLastReadVersion.pop_back()
node.producerIndexOfThis.pop_back()
func consumerPollProducersForChange(node: RdotNode) -> bool:
assertConsumerNode(node)
for i in range(node.producerNode.size()):
var producer = node.producerNode[i]
var seenVersion = node.producerLastReadVersion[i]
if seenVersion != producer.version:
return true
producerUpdateValueVersion(producer)
if seenVersion != producer.version:
return true
return false
func consumerDestroy(node: RdotNode):
assertConsumerNode(node)
if consumerIsLive(node):
for i in range(node.producerNode.size()):
producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i])
node.producerNode.clear()
node.producerLastReadVersion.clear()
node.producerIndexOfThis.clear()
if node.liveConsumerNode:
node.liveConsumerNode.clear()
node.liveConsumerIndexOfThis.clear()
static func producerAddLiveConsumer(node: RdotNode, consumer: RdotNode, indexOfThis: int) -> int:
assertProducerNode(node)
assertConsumerNode(node)
if node.liveConsumerNode.size() == 0:
if node.watched.is_null() == false:
node.watched.call(node.wrapper)
for i in range(node.producerNode.size()):
node.producerIndexOfThis[i] = producerAddLiveConsumer(node.producerNode[i], node, i)
node.liveConsumerIndexOfThis.push_back(indexOfThis)
node.liveConsumerNode.push_back(consumer)
return node.liveConsumerNode.size() - 1
static func producerRemoveLiveConsumerAtIndex(node: RdotNode, idx: int):
assertProducerNode(node)
assertConsumerNode(node)
assert(idx < node.liveConsumerNode.size(), "active consumer index %s is out of bounds of %s consumers)" % [idx, node.liveConsumerNode.size()])
if node.liveConsumerNode.size() == 1:
if node.unwatched.is_null() == false:
node.unwatched.call(node.wrapper)
for i in range(node.producerNode.size()):
producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i])
var lastIdx = node.liveConsumerNode.size() - 1
node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx]
node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx]
node.liveConsumerNode.pop_back()
node.liveConsumerIndexOfThis.pop_back()
if idx < node.liveConsumerNode.size():
var idxProducer = node.liveConsumerIndexOfThis[idx]
var consumer = node.liveConsumerNode[idx]
assertConsumerNode(consumer)
consumer.producerIndexOfThis[idxProducer] = idx
static func consumerIsLive(node: RdotNode) -> bool:
return node.consumerIsAlwaysLive||(node.liveConsumerNode != null&&node.liveConsumerNode.size() > 0)
static func assertConsumerNode(node: RdotNode):
if node.producerNode == null:
node.producerNode = []
if node.producerIndexOfThis == null:
node.producerIndexOfThis = []
if node.producerLastReadVersion == null:
node.producerLastReadVersion = []
static func assertProducerNode(node: RdotNode):
if node.liveConsumerNode == null:
node.liveConsumerNode = []
if node.liveConsumerIndexOfThis == null:
node.liveConsumerIndexOfThis = []

38
app/addons/rdot/node.gd Normal file
View File

@ -0,0 +1,38 @@
extends RefCounted
class_name RdotNode
var version := 0
var lastCleanEpoch := 0
var dirty := false
## Array[RdotNode] | null
var producerNode = null
## Array[int] | null
var producerLastReadVersion = null
## Array[int] | null
var producerIndexOfThis = null
var nextProducerIndex := 0
## Array[RdotNode] | null
var liveConsumerNode = null
## Array[int] | null
var liveConsumerIndexOfThis = null
var consumerAllowSignalWrites := false
var consumerIsAlwaysLive := false
var watched: Callable = Callable()
var unwatched: Callable = Callable()
var wrapper
func producerMustRecompute(node: RdotNode) -> bool:
return false
var producerRecomputeValue: Callable = Callable()
var consumerMarkedDirty: Callable = Callable()
var consumerOnSignalRead: Callable = Callable()
# func _to_string():
# return "RdotNode {\n" + ",\n\t".join(get_property_list().map(func(dict): return dict.name + ": " + str(get(dict.name)))) + "\n}"

50
app/addons/rdot/state.gd Normal file
View File

@ -0,0 +1,50 @@
extends RdotNode
class_name RdotState
var equal: Callable = func(this, a, b): a == b
var value: Variant = null
static func createSignal(initialValue: Variant):
var node = RdotState.new()
node.value = initialValue
var getter = func():
RdotGraph.getInstance().producerAccessed(node)
return node.value
return [getter, node]
static func setPostSignalSetFn(fn: Callable) -> Callable:
var graph := RdotGraph.getInstance()
var prev = graph.postSignalSetFn
graph.postSignalSetFn = fn
return prev
static func signalGetFn(this: RdotState):
RdotGraph.getInstance().producerAccessed(this)
return this.value
static func signalSetFn(node: RdotState, newValue: Variant):
var graph := RdotGraph.getInstance()
assert(graph.producerUpdatesAllowed())
if !node.equal.call(node.wrapper, node.value, newValue):
node.value = newValue
signalValueChanged(node)
static func signalUpdateFn(node: RdotState, updater: Callable):
var graph := RdotGraph.getInstance()
assert(graph.producerUpdatesAllowed())
signalSetFn(node, updater.call(node.value))
static func signalValueChanged(node: RdotState):
var graph := RdotGraph.getInstance()
node.version += 1
graph.producerIncrementEpoch()
graph.producerNotifyConsumers(node)
if !graph.postSignalSetFn.is_null():
graph.postSignalSetFn.call()

37
app/addons/rdot/store.gd Normal file
View File

@ -0,0 +1,37 @@
extends Object
class_name RdotStore
var _proxied_value = {}
var _property_list = []
func _init(initial_value: Dictionary={}):
_proxied_value = initial_value
_property_list = _proxied_value.keys().map(func(key):
return {
"name": key,
"type": typeof(_proxied_value[key])
}
)
func _get(property):
_access_property(property)
if _proxied_value[property] is RdotStore:
return _proxied_value[property]
return _proxied_value[property].value
func _set(property, value):
_access_property(property)
_proxied_value[property].value = value
return true
func _access_property(property):
if (_proxied_value[property] is R.State) == false:
_proxied_value[property] = R.state(_proxied_value[property])
func _get_property_list():
return _property_list