plezi 0.14.4 → 0.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/bin/ws_shootout +30 -0
- data/exe/plezi +108 -108
- data/lib/plezi.rb +18 -26
- data/lib/plezi/activation.rb +16 -16
- data/lib/plezi/api.rb +51 -50
- data/lib/plezi/controller/controller.rb +249 -229
- data/lib/plezi/controller/controller_class.rb +189 -174
- data/lib/plezi/controller/cookies.rb +49 -47
- data/lib/plezi/helpers.rb +35 -35
- data/lib/plezi/render/erb.rb +25 -26
- data/lib/plezi/render/has_cache.rb +31 -31
- data/lib/plezi/render/markdown.rb +53 -53
- data/lib/plezi/render/render.rb +36 -38
- data/lib/plezi/render/sass.rb +43 -44
- data/lib/plezi/render/slim.rb +25 -25
- data/lib/plezi/router/adclient.rb +14 -15
- data/lib/plezi/router/assets.rb +59 -61
- data/lib/plezi/router/errors.rb +22 -22
- data/lib/plezi/router/route.rb +98 -100
- data/lib/plezi/router/router.rb +120 -113
- data/lib/plezi/version.rb +1 -1
- data/lib/plezi/websockets/message_dispatch.rb +118 -80
- data/lib/plezi/websockets/redis.rb +42 -43
- data/plezi.gemspec +3 -3
- data/resources/client.js +229 -204
- data/resources/ctrlr.rb +26 -26
- data/resources/mini_app.rb +1 -1
- data/resources/simple-client.js +50 -43
- metadata +9 -8
@@ -4,257 +4,277 @@ require 'plezi/controller/controller_class'
|
|
4
4
|
require 'plezi/websockets/message_dispatch'
|
5
5
|
|
6
6
|
module Plezi
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
# A Rack::Request object for the current request.
|
16
|
-
attr_reader :request
|
17
|
-
# A Rack::Response object used for the current request.
|
18
|
-
attr_reader :response
|
19
|
-
# A union between the `request.params` and the route's inline parameters. This is different then `request.params`
|
20
|
-
attr_reader :params
|
21
|
-
# A cookie jar for both accessing and setting cookies. Unifies `request.set_cookie`, `request.delete_cookie` and `request.cookies` with a single Hash like inteface.
|
22
|
-
#
|
23
|
-
# Read a cookie:
|
24
|
-
#
|
25
|
-
# cookies["name"]
|
26
|
-
#
|
27
|
-
# Set a cookie:
|
28
|
-
#
|
29
|
-
# cookies["name"] = "value"
|
30
|
-
# cookies["name"] = {value: "value", secure: true}
|
31
|
-
#
|
32
|
-
# Delete a cookie:
|
33
|
-
#
|
34
|
-
# cookies["name"] = nil
|
35
|
-
#
|
36
|
-
attr_reader :cookies
|
7
|
+
# This module contains the functionality provided to any Controller class.
|
8
|
+
#
|
9
|
+
# This module will be included within every Class that is asigned to a route, providing the functionality without forcing an inheritance model.
|
10
|
+
module Controller
|
11
|
+
def self.included(base)
|
12
|
+
base.extend ::Plezi::Controller::ClassMethods
|
13
|
+
end
|
37
14
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
15
|
+
# A Rack::Request object for the current request.
|
16
|
+
attr_reader :request
|
17
|
+
# A Rack::Response object used for the current request.
|
18
|
+
attr_reader :response
|
19
|
+
# A union between the `request.params` and the route's inline parameters. This is different then `request.params`
|
20
|
+
attr_reader :params
|
21
|
+
# A cookie jar for both accessing and setting cookies. Unifies `request.set_cookie`, `request.delete_cookie` and `request.cookies` with a single Hash like inteface.
|
22
|
+
#
|
23
|
+
# Read a cookie:
|
24
|
+
#
|
25
|
+
# cookies["name"]
|
26
|
+
#
|
27
|
+
# Set a cookie:
|
28
|
+
#
|
29
|
+
# cookies["name"] = "value"
|
30
|
+
# cookies["name"] = {value: "value", secure: true}
|
31
|
+
#
|
32
|
+
# Delete a cookie:
|
33
|
+
#
|
34
|
+
# cookies["name"] = nil
|
35
|
+
#
|
36
|
+
attr_reader :cookies
|
50
37
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
38
|
+
# @private
|
39
|
+
# This function is used internally by Plezi, do not call.
|
40
|
+
def _pl_respond(request, response, params)
|
41
|
+
@request = request
|
42
|
+
@response = response
|
43
|
+
@params = params
|
44
|
+
@cookies = Cookies.new(request, response)
|
45
|
+
mthd = requested_method
|
46
|
+
# puts "m == #{m.nil? ? 'nil' : m.to_s}"
|
47
|
+
return _pl_ad_httpreview(__send__(mthd)) if mthd
|
48
|
+
false
|
49
|
+
end
|
60
50
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
# render("users/layout") { render "users/index" }
|
71
|
-
#
|
72
|
-
def render(template, &block)
|
73
|
-
frmt = params['format'.freeze] || 'html'.freeze
|
74
|
-
mime = nil
|
75
|
-
ret = ::Plezi::Renderer.render "#{File.join(::Plezi.templates, template.to_s)}.#{frmt}", binding, &block
|
76
|
-
response[Rack::CONTENT_TYPE] = mime if ret && !response.content_type && (mime = Rack::Mime.mime_type(".#{frmt}".freeze, nil))
|
77
|
-
ret
|
78
|
-
end
|
51
|
+
# Returns the method that was called by the HTTP request.
|
52
|
+
#
|
53
|
+
# It's possible to override this method to change the default Controller behavior.
|
54
|
+
#
|
55
|
+
# For Websocket connections this method is most likely to return :preform_upgrade
|
56
|
+
def requested_method
|
57
|
+
params['_method'.freeze] = (params['_method'.freeze] || request.request_method.downcase).to_sym
|
58
|
+
self.class._pl_params2method(params, request.env)
|
59
|
+
end
|
79
60
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
61
|
+
# Renders the requested template (should be a string, subfolders are fine).
|
62
|
+
#
|
63
|
+
# Template name shouldn't include the template's extension or format - this allows for dynamic format template resolution, so that `json` and `html` requests can share the same code. i.e.
|
64
|
+
#
|
65
|
+
# Plezi.templates = "views/"
|
66
|
+
# render "users/index"
|
67
|
+
#
|
68
|
+
# Using layouts (nested templates) is easy by using a block (a little different then other frameworks):
|
69
|
+
#
|
70
|
+
# render("users/layout") { render "users/index" }
|
71
|
+
#
|
72
|
+
def render(template, &block)
|
73
|
+
frmt = params['format'.freeze] || 'html'.freeze
|
74
|
+
mime = nil
|
75
|
+
ret = ::Plezi::Renderer.render "#{File.join(::Plezi.templates, template.to_s)}.#{frmt}", binding, &block
|
76
|
+
response[Rack::CONTENT_TYPE] = mime if ret && !response.content_type && (mime = Rack::Mime.mime_type(".#{frmt}".freeze, nil))
|
77
|
+
ret
|
78
|
+
end
|
94
79
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
80
|
+
# Sends a block of data, setting a file name, mime type and content disposition headers when possible. This should also be a good choice when sending large amounts of data.
|
81
|
+
#
|
82
|
+
# By default, `send_data` sends the data as an attachment, unless `inline: true` was set.
|
83
|
+
#
|
84
|
+
# If a mime type is provided, it will be used to set the Content-Type header. i.e. `mime: "text/plain"`
|
85
|
+
#
|
86
|
+
# If a file name was provided, Rack will be used to find the correct mime type (unless provided). i.e. `filename: "sample.pdf"` will set the mime type to `application/pdf`
|
87
|
+
#
|
88
|
+
# Available options: `:inline` (`true` / `false`), `:filename`, `:mime`.
|
89
|
+
def send_data(data, options = {})
|
90
|
+
response.write data if data
|
91
|
+
filename = options[:filename]
|
92
|
+
# set headers
|
93
|
+
content_disposition = options[:inline] ? 'inline'.dup : 'attachment'.dup
|
94
|
+
content_disposition << "; filename=#{::File.basename(options[:filename])}" if filename
|
95
|
+
cont_type = (options[:mime] ||= filename && Rack::Mime.mime_type(::File.extname(filename)))
|
96
|
+
response['content-type'.freeze] = cont_type if cont_type
|
97
|
+
response['content-disposition'.freeze] = content_disposition
|
98
|
+
true
|
99
|
+
end
|
100
100
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
101
|
+
# Same as {#send_data}, but accepts a file name (to be opened and sent) rather then a String.
|
102
|
+
#
|
103
|
+
# See {#send_data} for available options.
|
104
|
+
def send_file(filename, options = {})
|
105
|
+
response['X-Sendfile'.freeze] = filename
|
106
|
+
options[:filename] ||= File.basename(filename)
|
107
|
+
filename = File.open(filename, 'rb'.freeze) # unless Iodine::Rack.public
|
108
|
+
send_data filename, options
|
109
|
+
end
|
110
110
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
111
|
+
# A shortcut for Rack's `response.redirect`.
|
112
|
+
def redirect_to(target, status = 302)
|
113
|
+
response.redirect target, status
|
114
|
+
true
|
115
|
+
end
|
116
116
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
117
|
+
# Returns a relative URL for the controller, placing the requested parameters in the URL (inline, where possible and as query data when not possible).
|
118
|
+
def url_for(func, params = {})
|
119
|
+
::Plezi::Base::Router.url_for self.class, func, params
|
120
|
+
end
|
121
121
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
122
|
+
# A connection's Plezi ID uniquely identifies the connection across application instances, allowing it to receive and send messages using {#unicast}.
|
123
|
+
def id
|
124
|
+
@_pl_id ||= (conn_id && "#{::Plezi::Base::MessageDispatch.pid}-#{conn_id.to_s(16)}")
|
125
|
+
end
|
126
126
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
127
|
+
# @private
|
128
|
+
# This is the process specific Websocket's UUID. This function is here to protect you from yourself. Don't call it.
|
129
|
+
def conn_id
|
130
|
+
defined?(super) && super
|
131
|
+
end
|
132
132
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
133
|
+
# Override this method to read / write cookies, perform authentication or perform validation before establishing a Websocket connecion.
|
134
|
+
#
|
135
|
+
# Return `false` or `nil` to refuse the websocket connection.
|
136
|
+
def pre_connect
|
137
|
+
true
|
138
|
+
end
|
139
139
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
140
|
+
# Experimental: takes a module to be used for Websocket callbacks events.
|
141
|
+
#
|
142
|
+
# This function can only be called **after** a websocket connection was established (i.e., within the `on_open` callback).
|
143
|
+
#
|
144
|
+
# This allows a module "library" to be used similar to the way "rooms" are used in node.js, so that a number of different Controllers can listen to shared events.
|
145
|
+
#
|
146
|
+
# By dynamically extending a Controller instance using a module, Websocket broadcasts will be allowed to invoke the module's functions.
|
147
|
+
#
|
148
|
+
# Notice: It is impossible to `unextend` an extended module at this time.
|
149
|
+
def extend(mod)
|
150
|
+
raise TypeError, '`mod` should be a module' unless mod.class == Module
|
151
|
+
raise "#{self} already extended by #{mod.name}" if is_a?(mod)
|
152
|
+
mod.extend ::Plezi::Controller::ClassMethods
|
153
|
+
super(mod)
|
154
|
+
_pl_ws_map.update mod._pl_ws_map
|
155
|
+
_pl_ad_map.update mod._pl_ad_map
|
156
|
+
end
|
157
157
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
158
|
+
# Invokes a method on the `target` websocket connection. When using Iodine, the method is invoked asynchronously.
|
159
|
+
#
|
160
|
+
# def perform_poke(target)
|
161
|
+
# unicast target, :poke, self.id
|
162
|
+
# end
|
163
|
+
# def poke(from)
|
164
|
+
# unicast from, :poke_back, self.id
|
165
|
+
# end
|
166
|
+
# def poke_back(from)
|
167
|
+
# puts "#{from} is available"
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
|
171
|
+
def unicast(target, event_method, *args)
|
172
|
+
::Plezi::Base::MessageDispatch.unicast(id ? self : self.class, target, event_method, args)
|
173
|
+
end
|
174
174
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
175
|
+
# Invokes a method on every websocket connection (except `self`) that belongs to this Controller / Type. When using Iodine, the method is invoked asynchronously.
|
176
|
+
#
|
177
|
+
# self.broadcast :my_method, "argument 1", "argument 2", 3
|
178
|
+
#
|
179
|
+
# Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
|
180
|
+
def broadcast(event_method, *args)
|
181
|
+
::Plezi::Base::MessageDispatch.broadcast(id ? self : self.class, event_method, args)
|
182
|
+
end
|
183
183
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
184
|
+
# Invokes a method on every websocket connection in the application (except `self`).
|
185
|
+
#
|
186
|
+
# self.multicast :my_method, "argument 1", "argument 2", 3
|
187
|
+
#
|
188
|
+
# Methods invoked using {unicast}, {broadcast} or {multicast} will quietly fail if the connection was lost, the requested method is undefined or the 'target' was invalid.
|
189
|
+
def multicast(event_method, *args)
|
190
|
+
::Plezi::Base::MessageDispatch.multicast(id ? self : self.class, event_method, args)
|
191
|
+
end
|
192
192
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
193
|
+
# Writes a message to every client websocket connection, for all controllers(!), EXCEPT self. Accepts an optional filter method using a location reference for a *static* (Class/Module/global) method. The filter method will be passerd the websocket object and it should return `true` / `false`.
|
194
|
+
#
|
195
|
+
# self.write2everyone {event: "global", message: "This will be sent to everyone"}.to_json
|
196
|
+
# # or, we can define a filter method somewhere in our code
|
197
|
+
# module Filter
|
198
|
+
# def self.should_send? ws
|
199
|
+
# true
|
200
|
+
# end
|
201
|
+
# end
|
202
|
+
# # and we can use this filter method.
|
203
|
+
# data = {event: "global", message: "This will be sent to everyone"}.to_json
|
204
|
+
# self.write2everyone data, ::Filter, :should_send?
|
205
|
+
#
|
206
|
+
# It's important that the filter method is defined statically in our code and isn't dynamically allocated. Otherwise, scaling the application would be impossible.
|
207
|
+
def write2everyone(data, filter_owner = nil, filter_name = nil)
|
208
|
+
::Plezi::Base::MessageDispatch.write2everyone(id ? self : self.class, data, filter_owner, filter_name)
|
209
|
+
end
|
198
210
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
211
|
+
# @private
|
212
|
+
# This function is used internally by Plezi, do not call.
|
213
|
+
def _pl_ws_map
|
214
|
+
@_pl_ws_map ||= self.class._pl_ws_map.dup
|
215
|
+
end
|
204
216
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
begin
|
210
|
-
json = JSON.parse(data, symbolize_names: true)
|
211
|
-
rescue
|
212
|
-
puts 'AutoDispatch Warnnig: Received non-JSON message. Closing Connection.'
|
213
|
-
close
|
214
|
-
return
|
217
|
+
# @private
|
218
|
+
# This function is used internally by Plezi, do not call.
|
219
|
+
def _pl_ad_map
|
220
|
+
@_pl_ad_map ||= self.class._pl_ad_map.dup
|
215
221
|
end
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
222
|
+
|
223
|
+
# @private
|
224
|
+
# This function is used internally by Plezi, for Auto-Dispatch support do not call.
|
225
|
+
def on_message(data)
|
226
|
+
json = nil
|
227
|
+
begin
|
228
|
+
json = JSON.parse(data, symbolize_names: true)
|
229
|
+
rescue
|
230
|
+
puts 'AutoDispatch Warnnig: Received non-JSON message. Closing Connection.'
|
231
|
+
close
|
232
|
+
return
|
233
|
+
end
|
234
|
+
envt = _pl_ad_map[json[:event]] || _pl_ad_map[:unknown]
|
235
|
+
if json[:event].nil? || envt.nil?
|
236
|
+
puts _pl_ad_map
|
237
|
+
puts "AutoDispatch Warnnig: JSON missing/invalid `event` name '#{json[:event]}' for class #{self.class.name}. Closing Connection."
|
238
|
+
close
|
239
|
+
end
|
240
|
+
write("{\"event\":\"_ack_\",\"_EID_\":#{json[:_EID_].to_json}}") if json[:_EID_]
|
241
|
+
_pl_ad_review __send__(envt, json)
|
221
242
|
end
|
222
|
-
write("{\"event\":\"_ack_\",\"_EID_\":#{json[:_EID_].to_json}}") if json[:_EID_]
|
223
|
-
_pl_ad_review __send__(envt, json)
|
224
|
-
end
|
225
243
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
244
|
+
# @private
|
245
|
+
# This function is used internally by Plezi, do not call.
|
246
|
+
def _pl_ad_review(data)
|
247
|
+
if self.class._pl_is_ad?
|
248
|
+
case data
|
249
|
+
when Hash
|
250
|
+
write data.to_json
|
251
|
+
when String
|
252
|
+
write data
|
253
|
+
# when Array
|
254
|
+
# write ret
|
255
|
+
end
|
256
|
+
end
|
257
|
+
data
|
258
|
+
end
|
239
259
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
260
|
+
# @private
|
261
|
+
# This function is used internally by Plezi, do not call.
|
262
|
+
def _pl_ad_httpreview(data)
|
263
|
+
return data.to_json if self.class._pl_is_ad? && data.is_a?(Hash)
|
264
|
+
data
|
265
|
+
end
|
246
266
|
|
247
|
-
|
267
|
+
private
|
248
268
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
269
|
+
# @private
|
270
|
+
# This function is used internally by Plezi, do not call.
|
271
|
+
def preform_upgrade
|
272
|
+
return false unless pre_connect
|
273
|
+
request.env['upgrade.websocket'.freeze] = self
|
274
|
+
@params = @params.dup # disable memory saving (used a single object per thread)
|
275
|
+
@_pl_ws_map = self.class._pl_ws_map.dup
|
276
|
+
@_pl_ad_map = self.class._pl_ad_map.dup
|
277
|
+
true
|
278
|
+
end
|
279
|
+
end
|
260
280
|
end
|