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
class EmployeesPrx extends Ice.ObjectPrx
{
    getName(number, context) { ... }
    getAddress(number, context) { ... }
}
TypeScript
abstract class EmployeesPrx extends Ice.ObjectPrx
{
    getName(number:number, context?:Map<string, string>):Ice.AsyncResult<string>;
    getAddress(number:number, context?:Map<string, string>):Ice.AsyncResult<string>;
}


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, context); // Returns Ice.AsyncResult
TypeScript
op(inp1:number, inp2:string, context?:Map<string, string>):Ice.AsyncResult<[number, boolean, Ice.Long]>;


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
class ClientToServerPrx extends Ice.ObjectPrx
{
    op1(i, f, b, s, context) { ... }
    op2(ns, ss, st, context) { ... }
    op3(proxy, context) { ... }
}
TypeScript
abstract class ClientToServerPrx extends Ice.ObjectPrx
{
    op1(i:number, f:number, b:boolean, s:string, context?:Map<string, string>):Ice.AsyncResult<void>;
    op2(ns:NumberAndString, ss:StringSeq, st:StringTable, context?:Map<string, string>):Ice.AsyncResult<void>;
    op3(proxy:ClientToServerPrx, context?:Map<string, string>):Ice.AsyncResult<void>;
}


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

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

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

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

const ns = new NumberAndString();
ns.x = 42;
ns.str = "The Answer";
const ss = [];
ss.push("Hello world!");
const 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
(async function()
{
    const [retval, value] = await proxy.execute("--file log.txt");
    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 are two exceptions to this rule:

  • if you destroy the communicator and then make an invocation, the method invocation throws CommunicatorDestroyedException directly.
  • a call to a method that makes remote invocations can throw TwowayOnlyException. This exception is throw when you call an operation that has a return value or out-parameters on a oneway proxy.

AsyncResult Type in JavaScript

The AsyncResult object returned by an Ice API call encapsulates the state of an asynchronous invocation. The Ice.AsyncResult class inherits from the standard JavaScript Promise.

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:

  • AsyncResult.prototype.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.
  • AsyncResult.prototype.isCompleted()
    This method returns true if, at the time it is called, the result of an invocation is available. Otherwise, if the result is not yet available, the method returns false.
  • AsyncResult.prototype.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.
  • AsyncResult.prototype.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).

  • AsyncResult.prototype.throwLocalException()
    This method throws the local exception that caused the invocation to fail. If no exception has occurred yet, throwLocalException does nothing.

Completion Callbacks in JavaScript

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

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

For all asynchronous Ice APIs, the failure callbacks receives as its only argument the exception that caused the failure. The arguments pass to the success callback depends on the operation signature, for operations that return multiple values the callback receives an array containing the return value if any followed by the all out parameters, for operations with a single return value that is the argument of the success callback.  Recall the example we showed earlier:

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

On success the promise is resolved with an array containing the return value and out parameters, if the invocation fails because of an Ice run time or user exception the promise is rejected with the exception that cause the failure:

JavaScript
p.op(...).then(r =>
    {
        const [returnVal, outp1, outp2] = r;
        ...
    }
).catch(ex
    {
       console.log(`Operation failed ${ex.toString()}`);
    });
TypeScript
p.op(...).then(r:AsyncResult<[number, boolean, Ice.Long]> =>
    {
        const [returnVal:number, outp1:boolean, outp2:Ice.Long] = r;
        ...
    }
).catch(ex =>
    {
       console.log(`Operation failed ${ex.toString()}`);
    });



Using await in JavaScript

While inside an asynchronous function (i.e., a function marked with the async keyword), you may also use the await keyword to give your code a control flow similar to that of a synchronous program. We can rewrite the example from the previous section as follows:

JavaScript
(async function()
{
    try
    {
        const [returnVal, outp1, outp2] = await p.op(...);
    }
    catch(ex)
    {
        console.log(ex);
    }
}());
TypeScript
(async function()
{
    try
    {
        const [returnVal:number, outp1:boolean, outp2:Ice.Long] = await p.op(...);
    }
    catch(ex)
    {
        console.log(ex);
    }
}());


The target of the await keyword is a promise.

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 TwowayOnlyException.

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 no arguments.

Asynchronous Batch Requests in JavaScript

You can invoke operations via batch 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 TwowayOnlyException.

The callback functions look exactly as for a twoway invocation. The failure function is only called if the batch invocation raised an exception before being queued. The success function takes no arguments. The returned promise for a batch oneway invocation is always completed and indicates the successful queuing of the batch invocation. It can also be marked completed if an error occurs before the request is queued.

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 no arguments. Ice only invokes the failure callback if an error occurs while attempting to send the message.

See Also