1190 lines
46 KiB
JavaScript
1190 lines
46 KiB
JavaScript
(function (EXPORTS) { //floCloudAPI v2.4.5a
|
||
/* FLO Cloud operations to send/request application data*/
|
||
'use strict';
|
||
const floCloudAPI = EXPORTS;
|
||
|
||
const DEFAULT = {
|
||
blockchainPrefix: 0x23, //Prefix version for FLO blockchain
|
||
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;
|
||
|
||
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);
|
||
aes_key = floCrypto.randString(n);
|
||
user_private = Crypto.AES.encrypt(priv, aes_key);
|
||
user_public = pub;
|
||
user_id = id;
|
||
return user_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 = undefined
|
||
}
|
||
})
|
||
|
||
Object.defineProperties(floCloudAPI, {
|
||
SNStorageID: {
|
||
get: () => DEFAULT.SNStorageID
|
||
},
|
||
SNStorageName: {
|
||
get: () => DEFAULT.SNStorageName
|
||
},
|
||
adminID: {
|
||
get: () => DEFAULT.adminID
|
||
},
|
||
application: {
|
||
get: () => DEFAULT.application
|
||
},
|
||
user: {
|
||
get: () => user
|
||
}
|
||
});
|
||
|
||
var appObjects, generalData, lastVC;
|
||
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 supernodes = {}; //each supnernode must be stored as floID : {uri:<uri>,pubKey:<publicKey>}
|
||
Object.defineProperty(floCloudAPI, 'nodes', {
|
||
get: () => JSON.parse(JSON.stringify(supernodes))
|
||
});
|
||
|
||
var kBucket;
|
||
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);
|
||
let nodeIdBigInt = new BigInteger(decodedId, 16);
|
||
let nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned();
|
||
let nodeIdNewInt8Array = new Uint8Array(nodeIdBytes);
|
||
return nodeIdNewInt8Array;
|
||
};
|
||
|
||
const _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]);
|
||
|
||
const self = this;
|
||
Object.defineProperty(self, 'tree', {
|
||
get: () => _KB
|
||
});
|
||
Object.defineProperty(self, 'list', {
|
||
get: () => Array.from(_CO)
|
||
});
|
||
|
||
self.isNode = floID => _CO.includes(floID);
|
||
self.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++) {
|
||
if (i < _CO.length)
|
||
iNodes.push(_CO[i])
|
||
else i = -1
|
||
}
|
||
return iNodes
|
||
}
|
||
self.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++) {
|
||
if (i < _CO.length)
|
||
oNodes.push(_CO[i])
|
||
else i = -1
|
||
}
|
||
return oNodes
|
||
}
|
||
self.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--) {
|
||
if (j == _CO.indexOf(id))
|
||
break;
|
||
else if (j > -1)
|
||
pNodes[i++] = _CO[j]
|
||
else j = _CO.length
|
||
}
|
||
return (N == 1 ? pNodes[0] : pNodes)
|
||
}
|
||
self.nextNode = function (id, N = 1) {
|
||
let n = N || _CO.length;
|
||
if (!_CO.includes(id))
|
||
throw Error('Given node is not supernode');
|
||
if (!n) n = _CO.length;
|
||
let nNodes = []
|
||
for (let i = 0, j = _CO.indexOf(id) + 1; i < n; j++) {
|
||
if (j == _CO.indexOf(id))
|
||
break;
|
||
else if (j < _CO.length)
|
||
nNodes[i++] = _CO[j]
|
||
else j = -1
|
||
}
|
||
return (N == 1 ? nNodes[0] : nNodes)
|
||
}
|
||
self.closestNode = function (id, N = 1) {
|
||
let decodedId = decodeID(id);
|
||
let n = N || _CO.length;
|
||
let cNodes = _KB.closest(decodedId, n)
|
||
.map(k => k.floID)
|
||
return (N == 1 ? cNodes[0] : cNodes)
|
||
}
|
||
}
|
||
|
||
floCloudAPI.init = function startCloudProcess(nodes) {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
// accept only plain-ish objects
|
||
nodes = (nodes && typeof nodes === 'object' && !Array.isArray(nodes)) ? nodes : {};
|
||
|
||
supernodes = nodes;
|
||
|
||
// reset liveness bookkeeping for the new set
|
||
if (_inactive && typeof _inactive.clear === 'function') _inactive.clear();
|
||
|
||
// (re)build the bucket with current IDs
|
||
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_connect(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`)
|
||
}
|
||
})
|
||
}
|
||
|
||
function ws_activeConnect(snID, reverse = false) {
|
||
return new Promise((resolve, reject) => {
|
||
// Safe guard: uninitialized kBucket, empty list, or all inactive
|
||
if (!kBucket || !kBucket.list || !kBucket.list.length || _inactive.size === kBucket.list.length)
|
||
return reject('Cloud offline');
|
||
|
||
if (!(snID in supernodes)) {
|
||
var closest = kBucket.closestNode(proxyID(snID));
|
||
if (!closest) return reject('Cloud offline'); // no candidate to try
|
||
snID = closest;
|
||
}
|
||
|
||
ws_connect(snID)
|
||
.then(node => resolve(node))
|
||
.catch(error => {
|
||
var nxtNode = reverse ? kBucket.prevNode(snID) : kBucket.nextNode(snID);
|
||
if (!nxtNode || nxtNode === snID) return reject('Cloud offline'); // nothing else to try
|
||
ws_activeConnect(nxtNode, reverse).then(resolve).catch(reject);
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
function fetch_API(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;
|
||
if (typeof data === "string")
|
||
fetcher = fetch(sn_url + "?" + data);
|
||
else if (typeof data === "object" && data.method === "POST")
|
||
fetcher = fetch(sn_url, data);
|
||
fetcher.then(response => {
|
||
if (response.ok || response.status === 400 || response.status === 500)
|
||
resolve(response);
|
||
else
|
||
reject(response);
|
||
}).catch(error => reject(error))
|
||
})
|
||
}
|
||
|
||
function fetch_ActiveAPI(snID, data, reverse = false) {
|
||
return new Promise((resolve, reject) => {
|
||
// Safe guard: uninitialized kBucket, empty list, or all inactive
|
||
if (!kBucket || !kBucket.list || !kBucket.list.length || _inactive.size === kBucket.list.length)
|
||
return reject('Cloud offline');
|
||
|
||
if (!(snID in supernodes)) {
|
||
var closest = kBucket.closestNode(proxyID(snID));
|
||
if (!closest) return reject('Cloud offline'); // no candidate available
|
||
snID = closest;
|
||
}
|
||
|
||
fetch_API(snID, data)
|
||
.then(resolve)
|
||
.catch(error => {
|
||
_inactive.add(snID);
|
||
var nxtNode = reverse ? kBucket.prevNode(snID) : kBucket.nextNode(snID);
|
||
if (!nxtNode || nxtNode === snID) return reject('Cloud offline'); // nothing else to try
|
||
fetch_ActiveAPI(nxtNode, data, reverse).then(resolve).catch(reject);
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
function singleRequest(floID, data_obj, method = "POST") {
|
||
return new Promise((resolve, reject) => {
|
||
let data;
|
||
if (method === "POST")
|
||
data = {
|
||
method: "POST",
|
||
body: JSON.stringify(data_obj)
|
||
};
|
||
else
|
||
data = new URLSearchParams(JSON.parse(JSON.stringify(data_obj))).toString();
|
||
fetch_ActiveAPI(floID, data).then(response => {
|
||
if (response.ok)
|
||
response.json()
|
||
.then(result => resolve(result))
|
||
.catch(error => reject(error))
|
||
else response.text()
|
||
.then(result => reject(response.status + ": " + result)) //Error Message from Node
|
||
.catch(error => reject(error))
|
||
}).catch(error => reject(error))
|
||
})
|
||
}
|
||
|
||
const _liveRequest = {};
|
||
|
||
function liveRequest(floID, request, callback) {
|
||
const filterData = typeof request.status !== 'undefined' ?
|
||
data => {
|
||
if (request.status)
|
||
return data;
|
||
else {
|
||
let filtered = {};
|
||
for (let i in data)
|
||
if (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];
|
||
if ((!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 = {};
|
||
|
||
//Updating encoding/Decoding to modern standards
|
||
const encodeMessage = util.encodeMessage = function (message) {
|
||
const bytes = new TextEncoder().encode(JSON.stringify(message));
|
||
let bin = "";
|
||
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
||
return btoa(bin);
|
||
};
|
||
|
||
const decodeMessage = util.decodeMessage = function (message) {
|
||
try {
|
||
// try modern decode first
|
||
const bin = atob(message);
|
||
const bytes = new Uint8Array(bin.length);
|
||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||
return JSON.parse(new TextDecoder().decode(bytes));
|
||
} catch (e1) {
|
||
try {
|
||
// fallback to legacy decode
|
||
return JSON.parse(decodeURIComponent(escape(atob(message))));
|
||
} catch (e2) {
|
||
// final fallback: return raw string to avoid hard crash
|
||
return message;
|
||
}
|
||
}
|
||
};
|
||
|
||
const filterKey = util.filterKey = function (type, options = {}) {
|
||
return type + (options.comment ? ':' + options.comment : '') +
|
||
'|' + (options.group || options.receiverID || DEFAULT.adminID) +
|
||
'|' + (options.application || DEFAULT.application);
|
||
}
|
||
|
||
const proxyID = util.proxyID = function (address) {
|
||
if (!address)
|
||
return;
|
||
var bytes;
|
||
if (address.length == 33 || address.length == 34) { //legacy encoding
|
||
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: true
|
||
}), {
|
||
asBytes: true
|
||
});
|
||
hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3] ?
|
||
bytes = undefined : bytes.shift();
|
||
} else if (address.length == 42 || address.length == 62) { //bech encoding
|
||
if (typeof coinjs !== 'function')
|
||
throw "library missing (lib_btc.js)";
|
||
let decode = coinjs.bech32_decode(address);
|
||
if (decode) {
|
||
bytes = decode.data;
|
||
bytes.shift();
|
||
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
|
||
if (address.length == 62) //for long bech, aggregate once more to get 160 bit
|
||
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
|
||
}
|
||
} else if (address.length == 66) { //public key hex
|
||
bytes = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address), {
|
||
asBytes: true
|
||
}));
|
||
}
|
||
if (!bytes)
|
||
throw "Invalid address: " + address;
|
||
else {
|
||
bytes.unshift(DEFAULT.blockchainPrefix);
|
||
let hash = Crypto.SHA256(Crypto.SHA256(bytes, {
|
||
asBytes: true
|
||
}), {
|
||
asBytes: true
|
||
});
|
||
return bitjs.Base58.encode(bytes.concat(hash.slice(0, 4)));
|
||
}
|
||
}
|
||
|
||
const lastCommit = {};
|
||
Object.defineProperty(lastCommit, 'get', {
|
||
value: objName => JSON.parse(lastCommit[objName])
|
||
});
|
||
Object.defineProperty(lastCommit, 'set', {
|
||
value: objName => lastCommit[objName] = JSON.stringify(appObjects[objName])
|
||
});
|
||
|
||
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)
|
||
continue;
|
||
switch (dataSet[vc].comment) {
|
||
case "RESET":
|
||
if (dataSet[vc].message.reset)
|
||
appObjects[objectName] = dataSet[vc].message.reset;
|
||
break;
|
||
case "UPDATE":
|
||
if (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 {
|
||
if (!dataSet || typeof dataSet !== "object") return;
|
||
|
||
// Ensure containers exist
|
||
if (typeof generalData[fk] !== "object" || generalData[fk] === null)
|
||
generalData[fk] = {};
|
||
if (typeof lastVC[fk] !== "number")
|
||
lastVC[fk] = 0;
|
||
|
||
// Merge data and track latest log_time
|
||
let updated = false;
|
||
let newLast = lastVC[fk];
|
||
|
||
for (const vc in dataSet) {
|
||
const rec = dataSet[vc];
|
||
// Skip bad records
|
||
if (!rec || typeof rec !== "object") continue;
|
||
|
||
// Assign only if changed reference (cheap check)
|
||
if (generalData[fk][vc] !== rec) {
|
||
generalData[fk][vc] = rec;
|
||
updated = true;
|
||
}
|
||
if (typeof rec.log_time === "number" && rec.log_time > newLast) {
|
||
newLast = rec.log_time;
|
||
updated = true;
|
||
}
|
||
}
|
||
|
||
if (!updated) return; // nothing new, avoid IDB writes
|
||
|
||
lastVC[fk] = newLast;
|
||
|
||
// --- Debounce writes per fk to avoid IDB thrash ---
|
||
storeGeneral._pending = storeGeneral._pending || Object.create(null);
|
||
const pend = storeGeneral._pending;
|
||
|
||
clearTimeout(pend[fk]);
|
||
pend[fk] = setTimeout(() => {
|
||
try {
|
||
// Fire-and-forget; callers don’t wait on these
|
||
compactIDB.writeData("lastVC", lastVC[fk], fk);
|
||
compactIDB.writeData("generalData", generalData[fk], fk);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}, 50);
|
||
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
|
||
function objectifier(data) {
|
||
if (!Array.isArray(data))
|
||
data = [data];
|
||
return Object.fromEntries(data.map(d => {
|
||
d.message = decodeMessage(d.message);
|
||
return [d.vectorClock, d];
|
||
}));
|
||
}
|
||
|
||
//set status as online for user_id
|
||
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: true,
|
||
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))
|
||
})
|
||
}
|
||
|
||
//request status of floID(s) in trackList
|
||
floCloudAPI.requestStatus = function (trackList, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!Array.isArray(trackList))
|
||
trackList = [trackList];
|
||
let callback = options.callback instanceof Function ? options.callback : DEFAULT.callback;
|
||
let request = {
|
||
status: false,
|
||
application: options.application || DEFAULT.application,
|
||
trackList: trackList
|
||
}
|
||
liveRequest(options.refID || DEFAULT.adminID, request, callback)
|
||
.then(result => resolve(result))
|
||
.catch(error => reject(error))
|
||
})
|
||
}
|
||
|
||
//send any message to supernode cloud storage
|
||
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))
|
||
})
|
||
}
|
||
|
||
//request any data from supernode cloud
|
||
const _requestApplicationData = function (type, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
var request = {
|
||
receiverID: options.receiverID || DEFAULT.adminID,
|
||
senderID: options.senderID || undefined,
|
||
application: options.application || DEFAULT.application,
|
||
type: type,
|
||
comment: options.comment || undefined,
|
||
lowerVectorClock: options.lowerVectorClock || undefined,
|
||
upperVectorClock: options.upperVectorClock || undefined,
|
||
atVectorClock: options.atVectorClock || undefined,
|
||
afterTime: options.afterTime || undefined,
|
||
mostRecent: options.mostRecent || undefined,
|
||
}
|
||
|
||
if (options.callback instanceof Function) {
|
||
liveRequest(request.receiverID, request, options.callback)
|
||
.then(result => resolve(result))
|
||
.catch(error => reject(error))
|
||
} else {
|
||
if (options.method === "POST")
|
||
request = {
|
||
time: Date.now(),
|
||
request
|
||
};
|
||
singleRequest(request.receiverID, request, options.method || "GET")
|
||
.then(data => resolve(data)).catch(error => reject(error))
|
||
}
|
||
})
|
||
}
|
||
|
||
floCloudAPI.requestApplicationData = function (type, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
let single_request_mode = !(options.callback instanceof Function);
|
||
_requestApplicationData(type, options).then(data => {
|
||
if (single_request_mode)
|
||
resolve(objectifier(data))
|
||
else resolve(data);
|
||
}).catch(error => reject(error))
|
||
})
|
||
}
|
||
|
||
/*(NEEDS UPDATE)
|
||
//delete data from supernode cloud (received only)
|
||
floCloudAPI.deleteApplicationData = function(vectorClocks, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
var delreq = {
|
||
requestorID: user.id,
|
||
pubKey: user.public,
|
||
time: Date.now(),
|
||
delete: (Array.isArray(vectorClocks) ? vectorClocks : [vectorClocks]),
|
||
application: options.application || DEFAULT.application
|
||
}
|
||
let hashcontent = ["time", "application", "delete"]
|
||
.map(d => delreq[d]).join("|")
|
||
delreq.sign = user.sign(hashcontent)
|
||
singleRequest(delreq.requestorID, delreq).then(result => {
|
||
let success = [],
|
||
failed = [];
|
||
result.forEach(r => r.status === 'fulfilled' ?
|
||
success.push(r.value) : failed.push(r.reason));
|
||
resolve({
|
||
success,
|
||
failed
|
||
})
|
||
}).catch(error => reject(error))
|
||
})
|
||
}
|
||
*/
|
||
//edit comment of data in supernode cloud (sender only)
|
||
floCloudAPI.editApplicationData = function (vectorClock, comment_edit, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
//request the data from cloud for resigning
|
||
let req_options = Object.assign({}, options);
|
||
req_options.atVectorClock = vectorClock;
|
||
_requestApplicationData(undefined, 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("|");
|
||
let 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))
|
||
})
|
||
}
|
||
|
||
//tag data in supernode cloud (subAdmin access only)
|
||
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))
|
||
})
|
||
}
|
||
|
||
//note data in supernode cloud (receiver only or subAdmin allowed if receiver is adminID)
|
||
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))
|
||
})
|
||
}
|
||
|
||
//send general data
|
||
floCloudAPI.sendGeneralData = function (message, type, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
if (options.encrypt) {
|
||
let encryptionKey = options.encrypt === true ?
|
||
floGlobals.settings.encryptionKey : options.encrypt
|
||
message = floCrypto.encryptData(JSON.stringify(message), encryptionKey)
|
||
}
|
||
sendApplicationData(message, type, options)
|
||
.then(result => resolve(result))
|
||
.catch(error => reject(error))
|
||
})
|
||
}
|
||
|
||
//request general data
|
||
floCloudAPI.requestGeneralData = function (type, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
var fk = filterKey(type, options)
|
||
lastVC[fk] = parseInt(lastVC[fk]) || 0;
|
||
options.afterTime = options.afterTime || lastVC[fk];
|
||
if (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))
|
||
}
|
||
})
|
||
}
|
||
|
||
//request an object data from supernode cloud
|
||
floCloudAPI.requestObjectData = function (objectName, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
options.lowerVectorClock = options.lowerVectorClock || lastVC[objectName] + 1;
|
||
options.senderID = [false, null].includes(options.senderID) ? null :
|
||
options.senderID || floGlobals.subAdmins;
|
||
options.mostRecent = true;
|
||
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 => {
|
||
updateObject(objectName, objectifier(dataSet));
|
||
delete options.comment;
|
||
options.lowerVectorClock = lastVC[objectName] + 1;
|
||
delete options.mostRecent;
|
||
if (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()
|
||
})
|
||
}
|
||
|
||
//reset or initialize an object and send it to cloud
|
||
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))
|
||
})
|
||
}
|
||
|
||
//update the diff and send it to cloud
|
||
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))
|
||
})
|
||
}
|
||
|
||
//upload file
|
||
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 };
|
||
file_data.content = Crypto.util.bytesToBase64(new Uint8Array(arraybuf));
|
||
if (options.encrypt) {
|
||
let encryptionKey = options.encrypt === true ?
|
||
floGlobals.settings.encryptionKey : options.encrypt
|
||
file_data = floCrypto.encryptData(JSON.stringify(file_data), encryptionKey)
|
||
}
|
||
sendApplicationData(file_data, type, options)
|
||
.then(({ vectorClock, receiverID, type, application }) => resolve({ vectorClock, receiverID, type, application }))
|
||
.catch(error => reject(error))
|
||
}).catch(error => reject(error))
|
||
})
|
||
}
|
||
|
||
//download file
|
||
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);
|
||
//file is encrypted: decryption required
|
||
if (file_data instanceof Object && "secret" in file_data) {
|
||
if (!options.decrypt)
|
||
return reject("Data is encrypted");
|
||
let decryptionKey = (options.decrypt === true) ? Crypto.AES.decrypt(user_private, aes_key) : options.decrypt;
|
||
if (!Array.isArray(decryptionKey))
|
||
decryptionKey = [decryptionKey];
|
||
let flag = false;
|
||
for (let key of decryptionKey) {
|
||
try {
|
||
let tmp = floCrypto.decryptData(file_data, key);
|
||
file_data = JSON.parse(tmp);
|
||
flag = true;
|
||
break;
|
||
} catch (error) { }
|
||
}
|
||
if (!flag)
|
||
return reject("Unable to decrypt file: Invalid private key");
|
||
}
|
||
//reconstruct the file
|
||
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))
|
||
})
|
||
}
|
||
|
||
/*
|
||
Functions:
|
||
findDiff(original, updatedObj) returns an object with the added, deleted and updated differences
|
||
mergeDiff(original, allDiff) returns a new object from original object merged with all differences (allDiff is returned object of findDiff)
|
||
*/
|
||
var diff = (function () {
|
||
const isDate = d => d instanceof Date;
|
||
const isEmpty = o => Object.keys(o).length === 0;
|
||
const isObject = o => o != null && typeof o === 'object';
|
||
const properObject = o => isObject(o) && !o.hasOwnProperty ? {
|
||
...o
|
||
} : o;
|
||
const getLargerArray = (l, r) => l.length > r.length ? l : r;
|
||
|
||
const preserve = (diff, left, right) => {
|
||
if (!isObject(diff)) return diff;
|
||
return Object.keys(diff).reduce((acc, key) => {
|
||
const leftArray = left[key];
|
||
const rightArray = right[key];
|
||
if (Array.isArray(leftArray) && Array.isArray(rightArray)) {
|
||
const array = [...getLargerArray(leftArray, rightArray)];
|
||
return {
|
||
...acc,
|
||
[key]: array.reduce((acc2, item, index) => {
|
||
if (diff[key].hasOwnProperty(index)) {
|
||
acc2[index] = preserve(diff[key][index], leftArray[index], rightArray[index]); // diff recurse and check for nested arrays
|
||
return acc2;
|
||
}
|
||
delete acc2[index]; // no diff aka empty
|
||
return acc2;
|
||
}, array)
|
||
};
|
||
}
|
||
return {
|
||
...acc,
|
||
[key]: diff[key]
|
||
};
|
||
}, {});
|
||
};
|
||
|
||
const updatedDiff = (lhs, rhs) => {
|
||
if (lhs === rhs) return {};
|
||
if (!isObject(lhs) || !isObject(rhs)) return rhs;
|
||
const l = properObject(lhs);
|
||
const r = properObject(rhs);
|
||
if (isDate(l) || isDate(r)) {
|
||
if (l.valueOf() == r.valueOf()) return {};
|
||
return r;
|
||
}
|
||
return Object.keys(r).reduce((acc, key) => {
|
||
if (l.hasOwnProperty(key)) {
|
||
const difference = updatedDiff(l[key], r[key]);
|
||
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc;
|
||
return {
|
||
...acc,
|
||
[key]: difference
|
||
};
|
||
}
|
||
return acc;
|
||
}, {});
|
||
};
|
||
|
||
|
||
const diff = (lhs, rhs) => {
|
||
if (lhs === rhs) return {}; // equal return no diff
|
||
if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs
|
||
const l = properObject(lhs);
|
||
const r = properObject(rhs);
|
||
const deletedValues = Object.keys(l).reduce((acc, key) => {
|
||
return r.hasOwnProperty(key) ? acc : {
|
||
...acc,
|
||
[key]: null
|
||
};
|
||
}, {});
|
||
if (isDate(l) || isDate(r)) {
|
||
if (l.valueOf() == r.valueOf()) return {};
|
||
return r;
|
||
}
|
||
return Object.keys(r).reduce((acc, key) => {
|
||
if (!l.hasOwnProperty(key)) return {
|
||
...acc,
|
||
[key]: r[key]
|
||
}; // return added r key
|
||
const difference = diff(l[key], r[key]);
|
||
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff
|
||
return {
|
||
...acc,
|
||
[key]: difference
|
||
}; // return updated key
|
||
}, deletedValues);
|
||
};
|
||
|
||
const addedDiff = (lhs, rhs) => {
|
||
if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {};
|
||
const l = properObject(lhs);
|
||
const r = properObject(rhs);
|
||
return Object.keys(r).reduce((acc, key) => {
|
||
if (l.hasOwnProperty(key)) {
|
||
const difference = addedDiff(l[key], r[key]);
|
||
if (isObject(difference) && isEmpty(difference)) return acc;
|
||
return {
|
||
...acc,
|
||
[key]: difference
|
||
};
|
||
}
|
||
return {
|
||
...acc,
|
||
[key]: r[key]
|
||
};
|
||
}, {});
|
||
};
|
||
|
||
const arrayDiff = (lhs, rhs) => {
|
||
if (lhs === rhs) return {}; // equal return no diff
|
||
if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs
|
||
const l = properObject(lhs);
|
||
const r = properObject(rhs);
|
||
const deletedValues = Object.keys(l).reduce((acc, key) => {
|
||
return r.hasOwnProperty(key) ? acc : {
|
||
...acc,
|
||
[key]: null
|
||
};
|
||
}, {});
|
||
if (isDate(l) || isDate(r)) {
|
||
if (l.valueOf() == r.valueOf()) return {};
|
||
return r;
|
||
}
|
||
if (Array.isArray(r) && Array.isArray(l)) {
|
||
const deletedValues = l.reduce((acc, item, index) => {
|
||
return r.hasOwnProperty(index) ? acc.concat(item) : acc.concat(null);
|
||
}, []);
|
||
return r.reduce((acc, rightItem, index) => {
|
||
if (!deletedValues.hasOwnProperty(index)) {
|
||
return acc.concat(rightItem);
|
||
}
|
||
const leftItem = l[index];
|
||
const difference = diff(rightItem, leftItem);
|
||
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) {
|
||
delete acc[index];
|
||
return acc; // return no diff
|
||
}
|
||
return acc.slice(0, index).concat(rightItem).concat(acc.slice(index + 1)); // return updated key
|
||
}, deletedValues);
|
||
}
|
||
|
||
return Object.keys(r).reduce((acc, key) => {
|
||
if (!l.hasOwnProperty(key)) return {
|
||
...acc,
|
||
[key]: r[key]
|
||
}; // return added r key
|
||
const difference = diff(l[key], r[key]);
|
||
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff
|
||
return {
|
||
...acc,
|
||
[key]: difference
|
||
}; // return updated key
|
||
}, deletedValues);
|
||
};
|
||
|
||
const deletedDiff = (lhs, rhs) => {
|
||
if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {};
|
||
const l = properObject(lhs);
|
||
const r = properObject(rhs);
|
||
return Object.keys(l).reduce((acc, key) => {
|
||
if (r.hasOwnProperty(key)) {
|
||
const difference = deletedDiff(l[key], r[key]);
|
||
if (isObject(difference) && isEmpty(difference)) return acc;
|
||
return {
|
||
...acc,
|
||
[key]: difference
|
||
};
|
||
}
|
||
return {
|
||
...acc,
|
||
[key]: null
|
||
};
|
||
}, {});
|
||
};
|
||
|
||
const mergeRecursive = (obj1, obj2, deleteMode = false) => {
|
||
for (var p in obj2) {
|
||
try {
|
||
if (obj2[p].constructor == Object)
|
||
obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode);
|
||
// Property in destination object set; update its value.
|
||
else if (Array.isArray(obj2[p])) {
|
||
// obj1[p] = [];
|
||
if (obj2[p].length < 1)
|
||
obj1[p] = obj2[p];
|
||
else
|
||
obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode);
|
||
} else
|
||
obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p];
|
||
} catch (e) {
|
||
// Property in destination object not set; create it and set its value.
|
||
obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p];
|
||
}
|
||
}
|
||
return obj1;
|
||
}
|
||
|
||
const cleanse = (obj) => {
|
||
Object.keys(obj).forEach(key => {
|
||
var value = obj[key];
|
||
if (typeof value === "object" && value !== null)
|
||
obj[key] = cleanse(value);
|
||
else if (typeof value === 'undefined')
|
||
delete obj[key]; // undefined, remove it
|
||
});
|
||
if (Array.isArray(obj))
|
||
obj = obj.filter(v => typeof v !== 'undefined');
|
||
return obj;
|
||
}
|
||
|
||
|
||
const findDiff = (lhs, rhs) => ({
|
||
added: addedDiff(lhs, rhs),
|
||
deleted: deletedDiff(lhs, rhs),
|
||
updated: updatedDiff(lhs, rhs),
|
||
});
|
||
|
||
/*obj is original object or array, diff is the output of findDiff */
|
||
const mergeDiff = (obj, diff) => {
|
||
if (Object.keys(diff.updated).length !== 0)
|
||
obj = mergeRecursive(obj, diff.updated)
|
||
if (Object.keys(diff.deleted).length !== 0) {
|
||
obj = mergeRecursive(obj, diff.deleted, true)
|
||
obj = cleanse(obj)
|
||
}
|
||
if (Object.keys(diff.added).length !== 0)
|
||
obj = mergeRecursive(obj, diff.added)
|
||
return obj
|
||
}
|
||
|
||
return {
|
||
find: findDiff,
|
||
merge: mergeDiff
|
||
}
|
||
})();
|
||
|
||
|
||
})('object' === typeof module ? module.exports : window.floCloudAPI = {});
|