Created
November 12, 2013 16:27
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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