1////////////////////////////////////////////////////////////////////////////////
2//
3//  ADOBE SYSTEMS INCORPORATED
4//  Copyright 2006-2007 Adobe Systems Incorporated
5//  All Rights Reserved.
6//
7//  NOTICE: Adobe permits you to use, modify, and distribute this file
8//  in accordance with the terms of the license agreement accompanying it.
9//
10////////////////////////////////////////////////////////////////////////////////
11
12package mx.messaging
13{
14
15import flash.errors.IllegalOperationError;
16import flash.events.EventDispatcher;
17import flash.events.TimerEvent;
18import flash.utils.Dictionary;
19import flash.utils.Timer;
20
21import mx.core.mx_internal;
22import mx.events.PropertyChangeEvent;
23import mx.messaging.channels.NetConnectionChannel;
24import mx.messaging.channels.PollingChannel;
25import mx.messaging.config.ServerConfig;
26import mx.messaging.errors.NoChannelAvailableError;
27import mx.messaging.events.ChannelEvent;
28import mx.messaging.events.ChannelFaultEvent;
29import mx.messaging.events.MessageEvent;
30import mx.messaging.events.MessageFaultEvent;
31import mx.messaging.messages.AcknowledgeMessage;
32import mx.messaging.messages.CommandMessage;
33import mx.messaging.messages.ErrorMessage;
34import mx.messaging.messages.IMessage;
35import mx.resources.IResourceManager;
36import mx.resources.ResourceManager;
37import mx.rpc.AsyncDispatcher;
38import mx.rpc.AsyncToken;
39import mx.rpc.events.AbstractEvent;
40import mx.rpc.events.FaultEvent;
41import mx.rpc.events.ResultEvent;
42import mx.utils.Base64Encoder;
43
44use namespace mx_internal;
45
46[DefaultProperty("channels")]
47
48/**
49 *  Dispatched after a Channel in the ChannelSet has connected to its endpoint.
50 *
51 *  @eventType mx.messaging.events.ChannelEvent.CONNECT
52 */
53[Event(name="channelConnect", type="mx.messaging.events.ChannelEvent")]
54
55/**
56 *  Dispatched after a Channel in the ChannelSet has disconnected from its
57 *  endpoint.
58 *
59 *  @eventType mx.messaging.events.ChannelEvent.DISCONNECT
60 */
61[Event(name="channelDisconnect", type="mx.messaging.events.ChannelEvent")]
62
63/**
64 *  Dispatched after a Channel in the ChannelSet has faulted.
65 *
66 *  @eventType mx.messaging.events.ChannelFaultEvent.FAULT
67 */
68[Event(name="channelFault", type="mx.messaging.events.ChannelFaultEvent")]
69
70/**
71 * The result event is dispatched when a login or logout call successfully returns.
72 * @eventType mx.rpc.events.ResultEvent.RESULT
73 */
74[Event(name="result", type="mx.rpc.events.ResultEvent")]
75
76/**
77 * The fault event is dispatched when a login or logout call fails.
78 * @eventType mx.rpc.events.FaultEvent.FAULT
79 */
80[Event(name="fault", type="mx.rpc.events.FaultEvent")]
81
82/**
83 *  Dispatched when a property of the ChannelSet changes.
84 *
85 *  @eventType mx.events.PropertyChangeEvent.PROPERTY_CHANGE
86 */
87[Event(name="propertyChange", type="mx.events.PropertyChangeEvent")]
88
89[ResourceBundle("messaging")]
90
91/**
92 *  The ChannelSet is a set of Channels that are used to send messages to a
93 *  target destination. The ChannelSet improves the quality of service on the
94 *  client by hunting through its Channels to send messages in the face of
95 *  network failures or individual Channel problems.
96 */
97public class ChannelSet extends EventDispatcher
98{
99    //--------------------------------------------------------------------------
100    //
101    // Constructor
102    //
103    //--------------------------------------------------------------------------
104
105    /**
106     *  Constructs a ChannelSet.
107     *  If the <code>channelIds</code> argument is provided, the ChannelSet will
108     *  use automatically configured Channels obtained via <code>ServerConfig.getChannel()</code>
109     *  to reach a destination.
110     *  Attempting to manually assign Channels to a ChannelSet that uses configured
111     *  Channels is not allowed.
112     *
113     *  <p>If the <code>channelIds</code> argument is not provided or is null,
114     *  Channels must be manually created and added to the ChannelSet in order
115     *  to connect and send messages.</p>
116     *
117     *  <p>If the ChannelSet is clustered using url-load-balancing (where each server
118     *  declares a unique RTMP or HTTP URL and the client fails over from one URL to
119     *  the next), the first time that a Channel in the ChannelSet successfully connects
120     *  the ChannelSet will automatically make a request for all of the endpoints across
121     *  the cluster for all member Channels and will assign these failover URLs to each
122     *  respective Channel.
123     *  This allows Channels in the ChannelSet to failover individually, and when failover
124     *  options for a specific Channel are exhausted the ChannelSet will advance to the next
125     *  Channel in the set to attempt to reconnect.</p>
126     *
127     *  <p>Regardless of clustering, if a Channel cannot connect or looses
128     *  connectivity, the ChannelSet will advance to its next available Channel
129     *  and attempt to reconnect.
130     *  This allows the ChannelSet to hunt through Channels that use different
131     *  protocols, ports, etc., in search of one that can connect to its endpoint
132     *  successfully.</p>
133     *
134     *  @param channelIds The ids of configured Channels obtained from ServerConfig for this ChannelSet to
135     *                    use. If null, Channels must be manually added to the ChannelSet.
136     *
137     *  @param clusteredWithURLLoadBalancing True if the Channels in the ChannelSet are clustered
138     *                   using url load balancing.
139     */
140    public function ChannelSet(channelIds:Array = null, clusteredWithURLLoadBalancing:Boolean = false)
141    {
142        super();
143        _clustered = clusteredWithURLLoadBalancing;
144        _connected = false;
145        _connecting = false;
146        _currentChannelIndex = -1;
147        if (channelIds != null)
148        {
149            _channelIds = channelIds;
150            _channels = new Array(_channelIds.length);
151            _configured = true;
152        }
153        else
154        {
155            _channels = [];
156            _configured = false;
157        }
158        _hasRequestedClusterEndpoints = false;
159        _hunting = false;
160        _messageAgents = [];
161        _pendingMessages = new Dictionary();
162        _pendingSends = [];
163        _shouldBeConnected = false;
164        _shouldHunt = true;
165    }
166
167    //--------------------------------------------------------------------------
168    //
169    // Variables
170    //
171    //--------------------------------------------------------------------------
172
173    /**
174     *  @private
175     *  Helper MessageAgent used for direct authentication.
176     */
177    private var _authAgent:AuthenticationAgent;
178
179    /**
180     *  @private
181     *  Flag indicating whether the ChannelSet is in the process of connecting
182     *  over the current Channel.
183     */
184    private var _connecting:Boolean;
185
186    /**
187     *  @private
188     *  Stored credentials to be set on the member channels.
189     */
190    private var _credentials:String;
191
192    /**
193     *  @private
194     *  The character-set encoding used to create the credentials String.
195     */
196    private var _credentialsCharset:String;
197
198    /**
199     *  @private
200     *  Current index into the _channels/_channelIds arrays.
201     */
202    private var _currentChannelIndex:int;
203
204    /**
205     *  @private
206     *  This flag restricts our cluster request to only happen upon initial
207     *  connect to the cluster.
208     */
209    private var _hasRequestedClusterEndpoints:Boolean;
210
211    /**
212     *  @private
213     *  Timer used to issue periodic heartbeats to the remote host if the
214     *  client is idle, and not actively sending messages.
215     */
216    private var _heartbeatTimer:Timer;
217
218    /**
219     *  @private
220     *  Flag indicating whether the ChannelSet is in the process of hunting to a
221     *  new Channel; this lets us control the "reconnecting" flag on
222     *  CONNECT ChannelEvents that we dispatch when we hunt to a new
223     *  Channel that isn't internally failing over. The new Channel doesn't know we're
224     *  in a reconnect attempt when it makes its initial connect attempt so this lets
225     *  us set "reconnecting" to true on the CONNECT event if it succeeds.
226     */
227    private var _hunting:Boolean;
228
229    /**
230     *  @private
231     *  A dictionary of pending messages used to filter out duplicate
232     *  messages passed to the ChannelSet to send while it is not connected.
233     *  This allows agents to perform message resend behavior (i.e. Consumer resubscribe
234     *  attempts) without worrying about duplicate messages queuing up and being sent to
235     *  the server once a connection is established.
236     */
237    private var _pendingMessages:Dictionary;
238
239    /**
240     *  @private
241     *  An array of PendingSend instances to pass into send() when a connection
242     *  is (re)established.
243     */
244    private var _pendingSends:Array;
245
246    /**
247     *  @private
248     *  A timer used to do a delayed reconnect for NetConnection channels.
249     */
250    private var _reconnectTimer:Timer = null;
251
252    /**
253     *  @private
254     *  Flag indicating whether the ChannelSet should be connected.
255     *  If true, the ChannelSet will attempt to hunt to the next available
256     *  Channel when a disconnect or fault occurs. If false, hunting is not
257     *  performed.
258     */
259    private var _shouldBeConnected:Boolean;
260
261    /**
262     *  @private
263     *  Flag indicating whether a Channel disconnect/fault should trigger hunting or not;
264     *  used when connected Channels are removed from the ChannelSet which should not trigger
265     *  hunting.
266     */
267    private var _shouldHunt:Boolean;
268
269    /**
270     *  @private
271     */
272    private var resourceManager:IResourceManager =
273                                    ResourceManager.getInstance();
274
275    //--------------------------------------------------------------------------
276    //
277    // Properties
278    //
279    //--------------------------------------------------------------------------
280
281    //----------------------------------
282    //  authenticated
283    //----------------------------------
284
285    /**
286     *  @private
287     */
288    private var _authenticated:Boolean;
289
290    [Bindable(event="propertyChange")]
291    /**
292     *  Indicates whether the ChannelSet has an underlying Channel that successfully
293     *  authenticated with its endpoint.
294     */
295    public function get authenticated():Boolean
296    {
297        return _authenticated;
298    }
299
300    /**
301     *  @private
302     */
303    mx_internal function setAuthenticated(value:Boolean, creds:String, notifyAgents:Boolean=true):void
304    {
305        if (_authenticated != value)
306        {
307            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "authenticated", _authenticated, value);
308            _authenticated = value;
309
310            if (notifyAgents)
311            {
312                var ma:MessageAgent;
313                for (var i:int = 0; i < _messageAgents.length; i++)
314                {
315                    ma = MessageAgent(_messageAgents[i]);
316                    ma.mx_internal::setAuthenticated(value, creds);
317                }
318            }
319
320            dispatchEvent(event);
321        }
322    }
323
324    //----------------------------------
325    //  channels
326    //----------------------------------
327
328    /**
329     *  @private
330     */
331    private var _channels:Array;
332
333    /**
334     *  Provides access to the Channels in the ChannelSet.
335     *  This property may be used to assign a set of channels at once or channels
336     *  may be added directly to the ChannelSet via addChannel() individually.
337     *  If this ChannelSet is <code>configured</code> automatically the individual
338     *  channels are created lazily and added to this property as needed.
339     *
340     *  @throws flash.errors.IllegalOperationError If the ChannelSet is
341     *             <code>configured</code>, assigning to this property is not allowed.
342     */
343    public function get channels():Array
344    {
345        return _channels;
346    }
347
348    [ArrayElementType("mx.messaging.Channel")]
349    /**
350     *  @private
351     */
352    public function set channels(values:Array):void
353    {
354        if (configured)
355        {
356            var message:String = resourceManager.getString(
357                "messaging", "cannotAddWhenConfigured");
358            throw new IllegalOperationError(message);
359        }
360
361        // Remove existing channels
362        var channelsToRemove:Array = _channels.slice();
363        var n:int = channelsToRemove.length;
364        for (var i:int = 0; i < n; i++)
365        {
366            removeChannel(channelsToRemove[i]);
367        }
368
369        // Add new channels
370        if (values != null && values.length > 0)
371        {
372            var m:int = values.length;
373            for (var j:int = 0; j < m; j++)
374            {
375                addChannel(values[j]);
376            }
377        }
378    }
379
380    //----------------------------------
381    //  channelIds
382    //----------------------------------
383
384    /**
385     *  @private
386     */
387    private var _channelIds:Array;
388
389    /**
390     *  The ids of the Channels used by the ChannelSet.
391     */
392    public function get channelIds():Array
393    {
394        if (_channelIds != null)
395        {
396            return _channelIds;
397        }
398        else
399        {
400            var ids:Array = [];
401            var n:int = _channels.length;
402            for (var i:int = 0; i < n; i++)
403            {
404                if (_channels[i] != null)
405                    ids.push(_channels[i].id);
406                else
407                    ids.push(null);
408            }
409            return ids;
410        }
411    }
412
413    //----------------------------------
414    //  currentChannel
415    //----------------------------------
416
417    /**
418     *  @private
419     */
420    private var _currentChannel:Channel;
421
422    /**
423     *  Returns the current Channel for the ChannelSet.
424     */
425    public function get currentChannel():Channel
426    {
427        return _currentChannel;
428    }
429
430    //----------------------------------
431    //  channelFailoverURIs
432    //----------------------------------
433
434    /**
435     *  @private
436     */
437    private var _channelFailoverURIs:Object;
438
439    /**
440     *  @private
441     *  Map of arrays of failoverURIs keyed by channel id for the Channels in this ChannelSet.
442     *  This property is assigned to by the ClusterMessageResponder in order to update the
443     *  member Channels with their failoverURIs.
444     */
445    mx_internal function get channelFailoverURIs():Object
446    {
447        return _channelFailoverURIs;
448    }
449
450    /**
451     *  @private
452     */
453    mx_internal function set channelFailoverURIs(value:Object):void
454    {
455        _channelFailoverURIs = value;
456        // Update any existing Channels in the set with their current failover endpoint URIs.
457        var n:int = _channels.length;
458        for (var i:int = 0; i < n; i++)
459        {
460            var channel:Channel = _channels[i];
461            if (channel == null)
462            {
463                break; // The rest of the Channels have not been loaded yet.
464            }
465            else if (_channelFailoverURIs[channel.id] != null)
466            {
467                channel.failoverURIs = _channelFailoverURIs[channel.id];
468            }
469        }
470    }
471
472    //----------------------------------
473    //  configured
474    //----------------------------------
475
476    /**
477     *  @private
478     */
479    private var _configured:Boolean;
480
481    /**
482     *  Indicates whether the ChannelSet is using automatically configured
483     *  Channels or manually assigned Channels.
484     */
485    mx_internal function get configured():Boolean
486    {
487        return _configured;
488    }
489
490    //----------------------------------
491    //  connected
492    //----------------------------------
493
494    /**
495     *  @private
496     */
497    private var _connected:Boolean;
498
499    [Bindable(event="propertyChange")]
500    /**
501     *  Indicates whether the ChannelSet is connected.
502     */
503    public function get connected():Boolean
504    {
505        return _connected;
506    }
507
508    /**
509     *  @private
510     */
511    protected function setConnected(value:Boolean):void
512    {
513        if (_connected != value)
514        {
515            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "connected", _connected, value)
516            _connected = value;
517            dispatchEvent(event);
518            setAuthenticated(value && currentChannel && currentChannel.authenticated, _credentials, false /* Agents also listen for channel disconnects */);
519            if (!connected)
520            {
521                unscheduleHeartbeat();
522            }
523            else if (heartbeatInterval > 0)
524            {
525                scheduleHeartbeat();
526            }
527        }
528    }
529
530    //----------------------------------
531    //  clustered
532    //----------------------------------
533
534    /**
535     *  @private
536     */
537    private var _clustered:Boolean;
538
539    /**
540     *  Indicates whether the ChannelSet targets a clustered destination.
541     *  If true, upon a successful connection the ChannelSet will query the
542     *  destination for all clustered endpoints for its Channels and will assign
543     *  failoverURIs to them.
544     *  Channel ids are used to assign failoverURIs to the proper Channel instances
545     *  so this requires that all Channels in the ChannelSet have non-null ids and an
546     *  Error will be thrown when this property is set to true if this is not the case.
547     *  If the ChannelSet is not using url load balancing on the client this
548     *  property should not be set to true.
549     */
550    public function get clustered():Boolean
551    {
552        return _clustered;
553    }
554
555    /**
556     *  @private
557     */
558    public function set clustered(value:Boolean):void
559    {
560        if (_clustered != value)
561        {
562            if (value)
563            {
564                // Cannot have a clustered ChannelSet that contains Channels with null ids.
565                var ids:Array = channelIds;
566                var n:int = ids.length;
567                for (var i:int = 0; i < n; i++)
568                {
569                    if (ids[i] == null)
570                    {
571                        var message:String = resourceManager.getString(
572                            "messaging", "cannotSetClusteredWithdNullChannelIds");
573                        throw new IllegalOperationError(message);
574                    }
575                }
576            }
577            _clustered = value;
578        }
579    }
580
581    //----------------------------------
582    //  heartbeatInterval
583    //----------------------------------
584
585    /**
586     *  @private
587     */
588    private var _heartbeatInterval:int = 0;
589
590    /**
591     *  The number of milliseconds between heartbeats sent to the remote
592     *  host while this ChannelSet is actively connected but idle.
593     *  Any outbound message traffic will delay heartbeats temporarily, with
594     *  this number of milliseconds elapsing after the last sent message before
595     *  the next heartbeat is issued.
596     *  <p>
597     *  This property is useful for applications that connect to a remote host
598     *  to received pushed updates and are not actively sending any messages, but
599     *  still wish to be notified of a dropped connection even when the networking
600     *  layer fails to provide such notification directly. By issuing periodic
601     *  heartbeats the client can force the networking layer to report a timeout
602     *  if the underlying connection has dropped without notification and the
603     *  application can respond to the disconnect appropriately.
604     *  </p>
605     *  <p>
606     *  Any non-positive value disables heartbeats to the remote host.
607     *  The default value is 0 indicating that heartbeats are disabled.
608     *  If the application sets this value it should prefer a longer rather than
609     *  shorter interval, to avoid placing unnecessary load on the remote host.
610     *  As an illustrative example, low-level TCP socket keep-alives generally
611     *  default to an interval of 2 hours. That is a longer interval than most
612     *  applications that enable heartbeats will likely want to use, but it
613     *  serves as a clear precedent to prefer a longer interval over a shorter
614     *  interval.
615     *  </p>
616     *  <p>
617     *  If the currently connected underlying Channel issues poll requests to
618     *  the remote host, heartbeats are suppressed because the periodic poll
619     *  requests effectively take their place.</p>
620     */
621    public function get heartbeatInterval():int
622    {
623        return _heartbeatInterval;
624    }
625
626    /**
627     *  @private
628     */
629    public function set heartbeatInterval(value:int):void
630    {
631        if (_heartbeatInterval != value)
632        {
633            var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "heartbeatInterval", _heartbeatInterval, value);
634            _heartbeatInterval = value;
635            dispatchEvent(event);
636            if (_heartbeatInterval > 0 && connected)
637            {
638                scheduleHeartbeat();
639            }
640        }
641    }
642
643    //----------------------------------
644    //  initialDestinationId
645    //----------------------------------
646
647    /**
648     *  @private
649     */
650    private var _initialDestinationId:String;
651
652    /**
653     *  Provides access to the initial destination this ChannelSet is used to access.
654     *  When the clustered property is true, this value is used to request available failover URIs
655     *  for the configured channels for the destination.
656     */
657    public function get initialDestinationId():String
658    {
659        return _initialDestinationId;
660    }
661
662    /**
663     *  @private
664     */
665    public function set initialDestinationId(value:String):void
666    {
667        _initialDestinationId = value;
668    }
669
670    //----------------------------------
671    //  messageAgents
672    //----------------------------------
673
674    /**
675     *  @private
676     */
677    private var _messageAgents:Array;
678
679    /**
680     *  Provides access to the set of MessageAgents that use this ChannelSet.
681     */
682    public function get messageAgents():Array
683    {
684        return _messageAgents;
685    }
686
687    //--------------------------------------------------------------------------
688    //
689    // Overridden Methods
690    //
691    //--------------------------------------------------------------------------
692
693    /**
694     *  Returns a String containing the ids of the Channels in the ChannelSet.
695     *
696     *  @return String representation of the ChannelSet.
697     */
698    override public function toString():String
699    {
700        var s:String = "[ChannelSet ";
701        for (var i:uint = 0; i < _channels.length; i++)
702        {
703            if (_channels[i] != null)
704                s += _channels[i].id + " ";
705        }
706        s += "]";
707        return s;
708    }
709
710    //--------------------------------------------------------------------------
711    //
712    // Methods
713    //
714    //--------------------------------------------------------------------------
715
716    /**
717     *  Adds a Channel to the ChannelSet. A Channel with a null id cannot be added
718     *  to the ChannelSet if the ChannelSet targets a clustered destination.
719     *
720     *  @param channel The Channel to add.
721     *
722     *  @throws flash.errors.IllegalOperationError If the ChannelSet is
723     *             <code>configured</code>, adding a Channel is not supported.
724     *             This error is also thrown if the ChannelSet's <code>clustered</code> property
725     *             is <code>true</code> but the Channel has a null id.
726     */
727    public function addChannel(channel:Channel):void
728    {
729        if (channel == null)
730            return;
731
732        var message:String;
733
734        if (configured)
735        {
736            message = resourceManager.getString(
737                "messaging", "cannotAddWhenConfigured");
738            throw new IllegalOperationError(message);
739        }
740
741        if (clustered && channel.id == null)
742        {
743            message = resourceManager.getString(
744                "messaging", "cannotAddNullIdChannelWhenClustered");
745            throw new IllegalOperationError(message);
746        }
747
748        if (_channels.indexOf(channel) != -1)
749            return; // Channel already exists in the set.
750
751        _channels.push(channel);
752        if (_credentials)
753            channel.setCredentials(_credentials, null, _credentialsCharset);
754    }
755
756    /**
757     *  Removes a Channel from the ChannelSet. If the Channel to remove is
758     *  currently connected and being used by the ChannelSet, it is
759     *  disconnected as well as removed.
760     *
761     *  @param channel The Channel to remove.
762     *
763     *  @throws flash.errors.IllegalOperationError If the ChannelSet is
764     *             <code>configured</code>, removing a Channel is not supported.
765     */
766    public function removeChannel(channel:Channel):void
767    {
768        if (configured)
769        {
770            var message:String = resourceManager.getString(
771                "messaging", "cannotRemoveWhenConfigured");
772            throw new IllegalOperationError(message);
773        }
774
775        var channelIndex:int = _channels.indexOf(channel);
776        if (channelIndex > -1)
777        {
778            _channels.splice(channelIndex, 1);
779            // If the Channel being removed is currently in use, we need
780            // to null it out for re-hunting, and potentially disconnect it.
781            if ((_currentChannel != null) && (_currentChannel == channel))
782            {
783                if (connected)
784                {
785                    _shouldHunt = false;
786                    disconnectChannel();
787                }
788                _currentChannel = null;
789                _currentChannelIndex = -1;
790            }
791        }
792    }
793
794    /**
795     *  Connects a MessageAgent to the ChannelSet. Once connected, the agent
796     *  can use the ChannelSet to send messages.
797     *
798     *  @param agent The MessageAgent to connect.
799     */
800    public function connect(agent:MessageAgent):void
801    {
802        if ((agent != null) && (_messageAgents.indexOf(agent) == -1))
803        {
804            _shouldBeConnected = true;
805            _messageAgents.push(agent);
806            agent.mx_internal::internalSetChannelSet(this);
807            // Wire up agent's channel event listeners to this ChannelSet.
808            addEventListener(ChannelEvent.CONNECT, agent.channelConnectHandler);
809            addEventListener(ChannelEvent.DISCONNECT, agent.channelDisconnectHandler);
810            addEventListener(ChannelFaultEvent.FAULT, agent.channelFaultHandler);
811
812            // If the ChannelSet is already connected, notify the agent.
813            if (connected && !agent.needsConfig)
814                agent.channelConnectHandler(ChannelEvent.createEvent(ChannelEvent.CONNECT,
815                                                                     _currentChannel,
816                                                                     false,
817                                                                     false,
818                                                                     connected));
819        }
820    }
821
822    /**
823     *  Disconnects a specific MessageAgent from the ChannelSet. If this is the
824     *  last MessageAgent using the ChannelSet and the current Channel in the set is
825     *  connected, the Channel will physically disconnect from the server.
826     *
827     *  @param agent The MessageAgent to disconnect.
828     */
829    public function disconnect(agent:MessageAgent):void
830    {
831        if (agent == null) // Disconnect the ChannelSet completely.
832        {
833            var allMessageAgents:Array = _messageAgents.slice();
834            var n:int = allMessageAgents.length;
835            for (var i:int = 0; i < n; i++)
836            {
837                allMessageAgents[i].disconnect();
838            }
839            if (_authAgent != null)
840            {
841                _authAgent.state = AuthenticationAgent.SHUTDOWN_STATE;
842                _authAgent = null;
843            }
844        }
845        else // Disconnect a specific MessageAgent.
846        {
847            var agentIndex:int = agent != null ? _messageAgents.indexOf(agent) : -1;
848            if (agentIndex != -1)
849            {
850                _messageAgents.splice(agentIndex, 1);
851                // Remove the agent as a listener to this ChannelSet.
852                removeEventListener(ChannelEvent.CONNECT, agent.channelConnectHandler);
853                removeEventListener(ChannelEvent.DISCONNECT, agent.channelDisconnectHandler);
854                removeEventListener(ChannelFaultEvent.FAULT, agent.channelFaultHandler);
855
856                if (connected || _connecting) // Notify the agent of the disconnect.
857                {
858                    agent.channelDisconnectHandler(ChannelEvent.createEvent(ChannelEvent.DISCONNECT,
859                                                                            _currentChannel, false));
860                }
861                else // Remove any pending sends for this agent.
862                {
863                    var n2:int = _pendingSends.length;
864                    for (var j:int = 0; j < n2; j++)
865                    {
866                        var ps:PendingSend = PendingSend(_pendingSends[j]);
867                        if (ps.agent == agent)
868                        {
869                            _pendingSends.splice(j, 1);
870                            j--;
871                            n2--;
872                            delete _pendingMessages[ps.message];
873                        }
874                    }
875                }
876                // Shut down the underlying Channel connection if this ChannelSet has
877                // no more agents using it.
878                if (_messageAgents.length == 0)
879                {
880                    _shouldBeConnected = false;
881                    _currentChannelIndex = -1;
882                    if (connected)
883                        disconnectChannel();
884                }
885
886                // Null out automatically assigned ChannelSet on agent; if manually assigned leave it alone.
887                if (agent.mx_internal::channelSetMode == MessageAgent.mx_internal::AUTO_CONFIGURED_CHANNELSET)
888                    agent.mx_internal::internalSetChannelSet(null);
889            }
890        }
891    }
892
893    /**
894     *  Disconnects all associated MessageAgents and disconnects any underlying Channel that
895     *  is connected.
896     *  Unlike <code>disconnect(MessageAgent)</code> which is invoked by the disconnect implementations
897     *  of specific service components, this method provides a single, convenient point to shut down
898     *  connectivity between the client and server.
899     */
900    public function disconnectAll():void
901    {
902        disconnect(null);
903    }
904
905    /**
906     *  Handles a CONNECT ChannelEvent and redispatches the event.
907     *
908     *  @param event The ChannelEvent.
909     */
910    public function channelConnectHandler(event:ChannelEvent):void
911    {
912        _connecting = false;
913        _connected = true; // Set internally to allow us to send pending messages before dispatching the connect event.
914        _currentChannelIndex = -1; // Reset index so that future disconnects are followed by hunting through all available options in order.
915
916        // Send any pending messages.
917        while (_pendingSends.length > 0)
918        {
919            var ps:PendingSend = PendingSend(_pendingSends.shift());
920            delete _pendingMessages[ps.message];
921
922            var command:CommandMessage = ps.message as CommandMessage;
923            if (command != null)
924            {
925                // Filter out any commands to trigger connection establishment, and ack them locally.
926                if (command.operation == CommandMessage.TRIGGER_CONNECT_OPERATION)
927                {
928                    var ack:AcknowledgeMessage = new AcknowledgeMessage();
929                    ack.clientId = ps.agent.clientId;
930                    ack.correlationId = command.messageId;
931                    ps.agent.acknowledge(ack, command);
932                    continue;
933                }
934
935                if (!ps.agent.configRequested && ps.agent.needsConfig &&
936                    (command.operation == CommandMessage.CLIENT_PING_OPERATION))
937                {
938                    command.headers[CommandMessage.NEEDS_CONFIG_HEADER] = true;
939                    ps.agent.configRequested = true;
940                }
941            }
942
943            send(ps.agent, ps.message);
944        }
945
946        if (_hunting)
947        {
948            event.reconnecting = true;
949            _hunting = false;
950        }
951
952        // Redispatch Channel connect event.
953        dispatchEvent(event);
954        // Dispatch delayed "connected" property change event.
955        var connectedChangeEvent:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "connected", false, true)
956        dispatchEvent(connectedChangeEvent);
957    }
958
959    /**
960     *  Handles a DISCONNECT ChannelEvent and redispatches the event.
961     *
962     *  @param event The ChannelEvent.
963     */
964    public function channelDisconnectHandler(event:ChannelEvent):void
965    {
966        _connecting = false;
967        setConnected(false);
968
969        // If we should be connected and the Channel isn't failing over
970        // internally and wasn't rejected, hunt and try to reconnect.
971        if (_shouldBeConnected && !event.reconnecting && !event.rejected)
972        {
973            if (_shouldHunt && hunt())
974            {
975                event.reconnecting = true;
976                dispatchEvent(event);
977                if (_currentChannel is NetConnectionChannel)
978                {
979                    // Insert slight delay for reconnect to allow NetConnection
980                    // based channels to shut down and clean up in preparation
981                    // for our next connect attempt.
982                    if (_reconnectTimer == null)
983                    {
984                        _reconnectTimer = new Timer(1, 1);
985                        _reconnectTimer.addEventListener(TimerEvent.TIMER, reconnectChannel);
986                        _reconnectTimer.start();
987                    }
988                }
989                else // No need to wait with other channel types.
990                {
991                    connectChannel();
992                }
993            }
994            else // No more hunting options; give up and fault pending sends.
995            {
996                dispatchEvent(event);
997                faultPendingSends(event);
998            }
999        }
1000        else
1001        {
1002            dispatchEvent(event);
1003            // If the underlying Channel was rejected, fault pending sends.
1004            if (event.rejected)
1005                faultPendingSends(event);
1006        }
1007        // Flip this back to true in case it was turned off by an explicit Channel removal
1008        // that triggered the current disconnect event.
1009        _shouldHunt = true;
1010    }
1011
1012    /**
1013     *  Handles a ChannelFaultEvent and redispatches the event.
1014     *
1015     *  @param event The ChannelFaultEvent.
1016     */
1017    public function channelFaultHandler(event:ChannelFaultEvent):void
1018    {
1019        if (event.channel.connected)
1020        {
1021            dispatchEvent(event);
1022        }
1023        else // The channel fault has resulted in disconnecting.
1024        {
1025            _connecting = false;
1026            setConnected(false);
1027
1028            // If we should be connected and the Channel isn't failing over
1029            // internally, hunt and try to reconnect.
1030            if (_shouldBeConnected && !event.reconnecting && !event.rejected)
1031            {
1032                if (hunt())
1033                {
1034                    event.reconnecting = true;
1035                    dispatchEvent(event);
1036                    if (_currentChannel is NetConnectionChannel)
1037                    {
1038                        // Insert slight delay for reconnect to allow
1039                        // NetConnection based channels to shut down and clean
1040                        // up in preparation for our next connect attempt.
1041                        if (_reconnectTimer == null)
1042                        {
1043                            _reconnectTimer = new Timer(1, 1);
1044                            _reconnectTimer.addEventListener(TimerEvent.TIMER, reconnectChannel);
1045                            _reconnectTimer.start();
1046                        }
1047                    }
1048                    else // No need to wait with other channel types.
1049                    {
1050                        connectChannel();
1051                    }
1052                }
1053                else // No more hunting options; give up and fault pending sends.
1054                {
1055                    dispatchEvent(event);
1056                    faultPendingSends(event);
1057                }
1058            }
1059            else
1060            {
1061                dispatchEvent(event);
1062                // If the underlying Channel was rejected, fault pending sends.
1063                if (event.rejected)
1064                    faultPendingSends(event);
1065            }
1066        }
1067    }
1068
1069    /**
1070     *  Authenticates the ChannelSet with the server using the provided credentials.
1071     *  Unlike other operations on Channels and the ChannelSet, this operation returns an
1072     *  AsyncToken that client code may add a responder to in order to handle success or
1073     *  failure directly.
1074     *  If the ChannelSet is not connected to the server when this method is invoked it will
1075     *  trigger a connect attempt, and if successful, send the login command to the server.
1076     *  Only one login or logout operation may be pending at a time and overlapping calls will
1077     *  generate an IllegalOperationError.
1078     *  Invoking login when the ChannelSet is already authenticated will generate also generate
1079     *  an IllegalOperationError.
1080     *
1081     *  @param username The username.
1082     *  @param password The password.
1083     *  @param charset The character set encoding to use while encoding the
1084     *  credentials. The default is null, which implies the legacy charset of
1085     *  ISO-Latin-1. The only other supported charset is &quot;UTF-8&quot;.
1086     *
1087     *  @return Returns a token that client code may add a responder to in order to handle
1088     *  success or failure directly.
1089     *
1090     *  @throws flash.errors.IllegalOperationError in two situations; if the ChannelSet is
1091     *          already authenticated, or if a login or logout operation is currently in progress.
1092     */
1093    public function login(username:String, password:String, charset:String=null):AsyncToken
1094    {
1095        if (authenticated)
1096            throw new IllegalOperationError("ChannelSet is already authenticated.");
1097
1098        if ((_authAgent != null) && (_authAgent.state != AuthenticationAgent.LOGGED_OUT_STATE))
1099            throw new IllegalOperationError("ChannelSet is in the process of logging in or logging out.");
1100
1101        if (charset != Base64Encoder.CHARSET_UTF_8)
1102            charset = null; // Use legacy charset, ISO-Latin-1.
1103
1104        var credentials:String = null;
1105        if (username != null && password != null)
1106        {
1107            var rawCredentials:String = username + ":" + password;
1108            var encoder:Base64Encoder = new Base64Encoder();
1109            if (charset == Base64Encoder.CHARSET_UTF_8)
1110                encoder.encodeUTFBytes(rawCredentials);
1111            else
1112                encoder.encode(rawCredentials);
1113            credentials = encoder.drain();
1114        }
1115
1116        var msg:CommandMessage = new CommandMessage();
1117        msg.operation = CommandMessage.LOGIN_OPERATION;
1118        msg.body = credentials;
1119        if (charset != null)
1120            msg.headers[CommandMessage.CREDENTIALS_CHARSET_HEADER] = charset;
1121
1122        // A non-null, non-empty destination is required to send using an agent.
1123        // This value is ignored on the server and the message must be handled by an AuthenticationService.
1124        msg.destination = "auth";
1125
1126        var token:AsyncToken = new AsyncToken(msg);
1127        if (_authAgent == null)
1128            _authAgent = new AuthenticationAgent(this);
1129        _authAgent.registerToken(token);
1130        _authAgent.state = AuthenticationAgent.LOGGING_IN_STATE;
1131        send(_authAgent, msg);
1132        return token;
1133    }
1134
1135    /**
1136     *  Logs the ChannelSet out from the server. Unlike other operations on Channels
1137     *  and the ChannelSet, this operation returns an AsyncToken that client code may
1138     *  add a responder to in order to handle success or failure directly.
1139     *  If logout is successful any credentials that have been cached for use in
1140     *  automatic reconnects are cleared for the ChannelSet and its Channels and their
1141     *  authenticated state is set to false.
1142     *  If the ChannelSet is not connected to the server when this method is invoked it
1143     *  will trigger a connect attempt, and if successful, send a logout command to the server.
1144     *
1145     *  <p>The MessageAgent argument is present to support legacy logout behavior and client code that
1146     *  invokes this method should not pass a MessageAgent reference. Just invoke <code>logout()</code>
1147     *  passing no arguments.</p>
1148     *
1149     *  <p>This method is also invoked by service components from their <code>logout()</code>
1150     *  methods, and these components pass a MessageAgent reference to this method when they logout.
1151     *  The presence of this argument is the trigger to execute legacy logout behavior that differs
1152     *  from the new behavior described above.
1153     *  Legacy behavior only sends a logout request to the server if the client is connected
1154     *  and authenticated.
1155     *  If these conditions are not met the legacy behavior for this method is to do nothing other
1156     *  than clear any credentials that have been cached for use in automatic reconnects.</p>
1157     *
1158     *  @param agent Legacy argument. The MessageAgent that is initiating the logout.
1159     *
1160     *  @return Returns a token that client code may
1161     *  add a responder to in order to handle success or failure directly.
1162     *
1163     *  @throws flash.errors.IllegalOperationError if a login or logout operation is currently in progress.
1164     */
1165    public function logout(agent:MessageAgent=null):AsyncToken
1166    {
1167        _credentials = null;
1168        if (agent == null)
1169        {
1170            if ((_authAgent != null) && (_authAgent.state == AuthenticationAgent.LOGGING_OUT_STATE
1171                                         || _authAgent.state == AuthenticationAgent.LOGGING_IN_STATE))
1172                throw new IllegalOperationError("ChannelSet is in the process of logging in or logging out.");
1173
1174            // Clear out current credentials on the client.
1175            var n:int = _messageAgents.length;
1176            var i:int = 0;
1177            for (; i < n; i++)
1178            {
1179                _messageAgents[i].internalSetCredentials(null);
1180            }
1181            n = _channels.length;
1182            for (i = 0; i < n; i++)
1183            {
1184                if (_channels[i] != null)
1185                {
1186                    _channels[i].internalSetCredentials(null);
1187                    if (_channels[i] is PollingChannel)
1188                        PollingChannel(_channels[i]).disablePolling();
1189                }
1190            }
1191
1192            var msg:CommandMessage = new CommandMessage();
1193            msg.operation = CommandMessage.LOGOUT_OPERATION;
1194
1195            // A non-null, non-empty destination is required to send using an agent.
1196            // This value is ignored on the server and the message must be handled by an AuthenticationService.
1197            msg.destination = "auth";
1198
1199            var token:AsyncToken = new AsyncToken(msg);
1200            if (_authAgent == null)
1201                _authAgent = new AuthenticationAgent(this);
1202            _authAgent.registerToken(token);
1203            _authAgent.state = AuthenticationAgent.LOGGING_OUT_STATE;
1204            send(_authAgent, msg);
1205            return token;
1206        }
1207        else // Legacy logout logic.
1208        {
1209            var n2:int = _channels.length;
1210            for (var i2:int = 0; i2 < n2; i2++)
1211            {
1212                if (_channels[i2] != null)
1213                    _channels[i2].logout(agent);
1214            }
1215            return null; // Legacy service logout() impls don't expect a token.
1216        }
1217    }
1218
1219    /**
1220     *  Sends a message from a MessageAgent over the currently connected Channel.
1221     *
1222     *  @param agent The MessageAgent sending the message.
1223     *
1224     *  @param message The Message to send.
1225     *
1226     *  @throws mx.messaging.errors.NoChannelAvailableError If the ChannelSet has no internal
1227     *                                  Channels to use.
1228     */
1229    public function send(agent:MessageAgent, message:IMessage):void
1230    {
1231        if (_currentChannel != null && _currentChannel.connected && !agent.needsConfig)
1232        {
1233            // Filter out any commands to trigger connection establishment, and ack them locally.
1234            if ((message is CommandMessage) && (CommandMessage(message).operation == CommandMessage.TRIGGER_CONNECT_OPERATION))
1235            {
1236                var ack:AcknowledgeMessage = new AcknowledgeMessage();
1237                ack.clientId = agent.clientId;
1238                ack.correlationId = message.messageId;
1239                new AsyncDispatcher(agent.acknowledge, [ack, message], 1);
1240                return;
1241            }
1242
1243            // If this ChannelSet targets a clustered destination, request the
1244            // endpoint URIs for the cluster.
1245            if (!_hasRequestedClusterEndpoints && clustered)
1246            {
1247                var msg:CommandMessage = new CommandMessage();
1248                // Fetch failover URIs for the correct destination.
1249                if (agent is AuthenticationAgent)
1250                {
1251                    msg.destination = initialDestinationId;
1252                }
1253                else
1254                {
1255                    msg.destination = agent.destination;
1256                }
1257                msg.operation = CommandMessage.CLUSTER_REQUEST_OPERATION;
1258                _currentChannel.sendInternalMessage(new ClusterMessageResponder(msg, this));
1259                _hasRequestedClusterEndpoints = true;
1260            }
1261            unscheduleHeartbeat();
1262            _currentChannel.send(agent, message);
1263            scheduleHeartbeat();
1264        }
1265        else
1266        {
1267            // Filter out duplicate messages here while waiting for the underlying Channel to connect.
1268            if (_pendingMessages[message] == null)
1269            {
1270                _pendingMessages[message] = true;
1271                _pendingSends.push(new PendingSend(agent, message));
1272            }
1273
1274            if (!_connecting)
1275            {
1276                if ((_currentChannel == null) || (_currentChannelIndex == -1))
1277                    hunt();
1278
1279                if (_currentChannel is NetConnectionChannel)
1280                {
1281                    // Insert a slight delay in case we've hunted to a
1282                    // NetConnection channel that doesn't allow a reconnect
1283                    // within the same frame as a disconnect.
1284                    if (_reconnectTimer == null)
1285                    {
1286                        _reconnectTimer = new Timer(1, 1);
1287                        _reconnectTimer.addEventListener(TimerEvent.TIMER, reconnectChannel);
1288                        _reconnectTimer.start();
1289                    }
1290                }
1291                else // No need to wait with other channel types.
1292                {
1293                    connectChannel();
1294                }
1295            }
1296        }
1297    }
1298
1299    /**
1300     *  Stores the credentials and passes them through to every connected channel.
1301     *
1302     *  @param credentials The credentials for the MessageAgent.
1303     *  @param agent The MessageAgent that is setting the credentials.
1304     *  @param charset The character set encoding used while encoding the
1305     *  credentials. The default is null, which implies the legacy encoding of
1306     *  ISO-Latin-1.
1307     *
1308     *  @throws flash.errors.IllegalOperationError in two situations; if credentials
1309     *  have already been set and an authentication is in progress with the remote
1310     *  detination, or if authenticated and the credentials specified don't match
1311     *  the currently authenticated credentials.
1312     */
1313    public function setCredentials(credentials:String, agent:MessageAgent, charset:String=null):void
1314    {
1315        _credentials = credentials;
1316        var n:int = _channels.length;
1317        for (var i:int = 0; i < n; i++)
1318        {
1319            if (_channels[i] != null)
1320                _channels[i].setCredentials(_credentials, agent, charset);
1321        }
1322    }
1323
1324    //--------------------------------------------------------------------------
1325    //
1326    // Internal Methods
1327    //
1328    //--------------------------------------------------------------------------
1329
1330    /**
1331     *  @private
1332     *  Handles a successful login or logout operation for the ChannelSet.
1333     */
1334    mx_internal function authenticationSuccess(agent:AuthenticationAgent, token:AsyncToken, ackMessage:AcknowledgeMessage):void
1335    {
1336        // Reset authentication state depending on whether a login or logout was successful.
1337        var command:CommandMessage = CommandMessage(token.message);
1338        var handlingLogin:Boolean = (command.operation == CommandMessage.LOGIN_OPERATION);
1339        var creds:String = (handlingLogin) ? String(command.body) : null;
1340
1341        if (handlingLogin)
1342        {
1343            // First, sync everything with the current credentials.
1344            _credentials = creds;
1345            var n:int = _messageAgents.length;
1346            var i:int = 0;
1347            for (; i < n; i++)
1348            {
1349                _messageAgents[i].internalSetCredentials(creds);
1350            }
1351            n = _channels.length;
1352            for (i = 0; i < n; i++)
1353            {
1354                if (_channels[i] != null)
1355                    _channels[i].internalSetCredentials(creds);
1356            }
1357
1358            agent.state = AuthenticationAgent.LOGGED_IN_STATE;
1359            // Flip the currently connected channel to authenticated; this percolates
1360            // back up through the ChannelSet and agent's authenticated properties.
1361            currentChannel.setAuthenticated(true);
1362        }
1363        else // Logout.
1364        {
1365            // Shutdown the current logged out agent.
1366            agent.state = AuthenticationAgent.SHUTDOWN_STATE;
1367            _authAgent = null;
1368            disconnect(agent);
1369
1370            // Flip current channel to *not* authenticated; this percolates
1371            // back up through the ChannelSet and agent's authenticated properties.
1372            currentChannel.setAuthenticated(false);
1373        }
1374
1375        // Notify.
1376        var resultEvent:ResultEvent = ResultEvent.createEvent(ackMessage.body, token, ackMessage);
1377        dispatchRPCEvent(resultEvent);
1378    }
1379
1380    /**
1381     *  @private
1382     *  Handles a failed login or logout operation for the ChannelSet.
1383     */
1384    mx_internal function authenticationFailure(agent:AuthenticationAgent, token:AsyncToken, faultMessage:ErrorMessage):void
1385    {
1386        var messageFaultEvent:MessageFaultEvent = MessageFaultEvent.createEvent(faultMessage);
1387        var faultEvent:FaultEvent = FaultEvent.createEventFromMessageFault(messageFaultEvent, token);
1388        // Leave the ChannelSet in its current auth state and dispose of the auth agent that failed.
1389        agent.state = AuthenticationAgent.SHUTDOWN_STATE;
1390        _authAgent = null;
1391        disconnect(agent);
1392        // And notify.
1393        dispatchRPCEvent(faultEvent);
1394    }
1395
1396    //--------------------------------------------------------------------------
1397    //
1398    // Protected Methods
1399    //
1400    //--------------------------------------------------------------------------
1401
1402    /**
1403     *  @private
1404     *  Helper method to fault pending messages.
1405     *  The ErrorMessage is tagged with a __retryable__ header to indicate that
1406     *  the error was due to connectivity problems on the client as opposed to
1407     *  a server error response and the message can be retried (resent).
1408     *
1409     *  @param event A ChannelEvent.DISCONNECT or a ChannelFaultEvent that is the root cause
1410     *               for faulting these pending sends.
1411     */
1412    protected function faultPendingSends(event:ChannelEvent):void
1413    {
1414        while (_pendingSends.length > 0)
1415        {
1416            var ps:PendingSend = _pendingSends.shift() as PendingSend;
1417            var pendingMsg:IMessage = ps.message;
1418            delete _pendingMessages[pendingMsg];
1419            // Fault the message to its agent.
1420            var errorMsg:ErrorMessage = new ErrorMessage();
1421            errorMsg.correlationId = pendingMsg.messageId;
1422            errorMsg.headers[ErrorMessage.RETRYABLE_HINT_HEADER] = true;
1423            errorMsg.faultCode = "Client.Error.MessageSend";
1424            errorMsg.faultString = resourceManager.getString(
1425                "messaging", "sendFailed");
1426            if (event is ChannelFaultEvent)
1427            {
1428                var faultEvent:ChannelFaultEvent = event as ChannelFaultEvent;
1429                errorMsg.faultDetail = faultEvent.faultCode + " " +
1430                                   faultEvent.faultString + " " +
1431                                   faultEvent.faultDetail;
1432                // This is to make streaming channels report authentication fault
1433                // codes correctly as they don't report connected until streaming
1434                // connection is established and hence end up here.
1435                if (faultEvent.faultCode == "Channel.Authentication.Error")
1436                    errorMsg.faultCode = faultEvent.faultCode;
1437            }
1438            // ChannelEvent.DISCONNECT is treated the same as never
1439            // being able to connect at all.
1440            else
1441            {
1442                errorMsg.faultDetail = resourceManager.getString(
1443                    "messaging", "cannotConnectToDestination");
1444            }
1445            errorMsg.rootCause = event;
1446            ps.agent.fault(errorMsg, pendingMsg);
1447        }
1448    }
1449
1450    /**
1451     *  Redispatches message events from the currently connected Channel.
1452     *
1453     *  @param event The MessageEvent from the Channel.
1454     */
1455    protected function messageHandler(event:MessageEvent):void
1456    {
1457        dispatchEvent(event);
1458    }
1459
1460    /**
1461     *  @private
1462     *  Schedules a heartbeat to be sent in heartbeatInterval milliseconds.
1463     */
1464    protected function scheduleHeartbeat():void
1465    {
1466        if (_heartbeatTimer == null && heartbeatInterval > 0)
1467        {
1468            _heartbeatTimer = new Timer(heartbeatInterval, 1);
1469            _heartbeatTimer.addEventListener(TimerEvent.TIMER, sendHeartbeatHandler);
1470            _heartbeatTimer.start();
1471        }
1472    }
1473
1474    /**
1475     *  @private
1476     *  Handles a heartbeat timer event by conditionally sending a heartbeat
1477     *  and scheduling the next.
1478     */
1479    protected function sendHeartbeatHandler(event:TimerEvent):void
1480    {
1481        unscheduleHeartbeat();
1482        if (currentChannel != null)
1483        {
1484            sendHeartbeat();
1485            scheduleHeartbeat();
1486        }
1487    }
1488
1489    /**
1490     *  @private
1491     *  Sends a heartbeat request.
1492     */
1493    protected function sendHeartbeat():void
1494    {
1495        // Current channel may be actively polling, which suppresses explicit heartbeats.
1496        var pollingChannel:PollingChannel = currentChannel as PollingChannel;
1497        if (pollingChannel != null && pollingChannel._shouldPoll) return;
1498        // Issue an explicit heartbeat and schedule the next.
1499        var heartbeat:CommandMessage = new CommandMessage();
1500        heartbeat.operation = CommandMessage.CLIENT_PING_OPERATION;
1501        heartbeat.headers[CommandMessage.HEARTBEAT_HEADER] = true;
1502        currentChannel.sendInternalMessage(new MessageResponder(null /* no agent */, heartbeat));
1503    }
1504
1505    /**
1506     *  @private
1507     *  Unschedules any currently scheduled pending heartbeat.
1508     */
1509    protected function unscheduleHeartbeat():void
1510    {
1511        if (_heartbeatTimer != null)
1512        {
1513            _heartbeatTimer.stop();
1514            _heartbeatTimer.removeEventListener(TimerEvent.TIMER, sendHeartbeatHandler);
1515            _heartbeatTimer = null;
1516        }
1517    }
1518
1519    //--------------------------------------------------------------------------
1520    //
1521    // Private Methods
1522    //
1523    //--------------------------------------------------------------------------
1524
1525    /**
1526     *  @private
1527     *  Helper method to connect the current internal Channel.
1528     */
1529    private function connectChannel():void
1530    {
1531        _connecting = true;
1532        _currentChannel.connect(this);
1533        // Listen for any server pushed messages on the Channel.
1534        _currentChannel.addEventListener(MessageEvent.MESSAGE, messageHandler);
1535    }
1536
1537    /**
1538     *  @private
1539     *  Helper method to disconnect the current internal Channel.
1540     */
1541    private function disconnectChannel():void
1542    {
1543        _connecting = false;
1544        // Stop listening for server pushed messages on the Channel.
1545        _currentChannel.removeEventListener(MessageEvent.MESSAGE, messageHandler);
1546        _currentChannel.disconnect(this);
1547    }
1548
1549    /**
1550     *  @private
1551     *  Helper method to dispatch authentication-related RPC events.
1552     *
1553     *  @param event The event to dispatch.
1554     */
1555    private function dispatchRPCEvent(event:AbstractEvent):void
1556    {
1557        event.callTokenResponders();
1558        dispatchEvent(event);
1559    }
1560
1561    /**
1562     *  @private
1563     *  Helper method to hunt to the next available internal Channel for the
1564     *  ChannelSet.
1565     *
1566     *  @return True if hunting to the next available Channel was successful; false if hunting
1567     *          exhausted available channels and has reset to the beginning of the set.
1568     *
1569     *  @throws mx.messaging.errors.NoChannelAvailableError If the ChannelSet has no internal
1570     *                                  Channels to use.
1571     */
1572    private function hunt():Boolean
1573    {
1574        if (_channels.length == 0)
1575        {
1576            var message:String = resourceManager.getString(
1577                "messaging", "noAvailableChannels");
1578            throw new NoChannelAvailableError(message);
1579        }
1580
1581        // Unwire from the current channel.
1582        if (_currentChannel != null)
1583            disconnectChannel();
1584
1585        // Advance to next channel, and reset to beginning if all Channels in the set
1586        // have been attempted.
1587        if (++_currentChannelIndex >= _channels.length)
1588        {
1589            _currentChannelIndex = -1;
1590            return false;
1591        }
1592
1593        // If we've advanced past the first channel, indicate that we're hunting.
1594        if (_currentChannelIndex > 0)
1595            _hunting = true;
1596
1597        // Set current channel.
1598        if (configured)
1599        {
1600            if (_channels[_currentChannelIndex] != null)
1601            {
1602                _currentChannel = _channels[_currentChannelIndex];
1603            }
1604            else
1605            {
1606                _currentChannel = ServerConfig.getChannel(_channelIds[
1607                                        _currentChannelIndex], _clustered);
1608                _currentChannel.setCredentials(_credentials);
1609                _channels[_currentChannelIndex] = _currentChannel;
1610            }
1611        }
1612        else
1613        {
1614            _currentChannel = _channels[_currentChannelIndex];
1615        }
1616
1617        // Ensure that the current channel is assigned failover URIs it if was lazily instantiated.
1618        if ((_channelFailoverURIs != null) && (_channelFailoverURIs[_currentChannel.id] != null))
1619            _currentChannel.failoverURIs = _channelFailoverURIs[_currentChannel.id];
1620
1621        return true;
1622    }
1623
1624    /**
1625     *  @private
1626     *  This method is invoked by a timer and it works around a reconnect issue
1627     *  with NetConnection based channels within a single frame by reconnecting after a slight delay.
1628     */
1629    private function reconnectChannel(event:TimerEvent):void
1630    {
1631        _reconnectTimer.stop();
1632        _reconnectTimer.removeEventListener(TimerEvent.TIMER, reconnectChannel);
1633        _reconnectTimer = null;
1634        connectChannel();
1635    }
1636}
1637
1638}
1639
1640//------------------------------------------------------------------------------
1641//
1642// Private Classes
1643//
1644//------------------------------------------------------------------------------
1645
1646import mx.core.mx_internal;
1647import mx.logging.Log;
1648import mx.messaging.ChannelSet;
1649import mx.messaging.MessageAgent;
1650import mx.messaging.MessageResponder;
1651import mx.messaging.events.ChannelEvent;
1652import mx.messaging.messages.IMessage;
1653import mx.messaging.messages.AcknowledgeMessage;
1654import mx.messaging.messages.CommandMessage;
1655import mx.messaging.messages.ErrorMessage;
1656import mx.rpc.AsyncToken;
1657import mx.collections.ArrayCollection;
1658
1659use namespace mx_internal;
1660
1661/**
1662 *  @private
1663 *  Clustered ChannelSets need to request the clustered channel endpoints for
1664 *  the channels they contain upon a successful connect. However, Channels
1665 *  require that all outbound messages be sent by a MessageAgent that their
1666 *  internal MessageResponder implementations can callback to upon a response
1667 *  or fault. The ChannelSet is not a MessageAgent, so in this case, it
1668 *  circumvents the regular Channel.send() by passing its own custom responder
1669 *  to Channel.sendUsingCustomResponder().
1670 *
1671 *  This is the custom responder.
1672 */
1673class ClusterMessageResponder extends MessageResponder
1674{
1675    //--------------------------------------------------------------------------
1676    //
1677    // Constructor
1678    //
1679    //--------------------------------------------------------------------------
1680
1681    /**
1682     *  Constructor.
1683     */
1684    public function ClusterMessageResponder(message:IMessage, channelSet:ChannelSet)
1685    {
1686        super(null, message);
1687        _channelSet = channelSet;
1688    }
1689
1690    //--------------------------------------------------------------------------
1691    //
1692    // Variables
1693    //
1694    //--------------------------------------------------------------------------
1695
1696    /**
1697     *  @private
1698     *  Gives the responder access to this ChannelSet, to pass it failover URIs for
1699     *  its channels.
1700     */
1701    private var _channelSet:ChannelSet;
1702
1703    //--------------------------------------------------------------------------
1704    //
1705    // Methods
1706    //
1707    //--------------------------------------------------------------------------
1708
1709    /**
1710     *  Handles a cluster message response.
1711     *
1712     *  @param message The response Message.
1713     */
1714    override protected function resultHandler(message:IMessage):void
1715    {
1716        if ((message.body != null) && (message.body is Array || message.body is ArrayCollection))
1717        {
1718            var channelFailoverURIs:Object = {};
1719            var mappings:Array = message.body is Array? message.body as Array : (message.body as ArrayCollection).toArray();
1720            var n:int = mappings.length;
1721            for (var i:int = 0; i < n; i++)
1722            {
1723                var channelToEndpointMap:Object = mappings[i];
1724                for (var channelId:Object in channelToEndpointMap)
1725                {
1726                    if (channelFailoverURIs[channelId] == null)
1727                        channelFailoverURIs[channelId] = [];
1728
1729                    channelFailoverURIs[channelId].push(channelToEndpointMap[channelId]);
1730                }
1731            }
1732            _channelSet.channelFailoverURIs = channelFailoverURIs;
1733        }
1734    }
1735}
1736
1737/**
1738 *  @private
1739 *  Stores a pending message to send when the ChannelSet does not have a
1740 *  connected Channel to use immediately.
1741 */
1742class PendingSend
1743{
1744    //--------------------------------------------------------------------------
1745    //
1746    // Constructor
1747    //
1748    //--------------------------------------------------------------------------
1749
1750    /**
1751     *  @private
1752     *  Constructor.
1753     *
1754     *  @param agent The MessageAgent sending the message.
1755     *
1756     *  @param msg The Message to send.
1757     */
1758    public function PendingSend(agent:MessageAgent, message:IMessage)
1759    {
1760        super();
1761        this.agent = agent;
1762        this.message = message;
1763    }
1764
1765    //--------------------------------------------------------------------------
1766    //
1767    // Properties
1768    //
1769    //--------------------------------------------------------------------------
1770
1771    /**
1772     *  @private
1773     *  The MessageAgent.
1774     */
1775    public var agent:MessageAgent;
1776
1777    /**
1778     *  @private
1779     *  The Message to send.
1780     */
1781    public var message:IMessage;
1782
1783}
1784
1785/**
1786 *  @private
1787 *  Helper class for handling and redispatching login and logout results or faults.
1788 */
1789class AuthenticationAgent extends MessageAgent
1790{
1791    //--------------------------------------------------------------------------
1792    //
1793    // Public Static Constants
1794    //
1795    //--------------------------------------------------------------------------
1796
1797    // State constants.
1798    public static const LOGGED_OUT_STATE:int = 0;
1799    public static const LOGGING_IN_STATE:int = 1;
1800    public static const LOGGED_IN_STATE:int = 2;
1801    public static const LOGGING_OUT_STATE:int = 3;
1802    public static const SHUTDOWN_STATE:int = 4;
1803
1804    //--------------------------------------------------------------------------
1805    //
1806    // Constructor
1807    //
1808    //--------------------------------------------------------------------------
1809
1810    /**
1811     *  Constructor.
1812     */
1813    public function AuthenticationAgent(channelSet:ChannelSet)
1814    {
1815        _log = Log.getLogger("ChannelSet.AuthenticationAgent");
1816        _agentType = "authentication agent";
1817        // Must set log and agent type before assigning channelSet.
1818        this.channelSet = channelSet;
1819    }
1820
1821    //--------------------------------------------------------------------------
1822    //
1823    // Variables
1824    //
1825    //--------------------------------------------------------------------------
1826
1827    /**
1828     * Map of login/logout message Ids to associated tokens.
1829     */
1830    private var tokens:Object = {};
1831
1832    //--------------------------------------------------------------------------
1833    //
1834    // Properties
1835    //
1836    //--------------------------------------------------------------------------
1837
1838    private var _state:int = LOGGED_OUT_STATE;
1839
1840    /**
1841     * Returns the current state for the agent.
1842     * See the static state constants defined by this class.
1843     */
1844    public function get state():int
1845    {
1846        return _state;
1847    }
1848
1849    public function set state(value:int):void
1850    {
1851        _state = value;
1852        if (value == SHUTDOWN_STATE)
1853            tokens = null;
1854    }
1855
1856    //--------------------------------------------------------------------------
1857    //
1858    // Public Methods
1859    //
1860    //--------------------------------------------------------------------------
1861
1862    /**
1863     * Registers an outbound login/logout message and its associated token for response/fault handling.
1864     */
1865    public function registerToken(token:AsyncToken):void
1866    {
1867        tokens[token.message.messageId] = token;
1868    }
1869
1870    /**
1871     * Acknowledge message callback.
1872     */
1873    override public function acknowledge(ackMsg:AcknowledgeMessage, msg:IMessage):void
1874    {
1875        if (state == SHUTDOWN_STATE)
1876            return;
1877
1878        var error:Boolean = ackMsg.headers[AcknowledgeMessage.ERROR_HINT_HEADER];
1879        // Super will clean the error hint from the message.
1880        super.acknowledge(ackMsg, msg);
1881        // If acknowledge is *not* for a message that caused an error
1882        // dispatch a result event.
1883        if (!error)
1884        {
1885            var token:AsyncToken = tokens[msg.messageId];
1886            delete tokens[msg.messageId];
1887            channelSet.authenticationSuccess(this, token, ackMsg as AcknowledgeMessage);
1888        }
1889    }
1890
1891    /**
1892     * Fault callback.
1893     */
1894    override public function fault(errMsg:ErrorMessage, msg:IMessage):void
1895    {
1896        if (state == SHUTDOWN_STATE)
1897            return;
1898
1899        // For some channel impls, when a logout request is processed the session at the remote host host
1900        // is invalidated which may trigger a disconnection/drop of the channel connection.
1901        // This channel disconnect may mask the logout ack. If the root cause for this error is a channel disconnect,
1902        // assume logout succeeded and locally acknowledge it.
1903        if (errMsg.rootCause is ChannelEvent && (errMsg.rootCause as ChannelEvent).type == ChannelEvent.DISCONNECT)
1904        {
1905            var ackMsg:AcknowledgeMessage = new AcknowledgeMessage();
1906            ackMsg.clientId = clientId;
1907            ackMsg.correlationId = msg.messageId;
1908            acknowledge(ackMsg, msg);
1909            return;
1910        }
1911
1912        super.fault(errMsg, msg);
1913
1914        var token:AsyncToken = tokens[msg.messageId];
1915        delete tokens[msg.messageId];
1916        channelSet.authenticationFailure(this, token, errMsg as ErrorMessage);
1917    }
1918}
1919