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
@@ -0,0 +1,87 @@
1
+ # The query listener is what gets notified on the backend when the results from
2
+ # a query have changed. It then will make the necessary changes to any ArrayStore's
3
+ # to get them to display the new data.
4
+ class QueryListener
5
+ def initialize(query_listener_pool, tasks, collection, query)
6
+ @query_listener_pool = query_listener_pool
7
+ @tasks = tasks
8
+ @stores = []
9
+
10
+ @collection = collection
11
+ @query = query
12
+
13
+ @listening = false
14
+ end
15
+
16
+ def add_listener
17
+ @listening = true
18
+ @tasks.call('QueryTasks', 'add_listener', @collection, @query) do |results|
19
+ # When the initial data comes back, add it into the stores.
20
+ @stores.each do |store|
21
+ store.model.clear
22
+ results.each do |index, data|
23
+ store.add(index, data)
24
+ end
25
+
26
+ store.change_state_to(:loaded)
27
+ end
28
+ end
29
+ end
30
+
31
+ def add_store(store, &block)
32
+ puts "ADD STORE: #{store.inspect} - to #{self.inspect}"
33
+ @stores << store
34
+
35
+ if @listening
36
+ # We are already listening and have this model somewhere else,
37
+ # copy the data from the existing model.
38
+ store.model.clear
39
+ @stores.first.model.each_with_index do |item, index|
40
+ store.add(index, item)
41
+ end
42
+ else
43
+ # First time we've added a store, setup the listener and get
44
+ # the initial data.
45
+ add_listener
46
+ end
47
+ end
48
+
49
+ def remove_store(store)
50
+ @stores.delete(store)
51
+
52
+ # When there are no stores left, remove the query listener from
53
+ # the pool, it can get created again later.
54
+ if @stores.size == 0
55
+ puts "OM"
56
+ @query_listener_pool.remove(@collection, @query)
57
+ puts "PM"
58
+
59
+ # Stop listening
60
+ if @listening
61
+ @listening = false
62
+ puts "TOTAL REMOVE"
63
+ @tasks.call('QueryTasks', 'remove_listener', @collection, @query)
64
+ end
65
+ end
66
+ end
67
+
68
+ def added(index, data)
69
+ @stores.each do |store|
70
+ store.add(index, data)
71
+ end
72
+ puts "Added: #{index} - #{data.inspect}"
73
+ end
74
+
75
+ def removed(ids)
76
+ @stores.each do |store|
77
+ store.remove(ids)
78
+ end
79
+ end
80
+
81
+ def changed(model_id, data)
82
+ $loading_models = true
83
+ puts "From Backend: UPDATE: #{model_id} with #{data.inspect}"
84
+ Persistors::ModelStore.changed(model_id, data)
85
+ $loading_models = false
86
+ end
87
+ end
@@ -0,0 +1,9 @@
1
+ require 'volt/utils/generic_pool'
2
+ require 'volt/models/persistors/query/query_listener'
3
+
4
+ # Keeps track of all query listeners, so they can be reused in different
5
+ # places. Dynamically generated queries may end up producing the same
6
+ # query in different places. This makes it so we only need to track a
7
+ # single query at once. Data updates will only be sent once as well.
8
+ class QueryListenerPool < GenericPool
9
+ end
@@ -1,23 +1,21 @@
1
1
  require 'volt/models/persistors/base'
2
+ require 'volt/models/persistors/model_identity_map'
2
3
 
3
4
  module Persistors
4
5
  class Store < Base
5
- def initialize(model, tasks=nil)
6
- @model = model
7
- @is_tracking = false
8
- @tasks = tasks
9
- end
10
6
 
11
- def change_channel_connection(add_or_remove, event=nil, scope=nil)
12
- if (@model.attributes && @model.path.size > 1) || @model.is_a?(ArrayModel)
13
- channel_name = self.channel_name.to_s
14
- channel_name += "-#{event}" if event
15
-
16
- puts "Event #{add_or_remove}: #{channel_name} -- #{@model.attributes.inspect}"
17
- @tasks.call('ChannelTasks', "#{add_or_remove}_listener", channel_name, scope)
18
- end
7
+ @@identity_map = ModelIdentityMap.new
8
+
9
+ def initialize(model, tasks=nil)
10
+ @tasks = tasks
11
+ @model = model
12
+
13
+ @saved = false
19
14
  end
20
15
 
16
+ def saved?
17
+ @saved
18
+ end
21
19
 
22
20
  # On stores, we store the model so we don't have to look it up
23
21
  # every time we do a read.
@@ -92,7 +92,7 @@ class URL
92
92
  # Scroll to anchor
93
93
  %x{
94
94
  var anchor = $('a[name="' + this.fragment + '"]');
95
- if (anchor) {
95
+ if (anchor && anchor.length > 0) {
96
96
  $(document.body).scrollTop(anchor.offset().top);
97
97
  }
98
98
  }
@@ -2,9 +2,9 @@ require 'volt/page/bindings/base_binding'
2
2
  require 'volt/page/targets/attribute_target'
3
3
 
4
4
  class AttributeBinding < BaseBinding
5
- def initialize(target, context, binding_name, attribute_name, getter)
5
+ def initialize(page, target, context, binding_name, attribute_name, getter)
6
6
  # puts "New Attribute Binding: #{binding_name}, #{attribute_name}, #{getter}"
7
- super(target, context, binding_name)
7
+ super(page, target, context, binding_name)
8
8
 
9
9
  @attribute_name = attribute_name
10
10
  @getter = getter
@@ -1,7 +1,19 @@
1
+ # The BaseBinding class is the base for all bindings. It takes
2
+ # 4 arguments that should be passed up from the children (via super)
3
+ #
4
+ # 1. page - this class instance should provide:
5
+ # - a #templates methods that returns a hash for templates
6
+ # - an #events methods that returns an instance of DocumentEvents
7
+ # 2. target - an DomTarget or AttributeTarget
8
+ # 3. context - the context object the binding will be evaluated in
9
+ # 4. binding_name - the id for the comment (or id for attributes) where the
10
+ # binding will be inserted.
1
11
  class BaseBinding
2
12
  attr_accessor :target, :context, :binding_name
3
13
 
4
- def initialize(target, context, binding_name)
14
+ def initialize(page, target, context, binding_name)
15
+ # puts "NEW #{context.inspect} - #{self.inspect}"
16
+ @page = page
5
17
  @target = target
6
18
  @context = context
7
19
  @binding_name = binding_name
@@ -25,7 +25,7 @@ class ComponentBinding < TemplateBinding
25
25
  current_context = SubContext.new(model_with_parent, $page)
26
26
  end
27
27
 
28
- @current_template = TemplateRenderer.new(@target, current_context, @binding_name, full_path)
28
+ @current_template = TemplateRenderer.new(@page, @target, current_context, @binding_name, full_path)
29
29
 
30
30
  call_ready
31
31
  end
@@ -1,9 +1,9 @@
1
1
  require 'volt/page/bindings/base_binding'
2
2
 
3
3
  class ContentBinding < BaseBinding
4
- def initialize(target, context, binding_name, getter)
4
+ def initialize(page, target, context, binding_name, getter)
5
5
  # puts "new content binding: #{getter}"
6
- super(target, context, binding_name)
6
+ super(page, target, context, binding_name)
7
7
 
8
8
  # Find the source for the content binding
9
9
  @value = value_from_getter(getter)
@@ -1,10 +1,10 @@
1
1
  require 'volt/page/bindings/base_binding'
2
2
 
3
3
  class EachBinding < BaseBinding
4
- def initialize(target, context, binding_name, getter, variable_name, template_name)
4
+ def initialize(page, target, context, binding_name, getter, variable_name, template_name)
5
5
  # puts "New EACH Binding"
6
6
 
7
- super(target, context, binding_name)
7
+ super(page, target, context, binding_name)
8
8
 
9
9
  @item_name = variable_name
10
10
  @template_name = template_name
@@ -51,20 +51,8 @@ class EachBinding < BaseBinding
51
51
  @templates[position].remove
52
52
  @templates.delete_at(position)
53
53
 
54
- value_obj = @value.cur
55
-
56
- if value_obj
57
- size = value_obj.size - 1
58
- else
59
- size = 0
60
- end
61
-
62
- # puts "Position: #{position} to #{size}"
63
-
64
54
  # Removed at the position, update context for every item after this position
65
- position.upto(size) do |index|
66
- @templates[index].context.locals[:index].cur = index
67
- end
55
+ update_indexes_after(position)
68
56
  end
69
57
 
70
58
  def item_added(position)
@@ -73,18 +61,34 @@ class EachBinding < BaseBinding
73
61
  binding_name = @@binding_number
74
62
  @@binding_number += 1
75
63
 
76
- # Setup new bindings in the spot we want to insert the item
77
- section.insert_anchor_before_end(binding_name)
64
+ if position >= @templates.size
65
+ # Setup new bindings in the spot we want to insert the item
66
+ section.insert_anchor_before_end(binding_name)
67
+ else
68
+ # Insert the item before an existing item
69
+ section.insert_anchor_before(binding_name, @templates[position].binding_name)
70
+ end
78
71
 
79
72
  index = ReactiveValue.new(position)
80
73
  value = @value[index]
81
74
 
82
75
  item_context = SubContext.new({@item_name => value, :index => index, :parent => @value}, @context)
83
76
 
84
- # ObjectTracker.enable_cache
85
- @templates << TemplateRenderer.new(@target, item_context, binding_name, @template_name)
86
- # puts "ADDED 2"
87
- # ObjectTracker.disable_cache
77
+ item_template = TemplateRenderer.new(@page, @target, item_context, binding_name, @template_name)
78
+ @templates.insert(position, item_template)
79
+
80
+ update_indexes_after(position)
81
+ end
82
+
83
+ # When items are added or removed in the middle of the list, we need
84
+ # to update each templates index value.
85
+ def update_indexes_after(start_index)
86
+ size = @templates.size
87
+ if size > 0
88
+ start_index.upto(@templates.size-1) do |index|
89
+ @templates[index].context.locals[:index].cur = index
90
+ end
91
+ end
88
92
  end
89
93
 
90
94
  def update(item=nil)
@@ -21,10 +21,8 @@ end
21
21
 
22
22
  class EventBinding < BaseBinding
23
23
  attr_accessor :context, :binding_name
24
- def initialize(target, context, binding_name, event_name, call_proc)
25
- @target = target
26
- @context = context
27
- @binding_name = binding_name
24
+ def initialize(page, target, context, binding_name, event_name, call_proc)
25
+ super(page, target, context, binding_name)
28
26
  @event_name = event_name
29
27
 
30
28
  handler = Proc.new do |js_event|
@@ -36,12 +34,12 @@ class EventBinding < BaseBinding
36
34
  result = @context.instance_exec(event, &call_proc)
37
35
  end
38
36
 
39
- @listener = $page.events.add(event_name, self, handler)
37
+ @listener = @page.events.add(event_name, self, handler)
40
38
  end
41
39
 
42
40
  # Remove the event binding
43
41
  def remove
44
42
  # puts "REMOVE EL FOR #{@event}"
45
- $page.events.remove(@event_name, self)
43
+ @page.events.remove(@event_name, self)
46
44
  end
47
45
  end
@@ -1,12 +1,11 @@
1
1
  require 'volt/page/bindings/base_binding'
2
2
 
3
3
  class IfBinding < BaseBinding
4
- def initialize(target, context, binding_name, branches)
5
- getter, template_name = branches[0]
4
+ def initialize(page, target, context, binding_name, branches)
6
5
  # puts "New If Binding: #{binding_name}, #{getter.inspect}"
6
+ super(page, target, context, binding_name)
7
7
 
8
-
9
- super(target, context, binding_name)
8
+ getter, template_name = branches[0]
10
9
 
11
10
  @branches = []
12
11
  @listeners = []
@@ -58,7 +57,7 @@ class IfBinding < BaseBinding
58
57
  end
59
58
 
60
59
  if true_template
61
- @template = TemplateRenderer.new(@target, @context, binding_name, true_template)
60
+ @template = TemplateRenderer.new(@page, @target, @context, binding_name, true_template)
62
61
  end
63
62
  end
64
63
  end
@@ -2,9 +2,9 @@ require 'volt/page/bindings/base_binding'
2
2
  require 'volt/page/template_renderer'
3
3
 
4
4
  class TemplateBinding < BaseBinding
5
- def initialize(target, context, binding_name, binding_in_path, getter)
5
+ def initialize(page, target, context, binding_name, binding_in_path, getter)
6
6
  # puts "New template binding: #{context.inspect} - #{binding_name.inspect}"
7
- super(target, context, binding_name)
7
+ super(page, target, context, binding_name)
8
8
 
9
9
  # Binding in path is the path for the template this binding is in
10
10
  setup_path(binding_in_path)
@@ -40,7 +40,7 @@ class TemplateBinding < BaseBinding
40
40
 
41
41
  # Returns true if there is a template at the path
42
42
  def check_for_template?(path)
43
- $page.templates[path]
43
+ @page.templates[path]
44
44
  end
45
45
 
46
46
  # Takes in a lookup path and returns the full path for the matching
@@ -141,7 +141,7 @@ class TemplateBinding < BaseBinding
141
141
  current_context = @context
142
142
  end
143
143
 
144
- @current_template = TemplateRenderer.new(@target, current_context, @binding_name, full_path)
144
+ @current_template = TemplateRenderer.new(@page, @target, current_context, @binding_name, full_path)
145
145
 
146
146
  call_ready
147
147
  end
@@ -89,7 +89,6 @@ class Channel
89
89
 
90
90
  def message_received(message)
91
91
  message = JSON.parse(message)
92
- # puts "GOT: #{message.inspect}"
93
92
  trigger!('message', nil, *message)
94
93
  end
95
94
 
@@ -0,0 +1,7 @@
1
+ # Should have
2
+ # templates
3
+ # events
4
+
5
+ class Document
6
+
7
+ end
@@ -61,7 +61,7 @@ class Page
61
61
  }
62
62
  });
63
63
 
64
- $(document).on('click', 'a', function(event) {
64
+ $(document).on('click', 'a', function(event) {
65
65
  Opal.gvars.page.$link_clicked($(this).attr('href'));
66
66
  event.stopPropagation();
67
67
 
@@ -141,7 +141,7 @@ class Page
141
141
  main_controller = IndexController.new
142
142
 
143
143
  # Setup main page template
144
- TemplateRenderer.new(DomTarget.new, main_controller, 'CONTENT', 'home/index/index/body')
144
+ TemplateRenderer.new(self, DomTarget.new, main_controller, 'CONTENT', 'home/index/index/body')
145
145
 
146
146
  # Setup title listener template
147
147
  title_target = AttributeTarget.new
@@ -149,7 +149,7 @@ class Page
149
149
  title = title_target.to_html
150
150
  `document.title = title;`
151
151
  end
152
- TemplateRenderer.new(title_target, main_controller, "main", "home/index/index/title")
152
+ TemplateRenderer.new(self, title_target, main_controller, "main", "home/index/index/title")
153
153
 
154
154
  # TODO: this dom ready should really happen in the template renderer
155
155
  main_controller.dom_ready if main_controller.respond_to?(:dom_ready)
@@ -181,7 +181,7 @@ if Volt.client?
181
181
  $page = Page.new
182
182
 
183
183
  # Call start once the page is loaded
184
- Document.ready? do
184
+ Document.ready? do
185
185
  $page.start
186
186
  end
187
187
  end
@@ -1,11 +1,11 @@
1
1
  class ReactiveTemplate
2
2
  include Events
3
3
 
4
- def initialize(context, template_path)
4
+ def initialize(page, context, template_path)
5
5
  # puts "New Reactive Template: #{context.inspect} - #{template_path.inspect}"
6
6
  @template_path = template_path
7
7
  @target = AttributeTarget.new
8
- @template = TemplateRenderer.new(@target, context, "main", template_path)
8
+ @template = TemplateRenderer.new(page, @target, context, "main", template_path)
9
9
  end
10
10
 
11
11
  def event_added(event, scope_provider, first)
@@ -47,6 +47,11 @@ class DomSection < BaseSection
47
47
  Element.find(@end_node).before("<!-- $#{binding_name} --><!-- $/#{binding_name} -->")
48
48
  end
49
49
 
50
+ def insert_anchor_before(binding_name, insert_after_binding)
51
+ node = find_by_comment("$#{insert_after_binding}")
52
+ Element.find(node).before("<!-- $#{binding_name} --><!-- $/#{binding_name} -->")
53
+ end
54
+
50
55
  # Takes in an array of dom nodes and replaces the current content
51
56
  # with the new nodes
52
57
  def nodes=(nodes)
@@ -29,19 +29,17 @@ class Tasks
29
29
 
30
30
  def received_message(name, callback_id, *args)
31
31
  case name
32
+ when 'added', 'removed', 'updated', 'changed'
33
+ notify_query(name, *args)
32
34
  when 'response'
33
35
  response(callback_id, *args)
34
- when 'changed'
35
- changed(*args)
36
- when 'added'
37
- added(*args)
38
- when 'removed'
39
- removed(*args)
40
36
  when 'reload'
41
37
  reload
42
38
  end
43
39
  end
44
40
 
41
+ # When a request is sent to the backend, it can attach a callback,
42
+ # this is called from the backend to pass to the callback.
45
43
  def response(callback_id, result, error)
46
44
  callback = @callbacks.delete(callback_id)
47
45
 
@@ -55,40 +53,12 @@ class Tasks
55
53
  end
56
54
  end
57
55
 
58
- def changed(model_id, data)
59
- $loading_models = true
60
- puts "From Backend: UPDATE: #{model_id} with #{data.inspect}"
61
- Persistors::ModelStore.update(model_id, data)
62
- $loading_models = false
63
- end
64
-
65
- def added(path, data)
66
- $loading_models = true
67
-
68
- # Don't add if already in there
69
- # TODO: shouldn't send twice
70
- unless Persistors::ModelStore.from_id(data[:_id])
71
-
72
- _, parent_id = data.find {|k,v| k != :_id && k[-3..-1] == '_id'}
73
- if parent_id
74
- parent_collection = Persistors::ModelStore.from_id(parent_id).model
75
- else
76
- # On the root
77
- parent_collection = $page.store
78
- end
79
-
80
- puts "From Backend: Add: #{path.inspect} - #{data.inspect}"
81
- parent_collection.send(path) << data
82
- end
83
- $loading_models = false
84
- end
85
-
86
- def removed(id)
87
- puts "From Backend: Remove: #{id}"
88
- $loading_models = true
89
- model = Persistors::ModelStore.from_id(id)
90
- model.delete!
91
- $loading_models = false
56
+ # Called when the backend sends a notification to change the results of
57
+ # a query.
58
+ def notify_query(method_name, collection, query, *args)
59
+ query_obj = Persistors::ArrayStore.query_pool.lookup(collection, query)
60
+ puts "FOUND QUERY: #{collection.inspect} - #{query.inspect} - #{query_obj.inspect} - #{method_name} - #{query_obj.instance_variable_get('@stores').inspect}"
61
+ query_obj.send(method_name, *args)
92
62
  end
93
63
 
94
64
  def reload