actioncable 6.0.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 +7 -0
- data/CHANGELOG.md +169 -0
- data/MIT-LICENSE +20 -0
- data/README.md +24 -0
- data/app/assets/javascripts/action_cable.js +517 -0
- data/lib/action_cable.rb +62 -0
- data/lib/action_cable/channel.rb +17 -0
- data/lib/action_cable/channel/base.rb +311 -0
- data/lib/action_cable/channel/broadcasting.rb +41 -0
- data/lib/action_cable/channel/callbacks.rb +37 -0
- data/lib/action_cable/channel/naming.rb +25 -0
- data/lib/action_cable/channel/periodic_timers.rb +78 -0
- data/lib/action_cable/channel/streams.rb +176 -0
- data/lib/action_cable/channel/test_case.rb +310 -0
- data/lib/action_cable/connection.rb +22 -0
- data/lib/action_cable/connection/authorization.rb +15 -0
- data/lib/action_cable/connection/base.rb +264 -0
- data/lib/action_cable/connection/client_socket.rb +157 -0
- data/lib/action_cable/connection/identification.rb +47 -0
- data/lib/action_cable/connection/internal_channel.rb +45 -0
- data/lib/action_cable/connection/message_buffer.rb +54 -0
- data/lib/action_cable/connection/stream.rb +117 -0
- data/lib/action_cable/connection/stream_event_loop.rb +136 -0
- data/lib/action_cable/connection/subscriptions.rb +79 -0
- data/lib/action_cable/connection/tagged_logger_proxy.rb +42 -0
- data/lib/action_cable/connection/test_case.rb +234 -0
- data/lib/action_cable/connection/web_socket.rb +41 -0
- data/lib/action_cable/engine.rb +79 -0
- data/lib/action_cable/gem_version.rb +17 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +42 -0
- data/lib/action_cable/remote_connections.rb +71 -0
- data/lib/action_cable/server.rb +17 -0
- data/lib/action_cable/server/base.rb +94 -0
- data/lib/action_cable/server/broadcasting.rb +54 -0
- data/lib/action_cable/server/configuration.rb +56 -0
- data/lib/action_cable/server/connections.rb +36 -0
- data/lib/action_cable/server/worker.rb +75 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +21 -0
- data/lib/action_cable/subscription_adapter.rb +12 -0
- data/lib/action_cable/subscription_adapter/async.rb +29 -0
- data/lib/action_cable/subscription_adapter/base.rb +30 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
- data/lib/action_cable/subscription_adapter/inline.rb +37 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +132 -0
- data/lib/action_cable/subscription_adapter/redis.rb +181 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +59 -0
- data/lib/action_cable/subscription_adapter/test.rb +40 -0
- data/lib/action_cable/test_case.rb +11 -0
- data/lib/action_cable/test_helper.rb +133 -0
- data/lib/action_cable/version.rb +10 -0
- data/lib/rails/generators/channel/USAGE +13 -0
- data/lib/rails/generators/channel/channel_generator.rb +52 -0
- data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
- data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
- data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
- data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
- data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
- data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
- data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
- metadata +149 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Connection
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
eager_autoload do
|
8
|
+
autoload :Authorization
|
9
|
+
autoload :Base
|
10
|
+
autoload :ClientSocket
|
11
|
+
autoload :Identification
|
12
|
+
autoload :InternalChannel
|
13
|
+
autoload :MessageBuffer
|
14
|
+
autoload :Stream
|
15
|
+
autoload :StreamEventLoop
|
16
|
+
autoload :Subscriptions
|
17
|
+
autoload :TaggedLoggerProxy
|
18
|
+
autoload :TestCase
|
19
|
+
autoload :WebSocket
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Connection
|
5
|
+
module Authorization
|
6
|
+
class UnauthorizedError < StandardError; end
|
7
|
+
|
8
|
+
# Closes the WebSocket connection if it is open and returns a 404 "File not Found" response.
|
9
|
+
def reject_unauthorized_connection
|
10
|
+
logger.error "An unauthorized connection attempt was rejected"
|
11
|
+
raise UnauthorizedError
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,264 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_dispatch"
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module Connection
|
7
|
+
# For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
|
8
|
+
# of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
|
9
|
+
# based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
|
10
|
+
# authentication and authorization.
|
11
|
+
#
|
12
|
+
# Here's a basic example:
|
13
|
+
#
|
14
|
+
# module ApplicationCable
|
15
|
+
# class Connection < ActionCable::Connection::Base
|
16
|
+
# identified_by :current_user
|
17
|
+
#
|
18
|
+
# def connect
|
19
|
+
# self.current_user = find_verified_user
|
20
|
+
# logger.add_tags current_user.name
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def disconnect
|
24
|
+
# # Any cleanup work needed when the cable connection is cut.
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# private
|
28
|
+
# def find_verified_user
|
29
|
+
# User.find_by_identity(cookies.encrypted[:identity_id]) ||
|
30
|
+
# reject_unauthorized_connection
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# First, we declare that this connection can be identified by its current_user. This allows us to later be able to find all connections
|
36
|
+
# established for that current_user (and potentially disconnect them). You can declare as many
|
37
|
+
# identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key.
|
38
|
+
#
|
39
|
+
# Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes
|
40
|
+
# it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection.
|
41
|
+
#
|
42
|
+
# Finally, we add a tag to the connection-specific logger with the name of the current user to easily distinguish their messages in the log.
|
43
|
+
#
|
44
|
+
# Pretty simple, eh?
|
45
|
+
class Base
|
46
|
+
include Identification
|
47
|
+
include InternalChannel
|
48
|
+
include Authorization
|
49
|
+
|
50
|
+
attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
|
51
|
+
delegate :event_loop, :pubsub, to: :server
|
52
|
+
|
53
|
+
def initialize(server, env, coder: ActiveSupport::JSON)
|
54
|
+
@server, @env, @coder = server, env, coder
|
55
|
+
|
56
|
+
@worker_pool = server.worker_pool
|
57
|
+
@logger = new_tagged_logger
|
58
|
+
|
59
|
+
@websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop)
|
60
|
+
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
|
61
|
+
@message_buffer = ActionCable::Connection::MessageBuffer.new(self)
|
62
|
+
|
63
|
+
@_internal_subscriptions = nil
|
64
|
+
@started_at = Time.now
|
65
|
+
end
|
66
|
+
|
67
|
+
# Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
|
68
|
+
# This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
|
69
|
+
def process #:nodoc:
|
70
|
+
logger.info started_request_message
|
71
|
+
|
72
|
+
if websocket.possible? && allow_request_origin?
|
73
|
+
respond_to_successful_request
|
74
|
+
else
|
75
|
+
respond_to_invalid_request
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Decodes WebSocket messages and dispatches them to subscribed channels.
|
80
|
+
# WebSocket message transfer encoding is always JSON.
|
81
|
+
def receive(websocket_message) #:nodoc:
|
82
|
+
send_async :dispatch_websocket_message, websocket_message
|
83
|
+
end
|
84
|
+
|
85
|
+
def dispatch_websocket_message(websocket_message) #:nodoc:
|
86
|
+
if websocket.alive?
|
87
|
+
subscriptions.execute_command decode(websocket_message)
|
88
|
+
else
|
89
|
+
logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def transmit(cable_message) # :nodoc:
|
94
|
+
websocket.transmit encode(cable_message)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Close the WebSocket connection.
|
98
|
+
def close(reason: nil, reconnect: true)
|
99
|
+
transmit(
|
100
|
+
type: ActionCable::INTERNAL[:message_types][:disconnect],
|
101
|
+
reason: reason,
|
102
|
+
reconnect: reconnect
|
103
|
+
)
|
104
|
+
websocket.close
|
105
|
+
end
|
106
|
+
|
107
|
+
# Invoke a method on the connection asynchronously through the pool of thread workers.
|
108
|
+
def send_async(method, *arguments)
|
109
|
+
worker_pool.async_invoke(self, method, *arguments)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Return a basic hash of statistics for the connection keyed with <tt>identifier</tt>, <tt>started_at</tt>, <tt>subscriptions</tt>, and <tt>request_id</tt>.
|
113
|
+
# This can be returned by a health check against the connection.
|
114
|
+
def statistics
|
115
|
+
{
|
116
|
+
identifier: connection_identifier,
|
117
|
+
started_at: @started_at,
|
118
|
+
subscriptions: subscriptions.identifiers,
|
119
|
+
request_id: @env["action_dispatch.request_id"]
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
def beat
|
124
|
+
transmit type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i
|
125
|
+
end
|
126
|
+
|
127
|
+
def on_open # :nodoc:
|
128
|
+
send_async :handle_open
|
129
|
+
end
|
130
|
+
|
131
|
+
def on_message(message) # :nodoc:
|
132
|
+
message_buffer.append message
|
133
|
+
end
|
134
|
+
|
135
|
+
def on_error(message) # :nodoc:
|
136
|
+
# log errors to make diagnosing socket errors easier
|
137
|
+
logger.error "WebSocket error occurred: #{message}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def on_close(reason, code) # :nodoc:
|
141
|
+
send_async :handle_close
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
attr_reader :websocket
|
146
|
+
attr_reader :message_buffer
|
147
|
+
|
148
|
+
# The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
|
149
|
+
def request # :doc:
|
150
|
+
@request ||= begin
|
151
|
+
environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
|
152
|
+
ActionDispatch::Request.new(environment || env)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks.
|
157
|
+
def cookies # :doc:
|
158
|
+
request.cookie_jar
|
159
|
+
end
|
160
|
+
|
161
|
+
def encode(cable_message)
|
162
|
+
@coder.encode cable_message
|
163
|
+
end
|
164
|
+
|
165
|
+
def decode(websocket_message)
|
166
|
+
@coder.decode websocket_message
|
167
|
+
end
|
168
|
+
|
169
|
+
def handle_open
|
170
|
+
@protocol = websocket.protocol
|
171
|
+
connect if respond_to?(:connect)
|
172
|
+
subscribe_to_internal_channel
|
173
|
+
send_welcome_message
|
174
|
+
|
175
|
+
message_buffer.process!
|
176
|
+
server.add_connection(self)
|
177
|
+
rescue ActionCable::Connection::Authorization::UnauthorizedError
|
178
|
+
close(reason: ActionCable::INTERNAL[:disconnect_reasons][:unauthorized], reconnect: false) if websocket.alive?
|
179
|
+
end
|
180
|
+
|
181
|
+
def handle_close
|
182
|
+
logger.info finished_request_message
|
183
|
+
|
184
|
+
server.remove_connection(self)
|
185
|
+
|
186
|
+
subscriptions.unsubscribe_from_all
|
187
|
+
unsubscribe_from_internal_channel
|
188
|
+
|
189
|
+
disconnect if respond_to?(:disconnect)
|
190
|
+
end
|
191
|
+
|
192
|
+
def send_welcome_message
|
193
|
+
# Send welcome message to the internal connection monitor channel.
|
194
|
+
# This ensures the connection monitor state is reset after a successful
|
195
|
+
# websocket connection.
|
196
|
+
transmit type: ActionCable::INTERNAL[:message_types][:welcome]
|
197
|
+
end
|
198
|
+
|
199
|
+
def allow_request_origin?
|
200
|
+
return true if server.config.disable_request_forgery_protection
|
201
|
+
|
202
|
+
proto = Rack::Request.new(env).ssl? ? "https" : "http"
|
203
|
+
if server.config.allow_same_origin_as_host && env["HTTP_ORIGIN"] == "#{proto}://#{env['HTTP_HOST']}"
|
204
|
+
true
|
205
|
+
elsif Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env["HTTP_ORIGIN"] }
|
206
|
+
true
|
207
|
+
else
|
208
|
+
logger.error("Request origin not allowed: #{env['HTTP_ORIGIN']}")
|
209
|
+
false
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def respond_to_successful_request
|
214
|
+
logger.info successful_request_message
|
215
|
+
websocket.rack_response
|
216
|
+
end
|
217
|
+
|
218
|
+
def respond_to_invalid_request
|
219
|
+
close(reason: ActionCable::INTERNAL[:disconnect_reasons][:invalid_request]) if websocket.alive?
|
220
|
+
|
221
|
+
logger.error invalid_request_message
|
222
|
+
logger.info finished_request_message
|
223
|
+
[ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
|
224
|
+
end
|
225
|
+
|
226
|
+
# Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags.
|
227
|
+
def new_tagged_logger
|
228
|
+
TaggedLoggerProxy.new server.logger,
|
229
|
+
tags: server.config.log_tags.map { |tag| tag.respond_to?(:call) ? tag.call(request) : tag.to_s.camelize }
|
230
|
+
end
|
231
|
+
|
232
|
+
def started_request_message
|
233
|
+
'Started %s "%s"%s for %s at %s' % [
|
234
|
+
request.request_method,
|
235
|
+
request.filtered_path,
|
236
|
+
websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
|
237
|
+
request.ip,
|
238
|
+
Time.now.to_s ]
|
239
|
+
end
|
240
|
+
|
241
|
+
def finished_request_message
|
242
|
+
'Finished "%s"%s for %s at %s' % [
|
243
|
+
request.filtered_path,
|
244
|
+
websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
|
245
|
+
request.ip,
|
246
|
+
Time.now.to_s ]
|
247
|
+
end
|
248
|
+
|
249
|
+
def invalid_request_message
|
250
|
+
"Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
|
251
|
+
env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
|
252
|
+
]
|
253
|
+
end
|
254
|
+
|
255
|
+
def successful_request_message
|
256
|
+
"Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
|
257
|
+
env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
|
258
|
+
]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
ActiveSupport.run_load_hooks(:action_cable_connection, ActionCable::Connection::Base)
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "websocket/driver"
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module Connection
|
7
|
+
#--
|
8
|
+
# This class is heavily based on faye-websocket-ruby
|
9
|
+
#
|
10
|
+
# Copyright (c) 2010-2015 James Coglan
|
11
|
+
class ClientSocket # :nodoc:
|
12
|
+
def self.determine_url(env)
|
13
|
+
scheme = secure_request?(env) ? "wss:" : "ws:"
|
14
|
+
"#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.secure_request?(env)
|
18
|
+
return true if env["HTTPS"] == "on"
|
19
|
+
return true if env["HTTP_X_FORWARDED_SSL"] == "on"
|
20
|
+
return true if env["HTTP_X_FORWARDED_SCHEME"] == "https"
|
21
|
+
return true if env["HTTP_X_FORWARDED_PROTO"] == "https"
|
22
|
+
return true if env["rack.url_scheme"] == "https"
|
23
|
+
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
CONNECTING = 0
|
28
|
+
OPEN = 1
|
29
|
+
CLOSING = 2
|
30
|
+
CLOSED = 3
|
31
|
+
|
32
|
+
attr_reader :env, :url
|
33
|
+
|
34
|
+
def initialize(env, event_target, event_loop, protocols)
|
35
|
+
@env = env
|
36
|
+
@event_target = event_target
|
37
|
+
@event_loop = event_loop
|
38
|
+
|
39
|
+
@url = ClientSocket.determine_url(@env)
|
40
|
+
|
41
|
+
@driver = @driver_started = nil
|
42
|
+
@close_params = ["", 1006]
|
43
|
+
|
44
|
+
@ready_state = CONNECTING
|
45
|
+
|
46
|
+
# The driver calls +env+, +url+, and +write+
|
47
|
+
@driver = ::WebSocket::Driver.rack(self, protocols: protocols)
|
48
|
+
|
49
|
+
@driver.on(:open) { |e| open }
|
50
|
+
@driver.on(:message) { |e| receive_message(e.data) }
|
51
|
+
@driver.on(:close) { |e| begin_close(e.reason, e.code) }
|
52
|
+
@driver.on(:error) { |e| emit_error(e.message) }
|
53
|
+
|
54
|
+
@stream = ActionCable::Connection::Stream.new(@event_loop, self)
|
55
|
+
end
|
56
|
+
|
57
|
+
def start_driver
|
58
|
+
return if @driver.nil? || @driver_started
|
59
|
+
@stream.hijack_rack_socket
|
60
|
+
|
61
|
+
if callback = @env["async.callback"]
|
62
|
+
callback.call([101, {}, @stream])
|
63
|
+
end
|
64
|
+
|
65
|
+
@driver_started = true
|
66
|
+
@driver.start
|
67
|
+
end
|
68
|
+
|
69
|
+
def rack_response
|
70
|
+
start_driver
|
71
|
+
[ -1, {}, [] ]
|
72
|
+
end
|
73
|
+
|
74
|
+
def write(data)
|
75
|
+
@stream.write(data)
|
76
|
+
rescue => e
|
77
|
+
emit_error e.message
|
78
|
+
end
|
79
|
+
|
80
|
+
def transmit(message)
|
81
|
+
return false if @ready_state > OPEN
|
82
|
+
case message
|
83
|
+
when Numeric then @driver.text(message.to_s)
|
84
|
+
when String then @driver.text(message)
|
85
|
+
when Array then @driver.binary(message)
|
86
|
+
else false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def close(code = nil, reason = nil)
|
91
|
+
code ||= 1000
|
92
|
+
reason ||= ""
|
93
|
+
|
94
|
+
unless code == 1000 || (code >= 3000 && code <= 4999)
|
95
|
+
raise ArgumentError, "Failed to execute 'close' on WebSocket: " \
|
96
|
+
"The code must be either 1000, or between 3000 and 4999. " \
|
97
|
+
"#{code} is neither."
|
98
|
+
end
|
99
|
+
|
100
|
+
@ready_state = CLOSING unless @ready_state == CLOSED
|
101
|
+
@driver.close(reason, code)
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse(data)
|
105
|
+
@driver.parse(data)
|
106
|
+
end
|
107
|
+
|
108
|
+
def client_gone
|
109
|
+
finalize_close
|
110
|
+
end
|
111
|
+
|
112
|
+
def alive?
|
113
|
+
@ready_state == OPEN
|
114
|
+
end
|
115
|
+
|
116
|
+
def protocol
|
117
|
+
@driver.protocol
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
def open
|
122
|
+
return unless @ready_state == CONNECTING
|
123
|
+
@ready_state = OPEN
|
124
|
+
|
125
|
+
@event_target.on_open
|
126
|
+
end
|
127
|
+
|
128
|
+
def receive_message(data)
|
129
|
+
return unless @ready_state == OPEN
|
130
|
+
|
131
|
+
@event_target.on_message(data)
|
132
|
+
end
|
133
|
+
|
134
|
+
def emit_error(message)
|
135
|
+
return if @ready_state >= CLOSING
|
136
|
+
|
137
|
+
@event_target.on_error(message)
|
138
|
+
end
|
139
|
+
|
140
|
+
def begin_close(reason, code)
|
141
|
+
return if @ready_state == CLOSED
|
142
|
+
@ready_state = CLOSING
|
143
|
+
@close_params = [reason, code]
|
144
|
+
|
145
|
+
@stream.shutdown if @stream
|
146
|
+
finalize_close
|
147
|
+
end
|
148
|
+
|
149
|
+
def finalize_close
|
150
|
+
return if @ready_state == CLOSED
|
151
|
+
@ready_state = CLOSED
|
152
|
+
|
153
|
+
@event_target.on_close(*@close_params)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|