plezi 0.12.6 → 0.12.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +171 -29
- data/lib/plezi/common/settings.rb +1 -1
- data/lib/plezi/handlers/controller_magic.rb +2 -2
- data/lib/plezi/handlers/route.rb +19 -0
- data/lib/plezi/handlers/ws_identity.rb +107 -25
- data/lib/plezi/version.rb +1 -1
- data/plezi.gemspec +1 -1
- data/resources/mini_app.rb +31 -35
- data/resources/mini_welcome_page.html +5 -2
- data/resources/rakefile +1 -1
- data/resources/redis_config.rb +9 -9
- data/resources/welcome_page.html +4 -1
- data/test/plezi_tests.rb +67 -6
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d91b4b4d0ac176a59cea5da3a27855993b556116
|
4
|
+
data.tar.gz: e441b5f902541c72c17ae1be1c86ac3597f9c111
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be3313b3e45c65dfbb9fe4d39d03de6af96dce77da8a4fb9e55a43b0637617611c239e0f643ec5e8931411f234d0f3b233b76ea310fae840d4201cce19b95237
|
7
|
+
data.tar.gz: b37aab54598e3e35621648fb5676149b206335b7b22da0d52c197d34943a05dd0fd9d589fc4d3fd4662f343f477c9c1135e1eda2242bd41af639bff494e1a76c
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
***
|
4
4
|
|
5
|
+
Change log v.0.12.7
|
6
|
+
|
7
|
+
**Identity API**: Identity API now allows you to set a higher number of allowable concurrent connections per identity, rather than the original single connection limit. Also, allows limited functionality when Redis isn't defined (registration lifetime is limited to the process lifetime and scaling will not work without Redis).
|
8
|
+
|
9
|
+
**Template**: minor template updates.
|
10
|
+
|
11
|
+
**Fix**: fixed an issue with data and (file) sending, introduced when extending the `send_data` method to allow for big File objects (buffering them through the connection instead of loading them to the memory).
|
12
|
+
|
13
|
+
***
|
14
|
+
|
5
15
|
Change log v.0.12.6
|
6
16
|
|
7
17
|
**Template Fix**: Heroku would load the `environment.rb` file while deploying the application. This would cause Plezi's server to kick in and hang deployment. This issue was circumvented by renaming the `environment.rb` file to `initialize.rb`. Thanks to Adrian Gomez for exposing the issue (issue#9)
|
data/README.md
CHANGED
@@ -7,19 +7,11 @@ Plezi is an easy to use Ruby Websocket Framework, with full RESTful routing supp
|
|
7
7
|
|
8
8
|
With Plezi, you can easily:
|
9
9
|
|
10
|
-
1.
|
10
|
+
1. Create a full fledged Ruby web application, taking full advantage of RESTful routing, HTTP streaming and scalable Websocket features;
|
11
11
|
|
12
|
-
2.
|
12
|
+
2. Add Websocket services and RESTful HTTP Streaming to your existing Web-App, (Rails/Sinatra or any other Rack based Ruby app);
|
13
13
|
|
14
|
-
3. Create
|
15
|
-
|
16
|
-
Plezi leverages [Iodine's server](https://github.com/boazsegev/iodine) new architecture. Iodine is a pure Ruby HTTP and Websocket Server built using [Iodine's](https://github.com/boazsegev/iodine) core library - a multi-threaded pure ruby alternative to EventMachine with process forking support (enjoy forking, if your code is scaling ready).
|
17
|
-
|
18
|
-
Plezi and Iodine are written for Ruby versions 2.1.0 or greater (or API compatible variants). Version 2.2.3 is the currently recommended version.
|
19
|
-
|
20
|
-
**Plezi version notice**
|
21
|
-
|
22
|
-
The `master` branch always refers to the latest edge version, which might also be a broken version. Please refer to the relevent version by using the version's `tag` in the branch selector.
|
14
|
+
3. Create an easily scalable backend for your SPA.
|
23
15
|
|
24
16
|
## Installation
|
25
17
|
|
@@ -35,26 +27,173 @@ Or install it yourself as:
|
|
35
27
|
|
36
28
|
## Creating a Plezi Application
|
37
29
|
|
38
|
-
|
30
|
+
I love starting small and growing. So, when I create a Plezi app, I just want the basics that allow me to easily deploy my application. with these goals, I run the following in my terminal:
|
31
|
+
|
32
|
+
$ plezi mini appname
|
33
|
+
|
34
|
+
But, some people prefer to have the application template already full blown and ready for heavy lifting, complete with some common settings for common gems and code snippets they can activate. These people open their terminal and execute:
|
39
35
|
|
40
36
|
$ plezi new appname
|
41
37
|
|
42
|
-
That's it, now
|
38
|
+
That's it, now we have a ready to use basic web server (with some demo code, such as a websocket chatroom).
|
43
39
|
|
44
|
-
|
40
|
+
On MacOS or linux, simply double click the `appname` script file to run. Or, from the terminal:
|
45
41
|
|
46
42
|
$ cd appname
|
47
43
|
$ ./appname # ( or: plezi s )
|
48
44
|
|
49
|
-
|
45
|
+
See it work: [http://localhost:3000/](http://localhost:3000/)
|
46
|
+
|
47
|
+
## So easy, we can an app in the terminal!
|
48
|
+
|
49
|
+
The Plezi framework was designed with intuitive ease of use in mind.
|
50
|
+
|
51
|
+
Question - what's the shortest "Hello World" web-application when writing for Sinatra or Rails? ... can you write one in your terminal window?
|
52
|
+
|
53
|
+
In Plezi, it looks like this:
|
54
|
+
|
55
|
+
require 'plezi'
|
56
|
+
route('*') { "Hello World!" }
|
57
|
+
|
58
|
+
Two lines! You can even start a Plezi application from `irb`, by adding the `exit` at the end:
|
59
|
+
|
60
|
+
require 'plezi'
|
61
|
+
route('*') { "Hello World!" }
|
62
|
+
exit # <- this exits the terminal and starts the server
|
63
|
+
|
64
|
+
Now visit [localhost:3000](http://localhost:3000/)
|
65
|
+
|
66
|
+
### Object Oriented design is fun!
|
67
|
+
|
68
|
+
While Plezi allows you to use methods like we just did, Plezi really shines when we use Controller classes.
|
69
|
+
|
70
|
+
Plezi will automatically map instance methods in any class to routes with complete RESTful routing support.
|
71
|
+
|
72
|
+
Let's try this terminal (`irb`):
|
50
73
|
|
51
|
-
|
74
|
+
require 'plezi'
|
75
|
+
class MyDemo
|
76
|
+
# the index will answer '/'
|
77
|
+
def index
|
78
|
+
"Hello World!"
|
79
|
+
end
|
80
|
+
# a regular method will answer it's own name i.e. '/foo'
|
81
|
+
def foo
|
82
|
+
"Bar!"
|
83
|
+
end
|
84
|
+
# show is RESTful, it will answer '/(:id)'
|
85
|
+
def show
|
86
|
+
"Are you looking for: #{params[:id]}?"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
route '/', MyDemo
|
91
|
+
exit
|
92
|
+
|
93
|
+
Now visit [index](http://localhost:3000/) and [foo](http://localhost:3000/foo) or request an id, i.e. [http://localhost:3000/1](http://localhost:3000/1).
|
94
|
+
|
95
|
+
Did you notice how the controller has natural access to the request's `params`?
|
52
96
|
|
53
|
-
|
97
|
+
This is because Plezi inherits our controller and adds some magic to it, allowing us to read and set cookies using the `cookies` Hash based cookie-jar, set or read session data using `session`, look into the `request`, set special headers for the `response`, store self destructing cookies using `flash` and so much more!
|
54
98
|
|
55
|
-
|
99
|
+
### Can websockets do that?!
|
56
100
|
|
57
|
-
|
101
|
+
Plezi was designed for websockets from the ground up. If your controller class defines an `on_message(data)` callback, plezi will automatically enable websocket connections for that route.
|
102
|
+
|
103
|
+
Here's a Websocket echo server using Plezi:
|
104
|
+
|
105
|
+
require 'plezi'
|
106
|
+
class MyDemo
|
107
|
+
def on_message data
|
108
|
+
# sanitize the data and write it to the websocket.
|
109
|
+
write ">> #{ERB::Util.html_escape data}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
route '/', MyDemo
|
114
|
+
exit
|
115
|
+
|
116
|
+
But that's not all, each controller is also a "channel" which can broadcast to everyone who's connected to it.
|
117
|
+
|
118
|
+
Here's a websocket chat-room server using Plezi, comeplete with minor authentication (requires a chat handle):
|
119
|
+
|
120
|
+
require 'plezi'
|
121
|
+
class MyDemo
|
122
|
+
def on_open
|
123
|
+
# there's a better way to require a user handle, but this is good enough for now.
|
124
|
+
close unless params[:id]
|
125
|
+
end
|
126
|
+
def on_message data
|
127
|
+
# sanitize the data.
|
128
|
+
data = ERB::Util.html_escape data
|
129
|
+
# broadcast to everyone else (NOT ourselves):
|
130
|
+
# this will have every connection execute the `chat_message` with the following argument(s).
|
131
|
+
broadcast :chat_message, "#{params[:id]}: #{data}"
|
132
|
+
# write to our own websocket:
|
133
|
+
write "Me: #{data}"
|
134
|
+
end
|
135
|
+
protected
|
136
|
+
# receive and implement the broadcast
|
137
|
+
def chat_message data
|
138
|
+
write data
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
route '/', MyDemo
|
143
|
+
# You can connect to this chatroom by going to ws://localhost:3000/any_nickname
|
144
|
+
# but you need to write a websocket client too...
|
145
|
+
# try two browsers with the client provided by http://www.websocket.org/echo.html
|
146
|
+
exit
|
147
|
+
|
148
|
+
Broadcasting in not the only help Plezi has to offer, we can also send a message to a specific connection using `unicast`, or send a message to everyone (no matter what controller is handling their connection) using `multicast`...
|
149
|
+
|
150
|
+
...It's even possible to register a unique identity, such as a specific user or even a `session.id`, so their messages are waiting for them even when they're off-line (you decide how long they wait)!
|
151
|
+
|
152
|
+
### Websocket scaling is as easy as one line of code!
|
153
|
+
|
154
|
+
A common issue with Websocket scaling is trying to send websocket messages from server X to a user connected to server Y... On Heroku, it's enough add one Dyno (a total of two Dynos) to break some websocket applications.
|
155
|
+
|
156
|
+
Plezi leverages the power or Redis to automatically push both websocket messages and Http session data across servers, so that you can easily scale your applications (on Heroku, add Dynos) with only one line of code!
|
157
|
+
|
158
|
+
Just tell Plezi how to acess your Redis server and Plezi will make sure that your users get their messages and that your application can access it's session data accross different servers:
|
159
|
+
|
160
|
+
# REDIS_URL is where Herolu-Redis stores it's URL
|
161
|
+
ENV['PL_REDIS_URL'] ||= ENV['REDIS_URL'] || "redis://username:password@my.host:6389"
|
162
|
+
|
163
|
+
### Hosts, template rendering, assets...?
|
164
|
+
|
165
|
+
Plezi allows us to use different host-names for different routes. i.e.:
|
166
|
+
|
167
|
+
require 'plezi'
|
168
|
+
|
169
|
+
host # this is the default host, it's always last to be checked.
|
170
|
+
route('/') {"this is localhost"}
|
171
|
+
|
172
|
+
host host: '127.0.0.1' # special host, for the IP name
|
173
|
+
route('/') {"this is only for the IP!"}
|
174
|
+
exit
|
175
|
+
|
176
|
+
Each host has it's own settings for a public folder, asset rendering, templates etc'. For example:
|
177
|
+
|
178
|
+
require 'plezi'
|
179
|
+
|
180
|
+
class MyDemo
|
181
|
+
def index
|
182
|
+
# to make this work, create a template and set the correct template folder
|
183
|
+
render :index
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
host public: File.join('my', 'public', 'folder'),
|
188
|
+
templates: File.join('my', 'templates', 'folder'),
|
189
|
+
assets: File.join('my', 'assets', 'folder')
|
190
|
+
|
191
|
+
route '/', MyDemo
|
192
|
+
exit
|
193
|
+
|
194
|
+
Plezi supports ERB (i.e. `template.html.erb`), Slim (i.e. `template.html.slim`), Haml (i.e. `template.html.haml`), CoffeeScript (i.e. `template.js.coffee`) and Sass (i.e. `template.css.scss`) right out of the box... but it's expendable using the `Plezi::Renderer.register` and `Plezi::AssetManager.register`
|
195
|
+
|
196
|
+
## More about Plezi Controller classes
|
58
197
|
|
59
198
|
One of the best things about the Plezi is it's ability to take in any class as a controller class and route to the classes methods with special support for RESTful methods (`index`, `show`, `new`, `save`, `update`, `delete`, `before` and `after`) and for WebSockets (`pre_connect`, `on_open`, `on_message(data)`, `on_close`, `broadcast`, `unicast`, `multicast`, `on_broadcast(data)`).
|
60
199
|
|
@@ -68,17 +207,12 @@ Here is a Hello World using a Controller class (run in `irb`):
|
|
68
207
|
end
|
69
208
|
end
|
70
209
|
|
71
|
-
# use the host method to set up any specific host options:
|
72
|
-
host :default,
|
73
|
-
public: File.join('my', 'public', 'folder'),
|
74
|
-
templates: File.join('my', 'template', 'folder')
|
75
|
-
|
76
210
|
|
77
211
|
route '*' , Controller
|
78
212
|
|
79
213
|
exit # Plezi will autostart once you exit irb.
|
80
214
|
|
81
|
-
Except
|
215
|
+
Except when using WebSockets, returning a String will automatically add the string to the response before sending the response - which makes for cleaner code. It's also possible to use the `response` object to set the response or stream HTTP (return true instead of a stream when you're done).
|
82
216
|
|
83
217
|
It's also possible to define a number of controllers for a similar route. The controllers will answer in the order in which the routes are defined (this allows to group code by logic instead of url).
|
84
218
|
|
@@ -86,7 +220,7 @@ It's also possible to define a number of controllers for a similar route. The co
|
|
86
220
|
|
87
221
|
## Native Websocket and Redis support
|
88
222
|
|
89
|
-
Plezi Controllers have access to native websocket support through the `pre_connect`, `on_open`, `on_message(data)`, `on_close`, `broadcast` and `
|
223
|
+
Plezi Controllers have access to native websocket support through the `pre_connect`, `on_open`, `on_message(data)`, `on_close`, `multicast`, `broadcast`, `unicast` and the Identity API (`register_as` and `notify` methods).
|
90
224
|
|
91
225
|
Here is some demo code for a simple Websocket broadcasting server, where messages sent to the server will be broadcasted back to all the **other** active connections (the connection sending the message will not recieve the broadcast).
|
92
226
|
|
@@ -125,7 +259,7 @@ Remember to connect to the service from at least two browser windows - to truly
|
|
125
259
|
route '/', BroadcastCtrl
|
126
260
|
```
|
127
261
|
|
128
|
-
method names starting with an underscore ('_')
|
262
|
+
method names starting with an underscore ('_') are protected from the Http router, even when they are public.
|
129
263
|
|
130
264
|
This is why even though both '/hello' and '/humans.txt' are public ( [try it](http://localhost:3000/humans.txt) ), '/_send_message' will return a 404 not found error ( [try it](http://localhost:3000/_send_message) ).
|
131
265
|
|
@@ -528,9 +662,15 @@ Whether such goodies are part of the Plezi-App Template (such as rake tasks for
|
|
528
662
|
|
529
663
|
## Plezi Settings
|
530
664
|
|
531
|
-
Plezi
|
665
|
+
Plezi leverages [Iodine's server](https://github.com/boazsegev/iodine) new architecture. Iodine is a pure Ruby HTTP and Websocket Server built using [Iodine's](https://github.com/boazsegev/iodine) core library - a multi-threaded pure ruby alternative to EventMachine with process forking support (enjoy forking, if your code is scaling ready).
|
532
666
|
|
533
|
-
|
667
|
+
Plezi and Iodine are meant to be very effective, allowing for much flexability where needed.
|
668
|
+
|
669
|
+
Settings for the Iodine's core allow you to change different things, such as the level of concurrency you want (`Iodine.threads = ` or `Iodine.processes = `), logging destination (such as logging to a file) and more.
|
670
|
+
|
671
|
+
Settings for Iodine's Http and Websockets server, allow you to change upload limits (which can be super important for security) using `Iodine::Http.max_http_buffer =`, limit websocket message sizes using `Iodine::Http::Websockets.message_size_limit =`, change the Websocket's auto-ping interval using `Iodine::Http::Websockets.default_timeout =` or `Plezi::Settings.ws_message_size_limit` and more... Poke around ;-)
|
672
|
+
|
673
|
+
Plezi and Iodine are written for Ruby versions 2.1.0 or greater (or API compatible variants). Version 2.2.3 is the currently recommended version.
|
534
674
|
|
535
675
|
## Who's afraid of multi-threading?
|
536
676
|
|
@@ -585,6 +725,8 @@ However, th following is unsafe:
|
|
585
725
|
|
586
726
|
## Contributing
|
587
727
|
|
728
|
+
Feel free to fork or contribute. right now I am one person, but together we can make something exciting that will help us enjoy Ruby in this brave new world and (hopefully) set an example that will induce progress in the popular mainstream frameworks such as Rails and Sinatra.
|
729
|
+
|
588
730
|
1. Fork it ( https://github.com/boazsegev/plezi/fork )
|
589
731
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
590
732
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
@@ -15,7 +15,7 @@ module Plezi
|
|
15
15
|
# Returns the Redis Channel Name used by this app.
|
16
16
|
# @return [String]
|
17
17
|
def redis_channel_name
|
18
|
-
@redis_channel_name ||= "#{File.basename($0, '.*')}
|
18
|
+
@redis_channel_name ||= "#{File.basename($0, '.*')}_redis_channel"
|
19
19
|
end
|
20
20
|
|
21
21
|
# Sets the message byte size limit for a Websocket message. Defaults to 0 (no limit)
|
@@ -133,13 +133,13 @@ module Plezi
|
|
133
133
|
Plezi.warn 'existing response body was cleared by `#send_data`!'
|
134
134
|
response.body.close if response.body.respond_to? :close
|
135
135
|
end
|
136
|
-
response = data
|
136
|
+
response.body = data
|
137
137
|
|
138
138
|
# set headers
|
139
139
|
content_disposition = options[:inline] ? 'inline' : 'attachment'
|
140
140
|
content_disposition << "; filename=#{::File.basename(options[:filename])}" if options[:filename]
|
141
141
|
|
142
|
-
response['content-type'] = (options[:type] ||= MimeTypeHelper::MIME_DICTIONARY[::File.extname(options[:filename])])
|
142
|
+
response['content-type'] = (options[:type] ||= options[:filename] && MimeTypeHelper::MIME_DICTIONARY[::File.extname(options[:filename])])
|
143
143
|
response['content-disposition'] = content_disposition
|
144
144
|
true
|
145
145
|
end
|
data/lib/plezi/handlers/route.rb
CHANGED
@@ -20,6 +20,8 @@ module Plezi
|
|
20
20
|
if controller
|
21
21
|
ret = controller.new(request, response)._route_path_to_methods_and_set_the_response_
|
22
22
|
elsif proc
|
23
|
+
# proc.init(request, response)
|
24
|
+
# ret = proc.instance_exec(request, response, &proc)
|
23
25
|
ret = proc.call(request, response)
|
24
26
|
elsif controller == false
|
25
27
|
request.path = path.match(request.path).to_a.last.to_s
|
@@ -54,6 +56,23 @@ module Plezi
|
|
54
56
|
# add controller magic
|
55
57
|
@controller = self.class.make_controller_magic controller, self
|
56
58
|
end
|
59
|
+
if @proc.is_a?(Proc)
|
60
|
+
# # proc's methods aren't executed since it's binding isn't `self`
|
61
|
+
# @proc.instance_exec do
|
62
|
+
# extend ::Plezi::ControllerMagic::InstanceMethods
|
63
|
+
# undef :url_for
|
64
|
+
# undef :full_url_for
|
65
|
+
# undef :requested_method
|
66
|
+
# def run request, response
|
67
|
+
# @request = request
|
68
|
+
# @params = request.params
|
69
|
+
# @flash = response.flash
|
70
|
+
# @host_params = request[:host_settings]
|
71
|
+
# @response = response
|
72
|
+
# @cookies = request.cookies
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
end
|
57
76
|
end
|
58
77
|
|
59
78
|
# # returns the url for THIS route (i.e. `url_for :index`)
|
@@ -4,6 +4,87 @@ module Plezi
|
|
4
4
|
|
5
5
|
module WSObject
|
6
6
|
|
7
|
+
module RedisEmultaion
|
8
|
+
public
|
9
|
+
def lrange key, first, last = -1
|
10
|
+
sync do
|
11
|
+
return [] unless @cache[key]
|
12
|
+
@cache[key][first..last] || []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
def llen key
|
16
|
+
sync do
|
17
|
+
return 0 unless @cache[key]
|
18
|
+
@cache[key].count
|
19
|
+
end
|
20
|
+
end
|
21
|
+
def ltrim key, first, last = -1
|
22
|
+
sync do
|
23
|
+
return "OK".freeze unless @cache[key]
|
24
|
+
@cache[key] = @cache[key][first..last]
|
25
|
+
"OK".freeze
|
26
|
+
end
|
27
|
+
end
|
28
|
+
def del *keys
|
29
|
+
sync do
|
30
|
+
ret = 0
|
31
|
+
keys.each {|k| ret += 1 if @cache.delete k }
|
32
|
+
ret
|
33
|
+
end
|
34
|
+
end
|
35
|
+
def lpush key, value
|
36
|
+
sync do
|
37
|
+
@cache[key] ||= []
|
38
|
+
@cache[key].unshift value
|
39
|
+
@cache[key].count
|
40
|
+
end
|
41
|
+
end
|
42
|
+
def rpush key, value
|
43
|
+
sync do
|
44
|
+
@cache[key] ||= []
|
45
|
+
@cache[key].push value
|
46
|
+
@cache[key].count
|
47
|
+
end
|
48
|
+
end
|
49
|
+
def expire key, seconds
|
50
|
+
Iodine.warn "Identity API requires Redis - no persistent storage!"
|
51
|
+
sync do
|
52
|
+
return 0 unless @cache[key]
|
53
|
+
if @timers[key]
|
54
|
+
@timers[key].stop!
|
55
|
+
end
|
56
|
+
@timers[key] = (Iodine.run_after(seconds) { self.del key })
|
57
|
+
end
|
58
|
+
end
|
59
|
+
def multi
|
60
|
+
sync do
|
61
|
+
@results = []
|
62
|
+
yield(self)
|
63
|
+
ret = @results
|
64
|
+
@results = nil
|
65
|
+
ret
|
66
|
+
end
|
67
|
+
end
|
68
|
+
alias :pipelined :multi
|
69
|
+
protected
|
70
|
+
@locker = Mutex.new
|
71
|
+
@cache = Hash.new
|
72
|
+
@timers = Hash.new
|
73
|
+
|
74
|
+
def sync &block
|
75
|
+
if @locker.locked? && @locker.owned?
|
76
|
+
ret = yield
|
77
|
+
@results << ret if @results
|
78
|
+
ret
|
79
|
+
else
|
80
|
+
@locker.synchronize { sync &block }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
public
|
85
|
+
extend self
|
86
|
+
end
|
87
|
+
|
7
88
|
# the following are additions to the WebSocket Object module,
|
8
89
|
# to establish identity to websocket realtionships, allowing for a
|
9
90
|
# websocket message bank.
|
@@ -16,15 +97,17 @@ module Plezi
|
|
16
97
|
# Like {Plezi::Base::WSObject::SuperClassMethods#notify}, using this method requires an active Redis connection
|
17
98
|
# to be set up. See {Plezi#redis} for more information.
|
18
99
|
#
|
19
|
-
#
|
100
|
+
# By default, only one connection at a time can respond to identity events. If the same identity
|
20
101
|
# connects more than once, only the last connection will receive the notifications.
|
102
|
+
# This default may be controlled by setting the `:max_connections` option to a number greater than 1.
|
21
103
|
#
|
22
104
|
# The method accepts:
|
23
105
|
# identity:: a global application wide unique identifier that will persist throughout all of the identity's connections.
|
24
106
|
# options:: an option's hash that sets the properties of the identity.
|
25
107
|
#
|
26
|
-
# The option's Hash, at the moment, accepts only the following (optional)
|
108
|
+
# The option's Hash, at the moment, accepts only the following (optional) options:
|
27
109
|
# lifetime:: sets how long the identity can survive. defaults to `604_800` seconds (7 days).
|
110
|
+
# max_connections:: sets the amount of concurrent connections an identity can have (akin to open browser tabs receiving notifications). defaults to 1 (a single connection).
|
28
111
|
#
|
29
112
|
# Calling this method will also initiate any events waiting in the identity's queue.
|
30
113
|
# make sure that the method is only called once all other initialization is complete.
|
@@ -32,20 +115,22 @@ module Plezi
|
|
32
115
|
# Do NOT call this method asynchronously unless Plezi is set to run as in a single threaded mode - doing so
|
33
116
|
# will execute any pending events outside the scope of the IO's mutex lock, thus introducing race conditions.
|
34
117
|
def register_as identity, options = {}
|
35
|
-
redis = Plezi.redis
|
36
|
-
|
118
|
+
redis = Plezi.redis || ::Plezi::Base::WSObject::RedisEmultaion
|
119
|
+
options[:max_connections] ||= 1
|
120
|
+
options[:max_connections] = 1 if options[:max_connections].to_i < 1
|
121
|
+
options[:lifetime] ||= 604_800
|
37
122
|
identity = identity.to_s.freeze
|
38
123
|
@___identity ||= [].to_set
|
39
124
|
@___identity << identity
|
40
125
|
redis.pipelined do
|
41
126
|
redis.lpush "#{identity}_uuid".freeze, uuid
|
42
|
-
redis.ltrim "#{identity}_uuid".freeze, 0,
|
127
|
+
redis.ltrim "#{identity}_uuid".freeze, 0, (options[:max_connections]-1)
|
43
128
|
end
|
44
|
-
___review_identity identity
|
45
129
|
redis.lpush(identity, ''.freeze) unless redis.llen(identity) > 0
|
130
|
+
___review_identity identity
|
46
131
|
redis.pipelined do
|
47
|
-
redis.expire identity,
|
48
|
-
redis.expire "#{identity}_uuid".freeze,
|
132
|
+
redis.expire identity, options[:lifetime]
|
133
|
+
redis.expire "#{identity}_uuid".freeze, options[:lifetime]
|
49
134
|
end
|
50
135
|
end
|
51
136
|
|
@@ -63,21 +148,21 @@ module Plezi
|
|
63
148
|
module SuperInstanceMethods
|
64
149
|
protected
|
65
150
|
def ___review_identity identity
|
66
|
-
redis = Plezi.redis
|
67
|
-
raise "unknown Redis initiation error" unless redis
|
151
|
+
redis = Plezi.redis || ::Plezi::Base::WSObject::RedisEmultaion
|
68
152
|
identity = identity.to_s.freeze
|
69
153
|
return Iodine.warn("Identity message reached wrong target (ignored).").clear unless @___identity.include?(identity)
|
70
|
-
redis.multi do
|
71
|
-
redis.
|
72
|
-
redis.
|
73
|
-
end
|
74
|
-
|
75
|
-
|
76
|
-
while (msg = redis.rpop(identity)) && msg != ''.freeze
|
154
|
+
messages = redis.multi do
|
155
|
+
redis.lrange identity, 1, -1
|
156
|
+
redis.ltrim identity, 0, 0
|
157
|
+
end[0]
|
158
|
+
targets = redis.lrange "#{identity}_uuid", 0, -1
|
159
|
+
while msg = messages.shift
|
77
160
|
msg = ::Plezi::Base::WSObject.translate_message(msg)
|
78
161
|
next unless msg
|
79
162
|
Iodine.error("Notification recieved but no method can handle it - dump:\r\n #{msg.to_s}") && next unless self.class.has_super_method?(msg[:method])
|
80
|
-
self.method(msg[:method]).call *msg[:data]
|
163
|
+
# targets.each {|target| target == uuid ? self.method(msg[:method]).call(*msg[:data]) : unicast(target, msg[:method], *msg[:data])}
|
164
|
+
targets.each {|target| unicast(target, msg[:method], *msg[:data])} # this allows for async execution
|
165
|
+
|
81
166
|
end
|
82
167
|
end
|
83
168
|
end
|
@@ -87,20 +172,17 @@ module Plezi
|
|
87
172
|
|
88
173
|
# sends a notification to an Identity. Returns false if the Identity never registered or it's registration expired.
|
89
174
|
def notify identity, event_name, *args
|
90
|
-
redis = Plezi.redis
|
91
|
-
raise "The identity API requires a Redis connection" unless redis
|
175
|
+
redis = Plezi.redis || ::Plezi::Base::WSObject::RedisEmultaion
|
92
176
|
identity = identity.to_s.freeze
|
93
177
|
return false unless redis.llen(identity).to_i > 0
|
94
|
-
redis.
|
95
|
-
|
96
|
-
unicast target_uuid, :___review_identity, identity if target_uuid
|
178
|
+
redis.rpush identity, ({method: event_name, data: args}).to_yaml
|
179
|
+
redis.lrange("#{identity}_uuid".freeze, 0, -1).each {|target| unicast target, :___review_identity, identity }
|
97
180
|
true
|
98
181
|
end
|
99
182
|
|
100
183
|
# returns true if the Identity in question is registered to receive notifications.
|
101
184
|
def registered? identity
|
102
|
-
redis = Plezi.redis
|
103
|
-
return Iodine.warn("Cannot check for Identity registration without a Redis connection (silent).") && false unless redis
|
185
|
+
redis = Plezi.redis || ::Plezi::Base::WSObject::RedisEmultaion
|
104
186
|
identity = identity.to_s.freeze
|
105
187
|
redis.llen(identity).to_i > 0
|
106
188
|
end
|
data/lib/plezi/version.rb
CHANGED
data/plezi.gemspec
CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency "iodine", "~> 0.1.
|
21
|
+
spec.add_dependency "iodine", "~> 0.1.10"
|
22
22
|
spec.add_development_dependency "bundler", "~> 1.7"
|
23
23
|
spec.add_development_dependency "rake", "~> 10.0"
|
24
24
|
|
data/resources/mini_app.rb
CHANGED
@@ -5,32 +5,29 @@
|
|
5
5
|
require 'pathname'
|
6
6
|
## Set up root object, it might be used by the environment and\or the plezi extension gems.
|
7
7
|
Root ||= Pathname.new(File.dirname(__FILE__)).expand_path
|
8
|
-
|
9
8
|
## If this app is independant, use bundler to load gems (including the plezi gem).
|
10
|
-
## otherwise, use the original app's Gemfile and
|
9
|
+
## otherwise, use the original app's Gemfile and Plezi will automatically switch to Rack mode.
|
11
10
|
require 'bundler'
|
12
11
|
Bundler.require(:default, ENV['ENV'].to_s.to_sym)
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
# # Optional code auto-loading and logging:
|
14
|
+
|
15
|
+
# # Load code from a subfolder called 'app'?
|
17
16
|
# Dir[File.join "{app}", "**" , "*.rb"].each {|file| load File.expand_path(file)}
|
18
|
-
## OR load code from all the ruby files in the main forlder (subfolder inclussion will fail on PaaS)
|
19
|
-
# Dir[File.join File.dirname(__FILE__), "*.rb"].each {|file| load File.expand_path(file) unless file == __FILE__}
|
20
17
|
|
21
|
-
##
|
22
|
-
# Iodine.logger = Logger.new
|
18
|
+
## Log to a file?
|
19
|
+
# Iodine.logger = Logger.new Root.join('server.log').to_s
|
20
|
+
|
21
|
+
# # Optional Scaling (across processes or machines):
|
22
|
+
ENV['PL_REDIS_URL'] ||= ENV['REDIS_URL'] ||
|
23
|
+
ENV['REDISCLOUD_URL'] ||
|
24
|
+
ENV['REDISTOGO_URL'] ||
|
25
|
+
nil # "redis://username:password@my.host:6389"
|
26
|
+
# # redis channel name should be changed is using Placebo API
|
27
|
+
# Plezi::Settings.redis_channel_name = 'appsecret'
|
23
28
|
|
24
|
-
# # Options for Scaling the app (across processes or machines):
|
25
29
|
# # uncomment to set up forking for 3 more processes (total of 4).
|
26
30
|
# Iodine.processes = 4
|
27
|
-
#
|
28
|
-
# # Redis scaling
|
29
|
-
# Plezi::Settings.redis_channel_name = 'appsecret'
|
30
|
-
# ENV['PL_REDIS_URL'] ||= ENV['REDIS_URL'] || ENV['REDISCLOUD_URL'] || ENV['REDISTOGO_URL'] || "redis://username:password@my.host:6389"
|
31
|
-
#
|
32
|
-
# # Consider setting a common session token for Redis supported sessions.
|
33
|
-
# Iodine::Http.session_token = 'appname_uui'
|
34
31
|
|
35
32
|
|
36
33
|
# The basic appname controller, to get you started
|
@@ -48,10 +45,11 @@ class MyController
|
|
48
45
|
end
|
49
46
|
def on_open
|
50
47
|
print 'Welcome!'
|
51
|
-
|
48
|
+
@handle = params[:id] || 'Somebody'
|
49
|
+
broadcast :print, "#{@handle} joind us :-)"
|
52
50
|
end
|
53
51
|
def on_close
|
54
|
-
broadcast :print, "
|
52
|
+
broadcast :print, "#{@handle} left us :-("
|
55
53
|
end
|
56
54
|
|
57
55
|
protected
|
@@ -63,23 +61,21 @@ end
|
|
63
61
|
|
64
62
|
|
65
63
|
# change some of the default settings here.
|
66
|
-
host
|
64
|
+
host templates: Root.join('templates').to_s,
|
67
65
|
# public: Root.join('public').to_s,
|
68
|
-
assets: Root.join('assets').to_s
|
69
|
-
|
70
|
-
|
71
|
-
# #
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
# # This callback has access to the controller's methods (request, cookies, response, etc')
|
82
|
-
# end
|
66
|
+
assets: Root.join('assets').to_s
|
67
|
+
|
68
|
+
# # I18n re-write, i.e.: `/en/home` will be rewriten as `/home`, while setting params[:locale] to "en"
|
69
|
+
# route "/:locale{#{I18n.available_locales.join "|"}}/*" , false if defined? I18n
|
70
|
+
|
71
|
+
# # OAuth2 - Facebook / Google authentication
|
72
|
+
# ENV["FB_APP_ID"] ||= "app id"; ENV["FB_APP_SECRET"] ||= "secret"; ENV['GOOGLE_APP_ID'] = "app id"; ENV['GOOGLE_APP_SECRET'] = "secret"
|
73
|
+
# require 'plezi/oauth' # do this AFTER setting ENV variables.
|
74
|
+
# create_auth_shared_route do |service_name, auth_token, remote_user_id, remote_user_email, remote_response|
|
75
|
+
# # ...callback for authentication.
|
76
|
+
# # This callback should return the app user object or false
|
77
|
+
# # This callback has access to the controller's methods (request, cookies, response, etc')
|
78
|
+
# end
|
83
79
|
|
84
80
|
# Add your routes and controllers by order of priority.
|
85
81
|
route '/(:id)', MyController
|
@@ -207,7 +207,10 @@ function init_websocket()
|
|
207
207
|
document.getElementById("output").appendChild(msg);
|
208
208
|
};
|
209
209
|
// you probably want to reopen the websocket if it closes (unless the issue repeats).
|
210
|
-
if(websocket_fail <= 5) {
|
210
|
+
if(websocket_fail <= 5) {
|
211
|
+
websocket_fail += 1;
|
212
|
+
setTimeout( init_websocket, 250);
|
213
|
+
};
|
211
214
|
};
|
212
215
|
websocket.onerror = function(e) {
|
213
216
|
// what do you want to do now?
|
@@ -249,7 +252,7 @@ function send_text()
|
|
249
252
|
<ul>
|
250
253
|
<li>Review and update MyController in your 'appname.rb'.</li>
|
251
254
|
<li>Edit or delete this file (appname/templates/welcome.html.erb).</li>
|
252
|
-
<li>Write your own controller and code, maybe using a sub-
|
255
|
+
<li>Write your own controller and code, maybe using a sub-folder as suggested in the 'appname.rb' file.</li>
|
253
256
|
<li>Set up your routes in the 'appname.rb' file.</li>
|
254
257
|
<li>Edit the javascript for the client in your 'appname/websockets.js' file and link to it from your html.</li>
|
255
258
|
</ul>
|
data/resources/rakefile
CHANGED
data/resources/redis_config.rb
CHANGED
@@ -2,26 +2,26 @@
|
|
2
2
|
|
3
3
|
if defined? Redis
|
4
4
|
|
5
|
-
Plezi::Settings.redis_channel_name = 'appsecret'
|
6
|
-
|
7
5
|
# ## Plezi Redis Automation
|
8
6
|
# ## ====
|
9
7
|
# ##
|
10
8
|
# ## Sets up Plezi to use Radis broadcast.
|
11
9
|
# ##
|
12
10
|
# ## If Plezi Redis Automation is enabled:
|
13
|
-
# ## Plezi creates is own listening thread that listens for
|
14
|
-
# ## (using the Controller.redis_connection and Controller.redis_channel_name class methods)
|
11
|
+
# ## Plezi creates is own listening thread that listens for messages and broadcasts using Redis.
|
15
12
|
# ##
|
16
13
|
# ## Only one thread will be created and initiated during startup (dynamically created controller routes might be ignored).
|
17
14
|
# ##
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
21
|
-
|
15
|
+
# `redis_channel_name` should be set when using the Placebo API.
|
16
|
+
# (otherwise, it's only optional, as the automatic settings are good enough)
|
17
|
+
Plezi::Settings.redis_channel_name = 'appsecret'
|
18
|
+
ENV['PL_REDIS_URL'] ||= ENV['REDIS_URL'] ||
|
19
|
+
ENV['REDISCLOUD_URL'] ||
|
20
|
+
ENV['REDISTOGO_URL'] ||
|
21
|
+
nil # use: "redis://username:password@my.host:6389"
|
22
22
|
|
23
23
|
|
24
|
-
# ## OR, write your own custom Redis
|
24
|
+
# ## OR, write your own custom Redis implementation here
|
25
25
|
# ## ====
|
26
26
|
# ##
|
27
27
|
# ## create a listening thread - rewrite the following code for your own Redis tailored solution.
|
data/resources/welcome_page.html
CHANGED
@@ -207,7 +207,10 @@ function init_websocket()
|
|
207
207
|
document.getElementById("output").appendChild(msg);
|
208
208
|
};
|
209
209
|
// you probably want to reopen the websocket if it closes (unless the issue repeats).
|
210
|
-
if(websocket_fail <= 5) {
|
210
|
+
if(websocket_fail <= 5) {
|
211
|
+
websocket_fail += 1;
|
212
|
+
setTimeout( init_websocket, 250);
|
213
|
+
};
|
211
214
|
};
|
212
215
|
websocket.onerror = function(e) {
|
213
216
|
// what do you want to do now?
|
data/test/plezi_tests.rb
CHANGED
@@ -160,27 +160,88 @@ class WSIdentity
|
|
160
160
|
"identity api testing path\n#{params}"
|
161
161
|
end
|
162
162
|
def show
|
163
|
-
if
|
164
|
-
|
163
|
+
if params[:message]
|
164
|
+
if notify params[:id], :notification, params[:message]
|
165
|
+
"Sent notification for #{params[:id]}: #{params[:message]}"
|
166
|
+
else
|
167
|
+
"The identity requested (#{params[:id]}) doesn't exist."
|
168
|
+
end
|
165
169
|
else
|
166
|
-
|
170
|
+
%{<html><head><script>
|
171
|
+
// Your websocket URI should be an absolute path. The following sets the base URI.
|
172
|
+
// remember to update to the specific controller's path to your websocket URI.
|
173
|
+
var ws_controller_path = window.location.pathname;
|
174
|
+
var ws_uri = (window.location.protocol.match(/https/) ? 'wss' : 'ws') + '://' + window.document.location.host + ws_controller_path
|
175
|
+
var websocket = 100
|
176
|
+
var websocket_fail_count = 0
|
177
|
+
|
178
|
+
function init_websocket()
|
179
|
+
{
|
180
|
+
if(websocket && websocket.readyState == 1) return true; // console.log('no need to renew socket connection');
|
181
|
+
websocket = new WebSocket(ws_uri);
|
182
|
+
websocket.onopen = function(e) {
|
183
|
+
//restart fail count
|
184
|
+
websocket_fail = 0
|
185
|
+
// what do you want to do now?
|
186
|
+
var msg = document.createElement("li");
|
187
|
+
msg.className = 'system_message'
|
188
|
+
msg.innerHTML = "Connected.";
|
189
|
+
document.getElementById("output").appendChild(msg);
|
190
|
+
};
|
191
|
+
|
192
|
+
websocket.onclose = function(e) {
|
193
|
+
// what do you want to do now?
|
194
|
+
if(websocket_fail == 0) {
|
195
|
+
var msg = document.createElement("li");
|
196
|
+
msg.className = 'system_message'
|
197
|
+
msg.innerHTML = "Disconnected.";
|
198
|
+
document.getElementById("output").appendChild(msg);
|
199
|
+
};
|
200
|
+
// you probably want to reopen the websocket if it closes (unless the issue repeats).
|
201
|
+
if(websocket_fail <= 5) {websocket_fail += 1; init_websocket(); };
|
202
|
+
};
|
203
|
+
websocket.onerror = function(e) {
|
204
|
+
// what do you want to do now?
|
205
|
+
};
|
206
|
+
websocket.onmessage = function(e) {
|
207
|
+
// what do you want to do now?
|
208
|
+
console.log(e.data);
|
209
|
+
var msg = document.createElement("li");
|
210
|
+
msg.innerHTML = e.data;
|
211
|
+
document.getElementById("output").appendChild(msg);
|
212
|
+
// to use JSON, use:
|
213
|
+
// msg = JSON.parse(e.data); // remember to use JSON also in your Plezi controller.
|
214
|
+
};
|
215
|
+
}
|
216
|
+
window.addEventListener("load", init_websocket, false);
|
217
|
+
</script></head>
|
218
|
+
<body>
|
219
|
+
<h3>You are now #{params[:id]}</h3>
|
220
|
+
<ul id='output'>
|
221
|
+
</ul>
|
222
|
+
</body></html>}
|
167
223
|
end
|
168
224
|
end
|
169
225
|
def pre_connect
|
170
226
|
params[:id] && true
|
171
227
|
end
|
172
228
|
def on_open
|
173
|
-
|
229
|
+
@id_count = self.class.counter
|
230
|
+
register_as params[:id], max_connections: 3, lifetime: 120
|
174
231
|
end
|
175
232
|
def on_message data
|
176
|
-
puts "
|
233
|
+
puts "websocket message (for identity #{@id_count}) : #{data}"
|
177
234
|
end
|
178
235
|
|
179
236
|
protected
|
180
237
|
|
181
238
|
def notification message
|
182
239
|
write message
|
183
|
-
puts "Identity Got: #{message}"
|
240
|
+
puts "Identity #{@id_count} Got: #{message}"
|
241
|
+
end
|
242
|
+
def self.counter
|
243
|
+
@count ||= 0
|
244
|
+
@count += 1
|
184
245
|
end
|
185
246
|
|
186
247
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: plezi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.12.
|
4
|
+
version: 0.12.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Boaz Segev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-11-
|
11
|
+
date: 2015-11-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: iodine
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.1.
|
19
|
+
version: 0.1.10
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.1.
|
26
|
+
version: 0.1.10
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|