diff --git a/support/aphlict/client/src/Aphlict.as b/support/aphlict/client/src/Aphlict.as index d61ad04b0..592bbd4e5 100644 --- a/support/aphlict/client/src/Aphlict.as +++ b/support/aphlict/client/src/Aphlict.as @@ -1,40 +1,44 @@ package { import flash.display.Sprite; import flash.external.ExternalInterface; import flash.net.LocalConnection; public class Aphlict extends Sprite { /** * A transport channel used to receive data. */ protected var recv:LocalConnection; /** * A transport channel used to send data. */ protected var send:LocalConnection; public function Aphlict() { super(); this.recv = new LocalConnection(); this.recv.client = this; this.send = new LocalConnection(); } protected function externalInvoke(type:String, object:Object = null):void { ExternalInterface.call('JX.Aphlict.didReceiveEvent', type, object); } + protected function error(error:Error):void { + this.externalInvoke('error', error.toString()); + } + protected function log(message:String):void { this.externalInvoke('log', message); } } } diff --git a/support/aphlict/client/src/AphlictMaster.as b/support/aphlict/client/src/AphlictMaster.as index 1991ab48b..ee07fe53e 100644 --- a/support/aphlict/client/src/AphlictMaster.as +++ b/support/aphlict/client/src/AphlictMaster.as @@ -1,166 +1,170 @@ package { import flash.events.Event; import flash.events.IOErrorEvent; import flash.events.ProgressEvent; import flash.events.SecurityErrorEvent; import flash.events.TimerEvent; import flash.net.Socket; import flash.utils.ByteArray; import flash.utils.Dictionary; import flash.utils.Timer; import vegas.strings.JSON; public class AphlictMaster extends Aphlict { /** * The pool of connected clients. */ private var clients:Dictionary; /** * A timer used to trigger periodic events. */ private var timer:Timer; /** * The interval after which clients will be considered dead and removed * from the pool. */ public static const PURGE_INTERVAL:Number = 3 * AphlictClient.INTERVAL; /** * The hostname for the Aphlict Server. */ private var remoteServer:String; /** * The port number for the Aphlict Server. */ private var remotePort:Number; private var socket:Socket; private var readBuffer:ByteArray; public function AphlictMaster(server:String, port:Number) { super(); this.remoteServer = server; this.remotePort = port; // Connect to the Aphlict Server. this.recv.connect('aphlict_master'); this.connectToServer(); this.clients = new Dictionary(); // Start a timer and regularly purge dead clients. this.timer = new Timer(AphlictMaster.PURGE_INTERVAL); this.timer.addEventListener(TimerEvent.TIMER, this.purgeClients); this.timer.start(); } /** * Register a @{class:AphlictClient}. */ public function register(client:String):void { if (!this.clients[client]) { this.log('Registering client: ' + client); this.clients[client] = new Date().getTime(); } } /** * Purge stale client connections from the client pool. */ private function purgeClients(event:TimerEvent):void { for (var client:String in this.clients) { var checkin:Number = this.clients[client]; if (new Date().getTime() - checkin > AphlictMaster.PURGE_INTERVAL) { this.log('Purging client: ' + client); delete this.clients[client]; } } } /** * Clients will regularly "ping" the master to let us know that they are * still alive. We will "pong" them back to let the client know that the * master is still alive. */ public function ping(client:String):void { this.clients[client] = new Date().getTime(); this.send.send(client, 'pong'); } private function connectToServer():void { var socket:Socket = new Socket(); socket.addEventListener(Event.CONNECT, didConnectSocket); socket.addEventListener(Event.CLOSE, didCloseSocket); socket.addEventListener(ProgressEvent.SOCKET_DATA, didReceiveSocket); socket.addEventListener(IOErrorEvent.IO_ERROR, didIOErrorSocket); socket.addEventListener( SecurityErrorEvent.SECURITY_ERROR, didSecurityErrorSocket); socket.connect(this.remoteServer, this.remotePort); this.readBuffer = new ByteArray(); this.socket = socket; } private function didConnectSocket(event:Event):void { this.externalInvoke('connected'); } private function didCloseSocket(event:Event):void { this.externalInvoke('close'); } private function didIOErrorSocket(event:IOErrorEvent):void { this.externalInvoke('error', event.text); } private function didSecurityErrorSocket(event:SecurityErrorEvent):void { this.externalInvoke('error', event.text); } private function didReceiveSocket(event:Event):void { - var b:ByteArray = this.readBuffer; - this.socket.readBytes(b, b.length); + try { + var b:ByteArray = this.readBuffer; + this.socket.readBytes(b, b.length); - do { - b = this.readBuffer; - b.position = 0; + do { + b = this.readBuffer; + b.position = 0; - if (b.length <= 8) { - break; - } + if (b.length <= 8) { + break; + } - var msg_len:Number = parseInt(b.readUTFBytes(8), 10); - if (b.length >= msg_len + 8) { - var bytes:String = b.readUTFBytes(msg_len); - var data:Object = vegas.strings.JSON.deserialize(bytes); - var t:ByteArray = new ByteArray(); - t.writeBytes(b, msg_len + 8); - this.readBuffer = t; - - // Send the message to all clients. - for (var client:String in this.clients) { - this.log('Sending message to client: ' + client); - this.send.send(client, 'receiveMessage', data); + var msg_len:Number = parseInt(b.readUTFBytes(8), 10); + if (b.length >= msg_len + 8) { + var bytes:String = b.readUTFBytes(msg_len); + var data:Object = vegas.strings.JSON.deserialize(bytes); + var t:ByteArray = new ByteArray(); + t.writeBytes(b, msg_len + 8); + this.readBuffer = t; + + // Send the message to all clients. + for (var client:String in this.clients) { + this.log('Sending message to client: ' + client); + this.send.send(client, 'receiveMessage', data); + } + } else { + break; } - } else { - break; - } - } while (true); + } while (true); + } catch (err:Error) { + this.error(err); + } } } } diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js index 5fc15968d..0f40d2c13 100644 --- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js +++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js @@ -1,94 +1,107 @@ /** * @provides javelin-behavior-aphlict-listen * @requires javelin-behavior * javelin-aphlict * javelin-stratcom * javelin-request * javelin-uri * javelin-dom * javelin-json * javelin-router * phabricator-notification */ JX.behavior('aphlict-listen', function(config) { var showing_reload = false; function onready() { var client = new JX.Aphlict(config.id, config.server, config.port) .setHandler(onaphlictmessage) .start(); } // Respond to a notification from the Aphlict notification server. We send // a request to Phabricator to get notification details. function onaphlictmessage(type, message) { - if (type == 'receive') { - var routable = new JX.Request('/notification/individual/', onnotification) - .addData({key: message.key}) - .getRoutable(); + switch (type) { + case 'error': + new JX.Notification() + .setContent('(Aphlict) ' + message) + .alterClassName('jx-notification-error', true) + .show(); + break; - routable - .setType('notification') - .setPriority(250); + case 'receive': + var routable = new JX.Request( + '/notification/individual/', + onnotification); - JX.Router.getInstance().queue(routable); - } else if (__DEV__) { - if (config.debug) { - var details = message ? JX.JSON.stringify(message) : ''; + routable + .addData({key: message.key}) + .getRoutable(); - new JX.Notification() - .setContent('(Aphlict) [' + type + '] ' + details) - .alterClassName('jx-notification-debug', true) - .show(); - } + routable + .setType('notification') + .setPriority(250); + + JX.Router.getInstance().queue(routable); + break; + + default: + if (__DEV__ && config.debug) { + var details = message ? JX.JSON.stringify(message) : ''; + + new JX.Notification() + .setContent('(Aphlict) [' + type + '] ' + details) + .alterClassName('jx-notification-debug', true) + .show(); + } } } // Respond to a response from Phabricator about a specific notification. function onnotification(response) { if (!response.pertinent) { return; } JX.Stratcom.invoke('notification-panel-update', null, {}); // Show the notification itself. new JX.Notification() .setContent(JX.$H(response.content)) .show(); // If the notification affected an object on this page, show a // permanent reload notification if we aren't already. - if ((response.primaryObjectPHID in config.pageObjects) && - !showing_reload) { + if ((response.primaryObjectPHID in config.pageObjects) && !showing_reload) { var reload = new JX.Notification() .setContent('Page updated, click to reload.') .alterClassName('jx-notification-alert', true) .setDuration(0); reload.listen('activate', function(e) { JX.$U().go(); }); reload.show(); showing_reload = true; } } // Wait for the element to load, and don't do anything if it never loads. // If we just go crazy and start making calls to it before it loads, its // interfaces won't be registered yet. JX.Stratcom.listen('aphlict-component-ready', null, onready); // Add Flash object to page JX.$(config.containerID).innerHTML = '<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000">' + '<param name="movie" value="/rsrc/swf/aphlict.swf" />' + '<param name="allowScriptAccess" value="always" />' + '<param name="wmode" value="opaque" />' + '<embed src="/rsrc/swf/aphlict.swf" wmode="opaque"' + 'width="0" height="0" id="' + config.id + '">' + '</embed></object>'; //Evan sanctioned }); diff --git a/webroot/rsrc/swf/aphlict.swf b/webroot/rsrc/swf/aphlict.swf index cee18c7dc..c6ffcdbd3 100644 Binary files a/webroot/rsrc/swf/aphlict.swf and b/webroot/rsrc/swf/aphlict.swf differ