1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
|
/*
Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved.
Available via Academic Free License >= 2.1 OR the modified BSD license.
see: http://dojotoolkit.org/license for details
*/
dojo._xdResourceLoaded(function(dojo, dijit, dojox){
return {depends: [["provide", "dojox.cometd.RestChannels"],
["require", "dojox.rpc.Client"],
["requireIf", dojox.data && !!dojox.data.JsonRestStore,"dojox.data.restListener"]],
defineResource: function(dojo, dijit, dojox){if(!dojo._hasResource["dojox.cometd.RestChannels"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojox.cometd.RestChannels"] = true;
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;
};
}
},
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.postData = 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.postData);
},
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
if(dojox.data && dojox.data.restListener){
dojox.data.restListener(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);
}
})();
}
}};});
|