ed0718674e
git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5483 542714f4-19e9-0310-aa3c-eee0fc999fb1
435 lines
16 KiB
ActionScript
435 lines
16 KiB
ActionScript
//
|
|
// $Id$
|
|
//
|
|
// Narya library - tools for developing networked games
|
|
// Copyright (C) 2002-2007 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.client {
|
|
|
|
import flash.errors.IllegalOperationError;
|
|
import flash.utils.getTimer; // function import
|
|
|
|
import com.threerings.util.Boxed;
|
|
import com.threerings.util.HashMap;
|
|
import com.threerings.util.Log;
|
|
import com.threerings.util.Short;
|
|
|
|
import com.threerings.presents.data.InvocationMarshaller_ListenerMarshaller;
|
|
|
|
import com.threerings.presents.dobj.CompoundEvent;
|
|
import com.threerings.presents.dobj.DEvent;
|
|
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.InvocationResponseEvent;
|
|
import com.threerings.presents.dobj.MessageEvent;
|
|
import com.threerings.presents.dobj.ObjectAccessError;
|
|
import com.threerings.presents.dobj.Subscriber;
|
|
import com.threerings.presents.dobj.SubscriberAdapter;
|
|
|
|
import com.threerings.presents.data.ClientObject;
|
|
|
|
public class InvocationDirector
|
|
implements EventListener
|
|
{
|
|
private static const log :Log = Log.getLog(InvocationDirector);
|
|
|
|
public function init (omgr :DObjectManager, cloid :int, client :Client) :void
|
|
{
|
|
if (_clobj != null) {
|
|
log.warning("Zoiks, client object around during invmgr init!");
|
|
cleanup();
|
|
}
|
|
|
|
_omgr = omgr;
|
|
_client = client;
|
|
|
|
_omgr.subscribeToObject(cloid, new SubscriberAdapter(
|
|
this.gotClientObject, this.gotClientObjectFailed));
|
|
}
|
|
|
|
public function cleanup () :void
|
|
{
|
|
// wipe our client object, receiver mappings and listener mappings
|
|
_clobj = null;
|
|
|
|
// also reset our counters
|
|
_requestId = 0;
|
|
_receiverId = 0;
|
|
}
|
|
|
|
/**
|
|
* Registers an invocation notification receiver by way of its notification event decoder.
|
|
*/
|
|
public function registerReceiver (decoder :InvocationDecoder) :void
|
|
{
|
|
_reclist.push(decoder);
|
|
|
|
// if we're already online, assign a recevier id now
|
|
if (_clobj != null) {
|
|
assignReceiverId(decoder);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes a receiver registration.
|
|
*/
|
|
public function unregisterReceiver (receiverCode :String) :void
|
|
{
|
|
// remove the receiver from the list
|
|
for (var ii :int = _reclist.length - 1; ii >= 0; ii--) {
|
|
var decoder :InvocationDecoder = (_reclist[ii] as InvocationDecoder);
|
|
if (decoder.getReceiverCode() === receiverCode) {
|
|
_reclist.splice(ii, 1);
|
|
}
|
|
}
|
|
|
|
// if we're logged on, clear out any receiver id mapping
|
|
if (_clobj != null) {
|
|
var rreg :InvocationReceiver_Registration =
|
|
(_clobj.receivers.get(receiverCode) as InvocationReceiver_Registration);
|
|
if (rreg == null) {
|
|
log.warning("Receiver unregistered for which we have no id to code mapping " +
|
|
"[code=" + receiverCode + "].");
|
|
} else {
|
|
var odecoder :Object = _receivers.remove(rreg.receiverId);
|
|
// log.info("Cleared receiver " + StringUtil.shortClassName(odecoder) +
|
|
// " " + rreg + ".");
|
|
}
|
|
_clobj.removeFromReceivers(receiverCode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assigns a receiver id to this decoder and publishes it in the {@link ClientObject#receivers}
|
|
* field.
|
|
*/
|
|
internal function assignReceiverId (decoder :InvocationDecoder) :void
|
|
{
|
|
var reg :InvocationReceiver_Registration =
|
|
new InvocationReceiver_Registration(decoder.getReceiverCode(), nextReceiverId());
|
|
_clobj.addToReceivers(reg);
|
|
_receivers.put(reg.receiverId, decoder);
|
|
}
|
|
|
|
/**
|
|
* Called when we log on; generates mappings for all receivers registered prior to logon.
|
|
*/
|
|
internal function assignReceiverIds () :void
|
|
{
|
|
_clobj.startTransaction();
|
|
try {
|
|
for each (var decoder :InvocationDecoder in _reclist) {
|
|
assignReceiverId(decoder);
|
|
}
|
|
} finally {
|
|
_clobj.commitTransaction();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts a transaction that allows multiple invocation service requests to be batched into a
|
|
* single message and sent to the server all at once.
|
|
*
|
|
* <p> When the transaction is complete, the caller must call {@link #commitTransaction} to
|
|
* cause the requests to be sent. Failure to do so will render the entire invocation services
|
|
* non-functional.
|
|
*/
|
|
public function startTransaction () :void
|
|
{
|
|
// just increment our transaction nesting count, sendRequest will handle everything else
|
|
_tcount++;
|
|
}
|
|
|
|
/**
|
|
* Commits a transaction started with {@link #startTransaction}.
|
|
*/
|
|
public function commitTransaction () :void
|
|
{
|
|
if (_tcount <= 0) {
|
|
throw new IllegalOperationError("Cannot commit: not involved in a transaction");
|
|
}
|
|
if (--_tcount == 0) {
|
|
for each (var event :CompoundEvent in _tevents) {
|
|
event.commit(_omgr);
|
|
}
|
|
_tevents = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Requests that the specified invocation request be packaged up and sent to the supplied
|
|
* invocation oid.
|
|
*/
|
|
public function sendRequest (invOid :int, invCode :int, methodId :int, args :Array) :void
|
|
{
|
|
// configure any invocation listener marshallers among the args
|
|
for each (var arg :Object in args) {
|
|
if (arg is InvocationMarshaller_ListenerMarshaller) {
|
|
var lm :InvocationMarshaller_ListenerMarshaller =
|
|
(arg as InvocationMarshaller_ListenerMarshaller);
|
|
lm.callerOid = _clobj.getOid();
|
|
lm.requestId = nextRequestId();
|
|
lm.mapStamp = getTimer();
|
|
|
|
// create a mapping for this marshaller so that we can properly dispatch responses
|
|
// sent to it
|
|
_listeners.put(lm.requestId, lm);
|
|
}
|
|
}
|
|
|
|
// create an invocation request event and dispatch it
|
|
var req :InvocationRequestEvent =
|
|
new InvocationRequestEvent(invOid, invCode, methodId, args);
|
|
if (_tcount > 0) {
|
|
var event :CompoundEvent = _tevents[invOid];
|
|
if (event == null) {
|
|
_tevents[invOid] = (event = new CompoundEvent(invOid));
|
|
}
|
|
event.postEvent(req);
|
|
} else {
|
|
_omgr.postEvent(req);
|
|
}
|
|
}
|
|
|
|
// documentation inherited from interface EventListener
|
|
public function eventReceived (event :DEvent) :void
|
|
{
|
|
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 :InvocationMarshaller_ListenerMarshaller =
|
|
(_listeners.remove(reqId) as InvocationMarshaller_ListenerMarshaller);
|
|
if (listener == null) {
|
|
log.warning("Received invocation response for which we have no registered listener " +
|
|
"[reqId=" + reqId + ", methId=" + methodId + ", args=" + args + "]. " +
|
|
"It is possible that this listener was flushed because the response did " +
|
|
"not arrive within " + LISTENER_MAX_AGE + " milliseconds.");
|
|
return;
|
|
}
|
|
|
|
unboxArgs(args);
|
|
|
|
// 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 = getTimer();
|
|
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.get(receiverId) as InvocationDecoder);
|
|
if (decoder == null) {
|
|
log.warning("Received notification for which we have no registered receiver " +
|
|
"[recvId=" + receiverId + ", methodId=" + methodId +
|
|
", args=" + args + "].");
|
|
return;
|
|
}
|
|
|
|
unboxArgs(args);
|
|
|
|
// 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 each (var reqId :int in _listeners.keys()) {
|
|
var lm :InvocationMarshaller_ListenerMarshaller =
|
|
(_listeners.get(reqId) as InvocationMarshaller_ListenerMarshaller);
|
|
if (lm.mapStamp < then) {
|
|
_listeners.remove(reqId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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?
|
|
}
|
|
|
|
/**
|
|
* Unbox any arguments that have arrived from the server in boxed types.
|
|
*/
|
|
protected function unboxArgs (args :Array) :void
|
|
{
|
|
if (args != null) {
|
|
for (var ii :int = 0; ii < args.length; ii++) {
|
|
if (args[ii] is Boxed) {
|
|
args[ii] = (args[ii] as Boxed).unbox();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used to generate (almost) monotonically increasing invocation request ids. The ids wrap
|
|
* around to Short.MIN_VALUE after Short.MAX_VALUE is reached.
|
|
*/
|
|
protected function nextRequestId () :int
|
|
{
|
|
// post increment with wraparound - this is necessary because the request id is
|
|
// serialized as a java short in all instances. since action script does not appear
|
|
// to support sign extension, wraparound in the integer domain explicitly. This will
|
|
// make the values in our _listeners map reliable. Cleverer ways to wrapround may exist.
|
|
var current :int = _requestId;
|
|
if (_requestId == Short.MAX_VALUE) { // x7fff
|
|
_requestId = -Short.MIN_VALUE; // x8000
|
|
} else {
|
|
_requestId++;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Used to generate monotonically increasing invocation receiver ids.
|
|
*/
|
|
protected function nextReceiverId () :int
|
|
{
|
|
return _receiverId++;
|
|
}
|
|
|
|
/**
|
|
* Called by the ClientObject SubscriberAdapter when the client object has been returned by the
|
|
* server.
|
|
*/
|
|
internal function gotClientObject (clobj :ClientObject) :void
|
|
{
|
|
clobj.addListener(this);
|
|
clobj.setReceivers(new DSet());
|
|
_clobj = clobj;
|
|
assignReceiverIds();
|
|
_client.gotClientObject(clobj);
|
|
}
|
|
|
|
/**
|
|
* Called by the ClientObject SubscriberAdapter when it fails.
|
|
*/
|
|
internal function gotClientObjectFailed (oid :int, cause :ObjectAccessError) :void
|
|
{
|
|
log.warning("Invocation director unable to subscribe to client object [cloid=" + oid +
|
|
", cause=" + cause + "]!");
|
|
_client.getClientObjectFailed(cause);
|
|
}
|
|
|
|
/** The distributed object manager with which we interact. */
|
|
internal var _omgr :DObjectManager;
|
|
|
|
/** The client for whom we're working. */
|
|
internal var _client :Client;
|
|
|
|
/** Our client object; invocation responses and notifications are received on this object. */
|
|
internal var _clobj :ClientObject;
|
|
|
|
/** Used to generate monotonically increasing request ids. */
|
|
protected var _requestId :int;
|
|
|
|
/** Used to generate monotonically increasing receiver ids. */
|
|
protected var _receiverId :int;
|
|
|
|
/** Used to keep track of invocation service listeners which will receive responses from
|
|
* invocation service requests. */
|
|
protected var _listeners :HashMap = new HashMap();
|
|
|
|
/** Used to keep track of invocation notification receivers. */
|
|
protected var _receivers :HashMap = new HashMap();
|
|
|
|
/** A count of how deeply nested we are in transaction land. */
|
|
protected var _tcount :int;
|
|
|
|
/** An event used to accumulate service request events when in a transaction. */
|
|
protected var _tevents :Array = [];
|
|
|
|
/** All registered receivers are maintained in a list so that we can assign receiver ids to
|
|
* them when we go online. */
|
|
internal var _reclist :Array = [];
|
|
|
|
/** 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;
|
|
}
|
|
}
|