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.
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