thunderwire 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 985b8c17c616f7c9a76e56313dc46e186875b479483ffa426c0745a9255f6514
4
+ data.tar.gz: 02a422c11d63251542d90466d52d2cf56fe292e6437d60d5b65b66373fdb3654
5
+ SHA512:
6
+ metadata.gz: 76635e57f908b2594521bcdd9965c5295a10b76649d0116713d7b26a7c05ba8d2b544a843c16264bcfc12f8a5450ce0dc2d9284f9994cf558b43a2353510e4e0
7
+ data.tar.gz: 609fdc9e8f8325ed53cebe16afe84d36d9c4579ff4f407998e74d87676e3708fb6bafca977c7be146598f2dfa8cc7511a8fd55f02cbf96fe55bc24c7d99003b8
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # thunderwire
2
+
3
+ External of the thunderbolt ecosystem.
@@ -0,0 +1,11 @@
1
+ module ThunderWire
2
+ class BackgroundTask
3
+ def start
4
+ raise NotImplementedError
5
+ end
6
+
7
+ def stop
8
+ raise NotImplementedError
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module ThunderWire
2
+ class Handler
3
+ class << self
4
+ # WARN: AI MADE
5
+ def get!(broker, name)
6
+ file = File.join(__dir__, 'message_brokers', broker.name.split('::').last.underscore, 'handlers',
7
+ "#{name.underscore}.rb")
8
+ raise(Error, 'This handler does not exists') unless File.exist?(file)
9
+
10
+ require file
11
+
12
+ broker::Handlers.const_get(name.camelize)
13
+ end
14
+ # WARN: ---
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ module ThunderWire
2
+ class MessageBroker
3
+ class << self
4
+ # WARN: AI MADE
5
+ def get!(message_broker)
6
+ files = Dir[File.join(__dir__, 'message_brokers', message_broker.to_s, '*.rb')]
7
+ raise(Error, 'This broker does not exists') if files.empty?
8
+
9
+ files.sort.each { |file| require file }
10
+
11
+ ThunderWire::MessageBrokers.const_get(message_broker.to_s.camelize)
12
+ end
13
+ # WARN: ---
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bunny'
4
+
5
+ # WARN: AI MADE
6
+ module ThunderWire
7
+ module MessageBrokers
8
+ module Rabbitmq
9
+ # Consumes the queue of the chunk: declares it with one binding per event the chunk subscribes to, and hands each
10
+ # message to the handler. A message requeued past the delivery limit, or rejected, goes to the dead exchange.
11
+ class Consumer < BackgroundTask
12
+ def initialize(url:, queue:, keys:, handler:, exchange: 'thunderbolt.events', dead_exchange: 'thunderbolt.dead',
13
+ prefetch: 10, concurrency: 5, delivery_limit: 5)
14
+ @url = url
15
+ @queue_name = queue
16
+ @keys = keys
17
+ @handler = handler
18
+ @exchange_name = exchange
19
+ @dead_exchange_name = dead_exchange
20
+ @prefetch = prefetch
21
+ @concurrency = concurrency
22
+ @delivery_limit = delivery_limit
23
+ @stopped = Queue.new
24
+
25
+ super()
26
+ end
27
+
28
+ def start
29
+ @connection = Bunny.new(@url).start
30
+ channel = @connection.create_channel(nil, @concurrency)
31
+ channel.prefetch(@prefetch)
32
+
33
+ events = channel.exchange(@exchange_name, type: :topic, durable: true)
34
+ dead = channel.exchange(@dead_exchange_name, type: :fanout, durable: true)
35
+ channel.queue(@dead_exchange_name, durable: true, arguments: { 'x-queue-type' => 'quorum' }).bind(dead)
36
+
37
+ queue = channel.queue(@queue_name, durable: true, arguments: {
38
+ 'x-queue-type' => 'quorum',
39
+ 'x-dead-letter-exchange' => @dead_exchange_name,
40
+ 'x-delivery-limit' => @delivery_limit
41
+ })
42
+ @keys.each { |key| queue.bind(events, routing_key: key) }
43
+
44
+ consumer = queue.subscribe(manual_ack: true) do |delivery, _properties, body|
45
+ case @handler.handle(delivery.routing_key, body)
46
+ when :ack then channel.ack(delivery.delivery_tag)
47
+ when :requeue then channel.nack(delivery.delivery_tag, false, true)
48
+ else channel.reject(delivery.delivery_tag, false)
49
+ end
50
+ rescue StandardError => e
51
+ Console.error(self, e)
52
+ channel.nack(delivery.delivery_tag, false, true)
53
+ end
54
+
55
+ @stopped.pop
56
+
57
+ consumer.cancel
58
+ channel.work_pool.join(30)
59
+ ensure
60
+ @connection&.close
61
+ end
62
+
63
+ # Stops taking messages; the ones in progress finish, and the ones not acked go back to the queue.
64
+ def stop
65
+ @stopped << true
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ # WARN: AI MADE
6
+ module ThunderWire
7
+ module MessageBrokers
8
+ module Rabbitmq
9
+ module Handlers
10
+ # Runs the subscriber of the event inside the consumer, and answers what the consumer does with the message.
11
+ # The keys of the payload the subscriber does not declare are left out; the ones it declares and the payload
12
+ # leaves out arrive as nil.
13
+ #
14
+ # A problem with the message itself (no subscriber, body that is not JSON, param of the wrong type) is logged
15
+ # and the message acked: delivering it again gives the same result. Any other failure of the subscriber is
16
+ # logged and the message requeued.
17
+ #
18
+ # FIX: no event is ever rejected: the ones with a problem of their own are acked, never sent to the dead
19
+ # exchange.
20
+ class Direct
21
+ def initialize(exporter)
22
+ @exporter = exporter
23
+ end
24
+
25
+ def self.configurations
26
+ {}
27
+ end
28
+
29
+ def handle(routing_key, body)
30
+ unless @exporter.exports?(routing_key)
31
+ Console.error(self, 'Event Without Subscriber', routing_key:)
32
+
33
+ return :ack
34
+ end
35
+
36
+ params = JSON.parse(body, symbolize_names: true)
37
+ declared = @exporter.get(routing_key).base_parameter.configuration.keys
38
+ unknown = params.keys - declared
39
+ missing = declared - params.keys
40
+
41
+ if unknown.any? || missing.any?
42
+ Console.error(self, 'Event Payload Off Contract', routing_key:, unknown:, missing:)
43
+ end
44
+
45
+ response = @exporter.call(routing_key, params: params.slice(*declared))
46
+
47
+ if response.success?
48
+ Console.info(self, 'Event Handled', routing_key:)
49
+
50
+ :ack
51
+ elsif response.error.is_a?(ThunderOperation::Exceptions::ParameterError)
52
+ Console.error(self, 'Event Payload Off Contract', routing_key:, error: response.error)
53
+
54
+ :ack
55
+ else
56
+ Console.error(self, 'Event Failed', routing_key:, error: response.error)
57
+
58
+ :requeue
59
+ end
60
+ rescue JSON::ParserError => e
61
+ Console.error(self, 'Event Body Is Not JSON', routing_key:, error: e)
62
+
63
+ :ack
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bunny'
4
+
5
+ module ThunderWire
6
+ module MessageBrokers
7
+ module Rabbitmq
8
+ class Outbox < BackgroundTask
9
+ def initialize(url:, chunk:, exchange: 'thunderbolt.events', batch_size: 100, idle: 0.5)
10
+ @url = url
11
+ @chunk = chunk
12
+ @exchange_name = exchange
13
+ @batch_size = batch_size
14
+ @idle = idle
15
+ @running = false
16
+
17
+ super()
18
+ end
19
+
20
+ # WARN: AI MADE
21
+ def start
22
+ @running = true
23
+ @connection = Bunny.new(@url).start
24
+ channel = @connection.create_channel
25
+ channel.confirm_select
26
+ exchange = channel.exchange(@exchange_name, type: :topic, durable: true)
27
+
28
+ while @running
29
+ begin
30
+ sent = ThunderChunk::Outbox.transaction do
31
+ messages = ThunderChunk::Outbox.pending.limit(@batch_size).lock('FOR UPDATE SKIP LOCKED').to_a
32
+
33
+ messages.each do |message|
34
+ routing_key = "#{@chunk}/#{message.routing_key}"
35
+
36
+ Console.info(self, 'Event Published', message_id: message.message_id,
37
+ routing_key:,
38
+ payload: message.payload)
39
+ exchange.publish(message.payload.to_json, routing_key:, message_id: message.message_id,
40
+ persistent: true, content_type: 'application/json')
41
+ end
42
+
43
+ raise Error, 'publish not confirmed by the broker' if messages.any? && !channel.wait_for_confirms
44
+
45
+ if messages.any?
46
+ ThunderChunk::Outbox.where(id: messages.map(&:id)).update_all(published_at: Time.current)
47
+ end
48
+
49
+ messages.size
50
+ end
51
+
52
+ sleep @idle if sent.zero?
53
+ rescue StandardError => e
54
+ Console.error(self, e)
55
+ sleep 1
56
+ end
57
+ end
58
+ ensure
59
+ @connection&.close
60
+ end
61
+
62
+ def stop
63
+ @running = false
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module ThunderWire
6
+ module MessageBrokers
7
+ module Rabbitmq
8
+ module Publishers
9
+ class Outbox
10
+ def initialize(table_name: 'thunder_outbox')
11
+ ThunderChunk::Outbox.table_name = table_name
12
+ end
13
+
14
+ def self.configurations
15
+ { table_name: 'messaging.outbox.table_name' }
16
+ end
17
+
18
+ def publish(key, params)
19
+ ThunderChunk::Outbox.create!(
20
+ message_id: SecureRandom.uuid,
21
+ routing_key: key,
22
+ payload: params
23
+ ).message_id
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ module ThunderWire
2
+ class Protocol
3
+ class << self
4
+ def get!(protocol)
5
+ Dir[File.join(__dir__, 'protocols', protocol.to_s, '*.rb')].sort.each { |file| require file }
6
+
7
+ ThunderWire::Protocols.const_get(protocol.camelize).tap do |v|
8
+ raise(Error, 'This protocol does not exists') unless v
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderWire
4
+ module Protocols
5
+ module Http
6
+ # The Rack request as the adapters read it. The body stream can be consumed only once, so it is read on the
7
+ # first call and kept; the parsed JSON and query are kept too.
8
+ class Request < ::Protocol::Rack::Request
9
+ ENV_KEY = 'thunderwire.request'
10
+
11
+ class InvalidJSON < StandardError; end
12
+
13
+ attr_reader :env
14
+
15
+ def self.[](env)
16
+ env[ENV_KEY] ||= new(env)
17
+ end
18
+
19
+ def body
20
+ return @raw_body if defined?(@raw_body)
21
+
22
+ @raw_body = super&.join.to_s
23
+ end
24
+
25
+ def parsed_body
26
+ return @parsed_body if defined?(@parsed_body)
27
+ return @parsed_body = body unless json?
28
+
29
+ @parsed_body = body.empty? ? {} : JSON.parse(body)
30
+ rescue JSON::ParserError => e
31
+ raise InvalidJSON, e.message
32
+ end
33
+
34
+ def query_string = @env['QUERY_STRING'].to_s
35
+
36
+ def query
37
+ @query ||= ::Rack::Utils.parse_nested_query(query_string)
38
+ end
39
+
40
+ def content_type = @env['CONTENT_TYPE']
41
+
42
+ def content_length = @env['CONTENT_LENGTH']&.to_i
43
+
44
+ def media_type = content_type&.split(';')&.first&.strip&.downcase
45
+
46
+ def json? = media_type == 'application/json'
47
+
48
+ def post? = method == 'POST'
49
+
50
+ def remote_ip = @env['REMOTE_ADDR']
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ module ThunderWire
2
+ module Protocols
3
+ module Http
4
+ class Server < ThunderWire::Server
5
+ attr_reader :async_server
6
+
7
+ def create_task(ip, port)
8
+ endpoint = Async::HTTP::Endpoint.for('http', ip, port:)
9
+
10
+ @async_server = Async::HTTP::Server.new(
11
+ ::Protocol::Rack::Adapter.new(
12
+ lambda do |request|
13
+ # Console.info(self, 'Http Request Received', **request)
14
+ @wire.call(ThunderWire::Protocols::Http::Request[request]).tap do |(status, headers, body)|
15
+ # Console.info(self, 'Http Response Sent', status:, headers:, body:)
16
+ end
17
+ end
18
+ ),
19
+ endpoint
20
+ )
21
+
22
+ @async_server.run
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,56 @@
1
+ module ThunderWire
2
+ module Protocols
3
+ module Http
4
+ module Wires
5
+ module Operation
6
+ class Receiver
7
+ def initialize(chunk)
8
+ @chunk = chunk
9
+ end
10
+
11
+ # FIX: Imrpove the code
12
+ def call(request)
13
+ key = path_to_key(request.path)
14
+ exporter = @chunk.exporter.get(:operation)
15
+
16
+ request.post? || raise(ProtocolError, "#{request.method} is not allowed; use POST")
17
+ exporter.exports?(key) || raise(ProtocolError, "No operation at #{request.path}")
18
+ request.json? || raise(ProtocolError, 'The body must be application/json')
19
+
20
+ params = request.parsed_body
21
+ params.is_a?(Hash) || raise(ProtocolError, 'The body must be a JSON object')
22
+
23
+ Console.info(self, 'Http Request Received', params:)
24
+
25
+ result = exporter.call(key, params: params.deep_symbolize_keys)
26
+
27
+ Console.info(self, 'Http Response Sent', result: result.to_h)
28
+
29
+ unless result.payload.nil? || result.payload.is_a?(Hash) || result.payload.is_a?(Array)
30
+ raise Error,
31
+ 'result.payload must be a hash or array'
32
+ end
33
+
34
+ [200, {}, [result.to_json]]
35
+ rescue ProtocolError, Request::InvalidJSON => e
36
+ [400, {}, [{ error: { type: 'ProtocolError', message: e.message } }.to_json]]
37
+ rescue Error => e
38
+ [500, {}, [{ error: { type: 'InternalError', message: e.message } }.to_json]]
39
+ end
40
+
41
+ # /_operations/books/show or /library/_operations/books/show -> 'books.show'. The chunk is known by the
42
+ # url; a prefix with its name is accepted for a router (ingress) that keeps it in the path.
43
+ def path_to_key(path)
44
+ segments = path.split('/').reject(&:empty?)
45
+ segments.shift if segments.first == @chunk.identifier.to_s
46
+ segment, *path_array = segments
47
+ return unless segment == '_operations' && path_array.any?
48
+
49
+ path_array.join('.')
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,50 @@
1
+ module ThunderWire
2
+ module Protocols
3
+ module Http
4
+ module Wires
5
+ module Operation
6
+ class Sender
7
+ def initialize(chunk_identifier, base_url)
8
+ @chunk_identifier = chunk_identifier
9
+ @endpoint = Async::HTTP::Endpoint.parse(base_url)
10
+ @client = Async::HTTP::Client.new(@endpoint)
11
+ end
12
+
13
+ # FIX: Imrpove the code
14
+ def call(key, params)
15
+ Sync do
16
+ Console.info(self, 'Http Request Sent', chunk: @chunk_identifier, params:)
17
+
18
+ answer = @client.post(
19
+ "#{@endpoint.path.chomp('/')}/_operations/#{key.tr('.', '/')}",
20
+ [['content-type', 'application/json']],
21
+ params.to_json
22
+ )
23
+
24
+ body = JSON.parse(answer.read)
25
+ failure = body['failure_data']
26
+
27
+ Console.info(self, 'Http Response Received', chunk: @chunk_identifier, status: answer.status,
28
+ body:)
29
+
30
+ error = if failure
31
+ ThunderChunk::Operations::Remote::RemoteResponseFailure.new(
32
+ failure['failure_message'],
33
+ error_class: failure['error_class'],
34
+ extras: failure['failure_extras'],
35
+ trail: failure['trail']
36
+ )
37
+ end
38
+
39
+ ThunderOperation::Response.new(
40
+ payload: body['payload'], was_success: body['was_success'], error:,
41
+ status: body['status'], failed_step: body['failed_step'], operation_name: body['operation_name']
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,14 @@
1
+ module ThunderWire
2
+ module Protocols
3
+ module Http
4
+ module Wires
5
+ module Operation
6
+ extend ThunderWire::Wire
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ require_relative 'operation/receiver'
14
+ require_relative 'operation/sender'
@@ -0,0 +1,39 @@
1
+ module ThunderWire
2
+ class Server
3
+ def initialize(ip, port, wire)
4
+ @ip = ip
5
+ @port = port
6
+ @wire = wire
7
+ end
8
+
9
+ def up
10
+ Signal.trap('TERM') { Thread.main.raise(Interrupt) }
11
+
12
+ Sync do
13
+ @server_task = create_task(@ip, @port)
14
+
15
+ Console.info(self, 'Server Started!')
16
+
17
+ @server_task.wait
18
+ rescue Interrupt
19
+ # graceful shutdown handled by ensure
20
+ ensure
21
+ down
22
+ end
23
+ end
24
+
25
+ def down
26
+ @server_task&.stop
27
+
28
+ Console.info(self, 'Server Stopped!')
29
+ end
30
+
31
+ class << self
32
+ def build(chunk, protocol_module, wire_klass, ip, port)
33
+ wire = wire_klass::Receiver.new(chunk)
34
+
35
+ protocol_module::Server.new(ip, port, wire)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderWire
4
+ VERSION = '0.2.0'
5
+ end
@@ -0,0 +1,21 @@
1
+ module ThunderWire
2
+ # Extended by each wire module (Protocols::Http::Wires::Operation), which holds that protocol's Receiver and Sender
3
+ # for one kind of export.
4
+ module Wire
5
+ def self.get!(protocol, name)
6
+ require "thunderwire/protocols/#{protocol.name.split('::').last.underscore}/wires/#{name.underscore}"
7
+
8
+ protocol::Wires.const_get(name.camelize).tap do |v|
9
+ raise(Error, 'This wire does not exists') unless v
10
+ end
11
+ end
12
+
13
+ def exporter
14
+ wire_name = name.split('::').last
15
+
16
+ require "thunderchunk/exporters/#{wire_name.underscore.pluralize}"
17
+
18
+ ThunderChunk::Exporters.const_get(wire_name.pluralize)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'active_support/core_ext/hash/keys'
6
+ require 'async'
7
+ require 'async/http/server'
8
+ require 'async/http/endpoint'
9
+ require 'async/http/client'
10
+ require 'protocol/rack'
11
+ require 'rack/utils'
12
+ require 'json'
13
+ require 'thunderchunk'
14
+
15
+ require_relative 'thunderwire/version'
16
+ require_relative 'thunderwire/server'
17
+ require_relative 'thunderwire/wire'
18
+ require_relative 'thunderwire/protocol'
19
+ require_relative 'thunderwire/background_task'
20
+ require_relative 'thunderwire/message_broker'
21
+ require_relative 'thunderwire/handler'
22
+
23
+ module ThunderWire
24
+ class Error < StandardError; end
25
+
26
+ class ProtocolError < Error; end
27
+
28
+ # WARN: AI MADE
29
+ class << self
30
+ def sender(protocol, wire)
31
+ Wire.get!(Protocol.get!(protocol.to_s), wire.to_s)::Sender
32
+ end
33
+
34
+ def publisher(broker, publisher)
35
+ require "thunderwire/message_brokers/#{broker}/publishers/#{publisher}"
36
+
37
+ MessageBrokers.const_get(broker.to_s.camelize)::Publishers.const_get(publisher.to_s.camelize)
38
+ end
39
+ end
40
+ # WARN: ---
41
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thunderwire
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Albert Alef
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '8.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: thunderchunk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.2.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 0.2.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: async
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: async-http
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.103'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.103'
68
+ - !ruby/object:Gem::Dependency
69
+ name: bunny
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: protocol-rack
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.22'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.22'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rack
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.1'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.1'
110
+ executables: []
111
+ extensions: []
112
+ extra_rdoc_files: []
113
+ files:
114
+ - README.md
115
+ - lib/thunderwire.rb
116
+ - lib/thunderwire/background_task.rb
117
+ - lib/thunderwire/handler.rb
118
+ - lib/thunderwire/message_broker.rb
119
+ - lib/thunderwire/message_brokers/rabbitmq/consumer.rb
120
+ - lib/thunderwire/message_brokers/rabbitmq/handlers/direct.rb
121
+ - lib/thunderwire/message_brokers/rabbitmq/outbox.rb
122
+ - lib/thunderwire/message_brokers/rabbitmq/publishers/outbox.rb
123
+ - lib/thunderwire/protocol.rb
124
+ - lib/thunderwire/protocols/http/request.rb
125
+ - lib/thunderwire/protocols/http/server.rb
126
+ - lib/thunderwire/protocols/http/wires/operation.rb
127
+ - lib/thunderwire/protocols/http/wires/operation/receiver.rb
128
+ - lib/thunderwire/protocols/http/wires/operation/sender.rb
129
+ - lib/thunderwire/server.rb
130
+ - lib/thunderwire/version.rb
131
+ - lib/thunderwire/wire.rb
132
+ licenses: []
133
+ metadata:
134
+ rubygems_mfa_required: 'true'
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '3.4'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubygems_version: 3.6.9
150
+ specification_version: 4
151
+ summary: External of the thunderbolt ecosystem
152
+ test_files: []