Permalink
Browse files

Added JSGI node

  • Loading branch information...
1 parent 1641f37 commit cdf4959bc27e56fa892ef56a7801c69994b84674 @kriszyp committed Feb 17, 2010
Showing with 213 additions and 0 deletions.
  1. +213 −0 jsgi-node.js
View
213 jsgi-node.js
@@ -0,0 +1,213 @@
+/*
+JSGI 0.3 Adapter for Node
+
+To use provide a JSGI application (can be application stack) to the start
+function:
+
+require("jsgi-node").start(function(request){
+ var requestBody = request.input.read().decodeToString("UTF-8");
+ return {
+ status:200,
+ headers:{},
+ body:["echo: " + requestBody]
+ };
+});
+
+This adapter should conform to the JSGI 0.3 (with promises) for full
+asynchronous support. For example:
+
+var posix = require("posix");
+require("jsgi-node").start(function(request){
+ var promise = new process.Promise();
+ posix.cat("jsgi-node.js").addCallback(function(body){
+ promise.emitSuccess({
+ status: 200,
+ headers: {},
+ body: [body]
+ });
+ });
+ return promise;
+});
+*/
+
+var
+ sys = require( "sys" ),
+ url = require( "url" );
+
+function Request( request ) {
+ var
+ uri = url.parse( request.url ),
+ headers = request.headers,
+ namePort = headers.host.split( ":" ),
+ lowerCaseHeaders = {};
+
+ this.method = request.method;
+ this.scriptName = "";
+ this.pathInfo = uri.pathname;
+ this.queryString = uri.query || "";
+ this.serverName = namePort[ 0 ];
+ this.serverPort = namePort[ 1 ] || 80;
+ this.scheme = "http";
+ this.env = { node: { request: request } };
+ this.input = new Input( request );
+
+ for(var i in headers){
+ lowerCaseHeaders[i.toLowerCase()] = headers[i];
+ }
+ this.headers = lowerCaseHeaders;
+ this.version = [ request.httpVersionMajor, request.httpVersionMinor ];
+}
+
+Request.prototype.jsgi = {
+ version: [ 0, 3 ],
+ multithread: false,
+ multiprocess: true,
+ async: true,
+ runOnce: false,
+ errors: {
+ print: sys.puts,
+ flush: function(){}
+ }
+};
+
+function Input( request ) {
+ var
+ inputBuffer = [],
+ inputLength = 0,
+ inputPromise,
+ waitingForLength = Infinity,
+ requestCompletePromise = new process.Promise();
+
+ request
+ .addListener( "body", function( data ) {
+ inputBuffer.push( data );
+ inputLength += data.length;
+ if ( inputLength >= waitingForLength ) {
+ inputPromise.emitSuccess();
+ }
+ })
+ .addListener( "complete", function() {
+ requestCompletePromise.emitSuccess();
+ });
+
+ this.read = function( length ) {
+ return {
+ decodeToString: function( encoding ) {
+ request.setBodyEncoding( encoding );
+ if ( !length ) {
+ if ( !requestCompletePromise.hasFired ) {
+ requestCompletePromise.wait();
+ }
+ }
+
+ else if ( length > inputLength ) {
+ waitingForLength = length;
+ inputPromise = new process.Promise();
+ inputPromise.wait();
+ waitingForLength = Infinity;
+ }
+
+ var chunk = inputBuffer.join("");
+ var keepInBuffer = length ? chunk.substring( length ) : "";
+
+ inputBuffer = [ keepInBuffer ];
+ inputLength = keepInBuffer.length;
+
+ return length ? chunk.substring( 0, length ) : chunk;
+ }
+ };
+ };
+}
+
+function Response( response ) {
+ var started = false;
+ return handle;
+
+ function handle( data, notDone ) {
+ var forEachResult;
+
+ if ( typeof data.then === "function" ) {
+ data.then(
+ handle,
+ function( error ) {
+ handle({ status:500, headers:{}, body:[error.message] });
+ },
+ function( data ){
+ handle( data, true);
+ }
+ );
+
+ return;
+ }
+
+ if ( !started ) {
+ started = true;
+ response.sendHeader( data.status, data.headers );
+ }
+
+ try {
+ if ( typeof data.body.forEach !== "function" ) {
+ throw new Error("The body does not have a forEach function");
+ }
+
+ forEachResult = data.body.forEach( function( chunk, encoding ) {
+ response.sendBody( chunk, encoding );
+ });
+
+ if ( !notDone && forEachResult && ( typeof forEachResult.then === "function" ) ) {
+ forEachResult.then( function() {
+ response.finish();
+ });
+ }
+
+ else if ( !notDone ) {
+ response.finish();
+ }
+ }
+
+ catch( e ) {
+ response.sendBody( "Error: " + e.stack );
+ response.finish();
+ }
+ }
+}
+
+function Listener( app ) {
+ return function( request, response ) {
+ request = new Request( request );
+ var respond = new Response( response );
+
+ var jsgiResponse;
+
+ setTimeout( function() {
+ // need to do this as the next event so that the request can be in a state to feed the input properly
+ try {
+ jsgiResponse = app( request )
+ } catch( error ) {
+ jsgiResponse = { status:500, headers:{}, body:[error.stack] };
+ }
+
+ respond( jsgiResponse );
+ }, 0 );
+ }
+}
+
+exports.start = function( app, options ) {
+ app = new Listener( app );
+ options = options || {};
+
+ var port = options.port || 8080;
+
+ require( "http" ).createServer( app ).listen( port );
+ sys.puts( "Server running on port " + port );
+};
+
+// Patch the Promise constructor if it is not correct, this is a very minimal
+// fix for promises in Node
+if ( typeof process.Promise.prototype.then !== "function" ) {
+ process.Promise.prototype.then = function( ok, error ) {
+ this.addCallback( ok );
+ this.addErrback( error );
+ return this;
+ };
+}

0 comments on commit cdf4959

Please sign in to comment.