hyper-operation 0.5.12 → 0.99.0

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 (277) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -1
  3. data/.travis.yml +33 -0
  4. data/DOCS-POLICIES.md +93 -0
  5. data/DOCS.md +1 -1
  6. data/Gemfile +5 -1
  7. data/Gemfile.lock +379 -0
  8. data/README.md +10 -9
  9. data/Rakefile +10 -2
  10. data/hyper-operation.gemspec +34 -29
  11. data/lib/hyper-operation.rb +5 -4
  12. data/lib/hyper-operation/boot.rb +1 -1
  13. data/lib/hyper-operation/engine.rb +1 -1
  14. data/lib/hyper-operation/http.rb +309 -0
  15. data/lib/hyper-operation/railway/params_wrapper.rb +1 -0
  16. data/lib/hyper-operation/server_op.rb +83 -18
  17. data/lib/hyper-operation/transport/client_drivers.rb +71 -28
  18. data/lib/hyper-operation/transport/connection.rb +22 -20
  19. data/lib/hyper-operation/transport/hyperloop.rb +1 -1
  20. data/lib/hyper-operation/transport/hyperloop_controller.rb +6 -1
  21. data/lib/hyper-operation/transport/policy.rb +78 -13
  22. data/lib/hyper-operation/version.rb +1 -1
  23. metadata +95 -319
  24. data/CODE_OF_CONDUCT.md +0 -49
  25. data/examples/chat-app/.gitignore +0 -21
  26. data/examples/chat-app/Gemfile +0 -57
  27. data/examples/chat-app/Gemfile.lock +0 -283
  28. data/examples/chat-app/README.md +0 -3
  29. data/examples/chat-app/Rakefile +0 -6
  30. data/examples/chat-app/app/assets/config/manifest.js +0 -3
  31. data/examples/chat-app/app/assets/images/.keep +0 -0
  32. data/examples/chat-app/app/assets/javascripts/application.js +0 -3
  33. data/examples/chat-app/app/assets/javascripts/cable.js +0 -13
  34. data/examples/chat-app/app/assets/javascripts/channels/.keep +0 -0
  35. data/examples/chat-app/app/assets/stylesheets/application.css +0 -15
  36. data/examples/chat-app/app/channels/application_cable/channel.rb +0 -4
  37. data/examples/chat-app/app/channels/application_cable/connection.rb +0 -4
  38. data/examples/chat-app/app/controllers/application_controller.rb +0 -3
  39. data/examples/chat-app/app/controllers/concerns/.keep +0 -0
  40. data/examples/chat-app/app/controllers/home_controller.rb +0 -5
  41. data/examples/chat-app/app/helpers/application_helper.rb +0 -2
  42. data/examples/chat-app/app/hyperloop/components/app.rb +0 -11
  43. data/examples/chat-app/app/hyperloop/components/formatted_div.rb +0 -13
  44. data/examples/chat-app/app/hyperloop/components/input_box.rb +0 -29
  45. data/examples/chat-app/app/hyperloop/components/message.rb +0 -29
  46. data/examples/chat-app/app/hyperloop/components/messages.rb +0 -9
  47. data/examples/chat-app/app/hyperloop/components/nav.rb +0 -30
  48. data/examples/chat-app/app/hyperloop/operations/operations.rb +0 -56
  49. data/examples/chat-app/app/hyperloop/stores/message_store.rb +0 -23
  50. data/examples/chat-app/app/models/application_record.rb +0 -3
  51. data/examples/chat-app/app/models/concerns/.keep +0 -0
  52. data/examples/chat-app/app/models/models.rb +0 -2
  53. data/examples/chat-app/app/models/public/.keep +0 -0
  54. data/examples/chat-app/app/models/public/announcement.rb +0 -8
  55. data/examples/chat-app/app/policies/application_policy.rb +0 -5
  56. data/examples/chat-app/app/views/layouts/application.html.erb +0 -51
  57. data/examples/chat-app/bin/bundle +0 -3
  58. data/examples/chat-app/bin/rails +0 -9
  59. data/examples/chat-app/bin/rake +0 -9
  60. data/examples/chat-app/bin/setup +0 -34
  61. data/examples/chat-app/bin/spring +0 -17
  62. data/examples/chat-app/bin/update +0 -29
  63. data/examples/chat-app/config.ru +0 -5
  64. data/examples/chat-app/config/application.rb +0 -13
  65. data/examples/chat-app/config/boot.rb +0 -3
  66. data/examples/chat-app/config/cable.yml +0 -9
  67. data/examples/chat-app/config/database.yml +0 -25
  68. data/examples/chat-app/config/environment.rb +0 -5
  69. data/examples/chat-app/config/environments/development.rb +0 -56
  70. data/examples/chat-app/config/environments/production.rb +0 -86
  71. data/examples/chat-app/config/environments/test.rb +0 -42
  72. data/examples/chat-app/config/initializers/application_controller_renderer.rb +0 -6
  73. data/examples/chat-app/config/initializers/assets.rb +0 -11
  74. data/examples/chat-app/config/initializers/backtrace_silencers.rb +0 -7
  75. data/examples/chat-app/config/initializers/cookies_serializer.rb +0 -5
  76. data/examples/chat-app/config/initializers/filter_parameter_logging.rb +0 -4
  77. data/examples/chat-app/config/initializers/hyperloop.rb +0 -4
  78. data/examples/chat-app/config/initializers/inflections.rb +0 -16
  79. data/examples/chat-app/config/initializers/mime_types.rb +0 -4
  80. data/examples/chat-app/config/initializers/new_framework_defaults.rb +0 -24
  81. data/examples/chat-app/config/initializers/session_store.rb +0 -3
  82. data/examples/chat-app/config/initializers/wrap_parameters.rb +0 -14
  83. data/examples/chat-app/config/locales/en.yml +0 -23
  84. data/examples/chat-app/config/puma.rb +0 -47
  85. data/examples/chat-app/config/routes.rb +0 -5
  86. data/examples/chat-app/config/secrets.yml +0 -22
  87. data/examples/chat-app/config/spring.rb +0 -6
  88. data/examples/chat-app/db/seeds.rb +0 -7
  89. data/examples/chat-app/lib/assets/.keep +0 -0
  90. data/examples/chat-app/lib/tasks/.keep +0 -0
  91. data/examples/chat-app/log/.keep +0 -0
  92. data/examples/chat-app/public/404.html +0 -67
  93. data/examples/chat-app/public/422.html +0 -67
  94. data/examples/chat-app/public/500.html +0 -66
  95. data/examples/chat-app/public/apple-touch-icon-precomposed.png +0 -0
  96. data/examples/chat-app/public/apple-touch-icon.png +0 -0
  97. data/examples/chat-app/public/favicon.ico +0 -0
  98. data/examples/chat-app/public/robots.txt +0 -5
  99. data/examples/chat-app/test/controllers/.keep +0 -0
  100. data/examples/chat-app/test/fixtures/.keep +0 -0
  101. data/examples/chat-app/test/fixtures/files/.keep +0 -0
  102. data/examples/chat-app/test/helpers/.keep +0 -0
  103. data/examples/chat-app/test/integration/.keep +0 -0
  104. data/examples/chat-app/test/mailers/.keep +0 -0
  105. data/examples/chat-app/test/models/.keep +0 -0
  106. data/examples/chat-app/test/test_helper.rb +0 -10
  107. data/examples/chat-app/tmp/.keep +0 -0
  108. data/examples/chat-app/vendor/assets/javascripts/.keep +0 -0
  109. data/examples/chat-app/vendor/assets/stylesheets/.keep +0 -0
  110. data/examples/five-letter-word-game/.gitignore +0 -21
  111. data/examples/five-letter-word-game/Gemfile +0 -62
  112. data/examples/five-letter-word-game/Gemfile.lock +0 -291
  113. data/examples/five-letter-word-game/README.md +0 -24
  114. data/examples/five-letter-word-game/Rakefile +0 -6
  115. data/examples/five-letter-word-game/app/assets/config/manifest.js +0 -3
  116. data/examples/five-letter-word-game/app/assets/images/.keep +0 -0
  117. data/examples/five-letter-word-game/app/assets/javascripts/application.js +0 -4
  118. data/examples/five-letter-word-game/app/assets/javascripts/cable.js +0 -13
  119. data/examples/five-letter-word-game/app/assets/javascripts/channels/.keep +0 -0
  120. data/examples/five-letter-word-game/app/assets/stylesheets/application.css +0 -15
  121. data/examples/five-letter-word-game/app/channels/application_cable/channel.rb +0 -4
  122. data/examples/five-letter-word-game/app/channels/application_cable/connection.rb +0 -4
  123. data/examples/five-letter-word-game/app/controllers/application_controller.rb +0 -14
  124. data/examples/five-letter-word-game/app/controllers/concerns/.keep +0 -0
  125. data/examples/five-letter-word-game/app/controllers/home_controller.rb +0 -5
  126. data/examples/five-letter-word-game/app/helpers/application_helper.rb +0 -2
  127. data/examples/five-letter-word-game/app/hyperloop/components/app.rb +0 -65
  128. data/examples/five-letter-word-game/app/hyperloop/components/guesses.rb +0 -8
  129. data/examples/five-letter-word-game/app/hyperloop/components/input_word.rb +0 -13
  130. data/examples/five-letter-word-game/app/hyperloop/models/user.rb +0 -27
  131. data/examples/five-letter-word-game/app/hyperloop/operations/ops.rb +0 -115
  132. data/examples/five-letter-word-game/app/hyperloop/stores/store.rb +0 -120
  133. data/examples/five-letter-word-game/app/jobs/application_job.rb +0 -2
  134. data/examples/five-letter-word-game/app/mailers/application_mailer.rb +0 -4
  135. data/examples/five-letter-word-game/app/models/application_record.rb +0 -3
  136. data/examples/five-letter-word-game/app/models/concerns/.keep +0 -0
  137. data/examples/five-letter-word-game/app/policies/hyperloop/application_policy.rb +0 -3
  138. data/examples/five-letter-word-game/app/policies/user_policy.rb +0 -4
  139. data/examples/five-letter-word-game/app/views/layouts/application.html.erb +0 -14
  140. data/examples/five-letter-word-game/app/views/layouts/mailer.html.erb +0 -13
  141. data/examples/five-letter-word-game/app/views/layouts/mailer.text.erb +0 -1
  142. data/examples/five-letter-word-game/bin/bundle +0 -3
  143. data/examples/five-letter-word-game/bin/rails +0 -9
  144. data/examples/five-letter-word-game/bin/rake +0 -9
  145. data/examples/five-letter-word-game/bin/setup +0 -34
  146. data/examples/five-letter-word-game/bin/spring +0 -17
  147. data/examples/five-letter-word-game/bin/update +0 -29
  148. data/examples/five-letter-word-game/config.ru +0 -5
  149. data/examples/five-letter-word-game/config/application.rb +0 -12
  150. data/examples/five-letter-word-game/config/boot.rb +0 -3
  151. data/examples/five-letter-word-game/config/cable.yml +0 -9
  152. data/examples/five-letter-word-game/config/database.yml +0 -46
  153. data/examples/five-letter-word-game/config/environment.rb +0 -5
  154. data/examples/five-letter-word-game/config/environments/development.rb +0 -56
  155. data/examples/five-letter-word-game/config/environments/production.rb +0 -86
  156. data/examples/five-letter-word-game/config/environments/test.rb +0 -42
  157. data/examples/five-letter-word-game/config/initializers/application_controller_renderer.rb +0 -6
  158. data/examples/five-letter-word-game/config/initializers/assets.rb +0 -15
  159. data/examples/five-letter-word-game/config/initializers/backtrace_silencers.rb +0 -7
  160. data/examples/five-letter-word-game/config/initializers/cookies_serializer.rb +0 -5
  161. data/examples/five-letter-word-game/config/initializers/filter_parameter_logging.rb +0 -4
  162. data/examples/five-letter-word-game/config/initializers/hyperloop.rb +0 -19
  163. data/examples/five-letter-word-game/config/initializers/inflections.rb +0 -16
  164. data/examples/five-letter-word-game/config/initializers/mime_types.rb +0 -4
  165. data/examples/five-letter-word-game/config/initializers/new_framework_defaults.rb +0 -24
  166. data/examples/five-letter-word-game/config/initializers/session_store.rb +0 -3
  167. data/examples/five-letter-word-game/config/initializers/wrap_parameters.rb +0 -14
  168. data/examples/five-letter-word-game/config/locales/en.yml +0 -23
  169. data/examples/five-letter-word-game/config/puma.rb +0 -47
  170. data/examples/five-letter-word-game/config/routes.rb +0 -5
  171. data/examples/five-letter-word-game/config/secrets.yml +0 -22
  172. data/examples/five-letter-word-game/config/spring.rb +0 -6
  173. data/examples/five-letter-word-game/db/schema.rb +0 -28
  174. data/examples/five-letter-word-game/db/seeds.rb +0 -7
  175. data/examples/five-letter-word-game/lib/assets/.keep +0 -0
  176. data/examples/five-letter-word-game/lib/tasks/.keep +0 -0
  177. data/examples/five-letter-word-game/log/.keep +0 -0
  178. data/examples/five-letter-word-game/public/404.html +0 -67
  179. data/examples/five-letter-word-game/public/422.html +0 -67
  180. data/examples/five-letter-word-game/public/500.html +0 -66
  181. data/examples/five-letter-word-game/public/apple-touch-icon-precomposed.png +0 -0
  182. data/examples/five-letter-word-game/public/apple-touch-icon.png +0 -0
  183. data/examples/five-letter-word-game/public/favicon.ico +0 -0
  184. data/examples/five-letter-word-game/public/robots.txt +0 -5
  185. data/examples/five-letter-word-game/test/controllers/.keep +0 -0
  186. data/examples/five-letter-word-game/test/fixtures/.keep +0 -0
  187. data/examples/five-letter-word-game/test/fixtures/files/.keep +0 -0
  188. data/examples/five-letter-word-game/test/helpers/.keep +0 -0
  189. data/examples/five-letter-word-game/test/integration/.keep +0 -0
  190. data/examples/five-letter-word-game/test/mailers/.keep +0 -0
  191. data/examples/five-letter-word-game/test/models/.keep +0 -0
  192. data/examples/five-letter-word-game/test/test_helper.rb +0 -10
  193. data/examples/five-letter-word-game/tmp/.keep +0 -0
  194. data/examples/five-letter-word-game/vendor/assets/javascripts/.keep +0 -0
  195. data/examples/five-letter-word-game/vendor/assets/stylesheets/.keep +0 -0
  196. data/examples/smoke_test/.gitignore +0 -21
  197. data/examples/smoke_test/Gemfile +0 -59
  198. data/examples/smoke_test/Gemfile.lock +0 -289
  199. data/examples/smoke_test/README.md +0 -24
  200. data/examples/smoke_test/Rakefile +0 -6
  201. data/examples/smoke_test/app/assets/config/manifest.js +0 -3
  202. data/examples/smoke_test/app/assets/images/.keep +0 -0
  203. data/examples/smoke_test/app/assets/javascripts/application.js +0 -15
  204. data/examples/smoke_test/app/assets/javascripts/cable.js +0 -13
  205. data/examples/smoke_test/app/assets/javascripts/channels/.keep +0 -0
  206. data/examples/smoke_test/app/assets/stylesheets/application.css +0 -15
  207. data/examples/smoke_test/app/channels/application_cable/channel.rb +0 -4
  208. data/examples/smoke_test/app/channels/application_cable/connection.rb +0 -4
  209. data/examples/smoke_test/app/controllers/app_controller.rb +0 -5
  210. data/examples/smoke_test/app/controllers/application_controller.rb +0 -3
  211. data/examples/smoke_test/app/helpers/application_helper.rb +0 -2
  212. data/examples/smoke_test/app/hyperloop/components/hello.rb +0 -25
  213. data/examples/smoke_test/app/hyperloop/operations/operations/nested_send_to_all.rb +0 -7
  214. data/examples/smoke_test/app/hyperloop/operations/send_to_all.rb +0 -6
  215. data/examples/smoke_test/app/hyperloop/stores/messages.rb +0 -4
  216. data/examples/smoke_test/app/jobs/application_job.rb +0 -2
  217. data/examples/smoke_test/app/mailers/application_mailer.rb +0 -4
  218. data/examples/smoke_test/app/models/application_record.rb +0 -3
  219. data/examples/smoke_test/app/models/concerns/.keep +0 -0
  220. data/examples/smoke_test/app/policies/application_policy.rb +0 -8
  221. data/examples/smoke_test/app/views/layouts/application.html.erb +0 -14
  222. data/examples/smoke_test/app/views/layouts/mailer.html.erb +0 -13
  223. data/examples/smoke_test/app/views/layouts/mailer.text.erb +0 -1
  224. data/examples/smoke_test/bin/bundle +0 -3
  225. data/examples/smoke_test/bin/rails +0 -9
  226. data/examples/smoke_test/bin/rake +0 -9
  227. data/examples/smoke_test/bin/setup +0 -34
  228. data/examples/smoke_test/bin/spring +0 -17
  229. data/examples/smoke_test/bin/update +0 -29
  230. data/examples/smoke_test/config.ru +0 -5
  231. data/examples/smoke_test/config/application.rb +0 -15
  232. data/examples/smoke_test/config/boot.rb +0 -3
  233. data/examples/smoke_test/config/cable.yml +0 -9
  234. data/examples/smoke_test/config/database.yml +0 -25
  235. data/examples/smoke_test/config/environment.rb +0 -5
  236. data/examples/smoke_test/config/environments/development.rb +0 -54
  237. data/examples/smoke_test/config/environments/production.rb +0 -86
  238. data/examples/smoke_test/config/environments/test.rb +0 -42
  239. data/examples/smoke_test/config/initializers/application_controller_renderer.rb +0 -6
  240. data/examples/smoke_test/config/initializers/assets.rb +0 -11
  241. data/examples/smoke_test/config/initializers/backtrace_silencers.rb +0 -7
  242. data/examples/smoke_test/config/initializers/cookies_serializer.rb +0 -5
  243. data/examples/smoke_test/config/initializers/filter_parameter_logging.rb +0 -4
  244. data/examples/smoke_test/config/initializers/hyperloop.rb +0 -32
  245. data/examples/smoke_test/config/initializers/inflections.rb +0 -16
  246. data/examples/smoke_test/config/initializers/mime_types.rb +0 -4
  247. data/examples/smoke_test/config/initializers/new_framework_defaults.rb +0 -24
  248. data/examples/smoke_test/config/initializers/session_store.rb +0 -3
  249. data/examples/smoke_test/config/initializers/wrap_parameters.rb +0 -14
  250. data/examples/smoke_test/config/locales/en.yml +0 -23
  251. data/examples/smoke_test/config/puma.rb +0 -47
  252. data/examples/smoke_test/config/routes.rb +0 -5
  253. data/examples/smoke_test/config/secrets.yml +0 -22
  254. data/examples/smoke_test/config/spring.rb +0 -6
  255. data/examples/smoke_test/db/seeds.rb +0 -7
  256. data/examples/smoke_test/lib/assets/.keep +0 -0
  257. data/examples/smoke_test/lib/tasks/.keep +0 -0
  258. data/examples/smoke_test/log/.keep +0 -0
  259. data/examples/smoke_test/public/404.html +0 -67
  260. data/examples/smoke_test/public/422.html +0 -67
  261. data/examples/smoke_test/public/500.html +0 -66
  262. data/examples/smoke_test/public/apple-touch-icon-precomposed.png +0 -0
  263. data/examples/smoke_test/public/apple-touch-icon.png +0 -0
  264. data/examples/smoke_test/public/favicon.ico +0 -0
  265. data/examples/smoke_test/public/robots.txt +0 -5
  266. data/examples/smoke_test/test/controllers/.keep +0 -0
  267. data/examples/smoke_test/test/fixtures/.keep +0 -0
  268. data/examples/smoke_test/test/fixtures/files/.keep +0 -0
  269. data/examples/smoke_test/test/helpers/.keep +0 -0
  270. data/examples/smoke_test/test/integration/.keep +0 -0
  271. data/examples/smoke_test/test/mailers/.keep +0 -0
  272. data/examples/smoke_test/test/models/.keep +0 -0
  273. data/examples/smoke_test/test/test_helper.rb +0 -10
  274. data/examples/smoke_test/tmp/.keep +0 -0
  275. data/examples/smoke_test/vendor/assets/javascripts/.keep +0 -0
  276. data/examples/smoke_test/vendor/assets/stylesheets/.keep +0 -0
  277. data/lib/hyper-operation/call_by_class_name.rb +0 -60
@@ -68,6 +68,7 @@ module Hyperloop
68
68
  end
69
69
 
70
70
  def hash_filter
71
+ # the :duck method is added in lib/hyper-operation.rb globally
71
72
  @hash_filter ||= Mutations::HashFilter.new
72
73
  end
73
74
 
@@ -1,37 +1,99 @@
1
+ require 'net/http' unless RUBY_ENGINE == 'opal'
2
+
1
3
  module Hyperloop
2
4
  class ServerOp < Operation
3
5
 
4
6
  class << self
5
- def run(*args)
6
- hash = _Railway.params_wrapper.combine_arg_array(args)
7
- hash = serialize_params(hash)
8
- HTTP.post(
9
- "#{`window.HyperloopEnginePath`}/execute_remote",
10
- payload: {json: {operation: name, params: hash}.to_json},
11
- headers: {'X-CSRF-Token' => Hyperloop::ClientDrivers.opts[:form_authenticity_token] }
12
- )
13
- .then do |response|
14
- deserialize_response response.json[:response]
15
- end.fail do |response|
16
- Exception.new response.json[:error]
7
+ include React::IsomorphicHelpers
8
+
9
+ if RUBY_ENGINE == 'opal'
10
+ if on_opal_client?
11
+ def run(*args)
12
+ hash = _Railway.params_wrapper.combine_arg_array(args)
13
+ hash = serialize_params(hash)
14
+ Hyperloop::HTTP.post(
15
+ "#{`window.HyperloopEnginePath`}/execute_remote",
16
+ payload: {json: {operation: name, params: hash}.to_json},
17
+ headers: {'X-CSRF-Token' => Hyperloop::ClientDrivers.opts[:form_authenticity_token] }
18
+ )
19
+ .then do |response|
20
+ deserialize_response response.json[:response]
21
+ end
22
+ .fail do |response|
23
+ Exception.new response.json[:error]
24
+ end
25
+ end
26
+ elsif on_opal_server?
27
+ def run(*args)
28
+ promise = Promise.new
29
+ response = internal_iso_run(name, args)
30
+ if response[:json][:response]
31
+ promise.resolve(response[:json][:response])
32
+ else
33
+ promise.reject Exception.new response[:json][:error]
34
+ end
35
+ promise
36
+ end
17
37
  end
18
- end if RUBY_ENGINE == 'opal'
38
+ end
39
+
40
+ isomorphic_method(:internal_iso_run) do |f, klass_name, op_params|
41
+ f.send_to_server(klass_name, op_params)
42
+ f.when_on_server {
43
+ Hyperloop::ServerOp.run_from_client(:acting_user, controller, klass_name, *op_params)
44
+ }
45
+ end
46
+
47
+ def descendants_map_cache
48
+ # calling descendants alone may take 10ms in a complex app, so better cache it
49
+ @cached_descendants ||= Hyperloop::ServerOp.descendants.map(&:to_s)
50
+ end
19
51
 
20
52
  def run_from_client(security_param, controller, operation, params)
53
+ if Rails.env.production?
54
+ # in production everything is eager loaded so ServerOp.descendants is filled and can be used to guard the .constantize
55
+ unless Hyperloop::ServerOp.descendants_map_cache.include?(operation)
56
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:illegal_remote_op_call, "Operation: #{operation} (in production)")
57
+ end
58
+ # however ...
59
+ else
60
+ # ... in development things are autoloaded on demand, thus ServerOp.descendants can be empty or partially filled and above guard
61
+ # would fail legal operations. To prevent this, the class has to be loaded first, what .const_get will take care of, and then
62
+ # its guarded, to achieve similar behaviour as in production. Doing the const_get first, before the guard,
63
+ # would not be safe for production and allow for potential remote code execution!
64
+ begin
65
+ const = Object.const_get(operation)
66
+ rescue NameError
67
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:illegal_remote_op_call, "Operation: #{operation} (const not found)")
68
+ end
69
+ unless const < Hyperloop::ServerOp
70
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:illegal_remote_op_call, "Operation: #{operation} (not a ServerOp subclass)")
71
+ end
72
+ end
21
73
  operation.constantize.class_eval do
22
74
  if _Railway.params_wrapper.method_defined?(:controller)
23
75
  params[:controller] = controller
24
76
  elsif !_Railway.params_wrapper.method_defined?(security_param)
25
77
  raise AccessViolation
26
78
  end
27
- run(params)
79
+ run(deserialize_params(params))
28
80
  .then { |r| return { json: { response: serialize_response(r) } } }
29
- .fail { |e| return { json: { error: e }, status: 500 } }
81
+ .fail { |e| return handle_exception(e, operation, params) }
30
82
  end
31
83
  rescue Exception => e
32
- { json: {error: e}, status: 500 }
84
+ handle_exception(e, operation, params)
33
85
  end
34
86
 
87
+ def handle_exception(e, operation, params)
88
+ if defined? ::Rails
89
+ params.delete(:controller)
90
+ ::Rails.logger.debug "\033[0;31;1mERROR: Hyperloop::ServerOp exception caught when running "\
91
+ "#{operation} with params \"#{params}\": #{e}\033[0;30;21m"
92
+ end
93
+ { json: { error: e }, status: 500 }
94
+ end
95
+
96
+
35
97
  def remote(path, *args)
36
98
  promise = Promise.new
37
99
  uri = URI("#{path}execute_remote_api")
@@ -39,7 +101,6 @@ module Hyperloop
39
101
  request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
40
102
  if uri.scheme == 'https'
41
103
  http.use_ssl = true
42
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
43
104
  end
44
105
  request.body = {
45
106
  operation: name,
@@ -86,9 +147,13 @@ module Hyperloop
86
147
  end
87
148
  regulation ||= proc { args }
88
149
  on_dispatch do |params, operation|
150
+ operation.instance_variable_set(:@_dispatched_channels, []) unless operation.instance_variable_get(:@_dispatched_channels)
89
151
  serialized_params = serialize_dispatch(params.to_h)
90
152
  [operation.instance_exec(*context, &regulation)].flatten.compact.uniq.each do |channel|
91
- Hyperloop.dispatch(channel: Hyperloop::InternalPolicy.channel_to_string(channel), operation: operation.class.name, params: serialized_params)
153
+ unless operation.instance_variable_get(:@_dispatched_channels).include?(channel)
154
+ operation.instance_variable_set(:@_dispatched_channels, operation.instance_variable_get(:@_dispatched_channels) << channel)
155
+ Hyperloop.dispatch(channel: Hyperloop::InternalPolicy.channel_to_string(channel), operation: operation.class.name, params: serialized_params)
156
+ end
92
157
  end
93
158
  end
94
159
  end if RUBY_ENGINE != 'opal'
@@ -7,8 +7,25 @@ module Hyperloop
7
7
  # client interface to sync_change or sync_destroy
8
8
 
9
9
  class Application
10
- def self.acting_user_id
11
- ClientDrivers.opts[:acting_user_id]
10
+ extend React::IsomorphicHelpers::ClassMethods
11
+
12
+ if on_opal_client?
13
+ def self.acting_user_id
14
+ ClientDrivers.opts[:acting_user_id]
15
+ end
16
+ else
17
+ def self.acting_user_id
18
+ ClientDrivers.client_drivers_get_acting_user_id
19
+ end
20
+ end
21
+
22
+ def self.env
23
+ @env = ClientDrivers.env unless @env
24
+ @env
25
+ end
26
+
27
+ def self.production?
28
+ env == 'production'
12
29
  end
13
30
  end
14
31
 
@@ -59,8 +76,9 @@ module Hyperloop
59
76
  }
60
77
  elsif ClientDrivers.opts[:transport] == :action_cable
61
78
  channel = "#{ClientDrivers.opts[:channel]}-#{channel_string}"
62
- HTTP.post(ClientDrivers.polling_path('action-cable-auth', channel)).then do |response|
79
+ Hyperloop::HTTP.post(ClientDrivers.polling_path('action-cable-auth', channel), headers: { 'X-CSRF-Token' => ClientDrivers.opts[:form_authenticity_token] }).then do |response|
63
80
  %x{
81
+ var fix_opal_0110 = 'return';
64
82
  #{Hyperloop.action_cable_consumer}.subscriptions.create(
65
83
  {
66
84
  channel: "Hyperloop::ActionCableChannel",
@@ -71,9 +89,11 @@ module Hyperloop
71
89
  },
72
90
  {
73
91
  connected: function() {
74
- #{ClientDrivers.get_queued_data("connect-to-transport", channel_string)}
92
+ if (#{ClientDrivers.env == 'development'}) { console.log("ActionCable connected to: ", channel_string); }
93
+ #{ClientDrivers.complete_connection(channel_string)}
75
94
  },
76
95
  received: function(data) {
96
+ if (#{ClientDrivers.env == 'development'}) { console.log("ActionCable received: ", data); }
77
97
  #{ClientDrivers.sync_dispatch(JSON.parse(`JSON.stringify(data)`)['data'])}
78
98
  }
79
99
  }
@@ -81,7 +101,7 @@ module Hyperloop
81
101
  }
82
102
  end
83
103
  else
84
- HTTP.get(ClientDrivers.polling_path(:subscribe, channel_string))
104
+ Hyperloop::HTTP.get(ClientDrivers.polling_path(:subscribe, channel_string))
85
105
  end
86
106
  end
87
107
  end
@@ -105,24 +125,26 @@ module Hyperloop
105
125
  # will remove the session from the list.
106
126
 
107
127
  prerender_footer do |controller|
108
- next if Hyperloop.transport == :none
109
- if defined?(PusherFake)
110
- path = ::Rails.application.routes.routes.detect do |route|
111
- route.app == Hyperloop::Engine ||
112
- (route.app.respond_to?(:app) && route.app.app == Hyperloop::Engine)
113
- end.path.spec
114
- pusher_fake_js = PusherFake.javascript(
115
- auth: { headers: { 'X-CSRF-Token' => controller.send(:form_authenticity_token) } },
116
- authEndpoint: "#{path}/hyperloop-pusher-auth"
117
- )
128
+ unless Hyperloop.transport == :none
129
+ if defined?(PusherFake)
130
+ path = ::Rails.application.routes.routes.detect do |route|
131
+ route.app == Hyperloop::Engine ||
132
+ (route.app.respond_to?(:app) && route.app.app == Hyperloop::Engine)
133
+ end.path.spec
134
+ pusher_fake_js = PusherFake.javascript(
135
+ auth: { headers: { 'X-CSRF-Token' => controller.send(:form_authenticity_token) } },
136
+ authEndpoint: "#{path}/hyperloop-pusher-auth"
137
+ )
138
+ end
139
+ controller.session.delete 'hyperloop-dummy-init' unless controller.session.id
140
+ id = "#{SecureRandom.uuid}-#{controller.session.id}"
141
+ auto_connections = Hyperloop::AutoConnect.channels(id, controller.acting_user)
118
142
  end
119
- controller.session.delete 'hyperloop-dummy-init' unless controller.session.id
120
- id = "#{SecureRandom.uuid}-#{controller.session.id}"
121
- auto_connections = Hyperloop::AutoConnect.channels(id, controller.acting_user)
122
143
  config_hash = {
123
144
  transport: Hyperloop.transport,
124
145
  id: id,
125
146
  acting_user_id: (controller.acting_user && controller.acting_user.id),
147
+ env: ::Rails.env,
126
148
  client_logging: Hyperloop.client_logging,
127
149
  pusher_fake_js: pusher_fake_js,
128
150
  key: Hyperloop.key,
@@ -149,9 +171,27 @@ module Hyperloop
149
171
  attr_reader :opts
150
172
  end
151
173
 
174
+ isomorphic_method(:client_drivers_get_acting_user_id) do |f|
175
+ f.send_to_server if RUBY_ENGINE == 'opal'
176
+ f.when_on_server { (controller.acting_user && controller.acting_user.id) }
177
+ end
178
+
179
+ isomorphic_method(:env) do |f|
180
+ f.when_on_client { opts[:env] }
181
+ f.send_to_server
182
+ f.when_on_server { ::Rails.env }
183
+ end
184
+
185
+ def self.complete_connection(channel, retries = 10)
186
+ get_queued_data('connect-to-transport', channel).fail do
187
+ after(0.25) { complete_connection(channel, retries - 1) } unless retries.zero?
188
+ end
189
+ end
190
+
152
191
  def self.get_queued_data(operation, channel = nil, opts = {})
153
- HTTP.get(polling_path(operation, channel), opts).then do |response|
192
+ Hyperloop::HTTP.get(polling_path(operation, channel), opts).then do |response|
154
193
  response.json.each do |data|
194
+ `console.log("simple_poller received: ", data)` if ClientDrivers.env == 'development'
155
195
  sync_dispatch(data[1])
156
196
  end
157
197
  end
@@ -161,21 +201,24 @@ module Hyperloop
161
201
 
162
202
  if @initialized
163
203
  # 1) skip initialization if already initialized
164
- # 2) if running action_cable make sure connection is up after pinging the server_up
165
- # action cable closes the connection if files change on the server
166
- HTTP.get("#{`window.HyperloopEnginePath`}/server_up") do
167
- `#{Hyperloop.action_cable_consumer}.connection.open()` if `#{Hyperloop.action_cable_consumer}.connection.disconnected`
168
- end if Hyperloop.action_cable_consumer
204
+ if on_opal_client? && Hyperloop.action_cable_consumer
205
+ # 2) if running action_cable make sure connection is up after pinging the server_up
206
+ # action cable closes the connection if files change on the server
207
+ Hyperloop::HTTP.get("#{`window.HyperloopEnginePath`}/server_up") do
208
+ `#{Hyperloop.action_cable_consumer}.connection.open()` if `#{Hyperloop.action_cable_consumer}.connection.disconnected`
209
+ end
210
+ end
169
211
  return
170
212
  end
171
213
 
172
214
  @initialized = true
215
+ @opts = {}
216
+
217
+ if on_opal_client?
173
218
 
174
- if RUBY_ENGINE == 'opal'
175
219
  @opts = Hash.new(`window.HyperloopOpts`)
176
- end
177
220
 
178
- if on_opal_client?
221
+
179
222
  if opts[:transport] == :pusher
180
223
 
181
224
  opts[:dispatch] = lambda do |data|
@@ -209,7 +252,7 @@ module Hyperloop
209
252
  elsif opts[:transport] == :simple_poller
210
253
  opts[:auto_connect].each { |channel| IncomingBroadcast.add_connection(*channel) }
211
254
  every(opts[:seconds_between_poll]) do
212
- get_queued_data(:read, nil, headers: {'X-HYPERLOOP-SILENT-REQUEST' => true })
255
+ get_queued_data(:read, nil)
213
256
  end
214
257
  end
215
258
  end
@@ -1,20 +1,16 @@
1
1
  module Hyperloop
2
2
  module AutoCreate
3
- def needs_init?
4
- return false if Hyperloop.transport == :none
5
- return true if connection.respond_to?(:data_sources) && !connection.data_sources.include?(table_name)
6
- return true if !connection.respond_to?(:data_sources) && !connection.tables.include?(table_name)
7
- return false unless Hyperloop.on_server?
8
- return true if defined?(Rails::Server)
9
- return true unless Connection.root_path
10
- uri = URI("#{Connection.root_path}server_up")
11
- http = Net::HTTP.new(uri.host, uri.port)
12
- request = Net::HTTP::Get.new(uri.path)
13
- if uri.scheme == 'https'
14
- http.use_ssl = true
15
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
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)
16
9
  end
17
- http.request(request) && return rescue true
10
+ end
11
+
12
+ def needs_init?
13
+ Hyperloop.transport != :none && Hyperloop.on_server? && !table_exists?
18
14
  end
19
15
 
20
16
  def create_table(*args, &block)
@@ -58,14 +54,14 @@ module Hyperloop
58
54
  extend AutoCreate
59
55
 
60
56
  def self.build_tables
61
- create_table(force: true) do |t|
57
+ create_table(force: :cascade) do |t|
62
58
  t.string :channel
63
59
  t.string :session
64
60
  t.datetime :created_at
65
61
  t.datetime :expires_at
66
62
  t.datetime :refresh_at
67
63
  end
68
- QueuedMessage.create_table(force: true) do |t|
64
+ QueuedMessage.create_table(force: :cascade) do |t|
69
65
  t.text :data
70
66
  t.integer :connection_id
71
67
  end
@@ -106,6 +102,10 @@ module Hyperloop
106
102
  attr_accessor :transport
107
103
 
108
104
  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
109
  if Hyperloop.on_server?
110
110
  expired.delete_all
111
111
  refresh_connections if needs_refresh?
@@ -153,9 +153,10 @@ module Hyperloop
153
153
  end
154
154
 
155
155
  def root_path
156
- QueuedMessage.root_path
157
- rescue
158
- nil
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?
159
160
  end
160
161
 
161
162
  def refresh_connections
@@ -163,7 +164,8 @@ module Hyperloop
163
164
  channels = transport.refresh_channels
164
165
  next_refresh = refresh_started_at + transport.refresh_channels_every
165
166
  channels.each do |channel|
166
- find_by(channel: channel, session: nil).update(refresh_at: next_refresh)
167
+ connection = find_by(channel: channel, session: nil)
168
+ connection.update(refresh_at: next_refresh) if connection
167
169
  end
168
170
  inactive.delete_all
169
171
  end
@@ -122,7 +122,7 @@ module Hyperloop
122
122
  end
123
123
 
124
124
  def self.on_server?
125
- Rails.const_defined? 'Server'
125
+ return defined? Rails::Server
126
126
  end
127
127
 
128
128
  def self.pusher
@@ -17,7 +17,7 @@ module Hyperloop
17
17
  unless method_defined? :pre_hyperloop_call
18
18
  alias pre_hyperloop_call call
19
19
  def call(env)
20
- if !Hyperloop.opts[:noisy] && env['HTTP_X_HYPERLOOP_SILENT_REQUEST']
20
+ if Hyperloop.transport == :simple_poller && env['PATH_INFO'] && env['PATH_INFO'].include?('/hyperloop-read/')
21
21
  Rails.logger.silence do
22
22
  pre_hyperloop_call(env)
23
23
  end
@@ -115,6 +115,7 @@ module Hyperloop
115
115
  end
116
116
 
117
117
  def pusher_auth
118
+ raise unless Hyperloop.transport == :pusher
118
119
  channel = regulate params[:channel_name].gsub(/^#{Regexp.quote(Hyperloop.channel)}\-/,'').gsub('==', '::')
119
120
  response = Hyperloop.pusher.authenticate(params[:channel_name], params[:socket_id])
120
121
  render json: response
@@ -123,6 +124,7 @@ module Hyperloop
123
124
  end
124
125
 
125
126
  def action_cable_auth
127
+ raise unless Hyperloop.transport == :action_cable
126
128
  channel = regulate params[:channel_name].gsub(/^#{Regexp.quote(Hyperloop.channel)}\-/,'')
127
129
  salt = SecureRandom.hex
128
130
  authorization = Hyperloop.authorization(salt, channel, client_id)
@@ -134,6 +136,8 @@ module Hyperloop
134
136
  def connect_to_transport
135
137
  root_path = request.original_url.gsub(/hyperloop-connect-to-transport.*$/, '')
136
138
  render json: Hyperloop::Connection.connect_to_transport(params[:channel], client_id, root_path)
139
+ rescue Exception => e
140
+ render status: :service_unavailable, json: {error: e}
137
141
  end
138
142
 
139
143
  def execute_remote
@@ -154,6 +158,7 @@ module Hyperloop
154
158
  end
155
159
 
156
160
  def console_update # TODO this should just become an execute-remote-api call
161
+ raise unless Rails.env.development?
157
162
  authorization = Hyperloop.authorization(params[:salt], params[:channel], params[:data][1][:broadcast_id]) #params[:data].to_json)
158
163
  return head :unauthorized if authorization != params[:authorization]
159
164
  Hyperloop::Connection.send_to_channel(params[:channel], params[:data])
@@ -3,6 +3,16 @@ module Hyperloop
3
3
  class InternalClassPolicy
4
4
 
5
5
  def initialize(regulated_klass)
6
+ unless regulated_klass.is_a?(Class)
7
+ # attempt to constantize the class in case eager_loading in production
8
+ # has loaded the policy before the class. THis will insure that if
9
+ # there is a class being regulated, it is loaded first.
10
+ begin
11
+ regulated_klass.constantize
12
+ rescue NameError
13
+ nil
14
+ end
15
+ end
6
16
  @regulated_klass = regulated_klass
7
17
  end
8
18
 
@@ -49,11 +59,17 @@ module Hyperloop
49
59
  end
50
60
 
51
61
  def dispatch_to(*args, &regulation)
52
- actual_klass = regulated_klass.is_a?(Class) ? regulated_klass : regulated_klass.constantize rescue nil
53
- actual_klass.dispatch_to(actual_klass) if actual_klass.respond_to? :dispatch_to
54
- unless actual_klass.respond_to? :dispatch_to
55
- raise 'you can only dispatch_to Operation classes'
56
- end
62
+ actual_klass = if regulated_klass.is_a?(Class)
63
+ regulated_klass
64
+ else
65
+ begin
66
+ regulated_klass.constantize
67
+ rescue NameError
68
+ nil
69
+ end
70
+ end
71
+ raise 'you can only dispatch_to Operation classes' unless actual_klass.respond_to? :dispatch_to
72
+ actual_klass.dispatch_to(actual_klass)
57
73
  actual_klass.dispatch_to(*args, &regulation)
58
74
  end
59
75
 
@@ -82,14 +98,40 @@ module Hyperloop
82
98
  end
83
99
  end
84
100
 
101
+ def self.ar_base_descendants_map_cache
102
+ @ar_base_descendants_map_cache ||= ActiveRecord::Base.descendants.map(&:name)
103
+ end
104
+
85
105
  def get_ar_model(str)
86
- str.is_a?(Class) ? str : Object.const_get(str)
87
- rescue
88
- raise "#{str} is not a class"
106
+ if str.is_a?(Class)
107
+ unless str <= ActiveRecord::Base
108
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:non_ar_class, "#{str} is not a subclass of ActiveRecord::Base")
109
+ end
110
+ str
111
+ else
112
+ # we used to cache this here, but during eager loading the cache may get partially filled and never updated
113
+ # so this guard will fail, now performance will be suckish, as this guard, required for security, takes some ms
114
+ # def self.ar_base_descendants_map_cache
115
+ # @ar_base_descendants_map_cache ||= ActiveRecord::Base.descendants.map(&:name)
116
+ # end
117
+ # if Rails.env.production? && !Hyperloop::InternalClassPolicy.ar_base_descendants_map_cache.include?(str)
118
+ if Rails.application.config.eager_load && !ActiveRecord::Base.descendants.map(&:name).include?(str)
119
+ # AR::Base.descendants is eager loaded in production -> this guard works.
120
+ # In development it may be empty or partially filled -> this guard may fail.
121
+ # Thus guarded here only in production.
122
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:non_ar_class, "#{str} is either not defined or is not a subclass of ActiveRecord::Base")
123
+ end
124
+ Object.const_get(str)
125
+ end
126
+ end
127
+
128
+ def self.regulated_klasses
129
+ @regulated_klasses ||= Set.new
89
130
  end
90
131
 
91
132
  def regulate(regulation_klass, policy, args, &regulation)
92
133
  process_args(policy, regulation_klass.allowed_opts, args, regulation) do |regulated_klass, opts|
134
+ self.class.regulated_klasses << regulated_klass.to_s
93
135
  regulation_klass.add_regulation regulated_klass, opts, &regulation
94
136
  end
95
137
  end
@@ -201,8 +243,23 @@ module Hyperloop
201
243
  class ClassConnectionRegulation < Regulation
202
244
 
203
245
  def self.add_regulation(klass, opts={}, &regulation)
204
- actual_klass = klass.is_a?(Class) ? klass : klass.constantize rescue nil
205
- actual_klass.dispatch_to(actual_klass) if actual_klass.respond_to? :dispatch_to rescue nil
246
+ actual_klass = if klass.is_a?(Class)
247
+ klass
248
+ else
249
+ begin
250
+ klass.constantize
251
+ rescue NameError
252
+ nil
253
+ end
254
+ end
255
+ if actual_klass && actual_klass.respond_to?(:dispatch_to)
256
+ begin
257
+ actual_klass.dispatch_to(actual_klass)
258
+ rescue NoMethodError
259
+ # this is the case for ClassPolicy where the instance method :dispatch_to has been deleted.
260
+ nil
261
+ end
262
+ end
206
263
  super
207
264
  end
208
265
 
@@ -321,10 +378,18 @@ module Hyperloop
321
378
  @obj
322
379
  end
323
380
 
381
+ def self.raise_operation_access_violation(message, details)
382
+ Hyperloop.on_error(Hyperloop::AccessViolation, message, details)
383
+ raise Hyperloop::AccessViolation
384
+ end
385
+
324
386
  def self.regulate_connection(acting_user, channel_string)
325
387
  channel = channel_string.split("-")
326
388
  if channel.length > 1
327
389
  id = channel[1..-1].join("-")
390
+ unless Hyperloop::InternalClassPolicy.regulated_klasses.include?(channel[0])
391
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:not_a_channel, "#{channel[0]} is not regulated channel class")
392
+ end
328
393
  object = Object.const_get(channel[0]).find(id)
329
394
  InstanceConnectionRegulation.connect(object, acting_user)
330
395
  else
@@ -486,7 +551,7 @@ module Hyperloop
486
551
 
487
552
  module PolicyAutoLoader
488
553
  def self.load(name, value)
489
- const_get("#{name}Policy") if name && !(name =~ /Policy$/) && value.is_a?(Class)
554
+ const_get("#{name}Policy") if name && !name.end_with?("Policy".freeze) && value.is_a?(Class)
490
555
  rescue Exception => e
491
556
  raise e if e.is_a?(LoadError) && e.message =~ /Unable to autoload constant #{name}Policy/
492
557
  end
@@ -515,8 +580,8 @@ class Class
515
580
 
516
581
  Hyperloop::ClassPolicyMethods.instance_methods.each do |method|
517
582
  define_method method do |*args, &block|
518
- if name =~ /Policy$/
519
- @hyperloop_internal_policy_object = Hyperloop::InternalClassPolicy.new(name.gsub(/Policy$/,""))
583
+ if name.end_with?("Policy".freeze)
584
+ @hyperloop_internal_policy_object = Hyperloop::InternalClassPolicy.new(name.sub(/Policy$/,""))
520
585
  include Hyperloop::PolicyMethods
521
586
  send method, *args, &block
522
587
  else