8398c9048d
code was modified slightly, so the code differs from the original downloadable 1.9.5 version
482 lines
17 KiB
JavaScript
482 lines
17 KiB
JavaScript
dojo.provide("dojox.cometd.RestChannels");
|
|
|
|
dojo.require("dojox.rpc.Client");
|
|
dojo.requireIf(dojox.data && !!dojox.data.JsonRestStore,"dojox.data.restListener");
|
|
|
|
// Note that cometd _base is _not_ required, this can run standalone, but ifyou want
|
|
// cometd functionality, you must explicitly load/require it elsewhere, and cometd._base
|
|
// MUST be loaded prior to RestChannels ifyou use it.
|
|
|
|
// summary:
|
|
// REST Channels - An HTTP/REST Based approach to Comet transport with full REST messaging
|
|
// semantics
|
|
// REST Channels is a efficient, reliable duplex transport for Comet
|
|
|
|
// description:
|
|
// This can be used:
|
|
// 1. As a cometd transport
|
|
// 2. As an enhancement for the REST RPC service, to enable "live" data (real-time updates directly alter the data in indexes)
|
|
// 2a. With the JsonRestStore (which is driven by the REST RPC service), so this dojo.data has real-time data. Updates can be heard through the dojo.data notification API.
|
|
// 3. As a standalone transport. To use it as a standalone transport looks like this:
|
|
// | dojox.cometd.RestChannels.open();
|
|
// | dojox.cometd.RestChannels.get("/myResource",{callback:function(){
|
|
// | // this is called when the resource is first retrieved and any time the
|
|
// | // resource is changed in the future. This provides a means for retrieving a
|
|
// | // resource and subscribing to it in a single request
|
|
// | });
|
|
// | dojox.cometd.RestChannels.subscribe("/anotherResource",{callback:function(){
|
|
// | // this is called when the resource is changed in the future
|
|
// | });
|
|
// Channels HTTP can be configured to a different delays:
|
|
// | dojox.cometd.RestChannels.defaultInstance.autoReconnectTime = 60000; // reconnect after one minute
|
|
//
|
|
|
|
(function(){
|
|
dojo.declare("dojox.cometd.RestChannels", null, {
|
|
constructor: function(options){
|
|
// summary:
|
|
// Initiates the REST Channels protocol
|
|
// options:
|
|
// Keyword arguments:
|
|
// The *autoSubscribeRoot* parameter:
|
|
// When this is set, all REST service requests that have this
|
|
// prefix will be auto-subscribed. The default is '/' (all REST requests).
|
|
// The *url* parameter:
|
|
// This is the url to connect to for server-sent messages. The default
|
|
// is "/channels".
|
|
// The *autoReconnectTime* parameter:
|
|
// This is amount time to wait to reconnect with a connection is broken
|
|
// The *reloadDataOnReconnect* parameter:
|
|
// This indicates whether RestChannels should re-download data when a connection
|
|
// is restored (value of true), or if it should re-subscribe with retroactive subscriptions
|
|
// (Subscribe-Since header) using HEAD requests (value of false). The
|
|
// default is true.
|
|
dojo.mixin(this,options);
|
|
// If we have a Rest service available and we are auto subscribing, we will augment the Rest service
|
|
if(dojox.rpc.Rest && this.autoSubscribeRoot){
|
|
// override the default Rest handler so we can add subscription requests
|
|
var defaultGet = dojox.rpc.Rest._get;
|
|
var self = this;
|
|
dojox.rpc.Rest._get = function(service, id){
|
|
// when there is a REST get, we will intercept and add our own xhr handler
|
|
var defaultXhrGet = dojo.xhrGet;
|
|
dojo.xhrGet = function(r){
|
|
var autoSubscribeRoot = self.autoSubscribeRoot;
|
|
return (autoSubscribeRoot && r.url.substring(0, autoSubscribeRoot.length) == autoSubscribeRoot) ?
|
|
self.get(r.url,r) : // auto-subscribe
|
|
defaultXhrGet(r); // plain XHR request
|
|
};
|
|
|
|
var result = defaultGet.apply(this,arguments);
|
|
dojo.xhrGet = defaultXhrGet;
|
|
return result;
|
|
};
|
|
}
|
|
if(dojox.data && dojox.data.restListener){
|
|
this.receive = dojox.data.restListener;
|
|
}
|
|
},
|
|
absoluteUrl: function(baseUrl,relativeUrl){
|
|
return new dojo._Url(baseUrl,relativeUrl)+'';
|
|
},
|
|
acceptType: "application/rest+json,application/http;q=0.9,*/*;q=0.7",
|
|
subscriptions: {},
|
|
subCallbacks: {},
|
|
autoReconnectTime: 3000,
|
|
reloadDataOnReconnect: true,
|
|
sendAsJson: false,
|
|
url: '/channels',
|
|
autoSubscribeRoot: '/',
|
|
open: function(){
|
|
// summary:
|
|
// Startup the transport (connect to the "channels" resource to receive updates from the server).
|
|
//
|
|
// description:
|
|
// Note that if there is no connection open, this is automatically called when you do a subscription,
|
|
// it is often not necessary to call this
|
|
//
|
|
this.started = true;
|
|
if(!this.connected){
|
|
this.connectionId = dojox.rpc.Client.clientId;
|
|
var clientIdHeader = this.createdClientId ? 'Client-Id' : 'Create-Client-Id';
|
|
this.createdClientId = true;
|
|
var headers = {Accept:this.acceptType};
|
|
headers[clientIdHeader] = this.connectionId;
|
|
var dfd = dojo.xhrPost({headers:headers, url: this.url, noStatus: true});
|
|
var self = this;
|
|
this.lastIndex = 0;
|
|
var onerror, onprogress = function(data){ // get all the possible event handlers
|
|
if(typeof dojo == 'undefined'){
|
|
return null;// this can be called after dojo is unloaded, just do nothing in that case
|
|
}
|
|
if(xhr && xhr.status > 400){
|
|
return onerror(true);
|
|
}
|
|
if(typeof data == 'string'){
|
|
data = data.substring(self.lastIndex);
|
|
}
|
|
var contentType = xhr && (xhr.contentType || xhr.getResponseHeader("Content-Type")) || (typeof data != 'string' && "already json");
|
|
var error = self.onprogress(xhr,data,contentType);
|
|
if(error){
|
|
if(onerror()){
|
|
return new Error(error);
|
|
}
|
|
}
|
|
if(!xhr || xhr.readyState==4){
|
|
xhr = null;
|
|
if(self.connected){
|
|
self.connected = false;
|
|
self.open();
|
|
}
|
|
}
|
|
return data;
|
|
};
|
|
onerror = function(error){
|
|
if(xhr && xhr.status == 409){
|
|
// a 409 indicates that there is a multiple connections, and we need to poll
|
|
console.log("multiple tabs/windows open, polling");
|
|
self.disconnected();
|
|
return null;
|
|
}
|
|
self.createdClientId = false;
|
|
self.disconnected();
|
|
return error;
|
|
};
|
|
dfd.addCallbacks(onprogress,onerror);
|
|
var xhr = dfd.ioArgs.xhr; // this may not exist if we are not using XHR, but an alternate XHR plugin
|
|
if(xhr){
|
|
// if we are doing a monitorable XHR, we want to listen to streaming events
|
|
xhr.onreadystatechange = function(){
|
|
var responseText;
|
|
try{
|
|
if(xhr.readyState == 3){// only for progress, the deferred object will handle the finished responses
|
|
self.readyState = 3;
|
|
responseText = xhr.responseText;
|
|
}
|
|
} catch(e){
|
|
}
|
|
if(typeof responseText=='string'){
|
|
onprogress(responseText);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if(window.attachEvent){// IE needs a little help with cleanup
|
|
window.attachEvent("onunload",function(){
|
|
self.connected= false;
|
|
if(xhr){
|
|
xhr.abort();
|
|
}
|
|
});
|
|
}
|
|
|
|
this.connected = true;
|
|
}
|
|
},
|
|
_send: function(method,args,data){
|
|
// fire an XHR with appropriate modification for JSON handling
|
|
if(this.sendAsJson){
|
|
// send use JSON Messaging
|
|
args.postBody = dojo.toJson({
|
|
target:args.url,
|
|
method:method,
|
|
content: data,
|
|
params:args.content,
|
|
subscribe:args.headers["Subscribe"]
|
|
});
|
|
args.url = this.url;
|
|
method = "POST";
|
|
}else{
|
|
args.postData = dojo.toJson(data);
|
|
}
|
|
return dojo.xhr(method,args,args.postBody);
|
|
},
|
|
subscribe: function(/*String*/channel, /*dojo.__XhrArgs?*/args){
|
|
// summary:
|
|
// Subscribes to a channel/uri, and returns a dojo.Deferred object for the response from
|
|
// the subscription request
|
|
//
|
|
// channel:
|
|
// the uri for the resource you want to monitor
|
|
//
|
|
// args:
|
|
// See dojo.xhr
|
|
//
|
|
// headers:
|
|
// These are the headers to be applied to the channel subscription request
|
|
//
|
|
// callback:
|
|
// This will be called when a event occurs for the channel
|
|
// The callback will be called with a single argument:
|
|
// | callback(message)
|
|
// where message is an object that follows the XHR API:
|
|
// status : Http status
|
|
// statusText : Http status text
|
|
// getAllResponseHeaders() : The response headers
|
|
// getResponseHeaders(headerName) : Retrieve a header by name
|
|
// responseText : The response body as text
|
|
// with the following additional Bayeux properties
|
|
// data : The response body as JSON
|
|
// channel : The channel/url of the response
|
|
args = args || {};
|
|
args.url = this.absoluteUrl(this.url, channel);
|
|
if(args.headers){
|
|
// FIXME: combining Ranges with notifications is very complicated, we will save that for a future version
|
|
delete args.headers.Range;
|
|
}
|
|
var oldSince = this.subscriptions[channel];
|
|
var method = args.method || "HEAD"; // HEAD is the default for a subscription
|
|
var since = args.since;
|
|
var callback = args.callback;
|
|
var headers = args.headers || (args.headers = {});
|
|
this.subscriptions[channel] = since || oldSince || 0;
|
|
var oldCallback = this.subCallbacks[channel];
|
|
if(callback){
|
|
this.subCallbacks[channel] = oldCallback ? function(m){
|
|
oldCallback(m);
|
|
callback(m);
|
|
} : callback;
|
|
}
|
|
if(!this.connected){
|
|
this.open();
|
|
}
|
|
if(oldSince === undefined || oldSince != since){
|
|
headers["Cache-Control"] = "max-age=0";
|
|
since = typeof since == 'number' ? new Date(since).toUTCString() : since;
|
|
if(since){
|
|
headers["Subscribe-Since"] = since;
|
|
}
|
|
headers["Subscribe"] = args.unsubscribe ? 'none' : '*';
|
|
var dfd = this._send(method,args);
|
|
|
|
var self = this;
|
|
dfd.addBoth(function(result){
|
|
var xhr = dfd.ioArgs.xhr;
|
|
if(!(result instanceof Error)){
|
|
if(args.confirmation){
|
|
args.confirmation();
|
|
}
|
|
}
|
|
if(xhr && xhr.getResponseHeader("Subscribed") == "OK"){
|
|
var lastMod = xhr.getResponseHeader('Last-Modified');
|
|
|
|
if(xhr.responseText){
|
|
self.subscriptions[channel] = lastMod || new Date().toUTCString();
|
|
}else{
|
|
return null; // don't process the response, the response will be received in the main channels response
|
|
}
|
|
}else if(xhr && !(result instanceof Error)){ // if the server response was successful and we have access to headers but it does indicate a subcription was successful, that means it is did not accept the subscription
|
|
delete self.subscriptions[channel];
|
|
}
|
|
if(!(result instanceof Error)){
|
|
var message = {
|
|
responseText:xhr && xhr.responseText,
|
|
channel:channel,
|
|
getResponseHeader:function(name){
|
|
return xhr.getResponseHeader(name);
|
|
},
|
|
getAllResponseHeaders:function(){
|
|
return xhr.getAllResponseHeaders();
|
|
},
|
|
result: result
|
|
};
|
|
if(self.subCallbacks[channel]){
|
|
self.subCallbacks[channel](message); // call with the fake xhr object
|
|
}
|
|
}else{
|
|
if(self.subCallbacks[channel]){
|
|
self.subCallbacks[channel](xhr); // call with the actual xhr object
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
return dfd;
|
|
}
|
|
return null;
|
|
},
|
|
publish: function(channel,data){
|
|
// summary:
|
|
// Publish an event.
|
|
// description:
|
|
// This does a simple POST operation to the provided URL,
|
|
// POST is the semantic equivalent of publishing a message within REST/Channels
|
|
// channel:
|
|
// Channel/resource path to publish to
|
|
// data:
|
|
// data to publish
|
|
return this._send("POST",{url:channel,contentType : 'application/json'},data);
|
|
},
|
|
_processMessage: function(message){
|
|
message.event = message.event || message.getResponseHeader('Event');
|
|
if(message.event=="connection-conflict"){
|
|
return "conflict"; // indicate an error
|
|
}
|
|
try{
|
|
message.result = message.result || dojo.fromJson(message.responseText);
|
|
}
|
|
catch(e){}
|
|
var self = this;
|
|
var loc = message.channel = new dojo._Url(this.url, message.source || message.getResponseHeader('Content-Location'))+'';//for cometd
|
|
if(loc in this.subscriptions && message.getResponseHeader){
|
|
this.subscriptions[loc] = message.getResponseHeader('Last-Modified');
|
|
}
|
|
if(this.subCallbacks[loc]){
|
|
setTimeout(function(){ //give it it's own stack
|
|
self.subCallbacks[loc](message);
|
|
},0);
|
|
}
|
|
this.receive(message);
|
|
return null;
|
|
},
|
|
onprogress: function(xhr,data,contentType){
|
|
// internal XHR progress handler
|
|
if(!contentType || contentType.match(/application\/rest\+json/)){
|
|
var size = data.length;
|
|
data = data.replace(/^\s*[,\[]?/,'['). // must start with a opening bracket
|
|
replace(/[,\]]?\s*$/,']'); // and end with a closing bracket
|
|
try{
|
|
// if this fails, it probably means we have an incomplete JSON object
|
|
var xhrs = dojo.fromJson(data);
|
|
this.lastIndex += size;
|
|
}
|
|
catch(e){
|
|
}
|
|
}else if(dojox.io && dojox.io.httpParse && contentType.match(/application\/http/)){
|
|
// do HTTP tunnel parsing
|
|
var topHeaders = '';
|
|
if(xhr && xhr.getAllResponseHeaders){
|
|
// mixin/inherit headers from the container response
|
|
topHeaders = xhr.getAllResponseHeaders();
|
|
}
|
|
xhrs = dojox.io.httpParse(data,topHeaders,xhr.readyState != 4);
|
|
}else if(typeof data == "object"){
|
|
xhrs = data;
|
|
}
|
|
if(xhrs){
|
|
for(var i = 0;i < xhrs.length;i++){
|
|
if(this._processMessage(xhrs[i])){
|
|
return "conflict";
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
if(!xhr){
|
|
//no streaming and we didn't get any message, must be an error
|
|
return "error";
|
|
}
|
|
if(xhr.readyState != 4){ // we only want finished responses here if we are not streaming
|
|
return null;
|
|
}
|
|
if(xhr.__proto__){// firefox uses this property, so we create an instance to shadow this property
|
|
xhr = {channel:"channel",__proto__:xhr};
|
|
}
|
|
return this._processMessage(xhr);
|
|
|
|
},
|
|
|
|
get: function(/*String*/channel, /*dojo.__XhrArgs?*/args){
|
|
// summary:
|
|
// GET the initial value of the resource and subscribe to it
|
|
// See subscribe for parameter values
|
|
(args = args || {}).method = "GET";
|
|
return this.subscribe(channel,args);
|
|
},
|
|
receive: function(message){
|
|
// summary:
|
|
// Called when a message is received from the server
|
|
// message:
|
|
// A cometd/XHR message
|
|
},
|
|
disconnected: function(){
|
|
// summary:
|
|
// called when our channel gets disconnected
|
|
var self = this;
|
|
if(this.connected){
|
|
this.connected = false;
|
|
if(this.started){ // if we are started, we shall try to reconnect
|
|
setTimeout(function(){ // auto reconnect
|
|
// resubscribe to our current subscriptions
|
|
var subscriptions = self.subscriptions;
|
|
self.subscriptions = {};
|
|
for(var i in subscriptions){
|
|
if(self.reloadDataOnReconnect && dojox.rpc.JsonRest){
|
|
// do a reload of the resource
|
|
delete dojox.rpc.Rest._index[i];
|
|
dojox.rpc.JsonRest.fetch(i);
|
|
}else{
|
|
self.subscribe(i,{since:subscriptions[i]});
|
|
}
|
|
}
|
|
self.open();
|
|
}, this.autoReconnectTime);
|
|
}
|
|
}
|
|
},
|
|
unsubscribe: function(/*String*/channel, /*dojo.__XhrArgs?*/args){
|
|
// summary:
|
|
// unsubscribes from the resource
|
|
// See subscribe for parameter values
|
|
|
|
args = args || {};
|
|
args.unsubscribe = true;
|
|
this.subscribe(channel,args); // change the time frame to after 5000AD
|
|
},
|
|
disconnect: function(){
|
|
// summary:
|
|
// disconnect from the server
|
|
this.started = false;
|
|
this.xhr.abort();
|
|
}
|
|
});
|
|
var Channels = dojox.cometd.RestChannels.defaultInstance = new dojox.cometd.RestChannels();
|
|
if(dojox.cometd.connectionTypes){
|
|
// register as a dojox.cometd transport and wire everything for cometd handling
|
|
// below are the necessary adaptions for cometd
|
|
Channels.startup = function(data){ // must be able to handle objects or strings
|
|
Channels.open();
|
|
this._cometd._deliver({channel:"/meta/connect",successful:true}); // tell cometd we are connected so it can proceed to send subscriptions, even though we aren't yet
|
|
|
|
};
|
|
Channels.check = function(types, version, xdomain){
|
|
for(var i = 0; i< types.length; i++){
|
|
if(types[i] == "rest-channels"){
|
|
return !xdomain;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
Channels.deliver = function(message){
|
|
// nothing to do
|
|
};
|
|
dojo.connect(this,"receive",null,function(message){
|
|
message.data = message.result;
|
|
this._cometd._deliver(message);
|
|
});
|
|
Channels.sendMessages = function(messages){
|
|
for(var i = 0; i < messages.length; i++){
|
|
var message = messages[i];
|
|
var channel = message.channel;
|
|
var cometd = this._cometd;
|
|
var args = {
|
|
confirmation: function(){ // send a confirmation back to cometd
|
|
cometd._deliver({channel:channel,successful:true});
|
|
}
|
|
};
|
|
if(channel == '/meta/subscribe'){
|
|
this.subscribe(message.subscription,args);
|
|
}else if(channel == '/meta/unsubscribe'){
|
|
this.unsubscribe(message.subscription,args);
|
|
}else if(channel == '/meta/connect'){
|
|
args.confirmation();
|
|
}else if(channel == '/meta/disconnect'){
|
|
Channels.disconnect();
|
|
args.confirmation();
|
|
}else if(channel.substring(0,6) != '/meta/'){
|
|
this.publish(channel,message.data);
|
|
}
|
|
}
|
|
};
|
|
dojox.cometd.connectionTypes.register("rest-channels", Channels.check, Channels,false,true);
|
|
}
|
|
})();
|