zzq 0.1.1 → 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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE-APACHE +202 -0
  4. data/LICENSE-ISC +15 -0
  5. data/README.md +67 -122
  6. data/ext/zzq_native/Cargo.toml +35 -0
  7. data/ext/zzq_native/extconf.rb +8 -0
  8. data/ext/zzq_native/src/broker.rs +163 -0
  9. data/ext/zzq_native/src/client.rs +294 -0
  10. data/ext/zzq_native/src/context.rs +51 -0
  11. data/ext/zzq_native/src/error.rs +117 -0
  12. data/ext/zzq_native/src/lib.rs +52 -0
  13. data/ext/zzq_native/src/op.rs +140 -0
  14. data/ext/zzq_native/src/options.rs +168 -0
  15. data/ext/zzq_native/src/rb.rs +424 -0
  16. data/ext/zzq_native/src/wake.rs +155 -0
  17. data/lib/zzq/broker.rb +90 -21
  18. data/lib/zzq/client.rb +202 -35
  19. data/lib/zzq/errors.rb +37 -0
  20. data/lib/zzq/message.rb +16 -107
  21. data/lib/zzq/options.rb +27 -86
  22. data/lib/zzq/version.rb +2 -1
  23. data/lib/zzq/waiting.rb +65 -0
  24. data/lib/zzq.rb +22 -19
  25. metadata +33 -77
  26. data/LICENSE +0 -13
  27. data/lib/zzq/engine/connection_lifecycle.rb +0 -139
  28. data/lib/zzq/engine/keepalive.rb +0 -75
  29. data/lib/zzq/engine/socket_lifecycle.rb +0 -79
  30. data/lib/zzq/engine.rb +0 -169
  31. data/lib/zzq/error.rb +0 -42
  32. data/lib/zzq/monitor_event.rb +0 -14
  33. data/lib/zzq/persistence/interface.rb +0 -43
  34. data/lib/zzq/persistence/memory.rb +0 -37
  35. data/lib/zzq/persistence/pstore.rb +0 -66
  36. data/lib/zzq/routing/broker.rb +0 -380
  37. data/lib/zzq/routing/client.rb +0 -327
  38. data/lib/zzq/routing/packet_id_allocator.rb +0 -54
  39. data/lib/zzq/routing/qos_tracker.rb +0 -108
  40. data/lib/zzq/routing/retained_store.rb +0 -67
  41. data/lib/zzq/routing/topic_match.rb +0 -49
  42. data/lib/zzq/routing/topic_trie.rb +0 -232
  43. data/lib/zzq/socket.rb +0 -146
  44. data/lib/zzq/subscription.rb +0 -70
  45. data/lib/zzq/transport/ipc.rb +0 -92
  46. data/lib/zzq/transport/mqtt.rb +0 -83
  47. data/lib/zzq/transport/mqtts.rb +0 -173
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zzq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -10,104 +10,60 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: async
13
+ name: rb_sys
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '2.38'
18
+ version: '0.9'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '2.38'
26
- - !ruby/object:Gem::Dependency
27
- name: io-stream
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '0.11'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '0.11'
40
- - !ruby/object:Gem::Dependency
41
- name: protocol-mqtt
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 0.1.0
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: 0.1.0
54
- - !ruby/object:Gem::Dependency
55
- name: pstore
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '0.2'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '0.2'
68
- description: Pure Ruby implementation of MQTT — client and broker in one gem — built
69
- on async, io-stream, and protocol-mqtt. Fiber-first, pluggable transports (tcp,
70
- tls, ws, ipc), pluggable persistence (in-memory, PStore).
25
+ version: '0.9'
26
+ description: 'Ruby binding for zzq: an MQTT 3.1.1 and 5.0 client and broker with an
27
+ MQTT-SN gateway, running on Rust IO threads. Works in threads and under fiber schedulers
28
+ such as Async.'
71
29
  email:
72
30
  - paddor@gmail.com
73
31
  executables: []
74
- extensions: []
32
+ extensions:
33
+ - ext/zzq_native/extconf.rb
75
34
  extra_rdoc_files: []
76
35
  files:
77
36
  - CHANGELOG.md
78
- - LICENSE
37
+ - LICENSE-APACHE
38
+ - LICENSE-ISC
79
39
  - README.md
40
+ - ext/zzq_native/Cargo.toml
41
+ - ext/zzq_native/extconf.rb
42
+ - ext/zzq_native/src/broker.rs
43
+ - ext/zzq_native/src/client.rs
44
+ - ext/zzq_native/src/context.rs
45
+ - ext/zzq_native/src/error.rs
46
+ - ext/zzq_native/src/lib.rs
47
+ - ext/zzq_native/src/op.rs
48
+ - ext/zzq_native/src/options.rs
49
+ - ext/zzq_native/src/rb.rs
50
+ - ext/zzq_native/src/wake.rs
80
51
  - lib/zzq.rb
81
52
  - lib/zzq/broker.rb
82
53
  - lib/zzq/client.rb
83
- - lib/zzq/engine.rb
84
- - lib/zzq/engine/connection_lifecycle.rb
85
- - lib/zzq/engine/keepalive.rb
86
- - lib/zzq/engine/socket_lifecycle.rb
87
- - lib/zzq/error.rb
54
+ - lib/zzq/errors.rb
88
55
  - lib/zzq/message.rb
89
- - lib/zzq/monitor_event.rb
90
56
  - lib/zzq/options.rb
91
- - lib/zzq/persistence/interface.rb
92
- - lib/zzq/persistence/memory.rb
93
- - lib/zzq/persistence/pstore.rb
94
- - lib/zzq/routing/broker.rb
95
- - lib/zzq/routing/client.rb
96
- - lib/zzq/routing/packet_id_allocator.rb
97
- - lib/zzq/routing/qos_tracker.rb
98
- - lib/zzq/routing/retained_store.rb
99
- - lib/zzq/routing/topic_match.rb
100
- - lib/zzq/routing/topic_trie.rb
101
- - lib/zzq/socket.rb
102
- - lib/zzq/subscription.rb
103
- - lib/zzq/transport/ipc.rb
104
- - lib/zzq/transport/mqtt.rb
105
- - lib/zzq/transport/mqtts.rb
106
57
  - lib/zzq/version.rb
107
- homepage: https://github.com/paddor/zzq
58
+ - lib/zzq/waiting.rb
59
+ homepage: https://github.com/paddor/zzq/tree/main/bindings/ruby
108
60
  licenses:
109
61
  - ISC
110
- metadata: {}
62
+ - Apache-2.0
63
+ metadata:
64
+ source_code_uri: https://github.com/paddor/zzq/tree/main/bindings/ruby
65
+ changelog_uri: https://github.com/paddor/zzq/tree/main/bindings/ruby/CHANGELOG.md
66
+ rubygems_mfa_required: 'true'
111
67
  rdoc_options: []
112
68
  require_paths:
113
69
  - lib
@@ -115,14 +71,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
115
71
  requirements:
116
72
  - - ">="
117
73
  - !ruby/object:Gem::Version
118
- version: '4.0'
74
+ version: '3.4'
119
75
  required_rubygems_version: !ruby/object:Gem::Requirement
120
76
  requirements:
121
77
  - - ">="
122
78
  - !ruby/object:Gem::Version
123
79
  version: '0'
124
80
  requirements: []
125
- rubygems_version: 4.0.6
81
+ rubygems_version: 4.0.20
126
82
  specification_version: 4
127
- summary: Pure Ruby MQTT 3.1.1 + 5.0 client and broker on Async
83
+ summary: MQTT 3.1.1 and 5.0 client and broker for Ruby, backed by Rust
128
84
  test_files: []
data/LICENSE DELETED
@@ -1,13 +0,0 @@
1
- Copyright (c) 2026, Patrik Wenger
2
-
3
- Permission to use, copy, modify, and/or distribute this software for any
4
- purpose with or without fee is hereby granted, provided that the above
5
- copyright notice and this permission notice appear in all copies.
6
-
7
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -1,139 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "async/barrier"
4
- require "protocol/mqtt"
5
-
6
- module ZZQ
7
- class Engine
8
- # Owns the full arc of one connection: new → handshaking → ready →
9
- # closed. Centralises side-effect ordering so routing doesn't have
10
- # to.
11
- #
12
- # The MQTT CONNECT/CONNACK exchange itself is NOT performed here —
13
- # it's role-asymmetric (client writes CONNECT first, broker reads
14
- # it first) and so lives in the role's routing layer, which calls
15
- # +handshake_succeeded!+ once the exchange is done.
16
- #
17
- # new → handshaking → ready → closed
18
- #
19
- # #lost! / #close! are idempotent.
20
- class ConnectionLifecycle
21
- class InvalidTransition < RuntimeError; end
22
-
23
- STATES = %i[new handshaking ready closed].freeze
24
-
25
- TRANSITIONS = {
26
- new: %i[handshaking ready closed].freeze,
27
- handshaking: %i[ready closed].freeze,
28
- ready: %i[closed].freeze,
29
- closed: [].freeze,
30
- }.freeze
31
-
32
-
33
- attr_reader :conn # Protocol::MQTT::Connection
34
- attr_reader :endpoint
35
- attr_reader :state
36
- attr_reader :barrier
37
-
38
-
39
- def initialize(engine, endpoint:)
40
- @engine = engine
41
- @endpoint = endpoint
42
- @state = :new
43
- @conn = nil
44
- @barrier = Async::Barrier.new(parent: engine.barrier)
45
- end
46
-
47
-
48
- # Wraps the accepted/connected IO in a Protocol::MQTT::Connection
49
- # and returns it. The handshake itself (CONNECT/CONNACK exchange)
50
- # is driven by the routing layer — this is "transport ready,
51
- # MQTT handshake pending".
52
- def begin_handshake!(io)
53
- transition!(:handshaking)
54
- mqtt = Protocol::MQTT::Connection.new(
55
- io,
56
- version: @engine.options.version,
57
- max_packet_size: @engine.options.max_packet_size,
58
- )
59
- @conn = mqtt
60
- mqtt
61
- end
62
-
63
-
64
- # Called by the routing layer after CONNECT/CONNACK succeeds.
65
- def handshake_succeeded!
66
- @engine.connections[@conn] = self
67
- transition!(:ready)
68
- begin
69
- @engine.routing.connection_added(@conn) if @engine.routing.respond_to?(:connection_added)
70
- rescue ConnectionRejected
71
- @engine.emit_monitor_event(:connection_rejected, endpoint: @endpoint)
72
- tear_down!(reconnect: false)
73
- raise
74
- end
75
- @engine.lifecycle.peer_connected.resolve(@conn) unless @engine.lifecycle.peer_connected.resolved?
76
- @engine.emit_monitor_event(:connected, endpoint: @endpoint)
77
- end
78
-
79
-
80
- # Called on handshake failure — e.g. refused CONNACK, RST mid-
81
- # CONNECT. Tears down and requests a reconnect if eligible.
82
- def handshake_failed!(error:)
83
- @engine.emit_monitor_event(:handshake_failed, endpoint: @endpoint, detail: { error: error })
84
- @conn&.close rescue nil
85
- tear_down!(reconnect: true)
86
- end
87
-
88
-
89
- # Unexpected loss of an established connection.
90
- def lost!
91
- tear_down!(reconnect: true)
92
- end
93
-
94
-
95
- # Deliberate close (engine shutdown, kicked by broker, …).
96
- def close!
97
- tear_down!(reconnect: false)
98
- end
99
-
100
-
101
- # Starts a supervisor task that tears down when any pump exits.
102
- def start_supervisor!
103
- return if @barrier.empty?
104
- @engine.barrier.async(transient: true, annotation: "zzq conn supervisor") do
105
- @barrier.wait { |task| task.wait; break }
106
- rescue Async::Stop, Async::Cancel
107
- rescue *CONNECTION_LOST
108
- ensure
109
- lost!
110
- end
111
- end
112
-
113
-
114
- private def tear_down!(reconnect: false)
115
- return if @state == :closed
116
- transition!(:closed)
117
- if @conn
118
- @engine.connections.delete(@conn)
119
- @engine.routing.connection_removed(@conn) if @engine.routing.respond_to?(:connection_removed)
120
- @conn.close rescue nil
121
- @engine.emit_monitor_event(:disconnected, endpoint: @endpoint)
122
- @engine.resolve_all_peers_gone_if_empty
123
- end
124
- @engine.maybe_reconnect(@endpoint) if reconnect
125
- # Stop after cleanup: when tear_down! is called from a task on
126
- # this barrier (e.g. the read-loop's ensure), stopping earlier
127
- # would cancel the current fiber mid-cleanup.
128
- @barrier.stop
129
- end
130
-
131
-
132
- private def transition!(new_state)
133
- allowed = TRANSITIONS[@state]
134
- raise InvalidTransition, "#{@state} → #{new_state}" unless allowed&.include?(new_state)
135
- @state = new_state
136
- end
137
- end
138
- end
139
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "async"
4
- require "async/clock"
5
- require "protocol/mqtt"
6
-
7
- module ZZQ
8
- class Engine
9
- # Keepalive driver for a single connection.
10
- #
11
- # +role+:
12
- # * +:sender+ (client default) — sends PINGREQ if locally idle
13
- # for keep_alive/2; closes if nothing received for 1.5 × keep_alive.
14
- # * +:watcher+ (broker default) — does not send; just closes if
15
- # nothing received for 1.5 × keep_alive. MQTT-3.1.2-22 is
16
- # enforced by the broker.
17
- #
18
- # One fiber per connection, attached to the connection's barrier
19
- # so it tears down with the connection.
20
- class Keepalive
21
- attr_accessor :last_sent_at, :last_received_at
22
-
23
-
24
- def initialize(conn, keep_alive:, on_timeout:, role: :sender)
25
- @conn = conn
26
- @keep_alive = keep_alive
27
- @on_timeout = on_timeout
28
- @role = role
29
- now = Async::Clock.now
30
- @last_sent_at = now
31
- @last_received_at = now
32
- end
33
-
34
-
35
- # Call this every time a packet is written, to reset the send-idle
36
- # timer.
37
- def note_sent
38
- @last_sent_at = Async::Clock.now
39
- end
40
-
41
-
42
- # Call this every time a packet is read, to reset the recv-idle
43
- # timer.
44
- def note_received
45
- @last_received_at = Async::Clock.now
46
- end
47
-
48
-
49
- def run
50
- return if @keep_alive.zero? # 0 disables keepalive
51
- loop do
52
- sleep(@keep_alive / 2.0)
53
- now = Async::Clock.now
54
-
55
- if (now - @last_received_at) > (@keep_alive * 1.5)
56
- @on_timeout.call
57
- break
58
- end
59
-
60
- next unless @role == :sender
61
-
62
- if (now - @last_sent_at) >= (@keep_alive / 2.0)
63
- begin
64
- @conn.write_packet(Protocol::MQTT::Packet::Pingreq.new)
65
- note_sent
66
- rescue IOError, Errno::EPIPE, Errno::ECONNRESET
67
- break
68
- end
69
- end
70
- end
71
- rescue Async::Stop
72
- end
73
- end
74
- end
75
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "async/barrier"
4
- require "async/promise"
5
-
6
- module ZZQ
7
- class Engine
8
- # Owns the socket-level state: `:new → :open → :closing → :closed`.
9
- #
10
- # Adapted from NNQ::Engine::SocketLifecycle, minus the shared-IO-
11
- # thread fallback (ZZQ is pure-Async).
12
- class SocketLifecycle
13
- class InvalidTransition < RuntimeError; end
14
-
15
- STATES = %i[new open closing closed].freeze
16
-
17
- TRANSITIONS = {
18
- new: %i[open closed].freeze,
19
- open: %i[closing closed].freeze,
20
- closing: %i[closed].freeze,
21
- closed: [].freeze,
22
- }.freeze
23
-
24
-
25
- attr_reader :state
26
- attr_reader :parent_task
27
- attr_reader :peer_connected
28
- attr_reader :all_peers_gone
29
- attr_reader :barrier
30
- attr_accessor :reconnect_enabled
31
-
32
-
33
- def initialize
34
- @state = :new
35
- @parent_task = nil
36
- @peer_connected = Async::Promise.new
37
- @all_peers_gone = Async::Promise.new
38
- @reconnect_enabled = true
39
- @barrier = nil
40
- end
41
-
42
-
43
- def open? = @state == :open
44
- def closing? = @state == :closing
45
- def closed? = @state == :closed
46
- def alive? = @state == :new || @state == :open
47
-
48
-
49
- # Must be called from within an Async::Task. Raises
50
- # NotInAsyncContext otherwise.
51
- def capture_parent_task
52
- return false if @parent_task
53
- task = Async::Task.current? or raise NotInAsyncContext
54
- @parent_task = task
55
- @barrier = Async::Barrier.new(parent: @parent_task)
56
- transition!(:open)
57
- true
58
- end
59
-
60
-
61
- def start_closing! = transition!(:closing)
62
- def finish_closing! = transition!(:closed)
63
-
64
-
65
- def resolve_all_peers_gone_if_empty(connections)
66
- return unless @peer_connected.resolved? && connections.empty?
67
- return if @all_peers_gone.resolved?
68
- @all_peers_gone.resolve(true)
69
- end
70
-
71
-
72
- private def transition!(new_state)
73
- allowed = TRANSITIONS[@state]
74
- raise InvalidTransition, "#{@state} → #{new_state}" unless allowed&.include?(new_state)
75
- @state = new_state
76
- end
77
- end
78
- end
79
- end
data/lib/zzq/engine.rb DELETED
@@ -1,169 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "async"
4
- require "async/clock"
5
- require "set"
6
- require_relative "error"
7
- require_relative "engine/socket_lifecycle"
8
- require_relative "engine/connection_lifecycle"
9
-
10
- module ZZQ
11
- # Per-socket orchestrator. Owns listeners, the connection map, the
12
- # transport registry, and the socket-level state machine. Adapted
13
- # from NNQ::Engine minus reactor / inproc / SP-handshake concerns.
14
- class Engine
15
- # Scheme → transport module registry. Populated by each transport
16
- # file on require.
17
- @transports = {}
18
-
19
-
20
- class << self
21
- attr_reader :transports
22
- end
23
-
24
-
25
- attr_reader :options
26
- attr_reader :routing
27
- attr_reader :connections # Hash{Protocol::MQTT::Connection => ConnectionLifecycle}
28
- attr_reader :lifecycle
29
- attr_reader :last_endpoint
30
- attr_reader :dialed
31
- attr_accessor :monitor_queue
32
-
33
-
34
- # @yieldparam engine [Engine] build_routing hook; returns the
35
- # role-specific routing strategy.
36
- def initialize(options:)
37
- @options = options
38
- @connections = {}
39
- @listeners = []
40
- @lifecycle = SocketLifecycle.new
41
- @last_endpoint = nil
42
- @monitor_queue = nil
43
- @dialed = Set.new
44
- @dial_opts = {}
45
- @routing = yield(self)
46
- end
47
-
48
-
49
- def emit_monitor_event(type, endpoint: nil, detail: nil)
50
- return unless @monitor_queue
51
- @monitor_queue.enqueue(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
52
- rescue Async::Stop
53
- end
54
-
55
-
56
- def parent_task = @lifecycle.parent_task
57
- def barrier = @lifecycle.barrier
58
- def closed? = @lifecycle.closed?
59
- def peer_connected = @lifecycle.peer_connected
60
- def all_peers_gone = @lifecycle.all_peers_gone
61
-
62
-
63
- def resolve_all_peers_gone_if_empty
64
- @lifecycle.resolve_all_peers_gone_if_empty(@connections)
65
- end
66
-
67
-
68
- # Must be called from inside an Async::Task.
69
- def capture_parent_task
70
- @lifecycle.capture_parent_task
71
- end
72
-
73
-
74
- # Bind (listener) — role-agnostic. The routing layer (on accept)
75
- # drives the MQTT handshake.
76
- def bind(endpoint, **opts)
77
- transport = transport_for(endpoint)
78
- listener = transport.bind(endpoint, self, **opts)
79
- listener.start_accept_loop(@lifecycle.barrier) do |io|
80
- handle_accepted(io, endpoint: endpoint)
81
- end
82
- @listeners << listener
83
- @last_endpoint = listener.endpoint
84
- emit_monitor_event(:listening, endpoint: @last_endpoint)
85
- end
86
-
87
-
88
- # Connect (dialer).
89
- def connect(endpoint, **opts)
90
- @dialed << endpoint
91
- @dial_opts[endpoint] = opts unless opts.empty?
92
- @last_endpoint = endpoint
93
- transport_for(endpoint).connect(endpoint, self, **opts)
94
- end
95
-
96
-
97
- def dial_opts_for(endpoint)
98
- @dial_opts[endpoint] || {}
99
- end
100
-
101
-
102
- def maybe_reconnect(endpoint)
103
- # Reconnect loop not wired up yet (M4 job).
104
- end
105
-
106
-
107
- def transport_for(endpoint)
108
- scheme = endpoint[/\A([a-z+]+):\/\//i, 1] or raise Error, "no scheme: #{endpoint}"
109
- Engine.transports[scheme] or raise Error, "unsupported transport: #{scheme}"
110
- end
111
-
112
-
113
- # Called by transports for each accepted connection. Hands off to
114
- # routing so the broker can read CONNECT / write CONNACK. Returns
115
- # the +ConnectionLifecycle+ so frame-oriented transports (e.g.
116
- # WebSocket) can attach cancel-supervisors to its barrier.
117
- def handle_accepted(io, endpoint:)
118
- lifecycle = ConnectionLifecycle.new(self, endpoint: endpoint)
119
- mqtt = lifecycle.begin_handshake!(io)
120
- @routing.handle_accepted(mqtt, lifecycle) if @routing.respond_to?(:handle_accepted)
121
- lifecycle
122
- rescue => e
123
- lifecycle.handshake_failed!(error: e)
124
- raise unless e.is_a?(ConnectionRejected) || CONNECTION_LOST.any? { |c| e.is_a?(c) }
125
- end
126
-
127
-
128
- # Called by transports for each dialed connection. Hands off to
129
- # routing so the client can write CONNECT / read CONNACK. Returns
130
- # the +ConnectionLifecycle+ so frame-oriented transports (e.g.
131
- # WebSocket) can attach cancel-supervisors to its barrier.
132
- def handle_connected(io, endpoint:)
133
- lifecycle = ConnectionLifecycle.new(self, endpoint: endpoint)
134
- mqtt = lifecycle.begin_handshake!(io)
135
- @routing.handle_connected(mqtt, lifecycle) if @routing.respond_to?(:handle_connected)
136
- lifecycle
137
- rescue => e
138
- lifecycle.handshake_failed!(error: e)
139
- raise unless e.is_a?(ConnectionRejected) || CONNECTION_LOST.any? { |c| e.is_a?(c) }
140
- end
141
-
142
-
143
- def spawn_task(annotation:, parent: @lifecycle.barrier, &block)
144
- parent.async(annotation: annotation, &block)
145
- end
146
-
147
-
148
- def close
149
- return unless @lifecycle.alive?
150
-
151
- @lifecycle.start_closing!
152
- @listeners.each(&:stop)
153
- @routing.close if @routing.respond_to?(:close)
154
- @connections.values.each(&:close!)
155
-
156
- emit_monitor_event(:closed)
157
- @monitor_queue&.enqueue(nil)
158
-
159
- @lifecycle.barrier&.stop
160
- @lifecycle.finish_closing!
161
- @lifecycle.peer_connected.resolve(nil) unless @lifecycle.peer_connected.resolved?
162
- end
163
-
164
-
165
- def handle_connection_lost(conn)
166
- @connections[conn]&.lost!
167
- end
168
- end
169
- end
data/lib/zzq/error.rb DELETED
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ZZQ
4
- class Error < StandardError; end
5
-
6
- # Raised when sending on or interacting with a closed socket.
7
- class ClosedError < Error; end
8
-
9
- # Raised when the routing layer refuses to accept a peer (e.g. a
10
- # duplicate client id collision on the broker).
11
- class ConnectionRejected < Error; end
12
-
13
- # Raised when a socket method is called outside an Async::Task. ZZQ
14
- # is fiber-first: wrap calls in `Async do ... end` or use the block
15
- # form of #connect / #bind.
16
- class NotInAsyncContext < Error
17
- def initialize(msg = "ZZQ socket methods must run inside an Async task. " \
18
- "Wrap in `Async do ... end`, or use the block form " \
19
- "of #connect/#bind.")
20
- super
21
- end
22
- end
23
-
24
- # Server responded with a non-success CONNACK.
25
- class ConnectionRefused < Error
26
- attr_reader :reason_code
27
-
28
- def initialize(reason_code, message = nil)
29
- @reason_code = reason_code
30
- super(message || "broker refused connect: reason=0x#{reason_code.to_s(16)}")
31
- end
32
- end
33
-
34
- # Authentication failed.
35
- class AuthFailed < Error; end
36
-
37
- # Network errors that indicate the connection is gone.
38
- CONNECTION_LOST = [
39
- Errno::ECONNRESET, Errno::EPIPE, Errno::ENOTCONN, Errno::ECONNABORTED,
40
- Errno::EBADF, EOFError, IOError
41
- ].freeze
42
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ZZQ
4
- # A single event emitted by a Socket's #monitor task. Event types
5
- # cover both infrastructure lifecycle (:listening, :connected,
6
- # :disconnected, :closed) and MQTT semantics (:session_resumed,
7
- # :session_expired, :retained_updated, :publish_delivered,
8
- # :will_published).
9
- MonitorEvent = Data.define(:type, :endpoint, :detail) do
10
- def initialize(type:, endpoint: nil, detail: nil)
11
- super
12
- end
13
- end
14
- end