volt 0.8.8 → 0.8.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/Readme.md +2 -1036
  4. data/VERSION +1 -1
  5. data/app/volt/models/user.rb +5 -0
  6. data/app/volt/tasks/query_tasks.rb +3 -3
  7. data/app/volt/tasks/store_tasks.rb +17 -15
  8. data/app/volt/tasks/user_tasks.rb +6 -0
  9. data/lib/volt/cli.rb +9 -0
  10. data/lib/volt/extra_core/string.rb +10 -2
  11. data/lib/volt/models/array_model.rb +0 -1
  12. data/lib/volt/models/model.rb +45 -28
  13. data/lib/volt/models/model_hash_behaviour.rb +16 -4
  14. data/lib/volt/models/model_helpers.rb +4 -2
  15. data/lib/volt/models/model_wrapper.rb +2 -1
  16. data/lib/volt/models/persistors/array_store.rb +6 -6
  17. data/lib/volt/models/persistors/local_store.rb +1 -1
  18. data/lib/volt/models/persistors/model_store.rb +3 -3
  19. data/lib/volt/models/persistors/query/query_listener.rb +6 -4
  20. data/lib/volt/models/persistors/query/query_listener_pool.rb +9 -0
  21. data/lib/volt/models/persistors/store.rb +1 -0
  22. data/lib/volt/models/url.rb +20 -10
  23. data/lib/volt/page/bindings/template_binding.rb +7 -1
  24. data/lib/volt/page/page.rb +3 -3
  25. data/lib/volt/page/tasks.rb +20 -19
  26. data/lib/volt/reactive/reactive_array.rb +44 -0
  27. data/lib/volt/router/routes.rb +5 -0
  28. data/lib/volt/server.rb +3 -1
  29. data/lib/volt/server/component_templates.rb +7 -1
  30. data/lib/volt/server/html_parser/attribute_scope.rb +1 -1
  31. data/lib/volt/server/rack/component_paths.rb +1 -10
  32. data/lib/volt/server/socket_connection_handler.rb +3 -0
  33. data/lib/volt/tasks/dispatcher.rb +3 -7
  34. data/lib/volt/tasks/task_handler.rb +41 -0
  35. data/spec/integration/bindings_spec.rb +1 -1
  36. data/spec/models/model_spec.rb +32 -32
  37. data/spec/models/persistors/params_spec.rb +1 -1
  38. data/spec/router/routes_spec.rb +4 -4
  39. data/templates/model/model.rb.tt +3 -0
  40. metadata +6 -2
@@ -15,20 +15,22 @@ class QueryListener
15
15
 
16
16
  def add_listener
17
17
  @listening = true
18
- @tasks.call('QueryTasks', 'add_listener', @collection, @query) do |results, errors|
19
- # puts "Query Tasks: #{results.inspect} - #{@stores.inspect} - #{self.inspect}"
18
+ QueryTasks.add_listener(@collection, @query).then do |ret|
19
+ results, errors = ret
20
+
20
21
  # When the initial data comes back, add it into the stores.
21
22
  @stores.dup.each do |store|
22
23
  # Clear if there are existing items
23
24
  store.model.clear if store.model.size > 0
24
25
 
25
26
  results.each do |index, data|
26
- # puts "ADD: #{index} - #{data.inspect}"
27
27
  store.add(index, data)
28
28
  end
29
29
 
30
30
  store.change_state_to(:loaded)
31
31
  end
32
+ end.fail do |err|
33
+ puts "Err: #{err.inspect}"
32
34
  end
33
35
  end
34
36
 
@@ -60,7 +62,7 @@ class QueryListener
60
62
  # Stop listening
61
63
  if @listening
62
64
  @listening = false
63
- @tasks.call('QueryTasks', 'remove_listener', @collection, @query)
65
+ QueryTasks.remove_listener(@collection, @query)
64
66
  end
65
67
  end
66
68
  end
@@ -6,4 +6,13 @@ require 'volt/models/persistors/query/query_listener'
6
6
  # query in different places. This makes it so we only need to track a
7
7
  # single query at once. Data updates will only be sent once as well.
8
8
  class QueryListenerPool < GenericPool
9
+ def print
10
+ puts "--- Running Queries ---"
11
+
12
+ @pool.each_pair do |table,query_hash|
13
+ query_hash.keys.each do |query|
14
+ puts "#{table}: #{query.inspect}"
15
+ end
16
+ end
17
+ end
9
18
  end
@@ -28,6 +28,7 @@ module Persistors
28
28
  else
29
29
  model = @model.new_model(nil, options)
30
30
 
31
+ # TODO: Might not need to assign this
31
32
  @model.attributes ||= {}
32
33
  @model.attributes[method_name] = model
33
34
  end
@@ -69,22 +69,30 @@ class URL
69
69
 
70
70
  path, params = @router.params_to_url(@params.to_h)
71
71
 
72
+ self.path = path if path
73
+
72
74
  new_url = "#{scheme}://#{host_with_port}#{(path || self.path).chomp('/')}"
73
75
 
76
+ params_str = ''
74
77
  unless params.empty?
75
- new_url += '?'
76
78
  query_parts = []
77
79
  nested_params_hash(params).each_pair do |key,value|
78
80
  # remove the _ from the front
79
81
  value = `encodeURI(value)`
80
- query_parts << "#{key}=#{value}"
82
+ query_parts << "#{key[1..-1]}=#{value}"
81
83
  end
82
84
 
83
- new_url += query_parts.join('&')
85
+ if query_parts.size > 0
86
+ self.query = query_parts.join('&')
87
+ new_url += '?' + self.query
88
+ end
84
89
  end
85
90
 
86
91
  frag = self.fragment
87
- new_url += '#' + frag if frag.present?
92
+ if frag.present?
93
+ self.fragment = frag
94
+ new_url += '#' + frag
95
+ end
88
96
 
89
97
  return new_url
90
98
  end
@@ -231,7 +239,7 @@ class URL
231
239
  def query_key(path)
232
240
  i = 0
233
241
  path.map do |v|
234
- v = v[1..-1]
242
+ # v = v[1..-1]
235
243
  i += 1
236
244
  if i != 1
237
245
  "[#{v}]"
@@ -245,11 +253,13 @@ class URL
245
253
  results = {}
246
254
 
247
255
  params.each_pair do |key,value|
248
- if value.respond_to?(:persistor) && value.persistor && value.persistor.is_a?(Persistors::Params)
249
- # TODO: Should be a param
250
- results.merge!(nested_params_hash(value, path + [key]))
251
- else
252
- results[query_key(path + [key])] = value
256
+ unless value.nil?
257
+ if value.respond_to?(:persistor) && value.persistor && value.persistor.is_a?(Persistors::Params)
258
+ # TODO: Should be a param
259
+ results.merge!(nested_params_hash(value, path + [key]))
260
+ else
261
+ results[query_key(path + [key])] = value
262
+ end
253
263
  end
254
264
  end
255
265
 
@@ -14,7 +14,13 @@ class TemplateBinding < BaseBinding
14
14
  @getter = getter
15
15
 
16
16
  # Run the initial render
17
- @computation = -> { update(*@context.instance_eval(&getter)) }.watch!
17
+ @computation = -> do
18
+ # Don't try to render if this has been removed
19
+ if @context
20
+ # Render
21
+ update(*@context.instance_eval(&getter))
22
+ end
23
+ end.watch!
18
24
  end
19
25
 
20
26
  def setup_path(binding_in_path)
@@ -4,6 +4,7 @@ if RUBY_PLATFORM == 'opal'
4
4
  end
5
5
  require 'volt/models'
6
6
  require 'volt/controllers/model_controller'
7
+ require 'volt/tasks/task_handler'
7
8
  require 'volt/page/bindings/attribute_binding'
8
9
  require 'volt/page/bindings/content_binding'
9
10
  require 'volt/page/bindings/each_binding'
@@ -29,7 +30,6 @@ require 'volt/benchmark/benchmark'
29
30
  require 'volt/page/tasks'
30
31
 
31
32
 
32
-
33
33
  class Page
34
34
  attr_reader :url, :params, :page, :templates, :routes, :events, :model_classes
35
35
 
@@ -138,7 +138,7 @@ class Page
138
138
  end
139
139
 
140
140
  def add_model(model_name)
141
- model_name = model_name.camelize
141
+ model_name = model_name.camelize.to_sym
142
142
  @model_classes[model_name] = Object.const_get(model_name)
143
143
  end
144
144
 
@@ -189,7 +189,7 @@ class Page
189
189
  `sessionStorage.removeItem('___page');`
190
190
 
191
191
  JSON.parse(page_obj_str).each_pair do |key, value|
192
- self.page.send(:"#{key}=", value)
192
+ self.page.send(:"_#{key}=", value)
193
193
  end
194
194
  `}`
195
195
  end
@@ -3,36 +3,36 @@
3
3
  class Tasks
4
4
  def initialize(page)
5
5
  @page = page
6
- @callback_id = 0
7
- @callbacks = {}
6
+ @promise_id = 0
7
+ @promises = {}
8
8
 
9
9
  page.channel.on('message') do |*args|
10
10
  received_message(*args)
11
11
  end
12
12
  end
13
13
 
14
- def call(class_name, method_name, *args, &callback)
15
- if callback
16
- callback_id = @callback_id
17
- @callback_id += 1
14
+ def call(class_name, method_name, *args)
15
+ promise_id = @promise_id
16
+ @promise_id += 1
18
17
 
19
- # Track the callback
20
- # TODO: Timeout on these callbacks
21
- @callbacks[callback_id] = callback
22
- else
23
- callback_id = nil
24
- end
18
+ # Track the callback
19
+ promise = Promise.new
20
+ @promises[promise_id] = promise
21
+
22
+ # TODO: Timeout on these callbacks
23
+
24
+ @page.channel.send_message([promise_id, class_name, method_name, *args])
25
25
 
26
- @page.channel.send_message([callback_id, class_name, method_name, *args])
26
+ promise
27
27
  end
28
28
 
29
29
 
30
- def received_message(name, callback_id, *args)
30
+ def received_message(name, promise_id, *args)
31
31
  case name
32
32
  when 'added', 'removed', 'updated', 'changed'
33
33
  notify_query(name, *args)
34
34
  when 'response'
35
- response(callback_id, *args)
35
+ response(promise_id, *args)
36
36
  when 'reload'
37
37
  reload
38
38
  end
@@ -40,15 +40,16 @@ class Tasks
40
40
 
41
41
  # When a request is sent to the backend, it can attach a callback,
42
42
  # this is called from the backend to pass to the callback.
43
- def response(callback_id, result, error)
44
- callback = @callbacks.delete(callback_id)
43
+ def response(promise_id, result, error)
44
+ promise = @promises.delete(promise_id)
45
45
 
46
- if callback
46
+ if promise
47
47
  if error
48
48
  # TODO: full error handling
49
49
  puts "Task Response: #{error.inspect}"
50
+ promise.reject(error)
50
51
  else
51
- callback.call(result)
52
+ promise.resolve(result)
52
53
  end
53
54
  end
54
55
  end
@@ -39,6 +39,50 @@ class ReactiveArray# < Array
39
39
  end
40
40
  end
41
41
 
42
+ def select
43
+ result = []
44
+ size.times do |index|
45
+ val = self[index]
46
+ if yield(val).true?
47
+ result << val
48
+ end
49
+ end
50
+
51
+ return result
52
+ end
53
+
54
+ def any?
55
+ if block_given?
56
+ size.times do |index|
57
+ val = self[index]
58
+
59
+ if yield(val).true?
60
+ return true
61
+ end
62
+ end
63
+
64
+ return false
65
+ else
66
+ return @array.any?
67
+ end
68
+ end
69
+
70
+ def all?
71
+ if block_given?
72
+ size.times do |index|
73
+ val = self[index]
74
+
75
+ if !yield(val).true?
76
+ return false
77
+ end
78
+ end
79
+
80
+ return true
81
+ else
82
+ return @array.all?
83
+ end
84
+ end
85
+
42
86
  # TODO: Handle a range
43
87
  def [](index)
44
88
  # Handle a negative index
@@ -74,6 +74,11 @@ class Routes
74
74
  #
75
75
  # returns the url and new params, or nil, nil if no match is found.
76
76
  def params_to_url(test_params)
77
+ # Add in underscores
78
+ test_params = test_params.each_with_object({}) do |(k,v), obj|
79
+ obj[:"_#{k}"] = v
80
+ end
81
+
77
82
  @param_matches.each do |param_matcher|
78
83
  # TODO: Maybe a deep dup?
79
84
  result, new_params = check_params_match(test_params.dup, param_matcher[0])
data/lib/volt/server.rb CHANGED
@@ -18,6 +18,8 @@ require 'listen'
18
18
 
19
19
  require 'volt'
20
20
  require 'volt/boot'
21
+ require 'volt/tasks/dispatcher'
22
+ require 'volt/tasks/task_handler'
21
23
  require 'volt/server/component_handler'
22
24
  if RUBY_PLATFORM != 'java'
23
25
  require 'volt/server/socket_connection_handler'
@@ -25,7 +27,7 @@ end
25
27
  require 'volt/server/rack/component_paths'
26
28
  require 'volt/server/rack/index_files'
27
29
  require 'volt/server/rack/opal_files'
28
- require 'volt/tasks/dispatcher'
30
+ require 'volt/page/page'
29
31
 
30
32
  module Rack
31
33
  # TODO: For some reason in Rack (or maybe thin), 304 headers close
@@ -13,7 +13,7 @@ class ComponentTemplates
13
13
  code = generate_view_code
14
14
  if @client
15
15
  # On the backend, we just need the views
16
- code << generate_controller_code + generate_model_code + generate_routes_code
16
+ code << generate_controller_code + generate_model_code + generate_routes_code + generate_tasks_code
17
17
  end
18
18
 
19
19
  return code
@@ -96,4 +96,10 @@ class ComponentTemplates
96
96
 
97
97
  return code
98
98
  end
99
+
100
+ def generate_tasks_code
101
+ return TaskHandler.known_handlers.map do |handler|
102
+ "class #{handler.name} < TaskHandler; end"
103
+ end.join "\n"
104
+ end
99
105
  end
@@ -89,7 +89,7 @@ module AttributeScope
89
89
  raise "The content of text area's can not be bound to multiple bindings."
90
90
  else
91
91
  # Multiple values can not be passed to value or checked attributes.
92
- raise "Multiple bindings can not be passed to a #{attribute_name} binding."
92
+ raise "Multiple bindings can not be passed to a #{attribute_name} binding: #{parts.inspect}"
93
93
  end
94
94
  end
95
95
  end
@@ -56,7 +56,7 @@ class ComponentPaths
56
56
  $LOAD_PATH.unshift(app_folder)
57
57
 
58
58
  # Sort so we get consistent load order across platforms
59
- Dir["#{app_folder}/*/{controllers,models}/*.rb"].sort.each do |ruby_file|
59
+ Dir["#{app_folder}/*/{controllers,models,tasks}/*.rb"].each do |ruby_file|
60
60
  path = ruby_file.gsub(/^#{app_folder}\//, '')[0..-4]
61
61
  require(path)
62
62
  end
@@ -70,15 +70,6 @@ class ComponentPaths
70
70
  end
71
71
  end
72
72
  end
73
-
74
- # add each tasks folder directly
75
- components.sort.each do |name,component_folders|
76
- component_folders.sort.each do |component_folder|
77
- Dir["#{component_folder}/tasks"].sort.each do |tasks_folder|
78
- $LOAD_PATH.unshift(tasks_folder)
79
- end
80
- end
81
- end
82
73
  end
83
74
 
84
75
  # Returns the path for a specific component
@@ -48,6 +48,9 @@ class SocketConnectionHandler < SockJS::Session
48
48
  send(str)
49
49
  rescue MetaState::WrongStateError => e
50
50
  puts "Tried to send to closed connection: #{e.inspect}"
51
+
52
+ # Mark this channel as closed
53
+ closed
51
54
  end
52
55
  end
53
56
 
@@ -5,14 +5,10 @@ class Dispatcher
5
5
  def dispatch(channel, message)
6
6
  callback_id, class_name, method_name, *args = message
7
7
 
8
- # TODO: Think about security?
9
- if class_name[/Tasks$/] && !class_name['::']
10
- # TODO: Improve error on a class we don't have
11
- require(class_name.underscore)
12
-
13
- # Get the class
14
- klass = Object.send(:const_get, class_name)
8
+ # Get the class
9
+ klass = Object.send(:const_get, class_name)
15
10
 
11
+ if klass.ancestors.include?(TaskHandler)
16
12
  # Init and send the method
17
13
  begin
18
14
  result = klass.new(channel, self).send(method_name, *args)
@@ -0,0 +1,41 @@
1
+ class TaskHandler
2
+ if RUBY_PLATFORM == 'opal'
3
+ # On the front-end we setup a proxy class to the backend that returns
4
+ # promises for all calls.
5
+ def self.method_missing(name, *args, &block)
6
+ $page.tasks.call(self.name, name, *args, &block)
7
+ end
8
+ else
9
+ def initialize(channel=nil, dispatcher=nil)
10
+ @channel = channel
11
+ @dispatcher = dispatcher
12
+ end
13
+
14
+ def self.inherited(subclass)
15
+ @subclasses ||= []
16
+ @subclasses << subclass
17
+ end
18
+
19
+ def self.known_handlers
20
+ @subclasses ||= []
21
+ end
22
+
23
+ # On the backend, we proxy all class methods like we would
24
+ # on the front-end. This returns promises.
25
+ def self.method_missing(name, *args, &block)
26
+ promise = Promise.new
27
+
28
+ begin
29
+ result = self.new(nil, nil).send(name, *args, &block)
30
+
31
+ promise.resolve(result)
32
+ rescue => e
33
+ puts "Task Error: #{e.inspect}"
34
+ puts e.backtrace
35
+ promise.reject(e)
36
+ end
37
+
38
+ return promise
39
+ end
40
+ end
41
+ end