├── .gitignore ├── .travis.yml ├── oauth+xauth.js ├── tests ├── index.js ├── querystring.js ├── promises-aplus.js ├── promise-spawn.js ├── promise.js └── oauth.js ├── process.js ├── step.js ├── delay.js ├── util ├── querystring.js └── uri.js ├── package.json ├── engines ├── rhino │ ├── fs.js │ ├── delay.js │ └── rhino-http-client.js └── node │ └── http-client.js ├── observe.js ├── querystring.js ├── lazy-array.js ├── fs.js ├── http-client.js ├── oauth.js ├── README.md └── promise.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /oauth+xauth.js: -------------------------------------------------------------------------------- 1 | var oauth = require("./oauth"); 2 | 3 | for(var p in oauth){ 4 | exports[p] = oauth[p]; 5 | } 6 | 7 | exports.xauth = function(client, username, password, mode){ 8 | return client.obtainTokenCredentials("", "", "", { 9 | x_auth_username: username, 10 | x_auth_password: password, 11 | x_auth_mode: mode 12 | }); 13 | }; -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | exports.testOAuth = require('./oauth'); 2 | exports.testPromise = require('./promise'); 3 | exports.testQuerystring = require('./querystring'); 4 | 5 | // test spawning coroutines if generators are supported on platform 6 | try { 7 | eval("(function* (){})()"); 8 | exports.testPromiseSpawn = require('./promise-spawn'); 9 | } 10 | catch (err) {} 11 | 12 | if (require.main === module) 13 | require("patr/runner").run(exports); 14 | -------------------------------------------------------------------------------- /process.js: -------------------------------------------------------------------------------- 1 | ({define:typeof define!="undefined"?define:function(factory){factory(require,exports)}}). 2 | define(function(req,exports){ 3 | if(typeof console !== "undefined"){ 4 | exports.print = function(){ 5 | console.log.apply(console, arguments); 6 | } 7 | } 8 | if(typeof process !== "undefined"){ 9 | exports.args = process.argv; 10 | exports.env = process.env; 11 | exports.print = console.log; 12 | exports.dir = console.dir; 13 | } 14 | else if(typeof navigator === "undefined"){ 15 | try{ 16 | exports.args = req("" + "system").args; 17 | exports.env = req("" + "system").env; 18 | }catch(e){ 19 | // in raw rhino, we don't even have system 20 | } 21 | exports.print = print; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /tests/querystring.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var querystring = require("../util/querystring"); 3 | 4 | exports.testMungingArrayParams = function(){ 5 | var arr = ["bar", "baz"]; 6 | var requestParams = []; 7 | querystring.addToArray(requestParams, "foo", arr); 8 | assert.deepEqual(requestParams, ["foo[]", "bar", "foo[]", "baz"]); 9 | }; 10 | 11 | exports.testMungingObjectParams = function(){ 12 | var obj = { "key": "value" }; 13 | var requestParams = []; 14 | querystring.addToArray(requestParams, "obj", obj); 15 | assert.deepEqual(requestParams, ["obj[key]", "value"]); 16 | }; 17 | 18 | exports.testParsingMungedQuerystring = function(){ 19 | var qs = "foo=bar&foo=baz&obj[key]=value"; 20 | var requestParams = []; 21 | querystring.parseToArray(requestParams, qs); 22 | assert.deepEqual(requestParams, ["foo[]", "bar", "foo[]", "baz", "obj[key]", "value"]); 23 | }; 24 | 25 | if (require.main === module) 26 | require("patr/runner").run(exports); 27 | -------------------------------------------------------------------------------- /step.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(define){ 4 | define(function(require,exports){ 5 | 6 | // Vladimir Dronnikov 7 | 8 | // inspired by creationix's Step 9 | 10 | var when = require('./promise').when; 11 | 12 | // execute sequentially functions taken from steps[] 13 | // each successive is fed with the result of prior 14 | // each function can return an immediate value, a promise, or just throw 15 | // in the latter case the next function will receive Error object 16 | // return "undefined" to full stop. 17 | // 18 | // "context" is available to each steps as "this" 19 | // 20 | exports.Step = function(context, steps) { 21 | var next; 22 | next = function() { 23 | var fn, result; 24 | if (!steps.length) { 25 | return arguments[0]; 26 | } 27 | fn = steps.shift(); 28 | try { 29 | result = fn.apply(context, arguments); 30 | if (result !== void 0) { 31 | result = when(result, next, next); 32 | } 33 | } catch (err) { 34 | next(err); 35 | } 36 | return result; 37 | }; 38 | return next(); 39 | }; 40 | 41 | }); 42 | })(typeof define!="undefined"?define:function(factory){factory(require,exports)}); 43 | -------------------------------------------------------------------------------- /tests/promises-aplus.js: -------------------------------------------------------------------------------- 1 | var lib = require("../promise"); 2 | var promisesAplusTests = require("promises-aplus-tests"); 3 | 4 | exports.baseAdapter = { 5 | resolved: Promise.resolve, 6 | rejected: Promise.reject, 7 | deferred: function () { 8 | var resolver, rejecter; 9 | var promise = new Promise(function (resolve, reject) { 10 | resolver = resolve; 11 | rejecter = reject; 12 | }); 13 | return { 14 | promise: promise, 15 | resolve: resolver, 16 | reject: rejecter 17 | }; 18 | } 19 | }; 20 | 21 | exports.libAdapter = { 22 | resolved: function (value) { 23 | var deferred = lib.defer(); 24 | deferred.resolve(value); 25 | return deferred.promise; 26 | }, 27 | rejected: function (reason) { 28 | var deferred = lib.defer(); 29 | deferred.reject(reason); 30 | return deferred.promise; 31 | }, 32 | deferred: function () { 33 | return lib.defer(); 34 | } 35 | } 36 | 37 | function run(adapter, callback) { 38 | promisesAplusTests(adapter, callback); 39 | } 40 | 41 | if (require.main === module) { 42 | // run(exports.baseAdapter, function () {}); 43 | run(exports.libAdapter, function () {}); 44 | } 45 | -------------------------------------------------------------------------------- /delay.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Provides time-based promise-returning delay and schedule functions 3 | */ 4 | ({define:typeof define!="undefined"?define:function(factory){module.exports=factory(require)}}). 5 | define(function(require){ 6 | if (typeof system === "object" && system.engine === "rhino"){ 7 | // for rhino 8 | return require("./engines/rhino/delay"); 9 | } 10 | var defer = require("./promise").defer, 11 | LazyArray = require("./lazy-array").LazyArray; 12 | // returns a promise that is fulfilled after the given number of milliseconds 13 | function delay(ms){ 14 | var deferred = defer(); 15 | setTimeout(deferred.resolve, ms); 16 | return deferred.promise; 17 | }; 18 | // returns a lazy array that iterates one every given number of milliseconds 19 | delay.schedule = function(ms){ 20 | var callbacks = []; 21 | setInterval(function(){ 22 | callbacks.forEach(function(callback){ 23 | if(callback()){ 24 | callbacks.splice(callbacks.indexOf(callback), 1); 25 | } 26 | }); 27 | }, ms); 28 | return LazyArray({ 29 | some: function(callback){ 30 | callbacks.push(callback); 31 | } 32 | }); 33 | }; 34 | return delay.delay = delay; 35 | }); -------------------------------------------------------------------------------- /util/querystring.js: -------------------------------------------------------------------------------- 1 | var querystring = require("querystring"); 2 | for(var i in querystring){ 3 | exports[i] = querystring[i]; 4 | } 5 | var type = Function.prototype.call.bind(Object.prototype.toString); 6 | 7 | // Parse the name/value pairs of the query string into a flattened array 8 | // Automatically munge the parameters 9 | exports.parseToArray = function(arr, qs){ 10 | var parsed = exports.parse(qs); 11 | for(var i in parsed){ 12 | exports.addToArray(arr, i, parsed[i]); 13 | } 14 | }; 15 | 16 | // Add munged values with name/value pairs to the flattened array 17 | exports.addToArray = function(arr, name, value){ 18 | if(value === undefined || value === null){ 19 | value = ""; 20 | } 21 | 22 | switch(type(value)){ 23 | case "[object String]": 24 | case "[object Number]": 25 | case "[object Boolean]": 26 | arr.push(name, value + ""); 27 | break; 28 | case "[object Array]": 29 | value.forEach(function(value){ 30 | exports.addToArray(arr, name + "[]", value); 31 | }); 32 | break; 33 | case "[object Object]": 34 | for(var k in value){ 35 | exports.addToArray(arr, name + "[" + k + "]", value[k]); 36 | } 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "promised-io", 3 | "version": "0.3.6", 4 | "author": { 5 | "name": "Kris Zyp" 6 | }, 7 | "description": "Promise-based IO", 8 | "licenses": [ 9 | { 10 | "type": "AFLv2.1", 11 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43" 12 | }, 13 | { 14 | "type": "BSD", 15 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13" 16 | } 17 | ], 18 | "repository": { 19 | "type":"git", 20 | "url":"http://github.com/kriszyp/promised-io" 21 | }, 22 | "contributors": [ 23 | "Dean Landolt ", 25 | "Mark Wubben ", 26 | "Vladimir Dronnikov " 27 | ], 28 | "keywords": [ 29 | "promise", 30 | "io" 31 | ], 32 | "mappings": { 33 | "patr": "http://github.com/kriszyp/patr/zipball/v0.2.5" 34 | }, 35 | "directories": { 36 | "lib": "." 37 | }, 38 | "main": "./promise", 39 | "devDependencies":{ 40 | "patr": ">=0.2.6", 41 | "promises-aplus-tests": "*" 42 | }, 43 | "scripts": { 44 | "test": "node tests/" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /util/uri.js: -------------------------------------------------------------------------------- 1 | // Adapted from the following: 2 | // 3 | // Original License 4 | // ---------------- 5 | // parseUri 1.2.2 6 | // (c) Steven Levithan 7 | // MIT License 8 | 9 | function parseUri (str) { 10 | var o = parseUri.options, 11 | m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), 12 | uri = {}, 13 | i = 14; 14 | 15 | while (i--) uri[o.key[i]] = m[i] || ""; 16 | 17 | uri[o.q.name] = {}; 18 | uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { 19 | if ($1) uri[o.q.name][$1] = $2; 20 | }); 21 | 22 | return uri; 23 | }; 24 | 25 | parseUri.options = { 26 | strictMode: false, 27 | key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], 28 | q: { 29 | name: "queryKey", 30 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g 31 | }, 32 | parser: { 33 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, 34 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 35 | } 36 | }; 37 | 38 | exports.parseUri = parseUri; -------------------------------------------------------------------------------- /tests/promise-spawn.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var promise = require("../promise"); 3 | 4 | exports.testResolved = function (){ 5 | return promise.spawn(function* (){ 6 | yield promise.delay(100); 7 | // can yield non-promise values 8 | var foo = yield 'foo'; 9 | assert.ok(foo === 'foo'); 10 | var result = yield promise.delay(50).then(function () { 11 | return foo; 12 | }); 13 | var error = new Error('Boom!'); 14 | var start = new Date(); 15 | try { 16 | yield promise.delay(100).then(function () { 17 | throw error; 18 | }); 19 | // Should never come here 20 | assert.ok(true === false); 21 | } 22 | catch (e) { 23 | assert.ok(e === error); 24 | assert.ok(new Date() - start >= 100); 25 | } 26 | return result; 27 | }).then(function (value) { 28 | assert.ok(value === 'foo'); 29 | return true; 30 | }, function (e) { 31 | // Should never come here 32 | assert.ok(true === false); 33 | }); 34 | }; 35 | 36 | exports.testRejected = function (){ 37 | var error = new Error('Boom!'); 38 | return promise.spawn(function* (){ 39 | var start = new Date(); 40 | yield promise.delay(100).then(function () { 41 | assert.ok(new Date() - start >= 100); 42 | throw error; 43 | }); 44 | // Should never come here 45 | assert.ok(true === false); 46 | }).then(function () { 47 | // Should never come here 48 | assert.ok(true === false); 49 | }, function (e) { 50 | assert.ok(e === error); 51 | return true; 52 | }); 53 | }; 54 | 55 | if (require.main === module) 56 | require("patr/runner").run(exports); 57 | -------------------------------------------------------------------------------- /engines/rhino/fs.js: -------------------------------------------------------------------------------- 1 | var File = require("fs"), 2 | LazyArray = require("../../lazy-array").LazyArray, 3 | defer = require("../../promise").defer; 4 | for(var i in File){ 5 | exports[i] = File[i]; 6 | } 7 | 8 | exports.readFileSync = File.read; 9 | exports.writeFileSync = File.write; 10 | exports.mkdirSync = File.mkdir; 11 | exports.readdir = exports.list; 12 | exports.stat = exports.statSync = function(path) { 13 | try{ 14 | return { 15 | isFile: function(){ 16 | return File.isFile(path); 17 | }, 18 | size: File.size(path) 19 | }; 20 | }catch(e){ 21 | var deferred = defer(); 22 | deferred.reject(e); 23 | return deferred.promise; 24 | } 25 | }; 26 | 27 | exports.makeTree = File.mkdirs; 28 | exports.makeDirectory = File.mkdir; 29 | 30 | exports.open = function(){ 31 | var file = File.open.apply(this, arguments); 32 | var array = LazyArray({ 33 | some: function(callback){ 34 | while(true){ 35 | var buffer = file.read(4096); 36 | if(buffer.length <= 0){ 37 | return; 38 | } 39 | if(callback(buffer)){ 40 | return; 41 | } 42 | } 43 | } 44 | }); 45 | for(var i in array){ 46 | file[i] = array[i]; 47 | } 48 | return file; 49 | }; 50 | exports.openSync = exports.open; 51 | 52 | exports.createWriteStream = function(path, options) { 53 | options = options || {}; 54 | options.flags = options.flags || "w"; 55 | var flags = options.flags || "w", 56 | f = File.open(path, flags); 57 | return { 58 | writable: true, 59 | write: function() { 60 | var deferred = defer(); 61 | try { 62 | f.write.apply(this, arguments); 63 | f.flush(); 64 | } 65 | catch (e) { 66 | return stream.writable = false; 67 | } 68 | deferred.resolve(); 69 | return deferred.promise; 70 | }, 71 | end: f.close, 72 | destroy: f.close 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /engines/rhino/delay.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Provides time-based promise-returning delay and schedule functions for Rhino 3 | */ 4 | var defer = require("../../promise").defer, 5 | LazyArray = require("../../lazy-array").LazyArray; 6 | // returns a promise that is fulfilled after the given number of milliseconds 7 | exports.delay = function(ms){ 8 | var deferred = defer(); 9 | _scheduleTimeout(deferred.resolve, ms, false); 10 | return deferred.promise; 11 | }; 12 | // returns a lazy array that iterates one every given number of milliseconds 13 | exports.schedule = function(ms){ 14 | var callbacks = []; 15 | _scheduleTimeout(function(){ 16 | callbacks.forEach(function(callback){ 17 | if(callback()){ 18 | callbacks.splice(callbacks.indexOf(callback), 1); 19 | } 20 | }); 21 | }, ms, true); 22 | return LazyArray({ 23 | some: function(callback){ 24 | callbacks.push(callback); 25 | } 26 | }); 27 | }; 28 | 29 | var nextId = 1, 30 | timeouts = {}, 31 | timer, 32 | queue; 33 | 34 | var _scheduleTimeout = function(callback, delay, repeat) 35 | { 36 | if (typeof callback == "function") 37 | var func = callback; 38 | else if (typeof callback == "string") 39 | var func = new Function(callback); 40 | else 41 | return; 42 | 43 | var timeout = { 44 | }; 45 | var id = nextId++; 46 | timeouts[id] = timeout; 47 | 48 | timer = timer || new java.util.Timer("JavaScript timer thread", true); 49 | queue = queue || require("event-loop"); 50 | var lastFinished = true; 51 | var task = timeout.task = new java.util.TimerTask({ 52 | run: function(){ 53 | if(lastFinished){ 54 | lastFinished = false; 55 | queue.enqueue(function(){ 56 | if(!timeout.cancelled){ // check to make sure it wasn't enqueued and then later cancelled 57 | try{ 58 | func(); 59 | }finally{ 60 | lastFinished = true; 61 | } 62 | } 63 | }); 64 | } 65 | } 66 | }); 67 | delay = Math.floor(delay); 68 | 69 | if(repeat){ 70 | timer.schedule(task, delay, delay); 71 | } 72 | else{ 73 | timer.schedule(task, delay); 74 | } 75 | 76 | return id; 77 | } 78 | 79 | 80 | -------------------------------------------------------------------------------- /engines/rhino/rhino-http-client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP Client using the JSGI standard objects 3 | */ 4 | 5 | var LazyArray = require("../../lazy-array").LazyArray; 6 | 7 | // configurable proxy server setting, defaults to http_proxy env var 8 | exports.proxyServer = require("../../process").env.http_proxy; 9 | 10 | exports.request = function(request){ 11 | var url = new java.net.URL(request.url), 12 | connection = url.openConnection(), 13 | method = request.method || "GET", 14 | is = null, 15 | promised = true; 16 | 17 | if (request.jsgi && "async" in request.jsgi) promised = request.jsgi.async; 18 | 19 | for (var header in this.headers) { 20 | var value = this.headers[header]; 21 | connection.addRequestProperty(String(header), String(value)); 22 | } 23 | connection.setFollowRedirects(false) 24 | connection.setDoInput(true); 25 | connection.setRequestMethod(method); 26 | if (request.body && typeof request.body.forEach === "function") { 27 | connection.setDoOutput(true); 28 | var writer = new java.io.OutputStreamWriter(connection.getOutputStream()); 29 | request.body.forEach(function(chunk) { 30 | writer.write(chunk); 31 | writer.flush(); 32 | }); 33 | } 34 | if (typeof writer !== "undefined") writer.close(); 35 | 36 | try { 37 | connection.connect(); 38 | is = connection.getInputStream(); 39 | } 40 | catch (e) { 41 | is = connection.getErrorStream(); 42 | } 43 | 44 | var status = Number(connection.getResponseCode()), 45 | headers = {}; 46 | for (var i = 0;; i++) { 47 | var key = connection.getHeaderFieldKey(i), 48 | value = connection.getHeaderField(i); 49 | if (!key && !value) 50 | break; 51 | // returns the HTTP status code with no key, ignore it. 52 | if (key) { 53 | key = String(key).toLowerCase(); 54 | value = String(value); 55 | if (headers[key]) { 56 | if (!Array.isArray(headers[key])) headers[key] = [headers[key]]; 57 | headers[key].push(value); 58 | } 59 | else { 60 | headers[key] = value; 61 | } 62 | } 63 | } 64 | 65 | var reader = new java.io.BufferedReader(new java.io.InputStreamReader(is)), 66 | builder = new java.lang.StringBuilder(), 67 | line; 68 | // FIXME create deferred and LazyArray 69 | while((line = reader.readLine()) != null){ 70 | builder.append(line + '\n'); 71 | } 72 | if (typeof writer !== "undefined") writer.close(); 73 | reader.close(); 74 | 75 | return { 76 | status: status, 77 | headers: headers, 78 | body: LazyArray({ 79 | some: function(write) { 80 | write(builder.toString()); 81 | } 82 | }) 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /observe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AOP style event handling, for listening for method calls. Very similar to dojo.connect 3 | 4 | /* Add a listener for the execution of the given function slot on the given object. 5 | * 6 | * When object[eventName]() is executed the handler is called. 7 | * The optional before parameter can be used to indicate if the listener 8 | * should be fired before or after the default action (default is after) 9 | */ 10 | exports.observe = function(object, eventName, listener, before) { 11 | if(!listener){ 12 | throw new Error("No listener provided"); 13 | } 14 | if(typeof object.observe === "function"){ 15 | // CommonJS observable object 16 | return object.observe(eventName, listener); 17 | } 18 | var listenerProxy = function(that, args){//make sure we have unique function so we can remove it 19 | try{ 20 | listener.apply(that, args); 21 | }catch(e){ 22 | require("./process").print(e); 23 | } 24 | }; 25 | if(typeof object.addListener === "function"){ 26 | // NodeJS EventEmitter 27 | object.addListener(eventName, listener); 28 | return { 29 | observe: function(listener){ 30 | return exports.observe(object, eventName, listener); 31 | }, 32 | emit: function(event){ 33 | object.emit(eventName, event); 34 | }, 35 | dismiss: function(){ 36 | object.removeListener(eventName, listener); 37 | } 38 | }; 39 | } 40 | var afters, befores, 41 | main = object[eventName]; 42 | if(typeof main != "function"){ 43 | main = function(){}; 44 | } 45 | if(main._afters){ 46 | afters = main._afters; 47 | befores = main._befores; 48 | } 49 | else{ 50 | befores = []; 51 | afters = []; 52 | var newFunc = object[eventName] = function(){ 53 | for(var i = 0; i < befores.length; i++){ 54 | befores[i](this, arguments); 55 | } 56 | try{ 57 | return main.apply(this, arguments); 58 | } 59 | finally{ 60 | for(var i = 0; i < afters.length; i++){ 61 | afters[i](this, arguments); 62 | } 63 | } 64 | }; 65 | newFunc._befores = befores; 66 | newFunc._afters = afters; 67 | } 68 | if(before){ 69 | befores.push(listenerProxy); 70 | } 71 | else{ 72 | afters.push(listenerProxy); 73 | } 74 | return createSignal(); 75 | function createSignal(){ 76 | var observers; 77 | return { 78 | observe: function(listener){ 79 | afters.push(listener); 80 | }, 81 | emit: function(){ 82 | main.apply(object, arguments); 83 | }, 84 | dismiss: function(){ 85 | if(before){ 86 | befores.splice(befores.indexOf(listenerProxy), 1); 87 | } 88 | else{ 89 | afters.splice(afters.indexOf(listenerProxy), 1); 90 | } 91 | } 92 | }; 93 | }; 94 | }; 95 | -------------------------------------------------------------------------------- /engines/node/http-client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP Client using the JSGI standard objects 3 | */ 4 | var defer = require("../../promise").defer, 5 | when = require("../../promise").when, 6 | LazyArray = require("../../lazy-array").LazyArray, 7 | http = require("http"), 8 | https = require("https"), 9 | parse = require("url").parse; 10 | 11 | // configurable proxy server setting, defaults to http_proxy env var 12 | exports.proxyServer = require("../../process").env.http_proxy; 13 | 14 | exports.request = function(originalRequest){ 15 | // make a shallow copy of original request object 16 | var request = {}; 17 | for(var key in originalRequest){ 18 | if(originalRequest.hasOwnProperty(key)){ 19 | request[key] = originalRequest[key]; 20 | } 21 | } 22 | 23 | if(request.timeout === undefined)request.timeout= 20000; // default timeout. 24 | if(request.url){ 25 | var parsed = parse(request.url); 26 | if (parsed.pathname) { 27 | parsed.pathInfo = parsed.pathname; 28 | } else { 29 | parsed.pathInfo = "/"; 30 | } 31 | request.queryString = parsed.query || ""; 32 | for(var i in parsed){ 33 | request[i] = parsed[i]; 34 | } 35 | } 36 | var deferred = defer(); 37 | if(exports.proxyServer){ 38 | request.pathname = request.url; 39 | var proxySettings = parse(exports.proxyServer); 40 | request.port = proxySettings.port; 41 | request.protocol = proxySettings.protocol; 42 | request.hostname = proxySettings.hostname; 43 | } 44 | if(!request.protocol){ 45 | throw new Error("No valid protocol/URL provided"); 46 | } 47 | var timedOut, bodyDeferred; 48 | // Limits the time of sending the request + receiving the response header to 20 seconds. 49 | // No timeout is used on the client stream, but we do destroy the stream if a timeout is reached. 50 | var timeout = setTimeout(function(){ 51 | timedOut = true; 52 | req.destroy(); 53 | deferred.reject(new Error("Timeout")); 54 | }, request.timeout); 55 | 56 | var secure = request.protocol.indexOf("s") > -1; 57 | request.port = request.port || (secure ? 443 : 80); 58 | request.headers = request.headers || {host: request.host || request.hostname + (request.port ? ":" + request.port : "")}; 59 | request.host = request.hostname; 60 | request.method = request.method || "GET"; 61 | request.path = request.pathname || request.pathInfo || ""; 62 | if (request.queryString) { 63 | request.path += "?"+request.queryString; 64 | } 65 | var timedOut; 66 | 67 | var req = (secure ? https : http).request(request); 68 | req.on("response", function (response){ 69 | if(timedOut){ 70 | return; 71 | } 72 | response.status = response.statusCode; 73 | var sendData = function(block){ 74 | buffer.push(block); 75 | }; 76 | var buffer = []; 77 | bodyDeferred = defer(); 78 | var body = response.body = LazyArray({ 79 | some: function(callback){ 80 | buffer.forEach(callback); 81 | sendData = callback; 82 | return bodyDeferred.promise; 83 | } 84 | }); 85 | response.setEncoding(request.encoding || "utf8"); 86 | 87 | response.on("data", function (chunk) { 88 | sendData(chunk); 89 | }); 90 | response.on("end", function(){ 91 | bodyDeferred.resolve(); 92 | }); 93 | deferred.resolve(response); 94 | clearTimeout(timeout); 95 | }); 96 | req.on('error', function(e) { 97 | if (!timedOut) { 98 | deferred.reject(e); 99 | clearTimeout(timeout); 100 | } 101 | }); 102 | if(request.body){ 103 | return when(request.body.forEach(function(block){ 104 | req.write(block); 105 | }), function(){ 106 | req.end(); 107 | return deferred.promise; 108 | }); 109 | }else{ 110 | req.end(); 111 | return deferred.promise; 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /tests/promise.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | all = require("../promise").all, 3 | when = require("../promise").when, 4 | whenPromise = require("../promise").whenPromise, 5 | defer = require("../promise").defer, 6 | Promise = require("../promise"), 7 | Step = require("../step").Step; 8 | 9 | exports.testSpeedPlainValue = function(){ 10 | for(var i = 0; i < 1000; i++){ 11 | when(3, function(){ 12 | }); 13 | } 14 | }; 15 | 16 | exports.testSpeedPromise = function(){ 17 | var deferred = defer(); 18 | for(var i = 0; i < 1000; i++){ 19 | when(deferred.promise, function(){ 20 | }); 21 | } 22 | deferred.resolve(3); 23 | }; 24 | 25 | exports.testWhenPromiseRejectHandled = function(){ 26 | // The inner whenPromise doesn't have a rejectCallback, but the outer one does. 27 | // This means the error then *is* handled, but the inner whenPromise doesn't know about that. 28 | // This shouldn't result in an uncaught exception thrown by the promise library. 29 | whenPromise(true, function(){ 30 | return whenPromise((function(){ 31 | var deferred = defer(); 32 | deferred.reject({}); 33 | return deferred.promise; 34 | })()); 35 | }).then(null, function(){}); 36 | }; 37 | 38 | exports.testMultipleReject = function(){ 39 | all(delayedFail(25), delayedFail(50), delayedSuccess(75)).then(function () { 40 | throw new Error('There should be no success here.'); 41 | }, function () { 42 | // This is where we want to end up, once only. 43 | }); 44 | }; 45 | 46 | function delayedSuccess(delay) { 47 | var deferred = defer(); 48 | setTimeout(function () { 49 | deferred.resolve(); 50 | }, delay); 51 | return deferred.promise; 52 | } 53 | 54 | function delayedFail(delay) { 55 | var deferred = defer(); 56 | setTimeout(function () { 57 | deferred.reject(); 58 | }, delay); 59 | return deferred.promise; 60 | } 61 | 62 | function veryDeferred(){ 63 | var deferred = defer(); 64 | setTimeout(function(){ 65 | deferred.resolve(true); 66 | }, 100); 67 | return deferred.promise; 68 | } 69 | 70 | function veryDeferred(){ 71 | var deferred = defer(); 72 | setTimeout(function(){ 73 | deferred.resolve(true); 74 | }, 100); 75 | return deferred.promise; 76 | } 77 | 78 | exports.testStep = function(){ 79 | var deferred = defer(); 80 | Step({foo: 'bar'}, [ 81 | function(){ 82 | console.log('S1'); 83 | assert.ok(this.foo === 'bar'); 84 | return false; 85 | }, 86 | function(result){ 87 | console.log('S2'); 88 | assert.ok(result === false); 89 | this.foo = 'baz'; 90 | return veryDeferred(); 91 | }, 92 | function(result){ 93 | console.log('S3'); 94 | assert.ok(this.foo === 'baz'); 95 | assert.ok(result === true); 96 | throw Error('Catchme!'); 97 | }, 98 | function(result){ 99 | console.log('S4'); 100 | assert.ok(result instanceof Error); 101 | assert.ok(result.message === 'Catchme!'); 102 | deferred.resolve(true); 103 | // return undefined; 104 | }, 105 | function(result){ 106 | console.log('S5', result); 107 | // Should never come here 108 | deferred.reject(false); 109 | assert.ok(true === false); 110 | }, 111 | ]); 112 | return deferred.promise; 113 | }; 114 | 115 | exports.testAsValue = function (){ 116 | var deferred = Promise.as("supercala"); 117 | deferred.then(function (res) { 118 | assert.ok(res == "supercala"); 119 | }, function () { 120 | throw new Error("Unexpected error"); 121 | }); 122 | }; 123 | 124 | exports.testAsPromise = function (){ 125 | var temp = defer(); 126 | temp.resolve("supercala"); 127 | var deferred = Promise.as(temp); 128 | assert.ok(temp === deferred); 129 | deferred.then(function (res) { 130 | assert.ok(res == "supercala"); 131 | }, function () { 132 | throw new Error("Unexpected error"); 133 | }); 134 | }; 135 | 136 | if (require.main === module) 137 | require("patr/runner").run(exports); 138 | 139 | -------------------------------------------------------------------------------- /querystring.js: -------------------------------------------------------------------------------- 1 | // Query String Utilities 2 | // Taken from Jack 3 | 4 | var DEFAULT_SEP = "&"; 5 | var DEFAULT_EQ = "="; 6 | 7 | exports.unescape = function (str, decodeSpaces) { 8 | return decodeURIComponent(decodeSpaces ? str.replace(/\+/g, " ") : str); 9 | }; 10 | 11 | exports.escape = function (str) { 12 | return encodeURIComponent(str); 13 | }; 14 | 15 | exports.toQueryString = function (obj, sep, eq, name) { 16 | sep = sep || DEFAULT_SEP; 17 | eq = eq || DEFAULT_EQ; 18 | if (isA(obj, null) || isA(obj, undefined)) { 19 | return name ? encodeURIComponent(name) + eq : ''; 20 | } 21 | if (isNumber(obj) || isString(obj)) { 22 | return encodeURIComponent(name) + eq + encodeURIComponent(obj); 23 | } 24 | if (isA(obj, [])) { 25 | var s = []; 26 | name = name+'[]'; 27 | for (var i = 0, l = obj.length; i < l; i ++) { 28 | s.push( exports.toQueryString(obj[i], sep, eq, name) ); 29 | } 30 | return s.join(sep); 31 | } 32 | // now we know it's an object. 33 | var s = []; 34 | var begin = name ? name + '[' : ''; 35 | var end = name ? ']' : ''; 36 | for (var i in obj) if (obj.hasOwnProperty(i)) { 37 | var n = begin + i + end; 38 | s.push(exports.toQueryString(obj[i], sep, eq, n)); 39 | } 40 | return s.join(sep); 41 | }; 42 | 43 | exports.parseQuery = function(qs, sep, eq) { 44 | return qs 45 | .split(sep||DEFAULT_SEP) 46 | .map(pieceParser(eq||DEFAULT_EQ)) 47 | .reduce(mergeParams); 48 | }; 49 | 50 | // Parse a key=val string. 51 | // These can get pretty hairy 52 | // example flow: 53 | // parse(foo[bar][][bla]=baz) 54 | // return parse(foo[bar][][bla],"baz") 55 | // return parse(foo[bar][], {bla : "baz"}) 56 | // return parse(foo[bar], [{bla:"baz"}]) 57 | // return parse(foo, {bar:[{bla:"baz"}]}) 58 | // return {foo:{bar:[{bla:"baz"}]}} 59 | var pieceParser = function (eq) { 60 | return function parsePiece (key, val) { 61 | if (arguments.length !== 2) { 62 | // key=val, called from the map/reduce 63 | key = key.split(eq); 64 | return parsePiece( 65 | exports.unescape(key.shift(), true), exports.unescape(key.join(eq), true) 66 | ); 67 | } 68 | key = key.replace(/^\s+|\s+$/g, ''); 69 | if (isString(val)) val = val.replace(/^\s+|\s+$/g, ''); 70 | var sliced = /(.*)\[([^\]]*)\]$/.exec(key); 71 | if (!sliced) { 72 | var ret = {}; 73 | if (key) ret[key] = val; 74 | return ret; 75 | } 76 | // ["foo[][bar][][baz]", "foo[][bar][]", "baz"] 77 | var tail = sliced[2], head = sliced[1]; 78 | 79 | // array: key[]=val 80 | if (!tail) return parsePiece(head, [val]); 81 | 82 | // obj: key[subkey]=val 83 | var ret = {}; 84 | ret[tail] = val; 85 | return parsePiece(head, ret); 86 | }; 87 | }; 88 | 89 | // the reducer function that merges each query piece together into one set of params 90 | function mergeParams (params, addition) { 91 | return ( 92 | // if it's uncontested, then just return the addition. 93 | (!params) ? addition 94 | // if the existing value is an array, then concat it. 95 | : (isA(params, [])) ? params.concat(addition) 96 | // if the existing value is not an array, arrayify it. 97 | : (!isA(params, {}) || !isA(addition, {})) ? [params].concat(addition) 98 | // else merge them as objects, which is a little more complex 99 | : mergeObjects(params, addition) 100 | ) 101 | }; 102 | 103 | // Merge two *objects* together. If this is called, we've already ruled 104 | // out the simple cases, and need to do the for-in business. 105 | function mergeObjects (params, addition) { 106 | for (var i in addition) if (i && addition.hasOwnProperty(i)) { 107 | params[i] = mergeParams(params[i], addition[i]); 108 | } 109 | return params; 110 | }; 111 | 112 | // duck typing 113 | function isA (thing, canon) { 114 | return ( 115 | // truthiness. you can feel it in your gut. 116 | (!thing === !canon) 117 | // typeof is usually "object" 118 | && typeof(thing) === typeof(canon) 119 | // check the constructor 120 | && Object.prototype.toString.call(thing) === Object.prototype.toString.call(canon) 121 | ); 122 | }; 123 | function isNumber (thing) { 124 | return typeof(thing) === "number" && isFinite(thing); 125 | }; 126 | function isString (thing) { 127 | return typeof(thing) === "string"; 128 | }; 129 | -------------------------------------------------------------------------------- /lazy-array.js: -------------------------------------------------------------------------------- 1 | ({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory.apply(this, deps.map(function(id){return require(id)}))}}). 2 | define(["./promise"], function(promise){ 3 | try{ 4 | var when = promise.when; 5 | }catch(e){ 6 | console.log("couldn't load promise library", e.stack); 7 | when = function(value, callback){ 8 | return callback(value); 9 | } 10 | } 11 | function LazyArray(hasSomeAndLength){ 12 | return new SomeWrapper(hasSomeAndLength); 13 | }; 14 | var exports = LazyArray; 15 | exports.LazyArray = LazyArray; 16 | exports.first = function(array){ 17 | return exports.get(array, 0); 18 | }; 19 | exports.last = function(array){ 20 | return exports.get(array, array.length-1); 21 | }; 22 | exports.get = function(array, index){ 23 | var result, i = 0; 24 | return when(array.some(function(item){ 25 | if(i == index){ 26 | result = item; 27 | return true; 28 | } 29 | i++; 30 | }), 31 | function(){ 32 | return result; 33 | }); 34 | }; 35 | 36 | var testProto = {}; 37 | var testProto2 = {a:"b"}; 38 | testProto.__proto__ = testProto2; 39 | var mutableProto = testProto.a == "b"; 40 | function SomeWrapper(hasSomeAndLength){ 41 | if(mutableProto){ 42 | hasSomeAndLength.source = hasSomeAndLength; 43 | hasSomeAndLength.__proto__ = SomeWrapper.prototype; 44 | return hasSomeAndLength; 45 | } 46 | this.source = hasSomeAndLength; 47 | if(hasSomeAndLength.length){ 48 | this.length = hasSomeAndLength.length; 49 | } 50 | this.totalCount = hasSomeAndLength.totalCount; 51 | } 52 | exports.LazyArray.prototype = SomeWrapper.prototype = []; 53 | SomeWrapper.prototype.some = function(callback){ 54 | this.source.some(callback); 55 | } 56 | SomeWrapper.prototype.filter = function(fn, thisObj){ 57 | var results = []; 58 | return when(this.source.some(function(item){ 59 | if(fn.call(thisObj, item)){ 60 | results.push(item); 61 | } 62 | }), function(){ 63 | return results; 64 | }); 65 | }; 66 | 67 | SomeWrapper.prototype.every = function(fn, thisObj){ 68 | return when(this.source.some(function(item){ 69 | if(!fn.call(thisObj, item)){ 70 | return true; 71 | } 72 | }), function(result){return !result;}); 73 | }; 74 | SomeWrapper.prototype.forEach= function(fn, thisObj){ 75 | return this.source.some(function(item){ 76 | fn.call(thisObj, item); 77 | }); 78 | }; 79 | SomeWrapper.prototype.concat = function(someOther){ 80 | var source = this.source; 81 | return new SomeWrapper({ 82 | length : source.length + someOther.length, 83 | some : function(fn,thisObj){ 84 | return when(source.some(fn,thisObj), function(result){ 85 | return result || someOther.some(fn,thisObj); 86 | }); 87 | } 88 | }); 89 | }; 90 | SomeWrapper.prototype.map = function(mapFn, mapThisObj){ 91 | var source = this.source; 92 | return new SomeWrapper({ 93 | length : source.length, 94 | some : function(fn,thisObj){ 95 | return source.some(function(item){ 96 | return fn.call(thisObj, mapFn.call(mapThisObj, item)); 97 | }); 98 | } 99 | }); 100 | }; 101 | SomeWrapper.prototype.toRealArray= function(mapFn, mapThisObj){ 102 | var array = []; 103 | return when(this.source.some(function(item){ 104 | array.push(item); 105 | }), function(){ 106 | return array; 107 | }); 108 | }; 109 | SomeWrapper.prototype.join = function(){ 110 | var args = arguments; 111 | return when(this.toRealArray(), function(realArray){ 112 | return Array.prototype.join.apply(realArray, args); 113 | }); 114 | }; 115 | SomeWrapper.prototype.sort = function(){ 116 | var args = arguments; 117 | return when(this.toRealArray(), function(realArray){ 118 | return Array.prototype.sort.apply(realArray, args); 119 | }); 120 | }; 121 | SomeWrapper.prototype.reverse = function(){ 122 | var args = arguments; 123 | return when(this.toRealArray(), function(realArray){ 124 | return Array.prototype.reverse.apply(realArray, args); 125 | }); 126 | }; 127 | SomeWrapper.prototype.get = SomeWrapper.prototype.item = function(index){ 128 | var result, i = 0; 129 | return when(this.source.some(function(item){ 130 | if(i == index){ 131 | result = item; 132 | return true; 133 | } 134 | i++; 135 | }), function(){ 136 | return result; 137 | }); 138 | }; 139 | 140 | 141 | SomeWrapper.prototype.toSource = function(){ 142 | var serializedParts = []; 143 | return when(this.source.some(function(item){ 144 | serializedParts.push(item && item.toSource()); 145 | }), function(){ 146 | return '[' + serializedParts.join(",") + ']'; 147 | }); 148 | }; 149 | SomeWrapper.prototype.toJSON = function(){ 150 | var loadedParts = []; 151 | return when(this.source.some(function(item){ 152 | loadedParts.push(item); 153 | }), function(){ 154 | return loadedParts; 155 | }); 156 | }; 157 | return exports; 158 | }); 159 | -------------------------------------------------------------------------------- /fs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Node fs module that returns promises 3 | */ 4 | 5 | if (typeof java === "object"){ 6 | var fs = require("./engines/rhino/fs"); 7 | 8 | // for rhino 9 | for(var i in fs){ 10 | exports[i] = fs[i]; 11 | } 12 | } 13 | else{ 14 | var fs = require("fs"), 15 | LazyArray = require("./lazy-array").LazyArray, 16 | Buffer = require("buffer").Buffer, 17 | defer = require("./promise").defer, 18 | when = require("./promise").when, 19 | convertNodeAsyncFunction = require("./promise").convertNodeAsyncFunction; 20 | 21 | // convert all the non-sync functions that have a sync counterpart 22 | for (var i in fs) { 23 | if ((i + 'Sync') in fs) { 24 | // async 25 | if (fs[i]) { 26 | exports[i] = convertNodeAsyncFunction(fs[i], i === "readFile"); 27 | } 28 | } 29 | else{ 30 | // sync, or something that we can't auto-convert 31 | exports[i] = fs[i]; 32 | } 33 | } 34 | function File(fd){ 35 | var file = new LazyArray({ 36 | some: function(callback){ 37 | var deferred = defer(); 38 | function readAndSend(){ 39 | var buffer = new Buffer(4096); 40 | if(fd.then){ 41 | fd.then(function(resolvedFd){ 42 | fd = resolvedFd; 43 | fs.read(fd, buffer, 0, 4096, null, readResponse); 44 | }); 45 | }else{ 46 | fs.read(fd, buffer, 0, 4096, null, readResponse); 47 | } 48 | function readResponse(err, bytesRead){ 49 | if(err){ 50 | deferred.reject(err); 51 | return; 52 | } 53 | if (bytesRead === 0){ 54 | fs.close(fd, function (err) { 55 | if(err){ 56 | deferred.reject(err); 57 | return; 58 | } 59 | deferred.resolve(); 60 | }); 61 | } 62 | else { 63 | var result; 64 | if(bytesRead < 4096){ 65 | result = callback(buffer.slice(0, bytesRead)); 66 | }else{ 67 | result = callback(buffer); 68 | } 69 | if(result){ 70 | // if a promise is returned, we wait for it be fulfilled, allows for back-pressure indication 71 | if(result.then){ 72 | result.then(function(result){ 73 | if(result){ 74 | deferred.resolve(); 75 | } 76 | else{ 77 | readAndSend(fd); 78 | } 79 | }, deferred.reject); 80 | } 81 | else{ 82 | deferred.resolve(); 83 | } 84 | }else{ 85 | readAndSend(fd); 86 | } 87 | } 88 | } 89 | } 90 | readAndSend(); 91 | return deferred.promise; 92 | }, 93 | length: 0 94 | }); 95 | file.fd = fd; 96 | file.then = function(callback, errback){ 97 | fd.then(function(){ 98 | callback(file); 99 | }, errback); 100 | }; 101 | file.write = function(contents, options, encoding){ 102 | return exports.write(file, contents, options, encoding); 103 | } 104 | file.close = function(){ 105 | return exports.close(file); 106 | } 107 | file.writeSync = function(contents, options, encoding){ 108 | return exports.writeSync(file.fd, contents, options, encoding); 109 | } 110 | file.closeSync = function(){ 111 | return exports.closeSync(file.fd); 112 | } 113 | return file; 114 | } 115 | File.prototype = LazyArray.prototype; 116 | 117 | var nodeRead = exports.read; 118 | exports.read = function(path, options){ 119 | if(path instanceof File){ 120 | var args = arguments; 121 | return when(path.fd, function(fd){ 122 | args[0] = fd; 123 | return nodeRead.apply(this, args); 124 | }); 125 | }else{ 126 | return exports.readFileSync(path, options).toString((options && options.charset) || "utf8"); 127 | } 128 | }; 129 | 130 | var nodeWrite = exports.write; 131 | exports.write = function(path, contents, options, encoding){ 132 | if(path instanceof File){ 133 | var id = Math.random(); 134 | var args = arguments; 135 | return when(path.fd, function(fd){ 136 | args[0] = fd; 137 | if(typeof contents == "string"){ 138 | return nodeWrite(fd, contents, options, encoding); 139 | } 140 | return nodeWrite(fd, contents, 0, contents.length, null); 141 | }); 142 | }else{ 143 | return exports.writeFileSync(path, contents, options); 144 | } 145 | }; 146 | var nodeClose = exports.close; 147 | exports.close = function(file){ 148 | if(file instanceof File){ 149 | var args = arguments; 150 | return when(file.fd, function(fd){ 151 | args[0] = fd; 152 | return nodeClose.apply(this, args); 153 | }); 154 | } 155 | throw new Error("Must be given a file descriptor"); 156 | }; 157 | 158 | var nodeOpenSync = exports.openSync; 159 | exports.openSync = function(){ 160 | if(typeof mode == "string"){ 161 | arguments[1] = mode.replace(/b/,''); 162 | } 163 | return File(nodeOpenSync.apply(this, arguments)); 164 | }; 165 | 166 | nodeOpen = exports.open; 167 | exports.open = function(path, mode){ 168 | if(typeof mode == "string"){ 169 | arguments[1] = mode.replace(/b/,''); 170 | } 171 | return File(nodeOpen.apply(this, arguments)); 172 | }; 173 | 174 | exports.makeDirectory = exports.mkdirSync; 175 | 176 | exports.makeTree = function(path){ 177 | if(path.charAt(path.length-1) == '/') { 178 | path = path.substring(0, path.length - 1); 179 | } 180 | try{ 181 | fs.statSync(path); 182 | }catch(e){ 183 | var index = path.lastIndexOf('/'); 184 | if(index > -1){ 185 | exports.makeTree(path.substring(0, index)); 186 | } 187 | fs.mkdirSync(path, 0777); 188 | } 189 | }; 190 | exports.absolute = exports.realpathSync; 191 | exports.list = exports.readdirSync; 192 | exports.move = exports.rename; 193 | 194 | } 195 | -------------------------------------------------------------------------------- /tests/oauth.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var oauth = require("../oauth"); 3 | var querystring = require("../util/querystring"); 4 | 5 | exports.testGeneratingSignatureBaseString = function(){ 6 | // base string described in 7 | var client = new oauth.Client; 8 | var result = client._createSignatureBase("GET", "http://photos.example.net/photos", 9 | "file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original"); 10 | assert.equal(result, "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal"); 11 | }; 12 | 13 | exports.testNormalizingUrl = function(){ 14 | var client = new oauth.Client; 15 | 16 | // default ports should be stripped 17 | assert.equal(client._normalizeUrl({ protocol: "https:", hostname: "somehost.com", port: "443", pathname: "/foo/bar" }), "https://somehost.com/foo/bar"); 18 | assert.equal(client._normalizeUrl({ protocol: "http:", hostname: "somehost.com", port: "80", pathname: "/foo/bar" }), "http://somehost.com/foo/bar"); 19 | 20 | // should leave non-default ports from URLs for use in signature generation 21 | assert.equal(client._normalizeUrl({ protocol: "https:", hostname: "somehost.com", port: "446", pathname: "/foo/bar" }), "https://somehost.com:446/foo/bar"); 22 | assert.equal(client._normalizeUrl({ protocol: "http:", hostname: "somehost.com", port: "81", pathname: "/foo/bar" }), "http://somehost.com:81/foo/bar"); 23 | }; 24 | 25 | exports.testNormalizingRequestParams = function(){ 26 | var client = new oauth.Client; 27 | 28 | // ordered by name 29 | assert.equal(client._normalizeParams(["z", "a", "a", "b", "1", "c"]), "1=c&a=b&z=a"); 30 | 31 | // if two parameter names are the same then order by the value 32 | assert.equal(client._normalizeParams(["z", "b", "z", "a", "1", "c"]), "1=c&z=a&z=b"); 33 | 34 | // resulting parameters should be encoded and ordered as per (3.4.1.3.2) 35 | var requestParams = []; 36 | querystring.parseToArray(requestParams, querystring.stringify({ 37 | "b5" : "=%3D", 38 | "c@": "", 39 | "a2": "r b", 40 | "oauth_consumer_key": "9djdj82h48djs9d2", 41 | "oauth_token":"kkk9d7dh3k39sjv7", 42 | "oauth_signature_method": "HMAC-SHA1", 43 | "oauth_timestamp": "137131201", 44 | "oauth_nonce": "7d8f3e4a", 45 | "c2" : "" 46 | })); 47 | querystring.addToArray(requestParams, "a3", "a"); 48 | querystring.addToArray(requestParams, "a3", "2 q"); 49 | assert.equal(client._normalizeParams(requestParams), "a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9djdj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7"); 50 | }; 51 | 52 | function generateClient() { 53 | var client = new oauth.Client("consumerkey", "consumersecret", null, null, null, "1.0", null, function(){ return "ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp"; }); 54 | oauth.Client.getTimestamp = function(){ return "1272399856"; }; 55 | return client; 56 | } 57 | 58 | exports.testSigningUrl = { 59 | "test without token": function(){ 60 | var client = generateClient(); 61 | 62 | // provide a valid signature when no token is present 63 | var requestParams = ["bar", "foo"]; 64 | var oauthParams = client._collectOAuthParams({}, requestParams); 65 | var params = client._normalizeParams(requestParams); 66 | var baseString = client._createSignatureBase("GET", "http://somehost.com:3323/foo/poop", params); 67 | var signature = client._createSignature(baseString); 68 | assert.equal(signature, "7ytO8vPSLut2GzHjU9pn1SV9xjc="); 69 | }, 70 | 71 | "test with token": function(){ 72 | var client = generateClient(); 73 | 74 | // provide a valid signature when a token is present 75 | var bound = client.bind("token", ""); 76 | var requestParams = ["bar", "foo"]; 77 | var oauthParams = bound._collectOAuthParams({}, requestParams); 78 | var params = bound._normalizeParams(requestParams); 79 | var baseString = bound._createSignatureBase("GET", "http://somehost.com:3323/foo/poop", params); 80 | var signature = bound._createSignature(baseString); 81 | assert.equal(oauthParams.oauth_token, "token"); 82 | assert.equal(signature, "9LwCuCWw5sURtpMroIolU3YwsdI="); 83 | }, 84 | "test with token and secret": function(){ 85 | var client = generateClient(); 86 | 87 | // provide a valid signature when a token and a token secret are present 88 | var bound = client.bind("token", "tokensecret"); 89 | var requestParams = ["bar", "foo"]; 90 | var oauthParams = bound._collectOAuthParams({}, requestParams); 91 | var params = bound._normalizeParams(requestParams); 92 | var baseString = bound._createSignatureBase("GET", "http://somehost.com:3323/foo/poop", params); 93 | var signature = bound._createSignature(baseString); 94 | assert.equal(signature, "zeOR0Wsm6EG6XSg0Vw/sbpoSib8="); 95 | } 96 | }; 97 | 98 | exports.testBuildingAuthHeader = function(){ 99 | var client = generateClient(); 100 | 101 | // all provided OAuth arguments should be concatenated correctly 102 | var request = client._signRequest({ 103 | method: "GET", 104 | protocol: "http:", 105 | hostname: "somehost.com", 106 | port: "3323", 107 | pathname: "/foo/poop", 108 | headers: {} 109 | }, ["bar", "foo"]); 110 | assert.equal(request.headers.authorization, 'OAuth oauth_consumer_key="consumerkey",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1272399856",oauth_nonce="ybHPeOEkAUJ3k2wJT9Xb43MjtSgTvKqp",oauth_version="1.0",oauth_signature="7ytO8vPSLut2GzHjU9pn1SV9xjc%3D"'); 111 | }; 112 | 113 | if (require.main === module) 114 | require("patr/runner").run(exports); 115 | -------------------------------------------------------------------------------- /http-client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP Client using the JSGI standard objects 3 | */ 4 | ({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory.apply(this, deps.slice(0,2).map(require).concat(require))}}). 5 | define(["./promise", "./process", "require"], 6 | function(promise, process, require){ 7 | var defer = promise.defer, 8 | when = promise.when, 9 | print = process.print, 10 | request; 11 | 12 | if(typeof XMLHttpRequest === "undefined"){ 13 | request = require("./engines/node/http-client").request; 14 | } 15 | else{ 16 | request = function(request){ 17 | var 18 | scheme = request.scheme || "http", 19 | serverName = request.serverName || request.hostname || "localhost", 20 | serverPort = request.serverPort || request.port || 80, 21 | xhr = new XMLHttpRequest(); 22 | xhr.open(request.method || "GET", 23 | request.url || // allow request.url to shortcut creating a URL from all the various parts 24 | (scheme + "://" + serverName + ":" + serverPort + request.pathInfo + (request.queryString ? '?' + request.queryString : '')), true); 25 | for(var i in request.headers){ 26 | xhr.setRequestHeader(i, request.headers[i]); 27 | } 28 | var deferred = defer(), 29 | response, 30 | lastUpdate; 31 | xhr.onreadystatechange = function(){ 32 | if(xhr.readyState == 4 || xhr.readyState == 3){ 33 | if(!response){ 34 | response = { 35 | body: [xhr.responseText], 36 | status: xhr.status, 37 | headers: {} 38 | }; 39 | lastUpdate = xhr.responseText.length; 40 | var headers = xhr.getAllResponseHeaders(); 41 | headers = headers.split(/\n/); 42 | for(var i = 0; i < headers.length; i++){ 43 | var nameValue = headers[i].split(": ", 2); 44 | if(nameValue){ 45 | var name = nameValue[0]; 46 | response.headers[name.toLowerCase()] = xhr.getResponseHeader(name); 47 | } 48 | } 49 | } 50 | else{ 51 | response.body = [xhr.responseText]; 52 | lastUpdate = xhr.responseText.length; 53 | } 54 | if(xhr.readyState == 4){ 55 | deferred.resolve(response); 56 | } 57 | else{ 58 | deferred.progress(response); 59 | } 60 | } 61 | } 62 | xhr.send(request.body && request.body.toString()); 63 | return deferred.promise; 64 | } 65 | } 66 | // for back-compat 67 | request.request = request; 68 | // FIXME this way too naive 69 | var isRedirect = request.isRedirect = function(response){ 70 | return [301,302,303,307].indexOf(response.status) >= 0; 71 | } 72 | 73 | request.Redirect = function(nextApp, maxRedirects){ 74 | maxRedirects = maxRedirects || 10; 75 | return function(request){ 76 | var remaining = maxRedirects, 77 | deferred = defer(); 78 | function next(){ 79 | when(nextApp(request), function(response) { 80 | if(remaining--){ 81 | // TODO cache safe redirects when cache is added 82 | if(isRedirect(response)){ 83 | request.url = response.headers.location; 84 | next(); 85 | }else{ 86 | deferred.resolve(response); 87 | } 88 | }else{ 89 | if(isRedirect(response)) print("Maximum redirects reached") 90 | deferred.resolve(response); 91 | } 92 | }, deferred.reject); 93 | } 94 | next(); 95 | return deferred.promise; 96 | } 97 | } 98 | 99 | request.CookieJar = function(nextApp) { 100 | var domainToCookies = {}; 101 | 102 | return function(req) { 103 | var 104 | querystring = require("./querystring"), 105 | parseUri = require("./util/uri").parseUri; 106 | 107 | if (req.url) { 108 | var url = parseUri(req.url); 109 | req.hostname = url.host; 110 | req.port = url.port; 111 | req.pathInfo = url.path; 112 | req.authority = url.authority; 113 | } 114 | 115 | if (req.hostname && domainToCookies[req.hostname]) { 116 | var cookieString = ""; 117 | req.headers["Cookie"] = domainToCookies[req.hostname].map(function(cookie) { 118 | return querystring.toQueryString(cookie); 119 | }).join(','); 120 | } 121 | 122 | return when(nextApp(req), function(response) { 123 | var cookies; 124 | if (response.headers["set-cookie"]) { 125 | var path, domain = req.hostname + (req.port ? ":"+req.port : ""); 126 | 127 | if (Array.isArray(response.headers["set-cookie"])) { 128 | cookies = []; 129 | 130 | response.headers["set-cookie"].forEach(function(cookie) { 131 | cookie = querystring.parseQuery(cookie, /[;,]/g); 132 | if (cookie.Version !== undefined) { delete cookie.Version; } 133 | if (cookie.Path !== undefined) { path = cookie.Path; delete cookie.Path; } 134 | if (cookie.HttpOnly !== undefined) { delete cookie.HttpOnly; } 135 | if (cookie.Domain !== undefined) { domain = cookie.Domain; delete cookie.Domain; } 136 | 137 | cookies.push(cookie); 138 | }); 139 | } else { 140 | cookies = querystring.parseQuery(response.headers["set-cookie"], /[;,]/g); 141 | if (cookies.Version !== undefined) { delete cookies.Version; } 142 | if (cookies.Path !== undefined) { path = cookies.Path; delete cookies.Path; } 143 | if (cookies.HttpOnly !== undefined) { delete cookies.HttpOnly; } 144 | if (cookies.Domain !== undefined) { domain = cookies.Domain; delete cookies.Domain; } 145 | 146 | cookies = [ cookies ]; 147 | } 148 | 149 | for (var k in cookies) { 150 | if (Array.isArray(cookies[k])) { 151 | cookies[k] = cookies[k][0]; 152 | } 153 | } 154 | 155 | if (cookies) { 156 | domainToCookies[req.hostname] = cookies; 157 | } 158 | } 159 | 160 | return response; 161 | }); 162 | }; 163 | }; 164 | 165 | // TODO request.Cache 166 | 167 | // TODO request.CookieJar 168 | 169 | request.Client = function(options) { 170 | if (!(this instanceof request.Client)) return new request.Client(options); 171 | options = options || {}; 172 | for (var key in options) { 173 | this[key] = options[key]; 174 | } 175 | this.request = request; 176 | // turn on redirects by default 177 | var redirects = "redirects" in this ? this.redirects : 20; 178 | if (redirects) { 179 | this.request = request.CookieJar(request.Redirect(this.request, typeof redirects === "number" && redirects)); 180 | } 181 | 182 | var finalRequest = this.request; 183 | this.request = function(options) { 184 | if (typeof options === "string") options = {url: options}; 185 | return finalRequest(options); 186 | } 187 | } 188 | return request; 189 | }); 190 | -------------------------------------------------------------------------------- /oauth.js: -------------------------------------------------------------------------------- 1 | var parseUrl = require("url").parse; 2 | var querystring = require("./util/querystring"); 3 | var whenPromise = require("./promise").whenPromise; 4 | var makeRequest = require("./http-client").request; 5 | var crypto = require("crypto"); 6 | 7 | function encodeRfc3986(str){ 8 | return !str ? "" : encodeURIComponent(str) 9 | .replace(/\!/g, "%21") 10 | .replace(/\'/g, "%27") 11 | .replace(/\(/g, "%28") 12 | .replace(/\)/g, "%29") 13 | .replace(/\*/g, "%2A"); 14 | } 15 | 16 | function parseResponse(response){ 17 | return response.body.join("").then(function(body){ 18 | if(response.status == 200){ 19 | return querystring.parse(body); 20 | }else{ 21 | var err = new Error(response.status + ": " + body); 22 | err.status = response.status; 23 | err.headers = response.headers; 24 | err.body = body; 25 | throw err; 26 | } 27 | }); 28 | } 29 | 30 | exports.Client = Client; 31 | function Client(identifier, secret, tempRequestUrl, tokenRequestUrl, callback, version, signatureMethod, nonceGenerator, headers){ 32 | this.identifier = identifier; 33 | this.tempRequestUrl = tempRequestUrl; 34 | this.tokenRequestUrl = tokenRequestUrl; 35 | this.callback = callback; 36 | this.version = version || false; 37 | // _createSignature actually uses the variable, not the instance property 38 | this.signatureMethod = signatureMethod = signatureMethod || "HMAC-SHA1"; 39 | this.generateNonce = nonceGenerator || Client.makeNonceGenerator(32); 40 | this.headers = headers || Client.Headers; 41 | 42 | if(this.signatureMethod != "PLAINTEXT" && this.signatureMethod != "HMAC-SHA1"){ 43 | throw new Error("Unsupported signature method: " + this.signatureMethod); 44 | } 45 | 46 | // We don't store the secrets on the instance itself, that way it can 47 | // be passed to other actors without leaking 48 | secret = encodeRfc3986(secret); 49 | this._createSignature = function(tokenSecret, baseString){ 50 | if(baseString === undefined){ 51 | baseString = tokenSecret; 52 | tokenSecret = ""; 53 | } 54 | 55 | var key = secret + "&" + tokenSecret; 56 | if(signatureMethod == "PLAINTEXT"){ 57 | return key; 58 | }else{ 59 | return crypto.createHmac("SHA1", key).update(baseString).digest("base64"); 60 | } 61 | }; 62 | } 63 | 64 | Client.Headers = { 65 | Accept: "*/*", 66 | Connection: "close", 67 | "User-Agent": "promised-io/oauth" 68 | }; 69 | // The default headers shouldn't change after clients have been created, 70 | // but you're free to replace the object or pass headers to the Client 71 | // constructor. 72 | Object.freeze(Client.Headers); 73 | 74 | Client.NonceChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 75 | Client.makeNonceGenerator = function(nonceSize){ 76 | var nonce = Array(nonceSize + 1).join("-").split(""); 77 | var chars = Client.NonceChars.split(""); 78 | 79 | return function nonceGenerator(){ 80 | return nonce.map(getRandomChar).join(""); 81 | }; 82 | 83 | function getRandomChar(){ 84 | return chars[Math.floor(Math.random() * chars.length)]; 85 | } 86 | }; 87 | 88 | Client.getTimestamp = function(){ 89 | return Math.floor(Date.now() / 1000).toString(); 90 | }; 91 | 92 | // Binds the client against a set of token credentials. 93 | // The resulting object can be used to make signed requests. 94 | // The secret won't be exposed on the object itself. 95 | Client.prototype.bind = function(tokenIdentifier, tokenSecret){ 96 | var bound = { 97 | identifier: this.identifier, 98 | tokenIdentifier: tokenIdentifier, 99 | signatureMethod: this.signatureMethod, 100 | version: this.version, 101 | headers: this.headers, 102 | generateNonce: this.generateNonce 103 | }; 104 | bound._createSignature = this._createSignature.bind(this, encodeRfc3986(tokenSecret)); 105 | bound._createSignatureBase = this._createSignatureBase; 106 | bound._normalizeUrl = this._normalizeUrl; 107 | bound._collectOAuthParams = this._collectOAuthParams; 108 | bound._normalizeParams = this._normalizeParams; 109 | bound._signRequest = this._signRequest; 110 | bound.request = this.request; 111 | return bound; 112 | }; 113 | 114 | // Wrapper for `http-client.request` which signs the request 115 | // with the client credentials, and optionally token credentials if bound. 116 | Client.prototype.request = function(originalRequest){ 117 | var request = {}; 118 | for(var key in originalRequest){ 119 | if(originalRequest.hasOwnProperty(key)){ 120 | request[key] = originalRequest[key]; 121 | } 122 | } 123 | 124 | // Normalize the request. `engines/node/http-client.request` is 125 | // quite flexible, but this should do it. 126 | if(request.url){ 127 | var parsed = parseUrl(request.url); 128 | parsed.pathInfo = parsed.pathname; 129 | parsed.queryString = parsed.query; 130 | for(var i in parsed){ 131 | request[i] = parsed[i]; 132 | } 133 | } 134 | request.pathname = request.pathname || request.pathInfo || "/"; 135 | request.queryString = request.queryString || request.query || ""; 136 | request.method = (request.method || "GET").toUpperCase(); 137 | request.protocol = request.protocol.toLowerCase(); 138 | request.hostname = (request.host || request.hostname).toLowerCase(); 139 | request.headers = {}; 140 | for(var h in this.headers){ 141 | request.headers[h] = this.headers[h]; 142 | } 143 | for(var h in originalRequest.headers){ 144 | request.headers[h] = originalRequest.headers[h]; 145 | } 146 | // We'll be setting the Authorization header; due to how `engines/node/http-client.request` 147 | // is implemented we need to set the Host header as well. 148 | request.headers.host = request.headers.host || request.hostname + (request.port ? ":" + request.port : ""); 149 | 150 | // Parse all request parameters into a flattened array of parameter pairs. 151 | // Note that this array contains munged parameter names. 152 | var requestParams = [], uncombinedRequestParams = []; 153 | // Start with parameters that were defined in the query string 154 | if(request.queryString){ 155 | querystring.parseToArray(requestParams, request.queryString); 156 | } 157 | // Allow parameters to be defined in object notation, this is *not* part of `http-client.request`! 158 | // It saves an extra stringify+parse though. 159 | if(request.requestParams){ 160 | for(var i in request.requestParams){ 161 | if(request.requestParams.hasOwnProperty(i)){ 162 | querystring.addToArray(uncombinedRequestParams, i, request.requestParams[i]); 163 | } 164 | } 165 | } 166 | // Send the parameters from `request.requestParams` in the query string 167 | // for GET and DELETE requests. We immediately concat to the `requestParams` array, 168 | // which is then built into the query string. 169 | if(request.method == "GET" || request.method == "DELETE"){ 170 | requestParams = requestParams.concat(uncombinedRequestParams); 171 | } 172 | // Rebuild the query string 173 | request.queryString = requestParams.reduce(function(qs, v, i){ 174 | return qs + (i % 2 ? "=" + querystring.escape(v) : (qs.length ? "&" : "") + querystring.escape(v)); 175 | }, ""); 176 | 177 | // Depending on the request content type, look for request parameters in the body 178 | var waitForBody = false; 179 | if(request.headers && request.headers["Content-Type"] == "application/x-www-form-urlencoded"){ 180 | waitForBody = whenPromise(request.body.join(""), function(body){ 181 | querystring.parseToArray(requestParams, body); 182 | return body; 183 | }); 184 | } 185 | 186 | // If we're a POST or PUT and are not sending any content, or are sending urlencoded content, 187 | // add the `request.request` to the request body. If we are sending non-urlencoded content through 188 | // a POST or PUT, the `request.requestParams` are ignored. 189 | if(request.requestParams && (request.method == "POST" || request.method == "PUT") && (!request.headers || !request.headers["Content-Type"] || request.headers["Content-Type"] == "application/x-www-form-urlencoded")){ 190 | waitForBody = whenPromise(waitForBody, function(body){ 191 | requestParams = requestParams.concat(uncombinedRequestParams); 192 | body = (body ? body + "&" : "") + querystring.stringify(request.requestParams); 193 | request.body = [body]; 194 | request.headers["Content-Type"] = "application/x-www-form-urlencoded"; 195 | }); 196 | } 197 | 198 | // Sign the request and then actually make it. 199 | return whenPromise(waitForBody, function(){ 200 | this._signRequest(request, requestParams); 201 | return makeRequest(request); 202 | }.bind(this)); 203 | }; 204 | 205 | Client.prototype._normalizeUrl = function(request){ 206 | var normalized = request.protocol + "//" + request.hostname; 207 | if(request.protocol == "http:" && request.port && (request.port + "") != "80"){ 208 | normalized += ":" + request.port; 209 | } 210 | if(request.protocol == "https:" && request.port && (request.port + "") != "443"){ 211 | normalized += ":" + request.port; 212 | } 213 | return normalized + request.pathname; 214 | }; 215 | 216 | Client.prototype._collectOAuthParams = function(request, requestParams){ 217 | var oauthParams = {}; 218 | if(request.oauthParams){ 219 | for(var p in request.oauthParams){ 220 | // Don't allow `request.oauthParams` to override standard values. 221 | // `oauth_token` and `oauth_version` are conditionally added, 222 | // the other parameters are always set. Hence we just test for 223 | // the first two. 224 | if(p != "oauth_token" && p != "oauth_version"){ 225 | oauthParams[p] = request.oauthParams[p]; 226 | } 227 | } 228 | } 229 | oauthParams.oauth_consumer_key = this.identifier; 230 | oauthParams.oauth_signature_method = this.signatureMethod; 231 | oauthParams.oauth_timestamp = Client.getTimestamp(); 232 | oauthParams.oauth_nonce = this.generateNonce(); 233 | if(this.tokenIdentifier){ 234 | oauthParams.oauth_token = this.tokenIdentifier; 235 | } 236 | if(this.version){ 237 | oauthParams.oauth_version = this.version; 238 | } 239 | for(var i in oauthParams){ 240 | requestParams.push(i, oauthParams[i]); 241 | } 242 | return oauthParams; 243 | }; 244 | 245 | Client.prototype._normalizeParams = function(requestParams){ 246 | // Encode requestParams 247 | requestParams = requestParams.map(encodeRfc3986); 248 | // Unflatten the requestParams for sorting 249 | requestParams = requestParams.reduce(function(result, _, i, arr){ 250 | if(i % 2 == 0){ 251 | result.push(arr.slice(i, i + 2)); 252 | } 253 | return result; 254 | }, []); 255 | // Sort the unflattened requestParams 256 | requestParams.sort(function(a, b){ 257 | if(a[0] == b[0]){ 258 | return a[1] < b[1] ? -1 : 1; 259 | }else{ 260 | return a[0] < b[0] ? -1 : 1; 261 | } 262 | }); 263 | return requestParams.map(function(pair){ return pair.join("="); }).join("&"); 264 | }; 265 | 266 | Client.prototype._createSignatureBase = function(requestMethod, baseUri, params){ 267 | return [requestMethod, baseUri, params].map(encodeRfc3986).join("&"); 268 | }; 269 | 270 | Client.prototype._signRequest = function(request, requestParams){ 271 | // Calculate base URI string 272 | var baseUri = this._normalizeUrl(request); 273 | 274 | // Register OAuth parameters and add to the request parameters 275 | // Additional parameters can be specified via the `request.oauthParams` object 276 | var oauthParams = this._collectOAuthParams(request, requestParams); 277 | 278 | // Generate parameter string 279 | var params = this._normalizeParams(requestParams); 280 | 281 | // Sign the base string 282 | var baseString = this._createSignatureBase(request.method, baseUri, params); 283 | oauthParams.oauth_signature = this._createSignature(baseString); 284 | 285 | // Add Authorization header 286 | request.headers.authorization = "OAuth " + Object.keys(oauthParams).map(function(name){ 287 | return encodeRfc3986(name) + "=\"" + encodeRfc3986(oauthParams[name]) + "\""; 288 | }).join(","); 289 | 290 | // Now the request object can be used to make a signed request 291 | return request; 292 | }; 293 | 294 | Client.prototype.obtainTempCredentials = function(oauthParams, extraParams){ 295 | oauthParams = oauthParams || {}; 296 | if(this.callback && !oauthParams.oauth_callback){ 297 | oauthParams.oauth_callback = this.callback; 298 | } 299 | 300 | return this.request({ 301 | method: "POST", 302 | url: this.tempRequestUrl, 303 | oauthParams: oauthParams, 304 | requestParams: extraParams || {} 305 | }).then(parseResponse); 306 | }; 307 | 308 | Client.prototype.obtainTokenCredentials = function(tokenIdentifier, tokenSecret, verifierToken, extraParams){ 309 | return this.bind(tokenIdentifier, tokenSecret).request({ 310 | method: "POST", 311 | url: this.tokenRequestUrl, 312 | oauthParams: { oauth_verifier: verifierToken }, 313 | requestParams: extraParams 314 | }).then(parseResponse); 315 | }; 316 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Promised-IO is a cross-platform package for asynchronous promise-based IO. Promises 2 | provide a simple robust mechanism for asynchronicity with separation of concerns by encapsulating 3 | eventual completion of an operation with side effect free callback registration 4 | separate from call invocation. Promised-IO provides cross-platform 5 | file, HTTP, and system interaction with promises for asynchronous operations. 6 | 7 | Promised-IO also utilizes "lazy arrays" for progressively completed 8 | actions or for streaming of data. Lazy arrays provide all the standard iterative Array methods for 9 | receiving callbacks as actions are completed. Lazy arrays are utilized 10 | for progressive loading of files and HTTP responses. 11 | 12 | # Installation 13 | 14 | Promised-IO can be installed via npm: 15 | 16 | npm install promised-io 17 | 18 | # promise 19 | 20 | The promise module provides the primary tools for creating new promises and interacting 21 | with promises. The promise API used by promised-io is the [Promises/A](http://wiki.commonjs.org/wiki/Promises/A) 22 | proposal used by Dojo, jQuery, and other toolkits. Within promised-io, a promise is 23 | defined as any object that implements the Promises/A API, that is they provide a 24 | then() method that can take a callback. The then() methods definition is: 25 | 26 | promise.then(fulfilledHandler, errorHandler); 27 | 28 | Promises can originate from a variety of sources, and promised-io provides a constructor, Deferred, 29 | to create promises. 30 | 31 | ## when 32 | 33 | when = require("promised-io/promise").when; 34 | when(promiseOrValue, fulfilledHandler, errorHandler); 35 | 36 | You can pass a promise to the when() function and the fulfillment and error handlers will be registered for it's 37 | completion *or* you can pass a regular value, and the fulfillment handler will be 38 | immediately be called. The when function is a staple of working with promises because 39 | it allows you to write code that normalizes interaction with synchronous values and asynchronous promises. 40 | If you pass in a promise, a new promise for the result of execution of the callback handler 41 | will be returned. If you pass a normal value, the return value will be the value returned 42 | from the fulfilledHandler. 43 | 44 | ## Deferred 45 | 46 | deferred = require("promised-io/promise").Deferred(canceler); 47 | 48 | The Deferred constructor is the primary mechanism for creating new promises. The Deferred 49 | object is a form of a promise with an interface for fulfilling or rejecting the promise. 50 | A Deferred object is a means for a producer to resolve a promise and it also provides 51 | a promise for consumers that are listening for the resolution of the promise. The basic 52 | usage pattern looks like: 53 | 54 | var Deferred = require("promised-io/promise").Deferred; 55 | function delay(ms, value){ 56 | // create a new Deferred 57 | var deferred = new Deferred(); 58 | setTimeout(function(){ 59 | // fulfill the deferred/promise, all listeners to the promise will be notified, and 60 | // provided the value as the value of the promise 61 | deferred.resolve(value); 62 | }, ms); 63 | // return the promise that is associated with the Deferred object 64 | return deferred.promise; 65 | } 66 | 67 | The Deferred can optionally take a canceler function. This function will cause resulting 68 | promises to have a cancel() method, and if the cancel() method is called, the 69 | Deferred will be canceled and the canceler function will be called. 70 | 71 | The Deferred object has the following methods and properties: 72 | 73 | ### resolve 74 | 75 | deferred.resolve(value); 76 | 77 | This will fulfill the Deferred's promise with the provided value. The fulfillment listeners to the promise 78 | will be notified. 79 | 80 | ### reject 81 | 82 | deferred.reject(error); 83 | 84 | This will reject the Deferred's promise with the provided error. The error listeners to the promise 85 | will be notified. 86 | 87 | ### promise 88 | 89 | This is the promise object associated with the Deferred instance. The promise object 90 | will not have any of the Deferred's fulfill or reject methods, and only provides an interface 91 | for listening. This can be safely provided to consumers without any chance of being modified. 92 | 93 | ### cancel 94 | 95 | deferred.cancel(); 96 | 97 | This will cancel the Deferred. 98 | 99 | ## currentContext 100 | 101 | One of the challenges with working asynchronous code is that there can be times when 102 | you wish for some contextual state information to be preserved across multiple 103 | asynchronous actions, without having to actually pass the state to each function in 104 | the asynchronous chain. Common examples of such contextual state would be tracking 105 | the current transaction or the currently logged in user. Such state information could be 106 | stored in a singleton (a module property or a global variable), but with asynchronous 107 | actions being interleaved, this is unsuitable for tracking state across asynchronous continuations 108 | of an action. 109 | 110 | The promised-io package's promise module provides a facility for tracking state across 111 | asynchronous operations. The promise module tracks the "currentContext" global variable, 112 | and whatever value that was in the variable at the time a promise was created 113 | will be restored when that promise is fulfilled (or rejected). 114 | 115 | ## all 116 | 117 | group = require("promised-io/promise").all(arrayOfPromises); 118 | 119 | The all() function can be passed an array of promises, or multiple promises as individual 120 | arguments, and all() will return a new promise that represents the completed values when all the promises 121 | have been fulfilled. This allows you to easily run multiple asynchronous actions, and wait 122 | for the completion ("join") of all the actions. For example: 123 | 124 | group = all(promise1, promise2, promise3); 125 | group.then(function(array){ 126 | var value1 = array[0]; // result of promise1 127 | var value2 = array[1]; // result of promise2 128 | var value3 = array[2]; // result of promise3 129 | }); 130 | 131 | ## first 132 | 133 | first = require("promised-io/promise").first(arrayOfPromises); 134 | 135 | The first() function can be passed an array of promises, or multiple promises as individual 136 | arguments, and first() will return a new promise that represents the completed value when the first promise 137 | is fulfilled. This allows you to run multiple asynchronous actions and get the first result. For example: 138 | 139 | response = first(requestToMainSite, requestToMirrorSite1, requestToMirrorSite2); 140 | response.then(function(response){ 141 | // response from the first site to respond 142 | }); 143 | 144 | ## seq 145 | 146 | result = require("promised-io/promise").seq(arrayOfActionFunctions, startingValue); 147 | 148 | The seq() function can be passed an array of functions, and seq() will execute each function 149 | in sequence, waiting for the promise returned from each one to complete before executing 150 | the next function. Each function will be called with the result of the last function (or the 151 | startingValue for the first function). 152 | 153 | ## whenPromise 154 | 155 | resultPromise = require("promised-io/promise").whenPromise(valueOrPromise, fulfillmentHandler, errorHandler); 156 | 157 | The whenPromise() function behaves exactly like when() except that whenPromise 158 | will always return a promise, even if a non-promise value is passed in. 159 | 160 | ## allKeys 161 | 162 | group = require("promised-io/promise").allKeys(hashOfPromises); 163 | 164 | Takes a hash of promises and returns a promise that is fulfilled once all the promises in the hash keys are fulfilled. 165 | 166 | # fs 167 | 168 | This module provides promise-based access to the filesystem. The API of the fs module 169 | basically follows the [Node File System module API](http://nodejs.org/api/fs.html). 170 | Each of the asynchronous functions in the Node's FS API is reflected with a corresponding 171 | function in the fs module that returns a promise (instead of requiring a callback argument in the initial call). 172 | For example, where Node has fs.rename(path1, path2, [callback]), with promised-io 173 | you would call it: 174 | 175 | var fs = require("promised-io/fs"); 176 | fs.rename(path1, path2).then(function(){ 177 | // finished renaming 178 | }); 179 | 180 | Any callback arguments will be the same minus the error argument: 181 | 182 | var fs = require("promised-io/fs"); 183 | fs.readdir(path).then(function(files){ 184 | // use the result 185 | }, function(error) { 186 | // Handle errors here instead 187 | }); 188 | 189 | One function that does differ from NodeJS's fs module is the open() function. 190 | 191 | ## open 192 | 193 | var file = require("promised-io/fs").open(path, mode); 194 | 195 | The open() function differs from simply being a promise-based version of the Node's 196 | open() function in that it immediately returns (even though the opening of the 197 | file is asynchronous) a file object that be used to read from and write to the file. 198 | 199 | To write to the file object, we can write: 200 | 201 | promiseForCompletion = file.write(contents, options, encoding); 202 | 203 | To close the file object, we can write: 204 | 205 | promiseForCompletion = file.close(); 206 | 207 | We can also use file.writeSync and file.closeSync for the synchronous versions of these 208 | functions. 209 | 210 | The file object is also a lazy array, which means you can read from the file using 211 | standard array methods. To asynchronously read the contents of a file, you can do: 212 | 213 | file.forEach(function(chunk){ 214 | // called for each chunk of the file until the end of the file is reached. 215 | }); 216 | 217 | # lazy-array 218 | 219 | The lazy-array module provides the functionality for creating and using lazy arrays, 220 | which are objects that implement the interface of the standard iterative array methods for accessing 221 | streams of data. Array methods can be called and they will be 222 | asynchronously executed as data is available. Lazy arrays are powerful way to model 223 | asynchronous streams since they can used like other JavaScript arrays. 224 | 225 | Typically you don't need to directly use this module, rather other IO modules like the 226 | file system (fs) and HTTP (http-client) modules provide lazy arrays that you can interact 227 | with. For example, we could search through a file for the string "lazy" and stop reading 228 | once we find it using the standard some() method: 229 | 230 | if(file.some(function(chunk){ 231 | return chunk.toString().indexOf("lazy") > -1; 232 | })); 233 | 234 | Lazy arrays include the follow standard array methods, providing access to the data 235 | as the stream data becomes available: 236 | 237 | * forEach 238 | * concat 239 | 240 | Additional iterative methods can also access data as it available *and* as it is requested from 241 | the returned lazy array. This means that in order for this function to be excuted, the 242 | resulting array should have a forEach (or any of the other standard methods) called on 243 | it to trigger the request for data. These methods follow this behavior: 244 | 245 | * filter 246 | * every 247 | * some 248 | * map 249 | 250 | And also these standard methods, although these must fully fetch the stream: 251 | 252 | * join 253 | * sort 254 | * reverse 255 | 256 | Also the following additional methods are available on lazy arrays: 257 | 258 | * toRealArray() - This will fetch all the data and return it as a real JavaScript array. 259 | * get(index) - This retrieves an element by index. 260 | 261 | ## LazyArray 262 | 263 | lazyArray = require("promised-io/lazy-array").LazyArray({ 264 | some: someImplementation, 265 | length: arrayLength 266 | }); 267 | 268 | This function is a constructor for creating your own lazy arrays. With this function, 269 | you don't need to implement the entire set of array methods, you can just implement 270 | the some() method and provide an array length, if it is known. 271 | 272 | ## first 273 | 274 | first = require("promised-io/lazy-array").first(lazyArray); 275 | 276 | This function returns the first element in a lazy array. 277 | 278 | ## last 279 | 280 | last = require("promised-io/lazy-array").last(lazyArray); 281 | 282 | This function returns the last element in a lazy array. 283 | 284 | ## get 285 | 286 | item = require("promised-io/lazy-array").get(index); 287 | 288 | This function returns an element by index from a lazy array. 289 | 290 | # http-client 291 | 292 | This module provides convenient promise-based access to making HTTP requests. 293 | 294 | Promised-IO is part of the Persevere project, and therefore is licensed under the 295 | AFL or BSD license. The Persevere project is administered under the Dojo foundation, 296 | and all contributions require a Dojo CLA. 297 | -------------------------------------------------------------------------------- /promise.js: -------------------------------------------------------------------------------- 1 | (function(define){ 2 | define(function(require,exports){ 3 | 4 | // Kris Zyp 5 | 6 | // this is based on the CommonJS spec for promises: 7 | // http://wiki.commonjs.org/wiki/Promises 8 | // Includes convenience functions for promises, much of this is taken from Tyler Close's ref_send 9 | // and Kris Kowal's work on promises. 10 | // // MIT License 11 | 12 | // A typical usage: 13 | // A default Promise constructor can be used to create a self-resolving deferred/promise: 14 | // var Promise = require("promise").Promise; 15 | // var promise = new Promise(); 16 | // asyncOperation(function(){ 17 | // Promise.resolve("succesful result"); 18 | // }); 19 | // promise -> given to the consumer 20 | // 21 | // A consumer can use the promise 22 | // promise.then(function(result){ 23 | // ... when the action is complete this is executed ... 24 | // }, 25 | // function(error){ 26 | // ... executed when the promise fails 27 | // }); 28 | // 29 | // Alternately, a provider can create a deferred and resolve it when it completes an action. 30 | // The deferred object a promise object that provides a separation of consumer and producer to protect 31 | // promises from being fulfilled by untrusted code. 32 | // var defer = require("promise").defer; 33 | // var deferred = defer(); 34 | // asyncOperation(function(){ 35 | // deferred.resolve("succesful result"); 36 | // }); 37 | // deferred.promise -> given to the consumer 38 | // 39 | // Another way that a consumer can use the promise (using promise.then is also allowed) 40 | // var when = require("promise").when; 41 | // when(promise,function(result){ 42 | // ... when the action is complete this is executed ... 43 | // }, 44 | // function(error){ 45 | // ... executed when the promise fails 46 | // }); 47 | 48 | exports.errorTimeout = 100; 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 | var fnArgs = Array.prototype.slice.call(arguments, 1); 90 | return this.then(function(value){ 91 | return value[functionName].apply(value, fnArgs); 92 | }); 93 | }; 94 | 95 | /** 96 | * This can be used to conviently resolve a promise with auto-handling of errors: 97 | * setTimeout(deferred.resolverCallback(function(){ 98 | * return doSomething(); 99 | * }), 100); 100 | */ 101 | Promise.prototype.resolverCallback = function(callback){ 102 | var self = this; 103 | return function(){ 104 | try{ 105 | self.resolve(callback()); 106 | }catch(e){ 107 | self.reject(e); 108 | } 109 | } 110 | }; 111 | 112 | /** Dojo/NodeJS methods*/ 113 | Promise.prototype.addCallback = function(callback){ 114 | return this.then(callback); 115 | }; 116 | 117 | Promise.prototype.addErrback = function(errback){ 118 | return this.then(function(){}, errback); 119 | }; 120 | 121 | /*Dojo methods*/ 122 | Promise.prototype.addBoth = function(callback){ 123 | return this.then(callback, callback); 124 | }; 125 | 126 | Promise.prototype.addCallbacks = function(callback, errback){ 127 | return this.then(callback, errback); 128 | }; 129 | 130 | /*NodeJS method*/ 131 | Promise.prototype.wait = function(){ 132 | return exports.wait(this); 133 | }; 134 | 135 | Deferred.prototype = Promise.prototype; 136 | // A deferred provides an API for creating and resolving a promise. 137 | exports.Promise = exports.Deferred = exports.defer = defer; 138 | function defer(canceller){ 139 | return new Deferred(canceller); 140 | } 141 | 142 | 143 | // currentContext can be set to other values 144 | // and mirrors the global. We need to go off the global in case of multiple instances 145 | // of this module, which isn't rare with NPM's package policy. 146 | Object.defineProperty && Object.defineProperty(exports, "currentContext", { 147 | set: function(value){ 148 | currentContext = value; 149 | }, 150 | get: function(){ 151 | return currentContext; 152 | } 153 | }); 154 | exports.currentContext = null; 155 | 156 | 157 | function Deferred(canceller){ 158 | var result, finished, isError, waiting = [], handled; 159 | var promise = this.promise = new Promise(); 160 | var context = exports.currentContext; 161 | 162 | function notifyAll(value){ 163 | var previousContext = exports.currentContext; 164 | if(finished){ 165 | throw new Error("This deferred has already been resolved"); 166 | } 167 | try{ 168 | if(previousContext !== context){ 169 | if(previousContext && previousContext.suspend){ 170 | previousContext.suspend(); 171 | } 172 | exports.currentContext = context; 173 | if(context && context.resume){ 174 | context.resume(); 175 | } 176 | } 177 | result = value; 178 | finished = true; 179 | for(var i = 0; i < waiting.length; i++){ 180 | notify(waiting[i]); 181 | } 182 | } 183 | finally{ 184 | if(previousContext !== context){ 185 | if(context && context.suspend){ 186 | context.suspend(); 187 | } 188 | if(previousContext && previousContext.resume){ 189 | previousContext.resume(); 190 | } 191 | exports.currentContext = previousContext; 192 | } 193 | } 194 | } 195 | function notify(listener){ 196 | var func = (isError ? listener.error : listener.resolved); 197 | if(func){ 198 | handled ? 199 | (handled.handled = true) : (handled = true); 200 | try{ 201 | var newResult = func(result); 202 | if(newResult && typeof newResult.then === "function"){ 203 | newResult.then(listener.deferred.resolve, listener.deferred.reject); 204 | return; 205 | } 206 | listener.deferred.resolve(newResult); 207 | } 208 | catch(e){ 209 | listener.deferred.reject(e); 210 | } 211 | } 212 | else{ 213 | if(isError){ 214 | listener.deferred.reject(result, typeof handled === "object" ? handled : (handled = {})); 215 | } 216 | else{ 217 | listener.deferred.resolve.call(listener.deferred, result); 218 | } 219 | } 220 | } 221 | // calling resolve will resolve the promise 222 | this.resolve = this.callback = this.emitSuccess = function(value){ 223 | notifyAll(value); 224 | }; 225 | 226 | // calling error will indicate that the promise failed 227 | var reject = this.reject = this.errback = this.emitError = function(error, handledObject){ 228 | if (typeof handledObject == "object") { 229 | if (handled) { 230 | handledObject.handled = true; 231 | } else { 232 | handled = handledObject; 233 | } 234 | } 235 | isError = true; 236 | notifyAll(error); 237 | if (!handledObject && typeof setTimeout !== "undefined") { 238 | if (!(typeof handled == "object" ? handled.handled : handled)) { 239 | // set the time out if it has not already been handled 240 | setTimeout(function () { 241 | if (!(typeof handled == "object" ? handled.handled : handled)) { 242 | throw error; 243 | } 244 | }, exports.errorTimeout); 245 | } 246 | } 247 | return handled; 248 | }; 249 | 250 | // call progress to provide updates on the progress on the completion of the promise 251 | this.progress = function(update){ 252 | for(var i = 0; i < waiting.length; i++){ 253 | var progress = waiting[i].progress; 254 | progress && progress(update); 255 | } 256 | } 257 | // provide the implementation of the promise 258 | this.then = promise.then = function(resolvedCallback, errorCallback, progressCallback){ 259 | var returnDeferred = new Deferred(promise.cancel); 260 | var listener = {resolved: resolvedCallback, error: errorCallback, progress: progressCallback, deferred: returnDeferred}; 261 | if(finished){ 262 | notify(listener); 263 | } 264 | else{ 265 | waiting.push(listener); 266 | } 267 | return returnDeferred.promise; 268 | }; 269 | var timeout; 270 | if(typeof setTimeout !== "undefined") { 271 | this.timeout = function (ms) { 272 | if (ms === undefined) { 273 | return timeout; 274 | } 275 | timeout = ms; 276 | setTimeout(function () { 277 | if (!finished) { 278 | if (promise.cancel) { 279 | promise.cancel(new Error("timeout")); 280 | } 281 | else { 282 | reject(new Error("timeout")); 283 | } 284 | } 285 | }, ms); 286 | return promise; 287 | }; 288 | } 289 | 290 | if(canceller){ 291 | this.cancel = promise.cancel = function(){ 292 | var error = canceller(); 293 | if(!(error instanceof Error)){ 294 | error = new Error(error); 295 | } 296 | reject(error); 297 | } 298 | } 299 | freeze(promise); 300 | }; 301 | 302 | function perform(value, async, sync){ 303 | try{ 304 | if(value && typeof value.then === "function"){ 305 | value = async(value); 306 | } 307 | else{ 308 | value = sync(value); 309 | } 310 | if(value && typeof value.then === "function"){ 311 | return value; 312 | } 313 | var deferred = new Deferred(); 314 | deferred.resolve(value); 315 | return deferred.promise; 316 | }catch(e){ 317 | var deferred = new Deferred(); 318 | deferred.reject(e); 319 | return deferred.promise; 320 | } 321 | 322 | } 323 | /** 324 | * Promise manager to make it easier to consume promises 325 | */ 326 | 327 | function rethrow(err){ throw err; } 328 | 329 | /** 330 | * Registers an observer on a promise, always returning a promise 331 | * @param value promise or value to observe 332 | * @param resolvedCallback function to be called with the resolved value 333 | * @param rejectCallback function to be called with the rejection reason 334 | * @param progressCallback function to be called when progress is made 335 | * @return promise for the return value from the invoked callback 336 | */ 337 | exports.whenPromise = function(value, resolvedCallback, rejectCallback, progressCallback){ 338 | var deferred = defer(); 339 | if(value && typeof value.then === "function"){ 340 | value.then(function(next){ 341 | deferred.resolve(next); 342 | },function(error){ 343 | deferred.reject(error); 344 | }); 345 | rejectCallback = rejectCallback || rethrow; 346 | }else{ 347 | deferred.resolve(value); 348 | } 349 | return deferred.promise.then(resolvedCallback, rejectCallback, progressCallback); 350 | }; 351 | 352 | /** 353 | * Registers an observer on a promise. 354 | * @param value promise or value to observe 355 | * @param resolvedCallback function to be called with the resolved value 356 | * @param rejectCallback function to be called with the rejection reason 357 | * @param progressCallback function to be called when progress is made 358 | * @return promise for the return value from the invoked callback or the value if it 359 | * is a non-promise value 360 | */ 361 | exports.when = function(value, resolvedCallback, rejectCallback, progressCallback){ 362 | if(value && typeof value.then === "function"){ 363 | if(value instanceof Promise){ 364 | return value.then(resolvedCallback, rejectCallback, progressCallback); 365 | } 366 | else{ 367 | return exports.whenPromise(value, resolvedCallback, rejectCallback, progressCallback); 368 | } 369 | } 370 | return resolvedCallback ? resolvedCallback(value) : value; 371 | }; 372 | 373 | /** 374 | * This is convenience function for catching synchronously and asynchronously thrown 375 | * errors. This is used like when() except you execute the initial action in a callback: 376 | * whenCall(function(){ 377 | * return doSomethingThatMayReturnAPromise(); 378 | * }, successHandler, errorHandler); 379 | */ 380 | exports.whenCall = function(initialCallback, resolvedCallback, rejectCallback, progressCallback){ 381 | try{ 382 | return exports.when(initialCallback(), resolvedCallback, rejectCallback, progressCallback); 383 | }catch(e){ 384 | return rejectCallback(e); 385 | } 386 | } 387 | 388 | /** 389 | * Gets the value of a property in a future turn. 390 | * @param target promise or value for target object 391 | * @param property name of property to get 392 | * @return promise for the property value 393 | */ 394 | exports.get = function(target, property){ 395 | return perform(target, function(target){ 396 | return target.get(property); 397 | }, 398 | function(target){ 399 | return target[property] 400 | }); 401 | }; 402 | 403 | /** 404 | * Invokes a method in a future turn. 405 | * @param target promise or value for target object 406 | * @param methodName name of method to invoke 407 | * @param args array of invocation arguments 408 | * @return promise for the return value 409 | */ 410 | exports.call = function(target, methodName, args){ 411 | return perform(target, function(target){ 412 | return target.call(methodName, args); 413 | }, 414 | function(target){ 415 | return target[methodName].apply(target, args); 416 | }); 417 | }; 418 | 419 | /** 420 | * Sets the value of a property in a future turn. 421 | * @param target promise or value for target object 422 | * @param property name of property to set 423 | * @param value new value of property 424 | * @return promise for the return value 425 | */ 426 | exports.put = function(target, property, value){ 427 | return perform(target, function(target){ 428 | return target.put(property, value); 429 | }, 430 | function(target){ 431 | return target[property] = value; 432 | }); 433 | }; 434 | 435 | 436 | /** 437 | * Waits for the given promise to finish, blocking (and executing other events) 438 | * if necessary to wait for the promise to finish. If target is not a promise 439 | * it will return the target immediately. If the promise results in an reject, 440 | * that reject will be thrown. 441 | * @param target promise or value to wait for. 442 | * @return the value of the promise; 443 | */ 444 | var queue; 445 | //try { 446 | // queue = require("event-loop"); 447 | //} 448 | //catch (e) {} 449 | exports.wait = function(target){ 450 | if(!queue){ 451 | throw new Error("Can not wait, the event-queue module is not available"); 452 | } 453 | if(target && typeof target.then === "function"){ 454 | var isFinished, isError, result; 455 | target.then(function(value){ 456 | isFinished = true; 457 | result = value; 458 | }, 459 | function(error){ 460 | isFinished = true; 461 | isError = true; 462 | result = error; 463 | }); 464 | while(!isFinished){ 465 | queue.processNextEvent(true); 466 | } 467 | if(isError){ 468 | throw result; 469 | } 470 | return result; 471 | } 472 | else{ 473 | return target; 474 | } 475 | }; 476 | 477 | 478 | 479 | /** 480 | * Takes an array of promises and returns a promise that is fulfilled once all 481 | * the promises in the array are fulfilled 482 | * @param array The array of promises 483 | * @return the promise that is fulfilled when all the array is fulfilled, resolved to the array of results 484 | */ 485 | exports.all = function(array){ 486 | var deferred = new Deferred(); 487 | if(Object.prototype.toString.call(array) !== '[object Array]'){ 488 | array = Array.prototype.slice.call(arguments); 489 | } 490 | var fulfilled = 0, length = array.length, rejected = false; 491 | var results = []; 492 | if (length === 0) deferred.resolve(results); 493 | else { 494 | array.forEach(function(promise, index){ 495 | exports.when(promise, 496 | function(value){ 497 | results[index] = value; 498 | fulfilled++; 499 | if(fulfilled === length){ 500 | deferred.resolve(results); 501 | } 502 | }, 503 | function(error){ 504 | if(!rejected){ 505 | deferred.reject(error); 506 | } 507 | rejected = true; 508 | }); 509 | }); 510 | } 511 | return deferred.promise; 512 | }; 513 | 514 | /** 515 | * Takes a hash of promises and returns a promise that is fulfilled once all 516 | * the promises in the hash keys are fulfilled 517 | * @param hash The hash of promises 518 | * @return the promise that is fulfilled when all the hash keys is fulfilled, resolved to the hash of results 519 | */ 520 | exports.allKeys = function(hash){ 521 | var deferred = new Deferred(); 522 | var array = Object.keys(hash); 523 | var fulfilled = 0, length = array.length; 524 | var results = {}; 525 | if (length === 0) deferred.resolve(results); 526 | else { 527 | array.forEach(function(key){ 528 | exports.when(hash[key], 529 | function(value){ 530 | results[key] = value; 531 | fulfilled++; 532 | if(fulfilled === length){ 533 | deferred.resolve(results); 534 | } 535 | }, 536 | deferred.reject); 537 | }); 538 | } 539 | return deferred.promise; 540 | }; 541 | 542 | /** 543 | * Takes an array of promises and returns a promise that is fulfilled when the first 544 | * promise in the array of promises is fulfilled 545 | * @param array The array of promises 546 | * @return a promise that is fulfilled with the value of the value of first promise to be fulfilled 547 | */ 548 | exports.first = function(array){ 549 | var deferred = new Deferred(); 550 | if(Object.prototype.toString.call(array) !== '[object Array]'){ 551 | array = Array.prototype.slice.call(arguments); 552 | } 553 | var fulfilled; 554 | array.forEach(function(promise, index){ 555 | exports.when(promise, function(value){ 556 | if (!fulfilled) { 557 | fulfilled = true; 558 | deferred.resolve(value); 559 | } 560 | }, 561 | function(error){ 562 | if (!fulfilled) { 563 | fulfilled = true; 564 | deferred.resolve(error); 565 | } 566 | }); 567 | }); 568 | return deferred.promise; 569 | }; 570 | 571 | /** 572 | * Takes an array of asynchronous functions (that return promises) and 573 | * executes them sequentially. Each funtion is called with the return value of the last function 574 | * @param array The array of function 575 | * @param startingValue The value to pass to the first function 576 | * @return the value returned from the last function 577 | */ 578 | exports.seq = function(array, startingValue){ 579 | array = array.concat(); // make a copy 580 | var deferred = new Deferred(); 581 | function next(value){ 582 | var nextAction = array.shift(); 583 | if(nextAction){ 584 | exports.when(nextAction(value), next, function(error){ 585 | deferred.reject(error, true); 586 | }); 587 | } 588 | else { 589 | deferred.resolve(value); 590 | } 591 | } 592 | next(startingValue); 593 | return deferred.promise; 594 | }; 595 | 596 | 597 | /** 598 | * Delays for a given amount of time and then fulfills the returned promise. 599 | * @param milliseconds The number of milliseconds to delay 600 | * @return A promise that will be fulfilled after the delay 601 | */ 602 | if(typeof setTimeout !== "undefined") { 603 | exports.delay = function(milliseconds) { 604 | var deferred = new Deferred(); 605 | setTimeout(function(){ 606 | deferred.resolve(); 607 | }, milliseconds); 608 | return deferred.promise; 609 | }; 610 | } 611 | 612 | 613 | 614 | /** 615 | * Runs a function that takes a callback, but returns a Promise instead. 616 | * @param func node compatible async function which takes a callback as its last argument 617 | * @return promise for the return value from the callback from the function 618 | */ 619 | exports.execute = function(asyncFunction){ 620 | var args = Array.prototype.slice.call(arguments, 1); 621 | 622 | var deferred = new Deferred(); 623 | args.push(function(error, result){ 624 | if(error) { 625 | deferred.emitError(error); 626 | } 627 | else { 628 | if(arguments.length > 2){ 629 | // if there are multiple success values, we return an array 630 | Array.prototype.shift.call(arguments, 1); 631 | deferred.emitSuccess(arguments); 632 | } 633 | else{ 634 | deferred.emitSuccess(result); 635 | } 636 | } 637 | }); 638 | asyncFunction.apply(this, args); 639 | return deferred.promise; 640 | }; 641 | 642 | function isGeneratorFunction(obj){ 643 | return obj && obj.constructor && 'GeneratorFunction' == obj.constructor.name; 644 | } 645 | 646 | /** 647 | * Promise-based coroutine trampoline 648 | * Adapted from https://github.com/deanlandolt/copromise/blob/master/copromise.js 649 | */ 650 | function run(coroutine){ 651 | var deferred = defer(); 652 | (function next(value, exception) { 653 | var result; 654 | try { 655 | result = exception ? coroutine.throw(value) : coroutine.next(value); 656 | } 657 | catch (error) { 658 | return deferred.reject(error); 659 | } 660 | if (result.done) return deferred.resolve(result.value); 661 | exports.when(result.value, next, function(error) { 662 | next(error, true); 663 | }); 664 | })(); 665 | return deferred.promise; 666 | }; 667 | 668 | /** 669 | * Creates a task from a coroutine, provided as generator. The `yield` function can be provided 670 | * a promise (or any value) to wait on, and the value will be provided when the promise resolves. 671 | * @param coroutine generator or generator function to treat as a coroutine 672 | * @return promise for the return value from the coroutine 673 | */ 674 | exports.spawn = function(coroutine){ 675 | if (isGeneratorFunction(coroutine)) { 676 | coroutine = coroutine(); 677 | } 678 | return run(coroutine); 679 | } 680 | 681 | /** 682 | * Converts a Node async function to a promise returning function 683 | * @param func node compatible async function which takes a callback as its last argument 684 | * @return A function that returns a promise 685 | */ 686 | exports.convertNodeAsyncFunction = function(asyncFunction, callbackNotDeclared){ 687 | var arity = asyncFunction.length; 688 | return function(){ 689 | var deferred = new Deferred(); 690 | if(callbackNotDeclared){ 691 | arity = arguments.length + 1; 692 | } 693 | arguments.length = arity; 694 | arguments[arity - 1] = function(error, result){ 695 | if(error) { 696 | deferred.emitError(error); 697 | } 698 | else { 699 | if(arguments.length > 2){ 700 | // if there are multiple success values, we return an array 701 | Array.prototype.shift.call(arguments, 1); 702 | deferred.emitSuccess(arguments); 703 | } 704 | else{ 705 | deferred.emitSuccess(result); 706 | } 707 | } 708 | }; 709 | asyncFunction.apply(this, arguments); 710 | return deferred.promise; 711 | }; 712 | }; 713 | 714 | /** 715 | * Returns a promise. If the object is already a Promise it is returned; otherwise 716 | * the object is wrapped in a Promise. 717 | * @param value The value to be treated as a Promise 718 | * @return A promise wrapping the original value 719 | */ 720 | exports.as = function(value){ 721 | if (value instanceof Promise) { 722 | return value; 723 | } else { 724 | var ret = defer(); 725 | ret.resolve(value); 726 | return ret; 727 | } 728 | }; 729 | }); 730 | })(typeof define!="undefined"?define:function(factory){factory(require,exports)}); 731 | --------------------------------------------------------------------------------