JavaScript Mapping for Operations

JavaScript's event-driven APIs makes a synchronous invocation model impractical, therefore the JavaScript language mapping provides only an asynchronous model.

Asynchronous Method Invocation (AMI) is the term used to describe the client-side support for the asynchronous programming model. AMI supports both oneway and twoway requests and invocations never block. When a client issues an AMI request, the Ice run time hands the message off to the local transport buffer or, if the buffer is currently full, queues the request for later delivery. The application can then continue its activities and receive a callback when the invocation completes.

AMI is transparent to the server: there is no way for the server to tell whether a client sent a request synchronously or asynchronously.

On this page:

Basic Asynchronous API in JavaScript

Consider the following simple Slice definition:

Slice
module Demo { 
    interface Employees {
        string getName(int number);
        string getAddress(int number);
    };
};

Asynchronous Proxy Methods in JavaScript

slice2js generates the following asynchronous proxy methods:

JavaScript
EmployeesPrx.prototype.getName = function(number, __ctx) { ... }
EmployeesPrx.prototype.getAddress = function(number, __ctx) { ... }

As you can see, a Slice operation maps to a method of the same name. (Each function accepts an optional trailing per-invocation context.)

The getName function, for example, sends an invocation of getName. Because proxy invocations do not block, the application can do other things while the operation is in progress. The application receives notification about the completion of the request by registering callbacks for success and failure.

The getName function, as with all functions corresponding to Slice operations, returns a value of type Ice.AsyncResult. This value contains the state that the Ice run time requires to keep track of the asynchronous invocation.

A proxy function has one parameter for each in-parameter of the corresponding Slice operation. For example, consider the following operation:

Slice
double op(int inp1, string inp2, out bool outp1, out long outp2);

The op member function has the following signature:

JavaScript
function op(inp1, inp2, __ctx); // Returns Ice.AsyncResult

The operation's return value, as well as the values of its two out-parameters, are delivered to the success callback upon completion of the request.

Passing Parameters in JavaScript

The parameter passing rules for the JavaScript mapping are very simple: parameters are passed either by value (for simple types) or by reference (for complex types). Semantically, the two ways of passing parameters are identical: it is guaranteed that the value of a parameter will not be changed by the invocation.

Here is an interface with operations that pass parameters of various types from client to server:

Slice
struct NumberAndString {
    int x;
    string str;
};

sequence<string> StringSeq;

dictionary<long, StringSeq> StringTable;

interface ClientToServer {
    void op1(int i, float f, bool b, string s);
    void op2(NumberAndString ns, StringSeq ss, StringTable st);
    void op3(ClientToServer* proxy);
};

The Slice compiler generates the following proxy methods for these definitions:

JavaScript
ClientToServerPrx.prototype.op1 = function(i, f, b, s, __ctx) { ... }
ClientToServerPrx.prototype.op2 = function(ns, ss, st, __ctx) { ... }
ClientToServerPrx.prototype.op3 = function(proxy, __ctx) { ... }

Given a proxy to a ClientToServer interface, the client code can pass parameters as in the following example:

JavaScript
var p = ...;          // Get ClientToServerPrx proxy...

p.op1(42, 3.14, true, "Hello world!");  // Pass simple literals

var i = 42;
var f = 3.14;
var b = true;
var s = "Hello world!";
p.op1(i, f, b, s);                      // Pass simple variables

var ns = new NumberAndString();
ns.x = 42;
ns.str = "The Answer";
var ss = [];
ss.push("Hello world!");
var st = new StringTable();
st.set(0, ss);
p.op2(ns, ss, st);                      // Pass complex variables

p.op3(p);                               // Pass proxy

Null Parameters in JavaScript

Some Slice types naturally have "empty" or "not there" semantics. Specifically, sequences, dictionaries, and strings all can be null, but the corresponding Slice types do not have the concept of a null value. To make life with these types easier, whenever you pass null as a parameter or return value of type sequence, dictionary, or string, the Ice run time automatically sends an empty sequence, dictionary, or string to the receiver.

This behavior is useful as a convenience feature: especially for deeply-nested data types, members that are sequences, dictionaries, or strings automatically arrive as an empty value at the receiving end. This saves you having to explicitly initialize, for example, every string element in a large sequence before sending the sequence in order to avoid a run-time error. Note that using null parameters in this way does not create null semantics for Slice sequences, dictionaries, or strings. As far as the object model is concerned, these do not exist (only empty sequences, dictionaries, and strings do). For example, whether you send a string as null or as an empty string makes no difference to the receiver: either way, the receiver sees an empty string.

Optional Parameters in JavaScript

Optional parameters use the same mapping as required parameters. The only difference is that undefined can be passed as the value of an optional parameter or return value to indicate an "unset" condition. Consider the following operation:

Slice
optional(1) int execute(optional(2) string params, out optional(3) float value);

A client can invoke this operation as shown below:

JavaScript
proxy.execute("--file log.txt").then(
    function(retval, value)
    {
        if(value !== undefined)
            console.log("value =", value);
    });

A well-behaved program must always compare an optional parameter to undefined prior to using its value.

For Slice types that support null semantics, such as proxies and objects by value, passing null for an optional parameter has a different meaning than passing undefined:

  • null means a value was supplied for the parameter, and that value happens to be null
  • undefined means no value was supplied for the parameter

undefined is not a legal value for required parameters.

Asynchronous Exception Semantics in JavaScript

If an invocation raises an exception, the exception is delivered to the failure callback, even if the actual error condition for the exception was encountered during the proxy function ("on the way out"). The advantage of this behavior is that all exception handling is located in the failure callback (instead of being present twice, once where the proxy function is called, and again in the failure callback).

There is one exception to the above rule: if you destroy the communicator and then make an asynchronous invocation, the proxy function throws CommunicatorDestroyedException. This is necessary because, once the run time is finalized, it can no longer deliver an exception to the failure callback.

The only other exception that is thrown by the proxy function is Error. This exception indicates that you have used the API incorrectly. For example, the proxy function throws this exception if you pass an invalid argument for one of the operation's parameters.

AsyncResult Type in JavaScript

The AsyncResult object returned by an Ice API call encapsulates the state of the asynchronous invocation.

In other Ice language mappings, the AsyncResult type is only used for asynchronous remote invocations. In JavaScript, an AsyncResult object can also be returned by methods of local Ice run time objects such as Communicator and ObjectAdapter when those methods have the potential to block or internally make remote invocations.

An instance of AsyncResult defines the following properties:

  • communicator
    The communicator that sent the invocation.
  • connection
    This method returns the connection that was used for the invocation. Note that, for typical proxy invocations, this method returns a nil value because the possibility of automatic retries means the connection that is currently in use could change unexpectedly. The connection property only returns a non-nil value when the AsyncResult object is obtained by calling flushBatchRequests on a Connection object.
  • proxy
    The proxy that was used for the invocation, or nil if the AsyncResult object was not obtained via a proxy invocation.
  • adapter
    The object adapter that was used for the invocation, or nil if the AsyncResult object was not obtained via an object adapter invocation.
  • operation
    The name of the operation being invoked.

AsyncResult objects also support the following methods:

  • cancel()
    This method prevents a queued invocation from being sent or, if the invocation has already been sent, ignores a reply if the server sends one. cancel is a local operation and has no effect on the server. A canceled invocation is considered to be completed, meaning isCompleted returns true, and the result of the invocation is an Ice.InvocationCanceledException.
  • isCompleted()
    This method returns true if, at the time it is called, the result of an invocation is available, indicating that a call to the end_ method will not block the caller. Otherwise, if the result is not yet available, the method returns false.
  • isSent()
    When you make a proxy invocation, the Ice run time attempts to write the corresponding request to the client-side transport. If the transport cannot accept the request, the Ice run time queues the request for later transmission. isSent returns true if, at the time it is called, the request has been written to the local transport (whether it was initially queued or not). Otherwise, if the request is still queued or an exception occurred before the request could be sent, isSent returns false.
  • throwLocalException()
    This method throws the local exception that caused the invocation to fail. If no exception has occurred yet, throwLocalException does nothing.
  • sentSynchronously()
    This method returns true if a request was written to the client-side transport without first being queued. If the request was initially queued, sentSynchronously returns false (independent of whether the request is still in the queue or has since been written to the client-side transport).

As described in the next section, an AsyncResult object provides additional functionality, including support for success and failure callbacks, that it inherits from a base type.

Promise Type in JavaScript

The Ice.AsyncResult type actually derives from Ice.Promise. This type is an implementation of the promise concept commonly used in JavaScript applications for simplifying the handling of asynchronous invocations. As you'll see below, one especially useful feature of a promise is its support for chaining, which allows you to make a series of asynchronous calls while still maintaining code readability.

Basic Promise Concepts

A promise is a relatively simple object whose purpose is to invoke callback functions upon the success or failure of a condition. The object always starts out in a pending state, and then transitions to a success or failure state via calls to its succeed and fail methods, respectively. This state transition, known as resolving a promise, occurs only once, with any subsequent calls to succeed or fail ignored. The initial caller of succeed or fail can supply arguments that the promise object will pass along to its success or failure callbacks. Any number of callbacks can be registered on a single promise, and they can be registered before or after the promise has been resolved.

The promise implementation always invokes its callbacks asynchronously via setTimeout, which avoids the complexities that could occur if the callbacks were invoked immediately as nested calls. For the purposes of this discussion, when we say that a promise "invokes" its callbacks, we actually mean that it queues the callbacks for invocation.

A promise invokes any registered callbacks at the time the promise is resolved. Callbacks registered after a promise has already been resolved are invoked immediately. The promise keeps references to the arguments passed to succeed or fail so that it can pass them to callbacks registered later.

Getting Started with Promises

The Promise method you'll use most often is then, which accepts two arguments:

JavaScript
function then(successFn, failureFn) { ... }

Both arguments are optional. Here is a simple example:

JavaScript
var p = new Ice.Promise();
p.then(
    function(str, i) {
        console.log("received " + str + " and " + i);
    },
    function(ex) {
        console.log("failed: " + ex);
    });
 
try { 
    // do something...
    p.succeed("string arg", 5);
}
catch(ex) {
    p.fail(ex);
} 

The call to succeed resolves the promise and supplies two arguments that the promise passes to the success callback. Similarly, the call to fail passes along the exception caught by the caller.

As you can see, the success and fail callbacks typically must know what arguments to expect when the promise is resolved.

Chaining Promises

The convenience and power offered by the promise concept becomes clear when you need to execute a sequence of asynchronous actions. Consider this example:

JavaScript
var p = new Ice.Promise();
p.then(    // A
    function() {
        // Step 1...
    }
).then(    // B
    function() {
        // Step 2...
    }
).then(    // C
    function() {
        // Step 3...
    }
);

// None of this happens until...
p.succeed();

Each call to then returns a new promise that is automatically chained to the preceding one. The promise returned by then in A resolves when Step 1 completes. The successful completion of A causes Step 2 to execute, and the successful completion of Step 2 resolves the promise returned by then in B, triggering Step 3 to execute. Finally, the whole chain of events won't begin until the initial promise p resolves.

Handling Errors with Promises

Let's extend our previous example to include an error handler:

JavaScript
var p = new Ice.Promise();
p.then(    // A
    function() {
        // Step 1...
    }
).then(    // B
    function() {
        // Step 2...
    }
).exception(
    function(ex) {
        // handle errors
    }
);

Here we call the exception(failureFn) method to establish an exception handler at the end of the promise chain, which is equivalent to calling then(undefined, failureFn). You'll notice that none of the intermediate steps defines its own failure callback, with the intention that all errors will be handled in one location. After a call to p.succeed triggers execution of the chain, an exception raised by any of these steps propagates through each subsequent promise in the chain until a failure callback is found. If instead the application calls p.fail, none of the steps would execute and only the exception handler would be called.

Now let's examine the behavior when an intermediate step defines a failure callback:

JavaScript
var p = new Ice.Promise();
p.then(
    function() {
        console.log("step 1");
    },
    function() {
        console.log("fail 1");
    }
).then(
    function() {
        console.log("step 2");
    }
).exception(
    function(ex) {
        console.log("error");
    }
);
p.fail();
 
// Outputs:
fail 1
step 2

The results might surprise you at first. If a failure callback completes without raising an exception, the promise is considered to have resolved successfully and therefore triggers the success callback of the next promise in the chain. On the other hand, if a failure callback raises an exception, it will propagate to the next failure callback in the chain. This behavior allows a failure callback to recover from error situations (if possible) and continue with the normal logic flow, or raise an exception and skip to the next error handler.

If no failure callback is defined in a chain after the point at which an exception occurs, that exception will be silently ignored. For this reason, we always recommend terminating a chain with an exception handler. Furthermore, you should not let any uncaught exceptions be thrown from your final exception handler.

Next we'll illustrate this point by throwing an exception:

JavaScript
var p = new Ice.Promise();
p.then(
    function() {
        console.log("step 1");
    },
    function() {
        console.log("fail 1");
        throw new Error();
    }
).then(
    function() {
        console.log("step 2");
    }
).exception(
    function(ex) {
        console.log("error");
    }
);
p.fail();
 
// Outputs:
fail 1
error

Here you can see that the exception raised in the first failure callback bypasses Step 2 and triggers the final exception handler.

Passing Values between Promises

The promise implementation forwards any value returned by a success or failure callback to the next success callback in the chain, as shown below:

JavaScript
var p = new Ice.Promise();
p.then(
    function() {
        return "World!";
    }
).then(
    function(arg) {
        console.log("Hello " + arg);
    }
);
p.succeed();
 
// Outputs:
Hello World!

Chaining Asynchronous Events with Promises

Now that we've discussed promise fundamentals, let's explore more realistic use cases. The primary purpose of promises is chaining together a sequence of asynchronous events such that the next step in the chain won't execute until the previous step has completed. Common examples in Ice applications include a series of proxy invocations in which the calls must be made sequentially in a certain order, and proxy invocations in which the results of a preceding call must be obtained before the next call can be made.

Consider the following example:

JavaScript
var p = new Ice.Promise();
p.then(
    function() {  // Step 1
        var promise = // do something that creates a promise...
        return promise;
    }
).then(
    function() {  // Step 2
        var promise = // do something else that creates a promise...
        return promise;
    }
).then(
    function() {  // Step 3
        console.log("all done");
    }
);

p.succeed();

Steps 1 and 2 each initiate some asynchronous action that creates a promise, and the steps return the promise object as the result of the callback. Normally the result value would be passed as the argument to the next success callback in the chain, as we discussed earlier. However, when a callback returns a promise, execution of the next success or failure callback becomes dependent on the completion of the returned promise. In the example above, calling p.succeed causes Step 1 to execute, but Step 2 won't execute until the promise returned by Step 1 resolves successfully.

Promise API

Instances of Ice.Promise support the following methods:

  • then(successFn, failureFn)
    Executes the success function if this promise resolves successfully, or the failure function if this promise fails. then returns a new promise whose execution is dependent on the resolution of this promise. If either function argument is undefined, the success or failure status of this promise is passed to the next one in the chain. If a function is undefined and this is the last promise in the chain, the outcome is ignored.
  • exception(failureFn)
    A convenience method equivalent to calling then(undefined, failureFn). exception returns a new promise whose execution is dependent on the resolution of this promise.
  • finally(fn)
    Executes the given function when this promise succeeds or fails. The callback function must be prepared to receive any arguments resulting from the success or failure of this promise. finally returns a new promise whose execution is dependent on the resolution of the previous promise in the chain (see example below). Exceptions raised by the given callback function will be ignored.
  • delay(ms)
    Returns a new promise whose success or failure callback is invoked after the specified delay (in milliseconds). The results of the previous promise in the chain are forwarded to the next promise in the chain, as if the delay promise was not present.
  • succeed([args])
    Resolves this promise successfully. Any arguments are passed to the success callback of the next promise in the chain. Returns a reference to this promise.
  • fail([args])
    Resolves this promise as failed. Any arguments are passed to the failure callback of the next promise in the chain. Returns a reference to this promise.
  • succeeded()
    Returns true if this promise succeeded, false otherwise.
  • failed()
    Returns true if this promise failed, false otherwise.
  • completed()
    Returns true if this promise has been resolved, false otherwise.

Additionally, the Promise type defines the following functions:

  • try(fn)
    Returns a new promise whose execution depends on the completion of the given function. This function allows you to write code similar to the traditional try/catch/finally blocks of synchronous code, as shown below:

    JavaScript
    Ice.Promise.try(
        function() {
            // perform asynchronous actions...
     
            // return a promise to the last action in the chain...
        }
    ).exception(
        function(ex) {
            // handle errors
        }
    ).finally(
        function() {
            // clean up when the last action completes
        }
    ); 
    

    The try function makes error handling especially convenient because the chained exception handler will be called if an exception is thrown directly by the try callback, or if the callback returns a promise that later fails. Note that you could also arrange the exception and finally blocks this way:

    JavaScript
    Ice.Promise.try(
        function() {
            // perform asynchronous actions...
     
            // return a promise to the last action in the chain...
        }
    ).finally(
        function() {
            // clean up when the last action completes
        }
    ).exception(
        function(ex) {
            // handle errors
        }
    ); 
    

    If the try callback fails with an exception, the finally callback will be invoked and the failure will propagate to the exception callback. Of course, the order in which the callbacks are invoked in an error situation differs from the previous example.

  • all(promise1[, promise2, ...])
    Returns a new promise whose execution depends on the completion of all given promises. The promise arguments can be specified individually or supplied as a single array, meaning all(p1, p2, p3) is equivalent to all([p1, p2, p3]). The returned promise resolves successfully if all of the given promises resolve successfully, and the returned promise fails if any of the given promises fails. In the case that all of the given promises succeed, the result values from each promise's success function are passed as arguments to the success callback of the returned promise in the same order. Here is an example:

    JavaScript
    function asyncAction() {
        // returns a promise...
    }
     
    Ice.Promise.all(
        asyncAction(), // Action 1
        asyncAction(), // Action 2
        asyncAction()  // Action 3
    ).then(
        function(results1, results2, results3) {
            // Each argument is an array containing the results of the corresponding action
        }
    ).exception(
        function(ex) {
            // handle errors
        }
    ); 
    

    The success function of the promise returned by all does not execute until all of the promise arguments resolve successfully, at which time their results are passed as arguments to the success function. Each result argument is an array containing the results (if any) of the corresponding action's promise.

  • delay([args...,] ms)
    Similar to the prototype method described earlier, this function returns a promise whose success callback is invoked after the specified delay (in milliseconds). One difference between this function and the prototype method is the delay value must be the last argument, with any preceding arguments passed to the success callback:

    JavaScript
    Ice.Promise.delay("John", 100).then(
        function(name) { // Invoked after 100ms
            console.log("Hi " + name);
        },
        ...
    );
    

Completion Callbacks in JavaScript

The AsyncResult promise returned by a proxy invocation resolves when the remote operation completes successfully or fails. The semantics of the success and failure functions depend on the proxy's invocation mode:

  • Twoway proxies
    The success or failure function executes after the Ice run time in the client receives a reply from the server.
  • Oneway and datagram proxies
    The success function executes after the Ice run time passes the message off to the socket. The failure function will only be called if an error occurs while Ice is attempting to marshal or send the message.
  • Batch proxies
    The success function executes after the Ice run time queues the request. The failure function will only be called if an error occurs while Ice is attempting to marshal the message.

For all asynchronous Ice APIs, the failure function receives two arguments: the exception that caused the failure, and a reference to the AsyncResult promise for the failed invocation.

The success callback parameters depend on the operation signature. If the operation has non-void return type, the first parameter of the success callback is the return value. The return value (if any) is followed by a parameter for each out-parameter of the corresponding Slice operation, in the order of declaration. Ice adds a reference to the AsyncResult object associated with the request as the final parameter to every success callback.  Recall the example we showed earlier:

Slice
double op(int inp1, string inp2, out bool outp1, out long outp2);

The success callback for op must have the following signature:

JavaScript
function handleSuccess(returnVal, outp1, outp2, asyncResult)

You can omit the asyncResult parameter if your function has no use for it.

The failure callback is invoked if the invocation fails because of an Ice run time or user exception. This callback function must have the following signature:

JavaScript
function handleAnException(ex, asyncResult)

Again, you can omit the asyncResult parameter if your function has no use for it.

For example, the code below shows how to invoke the getName operation:

JavaScript
var proxy = ...;
var empNo = ...;
proxy.getName(empNo).then(
    function(name, asyncResult) {
        console.log("Name of employee #" + empNo + " is " + name);
    }
).exception(
    function(ex, asyncResult) {
        console.log("Failed to get name of employee #" + empNo);
        console.log(ex);
    }
);

Let's extend the example add a call to getAddress:

JavaScript
var proxy = ...;
var empNo = ...;
proxy.getName(empNo).then(
    function(name, asyncResult) {
        console.log("Name of employee #" + empNo + " is " + name);
        return proxy.getAddress(empNo);
    }
).then(
    function(addr, asyncResult) {
        console.log("Address of employee #" + empNo + " is " + addr);
    }
).exception(
    function(ex, asyncResult) {
        console.log(asyncResult.operation + " failed for employee #" + empNo);
        console.log(ex);
    }
);

Notice here that the success callback for getName returns the AsyncResult promise for getAddress, which means the second success callback won't be invoked until getAddress completes.

Asynchronous Oneway Invocations in JavaScript

You can invoke operations via oneway proxies asynchronously, provided the operation has void return type, does not have any out-parameters, and does not raise user exceptions. If you call the proxy function on a oneway proxy for an operation that returns values or raises a user exception, the proxy function throws an instance of Error.

The callback functions look exactly as for a twoway invocation. The failure function is only called if the invocation raised an exception during the proxy function ("on the way out"), and the success function is called as soon as the message is accepted by the local network connection. The success function takes a single argument representing the AsyncResult associated with the request.

Asynchronous Batch Requests in JavaScript

Applications that send batched requests can either flush a batch explicitly or allow the Ice run time to flush automatically. The asynchronous proxy method ice_flushBatchRequests initiates an immediate flush of any batch requests queued by that proxy.

In addition, similar methods are available on the communicator and the Connection object that is accessible via the connection property of AsyncResult. These methods flush batch requests sent via the same communicator and via the same connection, respectively.

All versions of ice_flushBatchRequests return a promise whose success callback executes as soon as the batch request message is accepted by the local network connection. The success function takes a single argument representing the AsyncResult associated with the flush request. Ice only invokes the failure callback if an error occurs while attempting to send the message.

See Also