plezi 0.14.9 → 0.15.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 75f5b97d8891d3fcdd74ee25d556883dbe50532a
4
- data.tar.gz: dc0d53e3107a0333a78c70d34900b72b6427615f
3
+ metadata.gz: 45a3eb75b619cb1724e18f7ae3124558769d09d1
4
+ data.tar.gz: a201af9772fccbe0f07462c42500861fee8fcdb8
5
5
  SHA512:
6
- metadata.gz: bc4f19777b080c9496d167b59aafd8e069e6efe8357980d8965382f7e2dba11760ef9a23d5a94fd8001929bff06297644b2d12e9291c4d740d0d805a542ac3ef
7
- data.tar.gz: 1ec40cf39efcc59d72c053e11ecc89cdcf782cf6fdd0ade51f80f0f470a5f05d03ada4da7c6bce2e2c25b1d3b606299a0224162f2fbc9b2e707b8fca81700ce0
6
+ metadata.gz: 74e90ba29166b17ce68bcfdae5d00ace0c8030e674afa2a60a88d15dd29b4ff304b685529ea59a4aee7fde1f35656fc4b2d52c0de24c785db56eddab632483d4
7
+ data.tar.gz: b515a650921a1fd5f0d592c563340405b28c3925ce84824f5266c785060fd605c900498f31f1f3ad7c325770092f762baa9afd894cd6cf2a616d25697cc5d561
@@ -2,7 +2,15 @@
2
2
 
3
3
  ***
4
4
 
5
- Change log v.0.14.8 (next)
5
+ Change log v.0.15.0
6
+
7
+ **Deprecation**: no more `broadcast`, `unicast`, `multicast` or `write2everyone`... Plezi fully embraced the Pub/Sub design and the [Iodine Extensions to the Rack Websocket Specification Proposal](https://github.com/boazsegev/iodine/blob/master/SPEC-Websocket-Draft.md).
8
+
9
+ **Feature**: Super powerful Pub/Sub support comes directly from the server layer (iodine), allowing for process cluster Pub/Sub without the need for any Pub/Sub service. Iodine also brings a native Redis connector to easily scale Pub/Sub to multiple machines using a Redis servers.
10
+
11
+ ***
12
+
13
+ Change log v.0.14.9 - EOL (last 0.14.x release)
6
14
 
7
15
  **Fix**: Asset Sass rendering will now only save the output to a file in production mode, allowing development mode to hot-load the assets and re-render them when changes occur.
8
16
 
data/README.md CHANGED
@@ -8,7 +8,32 @@ Are microservices on your mind? Do you dream of a an SPA that's easy to scale? D
8
8
 
9
9
  Welcome to your new home with [plezi.io](http://www.plezi.io), the Ruby real-time framework that assumes the business logic is *seperate* from the web service logic.
10
10
 
11
- **NOTICE**: Plezi 0.14.x (this branch) is NOT an update, it's a total rewrite. Features were _removed_ as well as altered. For example, Plezi is now a Rack framework, with the limitations of CGI design and the advantages of using existing middleware. API changes abound.
11
+ ## Short and Sweet
12
+
13
+ What if your next Pub/Sub application could be as easy as:
14
+
15
+ ```ruby
16
+ require 'plezi'
17
+ class MyChatroom
18
+ # HTTP
19
+ def index
20
+ render :index
21
+ end
22
+ def on_open
23
+ subscribe channel: :chat
24
+ @name = ::ERB::Util.h(params[:nickname] || "anonymous")
25
+ publish channel: :chat, message: "#{@name} joined the chat."
26
+ end
27
+ def on_message data
28
+ publish channel: :chat, message: "#{@name}: #{::ERB::Util.h data}"
29
+ end
30
+ def on_shutdown
31
+ write "Server is going away. Come back again some other time, #{@name}."
32
+ end
33
+ end
34
+
35
+ Plezi.route '/(:nickname)', MyChatroom
36
+ ```
12
37
 
13
38
  ## What does Plezi have to offer?
14
39
 
@@ -28,6 +53,8 @@ Plezi will provide the following features over plain Rack:
28
53
 
29
54
  * Auto-Dispatch (optional) to automatically map JSON websocket "events" to Controller functions (handlers).
30
55
 
56
+ * Native Pub/Sub provided by [Iodine](https://github.com/boazsegev/iodine).
57
+
31
58
  * Automatic (optional) scaling using Redis.
32
59
 
33
60
  * An extensible template rendering abstraction engine, supports Slim, Markdown (using RedCarpet) and ERB out of the box.
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # # Uncomment and set Redis URL for automatic scalling across proccesses and machines:
4
- # require 'redis'
5
- # ENV['PL_REDIS_URL'] ||= "redis://localhost:7777/0"
6
- # ENV['PL_REDIS_URL'] ||= "redis://:password@my.host:6389/0"
3
+ # Example benchmark scripts:
4
+ #
5
+ # websocket-bench broadcast ws://127.0.0.1:3000/ --concurrent 10 --sample-size 100 --server-type binary --step-size 1000 --limit-percentile 95 --limit-rtt 250ms --initial-clients 1000
6
+ #
7
+ # websocket-bench broadcast ws://127.0.0.1:3000/ --concurrent 10 --sample-size 100 --step-size 1000 --limit-percentile 95 --limit-rtt 250ms --initial-clients 1000 --server-type json
8
+ #
7
9
 
8
10
  Dir.chdir(File.expand_path(File.join('..', '..', 'lib'), __FILE__))
9
11
  require 'bundler/setup'
@@ -14,25 +16,39 @@ class ShootoutApp
14
16
  def index
15
17
  "This application should be used with the websocket-shootout benchmark utility."
16
18
  end
19
+ def on_open
20
+ subscribe channel: "shootout"
21
+ end
17
22
  # we won't be using AutoDispatch, but directly using the `on_message` callback.
18
23
  def on_message data
24
+ if data[0] == 'b' # binary
25
+ publish(channel: "shootout", message: data)
26
+ data[0] = 'r'
27
+ write data
28
+ return
29
+ end
19
30
  cmd, payload = JSON(data).values_at('type', 'payload')
20
31
  if cmd == 'echo'
21
32
  write({type: 'echo', payload: payload}.to_json)
22
33
  else
23
34
  # data = {type: 'broadcast', payload: payload}.to_json
24
35
  # broadcast :push2client, data
25
- ShootoutApp.write2everyone({type: 'broadcast', payload: payload}.to_json)
36
+ publish(channel: "shootout", message: ({type: 'broadcast', payload: payload}.to_json))
26
37
  write({type: "broadcastResult", payload: payload}.to_json)
27
38
  end
28
39
  rescue
29
40
  puts "Incoming message format error - not JSON?"
30
41
  end
31
42
 
32
- def push2client data
33
- write data
34
- end
35
-
36
43
  end
37
44
 
38
45
  Plezi.route '*', ShootoutApp
46
+
47
+ #
48
+ # def cycle
49
+ # puts `websocket-bench broadcast ws://127.0.0.1:3000/ --concurrent 10 --sample-size 100 --server-type binary --step-size 1000 --limit-percentile 95 --limit-rtt 250ms --initial-clients 1000`
50
+ # sleep(2)
51
+ # puts `wrk -c4000 -d15 -t12 http://localhost:3000/`
52
+ # true
53
+ # end
54
+ # sleep(10) while cycle
@@ -0,0 +1,38 @@
1
+ # finish with `exit` if running within `irb`
2
+ require 'plezi'
3
+ class ChatServer
4
+ def index
5
+ "Use Websockets to connect."
6
+ end
7
+ def on_open
8
+ return close unless params['id']
9
+ @name = params['id']
10
+ subscribe channel: "chat"
11
+ publish channel: "chat", message: "#{@name} joind the chat."
12
+ write "Welcome, #{@name}!"
13
+ # if we have Redis
14
+ if(Iodine.default_pubsub.is_a? Iodine::PubSub::RedisEngine)
15
+ # We'll add the name to the list of people in the chat.
16
+ # Blocks are used as event callbacks and are executed asynchronously.
17
+ Iodine.default_pubsub.send("SADD", "chat_members", @name) do
18
+ # after the name was added, we'll get all the current people in the chat
19
+ Iodine.default_pubsub.send("SMEMBERS", "chat_members") do |members|
20
+ # By now, we're outside the Websocket connection's lock.
21
+ # To safely access the connection, we'll use `defer`
22
+ defer { write "Currently in the chatroom: #{members.join ', '}" }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ def on_close
28
+ publish channel: "chat", message: "#{@name} joind the chat."
29
+ # if we have Redis
30
+ Iodine.default_pubsub.send("SREM", "chat_members", @name) if(Iodine.default_pubsub.is_a? Iodine::PubSub::RedisEngine)
31
+ end
32
+ def on_message data
33
+ publish channel: "chat", message: "#{@name}: #{data}"
34
+ end
35
+ end
36
+ path_to_client = File.expand_path( File.dirname(__FILE__) )
37
+ Plezi.templates = path_to_client
38
+ Plezi.route '/', ChatServer
data/exe/plezi CHANGED
@@ -104,7 +104,7 @@ module Builder
104
104
  puts ''
105
105
  puts "please change directory into the app directory: cd #{app_name}"
106
106
  puts ''
107
- puts "run the #{app_name} app using: ./#{app_name} or using the iodine / rackup commands."
107
+ puts "run the #{app_name} app using: ./#{app_name} or using the iodine or rackup commands."
108
108
  puts ''
109
109
  end
110
110
  end
@@ -1,5 +1,4 @@
1
- require 'plezi/websockets/message_dispatch' unless defined?(::Plezi::Base::MessageDispatch)
2
-
1
+ require 'uri' unless defined?(::URI)
3
2
  module Plezi
4
3
  protected
5
4
 
@@ -14,15 +13,13 @@ module Plezi
14
13
  def self.plezi_initialize
15
14
  if @plezi_initialize.nil?
16
15
  @plezi_initialize = true
17
- self.hash_proc_4symstr # crerate the Proc object used for request params
16
+ self.hash_proc_4symstr # creates the Proc object used for request params
18
17
  @plezi_autostart = true if @plezi_autostart.nil?
19
- if ENV['PL_REDIS_URL'.freeze] && !defined?(::Redis)
20
- puts "WARNNING: auto-scaling with redis is set using ENV['PL_REDIS_URL'.freeze]\r\n but the Redis gem isn't included! - SCALING IS IGNORED!"
21
- ::Iodine.processes ||= 1
22
- elsif !ENV['PL_REDIS_URL'.freeze]
23
- ::Iodine.processes ||= 1
18
+ Iodine.patch_rack
19
+ if((ENV['PL_REDIS_URL'.freeze] ||= ENV["REDIS_URL"]))
20
+ uri = URI(ENV['PL_REDIS_URL'.freeze])
21
+ Iodine.default_pubsub = Iodine::PubSub::RedisEngine.new(uri.host, uri.port, 0, uri.password)
24
22
  end
25
- ::Iodine.processes ||= 4
26
23
  at_exit do
27
24
  next if @plezi_autostart == false
28
25
  ::Iodine::Rack.app = ::Plezi.app
@@ -32,7 +29,3 @@ module Plezi
32
29
  true
33
30
  end
34
31
  end
35
-
36
- ::Iodine.threads ||= 16
37
- # ::Iodine.processes ||= (ENV['PL_REDIS_URL'.freeze] ? 4 : 1)
38
- ::Iodine.run { ::Plezi::Base::MessageDispatch._init }
@@ -1,7 +1,7 @@
1
1
  require 'plezi/render/render'
2
+ require 'plezi/controller/identification'
2
3
  require 'plezi/controller/cookies'
3
4
  require 'plezi/controller/controller_class'
4
- require 'plezi/websockets/message_dispatch'
5
5
 
6
6
  module Plezi
7
7
  # This module contains the functionality provided to any Controller class.
@@ -119,13 +119,13 @@ module Plezi
119
119
  ::Plezi::Base::Router.url_for self.class, func, params
120
120
  end
121
121
 
122
- # A connection's Plezi ID uniquely identifies the connection across application instances, allowing it to receive and send messages using {#unicast}.
122
+ # A connection's Plezi ID uniquely identifies the connection across application instances.
123
123
  def id
124
- @_pl_id ||= (conn_id && "#{::Plezi::Base::MessageDispatch.pid}-#{conn_id.to_s(16)}")
124
+ @_pl_id ||= (conn_id && "#{::Plezi::Base::Identification.pid}-#{conn_id.to_s(16)}")
125
125
  end
126
126
 
127
127
  # @private
128
- # This is the process specific Websocket's UUID. This function is here to protect you from yourself. Don't call it.
128
+ # This is the process specific Websocket's ID. This function is here to protect you from yourself. Don't call it.
129
129
  def conn_id
130
130
  defined?(super) && super
131
131
  end
@@ -143,71 +143,19 @@ module Plezi
143
143
  #
144
144
  # This allows a module "library" to be used similar to the way "rooms" are used in node.js, so that a number of different Controllers can listen to shared events.
145
145
  #
146
- # By dynamically extending a Controller instance using a module, Websocket broadcasts will be allowed to invoke the module's functions.
146
+ # By dynamically extending a Controller instance using a module, Auto Dispatch events can be routed to the newly available methods.
147
147
  #
148
148
  # Notice: It is impossible to `unextend` an extended module at this time.
149
149
  def extend(mod)
150
150
  raise TypeError, '`mod` should be a module' unless mod.class == Module
151
- raise "#{self} already extended by #{mod.name}" if is_a?(mod)
152
- mod.extend ::Plezi::Controller::ClassMethods
153
- super(mod)
151
+ unless is_a?(mod)
152
+ mod.extend ::Plezi::Controller::ClassMethods
153
+ super(mod)
154
+ end
154
155
  _pl_ws_map.update mod._pl_ws_map
155
156
  _pl_ad_map.update mod._pl_ad_map
156
157
  end
157
158
 
158
- # Invokes a method on the `target` websocket connection. When using Iodine, the method is invoked asynchronously.
159
- #
160
- # def perform_poke(target)
161
- # unicast target, :poke, self.id
162
- # end
163
- # def poke(from)
164
- # unicast from, :poke_back, self.id
165
- # end
166
- # def poke_back(from)
167
- # puts "#{from} is available"
168
- # end
169
- #
170
- # Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
171
- def unicast(target, event_method, *args)
172
- ::Plezi::Base::MessageDispatch.unicast(id ? self : self.class, target, event_method, args)
173
- end
174
-
175
- # Invokes a method on every websocket connection (except `self`) that belongs to this Controller / Type. When using Iodine, the method is invoked asynchronously.
176
- #
177
- # self.broadcast :my_method, "argument 1", "argument 2", 3
178
- #
179
- # Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
180
- def broadcast(event_method, *args)
181
- ::Plezi::Base::MessageDispatch.broadcast(id ? self : self.class, event_method, args)
182
- end
183
-
184
- # Invokes a method on every websocket connection in the application (except `self`).
185
- #
186
- # self.multicast :my_method, "argument 1", "argument 2", 3
187
- #
188
- # Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
189
- def multicast(event_method, *args)
190
- ::Plezi::Base::MessageDispatch.multicast(id ? self : self.class, event_method, args)
191
- end
192
-
193
- # Writes a message to every client websocket connection, for all controllers(!), EXCEPT self. Accepts an optional filter method using a location reference for a *static* (Class/Module/global) method. The filter method will be passerd the websocket object and it should return `true` / `false`.
194
- #
195
- # self.write2everyone {event: "global", message: "This will be sent to everyone"}.to_json
196
- # # or, we can define a filter method somewhere in our code
197
- # module Filter
198
- # def self.should_send? ws
199
- # true
200
- # end
201
- # end
202
- # # and we can use this filter method.
203
- # data = {event: "global", message: "This will be sent to everyone"}.to_json
204
- # self.write2everyone data, ::Filter, :should_send?
205
- #
206
- # It's important that the filter method is defined statically in our code and isn't dynamically allocated. Otherwise, scaling the application would be impossible.
207
- def write2everyone(data, filter_owner = nil, filter_name = nil)
208
- ::Plezi::Base::MessageDispatch.write2everyone(id ? self : self.class, data, filter_owner, filter_name)
209
- end
210
-
211
159
  # @private
212
160
  # This function is used internally by Plezi, do not call.
213
161
  def _pl_ws_map
@@ -226,6 +174,7 @@ module Plezi
226
174
  json = nil
227
175
  begin
228
176
  json = JSON.parse(data, symbolize_names: true)
177
+ # json.default_proc = Plezi.hash_proc_4symstr
229
178
  rescue
230
179
  puts 'AutoDispatch Warnnig: Received non-JSON message. Closing Connection.'
231
180
  close
@@ -7,55 +7,16 @@ module Plezi
7
7
  base._pl_init_class_data
8
8
  end
9
9
 
10
+ # Publishes a message to a channel.
11
+ def publish(args)
12
+ Iodine.publish args
13
+ end
14
+
10
15
  # Returns a relative URL for the controller, placing the requested parameters in the URL (inline, where possible and as query data when not possible).
11
16
  def url_for(func, params = {})
12
17
  ::Plezi::Base::Router.url_for self, func, params
13
18
  end
14
19
 
15
- # Invokes a method on the `target` websocket connection. When using Iodine, the method is invoked asynchronously.
16
- #
17
- # self.unicast target, :my_method, "argument 1"
18
- #
19
- # Methods invoked using {unicast}, {broadcast} or {multicast} will quitely fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
20
- def unicast(target, event_method, *args)
21
- ::Plezi::Base::MessageDispatch.unicast(self, target, event_method, args)
22
- end
23
-
24
- # Invokes a method on every websocket connection that belongs to this Controller / Type. When using Iodine, the method is invoked asynchronously.
25
- #
26
- # self.broadcast :my_method, "argument 1", "argument 2", 3
27
- #
28
- # Methods invoked using {unicast}, {broadcast} or {multicast} will quitely fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
29
- def broadcast(event_method, *args)
30
- ::Plezi::Base::MessageDispatch.broadcast(self, event_method, args)
31
- end
32
-
33
- # Invokes a method on every websocket connection in the application.
34
- #
35
- # self.multicast :my_method, "argument 1", "argument 2", 3
36
- #
37
- # Methods invoked using {unicast}, {broadcast} or {multicast} will quitely fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
38
- def multicast(event_method, *args)
39
- ::Plezi::Base::MessageDispatch.multicast(self, event_method, args)
40
- end
41
- # Writes a message to every client websocket connection in all controllers(!). Accepts an optional filter method using a location reference for a *static* (Class/Module/global) method. The filter method will be passerd the websocket object and it should return `true` / `false`.
42
- #
43
- # self.write2everyone {event: "global", message: "This will be sent to everyone"}.to_json
44
- # # or, we can define a filter method somewhere in our code
45
- # module Filter
46
- # def self.should_send? ws
47
- # true
48
- # end
49
- # end
50
- # # and we can use this filter method.
51
- # data = {event: "global", message: "This will be sent to everyone"}.to_json
52
- # self.write2everyone data, ::Filter, :should_send?
53
- #
54
- # It's important that the filter method is defined statically in our code and isn't dynamically allocated. Otherwise, scaling the application would be impossible.
55
- def write2everyone(data, filter_owner = nil, filter_name = nil)
56
- ::Plezi::Base::MessageDispatch.write2everyone(self, data, filter_owner, filter_name)
57
- end
58
-
59
20
  # @private
60
21
  # This is used internally by Plezi, do not use.
61
22
  RESERVED_METHODS = [:delete, :create, :update, :new, :show, :pre_connect, :on_open, :on_close, :on_shutdown, :on_message].freeze
@@ -0,0 +1,29 @@
1
+ module Plezi
2
+ module Base
3
+ module Identification
4
+
5
+ module_function
6
+
7
+ @ppid = ::Process.pid
8
+ # returns a Plezi flavored pid UUID, used to set the pub/sub channel when scaling
9
+ def pid
10
+ process_pid = ::Process.pid
11
+ if @ppid != process_pid
12
+ @pid = nil
13
+ @ppid = process_pid
14
+ end
15
+ @pid ||= SecureRandom.urlsafe_base64.tap { |str| @prefix_len = str.length }
16
+ end
17
+ # Converts a target Global UUID to a localized UUID
18
+ def target2uuid(target)
19
+ return nil unless target.start_with? pid
20
+ target[@prefix_len..-1].to_i
21
+ end
22
+
23
+ # Extracts the machine part from a target's Global UUID
24
+ def target2pid(target)
25
+ target ? target[0..(@prefix_len - 1)] : Plezi.app_name
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,3 +1,3 @@
1
1
  module Plezi
2
- VERSION = '0.14.9'.freeze
2
+ VERSION = '0.15.0'.freeze
3
3
  end
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ['lib']
29
29
 
30
- spec.add_dependency 'iodine', '~> 0.3', '< 0.4'
30
+ spec.add_dependency 'iodine', '~> 0.4'
31
31
  spec.add_dependency 'rack', '>= 2.0.0'
32
32
  spec.add_dependency 'bundler', '~> 1.14'
33
33
  # spec.add_dependency 'redcarpet', '> 3.3.0'
@@ -1,34 +1,25 @@
1
1
  # Replace this sample with real code.
2
2
  class ExampleCtrl
3
- # HTTP
4
- def index
5
- # any String returned will be appended to the response. We return a String.
6
- render 'welcome'
7
- end
3
+ # HTTP
4
+ def index
5
+ # any String returned will be appended to the response. We return a String.
6
+ render 'welcome'
7
+ end
8
8
 
9
- # Websockets
10
- def on_message(data)
11
- data = ERB::Util.html_escape data
12
- print data
13
- broadcast :print, data
14
- end
9
+ # Websockets
10
+ def on_open
11
+ subscribe channel: "chat"
12
+ write 'Welcome to appname!'
13
+ @handle = params['id'.freeze] || 'Somebody'
14
+ publish channel: "chat", message: "#{ERB::Util.html_escape @handle} joind us :-)"
15
+ end
16
+ def on_message(data)
17
+ data = ERB::Util.html_escape data
18
+ publish channel: "chat", message: data
19
+ end
15
20
 
16
- def on_open
17
- print 'Welcome to appname!'
18
- @handle = params['id'.freeze] || 'Somebody'
19
- broadcast :print, "#{ERB::Util.html_escape @handle} joind us :-)"
20
- end
21
+ def on_close
22
+ publish channel: "chat", message: "#{@handle} left us :-("
23
+ end
21
24
 
22
- def on_close
23
- broadcast :print, "#{@handle} left us :-("
24
- end
25
-
26
- protected
27
-
28
- # write is inherites when a Websocket connection is opened.
29
- #
30
- # Inherited functions aren't exposed (for our security), so we need to wrap it.
31
- def print(data)
32
- write data
33
- end
34
25
  end
@@ -4,4 +4,4 @@
4
4
  # set the working directory
5
5
  cd "$(dirname "$0")"
6
6
  # load the website-app
7
- bundle exec iodine -t 16 -w 1 -www ./public
7
+ bundle exec iodine -t 16 -w 4 -www ./public
@@ -187,7 +187,7 @@ var websocket_fail_count = 0
187
187
  function init_websocket()
188
188
  {
189
189
  if(websocket && websocket.readyState == 1) return true; // console.log('no need to renew socket connection');
190
- websocket = new WebSocket(ws_uri);
190
+ websocket = new WebSocket(ws_uri + document.getElementById("input").value);
191
191
  websocket.onopen = function(e) {
192
192
  //restart fail count
193
193
  websocket_fail = 0
@@ -266,7 +266,7 @@ function send_text()
266
266
  </ol>
267
267
  <p class="bold">Good Luck!</p>
268
268
  <h3> Did you try our websocket broadcast? </h3>
269
- <p>Your appname app can send <span class="bold">WebSocket</span> broadcasts (and unicasts). Why not try them out? You can even try them across multiple windows.</p>
269
+ <p>Your appname app can send <span class="bold">WebSocket</span> messages and subscribe / publish to channels of information. This chat works across browser windows.</p>
270
270
  <form onsubmit='send_text(); return false;' id='monitor'>
271
271
  <p><input type='text' id='input' placeholder='Your message' /> <input type='submit' value='Broadcast'/> </p>
272
272
  <ul id='output'>
@@ -1,3 +1,4 @@
1
- # Edit these parameters if you wish to leverage multi-process concurrency. i.e.:
2
- # bundle exec iodine -p $PORT -t 16 -w 4 -www ./public -warmup # => 4 processes + fix lazy loading
3
- web: bundle exec iodine -p $PORT -t 16 -w 1 -www ./public
1
+ # # Edit these parameters if you wish to manually manage concurrency. i.e.:
2
+ # web: bundle exec iodine -p $PORT -t 16 -w 4 -www ./public -warmup
3
+ #
4
+ web: bundle exec iodine -p $PORT -www ./public
metadata CHANGED
@@ -1,23 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plezi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.9
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Boaz Segev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-06-18 00:00:00.000000000 Z
11
+ date: 2017-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: iodine
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '0.3'
20
- - - "<"
21
18
  - !ruby/object:Gem::Version
22
19
  version: '0.4'
23
20
  type: :runtime
@@ -25,9 +22,6 @@ dependencies:
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - "~>"
28
- - !ruby/object:Gem::Version
29
- version: '0.3'
30
- - - "<"
31
25
  - !ruby/object:Gem::Version
32
26
  version: '0.4'
33
27
  - !ruby/object:Gem::Dependency
@@ -106,6 +100,7 @@ files:
106
100
  - bin/hello_world
107
101
  - bin/setup
108
102
  - bin/ws_shootout
103
+ - examples/chat.rb
109
104
  - exe/plezi
110
105
  - lib/plezi.rb
111
106
  - lib/plezi/activation.rb
@@ -113,6 +108,7 @@ files:
113
108
  - lib/plezi/controller/controller.rb
114
109
  - lib/plezi/controller/controller_class.rb
115
110
  - lib/plezi/controller/cookies.rb
111
+ - lib/plezi/controller/identification.rb
116
112
  - lib/plezi/helpers.rb
117
113
  - lib/plezi/rake.rb
118
114
  - lib/plezi/render/erb.rb
@@ -127,8 +123,6 @@ files:
127
123
  - lib/plezi/router/route.rb
128
124
  - lib/plezi/router/router.rb
129
125
  - lib/plezi/version.rb
130
- - lib/plezi/websockets/message_dispatch.rb
131
- - lib/plezi/websockets/redis.rb
132
126
  - plezi.gemspec
133
127
  - resources/404.erb
134
128
  - resources/500.erb
@@ -1,135 +0,0 @@
1
- module Plezi
2
- module Base
3
- # Websocket Message Dispatching Service, including the autoscaling driver control (at the moment Redis is the only builtin driver).
4
- module MessageDispatch
5
- # add class attribute accessors.
6
- class << self
7
- # Allows pub/sub drivers to attach to the message dispatch using `MessageDispatch.drivers << driver`
8
- attr_reader :drivers
9
- end
10
- @drivers = [].to_set
11
-
12
- module_function
13
-
14
- # The YAML safe types used by Plezi
15
- SAFE_TYPES = [Symbol, Date, Time, Encoding, Struct, Regexp, Range, Set].freeze
16
- # a single use empty array (prevents the use of temporary objects where possible)
17
- EMPTY_ARGS = [].freeze
18
- # keeps track of the current process ID
19
- @ppid = ::Process.pid
20
- # returns a Plezi flavored pid UUID, used to set the pub/sub channel when scaling
21
- def pid
22
- process_pid = ::Process.pid
23
- if @ppid != process_pid
24
- @pid = nil
25
- @ppid = process_pid
26
- end
27
- @pid ||= SecureRandom.urlsafe_base64.tap { |str| @prefix_len = str.length }
28
- end
29
-
30
- # initializes the drivers when possible.
31
- def _init
32
- @drivers.each(&:connect)
33
- end
34
-
35
- # Pushes a message to the Pub/Sub drivers
36
- def push(message)
37
- # message[:type] = message[:type].name if message[:type].is_a?(Class)
38
- message[:origin] = pid
39
- hst = message.delete(:host) || Plezi.app_name
40
- yml = message.to_yaml
41
- @drivers.each { |drv| drv.push(hst, yml) }
42
- end
43
-
44
- # Parses a text message received through a Pub/Sub service.
45
- def <<(msg)
46
- msg = YAML.safe_load(msg, SAFE_TYPES)
47
- return if msg[:origin] == pid
48
- target_type = msg[:type] || :all
49
- event = msg[:event]
50
- if (target = msg[:target])
51
- Iodine::Websocket.defer(target2uuid(target)) { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[event], *(msg[:args]))) if ws._pl_ws_map[event] }
52
- return
53
- end
54
- if target_type == :all
55
- Iodine::Websocket.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[event], *(msg[:args]))) if ws._pl_ws_map[event] }
56
- return
57
- end
58
- if event == :write2everyone
59
- return unless msg[:data]
60
- mth = msg[:method]
61
- if(mth)
62
- target_type = Object.const_get target_type
63
- mth = target_type.method(mth)
64
- return unless mth
65
- Iodine::Websocket.each_write msg[:data], &mth
66
- else
67
- Iodine::Websocket.each_write msg[:data]
68
- end
69
- return
70
- end
71
- target_type = Object.const_get target_type
72
- if target_type._pl_ws_map[event]
73
- Iodine::Websocket.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[event], *(msg[:args]))) if ws.is_a?(target_type) }
74
- return
75
- end
76
-
77
- rescue => e
78
- puts '*** The following could be a security breach attempt:', e.message, e.backtrace
79
- nil
80
- end
81
-
82
- # Sends a message to a specific target, if it's on this machine, otherwise forwards the message to the Pub/Sub.
83
- def unicast(_sender, target, meth, args)
84
- return false if target.nil?
85
- if (tuuid = target2uuid(target))
86
- Iodine::Websocket.defer(tuuid) { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[meth], *args)) if ws._pl_ws_map[meth] }
87
- return true
88
- end
89
- push target: target, args: args, host: target2pid(target)
90
- end
91
-
92
- # Sends a message to a all targets of a speific **type**, as well as pushing the message to the Pub/Sub drivers.
93
- def broadcast(sender, meth, args)
94
- target_type = nil
95
- if sender.is_a?(Class)
96
- target_type = sender
97
- sender = Iodine::Websocket
98
- else
99
- target_type = sender.class
100
- end
101
- sender.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[meth], *args)) if ws.is_a?(target_type) && ws._pl_ws_map[meth] }
102
- push type: target_type.name, args: args, event: meth
103
- end
104
-
105
- # Sends a message to a all existing websocket connections, as well as pushing the message to the Pub/Sub drivers.
106
- def multicast(sender, meth, args)
107
- sender = Iodine::Websocket if sender.is_a?(Class)
108
- sender.each { |ws| ws._pl_ad_review(ws.__send__(ws._pl_ws_map[meth], *args)) if ws._pl_ws_map[meth] }
109
- push type: :all, args: args, event: meth
110
- end
111
-
112
- # Writes directly to all clients of all controllers.
113
- def write2everyone(sender, data, filter_owner = nil, filter_name = nil)
114
- sender = Iodine::Websocket if sender.is_a?(Class)
115
- mth = nil
116
- raise TypeError, "Plezi\#write_each filter error - method doesn't exist? #{filter_owner}.#{filter_name}" if(filter_owner && !(mth = filter_owner.method(filter_name)))
117
- mth ? sender.each_write(data, &mth) : sender.each_write(data)
118
- push event: :write2everyone, data: data, type: (filter_owner || Iodine::Websocket).name, method: (filter_owner && filter_name)
119
- end
120
-
121
- # Converts a target Global UUID to a localized UUID
122
- def target2uuid(target)
123
- return nil unless target.start_with? pid
124
- target[@prefix_len..-1].to_i
125
- end
126
-
127
- # Extracts the machine part from a target's Global UUID
128
- def target2pid(target)
129
- target ? target[0..(@prefix_len - 1)] : Plezi.app_name
130
- end
131
- end
132
- end
133
- end
134
- # connect default drivers
135
- require 'plezi/websockets/redis'
@@ -1,54 +0,0 @@
1
- module Plezi
2
- module Base
3
- module MessageDispatch
4
- module RedisDriver
5
- @redis_locker ||= Mutex.new
6
- @redis = @redis_sub_thread = nil
7
-
8
- module_function
9
-
10
- def connect
11
- return false unless ENV['PL_REDIS_URL'] && defined?(::Redis)
12
- return @redis if (@redis_sub_thread && @redis_sub_thread.alive?) && @redis
13
- @redis_locker.synchronize do
14
- return @redis if (@redis_sub_thread && @redis_sub_thread.alive?) && @redis # repeat the test inside syncing, things change.
15
- @redis.quit if @redis
16
- @redis = ::Redis.new(url: ENV['PL_REDIS_URL'])
17
- raise "Redis connction failed for: #{ENV['PL_REDIS_URL']}" unless @redis
18
- @redis_sub_thread = Thread.new do
19
- begin
20
- ::Redis.new(url: ENV['PL_REDIS_URL']).subscribe(::Plezi.app_name, ::Plezi::Base::MessageDispatch.pid) do |on|
21
- on.message do |_channel, msg|
22
- ::Plezi::Base::MessageDispatch << msg
23
- end
24
- end
25
- rescue => e
26
- puts e.message, e.backtrace
27
- retry
28
- end
29
- end
30
- @redis
31
- end
32
- end
33
-
34
- # Get the current redis connection.
35
- def redis
36
- @redis || connect
37
- end
38
-
39
- def push(channel, message)
40
- return unless connect
41
- return if away?(channel)
42
- redis.publish(channel, message)
43
- end
44
-
45
- def away?(server)
46
- return true unless connect
47
- @redis.pubsub('CHANNELS', server).empty?
48
- end
49
- end
50
- end
51
- end
52
- end
53
-
54
- ::Plezi::Base::MessageDispatch.drivers << ::Plezi::Base::MessageDispatch::RedisDriver