2014-03-25 13:43:11 -07:00
|
|
|
/*
|
|
|
|
|
Copyright 2014, Ian Gulliver
|
|
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
|
limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
var cosmopolite = {};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client = function(opt_callbacks, opt_urlPrefix, opt_namespace) {
|
|
|
|
|
this.callbacks_ = opt_callbacks || {};
|
|
|
|
|
this.urlPrefix_ = opt_urlPrefix || '/cosmopolite';
|
|
|
|
|
this.namespace_ = opt_namespace || 'cosmopolite';
|
|
|
|
|
|
|
|
|
|
this.stateCache_ = {};
|
2014-05-10 15:47:33 +02:00
|
|
|
this.subscriptions_ = {};
|
2014-03-25 13:43:11 -07:00
|
|
|
|
2014-04-30 11:17:25 -07:00
|
|
|
var scriptUrls = [
|
2014-03-25 13:43:11 -07:00
|
|
|
'https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js',
|
|
|
|
|
'/_ah/channel/jsapi',
|
|
|
|
|
];
|
2014-04-30 11:17:25 -07:00
|
|
|
this.numScriptsToLoad_ = scriptUrls.length;
|
|
|
|
|
scriptUrls.forEach(function(scriptUrl) {
|
2014-03-25 13:43:11 -07:00
|
|
|
var script = document.createElement('script');
|
2014-04-30 11:17:25 -07:00
|
|
|
script.src = scriptUrl;
|
2014-03-25 13:43:11 -07:00
|
|
|
script.onload = this.onLoad_.bind(this);
|
|
|
|
|
document.body.appendChild(script);
|
2014-04-30 11:17:25 -07:00
|
|
|
}, this);
|
2014-03-25 13:43:11 -07:00
|
|
|
};
|
|
|
|
|
|
2014-05-10 15:47:33 +02:00
|
|
|
cosmopolite.Client.prototype.setValue = function(key, value, is_public) {
|
|
|
|
|
this.sendRPC_('setValue', {
|
|
|
|
|
'key': key,
|
|
|
|
|
'value': value,
|
|
|
|
|
'public': is_public,
|
|
|
|
|
});
|
|
|
|
|
// Provide immediate feedback without waiting for a round trip.
|
|
|
|
|
// We'll also get a response from the server, so this should be eventually
|
|
|
|
|
// consistent.
|
|
|
|
|
if ('onStateChange' in this.callbacks_) {
|
|
|
|
|
this.callbacks_['onStateChange'](key, value);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.getValue = function(key) {
|
|
|
|
|
return this.stateCache_[key];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.subscribe = function(subject, messages) {
|
|
|
|
|
if (subject in this.subscriptions_) {
|
|
|
|
|
console.log('Not sending duplication subscription request for subject:', subject);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.subscriptions_[subject] = {
|
|
|
|
|
'messages': [],
|
|
|
|
|
};
|
|
|
|
|
this.sendRPC_('subscribe', {
|
|
|
|
|
'subject': subject,
|
|
|
|
|
'messages': messages,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.unsubscribe = function(subject) {
|
|
|
|
|
delete this.subscriptions_[subject];
|
|
|
|
|
this.sendRPC_('unsubscribe', {
|
|
|
|
|
'subject': subject,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.sendMessage = function(subject, message) {
|
|
|
|
|
this.sendRPC_('sendMessage', {
|
|
|
|
|
'subject': subject,
|
|
|
|
|
'message': message,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.getMessages = function(subject) {
|
|
|
|
|
return this.subscriptions_[subject].messages;
|
|
|
|
|
};
|
|
|
|
|
|
2014-03-25 13:43:11 -07:00
|
|
|
cosmopolite.Client.prototype.onLoad_ = function() {
|
|
|
|
|
if (--this.numScriptsToLoad_ > 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.$ = jQuery.noConflict(true);
|
|
|
|
|
this.registerMessageHandlers_();
|
|
|
|
|
this.createChannel_();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Message from another browser window
|
|
|
|
|
cosmopolite.Client.prototype.onReceiveMessage_ = function(data) {
|
|
|
|
|
switch (data) {
|
|
|
|
|
case 'login_complete':
|
2014-05-01 14:55:50 -07:00
|
|
|
this.socket.close();
|
2014-03-25 13:43:11 -07:00
|
|
|
break;
|
|
|
|
|
case 'logout_complete':
|
|
|
|
|
localStorage.removeItem(this.namespace_ + ':client_id');
|
|
|
|
|
localStorage.removeItem(this.namespace_ + ':google_user_id');
|
|
|
|
|
this.$('#google_user').empty();
|
2014-05-01 14:55:50 -07:00
|
|
|
this.socket.close();
|
2014-03-25 13:43:11 -07:00
|
|
|
break;
|
|
|
|
|
default:
|
2014-05-06 22:46:07 -07:00
|
|
|
console.log('Unknown event type:', data);
|
2014-03-25 13:43:11 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.registerMessageHandlers_ = function() {
|
|
|
|
|
this.$(window).on('message', this.$.proxy(function(e) {
|
|
|
|
|
if (e.originalEvent.origin != window.location.origin) {
|
|
|
|
|
console.log(
|
2014-05-06 22:46:07 -07:00
|
|
|
'Received message from bad origin:', e.originalEvent.origin);
|
2014-03-25 13:43:11 -07:00
|
|
|
return;
|
|
|
|
|
}
|
2014-05-09 15:00:48 -07:00
|
|
|
console.log('Received browser message:', e.originalEvent.data);
|
2014-03-25 13:43:11 -07:00
|
|
|
this.onReceiveMessage_(e.originalEvent.data);
|
|
|
|
|
}, this));
|
|
|
|
|
};
|
|
|
|
|
|
2014-05-06 13:38:40 -07:00
|
|
|
cosmopolite.Client.prototype.sendRPC_ = function(command, args, onSuccess) {
|
|
|
|
|
this.sendRPCs_([
|
|
|
|
|
{
|
|
|
|
|
'command': command,
|
|
|
|
|
'arguments': args,
|
|
|
|
|
'onSuccess': onSuccess,
|
|
|
|
|
}
|
|
|
|
|
]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.sendRPCs_ = function(commands, delay) {
|
|
|
|
|
var request = {
|
|
|
|
|
'commands': [],
|
|
|
|
|
};
|
|
|
|
|
commands.forEach(function(command) {
|
|
|
|
|
var request_command = {
|
|
|
|
|
'command': command['command'],
|
|
|
|
|
};
|
|
|
|
|
if ('arguments' in command) {
|
|
|
|
|
request_command['arguments'] = command['arguments'];
|
|
|
|
|
}
|
|
|
|
|
request.commands.push(request_command);
|
|
|
|
|
});
|
2014-03-25 13:43:11 -07:00
|
|
|
if (this.namespace_ + ':client_id' in localStorage) {
|
2014-05-06 13:38:40 -07:00
|
|
|
request['client_id'] = localStorage[this.namespace_ + ':client_id'];
|
2014-03-25 13:43:11 -07:00
|
|
|
}
|
|
|
|
|
if (this.namespace_ + ':google_user_id' in localStorage) {
|
2014-05-06 13:38:40 -07:00
|
|
|
request['google_user_id'] = localStorage[this.namespace_ + ':google_user_id'];
|
2014-03-25 13:43:11 -07:00
|
|
|
}
|
|
|
|
|
this.$.ajax({
|
2014-05-06 13:38:40 -07:00
|
|
|
url: this.urlPrefix_ + '/api',
|
2014-03-25 13:43:11 -07:00
|
|
|
type: 'post',
|
2014-05-06 13:38:40 -07:00
|
|
|
data: JSON.stringify(request),
|
2014-03-25 13:43:11 -07:00
|
|
|
dataType: 'json',
|
|
|
|
|
context: this,
|
|
|
|
|
})
|
|
|
|
|
.done(function(data, stat, xhr) {
|
|
|
|
|
if ('google_user_id' in data) {
|
|
|
|
|
localStorage[this.namespace_ + ':google_user_id'] =
|
|
|
|
|
data['google_user_id'];
|
|
|
|
|
}
|
|
|
|
|
if ('client_id' in data) {
|
|
|
|
|
localStorage[this.namespace_ + ':client_id'] = data['client_id'];
|
|
|
|
|
}
|
|
|
|
|
if (data['status'] == 'retry') {
|
|
|
|
|
// Discard delay
|
2014-05-06 13:38:40 -07:00
|
|
|
this.sendRPCs_(commands, onSuccess);
|
2014-03-25 13:43:11 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (data['status'] != 'ok') {
|
2014-05-06 22:46:07 -07:00
|
|
|
console.log('Server returned unknown status:', data['status']);
|
2014-03-25 13:43:11 -07:00
|
|
|
// TODO(flamingcow): Refresh the page? Show an alert?
|
|
|
|
|
return;
|
|
|
|
|
}
|
2014-05-06 13:38:40 -07:00
|
|
|
for (var i = 0; i < data.responses.length; i++) {
|
|
|
|
|
if (commands[i]['onSuccess']) {
|
|
|
|
|
this.$.proxy(commands[i]['onSuccess'], this)(data.responses[i]);
|
|
|
|
|
}
|
2014-03-25 13:43:11 -07:00
|
|
|
}
|
2014-05-09 15:00:48 -07:00
|
|
|
// Handle events that were immediately available as if they came over the
|
2014-05-06 13:47:57 -07:00
|
|
|
// channel.
|
2014-05-09 15:00:48 -07:00
|
|
|
data['events'].forEach(this.onServerEvent_, this);
|
2014-03-25 13:43:11 -07:00
|
|
|
})
|
|
|
|
|
.fail(function(xhr) {
|
|
|
|
|
var intDelay =
|
|
|
|
|
xhr.getResponseHeader('Retry-After') ||
|
|
|
|
|
Math.min(32, Math.max(2, delay || 2));
|
|
|
|
|
console.log(
|
2014-05-06 13:38:40 -07:00
|
|
|
'RPC failed. Will retry in ' + intDelay + ' seconds');
|
2014-03-25 13:43:11 -07:00
|
|
|
function retry() {
|
2014-05-06 13:38:40 -07:00
|
|
|
this.sendRPCs_(commands, Math.pow(intDelay, 2));
|
2014-03-25 13:43:11 -07:00
|
|
|
}
|
|
|
|
|
window.setTimeout(this.$.proxy(retry, this), intDelay * 1000);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.createChannel_ = function() {
|
2014-05-10 15:47:33 +02:00
|
|
|
var rpcs = [
|
|
|
|
|
{
|
|
|
|
|
'command': 'createChannel',
|
|
|
|
|
'onSuccess': this.onCreateChannel_,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
// TODO(flamingcow): Need to restart from the latest message.
|
|
|
|
|
for (var subject in this.subscriptions_) {
|
|
|
|
|
rpcs.push({
|
|
|
|
|
'command': 'subscribe',
|
|
|
|
|
'arguments': {
|
|
|
|
|
'subject': subject,
|
|
|
|
|
'messages': 0,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
this.sendRPCs_(rpcs);
|
2014-03-25 13:43:11 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.onCreateChannel_ = function(data) {
|
|
|
|
|
var channel = new goog.appengine.Channel(data['token']);
|
|
|
|
|
console.log('Opening channel...');
|
|
|
|
|
this.socket = channel.open({
|
|
|
|
|
onopen: this.$.proxy(this.onSocketOpen_, this),
|
|
|
|
|
onclose: this.$.proxy(this.onSocketClose_, this),
|
|
|
|
|
onmessage: this.$.proxy(this.onSocketMessage_, this),
|
|
|
|
|
onerror: this.$.proxy(this.onSocketError_, this),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.onSocketOpen_ = function() {
|
|
|
|
|
console.log('Channel opened');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.onSocketClose_ = function() {
|
|
|
|
|
if (!this.socket) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.log('Channel closed');
|
|
|
|
|
this.socket = null;
|
|
|
|
|
this.createChannel_();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.onSocketMessage_ = function(msg) {
|
2014-05-09 15:00:48 -07:00
|
|
|
this.onServerEvent_(JSON.parse(msg.data));
|
2014-03-25 14:19:13 -07:00
|
|
|
};
|
|
|
|
|
|
2014-05-09 15:00:48 -07:00
|
|
|
cosmopolite.Client.prototype.onServerEvent_ = function(e) {
|
|
|
|
|
switch (e.event_type) {
|
2014-03-25 13:43:11 -07:00
|
|
|
case 'state':
|
2014-05-09 15:00:48 -07:00
|
|
|
var key = e['key'];
|
2014-05-01 11:33:29 -07:00
|
|
|
if (this.stateCache_[key] &&
|
2014-05-09 15:00:48 -07:00
|
|
|
this.stateCache_[key]['value'] == e['value'] &&
|
|
|
|
|
this.stateCache_[key]['last_set'] == e['last_set'] &&
|
|
|
|
|
this.stateCache_[key]['public'] == e['public']) {
|
|
|
|
|
// Duplicate event.
|
2014-03-25 13:43:11 -07:00
|
|
|
break;
|
|
|
|
|
}
|
2014-05-01 11:33:29 -07:00
|
|
|
this.stateCache_[key] = {
|
2014-05-09 15:00:48 -07:00
|
|
|
'value': e['value'],
|
|
|
|
|
'last_set': e['last_set'],
|
|
|
|
|
'public': e['public'],
|
2014-05-01 11:33:29 -07:00
|
|
|
}
|
2014-03-25 13:43:11 -07:00
|
|
|
if ('onStateChange' in this.callbacks_) {
|
2014-05-01 11:33:29 -07:00
|
|
|
this.callbacks_['onStateChange'](key, this.stateCache_[key]);
|
2014-03-25 13:43:11 -07:00
|
|
|
}
|
|
|
|
|
break;
|
2014-05-01 14:55:50 -07:00
|
|
|
case 'login':
|
|
|
|
|
if ('onLogin' in this.callbacks_) {
|
|
|
|
|
this.callbacks_['onLogin'](
|
2014-05-10 15:47:33 +02:00
|
|
|
e['google_user'],
|
2014-05-09 15:00:48 -07:00
|
|
|
this.urlPrefix_ + '/auth/logout');
|
2014-05-01 14:55:50 -07:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'logout':
|
|
|
|
|
if ('onLogout' in this.callbacks_) {
|
|
|
|
|
this.callbacks_['onLogout'](
|
|
|
|
|
this.urlPrefix_ + '/auth/login');
|
|
|
|
|
}
|
|
|
|
|
break;
|
2014-05-10 15:47:33 +02:00
|
|
|
case 'message':
|
|
|
|
|
if ('onMessage' in this.callbacks_) {
|
|
|
|
|
if (!(e['subject'] in this.subscriptions_)) {
|
|
|
|
|
console.log('Message from unrecognized subject:', e);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
var subscription = this.subscriptions_[e['subject']];
|
|
|
|
|
var duplicate = subscription.messages.some(function(message) {
|
|
|
|
|
return message['id'] == e.id;
|
|
|
|
|
});
|
|
|
|
|
if (duplicate) {
|
|
|
|
|
console.log('Duplicate message:', e);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
subscription.messages.push(e);
|
|
|
|
|
this.callbacks_['onMessage'](e);
|
|
|
|
|
}
|
|
|
|
|
break;
|
2014-03-25 13:43:11 -07:00
|
|
|
default:
|
2014-03-25 14:19:13 -07:00
|
|
|
// Client out of date? Force refresh?
|
2014-05-09 15:00:48 -07:00
|
|
|
console.log('Unknown channel event:', e);
|
2014-03-25 13:43:11 -07:00
|
|
|
break;
|
|
|
|
|
}
|
2014-03-25 14:19:13 -07:00
|
|
|
};
|
2014-03-25 13:43:11 -07:00
|
|
|
|
|
|
|
|
cosmopolite.Client.prototype.onSocketError_ = function(msg) {
|
2014-05-06 22:46:07 -07:00
|
|
|
console.log('Socket error:', msg);
|
2014-03-25 13:43:11 -07:00
|
|
|
this.socket.close();
|
|
|
|
|
};
|