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 (iife bundle) 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:
const ctrl = new VisionAppster.RemoteObject('http://localhost:2015/manager');
let manager;
ctrl.connect().then(obj => manager = obj);
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 with 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.
if (ctrl.isConnected()) {
ctrl.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
:
const ctrl = new VisionAppster.RemoteObject('http://localhost:2015/manager');
let manager;
ctrl.connect().then(() => manager = ctrl.object);
Instead of explicitly chaining promises, one can use the await
keyword
in async
functions:
async function test() {
const ctrl = new VisionAppster.RemoteObject('http://localhost:2015/manager');
const manager = await ctrl.connect();
}
Return channel
The JS client uses a WebSocket connection as a return channel to push
data from the server. The return channel is used to pass signals,
callbacks and asynchronous function call results.
Unless instructed otherwise, the JS client establishes a single
WebSocket connection per host. Thus, if many remote objects are accessed
on the same server, they all use a shared return channel.
Return channels are identified by a client ID that is generated on the
client side. By convention,
UUIDs are
used. The JS client automatically generates one for each return channel,
but it is also possible to pass a pre-generated UUID to the
RemoteObject
constructor. This may be useful if you want to control
which objects share a return channel. To give one object a dedicated
channel, generate the client ID yourself:
const clientId = generateClientUuid();
const ctrl = new VisionAppster.RemoteObject({
url: 'http://localhost:2015/manager',
clientId: clientId
});
In some cases, you may want to use a remote object without a return
channel. For example, if you just want to set or retrieve the value of a
property or call a function, establishing a WebSocket connection would
be an overkill. The return channel can be disabled by explicitly passing
null
as the client ID:
const ctrl = new VisionAppster.RemoteObject({
url: 'http://localhost:2015/manager',
clientId: null
});
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 with 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}`));
Although calling functions with named arguments is handy, it comes with
a pitfall: if the function to be called takes an object as the first
argument, the caller must wrap the object into an array:
remoteObject.funcThatTakesObject([{key: "value"}]);
When a remote function is called, the server responds with the return
value. This synchronous mode of operation has the disadvantage that it
blocks the HTTP connection to the server: a new request can only be
served once the previous one has finished. With simple functions this is
usually not an issue, but may become such with functions that take time
to complete.
Asynchronous calls can be utilised to avoid blocking the HTTP
connection. When an asynchronous request is made, the server puts the
call in a queue, returns immediately and pushes the results back through
the client’s return channel once the function finishes.
Just like other function calls, asynchronous calls return a Promise
that resolves with the return value of the called function. To call a
function asynchronously, append .async
to the function name:
manager.start.async({appUri: 'id://my-app-id'})
.then(pid => console.log(`Async call returned a pid: ${pid}`))
.catch(e => console.log('Async call failed.'));
If the server is not able to enqueue the request or if it does not
respond in a timely manner, the returned promise will be rejected. The
asyncTimeout
member of the function can be used to adjust the timeout:
manager.start.asyncTimeout = 10000;
If the client is connected without a return channel, async functions
will not be available.
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}`); });
manager.lastError.set('Error')
.catch(e => { console.log(e); });
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);
}
}
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() {
await info.userDefinedName.$changed.connect(showName);
await info.userDefinedName.$changed.connect(showFirstChar);
if (info.userDefinedName.$changed.isConnected(showFirstChar)) {
console.log('Yep.');
}
await info.userDefinedName.$changed.disconnect(snowName);
await info.userDefinedName.$changed.disconnect();
if (info.userDefinedName.$changed.isConnected()) {
console.log('This will not happen.');
}
}
By default, the remote object system will find a suitable encoding and
data type for the signal’s parameters automatically. In some cases, the
result may however not be what you want. A typical example is an image
you want to just display instead of processing it on the client side. In
such cases, it is possible to parameterize the connection:
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();
await obj.$image.connect(handleImage, {mediaType: ['image/jpeg']});
}
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”.
Note that there is a 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
all of the arguments are representable as JSON.
Finally, it is possible to filter out signal parameters by giving null
as the media type. This will save bandwidth if you don’t need all of the
parameters. If all parameters are filtered out, the server will send an
empty message.
obj.$imgAndParams.connect(
(params) => console.log('received', params),
{mediaType: [null, 'application/json']});
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.
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');
}
Callbacks also work as function call arguments. Let’s assume the server
provides a function that has two arguments: a callback function and a
value that will be passed to the callback. The function returns whatever
the callback returns. The signature of the function is
call: (callback: (double) -> double, value: double) -> double
.
let sixteen = await obj.call(value => value * value, 4);
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));