volt 0.5.18 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Readme.md +14 -0
- data/VERSION +1 -1
- data/app/volt/controllers/notices_controller.rb +9 -0
- data/app/volt/tasks/live_query/data_store.rb +12 -0
- data/app/volt/tasks/live_query/live_query.rb +86 -0
- data/app/volt/tasks/live_query/live_query_pool.rb +36 -0
- data/app/volt/tasks/live_query/query_tracker.rb +95 -0
- data/app/volt/tasks/query_tasks.rb +57 -0
- data/app/volt/tasks/store_tasks.rb +4 -17
- data/lib/volt.rb +2 -0
- data/lib/volt/console.rb +1 -1
- data/lib/volt/controllers/model_controller.rb +4 -0
- data/lib/volt/extra_core/array.rb +9 -0
- data/lib/volt/extra_core/extra_core.rb +1 -0
- data/lib/volt/extra_core/hash.rb +11 -0
- data/lib/volt/extra_core/object.rb +4 -0
- data/lib/volt/models/array_model.rb +56 -0
- data/lib/volt/models/model.rb +6 -11
- data/lib/volt/models/model_helpers.rb +12 -0
- data/lib/volt/models/persistors/array_store.rb +120 -21
- data/lib/volt/models/persistors/model_identity_map.rb +12 -0
- data/lib/volt/models/persistors/model_store.rb +20 -60
- data/lib/volt/models/persistors/query/query_listener.rb +87 -0
- data/lib/volt/models/persistors/query/query_listener_pool.rb +9 -0
- data/lib/volt/models/persistors/store.rb +11 -13
- data/lib/volt/models/url.rb +1 -1
- data/lib/volt/page/bindings/attribute_binding.rb +2 -2
- data/lib/volt/page/bindings/base_binding.rb +13 -1
- data/lib/volt/page/bindings/component_binding.rb +1 -1
- data/lib/volt/page/bindings/content_binding.rb +2 -2
- data/lib/volt/page/bindings/each_binding.rb +25 -21
- data/lib/volt/page/bindings/event_binding.rb +4 -6
- data/lib/volt/page/bindings/if_binding.rb +4 -5
- data/lib/volt/page/bindings/template_binding.rb +4 -4
- data/lib/volt/page/channel.rb +0 -1
- data/lib/volt/page/document.rb +7 -0
- data/lib/volt/page/page.rb +4 -4
- data/lib/volt/page/reactive_template.rb +2 -2
- data/lib/volt/page/targets/dom_section.rb +5 -0
- data/lib/volt/page/tasks.rb +10 -40
- data/lib/volt/page/template_renderer.rb +4 -4
- data/lib/volt/reactive/events.rb +14 -0
- data/lib/volt/reactive/reactive_array.rb +17 -7
- data/lib/volt/reactive/reactive_value.rb +65 -1
- data/lib/volt/server.rb +1 -1
- data/lib/volt/server/if_binding_setup.rb +3 -1
- data/lib/volt/server/socket_connection_handler.rb +7 -5
- data/lib/volt/server/template_parser.rb +7 -7
- data/lib/volt/tasks/dispatcher.rb +3 -0
- data/lib/volt/utils/ejson.rb +9 -0
- data/lib/volt/utils/generic_counting_pool.rb +44 -0
- data/lib/volt/utils/generic_pool.rb +88 -0
- data/spec/models/reactive_array_spec.rb +43 -0
- data/spec/models/reactive_generator_spec.rb +58 -0
- data/spec/models/reactive_value_spec.rb +6 -0
- data/spec/page/bindings/content_binding_spec.rb +36 -0
- data/spec/spec_helper.rb +13 -12
- data/spec/tasks/live_query_spec.rb +20 -0
- data/spec/tasks/query_tasks.rb +10 -0
- data/spec/tasks/query_tracker_spec.rb +120 -0
- data/spec/templates/template_binding_spec.rb +16 -10
- data/spec/utils/generic_counting_pool_spec.rb +36 -0
- data/spec/utils/generic_pool_spec.rb +50 -0
- metadata +29 -5
- data/app/volt/tasks/channel_tasks.rb +0 -55
- data/spec/tasks/channel_tasks_spec.rb +0 -74
@@ -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
|
data/lib/volt/models/model.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
121
|
+
@model.insert(index, new_model)
|
27
122
|
|
28
|
-
|
29
|
-
change_channel_connection('add', 'removed')
|
123
|
+
$loading_models = false
|
30
124
|
end
|
31
125
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
126
|
+
def remove(ids)
|
127
|
+
$loading_models = true
|
128
|
+
ids.each do |id|
|
129
|
+
puts "delete at: #{id} on #{@model.inspect}"
|
36
130
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
39
|
-
|
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.
|
113
|
-
|
76
|
+
def self.changed(model_id, data)
|
77
|
+
model = @@identity_map.lookup(model_id)
|
114
78
|
|
115
|
-
if
|
79
|
+
if model
|
116
80
|
data.each_pair do |key, value|
|
117
81
|
if key != '_id'
|
118
|
-
|
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.
|