ndn-js
Version:
A JavaScript client library for Named Data Networking
510 lines (450 loc) • 17 kB
JavaScript
/*
* Copyright (C) 2014-2017 Regents of the University of California.
* @author: Zhehao Wang
* @author: Jeff Thompson <jefft0@remap.ucla.edu>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* A copy of the GNU Lesser General Public License is in the file COPYING.
*/
var ChronoChat = function(screenName, chatRoom, rootPrefix, chat, face, syncDoc, keyChain, certificateName)
{
this.screen_name = screenName;
this.chatroom = chatRoom;
this.maxmsgcachelength = 10000;
this.isRecoverySyncState = true;
this.sync_lifetime = 5000.0;
this.face = face;
this.keyChain = keyChain;
this.certificateName = certificateName;
this.fetchedHistorical = false;
this.chat_prefix = (new Name(rootPrefix)).append(this.chatroom)
.append(screenName);
/*this.roster = [screenName];
document.getElementById('menu').innerHTML = '<p><b>Member</b></p><ul><li>' + screenName + '</li></ul>';*/
this.msgcache = [];
this.roster = [];
//console.log("The local chat prefix " + this.chat_prefix.toUri() + " ***");
/* TODO: Do we need session numbers?
var session = (new Date()).getTime();
session = parseInt(session/1000);
*/
var session = 0;
this.usrname = this.screen_name + session;
this.ChatMessage = SyncDemo.ChatMessage;
if (this.screen_name == "" || this.chatroom == "") {
console.log("input usrname and chatroom");
}
else {
this.sync = new FireflySync(face.db_,
syncDoc,
this.chat_prefix.toUri(),
this.initial.bind(this),
this.sendInterest.bind(this));
face.registerPrefix
(this.chat_prefix, this.onInterest.bind(this),
this.onRegisterFailed.bind(this));
}
};
/**
* Send the data packet which contains the user's message
* @param {Name} Interest name prefix
* @param {Interest} The interest
* @param {Face} The face
* @param {number} interestFilterId
* @param {InterestFilter} filter
*/
ChronoChat.prototype.onInterest = function
(prefix, interest, face, interestFilterId, filter)
{
var content = {};
// chat_prefix should really be saved as a name, not a URI string.
var chatPrefixSize = new Name(this.chat_prefix).size();
var seq = parseInt(interest.getName().get(chatPrefixSize + 1).toEscapedString());
for (var i = this.msgcache.length - 1 ; i >= 0; i--) {
if (this.msgcache[i].seqno == seq) {
if(this.msgcache[i].msgtype != 'CHAT')
content = new this.ChatMessage({from:this.screen_name, to:this.chatroom, type:this.msgcache[i].msgtype, timestamp:parseInt(this.msgcache[i].time/1000)});
else
content = new this.ChatMessage({from:this.screen_name, to:this.chatroom, type:this.msgcache[i].msgtype, data:this.msgcache[i].msg, timestamp:parseInt(this.msgcache[i].time/1000)});
break;
}
}
if (content.from != null) {
var str = new Uint8Array(content.toArrayBuffer());
var co = new Data(interest.getName());
co.setContent(str);
this.keyChain.sign(co, this.certificateName, function() {
try {
face.putData(co);
}
catch (e) {
console.log(e.toString());
}
});
}
};
ChronoChat.prototype.onRegisterFailed = function(prefix)
{
};
ChronoChat.prototype.initial = function()
{
// fetch historical data
var self = this;
this.sync.getHistoricalDeltas(function(deltas){
if (LOG > 0) console.log('got historical deltas')
if (deltas.length)
deltas.forEach(function(delta, idx, arr){
for (var key in delta)
{
var decodedKey = decodeURIComponent(key);
var uri = decodedKey+'/0/'+delta[key]; // beware! session number is hardcoded as zero here!!
var interest = new Interest(new Name(uri));
interest.setInterestLifetimeMilliseconds(self.sync_lifetime);
if (LOG > 0) console.log('expressing interest for historical data ', interest.getName().toUri());
self.face.expressInterest(interest, self.onData.bind(self), self.chatTimeout.bind(self));
}
if (idx == arr.length-1)
{
self.fetchedHistorical = true;
if (LOG > 0) console.log('requested all historical ',self.fetchedHistorical);
}
});
else
self.fetchedHistorical = true;
});
/*var timeout = new Interest(new Name("/timeout"));
timeout.setInterestLifetimeMilliseconds(60000);
//TODO: figure out how to do the heartbeat
this.face.expressInterest(timeout, this.dummyOnData, this.heartbeat.bind(this));*/
if (this.roster.indexOf(this.usrname) == -1) {
this.roster.push(this.usrname);
document.getElementById('menu').innerHTML = '<p><b>Member</b></p>';
document.getElementById('menu').innerHTML += '<ul><li>' + this.screen_name +
'</li></ul>';
var d = new Date();
document.getElementById('txt').innerHTML += '<div><b><grey>' +
this.screen_name + '-' + d.toLocaleTimeString() +
': Join</grey></b><br /></div>'
var objDiv = document.getElementById("txt");
objDiv.scrollTop = objDiv.scrollHeight;
this.sync.publishNextSequenceNo();
this.messageCacheAppend('JOIN', 'xxx');
}
};
/**
* This onData is passed as onData for timeout interest in initial, which means it
* should not be called under any circumstances.
*/
ChronoChat.prototype.dummyOnData = function(interest, co)
{
console.log("*** dummyOndata called, name: " + interest.getName().toUri() + " ***");
};
/**
* Send a Chat interest to fetch chat messages after the user gets the Sync data packet
* @param {SyncStates[]} The array of sync states
* @param {bool} if it's in recovery state
*/
ChronoChat.prototype.sendInterest = function(syncStates, isRecovery)
{
this.isRecoverySyncState = isRecovery;
// this one is to prevent chronochat from retrieving latest data which
// should be fetched as part of initial historical retrieval
if (!this.fetchedHistorical)
{
if (LOG > 0) console.log('hasnt fetched historical yet ', this.fetchedHistorical);
return;
}
var sendList = []; // of String
var sessionNoList = []; // of number
var sequenceNoList = []; // of number
for (var j = 0; j < syncStates.length; j++) {
var syncState = syncStates[j];
var nameComponents = new Name(syncState.getDataPrefix());
var tempName = nameComponents.get(-1).toEscapedString();
var sessionNo = syncState.getSessionNo();
if (tempName != this.screen_name) {
var index = -1;
for (var k = 0; k < sendList.length; ++k) {
if (sendList[k] == syncState.getDataPrefix()) {
index = k;
break;
}
}
if (index != -1) {
sessionNoList[index] = sessionNo;
sequenceNoList[index] = syncState.getSequenceNo();
}
else {
sendList.push(syncState.getDataPrefix());
sessionNoList.push(sessionNo);
sequenceNoList.push(syncState.getSequenceNo());
}
}
}
for (var i = 0; i < sendList.length; ++i) {
var uri = sendList[i] + "/" + sessionNoList[i] + "/" + sequenceNoList[i];
var interest = new Interest(new Name(uri));
interest.setInterestLifetimeMilliseconds(this.sync_lifetime);
if (LOG > 0) console.log('expressing interest for data ', interest.getName().toUri());
this.face.expressInterest(interest, this.onData.bind(this), this.chatTimeout.bind(this));
}
};
/**
* Process the incoming data
* @param {Interest} interest
* @param {Data} co
*/
ChronoChat.prototype.onData = function(interest, co)
{
var arr = new Uint8Array(co.getContent().size());
arr.set(co.getContent().buf());
var content = this.ChatMessage.decode(arr.buffer);
var temp = (new Date()).getTime();
// if (temp - content.timestamp * 1000 < 120000)
{
var t = (new Date(content.timestamp * 1000)).toLocaleTimeString();
var name = content.from;
// chat_prefix should be saved as a name, not a URI string.
var prefix = co.getName().getPrefix(-2).toUri();
var session = parseInt((co.getName().get(-2)).toEscapedString());
var seqno = parseInt((co.getName().get(-1)).toEscapedString());
var l = 0;
//update roster
while (l < this.roster.length) {
var name_t = this.roster[l].substring(0,this.roster[l].length-1);
var session_t = this.roster[l].substring(this.roster[l].length-1,this.roster[l].length);
if (name != name_t && content.type != 2)
l++;
else{
if(name == name_t && session > session_t){
this.roster[l] = name + session;
}
break;
}
}
if(l == this.roster.length) {
this.roster.push(name + session);
document.getElementById('txt').innerHTML += '<div><b><grey>' + name + '-' +
t + ': Join' + '</grey></b><br /></div>';
var objDiv = document.getElementById("txt");
objDiv.scrollTop = objDiv.scrollHeight;
document.getElementById('menu').innerHTML = '<p><b>Member</b></p><ul>';
for (var i = 0; i < this.roster.length ; i++) {
var name_t = this.roster[i].substring(0,this.roster[i].length - 1);
document.getElementById('menu').innerHTML += '<li>' + name_t + '</li>';
}
document.getElementById('menu').innerHTML += '</ul>';
}
/* TODO: Restore heartbeat functionality.
var timeout = new Interest(new Name("/timeout"));
timeout.setInterestLifetimeMilliseconds(120000);
this.face.expressInterest(timeout, this.dummyOnData, this.alive.bind(this, timeout, seqno, name, session, prefix));
*/
//if (content.type == 0 && this.isRecoverySyncState == false && content.from != this.screen_name){
// Note: the original logic does not display old data;
// But what if an ordinary application data interest gets answered after entering recovery state?
if (content.type == 0) // && content.from != this.screen_name)
{
// Display on the screen will not display old data.
// Encode special html characters to avoid script injection.
var escaped_msg = $('<div/>').text(content.data).html();
document.getElementById('txt').innerHTML +='<p><grey>' + content.from + '-' +
t + ':</grey><br />' + escaped_msg + '</p>';
var objDiv = document.getElementById("txt");
objDiv.scrollTop = objDiv.scrollHeight;
}
else if (content.type == 2) {
//leave message
var n = this.roster.indexOf(name + session);
if(n != -1 && name != this.screen_name) {
this.roster.splice(n,1);
document.getElementById('menu').innerHTML = '<p><b>Member</b></p><ul>';
for(var i = 0; i<this.roster.length; i++) {
var name_t = this.roster[i].substring(0,this.roster[i].length - 1);
document.getElementById('menu').innerHTML += '<li>' + name_t + '</li>';
}
document.getElementById('menu').innerHTML += '</ul>';
var d = new Date(content.timestamp * 1000);
var t = d.toLocaleTimeString();
document.getElementById('txt').innerHTML += '<div><b><grey>' + name +
'-' + t + ': Leave</grey></b><br /></div>';
var objDiv = document.getElementById("txt");
objDiv.scrollTop = objDiv.scrollHeight;
}
}
}
};
/**
* No chat data coming back.
* @param {Interest}
*/
ChronoChat.prototype.chatTimeout = function(interest)
{
console.log("Timeout waiting for chat data");
};
/**
*
* @param {Interest}
*/
/* TODO: Restore heartbeat functionality.
ChronoChat.prototype.heartbeat = function(interest)
{
// Based on ndn-cpp library approach
if (this.msgcache.length == 0) {
// Is it possible that this gets executed?
this.messageCacheAppend("JOIN", "xxx");
}
this.sync.publishNextSequenceNo();
this.messageCacheAppend("HELLO", "xxx");
// Making a timeout interest for heartbeat...
var timeout = new Interest(new Name("/timeout"));
timeout.setInterestLifetimeMilliseconds(60000);
//console.log("*** Chat heartbeat expressed interest with name: " + timeout.getName().toUri() + " ***");
this.face.expressInterest(timeout, this.dummyOnData, this.heartbeat.bind(this));
};
*/
/**
* This is called after a timeout to check if the user with prefix has a newer sequence
* number than the given temp_seq. If not, assume the user is idle and remove from the
* roster and print a leave message.
* This method has an interest argument because we use it as the onTimeout for
* Face.expressInterest.
* @param {Interest}
* @param {int}
* @param {string}
* @param {int}
* @param {string}
*/
/* TODO: Restore heartbeat functionality.
ChronoChat.prototype.alive = function(interest, temp_seq, name, session, prefix)
{
//console.log("check alive");
var index_n = this.sync.digest_tree.find(prefix, session);
var n = this.roster.indexOf(name + session);
if (index_n != -1 && n != -1) {
var seq = this.sync.digest_tree.digestnode[index_n].seqno_seq;
if (temp_seq == seq) {
this.roster.splice(n,1);
console.log(name+" leave");
var d = new Date();
var t = d.toLocaleTimeString();
document.getElementById('txt').innerHTML += '<div><b><grey>' + name + '-' +
t + ': Leave</grey></b><br /></div>';
var objDiv = document.getElementById("txt");
objDiv.scrollTop = objDiv.scrollHeight;
document.getElementById('menu').innerHTML = '<p><b>Member</b></p><ul>';
for (var i = 0; i < this.roster.length; i++) {
var name_t = this.roster[i].substring(0, this.roster[i].length - 10);
document.getElementById('menu').innerHTML += '<li>' + name_t + '</li>';
}
document.getElementById('menu').innerHTML += '</ul>';
}
}
};
*/
ChronoChat.prototype.sendMessage = function()
{
if (this.msgcache.length == 0)
this.messageCacheAppend("JOIN", "xxx");
var chatmsg = document.getElementById('fname').value.trim();
if (chatmsg != "") {
document.getElementById('fname').value = "";
this.sync.publishNextSequenceNo();
this.messageCacheAppend("CHAT", chatmsg);
var d = new Date();
var tt = d.toLocaleTimeString();
// Encode special html characters to avoid script injection.
var escaped_msg = $('<div/>').text(chatmsg).html();
document.getElementById('txt').innerHTML += '<p><grey>' + this.screen_name +
'-' + tt + ':</grey><br />' + escaped_msg + '</p>';
var objDiv = document.getElementById("txt");
objDiv.scrollTop = objDiv.scrollHeight;
}
else
alert("Message cannot be empty");
}
/**
* Send the leave message and leave.
*/
ChronoChat.prototype.leave = function()
{
alert("Leaving the Chatroom...");
$("#chat").hide();
document.getElementById('room').innerHTML = 'Please close the window. Thank you';
this.sync.publishNextSequenceNo();
this.messageCacheAppend("LEAVE", "xxx");
};
/**
* Append a new CachedMessage to msgcache, using given messageType and message,
* the sequence number from this.sync.getSequenceNo() and the current time.
* Also remove elements from the front of the cache as needed to keep the size to
* this.maxmsgcachelength.
*/
ChronoChat.prototype.messageCacheAppend = function(messageType, message)
{
var d = new Date();
var t = d.getTime();
var usrseq = this.sync.getSequenceNo();
this.msgcache.push(new ChronoChat.CachedMessage(usrseq, messageType, message, t));
while (this.msgcache.length > this.maxmsgcachelength) {
this.msgcache.shift();
}
};
ChronoChat.prototype.getRandomString = function()
{
var seed = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789';
var result = '';
for (var i = 0; i < 10; i++) {
var pos = Math.floor(Math.random() * seed.length);
result += seed[pos];
}
return result;
};
// Embedded class CachedMessage; defining class with its constructor
ChronoChat.CachedMessage = function (seqno, msgtype, msg, time)
{
this.seqno = seqno;
this.msgtype = msgtype;
this.msg = msg;
this.time = time;
};
ChronoChat.CachedMessage.prototype.getSequenceNo = function()
{
return this.seqno;
};
ChronoChat.CachedMessage.prototype.getMessageType = function()
{
return this.msgtype;
};
ChronoChat.CachedMessage.prototype.getMessage = function()
{
return this.msg;
};
/**
* @return MillisecondsSince1970
*/
ChronoChat.CachedMessage.prototype.getTime = function()
{
return this.time;
};
function getRandomNameString()
{
var seed = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM';
var result = '';
for (var i = 0; i < 3; i++) {
var pos = Math.floor(Math.random() * seed.length);
result += seed[pos];
}
return result;
};