hyper-state 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ namespace :spec do
7
+ task :prepare do
8
+ end
9
+ end
10
+
11
+ task :default => :spec
@@ -0,0 +1,47 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hyperstack/state/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'hyper-state'
8
+ spec.version = Hyperstack::State::VERSION
9
+ spec.authors = ['Mitch VanDuyn', 'Adam Creekroad', 'Jan Biedermann']
10
+ spec.email = ['mitch@catprint.com', 'jan@kursator.com']
11
+ spec.summary = 'Flux Stores and more for Hyperloop'
12
+ spec.homepage = 'https://ruby-hyperloop.org'
13
+ spec.license = 'MIT'
14
+ # spec.metadata = {
15
+ # "homepage_uri" => 'http://ruby-hyperloop.org',
16
+ # "source_code_uri" => 'https://github.com/ruby-hyperloop/hyper-component'
17
+ # }
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(gemfiles|spec)/}) }
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'hyperstack-config', Hyperstack::State::VERSION
25
+ spec.add_dependency 'opal', '>= 0.11.0', '< 0.12.0'
26
+ spec.add_development_dependency 'bundler'
27
+ spec.add_development_dependency 'chromedriver-helper'
28
+ spec.add_development_dependency 'hyper-component', Hyperstack::State::VERSION
29
+ spec.add_development_dependency 'hyper-spec', Hyperstack::State::VERSION
30
+ spec.add_development_dependency 'listen'
31
+ spec.add_development_dependency 'mini_racer', '~> 0.1.15'
32
+ spec.add_development_dependency 'opal-browser', '~> 0.2.0'
33
+ spec.add_development_dependency 'opal-rails', '~> 0.9.4'
34
+ spec.add_development_dependency 'pry-byebug'
35
+ spec.add_development_dependency 'pry-rescue'
36
+ spec.add_development_dependency 'puma'
37
+ spec.add_development_dependency 'rails', '>= 4.0.0'
38
+ spec.add_development_dependency 'rake'
39
+ spec.add_development_dependency 'react-rails', '>= 2.4.0', '< 2.5.0'
40
+ spec.add_development_dependency 'rspec', '~> 3.7.0'
41
+ spec.add_development_dependency 'rspec-rails'
42
+ spec.add_development_dependency 'rspec-steps', '~> 2.1.1'
43
+ spec.add_development_dependency 'rubocop', '~> 0.51.0'
44
+ spec.add_development_dependency 'sqlite3'
45
+ spec.add_development_dependency 'timecop', '~> 0.8.1'
46
+
47
+ end
@@ -0,0 +1,17 @@
1
+ require 'set'
2
+ require 'hyperstack-config'
3
+ Hyperstack.import 'hyper-state'
4
+ require 'hyperstack/internal/callbacks'
5
+ require 'hyperstack/internal/auto_unmount'
6
+
7
+ require 'hyperstack/internal/state/mapper'
8
+ require 'hyperstack/internal/auto_unmount'
9
+ require 'hyperstack/internal/receiver'
10
+ require 'hyperstack/state/observable'
11
+ require 'hyperstack/state/observer'
12
+ require 'hyperstack/state/version'
13
+
14
+ if RUBY_ENGINE != 'opal'
15
+ require 'opal'
16
+ Opal.append_path(File.expand_path('../', __FILE__).untaint)
17
+ end
@@ -0,0 +1,55 @@
1
+ module Hyperstack
2
+ module Internal
3
+ module AutoUnmount
4
+ def self.included(base)
5
+ base.include(Hyperstack::Internal::Callbacks)
6
+ base.class_eval do
7
+ define_callback :before_unmount
8
+ end
9
+ end
10
+
11
+ def unmounted?
12
+ @__hyperstack_internal_auto_unmount_unmounted
13
+ end
14
+
15
+ def unmount
16
+ run_callback(:before_unmount)
17
+ AutoUnmount.objects_to_unmount[self].each(&:unmount)
18
+ AutoUnmount.objects_to_unmount.delete(self)
19
+ instance_variables.each do |var|
20
+ val = instance_variable_get(var)
21
+ begin
22
+ val.unmount if val.respond_to?(:unmount)
23
+ rescue RUBY_ENGINE == 'opal' ? JS::Error : nil
24
+ nil
25
+ end
26
+ end
27
+ @__hyperstack_internal_auto_unmount_unmounted = true
28
+ end
29
+
30
+ def every(*args, &block)
31
+ return if unmounted?
32
+ super.tap do |id|
33
+ sself = self
34
+ id.define_singleton_method(:unmount) { abort }
35
+ AutoUnmount.objects_to_unmount[self] << id
36
+ end
37
+ end
38
+
39
+ def after(*args, &block)
40
+ return if unmounted?
41
+ super.tap do |id|
42
+ sself = self
43
+ id.define_singleton_method(:unmount) { abort }
44
+ AutoUnmount.objects_to_unmount[self] << id
45
+ end
46
+ end
47
+
48
+ class << self
49
+ def objects_to_unmount
50
+ @objects_to_unmount ||= Hash.new { |h, k| h[k] = Set.new }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,48 @@
1
+ module Hyperstack
2
+ module Internal
3
+ module Callbacks
4
+ if RUBY_ENGINE != 'opal'
5
+ class Hyperstack::Hotloader
6
+ def self.record(*args); end
7
+ end
8
+ end
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ def run_callback(name, *args)
14
+ self.class.callbacks_for(name).each do |callback|
15
+ if callback.is_a?(Proc)
16
+ instance_exec(*args, &callback)
17
+ else
18
+ send(callback, *args)
19
+ end
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ def define_callback(callback_name, &after_define_hook)
25
+ wrapper_name = "_#{callback_name}_callbacks"
26
+ define_singleton_method(wrapper_name) do
27
+ Context.set_var(self, "@#{wrapper_name}", force: true) { [] }
28
+ end
29
+ define_singleton_method(callback_name) do |*args, &block|
30
+ send(wrapper_name).concat(args)
31
+ send(wrapper_name).push(block) if block_given?
32
+ Hotloader.record(self, "@#{wrapper_name}", 4, *args, block)
33
+ after_define_hook.call(*args, &block) if after_define_hook
34
+ end
35
+ end
36
+
37
+ def callbacks_for(callback_name)
38
+ wrapper_name = "_#{callback_name}_callbacks"
39
+ if superclass.respond_to? :callbacks_for
40
+ superclass.callbacks_for(callback_name)
41
+ else
42
+ []
43
+ end + send(wrapper_name)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ module Hyperstack
2
+ module Internal
3
+ module Receiver
4
+ class << self
5
+ def mount(receiver, *args, &block)
6
+ return if receiver.respond_to?(:unmounted?) && receiver.unmounted?
7
+ # Format the callback to be Proc or Nil
8
+ callback = format_callback(receiver, args)
9
+
10
+ # Loop through receivers and call callback and/or block on dispatch
11
+ args.each do |operation|
12
+ id = operation.on_dispatch do |params|
13
+ callback.call(params) if callback
14
+ yield params if block
15
+ end
16
+ # TODO: broadcaster classes need to define unmount as well
17
+ AutoUnmount.objects_to_unmount[receiver] << id if receiver.respond_to? :unmount
18
+ end
19
+ end
20
+
21
+ def format_callback(receiver, args)
22
+ call_back =
23
+ if args.last.is_a?(Symbol)
24
+ method_name = args.pop
25
+ ->(*aargs) { receiver.send(:"#{method_name}", *aargs) }
26
+ elsif args.last.is_a?(Proc)
27
+ args.pop
28
+ end
29
+ return call_back unless args.empty?
30
+ message = 'At least one operation must be passed in to the \'receives\' macro'
31
+ raise Legacy::Store::InvalidOperationError, message
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,274 @@
1
+ module Hyperstack
2
+ module Internal
3
+ module State
4
+ # State::Mapper bidirectionally maps observers to objects during a rendering cycle.
5
+
6
+ # Observers: Any object that responds to the `mutations` method can become
7
+ # an observer by calling Mapper.observing.
8
+
9
+ # State Objects: any object can be observed by calling the observed! method, and
10
+ # an object indicates that it has changed state by calling the mutated! method,
11
+ # which will notify all observers via their mutations methods.
12
+
13
+ # During each rendering cycle a list of objects observed during rendering
14
+ # is built called new_objects.
15
+
16
+ # At the end of the rendering cycle new_objects becomes current_objects, and
17
+ # its inverse called current_observers.
18
+
19
+ # When mutated! is called, the current_observers list is used to find the list of
20
+ # observers.
21
+
22
+ # Typically mutated! is called during some javascript event, and we will want to
23
+ # delay notification until the event handler has completed execution.
24
+ module Mapper
25
+ @rendering_level = 0
26
+
27
+ class << self
28
+ # Entry Points:
29
+ # observing setup an observer
30
+ # observed! indicate an object has been observed
31
+ # mutated! indicate an object has been mutated
32
+ # observed? has this object been observed?
33
+ # bulk_update prevent notifications until the event completes
34
+ # update_objects_to_observe called at end of each rendering cycle
35
+ # remove called when a component unmounts
36
+
37
+ # Observers wrap code in observe. Any calls
38
+ # made to the public entry points will then know which
39
+ # observer is executing, along with whether this is the
40
+ # outer most component, and whether to delay or handle state
41
+ # changes immediately.
42
+
43
+ # Once the observer's block completes execution, the
44
+ # context instance variables are restored.
45
+ def observing(observer, immediate_update, rendering, update_objects)
46
+ saved_context = [@current_observer, @immediate_update]
47
+ @current_observer = observer
48
+ @immediate_update = immediate_update && observer
49
+ if rendering
50
+ @rendering_level += 1
51
+ observed!(observer)
52
+ observed!(observer.class)
53
+ end
54
+ return_value = yield
55
+ update_objects_to_observe(observer) if update_objects
56
+ return_value
57
+ ensure
58
+ @current_observer, @immediate_update = saved_context
59
+ @rendering_level -= 1 if rendering
60
+ return_value
61
+ end
62
+
63
+ # called when an object has been observed (i.e. read) by somebody
64
+ def observed!(object)
65
+ return unless @current_observer
66
+ new_objects[@current_observer] << object
67
+ return unless update_exclusions[object]
68
+ update_exclusions[object] << @current_observer
69
+ end
70
+
71
+ # Called when an object has been mutated.
72
+ # Depending on the state of StateContext we will either
73
+ # schedule the update notification for later, immediately
74
+ # notify any observers, or do nothing.
75
+ def mutated!(object)
76
+ return if @ignore_mutations
77
+ if delay_updates?(object)
78
+ schedule_delayed_updater(object)
79
+ elsif @rendering_level.zero?
80
+ current_observers[object].each do |observer|
81
+ observer.mutations([object])
82
+ end if current_observers.key? object
83
+ end
84
+ end
85
+
86
+ # Check to see if an object has been observed.
87
+ def observed?(object)
88
+ # we don't want to unnecessarily create a reference to ourselves
89
+ # in the current_observers hash so we just look for the key.
90
+ current_observers.key?(object)# && current_observers[object].any?
91
+ end
92
+
93
+ # Code can be wrapped in the bulk_update method, and
94
+ # notifications of any mutations that occur during
95
+ # the yield will be scheduled for after the current
96
+ # event finishes.
97
+ def bulk_update
98
+ saved_bulk_update_flag = @bulk_update_flag
99
+ @bulk_update_flag = true
100
+ yield
101
+ ensure
102
+ @bulk_update_flag = saved_bulk_update_flag
103
+ end
104
+
105
+ def ignore_mutations
106
+ saved_ignore_mutations_flag = @ignore_mutations
107
+ @ignore_mutations = true
108
+ yield
109
+ ensure
110
+ @ignore_mutations = saved_ignore_mutations_flag
111
+ end
112
+
113
+ # Call after each component updates. (in the after_update/after_mount callbacks)
114
+ # During the rendering cycle the observers and objects are held in the
115
+ # current_observers and current_objects hashes, which are just inverses of each
116
+ # so that current_observers can be accessed via objects, and current_objects can be
117
+ # accessed by observers.
118
+
119
+ # While rendering is going on, observers may add new objects to the new_objects list
120
+
121
+ # When rendering completes we clear the current observers and objects lists, and
122
+ # the new_objects list gets transferred in to current_objects and current_observers
123
+ # and the process repeats.
124
+
125
+ # When an event triggers a state change the current_objects list is used to determine
126
+ # what observers (components) need to be updated. Note that the new_objects during this
127
+ # phase is still empty, and that is why we need two lists.
128
+
129
+ # TODO: see if we can get rid of all this and simply calling
130
+ # remove_current_observers_and_objects at the START of each components rendering
131
+ # cycle (i.e. before_mount and before_update)
132
+ def update_objects_to_observe(observer = @current_observer)
133
+ remove_current_observers_and_objects(observer)
134
+ objects = new_objects.delete(observer)
135
+ objects.each { |object| current_observers[object] << observer } if objects
136
+ current_objects[observer] = objects
137
+ end
138
+
139
+ # call remove before unmounting components to prevent stray events
140
+ # from being sent to unmounted components.
141
+ def remove(observer = @current_observer)
142
+ remove_current_observers_and_objects(observer)
143
+ new_objects.delete observer
144
+ # see run_delayed_updater for the purpose of @removed_observers
145
+ @removed_observers << observer if @removed_observers
146
+ end
147
+
148
+ # Internal (Private) Methods
149
+
150
+ # These four hashes track the current relationship between
151
+ # observers and observable objects
152
+
153
+ # new_objects are added as the @current_observer reads
154
+ # an objects state
155
+ def new_objects
156
+ @new_objects ||= Hash.new { |h, k| h[k] = Set.new }
157
+ end
158
+
159
+ # at the end of the rendering cycle the new_objects are
160
+ # processed into a list of observers indexed by objects...
161
+ def current_observers
162
+ @current_observers ||= Hash.new { |h, k| h[k] = [] }
163
+ end
164
+
165
+ # and a list of objects indexed by observers
166
+ def current_objects
167
+ @current_objects ||= Hash.new { |h, k| h[k] = [] }
168
+ end
169
+
170
+ # Normally notification of changes to state are queued up
171
+ # and will be run after the event has completed processing.
172
+ # Then each observer is notified of the states that changed
173
+ # during the event. The observers may then begin reading
174
+ # state before the notification has completed. To prevent
175
+ # redundant notifications in this case, a list of observers
176
+ # indexed by objects is kept in the update_exclusions hash.
177
+
178
+ # We avoid keeping empty lists of observers on the exclusion
179
+ # lists by not adding an object hash key unless the object
180
+ # already has pending state changes. (See the
181
+ # schedule_delayed_updater method below)
182
+
183
+ def update_exclusions
184
+ @update_exclusions ||= Hash.new
185
+ end
186
+
187
+ # remove_current_observers_and_objects clears the hashes between renders
188
+
189
+ def remove_current_observers_and_objects(observer)
190
+ raise 'state management called outside of watch block' unless observer
191
+ deleted_objects = current_objects.delete(observer)
192
+ return unless deleted_objects
193
+ deleted_objects.each do |object|
194
+ # to allow for GC we never want objects hanging around as keys in
195
+ # the current_observers hash, so we tread carefully here.
196
+ next unless current_observers.key? object
197
+ current_observers[object].delete(observer)
198
+ current_observers.delete object if current_observers[object].empty?
199
+ end
200
+ end
201
+
202
+ # determine if updates should be delayed.
203
+ # always delay updates if the bulk_update_flag is set
204
+ # otherwise delayed updates only occurs if
205
+ # Hyperstack.on_client? is true WITH ONE EXCEPTION:
206
+ # observers can indicate that they need immediate updates in
207
+ # case that the object being updated is themselves.
208
+
209
+ def delay_updates?(object)
210
+ @bulk_update_flag ||
211
+ (Hyperstack.on_client? &&
212
+ (@immediate_update != @current_observer || @current_observer != object))
213
+ end
214
+
215
+ # schedule_delayed_updater adds a new set to the
216
+ # update_exclusions hash (indexed by object) then makes
217
+ # sure that the updater is scheduled to run as soon as the current
218
+ # event completes.
219
+
220
+ # the update_exclusions hash tells us two things. First any object that
221
+ # is a key in the hash has been changed, but the notification of the change
222
+ # has been delayed. Secondly the associated Set will contain a list of observers
223
+ # that have already read the current state, between the time
224
+ # schedule_delayed_updater has been called, and the updater runs. These
225
+ # observers don't need notification since they already know the current state.
226
+
227
+ # If an object changes state again then the Set will be reinitialized, and all
228
+ # the observers that might have been on a previous exclusion list, will now be
229
+ # notified.
230
+
231
+ def schedule_delayed_updater(object)
232
+ update_exclusions[object] = Set.new
233
+ @delayed_updater ||= after(0) { run_delayed_updater }
234
+ end
235
+
236
+ # run_delayed_updater will call the mutations method for each observer passing
237
+ # the entire list of objects that changed while waiting for the delay except
238
+ # those that the observer has already seen (the exclusion list). The observers
239
+ # mutation method may cause some other observer already on the observers_to_update
240
+ # list to be removed. To prevent these observers from receiving mutations we keep a
241
+ # temporary set of removed_observers. This is initialized before the mutations,
242
+ # and then cleared as soon as we are done.
243
+
244
+ def run_delayed_updater
245
+ current_update_exclusions = @update_exclusions
246
+ @update_exclusions = @delayed_updater = nil
247
+ @removed_observers = Set.new
248
+ observers_to_update(current_update_exclusions).each do |observer, objects|
249
+ observer.mutations objects unless @removed_observers.include? observer
250
+ end
251
+ ensure
252
+ @removed_observers = nil
253
+ end
254
+
255
+ # observers_to_update returns a hash with observers as keys, and lists of objects
256
+ # as values. The hash is built by filtering the current_observers list
257
+ # including only observers that have mutated objects, that are not on the exclusion
258
+ # list.
259
+
260
+ def observers_to_update(exclusions)
261
+ Hash.new { |hash, key| hash[key] = Array.new }.tap do |updates|
262
+ exclusions.each do |object, excluded_observers|
263
+ current_observers[object].each do |observer|
264
+ next if excluded_observers.include?(observer)
265
+ updates[observer] << object
266
+ end if current_observers.key? object
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end