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

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