summaryrefslogtreecommitdiff
path: root/js/codeq
diff options
context:
space:
mode:
authorAleš Smodiš <aless@guru.si>2015-09-14 14:52:30 +0200
committerAleš Smodiš <aless@guru.si>2015-09-14 14:52:30 +0200
commit18a575e02816622706f740e9938515869b58a375 (patch)
tree0b3522830859896d389c6c44090085c27081a437 /js/codeq
parent517fb5cec0dab7672cd6ce97fe72906f09125435 (diff)
Reimplemented communication with the server.
* Using engine.io to use web sockets or long polling. * Changed to a single-page app.
Diffstat (limited to 'js/codeq')
-rw-r--r--js/codeq/comms.js271
-rw-r--r--js/codeq/login.js2
-rw-r--r--js/codeq/startup.js97
3 files changed, 368 insertions, 2 deletions
diff --git a/js/codeq/comms.js b/js/codeq/comms.js
index 25c1f8c..f32b4db 100644
--- a/js/codeq/comms.js
+++ b/js/codeq/comms.js
@@ -1,4 +1,4 @@
-(function () {
+/*(function () {
var send = function (service, json) {
if (json instanceof Object) json = codeq.jsonize(json);
@@ -150,4 +150,273 @@
});
}
};
+})();*/
+
+(function () {
+ var BinaryMessageError = function (message) {
+ var e = new Error(message);
+ this.message = message;
+ if (e.stack) this.stack = e.stack;
+ };
+ BinaryMessageError.prototype = new Error();
+ BinaryMessageError.prototype.name = 'BinaryMessageError';
+
+ var MessageParseError = function (message) {
+ var e;
+ if (message instanceof Error) {
+ e = message;
+ this.message = 'Message parse error: ' + e.toString();
+ }
+ else {
+ e = new Error(message);
+ this.message = message;
+ }
+ if (e.stack) this.stack = e.stack;
+ };
+ MessageParseError.prototype = new Error();
+ MessageParseError.prototype.name = 'MessageParseError';
+
+ var currentTID = 0, // transaction ID of the next message
+ sid = null, // session ID, changes after each successful initial login
+ socket = null,
+ reconnectTimer = null,
+ login_username = null,
+ login_password = null,
+
+ waiters = {}, // keyed by TID: {packet, created, promise}
+ connected = false,
+ connectPromise = null,
+ handleIncomingMessageError = function (error, data) {
+ if (error instanceof BinaryMessageError) {
+ codeq.log.error('Incoming message is not a string, attempting reconnect');
+ connected = false;
+ socket.close(); // connection will be reestablished in the close event handler
+ }
+ else if (error instanceof MessageParseError) {
+ codeq.log.error('Incoming message is not a JSON message, ignoring: ' + data);
+ }
+ else {
+ codeq.log.error('Unknown error, attempting reconnect: ' + error);
+ connected = false;
+ socket.close(); // connection will be reestablished in the close event handler
+ }
+ },
+ parseData = function (data) {
+ if (typeof data !== 'string') {
+ throw new BinaryMessageError('Message is not a string');
+ }
+
+ try {
+ return JSON.parse(data);
+ }
+ catch (e) {
+ codeq.log.error('Incoming message is not a JSON message, ignoring: ' + data);
+ throw new MessageParseError(e);
+ }
+ },
+ onMessage = function (data) {
+ var m, waiter;
+
+ try {
+ m = parseData(data);
+ }
+ catch (e) {
+ handleIncomingMessageError(e, data);
+ return;
+ }
+
+ if ('tid' in m) {
+ waiter = waiters[m.tid];
+ if (waiter) {
+ delete waiters[m.tid];
+ waiter.promise.resolve(m);
+ codeq.log.debug('Delivered an incoming message, TID=' + m.tid);
+ return;
+ }
+ else {
+ codeq.log.debug('There is noone waiting for the incoming message, TID=' + m.tid);
+ }
+ }
+ else {
+ codeq.log.debug('Incoming message without a TID, handing it off to handlers');
+ }
+
+ // TODO handle message delivery
+ },
+ onConnect = function () {
+ var tid, sortedWaiters = [], i;
+ connected = true;
+ if (connectPromise) {
+ // manual connect
+ connectPromise.resolve();
+ connectPromise = null;
+ }
+ else {
+ // auto re-connect
+ }
+ // resend anything not already sent
+ for (tid in waiters) {
+ if (waiters.hasOwnProperty(tid)) {
+ sortedWaiters.push(waiters[tid]);
+ }
+ }
+ // sort by ascending transaction ID before sending
+ sortedWaiters.sort(function (a, b) {return a.tid - b.tid;});
+ for (i = 0; i < sortedWaiters.length; i++) {
+ (function (waiter) {
+ if (!waiter.sent) {
+ socket.send(JSON.stringify(waiter.packet), function () {
+ waiter.sent = true;
+ });
+ }
+ })(sortedWaiters[i]);
+ }
+ },
+ onClose = function () {
+ connected = false;
+ if (connectPromise) {
+ // resulted from manual connect, don't attempt reconnect
+ connectPromise.reject(new Error('Connection failed'));
+ connectPromise = null;
+ socket.off('open', onConnect);
+ socket.off('close', onClose);
+ socket.off('message', onMessage);
+ socket = null;
+ }
+ else {
+ // spurious disconnect, attempt reconnect
+ reconnectTimer = setTimeout(function () {
+ reconnectTimer = null;
+ socket.open();
+ }, 250); // reconnect in quarter of a second
+ }
+ };
+
+ codeq.comms = {
+ 'connect': function () {
+ var deferred = Q.defer();
+
+ if (connectPromise) {
+ // connection already taking place
+ connectPromise.then(deferred.resolve, deferred.reject, deferred.notify);
+ }
+ else if (socket) {
+ // already connected
+ if (connected) {
+ deferred.resolve();
+ }
+ else {
+ // auto re-connecting
+ connectPromise = deferred;
+ }
+ }
+ else {
+ // create a new connection
+ connectPromise = deferred;
+ socket = eio('ws://' + location.host, {
+ 'upgrade': true,
+ 'path': '/ws',
+ 'transports': ['polling', 'websocket']
+ });
+ socket.on('open', onConnect);
+ socket.on('close', onClose);
+ socket.on('message', onMessage);
+ }
+
+ return deferred.promise;
+ },
+ 'disconnect': function () {
+ if (socket) {
+ socket.off('open', onConnect);
+ socket.off('close', onClose);
+ socket.off('message', onMessage);
+ socket.close();
+ socket = null;
+ }
+ if (reconnectTimer !== null) {
+ clearTimeout(reconnectTimer);
+ reconnectTimer = null;
+ }
+ connected = false;
+ if (connectPromise) {
+ connectPromise.reject(new Error('Forced disconnect'));
+ connectPromise = null;
+ }
+ login_password = null; // manual disconnect will prevent auto-login
+ sid = null;
+ },
+ 'send': function (packet) {
+ return Q.Promise(function (resolve, reject, notify) {
+ var waiter = {
+ 'packet': packet,
+ 'created': Date.now(),
+ 'promise': {'resolve': resolve, 'reject': reject, 'notify': notify},
+ 'sent': false
+ };
+ packet.tid = currentTID;
+ if (sid !== null) packet.sid = sid;
+ waiters[currentTID] = waiter;
+ currentTID++;
+ if (connected) {
+ socket.send(JSON.stringify(packet), function () { waiter.sent = true; });
+ }
+ });
+ },
+ 'login': function (username, password) {
+ var packet = {'action': 'login', 'username': username, 'password': password},
+ myself = this,
+ performLogin = function () {
+ return myself.send(packet).then(function (response) {
+ if (response.code === 0) {
+ // login successful, remember the login credentials for auto-reconnects
+ login_username = username;
+ login_password = password;
+ sid = response.new_sid || response.sid;
+ }
+ return response;
+ });
+ };
+ if (socket) return performLogin();
+ // this is the only method where we do connect() first, if we're not already connected
+ return this.connect().then(performLogin);
+ },
+
+ sendActivity: function commsSendActivity (trace, solution, problem_id) {
+ return this.send({'action': 'activity', 'trace': trace, 'solution': solution, 'problem_id': problem_id});
+ },
+
+ sendQuery: function commsSendQuery (query, problem_id) {
+ query['action'] = 'query';
+ return this.send(query);
+ },
+
+ sendPush: function commsSendPush (json) {
+ json['action'] = 'python_push';
+ return this.send(json);
+ },
+
+ sendPull: function commsSendPull (json) {
+ json['action'] = 'python_pull';
+ return this.send(json);
+ },
+
+ sendHint: function commsSendHint (json) {
+ json['action'] = 'hint';
+ return this.send(json);
+ },
+
+ sendTest: function commsSendTest (json) {
+ json['action'] = 'test';
+ return this.send(json);
+ },
+
+ getProblem: function commsGetProblem (language, problem_group, problem) {
+ return this.send({
+ 'action': 'get_problem',
+ 'language': language,
+ 'problem_group': problem_group,
+ 'problem': problem
+ });
+ }
+ };
})();
diff --git a/js/codeq/login.js b/js/codeq/login.js
index e1daadf..ef67304 100644
--- a/js/codeq/login.js
+++ b/js/codeq/login.js
@@ -20,7 +20,7 @@
jqGroup = $('#problem_group'),
jqProblems = $('#problems'),
html = [],
- mapping = {}
+ mapping = {},
onGroupChange = function () {
var problems = mapping[jqGroup.val()],
html = [],
diff --git a/js/codeq/startup.js b/js/codeq/startup.js
new file mode 100644
index 0000000..980d3d7
--- /dev/null
+++ b/js/codeq/startup.js
@@ -0,0 +1,97 @@
+$(document).ready(function () {
+ codeq.comms.connect().then(function () {
+ return codeq.comms.send({'action': 'list_problems'});
+ }).then(
+ function success(data) {
+ var i, groups, group, problems, problem, first_group,
+ jqGroup = $('#problem_group'),
+ jqProblems = $('#problems'),
+ html = [],
+ mapping = {},
+ onGroupChange = function () {
+ var problems = mapping[jqGroup.val()],
+ html = [],
+ i, p;
+ if (problems) {
+ for (i = 0; i < problems.length; i++) {
+ p = problems[i];
+ html.push('<option value="', p.identifier, '">', p.name, '</option>\n')
+ }
+ }
+ jqProblems.html(html.join(''));
+ };
+
+ if (data && (data.code === 0)) {
+ $('#disabled').css('display', 'none');
+ groups = data.problems;
+ for (i = 0; i < groups.length; i++) {
+ group = groups[i];
+ var identifier = group.identifier.language + '/' + group.identifier.group;
+ var name = group.name.language + ': ' + group.name.group;
+ html.push('<option value="', identifier, '">', name, '</option>\n');
+ mapping[identifier] = group.problems;
+ }
+ jqGroup.html(html.join(''));
+ first_group = html[1];
+ html = null;
+
+ jqGroup.on('click', onGroupChange);
+ jqGroup.val(first_group);
+ onGroupChange();
+
+ $("#submit").on('click', function () {
+ var identifier = $('#problem_group').val().split('/'),
+ problem = $('#problems').val();
+ if (identifier.length < 2) alert('Choose a problem group');
+ else if (!problem) alert('Choose a problem');
+ else {
+ $('#disabled').css('display', '');
+ codeq.comms.login($('#username').val(), $('#password').val())
+ .then(function (data) {
+ $('#disabled').css('display', 'none');
+ if (data.code !== 0) throw new Error('Login failed, code: ' + data.code + ', message: ' + data.message);
+ })
+ .then(function () {
+ return codeq.comms.getProblem(identifier[0], identifier[1], problem);
+ })
+ .then(function (data) {
+ if (data.code !== 0) throw new Error('Failed to obtain problem data, code: ' + data.code + ', message: ' + data.message);
+ $('#disabled').css('display', 'none');
+ switch (identifier[0]) {
+ case 'prolog':
+ // TODO: assignment to window for debug only
+ $('#screen_login').css('display', 'none');
+ $('#screen_prolog').css('display', '');
+ window.phandler = codeq.createPrologHandler(data.data);
+ break;
+ case 'python':
+ // TODO: assignment to window for debug only
+ $('#screen_login').css('display', 'none');
+ $('#screen_prolog').css('display', '');
+ window.phandler = codeq.createPythonHandler(data.data);
+ break;
+ default:
+ alert('Unknown language: ' + identifier[0]);
+ break;
+ }
+ })
+ .fail(function (reason) {
+ $('#disabled').css('display', 'none');
+ alert('Login request failed: ' + reason);
+ })
+ .done();
+ }
+ });
+ }
+ else {
+ $('#disabled').css('cursor', '');
+ alert('Obtaining list of problems failed: code=' + data.code + ', reason=' + data.message);
+ }
+ },
+
+ function failure(reason) {
+ $('#disabled').css('cursor', '');
+ alert('Request to obtain list of problems failed: ' + reason);
+ }
+ ).done();
+}); \ No newline at end of file