Skip to content

Instantly share code, notes, and snippets.

@peta
Created September 5, 2011 21:03
Show Gist options
  • Save peta/1195916 to your computer and use it in GitHub Desktop.
Save peta/1195916 to your computer and use it in GitHub Desktop.
Proof of concept: Piped AJAX polling
<?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);
}
?>
<!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>
@peta
Copy link
Author

peta commented Sep 5, 2011

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment