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