com.threerings.nexus.client.NexusClient Maven / Gradle / Ivy
The newest version!
//
// Nexus Core - a framework for developing distributed applications
// http://github.com/threerings/nexus/blob/master/LICENSE
package com.threerings.nexus.client;
import java.util.HashMap;
import java.util.Map;
import react.Slot;
import com.threerings.nexus.distrib.Address;
import com.threerings.nexus.distrib.NexusObject;
import com.threerings.nexus.net.Connection;
import com.threerings.nexus.util.Callback;
import com.threerings.nexus.util.CallbackList;
import static com.threerings.nexus.util.Log.log;
/**
* Manages connections to Nexus servers. Provides access to distributed objects and services.
*/
public abstract class NexusClient
{
/**
* Requests to subscribe to the object identified by the supplied address.
*/
public void subscribe (final Address addr,
final Callback callback) {
withConnection(addr.host, new Callback.Chain(callback) {
public void onSuccess (Connection conn) {
conn.subscribe(addr, callback);
}
});
}
/**
* Unsubscribes from the specified object. Any events in-flight will be sent to the server, and
* any events generated after this unsubscription request will be dropped.
*/
public void unsubscribe (NexusObject object) {
// TODO: object.postEvent(new UnsubscribeMarker())
// TODO: catch UnsubscribeMarker in Connection.postEvent, and unsubscribe instead of
// forwarding the event to the server
}
/**
* Creates a callback that will subscribe to an object of the specified address and pass the
* successfully subscribed object through to the supplied callback. All failures will be
* propagated through to the supplied callback as well. This simplifies the handling of a
* common pattern, which is to make a service request, receive an object address in response
* and immediately subscribe to the object in question. One can write code like so:
* {@code
* // assume RoomService.joinRoom (String roomId, Callback> callback);
* obj.joinRoom(roomId, client.subscriber(new Callback() {
* public void onSuccess (RoomObject obj) { ... }
* public void onFailure (Throwable cause) { ... }
* }));
* }
*/
public Callback> subscriber (final Callback callback) {
return new Callback>() {
public void onSuccess (Address address) {
subscribe(address, callback);
}
public void onFailure (Throwable cause) {
callback.onFailure(cause);
}
};
}
protected abstract void connect (String host, Callback callback);
// TODO: should we disconnect immediately when clearing last subscription from a given
// connection, or should we periodically poll our open connections and disconnect any with no
// active subscriptions (this would allow a little leeway, so that a usage pattern wherein one
// unsubscribed from their last object on a given server and then immediately subscribed to a
// new one, did not cause needless disconnect and reconnect)
protected void disconnect (String serverHost) {
// TODO
}
/**
* Establishes a connection with the supplied host (if one does not already exist), and invokes
* the supplied action with said connection. Ensures thread-safety in the process.
*/
protected synchronized void withConnection (final String host,
Callback super Connection> action) {
Connection conn = _connections.get(host);
if (conn != null) {
action.onSuccess(conn);
return;
}
CallbackList plist = _penders.get(host);
if (plist != null) {
plist.add(action);
return;
}
log.info("Connecting to " + host);
_penders.put(host, plist = CallbackList.create(action));
connect(host, new Callback() {
public void onSuccess (Connection conn) {
onConnectSuccess(conn);
}
public void onFailure (Throwable cause) {
onConnectFailure(host, cause);
}
});
}
protected synchronized void onConnectSuccess (final Connection conn) {
CallbackList plist = _penders.remove(conn.getHost());
if (plist == null) {
log.warning("Have no penders for established connection?!", "host", conn.getHost());
conn.close(); // shutdown and drop connection
} else {
_connections.put(conn.getHost(), conn);
// we want to be notified when this connection closes
conn.onClose.connect(new Slot() {
@Override public void onEmit (Throwable error) {
onConnectionClose(conn, error);
}
});
plist.onSuccess(conn);
}
}
protected synchronized void onConnectFailure (String host, Throwable cause) {
CallbackList plist = _penders.remove(host);
if (plist == null) {
log.warning("Have no penders for failed connection?!", "host", host);
} else {
plist.onFailure(cause);
}
}
protected synchronized void onConnectionClose (Connection conn, Throwable error) {
// TODO: do we care about orderly versus exceptional closure?
_connections.remove(conn.getHost());
}
/** A mapping from hostname to connection instance for all active connections. */
protected Map _connections = new HashMap();
/** A mapping from hostname to listener list, for all pending connections. */
protected Map> _penders =
new HashMap>();
}