volt 0.5.18 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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