hyper-operation 1.0.alpha1.2 → 1.0.alpha1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.travis.yml +1 -0
  4. data/hyper-operation.gemspec +6 -4
  5. data/lib/hyper-operation.rb +4 -1
  6. data/lib/hyper-operation/api.rb +6 -2
  7. data/lib/hyper-operation/async_sleep.rb +23 -0
  8. data/lib/hyper-operation/exception.rb +29 -3
  9. data/lib/hyper-operation/promise.rb +32 -2
  10. data/lib/hyper-operation/railway/dispatcher.rb +0 -1
  11. data/lib/hyper-operation/railway/run.rb +57 -48
  12. data/lib/hyper-operation/railway/validations.rb +9 -2
  13. data/lib/hyper-operation/server_op.rb +31 -9
  14. data/lib/hyper-operation/transport/client_drivers.rb +45 -11
  15. data/lib/hyper-operation/transport/connection.rb +58 -136
  16. data/lib/hyper-operation/transport/connection_adapter/active_record.rb +113 -0
  17. data/lib/hyper-operation/transport/connection_adapter/active_record/auto_create.rb +26 -0
  18. data/lib/hyper-operation/transport/connection_adapter/active_record/connection.rb +47 -0
  19. data/lib/hyper-operation/transport/connection_adapter/active_record/queued_message.rb +42 -0
  20. data/lib/hyper-operation/transport/connection_adapter/redis.rb +94 -0
  21. data/lib/hyper-operation/transport/connection_adapter/redis/connection.rb +85 -0
  22. data/lib/hyper-operation/transport/connection_adapter/redis/queued_message.rb +34 -0
  23. data/lib/hyper-operation/transport/connection_adapter/redis/redis_record.rb +158 -0
  24. data/lib/hyper-operation/transport/hyperstack.rb +15 -2
  25. data/lib/hyper-operation/transport/hyperstack_controller.rb +6 -2
  26. data/lib/hyper-operation/transport/policy.rb +16 -26
  27. data/lib/hyper-operation/transport/policy_diagnostics.rb +106 -0
  28. data/lib/hyper-operation/version.rb +1 -1
  29. metadata +79 -38
  30. data/Gemfile.lock +0 -385
  31. data/lib/hyper-operation/delay_and_interval.rb +0 -9
@@ -13,14 +13,18 @@ module Hyperstack
13
13
  hash = serialize_params(hash)
14
14
  Hyperstack::HTTP.post(
15
15
  "#{`window.HyperstackEnginePath`}/execute_remote",
16
- payload: {json: {operation: name, params: hash}.to_json},
17
- headers: {'X-CSRF-Token' => Hyperstack::ClientDrivers.opts[:form_authenticity_token] }
16
+ payload: {hyperstack_secured_json: {operation: name, params: hash}.to_json},
17
+ headers: headers.merge('X-CSRF-Token' => Hyperstack::ClientDrivers.opts[:form_authenticity_token])
18
18
  )
19
19
  .then do |response|
20
20
  deserialize_response response.json[:response]
21
21
  end
22
22
  .fail do |response|
23
- Exception.new response.json[:error]
23
+ begin
24
+ const_get(response.json[:error_class]).new(response.json[:error])
25
+ rescue
26
+ Exception.new response.json[:error]
27
+ end
24
28
  end
25
29
  end
26
30
  elsif on_opal_server?
@@ -74,7 +78,7 @@ module Hyperstack
74
78
  if _Railway.params_wrapper.method_defined?(:controller)
75
79
  params[:controller] = controller
76
80
  elsif !_Railway.params_wrapper.method_defined?(security_param)
77
- raise AccessViolation
81
+ raise AccessViolation.new(:remote_access_not_allowed)
78
82
  end
79
83
  run(deserialize_params(params))
80
84
  .then { |r| return { json: { response: serialize_response(r) } } }
@@ -84,13 +88,27 @@ module Hyperstack
84
88
  handle_exception(e, operation, params)
85
89
  end
86
90
 
91
+ def status(e)
92
+ if e.is_a? AccessViolation
93
+ 403
94
+ elsif e.is_a? Operation::ValidationException
95
+ 400
96
+ else
97
+ 500
98
+ end
99
+ end
100
+
87
101
  def handle_exception(e, operation, params)
88
- if defined? ::Rails
89
- params.delete(:controller)
90
- ::Rails.logger.debug "\033[0;31;1mERROR: Hyperstack::ServerOp exception caught when running "\
91
- "#{operation} with params \"#{params}\": #{e}\033[0;30;21m"
102
+ if e.respond_to? :__hyperstack_on_error
103
+ params = params.to_h
104
+ message = []
105
+ message << Pastel.new.red("HYPERSTACK ERROR during #{operation} #{e.inspect}")
106
+ params.each { |param, value| message << " #{param} => #{value.inspect.truncate(120, separator: '...')}" }
107
+ message << "\n#{e.details}" if e.respond_to? :details
108
+ e.__hyperstack_on_error(operation, params, message.join("\n"))
92
109
  end
93
- { json: { error: e }, status: 500 }
110
+
111
+ { json: { error_class: e.class.to_s, error: e}, status: status(e) }
94
112
  end
95
113
 
96
114
 
@@ -111,6 +129,10 @@ module Hyperstack
111
129
  promise.reject e
112
130
  end
113
131
 
132
+ def headers
133
+ {}
134
+ end
135
+
114
136
  def serialize_params(hash)
115
137
  hash
116
138
  end
@@ -6,6 +6,10 @@ module Hyperstack
6
6
  # We use ERB to determine the configuration and implement the appropriate
7
7
  # client interface to sync_change or sync_destroy
8
8
 
9
+ def self.anti_csrf_token
10
+ ClientDrivers.opts[:form_authenticity_token]
11
+ end
12
+
9
13
  class Application
10
14
  extend Component::IsomorphicHelpers::ClassMethods
11
15
 
@@ -31,16 +35,35 @@ module Hyperstack
31
35
 
32
36
 
33
37
  if RUBY_ENGINE == 'opal'
38
+ # Patch in a dummy copy of Model.load in case we are not using models
39
+ # this will be defined properly by hyper-model
40
+ module Model
41
+ def self.load
42
+ Promise.new.tap { |promise| promise.resolve(yield) }
43
+ end unless respond_to?(:load)
44
+ end
45
+
34
46
  def self.connect(*channels)
35
47
  channels.each do |channel|
36
48
  if channel.is_a? Class
37
49
  IncomingBroadcast.connect_to(channel.name)
38
50
  elsif channel.is_a?(String) || channel.is_a?(Array)
39
51
  IncomingBroadcast.connect_to(*channel)
40
- elsif channel.id
41
- IncomingBroadcast.connect_to(channel.class.name, channel.id)
52
+ elsif channel.respond_to?(:id)
53
+ Hyperstack::Model.load do
54
+ channel.id
55
+ end.then do |id|
56
+ raise "Hyperstack.connect cannot connect to #{channel.inspect}. "\
57
+ "The id is nil. This can be caused by connecting to a model "\
58
+ "that is not saved, or that does not exist." unless id
59
+ IncomingBroadcast.connect_to(channel.class.name, id)
60
+ end
42
61
  else
43
- raise "cannot connect to model before it has been saved"
62
+ raise "Hyperstack.connect cannot connect to #{channel.inspect}.\n"\
63
+ "Channels must be either a class, or a class name,\n"\
64
+ "a string in the form 'ClassName-id',\n"\
65
+ "an array in the form [class, id] or [class-name, id],\n"\
66
+ "or an object that responds to the id method with a non-nil value"
44
67
  end
45
68
  end
46
69
  end
@@ -61,19 +84,22 @@ module Hyperstack
61
84
 
62
85
  def self.add_connection(channel_name, id = nil)
63
86
  channel_string = "#{channel_name}#{'-'+id.to_s if id}"
87
+ return if open_channels.include? channel_string
64
88
  open_channels << channel_string
65
89
  channel_string
66
90
  end
67
91
 
68
92
  def self.connect_to(channel_name, id = nil)
69
93
  channel_string = add_connection(channel_name, id)
94
+ return unless channel_string # already connected!
70
95
  if ClientDrivers.opts[:transport] == :pusher
71
96
  channel = "#{ClientDrivers.opts[:channel]}-#{channel_string}"
72
97
  %x{
73
98
  var channel = #{ClientDrivers.opts[:pusher_api]}.subscribe(#{channel.gsub('::', '==')});
74
99
  channel.bind('dispatch', #{ClientDrivers.opts[:dispatch]})
75
- channel.bind('pusher:subscription_succeeded', #{lambda {ClientDrivers.get_queued_data("connect-to-transport", channel_string)}})
100
+ channel.bind('pusher:subscription_succeeded', #{->(*) { ClientDrivers.get_queued_data("connect-to-transport", channel_string)}})
76
101
  }
102
+ @pusher_dispatcher_registered = true
77
103
  elsif ClientDrivers.opts[:transport] == :action_cable
78
104
  channel = "#{ClientDrivers.opts[:channel]}-#{channel_string}"
79
105
  Hyperstack::HTTP.post(ClientDrivers.polling_path('action-cable-auth', channel), headers: { 'X-CSRF-Token' => ClientDrivers.opts[:form_authenticity_token] }).then do |response|
@@ -143,7 +169,7 @@ module Hyperstack
143
169
  config_hash = {
144
170
  transport: Hyperstack.transport,
145
171
  id: id,
146
- acting_user_id: (controller.acting_user && controller.acting_user.id),
172
+ acting_user_id: (controller.acting_user.respond_to?(:id) && controller.acting_user.id),
147
173
  env: ::Rails.env,
148
174
  client_logging: Hyperstack.client_logging,
149
175
  pusher_fake_js: pusher_fake_js,
@@ -159,12 +185,17 @@ module Hyperstack
159
185
  # not sure why the second check is needed. It happens in the test app
160
186
  route.app == Hyperstack::Engine or (route.app.respond_to?(:app) and route.app.app == Hyperstack::Engine)
161
187
  end
162
- raise 'Hyperstack::Engine mount point not found. Check your config/routes.rb file' unless path
163
- path = path.path.spec
164
- "<script type='text/javascript'>\n"\
165
- "window.HyperstackEnginePath = '#{path}';\n"\
166
- "window.HyperstackOpts = #{config_hash.to_json}\n"\
167
- "</script>\n"
188
+ if path
189
+ path = path.path.spec
190
+ "<script type='text/javascript'>\n"\
191
+ "window.HyperstackEnginePath = '#{path}';\n"\
192
+ "window.HyperstackOpts = #{config_hash.to_json}\n"\
193
+ "</script>\n"
194
+ else
195
+ "<script type='text/javascript'>\n"\
196
+ "window.HyperstackOpts = #{config_hash.to_json}\n"\
197
+ "</script>\n"
198
+ end
168
199
  end if RUBY_ENGINE != 'opal'
169
200
 
170
201
  class << self
@@ -218,6 +249,9 @@ module Hyperstack
218
249
 
219
250
  @opts = Hash.new(`window.HyperstackOpts`)
220
251
 
252
+ if opts[:transport] != :none && `typeof(window.HyperstackEnginePath) == 'undefined'`
253
+ raise "No hyperstack mount point found!\nCheck your Rails routes.rb file";
254
+ end
221
255
 
222
256
  if opts[:transport] == :pusher
223
257
 
@@ -1,173 +1,95 @@
1
- module Hyperstack
2
- module AutoCreate
3
- def table_exists?
4
- # works with both rails 4 and 5 without deprecation warnings
5
- if connection.respond_to?(:data_sources)
6
- connection.data_sources.include?(table_name)
7
- else
8
- connection.tables.include?(table_name)
9
- end
10
- end
11
-
12
- def needs_init?
13
- Hyperstack.transport != :none && Hyperstack.on_server? && !table_exists?
14
- end
15
-
16
- def create_table(*args, &block)
17
- connection.create_table(table_name, *args, &block) if needs_init?
18
- end
19
- end
20
-
21
- class Connection < ActiveRecord::Base
22
- class QueuedMessage < ActiveRecord::Base
23
-
24
- extend AutoCreate
25
-
26
- self.table_name = 'hyperstack_queued_messages'
27
-
28
- do_not_synchronize
29
-
30
- serialize :data
1
+ # frozen_string_literal: true
31
2
 
32
- belongs_to :hyperstack_connection,
33
- class_name: 'Hyperstack::Connection',
34
- foreign_key: 'connection_id'
35
-
36
- scope :for_session,
37
- ->(session) { joins(:hyperstack_connection).where('session = ?', session) }
38
-
39
- # For simplicity we use QueuedMessage with connection_id 0
40
- # to store the current path which is used by consoles to
41
- # communicate back to the server
42
-
43
- default_scope { where('connection_id IS NULL OR connection_id != 0') }
3
+ module Hyperstack
4
+ class Connection
5
+ class << self
6
+ attr_accessor :transport, :connection_adapter, :show_diagnostics
7
+
8
+ def adapter
9
+ adapter_name = Hyperstack.connection[:adapter].to_s
10
+ adapter_path = "hyper-operation/transport/connection_adapter/#{adapter_name}"
11
+
12
+ begin
13
+ require adapter_path
14
+ rescue LoadError => e
15
+ if e.path == adapter_path
16
+ raise e.class, "Could not load the '#{adapter_name}' adapter. Make sure the adapter is spelled correctly in your Hyperstack config, and the necessary gems are in your Gemfile.", e.backtrace
17
+
18
+ # Bubbled up from the adapter require. Prefix the exception message
19
+ # with some guidance about how to address it and reraise.
20
+ else
21
+ raise e.class, "Error loading the '#{adapter_name}' adapter. Missing a gem it depends on? #{e.message}", e.backtrace
22
+ end
23
+ end
44
24
 
45
- def self.root_path=(path)
46
- unscoped.find_or_create_by(connection_id: 0).update(data: path)
25
+ adapter_name = adapter_name.camelize
26
+ "Hyperstack::ConnectionAdapter::#{adapter_name}".constantize
47
27
  end
48
28
 
49
- def self.root_path
50
- unscoped.find_or_create_by(connection_id: 0).data
29
+ def build_tables
30
+ adapter.build_tables
51
31
  end
52
- end
53
-
54
- extend AutoCreate
55
32
 
56
- def self.build_tables
57
- create_table(force: :cascade) do |t|
58
- t.string :channel
59
- t.string :session
60
- t.datetime :created_at
61
- t.datetime :expires_at
62
- t.datetime :refresh_at
63
- end
64
- QueuedMessage.create_table(force: :cascade) do |t|
65
- t.text :data
66
- t.integer :connection_id
33
+ def build_tables?
34
+ adapter.respond_to?(:build_tables)
67
35
  end
68
- end
69
-
70
- do_not_synchronize
71
-
72
- self.table_name = 'hyperstack_connections'
73
-
74
- has_many :messages,
75
- foreign_key: 'connection_id',
76
- class_name: 'Hyperstack::Connection::QueuedMessage',
77
- dependent: :destroy
78
- scope :expired,
79
- -> { where('expires_at IS NOT NULL AND expires_at < ?', Time.zone.now) }
80
- scope :pending_for,
81
- ->(channel) { where(channel: channel).where('session IS NOT NULL') }
82
- scope :inactive,
83
- -> { where('session IS NULL AND refresh_at < ?', Time.zone.now) }
84
-
85
- def self.needs_refresh?
86
- exists?(['refresh_at IS NOT NULL AND refresh_at < ?', Time.zone.now])
87
- end
88
-
89
- def transport
90
- self.class.transport
91
- end
92
-
93
- before_create do
94
- if session
95
- self.expires_at = Time.now + transport.expire_new_connection_in
96
- elsif transport.refresh_channels_every != :never
97
- self.refresh_at = Time.now + transport.refresh_channels_every
98
- end
99
- end
100
-
101
- class << self
102
- attr_accessor :transport
103
36
 
104
37
  def active
105
- # if table doesn't exist then we are either calling from within
106
- # a migration or from a console before the server has ever started
107
- # in these cases there are no channels so we return nothing
108
- return [] unless table_exists?
109
- if Hyperstack.on_server?
110
- expired.delete_all
111
- refresh_connections if needs_refresh?
112
- end
113
- all.pluck(:channel).uniq
38
+ adapter.active
114
39
  end
115
40
 
116
41
  def open(channel, session = nil, root_path = nil)
117
- self.root_path = root_path
118
- find_or_create_by(channel: channel, session: session)
42
+ puts "open(#{channel}, #{session}, #{root_path})" if show_diagnostics
43
+
44
+ adapter.open(channel, session, root_path).tap do |c|
45
+ puts " - open returning #{c}" if show_diagnostics
46
+ end
119
47
  end
120
48
 
121
49
  def send_to_channel(channel, data)
122
- pending_for(channel).each do |connection|
123
- QueuedMessage.create(data: data, hyperstack_connection: connection)
124
- end
125
- transport.send_data(channel, data) if exists?(channel: channel, session: nil)
50
+ puts "send_to_channel(#{channel}, #{data})" if show_diagnostics
51
+
52
+ adapter.send_to_channel(channel, data)
126
53
  end
127
54
 
128
55
  def read(session, root_path)
129
- self.root_path = root_path
130
- where(session: session)
131
- .update_all(expires_at: Time.now + transport.expire_polled_connection_in)
132
- QueuedMessage.for_session(session).destroy_all.pluck(:data)
56
+ puts "read(#{session}, #{root_path})" if show_diagnostics
57
+
58
+ adapter.read(session, root_path)
133
59
  end
134
60
 
135
61
  def connect_to_transport(channel, session, root_path)
136
- self.root_path = root_path
137
- if (connection = find_by(channel: channel, session: session))
138
- messages = connection.messages.pluck(:data)
139
- connection.destroy
140
- else
141
- messages = []
142
- end
143
- open(channel)
144
- messages
62
+ puts "connect_to_transport(#{channel}, #{session}, #{root_path})" if show_diagnostics
63
+
64
+ adapter.connect_to_transport(channel, session, root_path)
145
65
  end
146
66
 
147
67
  def disconnect(channel)
148
- find_by(channel: channel, session: nil).destroy
68
+ adapter.disconnect(channel)
149
69
  end
150
70
 
151
71
  def root_path=(path)
152
- QueuedMessage.root_path = path if path
72
+ adapter.root_path = path
153
73
  end
154
74
 
155
75
  def root_path
156
- # if the QueuedMessage table doesn't exist then we are either calling from within
157
- # a migration or from a console before the server has ever started
158
- # in these cases there is no root path to the server
159
- QueuedMessage.root_path if QueuedMessage.table_exists?
76
+ adapter.root_path
160
77
  end
161
78
 
162
79
  def refresh_connections
163
- refresh_started_at = Time.zone.now
164
- channels = transport.refresh_channels
165
- next_refresh = refresh_started_at + transport.refresh_channels_every
166
- channels.each do |channel|
167
- connection = find_by(channel: channel, session: nil)
168
- connection.update(refresh_at: next_refresh) if connection
80
+ adapter.refresh_connections
81
+ end
82
+
83
+ def method_missing(method_name, *args, &block)
84
+ if adapter::Connection.respond_to?(method_name)
85
+ adapter::Connection.send(method_name, *args, &block)
86
+ else
87
+ super
169
88
  end
170
- inactive.delete_all
89
+ end
90
+
91
+ def respond_to_missing?(method_name, include_private = false)
92
+ adapter::Connection.respond_to?(method_name)
171
93
  end
172
94
  end
173
95
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'active_record/connection'
4
+ require_relative 'active_record/queued_message'
5
+
6
+ module Hyperstack
7
+ module ConnectionAdapter
8
+ module ActiveRecord
9
+ class << self
10
+ def build_tables
11
+ Connection.create_table(force: :cascade) do |t|
12
+ t.string :channel
13
+ t.string :session
14
+ t.datetime :created_at
15
+ t.datetime :expires_at
16
+ t.datetime :refresh_at
17
+ end
18
+
19
+ QueuedMessage.create_table(force: :cascade) do |t|
20
+ t.text :data
21
+ t.integer :connection_id
22
+ end
23
+ end
24
+
25
+ def transport
26
+ Hyperstack::Connection.transport
27
+ end
28
+
29
+ def active
30
+ # if table doesn't exist then we are either calling from within
31
+ # a migration or from a console before the server has ever started
32
+ # in these cases there are no channels so we return nothing
33
+ return [] unless Connection.table_exists?
34
+
35
+ if Hyperstack.on_server?
36
+ Connection.expired.delete_all
37
+ refresh_connections if Connection.needs_refresh?
38
+ end
39
+
40
+ Connection.all.pluck(:channel).uniq
41
+ rescue ::ActiveRecord::StatementInvalid
42
+ []
43
+ end
44
+
45
+ def open(channel, session = nil, root_path = nil)
46
+ self.root_path = root_path
47
+
48
+ Connection.find_or_create_by(channel: channel, session: session)
49
+ end
50
+
51
+ def send_to_channel(channel, data)
52
+ Connection.pending_for(channel).each do |connection|
53
+ QueuedMessage.create(data: data, hyperstack_connection: connection)
54
+ end
55
+
56
+ transport.send_data(channel, data) if Connection.exists?(channel: channel, session: nil)
57
+ end
58
+
59
+ def read(session, root_path)
60
+ self.root_path = root_path
61
+
62
+ Connection.where(session: session)
63
+ .update_all(expires_at: Time.current + transport.expire_polled_connection_in)
64
+
65
+ QueuedMessage.for_session(session).destroy_all.pluck(:data)
66
+ end
67
+
68
+ def connect_to_transport(channel, session, root_path)
69
+ self.root_path = root_path
70
+
71
+ if (connection = Connection.find_by(channel: channel, session: session))
72
+ messages = connection.messages.pluck(:data)
73
+ connection.destroy
74
+ else
75
+ messages = []
76
+ end
77
+
78
+ open(channel)
79
+
80
+ messages
81
+ end
82
+
83
+ def disconnect(channel)
84
+ Connection.find_by(channel: channel, session: nil).destroy
85
+ end
86
+
87
+ def root_path=(path)
88
+ QueuedMessage.root_path = path if path
89
+ end
90
+
91
+ def root_path
92
+ # if the QueuedMessage table doesn't exist then we are either calling from within
93
+ # a migration or from a console before the server has ever started
94
+ # in these cases there is no root path to the server
95
+ QueuedMessage.root_path if QueuedMessage.table_exists?
96
+ end
97
+
98
+ def refresh_connections
99
+ refresh_started_at = Time.current
100
+ channels = transport.refresh_channels
101
+ next_refresh = refresh_started_at + transport.refresh_channels_every
102
+
103
+ channels.each do |channel|
104
+ connection = Connection.find_by(channel: channel, session: nil)
105
+ connection.update(refresh_at: next_refresh) if connection
106
+ end
107
+
108
+ Connection.inactive.delete_all
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end