firehose 0.2.alpha.3 → 0.2.alpha.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.
data/firehose.gemspec CHANGED
@@ -36,4 +36,5 @@ Gem::Specification.new do |s|
36
36
  s.add_development_dependency "rack-test"
37
37
  s.add_development_dependency "async_rack_test"
38
38
  s.add_development_dependency "foreman"
39
+ s.add_development_dependency "sprockets"
39
40
  end
@@ -1,6 +1,6 @@
1
+ #= require firehose/json2
1
2
  #= require firehose/base
2
3
  #= require firehose/transport
3
- #= require firehose/lib/jquery.cors.headers
4
4
  #= require firehose/long_poll
5
5
  #= require firehose/web_socket
6
6
  #= require firehose/consumer
@@ -24,7 +24,7 @@ class Firehose.Consumer
24
24
  # Do stuff before we send the message into config.message. The sensible
25
25
  # default on the webs is to parse JSON.
26
26
  config.parse ||= (body) ->
27
- $.parseJSON(body)
27
+ JSON.parse(body)
28
28
 
29
29
  # Hang on to these config for when we connect.
30
30
  @config = config
@@ -34,7 +34,7 @@ class Firehose.Consumer
34
34
  connect: =>
35
35
  # Get a list of transports that the browser supports
36
36
  supportedTransports = (transport for transport in @config.transports when transport.supported())
37
- # Mmmkay, we've got transports supported by the browser, now lets try connecting
37
+ # Mmmkay, we've got transports supported by the browser, now lets try connecting
38
38
  # to them and dealing with failed connections that might be caused by firewalls,
39
39
  # or other network connectivity issues.
40
40
  transports = for transport in supportedTransports
@@ -0,0 +1,487 @@
1
+ /*
2
+ json2.js
3
+ 2011-10-19
4
+
5
+ Public Domain.
6
+
7
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8
+
9
+ See http://www.JSON.org/js.html
10
+
11
+
12
+ This code should be minified before deployment.
13
+ See http://javascript.crockford.com/jsmin.html
14
+
15
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16
+ NOT CONTROL.
17
+
18
+
19
+ This file creates a global JSON object containing two methods: stringify
20
+ and parse.
21
+
22
+ JSON.stringify(value, replacer, space)
23
+ value any JavaScript value, usually an object or array.
24
+
25
+ replacer an optional parameter that determines how object
26
+ values are stringified for objects. It can be a
27
+ function or an array of strings.
28
+
29
+ space an optional parameter that specifies the indentation
30
+ of nested structures. If it is omitted, the text will
31
+ be packed without extra whitespace. If it is a number,
32
+ it will specify the number of spaces to indent at each
33
+ level. If it is a string (such as '\t' or ' '),
34
+ it contains the characters used to indent at each level.
35
+
36
+ This method produces a JSON text from a JavaScript value.
37
+
38
+ When an object value is found, if the object contains a toJSON
39
+ method, its toJSON method will be called and the result will be
40
+ stringified. A toJSON method does not serialize: it returns the
41
+ value represented by the name/value pair that should be serialized,
42
+ or undefined if nothing should be serialized. The toJSON method
43
+ will be passed the key associated with the value, and this will be
44
+ bound to the value
45
+
46
+ For example, this would serialize Dates as ISO strings.
47
+
48
+ Date.prototype.toJSON = function (key) {
49
+ function f(n) {
50
+ // Format integers to have at least two digits.
51
+ return n < 10 ? '0' + n : n;
52
+ }
53
+
54
+ return this.getUTCFullYear() + '-' +
55
+ f(this.getUTCMonth() + 1) + '-' +
56
+ f(this.getUTCDate()) + 'T' +
57
+ f(this.getUTCHours()) + ':' +
58
+ f(this.getUTCMinutes()) + ':' +
59
+ f(this.getUTCSeconds()) + 'Z';
60
+ };
61
+
62
+ You can provide an optional replacer method. It will be passed the
63
+ key and value of each member, with this bound to the containing
64
+ object. The value that is returned from your method will be
65
+ serialized. If your method returns undefined, then the member will
66
+ be excluded from the serialization.
67
+
68
+ If the replacer parameter is an array of strings, then it will be
69
+ used to select the members to be serialized. It filters the results
70
+ such that only members with keys listed in the replacer array are
71
+ stringified.
72
+
73
+ Values that do not have JSON representations, such as undefined or
74
+ functions, will not be serialized. Such values in objects will be
75
+ dropped; in arrays they will be replaced with null. You can use
76
+ a replacer function to replace those with JSON values.
77
+ JSON.stringify(undefined) returns undefined.
78
+
79
+ The optional space parameter produces a stringification of the
80
+ value that is filled with line breaks and indentation to make it
81
+ easier to read.
82
+
83
+ If the space parameter is a non-empty string, then that string will
84
+ be used for indentation. If the space parameter is a number, then
85
+ the indentation will be that many spaces.
86
+
87
+ Example:
88
+
89
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
90
+ // text is '["e",{"pluribus":"unum"}]'
91
+
92
+
93
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95
+
96
+ text = JSON.stringify([new Date()], function (key, value) {
97
+ return this[key] instanceof Date ?
98
+ 'Date(' + this[key] + ')' : value;
99
+ });
100
+ // text is '["Date(---current time---)"]'
101
+
102
+
103
+ JSON.parse(text, reviver)
104
+ This method parses a JSON text to produce an object or array.
105
+ It can throw a SyntaxError exception.
106
+
107
+ The optional reviver parameter is a function that can filter and
108
+ transform the results. It receives each of the keys and values,
109
+ and its return value is used instead of the original value.
110
+ If it returns what it received, then the structure is not modified.
111
+ If it returns undefined then the member is deleted.
112
+
113
+ Example:
114
+
115
+ // Parse the text. Values that look like ISO date strings will
116
+ // be converted to Date objects.
117
+
118
+ myData = JSON.parse(text, function (key, value) {
119
+ var a;
120
+ if (typeof value === 'string') {
121
+ a =
122
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123
+ if (a) {
124
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125
+ +a[5], +a[6]));
126
+ }
127
+ }
128
+ return value;
129
+ });
130
+
131
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132
+ var d;
133
+ if (typeof value === 'string' &&
134
+ value.slice(0, 5) === 'Date(' &&
135
+ value.slice(-1) === ')') {
136
+ d = new Date(value.slice(5, -1));
137
+ if (d) {
138
+ return d;
139
+ }
140
+ }
141
+ return value;
142
+ });
143
+
144
+
145
+ This is a reference implementation. You are free to copy, modify, or
146
+ redistribute.
147
+ */
148
+
149
+ /*jslint evil: true, regexp: true */
150
+
151
+ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152
+ call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154
+ lastIndex, length, parse, prototype, push, replace, slice, stringify,
155
+ test, toJSON, toString, valueOf
156
+ */
157
+
158
+
159
+ // Create a JSON object only if one does not already exist. We create the
160
+ // methods in a closure to avoid creating global variables.
161
+
162
+ var JSON;
163
+ if (!JSON) {
164
+ JSON = {};
165
+ }
166
+
167
+ (function () {
168
+ 'use strict';
169
+
170
+ function f(n) {
171
+ // Format integers to have at least two digits.
172
+ return n < 10 ? '0' + n : n;
173
+ }
174
+
175
+ if (typeof Date.prototype.toJSON !== 'function') {
176
+
177
+ Date.prototype.toJSON = function (key) {
178
+
179
+ return isFinite(this.valueOf())
180
+ ? this.getUTCFullYear() + '-' +
181
+ f(this.getUTCMonth() + 1) + '-' +
182
+ f(this.getUTCDate()) + 'T' +
183
+ f(this.getUTCHours()) + ':' +
184
+ f(this.getUTCMinutes()) + ':' +
185
+ f(this.getUTCSeconds()) + 'Z'
186
+ : null;
187
+ };
188
+
189
+ String.prototype.toJSON =
190
+ Number.prototype.toJSON =
191
+ Boolean.prototype.toJSON = function (key) {
192
+ return this.valueOf();
193
+ };
194
+ }
195
+
196
+ var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
197
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
198
+ gap,
199
+ indent,
200
+ meta = { // table of character substitutions
201
+ '\b': '\\b',
202
+ '\t': '\\t',
203
+ '\n': '\\n',
204
+ '\f': '\\f',
205
+ '\r': '\\r',
206
+ '"' : '\\"',
207
+ '\\': '\\\\'
208
+ },
209
+ rep;
210
+
211
+
212
+ function quote(string) {
213
+
214
+ // If the string contains no control characters, no quote characters, and no
215
+ // backslash characters, then we can safely slap some quotes around it.
216
+ // Otherwise we must also replace the offending characters with safe escape
217
+ // sequences.
218
+
219
+ escapable.lastIndex = 0;
220
+ return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
221
+ var c = meta[a];
222
+ return typeof c === 'string'
223
+ ? c
224
+ : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
225
+ }) + '"' : '"' + string + '"';
226
+ }
227
+
228
+
229
+ function str(key, holder) {
230
+
231
+ // Produce a string from holder[key].
232
+
233
+ var i, // The loop counter.
234
+ k, // The member key.
235
+ v, // The member value.
236
+ length,
237
+ mind = gap,
238
+ partial,
239
+ value = holder[key];
240
+
241
+ // If the value has a toJSON method, call it to obtain a replacement value.
242
+
243
+ if (value && typeof value === 'object' &&
244
+ typeof value.toJSON === 'function') {
245
+ value = value.toJSON(key);
246
+ }
247
+
248
+ // If we were called with a replacer function, then call the replacer to
249
+ // obtain a replacement value.
250
+
251
+ if (typeof rep === 'function') {
252
+ value = rep.call(holder, key, value);
253
+ }
254
+
255
+ // What happens next depends on the value's type.
256
+
257
+ switch (typeof value) {
258
+ case 'string':
259
+ return quote(value);
260
+
261
+ case 'number':
262
+
263
+ // JSON numbers must be finite. Encode non-finite numbers as null.
264
+
265
+ return isFinite(value) ? String(value) : 'null';
266
+
267
+ case 'boolean':
268
+ case 'null':
269
+
270
+ // If the value is a boolean or null, convert it to a string. Note:
271
+ // typeof null does not produce 'null'. The case is included here in
272
+ // the remote chance that this gets fixed someday.
273
+
274
+ return String(value);
275
+
276
+ // If the type is 'object', we might be dealing with an object or an array or
277
+ // null.
278
+
279
+ case 'object':
280
+
281
+ // Due to a specification blunder in ECMAScript, typeof null is 'object',
282
+ // so watch out for that case.
283
+
284
+ if (!value) {
285
+ return 'null';
286
+ }
287
+
288
+ // Make an array to hold the partial results of stringifying this object value.
289
+
290
+ gap += indent;
291
+ partial = [];
292
+
293
+ // Is the value an array?
294
+
295
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
296
+
297
+ // The value is an array. Stringify every element. Use null as a placeholder
298
+ // for non-JSON values.
299
+
300
+ length = value.length;
301
+ for (i = 0; i < length; i += 1) {
302
+ partial[i] = str(i, value) || 'null';
303
+ }
304
+
305
+ // Join all of the elements together, separated with commas, and wrap them in
306
+ // brackets.
307
+
308
+ v = partial.length === 0
309
+ ? '[]'
310
+ : gap
311
+ ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
312
+ : '[' + partial.join(',') + ']';
313
+ gap = mind;
314
+ return v;
315
+ }
316
+
317
+ // If the replacer is an array, use it to select the members to be stringified.
318
+
319
+ if (rep && typeof rep === 'object') {
320
+ length = rep.length;
321
+ for (i = 0; i < length; i += 1) {
322
+ if (typeof rep[i] === 'string') {
323
+ k = rep[i];
324
+ v = str(k, value);
325
+ if (v) {
326
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
327
+ }
328
+ }
329
+ }
330
+ } else {
331
+
332
+ // Otherwise, iterate through all of the keys in the object.
333
+
334
+ for (k in value) {
335
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
336
+ v = str(k, value);
337
+ if (v) {
338
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ // Join all of the member texts together, separated with commas,
345
+ // and wrap them in braces.
346
+
347
+ v = partial.length === 0
348
+ ? '{}'
349
+ : gap
350
+ ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
351
+ : '{' + partial.join(',') + '}';
352
+ gap = mind;
353
+ return v;
354
+ }
355
+ }
356
+
357
+ // If the JSON object does not yet have a stringify method, give it one.
358
+
359
+ if (typeof JSON.stringify !== 'function') {
360
+ JSON.stringify = function (value, replacer, space) {
361
+
362
+ // The stringify method takes a value and an optional replacer, and an optional
363
+ // space parameter, and returns a JSON text. The replacer can be a function
364
+ // that can replace values, or an array of strings that will select the keys.
365
+ // A default replacer method can be provided. Use of the space parameter can
366
+ // produce text that is more easily readable.
367
+
368
+ var i;
369
+ gap = '';
370
+ indent = '';
371
+
372
+ // If the space parameter is a number, make an indent string containing that
373
+ // many spaces.
374
+
375
+ if (typeof space === 'number') {
376
+ for (i = 0; i < space; i += 1) {
377
+ indent += ' ';
378
+ }
379
+
380
+ // If the space parameter is a string, it will be used as the indent string.
381
+
382
+ } else if (typeof space === 'string') {
383
+ indent = space;
384
+ }
385
+
386
+ // If there is a replacer, it must be a function or an array.
387
+ // Otherwise, throw an error.
388
+
389
+ rep = replacer;
390
+ if (replacer && typeof replacer !== 'function' &&
391
+ (typeof replacer !== 'object' ||
392
+ typeof replacer.length !== 'number')) {
393
+ throw new Error('JSON.stringify');
394
+ }
395
+
396
+ // Make a fake root object containing our value under the key of ''.
397
+ // Return the result of stringifying the value.
398
+
399
+ return str('', {'': value});
400
+ };
401
+ }
402
+
403
+
404
+ // If the JSON object does not yet have a parse method, give it one.
405
+
406
+ if (typeof JSON.parse !== 'function') {
407
+ JSON.parse = function (text, reviver) {
408
+
409
+ // The parse method takes a text and an optional reviver function, and returns
410
+ // a JavaScript value if the text is a valid JSON text.
411
+
412
+ var j;
413
+
414
+ function walk(holder, key) {
415
+
416
+ // The walk method is used to recursively walk the resulting structure so
417
+ // that modifications can be made.
418
+
419
+ var k, v, value = holder[key];
420
+ if (value && typeof value === 'object') {
421
+ for (k in value) {
422
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
423
+ v = walk(value, k);
424
+ if (v !== undefined) {
425
+ value[k] = v;
426
+ } else {
427
+ delete value[k];
428
+ }
429
+ }
430
+ }
431
+ }
432
+ return reviver.call(holder, key, value);
433
+ }
434
+
435
+
436
+ // Parsing happens in four stages. In the first stage, we replace certain
437
+ // Unicode characters with escape sequences. JavaScript handles many characters
438
+ // incorrectly, either silently deleting them, or treating them as line endings.
439
+
440
+ text = String(text);
441
+ cx.lastIndex = 0;
442
+ if (cx.test(text)) {
443
+ text = text.replace(cx, function (a) {
444
+ return '\\u' +
445
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
446
+ });
447
+ }
448
+
449
+ // In the second stage, we run the text against regular expressions that look
450
+ // for non-JSON patterns. We are especially concerned with '()' and 'new'
451
+ // because they can cause invocation, and '=' because it can cause mutation.
452
+ // But just to be safe, we want to reject all unexpected forms.
453
+
454
+ // We split the second stage into 4 regexp operations in order to work around
455
+ // crippling inefficiencies in IE's and Safari's regexp engines. First we
456
+ // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
457
+ // replace all simple value tokens with ']' characters. Third, we delete all
458
+ // open brackets that follow a colon or comma or that begin the text. Finally,
459
+ // we look to see that the remaining characters are only whitespace or ']' or
460
+ // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
461
+
462
+ if (/^[\],:{}\s]*$/
463
+ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
464
+ .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
465
+ .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
466
+
467
+ // In the third stage we use the eval function to compile the text into a
468
+ // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
469
+ // in JavaScript: it can begin a block or an object literal. We wrap the text
470
+ // in parens to eliminate the ambiguity.
471
+
472
+ j = eval('(' + text + ')');
473
+
474
+ // In the optional fourth stage, we recursively walk the new structure, passing
475
+ // each name/value pair to a reviver function for possible transformation.
476
+
477
+ return typeof reviver === 'function'
478
+ ? walk({'': j}, '')
479
+ : j;
480
+ }
481
+
482
+ // If the text is not JSON parseable, then a SyntaxError is thrown.
483
+
484
+ throw new SyntaxError('JSON.parse');
485
+ };
486
+ }
487
+ }());
@@ -7,7 +7,8 @@ class Firehose.LongPoll extends Firehose.Transport
7
7
 
8
8
  @supported: =>
9
9
  # IE 8+, FF 3.5+, Chrome 4+, Safari 4+, Opera 12+, iOS 3.2+, Android 2.1+
10
- $.support.cors || Firehose.LongPoll.ieSupported()
10
+ if xhr = $.ajaxSettings.xhr()
11
+ "withCredentials" of xhr || Firehose.LongPoll.ieSupported()
11
12
 
12
13
  constructor: (args) ->
13
14
  super args
@@ -26,7 +27,7 @@ class Firehose.LongPoll extends Firehose.Transport
26
27
  @_okInterval = 0
27
28
 
28
29
  @_isConnected = false
29
-
30
+
30
31
  connect: (delay = 0) =>
31
32
  unless @_isConnected
32
33
  @_isConnected = true
@@ -41,12 +42,34 @@ class Firehose.LongPoll extends Firehose.Transport
41
42
  data.last_message_sequence = @_lastMessageSequence
42
43
  # TODO: Some of these options will be deprecated in jQurey 1.8
43
44
  # See: http://api.jquery.com/jQuery.ajax/#jqXHR
45
+
44
46
  $.ajax @config.longPoll.url,
45
47
  crossDomain: true
46
48
  data: data
47
49
  timeout: @_timeout
48
50
  success: @_success
49
51
  error: @_error
52
+ xhr: ->
53
+ # TODO - This while `xrh` attr is a stupid hack to deal with CORS short-comings in jQuery in Firefox.
54
+ # This ticket can be viewed at http://bugs.jquery.com/ticket/10338. Once jQuery is
55
+ # upgraded to this version, we can probably remove this, but be sure you test the
56
+ # crap out of Firefox!
57
+ #
58
+ # Its also worth noting that I had to localize this monkey-patch to the Firehose.LongPoll
59
+ # consumer because a previous global patch on jQuery.ajaxSettings.xhr was breaking regular IE7
60
+ # loading. I figured its better to localize this anyway to solve that problem and loading order issues.
61
+ xhr = jQuery.ajaxSettings.xhr()
62
+ getAllResponseHeaders = xhr.getAllResponseHeaders
63
+ xhr.getAllResponseHeaders = ->
64
+ allHeaders = getAllResponseHeaders.call(xhr)
65
+ return allHeaders if allHeaders
66
+ allHeaders = ""
67
+ for headerName in [ "Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma" ]
68
+ do (headerName) ->
69
+ allHeaders += headerName + ": " + xhr.getResponseHeader(headerName) + "\n" if xhr.getResponseHeader(headerName)
70
+ allHeaders
71
+ xhr
72
+
50
73
  complete: (jqXhr) =>
51
74
  # Get the last sequence from the server if specified.
52
75
  if jqXhr.status == 200
@@ -62,7 +85,7 @@ class Firehose.LongPoll extends Firehose.Transport
62
85
  if jqXhr.status == 204
63
86
  # If we get a 204 back, that means the server timed-out and sent back a 204 with a
64
87
  # X-Http-Next-Request header
65
- #
88
+ #
66
89
  # Why did we use a 204 and not a 408? Because FireFox is really stupid about 400 level error
67
90
  # codes and would claims its a 0 error code, which we use for something else. Firefox is IE
68
91
  # in this case
@@ -86,7 +109,7 @@ class Firehose.LongPoll extends Firehose.Transport
86
109
  _error: (jqXhr, status, error) =>
87
110
  @_isConnected = false
88
111
  @config.disconnected()
89
-
112
+
90
113
  # Ping the server to make sure this isn't a network connectivity error
91
114
  setTimeout @_ping, @_retryDelay + @_lagTime
92
115
 
data/lib/firehose.rb CHANGED
@@ -1,19 +1,25 @@
1
- ENV['RACK_ENV'] ||= 'development'
1
+ ENV['RACK_ENV'] ||= 'development' # TODO - Lets not rock out envs like its 1999.
2
2
 
3
3
  require 'firehose/version'
4
4
  require 'em-hiredis' # TODO Move this into a Redis module so that we can auto-load it. Lots of the CLI tools don't need this.
5
5
  require 'firehose/logging'
6
- require 'firehose/rails' if defined?(::Rails::Engine) # TODO Detect Sprockets instead of the jankin Rails::Engine test.
6
+
7
+ # TODO - Figure if we need to have an if/else for Rails::Engine loading and Firehose::Assets::Sprockets.auto_detect
8
+ require 'firehose/rails' if defined?(::Rails::Engine)
7
9
 
8
10
  module Firehose
9
11
  autoload :Subscriber, 'firehose/subscriber'
10
12
  autoload :Publisher, 'firehose/publisher'
11
13
  autoload :Producer, 'firehose/producer' # TODO Move this into the Firehose::Client namespace.
12
14
  autoload :Default, 'firehose/default'
15
+ autoload :Assets, 'firehose/assets'
13
16
  autoload :Rack, 'firehose/rack'
14
17
  autoload :CLI, 'firehose/cli'
15
18
  autoload :Client, 'firehose/client'
16
19
  autoload :Server, 'firehose/server'
17
20
  autoload :Channel, 'firehose/channel'
18
21
  autoload :SwfPolicyRequest, 'firehose/swf_policy_request'
19
- end
22
+ end
23
+
24
+ # Detect if Sprockets is loaded. If it is, lets configure Firehose to use it!
25
+ Firehose::Assets::Sprockets.auto_detect
@@ -0,0 +1,24 @@
1
+ module Firehose
2
+ # Deal with bundling Sprocket assets into environments (like Rails or Sprockets)
3
+ module Assets
4
+ def self.path(*segs)
5
+ File.join File.expand_path('../../assets', __FILE__), segs
6
+ end
7
+
8
+ module Sprockets
9
+ # Drop flash and javascript paths to Firehose assets into a sprockets environment.
10
+ def self.configure(env)
11
+ env.append_path Firehose::Assets.path('flash')
12
+ env.append_path Firehose::Assets.path('javascripts')
13
+ env
14
+ end
15
+
16
+ # Try to automatically configure Sprockets if its detected in the project.
17
+ def self.auto_detect
18
+ if defined? ::Sprockets and ::Sprockets.respond_to? :append_path
19
+ Firehose::Assets::Sprockets.configure ::Sprockets
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/firehose/cli.rb CHANGED
@@ -41,26 +41,29 @@ module Firehose
41
41
  end
42
42
 
43
43
  desc "publish URI [PAYLOAD]", "Publish messages to a resource."
44
+ method_option :ttl, :type => :numeric, :aliases => '-t'
45
+ method_option :times, :type => :numeric, :aliases => '-n', :default => 1
44
46
  method_option :interval, :type => :numeric, :aliases => '-i'
45
- method_option :times, :type => :numeric, :aliases => '-n'
47
+
46
48
  def publish(uri, payload=nil)
47
49
  payload ||= $stdin.read
48
50
  client = Firehose::Producer.new(uri)
49
51
  path = URI.parse(uri).path
50
52
  times = options[:times]
53
+ ttl = options[:ttl]
51
54
 
52
55
  EM.run do
53
56
  # TODO I think this can be cleaned up so the top-level if/else can be ditched.
54
57
  if interval = options[:interval]
55
58
  # Publish messages at a forced interval.
56
- EM.add_periodic_timer interval do
57
- client.publish(payload).to(path)
59
+ EM.add_periodic_timer interval do
60
+ client.publish(payload).to(path, :ttl => ttl)
58
61
  EM.stop if times && (times-=1).zero?
59
62
  end
60
63
  else
61
64
  # Publish messages as soon as the last message was published.
62
65
  worker = Proc.new do
63
- client.publish(payload).to(path)
66
+ client.publish(payload).to(path, :ttl => ttl)
64
67
  times && (times-=1).zero? ? EM.stop : worker.call
65
68
  end
66
69
  worker.call
@@ -20,8 +20,8 @@ module Firehose
20
20
  self
21
21
  end
22
22
 
23
- def to(channel, &callback)
24
- @producer.put(@message, channel, &callback)
23
+ def to(channel, opts={}, &callback)
24
+ @producer.put(@message, channel, opts, &callback)
25
25
  end
26
26
  end
27
27
 
@@ -39,11 +39,14 @@ module Firehose
39
39
  end
40
40
 
41
41
  # Publish the message via HTTP.
42
- def put(message, channel, &block)
42
+ def put(message, channel, opts, &block)
43
+ ttl = opts[:ttl]
44
+
43
45
  response = conn.put do |req|
44
46
  req.options[:timeout] = Timeout
45
47
  req.path = channel
46
48
  req.body = message
49
+ req.headers['Cache-Control'] = "max-age=#{ttl.to_i}" if ttl
47
50
  end
48
51
  response.on_complete do
49
52
  case response.status
@@ -4,7 +4,10 @@ module Firehose
4
4
  TTL = 60*60*24 # 1 day of time, yay!
5
5
  PAYLOAD_DELIMITER = "\n"
6
6
 
7
- def publish(channel_key, message)
7
+ def publish(channel_key, message, opts={})
8
+ # How long should we hang on to the resource once is published?
9
+ ttl = (opts[:ttl] || TTL).to_i
10
+
8
11
  # TODO hi-redis isn't that awesome... we have to setup an errback per even for wrong
9
12
  # commands because of the lack of a method_missing whitelist. Perhaps implement a whitelist in
10
13
  # em-hiredis or us a diff lib?
@@ -22,10 +25,10 @@ module Firehose
22
25
  end
23
26
  local sequence = current_sequence + 1
24
27
  redis.call('set', KEYS[1], sequence)
25
- redis.call('expire', KEYS[1], #{TTL})
28
+ redis.call('expire', KEYS[1], #{ttl})
26
29
  redis.call('lpush', KEYS[2], "#{lua_escape(message)}")
27
30
  redis.call('ltrim', KEYS[2], 0, #{MAX_MESSAGES - 1})
28
- redis.call('expire', KEYS[2], #{TTL})
31
+ redis.call('expire', KEYS[2], #{ttl})
29
32
  redis.call('publish', KEYS[3], "#{lua_escape(channel_key + PAYLOAD_DELIMITER)}" .. sequence .. "#{lua_escape(PAYLOAD_DELIMITER + message)}")
30
33
  return sequence
31
34
  ), 3, sequence_key, list_key, key(:channel_updates)).
@@ -7,12 +7,26 @@ module Firehose
7
7
  req = env['parsed_request'] ||= ::Rack::Request.new(env)
8
8
  path = req.path
9
9
  method = req.request_method
10
+ cache_control = {}
11
+
12
+ # Parse out cache control directives from the Cache-Control header.
13
+ if cache_control_header = env['HTTP_CACHE_CONTROL']
14
+ cache_control = cache_control_header.split(',').map(&:strip).inject({}) do |memo, directive|
15
+ key, value = directive.split('=')
16
+ memo[key.downcase] = value
17
+ memo
18
+ end
19
+ end
20
+
21
+ # Read the max-age directive from the cache so that we can set a TTL on the redis key. This will
22
+ # prevent stale content from being served up to the client.
23
+ ttl = cache_control['max-age']
10
24
 
11
25
  if method == 'PUT'
12
26
  EM.next_tick do
13
27
  body = env['rack.input'].read
14
- Firehose.logger.debug "HTTP published `#{body}` to `#{path}`"
15
- publisher.publish(path, body).callback do
28
+ Firehose.logger.debug "HTTP published #{body.inspect} to #{path.inspect} with ttl #{ttl.inspect}"
29
+ publisher.publish(path, body, :ttl => ttl).callback do
16
30
  env['async.callback'].call [202, {'Content-Type' => 'text/plain', 'Content-Length' => '0'}, []]
17
31
  env['async.callback'].call response(202, '', 'Content-Type' => 'text/plain')
18
32
  end.errback do |e|
@@ -30,8 +44,7 @@ module Firehose
30
44
  end
31
45
  end
32
46
 
33
-
34
- private
47
+ private
35
48
  def publisher
36
49
  @publisher ||= Firehose::Publisher.new
37
50
  end
@@ -1,4 +1,4 @@
1
1
  module Firehose
2
- VERSION = "0.2.alpha.3"
3
- CODENAME = "Timeout!"
2
+ VERSION = "0.2.alpha.5"
3
+ CODENAME = "Time To Live (it up?)"
4
4
  end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+ require 'sprockets'
3
+
4
+ describe Firehose::Assets do
5
+ describe ".path" do
6
+ it "should have root path" do
7
+ Firehose::Assets.path('poop').should == File.expand_path('../../../lib/assets/poop', __FILE__)
8
+ end
9
+
10
+ it "should accept folders" do
11
+ Firehose::Assets.path('poop').should == File.join(Firehose::Assets.path, 'poop')
12
+ end
13
+ end
14
+
15
+ describe "Sprockets.configure" do
16
+ it "should configure environment" do
17
+ env = Firehose::Assets::Sprockets.configure Sprockets::Environment.new
18
+ env.paths.should include(Firehose::Assets.path('flash'), Firehose::Assets.path('javascripts'))
19
+ end
20
+ end
21
+ end
@@ -21,6 +21,14 @@ describe Firehose::Producer do
21
21
  WebMock.should have_requested(:put, url).with { |req| req.body == message }
22
22
  end
23
23
 
24
+ it "should publish message to channel with expiry headers" do
25
+ publish_stub.to_return(:body => "", :status => 202)
26
+ ttl = 20
27
+
28
+ Firehose::Producer.new.publish(message).to(channel, :ttl => ttl)
29
+ WebMock.should have_requested(:put, url).with { |req| req.body == message and req.headers['Cache-Control'] == "max-age=#{ttl}" }
30
+ end
31
+
24
32
  describe "connection error handling" do
25
33
  it "should raise PublishError if not 201" do
26
34
  publish_stub.to_return(:body => "", :status => 500)
@@ -28,7 +28,6 @@ describe Firehose::Publisher do
28
28
  end
29
29
  end
30
30
 
31
-
32
31
  "\"'\r\t\n!@\#$%^&*()[]\v\f\a\b\e{}/=?+\\|".each_char do |char|
33
32
  it "should publish messages with the '#{char.inspect}' character" do
34
33
  msg = [char, message, char].join
@@ -69,14 +68,17 @@ describe Firehose::Publisher do
69
68
  end
70
69
 
71
70
  it "should set expirery on sequence and list keys" do
71
+ ttl = 78 # Why 78? Why not!
72
+
72
73
  em do
73
- publisher.publish(channel_key, message).callback do
74
+ publisher.publish(channel_key, message, :ttl => 78).callback do
74
75
  # Allow for 1 second of slippage/delay
75
- redis_exec('TTL', "firehose:#{channel_key}:sequence").should > (Firehose::Publisher::TTL - 1)
76
- redis_exec('TTL', "firehose:#{channel_key}:list").should > (Firehose::Publisher::TTL - 1)
76
+ redis_exec('TTL', "firehose:#{channel_key}:sequence").should > (ttl- 1)
77
+ redis_exec('TTL', "firehose:#{channel_key}:list").should > (ttl - 1)
77
78
  em.stop
78
79
  end
79
80
  end
80
81
  end
82
+
81
83
  end
82
84
  end
@@ -22,5 +22,13 @@ describe Firehose::Rack::PublisherApp, :type => :request do
22
22
  aput path, :body => "some nice little message"
23
23
  last_response.headers['Content-Length'].should == '0'
24
24
  end
25
+
26
+ it "should parse Cache-Control max-age" do
27
+ body = "howdy dude!"
28
+ ttl = '92'
29
+
30
+ Firehose::Publisher.any_instance.stub(:publish).with(path, body, :ttl => ttl).and_return(deferrable)
31
+ aput path, body, 'HTTP_CACHE_CONTROL' => 'max-age=92'
32
+ end
25
33
  end
26
- end
34
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: firehose
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.alpha.3
4
+ version: 0.2.alpha.5
5
5
  prerelease: 4
6
6
  platform: ruby
7
7
  authors:
@@ -11,11 +11,11 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-07-20 00:00:00.000000000 Z
14
+ date: 2012-08-12 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: eventmachine
18
- requirement: &70273542312980 !ruby/object:Gem::Requirement
18
+ requirement: &70227705301260 !ruby/object:Gem::Requirement
19
19
  none: false
20
20
  requirements:
21
21
  - - ! '>='
@@ -23,10 +23,10 @@ dependencies:
23
23
  version: 1.0.0.rc
24
24
  type: :runtime
25
25
  prerelease: false
26
- version_requirements: *70273542312980
26
+ version_requirements: *70227705301260
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: em-hiredis
29
- requirement: &70273542312560 !ruby/object:Gem::Requirement
29
+ requirement: &70227705300800 !ruby/object:Gem::Requirement
30
30
  none: false
31
31
  requirements:
32
32
  - - ! '>='
@@ -34,10 +34,10 @@ dependencies:
34
34
  version: '0'
35
35
  type: :runtime
36
36
  prerelease: false
37
- version_requirements: *70273542312560
37
+ version_requirements: *70227705300800
38
38
  - !ruby/object:Gem::Dependency
39
39
  name: thor
40
- requirement: &70273542312100 !ruby/object:Gem::Requirement
40
+ requirement: &70227705300260 !ruby/object:Gem::Requirement
41
41
  none: false
42
42
  requirements:
43
43
  - - ! '>='
@@ -45,10 +45,10 @@ dependencies:
45
45
  version: '0'
46
46
  type: :runtime
47
47
  prerelease: false
48
- version_requirements: *70273542312100
48
+ version_requirements: *70227705300260
49
49
  - !ruby/object:Gem::Dependency
50
50
  name: faraday
51
- requirement: &70273542311680 !ruby/object:Gem::Requirement
51
+ requirement: &70227705299800 !ruby/object:Gem::Requirement
52
52
  none: false
53
53
  requirements:
54
54
  - - ! '>='
@@ -56,10 +56,10 @@ dependencies:
56
56
  version: '0'
57
57
  type: :runtime
58
58
  prerelease: false
59
- version_requirements: *70273542311680
59
+ version_requirements: *70227705299800
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: faye-websocket
62
- requirement: &70273542311260 !ruby/object:Gem::Requirement
62
+ requirement: &70227705299260 !ruby/object:Gem::Requirement
63
63
  none: false
64
64
  requirements:
65
65
  - - ! '>='
@@ -67,10 +67,10 @@ dependencies:
67
67
  version: '0'
68
68
  type: :runtime
69
69
  prerelease: false
70
- version_requirements: *70273542311260
70
+ version_requirements: *70227705299260
71
71
  - !ruby/object:Gem::Dependency
72
72
  name: em-http-request
73
- requirement: &70273542310760 !ruby/object:Gem::Requirement
73
+ requirement: &70227705298620 !ruby/object:Gem::Requirement
74
74
  none: false
75
75
  requirements:
76
76
  - - ~>
@@ -78,10 +78,10 @@ dependencies:
78
78
  version: 1.0.0
79
79
  type: :runtime
80
80
  prerelease: false
81
- version_requirements: *70273542310760
81
+ version_requirements: *70227705298620
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: rspec
84
- requirement: &70273542310340 !ruby/object:Gem::Requirement
84
+ requirement: &70227705297680 !ruby/object:Gem::Requirement
85
85
  none: false
86
86
  requirements:
87
87
  - - ! '>='
@@ -89,10 +89,10 @@ dependencies:
89
89
  version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
- version_requirements: *70273542310340
92
+ version_requirements: *70227705297680
93
93
  - !ruby/object:Gem::Dependency
94
94
  name: webmock
95
- requirement: &70273542309880 !ruby/object:Gem::Requirement
95
+ requirement: &70227705340680 !ruby/object:Gem::Requirement
96
96
  none: false
97
97
  requirements:
98
98
  - - ! '>='
@@ -100,10 +100,10 @@ dependencies:
100
100
  version: '0'
101
101
  type: :development
102
102
  prerelease: false
103
- version_requirements: *70273542309880
103
+ version_requirements: *70227705340680
104
104
  - !ruby/object:Gem::Dependency
105
105
  name: guard-rspec
106
- requirement: &70273542309460 !ruby/object:Gem::Requirement
106
+ requirement: &70227705340080 !ruby/object:Gem::Requirement
107
107
  none: false
108
108
  requirements:
109
109
  - - ! '>='
@@ -111,10 +111,10 @@ dependencies:
111
111
  version: '0'
112
112
  type: :development
113
113
  prerelease: false
114
- version_requirements: *70273542309460
114
+ version_requirements: *70227705340080
115
115
  - !ruby/object:Gem::Dependency
116
116
  name: guard-bundler
117
- requirement: &70273542309040 !ruby/object:Gem::Requirement
117
+ requirement: &70227705339180 !ruby/object:Gem::Requirement
118
118
  none: false
119
119
  requirements:
120
120
  - - ! '>='
@@ -122,10 +122,10 @@ dependencies:
122
122
  version: '0'
123
123
  type: :development
124
124
  prerelease: false
125
- version_requirements: *70273542309040
125
+ version_requirements: *70227705339180
126
126
  - !ruby/object:Gem::Dependency
127
127
  name: guard-coffeescript
128
- requirement: &70273542308620 !ruby/object:Gem::Requirement
128
+ requirement: &70227705338480 !ruby/object:Gem::Requirement
129
129
  none: false
130
130
  requirements:
131
131
  - - ! '>='
@@ -133,10 +133,10 @@ dependencies:
133
133
  version: '0'
134
134
  type: :development
135
135
  prerelease: false
136
- version_requirements: *70273542308620
136
+ version_requirements: *70227705338480
137
137
  - !ruby/object:Gem::Dependency
138
138
  name: rainbows
139
- requirement: &70273542308200 !ruby/object:Gem::Requirement
139
+ requirement: &70227705337780 !ruby/object:Gem::Requirement
140
140
  none: false
141
141
  requirements:
142
142
  - - ! '>='
@@ -144,10 +144,10 @@ dependencies:
144
144
  version: '0'
145
145
  type: :development
146
146
  prerelease: false
147
- version_requirements: *70273542308200
147
+ version_requirements: *70227705337780
148
148
  - !ruby/object:Gem::Dependency
149
149
  name: thin
150
- requirement: &70273542307780 !ruby/object:Gem::Requirement
150
+ requirement: &70227705337360 !ruby/object:Gem::Requirement
151
151
  none: false
152
152
  requirements:
153
153
  - - ! '>='
@@ -155,10 +155,10 @@ dependencies:
155
155
  version: '0'
156
156
  type: :development
157
157
  prerelease: false
158
- version_requirements: *70273542307780
158
+ version_requirements: *70227705337360
159
159
  - !ruby/object:Gem::Dependency
160
160
  name: rack-test
161
- requirement: &70273542307360 !ruby/object:Gem::Requirement
161
+ requirement: &70227705336940 !ruby/object:Gem::Requirement
162
162
  none: false
163
163
  requirements:
164
164
  - - ! '>='
@@ -166,10 +166,10 @@ dependencies:
166
166
  version: '0'
167
167
  type: :development
168
168
  prerelease: false
169
- version_requirements: *70273542307360
169
+ version_requirements: *70227705336940
170
170
  - !ruby/object:Gem::Dependency
171
171
  name: async_rack_test
172
- requirement: &70273542306940 !ruby/object:Gem::Requirement
172
+ requirement: &70227705336480 !ruby/object:Gem::Requirement
173
173
  none: false
174
174
  requirements:
175
175
  - - ! '>='
@@ -177,10 +177,10 @@ dependencies:
177
177
  version: '0'
178
178
  type: :development
179
179
  prerelease: false
180
- version_requirements: *70273542306940
180
+ version_requirements: *70227705336480
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: foreman
183
- requirement: &70273542306520 !ruby/object:Gem::Requirement
183
+ requirement: &70227705336020 !ruby/object:Gem::Requirement
184
184
  none: false
185
185
  requirements:
186
186
  - - ! '>='
@@ -188,7 +188,18 @@ dependencies:
188
188
  version: '0'
189
189
  type: :development
190
190
  prerelease: false
191
- version_requirements: *70273542306520
191
+ version_requirements: *70227705336020
192
+ - !ruby/object:Gem::Dependency
193
+ name: sprockets
194
+ requirement: &70227705335600 !ruby/object:Gem::Requirement
195
+ none: false
196
+ requirements:
197
+ - - ! '>='
198
+ - !ruby/object:Gem::Version
199
+ version: '0'
200
+ type: :development
201
+ prerelease: false
202
+ version_requirements: *70227705335600
192
203
  description: Firehose is a realtime web application toolkit for building realtime
193
204
  Ruby web applications.
194
205
  email:
@@ -216,13 +227,14 @@ files:
216
227
  - lib/assets/javascripts/firehose.js.coffee
217
228
  - lib/assets/javascripts/firehose/base.js.coffee
218
229
  - lib/assets/javascripts/firehose/consumer.js.coffee
219
- - lib/assets/javascripts/firehose/lib/jquery.cors.headers.js.coffee
230
+ - lib/assets/javascripts/firehose/json2.js
220
231
  - lib/assets/javascripts/firehose/lib/swfobject.js
221
232
  - lib/assets/javascripts/firehose/lib/web_socket.js
222
233
  - lib/assets/javascripts/firehose/long_poll.js.coffee
223
234
  - lib/assets/javascripts/firehose/transport.js.coffee
224
235
  - lib/assets/javascripts/firehose/web_socket.js.coffee
225
236
  - lib/firehose.rb
237
+ - lib/firehose/assets.rb
226
238
  - lib/firehose/channel.rb
227
239
  - lib/firehose/cli.rb
228
240
  - lib/firehose/client.rb
@@ -245,6 +257,7 @@ files:
245
257
  - spec/integrations/rainbows_spec.rb
246
258
  - spec/integrations/shared_examples.rb
247
259
  - spec/integrations/thin_spec.rb
260
+ - spec/lib/assets_spec.rb
248
261
  - spec/lib/broker_spec.rb
249
262
  - spec/lib/channel_spec.rb
250
263
  - spec/lib/client_spec.rb
@@ -286,6 +299,7 @@ test_files:
286
299
  - spec/integrations/rainbows_spec.rb
287
300
  - spec/integrations/shared_examples.rb
288
301
  - spec/integrations/thin_spec.rb
302
+ - spec/lib/assets_spec.rb
289
303
  - spec/lib/broker_spec.rb
290
304
  - spec/lib/channel_spec.rb
291
305
  - spec/lib/client_spec.rb
@@ -1,16 +0,0 @@
1
- _super = jQuery.ajaxSettings.xhr
2
-
3
- jQuery.ajaxSettings.xhr = ->
4
- xhr = _super()
5
- getAllResponseHeaders = xhr.getAllResponseHeaders
6
- xhr.getAllResponseHeaders = ->
7
- allHeaders = getAllResponseHeaders.call(xhr)
8
- return allHeaders if allHeaders
9
-
10
- allHeaders = ""
11
- for headerName in [ "Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma" ]
12
- do (headerName) ->
13
- allHeaders += headerName + ": " + xhr.getResponseHeader(headerName) + "\n" if xhr.getResponseHeader(headerName)
14
-
15
- allHeaders
16
- xhr