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