litecable 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +40 -0
- data/.rubocop.yml +63 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +128 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/circle.yml +8 -0
- data/examples/sinatra/Gemfile +16 -0
- data/examples/sinatra/Procfile +3 -0
- data/examples/sinatra/README.md +33 -0
- data/examples/sinatra/anycable +18 -0
- data/examples/sinatra/app.rb +52 -0
- data/examples/sinatra/assets/app.css +169 -0
- data/examples/sinatra/assets/cable.js +584 -0
- data/examples/sinatra/assets/reset.css +223 -0
- data/examples/sinatra/bin/anycable-go +0 -0
- data/examples/sinatra/chat.rb +39 -0
- data/examples/sinatra/config.ru +28 -0
- data/examples/sinatra/views/index.slim +8 -0
- data/examples/sinatra/views/layout.slim +15 -0
- data/examples/sinatra/views/login.slim +8 -0
- data/examples/sinatra/views/resetcss.slim +224 -0
- data/examples/sinatra/views/room.slim +68 -0
- data/lib/lite_cable.rb +29 -0
- data/lib/lite_cable/anycable.rb +62 -0
- data/lib/lite_cable/channel.rb +8 -0
- data/lib/lite_cable/channel/base.rb +165 -0
- data/lib/lite_cable/channel/registry.rb +34 -0
- data/lib/lite_cable/channel/streams.rb +56 -0
- data/lib/lite_cable/coders.rb +7 -0
- data/lib/lite_cable/coders/json.rb +19 -0
- data/lib/lite_cable/coders/raw.rb +15 -0
- data/lib/lite_cable/config.rb +18 -0
- data/lib/lite_cable/connection.rb +10 -0
- data/lib/lite_cable/connection/authorization.rb +13 -0
- data/lib/lite_cable/connection/base.rb +131 -0
- data/lib/lite_cable/connection/identification.rb +88 -0
- data/lib/lite_cable/connection/streams.rb +28 -0
- data/lib/lite_cable/connection/subscriptions.rb +108 -0
- data/lib/lite_cable/internal.rb +13 -0
- data/lib/lite_cable/logging.rb +28 -0
- data/lib/lite_cable/server.rb +27 -0
- data/lib/lite_cable/server/client_socket.rb +9 -0
- data/lib/lite_cable/server/client_socket/base.rb +163 -0
- data/lib/lite_cable/server/client_socket/subscriptions.rb +23 -0
- data/lib/lite_cable/server/heart_beat.rb +50 -0
- data/lib/lite_cable/server/middleware.rb +55 -0
- data/lib/lite_cable/server/subscribers_map.rb +67 -0
- data/lib/lite_cable/server/websocket_ext/protocols.rb +45 -0
- data/lib/lite_cable/version.rb +4 -0
- data/lib/litecable.rb +2 -0
- data/litecable.gemspec +33 -0
- metadata +256 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
h2 ="Room: #{@room_id}"
|
2
|
+
|
3
|
+
.messages#message_list
|
4
|
+
|
5
|
+
.message-form
|
6
|
+
form#message_form
|
7
|
+
.row
|
8
|
+
input#message_txt type="text" required="required"
|
9
|
+
.row
|
10
|
+
button.btn type="submit"
|
11
|
+
span Send!
|
12
|
+
|
13
|
+
javascript:
|
14
|
+
var roomId = #{{ @room_id }};
|
15
|
+
var user = "#{{ @user }}";
|
16
|
+
var socketId = Date.now();
|
17
|
+
|
18
|
+
var messageList = document.getElementById("message_list");
|
19
|
+
var messageForm = document.getElementById("message_form");
|
20
|
+
var textInput = document.getElementById("message_txt");
|
21
|
+
|
22
|
+
messageForm.onsubmit = function(e){
|
23
|
+
e.preventDefault();
|
24
|
+
var msg = textInput.value;
|
25
|
+
console.log("Send message", msg);
|
26
|
+
textInput.value = null;
|
27
|
+
chatChannel.perform('speak', { message: msg });
|
28
|
+
};
|
29
|
+
|
30
|
+
var escape = function(str) {
|
31
|
+
return ('' + str).replace(/&/g, '&')
|
32
|
+
.replace(/</g, '<')
|
33
|
+
.replace(/>/g, '>')
|
34
|
+
.replace(/"/g, '"');
|
35
|
+
}
|
36
|
+
|
37
|
+
var addMessage = function(data){
|
38
|
+
var node = document.createElement('div');
|
39
|
+
var me = data['user'] == user && data['sid'] == socketId
|
40
|
+
node.className = "message" + (me ? ' me' : '') + (data['system'] ? ' system' : '');
|
41
|
+
node.innerHTML =
|
42
|
+
'<div class="author">' + escape(data['user']) + '</div>' +
|
43
|
+
'<div class="txt">' + escape(data['message']) + '</div>';
|
44
|
+
messageList.appendChild(node);
|
45
|
+
};
|
46
|
+
|
47
|
+
ActionCable.startDebugging();
|
48
|
+
var cable = ActionCable.createConsumer('#{{ CABLE_URL }}?sid=' + socketId);
|
49
|
+
|
50
|
+
var chatChannel = cable.subscriptions.create(
|
51
|
+
{ channel: 'chat', id: roomId },
|
52
|
+
{
|
53
|
+
connected: function(){
|
54
|
+
console.log("Connected");
|
55
|
+
addMessage({ user: 'BOT', message: "I'm connected", system: true });
|
56
|
+
},
|
57
|
+
|
58
|
+
disconnected: function(){
|
59
|
+
console.log("Connected");
|
60
|
+
addMessage({ user: 'BOT', message: "Sorry, but you've been disconnected(", system: true });
|
61
|
+
},
|
62
|
+
|
63
|
+
received: function(data){
|
64
|
+
console.log("Received", data);
|
65
|
+
addMessage(data);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
)
|
data/lib/lite_cable.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "lite_cable/version"
|
3
|
+
require "lite_cable/internal"
|
4
|
+
require "lite_cable/logging"
|
5
|
+
|
6
|
+
# Lightwieght ActionCable implementation.
|
7
|
+
#
|
8
|
+
# Contains application logic (channels, streams, broadcasting) and
|
9
|
+
# also (optional) Rack hijack based server (suitable only for development and test).
|
10
|
+
#
|
11
|
+
# Compatible with AnyCable (for production usage).
|
12
|
+
module LiteCable
|
13
|
+
require "lite_cable/connection"
|
14
|
+
require "lite_cable/channel"
|
15
|
+
require "lite_cable/coders"
|
16
|
+
require "lite_cable/config"
|
17
|
+
require "lite_cable/anycable"
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def config
|
21
|
+
@config ||= Config.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Broadcast encoded message to the stream
|
25
|
+
def broadcast(*args)
|
26
|
+
LiteCable::Server.broadcast(*args)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
# AnyCable extensions
|
4
|
+
module AnyCable
|
5
|
+
module Broadcasting # :nodoc:
|
6
|
+
def broadcast(stream, message, coder: nil)
|
7
|
+
coder ||= LiteCable.config.coder
|
8
|
+
Anycable.broadcast stream, coder.encode(message)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Connection # :nodoc:
|
13
|
+
def self.extended(base)
|
14
|
+
base.prepend InstanceMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
def create(socket, **options)
|
18
|
+
new(socket, **options)
|
19
|
+
end
|
20
|
+
|
21
|
+
module InstanceMethods # :nodoc:
|
22
|
+
def initialize(socket, subscriptions: nil, **hargs)
|
23
|
+
super(socket, **hargs)
|
24
|
+
# Initialize channels if any
|
25
|
+
subscriptions&.each { |id| @subscriptions.add(id, false) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def request
|
29
|
+
@request ||= Rack::Request.new(socket.env)
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_channel_command(identifier, command, data)
|
33
|
+
channel = subscriptions.add(identifier, false)
|
34
|
+
case command
|
35
|
+
when "subscribe"
|
36
|
+
!subscriptions.send(:subscribe_channel, channel).nil?
|
37
|
+
when "unsubscribe"
|
38
|
+
subscriptions.remove(identifier)
|
39
|
+
true
|
40
|
+
when "message"
|
41
|
+
subscriptions.perform_action identifier, data
|
42
|
+
true
|
43
|
+
else
|
44
|
+
false
|
45
|
+
end
|
46
|
+
rescue LiteCable::Connection::Subscriptions::Error,
|
47
|
+
LiteCable::Channel::Error,
|
48
|
+
LiteCable::Channel::Registry::Error => e
|
49
|
+
log(:error, log_fmt("Connection command failed: #{e}"))
|
50
|
+
close
|
51
|
+
false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Patch Lite Cable with AnyCable functionality
|
58
|
+
def self.anycable!
|
59
|
+
LiteCable::Connection::Base.extend LiteCable::AnyCable::Connection
|
60
|
+
LiteCable.singleton_class.prepend LiteCable::AnyCable::Broadcasting
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
# rubocop:disable Metrics/LineLength
|
4
|
+
module Channel
|
5
|
+
class Error < StandardError; end
|
6
|
+
class RejectedError < Error; end
|
7
|
+
class UnproccessableActionError < Error; end
|
8
|
+
|
9
|
+
# The channel provides the basic structure of grouping behavior into logical units when communicating over the connection.
|
10
|
+
# You can think of a channel like a form of controller, but one that's capable of pushing content to the subscriber in addition to simply
|
11
|
+
# responding to the subscriber's direct requests.
|
12
|
+
#
|
13
|
+
# == Identification
|
14
|
+
#
|
15
|
+
# Each channel must have a unique identifier, which is used by the connection to resolve the channel's class.
|
16
|
+
#
|
17
|
+
# Example:
|
18
|
+
#
|
19
|
+
# class SecretChannel < LiteCable::Channel::Base
|
20
|
+
# identifier 'my_super_secret_channel'
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # client-side
|
24
|
+
# App.cable.subscriptions.create('my_super_secret_channel')
|
25
|
+
#
|
26
|
+
# == Action processing
|
27
|
+
#
|
28
|
+
# You can declare any public method on the channel (optionally taking a `data` argument),
|
29
|
+
# and this method is automatically exposed as callable to the client.
|
30
|
+
#
|
31
|
+
# Example:
|
32
|
+
#
|
33
|
+
# class AppearanceChannel < LiteCable::Channel::Base
|
34
|
+
# def unsubscribed
|
35
|
+
# # here `current_user` is a connection identifier
|
36
|
+
# current_user.disappear
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# def appear(data)
|
40
|
+
# current_user.appear on: data['appearing_on']
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# def away
|
44
|
+
# current_user.away
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# == Rejecting subscription requests
|
49
|
+
#
|
50
|
+
# A channel can reject a subscription request in the #subscribed callback by
|
51
|
+
# invoking the #reject method:
|
52
|
+
#
|
53
|
+
# class ChatChannel < ApplicationCable::Channel
|
54
|
+
# def subscribed
|
55
|
+
# room = Chat::Room[params['room_number']]
|
56
|
+
# reject unless current_user.can_access?(room)
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# In this example, the subscription will be rejected if the
|
61
|
+
# <tt>current_user</tt> does not have access to the chat room. On the
|
62
|
+
# client-side, the <tt>Channel#rejected</tt> callback will get invoked when
|
63
|
+
# the server rejects the subscription request.
|
64
|
+
class Base
|
65
|
+
# rubocop:enable Metrics/LineLength
|
66
|
+
class << self
|
67
|
+
# A set of method names that should be considered actions.
|
68
|
+
# This includes all public instance methods on a channel except from Channel::Base methods.
|
69
|
+
def action_methods
|
70
|
+
@action_methods ||= begin
|
71
|
+
# All public instance methods of this class, including ancestors
|
72
|
+
methods = (public_instance_methods(true) -
|
73
|
+
# Except for public instance methods of Base and its ancestors
|
74
|
+
LiteCable::Channel::Base.public_instance_methods(true) +
|
75
|
+
# Be sure to include shadowed public instance methods of this class
|
76
|
+
public_instance_methods(false)).uniq.map(&:to_s)
|
77
|
+
methods.to_set
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
attr_reader :id
|
82
|
+
|
83
|
+
# Register the channel by its unique identifier
|
84
|
+
# (in order to resolve the channel's class for connections)
|
85
|
+
def identifier(id)
|
86
|
+
Registry.add(id.to_s, self)
|
87
|
+
@id = id
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
include Logging
|
92
|
+
prepend Streams
|
93
|
+
|
94
|
+
attr_reader :connection, :identifier, :params
|
95
|
+
|
96
|
+
def initialize(connection, identifier, params)
|
97
|
+
@connection = connection
|
98
|
+
@identifier = identifier
|
99
|
+
@params = params.freeze
|
100
|
+
|
101
|
+
delegate_connection_identifiers
|
102
|
+
end
|
103
|
+
|
104
|
+
def handle_subscribe
|
105
|
+
subscribed if respond_to?(:subscribed)
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_unsubscribe
|
109
|
+
unsubscribed if respond_to?(:unsubscribed)
|
110
|
+
end
|
111
|
+
|
112
|
+
def handle_action(encoded_message)
|
113
|
+
perform_action connection.coder.decode(encoded_message)
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
def reject
|
119
|
+
raise RejectedError
|
120
|
+
end
|
121
|
+
|
122
|
+
def transmit(data)
|
123
|
+
connection.transmit identifier: identifier, message: data
|
124
|
+
end
|
125
|
+
|
126
|
+
# Extract the action name from the passed data and process it via the channel.
|
127
|
+
def perform_action(data)
|
128
|
+
action = extract_action(data)
|
129
|
+
|
130
|
+
raise UnproccessableActionError unless processable_action?(action)
|
131
|
+
log(:debug) { log_fmt("Perform action #{action}(#{data})") }
|
132
|
+
dispatch_action(action, data)
|
133
|
+
end
|
134
|
+
|
135
|
+
def dispatch_action(action, data)
|
136
|
+
if method(action).arity == 1
|
137
|
+
public_send action, data
|
138
|
+
else
|
139
|
+
public_send action
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def extract_action(data)
|
144
|
+
data.delete("action") || "receive"
|
145
|
+
end
|
146
|
+
|
147
|
+
def processable_action?(action)
|
148
|
+
self.class.action_methods.include?(action)
|
149
|
+
end
|
150
|
+
|
151
|
+
def delegate_connection_identifiers
|
152
|
+
connection.identifiers.each do |identifier|
|
153
|
+
define_singleton_method(identifier) do
|
154
|
+
connection.send(identifier)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Add prefix to channel log messages
|
160
|
+
def log_fmt(msg)
|
161
|
+
"[connection:#{connection.identifier}] [channel:#{self.class.id}] #{msg}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
module Channel
|
4
|
+
# Stores channels identifiers and corresponding classes.
|
5
|
+
module Registry
|
6
|
+
class Error < StandardError; end
|
7
|
+
class AlreadyRegisteredError < Error; end
|
8
|
+
class UnknownChannelError < Error; end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def add(id, channel_class)
|
12
|
+
raise AlreadyRegisteredError if find(id)
|
13
|
+
channels[id] = channel_class
|
14
|
+
end
|
15
|
+
|
16
|
+
def find(id)
|
17
|
+
channels[id]
|
18
|
+
end
|
19
|
+
|
20
|
+
def find!(id)
|
21
|
+
channel_class = find(id)
|
22
|
+
raise UnknownChannelError unless channel_class
|
23
|
+
channel_class
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def channels
|
29
|
+
@channels ||= {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
# rubocop:disable Metrics/LineLength
|
4
|
+
module Channel
|
5
|
+
# Streams allow channels to route broadcastings to the subscriber. A broadcasting is a pubsub queue where any data
|
6
|
+
# placed into it is automatically sent to the clients that are connected at that time.
|
7
|
+
|
8
|
+
# Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between
|
9
|
+
# the two parties (the broadcaster and the channel subscriber). Here's an example of a channel that allows subscribers to get all new
|
10
|
+
# comments on a given page:
|
11
|
+
#
|
12
|
+
# class CommentsChannel < ApplicationCable::Channel
|
13
|
+
# def follow(data)
|
14
|
+
# stream_from "comments_for_#{data['recording_id']}"
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# def unfollow
|
18
|
+
# stop_all_streams
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Based on the above example, the subscribers of this channel will get whatever data is put into the,
|
23
|
+
# let's say, `comments_for_45` broadcasting as soon as it's put there.
|
24
|
+
#
|
25
|
+
# An example broadcasting for this channel looks like so:
|
26
|
+
#
|
27
|
+
# LiteCable.server.broadcast "comments_for_45", author: 'Donald Duck', content: 'Quack-quack-quack'
|
28
|
+
#
|
29
|
+
# You can stop streaming from all broadcasts by calling #stop_all_streams or use #stop_from to stop streaming broadcasts from the specified stream.
|
30
|
+
module Streams
|
31
|
+
# rubocop:enable Metrics/LineLength
|
32
|
+
def handle_unsubscribe
|
33
|
+
stop_all_streams
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
# Start streaming from the named broadcasting pubsub queue.
|
38
|
+
def stream_from(broadcasting)
|
39
|
+
log(:debug) { log_fmt("Stream from #{broadcasting}") }
|
40
|
+
connection.streams.add(identifier, broadcasting)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Stop streaming from the named broadcasting pubsub queue.
|
44
|
+
def stop_stream(broadcasting)
|
45
|
+
log(:debug) { log_fmt("Stop stream from #{broadcasting}") }
|
46
|
+
connection.streams.remove(identifier, broadcasting)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Unsubscribes all streams associated with this channel from the pubsub queue.
|
50
|
+
def stop_all_streams
|
51
|
+
log(:debug) { log_fmt("Stop all streams") }
|
52
|
+
connection.streams.remove_all(identifier)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module LiteCable
|
5
|
+
module Coders
|
6
|
+
# Wrapper over JSON
|
7
|
+
module JSON
|
8
|
+
class << self
|
9
|
+
def decode(json_str)
|
10
|
+
::JSON.parse(json_str)
|
11
|
+
end
|
12
|
+
|
13
|
+
def encode(ruby_obj)
|
14
|
+
ruby_obj.to_json
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|