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
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'volt/models'
|
2
|
+
|
3
|
+
describe ReactiveGenerator do
|
4
|
+
before do
|
5
|
+
@object = {
|
6
|
+
name: ReactiveValue.new('bob'),
|
7
|
+
location: {
|
8
|
+
town: ReactiveValue.new('Bozeman'),
|
9
|
+
places: [
|
10
|
+
ReactiveValue.new('The Garage'),
|
11
|
+
ReactiveValue.new('Ale Works')
|
12
|
+
]
|
13
|
+
}
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should find all reactive values in any object" do
|
18
|
+
values = ReactiveGenerator.find_reactives(@object)
|
19
|
+
|
20
|
+
expect(values.map(&:cur)).to eq(['bob', 'Bozeman', 'The Garage', 'Ale Works'])
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return a reactive value that changes whenever a child reactive value changes" do
|
24
|
+
values = ReactiveGenerator.from_hash(@object)
|
25
|
+
|
26
|
+
count = 0
|
27
|
+
values.on('changed') { count += 1 }
|
28
|
+
expect(count).to eq(0)
|
29
|
+
|
30
|
+
@object[:name].cur = 'jim'
|
31
|
+
|
32
|
+
expect(count).to eq(1)
|
33
|
+
|
34
|
+
@object[:location][:places].last.cur = 'Starkies'
|
35
|
+
expect(count).to eq(2)
|
36
|
+
|
37
|
+
expect(values.to_h).to eq({
|
38
|
+
name: 'jim',
|
39
|
+
location: {
|
40
|
+
town: 'Bozeman',
|
41
|
+
places: [
|
42
|
+
'The Garage',
|
43
|
+
'Starkies'
|
44
|
+
]
|
45
|
+
}
|
46
|
+
})
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should optionally return a normal hash if there are no child reactive values" do
|
50
|
+
values = ReactiveGenerator.from_hash({name: 'bob'})
|
51
|
+
expect(values.reactive?).to eq(true)
|
52
|
+
expect(values.is_a?(Hash)).to eq(false)
|
53
|
+
|
54
|
+
values = ReactiveGenerator.from_hash({name: 'bob'}, true)
|
55
|
+
expect(values.reactive?).to eq(false)
|
56
|
+
expect(values.is_a?(Hash)).to eq(true)
|
57
|
+
end
|
58
|
+
end
|
@@ -344,4 +344,10 @@ describe ReactiveValue do
|
|
344
344
|
expect(count).to eq(2)
|
345
345
|
end
|
346
346
|
end
|
347
|
+
|
348
|
+
it "should give you back the object without any ReactiveValue's if you call .deep_cur on it." do
|
349
|
+
a = ReactiveValue.new({_names: [ReactiveValue.new('bob'), ReactiveValue.new('jim')]})
|
350
|
+
|
351
|
+
expect(a.deep_cur).to eq({_names: ['bob', 'jim']})
|
352
|
+
end
|
347
353
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'volt/page/bindings/content_binding'
|
3
|
+
require 'volt/page/targets/attribute_target'
|
4
|
+
require 'volt/page/template_renderer'
|
5
|
+
|
6
|
+
|
7
|
+
describe ContentBinding do
|
8
|
+
it "should render the content in a content binding" do
|
9
|
+
dom = AttributeTarget.new(0)
|
10
|
+
context = {:name => 'jimmy'}
|
11
|
+
binding = ContentBinding.new(nil, dom, context, 0, Proc.new { self[:name] })
|
12
|
+
|
13
|
+
expect(dom.to_html).to eq('jimmy')
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should render with a template" do
|
17
|
+
context = {:name => 'jimmy'}
|
18
|
+
binding = lambda {|page, target, context, id| ContentBinding.new(page, target, context, id, Proc.new { self[:name] }) }
|
19
|
+
|
20
|
+
templates = {
|
21
|
+
'home/index' => {
|
22
|
+
'html' => 'hello <!-- $1 --><!-- $/1 -->',
|
23
|
+
'bindings' => {1 => [binding]}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
page = double('page')
|
28
|
+
expect(page).to receive(:templates).and_return(templates)
|
29
|
+
|
30
|
+
dom = AttributeTarget.new(0)
|
31
|
+
|
32
|
+
TemplateRenderer.new(page, dom, context, 'main', 'home/index')
|
33
|
+
|
34
|
+
expect(dom.to_html).to eq('hello jimmy')
|
35
|
+
end
|
36
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -5,16 +5,17 @@
|
|
5
5
|
#
|
6
6
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
7
|
|
8
|
-
|
9
|
-
config.treat_symbols_as_metadata_keys_with_true_values = true
|
10
|
-
config.run_all_when_everything_filtered = true
|
11
|
-
config.filter_run :focus
|
8
|
+
require 'volt'
|
12
9
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
10
|
+
if RUBY_PLATFORM != 'opal'
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.run_all_when_everything_filtered = true
|
13
|
+
config.filter_run :focus
|
14
|
+
|
15
|
+
# Run specs in random order to surface order dependencies. If you find an
|
16
|
+
# order dependency and want to debug it, you can fix the order by providing
|
17
|
+
# the seed, which is printed after each run.
|
18
|
+
# --seed 1234
|
19
|
+
config.order = 'random'
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
if RUBY_PLATFORM != 'opal'
|
2
|
+
describe "LiveQuery" do
|
3
|
+
before do
|
4
|
+
load File.join(File.dirname(__FILE__), "../../app/volt/tasks/live_query/live_query.rb")
|
5
|
+
end
|
6
|
+
|
7
|
+
it "should run a query" do
|
8
|
+
pool = double('pool')
|
9
|
+
data_store = double('data store')
|
10
|
+
|
11
|
+
expect(data_store).to receive(:query).with('_items', {}).and_return([
|
12
|
+
{'_id' => 0, '_name' => 'one'}
|
13
|
+
])
|
14
|
+
|
15
|
+
live_query = LiveQuery.new(pool, data_store, '_items', {})
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
if RUBY_PLATFORM != 'opal'
|
2
|
+
describe "LiveQuery" do
|
3
|
+
before do
|
4
|
+
load File.join(File.dirname(__FILE__), "../../app/volt/tasks/live_query/live_query.rb")
|
5
|
+
end
|
6
|
+
|
7
|
+
# LiveQueryStub behaves as the front-end would with the changes to a
|
8
|
+
# live query. Instead of passing changes to the models to the front
|
9
|
+
# end, the changes are applied locally, then can be checked to see if
|
10
|
+
# the correct transitions have taken place.
|
11
|
+
class LiveQueryStub
|
12
|
+
attr_reader :collection, :query, :items
|
13
|
+
def initialize
|
14
|
+
@collection = '_items'
|
15
|
+
@query = {}
|
16
|
+
@items = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def notify_removed(ids, skip_channel)
|
20
|
+
# Remove the id's that need to be removed
|
21
|
+
@items.reject! {|item| ids.include?(item['_id']) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def notify_added(index, data, skip_channel)
|
25
|
+
@items.insert(index, data)
|
26
|
+
end
|
27
|
+
|
28
|
+
def notify_moved(id, index, skip_channel)
|
29
|
+
item = @items.find {|item| item['_id'] == id }
|
30
|
+
@items.delete(item)
|
31
|
+
|
32
|
+
@items.insert(index, item)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
before do
|
37
|
+
# Setup a live query stub
|
38
|
+
@live_query = LiveQueryStub.new
|
39
|
+
data_store = double('data store')
|
40
|
+
|
41
|
+
# return an empty collection
|
42
|
+
@items = []
|
43
|
+
expect(data_store).to receive(:query).at_least(:once) { @items.dup }
|
44
|
+
|
45
|
+
@query_tracker = QueryTracker.new(@live_query, data_store)
|
46
|
+
@query_tracker.run
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should add items" do
|
51
|
+
@items = [
|
52
|
+
{'_id' => 1, '_name' => 'one'}
|
53
|
+
]
|
54
|
+
|
55
|
+
expect(@live_query.items).to eq([])
|
56
|
+
|
57
|
+
@query_tracker.run
|
58
|
+
|
59
|
+
expect(@live_query.items).to eq(@items)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should remove items" do
|
63
|
+
@items = [
|
64
|
+
{'_id' => 1, '_name' => 'one'},
|
65
|
+
{'_id' => 2, '_name' => 'two'}
|
66
|
+
]
|
67
|
+
@query_tracker.run
|
68
|
+
expect(@live_query.items).to eq(@items)
|
69
|
+
|
70
|
+
@items = [
|
71
|
+
{'_id' => 2, '_name' => 'two'}
|
72
|
+
]
|
73
|
+
@query_tracker.run
|
74
|
+
expect(@live_query.items).to eq(@items)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should move items" do
|
78
|
+
@items = [
|
79
|
+
{'_id' => 1, '_name' => 'one'},
|
80
|
+
{'_id' => 2, '_name' => 'two'},
|
81
|
+
{'_id' => 3, '_name' => 'three'}
|
82
|
+
]
|
83
|
+
@query_tracker.run
|
84
|
+
expect(@live_query.items).to eq(@items)
|
85
|
+
|
86
|
+
@items = [
|
87
|
+
{'_id' => 2, '_name' => 'two'},
|
88
|
+
{'_id' => 3, '_name' => 'three'},
|
89
|
+
{'_id' => 1, '_name' => 'one'}
|
90
|
+
]
|
91
|
+
@query_tracker.run
|
92
|
+
expect(@live_query.items).to eq(@items)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should handle complex transforms" do
|
96
|
+
@items = [
|
97
|
+
{'_id' => 1, '_name' => 'one'},
|
98
|
+
{'_id' => 2, '_name' => 'two'},
|
99
|
+
{'_id' => 3, '_name' => 'three'},
|
100
|
+
{'_id' => 4, '_name' => 'four'},
|
101
|
+
{'_id' => 5, '_name' => 'five'}
|
102
|
+
]
|
103
|
+
@query_tracker.run
|
104
|
+
expect(@live_query.items).to eq(@items)
|
105
|
+
|
106
|
+
@items = [
|
107
|
+
{'_id' => 7, '_name' => 'seven'},
|
108
|
+
{'_id' => 4, '_name' => 'four'},
|
109
|
+
{'_id' => 1, '_name' => 'one'},
|
110
|
+
{'_id' => 5, '_name' => 'five'},
|
111
|
+
{'_id' => 3, '_name' => 'three'},
|
112
|
+
{'_id' => 6, '_name' => 'five'}
|
113
|
+
]
|
114
|
+
@query_tracker.run
|
115
|
+
expect(@live_query.items).to eq(@items)
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
@@ -9,19 +9,25 @@ end
|
|
9
9
|
|
10
10
|
describe TemplateBinding do
|
11
11
|
before do
|
12
|
-
|
12
|
+
@page = double('page')
|
13
|
+
expect(@page).to receive(:templates).at_least(1).times.and_return { @templates }
|
13
14
|
|
14
15
|
# TODO: We should decouple things so we don't need to allocate
|
15
16
|
@template_binding = TemplateBinding.allocate
|
17
|
+
@template_binding.instance_variable_set('@page', @page)
|
16
18
|
@template_binding.setup_path('home/index/index')
|
17
19
|
end
|
18
20
|
|
21
|
+
def set_template(templates)
|
22
|
+
@page.instance_variable_set('@templates', templates)
|
23
|
+
end
|
24
|
+
|
19
25
|
after do
|
20
26
|
$page = nil
|
21
27
|
end
|
22
28
|
|
23
29
|
it "should lookup sub-templates within its own file" do
|
24
|
-
|
30
|
+
@templates = {
|
25
31
|
'home/index/blog/nav' => '',
|
26
32
|
'home/index/index/nav' => '',
|
27
33
|
}
|
@@ -30,7 +36,7 @@ describe TemplateBinding do
|
|
30
36
|
end
|
31
37
|
|
32
38
|
it "should lookup sub-templates within another local view" do
|
33
|
-
|
39
|
+
@templates = {
|
34
40
|
'home/index/blog/nav' => '',
|
35
41
|
'home/index/index/nav' => '',
|
36
42
|
}
|
@@ -39,7 +45,7 @@ describe TemplateBinding do
|
|
39
45
|
end
|
40
46
|
|
41
47
|
it "should lookup in another view" do
|
42
|
-
|
48
|
+
@templates = {
|
43
49
|
'home/index/nav/body' => '',
|
44
50
|
}
|
45
51
|
|
@@ -47,7 +53,7 @@ describe TemplateBinding do
|
|
47
53
|
end
|
48
54
|
|
49
55
|
it "should lookup in a controller" do
|
50
|
-
|
56
|
+
@templates = {
|
51
57
|
'home/nav/index/body' => ''
|
52
58
|
}
|
53
59
|
|
@@ -55,7 +61,7 @@ describe TemplateBinding do
|
|
55
61
|
end
|
56
62
|
|
57
63
|
it "should lookup in a controller/view" do
|
58
|
-
|
64
|
+
@templates = {
|
59
65
|
'home/blog/nav/body' => ''
|
60
66
|
}
|
61
67
|
|
@@ -63,7 +69,7 @@ describe TemplateBinding do
|
|
63
69
|
end
|
64
70
|
|
65
71
|
it "should lookup in a controller" do
|
66
|
-
|
72
|
+
@templates = {
|
67
73
|
'home/nav/index/body' => ''
|
68
74
|
}
|
69
75
|
|
@@ -71,7 +77,7 @@ describe TemplateBinding do
|
|
71
77
|
end
|
72
78
|
|
73
79
|
it "should lookup in a component" do
|
74
|
-
|
80
|
+
@templates = {
|
75
81
|
'nav/index/index/body' => ''
|
76
82
|
}
|
77
83
|
|
@@ -79,7 +85,7 @@ describe TemplateBinding do
|
|
79
85
|
end
|
80
86
|
|
81
87
|
it "should lookup in a component/controller/view" do
|
82
|
-
|
88
|
+
@templates = {
|
83
89
|
'nav/index/index/body' => '',
|
84
90
|
'auth/login/new/body' => ''
|
85
91
|
}
|
@@ -88,7 +94,7 @@ describe TemplateBinding do
|
|
88
94
|
end
|
89
95
|
|
90
96
|
it "should let you force a sub template" do
|
91
|
-
|
97
|
+
@templates = {
|
92
98
|
'nav/index/index/title' => '',
|
93
99
|
'auth/login/new/title' => ''
|
94
100
|
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'volt/utils/generic_counting_pool'
|
2
|
+
|
3
|
+
class CountingPoolTest < GenericCountingPool
|
4
|
+
def create(id, name=nil)
|
5
|
+
return Object.new
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe GenericCountingPool do
|
10
|
+
before do
|
11
|
+
@count_pool = CountingPoolTest.new
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should lookup and retrieve" do
|
15
|
+
item1 = @count_pool.find('one')
|
16
|
+
|
17
|
+
item2 = @count_pool.find('one')
|
18
|
+
item3 = @count_pool.find('two')
|
19
|
+
|
20
|
+
expect(item1).to eq(item2)
|
21
|
+
expect(item2).to_not eq(item3)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should only remove items when the same number have been removed as have been added" do
|
25
|
+
item1 = @count_pool.find('_items', 'one')
|
26
|
+
item2 = @count_pool.find('_items', 'one')
|
27
|
+
expect(@count_pool.instance_variable_get('@pool')).to_not eq({})
|
28
|
+
|
29
|
+
@count_pool.remove('_items', 'one')
|
30
|
+
expect(@count_pool.instance_variable_get('@pool')).to_not eq({})
|
31
|
+
|
32
|
+
@count_pool.remove('_items', 'one')
|
33
|
+
expect(@count_pool.instance_variable_get('@pool')).to eq({})
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'volt/utils/generic_pool'
|
2
|
+
|
3
|
+
class PoolTest < GenericPool
|
4
|
+
def create(collection, query, other=nil)
|
5
|
+
return Object.new
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe GenericPool do
|
10
|
+
|
11
|
+
before do
|
12
|
+
@pool_test = PoolTest.new
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should insert nested for fast lookup at a path" do
|
16
|
+
item1 = @pool_test.lookup('_items', 'one')
|
17
|
+
expect(@pool_test.instance_variable_get('@pool')).to eq({'_items' => {'one' => item1}})
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should retrieve the same item both times" do
|
21
|
+
item1 = @pool_test.lookup('_items', {})
|
22
|
+
item2 = @pool_test.lookup('_items', {})
|
23
|
+
expect(item1.object_id).to eq(item2.object_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should recreate after being removed" do
|
27
|
+
item1 = @pool_test.lookup('_items', {})
|
28
|
+
item2 = @pool_test.lookup('_items', {})
|
29
|
+
expect(item1.object_id).to eq(item2.object_id)
|
30
|
+
|
31
|
+
@pool_test.remove('_items', {})
|
32
|
+
item3 = @pool_test.lookup('_items', {})
|
33
|
+
expect(item3.object_id).to_not eq(item2.object_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should remove all of the way down" do
|
37
|
+
@pool_test.instance_variable_set('@pool', {:name => {:ok => true}, :yep => true})
|
38
|
+
|
39
|
+
@pool_test.remove(:name, :ok)
|
40
|
+
|
41
|
+
expect(@pool_test.instance_variable_get('@pool')).to eq({:yep => true})
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should lookup all items at a path" do
|
45
|
+
item1 = @pool_test.lookup('_items', '_some', {:name => 'bob'})
|
46
|
+
item2 = @pool_test.lookup('_items', '_some', {:name => 'jim'})
|
47
|
+
|
48
|
+
expect(@pool_test.lookup_all('_items', '_some')).to eq([item1, item2])
|
49
|
+
end
|
50
|
+
end
|