plezi 0.12.2 → 0.12.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +56 -4
- data/docs/controllers.md +27 -0
- data/docs/routes.md +30 -30
- data/docs/websockets.md +200 -1
- data/lib/plezi.rb +6 -11
- data/lib/plezi/common/cache.rb +8 -2
- data/lib/plezi/common/redis.rb +1 -14
- data/lib/plezi/handlers/controller_core.rb +0 -12
- data/lib/plezi/handlers/controller_magic.rb +1 -1
- data/lib/plezi/handlers/placebo.rb +2 -2
- data/lib/plezi/handlers/route.rb +3 -3
- data/lib/plezi/handlers/session.rb +14 -0
- data/lib/plezi/handlers/ws_identity.rb +110 -0
- data/lib/plezi/handlers/ws_object.rb +85 -28
- data/lib/plezi/helpers/magic_helpers.rb +7 -0
- data/lib/plezi/version.rb +1 -1
- data/plezi.gemspec +1 -1
- data/resources/websockets.js +1 -1
- data/test/plezi_tests.rb +35 -3
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 083a5d3e4fe51a9892e41c1b4271ccb84b40ccbd
|
4
|
+
data.tar.gz: fc65f387b0eb8c3828a318d6c2029097f726ca5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae792a668041bb5e91b1881f5f0b19374a6387ffed4218e07433eef65b307ac94ad314e39b4db75ae75f625132492153710cc4e80c4a305fae15b65f8900337c
|
7
|
+
data.tar.gz: 29f30a7734ae412e10dc1256e277db04d3f7d66e66ef70116bd69cde2f2d78e03134042b5df1aee695f1e7d6b1d7a4393ddfd4cb5357c64fdb15e8397c20afed
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,32 @@
|
|
2
2
|
|
3
3
|
***
|
4
4
|
|
5
|
+
Change log v.0.12.3
|
6
|
+
|
7
|
+
**Feature**: (requires Redis) Identity API is here (read more on the [Websockets guide](./websockets.md))
|
8
|
+
|
9
|
+
* Websocket Identity API allows you to link a websocket connection with a unique "identity" (i.e., `user.id` or even `session.id`).
|
10
|
+
|
11
|
+
This is called "registering", as the identity "registers" and is henceforth recognized.
|
12
|
+
|
13
|
+
* Notifications sent to the identidy will persist until the identity's "lifetime" expires.
|
14
|
+
|
15
|
+
The default "lifetime" is 7 days, meaning an "Identity" message queue will survive for 7 days since the last time the Identity was "registered". This lifetime can be set for each identity during registration.
|
16
|
+
|
17
|
+
* This allows you to send notifications that will "wait" until a user or visitor reconnects and registers the new connection under their Identity.
|
18
|
+
|
19
|
+
* This is an alternative to persistant storage, where either visitors messages that aren't read within a certain timespan
|
20
|
+
|
21
|
+
**Fix**: the Placebo API was fixed to correspond with the changes in Iodine's API.
|
22
|
+
|
23
|
+
**Fix**: fixed an issue where Placebo's on_close would through an exception.
|
24
|
+
|
25
|
+
**Fix**: Some websocket API methods were exposed to the Http router as paths (causing internal 500 errors, as they couldn't be invoked by the router). These methods are now `protected` and by doing so the Http router ignores them. Also, `has_exposed_method?` was reviewed in a way that should help avoid future occurrences of these issues.
|
26
|
+
|
27
|
+
**Fix**: fixed an issue where AJAX parameters weren't form-decoded (`'%20'` wasn't replaced with `' '` etc'). Now the parameters are decoded as expected.
|
28
|
+
|
29
|
+
***
|
30
|
+
|
5
31
|
Change log v.0.12.2
|
6
32
|
|
7
33
|
**Update**: Plezi now leverages Iodine's support for a File response body, allowing for a smaller memory footpring when sending large files.
|
data/README.md
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
# [Plezi](https://github.com/boazsegev/plezi), The Ruby framework for realtime web-apps
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/plezi.svg)](http://badge.fury.io/rb/plezi)
|
3
|
+
[![Inline docs](http://inch-ci.org/github/boazsegev/plezi.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/plezi/master)
|
4
|
+
[![GitHub](https://img.shields.io/badge/GitHub-Open%20Source-blue.svg)](https://github.com/boazsegev/plezi)
|
2
5
|
|
3
6
|
Plezi is an easy to use Ruby Websocket Framework, with full RESTful routing support and HTTP streaming support. It's name comes from the word "fun", or "pleasure", since Plezi is a pleasure to work with.
|
4
7
|
|
@@ -14,9 +17,7 @@ Plezi leverages [Iodine's server](https://github.com/boazsegev/iodine) new archi
|
|
14
17
|
|
15
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.
|
16
19
|
|
17
|
-
|
18
|
-
[![Gem Version](https://badge.fury.io/rb/plezi.svg)](http://badge.fury.io/rb/plezi)
|
19
|
-
[![Inline docs](http://inch-ci.org/github/boazsegev/plezi.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/plezi/master)
|
20
|
+
**Plezi version notice**
|
20
21
|
|
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.
|
22
23
|
|
@@ -322,7 +323,7 @@ now visit:
|
|
322
323
|
* [http://localhost:3000/post/12/1.3/1](http://localhost:3000/post/12/1.3/1)
|
323
324
|
* [http://localhost:3000/post/12/1](http://localhost:3000/post/12/1)
|
324
325
|
|
325
|
-
**please see the `route` documentation for more information on routes**.
|
326
|
+
**[please see the `route` documentation for more information on routes](./docs/routes.md)**.
|
326
327
|
|
327
328
|
## Plezi Virtual Hosts
|
328
329
|
|
@@ -531,6 +532,57 @@ Plezi is meant to be very flexible. please take a look at the Plezi Module for s
|
|
531
532
|
|
532
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.
|
533
534
|
|
535
|
+
## Who's afraid of multi-threading?
|
536
|
+
|
537
|
+
Plezi builds on Iodine's concept of "connection locking", meaning that your controllers shouldn't be acessed by more than one thread at the same time.
|
538
|
+
|
539
|
+
This allows you to run Plezi as a multi-threaded (and even multi-process) application as long as your controllers don't change or set any global data... Readeing global data after it was set during initialization is totally fine, just not changing or setting it...
|
540
|
+
|
541
|
+
But wait, global data is super important, right?
|
542
|
+
|
543
|
+
Well, sometimes it is. And although it's a better practice to avoide storing any global data in global variables, sometimes storing stuff in the global space is exactly what we need.
|
544
|
+
|
545
|
+
The solution is simple - if you can't use persistent databases with thread-safe libraries (i.e. Sequel / ActiveRecord / Redis, etc'), use Plezi's global cache storage (see Plezi::Cache).
|
546
|
+
|
547
|
+
Plezi's global cache storage is a memory based storage protected by a mutex for any reading or writing from the cache.
|
548
|
+
|
549
|
+
So... these are protected:
|
550
|
+
|
551
|
+
# set data
|
552
|
+
Plezi.cache_data :my_global_variable, 32
|
553
|
+
# get data
|
554
|
+
Plezi.get_cached :my_global_variable # => 32
|
555
|
+
|
556
|
+
However, although Ruby seems innocent, it's super powerful when it comes to using pointers and references behind the scenes. This could allow you to change a protected object in an unprotected way... consider this:
|
557
|
+
|
558
|
+
a = []
|
559
|
+
b = a
|
560
|
+
b << '1'
|
561
|
+
# we changed `a` without noticing
|
562
|
+
a # => [1]
|
563
|
+
|
564
|
+
For this reason, it's important that Strings, Arrays and Hashes will be protected if they are to be manipulated in any way.
|
565
|
+
|
566
|
+
The following is safe:
|
567
|
+
|
568
|
+
# set data
|
569
|
+
Plezi.cache_data :global_hash, Hash.new
|
570
|
+
# manipulate data
|
571
|
+
Plezi.get_cached :global_hash do |global_hash|
|
572
|
+
global_hash[:change] = "safe"
|
573
|
+
end
|
574
|
+
|
575
|
+
However, th following is unsafe:
|
576
|
+
|
577
|
+
# set data
|
578
|
+
Plezi.cache_data :global_hash, Hash.new
|
579
|
+
# manipulate data
|
580
|
+
global_hash = Plezi.get_cached :global_hash do |global_hash|
|
581
|
+
global_hash[:change] = "NOT safe"
|
582
|
+
|
583
|
+
|
584
|
+
\* be aware, if using Plezi in as a multi-process application, that each process has it's own cache and that processes can't share the cache. The different threads in each of the processes will be able to acess their process's cache, but each process runs in a different memory space, so they can't share.
|
585
|
+
|
534
586
|
## Contributing
|
535
587
|
|
536
588
|
1. Fork it ( https://github.com/boazsegev/plezi/fork )
|
data/docs/controllers.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Plezi Controllers - Virtual folders, RESTful requests, Websockets and more.
|
2
|
+
|
3
|
+
In the core of Plezi's framework is a smart Object Oriented Router which acts like a "virtual folder" with RESTful routing and Websocket support.
|
4
|
+
|
5
|
+
RESTful routing and Websocket callback support both allow us to use conventionally named methods in our Controller to achive common tasks. Such names methods, as will be explored further on, include the `update`, `save` and `show` RESTful method names, as well as the `on_open`, `on_message(data)` and `on_close` Websocket callbacks.
|
6
|
+
|
7
|
+
The first layer of this powerful routing system is [the Plezi's Http Router and the core method `Plezi.route` which we already explored](./routes.md).
|
8
|
+
|
9
|
+
The second layer of this powerful routing system is the Controller class which we are about explore.
|
10
|
+
|
11
|
+
## What is a Controller Class?
|
12
|
+
|
13
|
+
(todo: write documentation)
|
14
|
+
|
15
|
+
### A Virtual Folder
|
16
|
+
|
17
|
+
(todo: write documentation)
|
18
|
+
|
19
|
+
### RESTful methods
|
20
|
+
|
21
|
+
(todo: write documentation)
|
22
|
+
|
23
|
+
### Websocket Callbacks
|
24
|
+
|
25
|
+
(todo: write documentation)
|
26
|
+
|
27
|
+
|
data/docs/routes.md
CHANGED
@@ -4,15 +4,19 @@ In the core of Plezi's framework is a smart Object Oriented Router which acts li
|
|
4
4
|
|
5
5
|
RESTful routing and Websocket callback support both allow us to use conventionally named methods in our Controller to achive common tasks. Such names methods, as will be explored further on, include the `update`, `save` and `show` RESTful method names, as well as the `on_open`, `on_message(data)` and `on_close` Websocket callbacks.
|
6
6
|
|
7
|
+
The first layer of this powerful routing system is the Plezi's Http Router and the core method `Plezi.route`.
|
8
|
+
|
7
9
|
## What is a Route?
|
8
10
|
|
9
11
|
Routes are what connects different URLs to different parts of our code.
|
10
12
|
|
11
|
-
We when we visit `www.example.com/users/index` we expect a different page than when we go to `www.example.com/users/1`. This is because we expect the first URL to provide a page with the list of users while we expect the second URL to show us
|
13
|
+
We when we visit `www.example.com/users/index` we expect a different page than when we go to `www.example.com/users/1`. This is because we expect the first URL to provide a page with the list of users while we expect the second URL to show us a specific user's page or data.
|
14
|
+
|
15
|
+
in the example above, all the requests are made to the server at `www.example.com` and it is the server's inner workings - the server's inner router - that directs the `/users/index` to one part of our code and `/users/1` to another.
|
12
16
|
|
13
|
-
|
17
|
+
Like all web applications, Plezi also has an inner router which routes each request to the corresponding method or code.
|
14
18
|
|
15
|
-
Plezi
|
19
|
+
Plezi's routing system was designed to build upon conventions used in other routing systems together with an intuitive approach that allows for agail application development.
|
16
20
|
|
17
21
|
\* It should be noted that except for file handling and the asset pipeline - which are file-system dependent - routes are case-sensitive.
|
18
22
|
|
@@ -24,7 +28,7 @@ This method accept a String, or Regexp, that should point to a "group" of routes
|
|
24
28
|
|
25
29
|
The method also requires either a class that "controls" that group of routes or a block of code that responds to that route.
|
26
30
|
|
27
|
-
Here are a
|
31
|
+
Here are a few examples for valid routes. You can run the following script in the `irb` terminal:
|
28
32
|
|
29
33
|
```ruby
|
30
34
|
require 'plezi'
|
@@ -39,18 +43,20 @@ end
|
|
39
43
|
# this is because that's all that UsersController defines.
|
40
44
|
Plezi.route "/users", UsersController
|
41
45
|
|
42
|
-
# this route isn't grouped under a controller.
|
46
|
+
# this route isn't grouped under a controller.
|
47
|
+
# it will answer to "/people" and "/people/something" with the same response.
|
43
48
|
Plezi.route("/people") { "People" }
|
44
49
|
|
45
|
-
# this route includes a catch-all at the end and will
|
50
|
+
# this route includes a catch-all at the end and will
|
51
|
+
# catch anything that starts with "/stuff/"
|
46
52
|
Plezi.route("/stuff/*") { "More and more stuff" }
|
47
53
|
|
48
|
-
# this is catch-all
|
54
|
+
# this is catch-all that answers any requests not yet answered.
|
49
55
|
Plezi.route("*") { "Lost?" }
|
50
56
|
|
51
57
|
# this route will never be seen,
|
52
58
|
# because the catch-all route answers any request before it gets here.
|
53
|
-
Plezi.route("/never-seen") { "You
|
59
|
+
Plezi.route("/never-seen") { "You can't see me..." }
|
54
60
|
|
55
61
|
|
56
62
|
exit
|
@@ -99,13 +105,14 @@ class UsersController
|
|
99
105
|
end
|
100
106
|
end
|
101
107
|
|
102
|
-
# try visiting "/users/1/John"
|
103
108
|
route "/users/(:id)/(:name)", UsersController
|
104
109
|
|
105
110
|
exit
|
106
111
|
```
|
107
112
|
|
108
|
-
|
113
|
+
* now visit [/users/1/John](http://localhost:3000/users/1/John)
|
114
|
+
|
115
|
+
As you noticed, providing an `:id` parameter invoked the RESTful method `show`. This is only one possible outcome. We will discuss this more [when we look at the Controller](./controllers.md) being used as a virtual folder and as we discuss about RESTful method.
|
109
116
|
|
110
117
|
### More inline parameters
|
111
118
|
|
@@ -118,7 +125,7 @@ Inline parameters come in more flavors:
|
|
118
125
|
|
119
126
|
Using inline parameters, it's possible to achive great flexability with the same route, allowing our code to be better organized. This is especially helpful when expecting data to be received using AJAX or when creating an accessible API for native apps to utilize.
|
120
127
|
|
121
|
-
It should be noted, that since a parameter matches against the whole of the pattern, parenthesis shouldn't be used and could
|
128
|
+
It should be noted, when using parameters matching a specific pattern, that since a parameter matches against the whole of the pattern, parenthesis within the shouldn't be used and could lead to parsing errors.
|
122
129
|
|
123
130
|
### Re-Write Routes
|
124
131
|
|
@@ -165,15 +172,22 @@ Plezi.route "/users", UsersController
|
|
165
172
|
exit
|
166
173
|
```
|
167
174
|
|
168
|
-
|
175
|
+
Try the code above and visit:
|
176
|
+
|
177
|
+
* [localhost:3000/fr/users](http://localhost:3000/fr/users)
|
178
|
+
* [localhost:3000/ru/users](http://localhost:3000/ru/users)
|
179
|
+
* [localhost:3000/en/users](http://localhost:3000/en/users)
|
180
|
+
* [localhost:3000/it/users](http://localhost:3000/it/users)
|
181
|
+
|
182
|
+
Notice the re-write route contained a catch all. This catch-all is automatically added if missing. The catch-all is the part of the path that will remain for the following routes to check against.
|
169
183
|
|
170
184
|
### Routes with Blocks instead of Controllers
|
171
185
|
|
172
|
-
Routes that respond with a block of code can receive the `request` and `response` objects,
|
186
|
+
Routes that respond with a block of code can receive the `request` and `response` objects, to be used within the block of code (the controller also has access to these objects).
|
173
187
|
|
174
188
|
These Routes favor a global response over the different features offered by Controllers (i.e. RESTful routing and virtual folder method routing). Also, the block of code does NOT inherit all the magical abilities bestowed on Controllers which could allow for a slight increase in response time.
|
175
189
|
|
176
|
-
Here's a more powerful example of a route with a block of code,
|
190
|
+
Here's a more powerful example of a route with a block of code, this time using the `request` and `response` passed on to it:
|
177
191
|
|
178
192
|
```ruby
|
179
193
|
require 'plezi'
|
@@ -190,20 +204,6 @@ end
|
|
190
204
|
exit
|
191
205
|
```
|
192
206
|
|
193
|
-
## The
|
194
|
-
|
195
|
-
(todo: write documentation)
|
196
|
-
|
197
|
-
### A Virtual Folder
|
198
|
-
|
199
|
-
(todo: write documentation)
|
200
|
-
|
201
|
-
### RESTful methods
|
202
|
-
|
203
|
-
(todo: write documentation)
|
204
|
-
|
205
|
-
### Websocket Callbacks
|
206
|
-
|
207
|
-
(todo: write documentation)
|
208
|
-
|
207
|
+
## The next step
|
209
208
|
|
209
|
+
Now that we have learned more about the power of Plezi's routing system, it's time to [learn more about what Controller classes can do for us](./controllers.md).
|
data/docs/websockets.md
CHANGED
@@ -1,14 +1,213 @@
|
|
1
1
|
# Plezi Websockets
|
2
2
|
|
3
|
-
Inside Plezi's core code is the pure Ruby HTTP and Websocket Server (and client) that comes with [Iodine](https://github.com/boazsegev/iodine), a wonderful little server that supports an effective websocket
|
3
|
+
Inside Plezi's core code is the pure Ruby HTTP and Websocket Server (and client) that comes with [Iodine](https://github.com/boazsegev/iodine), a wonderful little server that supports an effective websocket functionality both as a server and as a client.
|
4
4
|
|
5
5
|
Plezi augmentes Iodine by adding auto-Redis support for scaling and automatically mapping each Contoller Class as a broadcast channel and each server instance to it's own unique channel - allowing unicasting to direct it's message at the target connection's server and optimizing resources.
|
6
6
|
|
7
|
+
Reading through this document, you should remember that Plezi's websocket connections are object oriented - they are instances of Controller classes that answer a specific url/path in the Plezi application. More than one type of connection (Controller instance) could exist in the same application.
|
7
8
|
|
9
|
+
## A short intro to Websockets (skip this if you can)
|
10
|
+
|
11
|
+
In a very broad sense, Websockets allow the browser communicate with the server in a bi-directional manner. This overcomes some of the limitations imposed by Http alone, allowing (for instance) to push real-time data, such as chat messages or stock quotes, directly to the browser.
|
12
|
+
|
13
|
+
In essense, while Http's worflow is a call and response (the browser "calls", the server "responds"), Websockets is a conversation, sometimes with long pauses, where both sides can speak whenever they feel the need to.
|
14
|
+
|
15
|
+
This, in nature, requires that both sides of the conversation establish a common language... this part is pretty much up to each application.
|
16
|
+
|
17
|
+
It's easy to think about it this way:
|
18
|
+
|
19
|
+
the browsers starts a call-response sequence. All websocket connections start as Http call-response. The browser shouts over the internet "I want to start a conversation".
|
20
|
+
|
21
|
+
The server responds: "Sure thing, let's talk".
|
22
|
+
|
23
|
+
Than they start their websocket conversation, keeping the connection between them open. The server can also answer "no thanks", but than there's no websocket connection and the Http connection will probably die out (unless it's Http/2).
|
24
|
+
|
25
|
+
### initiating a websocket connection
|
26
|
+
|
27
|
+
The websocket connection is initiated by the browser using `Javascript`.
|
28
|
+
|
29
|
+
The `Javascript` should, in most applications, handle the following three Websocket `Javascript` events:
|
30
|
+
|
31
|
+
- `onopen`: a connection was established.
|
32
|
+
- `onmessage`: a message was received through the connection.
|
33
|
+
- `onclose`: an open connection had closed, or a connection initiated couldn't be established.
|
34
|
+
|
35
|
+
Here is a common enough example of a script designed to open a websocket:
|
36
|
+
|
37
|
+
```js
|
38
|
+
|
39
|
+
websocket = NaN
|
40
|
+
|
41
|
+
function init_websocket()
|
42
|
+
{
|
43
|
+
// no need to renew socket connection if it's open
|
44
|
+
if(websocket && websocket.readyState == 1) return true;
|
45
|
+
|
46
|
+
// initiate the url for the websocket... this is a bit of an overkill,
|
47
|
+
// but it will allow you to copy & paste decent code
|
48
|
+
var ws_uri = (window.location.protocol.match(/https/) ? 'wss' : 'ws') + '://' + window.document.location.host
|
49
|
+
|
50
|
+
// initiate a new websocket connection
|
51
|
+
websocket = new WebSocket(ws_uri);
|
52
|
+
|
53
|
+
// define the onopen event callback
|
54
|
+
websocket.onopen = function(e) {
|
55
|
+
// what do you want to do now?
|
56
|
+
// maybe send a message?
|
57
|
+
websocket.send("Hello there!");
|
58
|
+
// a common practice is to use JSON
|
59
|
+
var msg = JSON.stringify({msg: 'chat', data: 'Hello there!'})
|
60
|
+
websocket.send(msg);
|
61
|
+
};
|
62
|
+
|
63
|
+
// define the onclose event callback
|
64
|
+
websocket.onclose = function(e) {
|
65
|
+
// you probably want to reopen the websocket if it closes
|
66
|
+
setTimeout( init_websocket, 100 );
|
67
|
+
};
|
68
|
+
|
69
|
+
// define the onmessage event callback
|
70
|
+
websocket.onmessage = function(e) {
|
71
|
+
// what do you want to do now?
|
72
|
+
console.log(e.data);
|
73
|
+
// to use JSON, use:
|
74
|
+
// msg = JSON.parse(e.data);
|
75
|
+
};
|
76
|
+
}
|
77
|
+
|
78
|
+
init_websocket();
|
79
|
+
|
80
|
+
```
|
81
|
+
|
82
|
+
As you can tell from reading through the code, this means that the browser will open a new connection to the server, using the websocket protocol.
|
83
|
+
|
84
|
+
In our example the script sent a message: `"Hello there!"`. It's up to your code to decide what to do with the data it receives, be it using JSON or raw data.
|
85
|
+
|
86
|
+
When data comes in from the browser, the `onmessage` event is raised. It's up to your script to decypher the meaning of that message within the `onmessage` callback.
|
87
|
+
|
88
|
+
###
|
89
|
+
|
90
|
+
No we know a bit about what Websockets are and how to initiate a websocket connection to send and receive data... next up, how to get Plezi to answer (or refuse) websocket requests?
|
91
|
+
|
92
|
+
## Communicating between the application and clients
|
8
93
|
|
9
94
|
(todo: write documentation)
|
10
95
|
|
11
96
|
|
97
|
+
## Communicating between different Websocket clients
|
98
|
+
|
99
|
+
Plezi supports three models of communication:
|
100
|
+
|
101
|
+
### General websocket communication
|
102
|
+
|
103
|
+
When using this type of communication, it is expected that each connection's controller provide a protected instance method with a name matching the event name and that this method will accept, as arguments, the data sent with the event.
|
104
|
+
|
105
|
+
This type of communication includes:
|
106
|
+
|
107
|
+
- **Multicasting**:
|
108
|
+
|
109
|
+
Use `multicast` to send an event to all the websocket connections currently connected to the application (including connections on other servers running the application, if Redis is used).
|
110
|
+
|
111
|
+
- **Unicasting**:
|
12
112
|
|
113
|
+
Use `unicast` to send an event to a specific websocket connection.
|
13
114
|
|
115
|
+
This uses a unique UUID that contains both the target server's information and the unique connection identifier. This allows a message to be sent to any connected websocket across multiple application instances when using Redis, minimizing network activity and server load as much as effectively possible.
|
116
|
+
|
117
|
+
Again, exacly like when using multicasting, any connection targeted by the message is expected to implemnt a method matching the name of the event, which will accept (as arguments) the data sent.
|
118
|
+
|
119
|
+
For instance, when using:
|
120
|
+
|
121
|
+
unicast target_id, :event_name, "string", and: :hash
|
122
|
+
|
123
|
+
The receiving websocket controller is expected to have a protected method named `event_name` like so:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class MyController
|
127
|
+
#...
|
128
|
+
protected
|
129
|
+
def event_name str, options_hash
|
130
|
+
#...
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
### Object Oriented communication
|
136
|
+
|
137
|
+
Use `broadcast` or `Controller.broadcast` to send an event to a all the websocket connections that are managed by a specific Controller class.
|
138
|
+
|
139
|
+
The controller is expected to provide a protected instance method with a name matching the event name and that this method will accept, as arguments, the data sent with the event.
|
140
|
+
|
141
|
+
The benifit of using this approach is knowing exacly what type of objects handle the message - all the websocket connections receiving the message will be members (instances) of the same class.
|
142
|
+
|
143
|
+
For instance, when using:
|
144
|
+
|
145
|
+
MyController.broadcast :event_name, "string", and: :hash
|
146
|
+
|
147
|
+
The receiving websocket controller is expected to have a protected method named `event_name` like so:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class MyController
|
151
|
+
#...
|
152
|
+
protected
|
153
|
+
def event_name str, options_hash
|
154
|
+
#...
|
155
|
+
end
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
### Identity oriented communication
|
160
|
+
|
161
|
+
Identity oriented communication will only work if Plezi's Redis features are enabled. To enable Plezi's automatic Redis features (such as websocket scaling automation, Redis Session Store, etc'), use:
|
162
|
+
|
163
|
+
ENV['PL_REDIS_URL'] ||= "redis://user:password@redis.example.com:9999"
|
164
|
+
|
165
|
+
Use `#register_as` or `#notify(identity, event_name, data)` to send make sure a certain Identity object (i.e. an app's User) receives notifications either in real-time (if connected) or the next time the identity connects to a websocket and identifies itself using `#register_as`.
|
166
|
+
|
167
|
+
Much like General Websocket Communication, the identity can call `#register_as` from different Controller classes and it is expected that each of these Controller classes implement the necessary methods.
|
168
|
+
|
169
|
+
It is a good enough practice that an Identity based websocket connection will utilize the `#on_open` callback to authenticate and register an identity. For example:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
class MyController
|
173
|
+
#...
|
174
|
+
def on_open
|
175
|
+
user = suthenticate_user
|
176
|
+
close unless user
|
177
|
+
register_as user.id
|
178
|
+
end
|
179
|
+
|
180
|
+
protected
|
181
|
+
|
182
|
+
def event_name str, options_hash
|
183
|
+
#...
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
It is recommended that the authentication and registration are split into two different events - the `pre_connect` for authentication and the `on_open` for registration - since, as a matter of security, it is better to prevent someone from entering (not establishing a websocket connection) than throwing them out (closing the connection within the `on_open` event).
|
189
|
+
|
190
|
+
Sending messages to the identity is similar to the other communication API methods. For example:
|
191
|
+
|
192
|
+
notify user_id, :event_name, "string data", hash: :data, more_hash: :data
|
193
|
+
|
194
|
+
As expected, it could be that an Identity will never revisit the application or messages become outdated after a while. For this reason limits must be set as to how long any specific "mailbox" should remain alive in the database when it isn't acessed by the Identity. This is done within the `register_as` method i.e.:
|
195
|
+
|
196
|
+
register_as user.id, lifetime: 1_814_400 # 21 days
|
197
|
+
|
198
|
+
Another consideration is that more than one "lifetime" setting might be required for defferent types of messages. The solution for this will be to allow a single connection to register as a number of different identities, each with it's own lifetime:
|
199
|
+
|
200
|
+
# to register:
|
201
|
+
register_as "#{user.id}-long", lifetime: 1_814_400 # 21 days
|
202
|
+
register_as "#{user.id}-short", lifetime: 3_600 # 1 hour
|
203
|
+
|
204
|
+
# to notify:
|
205
|
+
notify "#{user.id}-long", :event_name #...
|
206
|
+
notify "#{user.id}-short", :event_name #...
|
207
|
+
|
208
|
+
It should be noted that the lifetime is for the identity's lifetime and NOT the notification's lifetime. A notification sent a second before the identity "dies" will live for only a second and notify will return `true` all the same.
|
209
|
+
|
210
|
+
`notify` should return `true` or `false`, depending on whether the identity still exists.
|
211
|
+
|
212
|
+
(todo: write documentation)
|
14
213
|
|
data/lib/plezi.rb
CHANGED
@@ -14,14 +14,6 @@ require 'set'
|
|
14
14
|
# Iodine server
|
15
15
|
require 'iodine/http'
|
16
16
|
|
17
|
-
|
18
|
-
## erb templating
|
19
|
-
begin
|
20
|
-
require 'erb'
|
21
|
-
rescue => e
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
17
|
### version
|
26
18
|
|
27
19
|
require "plezi/version"
|
@@ -46,13 +38,19 @@ require 'plezi/helpers/mime_types.rb'
|
|
46
38
|
require 'plezi/handlers/http_router.rb'
|
47
39
|
require 'plezi/handlers/route.rb'
|
48
40
|
require 'plezi/handlers/ws_object.rb'
|
41
|
+
require 'plezi/handlers/ws_identity.rb'
|
49
42
|
require 'plezi/handlers/controller_magic.rb'
|
50
43
|
require 'plezi/handlers/controller_core.rb'
|
51
44
|
require 'plezi/handlers/placebo.rb'
|
52
45
|
require 'plezi/handlers/stubs.rb'
|
53
46
|
require 'plezi/handlers/session.rb'
|
54
47
|
|
48
|
+
## erb templating
|
49
|
+
begin
|
50
|
+
require 'erb'
|
51
|
+
rescue => e
|
55
52
|
|
53
|
+
end
|
56
54
|
|
57
55
|
##############################################################################
|
58
56
|
#
|
@@ -148,6 +146,3 @@ Iodine.threads = 30
|
|
148
146
|
Iodine.run { puts "Plezi is feeling optimistic running version #{::Plezi::VERSION}.\n\n"}
|
149
147
|
# PL is a shortcut for the Plezi module, so that `PL == Plezi`.
|
150
148
|
PL = Plezi
|
151
|
-
|
152
|
-
|
153
|
-
|
data/lib/plezi/common/cache.rb
CHANGED
@@ -56,14 +56,20 @@ module Plezi
|
|
56
56
|
end
|
57
57
|
|
58
58
|
# places data into the cache, under an identifier ( file name ).
|
59
|
-
def cache_data filename, data, mtime =
|
59
|
+
def cache_data filename, data, mtime = Iodine.time
|
60
60
|
CACHE_LOCK.synchronize { CACHE_STORE[filename] = CacheObject.new( data, mtime ) }
|
61
61
|
data
|
62
62
|
end
|
63
63
|
|
64
64
|
# Get data from the cache. will throw an exception if there is no data in the cache.
|
65
|
+
#
|
66
|
+
# If a block is passed to the method, it will allows you to modify the protected cache in a thread safe manner.
|
67
|
+
#
|
68
|
+
# This is useful for manipulating strings or arrays that are stored in the cache.
|
65
69
|
def get_cached filename
|
66
|
-
CACHE_STORE[filename].data
|
70
|
+
return CACHE_STORE[filename].data unless block_given?
|
71
|
+
data = CACHE_STORE[filename].data
|
72
|
+
CACHE_LOCK.synchronize { yield(data) }
|
67
73
|
end
|
68
74
|
|
69
75
|
# Remove data from the cache, if it exists.
|
data/lib/plezi/common/redis.rb
CHANGED
@@ -16,22 +16,9 @@ module Plezi
|
|
16
16
|
raise "Redis connction failed for: #{ENV['PL_REDIS_URL']}" unless @redis
|
17
17
|
@redis_sub_thread = Thread.new do
|
18
18
|
begin
|
19
|
-
safe_types = [Symbol, Date, Time, Encoding, Struct, Regexp, Range, Set]
|
20
19
|
::Redis.new(host: @redis_uri.host, port: @redis_uri.port, password: @redis_uri.password).subscribe(Plezi::Settings.redis_channel_name, Plezi::Settings.uuid) do |on|
|
21
20
|
on.message do |channel, msg|
|
22
|
-
|
23
|
-
data = YAML.safe_load(msg, safe_types)
|
24
|
-
next if data[:server] == Plezi::Settings.uuid
|
25
|
-
data[:type] = Object.const_get(data[:type]) unless data[:type].nil? || data[:type] == :all
|
26
|
-
if data[:target]
|
27
|
-
data[:type].___faild_unicast( data ) unless Iodine::Http::Websockets.unicast data[:target], data
|
28
|
-
else
|
29
|
-
Iodine::Http::Websockets.broadcast data
|
30
|
-
end
|
31
|
-
rescue => e
|
32
|
-
Iodine.error "The following could be a security breach attempt:"
|
33
|
-
Iodine.error e
|
34
|
-
end
|
21
|
+
::Plezi::Base::WSObject.forward_message ::Plezi::Base::WSObject.translate_message(msg)
|
35
22
|
end
|
36
23
|
end
|
37
24
|
rescue => e
|
@@ -42,18 +42,6 @@ module Plezi
|
|
42
42
|
# complete handshake
|
43
43
|
return self
|
44
44
|
end
|
45
|
-
# handles websocket opening.
|
46
|
-
def on_open
|
47
|
-
super() if defined?(super)
|
48
|
-
end
|
49
|
-
# handles websocket messages.
|
50
|
-
def on_message data
|
51
|
-
super if defined?(super)
|
52
|
-
end
|
53
|
-
# handles websocket being closed.
|
54
|
-
def on_close
|
55
|
-
super if defined? super
|
56
|
-
end
|
57
45
|
|
58
46
|
# Inner Routing
|
59
47
|
def _route_path_to_methods_and_set_the_response_
|
@@ -229,7 +229,7 @@ module Plezi
|
|
229
229
|
|
230
230
|
# This class method behaves the same way as the instance method #url_for. See the instance method's documentation for more details.
|
231
231
|
def url_for dest
|
232
|
-
get_pl_route.url_for dest
|
232
|
+
get_pl_route.url_for dest
|
233
233
|
end
|
234
234
|
|
235
235
|
# resets the routing cache
|
@@ -57,7 +57,7 @@ module Plezi
|
|
57
57
|
end
|
58
58
|
# Cleanup on disconnection
|
59
59
|
def on_close
|
60
|
-
io_out.close unless io_out.closed?
|
60
|
+
@io_out.close unless @io_out.closed?
|
61
61
|
return super() if defined? super
|
62
62
|
Iodine.warn "Placebo #{self.class.superclass.name} disconnected. Ignore if this message appears during shutdown."
|
63
63
|
end
|
@@ -104,7 +104,7 @@ module Plezi
|
|
104
104
|
i, o = IO.pipe
|
105
105
|
req = {}
|
106
106
|
handler = new_class.new(i, o, req)
|
107
|
-
io = Placebo::Base::PlaceboIO.new i, handler, req
|
107
|
+
io = Placebo::Base::PlaceboIO.new i, handler: handler, request: req
|
108
108
|
handler
|
109
109
|
end
|
110
110
|
end
|
data/lib/plezi/handlers/route.rb
CHANGED
@@ -15,7 +15,7 @@ module Plezi
|
|
15
15
|
fill_parameters = match request.path
|
16
16
|
return false unless fill_parameters
|
17
17
|
old_params = request.params.dup
|
18
|
-
fill_parameters.each {|k,v| Plezi::Base::Helpers.add_param_to_hash k, v, request.params }
|
18
|
+
fill_parameters.each {|k,v| Plezi::Base::Helpers.add_param_to_hash k, ::Plezi::Base::Helpers.form_decode(v), request.params }
|
19
19
|
ret = false
|
20
20
|
if controller
|
21
21
|
ret = controller.new(request, response)._route_path_to_methods_and_set_the_response_
|
@@ -86,6 +86,8 @@ module Plezi
|
|
86
86
|
def url_for dest = :index
|
87
87
|
raise NotImplementedError, "#url_for isn't implemented for this router - could this be a Regexp based router?" unless @url_array
|
88
88
|
case dest
|
89
|
+
when :index, nil, false
|
90
|
+
dest = {}
|
89
91
|
when String
|
90
92
|
dest = {id: dest.dup}
|
91
93
|
when Numeric, Symbol
|
@@ -93,8 +95,6 @@ module Plezi
|
|
93
95
|
when Hash
|
94
96
|
dest = dest.dup
|
95
97
|
dest.each {|k,v| dest[k] = v.dup if v.is_a? String }
|
96
|
-
when nil, false
|
97
|
-
dest = {}
|
98
98
|
else
|
99
99
|
# convert dest.id and dest[:id] to their actual :id value.
|
100
100
|
dest = {id: (dest.id rescue false) || (raise TypeError, "Expecting a Symbol, Hash, String, Numeric or an object that answers to obj[:id] or obj.id") }
|
@@ -25,6 +25,10 @@ module Plezi
|
|
25
25
|
end
|
26
26
|
failed
|
27
27
|
end
|
28
|
+
# returns the session id (the session cookie value).
|
29
|
+
def id
|
30
|
+
@id
|
31
|
+
end
|
28
32
|
# Get a key from the session data store. If a Redis server is supplied, it will be used to synchronize session data.
|
29
33
|
#
|
30
34
|
# Due to scaling considirations, all keys will be converted to strings, so that `"name" == :name` and `1234 == "1234"`.
|
@@ -44,6 +48,7 @@ module Plezi
|
|
44
48
|
# Due to scaling considirations, all keys will be converted to strings, so that `"name" == :name` and `1234 == "1234"`.
|
45
49
|
# If you store two keys that evaluate as the same string, they WILL override each other.
|
46
50
|
def []= key, value
|
51
|
+
return delete key if value.nil?
|
47
52
|
key = key.to_s
|
48
53
|
if (conn=Plezi.redis)
|
49
54
|
conn.hset @id, key, value
|
@@ -63,6 +68,15 @@ module Plezi
|
|
63
68
|
failed
|
64
69
|
end
|
65
70
|
|
71
|
+
# @return [String] returns the Session data in YAML format.
|
72
|
+
def to_s
|
73
|
+
if (conn=Plezi.redis)
|
74
|
+
conn.expire @id, SESSION_LIFETIME
|
75
|
+
return conn.hgetall(@id).to_yaml
|
76
|
+
end
|
77
|
+
failed
|
78
|
+
end
|
79
|
+
|
66
80
|
# Removes a key from the session's data store.
|
67
81
|
def delete key
|
68
82
|
key = key.to_s
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Plezi
|
2
|
+
|
3
|
+
module Base
|
4
|
+
|
5
|
+
module WSObject
|
6
|
+
|
7
|
+
# the following are additions to the WebSocket Object module,
|
8
|
+
# to establish identity to websocket realtionships, allowing for a
|
9
|
+
# websocket message bank.
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
protected
|
13
|
+
|
14
|
+
# The following method registers the connections as a unique global identity.
|
15
|
+
#
|
16
|
+
# Like {Plezi::Base::WSObject::SuperClassMethods#notify}, using this method requires an active Redis connection
|
17
|
+
# to be set up. See {Plezi#redis} for more information.
|
18
|
+
#
|
19
|
+
# Only one connection at a time can respond to identity events. If the same identity
|
20
|
+
# connects more than once, only the last connection will receive the notifications.
|
21
|
+
#
|
22
|
+
# The method accepts:
|
23
|
+
# identity:: a global application wide unique identifier that will persist throughout all of the identity's connections.
|
24
|
+
# options:: an option's hash that sets the properties of the identity.
|
25
|
+
#
|
26
|
+
# The option's Hash, at the moment, accepts only the following (optional) option:
|
27
|
+
# lifetime:: sets how long the identity can survive. defaults to `604_800` seconds (7 days).
|
28
|
+
#
|
29
|
+
# Calling this method will also initiate any events waiting in the identity's queue.
|
30
|
+
# make sure that the method is only called once all other initialization is complete.
|
31
|
+
#
|
32
|
+
# Do NOT call this method asynchronously unless Plezi is set to run as in a single threaded mode - doing so
|
33
|
+
# will execute any pending events outside the scope of the IO's mutex lock, thus introducing race conditions.
|
34
|
+
def register_as identity, options = {}
|
35
|
+
redis = Plezi.redis
|
36
|
+
raise "The identity API requires a Redis connection" unless redis
|
37
|
+
identity = identity.to_s.freeze
|
38
|
+
@___identity ||= [].to_set
|
39
|
+
@___identity << identity
|
40
|
+
redis.pipelined do
|
41
|
+
redis.lpush "#{identity}_uuid".freeze, uuid
|
42
|
+
redis.ltrim "#{identity}_uuid".freeze, 0, 0
|
43
|
+
end
|
44
|
+
___review_identity identity
|
45
|
+
redis.lpush(identity, ''.freeze) unless redis.llen(identity) > 0
|
46
|
+
redis.pipelined do
|
47
|
+
redis.expire identity, (options[:lifetime] || 604_800)
|
48
|
+
redis.expire "#{identity}_uuid".freeze, (options[:lifetime] || 604_800)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# sends a notification to an Identity. Returns false if the Identity never registered or it's registration expired.
|
53
|
+
def notify identity, event_name, *args
|
54
|
+
self.class.notify identity, event_name, *args
|
55
|
+
end
|
56
|
+
# returns true if the Identity in question is registered to receive notifications.
|
57
|
+
def registered? identity
|
58
|
+
self.class.registered? identity
|
59
|
+
end
|
60
|
+
end
|
61
|
+
module ClassMethods
|
62
|
+
end
|
63
|
+
module SuperInstanceMethods
|
64
|
+
protected
|
65
|
+
def ___review_identity identity
|
66
|
+
redis = Plezi.redis
|
67
|
+
raise "unknown Redis initiation error" unless redis
|
68
|
+
identity = identity.to_s.freeze
|
69
|
+
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
|
77
|
+
msg = ::Plezi::Base::WSObject.translate_message(msg)
|
78
|
+
next unless msg
|
79
|
+
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]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
module SuperClassMethods
|
86
|
+
public
|
87
|
+
|
88
|
+
# sends a notification to an Identity. Returns false if the Identity never registered or it's registration expired.
|
89
|
+
def notify identity, event_name, *args
|
90
|
+
redis = Plezi.redis
|
91
|
+
raise "The identity API requires a Redis connection" unless redis
|
92
|
+
identity = identity.to_s.freeze
|
93
|
+
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
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
# returns true if the Identity in question is registered to receive notifications.
|
101
|
+
def registered? identity
|
102
|
+
redis = Plezi.redis
|
103
|
+
return Iodine.warn("Cannot check for Identity registration without a Redis connection (silent).") && false unless redis
|
104
|
+
identity = identity.to_s.freeze
|
105
|
+
redis.llen(identity).to_i > 0
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -1,31 +1,74 @@
|
|
1
1
|
module Plezi
|
2
2
|
|
3
|
-
# the methods defined in this module will be injected into the Controller class passed to
|
4
|
-
# Plezi (using the `route` or `shared_route` commands), and will be available
|
5
|
-
# for the controller to use within it's methods.
|
6
|
-
#
|
7
|
-
# for some reason, the documentation ignores the following additional attributes, which are listed here:
|
8
|
-
#
|
9
|
-
# request:: the HTTPRequest object containing all the data from the HTTP request. If a WebSocket connection was established, the `request` object will continue to contain the HTTP request establishing the connection (cookies, parameters sent and other information).
|
10
|
-
# params:: any parameters sent with the request (short-cut for `request.params`), will contain any GET or POST form data sent (including file upload and JSON format support).
|
11
|
-
# cookies:: a cookie-jar to get and set cookies (set: `cookie\[:name] = data` or get: `cookie\[:name]`). Cookies and some other data must be set BEFORE the response's headers are sent.
|
12
|
-
# flash:: a temporary cookie-jar, good for one request. this is a short-cut for the `response.flash` which handles this magical cookie style.
|
13
|
-
# response:: the HTTPResponse **OR** the WSResponse object that formats the response and sends it. use `response << data`. This object can be used to send partial data (such as headers, or partial html content) in blocking mode as well as sending data in the default non-blocking mode.
|
14
|
-
# host_params:: a copy of the parameters used to create the host and service which accepted the request and created this instance of the controller class.
|
15
|
-
#
|
16
3
|
module Base
|
17
4
|
|
18
5
|
# This module includes all the methods that will be injected into Websocket objects,
|
19
6
|
# specifically into Plezi Controllers and Placebo objects.
|
7
|
+
#
|
8
|
+
# the methods defined in this module will be injected into the Controller class passed to
|
9
|
+
# Plezi (using the `route` or `shared_route` commands), and will be available
|
10
|
+
# for the controller to use within it's methods.
|
11
|
+
#
|
12
|
+
# for some reason, the documentation ignores the following additional attributes, which are listed here:
|
13
|
+
#
|
14
|
+
# request:: the HTTPRequest object containing all the data from the HTTP request. If a WebSocket connection was established, the `request` object will continue to contain the HTTP request establishing the connection (cookies, parameters sent and other information).
|
15
|
+
# params:: any parameters sent with the request (short-cut for `request.params`), will contain any GET or POST form data sent (including file upload and JSON format support).
|
16
|
+
# cookies:: a cookie-jar to get and set cookies (set: `cookie\[:name] = data` or get: `cookie\[:name]`). Cookies and some other data must be set BEFORE the response's headers are sent.
|
17
|
+
# flash:: a temporary cookie-jar, good for one request. this is a short-cut for the `response.flash` which handles this magical cookie style.
|
18
|
+
# response:: the HTTPResponse **OR** the WSResponse object that formats the response and sends it. use `response << data`. This object can be used to send partial data (such as headers, or partial html content) in blocking mode as well as sending data in the default non-blocking mode.
|
19
|
+
# host_params:: a copy of the parameters used to create the host and service which accepted the request and created this instance of the controller class.
|
20
|
+
#
|
20
21
|
module WSObject
|
21
22
|
def self.included base
|
22
23
|
base.send :include, InstanceMethods
|
23
24
|
base.extend ClassMethods
|
24
25
|
base.superclass.instance_eval {extend SuperClassMethods}
|
26
|
+
base.superclass.instance_eval {include SuperInstanceMethods}
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.translate_message msg
|
30
|
+
begin
|
31
|
+
@safe_types ||= [Symbol, Date, Time, Encoding, Struct, Regexp, Range, Set]
|
32
|
+
data = YAML.safe_load(msg, @safe_types)
|
33
|
+
rescue => e
|
34
|
+
Iodine.error "The following could be a security breach attempt:"
|
35
|
+
Iodine.error e
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
def self.forward_message data
|
40
|
+
begin
|
41
|
+
return false if data[:server] == Plezi::Settings.uuid
|
42
|
+
data[:type] = Object.const_get(data[:type]) unless data[:type].nil? || data[:type] == :all
|
43
|
+
if data[:target]
|
44
|
+
data[:type].___faild_unicast( data ) unless Iodine::Http::Websockets.unicast data[:target], data
|
45
|
+
else
|
46
|
+
Iodine::Http::Websockets.broadcast data
|
47
|
+
end
|
48
|
+
rescue => e
|
49
|
+
Iodine.error "The following could be a security breach attempt:"
|
50
|
+
Iodine.error e
|
51
|
+
nil
|
52
|
+
end
|
25
53
|
end
|
26
54
|
|
27
55
|
module InstanceMethods
|
28
56
|
public
|
57
|
+
|
58
|
+
# handles websocket opening.
|
59
|
+
def on_open
|
60
|
+
@ws_io = @request[:io]
|
61
|
+
super() if defined?(super)
|
62
|
+
end
|
63
|
+
# handles websocket messages.
|
64
|
+
def on_message data
|
65
|
+
super if defined?(super)
|
66
|
+
end
|
67
|
+
# handles websocket being closed.
|
68
|
+
def on_close
|
69
|
+
super if defined? super
|
70
|
+
end
|
71
|
+
|
29
72
|
# handles broadcasts / unicasts
|
30
73
|
def on_broadcast data
|
31
74
|
unless data.is_a?(Hash) && (data[:type] || data[:target]) && data[:method] && data[:data]
|
@@ -39,6 +82,26 @@ module Plezi
|
|
39
82
|
self.method(data[:method]).call *data[:data]
|
40
83
|
end
|
41
84
|
|
85
|
+
# Get's the websocket's unique identifier for unicast transmissions.
|
86
|
+
#
|
87
|
+
# This UUID is also used to make sure Radis broadcasts don't triger the
|
88
|
+
# boadcasting object's event.
|
89
|
+
def uuid
|
90
|
+
return @uuid if @uuid
|
91
|
+
if __get_io
|
92
|
+
return (@uuid ||= Plezi::Settings.uuid + @io.id)
|
93
|
+
end
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
alias :unicast_id :uuid
|
97
|
+
|
98
|
+
protected
|
99
|
+
|
100
|
+
# allows writing of data to the websocket (if opened). Otherwise appends the message to the Http response.
|
101
|
+
def write data
|
102
|
+
(@ws_io || @response) << data
|
103
|
+
end
|
104
|
+
|
42
105
|
# Performs a websocket unicast to the specified target.
|
43
106
|
def unicast target_uuid, method_name, *args
|
44
107
|
self.class.unicast target_uuid, method_name, *args
|
@@ -68,20 +131,6 @@ module Plezi
|
|
68
131
|
self.class._inner_broadcast({ method: method_name, data: args, type: :all}, __get_io )
|
69
132
|
end
|
70
133
|
|
71
|
-
# Get's the websocket's unique identifier for unicast transmissions.
|
72
|
-
#
|
73
|
-
# This UUID is also used to make sure Radis broadcasts don't triger the
|
74
|
-
# boadcasting object's event.
|
75
|
-
def uuid
|
76
|
-
return @uuid if @uuid
|
77
|
-
if __get_io
|
78
|
-
return (@uuid ||= Plezi::Settings.uuid + @io.id)
|
79
|
-
end
|
80
|
-
nil
|
81
|
-
end
|
82
|
-
alias :unicast_id :uuid
|
83
|
-
|
84
|
-
protected
|
85
134
|
def __get_io
|
86
135
|
@io ||= (@request ? @request[:io] : nil)
|
87
136
|
end
|
@@ -105,7 +154,13 @@ module Plezi
|
|
105
154
|
@super_methods_list.include? method_name
|
106
155
|
end
|
107
156
|
def has_exposed_method? method_name
|
108
|
-
@
|
157
|
+
@reserved_methods_list ||= Class.new.public_instance_methods +
|
158
|
+
Plezi::Base::WSObject::InstanceMethods.public_instance_methods +
|
159
|
+
Plezi::Base::WSObject::SuperInstanceMethods.public_instance_methods +
|
160
|
+
Plezi::ControllerMagic::InstanceMethods.public_instance_methods +
|
161
|
+
Plezi::Base::ControllerCore::InstanceMethods.public_instance_methods +
|
162
|
+
[:before, :after, :save, :show, :update, :delete, :initialize]
|
163
|
+
@exposed_methods_list ||= ( (self.public_instance_methods - @reserved_methods_list ).delete_if {|m| m.to_s[0] == '_'} ).to_set
|
109
164
|
@exposed_methods_list.include? method_name
|
110
165
|
end
|
111
166
|
|
@@ -125,6 +180,8 @@ module Plezi
|
|
125
180
|
end
|
126
181
|
|
127
182
|
end
|
183
|
+
module SuperInstanceMethods
|
184
|
+
end
|
128
185
|
|
129
186
|
module SuperClassMethods
|
130
187
|
public
|
@@ -32,6 +32,13 @@ module Plezi
|
|
32
32
|
(str.to_s.gsub(/[^a-z0-9\*\.\_\-]/i) {|m| '%%%02x'.freeze % m.ord }).force_encoding(::Encoding::ASCII_8BIT)
|
33
33
|
end
|
34
34
|
|
35
|
+
# decode percent-encoded data (excluding the '+' sign for encoding).
|
36
|
+
def self.form_decode s
|
37
|
+
s = s.to_s.gsub(/\%[0-9a-f]{2}/i) {|m| m[1..2].to_i(16).chr}
|
38
|
+
s.gsub!(/&#[0-9]{4};/i) {|m| [m[2..5].to_i].pack 'U'.freeze }
|
39
|
+
s
|
40
|
+
end
|
41
|
+
|
35
42
|
# Adds paramaters to a Hash object, according to the Iodine's server conventions.
|
36
43
|
def self.add_param_to_hash name, value, target
|
37
44
|
begin
|
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.8"
|
22
22
|
spec.add_development_dependency "bundler", "~> 1.7"
|
23
23
|
spec.add_development_dependency "rake", "~> 10.0"
|
24
24
|
|
data/resources/websockets.js
CHANGED
@@ -24,7 +24,7 @@ function init_websocket()
|
|
24
24
|
};
|
25
25
|
|
26
26
|
websocket.onclose = function(e) {
|
27
|
-
// If the websocket repeatedly you probably want to
|
27
|
+
// If the websocket repeatedly you probably want to report an error
|
28
28
|
if(!isNaN(websocket_fail_limit) && websocket_fail_count >= websocket_fail_limit) {
|
29
29
|
// What to do if we can't reconnect so many times?
|
30
30
|
return
|
data/test/plezi_tests.rb
CHANGED
@@ -155,6 +155,36 @@ class WSsizeTestCtrl
|
|
155
155
|
end
|
156
156
|
end
|
157
157
|
|
158
|
+
class WSIdentity
|
159
|
+
def index
|
160
|
+
"identity api testing path\n#{params}"
|
161
|
+
end
|
162
|
+
def show
|
163
|
+
if notify params[:id], :notification, (params[:message] || 'no message')
|
164
|
+
"Send notification for #{params[:id]}: #{(params[:message] || 'no message')}"
|
165
|
+
else
|
166
|
+
"The identity requested (#{params[:id]}) doesn't exist."
|
167
|
+
end
|
168
|
+
end
|
169
|
+
def pre_connect
|
170
|
+
params[:id] && true
|
171
|
+
end
|
172
|
+
def on_open
|
173
|
+
register_as params[:id]
|
174
|
+
end
|
175
|
+
def on_message data
|
176
|
+
puts "Got websocket message: #{data}"
|
177
|
+
end
|
178
|
+
|
179
|
+
protected
|
180
|
+
|
181
|
+
def notification message
|
182
|
+
write message
|
183
|
+
puts "Identity Got: #{message}"
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
158
188
|
module PleziTestTasks
|
159
189
|
module_function
|
160
190
|
|
@@ -239,7 +269,7 @@ module PleziTestTasks
|
|
239
269
|
begin
|
240
270
|
puts " * Streaming test: #{RESULTS[URI.parse("http://localhost:3000/streamer").read == 'streamed']}"
|
241
271
|
rescue => e
|
242
|
-
puts " **** Streaming test FAILED TO RUN!!!"
|
272
|
+
puts " **** Streaming test FAILED TO RUN #{e.message}!!!"
|
243
273
|
puts e
|
244
274
|
end
|
245
275
|
end
|
@@ -442,6 +472,8 @@ end
|
|
442
472
|
|
443
473
|
host
|
444
474
|
|
475
|
+
shared_route 'id/(:id)/(:message)', WSIdentity
|
476
|
+
|
445
477
|
shared_route 'ws/no', Nothing
|
446
478
|
shared_route 'ws/placebo', PlaceboTestCtrl
|
447
479
|
shared_route 'ws/size', WSsizeTestCtrl
|
@@ -460,8 +492,8 @@ end
|
|
460
492
|
# mem_print_proc.call
|
461
493
|
# Plezi.run_every 30, &mem_print_proc
|
462
494
|
|
463
|
-
|
464
|
-
|
495
|
+
require 'redis'
|
496
|
+
ENV['PL_REDIS_URL'] ||= ENV['REDIS_URL'] || ENV['REDISCLOUD_URL'] || ENV['REDISTOGO_URL'] || "redis://test:1234@pub-redis-11008.us-east-1-4.5.ec2.garantiadata.com:11008"
|
465
497
|
# Plezi.processes = 3
|
466
498
|
|
467
499
|
Plezi.threads = 9
|
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.3
|
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-10-
|
11
|
+
date: 2015-10-30 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.8
|
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.8
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- Rakefile
|
71
71
|
- bin/plezi
|
72
72
|
- docs/async_helpers.md
|
73
|
+
- docs/controllers.md
|
73
74
|
- docs/logging.md
|
74
75
|
- docs/routes.md
|
75
76
|
- docs/websockets.md
|
@@ -91,6 +92,7 @@ files:
|
|
91
92
|
- lib/plezi/handlers/route.rb
|
92
93
|
- lib/plezi/handlers/session.rb
|
93
94
|
- lib/plezi/handlers/stubs.rb
|
95
|
+
- lib/plezi/handlers/ws_identity.rb
|
94
96
|
- lib/plezi/handlers/ws_object.rb
|
95
97
|
- lib/plezi/helpers/http_sender.rb
|
96
98
|
- lib/plezi/helpers/magic_helpers.rb
|