1 line
22 KiB
JavaScript
1 line
22 KiB
JavaScript
!function(EXPORTS){"use strict";const floCloudAPI="object"===typeof module?module.exports:window.floCloudAPI={},DEFAULT={blockchainPrefix:35,SNStorageID:floGlobals.SNStorageID||"FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk",adminID:floGlobals.adminID,application:floGlobals.application,SNStorageName:"SuperNodeStorage",callback:(d,e)=>console.debug(d,e)};var user_id,user_public,user_private,aes_key,appObjects,generalData,lastVC;function user(id,priv){if(!priv||!id)return user.clear();let pub=floCrypto.getPubKeyHex(priv);if(!pub||!floCrypto.verifyPubKey(pub,id))return user.clear();let n=floCrypto.randInt(12,20);return aes_key=floCrypto.randString(n),user_private=Crypto.AES.encrypt(priv,aes_key),user_public=pub,user_id=id}Object.defineProperties(user,{id:{get:()=>{if(!user_id)throw"User not set";return user_id}},public:{get:()=>{if(!user_public)throw"User not set";return user_public}},sign:{value:msg=>{if(!user_private)throw"User not set";return floCrypto.signData(msg,Crypto.AES.decrypt(user_private,aes_key))}},clear:{value:()=>user_id=user_public=user_private=aes_key=void 0}}),Object.defineProperties(floCloudAPI,{SNStorageID:{get:()=>DEFAULT.SNStorageID},SNStorageName:{get:()=>DEFAULT.SNStorageName},adminID:{get:()=>DEFAULT.adminID},application:{get:()=>DEFAULT.application},user:{get:()=>user}}),Object.defineProperties(floGlobals,{appObjects:{get:()=>appObjects,set:obj=>appObjects=obj},generalData:{get:()=>generalData,set:data=>generalData=data},generalDataset:{value:(type,options={})=>generalData[filterKey(type,options)]},lastVC:{get:()=>lastVC,set:vc=>lastVC=vc}});var kBucket,supernodes={};Object.defineProperty(floCloudAPI,"nodes",{get:()=>JSON.parse(JSON.stringify(supernodes))});const K_Bucket=floCloudAPI.K_Bucket=function(masterID,nodeList){const decodeID=floID=>{let k=bitjs.Base58.decode(floID);k.shift(),k.splice(-4,4);let decodedId=Crypto.util.bytesToHex(k),nodeIdBytes=new BigInteger(decodedId,16).toByteArrayUnsigned();return new Uint8Array(nodeIdBytes)},_KB=new BuildKBucket({localNodeId:decodeID(masterID)});nodeList.forEach((id=>_KB.add({id:decodeID(id),floID:id})));const _CO=nodeList.map((id=>[_KB.distance(_KB.localNodeId,decodeID(id)),id])).sort(((a,b)=>a[0]-b[0])).map((a=>a[1]));Object.defineProperty(this,"tree",{get:()=>_KB}),Object.defineProperty(this,"list",{get:()=>Array.from(_CO)}),this.isNode=floID=>_CO.includes(floID),this.innerNodes=function(id1,id2){if(!_CO.includes(id1)||!_CO.includes(id2))throw Error("Given nodes are not supernode");let iNodes=[];for(let i=_CO.indexOf(id1)+1;_CO[i]!=id2;i++)i<_CO.length?iNodes.push(_CO[i]):i=-1;return iNodes},this.outterNodes=function(id1,id2){if(!_CO.includes(id1)||!_CO.includes(id2))throw Error("Given nodes are not supernode");let oNodes=[];for(let i=_CO.indexOf(id2)+1;_CO[i]!=id1;i++)i<_CO.length?oNodes.push(_CO[i]):i=-1;return oNodes},this.prevNode=function(id,N=1){let n=N||_CO.length;if(!_CO.includes(id))throw Error("Given node is not supernode");let pNodes=[];for(let i=0,j=_CO.indexOf(id)-1;i<n&&j!=_CO.indexOf(id);j--)j>-1?pNodes[i++]=_CO[j]:j=_CO.length;return 1==N?pNodes[0]:pNodes},this.nextNode=function(id,N=1){let n=N||_CO.length;if(!_CO.includes(id))throw Error("Given node is not supernode");n||(n=_CO.length);let nNodes=[];for(let i=0,j=_CO.indexOf(id)+1;i<n&&j!=_CO.indexOf(id);j++)j<_CO.length?nNodes[i++]=_CO[j]:j=-1;return 1==N?nNodes[0]:nNodes},this.closestNode=function(id,N=1){let decodedId=decodeID(id),n=N||_CO.length,cNodes=_KB.closest(decodedId,n).map((k=>k.floID));return 1==N?cNodes[0]:cNodes}};floCloudAPI.init=function(nodes){return new Promise(((resolve,reject)=>{try{supernodes=nodes,kBucket=new K_Bucket(DEFAULT.SNStorageID,Object.keys(supernodes)),resolve("Cloud init successful")}catch(error){reject(error)}}))},Object.defineProperty(floCloudAPI,"kBucket",{get:()=>kBucket});const _inactive=new Set;function ws_activeConnect(snID,reverse=!1){return new Promise(((resolve,reject)=>{if(_inactive.size===kBucket.list.length)return reject("Cloud offline");snID in supernodes||(snID=kBucket.closestNode(proxyID(snID))),function(snID){return new Promise(((resolve,reject)=>{if(!(snID in supernodes))return reject(`${snID} is not a supernode`);if(_inactive.has(snID))return reject(`${snID} is not active`);var wsConn=new WebSocket("wss://"+supernodes[snID].uri+"/");wsConn.onopen=evt=>resolve(wsConn),wsConn.onerror=evt=>{_inactive.add(snID),reject(`${snID} is unavailable`)}}))}(snID).then((node=>resolve(node))).catch((error=>{if(reverse)var nxtNode=kBucket.prevNode(snID);else nxtNode=kBucket.nextNode(snID);ws_activeConnect(nxtNode,reverse).then((node=>resolve(node))).catch((error=>reject(error)))}))}))}function fetch_ActiveAPI(snID,data,reverse=!1){return new Promise(((resolve,reject)=>{if(_inactive.size===kBucket.list.length)return reject("Cloud offline");snID in supernodes||(snID=kBucket.closestNode(proxyID(snID))),function(snID,data){return new Promise(((resolve,reject)=>{if(_inactive.has(snID))return reject(`${snID} is not active`);let fetcher,sn_url="https://"+supernodes[snID].uri;"string"==typeof data?fetcher=fetch(sn_url+"?"+data):"object"==typeof data&&"POST"===data.method&&(fetcher=fetch(sn_url,data)),fetcher.then((response=>{response.ok||400===response.status||500===response.status?resolve(response):reject(response)})).catch((error=>reject(error)))}))}(snID,data).then((result=>resolve(result))).catch((error=>{if(_inactive.add(snID),reverse)var nxtNode=kBucket.prevNode(snID);else nxtNode=kBucket.nextNode(snID);fetch_ActiveAPI(nxtNode,data,reverse).then((result=>resolve(result))).catch((error=>reject(error)))}))}))}function singleRequest(floID,data_obj,method="POST"){return new Promise(((resolve,reject)=>{let data;data="POST"===method?{method:"POST",body:JSON.stringify(data_obj)}:new URLSearchParams(JSON.parse(JSON.stringify(data_obj))).toString(),fetch_ActiveAPI(floID,data).then((response=>{response.ok?response.json().then((result=>resolve(result))).catch((error=>reject(error))):response.text().then((result=>reject(response.status+": "+result))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))}const _liveRequest={};function liveRequest(floID,request,callback){const filterData=void 0!==request.status?data=>{if(request.status)return data;{let filtered={};for(let i in data)request.trackList.includes(i)&&(filtered[i]=data[i]);return filtered}}:data=>{data=objectifier(data);let filtered={},proxy=proxyID(request.receiverID),r=request;for(let v in data){let d=data[v];r.atVectorClock&&r.atVectorClock!=v||!(r.atVectorClock||!r.lowerVectorClock||r.lowerVectorClock<=v)||!(r.atVectorClock||!r.upperVectorClock||r.upperVectorClock>=v)||r.afterTime&&!(r.afterTime<d.log_time)||r.application!=d.application||proxy!=d.receiverID&&proxy!=d.proxyID||r.comment&&r.comment!=d.comment||r.type&&r.type!=d.type||r.senderID&&!r.senderID.includes(d.senderID)||(filtered[v]=data[v])}return filtered};return new Promise(((resolve,reject)=>{ws_activeConnect(floID).then((node=>{let randID=floCrypto.randString(5);node.send(JSON.stringify(request)),node.onmessage=evt=>{let d=null,e=null;try{d=filterData(JSON.parse(evt.data))}catch(error){e=evt.data}finally{callback(d,e)}},_liveRequest[randID]=node,_liveRequest[randID].request=request,resolve(randID)})).catch((error=>reject(error)))}))}Object.defineProperty(floCloudAPI,"liveRequest",{get:()=>_liveRequest}),Object.defineProperty(floCloudAPI,"inactive",{get:()=>_inactive});const util=floCloudAPI.util={},encodeMessage=util.encodeMessage=function(message){return btoa(unescape(encodeURIComponent(JSON.stringify(message))))},decodeMessage=util.decodeMessage=function(message){return JSON.parse(decodeURIComponent(escape(atob(message))))},filterKey=util.filterKey=function(type,options={}){return type+(options.comment?":"+options.comment:"")+"|"+(options.group||options.receiverID||DEFAULT.adminID)+"|"+(options.application||DEFAULT.application)},proxyID=util.proxyID=function(address){if(address){var bytes;if(33==address.length||34==address.length){let decode=bitjs.Base58.decode(address);bytes=decode.slice(0,decode.length-4);let checksum=decode.slice(decode.length-4),hash=Crypto.SHA256(Crypto.SHA256(bytes,{asBytes:!0}),{asBytes:!0});hash[0]!=checksum[0]||hash[1]!=checksum[1]||hash[2]!=checksum[2]||hash[3]!=checksum[3]?bytes=void 0:bytes.shift()}else if(!address.startsWith("0x")&&42==address.length||62==address.length){if("function"!=typeof coinjs)throw"library missing (lib_btc.js)";let decode=coinjs.bech32_decode(address);decode&&((bytes=decode.data).shift(),bytes=coinjs.bech32_convert(bytes,5,8,!1),62==address.length&&(bytes=coinjs.bech32_convert(bytes,5,8,!1)))}else 66==address.length?bytes=ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address),{asBytes:!0})):(42==address.length&&address.startsWith("0x")||40==address.length&&!address.startsWith("0x"))&&(address.startsWith("0x")&&(address=address.substring(2)),bytes=Crypto.util.hexToBytes(address));if(bytes){bytes.unshift(DEFAULT.blockchainPrefix);let hash=Crypto.SHA256(Crypto.SHA256(bytes,{asBytes:!0}),{asBytes:!0});return bitjs.Base58.encode(bytes.concat(hash.slice(0,4)))}throw"Invalid address: "+address}},lastCommit={};function updateObject(objectName,dataSet){try{console.log(dataSet);let vcList=Object.keys(dataSet).sort();for(let vc of vcList)if(!(vc<lastVC[objectName]||dataSet[vc].type!==objectName)){switch(dataSet[vc].comment){case"RESET":dataSet[vc].message.reset&&(appObjects[objectName]=dataSet[vc].message.reset);break;case"UPDATE":dataSet[vc].message.diff&&(appObjects[objectName]=diff.merge(appObjects[objectName],dataSet[vc].message.diff))}lastVC[objectName]=vc}lastCommit.set(objectName),compactIDB.writeData("appObjects",appObjects[objectName],objectName),compactIDB.writeData("lastVC",lastVC[objectName],objectName)}catch(error){console.error(error)}}function storeGeneral(fk,dataSet){try{console.log(dataSet),"object"!=typeof generalData[fk]&&(generalData[fk]={});for(let vc in dataSet)generalData[fk][vc]=dataSet[vc],dataSet[vc].log_time>lastVC[fk]&&(lastVC[fk]=dataSet[vc].log_time);compactIDB.writeData("lastVC",lastVC[fk],fk),compactIDB.writeData("generalData",generalData[fk],fk)}catch(error){console.error(error)}}function objectifier(data){return Array.isArray(data)||(data=[data]),Object.fromEntries(data.map((d=>(d.message=decodeMessage(d.message),[d.vectorClock,d]))))}Object.defineProperty(lastCommit,"get",{value:objName=>JSON.parse(lastCommit[objName])}),Object.defineProperty(lastCommit,"set",{value:objName=>lastCommit[objName]=JSON.stringify(appObjects[objName])}),floCloudAPI.setStatus=function(options={}){return new Promise(((resolve,reject)=>{let callback=options.callback instanceof Function?options.callback:DEFAULT.callback;var request={floID:user.id,application:options.application||DEFAULT.application,time:Date.now(),status:!0,pubKey:user.public};let hashcontent=["time","application","floID"].map((d=>request[d])).join("|");request.sign=user.sign(hashcontent),liveRequest(options.refID||DEFAULT.adminID,request,callback).then((result=>resolve(result))).catch((error=>reject(error)))}))},floCloudAPI.requestStatus=function(trackList,options={}){return new Promise(((resolve,reject)=>{Array.isArray(trackList)||(trackList=[trackList]);let callback=options.callback instanceof Function?options.callback:DEFAULT.callback,request={status:!1,application:options.application||DEFAULT.application,trackList:trackList};liveRequest(options.refID||DEFAULT.adminID,request,callback).then((result=>resolve(result))).catch((error=>reject(error)))}))};const sendApplicationData=floCloudAPI.sendApplicationData=function(message,type,options={}){return new Promise(((resolve,reject)=>{var data={senderID:user.id,receiverID:options.receiverID||DEFAULT.adminID,pubKey:user.public,message:encodeMessage(message),time:Date.now(),application:options.application||DEFAULT.application,type:type,comment:options.comment||""};let hashcontent=["receiverID","time","application","type","message","comment"].map((d=>data[d])).join("|");data.sign=user.sign(hashcontent),singleRequest(data.receiverID,data).then((result=>resolve(result))).catch((error=>reject(error)))}))},requestApplicationData=floCloudAPI.requestApplicationData=function(type,options={}){return new Promise(((resolve,reject)=>{var request={receiverID:options.receiverID||DEFAULT.adminID,senderID:options.senderID||void 0,application:options.application||DEFAULT.application,type:type,comment:options.comment||void 0,lowerVectorClock:options.lowerVectorClock||void 0,upperVectorClock:options.upperVectorClock||void 0,atVectorClock:options.atVectorClock||void 0,afterTime:options.afterTime||void 0,mostRecent:options.mostRecent||void 0};options.callback instanceof Function?liveRequest(request.receiverID,request,options.callback).then((result=>resolve(result))).catch((error=>reject(error))):("POST"===options.method&&(request={time:Date.now(),request:request}),singleRequest(request.receiverID,request,options.method||"GET").then((data=>resolve(data))).catch((error=>reject(error))))}))};floCloudAPI.editApplicationData=function(vectorClock,comment_edit,options={}){return new Promise(((resolve,reject)=>{let req_options=Object.assign({},options);req_options.atVectorClock=vectorClock,requestApplicationData(void 0,req_options).then((result=>{if(!result.length)return reject("Data not found");let data=result[0];if(data.senderID!==user.id)return reject("Only sender can edit comment");data.comment=comment_edit;let hashcontent=["receiverID","time","application","type","message","comment"].map((d=>data[d])).join("|"),re_sign=user.sign(hashcontent);var request={receiverID:options.receiverID||DEFAULT.adminID,requestorID:user.id,pubKey:user.public,time:Date.now(),vectorClock:vectorClock,edit:comment_edit,re_sign:re_sign};let request_hash=["time","vectorClock","edit","re_sign"].map((d=>request[d])).join("|");request.sign=user.sign(request_hash),singleRequest(request.receiverID,request).then((result=>resolve(result))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},floCloudAPI.tagApplicationData=function(vectorClock,tag,options={}){return new Promise(((resolve,reject)=>{if(!floGlobals.subAdmins.includes(user.id))return reject("Only subAdmins can tag data");var request={receiverID:options.receiverID||DEFAULT.adminID,requestorID:user.id,pubKey:user.public,time:Date.now(),vectorClock:vectorClock,tag:tag};let hashcontent=["time","vectorClock","tag"].map((d=>request[d])).join("|");request.sign=user.sign(hashcontent),singleRequest(request.receiverID,request).then((result=>resolve(result))).catch((error=>reject(error)))}))},floCloudAPI.noteApplicationData=function(vectorClock,note,options={}){return new Promise(((resolve,reject)=>{var request={receiverID:options.receiverID||DEFAULT.adminID,requestorID:user.id,pubKey:user.public,time:Date.now(),vectorClock:vectorClock,note:note};let hashcontent=["time","vectorClock","note"].map((d=>request[d])).join("|");request.sign=user.sign(hashcontent),singleRequest(request.receiverID,request).then((result=>resolve(result))).catch((error=>reject(error)))}))},floCloudAPI.sendGeneralData=function(message,type,options={}){return new Promise(((resolve,reject)=>{if(options.encrypt){let encryptionKey=!0===options.encrypt?floGlobals.settings.encryptionKey:options.encrypt;message=floCrypto.encryptData(JSON.stringify(message),encryptionKey)}sendApplicationData(message,type,options).then((result=>resolve(result))).catch((error=>reject(error)))}))},floCloudAPI.requestGeneralData=function(type,options={}){return new Promise(((resolve,reject)=>{var fk=filterKey(type,options);if(lastVC[fk]=parseInt(lastVC[fk])||0,options.afterTime=options.afterTime||lastVC[fk],options.callback instanceof Function){let new_options=Object.create(options);new_options.callback=(d,e)=>{storeGeneral(fk,d),options.callback(d,e)},requestApplicationData(type,new_options).then((result=>resolve(result))).catch((error=>reject(error)))}else requestApplicationData(type,options).then((dataSet=>{storeGeneral(fk,objectifier(dataSet)),resolve(dataSet)})).catch((error=>reject(error)))}))},floCloudAPI.requestObjectData=function(objectName,options={}){return new Promise(((resolve,reject)=>{options.lowerVectorClock=options.lowerVectorClock||lastVC[objectName]+1,options.senderID=[!1,null].includes(options.senderID)?null:options.senderID||floGlobals.subAdmins,options.mostRecent=!0,options.comment="RESET";let callback=null;if(options.callback instanceof Function){let old_callback=options.callback;callback=(d,e)=>{updateObject(objectName,d),old_callback(d,e)},delete options.callback}requestApplicationData(objectName,options).then((dataSet=>{if(updateObject(objectName,objectifier(dataSet)),delete options.comment,options.lowerVectorClock=lastVC[objectName]+1,delete options.mostRecent,callback){let new_options=Object.create(options);new_options.callback=callback,requestApplicationData(objectName,new_options).then((result=>resolve(result))).catch((error=>reject(error)))}else requestApplicationData(objectName,options).then((dataSet=>{updateObject(objectName,objectifier(dataSet)),resolve(appObjects[objectName])})).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},floCloudAPI.closeRequest=function(requestID){return new Promise(((resolve,reject)=>{let conn=_liveRequest[requestID];if(!conn)return reject("Request not found");conn.onclose=evt=>{delete _liveRequest[requestID],resolve("Request connection closed")},conn.close()}))},floCloudAPI.resetObjectData=function(objectName,options={}){return new Promise(((resolve,reject)=>{let message={reset:appObjects[objectName]};options.comment="RESET",sendApplicationData(message,objectName,options).then((result=>{lastCommit.set(objectName),resolve(result)})).catch((error=>reject(error)))}))},floCloudAPI.updateObjectData=function(objectName,options={}){return new Promise(((resolve,reject)=>{let message={diff:diff.find(lastCommit.get(objectName),appObjects[objectName])};options.comment="UPDATE",sendApplicationData(message,objectName,options).then((result=>{lastCommit.set(objectName),resolve(result)})).catch((error=>reject(error)))}))},floCloudAPI.uploadFile=function(fileBlob,type,options={}){return new Promise(((resolve,reject)=>{if(!(fileBlob instanceof File||fileBlob instanceof Blob))return reject("file must be instance of File/Blob");fileBlob.arrayBuffer().then((arraybuf=>{let file_data={type:fileBlob.type,name:fileBlob.name};if(file_data.content=Crypto.util.bytesToBase64(new Uint8Array(arraybuf)),options.encrypt){let encryptionKey=!0===options.encrypt?floGlobals.settings.encryptionKey:options.encrypt;file_data=floCrypto.encryptData(JSON.stringify(file_data),encryptionKey)}sendApplicationData(file_data,type,options).then((({vectorClock:vectorClock,receiverID:receiverID,type:type,application:application})=>resolve({vectorClock:vectorClock,receiverID:receiverID,type:type,application:application}))).catch((error=>reject(error)))})).catch((error=>reject(error)))}))},floCloudAPI.downloadFile=function(vectorClock,options={}){return new Promise(((resolve,reject)=>{options.atVectorClock=vectorClock,requestApplicationData(options.type,options).then((result=>{if(!result.length)return reject("File not found");result=result[0];try{let file_data=decodeMessage(result.message);if(file_data instanceof Object&&"secret"in file_data){if(!options.decrypt)return reject("Data is encrypted");let decryptionKey=!0===options.decrypt?Crypto.AES.decrypt(user_private,aes_key):options.decrypt;Array.isArray(decryptionKey)||(decryptionKey=[decryptionKey]);let flag=!1;for(let key of decryptionKey)try{let tmp=floCrypto.decryptData(file_data,key);file_data=JSON.parse(tmp),flag=!0;break}catch(error){}if(!flag)return reject("Unable to decrypt file: Invalid private key")}let arraybuf=new Uint8Array(Crypto.util.base64ToBytes(file_data.content));result.file=new File([arraybuf],file_data.name,{type:file_data.type}),resolve(result)}catch(error){console.error(error),reject("Data is not a file")}})).catch((error=>reject(error)))}))};var diff=function(){const isDate=d=>d instanceof Date,isEmpty=o=>0===Object.keys(o).length,isObject=o=>null!=o&&"object"==typeof o,properObject=o=>isObject(o)&&!o.hasOwnProperty?{...o}:o,updatedDiff=(lhs,rhs)=>{if(lhs===rhs)return{};if(!isObject(lhs)||!isObject(rhs))return rhs;const l=properObject(lhs),r=properObject(rhs);return isDate(l)||isDate(r)?l.valueOf()==r.valueOf()?{}:r:Object.keys(r).reduce(((acc,key)=>{if(l.hasOwnProperty(key)){const difference=updatedDiff(l[key],r[key]);return isObject(difference)&&isEmpty(difference)&&!isDate(difference)?acc:{...acc,[key]:difference}}return acc}),{})},addedDiff=(lhs,rhs)=>{if(lhs===rhs||!isObject(lhs)||!isObject(rhs))return{};const l=properObject(lhs),r=properObject(rhs);return Object.keys(r).reduce(((acc,key)=>{if(l.hasOwnProperty(key)){const difference=addedDiff(l[key],r[key]);return isObject(difference)&&isEmpty(difference)?acc:{...acc,[key]:difference}}return{...acc,[key]:r[key]}}),{})},deletedDiff=(lhs,rhs)=>{if(lhs===rhs||!isObject(lhs)||!isObject(rhs))return{};const l=properObject(lhs),r=properObject(rhs);return Object.keys(l).reduce(((acc,key)=>{if(r.hasOwnProperty(key)){const difference=deletedDiff(l[key],r[key]);return isObject(difference)&&isEmpty(difference)?acc:{...acc,[key]:difference}}return{...acc,[key]:null}}),{})},mergeRecursive=(obj1,obj2,deleteMode=!1)=>{for(var p in obj2)try{obj2[p].constructor==Object?obj1[p]=mergeRecursive(obj1[p],obj2[p],deleteMode):Array.isArray(obj2[p])?obj2[p].length<1?obj1[p]=obj2[p]:obj1[p]=mergeRecursive(obj1[p],obj2[p],deleteMode):obj1[p]=deleteMode&&null===obj2[p]?void 0:obj2[p]}catch(e){obj1[p]=deleteMode&&null===obj2[p]?void 0:obj2[p]}return obj1},cleanse=obj=>(Object.keys(obj).forEach((key=>{var value=obj[key];"object"==typeof value&&null!==value?obj[key]=cleanse(value):void 0===value&&delete obj[key]})),Array.isArray(obj)&&(obj=obj.filter((v=>void 0!==v))),obj);return{find:(lhs,rhs)=>({added:addedDiff(lhs,rhs),deleted:deletedDiff(lhs,rhs),updated:updatedDiff(lhs,rhs)}),merge:(obj,diff)=>(0!==Object.keys(diff.updated).length&&(obj=mergeRecursive(obj,diff.updated)),0!==Object.keys(diff.deleted).length&&(obj=mergeRecursive(obj,diff.deleted,!0),obj=cleanse(obj)),0!==Object.keys(diff.added).length&&(obj=mergeRecursive(obj,diff.added)),obj)}}()}(); |