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
@@ -1,4 +1,5 @@
1
1
  require 'volt/extra_core/array'
2
+ require 'volt/extra_core/hash'
2
3
  require 'volt/extra_core/object'
3
4
  require 'volt/extra_core/blank'
4
5
  require 'volt/extra_core/stringify_keys'
@@ -0,0 +1,11 @@
1
+ class Hash
2
+ def deep_cur
3
+ new_hash = {}
4
+
5
+ each_pair do |key, value|
6
+ new_hash[key.deep_cur] = value.deep_cur
7
+ end
8
+
9
+ return new_hash
10
+ end
11
+ end
@@ -33,4 +33,8 @@ class Object
33
33
  __send__(*a, &b)
34
34
  end
35
35
  end
36
+
37
+ def deep_cur
38
+ self.cur
39
+ end
36
40
  end
@@ -20,6 +20,42 @@ class ArrayModel < ReactiveArray
20
20
  @persistor.loaded if @persistor
21
21
  end
22
22
 
23
+ # For stored items, tell the collection to load the data when it
24
+ # is requested.
25
+ def [](index)
26
+ load_data
27
+ super
28
+ end
29
+
30
+ def size
31
+ load_data
32
+ super
33
+ end
34
+
35
+ def state
36
+ if @persistor
37
+ @persistor.state
38
+ else
39
+ :loaded
40
+ end
41
+ end
42
+
43
+ def loaded?
44
+ state == :loaded
45
+ end
46
+
47
+ tag_method(:find) do
48
+ destructive!
49
+ pass_reactive!
50
+ end
51
+ def find(*args)
52
+ if @persistor
53
+ return @persistor.find(*args)
54
+ else
55
+ raise "this model's persistance layer does not support find, try using store"
56
+ end
57
+ end
58
+
23
59
  def attributes
24
60
  self
25
61
  end
@@ -63,6 +99,17 @@ class ArrayModel < ReactiveArray
63
99
  return array
64
100
  end
65
101
 
102
+ def inspect
103
+ if @persistor && @persistor.is_a?(Persistors::ArrayStore) && state == :not_loaded
104
+ # Show a special message letting users know it is not loaded yet.
105
+ return "#<#{self.class.to_s}:not loaded, access with [] or size to load>"
106
+ end
107
+
108
+ # Otherwise inspect normally
109
+ super
110
+ end
111
+
112
+
66
113
  private
67
114
  # Takes the persistor if there is one and
68
115
  def setup_persistor(persistor)
@@ -70,4 +117,13 @@ class ArrayModel < ReactiveArray
70
117
  @persistor = persistor.new(self)
71
118
  end
72
119
  end
120
+
121
+ # Loads data in an array store persistor when data is requested.
122
+ def load_data
123
+ if @persistor && @persistor.is_a?(Persistors::ArrayStore)
124
+ @persistor.load_data
125
+ end
126
+ end
127
+
128
+
73
129
  end
@@ -51,16 +51,7 @@ class Model
51
51
  def !
52
52
  !attributes
53
53
  end
54
-
55
- # Pass to the persisotr
56
- def event_added(event, scope_provider, first)
57
- @persistor.event_added(event, scope_provider, first) if @persistor
58
- end
59
-
60
- # Pass to the persistor
61
- def event_removed(event, no_more_events)
62
- @persistor.event_removed(event, no_more_events) if @persistor
63
- end
54
+
64
55
 
65
56
  tag_all_methods do
66
57
  pass_reactive! do |method_name|
@@ -204,7 +195,11 @@ class Model
204
195
 
205
196
  def inspect
206
197
  "<#{self.class.to_s} #{attributes.inspect}>"
207
- end
198
+ end
199
+
200
+ def deep_cur
201
+ attributes
202
+ end
208
203
 
209
204
  private
210
205
  # Clear the previous value and assign a new one
@@ -1,3 +1,5 @@
1
+ # A place for things shared between an ArrayModel and a Model
2
+
1
3
  module ModelHelpers
2
4
  def deep_unwrap(value)
3
5
  if value.is_a?(Model)
@@ -8,4 +10,14 @@ module ModelHelpers
8
10
 
9
11
  return value
10
12
  end
13
+
14
+ # Pass to the persisotr
15
+ def event_added(event, scope_provider, first)
16
+ @persistor.event_added(event, scope_provider, first) if @persistor
17
+ end
18
+
19
+ # Pass to the persistor
20
+ def event_removed(event, no_more_events)
21
+ @persistor.event_removed(event, no_more_events) if @persistor
22
+ end
11
23
  end
@@ -1,46 +1,145 @@
1
1
  require 'volt/models/persistors/store'
2
+ require 'volt/models/persistors/query/query_listener_pool'
2
3
 
3
4
  module Persistors
4
5
  class ArrayStore < Store
6
+ @@query_pool = QueryListenerPool.new
7
+
8
+ attr_reader :model
9
+ attr_accessor :state
10
+
11
+ def self.query_pool
12
+ @@query_pool
13
+ end
14
+
15
+ def initialize(model, tasks=nil)
16
+ super
17
+
18
+ @query = ReactiveValue.from_hash(@model.options[:query] || {})
19
+
20
+ end
5
21
 
6
22
  # Called when a collection loads
7
23
  def loaded
8
- scope = {}
24
+ change_state_to :not_loaded
25
+ end
9
26
 
27
+ def event_added(event, scope_provider, first)
28
+ puts "ADD EV: #{event} - #{first}"
29
+ # First event, we load the data.
30
+ load_data if first
31
+ end
10
32
 
33
+ def event_removed(event, no_more_events)
34
+ # Remove listener where there are no more events on this model
35
+ if no_more_events && @query_listener && @model.listeners.size == 0
36
+ stop_listening
37
+ end
38
+ end
11
39
 
12
- # Scope to the parent
13
- if @model.path.size > 1
14
- parent = @model.parent
40
+ # Called when an event is removed and we no longer want to keep in
41
+ # sync with the database.
42
+ def stop_listening
43
+ @query_listener.remove_store(self)
44
+ @query_listener = nil
45
+
46
+ change_state_to :dirty
47
+ end
48
+
49
+ # Called from the QueryListener when the data is loaded
50
+ def change_state_to(new_state)
51
+ @state = new_state
52
+
53
+ # Trigger changed on the 'state' method
54
+ @model.trigger_for_methods!('changed', :state, :loaded?)
55
+ end
56
+
57
+ # Called the first time data is requested from this collection
58
+ def load_data
59
+ # Don't load data from any queried
60
+ if @state == :not_loaded || @state == :dirty
61
+ puts "Load Data"
62
+ change_state_to :loading
15
63
 
16
- parent.persistor.ensure_setup if parent.persistor
17
- puts @model.parent.inspect
64
+ @query_changed_listener.remove if @query_changed_listener
65
+ if @query.reactive?
66
+ # Query might change, change the query when it does
67
+ @query_changed_listener = @query.on('changed') do
68
+ stop_listening
69
+
70
+ load_data
71
+ end
72
+ end
18
73
 
74
+ puts "QUERY: #{@query.deep_cur.inspect}"
75
+
76
+ run_query(@model, @query.deep_cur)
77
+ end
78
+ end
79
+
80
+ # Clear out the models data, since we're not listening anymore.
81
+ def unload_data
82
+ change_state_to :not_loaded
83
+ @model.clear
84
+ end
85
+
86
+ def run_query(model, query={})
87
+ collection = model.path.last
88
+ # Scope to the parent
89
+ if model.path.size > 1
90
+ parent = model.parent
91
+
92
+ parent.persistor.ensure_setup if parent.persistor
93
+
19
94
  if parent && (attrs = parent.attributes) && attrs[:_id].true?
20
- scope[:"#{@model.path[-3].singularize}_id"] = attrs[:_id]
95
+ query[:"#{model.path[-3].singularize}_id"] = attrs[:_id]
21
96
  end
22
97
  end
98
+
99
+ @query_listener = @@query_pool.lookup(collection, query) do
100
+ # Create if it does not exist
101
+ QueryListener.new(@@query_pool, @tasks, collection, query)
102
+ end
103
+ @query_listener.add_store(model.persistor)
104
+ end
105
+
106
+ def find(query={})
107
+ model = ArrayModel.new([], @model.options.merge(:query => query))
108
+
109
+ return ReactiveValue.new(model)
110
+ end
111
+
112
+ # Called from backend
113
+ def add(index, data)
114
+ $loading_models = true
115
+
116
+ new_options = @model.options.merge(path: @model.path + [:[]], parent: @model)
23
117
 
24
- puts "Load At Scope: #{scope.inspect}"
118
+ # Find the existing model, or create one
119
+ new_model = @@identity_map.find(data['_id']) { Model.new(data.symbolize_keys, new_options) }
25
120
 
26
- query(scope)
121
+ @model.insert(index, new_model)
27
122
 
28
- change_channel_connection('add', 'added')
29
- change_channel_connection('add', 'removed')
123
+ $loading_models = false
30
124
  end
31
125
 
32
- def query(query)
33
- @tasks.call('StoreTasks', 'find', @model.path.last, query) do |results|
34
- # TODO: Globals evil, replace
35
- $loading_models = true
126
+ def remove(ids)
127
+ $loading_models = true
128
+ ids.each do |id|
129
+ puts "delete at: #{id} on #{@model.inspect}"
36
130
 
37
- new_options = @model.options.merge(path: @model.path + [:[]], parent: @model)
38
-
39
- results.each do |result|
40
- @model << Model.new(result, new_options)
131
+ # TODO: optimize this delete so we don't need to loop
132
+ @model.each_with_index do |model, index|
133
+ puts "#{model._id.inspect} vs #{id.inspect} - #{index}"
134
+ if model._id == id
135
+ del = @model.delete_at(index)
136
+ puts "DELETED AT #{index}: #{del.inspect} - #{@model.inspect}"
137
+ break
138
+ end
41
139
  end
42
- $loading_models = false
43
140
  end
141
+
142
+ $loading_models = false
44
143
  end
45
144
 
46
145
  def channel_name
@@ -50,7 +149,7 @@ module Persistors
50
149
 
51
150
  # When a model is added to this collection, we call its "changed"
52
151
  # method. This should trigger a save.
53
- def added(model)
152
+ def added(model, index)
54
153
  unless defined?($loading_models) && $loading_models
55
154
  model.persistor.changed
56
155
  end
@@ -0,0 +1,12 @@
1
+ require 'volt/utils/generic_counting_pool'
2
+
3
+ # The identity map ensures that there is only one copy of a model
4
+ # used on the front end at a time.
5
+ class ModelIdentityMap < GenericCountingPool
6
+ # add extends GenericCountingPool so it can add in a model without
7
+ # a direct lookup. We use this when we create a model (without an id)
8
+ # then save it and it gets assigned an id.
9
+ def add(id, model)
10
+ @pool[id] = [1, model]
11
+ end
12
+ end
@@ -1,12 +1,18 @@
1
1
  require 'volt/models/persistors/store'
2
2
 
3
+
3
4
  module Persistors
4
5
  class ModelStore < Store
5
6
  ID_CHARS = [('a'..'z'), ('A'..'Z'), ('0'..'9')].map {|v| v.to_a }.flatten
6
-
7
- @@identity_map = {}
8
7
 
9
8
  attr_reader :model
9
+ attr_accessor :in_identity_map
10
+
11
+ def initialize(model, tasks)
12
+ super
13
+
14
+ @in_identity_map = false
15
+ end
10
16
 
11
17
  def add_to_collection
12
18
  @in_collection = true
@@ -16,7 +22,6 @@ module Persistors
16
22
 
17
23
  def remove_from_collection
18
24
  @in_collection = false
19
- stop_listening_for_changes
20
25
  end
21
26
 
22
27
  # Called the first time a value is assigned into this model
@@ -24,21 +29,18 @@ module Persistors
24
29
  if @model.attributes
25
30
  @model.attributes[:_id] ||= generate_id
26
31
 
27
- if !model_in_identity_map?
28
- @@identity_map[@model.attributes[:_id]] ||= self
29
- end
30
-
31
- # Check to see if we already have listeners setup
32
- if @model.listeners[:changed]
33
- listen_for_changes
34
- end
32
+ add_to_identity_map
35
33
  end
36
34
  end
37
35
 
38
- def model_in_identity_map?
39
- @@identity_map[@model.attributes[:_id]]
36
+ def add_to_identity_map
37
+ unless @in_identity_map
38
+ @@identity_map.add(@model._id, @model)
39
+
40
+ @in_identity_map = true
41
+ end
40
42
  end
41
-
43
+
42
44
  # Create a random unique id that can be used as the mongo id as well
43
45
  def generate_id
44
46
  id = []
@@ -63,67 +65,25 @@ module Persistors
63
65
  end
64
66
  end
65
67
 
66
- def listen_for_changes
67
- unless @change_listening
68
- if @in_collection
69
- @change_listening = true
70
- change_channel_connection("add")
71
- end
72
- end
73
- end
74
-
75
- def stop_listening_for_changes
76
- if @change_listening
77
- @change_listening = false
78
- change_channel_connection("remove")
79
- end
80
- end
81
-
82
68
  def event_added(event, scope_provider, first)
83
69
  if first && event == :changed
84
- # Start listening
85
70
  ensure_setup
86
- listen_for_changes
87
71
  end
88
72
  end
89
-
90
- def event_removed(event, no_more_events)
91
- if no_more_events && event == :changed
92
- # Stop listening
93
- stop_listening_for_changes
94
- end
95
- end
96
-
97
- def channel_name
98
- @channel_name ||= "#{@model.path[-2]}##{@model.attributes[:_id]}"
99
- end
100
-
101
- # Finds the model in its parent collection and deletes it.
102
- def delete!
103
- if @model.path.size == 0
104
- raise "Not in a collection"
105
- end
106
-
107
- @model.parent.delete(@model)
108
- end
109
73
 
110
74
  # Update the models based on the id/identity map. Usually these requests
111
75
  # will come from the backend.
112
- def self.update(model_id, data)
113
- persistor = @@identity_map[model_id]
76
+ def self.changed(model_id, data)
77
+ model = @@identity_map.lookup(model_id)
114
78
 
115
- if persistor
79
+ if model
116
80
  data.each_pair do |key, value|
117
81
  if key != '_id'
118
- persistor.model.send(:"#{key}=", value)
82
+ model.send(:"#{key}=", value)
119
83
  end
120
84
  end
121
85
  end
122
86
  end
123
-
124
- def self.from_id(id)
125
- @@identity_map[id]
126
- end
127
87
 
128
88
  private
129
89
  # Return the attributes that are only for this store, not any sub-associations.