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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fd6066ad34f74ff29d617a7e1a535b07dfc0608c
4
- data.tar.gz: 16ee892e498e466a8b11dabc484f053729b11f81
3
+ metadata.gz: d91b4b4d0ac176a59cea5da3a27855993b556116
4
+ data.tar.gz: e441b5f902541c72c17ae1be1c86ac3597f9c111
5
5
  SHA512:
6
- metadata.gz: e7f514c042d6c7e20c54948b3343765a68a71c2485155de4e14dbb7f2337daf5990a877e37b8e1c0aa65e609d4bfa4859cff221a9e1cdd962df2ee93352d16e5
7
- data.tar.gz: 18e245d1ba234960b72ba5ab5a65ecb05843a96097fb30d4480884d1532f79b84ca16bced7f650898fbe2f6b9c8e474d64a823e739b2286f5cc621f0a3693de6
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. Add Websocket services and RESTful HTTP Streaming to your existing Web-App, (Rails/Sinatra or any other Rack based Ruby app).
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. Create an easily scalable backend for your SPA.
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 a full fledged Ruby web application, taking full advantage of RESTful routing, HTTP streaming and scalable Websocket features.
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
- to create a new barebones app using the Plezi framework, run from terminal:
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 you have a ready to use basic web server (with some demo code, such as a websocket chatroom).
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
- If you're on MacOS or linux you can simply double click the `appname` script file in the `appname` folder. Or, from the terminal, you can type:
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
- now go, in your browser, to: [http://localhost:3000/](http://localhost:3000/)
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
- the default port for the app is 3000. you can set the port to listen to by using the `-p ` option (make sure you have permissions for the requested port):
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
- $ ./appname -p 80
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
- you now have a smart framework app that will happily assimilate any gem you feed it. it responds extra well to Haml, Sass and Coffee-Script, which you can enable in it's Gemfile.
99
+ ### Can websockets do that?!
56
100
 
57
- ## Plezi Controller classes
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 while 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).
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 `unicast` methods.
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 ('_') will NOT be made public by the router.
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 is meant to be very flexible. please take a look at the Plezi Module for settings you might want to play with (max_threads, idle_sleep, create_logger) or any monkey patching you might enjoy.
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
- 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.
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, '.*')}_Redis_Channel"
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
@@ -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
- # Only one connection at a time can respond to identity events. If the same identity
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) option:
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
- raise "The identity API requires a Redis connection" unless redis
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, 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, (options[:lifetime] || 604_800)
48
- redis.expire "#{identity}_uuid".freeze, (options[:lifetime] || 604_800)
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.lpush identity, ''.freeze
72
- redis.lpush identity, ''.freeze
73
- end
74
- msg = redis.rpop(identity)
75
- Iodine.error "Unknown Identity Queue error - both messages and identity might be lost!\nExpected no data, but got: #{msg}" unless msg == ''.freeze
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.lpush identity, ({method: event_name, data: args}).to_yaml
95
- target_uuid = redis.lindex "#{identity}_uuid".freeze, 0
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
@@ -1,3 +1,3 @@
1
1
  module Plezi
2
- VERSION = "0.12.6"
2
+ VERSION = "0.12.7"
3
3
  end
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.9"
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
 
@@ -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 start Plezi's Rack mode.
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
- ## make sure all file access and file loading is relative to the application's root folder
15
- # Dir.chdir Root.to_s
16
- ## load code from a subfolder called 'app'
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
- ## Uncomment to create a log file
22
- # Iodine.logger = Logger.new File.expand_path(Root.join('server.log').to_s)
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
- broadcast :print, "Somebody joind us :-)"
48
+ @handle = params[:id] || 'Somebody'
49
+ broadcast :print, "#{@handle} joind us :-)"
52
50
  end
53
51
  def on_close
54
- broadcast :print, "Somebody left us :-("
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 host: :default,
64
+ host templates: Root.join('templates').to_s,
67
65
  # public: Root.join('public').to_s,
68
- assets: Root.join('assets').to_s,
69
- templates: Root.join('templates').to_s
70
-
71
- # # Optional stuff:
72
- # # I18n re-write, i.e.: `/en/home` will be rewriten as `/home`, while setting params[:locale] to "en"
73
- # route "/:locale{#{I18n.available_locales.join "|"}}/*" , false if defined? I18n
74
- #
75
- # # OAuth2 - Facebook / Google authentication
76
- # ENV["FB_APP_ID"] ||= "app id"; ENV["FB_APP_SECRET"] ||= "secret"; ENV['GOOGLE_APP_ID'] = "app id"; ENV['GOOGLE_APP_SECRET'] = "secret"
77
- # require 'plezi/oauth' # do this AFTER setting ENV variables.
78
- # create_auth_shared_route do |service_name, auth_token, remote_user_id, remote_user_email, remote_response|
79
- # # ...callback for authentication.
80
- # # This callback should return the app user object or false
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) {websocket_fail += 1; init_websocket(); };
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-foler as suggested in the 'appname.rb' file.</li>
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
@@ -1,4 +1,4 @@
1
- # load all framework and gems
1
+ # load all application code and gems
2
2
  load ::File.expand_path(File.join("..", "initialize.rb"), __FILE__)
3
3
 
4
4
  # Make sure the server doesn't start
@@ -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 each Controller class that broadcasts using Redis.
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
- # ## this overrides the default Controller#broadcast method which is very powerful but
19
- # ## is limited to one process.
20
- # ##
21
- # ENV['PL_REDIS_URL'] ||= ENV['REDIS_URL'] || ENV['REDISCLOUD_URL'] || ENV['REDISTOGO_URL'] || "redis://username:password@my.host:6389"
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 Automation here
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.
@@ -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) {websocket_fail += 1; init_websocket(); };
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 notify params[:id], :notification, (params[:message] || 'no message')
164
- "Send notification for #{params[:id]}: #{(params[:message] || 'no message')}"
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
- "The identity requested (#{params[:id]}) doesn't exist."
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
- register_as params[:id]
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 "Got websocket message: #{data}"
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.6
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-02 00:00:00.000000000 Z
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.9
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.9
26
+ version: 0.1.10
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement