litecable 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +40 -0
  3. data/.rubocop.yml +63 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGELOG.md +7 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +128 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/circle.yml +8 -0
  13. data/examples/sinatra/Gemfile +16 -0
  14. data/examples/sinatra/Procfile +3 -0
  15. data/examples/sinatra/README.md +33 -0
  16. data/examples/sinatra/anycable +18 -0
  17. data/examples/sinatra/app.rb +52 -0
  18. data/examples/sinatra/assets/app.css +169 -0
  19. data/examples/sinatra/assets/cable.js +584 -0
  20. data/examples/sinatra/assets/reset.css +223 -0
  21. data/examples/sinatra/bin/anycable-go +0 -0
  22. data/examples/sinatra/chat.rb +39 -0
  23. data/examples/sinatra/config.ru +28 -0
  24. data/examples/sinatra/views/index.slim +8 -0
  25. data/examples/sinatra/views/layout.slim +15 -0
  26. data/examples/sinatra/views/login.slim +8 -0
  27. data/examples/sinatra/views/resetcss.slim +224 -0
  28. data/examples/sinatra/views/room.slim +68 -0
  29. data/lib/lite_cable.rb +29 -0
  30. data/lib/lite_cable/anycable.rb +62 -0
  31. data/lib/lite_cable/channel.rb +8 -0
  32. data/lib/lite_cable/channel/base.rb +165 -0
  33. data/lib/lite_cable/channel/registry.rb +34 -0
  34. data/lib/lite_cable/channel/streams.rb +56 -0
  35. data/lib/lite_cable/coders.rb +7 -0
  36. data/lib/lite_cable/coders/json.rb +19 -0
  37. data/lib/lite_cable/coders/raw.rb +15 -0
  38. data/lib/lite_cable/config.rb +18 -0
  39. data/lib/lite_cable/connection.rb +10 -0
  40. data/lib/lite_cable/connection/authorization.rb +13 -0
  41. data/lib/lite_cable/connection/base.rb +131 -0
  42. data/lib/lite_cable/connection/identification.rb +88 -0
  43. data/lib/lite_cable/connection/streams.rb +28 -0
  44. data/lib/lite_cable/connection/subscriptions.rb +108 -0
  45. data/lib/lite_cable/internal.rb +13 -0
  46. data/lib/lite_cable/logging.rb +28 -0
  47. data/lib/lite_cable/server.rb +27 -0
  48. data/lib/lite_cable/server/client_socket.rb +9 -0
  49. data/lib/lite_cable/server/client_socket/base.rb +163 -0
  50. data/lib/lite_cable/server/client_socket/subscriptions.rb +23 -0
  51. data/lib/lite_cable/server/heart_beat.rb +50 -0
  52. data/lib/lite_cable/server/middleware.rb +55 -0
  53. data/lib/lite_cable/server/subscribers_map.rb +67 -0
  54. data/lib/lite_cable/server/websocket_ext/protocols.rb +45 -0
  55. data/lib/lite_cable/version.rb +4 -0
  56. data/lib/litecable.rb +2 -0
  57. data/litecable.gemspec +33 -0
  58. 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, '&lt;')
33
+ .replace(/>/g, '&gt;')
34
+ .replace(/"/g, '&quot;');
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
+ )
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ module Channel # :nodoc:
4
+ require "lite_cable/channel/registry"
5
+ require "lite_cable/channel/streams"
6
+ require "lite_cable/channel/base"
7
+ end
8
+ 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,7 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ module Coders # :nodoc:
4
+ require "lite_cable/coders/raw"
5
+ require "lite_cable/coders/json"
6
+ end
7
+ 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