zzq 0.1.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 +7 -0
- data/CHANGELOG.md +17 -0
- data/LICENSE +13 -0
- data/README.md +153 -0
- data/lib/zzq/broker.rb +34 -0
- data/lib/zzq/client.rb +51 -0
- data/lib/zzq/engine/connection_lifecycle.rb +139 -0
- data/lib/zzq/engine/keepalive.rb +75 -0
- data/lib/zzq/engine/socket_lifecycle.rb +79 -0
- data/lib/zzq/engine.rb +163 -0
- data/lib/zzq/error.rb +42 -0
- data/lib/zzq/message.rb +113 -0
- data/lib/zzq/monitor_event.rb +14 -0
- data/lib/zzq/options.rb +94 -0
- data/lib/zzq/persistence/interface.rb +43 -0
- data/lib/zzq/persistence/memory.rb +37 -0
- data/lib/zzq/persistence/pstore.rb +66 -0
- data/lib/zzq/routing/broker.rb +380 -0
- data/lib/zzq/routing/client.rb +327 -0
- data/lib/zzq/routing/packet_id_allocator.rb +54 -0
- data/lib/zzq/routing/qos_tracker.rb +108 -0
- data/lib/zzq/routing/retained_store.rb +67 -0
- data/lib/zzq/routing/topic_match.rb +49 -0
- data/lib/zzq/routing/topic_trie.rb +232 -0
- data/lib/zzq/socket.rb +146 -0
- data/lib/zzq/subscription.rb +70 -0
- data/lib/zzq/transport/ipc.rb +92 -0
- data/lib/zzq/transport/mqtt.rb +83 -0
- data/lib/zzq/transport/mqtts.rb +173 -0
- data/lib/zzq/version.rb +5 -0
- data/lib/zzq.rb +26 -0
- metadata +128 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/queue"
|
|
4
|
+
require "async/limited_queue"
|
|
5
|
+
|
|
6
|
+
module ZZQ
|
|
7
|
+
# Handle returned by {Client#subscribe} and {Broker#subscribe}. A
|
|
8
|
+
# thin wrapper around an +Async::LimitedQueue+ that also owns the
|
|
9
|
+
# UNSUBSCRIBE lifecycle.
|
|
10
|
+
#
|
|
11
|
+
# Callers consume with {#pop} or {#each}; {#close} sends UNSUBSCRIBE
|
|
12
|
+
# and wakes any blocked consumer with +nil+.
|
|
13
|
+
#
|
|
14
|
+
# Created internally — users should never call +.new+ directly.
|
|
15
|
+
class Subscription
|
|
16
|
+
# @return [Array<String>] topic filters this subscription matches.
|
|
17
|
+
attr_reader :filters
|
|
18
|
+
|
|
19
|
+
# @return [Integer] subscription identifier (v5) or nil (v3).
|
|
20
|
+
attr_reader :subscription_id
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def initialize(filters:, hwm:, subscription_id: nil, on_close: nil)
|
|
24
|
+
@filters = Array(filters).freeze
|
|
25
|
+
@subscription_id = subscription_id
|
|
26
|
+
@queue = Async::LimitedQueue.new(hwm)
|
|
27
|
+
@on_close = on_close
|
|
28
|
+
@closed = false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Blocks the fiber until a message arrives. Returns +nil+ when
|
|
33
|
+
# the subscription is closed.
|
|
34
|
+
def pop
|
|
35
|
+
return nil if @closed && @queue.empty?
|
|
36
|
+
@queue.dequeue
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Yields each message until the subscription is closed.
|
|
41
|
+
def each
|
|
42
|
+
return enum_for(:each) unless block_given?
|
|
43
|
+
while (msg = pop)
|
|
44
|
+
yield msg
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Sends UNSUBSCRIBE and wakes the consumer. Idempotent.
|
|
50
|
+
def close
|
|
51
|
+
return if @closed
|
|
52
|
+
@closed = true
|
|
53
|
+
@on_close&.call(self)
|
|
54
|
+
@queue.enqueue(nil)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def closed?
|
|
59
|
+
@closed
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Internal: deliver a message to the subscriber's queue. Blocks
|
|
64
|
+
# the caller if the queue is at high-water-mark.
|
|
65
|
+
def deliver(message)
|
|
66
|
+
return if @closed
|
|
67
|
+
@queue.enqueue(message)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "io/stream"
|
|
5
|
+
require_relative "../engine"
|
|
6
|
+
|
|
7
|
+
module ZZQ
|
|
8
|
+
module Transport
|
|
9
|
+
# IPC transport using Unix domain sockets.
|
|
10
|
+
#
|
|
11
|
+
# Endpoint forms:
|
|
12
|
+
# * `ipc:///var/run/zzq.sock` — filesystem path (portable).
|
|
13
|
+
# * `ipc://@zzq-local` — Linux abstract namespace (the
|
|
14
|
+
# leading `@` becomes a NUL byte in sockaddr.sun_path).
|
|
15
|
+
module IPC
|
|
16
|
+
Engine.transports["ipc"] = self
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def bind(endpoint, engine, **)
|
|
21
|
+
path = parse_path(endpoint)
|
|
22
|
+
sock_path = to_socket_path(path)
|
|
23
|
+
|
|
24
|
+
File.delete(sock_path) if !abstract?(path) && File.exist?(sock_path)
|
|
25
|
+
|
|
26
|
+
server = UNIXServer.new(sock_path)
|
|
27
|
+
Listener.new(endpoint, server, path, engine)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def connect(endpoint, engine, **)
|
|
32
|
+
path = parse_path(endpoint)
|
|
33
|
+
sock_path = to_socket_path(path)
|
|
34
|
+
sock = UNIXSocket.new(sock_path)
|
|
35
|
+
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_path(endpoint)
|
|
40
|
+
endpoint.sub(%r{\Aipc://}, "")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Linux abstract-namespace Unix sockets: leading `@` becomes a
|
|
45
|
+
# NUL byte in sun_path. Only supported on Linux.
|
|
46
|
+
def to_socket_path(path)
|
|
47
|
+
abstract?(path) ? "\0#{path[1..]}" : path
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def abstract?(path)
|
|
52
|
+
path.start_with?("@")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Listener
|
|
58
|
+
attr_reader :endpoint
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def initialize(endpoint, server, path, engine)
|
|
62
|
+
@endpoint = endpoint
|
|
63
|
+
@server = server
|
|
64
|
+
@path = path
|
|
65
|
+
@engine = engine
|
|
66
|
+
@task = nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def start_accept_loop(parent_task, &on_accepted)
|
|
71
|
+
@task = parent_task.async(annotation: "zzq ipc accept #{@endpoint}") do
|
|
72
|
+
loop do
|
|
73
|
+
client = @server.accept
|
|
74
|
+
on_accepted.call(IO::Stream::Buffered.wrap(client))
|
|
75
|
+
rescue Async::Stop, IOError
|
|
76
|
+
break
|
|
77
|
+
end
|
|
78
|
+
ensure
|
|
79
|
+
@server.close rescue nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def stop
|
|
85
|
+
@task&.stop
|
|
86
|
+
@server.close rescue nil
|
|
87
|
+
File.delete(@path) rescue nil unless IPC.abstract?(@path)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "io/stream"
|
|
6
|
+
require_relative "../engine"
|
|
7
|
+
|
|
8
|
+
module ZZQ
|
|
9
|
+
module Transport
|
|
10
|
+
# Plain MQTT-over-TCP transport. Endpoint scheme: `mqtt://host:port`
|
|
11
|
+
# (default port 1883). One server per bind, blocking accept inside
|
|
12
|
+
# an Async fiber.
|
|
13
|
+
module MQTT
|
|
14
|
+
Engine.transports["mqtt"] = self
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def bind(endpoint, engine, **)
|
|
19
|
+
host, port = parse_endpoint(endpoint)
|
|
20
|
+
host = "0.0.0.0" if host == "*"
|
|
21
|
+
server = TCPServer.new(host, port)
|
|
22
|
+
actual = server.local_address.ip_port
|
|
23
|
+
host_part = host.include?(":") ? "[#{host}]" : host
|
|
24
|
+
Listener.new("mqtt://#{host_part}:#{actual}", server, actual, engine)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def connect(endpoint, engine, **)
|
|
29
|
+
host, port = parse_endpoint(endpoint)
|
|
30
|
+
sock = ::Socket.tcp(host, port, connect_timeout: connect_timeout(engine.options))
|
|
31
|
+
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def connect_timeout(options)
|
|
36
|
+
ri = options.reconnect_interval
|
|
37
|
+
ri = ri.end if ri.is_a?(Range)
|
|
38
|
+
[ri, 0.5].max
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_endpoint(endpoint)
|
|
43
|
+
uri = URI.parse(endpoint.sub("mqtt://", "tcp://"))
|
|
44
|
+
[uri.hostname, uri.port]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Listener
|
|
50
|
+
attr_reader :endpoint, :port
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def initialize(endpoint, server, port, engine)
|
|
54
|
+
@endpoint = endpoint
|
|
55
|
+
@server = server
|
|
56
|
+
@port = port
|
|
57
|
+
@engine = engine
|
|
58
|
+
@task = nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def start_accept_loop(parent_task, &on_accepted)
|
|
63
|
+
@task = parent_task.async(annotation: "zzq mqtt accept #{@endpoint}") do
|
|
64
|
+
loop do
|
|
65
|
+
client = @server.accept
|
|
66
|
+
on_accepted.call(IO::Stream::Buffered.wrap(client))
|
|
67
|
+
rescue Async::Stop, IOError
|
|
68
|
+
break
|
|
69
|
+
end
|
|
70
|
+
ensure
|
|
71
|
+
@server.close rescue nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def stop
|
|
77
|
+
@task&.stop
|
|
78
|
+
@server.close rescue nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "io/stream"
|
|
7
|
+
require_relative "../engine"
|
|
8
|
+
|
|
9
|
+
module ZZQ
|
|
10
|
+
module Transport
|
|
11
|
+
# MQTT-over-TLS transport — endpoint scheme `mqtts://host:port`
|
|
12
|
+
# (default port 8883).
|
|
13
|
+
#
|
|
14
|
+
# Both bind and connect accept a `tls_context:` option — any
|
|
15
|
+
# +OpenSSL::SSL::SSLContext+. On bind the context must carry a
|
|
16
|
+
# cert + key; on connect a default client context is used if
|
|
17
|
+
# none is given. The transport does not constrain the context;
|
|
18
|
+
# see the README for a recommended TLS 1.3 +
|
|
19
|
+
# `TLS_CHACHA20_POLY1305_SHA256` configuration.
|
|
20
|
+
#
|
|
21
|
+
# Per-connection application data is capped at
|
|
22
|
+
# {DATA_LIMIT} bytes, after which the socket is torn down and
|
|
23
|
+
# the peer must reconnect. This keeps AEAD nonce counters well
|
|
24
|
+
# below their unsafe bound (e.g. ChaCha20-Poly1305's ~256 GiB
|
|
25
|
+
# per-key integrity bound) without relying on a specific cipher
|
|
26
|
+
# choice.
|
|
27
|
+
module MQTTS
|
|
28
|
+
Engine.transports["mqtts"] = self
|
|
29
|
+
|
|
30
|
+
# 128 GiB — conservative for every AEAD in TLS 1.3 (ChaCha20-
|
|
31
|
+
# Poly1305 at ~256 GiB, AES-GCM limits are higher under TLS 1.3
|
|
32
|
+
# record sizes). Reconnection negotiates fresh keys, so this
|
|
33
|
+
# caps nonce reuse exposure regardless of cipher choice.
|
|
34
|
+
DATA_LIMIT = 128 * 1024 * 1024 * 1024
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
def bind(endpoint, engine, tls_context: nil, **)
|
|
39
|
+
raise Error, "mqtts:// bind requires tls_context:" unless tls_context
|
|
40
|
+
|
|
41
|
+
host, port = parse_endpoint(endpoint)
|
|
42
|
+
host = "0.0.0.0" if host == "*"
|
|
43
|
+
server = TCPServer.new(host, port)
|
|
44
|
+
actual = server.local_address.ip_port
|
|
45
|
+
host_part = host.include?(":") ? "[#{host}]" : host
|
|
46
|
+
Listener.new("mqtts://#{host_part}:#{actual}", server, tls_context, engine)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def connect(endpoint, engine, tls_context: nil, **)
|
|
51
|
+
host, port = parse_endpoint(endpoint)
|
|
52
|
+
ctx = tls_context || OpenSSL::SSL::SSLContext.new
|
|
53
|
+
tcp = ::Socket.tcp(host, port, connect_timeout: connect_timeout(engine.options))
|
|
54
|
+
ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
|
|
55
|
+
ssl.hostname = host # SNI
|
|
56
|
+
ssl.sync_close = true
|
|
57
|
+
ssl.connect
|
|
58
|
+
engine.handle_connected(LimitedStream.wrap(ssl), endpoint: endpoint)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def connect_timeout(options)
|
|
63
|
+
ri = options.reconnect_interval
|
|
64
|
+
ri = ri.end if ri.is_a?(Range)
|
|
65
|
+
[ri, 0.5].max
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_endpoint(endpoint)
|
|
70
|
+
uri = URI.parse(endpoint.sub("mqtts://", "tcp://"))
|
|
71
|
+
[uri.hostname, uri.port]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Listener
|
|
77
|
+
attr_reader :endpoint
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def initialize(endpoint, server, tls_context, engine)
|
|
81
|
+
@endpoint = endpoint
|
|
82
|
+
@server = server
|
|
83
|
+
@ctx = tls_context
|
|
84
|
+
@engine = engine
|
|
85
|
+
@task = nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def start_accept_loop(parent_task, &on_accepted)
|
|
90
|
+
@task = parent_task.async(annotation: "zzq mqtts accept #{@endpoint}") do |task|
|
|
91
|
+
loop do
|
|
92
|
+
client = @server.accept
|
|
93
|
+
# TLS handshake per-connection, off the accept loop, so
|
|
94
|
+
# one slow handshake can't block the next client.
|
|
95
|
+
task.async(annotation: "zzq mqtts handshake") do
|
|
96
|
+
ssl = OpenSSL::SSL::SSLSocket.new(client, @ctx)
|
|
97
|
+
ssl.sync_close = true
|
|
98
|
+
ssl.accept
|
|
99
|
+
on_accepted.call(LimitedStream.wrap(ssl))
|
|
100
|
+
rescue Async::Stop, IOError, OpenSSL::SSL::SSLError
|
|
101
|
+
ssl&.close rescue nil
|
|
102
|
+
client.close rescue nil
|
|
103
|
+
end
|
|
104
|
+
rescue Async::Stop, IOError
|
|
105
|
+
break
|
|
106
|
+
end
|
|
107
|
+
ensure
|
|
108
|
+
@server.close rescue nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def stop
|
|
114
|
+
@task&.stop
|
|
115
|
+
@server.close rescue nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Buffered stream with a per-connection byte cap. When the cap
|
|
121
|
+
# is reached on either direction the underlying socket is
|
|
122
|
+
# closed and the next I/O raises — forcing the peer to
|
|
123
|
+
# reconnect and negotiate fresh keys before ChaCha20-Poly1305's
|
|
124
|
+
# nonce counter approaches its unsafe range.
|
|
125
|
+
class LimitedStream < IO::Stream::Buffered
|
|
126
|
+
def self.wrap(io, limit: DATA_LIMIT)
|
|
127
|
+
if io.respond_to?(:buffered=)
|
|
128
|
+
io.buffered = false
|
|
129
|
+
elsif io.respond_to?(:sync=)
|
|
130
|
+
io.sync = true
|
|
131
|
+
end
|
|
132
|
+
new(io, limit)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def initialize(io, limit = DATA_LIMIT)
|
|
137
|
+
super(io)
|
|
138
|
+
@limit = limit
|
|
139
|
+
@bytes = 0
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
protected
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def syswrite(buffer)
|
|
147
|
+
enforce!(buffer.bytesize)
|
|
148
|
+
super
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def sysread(size, buffer)
|
|
153
|
+
result = super
|
|
154
|
+
enforce!(result.bytesize) if result.is_a?(String)
|
|
155
|
+
result
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def enforce!(n)
|
|
163
|
+
@bytes += n
|
|
164
|
+
return if @bytes < @limit
|
|
165
|
+
|
|
166
|
+
close rescue nil
|
|
167
|
+
raise ZZQ::Error,
|
|
168
|
+
"mqtts:// data limit reached (#{@limit} B): dropping connection to force fresh keys"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/lib/zzq/version.rb
ADDED
data/lib/zzq.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ZZQ — async-native MQTT client and broker for Ruby.
|
|
4
|
+
#
|
|
5
|
+
# Built on `async`, `io-stream`, and `protocol-mqtt`. Applies the
|
|
6
|
+
# omq/nnq architecture (Socket base, Engine, pluggable transports,
|
|
7
|
+
# barrier-scoped lifecycle) to MQTT 3.1.1 and 5.0.
|
|
8
|
+
module ZZQ
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require_relative "zzq/version"
|
|
12
|
+
require_relative "zzq/error"
|
|
13
|
+
require_relative "zzq/monitor_event"
|
|
14
|
+
require_relative "zzq/options"
|
|
15
|
+
require_relative "zzq/message"
|
|
16
|
+
require_relative "zzq/subscription"
|
|
17
|
+
require_relative "zzq/engine"
|
|
18
|
+
require_relative "zzq/socket"
|
|
19
|
+
require_relative "zzq/transport/mqtt"
|
|
20
|
+
require_relative "zzq/transport/ipc"
|
|
21
|
+
require_relative "zzq/transport/mqtts"
|
|
22
|
+
require_relative "zzq/persistence/interface"
|
|
23
|
+
require_relative "zzq/persistence/memory"
|
|
24
|
+
require_relative "zzq/persistence/pstore"
|
|
25
|
+
require_relative "zzq/client"
|
|
26
|
+
require_relative "zzq/broker"
|
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: zzq
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Patrik Wenger
|
|
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: async
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.38'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
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).
|
|
71
|
+
email:
|
|
72
|
+
- paddor@gmail.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- CHANGELOG.md
|
|
78
|
+
- LICENSE
|
|
79
|
+
- README.md
|
|
80
|
+
- lib/zzq.rb
|
|
81
|
+
- lib/zzq/broker.rb
|
|
82
|
+
- 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
|
|
88
|
+
- lib/zzq/message.rb
|
|
89
|
+
- lib/zzq/monitor_event.rb
|
|
90
|
+
- 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
|
+
- lib/zzq/version.rb
|
|
107
|
+
homepage: https://github.com/paddor/zzq
|
|
108
|
+
licenses:
|
|
109
|
+
- ISC
|
|
110
|
+
metadata: {}
|
|
111
|
+
rdoc_options: []
|
|
112
|
+
require_paths:
|
|
113
|
+
- lib
|
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '4.0'
|
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
requirements: []
|
|
125
|
+
rubygems_version: 4.0.6
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: Pure Ruby MQTT 3.1.1 + 5.0 client and broker on Async
|
|
128
|
+
test_files: []
|