plezi 0.14.9 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -1
- data/README.md +28 -1
- data/bin/ws_shootout +25 -9
- data/examples/chat.rb +38 -0
- data/exe/plezi +1 -1
- data/lib/plezi/activation.rb +6 -13
- data/lib/plezi/controller/controller.rb +10 -61
- data/lib/plezi/controller/controller_class.rb +5 -44
- data/lib/plezi/controller/identification.rb +29 -0
- data/lib/plezi/version.rb +1 -1
- data/plezi.gemspec +1 -1
- data/resources/ctrlr.rb +19 -28
- data/resources/mini_exec +1 -1
- data/resources/mini_welcome_page.html +2 -2
- data/resources/procfile +4 -3
- metadata +4 -10
- data/lib/plezi/websockets/message_dispatch.rb +0 -135
- data/lib/plezi/websockets/redis.rb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45a3eb75b619cb1724e18f7ae3124558769d09d1
|
4
|
+
data.tar.gz: a201af9772fccbe0f07462c42500861fee8fcdb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 74e90ba29166b17ce68bcfdae5d00ace0c8030e674afa2a60a88d15dd29b4ff304b685529ea59a4aee7fde1f35656fc4b2d52c0de24c785db56eddab632483d4
|
7
|
+
data.tar.gz: b515a650921a1fd5f0d592c563340405b28c3925ce84824f5266c785060fd605c900498f31f1f3ad7c325770092f762baa9afd894cd6cf2a616d25697cc5d561
|
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,15 @@
|
|
2
2
|
|
3
3
|
***
|
4
4
|
|
5
|
-
Change log v.0.
|
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
|
-
|
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.
|
data/bin/ws_shootout
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
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
|
-
|
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
|
data/examples/chat.rb
ADDED
@@ -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
|
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
|
data/lib/plezi/activation.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
require '
|
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 #
|
16
|
+
self.hash_proc_4symstr # creates the Proc object used for request params
|
18
17
|
@plezi_autostart = true if @plezi_autostart.nil?
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
122
|
+
# A connection's Plezi ID uniquely identifies the connection across application instances.
|
123
123
|
def id
|
124
|
-
@_pl_id ||= (conn_id && "#{::Plezi::Base::
|
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
|
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,
|
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
|
-
|
152
|
-
|
153
|
-
|
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
|
data/lib/plezi/version.rb
CHANGED
data/plezi.gemspec
CHANGED
@@ -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.
|
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'
|
data/resources/ctrlr.rb
CHANGED
@@ -1,34 +1,25 @@
|
|
1
1
|
# Replace this sample with real code.
|
2
2
|
class ExampleCtrl
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
data/resources/mini_exec
CHANGED
@@ -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>
|
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'>
|
data/resources/procfile
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
-
# Edit these parameters if you wish to
|
2
|
-
#
|
3
|
-
|
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.
|
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-
|
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
|