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