sockjs 0.2.1
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/LICENCE +19 -0
- data/README.textile +118 -0
- data/lib/meta-state.rb +151 -0
- data/lib/rack/sockjs.rb +173 -0
- data/lib/sockjs.rb +59 -0
- data/lib/sockjs/callbacks.rb +19 -0
- data/lib/sockjs/connection.rb +45 -0
- data/lib/sockjs/delayed-response-body.rb +99 -0
- data/lib/sockjs/duck-punch-rack-mount.rb +12 -0
- data/lib/sockjs/duck-punch-thin-response.rb +15 -0
- data/lib/sockjs/examples/protocol_conformance_test.rb +73 -0
- data/lib/sockjs/faye.rb +15 -0
- data/lib/sockjs/protocol.rb +97 -0
- data/lib/sockjs/servers/request.rb +136 -0
- data/lib/sockjs/servers/response.rb +169 -0
- data/lib/sockjs/session.rb +388 -0
- data/lib/sockjs/transport.rb +354 -0
- data/lib/sockjs/transports/eventsource.rb +30 -0
- data/lib/sockjs/transports/htmlfile.rb +69 -0
- data/lib/sockjs/transports/iframe.rb +68 -0
- data/lib/sockjs/transports/info.rb +48 -0
- data/lib/sockjs/transports/jsonp.rb +84 -0
- data/lib/sockjs/transports/websocket.rb +166 -0
- data/lib/sockjs/transports/welcome_screen.rb +17 -0
- data/lib/sockjs/transports/xhr.rb +75 -0
- data/lib/sockjs/version.rb +13 -0
- data/spec/sockjs/protocol_spec.rb +49 -0
- data/spec/sockjs/session_spec.rb +51 -0
- data/spec/sockjs/transport_spec.rb +73 -0
- data/spec/sockjs/transports/eventsource_spec.rb +56 -0
- data/spec/sockjs/transports/htmlfile_spec.rb +72 -0
- data/spec/sockjs/transports/iframe_spec.rb +66 -0
- data/spec/sockjs/transports/jsonp_spec.rb +252 -0
- data/spec/sockjs/transports/websocket_spec.rb +101 -0
- data/spec/sockjs/transports/welcome_screen_spec.rb +36 -0
- data/spec/sockjs/transports/xhr_spec.rb +314 -0
- data/spec/sockjs/version_spec.rb +18 -0
- data/spec/sockjs_spec.rb +8 -0
- data/spec/spec_helper.rb +121 -0
- data/spec/support/async-test.rb +42 -0
- metadata +171 -0
data/lib/sockjs.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "eventmachine"
|
4
|
+
require "forwardable"
|
5
|
+
require 'sockjs/callbacks'
|
6
|
+
require "sockjs/version"
|
7
|
+
require 'sockjs/connection'
|
8
|
+
|
9
|
+
def Time.timer(&block)
|
10
|
+
- (Time.now.tap { yield } - Time.now)
|
11
|
+
end
|
12
|
+
|
13
|
+
module SockJS
|
14
|
+
def self.debug!
|
15
|
+
@debug = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.no_debug!
|
19
|
+
@debug = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.debug?
|
23
|
+
@debug
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.puts(message)
|
27
|
+
if self.debug?
|
28
|
+
STDERR.puts(message)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.debug(message)
|
33
|
+
self.puts("~ #{message}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.debug_exception(exception)
|
37
|
+
self.debug(([exception.class.name, exception.message].join(": ") + exception.backtrace).join("\n"))
|
38
|
+
end
|
39
|
+
|
40
|
+
class CloseError < StandardError
|
41
|
+
attr_reader :status, :message
|
42
|
+
def initialize(status, message)
|
43
|
+
@status, @message = status, message
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class HttpError < StandardError
|
48
|
+
attr_reader :status, :message
|
49
|
+
|
50
|
+
def initialize(status, message, &block)
|
51
|
+
@message = message
|
52
|
+
@status = status
|
53
|
+
raise "Block passed to HttpError" unless block.nil?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class InvalidJSON < HttpError
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SockJS
|
2
|
+
module CallbackMixin
|
3
|
+
attr_accessor :status
|
4
|
+
|
5
|
+
def callbacks
|
6
|
+
@callbacks ||= Hash.new { |hash, key| hash[key] = Array.new }
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute_callback(name, *args)
|
10
|
+
if self.callbacks.has_key?(name)
|
11
|
+
self.callbacks[name].each do |callback|
|
12
|
+
callback.call(*args)
|
13
|
+
end
|
14
|
+
else
|
15
|
+
raise ArgumentError.new("There's no callback #{name.inspect}. Available callbacks: #{self.callbacks.keys.inspect}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'sockjs'
|
2
|
+
require 'sockjs/callbacks'
|
3
|
+
|
4
|
+
module SockJS
|
5
|
+
class Connection
|
6
|
+
def initialize(session_class, options)
|
7
|
+
self.status = :not_connected
|
8
|
+
@session_class = session_class
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
attr_accessor :status, :options
|
12
|
+
|
13
|
+
#XXX TODO: remove dead sessions as they're get_session'd, along with a
|
14
|
+
#recurring clearout
|
15
|
+
def sessions
|
16
|
+
SockJS.debug "Refreshing sessions"
|
17
|
+
|
18
|
+
if @sessions
|
19
|
+
@sessions.delete_if do |_, session|
|
20
|
+
unless session.alive?
|
21
|
+
SockJS.debug "Removing closed session #{_}"
|
22
|
+
end
|
23
|
+
|
24
|
+
!session.alive?
|
25
|
+
end
|
26
|
+
else
|
27
|
+
@sessions = {}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_session(session_key)
|
32
|
+
SockJS.debug "Looking up session at #{session_key.inspect}"
|
33
|
+
sessions.fetch(session_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_session(session_key)
|
37
|
+
SockJS.debug "Creating session at #{session_key.inspect}"
|
38
|
+
raise "Session already exists for #{session_key.inspect}" if sessions.has_key?(session_key)
|
39
|
+
session = @session_class.new(self)
|
40
|
+
sessions[session_key] = session
|
41
|
+
session.opened
|
42
|
+
session
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
class DelayedResponseBody
|
7
|
+
include EventMachine::Deferrable
|
8
|
+
|
9
|
+
attr_accessor :session
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@status = :created
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(body)
|
16
|
+
body.each do |chunk|
|
17
|
+
self.write(chunk)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def write(chunk)
|
22
|
+
unless @status == :open
|
23
|
+
raise "Body isn't open (status: #{@status}, trying to write #{chunk.inspect})"
|
24
|
+
end
|
25
|
+
|
26
|
+
unless chunk.respond_to?(:bytesize)
|
27
|
+
raise "Chunk is supposed to respond to #bytesize, but it doesn't.\nChunk: #{chunk.inspect} (#{chunk.class})"
|
28
|
+
end
|
29
|
+
|
30
|
+
SockJS.debug "body#write #{chunk.inspect}"
|
31
|
+
|
32
|
+
self.write_chunk(chunk)
|
33
|
+
end
|
34
|
+
|
35
|
+
def each(&block)
|
36
|
+
SockJS.debug "Opening the response."
|
37
|
+
@status = :open
|
38
|
+
@body_callback = block
|
39
|
+
end
|
40
|
+
|
41
|
+
def succeed(from_server = true)
|
42
|
+
SockJS.debug "Closing the response."
|
43
|
+
if $DEBUG and false
|
44
|
+
SockJS.debug caller[0..-8].map { |item| item.sub(Dir.pwd + "/lib/", "") }.inspect
|
45
|
+
end
|
46
|
+
|
47
|
+
@status = :closed
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def finish(data = nil)
|
52
|
+
if @status == :closed
|
53
|
+
raise "Body is already closed!"
|
54
|
+
end
|
55
|
+
|
56
|
+
self.write(data) if data
|
57
|
+
|
58
|
+
self.succeed(true)
|
59
|
+
end
|
60
|
+
|
61
|
+
def closed?
|
62
|
+
@status == :closed
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
def write_chunk(chunk)
|
67
|
+
self.__write__(chunk)
|
68
|
+
end
|
69
|
+
|
70
|
+
def __write__(data)
|
71
|
+
SockJS.debug "Data to client %% #{data.inspect}"
|
72
|
+
@body_callback.call(data)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# https://github.com/rack/rack/blob/master/lib/rack/chunked.rb
|
78
|
+
class DelayedResponseChunkedBody < DelayedResponseBody
|
79
|
+
TERM ||= "\r\n"
|
80
|
+
TAIL ||= "0#{TERM}#{TERM}"
|
81
|
+
|
82
|
+
def finish(data = nil)
|
83
|
+
if @status == :closed
|
84
|
+
raise "Body is already closed!"
|
85
|
+
end
|
86
|
+
|
87
|
+
self.write(data) if data
|
88
|
+
self.__write__(TAIL)
|
89
|
+
|
90
|
+
self.succeed(true)
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
def write_chunk(chunk)
|
95
|
+
data = [chunk.bytesize.to_s(16), TERM, chunk, TERM].join
|
96
|
+
self.__write__(data)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'thin/response'
|
2
|
+
module Thin
|
3
|
+
class Response
|
4
|
+
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
5
|
+
def persistent?
|
6
|
+
return true if PERSISTENT_STATUSES.include?(@status)
|
7
|
+
return false unless @persistent
|
8
|
+
return true if @headers.has_key?(CONTENT_LENGTH)
|
9
|
+
if @headers.has_key?(TRANSFER_ENCODING)
|
10
|
+
header_string ||= @headers.to_s
|
11
|
+
return true if /transfer-encoding: identity/i !~ header_string
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'thin'
|
2
|
+
require 'rack/sockjs'
|
3
|
+
require 'rack/builder'
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
module Examples
|
7
|
+
class ProtocolConformanceTest
|
8
|
+
class MyHelloWorld
|
9
|
+
BODY = [<<-HTML].freeze
|
10
|
+
<html>
|
11
|
+
<head>
|
12
|
+
<title>Hello World!</title>
|
13
|
+
</head>
|
14
|
+
|
15
|
+
<body>
|
16
|
+
<h1>Hello World!</h1>
|
17
|
+
<p>
|
18
|
+
This is the app, not SockJS.
|
19
|
+
</p>
|
20
|
+
</body>
|
21
|
+
</html>
|
22
|
+
HTML
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
[200, {"Content-Type" => "text/html; charset=UTF-8", "Content-Length" => BODY.join("").bytesize.to_s}, BODY]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.build_app(*args)
|
30
|
+
self.new(*args).to_app
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(options = nil)
|
34
|
+
@options = options || {}
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :options
|
38
|
+
|
39
|
+
class EchoSession < Session
|
40
|
+
def process_message(message)
|
41
|
+
SockJS.debug "\033[0;31;40m[Echo]\033[0m message: #{message.inspect}, session: #{self.object_id}"
|
42
|
+
send(message)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class CloseSession < Session
|
47
|
+
def opened
|
48
|
+
SockJS.debug "\033[0;31;40m[Close]\033[0m closing the session ..."
|
49
|
+
close(3000, "Go away!")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_app
|
54
|
+
options = self.options
|
55
|
+
::Rack::Builder.new do
|
56
|
+
map '/echo' do
|
57
|
+
run ::Rack::SockJS.new(EchoSession, options)
|
58
|
+
end
|
59
|
+
|
60
|
+
map '/disabled_websocket_echo' do
|
61
|
+
run ::Rack::SockJS.new(EchoSession, options.merge(:websocket => false))
|
62
|
+
end
|
63
|
+
|
64
|
+
map '/close' do
|
65
|
+
run ::Rack::SockJS.new(CloseSession, options)
|
66
|
+
end
|
67
|
+
|
68
|
+
run MyHelloWorld.new
|
69
|
+
end.to_app
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/sockjs/faye.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "faye/websocket"
|
4
|
+
|
5
|
+
class Thin::Request
|
6
|
+
WEBSOCKET_RECEIVE_CALLBACK = 'websocket.receive_callback'.freeze
|
7
|
+
GET = 'GET'.freeze
|
8
|
+
|
9
|
+
def websocket?
|
10
|
+
@env['REQUEST_METHOD'] == GET and
|
11
|
+
@env['HTTP_CONNECTION'] and
|
12
|
+
@env['HTTP_CONNECTION'].split(/\s*,\s*/).include?('Upgrade') and
|
13
|
+
['WebSocket', 'websocket'].include?(@env['HTTP_UPGRADE'])
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
module Protocol
|
7
|
+
CHARS_TO_BE_ESCAPED ||= /[\x00-\x1f\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufff0-\uffff]/
|
8
|
+
|
9
|
+
class Frame
|
10
|
+
# JSON Unicode Encoding
|
11
|
+
# =====================
|
12
|
+
#
|
13
|
+
# SockJS takes the responsibility of encoding Unicode strings for
|
14
|
+
# the user. The idea is that SockJS should properly deliver any
|
15
|
+
# valid string from the browser to the server and back. This is
|
16
|
+
# actually quite hard, as browsers do some magical character
|
17
|
+
# translations. Additionally there are some valid characters from
|
18
|
+
# JavaScript point of view that are not valid Unicode, called
|
19
|
+
# surrogates (JavaScript uses UCS-2, which is not really Unicode).
|
20
|
+
#
|
21
|
+
# Dealing with unicode surrogates (0xD800-0xDFFF) is quite special.
|
22
|
+
# If possible we should make sure that server does escape decode
|
23
|
+
# them. This makes sense for SockJS servers that support UCS-2
|
24
|
+
# (SockJS-node), but can't really work for servers supporting unicode
|
25
|
+
# properly (Python).
|
26
|
+
#
|
27
|
+
# The server can't send Unicode surrogates over Websockets, also various
|
28
|
+
# \u2xxxx chars get mangled. Additionally, if the server is capable of
|
29
|
+
# handling UCS-2 (ie: 16 bit character size), it should be able to deal
|
30
|
+
# with Unicode surrogates 0xD800-0xDFFF:
|
31
|
+
# http://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Surrogates
|
32
|
+
def escape(string)
|
33
|
+
string.gsub(CHARS_TO_BE_ESCAPED) do |match|
|
34
|
+
'\u%04x' % (match.ord)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate(desired_class, object)
|
39
|
+
unless object.is_a?(desired_class)
|
40
|
+
raise TypeError.new("#{desired_class} object expected, but object is an instance of #{object.class} (object: #{object.inspect}).")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class HeartbeatFrame < Frame
|
46
|
+
def initialize
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.instance
|
50
|
+
@instance ||= self.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_s
|
54
|
+
"h"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class OpeningFrame < Frame
|
59
|
+
def initialize
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.instance
|
63
|
+
@instance ||= self.new
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
"o"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class ArrayFrame < Frame
|
72
|
+
def initialize(array)
|
73
|
+
@array = array
|
74
|
+
validate Array, array
|
75
|
+
end
|
76
|
+
attr_reader :array
|
77
|
+
|
78
|
+
def to_s
|
79
|
+
"a#{escape(array.to_json)}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
class ClosingFrame < Frame
|
85
|
+
def initialize(status, message)
|
86
|
+
validate Integer, status
|
87
|
+
validate String, message
|
88
|
+
|
89
|
+
@status, @message = status, message
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_s
|
93
|
+
"c[#{@status},#{escape(@message.inspect)}]"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module SockJS
|
6
|
+
#This is the SockJS wrapper for a Rack env hash-like. Currently it requires
|
7
|
+
#that we're running under Thin - someday we may break this out such that can
|
8
|
+
#adapt to other webservers or compatiblity layers. For now: do your SockJS
|
9
|
+
#stuff in Thin.
|
10
|
+
#
|
11
|
+
class Request
|
12
|
+
attr_reader :env
|
13
|
+
def initialize(env)
|
14
|
+
@env = env
|
15
|
+
end
|
16
|
+
|
17
|
+
# request.path_info
|
18
|
+
# => /echo/abc
|
19
|
+
def path_info
|
20
|
+
env["PATH_INFO"]
|
21
|
+
end
|
22
|
+
|
23
|
+
# request.http_method
|
24
|
+
# => "GET"
|
25
|
+
def http_method
|
26
|
+
env["REQUEST_METHOD"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def async_callback
|
30
|
+
env["async.callback"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def async_close
|
34
|
+
env["async.close"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_close(&block)
|
38
|
+
async_close.callback( &block)
|
39
|
+
async_close.errback( &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def succeed
|
43
|
+
async_close.succeed
|
44
|
+
end
|
45
|
+
|
46
|
+
def fail
|
47
|
+
async_close.fail
|
48
|
+
end
|
49
|
+
|
50
|
+
#Somehow, default inspect pulls in the whole app...
|
51
|
+
def inspect
|
52
|
+
position = data.pos
|
53
|
+
data.rewind
|
54
|
+
body = data.read
|
55
|
+
"<<#{self.class.name}: #{http_method}/#{path_info} #{body.inspect}>>"
|
56
|
+
ensure
|
57
|
+
data.pos = position
|
58
|
+
end
|
59
|
+
|
60
|
+
# request.headers["origin"]
|
61
|
+
# => http://foo.bar
|
62
|
+
def headers
|
63
|
+
@headers ||=
|
64
|
+
begin
|
65
|
+
permitted_keys = /^(CONTENT_(LENGTH|TYPE))$/
|
66
|
+
|
67
|
+
@env.reduce(Hash.new) do |headers, (key, value)|
|
68
|
+
if key.match(/^HTTP_(.+)$/) || key.match(permitted_keys)
|
69
|
+
headers[$1.downcase.tr("_", "-")] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
headers
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# request.query_string["callback"]
|
78
|
+
# => "myFn"
|
79
|
+
def query_string
|
80
|
+
@query_string ||=
|
81
|
+
begin
|
82
|
+
@env["QUERY_STRING"].split("=").each_slice(2).each_with_object({}) do |pair, buffer|
|
83
|
+
buffer[pair.first] = pair.last
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# request.cookies["JSESSIONID"]
|
90
|
+
# => "123sd"
|
91
|
+
def cookies
|
92
|
+
@cookies ||=
|
93
|
+
begin
|
94
|
+
::Rack::Request.new(@env).cookies
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
# request.data.read
|
100
|
+
# => "message"
|
101
|
+
def data
|
102
|
+
@env["rack.input"]
|
103
|
+
end
|
104
|
+
HTTP_1_0 ||= "HTTP/1.0"
|
105
|
+
HTTP_VERSION ||= "version"
|
106
|
+
|
107
|
+
def http_1_0?
|
108
|
+
self.headers[HTTP_VERSION] == HTTP_1_0
|
109
|
+
end
|
110
|
+
|
111
|
+
def origin
|
112
|
+
self.headers["origin"] || "*"
|
113
|
+
end
|
114
|
+
|
115
|
+
def content_type
|
116
|
+
self.headers["content-type"]
|
117
|
+
end
|
118
|
+
|
119
|
+
def callback
|
120
|
+
callback = self.query_string["callback"] || self.query_string["c"]
|
121
|
+
URI.unescape(callback) if callback
|
122
|
+
end
|
123
|
+
|
124
|
+
def keep_alive?
|
125
|
+
headers["connection"].downcase == "keep-alive"
|
126
|
+
end
|
127
|
+
|
128
|
+
def session_id
|
129
|
+
self.cookies["JSESSIONID"] || "dummy"
|
130
|
+
end
|
131
|
+
|
132
|
+
def fresh?(etag)
|
133
|
+
self.headers["if-none-match"] == etag
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|