angoose
Version:
Angoose is a Remote Method Invocation module that comes with built-in mongoose/angular support. Now you can call server side module in browser just like you're in the server side!
1,718 lines (1,564 loc) • 82.1 kB
JavaScript
/** Angoose Client Core */
/** depdnencies: http, Q */
var AngooseClient = function() {};
function BootstrapAngoose(){
if(typeof(console) == 'undefined'){
console = { log: function(){} }
}
console.log("Bootstraping angoose-client");
// ** getClass **
//
// Get the model class, you may call static method such as findXXX or create new model instance
//
// var SampleUser = angoose.getClass('SampleUser');
// SampleUser.findOne();
// var u = new SampleUser();
//
var counterBase = Math.round(Math.random() * 10000) * 10000;
var nextCounter = function() {
return counterBase++;
}
var encode$ = function(obj){
if(!obj || typeof obj != 'object') return obj;
if(obj && obj.length){
for(var i=0;i<obj.length;i++){
obj[i] = encode$(obj[i]);
};
}
else{
//console.log("Type", (typeof obj), obj)
for(var key in obj){
var val = encode$(obj[key]);
if(key.indexOf('$')==0){
delete obj[key];
key = "_mongo_"+key;
}
obj[key] = val;
}
}
return obj;
}
var getter = function (path) {
var pieces = path.split('.');
var obj = this;
for (var i = 0, l = pieces.length; i < l; i++) {
obj = undefined === obj || null === obj
? undefined
: obj[pieces[i]];
}
return obj;
};
var handleReturn = function(retval, callback, deferred, error){
if(callback) return callback(error, retval);
return error? deferred.reject(error) : deferred.resolve(retval);
}
function staticInvoker(modelClass, methodName, methodArgs) {
return invoker( modelClass, methodName, methodArgs, null);
};
function instanceInvoker(modelClass, model, methodName, methodArgs) {
//var modelClass = getClass(model.classname$)
return invoker( modelClass, methodName, methodArgs, model);
}
function invoker(modelClass, methodName, methodArgs, modelInstance) {
// modelInstance can be null, everything else is required.
var modelName = modelClass.modelName;
console.log("invoker: ", modelName, methodName, methodArgs, modelClass);
var callback = null;
var isStatic = modelInstance==null;
var depends = modelClass.dependencies$;
var data = {
method : methodName,
seqnumber : nextCounter(),
args : [],
clazz: modelName,
static: isStatic
}
for (var i = 0; methodArgs && methodArgs.length > i; i++) {
if(typeof methodArgs[i] != 'function')
data.args.push(methodArgs[i]);
else{
callback = methodArgs[i]
data.args.push("$callback")
console.log("Callback provided");
}
}
if(!isStatic) {
data.instance = modelInstance; // including model instance data for instance methods
}
// angular http ignores all key names starting with $, this will break the mongo query
data = encode$(data);
console.log("****** BEGIN Client Call #", data.seqnumber,modelName, methodName, data);
var http = depends['http'];
var theQ = depends['promise'];
if (!http || !theQ) throw "Missing http and/or Q dependencies";
var deferred = theQ.defer();
var ret = http.post((getConfigs().urlPrefix || '/angoose') + "/rmi/" + modelName + "/" + methodName, data);
ret.done(function(retdata) {
console.log("****** END Client Call #", data.seqnumber, " result:", retdata);
var val = undefined;
//@todo construct object, ret value types: 1) model data, 2) list of model data, 3) String, 4) object
if (retdata && retdata.retval && ( retdata.datatype =='model' || retdata.datatype =='models')) {
if(retdata.datatype == 'model'){
val = new modelClass(retdata.retval);
}
else{
var models = [];
for(var i =0; i< retdata.retval.length; i++){
models.push( new modelClass( retdata.retval[i]));
}
val = models;
}
//return handleReturn(val, callback, deferred);
//
//return deferred.resolve(models);
}
if (retdata && retdata.instance && !isStatic) {
// state has been changed on the server side
modelInstance.mergeData(retdata.instance);
console.log("Merged server side data");
}
if(retdata.success)
return handleReturn(val || retdata.retval, callback, deferred);
var ex = retdata.exception? (retdata.exception.message || retdata.exception.value) : retdata;
return handleReturn(null, callback, deferred, ex);
//deferred.reject(ex)
}, function(errdata) {
console.log("****** END Client Call #", data.seqnumber, " error:", errdata);
if(!errdata) return handleReturn(null, callback, deferred, "Unexpected server error occurred, please contact admin");
var ex = errdata.exception? (errdata.exception.message || errdata.exception.value) : errdata;
return handleReturn(null, callback, deferred, ex);
//return deferred.reject(ex);
})
if(!callback) return deferred.promise;
}
function createProxy(module, funcName, func, funcType){
if(typeof(func) == 'function') return func;
if(typeof(func) == "string" && func.indexOf("function") == 0){
var vname;
return eval("vname="+func);
}
if(funcType == 'static'){
return function proxy(){
return staticInvoker(module, funcName, arguments);
}
};
if(funcType == 'instance'){
return function proxy(){
return instanceInvoker(module, this, funcName, arguments);
}
};
}
/** compile the model based on the server side schema */
function compile(modelName, schema, dependencies) {
console.log("Client: compiling schema " , modelName)
var model = function AngooseModule(data) {
//@todo proper clone
for (var i in data) {
this[i] = data[i];
}
};
// static methods
for(var name in schema.statics){
model[name] = createProxy(model, name, schema.statics[name], 'static');
}
for(var name in schema.methods){
model.prototype[name] = createProxy(model, name, schema.methods[name], 'instance');
}
//model.angoose$ = staticInvoker;
model.dependencies$ = dependencies;
model.schema = schema;
//model.prototype.angoose$ = instanceInvoker;
//model.prototype.classname$ = modelName;
//model.prototype.schema$ = schema;
model.prototype.get = getter;
model.modelName = modelName; // this is to be compatible with backend mongoose
model.name = modelName;
// merge data into this instance
model.prototype.mergeData = function(source) {
if ( typeof source != "object")
throw "Invalid source object, must be an model instance";
//@todo: proper implementation
for (var i in source) {
this[i] = source[i];
}
}
AngooseClient.models = AngooseClient.models || {};
AngooseClient.models[modelName] = model;
return model;
};
function modelNames() {
var ret = [];
for (var key in getSchemas()) {
ret.push(key);
}
return ret;
};
function init(dependencies) {
if (AngooseClient.initialized) {
console.log("init has been called");
return;
}
AngooseClient.dependencies = dependencies;
console.log("Initializing client angoose models")
for (var mName in getSchemas()) {
compile(mName, getSchemas()[mName], dependencies);
}
AngooseClient.initialized = true;
};
function getClass(name) {
if (!AngooseClient.initialized)
throw "Angoose models not initialized yet";
return AngooseClient.models[name];
};
function getConfigs() { return {"urlPrefix":"/angoose"} };
function getSchemas() { return {
"SampleService": {
"moduleName": "SampleService",
"methods": {},
"statics": {
"listFavoriteDestinations": "$PROXIED$",
"testExecutionContext": "$PROXIED$",
"testPromiseReturn": "$PROXIED$",
"testErrorBack": "$PROXIED$",
"config": "$PROXIED$"
}
},
"SampleUser": {
"moduleName": "SampleUser",
"methods": {
"getFullname": "function portable(){\n //_instance_portable\n console.log(\"getFullname\", this);\n return (this.firstname ? this.firstname +\" \": \"\") + (this.lastname || \"\");\n}",
"setPassword": "$PROXIED$",
"save": "$PROXIED$",
"remove": "$PROXIED$",
"populate": "$PROXIED$"
},
"statics": {
"getSample": "$PROXIED$",
"checkExists": "$PROXIED$",
"populate": "$PROXIED$",
"find": "$PROXIED$",
"findOne": "$PROXIED$",
"findById": "$PROXIED$",
"findByIdAndRemove": "$PROXIED$",
"findByIdAndUpdate": "$PROXIED$",
"findOneAndRemove": "$PROXIED$",
"findOneAndUpdate": "$PROXIED$",
"update": "$PROXIED$",
"remove": "$PROXIED$",
"count": "$PROXIED$",
"geoNear": "$PROXIED$",
"geoSearch": "$PROXIED$",
"create": "$PROXIED$"
},
"paths": {
"email": {
"enumValues": [],
"regExp": null,
"path": "email",
"instance": "String",
"setters": [],
"getters": [],
"options": {
"type": "String",
"required": true,
"match": {},
"unique": true
},
"_index": {
"unique": true,
"background": true
},
"isRequired": true
},
"firstname": {
"enumValues": [],
"regExp": null,
"path": "firstname",
"instance": "String",
"setters": [],
"getters": [],
"options": {
"type": "String",
"required": true
},
"_index": null,
"isRequired": true
},
"lastname": {
"enumValues": [],
"regExp": null,
"path": "lastname",
"instance": "String",
"setters": [],
"getters": [],
"options": {
"type": "String",
"required": true
},
"_index": null,
"isRequired": true
},
"status": {
"enumValues": [
"inactive",
"active",
"disabled",
"archived"
],
"regExp": null,
"path": "status",
"instance": "String",
"setters": [],
"getters": [],
"options": {
"type": "String",
"enum": [
"inactive",
"active",
"disabled",
"archived"
],
"required": true,
"def": "inactive"
},
"_index": null,
"isRequired": true
},
"password": {
"enumValues": [],
"regExp": null,
"path": "password",
"instance": "String",
"setters": [],
"getters": [],
"options": {
"type": "String"
},
"_index": null
},
"verified": {
"path": "verified",
"setters": [],
"getters": [],
"options": {
"type": "Boolean"
},
"_index": null
},
"groupRef": {
"path": "groupRef",
"instance": "ObjectID",
"setters": [],
"getters": [],
"options": {
"type": "ObjectId",
"ref": "SampeUserGroup"
},
"_index": null
},
"_id": {
"path": "_id",
"instance": "ObjectID",
"setters": [
"not-supported"
],
"getters": [],
"options": {
"type": "ObjectId",
"auto": true
},
"_index": null,
"defaultValue": "not-supported"
},
"__v": {
"path": "__v",
"instance": "Number",
"setters": [],
"getters": [],
"options": {
"type": "Number"
},
"_index": null
}
},
"options": {
"collection": "SampleUsers",
"discriminatorKey": "type",
"id": true,
"noVirtualId": false,
"_id": true,
"noId": false,
"read": null,
"shardKey": null,
"autoIndex": true,
"minimize": true,
"versionKey": "__v",
"capped": false,
"bufferCommands": true,
"strict": true,
"pluralization": true
}
},
"SampleUserGroup": {
"moduleName": "SampleUserGroup",
"methods": {
"save": "$PROXIED$",
"remove": "$PROXIED$",
"populate": "$PROXIED$"
},
"statics": {
"populate": "$PROXIED$",
"find": "$PROXIED$",
"findOne": "$PROXIED$",
"findById": "$PROXIED$",
"findByIdAndRemove": "$PROXIED$",
"findByIdAndUpdate": "$PROXIED$",
"findOneAndRemove": "$PROXIED$",
"findOneAndUpdate": "$PROXIED$",
"update": "$PROXIED$",
"remove": "$PROXIED$",
"count": "$PROXIED$",
"geoNear": "$PROXIED$",
"geoSearch": "$PROXIED$",
"create": "$PROXIED$"
},
"paths": {
"name": {
"enumValues": [],
"regExp": null,
"path": "name",
"instance": "String",
"setters": [],
"getters": [],
"options": {
"type": "String",
"required": true,
"unique": true
},
"_index": {
"unique": true,
"background": true
},
"isRequired": true
},
"description": {
"enumValues": [],
"regExp": null,
"path": "description",
"instance": "String",
"setters": [],
"getters": [],
"options": {
"type": "String"
},
"_index": null
},
"_id": {
"path": "_id",
"instance": "ObjectID",
"setters": [
"not-supported"
],
"getters": [],
"options": {
"type": "ObjectId",
"auto": true
},
"_index": null,
"defaultValue": "not-supported"
},
"__v": {
"path": "__v",
"instance": "Number",
"setters": [],
"getters": [],
"options": {
"type": "Number"
},
"_index": null
}
},
"options": {
"collection": "SampleGroups",
"id": true,
"noVirtualId": false,
"_id": true,
"noId": false,
"read": null,
"shardKey": null,
"autoIndex": true,
"minimize": true,
"discriminatorKey": "__t",
"versionKey": "__v",
"capped": false,
"bufferCommands": true,
"strict": true,
"pluralization": true
}
},
"Todo": {
"moduleName": "Todo",
"methods": {
"save": "$PROXIED$",
"remove": "$PROXIED$",
"populate": "$PROXIED$"
},
"statics": {
"populate": "$PROXIED$",
"find": "$PROXIED$",
"findOne": "$PROXIED$",
"findById": "$PROXIED$",
"findByIdAndRemove": "$PROXIED$",
"findByIdAndUpdate": "$PROXIED$",
"findOneAndRemove": "$PROXIED$",
"findOneAndUpdate": "$PROXIED$",
"update": "$PROXIED$",
"remove": "$PROXIED$",
"count": "$PROXIED$",
"geoNear": "$PROXIED$",
"geoSearch": "$PROXIED$",
"create": "$PROXIED$"
},
"paths": {
"title": {
"enumValues": [],
"regExp": null,
"path": "title",
"instance": "String",
"setters": [],
"getters": [],
"options": {
"type": "String",
"required": true
},
"_index": null,
"isRequired": true
},
"completed": {
"path": "completed",
"setters": [],
"getters": [],
"options": {
"type": "Boolean"
},
"_index": null
},
"_id": {
"path": "_id",
"instance": "ObjectID",
"setters": [
"not-supported"
],
"getters": [],
"options": {
"type": "ObjectId",
"auto": true
},
"_index": null,
"defaultValue": "not-supported"
},
"__v": {
"path": "__v",
"instance": "Number",
"setters": [],
"getters": [],
"options": {
"type": "Number"
},
"_index": null
}
},
"options": {
"id": true,
"noVirtualId": false,
"_id": true,
"noId": false,
"read": null,
"shardKey": null,
"autoIndex": true,
"minimize": true,
"discriminatorKey": "__t",
"versionKey": "__v",
"capped": false,
"bufferCommands": true,
"strict": true,
"pluralization": true
}
}
} };
AngooseClient.getClass = getClass;
AngooseClient.model = getClass;
AngooseClient.modelNames = modelNames;
AngooseClient.init = init;
AngooseClient.module = getClass;
}
BootstrapAngoose();
if (typeof(angoose) == 'undefined'){
function angoose(name){
return AngooseClient.getClass(name);
}
}
/****** Angular Adapter for Angoose Client *******/
// Angular client for Angoose automatically register the model and service classes as angular injectables.
// For instance, with the `SampleUser` model, you can inject in your controller:
//
// angular.module('myapp', ['angoose.client'])
// .controller('MyCtroller', function($scope, SampleUser){
// $scope.users = SampleUser.$query({'status':'active'});
// })
//
(function(){
if(typeof angular =='undefined'){
console.log("No angular")
return;
}
console.log("##### Angoose Client Loading");
var angularModule = angular.module("angoose.client", []);
AngooseClient.client='angular';
// ** angoose **
//
// You may require `angoose` client as an injectable
//
angularModule.factory('angoose', function($http, $q){
var myQ = addDoneMethod($q);
AngooseClient.init({
http:angularHttpWrapper($http, myQ),
promise: myQ
});
return AngooseClient;
})
function factoryFunc(modelName){
console.log("Create factory for model "+ modelName)
angularModule.factory(modelName , function($http, $q, $rootScope, $timeout, angoose){
var acModel = AngooseClient.model(modelName);
resourceAdapt(acModel, $rootScope, $timeout);
return acModel;
});
}
angular.forEach( AngooseClient.modelNames(), factoryFunc );
function addDoneMethod($q){
var myQ = angular.extend({}, $q)
myQ.defer = function(){
var out = $q.defer.apply($q, arguments);
if(out.promise.done) return out;
out.promise.done = function(successCallback, errorCallback){
out.promise.then(successCallback, errorCallback);
}
return out;
}
return myQ;
}
function angularHttpWrapper($http, $q){
var ret = {};
ret.post = function(){
var deferred = $q.defer();
$http.post.apply($http, arguments).success(function(data){
deferred.resolve( data );
}).error(function(err){
deferred.reject(err);
});
return deferred.promise;
}
return ret;
}
function resourceAdapt(modelClass, $rootScope, $timeout){
// now adpat to angular resource
modelClass.$get= function(){
if(!modelClass.findOne) throw "Model does not support findOne operation";
var modelInstance = new modelClass();
modelClass.findOne.apply(modelClass, arguments).done(function(object){
// the return data should be a model instance
modelInstance.mergeData(object);
//$rootScope.$digest();
console.log("Copied server data to placeholder", modelInstance)
}, function(err){
console.error("Unable to load server data", err);
});
return modelInstance;
};
modelClass.$query=function(){
if(!modelClass.find) throw "Model does not support find operation";
var models = [];
modelClass.find.apply(modelClass, arguments).done(function(retModels){
// the return data should be a list of models
models.length = 0;
for(var i=0; retModels && retModels.length>i; i++)
models[i] = retModels[i];
//$rootScope.$digest();
console.log("Copied server data to placeholder")
}, function(err){
console.error("Unable to load server data", err);
});
return models;
};
}
})();
// testing functions
function $anget(serviceName){
return angular.element(document).injector().get(serviceName)
}// depdnencies: http, Q
/****** jQuery plugin for Angoose Client *******/
(function(){
if(AngooseClient.client) return;
if(typeof($) == 'undefined' && typeof(jQuery) == 'undefined') return;
AngooseClient.client = 'jquery';
var $ = $ || jQuery;
var $q = typeof(Q) == 'undefined'? createQ() : Q;
console.log("##### Angoose Client for jQuery Initializing");
AngooseClient.init({
http: ajaxHttpWrapper(),
promise: $q
});
function ajaxHttpWrapper( ){
var ret = {};
ret.post = function(){
var deferred = $q.defer();
$.post.apply($, arguments).done(function(data, textStatus, jqXHR ){
console.log("Post return ", data)
deferred.resolve( data );
}).fail(function( jqXHR, textStatus, err){
deferred.reject(err);
});
return deferred.promise;
}
return ret;
}
})();
// testing functions
function $anget(serviceName){
return angular.element(document).injector().get(serviceName)
}
/** include Q for now */
function createQ() {
"use strict";
var hasStacks = false;
try {
throw new Error();
} catch (e) {
hasStacks = !!e.stack;
}
// All code after this point will be filtered from stack traces reported
// by Q.
var qStartingLine = captureLine();
var qFileName;
// shims
// used for fallback in "allResolved"
var noop = function () {};
// Use the fastest possible means to execute a task in a future turn
// of the event loop.
var nextTick =(function () {
// linked list of tasks (single, with head node)
var head = {task: void 0, next: null};
var tail = head;
var flushing = false;
var requestTick = void 0;
var isNodeJS = false;
function flush() {
/* jshint loopfunc: true */
while (head.next) {
head = head.next;
var task = head.task;
head.task = void 0;
var domain = head.domain;
if (domain) {
head.domain = void 0;
domain.enter();
}
try {
task();
} catch (e) {
if (isNodeJS) {
// In node, uncaught exceptions are considered fatal errors.
// Re-throw them synchronously to interrupt flushing!
// Ensure continuation if the uncaught exception is suppressed
// listening "uncaughtException" events (as domains does).
// Continue in next event to avoid tick recursion.
if (domain) {
domain.exit();
}
setTimeout(flush, 0);
if (domain) {
domain.enter();
}
throw e;
} else {
// In browsers, uncaught exceptions are not fatal.
// Re-throw them asynchronously to avoid slow-downs.
setTimeout(function() {
throw e;
}, 0);
}
}
if (domain) {
domain.exit();
}
}
flushing = false;
}
nextTick = function (task) {
tail = tail.next = {
task: task,
domain: isNodeJS && process.domain,
next: null
};
if (!flushing) {
flushing = true;
requestTick();
}
};
if (typeof process !== "undefined" && process.nextTick) {
// Node.js before 0.9. Note that some fake-Node environments, like the
// Mocha test runner, introduce a `process` global without a `nextTick`.
isNodeJS = true;
requestTick = function () {
process.nextTick(flush);
};
} else if (typeof setImmediate === "function") {
// In IE10, Node.js 0.9+, or https://github.com/NobleJS/setImmediate
if (typeof window !== "undefined") {
requestTick = setImmediate.bind(window, flush);
} else {
requestTick = function () {
setImmediate(flush);
};
}
} else if (typeof MessageChannel !== "undefined") {
// modern browsers
// http://www.nonblocking.io/2011/06/windownexttick.html
var channel = new MessageChannel();
// At least Safari Version 6.0.5 (8536.30.1) intermittently cannot create
// working message ports the first time a page loads.
channel.port1.onmessage = function () {
requestTick = requestPortTick;
channel.port1.onmessage = flush;
flush();
};
var requestPortTick = function () {
// Opera requires us to provide a message payload, regardless of
// whether we use it.
channel.port2.postMessage(0);
};
requestTick = function () {
setTimeout(flush, 0);
requestPortTick();
};
} else {
// old browsers
requestTick = function () {
setTimeout(flush, 0);
};
}
return nextTick;
})();
// Attempt to make generics safe in the face of downstream
// modifications.
// There is no situation where this is necessary.
// If you need a security guarantee, these primordials need to be
// deeply frozen anyway, and if you donb t need a security guarantee,
// this is just plain paranoid.
// However, this **might** have the nice side-effect of reducing the size of
// the minified code by reducing x.call() to merely x()
// See Mark Millerb s explanation of what this does.
// http://wiki.ecmascript.org/doku.php?id=conventions:safe_meta_programming
var call = Function.call;
function uncurryThis(f) {
return function () {
return call.apply(f, arguments);
};
}
// This is equivalent, but slower:
// uncurryThis = Function_bind.bind(Function_bind.call);
// http://jsperf.com/uncurrythis
var array_slice = uncurryThis(Array.prototype.slice);
var array_reduce = uncurryThis(
Array.prototype.reduce || function (callback, basis) {
var index = 0,
length = this.length;
// concerning the initial value, if one is not provided
if (arguments.length === 1) {
// seek to the first value in the array, accounting
// for the possibility that is is a sparse array
do {
if (index in this) {
basis = this[index++];
break;
}
if (++index >= length) {
throw new TypeError();
}
} while (1);
}
// reduce
for (; index < length; index++) {
// account for the possibility that the array is sparse
if (index in this) {
basis = callback(basis, this[index], index);
}
}
return basis;
}
);
var array_indexOf = uncurryThis(
Array.prototype.indexOf || function (value) {
// not a very good shim, but good enough for our one use of it
for (var i = 0; i < this.length; i++) {
if (this[i] === value) {
return i;
}
}
return -1;
}
);
var array_map = uncurryThis(
Array.prototype.map || function (callback, thisp) {
var self = this;
var collect = [];
array_reduce(self, function (undefined, value, index) {
collect.push(callback.call(thisp, value, index, self));
}, void 0);
return collect;
}
);
var object_create = Object.create || function (prototype) {
function Type() { }
Type.prototype = prototype;
return new Type();
};
var object_hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty);
var object_keys = Object.keys || function (object) {
var keys = [];
for (var key in object) {
if (object_hasOwnProperty(object, key)) {
keys.push(key);
}
}
return keys;
};
var object_toString = uncurryThis(Object.prototype.toString);
function isObject(value) {
return value === Object(value);
}
// generator related shims
// FIXME: Remove this function once ES6 generators are in SpiderMonkey.
function isStopIteration(exception) {
return (
object_toString(exception) === "[object StopIteration]" ||
exception instanceof QReturnValue
);
}
// FIXME: Remove this helper and Q.return once ES6 generators are in
// SpiderMonkey.
var QReturnValue;
if (typeof ReturnValue !== "undefined") {
QReturnValue = ReturnValue;
} else {
QReturnValue = function (value) {
this.value = value;
};
}
// Until V8 3.19 / Chromium 29 is released, SpiderMonkey is the only
// engine that has a deployed base of browsers that support generators.
// However, SM's generators use the Python-inspired semantics of
// outdated ES6 drafts. We would like to support ES6, but we'd also
// like to make it possible to use generators in deployed browsers, so
// we also support Python-style generators. At some point we can remove
// this block.
var hasES6Generators;
try {
/* jshint evil: true, nonew: false */
new Function("(function* (){ yield 1; })");
hasES6Generators = true;
} catch (e) {
hasES6Generators = false;
}
// long stack traces
var STACK_JUMP_SEPARATOR = "From previous event:";
function makeStackTraceLong(error, promise) {
// If possible, transform the error stack trace by removing Node and Q
// cruft, then concatenating with the stack trace of `promise`. See #57.
if (hasStacks &&
promise.stack &&
typeof error === "object" &&
error !== null &&
error.stack &&
error.stack.indexOf(STACK_JUMP_SEPARATOR) === -1
) {
var stacks = [];
for (var p = promise; !!p; p = p.source) {
if (p.stack) {
stacks.unshift(p.stack);
}
}
stacks.unshift(error.stack);
var concatedStacks = stacks.join("\n" + STACK_JUMP_SEPARATOR + "\n");
error.stack = filterStackString(concatedStacks);
}
}
function filterStackString(stackString) {
var lines = stackString.split("\n");
var desiredLines = [];
for (var i = 0; i < lines.length; ++i) {
var line = lines[i];
if (!isInternalFrame(line) && !isNodeFrame(line) && line) {
desiredLines.push(line);
}
}
return desiredLines.join("\n");
}
function isNodeFrame(stackLine) {
return stackLine.indexOf("(module.js:") !== -1 ||
stackLine.indexOf("(node.js:") !== -1;
}
function getFileNameAndLineNumber(stackLine) {
// Named functions: "at functionName (filename:lineNumber:columnNumber)"
// In IE10 function name can have spaces ("Anonymous function") O_o
var attempt1 = /at .+ \((.+):(\d+):(?:\d+)\)$/.exec(stackLine);
if (attempt1) {
return [attempt1[1], Number(attempt1[2])];
}
// Anonymous functions: "at filename:lineNumber:columnNumber"
var attempt2 = /at ([^ ]+):(\d+):(?:\d+)$/.exec(stackLine);
if (attempt2) {
return [attempt2[1], Number(attempt2[2])];
}
// Firefox style: "function@filename:lineNumber or @filename:lineNumber"
var attempt3 = /.*@(.+):(\d+)$/.exec(stackLine);
if (attempt3) {
return [attempt3[1], Number(attempt3[2])];
}
}
function isInternalFrame(stackLine) {
var fileNameAndLineNumber = getFileNameAndLineNumber(stackLine);
if (!fileNameAndLineNumber) {
return false;
}
var fileName = fileNameAndLineNumber[0];
var lineNumber = fileNameAndLineNumber[1];
return fileName === qFileName &&
lineNumber >= qStartingLine &&
lineNumber <= qEndingLine;
}
// discover own file name and line number range for filtering stack
// traces
function captureLine() {
if (!hasStacks) {
return;
}
try {
throw new Error();
} catch (e) {
var lines = e.stack.split("\n");
var firstLine = lines[0].indexOf("@") > 0 ? lines[1] : lines[2];
var fileNameAndLineNumber = getFileNameAndLineNumber(firstLine);
if (!fileNameAndLineNumber) {
return;
}
qFileName = fileNameAndLineNumber[0];
return fileNameAndLineNumber[1];
}
}
function deprecate(callback, name, alternative) {
return function () {
if (typeof console !== "undefined" &&
typeof console.warn === "function") {
console.warn(name + " is deprecated, use " + alternative +
" instead.", new Error("").stack);
}
return callback.apply(callback, arguments);
};
}
// end of shims
// beginning of real work
/**
* Constructs a promise for an immediate reference, passes promises through, or
* coerces promises from different systems.
* @param value immediate reference or promise
*/
function Q(value) {
// If the object is already a Promise, return it directly. This enables
// the resolve function to both be used to created references from objects,
// but to tolerably coerce non-promises to promises.
if (isPromise(value)) {
return value;
}
// assimilate thenables
if (isPromiseAlike(value)) {
return coerce(value);
} else {
return fulfill(value);
}
}
Q.resolve = Q;
/**
* Performs a task in a future turn of the event loop.
* @param {Function} task
*/
Q.nextTick = nextTick;
/**
* Controls whether or not long stack traces will be on
*/
Q.longStackSupport = false;
/**
* Constructs a {promise, resolve, reject} object.
*
* `resolve` is a callback to invoke with a more resolved value for the
* promise. To fulfill the promise, invoke `resolve` with any value that is
* not a thenable. To reject the promise, invoke `resolve` with a rejected
* thenable, or invoke `reject` with the reason directly. To resolve the
* promise to another thenable, thus putting it in the same state, invoke
* `resolve` with that other thenable.
*/
Q.defer = defer;
function defer() {
// if "messages" is an "Array", that indicates that the promise has not yet
// been resolved. If it is "undefined", it has been resolved. Each
// element of the messages array is itself an array of complete arguments to
// forward to the resolved promise. We coerce the resolution value to a
// promise using the `resolve` function because it handles both fully
// non-thenable values and other thenables gracefully.
var messages = [], progressListeners = [], resolvedPromise;
var deferred = object_create(defer.prototype);
var promise = object_create(Promise.prototype);
promise.promiseDispatch = function (resolve, op, operands) {
var args = array_slice(arguments);
if (messages) {
messages.push(args);
if (op === "when" && operands[1]) { // progress operand
progressListeners.push(operands[1]);
}
} else {
nextTick(function () {
resolvedPromise.promiseDispatch.apply(resolvedPromise, args);
});
}
};
// XXX deprecated
promise.valueOf = function () {
if (messages) {
return promise;
}
var nearerValue = nearer(resolvedPromise);
if (isPromise(nearerValue)) {
resolvedPromise = nearerValue; // shorten chain
}
return nearerValue;
};
promise.inspect = function () {
if (!resolvedPromise) {
return { state: "pending" };
}
return resolvedPromise.inspect();
};
if (Q.longStackSupport && hasStacks) {
try {
throw new Error();
} catch (e) {
// NOTE: don't try to use `Error.captureStackTrace` or transfer the
// accessor around; that causes memory leaks as per GH-111. Just
// reify the stack trace as a string ASAP.
//
// At the same time, cut off the first line; it's always just
// "[object Promise]\n", as per the `toString`.
promise.stack = e.stack.substring(e.stack.indexOf("\n") + 1);
}
}
// NOTE: we do the checks for `resolvedPromise` in each method, instead of
// consolidating them into `become`, since otherwise we'd create new
// promises with the lines `become(whatever(value))`. See e.g. GH-252.
function become(newPromise) {
resolvedPromise = newPromise;
promise.source = newPromise;
array_reduce(messages, function (undefined, message) {
nextTick(function () {
newPromise.promiseDispatch.apply(newPromise, message);
});
}, void 0);
messages = void 0;
progressListeners = void 0;
}
deferred.promise = promise;
deferred.resolve = function (value) {
if (resolvedPromise) {
return;
}
become(Q(value));
};
deferred.fulfill = function (value) {
if (resolvedPromise) {
return;
}
become(fulfill(value));
};
deferred.reject = function (reason) {
if (resolvedPromise) {
return;
}
become(reject(reason));
};
deferred.notify = function (progress) {
if (resolvedPromise) {
return;
}
array_reduce(progressListeners, function (undefined, progressListener) {
nextTick(function () {
progressListener(progress);
});
}, void 0);
};
return deferred;
}
/**
* Creates a Node-style callback that will resolve or reject the deferred
* promise.
* @returns a nodeback
*/
defer.prototype.makeNodeResolver = function () {
var self = this;
return function (error, value) {
if (error) {
self.reject(error);
} else if (arguments.length > 2) {
self.resolve(array_slice(arguments, 1));
} else {
self.resolve(value);
}
};
};
/**
* @param resolver {Function} a function that returns nothing and accepts
* the resolve, reject, and notify functions for a deferred.
* @returns a promise that may be resolved with the given resolve and reject
* functions, or rejected by a thrown exception in resolver
*/
Q.promise = promise;
function promise(resolver) {
if (typeof resolver !== "function") {
throw new TypeError("resolver must be a function.");
}
var deferred = defer();
try {
resolver(deferred.resolve, deferred.reject, deferred.notify);
} catch (reason) {
deferred.reject(reason);
}
return deferred.promise;
}
// XXX experimental. This method is a way to denote that a local value is
// serializable and should be immediately dispatched to a remote upon request,
// instead of passing a reference.
Q.passByCopy = function (object) {
//freeze(object);
//passByCopies.set(object, true);
return object;
};
Promise.prototype.passByCopy = function () {
//freeze(object);
//passByCopies.set(object, true);
return this;
};
/**
* If two promises eventually fulfill to the same value, promises that value,
* but otherwise rejects.
* @param x {Any*}
* @param y {Any*}
* @returns {Any*} a promise for x and y if they are the same, but a rejection
* otherwise.
*
*/
Q.join = function (x, y) {
return Q(x).join(y);
};
Promise.prototype.join = function (that) {
return Q([this, that]).spread(function (x, y) {
if (x === y) {
// TODO: "===" should be Object.is or equiv
return x;
} else {
throw new Error("Can't join: not the same: " + x + " " + y);
}
});
};
/**
* Returns a promise for the first of an array of promises to become fulfilled.
* @param answers {Array[Any*]} promises to race
* @returns {Any*} the first promise to be fulfilled
*/
Q.race = race;
function race(answerPs) {
return promise(function(resolve, reject) {
// Switch to this once we can assume at least ES5
// answerPs.forEach(function(answerP) {
// Q(answerP).then(resolve, reject);
// });
// Use this in the meantime
for (var i = 0, len = answerPs.length; i < len; i++) {
Q(answerPs[i]).then(resolve, reject);
}
});
}
Promise.prototype.race = function () {
return this.then(Q.race);
};
/**
* Constructs a Promise with a promise descriptor object and optional fallback
* function. The descriptor contains methods like when(rejected), get(name),
* set(name, value), post(name, args), and delete(name), which all
* return either a value, a promise for a value, or a rejection. The fallback
* accepts the operation name, a resolver, and any further arguments that would
* have been forwarded to the appropriate method above had a method been
* provided with the proper name. The API makes no guarantees about the nature
* of the returned object, apart from that it is usable whereever promises are
* bought and sold.
*/
Q.makePromise = Promise;
function Promise(descriptor, fallback, inspect) {
if (fallback === void 0) {
fallback = function (op) {
return reject(new Error(
"Promise does not support operation: " + op
));
};
}
if (inspect === void 0) {
inspect = function () {
return {state: "unknown"};
};
}
var promise = object_create(Promise.prototype);
promise.promiseDispatch = function (resolve, op, args) {
var result;
try {
if (descriptor[op]) {
result = descriptor[op].apply(promise, args);
} else {
result = fallback.call(promise, op, args);
}
} catch (exception) {
result = reject(exception);
}
if (resolve) {
resolve(result);
}
};
promise.inspect = inspect;
// XXX deprecated `valueOf` and `exception` support
if (inspect) {
var inspected = inspect();
if (inspected.state === "rejected") {
promise.exception = inspected.reason;
}
promise.valueOf = function () {
var inspected = inspect();
if (inspected.state === "pending" ||
inspected.state === "rejected") {
return promise;
}
return inspected.value;
};
}
return promise;
}
Promise.prototype.toString = function () {
return "[object Promise]";
};
Promise.prototype.then = function (fulfilled, rejected, progressed) {
var self = this;
var deferred = defer();
var done = false; // ensure the untrusted promise makes at most a
// single call to one of the callbacks
function _fulfilled(value) {
try {
return typeof fulfilled === "function" ? fulfilled(value) : value;
} catch (exception) {
return reject(exception);
}
}
function _rejected(exception) {
if (typeof rejected === "function") {
makeStackTraceLong(exception, self);
try {
return rejected(exception);
} catch (newException) {
return reject(newException);
}
}
return reject(exception);
}
function _progressed(value) {
return typeof progressed === "function" ? progressed(value) : value;
}
nextTick(function () {
self.promiseDispatch(function (value) {
if (done) {
return;
}
done = true;
deferred.resolve(_fulfilled(value));
}, "when", [function (exception) {
if (done) {
return;
}
done = true;
deferred.resolve(_rejected(exception));
}]);
});
// Progress propagator need to be attached in the current tick.
self.promiseDispatch(void 0, "when", [void 0, function (value) {
var newValue;
var threw = false;
try {
newValue = _progressed(value);
} catch (e) {
threw = true;
if (Q.onerror) {
Q.onerror(e);
} else {
throw e;
}
}
if (!threw) {
deferred.notify(newValue);
}
}]);
return deferred.promise;
};
/**
* Registers an observer on a promise.
*
* Guarantees:
*
* 1. that fulfilled and rejected will be called only once.
* 2. that either the fulfilled callback or the rejected callback will be
* called, but not both.
* 3. that fulfilled and rejected will not be called in this turn.
*
* @param value promise or immediate reference to observe
* @param fulfilled function to be called with the fulfilled value
* @param rejected function to be called with the rejection exception
* @param progressed function to be called on any progress notifications
* @return promise for the return value from the invoked callback
*/
Q.when = when;
function when(value, fulfilled, rejected, progressed) {
return Q(value).then(fulfilled, rejected, progressed);
}
Promise.prototype.thenResolve = function (value) {
return this.then(function () { return value; });
};
Q.thenResolve = function (promise, value) {
return Q(promise).thenResolve(value);
};
Promise.prototype.thenReject = function (reason) {
return this.then(function () { throw reason; });
};
Q.thenReject = function (promise, reason) {
return Q(promise).thenReject(reason);
};
/**
* If an object is not a promise, it is as "near" as possible.
* If a promise is rejected, it is as "near" as possible too.
* If itb s a fulfilled promise, the fulfillment value is nearer.
* If itb s a deferred promise and the deferred has been resolved, the
* resolution is "nearer".
* @param object
* @returns most resolved (nearest) form of the object
*/
// XXX should we re-do this?
Q.nearer = nearer;
function nearer(value) {
if (isPromise(value)) {
var inspected = value.inspect();
if (inspected.state === "fulfilled") {
return inspected.value;
}
}
return value;
}
/**
* @returns whether the given object is a promise.
* Otherwise it is a fulfilled value.
*/
Q.isPromise = isPromise;
function isPromise(object) {
return isObject(object) &&
typeof object.promiseDispatch === "function" &&
typeof object.inspect === "function";
}
Q.isPromiseAlike = isPromiseAlike;
function isPromiseAlike(object) {
return isObject(object) && typeof object.then === "function";
}
/**
* @returns whether the given object is a pending promise, meaning not
* fulfilled or rejected.
*/
Q.isPending = isPending;
function isPending(object) {
return isPromise(object) && object.inspect().state === "pending";
}
Promise.prototype.isPending = function () {
return this.inspect().state === "pending";
};
/**
* @returns whether the given object is a value or fulfilled
* promise.
*/
Q.isFulfilled = isFulfilled;
function isFulfilled(object) {
return !isPromise(object) || object.inspect().state === "fulfilled";
}
Promise.prototype.isFulfilled = function () {
return this.inspect().state === "fulfilled";
};
/**
* @returns whether the given object is a rejected promise.
*/
Q.isRejected = isRejected;
function isRejected(object) {
return isPromise(object) && object.inspect().state === "rejected";
}
Promise.prototype.isRejected = function () {
return this.inspect().state === "rejected";
};
//// BEGIN UNHANDLED REJECTION TRACKING
// This promise library consumes exceptions thrown in handlers so they can be
// handled by a subsequent promise. The exceptions get added to this array when
// they are created, and removed when they are handled. Note that in ES6 or
// shimmed environments, this would naturally be a `Set`.
var unhandledReasons = [];
var unhandledRejections = [];
var unhandledReasonsDisplayed = false;
var trackUnhandledRejections = true;
function displayUnhandledReasons() {
if (
!unhandledReasonsDisplayed &&
typeof window !== "undefined" &&
window.console
) {
console.warn("[Q] Unhandled rejection reasons (should be empty):",
unhandledReasons);
}
unhandledReasonsDisplayed = true;
}
function logUnhandledReasons() {
for (var i = 0; i < unhandledReasons.length; i++) {
var reason = unhandledReasons[i];
console.warn("Unhandled rejection reason:", reason);
}
}
function resetUnhandledRejections() {
unhandledReasons.length = 0;
unhandledRejections.length = 0;
unhandledReasonsDisplayed = false;
if (!trackUnhandledRejections) {
trackUnhandledRejections = true;
// Show unhandled rejection reasons if Node exits without handling an
// outstanding rejection. (Note that Browserify presently produces a
// `process` global without the `EventEmitter` `on` method.)
if (typeof process !== "undefined" && process.on) {
process.on("exit", logUnhandledReasons);
}
}
}
function trackRejection(promise, reason) {
if (!trackUnhandledRejections)