sumeru
Version:
A Realtime Javascript RIA Framework For Mobile WebApp
819 lines (672 loc) • 22.1 kB
JavaScript
;
var runnable = function(fw, findDiff, publishBaseDir, externalConfig, http, https, serverObjectId, url){
//package
var external = fw.addSubPackage('external');
//constants
var REQUEST_TIMEOUT = 6 * 1000; //request timeout config
var urlParser = fw.IS_SUMERU_SERVER && url;
//data managers
var remoteDataMgr = {}; //fetched and dev resolved data manager from external server
var localDataMgr = {}; //executed data manager by sumeru
var urlMgr = {}; //url manager arranged by modelName, record all subscribes.
var fetchTimer = {}; //fetch timer
//localData Constructor
function LocalData(){
this.data = [];
};
LocalData.prototype = {
getData : function(){
return this.data;
},
insert : function(item){
this.data.push(item);
},
remove : function(smr_id){
var item = this.find(smr_id);
if(item){
var index = this.data.indexOf(item);
this.data.splice(index, 1);
return true;
}
return false;
},
update : function(oldItem, newItem){
var index = this.data.indexOf(oldItem);
this.data.splice(index, 1, newItem);
},
find : function(smr_id){
var ret = this.data.filter(function(item){
return item.smr_id === smr_id;
});
if(ret.length > 1){
fw.log("ERROR: uniqueColumn data is not unique, it may cause error. ");
}
if(ret.length){
return ret[0];
}
return null;
},
findOne : function(key, value){
if(!key || !value){
return null;
}
var ret = this.data.filter(function(item){
return item[key] === value;
});
if(ret.length > 1){
fw.log("ERROR: uniqueColumn data is not unique, it may cause error. ");
}
if(ret.length){
return ret[0];
}
return null;
}
}
/**
* http.get util for external fetch
* @param {String} url: set external source localtion
* @param {Function} cb: success handler/callback
* @param {Function} errorHandler: error handler will be called automatically when error occurs in the get request.
* @param {Function} timeoutHandler: timeout handler will be called automatically when a get request is timeout.
*/
function _doGet(url, cb, errorHandler, timeoutHandler){
var chunks = [];
var size = 0;
var urlObj = urlParser && urlParser.parse(url);
var mode = urlObj.protocol === "https" ? https : http;
if(urlObj && urlObj.protocol){
delete urlObj.protocol;
}
var getRequest = mode.get(url, function(res){
var data = null;
res.on('data', function(chunk){
chunks.push(chunk);
size += chunk.length;
});
res.on('end', function(){
switch(chunks.length){
case 0 :
data = new Buffer(0);
break;
case 1 :
data = chunks[0];
break;
default :
data = new Buffer(size);
for (var i = 0, pos = 0, l = chunks.length; i < l; i++) {
var buf = chunks[i];
buf.copy(data, pos);
pos += buf.length;
}
break;
}
cb(data);
});
});
//error handler
getRequest.on('error', function(err){
var errMsg = "Error when do external fetch";
fw.log(errMsg, url);
errorHandler && errorHandler(err);
var errInfo = {
errMsg : errMsg,
errUrl : url,
err : err,
__smrerr__ : true
};
cb(errInfo);
});
//timeout handler
getRequest.setTimeout( REQUEST_TIMEOUT, function(info){
var timeoutMsg = "Timeout when do external fetch";
fw.log(timeoutMsg, url);
timeoutHandler && timeoutHandler();
var timeoutInfo = {
errMsg : timeoutMsg,
errUrl : url,
err : info,
__smrerr__ : true
};
cb(timeoutInfo);
});
}
/**
* http post util for external post
* @param {String} options: set external source localtion
* @param {String} postData: post data sent to external server.
* @param {Function} cb: success handler/callback
* @param {Function} errorHandler: error handler will be called automatically when error occurs in the get request.
* @param {Function} timeoutHandler: timeout handler will be called automatically when a get request is timeout.
*/
function _doPost(options, postData, cb, errorHandler, timeoutHandler){
var chunks = [];
var size = 0;
var mode = options.protocol === "https" ? https : http;
if(options && options.protocol){
delete options.protocol;
}
var postRequest = mode.request(options, function(res){
var data = null;
res.on('data', function(chunk){
chunks.push(chunk);
size += chunk.length;
});
res.on('end', function(){
switch(chunks.length){
case 0 :
data = new Buffer(0);
break;
case 1 :
data = chunks[0];
break;
default :
data = new Buffer(size);
for (var i = 0, pos = 0, l = chunks.length; i < l; i++) {
var buf = chunks[i];
buf.copy(data, pos);
pos += buf.length;
}
break;
}
cb(data);
});
});
postRequest.write(postData);
postRequest.end();
//error handler
postRequest.on('error', function(err){
var errMsg = "Error when do external post";
fw.log(errMsg, options, postData);
errorHandler && errorHandler(err);
var errInfo = {
errMsg : errMsg,
options : options,
requestBody : postData,
err : err,
__smrerr__ : true
};
cb(errInfo);
});
//timeout handler
postRequest.setTimeout( REQUEST_TIMEOUT, function(){
var timeoutMsg = "Timeout when do external post";
fw.log(timeoutMsg, options, postData);
timeoutHandler && timeoutHandler();
var timeoutInfo = {
errMsg : timeoutMsg,
options : options,
requestBody : postData,
__smrerr__ : true
};
cb(timeoutInfo);
});
}
//在各种post成功后,更新本地数据
/* function _updateLocalData(modelName, pubName, url, type, data){
var localData = localDataMgr[url];
if(type === 'insert'){
var struct = fw.server_model.getModelTemp(modelName);
var newItem = fw.utils.deepClone(struct);
for(var p in newItem){
newItem[p] = data[p];
}
newItem.smr_id = data.smr_id;
localData.insert(newItem);
}else if(type === 'delete'){
localData.remove(data.smr_id);
}else if(type === 'update'){
//抓取回来以后会自动update, 这里不用做localUpdate
}
} */
/**
* @method _resolve: resolve fetched originData to Array. 处理抓取的原始数据
* @param {Buffer} originData : origin data
* @param {String} pubName : publish name
* @return {Array} return the resolved data
*/
function _resolve(originData, pubName, url){
var config = externalConfig[pubName];
var data = config.buffer ? originData : originData.toString();
if(!config.resolve){ //强制有resolve函数
fw.log('Need resolve method for external fetch!');
return;
}
try{
var remoteData = config.resolve(data, pubName, url);
remoteData = Array.isArray(remoteData) ? remoteData : [remoteData];
return remoteData;
}catch(e){
fw.log("Please check fetch url, 3rd-party server encounters an error: ", url, "\n" ,data, "\n",e.stack);
return ;
}
}
/**
* @method _process: 处理外部数据,将其转成本地数据
* @param {String} modelName : name of model
* @param {String} pubName : publish name
* @param {String} url : external data source url
* @return {Array} return the processed localData
*/
function _process(modelName, pubName, url){
var struct = fw.server_model.getModelTemp(modelName);
var config = externalConfig[pubName];
var remoteData = remoteDataMgr[url];
var ret = new LocalData();
if(remoteData){
remoteData.forEach(function(item){
var newItem = fw.utils.deepClone(struct);
for(var p in newItem){
newItem[p] = item[p];
}
var unique = config.uniqueColumn || config.keyColume;
var oldItem = null;
if(unique){
oldItem = localDataMgr[url] && localDataMgr[url].findOne(unique, newItem[unique]);
}
if(oldItem){
newItem.smr_id = oldItem.smr_id;
}else{
newItem.smr_id = serverObjectId.ObjectId();
}
ret.insert(newItem);
});
localDataMgr[url] = null;
}
return ret;
}
/**
* @method _sync: 同步外部数据
* @param {String} modelName : name of model
* @param {String} pubName : publish name
* @param {String} url : external data source url
* @param {Function} callback : publish callback
* @param {Function} afterSync : after _doGet response from 3rd-party server.
*/
function _sync(modelName, pubName, url, callback, afterSync){
var config = externalConfig[pubName];
var method = config.method || "get";
var _doSync = function(data){
if(data.__smrerr__){
callback([]); //TODO when DB cache hierarchy is done by susu
return;
}
if(typeof remoteDataMgr[url] === "undefined"){ var firstFetch = true; } //首次抓取不必trigger_push
var remoteData = _resolve(data, pubName, url); //处理原始数据
if(firstFetch){
remoteDataMgr[url] = remoteData;
localDataMgr[url] = _process(modelName, pubName, url);
var dataArray = fw.utils.deepClone(localDataMgr[url].getData());
callback(dataArray);
}else{
var diff = (JSON.stringify(remoteData) === JSON.stringify(remoteDataMgr[url])); //这里可以不需要Diff工具,直接stringify对比
if(!diff && remoteData){
remoteDataMgr[url] = remoteData;
localDataMgr[url] = _process(modelName, pubName, url);
fw.netMessage.sendLocalMessage({modelName : modelName}, 'trigger_push');
}
}
afterSync && afterSync();
}
if(method.toLowerCase() === "post"){
var postData = encodeURIComponent(config.postData); //args为postData
try{
var postOptions = urlParser && urlParser.parse(url);
}catch(e){
return fw.log("externalConfig: fetchUrl must return a url \n\n", url);
}
if(!postOptions.hostname || !postOptions.pathname){
return fw.log('unexpected post url', url);
}
var opts = {
protocol : postOptions.protocol,
hostname : postOptions.hostname,
path : postOptions.pathname,
port : postOptions.port || 80,
method : 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
};
try{
_doPost(opts, postData, _doSync);
}catch(e){
fw.log("Fetch by post request failed", e);
return;
}
}else{
try{
_doGet(url, _doSync);
}catch(e){
fw.log("Fetch by get request failed", e);
}
}
}
//优先处理prepare方法,再次deInser/doDelete/doUpdate
function _getPostData(config, type, data, modelName, pubName){
var prefix = "on";
var handler, ret;
if(config.prepare){
handler = config.prepare;
}else{
var handlerName = prefix + type.charAt(0).toUpperCase() + type.substring(1);
handler = config[handlerName];
}
if(!handler){ fw.log("External Post", pubName, "unhandled operation type of", type); return false; } //hander未定义
//hack doDelele/toUpdate 增量只给了smr_id, 需要查到item, 并提供给devloper
if(type === 'delete'){
var item;
for(var i=0, l=urlMgr[modelName].length; i<l ;i++){
var url = urlMgr[modelName][i];
item = localDataMgr[url].find(data.smr_id);
if(item){break;}
}
if(item){ ret = item; }
}else if( type === 'update' ){
var item;
for(var i=0, l=urlMgr[modelName].length; i<l ;i++){
var url = urlMgr[modelName][i];
item = localDataMgr[url].find(data.smr_id);
if(item){break;}
}
if(item){
ret = fw.utils.merge(data, item); //更新操作, 提供最新数据
}
}else{
ret = data;
}
if(typeof ret === "undefined"){
fw.log("Cannot find model ", data.smr_id ,"external post");
}
if(config.prepare){
return handler(type, ret);
}else{
return handler(ret);
}
}
//优先处理getOptions函数, 再次是deleteUrl/insertUrl/updateUrl
function _getPostOptions(config, type, args){
var suffix = 'Url';
var opts;
args = args.concat(); //copy args
if(config.postUrl){
Array.prototype.unshift.call(args, type);
opts = config.postUrl.apply(null, args);
}else{
opts = config[type + suffix].apply(null, args);
}
if(!opts) { fw.log("External Post ", pubName, "options have no post config!" ); return false; }
return opts;
}
//receiver of fw.external.post()
//a low-level channel for client do post request.
fw.netMessage.setReceiver({
onMessage : {
target : "SEND_EXTERNAL_POST",
overwrite: true,
handle : function(pack,target,conn) {
var cbn = pack.cbn;
var postData = encodeURIComponent(JSON.stringify(pack.postData));
var buffer = pack.buffer;
var defaultOptions = {
method : 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
};
var opts = Library.objUtils.extend(true, defaultOptions, pack.options);
_doPost(opts, postData, function(data){
data = buffer ? data : data.toString();
fw.netMessage.sendMessage(data.toString(),cbn,conn._sumeru_socket_id);
});
}
}
});
//receiver of fw.external.get()
//a low-level channel for client do get request.
fw.netMessage.setReceiver({
onMessage : {
target : "SEND_EXTERNAL_GET",
overwrite: true,
handle : function(pack,target,conn) {
var cbn = pack.cbn;
var url = pack.url;
var buffer = pack.buffer;
_doGet(url, function(data){
data = buffer ? data : data.toString();
fw.netMessage.sendMessage(data,cbn,conn._sumeru_socket_id);
});
}
}
});
//receiver of sumeru.external.sync
fw.netMessage.setReceiver({
onMessage : {
target : "SEND_SYNC_REQUEST",
overwrite: true,
handle : function(pack,target,conn) {
var cbn = pack.cbn;
var modelName = pack.modelName;
var pubName = pack.pubName;
var url = pack.url;
var urls = urlMgr[modelName];
var config = externalConfig[pubName];
if(!urls || !config){
fw.netMessage.sendMessage({msg:"unknown modelName or pubName"},cbn,conn._sumeru_socket_id);
return false;
}
if(urls.indexOf(url) < 0){
fw.netMessage.sendMessage({msg:"unknown url"},cbn,conn._sumeru_socket_id);
return false;
}
_sync(modelName, pubName, url, function(){}, function(){
fw.netMessage.sendMessage({msg:"ok"},cbn,conn._sumeru_socket_id);
});
}
}
});
//---------------------------------- 以下为external接口 -------------------------------//
/**
* package: external
* method name: externalFetch
* @param {String} modelName: name of the model
* @param {String} pubName: name of external publish
* @param {Array} args: subscribe arguments
* @param {Function} callback: subscribe callback.
*/
function externalFetch(modelName, pubName, args, callback){
var config = externalConfig[pubName];
var method = config.method || "get";
var url;
if(method.toLowerCase() === "post"){
url = (config.fetchUrl && config.fetchUrl.apply(null, args));
config.postData = args[args.length - 1] || ""; //规定最后一个参数为postdata
}else{
url = (config.fetchUrl && config.fetchUrl.apply(null, args)) || config.geturl(args); //兼容老的geturl方法
}
//分modelName存下每一个做过external.fetch的url
if(!urlMgr[modelName]){ urlMgr[modelName] = []; }
if(urlMgr[modelName].indexOf(url) < 0){
urlMgr[modelName].push(url);
}
var localData = localDataMgr[url];
if(localData){
var dataArray = fw.utils.deepClone(localData.getData()); //生成一个对象,否则本地update导致数据同步异常
callback(dataArray); //run subsribe callback
}else{
_sync(modelName, pubName, url, callback); //同步数据
}
if(config.fetchInterval && !fetchTimer[url]){
fetchTimer[url] = setInterval(function(){
_sync(modelName, pubName, url, callback);
}, config.fetchInterval);
}
}
/**
* package: external
* method name: externalPost
* @param {String} modelName: name of the model
* @param {String} pubName: name of external publish
* @param {String} type: delta operation type, the possible values are 'delete', 'insert' or 'update';
* @param {Object} data: delta value generated by sumeru.
* @param {ArrayLike} args: subscribe arguments.
*/
function externalPost(modelName, pubName, type, smrdata, args, postCallback){
//generate postData and options by developers' config.
var config = externalConfig[pubName];
args = args.concat(); //copy args
Array.prototype.pop.call(args); //remove callback
var d = _getPostData(config, type, smrdata, modelName, pubName),
opt = _getPostOptions(config, type, args);
if(!(d && opt)){return false;} //post config error, stop post
var postData = encodeURIComponent(JSON.stringify(d)); //final postData
var defaultOptions = {
method : 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
};
var opts = Library.objUtils.extend(true, defaultOptions, opt); //final options
_doPost(opts, postData, function(data){
//成功的情况下,重新拉取数据
urlMgr[modelName].forEach(function(refetchurl){
//_updateLocalData(modelName, pubName, refetchurl, type, smrdata);
_sync(modelName, pubName, refetchurl, function(){}); //POST完成后重新抓取三方数据,trigger_push不用主动callback
});
postCallback();
});
}
/**
* package: external
* method name: sendPost
* description : send post request from client to external server
* @param {Object} options: set external source localtion
* @param {Object} postData: post data sent to external server.
* @param {Function} cb: get callback, result for getData;
*/
function sendGetRequest(url, cb, buffer){
//server
if(fw.IS_SUMERU_SERVER){
_doGet(url, function(data){
data = buffer ? data : data.toString();
cb(data);
});
} else { //client
if(!url || !cb){ fw.log('Please specify url and callback for sumeru.external.get!');}
var cbn = "WAITING_EXTERNAL_GET_CALLBACK_" + fw.utils.randomStr(8);
fw.netMessage.setReceiver({
onMessage : {
target : cbn,
overwrite: true,
once: true,
handle : function(data){
cb(data);
}
}
});
fw.netMessage.sendMessage({
cbn : cbn,
url : url,
buffer : buffer
}, "SEND_EXTERNAL_GET");
}
}
/**
* package: external
* method name: sendPost
* description : send post request from client to external server
* @param {Object} options: set external source localtion
* @param {Object} postData: post data sent to external server.
* @param {Function} cb: post callback, result for;
*/
function sendPostRequest(options, postData, cb, buffer){
//server
if(fw.IS_SUMERU_SERVER){
postData = encodeURIComponent(JSON.stringify(postData));
var defaultOptions = {
method : 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
};
var opts = Library.objUtils.extend(true, defaultOptions, options);
_doPost(opts, postData, function(data){
data = buffer ? data : data.toString();
cb(data);
});
} else { //client
if(!options || !postData){fw.log("please specify options or postData for sumeru.external.post");return false;}
cb = cb || function(){};
var cbn = "WAITING_EXTERNAL_POST_CALLBACK_" + fw.utils.randomStr(8);
fw.netMessage.setReceiver({
onMessage : {
target : cbn,
overwrite: true,
once:true,
handle : function(data){
cb(data);
}
}
});
fw.netMessage.sendMessage({
cbn : cbn,
options : options,
postData : postData,
buffer : buffer
}, "SEND_EXTERNAL_POST");
}
}
/**
* package: external
* method name: sync
* description : mandatory sync existed remote data
* @param {String} modelName: sync modelName
* @param {String} pubName: sync pubName.
* @param {String} url: sync url.
* @param {Function} cb: sync callback, result for;
*/
function synchronize(modelName, pubName, url, cb){
if(fw.IS_SUMERU_SERVER){
var urls = urlMgr[modelName];
var config = externalConfig[pubName];
_sync(modelName, pubName, url, function(){}, function(){
cb({msg:"ok"});
});
} else { //client
if(arguments.length < 3){
fw.log("please sepecify modelName, pubName and url in order.");
return false;
}
var cbn = "WAITING_SYNC_CALLBACK_" + fw.utils.randomStr(8);
fw.netMessage.setReceiver({
onMessage : {
target : cbn,
overwrite: true,
once:true,
handle : function(data){
cb && cb(data);
}
}
});
fw.netMessage.sendMessage({
cbn : cbn,
modelName : modelName,
pubName : pubName,
url : url
}, "SEND_SYNC_REQUEST");
}
}
external.__reg('doFetch', externalFetch, 'private'); //external.fetch
external.__reg('doPost', externalPost, 'private'); //external.post
external.__reg('get', sendGetRequest);
external.__reg('post', sendPostRequest);
external.__reg('sync', synchronize);
};
if(typeof module !='undefined' && module.exports){
module.exports = runnable;
}else{
runnable(sumeru);
}