rage-rb 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7684c979952ee81bee165b9b667dea87199f3fa28dadadc4878b29a12211fdac
4
- data.tar.gz: 2428443ee2456ef375990decfbf36a2952670ee517ac07cf4c994f5b9b7076f9
3
+ metadata.gz: 22f98c39badb0c2128b270ce3092d183f62db4bead34e8df627cf322a3697c71
4
+ data.tar.gz: ea49aeae3da7630f52e8c704bc042752a66b0fefc03a67d713ead33eec1fd006
5
5
  SHA512:
6
- metadata.gz: f59da6b77e69fa2b89761b30a497b4c0fa8f02cbabeed65c953b0f5e807b349e5b305bf31564bcc3eaeed4957215766e8781af5a5b8bbd9754e0461fa18f7b87
7
- data.tar.gz: 733a15ef91abcbfc079a3bbbb83f82686038059b3c518c1c0a279e8000ad885c65cc5019cc691e2731fa1e59ca1f62e8fe95c8f454837ed0bdc65e181157ffc7
6
+ metadata.gz: e9e57e1eafda6e41112b8af0cc8d23eac629e0964b441622c48173f936f45aac95d0482b3545c5feb047dee755889c30ac63d0f63332e59d36cdff5cb1800198
7
+ data.tar.gz: 481b9e52062350a9923c3b0195aea555ca0b8cb0c95fa95e76d3e7cd45a27e21a817ad259aed6e1cf9d90c74c17e99b8398066e84deda7e422247a1ea59254b9
data/CHANGELOG.md CHANGED
@@ -1,14 +1,44 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2023-11-25
4
+
5
+ ### Added
6
+
7
+ - Add sessions for compatibility with `Sidekiq::Web` (#35).
8
+ - Add logger (#33).
9
+
10
+ ### Fixed
11
+
12
+ - Fixes for `FiberScheduler#io_wait` and `FiberScheduler#io_read` (#32).
13
+ - Correctly handle exceptions in inner fibers (#34).
14
+ - Fixes for `FiberScheduler#kernel_sleep` (#36).
15
+
16
+ ### Changed
17
+
18
+ - Use config namespaces (#25).
19
+ - Update `Fiber.await` signature (#36).
20
+
21
+ ## [0.4.0] - 2023-10-31
22
+
23
+ ### Added
24
+
25
+ - Expose the `params` object (#23).
26
+ - Support header authentication with `authenticate_with_http_token` (#21).
27
+ - Add the `resources` route helper (#20).
28
+ - Add the `namespace` route helper by [@arikarim](https://github.com/arikarim) (#17).
29
+ - Add the `mount` and `match` route helpers by [@arikarim](https://github.com/arikarim) (#18) (#14).
30
+ - Allow to access request headers by [@arikarim](https://github.com/arikarim) (#15).
31
+ - Support custom ports when starting the app with `rage s`.
32
+
3
33
  ## [0.3.0] - 2023-10-08
4
34
 
5
35
  ### Added
6
36
 
7
- - CLI `routes` task.
8
- - CLI `console` task.
9
- - `:if` and `:unless` options in `before_action`.
10
- - Allow to set response headers.
11
- - Block version of `before_action`.
37
+ - CLI `routes` task by [@arikarim](https://github.com/arikarim) (#9).
38
+ - CLI `console` task (#12).
39
+ - `:if` and `:unless` options in `before_action` (#10).
40
+ - Allow to set response headers (#11).
41
+ - Block version of `before_action` by [@heysyam99](https://github.com/heysyam99) (#8).
12
42
 
13
43
  ## [0.2.0] - 2023-09-27
14
44
 
data/Gemfile CHANGED
@@ -8,6 +8,9 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
+ gem "http"
12
+ gem "yard"
11
13
 
12
14
  gem "pg"
13
15
  gem "mysql2"
16
+ gem "connection_pool", "~> 2.0"
data/README.md CHANGED
@@ -11,7 +11,7 @@ Inspired by [Deno](https://deno.com) and built on top of [Iodine](https://github
11
11
 
12
12
  * **High performance** - some think performance is not a major metric for a framework, but it's not true. Poor performance is a risk, and in today's world, companies refuse to use risky technologies.
13
13
 
14
- * **API-only** - the only technology we should be using to create web UI is JavaScript. Check out [Vite](https://vitejs.dev) if you don't know where to start.
14
+ * **API-only** - the only technology we should be using to create web UI is JavaScript. Using native technologies is always the most flexible, scalable, and simple solution in the long run. Check out [Vite](https://vitejs.dev) if you don't know where to start.
15
15
 
16
16
  * **Acceptance of modern Ruby** - the framework includes a fiber scheduler, which means your code never blocks while waiting on IO.
17
17
 
@@ -46,9 +46,10 @@ This gem is designed to be a drop-in replacement for Rails in API mode. Public A
46
46
 
47
47
  Check out in-depth API docs for more information:
48
48
 
49
- - [Controller API](https://rage-rb.github.io/rage/RageController/API.html)
50
- - [Routing API](https://rage-rb.github.io/rage/Rage/Router/DSL/Handler.html)
51
- - [Fiber API](https://rage-rb.github.io/rage/Fiber.html)
49
+ - [Controller API](https://rage-rb.pages.dev/RageController/API)
50
+ - [Routing API](https://rage-rb.pages.dev/Rage/Router/DSL/Handler)
51
+ - [Fiber API](https://rage-rb.pages.dev/Fiber)
52
+ - [Logger API](https://rage-rb.pages.dev/Rage/Logger)
52
53
 
53
54
  Also, see the [changelog](https://github.com/rage-rb/rage/blob/master/CHANGELOG.md) and [upcoming-releases](https://github.com/rage-rb/rage#upcoming-releases) for currently supported and planned features.
54
55
 
@@ -98,10 +99,10 @@ require "net/http"
98
99
 
99
100
  class PagesController < RageController::API
100
101
  def index
101
- pages = Fiber.await(
102
+ pages = Fiber.await([
102
103
  Fiber.schedule { Net::HTTP.get(URI("https://httpbin.org/json")) },
103
104
  Fiber.schedule { Net::HTTP.get(URI("https://httpbin.org/html")) },
104
- )
105
+ ])
105
106
 
106
107
  render json: { pages: pages }
107
108
  end
@@ -143,12 +144,13 @@ Version | Changes
143
144
  ------- |------------
144
145
  0.2 :white_check_mark: | ~~Gem configuration by env.<br>Add `skip_before_action`.<br>Add `rescue_from`.<br>Router updates:<br>&emsp;• make the `root` helper work correctly with `scope`;<br>&emsp;• support the `defaults` option;~~
145
146
  0.3 :white_check_mark: | ~~CLI updates:<br>&emsp;• `routes` task;<br>&emsp;• `console` task;<br>Support the `:if` and `:unless` options in `before_action`.<br>Allow to set response headers.~~
146
- 0.4 | Expose the `params` object.<br>Support header authentication with `authenticate_with_http_token`.<br>Router updates:<br>&emsp;• add the `resources` route helper;<br>&emsp;• add the `namespace` route helper;<br>&emsp;• support regexp constraints;
147
- 0.5 | Implement Iodine-based equivalent of `ActionController::Live`.<br>Use `ActionDispatch::RemoteIp`.
148
- 0.6 | Expose the `cookies` object.<br>Expose the `send_data` and `send_file` methods.<br>Support conditional get with `etag` and `last_modified`.
149
- 0.7 | Add request logging.
150
- 0.8 | Collect app metrics.
151
- 0.9 | Automatic code reloading in development.
147
+ 0.4 :white_check_mark: | ~~Expose the `params` object.<br>Support header authentication with `authenticate_with_http_token`.<br>Router updates:<br>&emsp;• add the `resources` route helper;<br>&emsp;• add the `namespace` route helper;~~
148
+ 0.5 :white_check_mark: | ~~Add request logging.~~
149
+ 0.6 | Automatic code reloading in development with Zeitwerk.
150
+ 0.7 | Expose the `send_data` and `send_file` methods.
151
+ 0.8 | Support conditional get with `etag` and `last_modified`.
152
+ 0.9 | Expose the `cookies` and `session` objects.
153
+ 1.0 | Implement Iodine-based equivalent of Action Cable.
152
154
 
153
155
  ## Development
154
156
 
data/lib/rage/all.rb ADDED
@@ -0,0 +1,27 @@
1
+ require_relative "../rage-rb"
2
+
3
+ require_relative "version"
4
+ require_relative "application"
5
+ require_relative "fiber"
6
+ require_relative "fiber_scheduler"
7
+ require_relative "configuration"
8
+ require_relative "request"
9
+ require_relative "uploaded_file"
10
+ require_relative "errors"
11
+ require_relative "params_parser"
12
+
13
+ require_relative "router/strategies/host"
14
+ require_relative "router/backend"
15
+ require_relative "router/constrainer"
16
+ require_relative "router/dsl"
17
+ require_relative "router/handler_storage"
18
+ require_relative "router/node"
19
+
20
+ require_relative "controller/api"
21
+
22
+ require_relative "logger/text_formatter"
23
+ require_relative "logger/logger"
24
+
25
+ if defined?(Sidekiq)
26
+ require_relative "sidekiq_session"
27
+ end
@@ -10,20 +10,25 @@ class Rage::Application
10
10
 
11
11
  def call(env)
12
12
  fiber = Fiber.schedule do
13
+ init_logger
14
+
13
15
  handler = @router.lookup(env)
14
16
 
15
- if handler
16
- handler[:handler].call(env, handler[:params])
17
+ response = if handler
18
+ params = Rage::ParamsParser.prepare(env, handler[:params])
19
+ handler[:handler].call(env, params)
17
20
  else
18
21
  [404, {}, ["Not Found"]]
19
22
  end
20
23
 
21
- rescue => e
22
- [500, {}, ["#{e.class}:#{e.message}\n\n#{e.backtrace.join("\n")}"]]
24
+ rescue Exception => e
25
+ exception_str = "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}"
26
+ Rage.logger.error(exception_str)
27
+ response = [500, {}, [exception_str]]
23
28
 
24
29
  ensure
25
- # notify Iodine the request can now be served
26
- Iodine.publish(env["IODINE_REQUEST_ID"], "")
30
+ finalize_logger(env, response, params)
31
+ Iodine.publish(env["IODINE_REQUEST_ID"], "") # notify Iodine the request can now be served
27
32
  end
28
33
 
29
34
  # the fiber encountered blocking IO and yielded; instruct Iodine to pause the request;
@@ -33,4 +38,28 @@ class Rage::Application
33
38
  fiber.__get_result
34
39
  end
35
40
  end
41
+
42
+ private
43
+
44
+ DEFAULT_LOG_CONTEXT = {}.freeze
45
+ private_constant :DEFAULT_LOG_CONTEXT
46
+
47
+ def init_logger
48
+ Thread.current[:rage_logger] = {
49
+ tags: [Iodine::Rack::Utils.gen_request_tag],
50
+ context: DEFAULT_LOG_CONTEXT,
51
+ request_start: Process.clock_gettime(Process::CLOCK_MONOTONIC)
52
+ }
53
+ end
54
+
55
+ def finalize_logger(env, response, params)
56
+ logger = Thread.current[:rage_logger]
57
+
58
+ duration = (
59
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - logger[:request_start]) * 1000
60
+ ).round(2)
61
+
62
+ logger[:final] = { env:, params:, response:, duration: }
63
+ Rage.logger.info("")
64
+ end
36
65
  end
data/lib/rage/cli.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "thor"
3
- require "rage"
4
- require "irb"
3
+ require "rack"
5
4
 
6
5
  module Rage
7
6
  class CLI < Thor
@@ -11,17 +10,19 @@ module Rage
11
10
 
12
11
  desc "new PATH", "Create a new application."
13
12
  def new(path)
13
+ require "rage/all"
14
14
  NewAppGenerator.start([path])
15
15
  end
16
16
 
17
17
  desc "s", "Start the app server."
18
+ option :port, aliases: "-p", desc: "Runs Rage on the specified port - defaults to 3000."
18
19
  def server
19
20
  app = ::Rack::Builder.parse_file("config.ru")
20
21
  app = app[0] if app.is_a?(Array)
21
22
 
22
- ::Iodine.listen service: :http, handler: app, port: Rage.config.port
23
- ::Iodine.threads = Rage.config.threads_count
24
- ::Iodine.workers = Rage.config.workers_count
23
+ ::Iodine.listen service: :http, handler: app, port: options[:port] || Rage.config.server.port
24
+ ::Iodine.threads = Rage.config.server.threads_count
25
+ ::Iodine.workers = Rage.config.server.workers_count
25
26
 
26
27
  ::Iodine.start
27
28
  end
@@ -30,62 +31,60 @@ module Rage
30
31
  option :grep, aliases: "-g", desc: "Filter routes by pattern"
31
32
  def routes
32
33
  # the result would be something like this:
33
- # Action Verb Path Controller#Action
34
- # index GET / application#index
34
+ # Verb Path Controller#Action
35
+ # GET / application#index
35
36
 
36
37
  # load config/application.rb
37
38
  environment
38
39
 
39
40
  routes = Rage.__router.routes
40
-
41
41
  pattern = options[:grep]
42
+ routes.unshift({ method: "Verb", path: "Path", meta: { raw_handler: "Controller#Action" } })
42
43
 
43
- if pattern
44
- routes = routes.select do |route|
45
- route[:path].match?(pattern) || route[:raw_handler].to_s.match?(pattern) || route[:method].match?(pattern)
44
+ grouped_routes = routes.each_with_object({}) do |route, memo|
45
+ if pattern && !memo.empty?
46
+ next unless route[:path].match?(pattern) || route[:meta][:raw_handler].to_s.match?(pattern) || route[:method].match?(pattern)
47
+ end
48
+
49
+ key = [route[:path], route[:meta][:raw_handler]]
50
+
51
+ if route[:meta][:mount]
52
+ memo[key] = route.merge(method: "") unless route[:path].end_with?("*")
53
+ next
54
+ end
55
+
56
+ if memo[key]
57
+ memo[key][:method] += "|#{route[:method]}"
58
+ else
59
+ memo[key] = route
46
60
  end
47
61
  end
48
62
 
49
- return puts 'Action Verb Path Controller#Action' if routes.empty?
50
-
51
- # construct a table
52
- table = []
53
-
54
- # longest_path is either the length of the longest path or 5
55
- longest_path = routes.map { |route| route[:path].length }.max + 3
56
- longest_path = longest_path > 5 ? longest_path : 5
57
-
58
- longest_verb = routes.map { |route| route[:method].length }.max + 3
59
- longest_verb = longest_verb > 4 ? longest_verb : 7
60
-
61
- # longest_handler is either the length of the longest handler or 7, since DELETE is the longest HTTP method
62
- longest_handler = routes.map { |route| route[:raw_handler].is_a?(Proc) ? 7 : route[:raw_handler].split('#').last.length }.max + 3
63
- longest_handler = longest_handler > 7 ? longest_handler : 7
64
-
65
- # longest_controller is either the length of the longest controller or 12, since Controller#{length} is the longest controller
66
- longest_controller = routes.map { |route| route[:raw_handler].is_a?(Proc) ? 7 : route[:raw_handler].to_s.length }.max + 3
67
- longest_controller = longest_controller > 12 ? longest_controller : 12
68
-
69
- routes.each do |route|
70
- table << [
71
- format("%- #{longest_handler}s", route[:raw_handler].is_a?(Proc) ? 'Lambda' : route[:raw_handler].split('#').last),
72
- format("%- #{longest_verb}s", route[:method]),
73
- format("%- #{longest_path}s", route[:path]),
74
- format("%- #{longest_controller}s", route[:raw_handler].is_a?(Proc) ? 'Lambda' : route[:raw_handler])
75
- ]
63
+ longest_path = longest_method = 0
64
+ grouped_routes.each do |_, route|
65
+ longest_path = route[:path].length if route[:path].length > longest_path
66
+ longest_method = route[:method].length if route[:method].length > longest_method
76
67
  end
77
68
 
78
- table.unshift([format("%- #{longest_handler}s", 'Action'), format("%- #{longest_verb}s", 'Verb'), format("%- #{longest_path}s", 'Path'),
79
- format("%- #{longest_path}s", "Controller#Action\n")])
80
- # print the table
81
- table.each do |row|
82
- # this should be changed to use the main logger when added
83
- puts row.join
69
+ margin = 3
70
+ longest_path += margin
71
+ longest_method += margin
72
+
73
+ grouped_routes.each_with_index do |(_, route), i|
74
+ meta = route[:constraints]
75
+ meta.merge!(route[:defaults]) if route[:defaults]
76
+
77
+ handler = route[:meta][:raw_handler]
78
+ handler = "#{handler} #{meta}" unless meta&.empty?
79
+
80
+ puts format("%-#{longest_method}s%-#{longest_path}s%s", route[:method], route[:path], handler)
81
+ puts "\n" if i == 0
84
82
  end
85
83
  end
86
84
 
87
85
  desc "c", "Start the app console."
88
86
  def console
87
+ require "irb"
89
88
  environment
90
89
  ARGV.clear
91
90
  IRB.start
@@ -1,10 +1,26 @@
1
1
  class Rage::Configuration
2
- attr_accessor :port, :workers_count
3
- attr_reader :threads_count
2
+ attr_accessor :logger, :log_formatter, :log_level
4
3
 
5
- def initialize
6
- @threads_count = 1
7
- @workers_count = -1
8
- @port = 3000
4
+ # used in DSL
5
+ def config = self
6
+
7
+ def server
8
+ @server ||= Server.new
9
+ end
10
+
11
+ class Server
12
+ attr_accessor :port, :workers_count
13
+ attr_reader :threads_count
14
+
15
+ def initialize
16
+ @threads_count = 1
17
+ @workers_count = -1
18
+ @port = 3000
19
+ end
20
+ end
21
+
22
+ def __finalize
23
+ @logger.formatter = @log_formatter if @logger && @log_formatter
24
+ @logger.level = @log_level if @logger && @log_level
9
25
  end
10
26
  end
@@ -50,7 +50,7 @@ class RageController::API
50
50
  ""
51
51
  end
52
52
 
53
- class_eval <<-RUBY
53
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
54
54
  def __run_#{action}
55
55
  #{before_actions_chunk}
56
56
  #{action}
@@ -103,7 +103,7 @@ class RageController::API
103
103
  # rescue_from User::NotAuthorized do |_|
104
104
  # head :forbidden
105
105
  # end
106
- # @note Unlike Rails, the handler must always take an argument. Use `_` if you don't care about the actual exception.
106
+ # @note Unlike in Rails, the handler must always take an argument. Use `_` if you don't care about the actual exception.
107
107
  def rescue_from(*klasses, with: nil, &block)
108
108
  unless with
109
109
  if block_given?
@@ -167,7 +167,7 @@ class RageController::API
167
167
  if: _if,
168
168
  unless: _unless
169
169
  }
170
-
170
+
171
171
  action[:if] = define_tmp_method(action[:if]) if action[:if].is_a?(Proc)
172
172
  action[:unless] = define_tmp_method(action[:unless]) if action[:unless].is_a?(Proc)
173
173
 
@@ -189,7 +189,7 @@ class RageController::API
189
189
  # skip_before_action :find_photo, only: :create
190
190
  def skip_before_action(action_name, only: nil, except: nil)
191
191
  i = @__before_actions&.find_index { |a| a[:name] == action_name }
192
- raise "The following action was specified to be skipped but cannot be found: #{self}##{action_name}" unless i
192
+ raise "The following action was specified to be skipped but couldn't be found: #{self}##{action_name}" unless i
193
193
 
194
194
  @__before_actions = @__before_actions.dup if @__before_actions.frozen?
195
195
 
@@ -221,6 +221,11 @@ class RageController::API
221
221
  @__rendered = false
222
222
  end
223
223
 
224
+ # Get the request object. See {Rage::Request}.
225
+ def request
226
+ @request ||= Rage::Request.new(@__env)
227
+ end
228
+
224
229
  # Send a response to the client.
225
230
  #
226
231
  # @param json [String, Object] send a json response to the client; objects like arrays will be serialized automatically
@@ -283,4 +288,47 @@ class RageController::API
283
288
  @__headers = {}.merge!(@__headers) if DEFAULT_HEADERS.equal?(@__headers)
284
289
  @__headers
285
290
  end
291
+
292
+ # Authenticate using an HTTP Bearer token. Returns the value of the block if a token is found. Returns `nil` if no token is found.
293
+ #
294
+ # @yield [token] token value extracted from the `Authorization` header
295
+ # @example
296
+ # user = authenticate_with_http_token do |token|
297
+ # User.find_by(key: token)
298
+ # end
299
+ def authenticate_with_http_token
300
+ auth_header = @__env["HTTP_AUTHORIZATION"]
301
+
302
+ if auth_header&.start_with?("Bearer")
303
+ yield auth_header[7..]
304
+ elsif auth_header&.start_with?("Token")
305
+ yield auth_header[6..]
306
+ end
307
+ end
308
+
309
+ if !defined?(::ActionController::Parameters)
310
+ # Get the request data. The keys inside the hash are symbols, so `params.keys` returns an array of `Symbol`.<br>
311
+ # You can also load Strong Params to have Rage automatically wrap `params` in an instance of `ActionController::Parameters`.<br>
312
+ # At the same time, if you are not implementing complex filtering rules or working with nested structures, consider using native `Hash#fetch` and `Hash#slice` instead.
313
+ #
314
+ # For multipart file uploads, the uploaded files are represented by an instance of {Rage::UploadedFile}.
315
+ #
316
+ # @return [Hash{Symbol=>String,Array,Hash,Numeric,NilClass,TrueClass,FalseClass}]
317
+ # @example
318
+ # # make sure to load strong params before the `require "rage/all"` call
319
+ # require "active_support/all"
320
+ # require "action_controller/metal/strong_parameters"
321
+ #
322
+ # params.permit(:user).require(:full_name, :dob)
323
+ # @example
324
+ # # without strong params
325
+ # params.fetch(:user).slice(:full_name, :dob)
326
+ def params
327
+ @__params
328
+ end
329
+ else
330
+ def params
331
+ @params ||= ActionController::Parameters.new(@__params)
332
+ end
333
+ end
286
334
  end
@@ -0,0 +1,4 @@
1
+ module Rage::Errors
2
+ class BadRequest < StandardError
3
+ end
4
+ end
data/lib/rage/fiber.rb CHANGED
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Fiber
4
+ AWAIT_ERROR_MESSAGE = "err"
5
+
2
6
  # @private
3
7
  def __set_result(result)
4
8
  @__result = result
@@ -9,30 +13,74 @@ class Fiber
9
13
  @__result
10
14
  end
11
15
 
16
+ # @private
17
+ def __set_err(err)
18
+ @__err = err
19
+ end
20
+
21
+ # @private
22
+ def __get_err
23
+ @__err
24
+ end
25
+
26
+ # @private
27
+ # pause a fiber and resume in the next iteration of the event loop
28
+ def self.pause
29
+ f = Fiber.current
30
+ Iodine.defer { f.resume }
31
+ Fiber.yield
32
+ end
33
+
12
34
  # Wait on several fibers at the same time. Calling this method will automatically pause the current fiber, allowing the
13
35
  # server to process other requests. Once all fibers have completed, the current fiber will be automatically resumed.
14
36
  #
15
37
  # @param fibers [Fiber, Array<Fiber>] one or several fibers to wait on. The fibers must be created using the `Fiber.schedule` call.
16
38
  # @example
17
- # Fiber.await(
39
+ # Fiber.await([
18
40
  # Fiber.schedule { request_1 },
19
41
  # Fiber.schedule { request_2 },
20
- # )
42
+ # ])
21
43
  # @note This method should only be used when multiple fibers have to be processed in parallel. There's no need to use `Fiber.await` for single IO calls.
22
- def self.await(*fibers)
23
- f = Fiber.current
44
+ def self.await(fibers)
45
+ f, fibers = Fiber.current, Array(fibers)
24
46
 
25
- num_wait_for = fibers.count(&:alive?)
26
- return fibers.map(&:__get_result) if num_wait_for == 0
47
+ # check which fibers are alive (i.e. have yielded) and which have errored out
48
+ i, err, num_wait_for = 0, nil, 0
49
+ while i < fibers.length
50
+ if fibers[i].alive?
51
+ num_wait_for += 1
52
+ else
53
+ err = fibers[i].__get_err
54
+ break if err
55
+ end
56
+ i += 1
57
+ end
58
+
59
+ # raise if one of the fibers has errored out or return the result if none have yielded
60
+ if err
61
+ raise err
62
+ elsif num_wait_for == 0
63
+ return fibers.map!(&:__get_result)
64
+ end
27
65
 
28
- Iodine.subscribe("await:#{f.object_id}") do
29
- num_wait_for -= 1
30
- f.resume if num_wait_for == 0
66
+ # wait on async fibers; resume right away if one of the fibers errors out
67
+ Iodine.subscribe("await:#{f.object_id}") do |_, err|
68
+ if err == AWAIT_ERROR_MESSAGE
69
+ f.resume
70
+ else
71
+ num_wait_for -= 1
72
+ f.resume if num_wait_for == 0
73
+ end
31
74
  end
32
75
 
33
76
  Fiber.yield
34
77
  Iodine.defer { Iodine.unsubscribe("await:#{f.object_id}") }
35
78
 
36
- fibers.map(&:__get_result)
79
+ # if num_wait_for is not 0 means we exited prematurely because of an error
80
+ if num_wait_for > 0
81
+ raise fibers.find(&:__get_err).__get_err
82
+ else
83
+ fibers.map!(&:__get_result)
84
+ end
37
85
  end
38
86
  end