Skip to content

Instantly share code, notes, and snippets.

@veproza
Created November 12, 2013 16:27
Show Gist options
  • Save veproza/7433985 to your computer and use it in GitHub Desktop.
Save veproza/7433985 to your computer and use it in GitHub Desktop.
Jednoduchá in-memory cache pro Node.js - Package a servisní třída pro HTTP request - Request
require! {
zlib
async
events.EventEmitter
}
module.exports = class Package extends EventEmitter
->
@type = "text/plain"
@lastModified = null
@content =
raw : null
gzip : null
deflate : null
@contentLength =
raw : 0
gzip : 0
deflate : 0
@doNotCompress = false
@ready = no
setContent: (data, cb) ->
data = @bufferize data
@lastModified = new Date!
..setMilliseconds 0
@content.raw = data
@contentLength.raw = data.length
if @doNotCompress
@ready = yes
@emit \ready
cb?!
return
<~ async.parallel do
* @~compressGzip
@~compressDeflate
@ready = yes
@emit \ready
cb?!
compressGzip: (cb) ->
(err, compressed) <~ zlib.gzip @content.raw
@content.gzip = compressed
@contentLength.gzip = compressed.length
cb!
compressDeflate: (cb) ->
(err, compressed) <~ zlib.deflate @content.raw
@content.deflate = compressed
@contentLength.deflate = compressed.length
cb!
respondTo: (request) ->
if !@ready
@once \ready ~> @respondTo request
return
switch request.shouldBeNotModified @lastModified
| yes
request.respondNotModified!
| no
headers = request.responseHeaders
headers["Content-Type"] = @type
headers["Cache-Control"] = "no-cache"
headers["Last-Modified"] = @lastModified.toUTCString!
{httpResponse} = request
compressionMethod = request.getCompressionMethod!
encoding = switch @doNotCompress
| yes
\raw
| no
switch compressionMethod
| \gzip => \gzip
| \deflate => \deflate
| otherwise => \raw
if encoding isnt \raw
headers["Content-Encoding"] = compressionMethod
headers["Content-Length"] = @contentLength[encoding]
httpResponse.writeHead 200, headers
httpResponse.end @content[encoding]
setType: (@type) ->
bufferize: (input = "") ->
| input instanceof Buffer => input
| typeof input == \object
@setType "application/json;charset=utf-8"
new Buffer JSON.stringify input
| otherwise => new Buffer input
require! {
expect : "expect.js"
"../src/Package"
"../src/Request"
http
fs
zlib
}
test = it
describe "Package" ->
mockGet = (...options, cb) ->
headers = options?0?headers
path= options?0?path
query = options?0?query
opts =
host: "127.0.0.1"
port: 8800
headers: headers
path: path
query: query
http.get opts, (response) ->
responseString = null
response.on \data ->
responseString := it
response.on \end ->
cb null responseString, response
pack = new Package
before (done) ->
server = http.createServer (request, response) ->
pack.respondTo new Request request, response
<~ server.listen 8800
done!
test "should hold the request when no content is set yet" (done) ->
setTimeout do
-> pack.setContent "Hell!"
200
(err, response, httpResponse) <~ mockGet
expect httpResponse.statusCode .to.equal 200
expect response.toString! .to.be "Hell!"
done!
test "Should respond with correct content" (done) ->
content = "Hello."
pack.setContent content
(err, response) <~ mockGet
expect response.toString! .to.equal content
done!
test "Should use 304 responses correctly" (done) ->
content = "Hello?"
pack.setContent content
date = new Date!
..setHours 25
(err, response, httpResponse) <~ mockGet headers: "if-modified-since": date
expect httpResponse.statusCode .to.equal 304
expect response .to.be null
done!
test "Should use compression correctly" (done) ->
content = "Hello!"
<~ pack.setContent content
date = new Date!
..setHours 25
(err, response, httpResponse) <~ mockGet headers: "Accept-Encoding": "gzip"
expect httpResponse.headers .to.have.property \content-encoding \gzip
(err, uncompressed) <~ zlib.gunzip response
expect err .to.be null
expect uncompressed.toString! .to.equal content
done!
test "Should not compress when triggered" (done) ->
content = "Hello!"
pack.doNotCompress = yes
<~ pack.setContent content
date = new Date!
..setHours 25
(err, response, httpResponse) <~ mockGet headers: "Accept-Encoding": "gzip"
expect httpResponse.headers .to.not.have.property \content-encoding
expect err .to.be null
expect response.toString! .to.equal content
done!
test "Should use JSON correctly" (done) ->
content = {foo: \bar}
pack.setContent content
date = new Date!
..setHours 25
(err, response, httpResponse) <~ mockGet
fullResponse = JSON.parse response
expect fullResponse .to.eql content
done!
require! {
zlib
url
querystring
}
module.exports = class Request
denyCompression: no
statusCode: 200
content:~
-> @_content
(stringOrBuffer) -> @_content = bufferize stringOrBuffer
_content: null
url:~ -> url.parse @httpRequest.url
query:~ -> querystring.parse @url.query
(@httpRequest, @httpResponse) ->
@responseHeaders =
"Connection": "close"
"Content-Type": "text/plain"
"Last-Modified": new Date!toUTCString!
@content = ""
@headers = @httpRequest.headers
@method = @httpRequest.method
@ip = @httpRequest.connection.remoteAddress
if @httpRequest.method == "POST"
@httpRequest.pause!
respond: (content) ->
if @getCompressionMethod!
(err, compressed) <~ @compress content, that
return @respondError if err
@respondCompressed compressed, that
else
@content = content
@end!
getPostQuery: (cb) ->
(err, data) <~ @getPost
data .= toString!
return cb err if err
cb null querystring.parse data
getPost: (cb) ->
data = new Buffer 0
@httpRequest.resume!
@httpRequest.on \data (chunk) ->
data := Buffer.concat [data, chunk]
@httpRequest.on \end -> cb null data
respondJSON: (content) ->
json = JSON.stringify content
@responseHeaders['Content-Type'] = 'application/json;charset=utf-8'
@respond json
respondCompressed: (compressedContent, method) ->
@responseHeaders["Content-Encoding"] = method
@content = compressedContent
@end!
end: ->
@responseHeaders["Content-Length"] = @content.length
@httpResponse.writeHead @statusCode, @responseHeaders
@httpResponse.end @content
respondError: (description = "") ->
@statusCode = 500
@content = description
@end!
respondNotModified: ->
@statusCode = 304
@end!
respondNotFound: ->
@statusCode = 404
@end!
respondBadRequest: ->
@statusCode = 400
@end!
respondNotModifiedIfPossible: (currentModifiedDate) ->
if @shouldBeNotModified currentModifiedDate
@respondNotModified!
return true
return false
shouldBeNotModified: (date) ->
return false unless @headers['if-modified-since']
requestDate = new Date @headers['if-modified-since']
requestDate >= date
compress: (input, compression, cb) ->
method = switch compression
| \gzip => zlib.gzip
| \deflate => zlib.deflate
(err, compressed) <~ method input
cb err, compressed
getCompressionMethod: ->
| @denyCompression => null
| not @headers["accept-encoding"] => null
| otherwise
encodings = @headers["accept-encoding"].split ","
gzip = encodings.some -> it is "gzip"
deflate = encodings.some -> it is "deflate"
switch
| gzip => "gzip"
| deflate => "deflate"
| otherwise => null
bufferize = (input) ->
| input instanceof Buffer => input
| otherwise => new Buffer input
require! {
expect : "expect.js"
"../src/Request"
http
fs
zlib
}
test = it
describe "Request handler" ->
onNewRequest = null
mockGet = (...options, cb) ->
headers = options?0?headers
path= options?0?path
query = options?0?query
opts =
host: "127.0.0.1"
port: 8800
headers: headers
path: path
query: query
http.get opts, (response) ->
responseString = null
response.on \data ->
responseString := it
response.on \end ->
cb null responseString, response
before (done) ->
server = http.createServer (request, response) ->
onNewRequest? new Request request, response
<~ server.listen 8800
done!
test "should handle primitive responses" (done) ->
sentResponse = new Buffer "Hello."
onNewRequest := (request) ->
request.respond sentResponse
(err, response) <~ mockGet
expect response .to.eql sentResponse
done!
test "should assign correct headers" (done) ->
sentResponse = new Buffer "Hello."
onNewRequest := (request) ->
request.respond sentResponse
(err, response, httpResponse) <~ mockGet
expect httpResponse.headers .to.have.property \content-length sentResponse.length.toString!
expect httpResponse.headers .to.have.property \connection \close
expect httpResponse.headers .to.have.property \content-type \text/plain
expect response .to.eql sentResponse
done!
test "should handle GZIP compression" (done) ->
onNewRequest := (request) ->
request.respond "Hello."
(err, response, httpResponse) <~ mockGet headers: "Accept-Encoding": "gzip"
expect httpResponse.headers["content-encoding"] .to.equal "gzip"
(err, uncompressed) <~ zlib.gunzip response
expect err .to.equal null
expect uncompressed.toString! .to.equal "Hello."
done!
test "should handle deflate/inflate compression" (done) ->
onNewRequest := (request) ->
request.respond "Hello."
(err, response, httpResponse) <~ mockGet headers: "Accept-Encoding": "deflate"
expect httpResponse.headers["content-encoding"] .to.equal "deflate"
(err, uncompressed) <~ zlib.inflate response
expect err .to.equal null
expect uncompressed.toString! .to.equal "Hello."
done!
test "should handle cached GZIP responses" (done) ->
onNewRequest := (request) ->
(err, content) <~ zlib.gzip "Hello."
request.respondCompressed content, "gzip"
(err, response, httpResponse) <~ mockGet headers: "Accept-Encoding": "gzip"
expect httpResponse.headers .to.have.property \content-encoding \gzip
expect httpResponse.headers .to.have.property \content-length response.length.toString!
(err, uncompressed) <~ zlib.gunzip response
expect err .to.equal null
expect uncompressed.toString! .to.equal "Hello."
done!
test "should detect not-modified-since and respond 304" (done) ->
date = new Date!
..setMinutes 0
onNewRequest := (request) ->
date.setHours 0
cached = request.respondNotModifiedIfPossible date
expect cached .to.be true
(err, response, httpResponse) <~ mockGet headers: "if-modified-since": date
expect httpResponse.statusCode .to.eql 304
done!
test "should detect if not-modified-since is expired and send full data" (done) ->
date = new Date!
..setMinutes 0
onNewRequest := (request) ->
date.setHours 1
cached = request.respondNotModifiedIfPossible date
expect cached .to.be false
request.respond "hello"
date.setHours 0
(err, response, httpResponse) <~ mockGet headers: "if-modified-since": date
expect httpResponse.statusCode .to.eql 200
done!
test "should handle 404 not found errors" (done) ->
onNewRequest := (request) ->
request.respondNotFound!
(err, response, httpResponse) <~ mockGet
expect httpResponse .to.have.property \statusCode 404
done!
test "should provide convenience methods for Path and Query" (done) ->
onNewRequest := (request) ->
expect request .to.have.property \url
expect request.url .to.have.property \pathname \/foo/bar
expect request .to.have.property \query
expect request.query .to.have.property \baz \lam
done!
(err, response, httpResponse) <~ mockGet {path: "/foo/bar?baz=lam"}
test "should provide convenience method for JSON" (done) ->
data = {foo: \bar}
onNewRequest := (request) ->
request.respondJSON data
(err, response, httpResponse) <~ mockGet
expect httpResponse.headers .to.have.property \content-type "application/json;charset=utf-8"
receivedData = JSON.parse response
expect receivedData .to.eql data
done!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment