JavaScript client API

Installation

To install the VisionAppster JavaScript API to your Node.js environment:

npm i visionappster

To use in your script:

var VisionAppster = require('visionappster');
new VisionAppster.RemoteObject('http://localhost:2015/info').connect()
  .then(function() { console.log('Connected'); });

If you want to use the API on a web page, download VisionAppster.js from your local Engine. A browser-compatible file is also available in the Node.js package at dist/VisionAppster.js and in the SDK directory under your VisionAppster installation (sdk/client/js). To get started, insert this to your HTML document:

<script src="VisionAppster.js"></script>
<script type="text/javascript">
new VisionAppster.RemoteObject('http://localhost:2015/info').connect()
  .then(function() { console.log('Connected'); });
</script>

Usage

If you have an instance of the VisionAppster Engine running locally, open its front page and inspect the source code for a comprehensive example.

Connecting and disconnecting

To connect to a remote object, create an instance of VisionAppster.RemoteObject and call its connect method:

let managerCtrl = new VisionAppster.RemoteObject('http://localhost:2015/manager');
let manager;
manager.connect().then(obj => manager = obj);

Note that in order to use ES2015 features such as the arrow function syntax in Node.js scripts you need to pass the --harmony command-line flag to node or use Babel. The VisionAppster module itself is built using Babel and does not require ES2015 to work.

All methods that need to make requests to the server are asynchronous and return a Promise object. As shown in the example, you can use Promise.then() to fire a callback when the asynchronous operation is done. The connect() method returns a promise that resolves to an object that reflects the functions, properties and signals of the object on the server.

If you need to break a connection, call the disconnect() method.

// isConnected() is a synchronous function
if (managerCtrl.isConnected()) {
  managerCtrl.disconnect().then(() => { console.log('disconnected'); });
}

The interface of a remote object is split into two parts. VisionAppster.RemoteObject provides an interface that lets you to control the object. Upon a successful connection, a reflection object is created. As shown above, the connect() function resolves this object. It is also available through the object member of RemoteObject:

let managerCtrl = new VisionAppster.RemoteObject('http://localhost:2015/manager');
let manager;
manager.connect().then(() => manager = managerCtrl.object);

Instead of explicitly chaining promises, one can use the await keyword in async functions:

async function test() {
  let managerCtrl = new VisionAppster.RemoteObject('http://localhost:2015/manager');
  const manager = await manager.connect();
}

Calling functions

The functions of the object on the server are mapped to methods in the local reflection object instance. You can call remote functions as if they were ordinary methods of the object instance, but instead of returning a value directly, the methods will return a Promise that resolves to the return value. For example:

manager.start('id://my-app-id')
  .then(pid => { console.log(`Process id: ${pid}`); });

A list of functions is available at /functions/. At a minimum, a function description specifies the name of the function. Optionally, there may be a return type and a list or parameter descriptions.

Usually, the most convenient way of calling a function is to pass arguments as a comma-separated list in the order they appear in the function declaration. If the function provides names for its parameters, it is also possible to pack the parameters in an object. This is equivalent to the previous example:

manager.start({appUri: 'id://my-app-id'})
  .then(pid => { console.log(`Process id: ${pid}`); });

Reading and writing properties

The properties of an object on the server are mapped directly to properties on the local reflection object instance. Since reading or writing a property may require a remote call, property accessor functions return a Promise.

manager.lastError.get()
  .then(e => { console.log(`Last error: ${e}`); });

// Trying to set a "const" property
manager.lastError.set('Error')
  .catch(e => { console.log(e); }); // Method Not Allowed

The last example shows how to handle remote call errors. It is a good practice to always catch errors, but doing so may become tedious if an error handler is put on each remote call separately. There is however another, more convenient way:

async function test() {
  try {
    await manager.lastError.set('Error');
  } catch (e) {
    console.log(e); // Method Not Allowed
  }
}

A function and a property may have the same name. In this case the property of the RemoteObject instance also works as a function. The properties of a remote object are listed at properties/.

Receiving signals

To invoke an action upon receiving a signal from the server one needs to connect a handler function to it. We'll do this in an asynchronous function to illustrate how remote calls can be used in an apparently synchronous manner:

async function signalTest(infoCtrl) {
  try {
    const info = await infoCtrl.connect();
    console.log(`VisionAppster AE version: ${await info.appEngineVersion.get()}`);
    info.$userDefinedNameChanged.connect(name => {
      console.log(`Name changed to ${name}`);
    });
  } catch (e) {
    console.log(e);
  }
}

let infoCtrl = new VisionAppster.RemoteObject('http://localhost:2015/info');

signalTest(infoCtrl)
  .then(() => { console.log('done'); });

Signals are separated from functions and properties by a '$' prefix. If a property has a change notifier signal, it will be automatically bound to the $changed member of the property. These two ways of connecting a signal are equivalent:

info.$userDefinedNameChanged.connect(() => {});
info.userDefinedName.$changed.connect(() => {});

The latter is easier as it makes it unnecessary to find out which change notifier signal corresponds to which property.

An arbitrary number of functions can be connected to each signal, and connections can be also be broken:

function showName(name) {
  console.log(name);
}

function showFirstChar(name) {
  console.log(name[0]);
}

async function test() {
  // Connect two handler functions
  await info.userDefinedName.$changed.connect(showName);
  await info.userDefinedName.$changed.connect(showFirstChar);

  // Check if a signal is connected to a specific function.
  // This is a synchronous call.
  if (info.userDefinedName.$changed.isConnected(showFirstChar)) {
    console.log('Yep.');
  }

  // Disconnect one of them
  await info.userDefinedName.$changed.disconnect(snowName);
  // Disconnect everything
  await info.userDefinedName.$changed.disconnect();

  // Check if a signal is connected to any function.
  if (info.userDefinedName.$changed.isConnected()) {
    console.log('This will not happen.');
  }
}

The connection to a signal can be parameterized:

function handleImage(blob) {
  console.log(`Received ${blob.size} bytes of encoded image data.`);
}

async function test() {
  let remote = new VisionAppster.RemoteObject('http://localhost:2015/apis/api-id');
  const obj = await remote.connect();
  // Push the image as JPEG and use Blob as the storage object type.
  await obj.$image.connect(handleImage,
                           {mediaType: ['image/jpeg'], responseType: 'blob'});
}

The mediaType parameter tells the client's preferred encoding for each of the signal's parameters. Depending on the type of the signal, different media types are available. The list of supported encoding schemes evolves constantly and is beyond the scope of this document. It is however always safe to request images as "image/jpeg" or "image/png".

The responseType parameter tells RemoteObject how to handle the encoded data received from the server. The default action is to convert the (usually JSON-encoded) data to an array of function call arguments.

The default behavior can be changed by explicitly specifying dataview or blob as the responseType. The functions connected to the signal will then be called with the raw message data either as a DataView or a Blob. responseType can also be an array, if parameters need different handling. null means default decoding.

obj.$imgAndParams.connect(
  (img, params) => {},
  {mediaType: ['image/jpeg', 'application/json'],
   responseType: ['blob', null]});

Note that there is subtle difference between a media type and an array of media types. If mediaType is a string, it applies to the whole argument array, not individual elements. For example, it is not possible to encode a parameter array as "image/png", but it may be possible to encode a single argument as an image by specifying ["image/png"] as the media type. On the other hand, "application/json" can be used to encode both the whole array and each individual argument, provided that the arguments are representable as JSON.

Finally, it is possible to filter out signal parameters by giving null as the media type. If parameters are filtered and responseType is an array, the number of elements in responseType must match the number of filtered parameters. The following example picks just the second parameter from a signal and passes it to the callback function as a DataView object.

obj.$imgAndParams.connect(
  (params) => { console.log(`${params.buffer.byteLength} bytes`); },
  {mediaType: [null, 'application/json'],
   responseType: ['dataview']});

// This is equivalent (using same responseType for all parameters)
obj.$imgAndParams.connect(
  (params) => { console.log(`${params.buffer.byteLength} bytes`); },
  {mediaType: [null, 'application/json'],
   responseType: 'dataview'});

Callback functions

The mechanism for invoking callback functions is similar to that of signals, with the exception that at most one handler function can be connected to each callback.

// Let's assume there is a callback with the signature
// plus(a: int32, b: int32): int32
// This will return a + b to the server:
obj.$plus.connect((a, b) => a + b);

To find out whether the member of an object is a signal or a callback, you can check the type:

if (obj.$plus instanceof VisionAppster.Callback) {
  console.log('Callback');
} else if (obj.$plus instanceof VisionAppster.Signal) {
  console.log('Signal');
}

Handling errors

Each RemoteObject instance provides a $connectedChanged signal. The signal has a single Boolean parameter that tells the current status of the connection. This signal is delivered locally and requires no remote call when connected or disconnected. Thus, one does not need to await connect(). This signal is especially useful in recovering lost connections:

ctrl.$connectedChanged.connect(connected => {
  if (!connected) {
    console.log('Connection lost. Reconnecting in a second.');
    setTimeout(() => { ctrl.connect() }, 1000);
  }
});

If the connection breaks spontaneously, calling connect() will try to automatically re-register all pushable sources (signals and callbacks) to the return channel. If you call disconnect() yourself, all connections must be manually re-established after reconnecting.

Other errors such as unexpected server responses and failures in decoding pushed data are signaled through the $error signal. The signal has one parameter that is an Error object. These errors are usually recoverable and don't cause a connection failure.

ctrl.$error.connect(error => console.log(error.message));

Errors in functions that are called directly must be handled by the caller. In the simplest case:

ctrl.connect().catch(e => console.log(e));