litecable 0.4.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/.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
|