anyt 0.8.5 → 1.1.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/MIT-LICENSE +1 -1
- data/README.md +4 -1
- data/lib/anyt/cli.rb +81 -14
- data/lib/anyt/client.rb +12 -7
- data/lib/anyt/command.rb +33 -18
- data/lib/anyt/config.rb +15 -7
- data/lib/anyt/dummy/application.rb +31 -6
- data/lib/anyt/dummy/config.ru +4 -0
- data/lib/anyt/dummy/tmp/development_secret.txt +1 -0
- data/lib/anyt/ext/minitest.rb +24 -7
- data/lib/anyt/remote_control.rb +33 -0
- data/lib/anyt/rpc.rb +6 -9
- data/lib/anyt/tests.rb +5 -7
- data/lib/anyt/tests/core/ping_test.rb +2 -2
- data/lib/anyt/tests/core/welcome_test.rb +2 -2
- data/lib/anyt/tests/features/channel_state_test.rb +81 -0
- data/lib/anyt/tests/features/remote_disconnect_test.rb +30 -0
- data/lib/anyt/tests/features/server_restart_test.rb +28 -0
- data/lib/anyt/tests/request/channel_test.rb +28 -0
- data/lib/anyt/tests/request/connection_test.rb +23 -21
- data/lib/anyt/tests/request/disconnect_reasons_test.rb +23 -0
- data/lib/anyt/tests/request/disconnection_test.rb +50 -32
- data/lib/anyt/tests/streams/broadcast_test.rb +13 -13
- data/lib/anyt/tests/streams/multiple_clients_test.rb +13 -13
- data/lib/anyt/tests/streams/multiple_test.rb +15 -15
- data/lib/anyt/tests/streams/single_test.rb +39 -14
- data/lib/anyt/tests/streams/stop_test.rb +57 -0
- data/lib/anyt/tests/subscriptions/ack_test.rb +9 -11
- data/lib/anyt/tests/subscriptions/params_test.rb +6 -7
- data/lib/anyt/tests/subscriptions/perform_test.rb +16 -18
- data/lib/anyt/tests/subscriptions/transmissions_test.rb +6 -7
- data/lib/anyt/version.rb +1 -1
- metadata +38 -74
- data/.gitignore +0 -43
- data/.rubocop.yml +0 -80
- data/Gemfile +0 -6
- data/LICENSE.txt +0 -21
- data/Makefile +0 -5
- data/anyt.gemspec +0 -42
- data/circle.yml +0 -14
- data/etc/tests/channel_broadcast_test.rb +0 -51
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "drb/drb"
|
4
|
+
|
5
|
+
module Anyt
|
6
|
+
# Invoke commands within the running Ruby (Action Cable) server
|
7
|
+
module RemoteControl
|
8
|
+
class Server
|
9
|
+
class << self
|
10
|
+
alias start new
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(port)
|
14
|
+
DRb.start_service(
|
15
|
+
"druby://localhost:#{port}",
|
16
|
+
Client.new
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Client
|
22
|
+
def self.connect(port)
|
23
|
+
DRb.start_service
|
24
|
+
|
25
|
+
DRbObject.new_with_uri("druby://localhost:#{port}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def restart_action_cable
|
29
|
+
ActionCable.server.restart
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/anyt/rpc.rb
CHANGED
@@ -11,36 +11,33 @@ module Anyt # :nodoc:
|
|
11
11
|
|
12
12
|
class << self
|
13
13
|
attr_accessor :running
|
14
|
+
attr_reader :server
|
14
15
|
|
15
16
|
# rubocop: disable Metrics/AbcSize,Metrics/MethodLength
|
16
17
|
def start
|
17
|
-
ActionCable.server.config.cable = { "adapter" => "any_cable" }
|
18
|
-
Rails.application.initialize!
|
19
|
-
|
20
18
|
AnyCable.logger.debug "Starting RPC server ..."
|
21
19
|
|
22
20
|
AnyCable.server_callbacks.each(&:call)
|
23
21
|
|
24
|
-
@server = AnyCable::Server.new(
|
22
|
+
@server = AnyCable::GRPC::Server.new(
|
25
23
|
host: AnyCable.config.rpc_host,
|
26
|
-
**AnyCable.config.to_grpc_params
|
27
|
-
interceptors: AnyCable.middleware.to_a
|
24
|
+
**AnyCable.config.to_grpc_params
|
28
25
|
)
|
29
26
|
|
30
27
|
AnyCable.middleware.freeze
|
31
28
|
|
32
|
-
|
29
|
+
server.start
|
33
30
|
|
34
31
|
AnyCable.logger.debug "RPC server started"
|
35
32
|
end
|
36
33
|
# rubocop: enable Metrics/AbcSize,Metrics/MethodLength
|
37
34
|
|
38
35
|
def stop
|
39
|
-
|
36
|
+
server&.stop
|
40
37
|
end
|
41
38
|
end
|
42
39
|
|
43
40
|
AnyCable.connection_factory = ActionCable.server.config.connection_class.call
|
44
|
-
AnyCable.
|
41
|
+
AnyCable.config.log_level = :fatal unless AnyCable.config.debug
|
45
42
|
end
|
46
43
|
end
|
data/lib/anyt/tests.rb
CHANGED
@@ -24,8 +24,6 @@ module Anyt
|
|
24
24
|
#
|
25
25
|
# NOTE: We should run this before launching RPC server
|
26
26
|
|
27
|
-
# rubocop:disable Metrics/AbcSize
|
28
|
-
# rubocop:disable Metrics/MethodLength
|
29
27
|
def load_tests
|
30
28
|
return load_all_tests unless Anyt.config.filter_tests?
|
31
29
|
|
@@ -33,22 +31,22 @@ module Anyt
|
|
33
31
|
filter = Anyt.config.tests_filter
|
34
32
|
|
35
33
|
test_files_patterns.each do |pattern|
|
36
|
-
Dir.glob(pattern).each do |file|
|
37
|
-
if
|
34
|
+
Dir.glob(pattern).sort.each do |file|
|
35
|
+
if filter.call(file)
|
38
36
|
require file
|
39
37
|
else
|
40
|
-
skipped << file.gsub(File.join(__dir__,
|
38
|
+
skipped << file.gsub(File.join(__dir__, "tests/"), "").gsub("_test.rb", "")
|
41
39
|
end
|
42
40
|
end
|
43
41
|
end
|
44
42
|
|
45
|
-
$stdout.print "Skipping tests: #{skipped.join(
|
43
|
+
$stdout.print "Skipping tests: #{skipped.join(", ")}\n"
|
46
44
|
end
|
47
45
|
|
48
46
|
# Load all test files
|
49
47
|
def load_all_tests
|
50
48
|
test_files_patterns.each do |pattern|
|
51
|
-
Dir.glob(pattern).each { |file| require file }
|
49
|
+
Dir.glob(pattern).sort.each { |file| require file }
|
52
50
|
end
|
53
51
|
end
|
54
52
|
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Channel state" do
|
4
|
+
channel do
|
5
|
+
state_attr_accessor :user, :count
|
6
|
+
|
7
|
+
def subscribed
|
8
|
+
self.user = {name: params["name"]}
|
9
|
+
self.count = 1
|
10
|
+
|
11
|
+
stream_from "state_counts"
|
12
|
+
end
|
13
|
+
|
14
|
+
def tick
|
15
|
+
self.count += 2
|
16
|
+
transmit({count: count, name: user[:name]})
|
17
|
+
end
|
18
|
+
|
19
|
+
def unsubscribed
|
20
|
+
return unless params["notify_disconnect"]
|
21
|
+
|
22
|
+
ActionCable.server.broadcast("state_counts", {data: "user left: #{user[:name]}"})
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:identifier) { {channel: channel, name: "chipolino"}.to_json }
|
27
|
+
|
28
|
+
let(:client2) { build_client(ignore: %w[ping welcome]) }
|
29
|
+
let(:identifier2) { {channel: channel, name: "chipollone", notify_disconnect: true}.to_json }
|
30
|
+
|
31
|
+
before do
|
32
|
+
subscribe_request = {command: "subscribe", identifier: identifier}
|
33
|
+
|
34
|
+
client.send(subscribe_request)
|
35
|
+
|
36
|
+
ack = {
|
37
|
+
"identifier" => identifier, "type" => "confirm_subscription"
|
38
|
+
}
|
39
|
+
|
40
|
+
assert_equal ack, client.receive
|
41
|
+
end
|
42
|
+
|
43
|
+
scenario %(
|
44
|
+
Channel state is kept between commands
|
45
|
+
) do
|
46
|
+
perform_request = {
|
47
|
+
:command => "message",
|
48
|
+
:identifier => identifier,
|
49
|
+
"data" => {"action" => "tick"}.to_json
|
50
|
+
}
|
51
|
+
|
52
|
+
client.send(perform_request)
|
53
|
+
|
54
|
+
msg = {"identifier" => identifier, "message" => {"count" => 3, "name" => "chipolino"}}
|
55
|
+
|
56
|
+
assert_equal msg, client.receive
|
57
|
+
end
|
58
|
+
|
59
|
+
scenario %(
|
60
|
+
Channel state is available in #unsubscribe callbacks
|
61
|
+
) do
|
62
|
+
subscribe_request = {command: "subscribe", identifier: identifier2}
|
63
|
+
|
64
|
+
client2.send(subscribe_request)
|
65
|
+
|
66
|
+
ack = {
|
67
|
+
"identifier" => identifier2, "type" => "confirm_subscription"
|
68
|
+
}
|
69
|
+
|
70
|
+
assert_equal ack, client2.receive
|
71
|
+
|
72
|
+
client2.close
|
73
|
+
|
74
|
+
msg = {
|
75
|
+
"identifier" => identifier,
|
76
|
+
"message" => {"data" => "user left: chipollone"}
|
77
|
+
}
|
78
|
+
|
79
|
+
assert_equal msg, client.receive
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Remote disconnect" do
|
4
|
+
connect_handler("uid") do
|
5
|
+
self.uid = request.params["uid"]
|
6
|
+
uid.present?
|
7
|
+
end
|
8
|
+
|
9
|
+
scenario %(
|
10
|
+
Close single connection by id
|
11
|
+
) do
|
12
|
+
client = build_client(qs: "test=uid&uid=26", ignore: %w[ping])
|
13
|
+
assert_equal client.receive, "type" => "welcome"
|
14
|
+
|
15
|
+
ActionCable.server.remote_connections.where(uid: "26").disconnect
|
16
|
+
|
17
|
+
# Waiting for https://github.com/rails/rails/pull/39544
|
18
|
+
unless Anyt.config.use_action_cable
|
19
|
+
assert_equal(
|
20
|
+
client.receive,
|
21
|
+
"type" => "disconnect",
|
22
|
+
"reconnect" => true,
|
23
|
+
"reason" => "remote"
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
client.wait_for_close
|
28
|
+
assert client.closed?
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Server restart" do
|
4
|
+
connect_handler("reasons") do
|
5
|
+
next false if request.params[:reason] == "unauthorized"
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
scenario %(
|
10
|
+
Client receives disconnect message
|
11
|
+
) do
|
12
|
+
client = build_client(
|
13
|
+
qs: "test=reasons&reason=server_restart",
|
14
|
+
ignore: %(ping)
|
15
|
+
)
|
16
|
+
|
17
|
+
assert_equal client.receive, "type" => "welcome"
|
18
|
+
|
19
|
+
restart_server!
|
20
|
+
|
21
|
+
assert_equal(
|
22
|
+
client.receive,
|
23
|
+
"type" => "disconnect",
|
24
|
+
"reconnect" => true,
|
25
|
+
"reason" => "server_restart"
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Request" do
|
4
|
+
channel do
|
5
|
+
delegate :params, to: :connection
|
6
|
+
|
7
|
+
def subscribed
|
8
|
+
reject unless params[:token] == "secret"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
scenario %(
|
13
|
+
Channel has access to request
|
14
|
+
) do
|
15
|
+
client = build_client(qs: "token=secret", ignore: %w[ping])
|
16
|
+
assert_equal client.receive, "type" => "welcome"
|
17
|
+
|
18
|
+
subscribe_request = {command: "subscribe", identifier: {channel: channel}.to_json}
|
19
|
+
|
20
|
+
client.send(subscribe_request)
|
21
|
+
|
22
|
+
ack = {
|
23
|
+
"identifier" => {channel: channel}.to_json, "type" => "confirm_subscription"
|
24
|
+
}
|
25
|
+
|
26
|
+
assert_equal ack, client.receive
|
27
|
+
end
|
28
|
+
end
|
@@ -1,52 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
feature "Request" do
|
4
|
-
|
5
|
-
|
4
|
+
target_host = URI.parse(Anyt.config.target_url).host
|
5
|
+
|
6
|
+
connect_handler("request_url") do
|
7
|
+
request.url =~ /test=request_url/ && request.host == target_host
|
6
8
|
end
|
7
9
|
|
8
|
-
scenario %
|
10
|
+
scenario %(
|
9
11
|
Url is set during connection
|
10
|
-
|
11
|
-
client = build_client(qs:
|
12
|
+
) do
|
13
|
+
client = build_client(qs: "test=request_url")
|
12
14
|
assert_equal client.receive, "type" => "welcome"
|
13
15
|
end
|
14
16
|
|
15
|
-
connect_handler(
|
16
|
-
cookies[:username] ==
|
17
|
+
connect_handler("cookies") do
|
18
|
+
cookies[:username] == "john green"
|
17
19
|
end
|
18
20
|
|
19
|
-
scenario %
|
21
|
+
scenario %(
|
20
22
|
Reject when required cookies are not set
|
21
|
-
|
22
|
-
client = build_client(qs:
|
23
|
+
) do
|
24
|
+
client = build_client(qs: "test=cookies")
|
23
25
|
client.wait_for_close
|
24
26
|
assert client.closed?
|
25
27
|
end
|
26
28
|
|
27
|
-
scenario %
|
29
|
+
scenario %(
|
28
30
|
Accepts when required cookies are set
|
29
|
-
|
30
|
-
client = build_client(qs:
|
31
|
+
) do
|
32
|
+
client = build_client(qs: "test=cookies", cookies: "username=john green")
|
31
33
|
assert_equal client.receive, "type" => "welcome"
|
32
34
|
end
|
33
35
|
|
34
|
-
connect_handler(
|
35
|
-
request.headers[
|
36
|
+
connect_handler("headers") do
|
37
|
+
request.headers["X-API-TOKEN"] == "abc"
|
36
38
|
end
|
37
39
|
|
38
|
-
scenario %
|
40
|
+
scenario %(
|
39
41
|
Reject when required header is missing
|
40
|
-
|
41
|
-
client = build_client(qs:
|
42
|
+
) do
|
43
|
+
client = build_client(qs: "test=headers")
|
42
44
|
client.wait_for_close
|
43
45
|
assert client.closed?
|
44
46
|
end
|
45
47
|
|
46
|
-
scenario %
|
48
|
+
scenario %(
|
47
49
|
Accepts when required header is set
|
48
|
-
|
49
|
-
client = build_client(qs:
|
50
|
+
) do
|
51
|
+
client = build_client(qs: "test=headers", headers: {"x-api-token" => "abc"})
|
50
52
|
assert_equal client.receive, "type" => "welcome"
|
51
53
|
end
|
52
54
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
feature "Request" do
|
4
|
+
connect_handler("reasons") do
|
5
|
+
next false if request.params[:reason] == "unauthorized"
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
scenario %(
|
10
|
+
Receives disconnect message when rejected
|
11
|
+
) do
|
12
|
+
client = build_client(qs: "test=reasons&reason=unauthorized")
|
13
|
+
assert_equal(
|
14
|
+
client.receive,
|
15
|
+
"type" => "disconnect",
|
16
|
+
"reconnect" => false,
|
17
|
+
"reason" => "unauthorized"
|
18
|
+
)
|
19
|
+
|
20
|
+
client.wait_for_close
|
21
|
+
assert client.closed?
|
22
|
+
end
|
23
|
+
end
|
@@ -3,68 +3,76 @@
|
|
3
3
|
feature "Request" do
|
4
4
|
channel(:a) do
|
5
5
|
def subscribed
|
6
|
-
stream_from "
|
6
|
+
stream_from "request_a"
|
7
7
|
end
|
8
8
|
|
9
9
|
def unsubscribed
|
10
|
-
ActionCable.server.broadcast("
|
10
|
+
ActionCable.server.broadcast("request_a", {data: "user left"})
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
14
|
channel(:b) do
|
15
15
|
def subscribed
|
16
|
-
stream_from "
|
16
|
+
stream_from "request_b"
|
17
17
|
end
|
18
18
|
|
19
19
|
def unsubscribed
|
20
|
-
ActionCable.server.broadcast("
|
20
|
+
ActionCable.server.broadcast("request_b", {data: "user left"})
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
channel(:c) do
|
25
|
+
def subscribed
|
26
|
+
stream_from "request_c"
|
27
|
+
end
|
28
|
+
|
29
|
+
def unsubscribed
|
30
|
+
ActionCable.server.broadcast("request_c", {data: "user left#{params[:id].presence}"})
|
21
31
|
end
|
22
32
|
end
|
23
33
|
|
24
34
|
let(:client2) { build_client(ignore: %w[ping welcome]) }
|
25
35
|
|
26
|
-
|
27
|
-
|
36
|
+
scenario %(
|
37
|
+
Client disconnect invokes #unsubscribe callbacks
|
38
|
+
for different channels
|
39
|
+
) do
|
40
|
+
subscribe_request = {command: "subscribe", identifier: {channel: a_channel}.to_json}
|
28
41
|
|
29
42
|
client.send(subscribe_request)
|
30
43
|
|
31
44
|
ack = {
|
32
|
-
"identifier" => {
|
45
|
+
"identifier" => {channel: a_channel}.to_json, "type" => "confirm_subscription"
|
33
46
|
}
|
34
47
|
|
35
48
|
assert_equal ack, client.receive
|
36
49
|
|
37
|
-
subscribe_request = {
|
50
|
+
subscribe_request = {command: "subscribe", identifier: {channel: b_channel}.to_json}
|
38
51
|
|
39
52
|
client.send(subscribe_request)
|
40
53
|
|
41
54
|
ack = {
|
42
|
-
"identifier" => {
|
55
|
+
"identifier" => {channel: b_channel}.to_json, "type" => "confirm_subscription"
|
43
56
|
}
|
44
57
|
|
45
58
|
assert_equal ack, client.receive
|
46
|
-
end
|
47
59
|
|
48
|
-
|
49
|
-
Client disconnect invokes #unsubscribe callbacks
|
50
|
-
for different channels
|
51
|
-
} do
|
52
|
-
subscribe_request = { command: "subscribe", identifier: { channel: a_channel }.to_json }
|
60
|
+
subscribe_request = {command: "subscribe", identifier: {channel: a_channel}.to_json}
|
53
61
|
|
54
62
|
client2.send(subscribe_request)
|
55
63
|
|
56
64
|
ack = {
|
57
|
-
"identifier" => {
|
65
|
+
"identifier" => {channel: a_channel}.to_json, "type" => "confirm_subscription"
|
58
66
|
}
|
59
67
|
|
60
68
|
assert_equal ack, client2.receive
|
61
69
|
|
62
|
-
subscribe_request = {
|
70
|
+
subscribe_request = {command: "subscribe", identifier: {channel: b_channel}.to_json}
|
63
71
|
|
64
72
|
client2.send(subscribe_request)
|
65
73
|
|
66
74
|
ack = {
|
67
|
-
"identifier" => {
|
75
|
+
"identifier" => {channel: b_channel}.to_json, "type" => "confirm_subscription"
|
68
76
|
}
|
69
77
|
|
70
78
|
assert_equal ack, client2.receive
|
@@ -72,12 +80,12 @@ feature "Request" do
|
|
72
80
|
client2.close
|
73
81
|
|
74
82
|
msg = {
|
75
|
-
"identifier" => {
|
76
|
-
"message" => {
|
83
|
+
"identifier" => {channel: a_channel}.to_json,
|
84
|
+
"message" => {"data" => "user left"}
|
77
85
|
}
|
78
86
|
msg2 = {
|
79
|
-
"identifier" => {
|
80
|
-
"message" => {
|
87
|
+
"identifier" => {channel: b_channel}.to_json,
|
88
|
+
"message" => {"data" => "user left"}
|
81
89
|
}
|
82
90
|
|
83
91
|
msgs = [client.receive, client.receive]
|
@@ -86,26 +94,36 @@ feature "Request" do
|
|
86
94
|
assert_includes msgs, msg2
|
87
95
|
end
|
88
96
|
|
89
|
-
scenario %
|
97
|
+
scenario %(
|
90
98
|
Client disconnect invokes #unsubscribe callbacks
|
91
99
|
for multiple subscriptions from the same channel
|
92
|
-
|
93
|
-
subscribe_request = {
|
100
|
+
) do
|
101
|
+
subscribe_request = {command: "subscribe", identifier: {channel: c_channel}.to_json}
|
102
|
+
|
103
|
+
client.send(subscribe_request)
|
104
|
+
|
105
|
+
ack = {
|
106
|
+
"identifier" => {channel: c_channel}.to_json, "type" => "confirm_subscription"
|
107
|
+
}
|
108
|
+
|
109
|
+
assert_equal ack, client.receive
|
110
|
+
|
111
|
+
subscribe_request = {command: "subscribe", identifier: {channel: c_channel, id: 1}.to_json}
|
94
112
|
|
95
113
|
client2.send(subscribe_request)
|
96
114
|
|
97
115
|
ack = {
|
98
|
-
"identifier" => {
|
116
|
+
"identifier" => {channel: c_channel, id: 1}.to_json, "type" => "confirm_subscription"
|
99
117
|
}
|
100
118
|
|
101
119
|
assert_equal ack, client2.receive
|
102
120
|
|
103
|
-
subscribe_request = {
|
121
|
+
subscribe_request = {command: "subscribe", identifier: {channel: c_channel, id: 2}.to_json}
|
104
122
|
|
105
123
|
client2.send(subscribe_request)
|
106
124
|
|
107
125
|
ack = {
|
108
|
-
"identifier" => {
|
126
|
+
"identifier" => {channel: c_channel, id: 2}.to_json, "type" => "confirm_subscription"
|
109
127
|
}
|
110
128
|
|
111
129
|
assert_equal ack, client2.receive
|
@@ -113,13 +131,13 @@ feature "Request" do
|
|
113
131
|
client2.close
|
114
132
|
|
115
133
|
msg = {
|
116
|
-
"identifier" => {
|
117
|
-
"message" => {
|
134
|
+
"identifier" => {channel: c_channel}.to_json,
|
135
|
+
"message" => {"data" => "user left1"}
|
118
136
|
}
|
119
137
|
|
120
138
|
msg2 = {
|
121
|
-
"identifier" => {
|
122
|
-
"message" => {
|
139
|
+
"identifier" => {channel: c_channel}.to_json,
|
140
|
+
"message" => {"data" => "user left2"}
|
123
141
|
}
|
124
142
|
|
125
143
|
msgs = [client.receive, client.receive]
|