anycable-rack-server 0.2.1 → 0.5.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 +4 -4
- data/LICENSE +1 -1
- data/README.md +2 -19
- data/lib/anycable/rack/broadcast_subscribers/base_subscriber.rb +1 -1
- data/lib/anycable/rack/coders/json.rb +1 -1
- data/lib/anycable/rack/coders/msgpack.rb +26 -0
- data/lib/anycable/rack/coders/proto/message_pb.rb +35 -0
- data/lib/anycable/rack/coders/protobuf.rb +38 -0
- data/lib/anycable/rack/config.rb +2 -2
- data/lib/anycable/rack/connection.rb +8 -3
- data/lib/anycable/rack/hub.rb +5 -6
- data/lib/anycable/rack/middleware.rb +2 -1
- data/lib/anycable/rack/pinger.rb +5 -2
- data/lib/anycable/rack/rpc/client.rb +2 -2
- data/lib/anycable/rack/server.rb +7 -6
- data/lib/anycable/rack/socket.rb +15 -2
- data/lib/anycable/rack/version.rb +1 -1
- data/lib/anycable-rack-server.rb +0 -7
- metadata +22 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e0fb06453d826cf3c314edb0dbb6ab1f1ab8774760b40985b105aaf87ca1316
|
4
|
+
data.tar.gz: a6fc8f21bf0944cf1cacb58b779b222782e485af7fe8dcec1bf719b31e05d38f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cba311e11d372c3c6b42f530350bd81729102814a24209c63ae37460fc810b638db350afc82b930b5f8623acdc9423182e70489e220247f82c7c89f09efb8b74
|
7
|
+
data.tar.gz: a8541982f21133340285977c16852f3bbc41c18f4ce431e5b0261f4d3509f8bdf7f9d3816f500c8401eeac3de7223da39b8f5e008fd9c28e131322d4ab8588ad
|
data/LICENSE
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
MIT License
|
2
2
|
|
3
|
-
Copyright (c) 2019-
|
3
|
+
Copyright (c) 2019-2022 Yulia Oletskaya, Vladimir Dementyev
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -74,7 +74,8 @@ config.any_cable_rack.rpc_port = 50051
|
|
74
74
|
|
75
75
|
## Broadcast adapters
|
76
76
|
|
77
|
-
AnyCable Rack supports Redis (default) and HTTP broadcast adapters
|
77
|
+
AnyCable Rack supports Redis (default) and HTTP broadcast adapters
|
78
|
+
(see [the documentation](https://docs.anycable.io/ruby/broadcast_adapters)).
|
78
79
|
|
79
80
|
Broadcast adapter is inherited from AnyCable configuration (so, you don't need to configure it twice).
|
80
81
|
|
@@ -110,24 +111,6 @@ config.any_cable_rack.http_broadcast_path = "/_my_broadcast"
|
|
110
111
|
|
111
112
|
**NOTE:** Don't forget to configure `http_broadcast_url` for AnyCable pointing to your web server and the specified broadcast path.
|
112
113
|
|
113
|
-
## Running RPC from the same process
|
114
|
-
|
115
|
-
The goal of the Rack server is to simplify the development/testing process. But we still have to run the RPC server.
|
116
|
-
|
117
|
-
This gem also provides a way to run RPC server within the same process.
|
118
|
-
All you need to do is set `run_rpc = true` in the configuration:
|
119
|
-
|
120
|
-
```ruby
|
121
|
-
# in Rack app
|
122
|
-
AnyCable::Rack.config.run_rpc = true
|
123
|
-
|
124
|
-
# and only after that
|
125
|
-
ws_server.start!
|
126
|
-
|
127
|
-
# in Rails
|
128
|
-
config.any_cable_rack.run_rpc = true
|
129
|
-
```
|
130
|
-
|
131
114
|
## Testing
|
132
115
|
|
133
116
|
Run units with `bundle exec rake`.
|
@@ -32,7 +32,7 @@ module AnyCable
|
|
32
32
|
if data["stream"]
|
33
33
|
hub.broadcast(data["stream"], data["data"], coder)
|
34
34
|
elsif data["command"] == "disconnect"
|
35
|
-
hub.disconnect(data["payload"]["identifier"], data["payload"]["reconnect"])
|
35
|
+
hub.disconnect(data["payload"]["identifier"], data["payload"]["reconnect"], coder)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem "msgpack", "~> 1.4"
|
4
|
+
require "msgpack"
|
5
|
+
|
6
|
+
module AnyCable
|
7
|
+
module Rack
|
8
|
+
module Coders
|
9
|
+
module Msgpack # :nodoc:
|
10
|
+
class << self
|
11
|
+
def decode(bin)
|
12
|
+
MessagePack.unpack(bin)
|
13
|
+
end
|
14
|
+
|
15
|
+
def encode(ruby_obj, binary_frame_wrap: true)
|
16
|
+
message_packed = MessagePack.pack(ruby_obj)
|
17
|
+
|
18
|
+
return message_packed unless binary_frame_wrap
|
19
|
+
|
20
|
+
BinaryFrame.new(message_packed)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "google/protobuf"
|
4
|
+
|
5
|
+
Google::Protobuf::DescriptorPool.generated_pool.build do
|
6
|
+
add_message "action_cable.Message" do
|
7
|
+
optional :type, :enum, 1, "action_cable.Type"
|
8
|
+
optional :command, :enum, 2, "action_cable.Command"
|
9
|
+
optional :identifier, :string, 3
|
10
|
+
optional :data, :string, 4
|
11
|
+
optional :message, :bytes, 5
|
12
|
+
optional :reason, :string, 6
|
13
|
+
optional :reconnect, :bool, 7
|
14
|
+
end
|
15
|
+
add_enum "action_cable.Type" do
|
16
|
+
value :no_type, 0
|
17
|
+
value :welcome, 1
|
18
|
+
value :disconnect, 2
|
19
|
+
value :ping, 3
|
20
|
+
value :confirm_subscription, 4
|
21
|
+
value :reject_subscription, 5
|
22
|
+
end
|
23
|
+
add_enum "action_cable.Command" do
|
24
|
+
value :unknown_command, 0
|
25
|
+
value :subscribe, 1
|
26
|
+
value :unsubscribe, 2
|
27
|
+
value :message, 3
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ActionCable
|
32
|
+
Message = Google::Protobuf::DescriptorPool.generated_pool.lookup("action_cable.Message").msgclass
|
33
|
+
Type = Google::Protobuf::DescriptorPool.generated_pool.lookup("action_cable.Type").enummodule
|
34
|
+
Command = Google::Protobuf::DescriptorPool.generated_pool.lookup("action_cable.Command").enummodule
|
35
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem "google-protobuf", "~> 3.19", ">= 3.19.1"
|
4
|
+
require_relative "./proto/message_pb"
|
5
|
+
require_relative "./msgpack"
|
6
|
+
|
7
|
+
module AnyCable
|
8
|
+
module Rack
|
9
|
+
module Coders
|
10
|
+
module Protobuf # :nodoc:
|
11
|
+
class << self
|
12
|
+
def decode(bin)
|
13
|
+
decoded_message = ActionCable::Message.decode(bin).to_h
|
14
|
+
|
15
|
+
decoded_message[:command] = decoded_message[:command].to_s
|
16
|
+
if decoded_message[:message].present?
|
17
|
+
decoded_message[:message] = Msgpack.decode(decoded_message[:message])
|
18
|
+
end
|
19
|
+
|
20
|
+
decoded_message.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
21
|
+
end
|
22
|
+
|
23
|
+
def encode(ruby_obj)
|
24
|
+
message = ruby_obj.delete(:message)
|
25
|
+
|
26
|
+
data = ActionCable::Message.new(ruby_obj)
|
27
|
+
|
28
|
+
if message
|
29
|
+
data.message = Msgpack.encode(message, binary_frame_wrap: false)
|
30
|
+
end
|
31
|
+
|
32
|
+
BinaryFrame.new(ActionCable::Message.encode(data))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/anycable/rack/config.rb
CHANGED
@@ -12,11 +12,11 @@ module AnyCable
|
|
12
12
|
|
13
13
|
attr_config mount_path: "/cable",
|
14
14
|
headers: DEFAULT_HEADERS,
|
15
|
+
coder: :json,
|
15
16
|
rpc_addr: "localhost:50051",
|
16
17
|
rpc_client_pool_size: 5,
|
17
18
|
rpc_client_timeout: 5,
|
18
|
-
http_broadcast_path: "/_anycable_rack_broadcast"
|
19
|
-
run_rpc: false
|
19
|
+
http_broadcast_path: "/_anycable_rack_broadcast"
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -67,7 +67,8 @@ module AnyCable
|
|
67
67
|
private
|
68
68
|
|
69
69
|
def transmit(cable_message)
|
70
|
-
|
70
|
+
encoded = encode(cable_message)
|
71
|
+
socket.transmit(encoded)
|
71
72
|
end
|
72
73
|
|
73
74
|
def close
|
@@ -133,7 +134,7 @@ module AnyCable
|
|
133
134
|
end
|
134
135
|
|
135
136
|
def process_command(response, identifier)
|
136
|
-
response.transmissions.each { |transmission| transmit(
|
137
|
+
response.transmissions.each { |transmission| transmit(decode_transmission(transmission)) }
|
137
138
|
hub.remove_channel(socket, identifier) if response.stop_streams
|
138
139
|
response.streams.each { |stream| hub.add_subscriber(stream, socket, identifier) }
|
139
140
|
response.stopped_streams.each { |stream| hub.remove_subscriber(stream, socket, identifier) }
|
@@ -145,7 +146,7 @@ module AnyCable
|
|
145
146
|
end
|
146
147
|
|
147
148
|
def process_open(response)
|
148
|
-
response.transmissions&.each { |transmission| transmit(
|
149
|
+
response.transmissions&.each { |transmission| transmit(decode_transmission(transmission)) }
|
149
150
|
if response.status == :SUCCESS
|
150
151
|
@_identifiers = response.identifiers
|
151
152
|
@_cstate = response.env.cstate&.to_h || {}
|
@@ -181,6 +182,10 @@ module AnyCable
|
|
181
182
|
coder.encode(cable_message)
|
182
183
|
end
|
183
184
|
|
185
|
+
def decode_transmission(json_message)
|
186
|
+
JSON.parse(json_message)
|
187
|
+
end
|
188
|
+
|
184
189
|
def decode(websocket_message)
|
185
190
|
coder.decode(websocket_message)
|
186
191
|
end
|
data/lib/anycable/rack/hub.rb
CHANGED
@@ -71,7 +71,7 @@ module AnyCable
|
|
71
71
|
end
|
72
72
|
|
73
73
|
list.each do |(channel_id, sockets)|
|
74
|
-
decoded =
|
74
|
+
decoded = JSON.parse(message)
|
75
75
|
cmessage = channel_message(channel_id, decoded, coder)
|
76
76
|
sockets.each { |socket| socket.transmit(cmessage) }
|
77
77
|
end
|
@@ -81,14 +81,14 @@ module AnyCable
|
|
81
81
|
sockets.each_key { |socket| socket.transmit(message) }
|
82
82
|
end
|
83
83
|
|
84
|
-
def disconnect(identifier, reconnect)
|
84
|
+
def disconnect(identifier, reconnect, coder)
|
85
85
|
sockets = @sync.synchronize do
|
86
86
|
return unless @streams[INTERNAL_STREAM].key?(identifier)
|
87
87
|
|
88
88
|
@streams[INTERNAL_STREAM][identifier].to_a
|
89
89
|
end
|
90
90
|
|
91
|
-
msg = disconnect_message("remote", reconnect)
|
91
|
+
msg = disconnect_message("remote", reconnect, coder)
|
92
92
|
|
93
93
|
sockets.each do |socket|
|
94
94
|
socket.transmit(msg)
|
@@ -115,9 +115,8 @@ module AnyCable
|
|
115
115
|
coder.encode(identifier: channel_id, message: message)
|
116
116
|
end
|
117
117
|
|
118
|
-
|
119
|
-
|
120
|
-
{type: :disconnect, reason: reason, reconnect: reconnect}.to_json
|
118
|
+
def disconnect_message(reason, reconnect, coder)
|
119
|
+
coder.encode({type: :disconnect, reason: reason, reconnect: reconnect})
|
121
120
|
end
|
122
121
|
end
|
123
122
|
end
|
@@ -9,7 +9,8 @@ require "anycable/rack/socket"
|
|
9
9
|
module AnyCable
|
10
10
|
module Rack
|
11
11
|
class Middleware # :nodoc:
|
12
|
-
PROTOCOLS = [
|
12
|
+
PROTOCOLS = %w[actioncable-v1-json actioncable-v1-msgpack actioncable-unsupported actioncable-v1-protobuf].freeze
|
13
|
+
|
13
14
|
attr_reader :pinger,
|
14
15
|
:hub,
|
15
16
|
:coder,
|
data/lib/anycable/rack/pinger.rb
CHANGED
@@ -8,7 +8,10 @@ module AnyCable
|
|
8
8
|
class Pinger
|
9
9
|
INTERVAL = 3
|
10
10
|
|
11
|
-
|
11
|
+
attr_reader :coder
|
12
|
+
|
13
|
+
def initialize(coder)
|
14
|
+
@coder = coder
|
12
15
|
@_sockets = []
|
13
16
|
@_stopped = false
|
14
17
|
end
|
@@ -45,7 +48,7 @@ module AnyCable
|
|
45
48
|
private
|
46
49
|
|
47
50
|
def ping_message(time)
|
48
|
-
{type: :ping, message: time}
|
51
|
+
coder.encode({type: :ping, message: time})
|
49
52
|
end
|
50
53
|
end
|
51
54
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "connection_pool"
|
4
|
-
require "grpc"
|
4
|
+
require "anycable/grpc"
|
5
5
|
|
6
6
|
module AnyCable
|
7
7
|
module Rack
|
@@ -12,7 +12,7 @@ module AnyCable
|
|
12
12
|
|
13
13
|
def initialize(host:, size:, timeout:)
|
14
14
|
@pool = ConnectionPool.new(size: size, timeout: timeout) do
|
15
|
-
AnyCable::
|
15
|
+
AnyCable::GRPC::Service.rpc_stub_class.new(host, :this_channel_is_insecure)
|
16
16
|
end
|
17
17
|
@metadata = {metadata: {"protov" => "v1"}}.freeze
|
18
18
|
end
|
data/lib/anycable/rack/server.rb
CHANGED
@@ -8,7 +8,6 @@ require "anycable/rack/errors"
|
|
8
8
|
require "anycable/rack/middleware"
|
9
9
|
require "anycable/rack/logging"
|
10
10
|
require "anycable/rack/broadcast_subscribers/base_subscriber"
|
11
|
-
require "anycable/rack/coders/json"
|
12
11
|
|
13
12
|
module AnyCable # :nodoc: all
|
14
13
|
module Rack
|
@@ -28,9 +27,8 @@ module AnyCable # :nodoc: all
|
|
28
27
|
def initialize(config: AnyCable::Rack.config)
|
29
28
|
@config = config
|
30
29
|
@hub = Hub.new
|
31
|
-
@
|
32
|
-
|
33
|
-
@coder = Coders::JSON
|
30
|
+
@coder = resolve_coder(config.coder)
|
31
|
+
@pinger = Pinger.new(coder)
|
34
32
|
|
35
33
|
@broadcast = resolve_broadcast_adapter
|
36
34
|
@rpc_client = RPC::Client.new(
|
@@ -58,8 +56,6 @@ module AnyCable # :nodoc: all
|
|
58
56
|
|
59
57
|
broadcast.start
|
60
58
|
|
61
|
-
Rack.rpc_server.run if config.run_rpc
|
62
|
-
|
63
59
|
@_started = true
|
64
60
|
end
|
65
61
|
|
@@ -114,6 +110,11 @@ module AnyCable # :nodoc: all
|
|
114
110
|
raise ArgumentError, "Unsupported broadcast adatper: #{adapter}. AnyCable Rack server only supports: redis, http"
|
115
111
|
end
|
116
112
|
end
|
113
|
+
|
114
|
+
def resolve_coder(name)
|
115
|
+
require "anycable/rack/coders/#{name}"
|
116
|
+
AnyCable::Rack::Coders.const_get(name.capitalize)
|
117
|
+
end
|
117
118
|
end
|
118
119
|
end
|
119
120
|
end
|
data/lib/anycable/rack/socket.rb
CHANGED
@@ -4,6 +4,17 @@ require "anycable/rack/logging"
|
|
4
4
|
|
5
5
|
module AnyCable
|
6
6
|
module Rack
|
7
|
+
# Wrapper for outgoing data used to correctly set the WS frame type
|
8
|
+
class BinaryFrame
|
9
|
+
def initialize(data)
|
10
|
+
@data = data
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
@data.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
7
18
|
# Socket wrapper
|
8
19
|
class Socket
|
9
20
|
include Logging
|
@@ -22,7 +33,9 @@ module AnyCable
|
|
22
33
|
@_active = true
|
23
34
|
end
|
24
35
|
|
25
|
-
def transmit(data, type:
|
36
|
+
def transmit(data, type: nil)
|
37
|
+
# p "DATA: #{data.class} — #{data.to_s}"
|
38
|
+
type ||= data.is_a?(BinaryFrame) ? :binary : :text
|
26
39
|
frame = WebSocket::Frame::Outgoing::Server.new(
|
27
40
|
version: version,
|
28
41
|
data: data,
|
@@ -144,7 +157,7 @@ module AnyCable
|
|
144
157
|
end
|
145
158
|
end
|
146
159
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
147
|
-
log(:error, "Socket frame error: #{e}")
|
160
|
+
log(:error, "Socket frame error: #{e}\n #{e.backtrace.take(4).join("\n")}")
|
148
161
|
nil # client disconnected or timed out
|
149
162
|
end
|
150
163
|
end
|
data/lib/anycable-rack-server.rb
CHANGED
@@ -10,13 +10,6 @@ module AnyCable
|
|
10
10
|
def config
|
11
11
|
@config ||= Config.new
|
12
12
|
end
|
13
|
-
|
14
|
-
def rpc_server
|
15
|
-
return @rpc_server if instance_variable_defined?(:@rpc_server)
|
16
|
-
|
17
|
-
require "anycable/cli"
|
18
|
-
@rpc_server = AnyCable::CLI.new(embedded: true)
|
19
|
-
end
|
20
13
|
end
|
21
14
|
end
|
22
15
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: anycable-rack-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yulia Oletskaya
|
8
8
|
- Vladimir Dementyev
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2022-04-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: anyway_config
|
@@ -17,28 +17,34 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: 1.
|
20
|
+
version: 2.1.0
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: 1.
|
27
|
+
version: 2.1.0
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: anycable
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
|
-
- - "
|
32
|
+
- - ">"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 1.0.99
|
35
|
+
- - "<"
|
33
36
|
- !ruby/object:Gem::Version
|
34
|
-
version:
|
37
|
+
version: '2.0'
|
35
38
|
type: :runtime
|
36
39
|
prerelease: false
|
37
40
|
version_requirements: !ruby/object:Gem::Requirement
|
38
41
|
requirements:
|
39
|
-
- - "
|
42
|
+
- - ">"
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 1.0.99
|
45
|
+
- - "<"
|
40
46
|
- !ruby/object:Gem::Version
|
41
|
-
version:
|
47
|
+
version: '2.0'
|
42
48
|
- !ruby/object:Gem::Dependency
|
43
49
|
name: connection_pool
|
44
50
|
requirement: !ruby/object:Gem::Requirement
|
@@ -164,6 +170,9 @@ files:
|
|
164
170
|
- lib/anycable/rack/broadcast_subscribers/http_subscriber.rb
|
165
171
|
- lib/anycable/rack/broadcast_subscribers/redis_subscriber.rb
|
166
172
|
- lib/anycable/rack/coders/json.rb
|
173
|
+
- lib/anycable/rack/coders/msgpack.rb
|
174
|
+
- lib/anycable/rack/coders/proto/message_pb.rb
|
175
|
+
- lib/anycable/rack/coders/protobuf.rb
|
167
176
|
- lib/anycable/rack/config.rb
|
168
177
|
- lib/anycable/rack/connection.rb
|
169
178
|
- lib/anycable/rack/errors.rb
|
@@ -177,11 +186,11 @@ files:
|
|
177
186
|
- lib/anycable/rack/server.rb
|
178
187
|
- lib/anycable/rack/socket.rb
|
179
188
|
- lib/anycable/rack/version.rb
|
180
|
-
homepage:
|
189
|
+
homepage:
|
181
190
|
licenses:
|
182
191
|
- MIT
|
183
192
|
metadata: {}
|
184
|
-
post_install_message:
|
193
|
+
post_install_message:
|
185
194
|
rdoc_options: []
|
186
195
|
require_paths:
|
187
196
|
- lib
|
@@ -196,8 +205,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
196
205
|
- !ruby/object:Gem::Version
|
197
206
|
version: '0'
|
198
207
|
requirements: []
|
199
|
-
rubygems_version: 3.
|
200
|
-
signing_key:
|
208
|
+
rubygems_version: 3.3.7
|
209
|
+
signing_key:
|
201
210
|
specification_version: 4
|
202
211
|
summary: AnyCable Rack Server
|
203
212
|
test_files: []
|