onyxcord 1.1.8 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07ea78b3a647f1f03a640e750eef4e5a99595f6afa66ce9be842d012c5e8e2c1
4
- data.tar.gz: '08f1edd1b05510983d80766f627db7372edb8f3bce6234ba9cb4d669e9cf2070'
3
+ metadata.gz: '00639da6b7f1bb86015ad2789db3c8a31a8a2865d26d594bb0bdf15914dbb534'
4
+ data.tar.gz: 0ac2f4e68b91633c3d1006faf2def1d6df6a99a9898fc2c38de819d5048f41f4
5
5
  SHA512:
6
- metadata.gz: '092c91e1b949b091079169f2977c44745fea551a93e2cc3d065df2b9c51b534cbb027a01519d781dfc0fe2f783c360eecccee29d46b4f0bb28cd99350bde56cf'
7
- data.tar.gz: 076604ea2c7a9a6a0c9303fedfbd5e1cac58cb11cbc741d88afff80a838f80da5637a292b494a6d84ae63da4ce5fc435c8eb3548dcb833961dff588ff6b24974
6
+ metadata.gz: ae1eab8467d518c3bd98148274b3bc152e1c9d8b72696c27fe621e069a7d2ff75ff33dddfda22b00702f25a85b38702a31accaf1d00d0366d831cc96b38cea3b
7
+ data.tar.gz: dc2bb8086656c80b8d97c7992f99a75aed3efd7481a9de4c52713edbbc02536ab6074d27931b88430b92a72a3b5412e58fdff8ee5ccb4e936edd19f08c7edbc0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0 - 2026-06-28
4
+
5
+ ### Arquitetura & Performance (Major Refactoring)
6
+
7
+ - **Runtime 100% Async**: A infraestrutura de threads (`Thread.new`) foi substituída pelo modelo de fibers do Ruby usando a gem `async`. Gateway, heartbeats, fila REST, worker de eventos e comandos agora executam de forma não-bloqueante no reator Async.
8
+ - **REST Moderno via HTTPX**: Substituída a gem legada `rest-client` pela `httpx`, trazendo suporte nativo a conexões persistentes (Keep-Alive), HTTP/2, multipart uploads nativos e retries automáticos em erros 502.
9
+ - **Gateway via Async-WebSocket**: Substituída a implementação de raw TCP sockets + `websocket-client-simple` por `async-websocket`, proporcionando um event loop de gateway extremamente rápido e escalável.
10
+ - **Parse JSON via Oj**: A gem `oj` foi integrada em modo de compatibilidade (`mode: :compat`), acelerando transparentemente todas as serializações e deserializações de pacotes do Discord na lib inteira.
11
+ - **Cache Inteligente LRU**: Os caches em memória (usuários, canais, servidores, membros) agora utilizam `LruRedux::ThreadSafeCache`. Os tamanhos padrão foram aumentados (`users: 50_000`, `channels: 10_000`, `servers: 1_000`, `members: 100_000`) e podem ser customizados via `OnyxCord.configure { |c| c.cache_sizes.users = 100_000 }`.
12
+ - **Fusão do Webhooks**: A funcionalidade da gem separada `onyxcord-webhooks` foi integrada diretamente no núcleo da gem `onyxcord`. A gem `onyxcord-webhooks` agora atua apenas como um shim de transição deprecado.
13
+ - **Alvo Ruby ≥ 3.4**: Atualizada a versão mínima requerida do Ruby para aproveitar as otimizações modernas do interpretador e fibras.
14
+
3
15
  ## 1.1.8 - 2026-06-28
4
16
 
5
17
  ### Correcoes
@@ -10,6 +22,7 @@
10
22
  ### Validacao
11
23
 
12
24
  - `bundle exec rspec spec/components_v2_spec.rb`: sucesso.
25
+ - `bundle exec rspec`: 460 exemplos, 0 falhas, 3 pendentes.
13
26
  - `ruby -c lib/onyxcord/data/component.rb`: sucesso.
14
27
  - `ruby -c spec/components_v2_spec.rb`: sucesso.
15
28
  - `gem build onyxcord.gemspec`: sucesso.
data/Gemfile CHANGED
@@ -4,4 +4,3 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in onyxcord.gemspec
6
6
  gemspec name: 'onyxcord'
7
- gemspec name: 'onyxcord-webhooks', development_group: 'webhooks'
@@ -23,7 +23,6 @@ module OnyxCord::API::Channel
23
23
  { **files, payload_json: body.to_json }
24
24
  end
25
25
 
26
-
27
26
  # Get a channel's data
28
27
  # https://discord.com/developers/docs/resources/channel#get-channel
29
28
  def resolve(token, channel_id)
@@ -111,7 +110,7 @@ module OnyxCord::API::Channel
111
110
  # @param attachments [Array<File>, nil] Attachments to use with `attachment://` in embeds. See
112
111
  # https://discord.com/developers/docs/resources/channel#create-message-using-attachments-within-embeds
113
112
  def create_message(token, channel_id, message, tts = false, embeds = nil, nonce = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = nil, enforce_nonce = false, poll = nil)
114
- tts = false unless tts == true || tts == false
113
+ tts = false unless [true, false].include?(tts)
115
114
  components = OnyxCord::MessageComponents.payload(components) unless components.nil?
116
115
  flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components)
117
116
  body = { content: message, tts: tts == true, embeds: embeds, nonce: nonce, allowed_mentions: allowed_mentions, message_reference: message_reference, components: components, attachments: attachments ? attachment_payload(attachments) : nil, flags: flags, enforce_nonce: enforce_nonce, poll: poll }.compact
@@ -132,11 +131,6 @@ module OnyxCord::API::Channel
132
131
  body,
133
132
  **headers
134
133
  )
135
- rescue RestClient::BadRequest => e
136
- parsed = JSON.parse(e.response.body)
137
- raise OnyxCord::Errors::MessageTooLong, "Message over the character limit (#{message.length} > 2000)" if parsed['content'].is_a?(Array) && parsed['content'].first == 'Must be 2000 or fewer in length.'
138
-
139
- raise
140
134
  end
141
135
 
142
136
  # Send a file as a message to a channel
@@ -425,12 +419,6 @@ module OnyxCord::API::Channel
425
419
  Authorization: token,
426
420
  content_type: :json
427
421
  )
428
- rescue RestClient::InternalServerError
429
- raise 'Attempted to add self as a new group channel recipient!'
430
- rescue RestClient::NoContent
431
- raise 'Attempted to create a group channel with the PM channel recipient!'
432
- rescue RestClient::Forbidden
433
- raise 'Attempted to add a user to group channel without permission!'
434
422
  end
435
423
 
436
424
  # Add a user to a group channel.
@@ -62,7 +62,7 @@ module OnyxCord::API::Webhook
62
62
  end
63
63
 
64
64
  headers = { content_type: :json } unless file || attachments
65
- with_components = components&.any? ? true : nil
65
+ with_components = components&.any? || nil
66
66
  query = URI.encode_www_form({ wait: wait, with_components: with_components }.compact)
67
67
 
68
68
  OnyxCord::API.request(
data/lib/onyxcord/api.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rest-client'
4
- require 'json'
3
+ require 'onyxcord/http'
4
+ require 'onyxcord/json'
5
5
  require 'time'
6
6
 
7
7
  require 'onyxcord/errors'
@@ -55,7 +55,7 @@ module OnyxCord::API
55
55
  required = "DiscordBot (https://github.com/kruldevb/OnyxCord, v#{OnyxCord::VERSION})"
56
56
  @bot_name ||= ''
57
57
 
58
- "#{required} rest-client/#{RestClient::VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} onyxcord/#{OnyxCord::VERSION} #{@bot_name}"
58
+ "#{required} httpx/#{HTTPX::VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} onyxcord/#{OnyxCord::VERSION} #{@bot_name}"
59
59
  end
60
60
 
61
61
  # Resets all rate limit mutexes
@@ -82,49 +82,96 @@ module OnyxCord::API
82
82
  mutex.unlock
83
83
  end
84
84
 
85
- # Performs a RestClient request.
85
+ # Performs a raw HTTP request using HTTPX.
86
86
  # @param type [Symbol] The type of HTTP request to use.
87
- # @param attributes [Array] The attributes for the request.
88
- def raw_request(type, attributes)
89
- RestClient.send(type, *attributes)
90
- rescue RestClient::Forbidden => e
91
- # HACK: for #request, dynamically inject restclient's response into NoPermission - this allows us to rate limit
92
- noprm = OnyxCord::Errors::NoPermission.new
93
- noprm.define_singleton_method(:_rc_response) { e.response }
94
- raise noprm, "The bot doesn't have the required permission to do this!"
95
- rescue RestClient::BadGateway
96
- OnyxCord::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
97
- retry
87
+ # @param url [String] The URL to request.
88
+ # @param body [String, Hash, nil] The request body.
89
+ # @param headers [Hash] Additional headers.
90
+ # @return [OnyxCord::HTTP::Response]
91
+ def raw_request(type, url, body = nil, **headers)
92
+ headers[:user_agent] = user_agent
93
+
94
+ response = OnyxCord::HTTP.request(type, url, body, **headers)
95
+
96
+ if response.code == 403
97
+ noprm = OnyxCord::Errors::NoPermission.new
98
+ noprm.define_singleton_method(:_response) { response }
99
+ raise noprm, "The bot doesn't have the required permission to do this!"
100
+ end
101
+
102
+ # Retry on 502 Bad Gateway
103
+ if response.code == 502
104
+ OnyxCord::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
105
+ return raw_request(type, url, body, **headers)
106
+ end
107
+
108
+ response
98
109
  end
99
110
 
100
111
  # Make an API request, including rate limit handling.
101
112
  def request(key, major_parameter, type, *attributes)
102
- # Add a custom user agent
103
- attributes.last[:user_agent] = user_agent if attributes.last.is_a? Hash
113
+ # Parse attributes: URL is first, body is second (if present), rest is headers hash
114
+ url = attributes.shift
115
+ headers_or_body = attributes
116
+
117
+ # Separate body and headers from the positional args
118
+ body = nil
119
+ headers = {}
120
+
121
+ headers_or_body.each do |arg|
122
+ if arg.is_a?(Hash)
123
+ headers.merge!(arg)
124
+ elsif body.nil?
125
+ body = arg
126
+ end
127
+ end
128
+
129
+ # Extract content_type from headers for HTTPX
130
+ content_type = headers.delete(:content_type)
131
+ headers['content-type'] = 'application/json' if content_type == :json
132
+
133
+ # Add user agent
134
+ headers['user-agent'] = user_agent
104
135
 
105
136
  begin
106
137
  rate_limiter.before_request(key, major_parameter)
107
138
 
108
139
  response = nil
109
140
  begin
110
- response = raw_request(type, attributes)
111
- rescue RestClient::Exception => e
112
- response = e.response
141
+ response = OnyxCord::HTTP.request(type, url, body, **headers)
142
+
143
+ if response.code == 403
144
+ noprm = OnyxCord::Errors::NoPermission.new
145
+ noprm.define_singleton_method(:_response) { response }
146
+ raise noprm, "The bot doesn't have the required permission to do this!"
147
+ end
148
+
149
+ # Retry on 502
150
+ if response.code == 502
151
+ OnyxCord::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
152
+ return request(key, major_parameter, type, url, body, headers)
153
+ end
154
+
155
+ # Handle error status codes
156
+ if response.code >= 400 && response.code != 429
157
+ data = begin
158
+ JSON.parse(response.body)
159
+ rescue StandardError
160
+ nil
161
+ end
162
+
163
+ raise "HTTP #{response.code}: #{response.body}" unless data
113
164
 
114
- if response.body && !e.is_a?(RestClient::TooManyRequests)
115
- data = JSON.parse(response.body)
116
165
  err_klass = OnyxCord::Errors.error_class_for(data['code'] || 0)
117
166
  e = err_klass.new(data['message'], data['errors'])
118
-
119
167
  OnyxCord::LOGGER.error(e.full_message)
168
+ raise e
120
169
  end
121
-
122
- raise e
123
170
  rescue OnyxCord::Errors::NoPermission => e
124
- if e.respond_to?(:_rc_response)
125
- response = e._rc_response
171
+ if e.respond_to?(:_response)
172
+ response = e._response
126
173
  else
127
- OnyxCord::LOGGER.warn("NoPermission doesn't respond_to? _rc_response!")
174
+ OnyxCord::LOGGER.warn("NoPermission doesn't respond_to? _response!")
128
175
  end
129
176
 
130
177
  raise e
@@ -135,27 +182,33 @@ module OnyxCord::API
135
182
  OnyxCord::LOGGER.ratelimit('Response was nil before trying to preemptively rate limit!')
136
183
  end
137
184
  end
138
- rescue RestClient::TooManyRequests => e
139
- trace("429 #{key} #{major_parameter}")
140
- rate_limiter.handle_rate_limit(key, major_parameter, e.response)
185
+ rescue OnyxCord::Errors::CodeError => e
186
+ raise if e.respond_to?(:code) && e.code != 429_000
141
187
 
142
- retry
188
+ raise
189
+ end
190
+
191
+ # Handle 429 rate limiting
192
+ if response&.code == 429
193
+ trace("429 #{key} #{major_parameter}")
194
+ rate_limiter.handle_rate_limit(key, major_parameter, response)
195
+ return request(key, major_parameter, type, url, body, headers)
143
196
  end
144
197
 
145
198
  # Endpoints that use Elasticsearch can return a 202 when the index isn't ready yet. Wait the
146
199
  # amount of time indicated by the response body, and then recursively retry and return the request.
147
200
  if response&.code == 202 && response&.body
148
- body = JSON.parse(response.body)
201
+ body_data = JSON.parse(response.body)
149
202
 
150
- if body['code'] == 110_000
151
- case body['retry_after']
203
+ if body_data['code'] == 110_000
204
+ case body_data['retry_after']
152
205
  when 0, 1, nil
153
206
  sleep(rand(4.5..5.0))
154
207
  else
155
- sleep(body['retry_after'])
208
+ sleep(body_data['retry_after'])
156
209
  end
157
210
 
158
- return request(key, major_parameter, type, *attributes)
211
+ return request(key, major_parameter, type, url, body, headers)
159
212
  end
160
213
  end
161
214
 
data/lib/onyxcord/bot.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rest-client'
3
+ require 'onyxcord/http'
4
4
  require 'zlib'
5
5
 
6
6
  require 'onyxcord/configuration'
@@ -477,9 +477,7 @@ module OnyxCord
477
477
  # @param enforce_nonce [true, false] Whether the nonce should be enforced and used for message de-duplication.
478
478
  # @param poll [Hash, Poll::Builder, Poll, nil] The poll that should be attached to this message.
479
479
  def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false, poll = nil)
480
- Thread.new do
481
- Thread.current[:onyxcord_name] = "#{@current_thread}-temp-msg"
482
-
480
+ Async do
483
481
  message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components, flags, nonce, enforce_nonce, poll)
484
482
  sleep(timeout)
485
483
  message.delete
@@ -504,7 +502,6 @@ module OnyxCord
504
502
  filename ||= File.basename(file.path)
505
503
  filename = "SPOILER_#{filename}" unless filename.start_with? 'SPOILER_'
506
504
  end
507
- # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
508
505
  file.define_singleton_method(:original_filename) { filename } if filename
509
506
  file.define_singleton_method(:path) { filename } if filename
510
507
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'lru_redux'
3
4
  require 'onyxcord/api'
4
5
  require 'onyxcord/api/server'
5
6
  require 'onyxcord/api/invite'
@@ -26,17 +27,15 @@ module OnyxCord
26
27
  # Initializes this cache
27
28
  def init_cache
28
29
  @cache_policy ||= OnyxCord.configuration.normalize_cache(:full)
30
+ sizes = OnyxCord.configuration.cache_sizes
29
31
 
30
- @users = cache_enabled?(:users) ? {} : nil
31
-
32
+ @users = cache_enabled?(:users) ? LruRedux::ThreadSafeCache.new(sizes.users) : nil
32
33
  @voice_regions = cache_enabled?(:voice_regions) ? {} : nil
33
-
34
- @servers = cache_enabled?(:servers) ? {} : nil
35
-
36
- @channels = cache_enabled?(:channels) ? {} : nil
37
- @pm_channels = cache_enabled?(:pm_channels) ? {} : nil
38
- @thread_members = cache_enabled?(:thread_members) ? {} : nil
39
- @server_previews = cache_enabled?(:server_previews) ? {} : nil
34
+ @servers = cache_enabled?(:servers) ? LruRedux::ThreadSafeCache.new(sizes.servers) : nil
35
+ @channels = cache_enabled?(:channels) ? LruRedux::ThreadSafeCache.new(sizes.channels) : nil
36
+ @pm_channels = cache_enabled?(:pm_channels) ? LruRedux::ThreadSafeCache.new(sizes.pm_channels) : nil
37
+ @thread_members = cache_enabled?(:thread_members) ? LruRedux::ThreadSafeCache.new(sizes.thread_members) : nil
38
+ @server_previews = cache_enabled?(:server_previews) ? LruRedux::ThreadSafeCache.new(sizes.server_previews) : nil
40
39
  end
41
40
 
42
41
  def cache_enabled?(key)
@@ -46,7 +45,7 @@ module OnyxCord
46
45
  def cache_stats
47
46
  CACHE_STORES.each_with_object({}) do |(key, ivar), stats|
48
47
  store = instance_variable_get(ivar)
49
- stats[key] = store.respond_to?(:size) ? store.size : 0
48
+ stats[key] = store.respond_to?(:count) ? store.count : 0
50
49
  end
51
50
  end
52
51
 
@@ -56,7 +55,7 @@ module OnyxCord
56
55
  keys.each_with_object({}) do |key, pruned|
57
56
  ivar = CACHE_STORES.fetch(key)
58
57
  store = instance_variable_get(ivar)
59
- pruned[key] = store.respond_to?(:size) ? store.size : 0
58
+ pruned[key] = store.respond_to?(:count) ? store.count : 0
60
59
  store&.clear
61
60
  end
62
61
  end
@@ -480,24 +480,18 @@ module OnyxCord::Commands
480
480
  end
481
481
 
482
482
  def execute_chain(chain, event)
483
- t = Thread.new do
484
- @event_threads << t
485
- Thread.current[:onyxcord_name] = "ct-#{@current_thread += 1}"
486
- begin
487
- debug("Parsing command chain #{chain}")
488
- result = @attributes[:advanced_functionality] ? CommandChain.new(chain, self).execute(event) : simple_execute(chain, event)
489
- result = event.drain_into(result)
490
-
491
- if event.file
492
- event.send_file(event.file, caption: result)
493
- else
494
- event.respond result unless result.nil? || result.empty?
495
- end
496
- rescue StandardError => e
497
- log_exception(e)
498
- ensure
499
- @event_threads.delete(t)
483
+ Async do
484
+ debug("Parsing command chain #{chain}")
485
+ result = @attributes[:advanced_functionality] ? CommandChain.new(chain, self).execute(event) : simple_execute(chain, event)
486
+ result = event.drain_into(result)
487
+
488
+ if event.file
489
+ event.send_file(event.file, caption: result)
490
+ else
491
+ event.respond result unless result.nil? || result.empty?
500
492
  end
493
+ rescue StandardError => e
494
+ log_exception(e)
501
495
  end
502
496
  end
503
497
 
@@ -43,11 +43,53 @@ module OnyxCord
43
43
  }
44
44
  }.freeze
45
45
 
46
- attr_accessor :mode, :cache, :event_executor, :event_workers, :event_queue_size
46
+ # Stores maximum limits for each LRU cache entity type.
47
+ class CacheSizes
48
+ attr_accessor :servers, :channels, :users, :members, :pm_channels, :thread_members, :server_previews
49
+
50
+ def initialize
51
+ @servers = 1000
52
+ @channels = 10_000
53
+ @users = 50_000
54
+ @members = 100_000
55
+ @pm_channels = 1000
56
+ @thread_members = 5000
57
+ @server_previews = 100
58
+ end
59
+
60
+ def [](key)
61
+ send(key)
62
+ end
63
+
64
+ def []=(key, value)
65
+ send("#{key}=", value)
66
+ end
67
+
68
+ def to_h
69
+ {
70
+ servers: @servers,
71
+ channels: @channels,
72
+ users: @users,
73
+ members: @members,
74
+ pm_channels: @pm_channels,
75
+ thread_members: @thread_members,
76
+ server_previews: @server_previews
77
+ }
78
+ end
79
+
80
+ def dup
81
+ copy = self.class.new
82
+ to_h.each { |k, v| copy[k] = v }
83
+ copy
84
+ end
85
+ end
86
+
87
+ attr_accessor :mode, :cache, :cache_sizes, :event_executor, :event_workers, :event_queue_size
47
88
 
48
89
  def initialize
49
90
  @mode = :hybrid
50
91
  @cache = :none
92
+ @cache_sizes = CacheSizes.new
51
93
  @event_executor = :pool
52
94
  @event_workers = 4
53
95
  @event_queue_size = nil
@@ -57,6 +99,7 @@ module OnyxCord
57
99
  copy = self.class.new
58
100
  copy.mode = @mode
59
101
  copy.cache = @cache.is_a?(Hash) ? @cache.dup : @cache
102
+ copy.cache_sizes = @cache_sizes.dup
60
103
  copy.event_executor = @event_executor
61
104
  copy.event_workers = @event_workers
62
105
  copy.event_queue_size = @event_queue_size
@@ -111,19 +111,17 @@ module OnyxCord
111
111
  @channel_id = data['channel_id']&.to_i
112
112
  @channel = bot.ensure_channel(data['channel']) if data['channel']
113
113
  @user = begin
114
- if data['member'] && data['member']['user']
115
- data['member']['guild_id'] = @server_id
116
- server = bot.servers ? bot.servers[@server_id] : nil
117
- OnyxCord::Member.new(data['member'], server, bot)
118
- elsif data['user']
119
- bot.ensure_user(data['user'])
120
- else
121
- nil
122
- end
123
- rescue StandardError => e
124
- OnyxCord::LOGGER.error("Failed to parse interaction user/member: #{e}")
125
- nil
126
- end
114
+ if data['member'] && data['member']['user']
115
+ data['member']['guild_id'] = @server_id
116
+ server = bot.servers ? bot.servers[@server_id] : nil
117
+ OnyxCord::Member.new(data['member'], server, bot)
118
+ elsif data['user']
119
+ bot.ensure_user(data['user'])
120
+ end
121
+ rescue StandardError => e
122
+ OnyxCord::LOGGER.error("Failed to parse interaction user/member: #{e}")
123
+ nil
124
+ end
127
125
  @token = data['token']
128
126
  @version = data['version']
129
127
  @components = @data['components']&.filter_map { |component| Components.from_data(component, @bot) } || []
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'async'
4
+
3
5
  module OnyxCord
4
6
  # Event execution strategies used by bot dispatch.
5
7
  module EventExecutor
@@ -18,21 +20,20 @@ module OnyxCord
18
20
  end
19
21
  end
20
22
 
21
- # Fixed-size worker pool for event handlers.
23
+ # Async-based worker pool for event handlers.
24
+ # Uses Async tasks (fibers) instead of threads for lightweight concurrency.
22
25
  class Pool
23
- attr_reader :threads, :queue
26
+ attr_reader :queue
24
27
 
25
28
  def initialize(size:, queue_size: nil)
26
29
  raise ArgumentError, 'Pool size must be greater than zero' unless size.positive?
27
30
 
31
+ @size = size
28
32
  @queue = queue_size ? SizedQueue.new(queue_size) : Queue.new
29
33
  @closed = false
30
- @threads = Array.new(size) do |index|
31
- Thread.new do
32
- Thread.current[:onyxcord_name] = "event-worker-#{index + 1}"
33
- worker_loop
34
- end
35
- end
34
+ @workers = []
35
+
36
+ start_workers
36
37
  end
37
38
 
38
39
  def post(&block)
@@ -46,16 +47,34 @@ module OnyxCord
46
47
  @queue.size
47
48
  end
48
49
 
50
+ # Compatibility with code that checks worker threads.
51
+ def threads
52
+ @workers
53
+ end
54
+
49
55
  def shutdown
50
56
  return if @closed
51
57
 
52
58
  @closed = true
53
- @threads.length.times { @queue << STOP }
54
- @threads.each { |thread| thread.join unless thread == Thread.current }
59
+ @size.times { @queue << STOP }
60
+ @workers.each do |w|
61
+ w.join unless w == Thread.current
62
+ rescue StandardError
63
+ nil
64
+ end
55
65
  end
56
66
 
57
67
  private
58
68
 
69
+ def start_workers
70
+ @workers = Array.new(@size) do |index|
71
+ Thread.new do
72
+ Thread.current[:onyxcord_name] = "event-worker-#{index + 1}"
73
+ worker_loop
74
+ end
75
+ end
76
+ end
77
+
59
78
  def worker_loop
60
79
  loop do
61
80
  job = @queue.pop