Subversion Repositories Applications.papyrus

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
2150 mathias 1
if(!dojo._hasResource["dojox._cometd.cometd"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2
dojo._hasResource["dojox._cometd.cometd"] = true;
3
dojo.provide("dojox._cometd.cometd");
4
dojo.require("dojo.AdapterRegistry");
5
dojo.require("dojo.io.script");
6
 
7
// FIXME: need to add local topic support to advise about:
8
//		successful handshake
9
//		network failure of channel
10
//		graceful disconnect
11
 
12
/*
13
 * this file defines Comet protocol client. Actual message transport is
14
 * deferred to one of several connection type implementations. The default is a
15
 * long-polling implementation. A single global object named "dojox.cometd" is
16
 * used to mediate for these connection types in order to provide a stable
17
 * interface.
18
 */
19
 
20
dojox.cometd = new function(){
21
 
22
	/* cometd states:
23
 	* DISCONNECTED:  _initialized==false && _connected==false
24
 	* CONNECTING:    _initialized==true  && _connected==false (handshake sent)
25
 	* CONNECTED:     _initialized==true  && _connected==true  (first successful connect)
26
 	* DISCONNECTING: _initialized==false && _connected==true  (disconnect sent)
27
 	*/
28
	this._initialized = false;
29
	this._connected = false;
30
	this._polling = false;
31
 
32
	this.connectionTypes = new dojo.AdapterRegistry(true);
33
 
34
	this.version = "1.0";
35
	this.minimumVersion = "0.9";
36
	this.clientId = null;
37
	this.messageId = 0;
38
	this.batch=0;
39
 
40
	this._isXD = false;
41
	this.handshakeReturn = null;
42
	this.currentTransport = null;
43
	this.url = null;
44
	this.lastMessage = null;
45
	this.topics = {};
46
	this._messageQ = [];
47
	this.handleAs="json-comment-optional";
48
	this.advice;
49
	this.pendingSubscriptions = {}
50
	this.pendingUnsubscriptions = {}
51
 
52
	this._subscriptions = [];
53
 
54
	this.tunnelInit = function(childLocation, childDomain){
55
		// placeholder
56
	}
57
 
58
	this.tunnelCollapse = function(){
59
		console.debug("tunnel collapsed!");
60
		// placeholder
61
	}
62
 
63
	this.init = function(root, props, bargs){
64
		// FIXME: if the root isn't from the same host, we should automatically
65
		// try to select an XD-capable transport
66
		props = props||{};
67
		// go ask the short bus server what we can support
68
		props.version = this.version;
69
		props.minimumVersion = this.minimumVersion;
70
		props.channel = "/meta/handshake";
71
		props.id = ""+this.messageId++;
72
 
73
		this.url = root||djConfig["cometdRoot"];
74
		if(!this.url){
75
			console.debug("no cometd root specified in djConfig and no root passed");
76
			return;
77
		}
78
 
79
		// Are we x-domain? borrowed from dojo.uri.Uri in lieu of fixed host and port properties
80
		var regexp = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$";
81
		var r = (""+window.location).match(new RegExp(regexp));
82
		if(r[4]){
83
			var tmp = r[4].split(":");
84
			var thisHost = tmp[0];
85
			var thisPort = tmp[1]||"80"; // FIXME: match 443
86
 
87
			r = this.url.match(new RegExp(regexp));
88
			if(r[4]){
89
				tmp = r[4].split(":");
90
				var urlHost = tmp[0];
91
				var urlPort = tmp[1]||"80";
92
				this._isXD = ((urlHost != thisHost)||(urlPort != thisPort));
93
			}
94
		}
95
 
96
		if(!this._isXD){
97
			if(props.ext){
98
				if(props.ext["json-comment-filtered"]!==true && props.ext["json-comment-filtered"]!==false){
99
					props.ext["json-comment-filtered"] = true;
100
				}
101
			}else{
102
				props.ext = { "json-comment-filtered": true };
103
			}
104
		}
105
 
106
		var bindArgs = {
107
			url: this.url,
108
			handleAs: this.handleAs,
109
			content: { "message": dojo.toJson([props]) },
110
			load: dojo.hitch(this, "finishInit"),
111
			error: function(e){ console.debug("handshake error!:", e); }
112
		};
113
 
114
		if(bargs){
115
			dojo.mixin(bindArgs, bargs);
116
		}
117
		this._props=props;
118
		this._initialized=true;
119
		this.batch=0;
120
		this.startBatch();
121
 
122
		// if xdomain, then we assume jsonp for handshake
123
		if(this._isXD){
124
			bindArgs.callbackParamName="jsonp";
125
			return dojo.io.script.get(bindArgs);
126
		}
127
		return dojo.xhrPost(bindArgs);
128
	}
129
 
130
	this.finishInit = function(data){
131
		data = data[0];
132
		this.handshakeReturn = data;
133
 
134
		// pick a transport
135
		if(data["advice"]){
136
			this.advice = data.advice;
137
		}
138
 
139
       		if(!data.successful){
140
			console.debug("cometd init failed");
141
			if(this.advice && this.advice["reconnect"]=="none"){
142
				return;
143
			}
144
 
145
			if( this.advice && this.advice["interval"] && this.advice.interval>0 ){
146
				var cometd=this;
147
				setTimeout(function(){ cometd.init(cometd.url,cometd._props); }, this.advice.interval);
148
			}else{
149
				this.init(this.url,this._props);
150
			}
151
 
152
			return;
153
		}
154
		if(data.version < this.minimumVersion){
155
			console.debug("cometd protocol version mismatch. We wanted", this.minimumVersion, "but got", data.version);
156
			return;
157
		}
158
		this.currentTransport = this.connectionTypes.match(
159
			data.supportedConnectionTypes,
160
			data.version,
161
			this._isXD
162
		);
163
		this.currentTransport._cometd = this;
164
		this.currentTransport.version = data.version;
165
		this.clientId = data.clientId;
166
		this.tunnelInit = dojo.hitch(this.currentTransport, "tunnelInit");
167
		this.tunnelCollapse = dojo.hitch(this.currentTransport, "tunnelCollapse");
168
 
169
		this.currentTransport.startup(data);
170
	}
171
 
172
	// public API functions called by cometd or by the transport classes
173
	this.deliver = function(messages){
174
		// console.debug(messages);
175
		dojo.forEach(messages, this._deliver, this);
176
		return messages;
177
	}
178
 
179
	this._deliver = function(message){
180
		// dipatch events along the specified path
181
 
182
		if(!message["channel"]){
183
			if(message["success"] !== true){
184
				console.debug("cometd error: no channel for message!", message);
185
				return;
186
			}
187
		}
188
		this.lastMessage = message;
189
 
190
		if(message.advice){
191
			this.advice = message.advice; // TODO maybe merge?
192
		}
193
 
194
		// check to see if we got a /meta channel message that we care about
195
		if(	(message["channel"]) &&
196
			(message.channel.length > 5)&&
197
			(message.channel.substr(0, 5) == "/meta")){
198
			// check for various meta topic actions that we need to respond to
199
			switch(message.channel){
200
				case "/meta/connect":
201
					if(message.successful && !this._connected){
202
						this._connected = this._initialized;
203
						this.endBatch();
204
					} else if(!this._initialized){
205
						this._connected = false; // finish disconnect
206
					}
207
					break;
208
				case "/meta/subscribe":
209
					var pendingDef = this.pendingSubscriptions[message.subscription];
210
					if(!message.successful){
211
						if(pendingDef){
212
							pendingDef.errback(new Error(message.error));
213
							delete this.pendingSubscriptions[message.subscription];
214
						}
215
						return;
216
					}
217
					dojox.cometd.subscribed(message.subscription, message);
218
					if(pendingDef){
219
						pendingDef.callback(true);
220
						delete this.pendingSubscriptions[message.subscription];
221
					}
222
					break;
223
				case "/meta/unsubscribe":
224
					var pendingDef = this.pendingUnsubscriptions[message.subscription];
225
					if(!message.successful){
226
						if(pendingDef){
227
							pendingDef.errback(new Error(message.error));
228
							delete this.pendingUnsubscriptions[message.subscription];
229
						}
230
						return;
231
					}
232
					this.unsubscribed(message.subscription, message);
233
					if(pendingDef){
234
						pendingDef.callback(true);
235
						delete this.pendingUnsubscriptions[message.subscription];
236
					}
237
					break;
238
			}
239
		}
240
 
241
		// send the message down for processing by the transport
242
		this.currentTransport.deliver(message);
243
 
244
		if(message.data){
245
			// dispatch the message to any locally subscribed listeners
246
			var tname = "/cometd"+message.channel;
247
			dojo.publish(tname, [ message ]);
248
		}
249
	}
250
 
251
	this.disconnect = function(){
252
		dojo.forEach(this._subscriptions, dojo.unsubscribe);
253
		this._subscriptions = [];
254
		this._messageQ = [];
255
		if(this._initialized && this.currentTransport){
256
			this._initialized=false;
257
			this.currentTransport.disconnect();
258
		}
259
		this._initialized=false;
260
		if(!this._polling)
261
			this._connected=false;
262
	}
263
 
264
	// public API functions called by end users
265
	this.publish = function(/*string*/channel, /*object*/data, /*object*/properties){
266
		// summary:
267
		//		publishes the passed message to the cometd server for delivery
268
		//		on the specified topic
269
		// channel:
270
		//		the destination channel for the message
271
		// data:
272
		//		a JSON object containing the message "payload"
273
		// properties:
274
		//		Optional. Other meta-data to be mixed into the top-level of the
275
		//		message
276
		var message = {
277
			data: data,
278
			channel: channel
279
		};
280
		if(properties){
281
			dojo.mixin(message, properties);
282
		}
283
		this._sendMessage(message);
284
	}
285
 
286
	this._sendMessage = function(/* object */ message){
287
		if(this.currentTransport && this._connected && this.batch==0){
288
			return this.currentTransport.sendMessages([message]);
289
		}
290
		else{
291
			this._messageQ.push(message);
292
		}
293
	}
294
 
295
	this.subscribe = function(	/*string */					channel,
296
								/*object, optional*/	     objOrFunc,
297
								/*string, optional*/	     funcName){ // return: boolean
298
		// summary:
299
		//		inform the server of this client's interest in channel
300
		// channel:
301
		//		name of the cometd channel to subscribe to
302
		// objOrFunc:
303
		//		an object scope for funcName or the name or reference to a
304
		//		function to be called when messages are delivered to the
305
		//		channel
306
		// funcName:
307
		//		the second half of the objOrFunc/funcName pair for identifying
308
		//		a callback function to notifiy upon channel message delivery
309
 
310
		if(this.pendingSubscriptions[channel]){
311
			// We already asked to subscribe to this channel, and
312
			// haven't heard back yet. Fail the previous attempt.
313
			var oldDef = this.pendingSubscriptions[channel];
314
			oldDef.cancel();
315
			delete this.pendingSubscriptions[channel];
316
		}
317
 
318
		var pendingDef = new dojo.Deferred();
319
		this.pendingSubscriptions[channel] = pendingDef;
320
 
321
		if(objOrFunc){
322
			var tname = "/cometd"+channel;
323
			if(this.topics[tname]){
324
				dojo.unsubscribe(this.topics[tname]);
325
			}
326
			var topic = dojo.subscribe(tname, objOrFunc, funcName);
327
			this.topics[tname] = topic;
328
		}
329
 
330
		this._sendMessage({
331
			channel: "/meta/subscribe",
332
			subscription: channel
333
		});
334
 
335
		return pendingDef;
336
 
337
	}
338
 
339
	this.subscribed = function(	/*string*/  channel,
340
								/*obj*/     message){
341
 	}
342
 
343
 
344
	this.unsubscribe = function(/*string*/			channel){ // return: boolean
345
		// summary:
346
		//		inform the server of this client's disinterest in channel
347
		// channel:
348
		//		name of the cometd channel to unsubscribe from
349
 
350
		if(this.pendingUnsubscriptions[channel]){
351
			// We already asked to unsubscribe from this channel, and
352
			// haven't heard back yet. Fail the previous attempt.
353
			var oldDef = this.pendingUnsubscriptions[channel];
354
			oldDef.cancel();
355
			delete this.pendingUnsubscriptions[channel];
356
		}
357
 
358
		var pendingDef = new dojo.Deferred();
359
		this.pendingUnsubscriptions[channel] = pendingDef;
360
 
361
		var tname = "/cometd"+channel;
362
		if(this.topics[tname]){
363
			dojo.unsubscribe(this.topics[tname]);
364
		}
365
 
366
		this._sendMessage({
367
			channel: "/meta/unsubscribe",
368
			subscription: channel
369
		});
370
 
371
		return pendingDef;
372
 
373
	}
374
 
375
	this.unsubscribed = function(	/*string*/  channel,
376
					/*obj*/     message){
377
	}
378
 
379
	this.startBatch = function(){
380
		this.batch++;
381
	}
382
 
383
	this.endBatch = function(){
384
		if(--this.batch <= 0 && this.currentTransport && this._connected){
385
			this.batch=0;
386
 
387
			var messages=this._messageQ;
388
			this._messageQ=[];
389
			if(messages.length>0){
390
				this.currentTransport.sendMessages(messages);
391
			}
392
		}
393
	}
394
 
395
	this._onUnload = function(){
396
		// make this the last of the onUnload method
397
		dojo.addOnUnload(dojox.cometd,"disconnect");
398
	}
399
}
400
 
401
/*
402
transport objects MUST expose the following methods:
403
	- check
404
	- startup
405
	- sendMessages
406
	- deliver
407
	- disconnect
408
optional, standard but transport dependent methods are:
409
	- tunnelCollapse
410
	- tunnelInit
411
 
412
Transports SHOULD be namespaced under the cometd object and transports MUST
413
register themselves with cometd.connectionTypes
414
 
415
here's a stub transport defintion:
416
 
417
cometd.blahTransport = new function(){
418
	this._connectionType="my-polling";
419
	this._cometd=null;
420
	this.lastTimestamp = null;
421
 
422
	this.check = function(types, version, xdomain){
423
		// summary:
424
		//		determines whether or not this transport is suitable given a
425
		//		list of transport types that the server supports
426
		return dojo.lang.inArray(types, "blah");
427
	}
428
 
429
	this.startup = function(){
430
		if(dojox.cometd._polling){ return; }
431
		// FIXME: fill in startup routine here
432
		dojox.cometd._polling = true;
433
	}
434
 
435
	this.sendMessages = function(message){
436
		// FIXME: fill in message array sending logic
437
	}
438
 
439
	this.deliver = function(message){
440
		if(message["timestamp"]){
441
			this.lastTimestamp = message.timestamp;
442
		}
443
		if(	(message.channel.length > 5)&&
444
			(message.channel.substr(0, 5) == "/meta")){
445
			// check for various meta topic actions that we need to respond to
446
			// switch(message.channel){
447
			// 	case "/meta/connect":
448
			//		// FIXME: fill in logic here
449
			//		break;
450
			//	// case ...: ...
451
			//	}
452
		}
453
	}
454
 
455
	this.disconnect = function(){
456
	}
457
}
458
cometd.connectionTypes.register("blah", cometd.blahTransport.check, cometd.blahTransport);
459
*/
460
 
461
dojox.cometd.longPollTransport = new function(){
462
	this._connectionType="long-polling";
463
	this._cometd=null;
464
	this.lastTimestamp = null;
465
 
466
	this.check = function(types, version, xdomain){
467
		return ((!xdomain)&&(dojo.indexOf(types, "long-polling") >= 0));
468
	}
469
 
470
	this.tunnelInit = function(){
471
		if(this._cometd._polling){ return; }
472
		this.openTunnelWith({
473
			message: dojo.toJson([
474
				{
475
					channel:	"/meta/connect",
476
					clientId:	this._cometd.clientId,
477
					connectionType: this._connectionType,
478
					id:		""+this._cometd.messageId++
479
				}
480
			])
481
		});
482
	}
483
 
484
	this.tunnelCollapse = function(){
485
		if(!this._cometd._polling){
486
			// try to restart the tunnel
487
			this._cometd._polling = false;
488
 
489
			// TODO handle transport specific advice
490
 
491
			if(this._cometd["advice"]){
492
				if(this._cometd.advice["reconnect"]=="none"){
493
					return;
494
				}
495
 
496
				if(	(this._cometd.advice["interval"])&&
497
					(this._cometd.advice.interval>0) ){
498
					var transport = this;
499
					setTimeout(function(){ transport._connect(); },
500
						this._cometd.advice.interval);
501
				}else{
502
					this._connect();
503
				}
504
			}else{
505
				this._connect();
506
			}
507
		}
508
	}
509
 
510
	this._connect = function(){
511
		if(	(this._cometd["advice"])&&
512
			(this._cometd.advice["reconnect"]=="handshake")
513
		){
514
			this._cometd.init(this._cometd.url,this._cometd._props);
515
 		}else if(this._cometd._connected){
516
			this.openTunnelWith({
517
				message: dojo.toJson([
518
					{
519
						channel:	"/meta/connect",
520
						connectionType: this._connectionType,
521
						clientId:	this._cometd.clientId,
522
						timestamp:	this.lastTimestamp,
523
						id:		""+this._cometd.messageId++
524
					}
525
				])
526
			});
527
		}
528
	}
529
 
530
	this.deliver = function(message){
531
		// console.debug(message);
532
		if(message["timestamp"]){
533
			this.lastTimestamp = message.timestamp;
534
		}
535
	}
536
 
537
	this.openTunnelWith = function(content, url){
538
		// console.debug("openTunnelWith:", content, (url||cometd.url));
539
		var d = dojo.xhrPost({
540
			url: (url||this._cometd.url),
541
			content: content,
542
			handleAs: this._cometd.handleAs,
543
			load: dojo.hitch(this, function(data){
544
				// console.debug(evt.responseText);
545
				// console.debug(data);
546
				this._cometd._polling = false;
547
				this._cometd.deliver(data);
548
				this.tunnelCollapse();
549
			}),
550
			error: function(err){
551
				console.debug("tunnel opening failed:", err);
552
				dojo.cometd._polling = false;
553
 
554
				// TODO - follow advice to reconnect or rehandshake?
555
			}
556
		});
557
		this._cometd._polling = true;
558
	}
559
 
560
	this.sendMessages = function(messages){
561
		for(var i=0; i<messages.length; i++){
562
			messages[i].clientId = this._cometd.clientId;
563
			messages[i].id = ""+this._cometd.messageId++;
564
		}
565
		return dojo.xhrPost({
566
			url: this._cometd.url||djConfig["cometdRoot"],
567
			handleAs: this._cometd.handleAs,
568
			load: dojo.hitch(this._cometd, "deliver"),
569
			content: {
570
				message: dojo.toJson(messages)
571
			}
572
		});
573
	}
574
 
575
	this.startup = function(handshakeData){
576
		if(this._cometd._connected){ return; }
577
		this.tunnelInit();
578
	}
579
 
580
	this.disconnect = function(){
581
		dojo.xhrPost({
582
			url: this._cometd.url||djConfig["cometdRoot"],
583
			handleAs: this._cometd.handleAs,
584
			content: {
585
				message: dojo.toJson([{
586
					channel:	"/meta/disconnect",
587
					clientId:	this._cometd.clientId,
588
					id:		""+this._cometd.messageId++
589
				}])
590
			}
591
		});
592
	}
593
}
594
 
595
dojox.cometd.callbackPollTransport = new function(){
596
	this._connectionType = "callback-polling";
597
	this._cometd = null;
598
	this.lastTimestamp = null;
599
 
600
	this.check = function(types, version, xdomain){
601
		// we handle x-domain!
602
		return (dojo.indexOf(types, "callback-polling") >= 0);
603
	}
604
 
605
	this.tunnelInit = function(){
606
		if(this._cometd._polling){ return; }
607
		this.openTunnelWith({
608
			message: dojo.toJson([
609
				{
610
					channel:	"/meta/connect",
611
					clientId:	this._cometd.clientId,
612
					connectionType: this._connectionType,
613
					id:		""+this._cometd.messageId++
614
				}
615
			])
616
		});
617
	}
618
 
619
	this.tunnelCollapse = dojox.cometd.longPollTransport.tunnelCollapse;
620
	this._connect = dojox.cometd.longPollTransport._connect;
621
	this.deliver = dojox.cometd.longPollTransport.deliver;
622
 
623
	this.openTunnelWith = function(content, url){
624
		// create a <script> element to generate the request
625
		dojo.io.script.get({
626
			load: dojo.hitch(this, function(data){
627
				this._cometd._polling = false;
628
				this._cometd.deliver(data);
629
				this.tunnelCollapse();
630
			}),
631
			error: function(){
632
				this._cometd._polling = false;
633
				console.debug("tunnel opening failed");
634
			},
635
			url: (url||this._cometd.url),
636
			content: content,
637
			callbackParamName: "jsonp"
638
		});
639
		this._cometd._polling = true;
640
	}
641
 
642
	this.sendMessages = function(/*array*/ messages){
643
		for(var i=0; i<messages.length; i++){
644
			messages[i].clientId = this._cometd.clientId;
645
			messages[i].id = ""+this._cometd.messageId++;
646
		}
647
		var bindArgs = {
648
			url: this._cometd.url||djConfig["cometdRoot"],
649
			load: dojo.hitch(this._cometd, "deliver"),
650
			callbackParamName: "jsonp",
651
			content: { message: dojo.toJson( messages ) }
652
		};
653
		return dojo.io.script.get(bindArgs);
654
	}
655
 
656
	this.startup = function(handshakeData){
657
		if(this._cometd._connected){ return; }
658
		this.tunnelInit();
659
	}
660
 
661
	this.disconnect = dojox.cometd.longPollTransport.disconnect;
662
 
663
 
664
	this.disconnect = function(){
665
		dojo.io.script.get({
666
			url: this._cometd.url||djConfig["cometdRoot"],
667
			callbackParamName: "jsonp",
668
			content: {
669
				message: dojo.toJson([{
670
					channel:        "/meta/disconnect",
671
					clientId:       this._cometd.clientId,
672
					id:             ""+this._cometd.messageId++
673
				}])
674
			}
675
		});
676
	}
677
}
678
dojox.cometd.connectionTypes.register("long-polling", dojox.cometd.longPollTransport.check, dojox.cometd.longPollTransport);
679
dojox.cometd.connectionTypes.register("callback-polling", dojox.cometd.callbackPollTransport.check, dojox.cometd.callbackPollTransport);
680
 
681
dojo.addOnUnload(dojox.cometd,"_onUnload");
682
 
683
 
684
}