rage-rb 1.22.0 → 1.23.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.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ if !defined?(RedisClient)
6
+ fail <<~ERR
7
+
8
+ Redis adapter depends on the `redis-client` gem. Ensure the following line is added to your Gemfile:
9
+ gem "redis-client"
10
+
11
+ ERR
12
+ end
13
+
14
+ class Rage::PubSub::Adapters::Redis
15
+ REDIS_STREAM_NAME = "rage:pubsub:messages"
16
+ DEFAULT_REDIS_OPTIONS = { reconnect_attempts: [0.05, 0.1, 0.5] }
17
+ DEFAULT_POOL_SIZE = 10
18
+ DEFAULT_POOL_TIMEOUT = 1
19
+ REDIS_MIN_VERSION_SUPPORTED = Gem::Version.create(6)
20
+
21
+ def initialize(config)
22
+ @redis_stream = if (prefix = config.delete(:channel_prefix))
23
+ "#{prefix}:#{REDIS_STREAM_NAME}"
24
+ else
25
+ REDIS_STREAM_NAME
26
+ end
27
+
28
+ @pool_size = (config.delete(:pool_size) || DEFAULT_POOL_SIZE).to_i
29
+ @pool_timeout = (config.delete(:pool_timeout) || DEFAULT_POOL_TIMEOUT).to_f
30
+ @redis_config = RedisClient.config(**DEFAULT_REDIS_OPTIONS.merge(config))
31
+ @server_uuid = SecureRandom.uuid
32
+ @broadcasters = {}
33
+
34
+ redis_version = get_redis_version
35
+ if redis_version.nil?
36
+ return
37
+ elsif redis_version < REDIS_MIN_VERSION_SUPPORTED
38
+ raise "Redis adapter only supports Redis 6+. Detected Redis version: #{redis_version}."
39
+ end
40
+
41
+ @trimming_strategy = redis_version < Gem::Version.create("6.2.0") ? :maxlen : :minid
42
+
43
+ Rage::Internal.pick_a_worker do
44
+ puts("INFO: #{Process.pid} is managing Redis subscriptions.") if Rage.logger.info?
45
+ poll
46
+ end
47
+ end
48
+
49
+ def add_broadcaster(broadcaster_id, broadcaster)
50
+ @broadcasters[broadcaster_id] = broadcaster
51
+ end
52
+
53
+ def publish(broadcaster_id, stream_name, data)
54
+ message_uuid = SecureRandom.uuid
55
+
56
+ redis_pool.with do |redis|
57
+ redis.call(
58
+ "XADD",
59
+ @redis_stream,
60
+ trimming_method, "~", trimming_value,
61
+ "*",
62
+ "1", stream_name,
63
+ "2", data.to_json,
64
+ "3", @server_uuid,
65
+ "4", message_uuid,
66
+ "5", broadcaster_id
67
+ )
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def redis_pool
74
+ @redis_pool ||= @redis_config.new_pool(size: @pool_size, timeout: @pool_timeout)
75
+ end
76
+
77
+ def trimming_method
78
+ @trimming_strategy == :maxlen ? "MAXLEN" : "MINID"
79
+ end
80
+
81
+ def trimming_value
82
+ @trimming_strategy == :maxlen ? "10000" : ((Time.now.to_f - 5 * 60) * 1000).to_i
83
+ end
84
+
85
+ def get_redis_version
86
+ service_redis = @redis_config.new_client
87
+ version = service_redis.call("INFO").match(/redis_version:([[:graph:]]+)/)[1]
88
+
89
+ Gem::Version.create(version)
90
+
91
+ rescue RedisClient::Error => e
92
+ puts "FATAL: Couldn't connect to Redis - all broadcasts will be limited to the current server."
93
+ puts e.backtrace.join("\n")
94
+ nil
95
+
96
+ ensure
97
+ service_redis.close
98
+ end
99
+
100
+ def error_backoff_intervals
101
+ @error_backoff_intervals ||= Enumerator.new do |y|
102
+ y << 0.2 << 0.5 << 1 << 2 << 5
103
+ loop { y << 10 }
104
+ end
105
+ end
106
+
107
+ def poll
108
+ unless Fiber.scheduler
109
+ Fiber.set_scheduler(Rage::FiberScheduler.new)
110
+ end
111
+
112
+ Iodine.on_state(:start_shutdown) do
113
+ @stopping = true
114
+ end
115
+
116
+ Fiber.schedule do
117
+ read_redis = @redis_config.new_client
118
+ last_id = (Time.now.to_f * 1000).to_i
119
+ last_message_uuid = nil
120
+
121
+ loop do
122
+ data = read_redis.blocking_call(5, "XREAD", "COUNT", "100", "BLOCK", "5000", "STREAMS", @redis_stream, last_id)
123
+
124
+ if data
125
+ data[@redis_stream].each do |id, (_, stream_name, _, serialized_data, _, server_uuid, _, message_uuid, _, broadcaster_id)|
126
+ if server_uuid != @server_uuid && message_uuid != last_message_uuid
127
+ @broadcasters[broadcaster_id]&.broadcast(stream_name, JSON.parse(serialized_data))
128
+ end
129
+
130
+ last_id = id
131
+ last_message_uuid = message_uuid
132
+ end
133
+ end
134
+
135
+ break if @stopping
136
+
137
+ rescue RedisClient::Error => e
138
+ Rage.logger.error("Subscriber error: #{e.message} (#{e.class})")
139
+ sleep error_backoff_intervals.next
140
+ rescue SystemCallError => e
141
+ @stopping ? break : raise(e)
142
+ else
143
+ error_backoff_intervals.rewind
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,25 @@
1
+ ##
2
+ # The module provides the support for multi-server setups for `Rage::Cable` and `Rage::SSE`. It allows for broadcasting messages across multiple servers or from different runtimes, e.g. Sidekiq.
3
+ #
4
+ # To use the module, add the `redis-client` gem to your Gemfile and create the environment-specific configuration in `config/pubsub.yml`:
5
+ #
6
+ # ```yaml
7
+ # production:
8
+ # adapter: redis
9
+ # url: <%= ENV["REDIS_URL"] %>
10
+ # ```
11
+ #
12
+ # The configuration supports the following options:
13
+ #
14
+ # - `adapter` (required): The adapter to use for Pub/Sub. The only supported value is `redis`.
15
+ # - `channel_prefix` (optional): A prefix to use for the Redis stream name. This can be useful if you want to share a Redis instance with other applications or services.
16
+ # - `pool_size` (optional): The size of the Redis connection pool. Default is 10.
17
+ # - `pool_timeout` (optional): The timeout in seconds for acquiring a connection from the pool. Default is 1 second.
18
+ #
19
+ # The rest of the options are passed directly to `redis-client`.
20
+ #
21
+ module Rage::PubSub
22
+ module Adapters
23
+ autoload :Redis, "rage/pubsub/adapters/redis"
24
+ end
25
+ end
data/lib/rage/rails.rb CHANGED
@@ -54,4 +54,20 @@ Rails.configuration.after_initialize do
54
54
  end
55
55
  end
56
56
 
57
+ # load deferred routes in Rails 8+
58
+ if Rails::VERSION::MAJOR >= 8
59
+ # reset Rage routes before Rails reloads routes
60
+ routes_reloader_patch = Module.new do
61
+ def reload!
62
+ Rage.__router.reset_routes
63
+ super
64
+ end
65
+ end
66
+ Rails::Application::RoutesReloader.prepend(routes_reloader_patch)
67
+
68
+ Rails.application.config.after_initialize do
69
+ Rails.application.reload_routes!
70
+ end
71
+ end
72
+
57
73
  require "rage/ext/setup"
@@ -1,6 +1,6 @@
1
1
  This is an almost complete rewrite of https://github.com/delvedor/find-my-way.
2
2
 
3
- Currrently, the only constraint supported is the `host` constraint. Regexp constraints are likely to be added. Custom/lambda constraints are unlikely to be added.
3
+ Currently, the only constraint supported is the `host` constraint. Regexp constraints are likely to be added. Custom/lambda constraints are unlikely to be added.
4
4
 
5
5
  Compared to the Rails router, the most notable difference except constraints is that a wildcard segment can only be in the last section of the path and cannot be named.
6
6
 
@@ -348,20 +348,12 @@ class Rage::Router::DSL
348
348
  _module, _path, _only, _except, _param = opts.values_at(:module, :path, :only, :except, :param)
349
349
  raise ArgumentError, ":param option can't contain colons" if _param.to_s.include?(":")
350
350
 
351
- _only = Array(_only) if _only
352
- _except = Array(_except) if _except
353
- actions = @default_actions.select do |action|
354
- (_only.nil? || _only.include?(action)) && (_except.nil? || !_except.include?(action))
355
- end
351
+ actions = __filter_actions(@default_actions, _only, _except)
356
352
 
357
353
  resource = _resources[0].to_s
358
- _path ||= resource
359
354
  _param ||= "id"
360
355
 
361
- scope_opts = { path: _path }
362
- scope_opts[:module] = _module if _module
363
-
364
- scope(scope_opts) do
356
+ __resource_scope(resource, _path, _module) do
365
357
  get("/", to: "#{resource}#index") if actions.include?(:index)
366
358
  post("/", to: "#{resource}#create") if actions.include?(:create)
367
359
  get("/:#{_param}", to: "#{resource}#show") if actions.include?(:show)
@@ -373,6 +365,45 @@ class Rage::Router::DSL
373
365
  end
374
366
  end
375
367
 
368
+ # Automatically create REST routes for a singular resource.
369
+ #
370
+ # @param [Hash] opts resource options
371
+ # @option opts [String] :module the namespace for the controller
372
+ # @option opts [String] :path the path prefix for the routes
373
+ # @option opts [Symbol, Array<Symbol>] :only only generate routes for the given actions
374
+ # @option opts [Symbol, Array<Symbol>] :except generate all routes except for the given actions
375
+ # @example Create singular routes mapped to a plural controller:
376
+ # resource :photo
377
+ # # POST /photo => photos#create
378
+ # # GET /photo => photos#show
379
+ # # PATCH /photo => photos#update
380
+ # # PUT /photo => photos#update
381
+ # # DELETE /photo => photos#destroy
382
+ # @note This helper doesn't generate the `new` and `edit` routes.
383
+ # @note :param is not supported for singular resources.
384
+ def resource(*_resources, **opts, &block)
385
+ if _resources.length > 1
386
+ _resources.each { |_resource| resource(_resource, **opts, &block) }
387
+ return
388
+ end
389
+
390
+ _module, _path, _only, _except = opts.values_at(:module, :path, :only, :except)
391
+
392
+ actions = __filter_actions(%i(create show update destroy), _only, _except)
393
+
394
+ resource_name = _resources[0].to_s
395
+ controller_name = to_plural(resource_name)
396
+ __resource_scope(resource_name, _path, _module) do
397
+ post("/", to: "#{controller_name}#create") if actions.include?(:create)
398
+ get("/", to: "#{controller_name}#show") if actions.include?(:show)
399
+ patch("/", to: "#{controller_name}#update") if actions.include?(:update)
400
+ put("/", to: "#{controller_name}#update") if actions.include?(:update)
401
+ delete("/", to: "#{controller_name}#destroy") if actions.include?(:destroy)
402
+
403
+ scope(controller: controller_name, &block) if block
404
+ end
405
+ end
406
+
376
407
  # Mount a Rack-based application to be used within the application.
377
408
  #
378
409
  # @example
@@ -445,6 +476,25 @@ class Rage::Router::DSL
445
476
  end
446
477
  end
447
478
 
479
+ # Filters a list of actions based on :only and :except options
480
+ def __filter_actions(default_actions, only, except)
481
+ only = Array(only) if only
482
+ except = Array(except) if except
483
+
484
+ default_actions.select do |action|
485
+ (only.nil? || only.include?(action)) &&
486
+ (except.nil? || !except.include?(action))
487
+ end
488
+ end
489
+
490
+ # Wraps route definitions in the correct path/module scope
491
+ def __resource_scope(resource, path, mod, &block)
492
+ path ||= resource
493
+ scope_opts = { path: path }
494
+ scope_opts[:module] = mod if mod
495
+ scope(scope_opts, &block)
496
+ end
497
+
448
498
  def to_singular(str)
449
499
  @active_support_loaded ||= str.respond_to?(:singularize) || :false
450
500
  return str.singularize if @active_support_loaded != :false
@@ -462,5 +512,12 @@ class Rage::Router::DSL
462
512
 
463
513
  str.sub(@regexp, @endings)
464
514
  end
515
+
516
+ def to_plural(str)
517
+ @active_support_loaded ||= str.respond_to?(:pluralize) || :false
518
+ return str.pluralize if @active_support_loaded != :false
519
+
520
+ str.end_with?("s") ? str : "#{str}s"
521
+ end
465
522
  end
466
523
  end
@@ -12,6 +12,8 @@ class Rage::SSE::Application
12
12
  :stream
13
13
  elsif @stream.is_a?(Proc)
14
14
  :manual
15
+ elsif @stream.is_a?(Rage::SSE::Stream)
16
+ :broadcast
15
17
  else
16
18
  :single
17
19
  end
@@ -20,7 +22,13 @@ class Rage::SSE::Application
20
22
  end
21
23
 
22
24
  def on_open(connection)
23
- @type == :single ? send_data(connection) : start_stream(connection)
25
+ if @type == :single
26
+ send_data(connection)
27
+ elsif @type == :broadcast
28
+ start_broadcast_stream(connection)
29
+ else
30
+ start_stream(connection)
31
+ end
24
32
  end
25
33
 
26
34
  private
@@ -28,18 +36,35 @@ class Rage::SSE::Application
28
36
  def send_data(connection)
29
37
  Rage::Telemetry.tracer.span_sse_stream_process(connection:, type: @type) do
30
38
  connection.write(Rage::SSE.__serialize(@stream))
31
- connection.close
39
+ end
40
+ ensure
41
+ connection.close
42
+ end
43
+
44
+ def start_broadcast_stream(connection)
45
+ channel = "sse:#{@stream.name}"
46
+
47
+ connection.subscribe(channel) do |_, msg|
48
+ msg == Rage::SSE::CLOSE_STREAM_MSG ? connection.close : connection.write(msg)
49
+ end
50
+
51
+ buffered_messages = Rage::SSE::Stream.__claim_buffered_messages(@stream)
52
+ buffered_messages&.each do |msg|
53
+ msg == Rage::SSE::CLOSE_STREAM_MSG ? connection.close : connection.write(msg)
32
54
  end
33
55
  end
34
56
 
35
57
  def start_stream(connection)
36
58
  Fiber.schedule do
59
+ Iodine.task_inc!
37
60
  Fiber[:__rage_logger_tags], Fiber[:__rage_logger_context] = @log_tags, @log_context
38
61
  Rage::Telemetry.tracer.span_sse_stream_process(connection:, type: @type) do
39
62
  @type == :stream ? start_formatted_stream(connection) : start_raw_stream(connection)
40
63
  end
41
64
  rescue => e
42
65
  Rage.logger.error("SSE stream failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
66
+ ensure
67
+ Iodine.task_dec!
43
68
  end
44
69
  end
45
70
 
@@ -54,5 +79,8 @@ class Rage::SSE::Application
54
79
 
55
80
  def start_raw_stream(connection)
56
81
  @stream.call(Rage::SSE::ConnectionProxy.new(connection))
82
+ rescue => e
83
+ connection.close if connection.open?
84
+ raise e
57
85
  end
58
86
  end
data/lib/rage/sse/sse.rb CHANGED
@@ -14,6 +14,18 @@ module Rage::SSE
14
14
  Message.new(data:, id:, event:, retry:)
15
15
  end
16
16
 
17
+ # A factory method for creating unbounded SSE streams.
18
+ #
19
+ # @param streamable [#id, String, Symbol, Numeric, Array] An object to generate the stream name from.
20
+ # @return [Stream] A new SSE stream instance.
21
+ # @example
22
+ # render sse: Rage::SSE.stream("#{current_user.id}-notifications")
23
+ # @example
24
+ # render sse: Rage::SSE.stream([current_user.id, "notifications"])
25
+ def self.stream(streamable)
26
+ Stream.new(streamable:)
27
+ end
28
+
17
29
  # @private
18
30
  def self.__serialize(data)
19
31
  if data.is_a?(String)
@@ -24,8 +36,92 @@ module Rage::SSE
24
36
  "data: #{data.to_json}\n\n"
25
37
  end
26
38
  end
39
+
40
+ # @private
41
+ def self.__adapter=(adapter)
42
+ @__adapter = adapter
43
+ end
44
+
45
+ # @private
46
+ CLOSE_STREAM_MSG = "rage-close-stream"
47
+
48
+ # @private
49
+ PUBSUB_BROADCASTER_ID = "sse"
50
+
51
+ # Close an unbounded SSE stream. Unbounded streams will remain open until either the client disconnects or the server explicitly closes them.
52
+ #
53
+ # @param streamable [#id, String, Symbol, Numeric, Array] The identifier of the stream to close.
54
+ # @example
55
+ # Rage::SSE.close_stream("#{current_user.id}-notifications")
56
+ # @example
57
+ # Rage::SSE.close_stream([current_user.id, "notifications"])
58
+ def self.close_stream(streamable)
59
+ stream_name = Rage::Internal.stream_name_for(streamable)
60
+
61
+ InternalBroadcast.broadcast(stream_name, CLOSE_STREAM_MSG, Iodine::PubSub::CLUSTER) if Iodine.running?
62
+ @__adapter&.publish(PUBSUB_BROADCASTER_ID, stream_name, CLOSE_STREAM_MSG)
63
+
64
+ true
65
+ end
66
+
67
+ # Broadcast a message to all clients subscribed to a given stream.
68
+ #
69
+ # @param streamable [#id, String, Symbol, Numeric, Array] The identifier of the stream to broadcast to.
70
+ # @param data [String, #to_json, Message] The message to broadcast.
71
+ # @example
72
+ # Rage::SSE.broadcast("#{current_user.id}-notifications", "You have a new notification!")
73
+ # @example
74
+ # Rage::SSE.broadcast([current_user.id, "notifications"], { title: "New Notification", body: "You have a new notification!" })
75
+ def self.broadcast(streamable, data)
76
+ Rage::Telemetry.tracer.span_sse_stream_broadcast(stream: streamable) do
77
+ stream_name = Rage::Internal.stream_name_for(streamable)
78
+ serialized_data = __serialize(data)
79
+
80
+ InternalBroadcast.broadcast(stream_name, serialized_data, Iodine::PubSub::CLUSTER) if Iodine.running?
81
+ @__adapter&.publish(PUBSUB_BROADCASTER_ID, stream_name, serialized_data)
82
+ end
83
+
84
+ true
85
+ end
86
+
87
+ # @private
88
+ module InternalBroadcast
89
+ def self.broadcast(stream_name, data, engine)
90
+ if Rage::SSE::Stream.__message_buffer.has_key?(stream_name)
91
+ Rage::SSE::Stream.__store_message(stream_name, data)
92
+ end
93
+
94
+ Iodine.publish("sse:#{stream_name}", data, engine)
95
+ end
96
+ end
97
+
98
+ # @private
99
+ module Relay
100
+ def self.broadcast(stream_name, data)
101
+ Iodine.publish("sse-relay", "#{stream_name}\x00#{data}")
102
+ end
103
+ end
27
104
  end
28
105
 
29
106
  require_relative "application"
30
107
  require_relative "connection_proxy"
31
108
  require_relative "message"
109
+ require_relative "stream"
110
+
111
+ Rage.config.after_initialize do
112
+ if (adapter = Rage.config.pubsub.adapter)
113
+ Iodine.on_state(:on_start) do
114
+ Iodine.subscribe("sse-relay") do |_, msg|
115
+ stream_name, data = msg.split("\x00", 2)
116
+ Rage::SSE::InternalBroadcast.broadcast(stream_name, data, Iodine::PubSub::PROCESS)
117
+ end
118
+ end
119
+
120
+ Iodine.on_state(:on_finish) do
121
+ Iodine.unsubscribe("sse-relay")
122
+ end
123
+
124
+ adapter.add_broadcaster(Rage::SSE::PUBSUB_BROADCASTER_ID, Rage::SSE::Relay)
125
+ Rage::SSE.__adapter = adapter
126
+ end
127
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The class representing an unbounded Server-Sent Events stream. It allows for broadcasting messages to all connected clients subscribed to the stream.
5
+ #
6
+ # Create a stream:
7
+ #
8
+ # ```ruby
9
+ # render sse: Rage::SSE.stream([current_user, "notifications"])
10
+ # ```
11
+ #
12
+ # Broadcast a message to all connections subscribed to the stream:
13
+ # ```ruby
14
+ # Rage::SSE.broadcast([current_user, "notifications"], "You have a new notification!")
15
+ # ```
16
+ #
17
+ # Close the stream:
18
+ # ```ruby
19
+ # Rage::SSE.close_stream([current_user, "notifications"])
20
+ # ```
21
+ #
22
+ # Messages to known streams are buffered until a connection is fully established:
23
+ # ```ruby
24
+ # # Create a stream first
25
+ # stream = Rage::SSE.stream([current_user, "notifications"])
26
+ #
27
+ # # No connection yet, but the message is buffered
28
+ # Rage::SSE.broadcast([current_user, "notifications"], "You have a new notification!")
29
+ #
30
+ # # Establish a connection, which will claim the buffered message
31
+ # render sse: stream
32
+ # ```
33
+ #
34
+ class Rage::SSE::Stream
35
+ class << self
36
+ # @private
37
+ def __message_buffer
38
+ @__message_buffer ||= Hash.new { |h, k| h[k] = {} }
39
+ end
40
+
41
+ # @private
42
+ def __store_message(stream, message)
43
+ __message_buffer[stream].transform_values! do |buffer|
44
+ buffer.frozen? ? [message] : buffer.push(message)
45
+ end
46
+ end
47
+
48
+ # @private
49
+ def __claim_buffered_messages(stream)
50
+ messages = __message_buffer[stream.name][stream.owner] if __message_buffer.has_key?(stream.name)
51
+ cleanup_message_buffer
52
+
53
+ messages
54
+ end
55
+
56
+ private
57
+
58
+ def cleanup_message_buffer
59
+ __message_buffer.delete_if do |_, connection_buffers|
60
+ connection_buffers.keys.none?(&:alive?)
61
+ end
62
+ end
63
+ end
64
+
65
+ DEFAULT_BUFFER = [].freeze
66
+ private_constant :DEFAULT_BUFFER
67
+
68
+ # @private
69
+ attr_reader :name, :owner
70
+
71
+ # @param streamable [#id, String, Symbol, Numeric, Array] an object that will be used to generate the stream name
72
+ def initialize(streamable:)
73
+ @name = Rage::Internal.stream_name_for(streamable)
74
+ @owner = Fiber.current
75
+
76
+ self.class.__message_buffer[@name][@owner] ||= DEFAULT_BUFFER
77
+ end
78
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The **sse.stream.broadcast** span wraps the process of broadcasting a message to an unbounded SSE stream.
5
+ #
6
+ # This span is started when {Rage::SSE.broadcast Rage::SSE.broadcast} is called, and ends when the broadcast operation is complete.
7
+ # See {handle handle} for the list of arguments passed to handler methods.
8
+ #
9
+ # @see Rage::Telemetry::Handler Rage::Telemetry::Handler
10
+ #
11
+ class Rage::Telemetry::Spans::BroadcastSSEStream
12
+ class << self
13
+ # @private
14
+ def id
15
+ "sse.stream.broadcast"
16
+ end
17
+
18
+ # @private
19
+ def span_parameters
20
+ %w[stream:]
21
+ end
22
+
23
+ # @private
24
+ def handler_arguments
25
+ {
26
+ name: '"Rage::SSE.broadcast"',
27
+ stream: "stream"
28
+ }
29
+ end
30
+
31
+ # @!parse [ruby]
32
+ # # @param id ["sse.stream.broadcast"] ID of the span
33
+ # # @param name ["Rage::SSE.broadcast"] human-readable name of the operation
34
+ # # @param stream [Object] the identifier of the stream to which the message is being broadcasted
35
+ # # @yieldreturn [Rage::Telemetry::SpanResult]
36
+ # #
37
+ # # @example
38
+ # # class MyTelemetryHandler < Rage::Telemetry::Handler
39
+ # # handle "sse.stream.broadcast", with: :my_handler
40
+ # #
41
+ # # def my_handler(id:, name:, stream:)
42
+ # # yield
43
+ # # end
44
+ # # end
45
+ # # @note Rage automatically detects which parameters your handler method accepts and only passes those parameters.
46
+ # # You can omit any of the parameters described here.
47
+ # def handle(id:, name:, stream:)
48
+ # end
49
+ end
50
+ end
@@ -6,6 +6,7 @@
6
6
  # This span starts when a connection is opened and ends when the stream is finished.
7
7
  # See {handle handle} for the list of arguments passed to handler methods.
8
8
  #
9
+ # @note This span is not used for unbounded SSE streams created via {Rage::SSE.stream Rage::SSE.stream}.
9
10
  # @see Rage::Telemetry::Handler Rage::Telemetry::Handler
10
11
  #
11
12
  class Rage::Telemetry::Spans::ProcessSSEStream
@@ -70,7 +70,7 @@ module Rage::Telemetry
70
70
  #
71
71
  # | ID | Reference | Description |
72
72
  # | --- | --- |
73
- # | `core.fiber.dispatch` | {DispatchFiber} | Wraps the scheduling and processing of system-level fibers created by the framework to process requests and deferred tasks |
73
+ # | `core.fiber.dispatch` | {DispatchFiber} | Wraps the scheduling and processing of system-level fibers created by the framework to process requests, deferred tasks, or SSE streams |
74
74
  # | `core.fiber.spawn` | {SpawnFiber} | Wraps the scheduling and processing of application-level fibers created via {Fiber.schedule} |
75
75
  # | `core.fiber.await` | {AwaitFiber} | Wraps the processing of the {Fiber.await} calls |
76
76
  # | `controller.action.process` | {ProcessControllerAction} | Wraps the processing of controller actions |
@@ -83,6 +83,7 @@ module Rage::Telemetry
83
83
  # | `events.event.publish` | {PublishEvent} | Wraps the publishing of events via {Rage::Events Rage::Events} |
84
84
  # | `events.subscriber.process` | {ProcessEventSubscriber} | Wraps the processing of events by subscribers |
85
85
  # | `sse.stream.process` | {ProcessSSEStream} | Wraps the processing of an SSE stream |
86
+ # | `sse.stream.broadcast` | {BroadcastSSEStream} | Wraps the process of broadcasting a message to an unbounded SSE stream |
86
87
  #
87
88
  module Spans
88
89
  end
@@ -7,6 +7,7 @@
7
7
  # of its interface is available directly for convenience.
8
8
  #
9
9
  # Rage will automatically unlink the files, so there is no need to clean them with a separate maintenance task.
10
+ #
10
11
  class Rage::UploadedFile
11
12
  # The basename of the file in the client.
12
13
  attr_reader :original_filename
@@ -29,14 +30,9 @@ class Rage::UploadedFile
29
30
  @file.read(length, buffer)
30
31
  end
31
32
 
32
- # Shortcut for `file.open`.
33
- def open
34
- @file.open
35
- end
36
-
37
33
  # Shortcut for `file.close`.
38
- def close(unlink_now = false)
39
- @file.close(unlink_now)
34
+ def close
35
+ @file.close
40
36
  end
41
37
 
42
38
  # Shortcut for `file.path`.
data/lib/rage/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rage
4
- VERSION = "1.22.0"
4
+ VERSION = "1.23.0"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -191,10 +191,11 @@ module Rage
191
191
  autoload :OpenAPI, "rage/openapi/openapi"
192
192
  autoload :Deferred, "rage/deferred/deferred"
193
193
  autoload :Events, "rage/events/events"
194
- autoload :SSE, "rage/sse/sse"
194
+ autoload :PubSub, "rage/pubsub/pubsub"
195
195
  end
196
196
 
197
197
  module RageController
198
+ autoload :Renderers, "rage/controller/renderers"
198
199
  end
199
200
 
200
201
  require_relative "rage/env"