Created
September 5, 2011 21:03
-
-
Save peta/1195916 to your computer and use it in GitHub Desktop.
Proof of concept: Piped AJAX polling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// This script's purpose is to echo some distinguishable dummy values | |
// Dummy values | |
$known_resources = array( | |
'resource1' => 'News', | |
'resource2' => 'Status', | |
'resource3' => 'Images', | |
'resource4' => 'Messages', | |
'news1' => '1. Sports', | |
'news2' => '2. Politics', | |
'news3' => '3. Economy', | |
'news4' => '4. Glamour', | |
'news5' => '5. IT' | |
); | |
if ($_SERVER['REQUEST_METHOD'] === 'POST') { | |
$resp = array( | |
'type' => 'ajaxpipe-response', | |
'version' => 0.1, | |
'resources' => array() | |
); | |
$input = json_decode(stripslashes($_POST['data'])); | |
if (NULL !== $input AND isset($input->payload) AND is_array($input->payload)) { | |
foreach ($input->payload as $resource) { | |
$resp['resources'][$resource] = '<p>'.$known_resources[$resource].' ('.time().')</p>'; | |
} | |
} | |
header('Content-Type: appplication/json; charset=utf-8'); | |
echo json_encode($resp); | |
} | |
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | |
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> | |
<title>Tryout: Piped AJAX</title> | |
<style type="text/css" media="screen"> | |
div { | |
outline: 2px solid blue; | |
padding: 10px; | |
margin: 20px; | |
position: relative; | |
} | |
#content, #header, #footer { | |
margin-right: 14%; | |
} | |
#sidebar { bottom: 0; | |
position: absolute; | |
right: 0; | |
top: 0; | |
width: 10%; | |
} | |
#content > div { | |
float: left; | |
margin-right: 10px; | |
width: 20%; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="page"> | |
<div id="header"> | |
</div> | |
<div id="content"> | |
<div id="section1" class="ajaxpipe" data-pipe="[500,'news1','replace']"></div> | |
<div id="section2" class="ajaxpipe" data-pipe="[1000,'news2','append']"></div> | |
<div id="section3" class="ajaxpipe" data-pipe="[1500,'news3','replace']"></div> | |
<div id="section4" class="ajaxpipe" data-pipe="[4000,'news4','append']"></div> | |
<p style="clear:both;"></p> | |
</div> | |
<div id="sidebar" class="ajaxpipe" data-pipe="[null,'resource4','append']"> | |
</div> | |
<div id="footer"> | |
</div> | |
</div> | |
<script type="text/javascript" charset="utf-8" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.js"></script> | |
<script type="text/javascript" charset="utf-8"> | |
(function($) { | |
function AjaxPipe(int) { | |
// Factory pattern | |
if (window === this) { | |
var inst = AjaxPipe.prototype._findByInterval(int); | |
return (null === inst) ? new AjaxPipe(int) : inst; | |
} | |
// Setup instance | |
if ((int = ~~int) < 100) throw new RangeError('Invalid interval value'); | |
this._interval = int; | |
this._wires = {}; | |
this._resourceIdx = {}; | |
this._resourceCache = []; | |
this._boundPoll = this._poll.bind(this); | |
AjaxPipe.prototype._pipeIdx[''+int] = this; | |
console.info('Created pipe for interval "'+int+'"', this); | |
this._boundPoll(); | |
} | |
AjaxPipe.prototype = { | |
constructor: AjaxPipe | |
, _guid: 0 | |
, _pipeIdx: {} | |
, _findByInterval: function(int) { | |
return AjaxPipe.prototype._pipeIdx[''+int] || null; | |
} | |
, _notify: function(resp, status) { | |
var data = JSON.parse(resp.responseText); | |
if (status === 'success') { | |
var resIdx = this._resourceIdx | |
, res = data.resources | |
, wireIds; | |
//for (var i=0, j=res.length, wireIds; i < j; i++) { | |
for (var r in res) { | |
wireIds = resIdx[r]; | |
// Notify all receivers | |
for (var l=0, m=wireIds.length, wire; l < m; l++) { | |
wire = this._wires[wireIds[l]]; | |
wire[2].call(wire[0], res[r]); | |
} | |
} | |
} | |
setTimeout(this._boundPoll, this._interval); | |
} | |
, _poll: function() { | |
$.ajax({ | |
url: 'pipe-ajax-endpoint.php', | |
dataType: 'json', | |
type: 'POST', | |
data: { | |
data: JSON.stringify({ | |
type: 'ajaxpipe', | |
version: 0.1, | |
payload: this._resourceCache | |
}) | |
}, | |
context: this, | |
complete: this._notify | |
}); | |
} | |
, _rebuildCache: function() { | |
var cache = []; | |
this._resourceCache = null; | |
for (var r in this._resourceIdx) cache.push(r); | |
this._resourceCache = cache; | |
} | |
, add: function(elem, resource, method) { | |
// I assume that one element will be part of only one pipe at a time | |
if (true === $.data(elem, 'ap-piped')) return; | |
var id = ++this._guid+''; | |
this._wires[id] = [elem, resource, method]; | |
// Create entry in resource index | |
var idxEntry = this._resourceIdx[resource] || []; | |
if (-1 === idxEntry.indexOf(id)) idxEntry.push(id); | |
this._resourceIdx[resource] = idxEntry; | |
// Add element-specific data | |
$.data(elem, 'ap-piped', true); | |
$.data(elem, 'ap-interval', this._interval); | |
$.data(elem, 'ap-id', id); | |
this._rebuildCache(); | |
} | |
, remove: function(elem /*, resource, method*/) { | |
var id = ''+$.data(elem, 'ap-id'); | |
if (undefined === id && false === $.data(elem, 'ap-piped')) return; | |
// Remove it (naive strategy) | |
var resource = this._wires[id][1]; | |
this._resourceIdx[resource].splice(idxEntry.indexOf(id), 1); | |
delete this._wires[id]; | |
// and its elem-specific data | |
$.removeData(elem, 'ap-piped'); | |
$.removeData(elem, 'ap-interval'); | |
$.removeData(elem, 'ap-id'); | |
this._rebuildCache(); | |
} | |
}; | |
var payloadMethods = { | |
append: function(content) { | |
$(this).append(content); | |
//console.log('Payload method: "append"', this, arguments); | |
}, | |
replace: function(content) { | |
this.innerHTML = content; | |
//console.log('Payload method: "replace"', this, arguments); | |
} | |
} | |
, pipeMethods = ['add', 'remove'] | |
, defaults = { | |
interval: 5000, | |
cb: 'append' | |
}; | |
$.fn.ajaxPipe = function(method, opts) { | |
if (-1 === pipeMethods.indexOf(method)) throw new RangeError('Invalid pipe method "'+method+'"'); | |
var resource = ''+(opts.resource || ''); | |
if (0 === resource.length) throw new RangeError('Invalid resource value'); | |
var interval = ~~opts.interval || defaults.interval; | |
var plMethod = (typeof opts.cb === 'function') ? opts.cb : payloadMethods[''+opts.cb]; | |
if (typeof plMethod !== 'function') throw new RangeError('Invalid pipe callback'); | |
var pipe = AjaxPipe(interval); | |
for (var i=0, j=this.length; i < j; i++) pipe[method](this[0], resource, plMethod); | |
return this; | |
}; | |
// TODO: Add listener for DOM mutation events and handle them (remove removed elems from pipes) | |
$(document).ready(function() { | |
// Create wires for elems with inline declarations | |
$('.ajaxpipe').each(function() { | |
try { | |
var t = $(this), opts = t.attr('data-pipe').replace(/'/igm, '"'); | |
opts = JSON.parse('{"opts":'+opts+'}').opts; | |
t.ajaxPipe('add', { | |
interval: opts[0], | |
resource: opts[1], | |
cb: opts[2] | |
}); | |
} catch (exc) { /* Fault of user */ } | |
}); | |
}); | |
})(jQuery); | |
// Hook it up | |
$(document).ready(function() { | |
// Add manually | |
console.log('Added to AJAX pipe: ', | |
$('#footer').ajaxPipe('add', { | |
resource: 'resource3', | |
cb: 'append' | |
})); | |
console.log('Added to AJAX pipe: ', | |
$('#header').ajaxPipe('add', { | |
resource: 'resource2' | |
, cb: function(content) { | |
$(this).hide().html(content).fadeIn(); | |
} | |
})); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A proof of concept for simple piped AJAX polling. When a page consists of several (independant) components that should be automatically updated in a given interval, we could issue separate XHRs everytime a component needs to be updated and risk unresponsive behaviour, caused by a dozen of pending connections hanging around or we look for a way to "bundle" them -- in my proof of concept I call such a bundle an "AjaxPipe". When no WebSockets are available and/or the Comet stack is too overkill, AjaxPipe might be useful. Each pipe has a given interval after which it bundles all outstanding resource polls in a single XHR and posts it to the server. After the response arrived, all elements (called "wires") are updated automatically, whereas the exact method (what to do with the received payload) can be specified individually. Thus we're reducing the amount of active connections on both, the server and the client side.