diff --git a/src/as/com/threerings/presents/client/InvocationDirector.as b/src/as/com/threerings/presents/client/InvocationDirector.as index 5f25a0b0a..45adf29c6 100644 --- a/src/as/com/threerings/presents/client/InvocationDirector.as +++ b/src/as/com/threerings/presents/client/InvocationDirector.as @@ -12,6 +12,7 @@ import com.threerings.presents.dobj.DObject; import com.threerings.presents.dobj.DObjectManager; import com.threerings.presents.dobj.DSet; import com.threerings.presents.dobj.EventListener; +import com.threerings.presents.dobj.InvocationNotificationEvent; import com.threerings.presents.dobj.InvocationRequestEvent; import com.threerings.presents.dobj.ObjectAccessError; import com.threerings.presents.dobj.Subscriber; @@ -159,7 +160,132 @@ public class InvocationDirector // documentation inherited from interface EventListener public function eventReceived (event :DEvent) :void { - // TODO + if (event is InvocationResponseEvent) { + var ire :InvocationResponseEvent = + (event as InvocationResponseEvent); + handleInvocationResponse(ire.getRequestId(), ire.getMethodId(), + ire.getArgs()); + + } else if (event is InvocationNotificationEvent) { + var ine :InvocationNotificationEvent = + (event as InvocationNotificationEvent); + handleInvocationNotification(ine.getReceiverId(), ine.getMethodId(), + ine.getArgs()); + + } else if (event is MessageEvent) { + var me :MessageEvent = (event as MessageEvent); + if (me.getName() === ClientObject.CLOBJ_CHANGED) { + handleClientObjectChanged(me.getArgs()[0]); + } + } + } + + /** + * Dispatches an invocation response. + */ + protected function handleInvocationResponse + (reqId :int, methodId :int, args :Array) :void + { + var listener :ListenerMarshaller = + (_listeners.remove(reqId) as ListenerMarshaller); + if (listener == null) { + Log.warning("Received invocation response for which we have " + + "no registered listener [reqId=" + reqId + + ", methId=" + methodId + + ", args=" + args + "]. " + + "It is possble that this listener was flushed " + + "because the response did not arrive within " + + LISTENER_MAX_AGE + " milliseconds."); + return; + } + +// Log.info("Dispatching invocation response " + +// "[listener=" + listener + ", methId=" + methodId + +// ", args=" + StringUtil.toString(args) + "]."); + + // dispatch the response + try { + listener.dispatchResponse(methodId, args); + } catch (e :Error) { + Log.warning("Invocation response listener choked " + + "[listener=" + listener + ", methId=" + methodId + + ", args=" + args + "]."); + Log.logStackTrace(e); + } + + // flush expired listeners periodically + var now :Number = new Date().getTime(); + if (now - _lastFlushTime > LISTENER_FLUSH_INTERVAL) { + _lastFlushTime = now; + flushListeners(now); + } + } + + /** + * Dispatches an invocation notification. + */ + protected function handleInvocationNotification ( + receiverId :int, methodId :int, args :Array) :void + { + // look up the decoder registered for this receiver + var decoder :InvocationDecoder = + (_receivers[receiverId] as InvocationDecoder); + if (decoder == null) { + Log.warning("Received notification for which we have no " + + "registered receiver [recvId=" + receiverId + + ", methodId=" + methodId + + ", args=" + args + "]."); + return; + } + +// Log.info("Dispatching invocation notification " + +// "[receiver=" + decoder.receiver + ", methodId=" + methodId + +// ", args=" + StringUtil.toString(args) + "]."); + + try { + decoder.dispatchNotification(methodId, args); + } catch (e :Error) { + Log.warning("Invocation notification receiver choked " + + "[receiver=" + decoder.receiver + + ", methId=" + methodId + + ", args=" + args + "]."); + Log.logStackTrace(e); + } + } + + /** + * Flushes listener mappings that are older than {@link + * #LISTENER_MAX_AGE} milliseconds. An alternative to flushing + * listeners that did not explicitly receive a response within our + * expiry time period is to have the server's proxy listener send a + * message to the client when it is finalized. We then know that no + * server entity will subsequently use that proxy listener to send a + * response to the client. This involves more network traffic and + * complexity than seems necessary and if a user of the system does + * respond after their listener has been flushed, an informative + * warning will be logged. (Famous last words.) + */ + protected function flushListeners (now :Number) :void + { + var then :Number = now - LISTENER_MAX_AGE; + for (var skey :String in _listeners) { + var lm :ListenerMarshaller = + (_listeners[skey] as ListenerMarshaller); + if (lm.mapStamp < then) { + _listeners.clear(skey); + } + } + } + + /** + * Called when the server has informed us that our previous client + * object is going the way of the Dodo because we're changing screen + * names. We subscribe to the new object and report to the client once + * we've got our hands on it. + */ + protected function handleClientObjectChanged (newCloid :int) :void + { + // TODO: or fuck it? } /** @@ -229,6 +355,15 @@ public class InvocationDirector /** All registered receivers are maintained in a list so that we can * assign receiver ids to them when we go online. */ internal var _reclist :ListCollectionView = new ListCollectionView(); + + /** The last time we flushed our listeners. */ + protected var _lastFlushTime :Number; + + /** The minimum interval between listener flush attempts. */ + protected const LISTENER_FLUSH_INTERVAL :int = 15000; + + /** Listener mappings older than 90 seconds are reaped. */ + protected const LISTENER_MAX_AGE :int = 90 * 1000; } } diff --git a/src/as/com/threerings/presents/data/ClientObject.as b/src/as/com/threerings/presents/data/ClientObject.as index e8790c26b..2b34813e7 100644 --- a/src/as/com/threerings/presents/data/ClientObject.as +++ b/src/as/com/threerings/presents/data/ClientObject.as @@ -42,6 +42,10 @@ public class ClientObject extends DObject public static const RECEIVERS :String = "receivers"; // AUTO-GENERATED: FIELDS END + /** The name of a message event delivered to the client when they + * switch usernames (and therefore user objects). */ + public static const CLOBJ_CHANGED :String = "!clobj_changed!"; + /** Used to publish all invocation service receivers registered on * this client. */ public var receivers :DSet; diff --git a/src/as/com/threerings/presents/data/ListenerMarshaller.as b/src/as/com/threerings/presents/data/ListenerMarshaller.as index 5cf82a169..0809dc85d 100644 --- a/src/as/com/threerings/presents/data/ListenerMarshaller.as +++ b/src/as/com/threerings/presents/data/ListenerMarshaller.as @@ -44,6 +44,7 @@ public class ListenerMarshaller callerOid, requestId, REQUEST_FAILED_RSPID, [ cause ])); } + /** * Called to dispatch an invocation response to our target * listener. diff --git a/src/as/com/threerings/presents/dobj/InvocationNotificationEvent.as b/src/as/com/threerings/presents/dobj/InvocationNotificationEvent.as new file mode 100644 index 000000000..08fddc936 --- /dev/null +++ b/src/as/com/threerings/presents/dobj/InvocationNotificationEvent.as @@ -0,0 +1,137 @@ +// +// $Id: InvocationNotificationEvent.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.presents.dobj { + +import flash.util.StringBuilder; + +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; + +/** + * Used to dispatch an invocation notification from the server to a + * client. + * + * @see DObjectManager#postEvent + */ +public class InvocationNotificationEvent extends DEvent +{ + /** + * Constructs a new invocation notification event on the specified + * target object with the supplied receiver id, method id and + * arguments. + * + * @param targetOid the object id of the object on which the event is + * to be dispatched. + * @param receiverId identifies the receiver to which this notification + * is being dispatched. + * @param methodId the id of the method to be invoked. + * @param args the arguments for the method. This array should contain + * only values of valid distributed object types. + */ + public function InvocationNotificationEvent ( + targetOid :int, receiverId :int, methodId :int, args :Array) + { + super(targetOid); + _receiverId = receiverId; + _methodId = methodId; + _args = args; + } + + /** + * Returns the receiver id associated with this notification. + */ + public function getReceiverId () :int + { + return _receiverId; + } + + /** + * Returns the id of the method associated with this notification. + */ + public function getMethodId () :int + { + return _methodId; + } + + /** + * Returns the arguments associated with this notification. + */ + public function getArgs () :Array + { + return _args; + } + + /** + * Applies this attribute change to the object. + */ + public override function applyToObject (target :DObject) :Boolean + //throws ObjectAccessException + { + // nothing to do here + return true; + } + + // documentation inherited + protected override function notifyListener (listener :Object) :void + { + // nothing to do here + } + + // documentation inherited + protected override function toStringBuf (buf :StringBuilder) :void + { + buf.append("INOT:"); + super.toStringBuf(buf); + buf.append(", rcvId=", _receiverId); + buf.append(", methodId=", _methodId); + buf.append(", args=", _args); + } + + // documentation inherited + public override function writeObject (out :ObjectOutputStream) :void + { + super.writeObject(out); + out.writeShort(_receiverId); + out.writeByte(_methodId); + out.writeObject(_args); + } + + // documentation inherited + public override function readObject (ins :ObjectInputStream) :void + { + super.readObject(ins); + _receiverId = ins.readShort(); + _methodId = ins.readByte(); + _args = (ins.readObject() as Array); + } + + /** Identifies the receiver to which this notification is being + * dispatched. */ + protected var _receiverId :int; + + /** The id of the receiver method being invoked. */ + protected var _methodId :int; + + /** The arguments to the receiver method being invoked. */ + protected var _args :Array; +} +}