anycable-core 1.1.0.pre1

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.
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