Skip to content

Instantly share code, notes, and snippets.

@amcgregor
Created May 16, 2012 17:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amcgregor/2712270 to your computer and use it in GitHub Desktop.
Save amcgregor/2712270 to your computer and use it in GitHub Desktop.
A fully functional pure-Nginx asynchronous pub/sub chat system.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Push Chat Example</title>
<style type="text/css" media="screen">
/* Reset */
* { outline: none; border: none; margin: 0; padding: 0; border: none; color: inherit; background-color: transparent; line-height: 1.3; font-size: 10pt; font-weight: normal; list-style: none; font-family: 'Lucida Grande', 'Verdana', sans-serif; cursor: default; }
/* Base */
html, body { min-height: 100%; }
body { padding: 10px; }
/* Button Common */
button, button label { display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }
button label { display: inline-block; }
button.icon label { padding-left: 22px; background: transparent url(icn/help.svg) center left no-repeat; background-size: 16px; }
/* Button Clean */
button { background: #e3e3e3; border: 1px solid #bbb; -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: inset 0 0 1px 1px #f6f6f6; -moz-box-shadow: inset 0 0 1px 1px #f6f6f6; -ms-box-shadow: inset 0 0 1px 1px #f6f6f6; -o-box-shadow: inset 0 0 1px 1px #f6f6f6; box-shadow: inset 0 0 1px 1px #f6f6f6; color: #333; line-height: 1; padding: 5px 8px 6px; text-align: center; text-shadow: 0 1px 0 #fff; }
button.pill { -webkit-border-radius: 1.1em; -moz-border-radius: 1.1em; -ms-border-radius: 1.1em; -o-border-radius: 1.1em; border-radius: 1.1em; }
button:hover { background: #d9d9d9; -webkit-box-shadow: inset 0 0 1px 1px #eaeaea; -moz-box-shadow: inset 0 0 1px 1px #eaeaea; -ms-box-shadow: inset 0 0 1px 1px #eaeaea; -o-box-shadow: inset 0 0 1px 1px #eaeaea; box-shadow: inset 0 0 1px 1px #eaeaea; color: #222; }
button:active, button:focus { background: #d0d0d0; -webkit-box-shadow: inset 0 0 1px 1px #e3e3e3; -moz-box-shadow: inset 0 0 1px 1px #e3e3e3; -ms-box-shadow: inset 0 0 1px 1px #e3e3e3; -o-box-shadow: inset 0 0 1px 1px #e3e3e3; box-shadow: inset 0 0 1px 1px #e3e3e3; color: #000; }
button.default { background: #d0d0d0; -webkit-box-shadow: inset 0 0 1px 1px #f6f6f6; -moz-box-shadow: inset 0 0 1px 1px #f6f6f6; -ms-box-shadow: inset 0 0 1px 1px #f6f6f6; -o-box-shadow: inset 0 0 1px 1px #f6f6f6; box-shadow: inset 0 0 1px 1px #f6f6f6; color: #111; }
button.default label { font-weight: bold; }
button.default:hover { background: #d9d9d9; -webkit-box-shadow: inset 0 0 1px 1px #eaeaea; -moz-box-shadow: inset 0 0 1px 1px #eaeaea; -ms-box-shadow: inset 0 0 1px 1px #eaeaea; -o-box-shadow: inset 0 0 1px 1px #eaeaea; box-shadow: inset 0 0 1px 1px #eaeaea; color: #222; }
button.default:active, button.default:focus { background: #d0d0d0; -webkit-box-shadow: inset 0 0 1px 1px #e3e3e3; -moz-box-shadow: inset 0 0 1px 1px #e3e3e3; -ms-box-shadow: inset 0 0 1px 1px #e3e3e3; -o-box-shadow: inset 0 0 1px 1px #e3e3e3; box-shadow: inset 0 0 1px 1px #e3e3e3; color: #000; }
button:disabled, button:disabled:hover, button:disabled:active, button:disabled:focus { background: #e3e3e3; -webkit-box-shadow: inset 0 0 1px 1px #f6f6f6; -moz-box-shadow: inset 0 0 1px 1px #f6f6f6; -ms-box-shadow: inset 0 0 1px 1px #f6f6f6; -o-box-shadow: inset 0 0 1px 1px #f6f6f6; box-shadow: inset 0 0 1px 1px #f6f6f6; color: #777; }
button.selected { background: #444; -webkit-box-shadow: inset 0 0 1px 1px #111; -moz-box-shadow: inset 0 0 1px 1px #111; -ms-box-shadow: inset 0 0 1px 1px #111; -o-box-shadow: inset 0 0 1px 1px #111; box-shadow: inset 0 0 1px 1px #111; color: #ddd; border-color: #999; text-shadow: 0 1px 0 #000; }
button.selected:hover { background: #222; color: #fff; }
button.selected:active, button.selected:focus { background: #d0d0d0; -webkit-box-shadow: inset 0 0 1px 1px #e3e3e3; -moz-box-shadow: inset 0 0 1px 1px #e3e3e3; -ms-box-shadow: inset 0 0 1px 1px #e3e3e3; -o-box-shadow: inset 0 0 1px 1px #e3e3e3; box-shadow: inset 0 0 1px 1px #e3e3e3; text-shadow: 0 1px 0 #fff; color: #000; }
button.selected:disabled { background: #444; -webkit-box-shadow: inset 0 0 1px 1px #111; -moz-box-shadow: inset 0 0 1px 1px #111; -ms-box-shadow: inset 0 0 1px 1px #111; -o-box-shadow: inset 0 0 1px 1px #111; box-shadow: inset 0 0 1px 1px #111; color: #999; border-color: #999; text-shadow: 0 1px 0 #000; }
::-webkit-input-placeholder { color: #aaa; font-style: italic; text-shadow: 0 1px 0 #fff; }
input:-moz-placeholder { color: #aaa; font-style: italic; text-shadow: 0 1px 0 #fff; }
input, textarea { background: #f0f0f0; border: 1px solid #bbb; -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: inset 0 0 1px 1px #f6f6f6; -moz-box-shadow: inset 0 0 1px 1px #f6f6f6; -ms-box-shadow: inset 0 0 1px 1px #f6f6f6; -o-box-shadow: inset 0 0 1px 1px #f6f6f6; box-shadow: inset 0 0 1px 1px #f6f6f6; color: #333; line-height: 1; padding: 4px 4px; text-shadow: 0 1px 0 #fff; }
.messages { width: 400px; height: 500px; position: relative; border: 1px solid #999; margin: 0 auto; }
.messages dl { position: absolute; overflow-y: auto; left: 0; top: 0; height: 445px; width: 280px; padding: 10px; }
.messages dt { clear: left; float: left; line-height: 1; margin-right: 1ex; }
.messages dd { line-height: 1; margin-bottom: 1ex; display: block; }
#messages { display: none; }
.messages form.send { position: absolute; bottom: 0; left: 0; width: 100%; background-color: #ccc; border-top: 1px solid #999; display: none; }
.messages form.send div { padding: 5px; }
.messages form.send input { margin: 0; display: inline-block; margin-left: 31px; width: 262px; }
.messages form.send button { position: absolute; right: 5px; top: 5px; width: 50px; }
.messages form.join { width: 100%; height: 500px; background-color: #ccc }
.messages form.join label { position: absolute; width: 270px; left: 10px; top: 175px; font-weight: bold; }
.messages form.join input { position: absolute; width: 270px; left: 10px; top: 200px; }
.messages form.join button { position: absolute; right: 10px; top: 235px; }
dt.message:after { content: ":"; }
</style>
</head>
<body>
<div class="messages">
<form class="join">
<label for="handle">To join, you will need a name:</label>
<input id="handle" type="text" name="handle" placeholder="Enter your handle here." autofocus>
<button>Join the Conversation</button>
</form>
<dl id="messages"></dl>
<form class="send">
<div>
<button style="position: absolute; left: 5px; top: 5px; height: 26px; width: 26px; text-indent: -10000em;">Target</button>
<input id="message" type="text" name="text" placeholder="Enter your text here..." autofocus>
<button style="position: absolute; right: 36px; top: 5px; width: 50px;" class="default">Send</button>
<button style="position: absolute; right: 5px; top: 5px; height: 26px; width: 26px; text-indent: -10000em;">Settings</button>
</div>
</form>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="jquery.nhpm.js"></script>
<script src="jquery.nhpm-chat.js"></script>
<script type="text/javascript">
if (typeof String.prototype.startsWith != 'function') {
String.prototype.startsWith = function (str){
return this.slice(0, str.length) == str;
};
}
var channel = $.channel();
var chat = null;
$('form.join').submit(function(e){
e.preventDefault();
chat = new Chat(channel, $('#handle').val(), DefaultChatInterface);
$('.join').hide();
$('#messages,.send').show();
return false;
});
$('form.send').submit(function(e){
e.preventDefault();
chat.send($('#message').val());
$('#message').val('');
return false;
});
</script>
</body>
</html>
var Chat = function(channel, handle, UI) {
this.channel = channel;
this.handle = handle;
$(this.channel).on('channel.message', this.process);
this.ui = new UI(this, this.channel);
this.channel.send({s: 'muIC', a: 'join', h: this.handle});
return this;
}
Chat.prototype.process = function(e, data, xhr) {
// Check which subsystem the message is directed to.
if ( data.s != 'muIC' ) return;
// If this is a message for us, prevent other handlers from wasting their time.
e.stopImmediatePropagation();
$(this).trigger('chat.' + data.a, [data, xhr]);
}
Chat.prototype.send = function(text) {
this.channel.send({s: 'muIC', a: 'text', h: this.handle, t: text});
};
var DefaultChatInterface = function(chat, channel) {
this.chat = chat;
this.channel = channel;
this.container = $('#messages');
var self = this;
$(channel).on({
'chat.join': $.proxy(self.join, this),
'chat.part': $.proxy(self.part, this),
'chat.nick': $.proxy(self.nick, this),
'chat.text': $.proxy(self.message, this),
'chat.announce': $.proxy(self.announce, this),
'chat.state': $.proxy(self.state, this)
});
}
DefaultChatInterface.prototype.join = function(e, data, xhr) {
this.printNotice(data.h, 'has joined the channel.', 'join');
this.channel.send({s: 'muIC', a: 'announce', h: this.chat.handle});
}
DefaultChatInterface.prototype.part = function(e, data, xhr) {
this.printNotice(data.h, 'has left the channel.', 'part');
}
DefaultChatInterface.prototype.nick = function(e, data, xhr) {
this.printNotice(data.h, 'is now known as ' + (data.n || "Unknown User") + '.', 'nick');
}
DefaultChatInterface.prototype.message = function(e, data, xhr) {
this.printMessage(data.h, data.t);
}
DefaultChatInterface.prototype.notify = function(e, data, xhr) {
this.printNotice(data.h, 'has joined the channel.');
}
DefaultChatInterface.prototype.announce = function(e, data, xhr) {
// Update known users list.
}
DefaultChatInterface.prototype.state = function(e, data, xhr) {
this.printNotice(data.h, 'has joined the channel.');
}
DefaultChatInterface.prototype.print = function(handle, text, css) {
if ( css === null || css === undefined ) css = '';
$('<dt>'+handle+'</dt><dd>'+text+'</dd>').addClass(css).appendTo(this.container);
this.container[0].scrollTop = this.container[0].scrollHeight;
}
DefaultChatInterface.prototype.printNotice = function(handle, text, css) {
if ( css === null || css === undefined ) css = '';
this.print(handle, text, 'notice ' + css);
}
DefaultChatInterface.prototype.printMessage = function(handle, text, css) {
if ( css === null || css === undefined ) css = '';
this.print(handle, text, 'message ' + css);
}
// A jQuery extension to handle Nginx HTTP Push Module (NHPM) communication.
var Channel = function(options) {
this.settings = jQuery.extend({}, Channel.defaults, options);
this.alive = true;
this.failures = 0;
if ( this.settings.onMessage )
jQuery(this).bind('channel.message', this.settings.onMessage);
if ( this.settings.onError )
jQuery(this).bind('channel.', this.settings.onMessage);
this.listen();
return this;
};
Channel.defaults = {
publish: '/publish',
subscribe: '/subscribe',
channel: 'general',
accept: 'text/plain, application/json',
type: 'json',
retry: 500, // retry after 5 seconds
timeout: 300000 // 5 minutes
};
Channel.prototype.listen = function() {
var self = this;
function closure() {
jQuery.ajax(this.settings.subscribe, {
accept: this.settings.accept,
cache: true,
data: {channel: this.settings.channel},
dataType: this.settings.type,
global: false,
headers: {},
ifModified: true,
type: 'GET',
context: this,
timeout: this.settings.timeout,
}).done(this.success).fail(this.failure).complete(this.done);
}
if ( this.failures ) {
setTimeout(function(){closure.apply(self)}, this.settings.retry);
return;
}
setTimeout(function(){closure.apply(self)}, 0);
}
Channel.prototype.error = function(xhr, status) {
if ( status == 'abort' )
this.alive = false;
else if ( status == 'error' || status == 'parsererror' )
this.failures++;
if ( this.failures > 3 )
this.alive = false;
window.console.log("Failure:", xhr, status);
};
Channel.prototype.success = function(data, status, xhr) {
// Reset failure count.
this.failures = 0;
window.console.log("Success:", data, status, xhr);
$(this).trigger('channel.message', [data, xhr]);
};
Channel.prototype.done = function(xhr, status) {
window.console.log("Done:", status);
// notmodified, error, timeout, abort, parsererror
if ( status != 'success' )
jQuery(this).trigger('channel.' + status, [xhr]);
if ( this.alive )
this.listen()
};
Channel.prototype.send = function(data) {
var encoded = JSON.stringify(data, null, 2);
var url = this.settings.publish + '?' + $.param({channel: this.settings.channel});
return jQuery.ajax(url, {
accept: this.settings.accept,
cache: true,
data: encoded,
global: false,
type: 'POST',
context: this,
mimeType: 'application/json'
});
};
(function($){
$.channel = function(options) {
var channel = new Channel(options);
return channel;
};
})(jQuery);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment