diff --git a/src/as/com/threerings/util/ExpiringSet.as b/src/as/com/threerings/util/ExpiringSet.as new file mode 100644 index 000000000..4e0e00095 --- /dev/null +++ b/src/as/com/threerings/util/ExpiringSet.as @@ -0,0 +1,140 @@ +// +// $Id$ + +package com.threerings.util { + +import flash.events.TimerEvent; +import flash.utils.Timer; + +/** + * Data structure that keeps its elements for a short time, and then removes them automatically. + */ +public class ExpiringSet +{ + /** + * Initializes the expiring set. + * + * @param ttl Time to live value for set elements, in seconds. + * @param expirationHandler Optional function to be called when elements are forcibly + * expired, due to exceeding their TTL. The function should be of the form: + *
  function (element :*) :void { }  
+ * and it will be called with the element expired from the set. + */ + public function ExpiringSet (ttl :Number, expirationHandler :Function) + { + _callback = expirationHandler; + _ttl = int(ttl * 1000); + + _timer.addEventListener(TimerEvent.TIMER, expireElements); + } + + public function empty () :Boolean + { + return _q.length == 0; + } + + /** Returns true if the set already contains this element. */ + public function contains (element :*) :Boolean + { + for each (var e :ExpiringElement in _q) { + if (e.equals(element)) { + return true; + } + } + return false; + } + + /** If element is not present in the set, adds it and returns true; otherwise returns false. */ + public function add (element :*) :Boolean + { + if (contains(element)) { + return false; + } + + // push the item on the end of the queue. since each element has the same TTL, + // elements end up being ordered by their expiration time. + _q.push(new ExpiringElement(element, _ttl)); + _timer.start(); + return true; + } + + /** If element is present in the set, removes it and returns true; otherwise returns false. */ + public function remove (element :*) :Boolean + { + var result :Boolean = false; + + // pull the item from anywhere in the queue + for (var ii :int = 0; ii < _q.length; ii++) { + var e :ExpiringElement = _q[ii] as ExpiringElement; + if (e.equals(element)) { + _q.splice(ii, 1); + result = true; + break; + } + } + + if (empty()) { + _timer.stop(); // we ran out of elements. nothing left to expire. + } + + return result; + } + + /** Is the oldest element expired already? */ + protected function headExpired () :Boolean + { + return empty() ? false : (_q[0] as ExpiringElement).expired(); + } + + /** Called on a timer, expires any elements that exceeded their time to live. */ + protected function expireElements (event :TimerEvent) :void + { + while (headExpired()) { + // pop the head of the queue and send it to the custom callback + var head :ExpiringElement = _q.shift() as ExpiringElement; + if (_callback != null) { + _callback(head.element); + } + } + + if (empty()) { + _timer.stop(); // nothing left to expire. let's wait until something is added again. + } + } + + /** Array of ExpiringElement instances, sorted by expiration time. */ + protected var _q :Array = new Array(); + + protected var _timer :Timer = new Timer(1000); // check every second + protected var _callback :Function; + protected var _ttl :int; +} +} + + +import flash.utils.getTimer; // function import + +import com.threerings.util.Util; + +internal class ExpiringElement +{ + public var expirationTime :int; + public var element :*; + + public function ExpiringElement (element :*, ttl :int) + { + this.element = element; + this.expirationTime = getTimer() + ttl; + } + + public function expired () :Boolean + { + return expirationTime < getTimer(); + } + + public function equals (element :*) :Boolean + { + return Util.equals(element, this.element); + } +} +