volt 0.8.8 → 0.8.9

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