-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstate.rb
203 lines (156 loc) · 4.25 KB
/
state.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# noinspection RubyClassVariableUsageInspection We want the undefined behavior, I promise.
module DecentState
extend self
@@effect_dependencies = []
@@current_computations = []
def effect_dependencies
@@effect_dependencies
end
def current_computations
@@current_computations
end
# An Unloadable is an object with a cleanup method used for disposing of hanging computations.
class Unloadable
def initialize(scope, &cleanup)
@cleanup = cleanup
@scope = scope
end
def cleanup
@scope&.delete self
@cleanup&.call
@scope = nil
@cleanup = nil
end
end
def scope(&computation_scope)
previous_computations = @@current_computations
computations = []
@@current_computations = computations
computation_scope.call
@@current_computations = previous_computations
cleanup = Unloadable.new previous_computations do
computations.each { |c| c.cleanup }
computations = []
end
previous_computations.push(cleanup)
cleanup
end
def effect(dependencies = nil, &effect_callback)
cleanups = []
if dependencies.is_a? Array
dependencies.each do |dependency|
cleanups.push(dependency.watch_reassignment {
previous_dependencies = @@effect_dependencies
@@effect_dependencies = []
effect_callback.call
@@effect_dependencies = previous_dependencies
})
end
else
recalculate_dependencies = -> {
previous_dependencies = @@effect_dependencies
@@effect_dependencies = []
# For some reason, RubyMine cannot figure out that this *does* have access to the outer scope.
cleanups.each(&:cleanup)
cleanups = []
effect_callback.call
@@effect_dependencies.each do |dependency|
cleanups.push(dependency.watch_reassignment {
recalculate_dependencies.call
})
end
@@effect_dependencies = previous_dependencies
}
recalculate_dependencies.call
end
cleanup = Unloadable.new @@current_computations do
cleanups.each { |c| c.cleanup }
cleanups = []
end
@@current_computations&.push(cleanup)
cleanup
end
class State
def initialize(initial_state = nil)
@visit_watchers = []
@reassignment_watchers = []
@state = initial_state
end
def untracked
@state
end
def untracked=(new)
@state = (new)
end
def value
@visit_watchers.each { |watcher| watcher.call }
deps = DecentState.effect_dependencies
deps.push(self) unless deps.include? self
@state
end
def value=(new)
@state = new
@reassignment_watchers.each { |watcher| watcher.call(new) }
end
def watch_visit(&callback)
@visit_watchers.push callback
Unloadable.new [] do
@visit_watchers.delete callback
end
end
def watch_reassignment(&callback)
@reassignment_watchers.push callback
Unloadable.new [] do
@reassignment_watchers.delete callback
end
end
def watch(&callback)
unwatchers = [watch_visit(&callback), watch_reassignment(&callback)]
Unloadable.new [] do
unwatchers.each { |unwatch| unwatch.cleanup }
unwatchers = []
end
end
end
class DerivedState < State
def initialize(&calculation)
super nil
@cleanup = DecentState.effect do
self.value = calculation.call
end
end
def cleanup
@cleanup.cleanup
end
end
# Creates a new reactive state object.
def state(initial = nil)
State.new initial
end
def derived(&calculation)
DerivedState.new(&calculation)
end
def is_state?(obj)
obj.is_a?(State) || obj.is_a?(DerivedState)
end
def unwrap_state(obj)
if is_state? obj
obj.value
else
obj
end
end
def unwrap_hash(hash)
hash.map { |k, v| [k, unwrap_state(v)] }.to_h
end
def reactive(hash = {})
# TODO: Overhaul this entire thing to use a real hash.
fake_hash = Object.new
hash.each_pair do |key, value|
ref = state value
fake_hash.define_singleton_method(key) { ref.value }
fake_hash.define_singleton_method((key.to_s + "=").to_sym) { |new| ref.value = new }
end
fake_hash
end
end