volt 0.5.18 → 0.6.0

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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/Readme.md +14 -0
  3. data/VERSION +1 -1
  4. data/app/volt/controllers/notices_controller.rb +9 -0
  5. data/app/volt/tasks/live_query/data_store.rb +12 -0
  6. data/app/volt/tasks/live_query/live_query.rb +86 -0
  7. data/app/volt/tasks/live_query/live_query_pool.rb +36 -0
  8. data/app/volt/tasks/live_query/query_tracker.rb +95 -0
  9. data/app/volt/tasks/query_tasks.rb +57 -0
  10. data/app/volt/tasks/store_tasks.rb +4 -17
  11. data/lib/volt.rb +2 -0
  12. data/lib/volt/console.rb +1 -1
  13. data/lib/volt/controllers/model_controller.rb +4 -0
  14. data/lib/volt/extra_core/array.rb +9 -0
  15. data/lib/volt/extra_core/extra_core.rb +1 -0
  16. data/lib/volt/extra_core/hash.rb +11 -0
  17. data/lib/volt/extra_core/object.rb +4 -0
  18. data/lib/volt/models/array_model.rb +56 -0
  19. data/lib/volt/models/model.rb +6 -11
  20. data/lib/volt/models/model_helpers.rb +12 -0
  21. data/lib/volt/models/persistors/array_store.rb +120 -21
  22. data/lib/volt/models/persistors/model_identity_map.rb +12 -0
  23. data/lib/volt/models/persistors/model_store.rb +20 -60
  24. data/lib/volt/models/persistors/query/query_listener.rb +87 -0
  25. data/lib/volt/models/persistors/query/query_listener_pool.rb +9 -0
  26. data/lib/volt/models/persistors/store.rb +11 -13
  27. data/lib/volt/models/url.rb +1 -1
  28. data/lib/volt/page/bindings/attribute_binding.rb +2 -2
  29. data/lib/volt/page/bindings/base_binding.rb +13 -1
  30. data/lib/volt/page/bindings/component_binding.rb +1 -1
  31. data/lib/volt/page/bindings/content_binding.rb +2 -2
  32. data/lib/volt/page/bindings/each_binding.rb +25 -21
  33. data/lib/volt/page/bindings/event_binding.rb +4 -6
  34. data/lib/volt/page/bindings/if_binding.rb +4 -5
  35. data/lib/volt/page/bindings/template_binding.rb +4 -4
  36. data/lib/volt/page/channel.rb +0 -1
  37. data/lib/volt/page/document.rb +7 -0
  38. data/lib/volt/page/page.rb +4 -4
  39. data/lib/volt/page/reactive_template.rb +2 -2
  40. data/lib/volt/page/targets/dom_section.rb +5 -0
  41. data/lib/volt/page/tasks.rb +10 -40
  42. data/lib/volt/page/template_renderer.rb +4 -4
  43. data/lib/volt/reactive/events.rb +14 -0
  44. data/lib/volt/reactive/reactive_array.rb +17 -7
  45. data/lib/volt/reactive/reactive_value.rb +65 -1
  46. data/lib/volt/server.rb +1 -1
  47. data/lib/volt/server/if_binding_setup.rb +3 -1
  48. data/lib/volt/server/socket_connection_handler.rb +7 -5
  49. data/lib/volt/server/template_parser.rb +7 -7
  50. data/lib/volt/tasks/dispatcher.rb +3 -0
  51. data/lib/volt/utils/ejson.rb +9 -0
  52. data/lib/volt/utils/generic_counting_pool.rb +44 -0
  53. data/lib/volt/utils/generic_pool.rb +88 -0
  54. data/spec/models/reactive_array_spec.rb +43 -0
  55. data/spec/models/reactive_generator_spec.rb +58 -0
  56. data/spec/models/reactive_value_spec.rb +6 -0
  57. data/spec/page/bindings/content_binding_spec.rb +36 -0
  58. data/spec/spec_helper.rb +13 -12
  59. data/spec/tasks/live_query_spec.rb +20 -0
  60. data/spec/tasks/query_tasks.rb +10 -0
  61. data/spec/tasks/query_tracker_spec.rb +120 -0
  62. data/spec/templates/template_binding_spec.rb +16 -10
  63. data/spec/utils/generic_counting_pool_spec.rb +36 -0
  64. data/spec/utils/generic_pool_spec.rb +50 -0
  65. metadata +29 -5
  66. data/app/volt/tasks/channel_tasks.rb +0 -55
  67. data/spec/tasks/channel_tasks_spec.rb +0 -74
@@ -2,12 +2,12 @@ require 'volt/page/bindings/base_binding'
2
2
 
3
3
  class TemplateRenderer < BaseBinding
4
4
  attr_reader :context
5
- def initialize(target, context, binding_name, template_name)
5
+ def initialize(page, target, context, binding_name, template_name)
6
6
  # puts "new template renderer: #{context.inspect} - #{binding_name.inspect}"
7
- super(target, context, binding_name)
7
+ super(page, target, context, binding_name)
8
8
 
9
9
  # puts "Template Name: #{template_name}"
10
- @template = $page.templates[template_name]
10
+ @template = @page.templates[template_name]
11
11
  @sub_bindings = []
12
12
 
13
13
  if @template
@@ -22,7 +22,7 @@ class TemplateRenderer < BaseBinding
22
22
 
23
23
  bindings.each_pair do |id,bindings_for_id|
24
24
  bindings_for_id.each do |binding|
25
- @sub_bindings << binding.call(target, context, id)
25
+ @sub_bindings << binding.call(page, target, context, id)
26
26
  end
27
27
  end
28
28
  end
@@ -201,5 +201,19 @@ module Events
201
201
  def trigger_by_scope!(event, *args, &block)
202
202
  trigger!(event, block, *args)
203
203
  end
204
+
205
+ # Takes an event and a list of method names, and triggers the event for each listener
206
+ # coming off of those methods.
207
+ def trigger_for_methods!(event, *method_names)
208
+ trigger_by_scope!(event, [], nil) do |scope|
209
+ if scope
210
+ method_name = scope.first
211
+
212
+ method_names.include?(method_name)
213
+ else
214
+ false
215
+ end
216
+ end
217
+ end
204
218
 
205
219
  end
@@ -87,6 +87,14 @@ class ReactiveArray# < Array
87
87
  self.delete_at(@array.index(val))
88
88
  end
89
89
 
90
+ # Removes all items in the array model.
91
+ tag_method(:clear) do
92
+ destructive!
93
+ end
94
+ def clear
95
+ @array = []
96
+ trigger!('changed')
97
+ end
90
98
 
91
99
  tag_method(:<<) do
92
100
  pass_reactive!
@@ -126,14 +134,16 @@ class ReactiveArray# < Array
126
134
  tag_method(:insert) do
127
135
  destructive!
128
136
  end
129
- # alias :__old_insert :insert
130
- def insert(*args)
131
- old_size = self.size
132
- result = @array.insert(*args)
137
+ def insert(index, *objects)
138
+ result = @array.insert(index, *objects)
133
139
 
134
- old_size.upto(result.size-1) do |index|
135
- trigger_for_index!('changed', index)
136
- trigger_on_direct_listeners!('added', old_size+index)
140
+ # All objects from index to the end have "changed"
141
+ index.upto(result.size-1) do |idx|
142
+ trigger_for_index!('changed', idx)
143
+ end
144
+
145
+ objects.size.times do |count|
146
+ trigger_on_direct_listeners!('added', index+count)
137
147
  end
138
148
 
139
149
  trigger_size_change!
@@ -42,7 +42,7 @@ class ReactiveValue < BasicObject
42
42
  # Proxy methods to the ReactiveManager. We want to have as few
43
43
  # as possible methods on reactive values, so all other methods
44
44
  # are forwarded to the object the reactive value points to.
45
- [:cur, :cur=, :on, :trigger!, :trigger_by_scope!].each do |method_name|
45
+ [:cur, :cur=, :deep_cur, :on, :trigger!, :trigger_by_scope!].each do |method_name|
46
46
  define_method(method_name) do |*args, &block|
47
47
  @reactive_manager.send(method_name, *args, &block)
48
48
  end
@@ -173,6 +173,63 @@ class ReactiveValue < BasicObject
173
173
  return [wrapped_object, self]
174
174
  end
175
175
  end
176
+
177
+ # Return a new reactive value that listens for changes on any
178
+ # ReactiveValues inside of its children (hash values, array items, etc..)
179
+ # This is useful if someone is passing in a set of options, but the main
180
+ # hash isn't a ReactiveValue, but you want to listen for changes inside
181
+ # of the hash.
182
+ #
183
+ # skip_if_no_reactives lets you get back a non-reactive value in the event
184
+ # that there are no child reactive values.
185
+ def self.from_hash(hash, skip_if_no_reactives=false)
186
+ ::ReactiveGenerator.from_hash(hash)
187
+ end
188
+ end
189
+
190
+ class ReactiveGenerator
191
+ # Takes a hash and returns a ReactiveValue that depends on
192
+ # any ReactiveValue's inside of the hash (or children).
193
+ def self.from_hash(hash, skip_if_no_reactives=false)
194
+ reactives = find_reactives(hash)
195
+
196
+ if skip_if_no_reactives && reactives.size == 0
197
+ # There weren't any reactives, we can just use the hash
198
+ return hash
199
+ else
200
+ # Create a new reactive value that listens on all of its
201
+ # child reactive values.
202
+ value = ReactiveValue.new(hash)
203
+
204
+ reactives.each do |child|
205
+ value.reactive_manager.add_parent!(child)
206
+ end
207
+
208
+ return value
209
+ end
210
+ end
211
+
212
+ # Recursively loop through the data, returning a list of all
213
+ # reactive values in the hash, array, etc..
214
+ def self.find_reactives(object)
215
+ found = []
216
+ if object.reactive?
217
+ found << object
218
+
219
+ found += find_reactives(object.cur)
220
+ elsif object.is_a?(Array)
221
+ object.each do |item|
222
+ found += find_reactives(item)
223
+ end
224
+ elsif object.is_a?(Hash)
225
+ object.each_pair do |key, value|
226
+ found += find_reactives(key)
227
+ found += find_reactives(value)
228
+ end
229
+ end
230
+
231
+ return found.flatten
232
+ end
176
233
  end
177
234
 
178
235
  class ReactiveManager
@@ -266,6 +323,13 @@ class ReactiveManager
266
323
  end
267
324
  end
268
325
 
326
+ # Returns a copy of the object with where all ReactiveValue's are replaced
327
+ # with their current value.
328
+ # NOTE: Classes need to implement their own deep_cur method for this to work,
329
+ # it works out of the box with arrays and hashes.
330
+ def deep_cur
331
+ self.cur.deep_cur
332
+ end
269
333
 
270
334
  # Method calls can be tagged so the reactive value knows
271
335
  # how to handle them. This lets you check the state of
data/lib/volt/server.rb CHANGED
@@ -10,7 +10,7 @@ require "sass"
10
10
  require "sprockets-sass"
11
11
  require 'listen'
12
12
 
13
- require 'volt/extra_core/extra_core'
13
+ require 'volt'
14
14
  require 'volt/server/component_handler'
15
15
  if RUBY_PLATFORM != 'java'
16
16
  require 'volt/server/socket_connection_handler'
@@ -24,6 +24,8 @@ class IfBindingSetup < BindingSetup
24
24
  "[#{content}, #{branch[1].inspect}]"
25
25
  end.join(', ')
26
26
 
27
- "lambda { |target, context, id| IfBinding.new(target, context, id, [#{branches}]) }"
27
+ # variables are captured for branches, so we must prefix them so they don't conflict.
28
+ # page, target, context, id
29
+ "lambda { |__p, __t, __c, __id| IfBinding.new(__p, __t, __c, __id, [#{branches}]) }"
28
30
  end
29
31
  end
@@ -37,7 +37,6 @@ class SocketConnectionHandler < SockJS::Session
37
37
  # Messages are json and wrapped in an array
38
38
  message = JSON.parse(message).first
39
39
 
40
- puts "GOT: #{message.inspect}"
41
40
  @@dispatcher.dispatch(self, message)
42
41
  end
43
42
 
@@ -48,12 +47,15 @@ class SocketConnectionHandler < SockJS::Session
48
47
  end
49
48
 
50
49
  def closed
51
- puts "CHANNEL CLOSED"
50
+ puts "CHANNEL CLOSED: #{self.inspect}"
52
51
  # Remove ourself from the available channels
53
52
  @@channels.delete(self)
54
-
55
- # Remove any listening channels
56
- ChannelTasks.new(self).close!
53
+
54
+ QueryTasks.new(self).close!
55
+ end
56
+
57
+ def inspect
58
+ "<#{self.class.to_s}:#{object_id}>"
57
59
  end
58
60
 
59
61
  end
@@ -72,7 +72,7 @@ class Template
72
72
  def add_template(node, content, name='Template')
73
73
  html = "<!-- $#{@binding_number} --><!-- $/#{@binding_number} -->"
74
74
 
75
- @current_scope.add_binding(@binding_number, "lambda { |target, context, id| #{name}Binding.new(target, context, id, #{@template_parser.template_path.inspect}, Proc.new { [#{content}] }) }")
75
+ @current_scope.add_binding(@binding_number, "lambda { |__p, __t, __c, __id| #{name}Binding.new(__p, __t, __c, __id, #{@template_parser.template_path.inspect}, Proc.new { [#{content}] }) }")
76
76
 
77
77
  @binding_number += 1
78
78
  return html
@@ -84,7 +84,7 @@ class Template
84
84
  content, variable_name = content.strip.split(/ as /)
85
85
 
86
86
  template_name = "#{@template_parser.template_path}/#{section_name}/__template/#{@binding_number}"
87
- @current_scope.add_binding(@binding_number, "lambda { |target, context, id| EachBinding.new(target, context, id, Proc.new { #{content} }, #{variable_name.inspect}, #{template_name.inspect}) }")
87
+ @current_scope.add_binding(@binding_number, "lambda { |__p, __t, __c, __id| EachBinding.new(__p, __t, __c, __id, Proc.new { #{content} }, #{variable_name.inspect}, #{template_name.inspect}) }")
88
88
 
89
89
  # Add the node, the binding number, then store the location where the
90
90
  # bindings for this block starts.
@@ -151,7 +151,7 @@ class Template
151
151
  def add_text_binding(content)
152
152
  html = "<!-- $#{@binding_number} --><!-- $/#{@binding_number} -->"
153
153
 
154
- @current_scope.add_binding(@binding_number, "lambda { |target, context, id| ContentBinding.new(target, context, id, Proc.new { #{content} }) }")
154
+ @current_scope.add_binding(@binding_number, "lambda { |__p, __t, __c, __id| ContentBinding.new(__p, __t, __c, __id, Proc.new { #{content} }) }")
155
155
 
156
156
  @binding_number += 1
157
157
  return html
@@ -194,7 +194,7 @@ class Template
194
194
  getter = "_tmp = #{content[1..-2]}.or('') ; _tmp.reactive_manager.setter! { |val| self.#{content[1..-2]} = val } ; _tmp"
195
195
  end
196
196
 
197
- @current_scope.add_binding(node['id'], "lambda { |target, context, id| AttributeBinding.new(target, context, id, #{attribute.inspect}, Proc.new { #{getter} }) }")
197
+ @current_scope.add_binding(node['id'], "lambda { |__p, __t, __c, __id| AttributeBinding.new(__p, __t, __c, __id, #{attribute.inspect}, Proc.new { #{getter} }) }")
198
198
  end
199
199
 
200
200
  def add_multiple_getters(node, attribute, content)
@@ -208,7 +208,7 @@ class Template
208
208
 
209
209
  reactive_template_path = add_reactive_template(content)
210
210
 
211
- @current_scope.add_binding(node['id'], "lambda { |target, context, id| AttributeBinding.new(target, context, id, #{attribute.inspect}, Proc.new { ReactiveTemplate.new(context, #{reactive_template_path.inspect}) }) }")
211
+ @current_scope.add_binding(node['id'], "lambda { |__p, __t, __c, __id| AttributeBinding.new(__p, __t, __c, __id, #{attribute.inspect}, Proc.new { ReactiveTemplate.new(__p, __c, #{reactive_template_path.inspect}) }) }")
212
212
  end
213
213
 
214
214
  # Returns a path to a template for the content. This can be passed
@@ -237,7 +237,7 @@ class Template
237
237
  node['href'] ||= ''
238
238
  end
239
239
 
240
- @current_scope.add_binding(node['id'], "lambda { |target, context, id| EventBinding.new(target, context, id, #{event.inspect}, Proc.new {|event| #{content} })}")
240
+ @current_scope.add_binding(node['id'], "lambda { |__p, __t, __c, __id| EventBinding.new(__p, __t, __c, __id, #{event.inspect}, Proc.new {|event| #{content} })}")
241
241
  end
242
242
 
243
243
  def pull_closed_block_scopes(scope=@current_scope)
@@ -337,7 +337,7 @@ class Template
337
337
  # Has multiple bindings, we need to render a template here
338
338
  attr_template_path = add_reactive_template(content)
339
339
 
340
- value = "Proc.new { ReactiveTemplate.new(context, #{attr_template_path.inspect}) }"
340
+ value = "Proc.new { ReactiveTemplate.new(__p, __c, #{attr_template_path.inspect}) }"
341
341
  end
342
342
 
343
343
  attribute_hash[attribute_node.name] = value
@@ -18,6 +18,9 @@ class Dispatcher
18
18
  result = klass.new(channel, self).send(method_name, *args)
19
19
  error = nil
20
20
  rescue => e
21
+ # TODO: Log these errors better
22
+ puts "ERROR: #{e.inspect}"
23
+ puts e.backtrace
21
24
  result = nil
22
25
  error = e
23
26
  end
@@ -0,0 +1,9 @@
1
+ class EJson
2
+ def self.dump_as(obj)
3
+ obj
4
+ end
5
+
6
+ def self.dump(obj)
7
+ JSON.dump(dump_as(obj))
8
+ end
9
+ end
@@ -0,0 +1,44 @@
1
+ require 'volt/utils/generic_pool'
2
+
3
+ # A counting pool behaves like a normal GenericPool, except for
4
+ # each time lookup is called, remove should be called when complete.
5
+ # The item will be completely removed from the GenericCountingPool
6
+ # only when it has been removed an equal number of times it has been
7
+ # looked up.
8
+ class GenericCountingPool < GenericPool
9
+ # return a created item with a count
10
+ def generate_new(*args)
11
+ [0, create(*args)]
12
+ end
13
+
14
+ # Finds an item and tracks that it was checked out. Use
15
+ # #remove when the item is no longer needed.
16
+ def find(*args, &block)
17
+ item = __lookup(*args, &block)
18
+
19
+ item[0] += 1
20
+
21
+ return item[1]
22
+ end
23
+
24
+ # Lookups an item
25
+ def lookup(*args, &block)
26
+ item = super(*args, &block)
27
+
28
+ return item[1]
29
+ end
30
+
31
+ def transform_item(item)
32
+ [0, item]
33
+ end
34
+
35
+ def remove(*args)
36
+ item = __lookup(*args)
37
+ item[0] -= 1
38
+
39
+ if item[0] == 0
40
+ # Last one using this item has removed it.
41
+ super(*args)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,88 @@
1
+ # GenericPool is a base class you can inherit from to cache items
2
+ # based on a lookup.
3
+ #
4
+ # GenericPool assumes either a block is passed to lookup, or a
5
+ # #create method, that takes the path arguments and reutrns a new instance.
6
+ #
7
+ # GenericPool can handle as deep of paths as needed. You can also lookup
8
+ # all of the items at a sub-path with #lookup_all
9
+ #
10
+ # TODO: make the lookup/create threadsafe
11
+ class GenericPool
12
+ attr_reader :pool
13
+ def initialize
14
+ @pool = {}
15
+ end
16
+
17
+ def lookup(*args, &block)
18
+ section = @pool
19
+
20
+ # TODO: This is to work around opal issue #500
21
+ if RUBY_PLATFORM == 'opal'
22
+ args.pop if args.last == nil
23
+ end
24
+
25
+
26
+ args.each_with_index do |arg, index|
27
+ last = (args.size-1) == index
28
+
29
+ if last
30
+ # return, creating if needed
31
+ return(section[arg] ||= create_new_item(*args, &block))
32
+ else
33
+ next_section = section[arg]
34
+ next_section ||= (section[arg] = {})
35
+ section = next_section
36
+ end
37
+ end
38
+ end
39
+
40
+ # Does the actual creating, if a block is not passed in, it calls
41
+ # #create on the class.
42
+ def create_new_item(*args)
43
+ if block_given?
44
+ new_item = yield(*args)
45
+ else
46
+ new_item = create(*args)
47
+ end
48
+
49
+ return transform_item(new_item)
50
+ end
51
+
52
+ # Allow other pools to override how the created item gets stored.
53
+ def transform_item(item)
54
+ item
55
+ end
56
+
57
+ # Make sure we call the pool one from lookup_all and not
58
+ # an overridden one.
59
+ alias_method :__lookup, :lookup
60
+
61
+ def lookup_all(*args)
62
+ __lookup(*args).values
63
+ end
64
+
65
+ def remove(*args)
66
+ stack = []
67
+ section = @pool
68
+
69
+ args.each_with_index do |arg, index|
70
+ stack << section
71
+
72
+ if args.size-1 == index
73
+ section.delete(arg)
74
+ else
75
+ section = section[arg]
76
+ end
77
+ end
78
+
79
+ (stack.size-1).downto(1) do |index|
80
+ node = stack[index]
81
+ parent = stack[index-1]
82
+
83
+ if node.size == 0
84
+ parent.delete(args[index-1])
85
+ end
86
+ end
87
+ end
88
+ end
@@ -17,6 +17,49 @@ describe ReactiveArray do
17
17
  expect(@changed).to eq(true)
18
18
  end
19
19
 
20
+ it "should trigger changed from an insert in all places after the index" do
21
+ model = ReactiveValue.new(Model.new)
22
+ model._my_ary = [1,2,3]
23
+
24
+ count1 = 0
25
+ count2 = 0
26
+ model._my_ary[1].on('changed') { count1 += 1 }
27
+ model._my_ary[3].on('changed') { count2 += 1 }
28
+ expect(count1).to eq(0)
29
+ expect(count2).to eq(0)
30
+
31
+ model._my_ary.insert(1, 10)
32
+ expect(count1).to eq(1)
33
+ expect(count2).to eq(1)
34
+
35
+ expect(model._my_ary.cur).to eq([1,10,2,3])
36
+ end
37
+
38
+ it "should pass the index the item was inserted at" do
39
+ model = ReactiveValue.new(Model.new)
40
+ model._my_ary = [1,2,3]
41
+
42
+ model._my_ary.on('added') do |_, index|
43
+ expect(index).to eq(2)
44
+ end
45
+
46
+ model._my_ary.insert(2, 20)
47
+ end
48
+
49
+ it "should pass the index the item was inserted at with multiple inserted objects" do
50
+ model = ReactiveValue.new(Model.new)
51
+ model._my_ary = [1,2,3]
52
+
53
+ received = []
54
+ model._my_ary.on('added') do |_, index|
55
+ received << index
56
+ end
57
+
58
+ model._my_ary.insert(2, 20, 30)
59
+
60
+ expect(received).to eq([2, 3])
61
+ end
62
+
20
63
  it "should trigger changed on methods of an array model that involve just one cell" do
21
64
  model = ReactiveValue.new(ReactiveArray.new)
22
65