visionappster
v1.0.1
Published
VisionAppster Engine client API
Downloads
22
Maintainers
Readme
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.
// isConnected() is a synchronous function
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}`); });
// 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.');
}
}
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();
// Push the image as JPEG and use Blob as the storage object type.
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.
// Let's assume there is a callback with the signature
// int32 plus(int 32 a, int32 b)
// 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');
}
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
.
// Returns 16
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));