anycable-rack-server 0.1.0 → 0.4.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: 9e57a096cf575590410db4c25e824d0b754af2b85eb0927e4d9fdf553da4ccea
4
- data.tar.gz: e6c5fce5b74f12bc41ce06955812cff608f50c4c66b063a459fa34ca8ed5e8e1
3
+ metadata.gz: ac33d07a88c1d94d982a7068f7636fc74013dd13e18a08ba29019c1b6e90bdb1
4
+ data.tar.gz: c655148349178d792f6609202762845189669053a14f89aa4a5f0c6efae67ff6
5
5
  SHA512:
6
- metadata.gz: 304f417fc3d477625effcc7134c81708d222df3da565613f4f5878b5c0365fecd5ed2a3e9620cc692976051f9a634a66623f8adfce2729664f34882135eeec2b
7
- data.tar.gz: d5a926617db60ec606f15b73189da24ed80d69012e87b8b4968ea6e0c1e0b7f26b86a3af593a48804f575dfd2c5c7867070555df968c69ba43926d4c97df4501
6
+ metadata.gz: 4ecbcb8c30cfa5e4b79948bc37c06405068d80bb192f53d98901d7177110da4822a20eff456a12fb7da5e7a3c3f8757ad219acb0befb2d7cd705288b3b1a3b12
7
+ data.tar.gz: a687c493138b5e0c7429e6a58c1b99ea4388a8b988988f0cc4bcab656db4789494e6b0fb05d59936794dfb6a99dc1b4868b90f82c551c049a2c33cf1c9c78d98
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019 Yulia Oletskaya
3
+ Copyright (c) 2019-2020 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
@@ -1,4 +1,6 @@
1
- [![Gem Version](https://badge.fury.io/rb/anycable-rack-server.svg)](https://rubygems.org/gems/anycable-rack-server) [![Build Status](https://travis-ci.org/anycable/anycable-rack-server.svg?branch=master)](https://travis-ci.org/anycable/anycable-rack-server)
1
+ [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/anycable-ruby-server.html)
2
+ [![Gem Version](https://badge.fury.io/rb/anycable-rack-server.svg)](https://rubygems.org/gems/anycable-rack-server)
3
+ [![Build](https://github.com/anycable/anycable-rack-server/workflows/Build/badge.svg)](https://github.com/anycable/anycable-rack-server/actions)
2
4
 
3
5
  # anycable-rack-server
4
6
 
@@ -8,9 +10,7 @@
8
10
 
9
11
  ```ruby
10
12
  # Initialize server instance first.
11
- #
12
- # NOTE: you must run RPC server yourself and provide its host
13
- ws_server = AnyCable::Rack::Server.new rpc_host: "localhost:50051"
13
+ ws_server = AnyCable::Rack::Server.new
14
14
 
15
15
  app = Rack::Builder.new do
16
16
  map "/cable" do
@@ -18,7 +18,7 @@ app = Rack::Builder.new do
18
18
  end
19
19
  end
20
20
 
21
- # NOTE: don't forget to call `start!` method
21
+ # NOTE: don't forget to call `start!` method
22
22
  ws_server.start!
23
23
 
24
24
  run app
@@ -28,59 +28,96 @@ run app
28
28
 
29
29
  Add `gem "anycable-rack-server"` to you `Gemfile` and make sure your Action Cable adapter is set to `:any_cable`. That's it! We automatically start AnyCable Rack server for your at `/cable` path.
30
30
 
31
- ## Settings
31
+ ## Configuration
32
+
33
+ AnyCable Rack Server uses [`anyway_config`](https://github.com/palkan/anyway_config) gem for configuration; thus it is possible to set configuration parameters through environment vars (prefixed with `ANYCABLE_`), `config/anycable.yml` file or `secrets.yml` when using Rails.
34
+
35
+ **NOTE:** AnyCable Rack Server uses the same config name (i.e., env prefix, YML file name, etc.) as AnyCable itself.
36
+
37
+ You can pass a config object as the option to `AnyCable::Rack::Server.new`:
38
+
39
+ ```ruby
40
+ server = AnyCable::Server::Rack.new(config: AnyCable::Rack::Config.new(**params))
41
+ ```
42
+
43
+ If no config is passed, a default, global, configuration would be used (`AnyCable::Rack.config`).
44
+
45
+ When using Rails, `config.anycable_rack` points to `AnyCable::Rack.config`.
46
+
47
+ ### Headers
32
48
 
33
49
  You can customize the headers being sent with each gRPC request.
34
50
 
35
51
  Default headers: `'cookie', 'x-api-token'`.
36
52
 
37
- Can be specified via options:
53
+ Can be specified via configuration:
38
54
 
39
55
  ```ruby
40
- ws_server = AnyCable::Rack::Server.new(
41
- rpc_host: "localhost:50051",
42
- headers: ["cookie", "x-my-header"]
43
- )
56
+ AnyCable::Rack.config.headers = ["cookie", "x-my-header"]
44
57
  ```
45
58
 
46
- In case of Rails you can set server options via `config.any_cable_rack`:
59
+ Or in Rails:
47
60
 
48
61
  ```ruby
49
62
  # <environment>.rb
50
63
  config.any_cable_rack.headers = %w[cookie]
64
+ ```
65
+
66
+ ### Rails-specific options
67
+
68
+ ```ruby
69
+ # Mount WebSocket server at the specified path
51
70
  config.any_cable_rack.mount_path = "/cable"
52
71
  # NOTE: here we specify only the port (we assume that a server is running locally)
53
72
  config.any_cable_rack.rpc_port = 50051
54
73
  ```
55
74
 
56
- ## Running RPC from the same process
75
+ ## Broadcast adapters
76
+
77
+ AnyCable Rack supports Redis (default) and HTTP broadcast adapters (see [the documentation](https://docs.anycable.io/#/ruby/broadcast_adapters)).
78
+
79
+ Broadcast adapter is inherited from AnyCable configuration (so, you don't need to configure it twice).
57
80
 
58
- The goal of the Rack server is to simplify the development/testing process. But we still have to run the RPC server.
81
+ ### Using HTTP broadcast adapter
59
82
 
60
- This gem also provides a way to run RPC server from the same process (spawning a new process):
83
+ ### With Rack
61
84
 
62
85
  ```ruby
63
- # in Rack app
86
+ AnyCable::Rack.config.broadast_adapter = :http
64
87
 
65
- AnyCable::Rack::RPCRunner.run(
66
- root_dir: "<path to your app root directory to run `anycable` from>",
67
- rpc_host: "...", # optional host to run RPC server on (defaults to '[::]::50051')
68
- command_args: [] # additional CLI arguments
69
- )
88
+ ws_server = AnyCable::Rack::Server
70
89
 
71
- # in Rails app you can just specify the configuration parameter (`false` by default)
72
- # and we'll take care of it
73
- config.any_cable_rack.run_rpc = true
90
+ app = Rack::Builder.new do
91
+ map "/cable" do
92
+ run ws_server
93
+ end
94
+
95
+ map "/_anycable_rack_broadcast" do
96
+ run ws_server.broadcast
97
+ end
98
+ end
74
99
  ```
75
100
 
101
+ ### With Rails
102
+
103
+ By default, we mount broadcasts endpoint at `/_anycable_rack_broadcast`.
104
+
105
+ You can change this setting:
106
+
107
+ ```ruby
108
+ config.any_cable_rack.http_broadcast_path = "/_my_broadcast"
109
+ ```
110
+
111
+ **NOTE:** Don't forget to configure `http_broadcast_url` for AnyCable pointing to your web server and the specified broadcast path.
112
+
76
113
  ## Testing
77
114
 
78
115
  Run units with `bundle exec rake`.
79
116
 
80
117
  ## Contributing
81
118
 
82
- Bug reports and pull requests are welcome on GitHub at https://github.com/anycable/anycable-rack-server.
119
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/anycable/anycable-rack-server](https://github.com/anycable/anycable-rack-server).
83
120
 
84
121
  ## License
85
122
 
86
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
123
+ The gem is available as open source under the terms of the [MIT License](./LICENSE).
@@ -1,4 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "anycable/rack/version"
4
+ require "anycable/rack/config"
3
5
  require "anycable/rack/server"
6
+
7
+ module AnyCable
8
+ module Rack
9
+ class << self
10
+ def config
11
+ @config ||= Config.new
12
+ end
13
+ end
14
+ end
15
+ end
16
+
4
17
  require "anycable/rack/railtie" if defined?(Rails)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AnyCable
6
+ module Rack
7
+ module BroadcastSubscribers
8
+ class BaseSubscriber
9
+ include Logging
10
+
11
+ attr_reader :hub, :coder
12
+
13
+ def initialize(hub:, coder:, **options)
14
+ @hub = hub
15
+ @coder = coder
16
+ end
17
+
18
+ def start
19
+ # no-op
20
+ end
21
+
22
+ def stop
23
+ # no-op
24
+ end
25
+
26
+ private
27
+
28
+ def handle_message(msg)
29
+ log(:debug) { "Received pub/sub message: #{msg}" }
30
+
31
+ data = JSON.parse(msg)
32
+ if data["stream"]
33
+ hub.broadcast(data["stream"], data["data"], coder)
34
+ elsif data["command"] == "disconnect"
35
+ hub.disconnect(data["payload"]["identifier"], data["payload"]["reconnect"], coder)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AnyCable
6
+ module Rack
7
+ module BroadcastSubscribers
8
+ # HTTP Pub/Sub subscriber
9
+ class HTTPSubscriber < BaseSubscriber
10
+ attr_reader :token, :path
11
+
12
+ def initialize(**options)
13
+ super
14
+ @token = options[:token]
15
+ @path = options[:path]
16
+ end
17
+
18
+ def start
19
+ log(:info) { "Accepting pub/sub request at #{path}" }
20
+ end
21
+
22
+ def call(env)
23
+ req = ::Rack::Request.new(env)
24
+
25
+ return invalid_request unless req.post?
26
+
27
+ if token && req.get_header("HTTP_AUTHORIZATION") != "Bearer #{token}"
28
+ return invalid_request(401)
29
+ end
30
+
31
+ handle_message req.body.read
32
+
33
+ [201, {"Content-Type" => "text/plain"}, ["OK"]]
34
+ end
35
+
36
+ private
37
+
38
+ def invalid_request(code = 422)
39
+ [code, {"Content-Type" => "text/plain"}, ["Invalid request"]]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ gem "redis", "~> 4"
4
+
3
5
  require "redis"
4
6
  require "json"
5
7
 
@@ -7,34 +9,47 @@ module AnyCable
7
9
  module Rack
8
10
  module BroadcastSubscribers
9
11
  # Redis Pub/Sub subscriber
10
- class RedisSubscriber
11
- attr_reader :hub, :coder, :redis_conn, :threads
12
+ class RedisSubscriber < BaseSubscriber
13
+ attr_reader :redis_conn, :thread, :channel
12
14
 
13
- def initialize(hub:, coder:, **options)
14
- @hub = hub
15
- @coder = coder
15
+ def initialize(hub:, coder:, channel:, **options)
16
+ super
16
17
  @redis_conn = ::Redis.new(options)
17
- @threads = {}
18
+ @channel = channel
18
19
  end
19
20
 
20
- def subscribe(channel)
21
- @threads[channel] = Thread.new do
22
- redis_conn.subscribe(channel) do |on|
23
- on.message { |_channel, msg| handle_message(msg) }
24
- end
25
- end
21
+ def start
22
+ subscribe(channel)
23
+
24
+ log(:info) { "Subscribed to #{channel}" }
26
25
  end
27
26
 
28
- def unsubscribe(channel)
29
- @threads[channel]&.terminate
30
- @threads.delete(channel)
27
+ def stop
28
+ thread&.terminate
31
29
  end
32
30
 
33
- private
31
+ def subscribe(channel)
32
+ @thread ||= Thread.new do
33
+ Thread.current.abort_on_exception = true
34
+
35
+ redis_conn.without_reconnect do
36
+ redis_conn.subscribe(channel) do |on|
37
+ on.subscribe do |chan, count|
38
+ log(:debug) { "Redis subscriber connected to #{chan} (#{count})" }
39
+ end
40
+
41
+ on.unsubscribe do |chan, count|
42
+ log(:debug) { "Redis subscribed disconnected from #{chan} (#{count})" }
43
+ end
34
44
 
35
- def handle_message(msg)
36
- data = JSON.parse(msg)
37
- hub.broadcast(data["stream"], data["data"], coder)
45
+ on.message do |_channel, msg|
46
+ handle_message(msg)
47
+ rescue
48
+ log(:error) { "Failed to broadcast message: #{msg}" }
49
+ end
50
+ end
51
+ end
52
+ end
38
53
  end
39
54
  end
40
55
  end
@@ -5,7 +5,7 @@ require "json"
5
5
  module AnyCable
6
6
  module Rack
7
7
  module Coders
8
- module JSON # :nodoc:
8
+ module Json # :nodoc:
9
9
  class << self
10
10
  def decode(json_str)
11
11
  ::JSON.parse(json_str)
@@ -0,0 +1,22 @@
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)
16
+ BinaryFrame.new(MessagePack.pack(ruby_obj))
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway_config"
4
+
5
+ module AnyCable
6
+ module Rack
7
+ class Config < Anyway::Config
8
+ DEFAULT_HEADERS = %w[cookie x-api-token].freeze
9
+
10
+ config_name :anycable
11
+ env_prefix "ANYCABLE"
12
+
13
+ attr_config mount_path: "/cable",
14
+ headers: DEFAULT_HEADERS,
15
+ coder: :json,
16
+ rpc_addr: "localhost:50051",
17
+ rpc_client_pool_size: 5,
18
+ rpc_client_timeout: 5,
19
+ http_broadcast_path: "/_anycable_rack_broadcast"
20
+ end
21
+ end
22
+ end
@@ -14,23 +14,24 @@ module AnyCable
14
14
  include Logging
15
15
 
16
16
  attr_reader :coder,
17
- :headers,
18
- :hub,
19
- :socket,
20
- :rpc_client,
21
- :sid
17
+ :headers,
18
+ :hub,
19
+ :socket,
20
+ :rpc_client,
21
+ :sid
22
22
 
23
- def initialize(socket, hub:, coder:, rpc_host:, headers:)
23
+ def initialize(socket, hub:, coder:, rpc_client:, headers:)
24
24
  @socket = socket
25
25
  @coder = coder
26
26
  @headers = headers
27
27
  @hub = hub
28
28
  @sid = SecureRandom.hex(6)
29
29
 
30
- @rpc_client = RPC::Client.new(rpc_host)
30
+ @rpc_client = rpc_client
31
31
 
32
- @_identifiers = "{}"
32
+ @_identifiers = "{}"
33
33
  @_subscriptions = Set.new
34
+ @_istate = {}
34
35
  end
35
36
 
36
37
  def handle_open
@@ -53,20 +54,21 @@ module AnyCable
53
54
  log(:debug) { "Command: #{decoded}" }
54
55
 
55
56
  case command
56
- when "subscribe" then subscribe(channel_identifier)
57
+ when "subscribe" then subscribe(channel_identifier)
57
58
  when "unsubscribe" then unsubscribe(channel_identifier)
58
- when "message" then send_message(channel_identifier, decoded["data"])
59
+ when "message" then send_message(channel_identifier, decoded["data"])
59
60
  else
60
61
  log(:error, "Command not found #{command}")
61
62
  end
62
- rescue Exception => e
63
+ rescue Exception => e # rubocop:disable Lint/RescueException
63
64
  log(:error, "Failed to execute command #{command}: #{e.message}")
64
65
  end
65
66
 
66
67
  private
67
68
 
68
69
  def transmit(cable_message)
69
- socket.transmit(encode(cable_message))
70
+ encoded = encode(cable_message)
71
+ socket.transmit(encoded)
70
72
  end
71
73
 
72
74
  def close
@@ -77,12 +79,8 @@ module AnyCable
77
79
  socket.request
78
80
  end
79
81
 
80
- def request_path
81
- request.fullpath
82
- end
83
-
84
82
  def rpc_connect
85
- rpc_client.connect(headers: headers, path: request_path)
83
+ rpc_client.connect(headers: headers, url: request.url)
86
84
  end
87
85
 
88
86
  def rpc_disconnect
@@ -90,7 +88,9 @@ module AnyCable
90
88
  identifiers: @_identifiers,
91
89
  subscriptions: @_subscriptions.to_a,
92
90
  headers: headers,
93
- path: request_path
91
+ url: request.url,
92
+ state: @_cstate,
93
+ channels_state: @_istate
94
94
  )
95
95
  end
96
96
 
@@ -99,7 +99,11 @@ module AnyCable
99
99
  command: command,
100
100
  identifier: identifier,
101
101
  connection_identifiers: @_identifiers,
102
- data: data
102
+ data: data,
103
+ headers: headers,
104
+ url: request.url,
105
+ connection_state: @_cstate,
106
+ state: @_istate[identifier]
103
107
  )
104
108
  end
105
109
 
@@ -130,16 +134,23 @@ module AnyCable
130
134
  end
131
135
 
132
136
  def process_command(response, identifier)
133
- response.transmissions.each { |transmission| transmit(decode(transmission)) }
137
+ response.transmissions.each { |transmission| transmit(decode_transmission(transmission)) }
134
138
  hub.remove_channel(socket, identifier) if response.stop_streams
135
139
  response.streams.each { |stream| hub.add_subscriber(stream, socket, identifier) }
140
+ response.stopped_streams.each { |stream| hub.remove_subscriber(stream, socket, identifier) }
141
+
142
+ @_istate[identifier] ||= {}
143
+ @_istate[identifier].merge!(response.env.istate&.to_h || {})
144
+
136
145
  close_connection if response.disconnect
137
146
  end
138
147
 
139
148
  def process_open(response)
149
+ response.transmissions&.each { |transmission| transmit(decode_transmission(transmission)) }
140
150
  if response.status == :SUCCESS
141
151
  @_identifiers = response.identifiers
142
- response.transmissions.each { |transmission| transmit(decode(transmission)) }
152
+ @_cstate = response.env.cstate&.to_h || {}
153
+ hub.add_socket(socket, @_identifiers)
143
154
  log(:debug) { "Opened" }
144
155
  else
145
156
  log(:error, "RPC connection command failed: #{response.inspect}")
@@ -171,6 +182,10 @@ module AnyCable
171
182
  coder.encode(cable_message)
172
183
  end
173
184
 
185
+ def decode_transmission(json_message)
186
+ JSON.parse(json_message)
187
+ end
188
+
174
189
  def decode(websocket_message)
175
190
  coder.decode(websocket_message)
176
191
  end