├── .gitignore ├── package.json ├── ws-jsgi.js ├── jsgi └── node.js ├── README.md ├── jsgi-node.js └── promise.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsgi-node", 3 | "version": "0.3.3", 4 | "directories": { "lib": "." }, 5 | "main": "./jsgi-node", 6 | "description": "JSGI middleware server for NodeJS", 7 | "author": "Kris Zyp", 8 | "maintainers": [ 9 | { 10 | "name": "Kris Zyp", 11 | "email": "kriszyp@gmail.com" 12 | } 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/kriszyp/jsgi-node.git" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ws-jsgi.js: -------------------------------------------------------------------------------- 1 | var when = require("./promise").when, 2 | NodeRequest = require("./jsgi-node").Request; 3 | 4 | 5 | module.exports = function(socketServer, jsgiApp){ 6 | socketServer.on("connection", function(socket){ 7 | var req = socket.upgradeReq; 8 | req.setTimeout(0); 9 | function Request(){} 10 | Request.prototype = new NodeRequest(req); 11 | function Headers(){} 12 | Headers.prototype = Request.prototype.headers; 13 | socket.on("message", function(data){ 14 | var request = new Request(); 15 | request.body = [data]; 16 | request.headers = new Headers(); 17 | when(jsgiApp(request), function(response){ 18 | when(response.body, function(body){ 19 | var chunks = [], 20 | done = false; 21 | when(body.forEach(function (chunk) { 22 | chunks.push(chunk); 23 | }), function () { 24 | done = true; 25 | }); 26 | socket.stream(function (err, send) { 27 | if (!err && chunks.length) { 28 | send(chunks.join(''), done); 29 | chunks = []; 30 | } 31 | }); 32 | }); 33 | }); 34 | }); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /jsgi/node.js: -------------------------------------------------------------------------------- 1 | var when = require("../promise").when, 2 | defer = require("../promise").defer; 3 | // takes a Node HTTP app and runs it on top of a JSGI stack 4 | exports.Node = function(nodeApp){ 5 | return function(request){ 6 | var endListeners = []; 7 | var bodyDeferred; 8 | var responseDeferred = defer(); 9 | var nodeRequest = { 10 | headers: request.headers, 11 | httpVersionMajor: request.version[0], 12 | httpVersionMinor: request.version[1], 13 | addListener: function(event, callback){ 14 | process.nextTick(function(){ 15 | if(event == "data"){ 16 | when(request.body && request.body.forEach(function(data){ 17 | callback(data); 18 | }), function(){ 19 | endListeners.forEach(function(listener){ 20 | listener(); 21 | }); 22 | endListeners = null; 23 | }); 24 | } 25 | if(event == "end"){ 26 | if(endListeners){ 27 | endListeners.push(callback); 28 | }else{ 29 | callback(); 30 | } 31 | } 32 | }); 33 | return this; 34 | }, 35 | pause: function(){ 36 | 37 | }, 38 | resume: function(){ 39 | 40 | } 41 | } 42 | nodeRequest.on = nodeRequest.addListener; 43 | nodeApp(nodeRequest, 44 | { 45 | writeHead: function(status, headers){ 46 | var write; 47 | bodyDeferred = defer(); 48 | responseDeferred.resolve({ 49 | status: status, 50 | headers: headers, 51 | body: { 52 | forEach: function(callback){ 53 | write = callback; 54 | return bodyDeferred.promise; 55 | } 56 | } 57 | }); 58 | }, 59 | write: function(data){ 60 | write(data); 61 | }, 62 | end: function(data){ 63 | write(data); 64 | bodyDeferred.resolve(); 65 | } 66 | }); 67 | return responseDeferred.promise; 68 | } 69 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSGI 0.3 Adapter for Node 2 | 3 | JSGI-Node provides an interface for running [JSGI](http://wiki.commonjs.org/wiki/JSGI/Level0/A/Draft2) middleware on Node. 4 | JSGI is an asynchronous middleware interface based on solid mature middleware design 5 | principles, and the asynchronous design fits perfectly with Node. JSGI uses idiomatic JavaScript, 6 | leveraging closures for [simple and fast](http://www.sitepen.com/blog/2010/06/11/jsgi-vs-connect-for-node-middleware/) middleware connectivity. 7 | This project does not include any JSGI components itself, but 8 | a substantial set of JSGI middleware components that are available, many can be found 9 | in [Pintura](https://github.com/persvr/pintura). 10 | 11 | # Installation 12 | 13 | JSGI-Node can be installed with NPM: 14 | 15 | npm install jsgi-node 16 | 17 | # Usage 18 | 19 | To use, provide a JSGI application (can be application stack) to the start 20 | function: 21 | 22 | require("jsgi-node").start(function(request){ 23 | return { 24 | status:200, 25 | headers:{}, 26 | body:["Hello World!"] 27 | }; 28 | }); 29 | 30 | This adapter should conform to the JSGI 0.3 (with promises) for full 31 | asynchronous support. For example, here is an echo server that asynchronously 32 | waits for the request and asynchonously provides it as the response: 33 | 34 | 35 | require("jsgi-node").start(function(request){ 36 | return request.body.join().then(function(requestBody){ 37 | return { 38 | status:200, 39 | headers:{}, 40 | body:["echo: " + requestBody] 41 | }; 42 | }); 43 | }); 44 | 45 | And here is an example of using a promises from another source (from [promised-io's fs](http://github.com/persvr/promised-io)) and piping them to the 46 | response: 47 | 48 | var fs = require("promised-io/fs"); 49 | require("jsgi-node").start(function(request){ 50 | return fs.readFile("jsgi-node.js").then(function(body){ 51 | return { 52 | status: 200, 53 | headers: {}, 54 | body: [body] 55 | }; 56 | }); 57 | }); 58 | 59 | 60 | File objects returned from [promised-io's fs](http://github.com/persvr/promised-io) can be directly provided as body for 61 | automated streaming of data to the client from the filesystem: 62 | 63 | var fs = require("promised-io/fs"); 64 | require("jsgi-node").start(function(request){ 65 | return { 66 | status: 200, 67 | headers: {}, 68 | body: fs.open("some-file.txt","r") 69 | }; 70 | }); 71 | 72 | This package also includes an adapter for running Node HTTP apps on top of JSGI middleware: 73 | 74 | var fs = require("promised-io/fs"), 75 | Node = require("jsgi/node").Node; 76 | require("jsgi-node").start( 77 | SomeJSGIMiddleWare( 78 | OtherJSGIMiddleWare( 79 | Node(function(request, response){ 80 | // request and response conform to Node's HTTP API 81 | }) 82 | ) 83 | ) 84 | ); 85 | 86 | ## WebSocket with JSGI 87 | 88 | JSGI middleware can be used to handle incoming WebSocket messages. While JSGI 89 | is designed for HTTP, WebSocket includes HTTP elements and JSGI's streaming capabilities 90 | are well-suited for socket communication. JSGI delegation can be achieved by using 91 | the "ws-jsgi" module in conjunction with the node-websocket-server package. 92 | This "ws-jsgi" module exports a function that can be called with a socket server and 93 | a JSGI handler. For example: 94 |
 95 | 	var http = require("http").createServer(
 96 | 			require("jsgi-node").Listener(jsgiApp)
 97 | 		);
 98 | 	http.listen(80);
 99 | 	require("jsgi/ws-jsgi")(ws.createServer({
100 | 		server: http
101 | 	}), jsgiApp);
102 | 
103 | 104 | Licensing 105 | -------- 106 | 107 | The JSGI-Node package is an implementation of JSGI. JSGI is a standard that was 108 | developed in collaboration by many developers through the forums of JackJS, 109 | CommonJS, and Persevere. The implementation in this package is part of the Persevere 110 | project, and therefore is licensed under the 111 | AFL or BSD license. The Persevere project is administered under the Dojo foundation, 112 | and all contributions require a Dojo CLA. 113 | 114 | Authors include Kris Zyp and Jed Schmidt. -------------------------------------------------------------------------------- /jsgi-node.js: -------------------------------------------------------------------------------- 1 | /* 2 | JSGI 0.3 Adapter for Node 3 | 4 | To use provide a JSGI application (can be application stack) to the start 5 | function: 6 | 7 | require("jsgi-node").start(function(request){ 8 | return request.body.join().then(function(requestBody){ 9 | return { 10 | status:200, 11 | headers:{}, 12 | body:["echo: " + requestBody] 13 | }; 14 | }); 15 | }); 16 | 17 | This adapter should conform to the JSGI 0.3 (with promises) for full 18 | asynchronous support. For example: 19 | 20 | var fs = require("promised-io/fs"); 21 | require("jsgi-node").start(function(request){ 22 | return fs.readFile("jsgi-node.js").then(function(body){ 23 | return { 24 | status: 200, 25 | headers: {}, 26 | body: [body] 27 | }; 28 | }); 29 | }); 30 | */ 31 | 32 | var 33 | sys = require( "sys" ), 34 | url = require( "url" ), 35 | defer = require("./promise").defer; 36 | 37 | function Request( request ) { 38 | var url = request.url; 39 | var questionIndex = url.indexOf("?"); 40 | this.method = request.method; 41 | this.nodeRequest = request; 42 | this.headers = request.headers; 43 | if(questionIndex > -1){ 44 | this.pathInfo = url.substring(0, questionIndex); 45 | this.queryString = url.substring(questionIndex + 1); 46 | } 47 | else{ 48 | this.pathInfo = url; 49 | this.queryString = ""; 50 | } 51 | if(this.method != "GET"){ // optimize GET 52 | this.body = new Input( request ); 53 | } 54 | 55 | } 56 | 57 | Request.prototype = { 58 | jsgi:{ 59 | version: [ 0, 3 ], 60 | multithread: false, 61 | multiprocess: true, 62 | async: true, 63 | runOnce: false, 64 | errors: { 65 | print: sys.puts, 66 | flush: function(){} 67 | } 68 | }, 69 | get env(){ 70 | return this._env || (this._env = {}); 71 | }, 72 | scriptName: "", 73 | scheme:"http", 74 | get host(){ 75 | var host = this.headers.host; 76 | return host ? host.split(":")[0] : ""; 77 | }, 78 | get port(){ 79 | var host = this.headers.host; 80 | return host ? (host.split(":")[1] || 80) : 80; 81 | }, 82 | get remoteAddr(){ 83 | return this.nodeRequest.connection.remoteAddress; 84 | }, 85 | get version(){ 86 | return [ this.nodeRequest.httpVersionMajor, this.nodeRequest.httpVersionMinor ] 87 | } 88 | }; 89 | 90 | 91 | function Input( request ) { 92 | var 93 | inputBuffer = [], 94 | waitingForLength = Infinity; 95 | function callback(data){ 96 | inputBuffer.push(data); 97 | } 98 | var deferred = defer(); 99 | request 100 | .addListener( "data", function( data ) { 101 | callback(data); 102 | }) 103 | .addListener( "end", function() { 104 | deferred.resolve(); 105 | }); 106 | 107 | this.forEach = function (each) { 108 | if (this.encoding) { 109 | request.setBodyEncoding( this.encoding ); 110 | } 111 | inputBuffer.forEach(each); 112 | callback = each; 113 | return deferred.promise; 114 | }; 115 | } 116 | 117 | Input.prototype.join = function(token){ 118 | var parts = []; 119 | return this.forEach(function(part){ 120 | parts.push(part); 121 | }).then(function(){ 122 | return parts.join(token || ""); // yeah, I know Array.prototype.join defaults to ",", but clearly "" is more useful here 123 | }); 124 | } 125 | 126 | function Response( response, stream ) { 127 | var started = false, canceller, cancel; 128 | return handle; 129 | 130 | function handle( data, notDone ) { 131 | var forEachResult; 132 | if ( typeof data.then === "function" ) { 133 | if(!canceller){ 134 | stream.removeAllListeners("close"); 135 | canceller = function(){ 136 | stream.removeListener("close", canceller); 137 | cancel && cancel(); 138 | } 139 | stream.addListener("close", canceller); 140 | } 141 | cancel = data.cancel; 142 | data.then( 143 | handle, 144 | function( error ) { 145 | sys.puts("Error: " + error.stack); 146 | handle({ status:500, headers:{}, body:[error.message] }); 147 | }, 148 | function( data ){ 149 | handle( data, true); 150 | } 151 | ); 152 | 153 | return; 154 | } 155 | if ( !started ) { 156 | started = true; 157 | response.writeHead( data.status || 500, data.headers ); 158 | } 159 | 160 | try { 161 | if ( typeof data.body === "string" ) { 162 | response.write(data.body); 163 | } 164 | else if ( typeof data.body.forEach !== "function" ) { 165 | throw new Error("The body does not have a forEach function"); 166 | } 167 | else { 168 | forEachResult = data.body.forEach( function( chunk ) { 169 | try{ 170 | response.write( chunk, data.body.encoding || "utf8" ); 171 | }catch(e){ 172 | sys.puts( "error writing " + chunk + e); 173 | } 174 | }); 175 | } 176 | 177 | if ( !notDone && forEachResult && ( typeof forEachResult.then === "function" ) ) { 178 | cancel = forEachResult.cancel; 179 | forEachResult.then( function() { 180 | if(canceller){ 181 | stream.addListener("close", canceller); 182 | } 183 | response.end(); 184 | }); 185 | } 186 | 187 | else if ( !notDone ) { 188 | if(canceller){ 189 | stream.addListener("close", canceller); 190 | } 191 | response.end(); 192 | } 193 | } 194 | 195 | catch( e ) { 196 | if(canceller){ 197 | stream.addListener("close", canceller); 198 | } 199 | try{ 200 | // if it is not too late, set the status 201 | if(!response.statusCode){ 202 | response.writeHead(500, {}); 203 | } 204 | }catch(e2){} 205 | try{ 206 | response.write( "Error: " + e.stack ); 207 | response.end(); 208 | console.log("error",e); 209 | }catch(e3){ 210 | sys.puts(e3.stack); 211 | } 212 | } 213 | } 214 | } 215 | 216 | function Listener( app ) { 217 | if(typeof app != "function"){ 218 | throw new Error("app must be a function"); 219 | } 220 | return function( request, response ) { 221 | var connection = request.connection; 222 | request = new Request( request ); 223 | var respond = new Response( response, connection ); 224 | process.nextTick(function(){ 225 | var jsgiResponse; 226 | try { 227 | jsgiResponse = app( request ) 228 | } catch( error ) { 229 | jsgiResponse = { status:500, headers:{}, body:[error.stack] }; 230 | } 231 | respond( jsgiResponse ); 232 | }); 233 | } 234 | } 235 | start.Request = Request; 236 | start.Listener = Listener; 237 | start.start = start; 238 | 239 | function start( app, options ) { 240 | app = new Listener( app ); 241 | options = options || {}; 242 | 243 | var port = options.port || 8080, 244 | http; 245 | 246 | if ( options.ssl ) { 247 | http = require( "https" ).createServer( options.ssl, app ).listen( port ); 248 | } else { 249 | http = require( "http" ).createServer( app ).listen( port ); 250 | } 251 | 252 | sys.puts( "Server running on port " + port ); 253 | return http; 254 | }; 255 | module.exports = start; 256 | -------------------------------------------------------------------------------- /promise.js: -------------------------------------------------------------------------------- 1 | 2 | // Kris Zyp 3 | 4 | // this is based on the CommonJS spec for promises: 5 | // http://wiki.commonjs.org/wiki/Promises 6 | // Includes convenience functions for promises, much of this is taken from Tyler Close's ref_send 7 | // and Kris Kowal's work on promises. 8 | // // MIT License 9 | 10 | // A typical usage: 11 | // A default Promise constructor can be used to create a self-resolving deferred/promise: 12 | // var Promise = require("promise").Promise; 13 | // var promise = new Promise(); 14 | // asyncOperation(function(){ 15 | // Promise.resolve("succesful result"); 16 | // }); 17 | // promise -> given to the consumer 18 | // 19 | // A consumer can use the promise 20 | // promise.then(function(result){ 21 | // ... when the action is complete this is executed ... 22 | // }, 23 | // function(error){ 24 | // ... executed when the promise fails 25 | // }); 26 | // 27 | // Alternately, a provider can create a deferred and resolve it when it completes an action. 28 | // The deferred object a promise object that provides a separation of consumer and producer to protect 29 | // promises from being fulfilled by untrusted code. 30 | // var defer = require("promise").defer; 31 | // var deferred = defer(); 32 | // asyncOperation(function(){ 33 | // deferred.resolve("succesful result"); 34 | // }); 35 | // deferred.promise -> given to the consumer 36 | // 37 | // Another way that a consumer can use the promise (using promise.then is also allowed) 38 | // var when = require("promise").when; 39 | // when(promise,function(result){ 40 | // ... when the action is complete this is executed ... 41 | // }, 42 | // function(error){ 43 | // ... executed when the promise fails 44 | // }); 45 | var enqueue = (typeof process !== "undefined" && process.nextTick) || function(func){ 46 | func(); 47 | }; 48 | 49 | var freeze = Object.freeze || function(){}; 50 | 51 | /** 52 | * Default constructor that creates a self-resolving Promise. Not all promise implementations 53 | * need to use this constructor. 54 | */ 55 | var Promise = function(canceller){ 56 | }; 57 | 58 | /** 59 | * Promise implementations must provide a "then" function. 60 | */ 61 | Promise.prototype.then = function(resolvedCallback, errorCallback, progressCallback){ 62 | throw new TypeError("The Promise base class is abstract, this function must be implemented by the Promise implementation"); 63 | }; 64 | 65 | /** 66 | * If an implementation of a promise supports a concurrency model that allows 67 | * execution to block until the promise is resolved, the wait function may be 68 | * added. 69 | */ 70 | /** 71 | * If an implementation of a promise can be cancelled, it may add this function 72 | */ 73 | // Promise.prototype.cancel = function(){ 74 | // }; 75 | 76 | Promise.prototype.get = function(propertyName){ 77 | return this.then(function(value){ 78 | return value[propertyName]; 79 | }); 80 | }; 81 | 82 | Promise.prototype.put = function(propertyName, value){ 83 | return this.then(function(object){ 84 | return object[propertyName] = value; 85 | }); 86 | }; 87 | 88 | Promise.prototype.call = function(functionName /*, args */){ 89 | return this.then(function(value){ 90 | return value[propertyName].apply(value, Array.prototype.slice.call(arguments, 1)); 91 | }); 92 | }; 93 | 94 | /** Dojo/NodeJS methods*/ 95 | Promise.prototype.addCallback = function(callback){ 96 | return this.then(callback); 97 | }; 98 | 99 | Promise.prototype.addErrback = function(errback){ 100 | return this.then(function(){}, errback); 101 | }; 102 | 103 | /*Dojo methods*/ 104 | Promise.prototype.addBoth = function(callback){ 105 | return this.then(callback, callback); 106 | }; 107 | 108 | Promise.prototype.addCallbacks = function(callback, errback){ 109 | return this.then(callback, errback); 110 | }; 111 | 112 | /*NodeJS method*/ 113 | Promise.prototype.wait = function(){ 114 | return exports.wait(this); 115 | }; 116 | 117 | Deferred.prototype = Promise.prototype; 118 | // A deferred provides an API for creating and resolving a promise. 119 | exports.Promise = exports.Deferred = exports.defer = defer; 120 | function defer(){ 121 | return new Deferred(); 122 | } 123 | 124 | var contextHandler = exports.contextHandler = {}; 125 | 126 | function Deferred(canceller){ 127 | var result, finished, isError, waiting = [], handled; 128 | var promise = this.promise = new Promise(); 129 | var currentContextHandler = contextHandler.getHandler && contextHandler.getHandler(); 130 | 131 | function notifyAll(value){ 132 | if(finished){ 133 | throw new Error("This deferred has already been resolved"); 134 | } 135 | result = value; 136 | finished = true; 137 | for(var i = 0; i < waiting.length; i++){ 138 | notify(waiting[i]); 139 | } 140 | } 141 | function notify(listener){ 142 | var func = (isError ? listener.error : listener.resolved); 143 | if(func){ 144 | handled = true; 145 | enqueue(function(){ 146 | if(currentContextHandler){ 147 | currentContextHandler.resume(); 148 | } 149 | try{ 150 | var newResult = func(result); 151 | if(newResult && typeof newResult.then === "function"){ 152 | newResult.then(listener.deferred.resolve, listener.deferred.reject); 153 | return; 154 | } 155 | listener.deferred.resolve(newResult); 156 | } 157 | catch(e){ 158 | listener.deferred.reject(e); 159 | } 160 | finally{ 161 | if(currentContextHandler){ 162 | currentContextHandler.suspend(); 163 | } 164 | } 165 | }); 166 | } 167 | else{ 168 | if(isError){ 169 | if (listener.deferred.reject(result, true)) { 170 | handled = true; 171 | } 172 | } 173 | else{ 174 | listener.deferred.resolve.apply(listener.deferred, result); 175 | } 176 | } 177 | } 178 | // calling resolve will resolve the promise 179 | this.resolve = this.callback = this.emitSuccess = function(value){ 180 | notifyAll(value); 181 | }; 182 | 183 | // calling error will indicate that the promise failed 184 | var reject = this.reject = this.errback = this.emitError = function(error, dontThrow){ 185 | isError = true; 186 | notifyAll(error); 187 | if (!dontThrow) { 188 | enqueue(function () { 189 | if (!handled) { 190 | throw error; 191 | } 192 | }); 193 | } 194 | return handled; 195 | }; 196 | 197 | // call progress to provide updates on the progress on the completion of the promise 198 | this.progress = function(update){ 199 | for(var i = 0; i < waiting.length; i++){ 200 | var progress = waiting[i].progress; 201 | progress && progress(update); 202 | } 203 | } 204 | // provide the implementation of the promise 205 | this.then = promise.then = function(resolvedCallback, errorCallback, progressCallback){ 206 | var returnDeferred = new Deferred(promise.cancel); 207 | var listener = {resolved: resolvedCallback, error: errorCallback, progress: progressCallback, deferred: returnDeferred}; 208 | if(finished){ 209 | notify(listener); 210 | } 211 | else{ 212 | waiting.push(listener); 213 | } 214 | return returnDeferred.promise; 215 | }; 216 | var timeout; 217 | if(typeof setTimeout !== "undefined") { 218 | this.timeout = function (ms) { 219 | if (ms === undefined) { 220 | return timeout; 221 | } 222 | timeout = ms; 223 | setTimeout(function () { 224 | if (!finished) { 225 | if (promise.cancel) { 226 | promise.cancel(new Error("timeout")); 227 | } 228 | else { 229 | reject(new Error("timeout")); 230 | } 231 | } 232 | }, ms); 233 | return promise; 234 | }; 235 | } 236 | 237 | if(canceller){ 238 | this.cancel = promise.cancel = function(){ 239 | var error = canceller(); 240 | if(!(error instanceof Error)){ 241 | error = new Error(error); 242 | } 243 | reject(error); 244 | } 245 | } 246 | freeze(promise); 247 | }; 248 | 249 | function perform(value, async, sync){ 250 | try{ 251 | if(value && typeof value.then === "function"){ 252 | value = async(value); 253 | } 254 | else{ 255 | value = sync(value); 256 | } 257 | if(value && typeof value.then === "function"){ 258 | return value; 259 | } 260 | var deferred = new Deferred(); 261 | deferred.resolve(value); 262 | return deferred.promise; 263 | }catch(e){ 264 | var deferred = new Deferred(); 265 | deferred.reject(e); 266 | return deferred.promise; 267 | } 268 | 269 | } 270 | /** 271 | * Promise manager to make it easier to consume promises 272 | */ 273 | 274 | /** 275 | * Registers an observer on a promise. 276 | * @param value promise or value to observe 277 | * @param resolvedCallback function to be called with the resolved value 278 | * @param rejectCallback function to be called with the rejection reason 279 | * @param progressCallback function to be called when progress is made 280 | * @return promise for the return value from the invoked callback 281 | */ 282 | exports.whenPromise = function(value, resolvedCallback, rejectCallback, progressCallback){ 283 | return perform(value, function(value){ 284 | return value.then(resolvedCallback, rejectCallback, progressCallback); 285 | }, 286 | function(value){ 287 | return resolvedCallback(value); 288 | }); 289 | }; 290 | /** 291 | * Registers an observer on a promise. 292 | * @param value promise or value to observe 293 | * @param resolvedCallback function to be called with the resolved value 294 | * @param rejectCallback function to be called with the rejection reason 295 | * @param progressCallback function to be called when progress is made 296 | * @return promise for the return value from the invoked callback or the value if it 297 | * is a non-promise value 298 | */ 299 | exports.when = function(value, resolvedCallback, rejectCallback, progressCallback){ 300 | if(value && typeof value.then === "function"){ 301 | return exports.whenPromise(value, resolvedCallback, rejectCallback, progressCallback); 302 | } 303 | return resolvedCallback(value); 304 | }; 305 | 306 | /** 307 | * Gets the value of a property in a future turn. 308 | * @param target promise or value for target object 309 | * @param property name of property to get 310 | * @return promise for the property value 311 | */ 312 | exports.get = function(target, property){ 313 | return perform(target, function(target){ 314 | return target.get(property); 315 | }, 316 | function(target){ 317 | return target[property] 318 | }); 319 | }; 320 | 321 | /** 322 | * Invokes a method in a future turn. 323 | * @param target promise or value for target object 324 | * @param methodName name of method to invoke 325 | * @param args array of invocation arguments 326 | * @return promise for the return value 327 | */ 328 | exports.post = function(target, methodName, args){ 329 | return perform(target, function(target){ 330 | return target.call(property, args); 331 | }, 332 | function(target){ 333 | return target[methodName].apply(target, args); 334 | }); 335 | }; 336 | 337 | /** 338 | * Sets the value of a property in a future turn. 339 | * @param target promise or value for target object 340 | * @param property name of property to set 341 | * @param value new value of property 342 | * @return promise for the return value 343 | */ 344 | exports.put = function(target, property, value){ 345 | return perform(target, function(target){ 346 | return target.put(property, value); 347 | }, 348 | function(target){ 349 | return target[property] = value; 350 | }); 351 | }; 352 | 353 | 354 | /** 355 | * Waits for the given promise to finish, blocking (and executing other events) 356 | * if necessary to wait for the promise to finish. If target is not a promise 357 | * it will return the target immediately. If the promise results in an reject, 358 | * that reject will be thrown. 359 | * @param target promise or value to wait for. 360 | * @return the value of the promise; 361 | */ 362 | exports.wait = function(target){ 363 | if(!queue){ 364 | throw new Error("Can not wait, the event-queue module is not available"); 365 | } 366 | if(target && typeof target.then === "function"){ 367 | var isFinished, isError, result; 368 | target.then(function(value){ 369 | isFinished = true; 370 | result = value; 371 | }, 372 | function(error){ 373 | isFinished = true; 374 | isError = true; 375 | result = error; 376 | }); 377 | while(!isFinished){ 378 | queue.processNextEvent(true); 379 | } 380 | if(isError){ 381 | throw result; 382 | } 383 | return result; 384 | } 385 | else{ 386 | return target; 387 | } 388 | }; 389 | 390 | 391 | 392 | /** 393 | * Takes an array of promises and returns a promise that is fulfilled once all 394 | * the promises in the array are fulfilled 395 | * @param array The array of promises 396 | * @return the promise that is fulfilled when all the array is fulfilled, resolved to the array of results 397 | */ 398 | exports.all = function(array){ 399 | var deferred = new Deferred(); 400 | if(!(array instanceof Array)){ 401 | array = Array.prototype.slice.call(arguments); 402 | } 403 | var fulfilled = 0, length = array.length; 404 | var results = []; 405 | array.forEach(function(promise, index){ 406 | exports.when(promise, each, each); 407 | function each(value){ 408 | results[index] = value; 409 | fulfilled++; 410 | if(fulfilled === length){ 411 | deferred.resolve(results); 412 | } 413 | } 414 | }); 415 | return deferred.promise; 416 | }; 417 | 418 | /** 419 | * Takes an array of promises and returns a promise that is fulfilled when the first 420 | * promise in the array of promises is fulfilled 421 | * @param array The array of promises 422 | * @return a promise that is fulfilled with the value of the value of first promise to be fulfilled 423 | */ 424 | exports.first = function(array){ 425 | var deferred = new Deferred(); 426 | if(!(array instanceof Array)){ 427 | array = Array.prototype.slice.call(arguments); 428 | } 429 | var fulfilled; 430 | array.forEach(function(promise, index){ 431 | exports.when(promise, function(value){ 432 | if (!fulfilled) { 433 | fulfilled = true; 434 | deferred.resolve(value); 435 | } 436 | }, 437 | function(error){ 438 | if (!fulfilled) { 439 | fulfilled = true; 440 | deferred.resolve(error); 441 | } 442 | }); 443 | }); 444 | return deferred.promise; 445 | }; 446 | 447 | /** 448 | * Takes an array of asynchronous functions (that return promises) and 449 | * executes them sequentially. Each funtion is called with the return value of the last function 450 | * @param array The array of function 451 | * @param startingValue The value to pass to the first function 452 | * @return the value returned from the last function 453 | */ 454 | exports.seq = function(array, startingValue){ 455 | array = array.concat(); // make a copy 456 | var deferred = new Deferred(); 457 | function next(value){ 458 | var nextAction = array.shift(); 459 | if(nextAction){ 460 | exports.when(nextAction(value), next, deferred.reject); 461 | } 462 | else { 463 | deferred.resolve(value); 464 | } 465 | } 466 | next(startingValue); 467 | return deferred.promise; 468 | }; 469 | 470 | 471 | /** 472 | * Delays for a given amount of time and then fulfills the returned promise. 473 | * @param milliseconds The number of milliseconds to delay 474 | * @return A promise that will be fulfilled after the delay 475 | */ 476 | if(typeof setTimeout !== "undefined") { 477 | exports.delay = function(milliseconds) { 478 | var deferred = new Deferred(); 479 | setTimeout(function(){ 480 | deferred.resolve(); 481 | }, milliseconds); 482 | return deferred.promise; 483 | }; 484 | } 485 | 486 | 487 | 488 | /** 489 | * Runs a function that takes a callback, but returns a Promise instead. 490 | * @param func node compatible async function which takes a callback as its last argument 491 | * @return promise for the return value from the callback from the function 492 | */ 493 | exports.execute = function(asyncFunction){ 494 | var args = Array.prototype.slice.call(arguments, 1); 495 | 496 | var deferred = new Deferred(); 497 | args.push(function(error, result){ 498 | if(error) { 499 | deferred.emitError(error); 500 | } 501 | else { 502 | if(arguments.length > 2){ 503 | // if there are multiple success values, we return an array 504 | Array.prototype.shift.call(arguments, 1); 505 | deferred.emitSuccess(arguments); 506 | } 507 | else{ 508 | deferred.emitSuccess(result); 509 | } 510 | } 511 | }); 512 | asyncFunction.apply(this, args); 513 | return deferred.promise; 514 | }; 515 | 516 | /** 517 | * Converts a Node async function to a promise returning function 518 | * @param func node compatible async function which takes a callback as its last argument 519 | * @return A function that returns a promise 520 | */ 521 | exports.convertNodeAsyncFunction = function(asyncFunction, callbackNotDeclared){ 522 | var arity = asyncFunction.length; 523 | if(callbackNotDeclared){ 524 | arity++; 525 | } 526 | return function(){ 527 | var deferred = new Deferred(); 528 | arguments.length = arity; 529 | arguments[arity - 1] = function(error, result){ 530 | if(error) { 531 | deferred.emitError(error); 532 | } 533 | else { 534 | if(arguments.length > 2){ 535 | // if there are multiple success values, we return an array 536 | Array.prototype.shift.call(arguments, 1); 537 | deferred.emitSuccess(arguments); 538 | } 539 | else{ 540 | deferred.emitSuccess(result); 541 | } 542 | } 543 | }; 544 | asyncFunction.apply(this, arguments); 545 | return deferred.promise; 546 | }; 547 | }; 548 | --------------------------------------------------------------------------------