anycable-core 1.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +84 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +78 -0
  5. data/bin/anycable +13 -0
  6. data/bin/anycabled +30 -0
  7. data/bin/console +7 -0
  8. data/bin/setup +6 -0
  9. data/lib/anycable.rb +114 -0
  10. data/lib/anycable/broadcast_adapters.rb +34 -0
  11. data/lib/anycable/broadcast_adapters/base.rb +29 -0
  12. data/lib/anycable/broadcast_adapters/http.rb +131 -0
  13. data/lib/anycable/broadcast_adapters/redis.rb +46 -0
  14. data/lib/anycable/cli.rb +319 -0
  15. data/lib/anycable/config.rb +127 -0
  16. data/lib/anycable/exceptions_handling.rb +35 -0
  17. data/lib/anycable/grpc.rb +30 -0
  18. data/lib/anycable/grpc/check_version.rb +33 -0
  19. data/lib/anycable/grpc/config.rb +53 -0
  20. data/lib/anycable/grpc/handler.rb +25 -0
  21. data/lib/anycable/grpc/rpc_services_pb.rb +24 -0
  22. data/lib/anycable/grpc/server.rb +103 -0
  23. data/lib/anycable/health_server.rb +73 -0
  24. data/lib/anycable/middleware.rb +10 -0
  25. data/lib/anycable/middleware_chain.rb +74 -0
  26. data/lib/anycable/middlewares/exceptions.rb +35 -0
  27. data/lib/anycable/protos/rpc_pb.rb +74 -0
  28. data/lib/anycable/rpc.rb +91 -0
  29. data/lib/anycable/rpc/handler.rb +50 -0
  30. data/lib/anycable/rpc/handlers/command.rb +36 -0
  31. data/lib/anycable/rpc/handlers/connect.rb +33 -0
  32. data/lib/anycable/rpc/handlers/disconnect.rb +27 -0
  33. data/lib/anycable/rspec.rb +4 -0
  34. data/lib/anycable/rspec/rpc_command_context.rb +21 -0
  35. data/lib/anycable/socket.rb +169 -0
  36. data/lib/anycable/version.rb +5 -0
  37. data/sig/anycable.rbs +37 -0
  38. data/sig/anycable/broadcast_adapters.rbs +5 -0
  39. data/sig/anycable/cli.rbs +40 -0
  40. data/sig/anycable/config.rbs +46 -0
  41. data/sig/anycable/exceptions_handling.rbs +14 -0
  42. data/sig/anycable/health_server.rbs +21 -0
  43. data/sig/anycable/middleware.rbs +5 -0
  44. data/sig/anycable/middleware_chain.rbs +22 -0
  45. data/sig/anycable/rpc.rbs +143 -0
  46. data/sig/anycable/socket.rbs +40 -0
  47. data/sig/anycable/version.rbs +3 -0
  48. metadata +237 -0
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Middlewares
5
+ class Exceptions < AnyCable::Middleware
6
+ def call(method_name, request)
7
+ yield
8
+ rescue => exp
9
+ notify_exception(exp, method_name, request)
10
+
11
+ response_class(method_name).new(
12
+ status: AnyCable::Status::ERROR,
13
+ error_msg: exp.message
14
+ )
15
+ end
16
+
17
+ private
18
+
19
+ def notify_exception(exp, method_name, message)
20
+ AnyCable::ExceptionsHandling.notify(exp, method_name.to_s, message.to_h)
21
+ end
22
+
23
+ def response_class(method_name)
24
+ case method_name
25
+ when :connect
26
+ AnyCable::ConnectionResponse
27
+ when :disconnect
28
+ AnyCable::DisconnectResponse
29
+ else
30
+ AnyCable::CommandResponse
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
4
+ # source: rpc.proto
5
+
6
+ require "google/protobuf"
7
+
8
+ Google::Protobuf::DescriptorPool.generated_pool.build do
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
61
+ end
62
+ end
63
+
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
67
+ ConnectionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionRequest").msgclass
68
+ ConnectionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionResponse").msgclass
69
+ CommandMessage = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.CommandMessage").msgclass
70
+ CommandResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.CommandResponse").msgclass
71
+ DisconnectRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.DisconnectRequest").msgclass
72
+ DisconnectResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.DisconnectResponse").msgclass
73
+ Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.Status").enummodule
74
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/protos/rpc_pb"
4
+
5
+ require "anycable/rpc/handler"
6
+
7
+ # Extend some PB auto-generated classes
8
+ module AnyCable
9
+ # Current RPC proto version (used for compatibility checks)
10
+ PROTO_VERSION = "v1"
11
+ SESSION_KEY = "_s_"
12
+
13
+ # Add setters/getter for cstate field
14
+ module WithConnectionState
15
+ def initialize(session: nil, **other)
16
+ if session
17
+ other[:cstate] ||= {}
18
+ other[:cstate][SESSION_KEY] = session
19
+ end
20
+ super(**other)
21
+ end
22
+
23
+ def session=(val)
24
+ self.cstate = {} unless cstate
25
+ state_ = cstate
26
+ if state_
27
+ state_[SESSION_KEY] = val
28
+ end
29
+ end
30
+
31
+ def session
32
+ state_ = cstate
33
+ if state_
34
+ state_[SESSION_KEY]
35
+ end
36
+ end
37
+
38
+ def cstate
39
+ env.cstate
40
+ end
41
+
42
+ def cstate=(val)
43
+ env.cstate = val
44
+ end
45
+
46
+ def istate
47
+ env.istate
48
+ end
49
+
50
+ def istate=(val)
51
+ env.istate = val
52
+ end
53
+ end
54
+
55
+ # Status predicates
56
+ module StatusPredicates
57
+ def success?
58
+ status == :SUCCESS
59
+ end
60
+
61
+ def failure?
62
+ status == :FAILURE
63
+ end
64
+
65
+ def error?
66
+ status == :ERROR
67
+ end
68
+ end
69
+
70
+ class ConnectionResponse
71
+ prepend WithConnectionState
72
+ include StatusPredicates
73
+ end
74
+
75
+ class CommandMessage
76
+ prepend WithConnectionState
77
+ end
78
+
79
+ class CommandResponse
80
+ prepend WithConnectionState
81
+ include StatusPredicates
82
+ end
83
+
84
+ class DisconnectRequest
85
+ prepend WithConnectionState
86
+ end
87
+
88
+ class DisconnectResponse
89
+ include StatusPredicates
90
+ end
91
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/rpc/handlers/connect"
4
+ require "anycable/rpc/handlers/disconnect"
5
+ require "anycable/rpc/handlers/command"
6
+
7
+ module AnyCable
8
+ module RPC
9
+ # Generic RPC handler
10
+ class Handler
11
+ include Handlers::Connect
12
+ include Handlers::Disconnect
13
+ include Handlers::Command
14
+
15
+ def initialize(middleware: AnyCable.middleware)
16
+ @middleware = middleware
17
+ @commands = {}
18
+ end
19
+
20
+ def handle(cmd, data)
21
+ middleware.call(cmd, data) do
22
+ send(cmd, data)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :commands, :middleware
29
+
30
+ def build_socket(env:)
31
+ AnyCable::Socket.new(env: env)
32
+ end
33
+
34
+ def build_env_response(socket)
35
+ AnyCable::EnvResponse.new(
36
+ cstate: socket.cstate.changed_fields,
37
+ istate: socket.istate.changed_fields
38
+ )
39
+ end
40
+
41
+ def logger
42
+ AnyCable.logger
43
+ end
44
+
45
+ def factory
46
+ AnyCable.connection_factory
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module RPC
5
+ module Handlers
6
+ module Command
7
+ def command(message)
8
+ logger.debug("RPC Command: #{message.inspect}")
9
+
10
+ socket = build_socket(env: message.env)
11
+
12
+ connection = factory.call(
13
+ socket,
14
+ identifiers: message.connection_identifiers
15
+ )
16
+
17
+ result = connection.handle_channel_command(
18
+ message.identifier,
19
+ message.command,
20
+ message.data
21
+ )
22
+
23
+ AnyCable::CommandResponse.new(
24
+ status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
25
+ disconnect: socket.closed?,
26
+ stop_streams: socket.stop_streams?,
27
+ streams: socket.streams[:start],
28
+ stopped_streams: socket.streams[:stop],
29
+ transmissions: socket.transmissions,
30
+ env: build_env_response(socket)
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module RPC
5
+ module Handlers
6
+ module Connect
7
+ def connect(request)
8
+ logger.debug("RPC Connect: #{request.inspect}")
9
+
10
+ socket = build_socket(env: request.env)
11
+
12
+ connection = factory.call(socket)
13
+
14
+ connection.handle_open
15
+
16
+ if socket.closed?
17
+ AnyCable::ConnectionResponse.new(
18
+ status: AnyCable::Status::FAILURE,
19
+ transmissions: socket.transmissions
20
+ )
21
+ else
22
+ AnyCable::ConnectionResponse.new(
23
+ status: AnyCable::Status::SUCCESS,
24
+ identifiers: connection.identifiers_json,
25
+ transmissions: socket.transmissions,
26
+ env: build_env_response(socket)
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module RPC
5
+ module Handlers
6
+ module Disconnect
7
+ def disconnect(request)
8
+ logger.debug("RPC Disconnect: #{request.inspect}")
9
+
10
+ socket = build_socket(env: request.env)
11
+
12
+ connection = factory.call(
13
+ socket,
14
+ identifiers: request.identifiers,
15
+ subscriptions: request.subscriptions
16
+ )
17
+
18
+ if connection.handle_close
19
+ AnyCable::DisconnectResponse.new(status: AnyCable::Status::SUCCESS)
20
+ else
21
+ AnyCable::DisconnectResponse.new(status: AnyCable::Status::FAILURE)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Utils for testing AnyCable and its plugins
4
+ require "anycable/rspec/rpc_command_context"
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_context "anycable:rpc:command" do
4
+ let(:url) { "ws://example.anycable.com/cable" }
5
+ let(:headers) { {} }
6
+ let(:env) { AnyCable::Env.new(url: url, headers: headers) }
7
+ let(:command) { "" }
8
+ let(:channel_id) { "" }
9
+ let(:identifiers) { {} }
10
+ let(:data) { {} }
11
+
12
+ let(:request) do
13
+ AnyCable::CommandMessage.new(
14
+ command: command,
15
+ identifier: channel_id,
16
+ connection_identifiers: identifiers.to_json,
17
+ data: data.to_json,
18
+ env: env
19
+ )
20
+ end
21
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ # Socket mock to be used with application connection
5
+ class Socket
6
+ # Represents the per-connection store
7
+ # (for example, used to keep session beetween RPC calls)
8
+ class State
9
+ attr_reader :dirty_keys, :source
10
+
11
+ def initialize(from)
12
+ @source = from
13
+ @dirty_keys = nil
14
+ end
15
+
16
+ def read(key)
17
+ source&.[](key)
18
+ end
19
+
20
+ alias_method :[], :read
21
+
22
+ def write(key, val)
23
+ return if source&.[](key) == val
24
+
25
+ @source ||= {}
26
+
27
+ keys = (@dirty_keys ||= [])
28
+ keys << key
29
+
30
+ source[key] = val
31
+ end
32
+
33
+ alias_method :[]=, :write
34
+
35
+ def changed_fields
36
+ return unless source
37
+
38
+ keys = dirty_keys
39
+ return if keys.nil?
40
+
41
+ source.slice(*keys)
42
+ end
43
+ end
44
+
45
+ attr_reader :transmissions
46
+
47
+ def initialize(env:)
48
+ @transmissions = []
49
+ @request_env = env
50
+ end
51
+
52
+ def transmit(websocket_message)
53
+ transmissions << websocket_message
54
+ end
55
+
56
+ def subscribe(_channel, broadcasting)
57
+ streams[:start] << broadcasting
58
+ end
59
+
60
+ def unsubscribe(_channel, broadcasting)
61
+ streams[:stop] << broadcasting
62
+ end
63
+
64
+ def unsubscribe_from_all(_channel)
65
+ @stop_all_streams = true
66
+ end
67
+
68
+ def streams
69
+ @streams ||= {start: [], stop: []}
70
+ end
71
+
72
+ def close
73
+ @closed = true
74
+ @streams&.clear
75
+ @stop_all_streams = true
76
+ end
77
+
78
+ def closed?
79
+ @closed == true
80
+ end
81
+
82
+ def stop_streams?
83
+ @stop_all_streams == true
84
+ end
85
+
86
+ def session
87
+ cstate.read(SESSION_KEY)
88
+ end
89
+
90
+ def session=(val)
91
+ cstate.write(SESSION_KEY, val)
92
+ end
93
+
94
+ def env
95
+ return @env if defined?(@env)
96
+
97
+ @env = build_rack_env
98
+ end
99
+
100
+ def istate
101
+ return @istate if defined?(@istate)
102
+
103
+ @istate = env["anycable.istate"] = State.new(env["anycable.raw_istate"])
104
+ end
105
+
106
+ def cstate
107
+ return @cstate if defined?(@cstate)
108
+
109
+ @cstate = env["anycable.cstate"] = State.new(env["anycable.raw_cstate"])
110
+ end
111
+
112
+ private
113
+
114
+ attr_reader :request_env
115
+
116
+ # Build Rack env from request
117
+ def build_rack_env
118
+ uri = URI.parse(request_env.url)
119
+
120
+ env = base_rack_env
121
+ env.merge!({
122
+ "PATH_INFO" => uri.path,
123
+ "QUERY_STRING" => uri.query,
124
+ "SERVER_NAME" => uri.host,
125
+ "SERVER_PORT" => uri.port,
126
+ "HTTP_HOST" => uri.host,
127
+ "REMOTE_ADDR" => request_env.headers.delete("REMOTE_ADDR"),
128
+ "rack.url_scheme" => uri.scheme&.sub(/^ws/, "http"),
129
+ # AnyCable specific fields
130
+ "anycable.raw_cstate" => request_env.cstate&.to_h,
131
+ "anycable.raw_istate" => request_env.istate&.to_h
132
+ }.delete_if { |_k, v| v.nil? })
133
+
134
+ env.merge!(build_headers(request_env.headers))
135
+ end
136
+
137
+ def base_rack_env
138
+ # Minimum required variables according to Rack Spec
139
+ # (not all of them though, just those enough for Action Cable to work)
140
+ # See https://rubydoc.info/github/rack/rack/master/file/SPEC
141
+ # and https://github.com/rack/rack/blob/master/lib/rack/lint.rb
142
+ {
143
+ "REQUEST_METHOD" => "GET",
144
+ "SCRIPT_NAME" => "",
145
+ "PATH_INFO" => "/",
146
+ "QUERY_STRING" => "",
147
+ "SERVER_NAME" => "",
148
+ "SERVER_PORT" => "80",
149
+ "rack.url_scheme" => "http",
150
+ "rack.input" => StringIO.new("", "r").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
151
+ "rack.version" => ::Rack::VERSION,
152
+ "rack.errors" => StringIO.new("").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
153
+ "rack.multithread" => true,
154
+ "rack.multiprocess" => false,
155
+ "rack.run_once" => false,
156
+ "rack.hijack?" => false
157
+ }
158
+ end
159
+
160
+ def build_headers(headers)
161
+ headers.each_with_object({}) do |header, obj|
162
+ k, v = *header
163
+ k = k.upcase
164
+ k.tr!("-", "_")
165
+ obj["HTTP_#{k}"] = v
166
+ end
167
+ end
168
+ end
169
+ end