actioncable 0.0.0 → 5.0.0.beta1
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/CHANGELOG.md +8 -0
- data/MIT-LICENSE +20 -0
- data/README.md +439 -21
- data/lib/action_cable.rb +47 -2
- data/lib/action_cable/channel.rb +14 -0
- data/lib/action_cable/channel/base.rb +277 -0
- data/lib/action_cable/channel/broadcasting.rb +29 -0
- data/lib/action_cable/channel/callbacks.rb +35 -0
- data/lib/action_cable/channel/naming.rb +22 -0
- data/lib/action_cable/channel/periodic_timers.rb +41 -0
- data/lib/action_cable/channel/streams.rb +114 -0
- data/lib/action_cable/connection.rb +16 -0
- data/lib/action_cable/connection/authorization.rb +13 -0
- data/lib/action_cable/connection/base.rb +221 -0
- data/lib/action_cable/connection/identification.rb +46 -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/subscriptions.rb +76 -0
- data/lib/action_cable/connection/tagged_logger_proxy.rb +40 -0
- data/lib/action_cable/connection/web_socket.rb +29 -0
- data/lib/action_cable/engine.rb +38 -0
- data/lib/action_cable/gem_version.rb +15 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +29 -0
- data/lib/action_cable/process/logging.rb +10 -0
- data/lib/action_cable/remote_connections.rb +64 -0
- data/lib/action_cable/server.rb +19 -0
- data/lib/action_cable/server/base.rb +77 -0
- data/lib/action_cable/server/broadcasting.rb +54 -0
- data/lib/action_cable/server/configuration.rb +35 -0
- data/lib/action_cable/server/connections.rb +37 -0
- data/lib/action_cable/server/worker.rb +42 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +22 -0
- data/lib/action_cable/version.rb +6 -1
- data/lib/assets/javascripts/action_cable.coffee.erb +23 -0
- data/lib/assets/javascripts/action_cable/connection.coffee +84 -0
- data/lib/assets/javascripts/action_cable/connection_monitor.coffee +84 -0
- data/lib/assets/javascripts/action_cable/consumer.coffee +31 -0
- data/lib/assets/javascripts/action_cable/subscription.coffee +68 -0
- data/lib/assets/javascripts/action_cable/subscriptions.coffee +78 -0
- data/lib/rails/generators/channel/USAGE +14 -0
- data/lib/rails/generators/channel/channel_generator.rb +21 -0
- data/lib/rails/generators/channel/templates/assets/channel.coffee +14 -0
- data/lib/rails/generators/channel/templates/channel.rb +17 -0
- metadata +161 -26
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -2
- data/actioncable.gemspec +0 -22
- data/bin/console +0 -14
- data/bin/setup +0 -7
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Server
|
5
|
+
# Broadcasting is how other parts of your application can send messages to the channel subscribers. As explained in Channel, most of the time, these
|
6
|
+
# broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
|
7
|
+
#
|
8
|
+
# class WebNotificationsChannel < ApplicationCable::Channel
|
9
|
+
# def subscribed
|
10
|
+
# stream_from "web_notifications_#{current_user.id}"
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# # Somewhere in your app this is called, perhaps from a NewCommentJob
|
15
|
+
# ActionCable.server.broadcast \
|
16
|
+
# "web_notifications_1", { title: 'New things!', body: 'All shit fit for print' }
|
17
|
+
#
|
18
|
+
# # Client-side coffescript, which assumes you've already requested the right to send web notifications
|
19
|
+
# App.cable.subscriptions.create "WebNotificationsChannel",
|
20
|
+
# received: (data) ->
|
21
|
+
# new Notification data['title'], body: data['body']
|
22
|
+
module Broadcasting
|
23
|
+
# Broadcast a hash directly to a named <tt>broadcasting</tt>. It'll automatically be JSON encoded.
|
24
|
+
def broadcast(broadcasting, message)
|
25
|
+
broadcaster_for(broadcasting).broadcast(message)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have a object that
|
29
|
+
# may need multiple spots to transmit to a specific broadcasting over and over.
|
30
|
+
def broadcaster_for(broadcasting)
|
31
|
+
Broadcaster.new(self, broadcasting)
|
32
|
+
end
|
33
|
+
|
34
|
+
# The redis instance used for broadcasting. Not intended for direct user use.
|
35
|
+
def broadcasting_redis
|
36
|
+
@broadcasting_redis ||= Redis.new(config.redis)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
class Broadcaster
|
41
|
+
attr_reader :server, :broadcasting
|
42
|
+
|
43
|
+
def initialize(server, broadcasting)
|
44
|
+
@server, @broadcasting = server, broadcasting
|
45
|
+
end
|
46
|
+
|
47
|
+
def broadcast(message)
|
48
|
+
server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}"
|
49
|
+
server.broadcasting_redis.publish broadcasting, ActiveSupport::JSON.encode(message)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Server
|
3
|
+
# An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak the configuration points
|
4
|
+
# in a Rails config initializer.
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :logger, :log_tags
|
7
|
+
attr_accessor :connection_class, :worker_pool_size
|
8
|
+
attr_accessor :redis, :channels_path
|
9
|
+
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
|
10
|
+
attr_accessor :url
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@log_tags = []
|
14
|
+
|
15
|
+
@connection_class = ApplicationCable::Connection
|
16
|
+
@worker_pool_size = 100
|
17
|
+
|
18
|
+
@channels_path = Rails.root.join('app/channels')
|
19
|
+
|
20
|
+
@disable_request_forgery_protection = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def channel_paths
|
24
|
+
@channels ||= Dir["#{channels_path}/**/*_channel.rb"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def channel_class_names
|
28
|
+
@channel_class_names ||= channel_paths.collect do |channel_path|
|
29
|
+
Pathname.new(channel_path).basename.to_s.split('.').first.camelize
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Server
|
3
|
+
# Collection class for all the connections that's been established on this specific server. Remember, usually you'll run many cable servers, so
|
4
|
+
# you can't use this collection as an full list of all the connections established against your application. Use RemoteConnections for that.
|
5
|
+
# As such, this is primarily for internal use.
|
6
|
+
module Connections
|
7
|
+
BEAT_INTERVAL = 3
|
8
|
+
|
9
|
+
def connections
|
10
|
+
@connections ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_connection(connection)
|
14
|
+
connections << connection
|
15
|
+
end
|
16
|
+
|
17
|
+
def remove_connection(connection)
|
18
|
+
connections.delete connection
|
19
|
+
end
|
20
|
+
|
21
|
+
# WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you
|
22
|
+
# then can't rely on being able to receive and send to it. So there's a 3 second heartbeat running on all connections. If the beat fails, we automatically
|
23
|
+
# disconnect.
|
24
|
+
def setup_heartbeat_timer
|
25
|
+
EM.next_tick do
|
26
|
+
@heartbeat_timer ||= EventMachine.add_periodic_timer(BEAT_INTERVAL) do
|
27
|
+
EM.next_tick { connections.map(&:beat) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def open_connections_statistics
|
33
|
+
connections.map(&:statistics)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
require 'active_support/callbacks'
|
3
|
+
|
4
|
+
module ActionCable
|
5
|
+
module Server
|
6
|
+
# Worker used by Server.send_async to do connection work in threads. Only for internal use.
|
7
|
+
class Worker
|
8
|
+
include ActiveSupport::Callbacks
|
9
|
+
include Celluloid
|
10
|
+
|
11
|
+
attr_reader :connection
|
12
|
+
define_callbacks :work
|
13
|
+
include ActiveRecordConnectionManagement
|
14
|
+
|
15
|
+
def invoke(receiver, method, *args)
|
16
|
+
@connection = receiver
|
17
|
+
|
18
|
+
run_callbacks :work do
|
19
|
+
receiver.send method, *args
|
20
|
+
end
|
21
|
+
rescue Exception => e
|
22
|
+
logger.error "There was an exception - #{e.class}(#{e.message})"
|
23
|
+
logger.error e.backtrace.join("\n")
|
24
|
+
|
25
|
+
receiver.handle_exception if receiver.respond_to?(:handle_exception)
|
26
|
+
end
|
27
|
+
|
28
|
+
def run_periodic_timer(channel, callback)
|
29
|
+
@connection = channel.connection
|
30
|
+
|
31
|
+
run_callbacks :work do
|
32
|
+
callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def logger
|
38
|
+
ActionCable.server.logger
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Server
|
3
|
+
class Worker
|
4
|
+
# Clear active connections between units of work so the long-running channel or connection processes do not hoard connections.
|
5
|
+
module ActiveRecordConnectionManagement
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
if defined?(ActiveRecord::Base)
|
10
|
+
set_callback :work, :around, :with_database_connections
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_database_connections
|
15
|
+
connection.logger.tag(ActiveRecord::Base.logger) { yield }
|
16
|
+
ensure
|
17
|
+
ActiveRecord::Base.clear_active_connections!
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/action_cable/version.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
#= require_self
|
2
|
+
#= require action_cable/consumer
|
3
|
+
|
4
|
+
@ActionCable =
|
5
|
+
INTERNAL: <%= ActionCable::INTERNAL.to_json %>
|
6
|
+
|
7
|
+
createConsumer: (url = @getConfig("url")) ->
|
8
|
+
new ActionCable.Consumer @createWebSocketURL(url)
|
9
|
+
|
10
|
+
getConfig: (name) ->
|
11
|
+
element = document.head.querySelector("meta[name='action-cable-#{name}']")
|
12
|
+
element?.getAttribute("content")
|
13
|
+
|
14
|
+
createWebSocketURL: (url) ->
|
15
|
+
if url and not /^wss?:/i.test(url)
|
16
|
+
a = document.createElement("a")
|
17
|
+
a.href = url
|
18
|
+
# Fix populating Location properties in IE. Otherwise, protocol will be blank.
|
19
|
+
a.href = a.href
|
20
|
+
a.protocol = a.protocol.replace("http", "ws")
|
21
|
+
a.href
|
22
|
+
else
|
23
|
+
url
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.
|
2
|
+
|
3
|
+
{message_types} = ActionCable.INTERNAL
|
4
|
+
|
5
|
+
class ActionCable.Connection
|
6
|
+
@reopenDelay: 500
|
7
|
+
|
8
|
+
constructor: (@consumer) ->
|
9
|
+
@open()
|
10
|
+
|
11
|
+
send: (data) ->
|
12
|
+
if @isOpen()
|
13
|
+
@webSocket.send(JSON.stringify(data))
|
14
|
+
true
|
15
|
+
else
|
16
|
+
false
|
17
|
+
|
18
|
+
open: =>
|
19
|
+
if @webSocket and not @isState("closed")
|
20
|
+
throw new Error("Existing connection must be closed before opening")
|
21
|
+
else
|
22
|
+
@webSocket = new WebSocket(@consumer.url)
|
23
|
+
@installEventHandlers()
|
24
|
+
true
|
25
|
+
|
26
|
+
close: ->
|
27
|
+
@webSocket?.close()
|
28
|
+
|
29
|
+
reopen: ->
|
30
|
+
if @isState("closed")
|
31
|
+
@open()
|
32
|
+
else
|
33
|
+
try
|
34
|
+
@close()
|
35
|
+
finally
|
36
|
+
setTimeout(@open, @constructor.reopenDelay)
|
37
|
+
|
38
|
+
isOpen: ->
|
39
|
+
@isState("open")
|
40
|
+
|
41
|
+
# Private
|
42
|
+
|
43
|
+
isState: (states...) ->
|
44
|
+
@getState() in states
|
45
|
+
|
46
|
+
getState: ->
|
47
|
+
return state.toLowerCase() for state, value of WebSocket when value is @webSocket?.readyState
|
48
|
+
null
|
49
|
+
|
50
|
+
installEventHandlers: ->
|
51
|
+
for eventName of @events
|
52
|
+
handler = @events[eventName].bind(this)
|
53
|
+
@webSocket["on#{eventName}"] = handler
|
54
|
+
return
|
55
|
+
|
56
|
+
events:
|
57
|
+
message: (event) ->
|
58
|
+
{identifier, message, type} = JSON.parse(event.data)
|
59
|
+
|
60
|
+
switch type
|
61
|
+
when message_types.confirmation
|
62
|
+
@consumer.subscriptions.notify(identifier, "connected")
|
63
|
+
when message_types.rejection
|
64
|
+
@consumer.subscriptions.reject(identifier)
|
65
|
+
else
|
66
|
+
@consumer.subscriptions.notify(identifier, "received", message)
|
67
|
+
|
68
|
+
open: ->
|
69
|
+
@disconnected = false
|
70
|
+
@consumer.subscriptions.reload()
|
71
|
+
|
72
|
+
close: ->
|
73
|
+
@disconnect()
|
74
|
+
|
75
|
+
error: ->
|
76
|
+
@disconnect()
|
77
|
+
|
78
|
+
disconnect: ->
|
79
|
+
return if @disconnected
|
80
|
+
@disconnected = true
|
81
|
+
@consumer.subscriptions.notifyAll("disconnected")
|
82
|
+
|
83
|
+
toJSON: ->
|
84
|
+
state: @getState()
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
|
2
|
+
# revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
|
3
|
+
class ActionCable.ConnectionMonitor
|
4
|
+
@pollInterval:
|
5
|
+
min: 3
|
6
|
+
max: 30
|
7
|
+
|
8
|
+
@staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
|
9
|
+
|
10
|
+
identifier: ActionCable.INTERNAL.identifiers.ping
|
11
|
+
|
12
|
+
constructor: (@consumer) ->
|
13
|
+
@consumer.subscriptions.add(this)
|
14
|
+
@start()
|
15
|
+
|
16
|
+
connected: ->
|
17
|
+
@reset()
|
18
|
+
@pingedAt = now()
|
19
|
+
delete @disconnectedAt
|
20
|
+
|
21
|
+
disconnected: ->
|
22
|
+
@disconnectedAt = now()
|
23
|
+
|
24
|
+
received: ->
|
25
|
+
@pingedAt = now()
|
26
|
+
|
27
|
+
reset: ->
|
28
|
+
@reconnectAttempts = 0
|
29
|
+
|
30
|
+
start: ->
|
31
|
+
@reset()
|
32
|
+
delete @stoppedAt
|
33
|
+
@startedAt = now()
|
34
|
+
@poll()
|
35
|
+
document.addEventListener("visibilitychange", @visibilityDidChange)
|
36
|
+
|
37
|
+
stop: ->
|
38
|
+
@stoppedAt = now()
|
39
|
+
document.removeEventListener("visibilitychange", @visibilityDidChange)
|
40
|
+
|
41
|
+
poll: ->
|
42
|
+
setTimeout =>
|
43
|
+
unless @stoppedAt
|
44
|
+
@reconnectIfStale()
|
45
|
+
@poll()
|
46
|
+
, @getInterval()
|
47
|
+
|
48
|
+
getInterval: ->
|
49
|
+
{min, max} = @constructor.pollInterval
|
50
|
+
interval = 5 * Math.log(@reconnectAttempts + 1)
|
51
|
+
clamp(interval, min, max) * 1000
|
52
|
+
|
53
|
+
reconnectIfStale: ->
|
54
|
+
if @connectionIsStale()
|
55
|
+
@reconnectAttempts++
|
56
|
+
unless @disconnectedRecently()
|
57
|
+
@consumer.connection.reopen()
|
58
|
+
|
59
|
+
connectionIsStale: ->
|
60
|
+
secondsSince(@pingedAt ? @startedAt) > @constructor.staleThreshold
|
61
|
+
|
62
|
+
disconnectedRecently: ->
|
63
|
+
@disconnectedAt and secondsSince(@disconnectedAt) < @constructor.staleThreshold
|
64
|
+
|
65
|
+
visibilityDidChange: =>
|
66
|
+
if document.visibilityState is "visible"
|
67
|
+
setTimeout =>
|
68
|
+
if @connectionIsStale() or not @consumer.connection.isOpen()
|
69
|
+
@consumer.connection.reopen()
|
70
|
+
, 200
|
71
|
+
|
72
|
+
toJSON: ->
|
73
|
+
interval = @getInterval()
|
74
|
+
connectionIsStale = @connectionIsStale()
|
75
|
+
{@startedAt, @stoppedAt, @pingedAt, @reconnectAttempts, connectionIsStale, interval}
|
76
|
+
|
77
|
+
now = ->
|
78
|
+
new Date().getTime()
|
79
|
+
|
80
|
+
secondsSince = (time) ->
|
81
|
+
(now() - time) / 1000
|
82
|
+
|
83
|
+
clamp = (number, min, max) ->
|
84
|
+
Math.max(min, Math.min(max, number))
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#= require action_cable/connection
|
2
|
+
#= require action_cable/connection_monitor
|
3
|
+
#= require action_cable/subscriptions
|
4
|
+
#= require action_cable/subscription
|
5
|
+
|
6
|
+
# The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established,
|
7
|
+
# the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates.
|
8
|
+
# The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription
|
9
|
+
# method.
|
10
|
+
#
|
11
|
+
# The following example shows how this can be setup:
|
12
|
+
#
|
13
|
+
# @App = {}
|
14
|
+
# App.cable = ActionCable.createConsumer "ws://example.com/accounts/1"
|
15
|
+
# App.appearance = App.cable.subscriptions.create "AppearanceChannel"
|
16
|
+
#
|
17
|
+
# For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
|
18
|
+
class ActionCable.Consumer
|
19
|
+
constructor: (@url) ->
|
20
|
+
@subscriptions = new ActionCable.Subscriptions this
|
21
|
+
@connection = new ActionCable.Connection this
|
22
|
+
@connectionMonitor = new ActionCable.ConnectionMonitor this
|
23
|
+
|
24
|
+
send: (data) ->
|
25
|
+
@connection.send(data)
|
26
|
+
|
27
|
+
inspect: ->
|
28
|
+
JSON.stringify(this, null, 2)
|
29
|
+
|
30
|
+
toJSON: ->
|
31
|
+
{@url, @subscriptions, @connection, @connectionMonitor}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# A new subscription is created through the ActionCable.Subscriptions instance available on the consumer.
|
2
|
+
# It provides a number of callbacks and a method for calling remote procedure calls on the corresponding
|
3
|
+
# Channel instance on the server side.
|
4
|
+
#
|
5
|
+
# An example demonstrates the basic functionality:
|
6
|
+
#
|
7
|
+
# App.appearance = App.cable.subscriptions.create "AppearanceChannel",
|
8
|
+
# connected: ->
|
9
|
+
# # Called once the subscription has been successfully completed
|
10
|
+
#
|
11
|
+
# appear: ->
|
12
|
+
# @perform 'appear', appearing_on: @appearingOn()
|
13
|
+
#
|
14
|
+
# away: ->
|
15
|
+
# @perform 'away'
|
16
|
+
#
|
17
|
+
# appearingOn: ->
|
18
|
+
# $('main').data 'appearing-on'
|
19
|
+
#
|
20
|
+
# The methods #appear and #away forward their intent to the remote AppearanceChannel instance on the server
|
21
|
+
# by calling the `@perform` method with the first parameter being the action (which maps to AppearanceChannel#appear/away).
|
22
|
+
# The second parameter is a hash that'll get JSON encoded and made available on the server in the data parameter.
|
23
|
+
#
|
24
|
+
# This is how the server component would look:
|
25
|
+
#
|
26
|
+
# class AppearanceChannel < ApplicationActionCable::Channel
|
27
|
+
# def subscribed
|
28
|
+
# current_user.appear
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def unsubscribed
|
32
|
+
# current_user.disappear
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# def appear(data)
|
36
|
+
# current_user.appear on: data['appearing_on']
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# def away
|
40
|
+
# current_user.away
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# The "AppearanceChannel" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name.
|
45
|
+
# The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the @perform method.
|
46
|
+
class ActionCable.Subscription
|
47
|
+
constructor: (@subscriptions, params = {}, mixin) ->
|
48
|
+
@identifier = JSON.stringify(params)
|
49
|
+
extend(this, mixin)
|
50
|
+
@subscriptions.add(this)
|
51
|
+
@consumer = @subscriptions.consumer
|
52
|
+
|
53
|
+
# Perform a channel action with the optional data passed as an attribute
|
54
|
+
perform: (action, data = {}) ->
|
55
|
+
data.action = action
|
56
|
+
@send(data)
|
57
|
+
|
58
|
+
send: (data) ->
|
59
|
+
@consumer.send(command: "message", identifier: @identifier, data: JSON.stringify(data))
|
60
|
+
|
61
|
+
unsubscribe: ->
|
62
|
+
@subscriptions.remove(this)
|
63
|
+
|
64
|
+
extend = (object, properties) ->
|
65
|
+
if properties?
|
66
|
+
for key, value of properties
|
67
|
+
object[key] = value
|
68
|
+
object
|