anycable 0.6.4 → 1.0.0.rc2

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.
@@ -48,8 +48,8 @@ module AnyCable
48
48
 
49
49
  unless middleware.is_a?(AnyCable::Middleware)
50
50
  raise ArgumentError,
51
- "AnyCable middleware must be a subclass of AnyCable::Middleware, " \
52
- "got #{middleware} instead"
51
+ "AnyCable middleware must be a subclass of AnyCable::Middleware, " \
52
+ "got #{middleware} instead"
53
53
  end
54
54
 
55
55
  middleware
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Middlewares
5
+ # Checks that RPC client version is compatible with
6
+ # the current RPC proto version
7
+ class CheckVersion < Middleware
8
+ attr_reader :version
9
+
10
+ def initialize(version)
11
+ @version = version
12
+ end
13
+
14
+ def call(_request, call, _method)
15
+ supported_versions = call.metadata["protov"]&.split(",")
16
+ return yield if supported_versions&.include?(version)
17
+
18
+ raise GRPC::Internal,
19
+ "Incompatible AnyCable RPC client.\nCurrent server version: #{version}.\n" \
20
+ "Client supported versions: #{call.metadata["protov"] || "unknown"}."
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/rpc/rpc_pb"
4
+ require "anycable/rpc/rpc_services_pb"
5
+
6
+ # Extend some PB auto-generated classes
7
+ module AnyCable
8
+ # Current RPC proto version (used for compatibility checks)
9
+ PROTO_VERSION = "v1"
10
+ SESSION_KEY = "_s_"
11
+
12
+ # Add setters/getter for cstate field
13
+ module WithConnectionState
14
+ def initialize(session: nil, **other)
15
+ if session
16
+ other[:cstate] ||= {}
17
+ other[:cstate][SESSION_KEY] = session
18
+ end
19
+ super(**other)
20
+ end
21
+
22
+ def session=(val)
23
+ self.cstate = {} unless cstate
24
+ cstate[SESSION_KEY] = val
25
+ end
26
+
27
+ def session
28
+ cstate[SESSION_KEY]
29
+ end
30
+
31
+ def cstate
32
+ env.cstate
33
+ end
34
+
35
+ def cstate=(val)
36
+ env.cstate = val
37
+ end
38
+
39
+ def istate
40
+ env.istate
41
+ end
42
+
43
+ def istate=(val)
44
+ env.istate = val
45
+ end
46
+ end
47
+
48
+ # Status predicates
49
+ module StatusPredicates
50
+ def success?
51
+ status == :SUCCESS
52
+ end
53
+
54
+ def failure?
55
+ status == :FAILURE
56
+ end
57
+
58
+ def error?
59
+ status == :ERROR
60
+ end
61
+ end
62
+
63
+ class ConnectionResponse
64
+ prepend WithConnectionState
65
+ include StatusPredicates
66
+ end
67
+
68
+ class CommandMessage
69
+ prepend WithConnectionState
70
+ end
71
+
72
+ class CommandResponse
73
+ prepend WithConnectionState
74
+ include StatusPredicates
75
+ end
76
+
77
+ class DisconnectRequest
78
+ prepend WithConnectionState
79
+ end
80
+
81
+ class DisconnectResponse
82
+ include StatusPredicates
83
+ end
84
+ end
@@ -1,51 +1,69 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Generated by the protocol buffer compiler. DO NOT EDIT!
2
4
  # source: rpc.proto
3
5
 
4
- require 'google/protobuf'
6
+ require "google/protobuf"
5
7
 
6
8
  Google::Protobuf::DescriptorPool.generated_pool.build do
7
- add_message "anycable.ConnectionRequest" do
8
- optional :path, :string, 1
9
- map :headers, :string, :string, 2
10
- end
11
- add_message "anycable.ConnectionResponse" do
12
- optional :status, :enum, 1, "anycable.Status"
13
- optional :identifiers, :string, 2
14
- repeated :transmissions, :string, 3
15
- optional :error_msg, :string, 4
16
- end
17
- add_message "anycable.CommandMessage" do
18
- optional :command, :string, 1
19
- optional :identifier, :string, 2
20
- optional :connection_identifiers, :string, 3
21
- optional :data, :string, 4
22
- end
23
- add_message "anycable.CommandResponse" do
24
- optional :status, :enum, 1, "anycable.Status"
25
- optional :disconnect, :bool, 2
26
- optional :stop_streams, :bool, 3
27
- repeated :streams, :string, 4
28
- repeated :transmissions, :string, 5
29
- optional :error_msg, :string, 6
30
- end
31
- add_message "anycable.DisconnectRequest" do
32
- optional :identifiers, :string, 1
33
- repeated :subscriptions, :string, 2
34
- optional :path, :string, 3
35
- map :headers, :string, :string, 4
36
- end
37
- add_message "anycable.DisconnectResponse" do
38
- optional :status, :enum, 1, "anycable.Status"
39
- optional :error_msg, :string, 2
40
- end
41
- add_enum "anycable.Status" do
42
- value :ERROR, 0
43
- value :SUCCESS, 1
44
- value :FAILURE, 2
9
+ add_file("rpc.proto", syntax: :proto3) do
10
+ add_message "anycable.Env" do
11
+ optional :url, :string, 1
12
+ map :headers, :string, :string, 2
13
+ map :cstate, :string, :string, 3
14
+ map :istate, :string, :string, 4
15
+ end
16
+ add_message "anycable.EnvResponse" do
17
+ map :cstate, :string, :string, 1
18
+ map :istate, :string, :string, 2
19
+ end
20
+ add_message "anycable.ConnectionRequest" do
21
+ optional :env, :message, 3, "anycable.Env"
22
+ end
23
+ add_message "anycable.ConnectionResponse" do
24
+ optional :status, :enum, 1, "anycable.Status"
25
+ optional :identifiers, :string, 2
26
+ repeated :transmissions, :string, 3
27
+ optional :error_msg, :string, 4
28
+ optional :env, :message, 5, "anycable.EnvResponse"
29
+ end
30
+ add_message "anycable.CommandMessage" do
31
+ optional :command, :string, 1
32
+ optional :identifier, :string, 2
33
+ optional :connection_identifiers, :string, 3
34
+ optional :data, :string, 4
35
+ optional :env, :message, 5, "anycable.Env"
36
+ end
37
+ add_message "anycable.CommandResponse" do
38
+ optional :status, :enum, 1, "anycable.Status"
39
+ optional :disconnect, :bool, 2
40
+ optional :stop_streams, :bool, 3
41
+ repeated :streams, :string, 4
42
+ repeated :transmissions, :string, 5
43
+ optional :error_msg, :string, 6
44
+ optional :env, :message, 7, "anycable.EnvResponse"
45
+ repeated :stopped_streams, :string, 8
46
+ end
47
+ add_message "anycable.DisconnectRequest" do
48
+ optional :identifiers, :string, 1
49
+ repeated :subscriptions, :string, 2
50
+ optional :env, :message, 5, "anycable.Env"
51
+ end
52
+ add_message "anycable.DisconnectResponse" do
53
+ optional :status, :enum, 1, "anycable.Status"
54
+ optional :error_msg, :string, 2
55
+ end
56
+ add_enum "anycable.Status" do
57
+ value :ERROR, 0
58
+ value :SUCCESS, 1
59
+ value :FAILURE, 2
60
+ end
45
61
  end
46
62
  end
47
63
 
48
64
  module AnyCable
65
+ Env = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.Env").msgclass
66
+ EnvResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.EnvResponse").msgclass
49
67
  ConnectionRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionRequest").msgclass
50
68
  ConnectionResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionResponse").msgclass
51
69
  CommandMessage = Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.CommandMessage").msgclass
@@ -1,17 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Generated by the protocol buffer compiler. DO NOT EDIT!
2
4
  # Source: rpc.proto for package 'anycable'
3
5
 
4
- require 'grpc'
6
+ require "grpc"
5
7
 
6
8
  module AnyCable
7
9
  module RPC
8
10
  class Service
9
-
10
11
  include GRPC::GenericService
11
12
 
12
13
  self.marshal_class_method = :encode
13
14
  self.unmarshal_class_method = :decode
14
- self.service_name = 'anycable.RPC'
15
+ self.service_name = "anycable.RPC"
15
16
 
16
17
  rpc :Connect, ConnectionRequest, ConnectionResponse
17
18
  rpc :Command, CommandMessage, CommandResponse
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "anycable/socket"
4
- require "anycable/rpc/rpc_pb"
5
- require "anycable/rpc/rpc_services_pb"
4
+ require "anycable/rpc"
6
5
 
7
6
  # rubocop:disable Metrics/AbcSize
8
7
  # rubocop:disable Metrics/MethodLength
@@ -14,22 +13,26 @@ module AnyCable
14
13
  def connect(request, _unused_call)
15
14
  logger.debug("RPC Connect: #{request.inspect}")
16
15
 
17
- socket = build_socket(env: rack_env(request))
16
+ socket = build_socket(env: rack_env(request.env))
18
17
 
19
18
  connection = factory.call(socket)
20
19
 
21
20
  connection.handle_open
22
21
 
23
22
  if socket.closed?
24
- AnyCable::ConnectionResponse.new(status: AnyCable::Status::FAILURE)
23
+ AnyCable::ConnectionResponse.new(
24
+ status: AnyCable::Status::FAILURE,
25
+ transmissions: socket.transmissions
26
+ )
25
27
  else
26
28
  AnyCable::ConnectionResponse.new(
27
29
  status: AnyCable::Status::SUCCESS,
28
30
  identifiers: connection.identifiers_json,
29
- transmissions: socket.transmissions
31
+ transmissions: socket.transmissions,
32
+ env: build_env_response(socket)
30
33
  )
31
34
  end
32
- rescue StandardError => exp
35
+ rescue => exp
33
36
  notify_exception(exp, :connect, request)
34
37
 
35
38
  AnyCable::ConnectionResponse.new(
@@ -41,7 +44,7 @@ module AnyCable
41
44
  def disconnect(request, _unused_call)
42
45
  logger.debug("RPC Disconnect: #{request.inspect}")
43
46
 
44
- socket = build_socket(env: rack_env(request))
47
+ socket = build_socket(env: rack_env(request.env))
45
48
 
46
49
  connection = factory.call(
47
50
  socket,
@@ -54,7 +57,7 @@ module AnyCable
54
57
  else
55
58
  AnyCable::DisconnectResponse.new(status: AnyCable::Status::FAILURE)
56
59
  end
57
- rescue StandardError => exp
60
+ rescue => exp
58
61
  notify_exception(exp, :disconnect, request)
59
62
 
60
63
  AnyCable::DisconnectResponse.new(
@@ -66,9 +69,7 @@ module AnyCable
66
69
  def command(message, _unused_call)
67
70
  logger.debug("RPC Command: #{message.inspect}")
68
71
 
69
- # We don't have path/headers information here,
70
- # but we still want `connection.env` to work
71
- socket = build_socket(env: base_rack_env)
72
+ socket = build_socket(env: rack_env(message.env))
72
73
 
73
74
  connection = factory.call(
74
75
  socket,
@@ -85,10 +86,12 @@ module AnyCable
85
86
  status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
86
87
  disconnect: socket.closed?,
87
88
  stop_streams: socket.stop_streams?,
88
- streams: socket.streams,
89
- transmissions: socket.transmissions
89
+ streams: socket.streams[:start],
90
+ stopped_streams: socket.streams[:stop],
91
+ transmissions: socket.transmissions,
92
+ env: build_env_response(socket)
90
93
  )
91
- rescue StandardError => exp
94
+ rescue => exp
92
95
  notify_exception(exp, :command, message)
93
96
 
94
97
  AnyCable::CommandResponse.new(
@@ -100,27 +103,31 @@ module AnyCable
100
103
  private
101
104
 
102
105
  # Build Rack env from request
103
- def rack_env(request)
104
- uri = URI.parse(request.path)
106
+ def rack_env(request_env)
107
+ uri = URI.parse(request_env.url)
105
108
 
106
109
  env = base_rack_env
107
- env.merge!(
110
+ env.merge!({
108
111
  "PATH_INFO" => uri.path,
109
112
  "QUERY_STRING" => uri.query,
110
113
  "SERVER_NAME" => uri.host,
111
- "SERVER_PORT" => uri.port.to_s,
114
+ "SERVER_PORT" => uri.port,
112
115
  "HTTP_HOST" => uri.host,
113
- "REMOTE_ADDR" => request.headers.delete("REMOTE_ADDR"),
114
- "rack.url_scheme" => uri.scheme
115
- )
116
-
117
- env.merge!(build_headers(request.headers))
116
+ "REMOTE_ADDR" => request_env.headers.delete("REMOTE_ADDR"),
117
+ "rack.url_scheme" => uri.scheme&.sub(/^ws/, "http"),
118
+ # AnyCable specific fields
119
+ "anycable.raw_cstate" => request_env.cstate&.to_h,
120
+ "anycable.raw_istate" => request_env.istate&.to_h
121
+ }.delete_if { |_k, v| v.nil? })
122
+
123
+ env.merge!(build_headers(request_env.headers))
118
124
  end
119
125
 
120
126
  def base_rack_env
121
127
  # Minimum required variables according to Rack Spec
122
128
  # (not all of them though, just those enough for Action Cable to work)
123
129
  # See https://rubydoc.info/github/rack/rack/master/file/SPEC
130
+ # and https://github.com/rack/rack/blob/master/lib/rack/lint.rb
124
131
  {
125
132
  "REQUEST_METHOD" => "GET",
126
133
  "SCRIPT_NAME" => "",
@@ -129,7 +136,13 @@ module AnyCable
129
136
  "SERVER_NAME" => "",
130
137
  "SERVER_PORT" => "80",
131
138
  "rack.url_scheme" => "http",
132
- "rack.input" => ""
139
+ "rack.input" => StringIO.new("", "r").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
140
+ "rack.version" => Rack::VERSION,
141
+ "rack.errors" => StringIO.new("").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
142
+ "rack.multithread" => true,
143
+ "rack.multiprocess" => false,
144
+ "rack.run_once" => false,
145
+ "rack.hijack?" => false
133
146
  }
134
147
  end
135
148
 
@@ -145,6 +158,13 @@ module AnyCable
145
158
  end
146
159
  end
147
160
 
161
+ def build_env_response(socket)
162
+ AnyCable::EnvResponse.new(
163
+ cstate: socket.cstate.changed_fields,
164
+ istate: socket.istate.changed_fields
165
+ )
166
+ end
167
+
148
168
  def logger
149
169
  AnyCable.logger
150
170
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Utils for testing AnyCable and its plugins
4
+ require "anycable/rspec/rpc_stub_context"
5
+ require "anycable/rspec/rpc_command_context"
6
+ require "anycable/rspec/with_grpc_server"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_context "anycable:rpc:command" do
4
+ include_context "anycable:rpc:stub"
5
+
6
+ let(:command) { "" }
7
+ let(:channel_id) { "" }
8
+ let(:identifiers) { {} }
9
+ let(:data) { {} }
10
+
11
+ let(:request) do
12
+ AnyCable::CommandMessage.new(
13
+ command: command,
14
+ identifier: channel_id,
15
+ connection_identifiers: identifiers.to_json,
16
+ data: data.to_json,
17
+ env: env
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_context "anycable:rpc:stub" do
4
+ before(:all) do
5
+ @service = AnyCable::RPC::Stub.new(AnyCable.config.rpc_host, :this_channel_is_insecure)
6
+ end
7
+
8
+ let(:service) { @service }
9
+
10
+ let(:url) { "ws://example.anycable.com/cable" }
11
+ let(:headers) { {} }
12
+ let(:env) { AnyCable::Env.new(url: url, headers: headers) }
13
+ end