JavaScript Client

All actions that could potentially block or involve a remote invocation must be performed asynchronously when using the JavaScript language mapping. If you would like to see the full source code of the JavaScript client, please visit our github ice-demos repository.

Configuring the Client for a Router

One of the first tasks of any Ice client is initializing a communicator. If you already have experience using another Ice language mapping, much of the following code will look very familiar:

var id = new Ice.InitializationData();
id.properties = Ice.createProperties();
id.properties.setProperty(
    "Ice.Default.Router",
    "Glacier2/router:wss -p 443 -h zeroc.com -r /demo-proxy/chat/glacier2");
communicator = Ice.initialize(id);

The code above shows that we are setting the value of the property Ice.Default.Router to a stringified proxy. The target object of this proxy has the identity Glacier2/router and the accompanying endpoint uses the transport type wss, meaning we intend to communicate with the router over a secure WebSocket connection at port 5064.

On the server end, the Glacier2 router employs our C++ WebSocket transport to accept connections on this port. In this case the chat client establishes a connection to the chat server router indirectly via a reverse proxy hosted at zeroc.com, but it's also possible to connect to an Ice server directly.

Creating a Session

A push client needs to establish a session with the Glacier2 router. In the code below, the client performs a checked cast on the router proxy, causing the Ice run time in the client to establish a connection and verify that the target object exists. Upon completion of the checked cast, the client receives the router proxy and calls createSession, passing the credentials entered by the user:

return RouterPrx.checkedCast(communicator.getDefaultRouter()).then(router =>
    {
        //
        // Create a session with the Glacier2 router.
        //
        return router.createSession(
            $("#username").val(), $("#password").val()).then(
                session => run(router, ChatSessionPrx.uncheckedCast(session)));
    });

The value returned by createSession is a session proxy, which the code downcasts locally to the proxy type ChatSessionPrx and passes to run to continue the initialization process and execute the "main loop" of the application.

The run function needs to make three asynchronous invocations. Two are remote calls to query the router for the session timeout and identity category, and the third is a local call to create an object adapter. In the latter case, calling createObjectAdapterWithRouter has asynchronous semantics because the implementation may perform remote invocations. The client uses the Promise.all function to execute these tasks:

Promise.all([
    router.getSessionTimeout(),
    router.getCategoryForClient(),
    communicator.createObjectAdapterWithRouter("", router)]
).then(values =>
    {
        //
        // The results of each promise are provided in an array.
        //
        const [timeout, category, adapter] = values;
        ...
    }
);

All asynchronous invocations in Ice for JavaScript return a promise object. A promise allows you to specify success and failure callbacks that are called when the action completes. You can also chain promises together, which simplifies your code structure when executing a sequence of asynchronous actions.

The all convenience function accepts any number of promises, and returns a new promise that completes only when all actions have completed successfully. The client can use all here because there is no dependency among these actions, so they are essentially invoked simultaneously. When all actions have completed, the callback function receives their results.

Keeping a Session Alive

The JavaScript client configures ACM heartbeats to periodically send messages that keep the session alive. The client also configures a connection callback to detect session termination: 

var chat = new Ice.Promise();
...
//
// Use ACM heartbeat to keep session alive.
//
const connection = router.ice_getCachedConnection();
if(timeout > 0)
{
    connection.setACM(timeout, undefined, Ice.ACMHeartbeat.HeartbeatAlways);
}
connection.setCloseCallback(() =>
    {
        if(!completed)
        {
            chat.reject(new Error("Connection lost"));
        }
    });

The chat promise indicates the overall status of the client. If the close callback is invoked and the chat promise hasn't yet completed, it indicates that the connection with the server has been lost. The callback resolves the chat promise by invoking reject and passing it an exception with the cause of the failure.

In order to receive push notifications about chat activity, the client must register a callback. To do this, it must first create a servant and add it to the object adapter. The call to adapter.add returns a proxy for the Ice object, which the client downcasts to the type ChatRoomCallbackPrx prior to calling setCallback on the session:

const callback = ChatRoomCallbackPrx.uncheckedCast(
    adapter.add(new ChatCallbackI(),
                new Ice.Identity("callback", category)));

return session.setCallback(callback);

At this point, the client is ready to receive notifications. The servant, implemented here as the type ChatCallbackI, defines methods corresponding to the operations on a Slice interface. Notice also the identity assigned to this servant; the identity category is the one we obtained earlier from the router. A Glacier2 client must use its assigned category in the identities of all callback objects for push notifications to be routed successfully.

Sending a Message

A new chat message is sent each time the user hits the Enter key in the input text field. The client defines the following event handler:

$("#input").keypress(e =>
    {
        session.send(msg).then(
            timestamp =>
            {
                writeLine(formatDate(timestamp.toNumber()) + " - <" + username + "> - " + msg);
            },
            ex =>
            {
                if(ex instanceof Chat.InvalidMessageException)
                {
                    writeLine("<system-message> - " + ex.reason);
                }
                else
                {
                    chat.reject(ex);
                }
            });
    });

The code extracts the message and passes it to the chat server by calling session.send. The return value is a timestamp, which the client converts into a date and includes in the log message that it appends to the chat history.

If you would like to see the JavaScript client in action, you can use ZeroC's live chat server.