From 0d93214e223f7efce3b54552e2f248904e0c0ec7 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Mon, 23 Jun 2008 20:24:42 +0000 Subject: [PATCH] This has received the Ray seal of approval, and may now live amongst the other utilities in Narya. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5194 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../threerings/util/ElementExpiredEvent.as | 30 +++ src/as/com/threerings/util/ExpiringSet.as | 216 ++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 src/as/com/threerings/util/ElementExpiredEvent.as create mode 100644 src/as/com/threerings/util/ExpiringSet.as diff --git a/src/as/com/threerings/util/ElementExpiredEvent.as b/src/as/com/threerings/util/ElementExpiredEvent.as new file mode 100644 index 000000000..a9d5492bc --- /dev/null +++ b/src/as/com/threerings/util/ElementExpiredEvent.as @@ -0,0 +1,30 @@ +// +// $Id$ + +package com.threerings.util { + +import flash.events.Event; + +public class ElementExpiredEvent extends Event +{ + public static const ELEMENT_EXPIRED :String = "ElementExpired"; + + public function ElementExpiredEvent (element :Object) + { + super(ELEMENT_EXPIRED); + _element = element; + } + + public function get element () :Object + { + return _element; + } + + override public function clone () :Event + { + return new ElementExpiredEvent(_element); + } + + protected var _element :Object; +} +} diff --git a/src/as/com/threerings/util/ExpiringSet.as b/src/as/com/threerings/util/ExpiringSet.as new file mode 100644 index 000000000..f5f263967 --- /dev/null +++ b/src/as/com/threerings/util/ExpiringSet.as @@ -0,0 +1,216 @@ +// +// $Id$ + +package com.threerings.util { + +import flash.events.EventDispatcher; +import flash.events.TimerEvent; + +import flash.utils.getTimer; // function import +import flash.utils.Timer; + +/** + * Dispatched when a set element expires. + * + * @eventType com.threerings.util.ElementExpiredEvent.ELEMENT_EXPIRED + */ +[Event(name="ElementExpired", type="com.threerings.util.ElementExpiredEvent")] + +/** + * Data structure that keeps its elements for a short time, and then removes them automatically. + * + * All operations are O(n), including add(). + */ +public class ExpiringSet extends EventDispatcher + implements Set +{ + /** + * Initializes the expiring set. + * + * @param ttl Time to live value for set elements, in seconds. + */ + public function ExpiringSet (ttl :Number) + { + _ttl = Math.round(ttl * 1000); + } + + /** + * Returns the time to live value for this ExpiringSet. This value cannot be changed after + * set creation. + */ + public function get ttl () :Number + { + return _ttl / 1000; + } + + // from Set + public function isEmpty () :Boolean + { + return size() == 0; + } + + /** + * Calling this function will not expire the elements, it simply removes them. No + * ElementExpiredEvent will be dispatched. + */ + public function clear () :void + { + // simply trunate the data array + _data.length = 0; + } + + // from Set + public function size () :int + { + return _data.length; + } + + // from Set + public function contains (o :Object) :Boolean + { + for each (var e :ExpiringElement in _data) { + if (e.objectEquals(o)) { + return true; + } + } + return false; + } + + /** + * Note that if you add an object that the list already contains, this method will return + * false, but it will also update the expire time on that object to be this sets ttl from now, + * as if the item really were being added to the list now. + */ + public function add (o :Object) :Boolean + { + var added :Boolean = true; + var element :ExpiringElement; + var expire :int = getTimer() + _ttl; + for (var ii :int = 0; ii < _data.length; ii++) { + if ((_data[ii] as ExpiringElement).objectEquals(o)) { + // already contained - update expire time and remove from current position. + element = _data[ii] as ExpiringElement; + element.expirationTime = expire; + _data.splice(ii, 1); + added = false; + break; + } + } + + // push the item onto the queue. since each element has the same TTL, elements end up + // being ordered by their expiration time. + _data.push(element || new ExpiringElement(o, expire)); + checkTimer(); + return added; + } + + // from Set + public function remove (o :Object) :Boolean + { + // pull the item from anywhere in the queue. If we remove the first element, the timer + // will harmlessly NOOP when it wakes up + for (var ii :int = 0; ii < _data.length; ii++) { + var e :ExpiringElement = _data[ii] as ExpiringElement; + if (e.objectEquals(o)) { + _data.splice(ii, 1); + return true; + } + } + + return false; + } + + /** + * This implementation of Set returns a fresh array that it will never reference again. + * Modification of this array will not change the ExpiringSet's structure. + */ + public function toArray () :Array + { + var elements :Array = []; + for each (var element :ExpiringElement in _data) { + elements.push(element.element); + } + return elements; + } + + protected function checkTimer (...ignored) :void + { + // expiration check + var now :int = getTimer(); + while (headIsExpired(now)) { + // pop the head off the queue and dispatch an event + var head :ExpiringElement = _data.shift() as ExpiringElement; + dispatchEvent(new ElementExpiredEvent(head.element)); + } + + // empty check + if (isEmpty()) { + return; + } + + // if the timer is already running, and we're not empty, we want to just let it finish + // its current delay, and set up a new one then + if (_timer != null && _timer.running) { + return; + } + + // sanity check + var delay :int = (_data[0] as ExpiringElement).expirationTime - now; + if (delay <= 0) { + // blow up + log.warning("calculated delay after expiring elements is invalid! [" + delay + "]"); + return; + } + + // set up next delay + if (_timer == null) { + _timer = new Timer(delay, 1); + _timer.addEventListener(TimerEvent.TIMER, checkTimer); + } else { + _timer.delay = delay; + _timer.reset(); + } + _timer.start(); + } + + protected function headIsExpired (now :int) :Boolean + { + return isEmpty() ? false : (_data[0] as ExpiringElement).isExpired(now); + } + + protected static const log :Log = Log.getLog(ExpiringSet); + + /** The time to live for this set, not to be changed after construction. */ + protected /* final */ var _ttl :int; + + /** Array of ExpiringElement instances, sorted by expiration time. */ + protected var _data :Array = new Array(); + + protected var _timer :Timer; +} +} + +import com.threerings.util.Util; + +class ExpiringElement +{ + public var expirationTime :int; + public var element :Object; + + public function ExpiringElement (element :Object, expiration :int) + { + this.element = element; + this.expirationTime = expiration; + } + + public function isExpired (now :int) :Boolean + { + return expirationTime <= now; + } + + public function objectEquals (element :Object) :Boolean + { + return Util.equals(element, this.element); + } +} +