Commits

Fred T-H committed 548a6d5

Starting to refactor the JS and mochiweb client implementation

I'm moving things to JSONP in order to avoid the domain limitations
problems that were hit before. A dependency to json_parse (from
Douglas Crockford) and script_communicator.js are added to the
project.

This commit breaks the working state of a JS client; as such, it is
not pushed yet, but merely a save point of my own work. I'm sorry if
mercurial commits should not be used that way (in fact I should have
made a branch!). I just am dumb.

  • Participants
  • Parent commits 90fe9ec

Comments (0)

Files changed (5)

+// jQuery woo!
+Chut = {
+	// the server only has to return 'callback(JSON_Obj);' from these
+	// urls.
+	listen: function(id){
+		Chut._JSONP(Chut.url.listen(id),'Chut.callbacks.listen');
+	},
+	message: function(from, to, msg){
+		Chut._JSONP(Chut.url.message(from, to, msg),'Chut.callbacks.message');
+	},
+	history: function(id){
+		Chut._JSONP(Chut.url.history(id),'Chut.callbacks.history');
+	},
+	url: {
+		base: 'http://localhost:8080',
+		listen: function(user_id){
+			return Chut.url.base+"/listen?id="+escape(user_id);
+		},
+		message: function(from, to, msg){
+			from = escape(from);
+			to = escape(to);
+			msg = escape(msg);
+			return Chut.url.base+"/msg?from="+from+"&to="+to+"&msg="+msg;
+		},
+		history: function(user_id){
+			return Chut.url.base+"/hist?id="+escape(user_id);
+		}
+	},
+	callbacks: {
+		success: function(){
+			alert('huge success');
+		},
+		error: function(){
+			alert('cake is a lie?');
+		},
+		listen: function(e){
+			ScriptCommunicator.callback_called = true;
+			alert('received: ' + e);
+		},
+		message: function(e){
+			ScriptCommunicator.callback_called = true;
+			alert('called message: ' + e);
+		},
+		history: function(e){
+			ScriptCommunicator.callback_called = true;
+			alert('history is: ' + e);
+		}
+	},
+	// The server only has to return 'callback(JSON);'
+	_JSONP: function (url, callback) {
+		Chut._call(url+'&callback='+callback);
+	},
+	_call: function (url){
+		ScriptCommunicator.sourceJavaScript(url, Chut.callbacks.success, Chut.callbacks.error);
+	},
+	// define some kind of JSON parser, maybe Crockford's?
+	// Yes, it is crockford's!
+	_readJSON: function(e){ return json_parse(e); },
+	ui: {
+		add_chutbox: function(el, id){
+			var user_id = escape(id);
+			var txt = '<div class="chut">'+
+					'<div class="log" id="chut_'+user_id+'"></div>'+
+					'<textarea id="chut_'+user_id+'_msg"></textarea>'+
+					'<label for="chut_'+user_id+'_to">To:</label>'+
+					'<input type="text" id="chut_'+user_id+'_to" value='+user_id+' readonly="readonly" />'+
+					'<input type="submit" id="chut_'+user_id+'_send" />'+
+				'</div>';
+			$(el).append(txt);
+		}
+	}
+};
+
+
+
+$(document).ready(function(){
+	// Assumes that ScriptCommunicator is loaded
+	if(typeof(ScriptCommunicator) === 'undefined'){
+		alert("ScriptCommunicator is missing");
+		return false;
+	}
+	// events about logging on?
+	/* Reserved IDs
+	 * #chut:           general chut container
+	 * #chut_nick:      user_id
+	 * #chut_<id>_to:   user_id of message receiver
+	 * #chut_<id>_msg:  message to send to a user
+	 * #chut_<id>:      chat box of a discussion with a given user
+	 */
+	$('#connect').click(function(){
+		var nickbox = $('#chut_nick');
+		if(nickbox.val() !== ''){
+			var nick = escape(nickbox.val());
+			nickbox.attr('readonly','readonly');
+			$('#connect').remove();
+			Chut.listen(nick);
+		}
+	});
+	$('#submit_send_to').click(function(){
+		var to_box = $('#send_to');
+		var from_box = $('#chut_nick');
+		if(from_box.val() !== '' && to_box.val() !== ''){
+			var nick = to_box.val();
+			Chut.ui.add_chutbox('#chut', nick);
+			to_box.val('');
+		}
+	});
+});
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
+	<title>Chut - Demo comet implementation</title>
+	<style type="text/css">
+		label {
+			display: block;
+		}
+		div.chut {
+			width: 30em;
+			border: 0.1em solid #000;
+			padding: 1em;
+			margin: 1em;
+			overflow: hidden;
+		}
+		.chut textarea {
+			width: 70%;
+			height: 5em;
+			display: block;
+		}
+		.chut div.log {
+			border: 1px solid #333;
+			width: 95%;
+			height: 10em;
+			overflow-y: scroll;
+		}
+	</style>
+	<!-- javascript ici -->
+	<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
+	<script type="text/javascript" src="script_communicator.js"></script>
+	<script type="text/javascript" src="json_parse.js"></script>
+	<script type="text/javascript" src="client.js"></script>
+</head>
+<body>
+	<h1>Chut</h1>
+	<h2>Demo chat implementation</h2>
+	<p>No username list available at the moment.</p>
+	<!-- general chut container -->
+	<div id="chut">
+		<label for="chut_nick">Nickname:</label>
+		<input type="text" id="chut_nick" />
+		<button id="connect">Connect</button>
+		<!-- add a chat window -->
+		<label for="send_to">Send a message to:</label>
+		<input id="send_to" />
+		<button id="submit_send_to">Start</button>
+	</div>
+</body>
+</html>
+/*
+    http://www.JSON.org/json_parse.js
+    2009-05-31
+
+    Public Domain.
+
+    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+    This file creates a json_parse function.
+
+        json_parse(text, reviver)
+            This method parses a JSON text to produce an object or array.
+            It can throw a SyntaxError exception.
+
+            The optional reviver parameter is a function that can filter and
+            transform the results. It receives each of the keys and values,
+            and its return value is used instead of the original value.
+            If it returns what it received, then the structure is not modified.
+            If it returns undefined then the member is deleted.
+
+            Example:
+
+            // Parse the text. Values that look like ISO date strings will
+            // be converted to Date objects.
+
+            myData = json_parse(text, function (key, value) {
+                var a;
+                if (typeof value === 'string') {
+                    a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+                    if (a) {
+                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+                            +a[5], +a[6]));
+                    }
+                }
+                return value;
+            });
+
+    This is a reference implementation. You are free to copy, modify, or
+    redistribute.
+
+    This code should be minified before deployment.
+    See http://javascript.crockford.com/jsmin.html
+
+    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+    NOT CONTROL.
+*/
+
+/*members "", "\"", "\/", "\\", at, b, call, charAt, f, fromCharCode,
+    hasOwnProperty, message, n, name, push, r, t, text
+*/
+
+var json_parse = (function () {
+
+// This is a function that can parse a JSON text, producing a JavaScript
+// data structure. It is a simple, recursive descent parser. It does not use
+// eval or regular expressions, so it can be used as a model for implementing
+// a JSON parser in other languages.
+
+// We are defining the function inside of another function to avoid creating
+// global variables.
+
+    var at,     // The index of the current character
+        ch,     // The current character
+        escapee = {
+            '"':  '"',
+            '\\': '\\',
+            '/':  '/',
+            b:    '\b',
+            f:    '\f',
+            n:    '\n',
+            r:    '\r',
+            t:    '\t'
+        },
+        text,
+
+        error = function (m) {
+
+// Call error when something is wrong.
+
+            throw {
+                name:    'SyntaxError',
+                message: m,
+                at:      at,
+                text:    text
+            };
+        },
+
+        next = function (c) {
+
+// If a c parameter is provided, verify that it matches the current character.
+
+            if (c && c !== ch) {
+                error("Expected '" + c + "' instead of '" + ch + "'");
+            }
+
+// Get the next character. When there are no more characters,
+// return the empty string.
+
+            ch = text.charAt(at);
+            at += 1;
+            return ch;
+        },
+
+        number = function () {
+
+// Parse a number value.
+
+            var number,
+                string = '';
+
+            if (ch === '-') {
+                string = '-';
+                next('-');
+            }
+            while (ch >= '0' && ch <= '9') {
+                string += ch;
+                next();
+            }
+            if (ch === '.') {
+                string += '.';
+                while (next() && ch >= '0' && ch <= '9') {
+                    string += ch;
+                }
+            }
+            if (ch === 'e' || ch === 'E') {
+                string += ch;
+                next();
+                if (ch === '-' || ch === '+') {
+                    string += ch;
+                    next();
+                }
+                while (ch >= '0' && ch <= '9') {
+                    string += ch;
+                    next();
+                }
+            }
+            number = +string;
+            if (isNaN(number)) {
+                error("Bad number");
+            } else {
+                return number;
+            }
+        },
+
+        string = function () {
+
+// Parse a string value.
+
+            var hex,
+                i,
+                string = '',
+                uffff;
+
+// When parsing for string values, we must look for " and \ characters.
+
+            if (ch === '"') {
+                while (next()) {
+                    if (ch === '"') {
+                        next();
+                        return string;
+                    } else if (ch === '\\') {
+                        next();
+                        if (ch === 'u') {
+                            uffff = 0;
+                            for (i = 0; i < 4; i += 1) {
+                                hex = parseInt(next(), 16);
+                                if (!isFinite(hex)) {
+                                    break;
+                                }
+                                uffff = uffff * 16 + hex;
+                            }
+                            string += String.fromCharCode(uffff);
+                        } else if (typeof escapee[ch] === 'string') {
+                            string += escapee[ch];
+                        } else {
+                            break;
+                        }
+                    } else {
+                        string += ch;
+                    }
+                }
+            }
+            error("Bad string");
+        },
+
+        white = function () {
+
+// Skip whitespace.
+
+            while (ch && ch <= ' ') {
+                next();
+            }
+        },
+
+        word = function () {
+
+// true, false, or null.
+
+            switch (ch) {
+            case 't':
+                next('t');
+                next('r');
+                next('u');
+                next('e');
+                return true;
+            case 'f':
+                next('f');
+                next('a');
+                next('l');
+                next('s');
+                next('e');
+                return false;
+            case 'n':
+                next('n');
+                next('u');
+                next('l');
+                next('l');
+                return null;
+            }
+            error("Unexpected '" + ch + "'");
+        },
+
+        value,  // Place holder for the value function.
+
+        array = function () {
+
+// Parse an array value.
+
+            var array = [];
+
+            if (ch === '[') {
+                next('[');
+                white();
+                if (ch === ']') {
+                    next(']');
+                    return array;   // empty array
+                }
+                while (ch) {
+                    array.push(value());
+                    white();
+                    if (ch === ']') {
+                        next(']');
+                        return array;
+                    }
+                    next(',');
+                    white();
+                }
+            }
+            error("Bad array");
+        },
+
+        object = function () {
+
+// Parse an object value.
+
+            var key,
+                object = {};
+
+            if (ch === '{') {
+                next('{');
+                white();
+                if (ch === '}') {
+                    next('}');
+                    return object;   // empty object
+                }
+                while (ch) {
+                    key = string();
+                    white();
+                    next(':');
+                    if (Object.hasOwnProperty.call(object, key)) {
+                        error('Duplicate key "' + key + '"');
+                    }
+                    object[key] = value();
+                    white();
+                    if (ch === '}') {
+                        next('}');
+                        return object;
+                    }
+                    next(',');
+                    white();
+                }
+            }
+            error("Bad object");
+        };
+
+    value = function () {
+
+// Parse a JSON value. It could be an object, an array, a string, a number,
+// or a word.
+
+        white();
+        switch (ch) {
+        case '{':
+            return object();
+        case '[':
+            return array();
+        case '"':
+            return string();
+        case '-':
+            return number();
+        default:
+            return ch >= '0' && ch <= '9' ? number() : word();
+        }
+    };
+
+// Return the json_parse function. It will have access to all of the above
+// functions and variables.
+
+    return function (source, reviver) {
+        var result;
+
+        text = source;
+        at = 0;
+        ch = ' ';
+        result = value();
+        white();
+        if (ch) {
+            error("Syntax error");
+        }
+
+// If there is a reviver function, we recursively walk the new structure,
+// passing each name/value pair to the reviver function for possible
+// transformation, starting with a temporary root object that holds the result
+// in an empty key. If there is not a reviver function, we simply return the
+// result.
+
+        return typeof reviver === 'function' ? (function walk(holder, key) {
+            var k, v, value = holder[key];
+            if (value && typeof value === 'object') {
+                for (k in value) {
+                    if (Object.hasOwnProperty.call(value, k)) {
+                        v = walk(value, k);
+                        if (v !== undefined) {
+                            value[k] = v;
+                        } else {
+                            delete value[k];
+                        }
+                    }
+                }
+            }
+            return reviver.call(holder, key, value);
+        }({'': result}, '')) : result;
+    };
+}());

js/script_communicator.js

+/*
+ * Implementation of script communication that:
+ *  - uses script tags for communication, but can detect when a script isn't loaded (this is non-trivial to implement across browsers)
+ *  - works across domains as long as you control the domains
+ *  - works on IE 6, IE 7, IE 8, FF X, Safari, Chrome and Opera
+ *  - small (80 lines of code) with no dependencies
+ *
+ * For more info and usage check out:
+ *    http://amix.dk/blog/post/19489#ScriptCommunicator-implementing-comet-long-polling-for-all-browse
+ *
+ * Made by amix the lucky stiff - amix.dk
+ * Copyright Plurk 2010, released under BSD license
+ */
+ScriptCommunicator = {
+
+    callback_called: false,
+
+    /*
+     * Important:
+     * The JavaScript you source must do some kind of call back into your code
+     * and this call back has to set ScriptCommunicator.callback_called = true
+     * for this to work!
+     */
+    sourceJavaScript: function(uri, on_success, on_error) {
+        ScriptCommunicator.callback_called = false;
+
+        ScriptCommunicator._onSuccess = on_success;
+        ScriptCommunicator._onError = on_error;
+
+        var loaded_text = 'if(!ScriptCommunicator.callback_called) {' + 
+                              'ScriptCommunicator.onError();'+
+                          '}'+
+                          'else { ' +
+                               'ScriptCommunicator.onSuccess();'+ 
+                          '}';
+
+        var agent = navigator.userAgent.toLowerCase();
+
+        if(agent.indexOf("khtml") != -1) { //Safari
+            document.writeln('<script type="text/javascript" src="'+uri+'" class="temp_script"><\/script>');
+            document.writeln('<script type="text/javascript" class="temp_script">'+ loaded_text +'<\/script>');
+        }
+        else {
+            var script_channel = document.createElement('script');
+            script_channel.src = uri;
+            script_channel.type = "text/javascript";
+            script_channel.className = 'temp_script';
+
+            var loaded = null;
+            if(agent.indexOf("msie") != -1) { //IE
+                script_channel.onreadystatechange = ScriptCommunicator.onSuccess;
+            }
+            else {
+                var loaded = document.createElement('script');
+                loaded.type = "text/javascript";
+                loaded.className = 'temp_script';
+                loaded.text = loaded_text;
+            }
+
+            var body = document.getElementsByTagName('body')[0];
+            body.appendChild(script_channel);
+            if(loaded)
+                body.appendChild(loaded);
+        }
+    },
+
+    onSuccess: function() {
+        if(this.readyState == 'loaded' && !ScriptCommunicator.callback_called) {
+            return ScriptCommunicator.onError();
+        }
+
+        if(!this.readyState || this.readyState === "loaded" || this.readyState === "complete") { 
+            return ScriptCommunicator._onSuccess();
+        }
+    },
+
+    onError: function() {
+        return ScriptCommunicator._onError();
+    }
+
+}
+

src/client.js

-// html content
-is_listening = false;
-var html = {
-    nickBlock: '<div id="nickblock">nick: <input type="text" id="nickname" /><button id="regnick">OK</button>',
-    text: '<div id="text"></div>',
-    send_msg: '<div id="msgform">à: <input type="text" id="to"/><br />msg: <input type="text" id="send_msg" /><button id="submit_msg">OK</button>'
-};
-
-function get_nick() {
-    return $('#nick').text();
-}
-
-// call suivi d'un listen server-side
-function send_msg() {
-    var msg = $('#send_msg').val();
-    $('#send_msg').val('');
-    var to = $('#to').val();
-    $.getJSON('message?nick='+escape(get_nick())+'&to='+escape(to)+'&msg='+escape(msg)+'&rand='+Math.random());
-}
-function send_emsg(e){
-    if(e.which == 13){ // enter key pressed
-        send_msg();
-    }
-}
-function putmsgs(data){
-    $.map(data, function(e){
-        var from;
-        var to;
-        if(e.action == 'sent'){
-            from = get_nick();
-            to = e.to;
-        }else if (e.action == 'received') {
-            from = e.to;
-            to = get_nick();
-        }
-        var msg = '<p class="'+e.action+'">'+from+' -> '+to+'<br />'+e.message+'</p>';
-        $('#text').append(msg);
-    });
-    is_listening = false;
-}
-// mode passif!
-function listen(){
-    if(is_listening === false){
-        is_listening = true;
-        $.getJSON('listen?nick='+escape(get_nick())+'&rand='+Math.random(), function(data){ putmsgs(data); listen();} );
-    }
-}
-
-// listen checker!
-
-// functions and whatnot
-// registration pas necessaire, faite à chaque call
-function main(){
-    var nick = $('#nickname').val();
-    $('#content').prepend('<p>Connect&eacute; en tant que <span id="nick">'+nick+'</p>');
-    $('#nickblock').remove();
-    $('#content').append(html.text);
-    $('#content').append(html.send_msg);
-    $('#submit_msg').click(send_msg);
-    $('#submit_msg').keypress(send_emsg);
-    listen();
-}
-
-// main
-$(document).ready(function(){
-    // register user
-    $('#content').append(html.nickBlock);
-    $('#regnick').click(main);
-});