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
@@ -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