Table of Contents

HopScript Service

A Hop.js service is a function that is that is callable through the network. The service is declared on the server side, and invoked from a Hop.js client process, a Hop.js application running on a web browser, or a third party application (services are built on top of HTTP, they can be invoked using the hop.js API or from handcrafted GET and POST HTTP requests).

The name of the service defines the URL under which it is known to the web. The URL is building by prefixing the service name with the string /hop/. That is, the URL associated with a service myService will be /hop/myService, for instance invoking with a qualified URL such as http://myhost:8080/hop/myService.

Invoking a service builds a service frame. This frame can be used to actually invoke the service. If the service declaration used named arguments the frame can be automatically built out of a standard URI specified in RFC3986.

Example:

svc1/svc1.js

function computeFact( n ) {
   if( n <= 1 ) {
      return n;
   } else {
      return computeFact( n - 1 ) * n;
   }
}

service fact( n ) {
   return computeFact( n );
}

service svc1() {
   var input = <input size="5"/>;
   var result = <div/>;

   return <html>
     ${input}
     <button onclick=~{
        var inputVal = ${input}.value;
        ${fact}( inputVal )
           .post( function( res ) {
              ${result}.innerHTML = "fact(" + inputVal + ") = " + res;
           })}>
      compute!
     </button>
     ${result}
   </html>
}

console.log( "Go to \"http://%s:%d/hop/svc1\"", hop.hostname, hop.port );

Service Declarations

service[ name ]( [ arguments ] ) { body }

The syntax of service declarations is as follows:

<ServiceDeclaration> 
    service <Identifier> ( <FormalParameterListopt> ) 
       { <FunctionBody> }

<ServiceExpression> 
  | <ServiceDeclaration>
  | service ( <FormalParameterListopt> ) 
       { <FunctionBody> }

<ServiceImport>  service <Identier> ()

Examples:

function checkDB( fname, lname ) {
   return (fname + ":" + lname) in DB;
}
service svc1( fname, lname ) { return checkDB( fname, lname ) }
service svc2( {fname: "jean", lname: "dupond"} ) { return checkDB( fname, lname ) }

var srv = new hop.Server( "cloud.net" );
srv.getVersion = service getVersion();

Services have a lot in common with ordinary functions, they can be declared in statements, or within expressions. Service expressions can be named or anonymous.

Services can be invoked as soon as they are declared.

Service arguments are handled like in a function declaration, missing arguments are undefined in the function body, extra arguments are ignored, and the argumentsobject contains all the arguments passed to the service invocation.

Contrary to functions, services can also be declared with an object literal in place of the formal parameter list. This special form has two purposes: supporting RFC3986 compliant requests (keys correspond to the URI keys), and providing default values for missing arguments.

Note: When used within a service declaration, this is associated with the runtime request object corresponding to a service invocation. This object contains all the information about the current HTTP request.

Example:

service svc( name ) {
  console.log( "host=", this.host, " port=", this.port );
  console.log( "abspath=", this.abspath );
  return true;
}

Usable properties of the request object are listed below:

Service are free to return any serializable object. The value is first converted into a hop.HTTPResponse object by Hop.js. This converted value is sent to the client. The rules for converting values into hop.HTTPResponse are as follows:

The various Hop responses classes are documented here.

Service Constructor

Hop.js services are instances of the Service constructor.

new hop.Service( [ fun-or-name [, name ] ] )

Example:

function svcImpl( name, lname ) { return <html>${name},${lname}</html> };

// create an anonymous service with fixed arguments
var priv = new hop.Service( svcImpl );

// call the service
priv( "jeanne", "durand" ).post();
// will return <html> jeanne, durand </html>

Service.exists( name )

Returns true if the service exists, returns false otherwise.

Example:

Service.exists( "public" )
// true
Service.exists( "private" );
// false

Service.getService( name )

Returns the service named name. Raises an error if the service does not exist.

Example:

Service.getService( "myService" )

Service.getServiceFromPath( name )

Returns the service whose base url is name. Raises an error if the service does not exist.

Example:

Service.getService( "/hop/myService" )

Service.allowURL( url )

Add the url string to the list of URLs that can be used to alias services (see method service.addURL).

Importing Services

The implementation of a Hop service is not required to be known for being invoked by a remote client. This client can import the service and use it as if it was locally defined. The syntax for importing a service is as follows:

<ServiceImport> 
    service <Identifier> () 

Imported services are used as locally defined service.

This example shows how to import remote services. The following example simulates a remote service named dummy whose implementation is not available from the main program. The dummy service is made available using an import clause.

svc2/svc2.js

require( "./extern.js" );

service svc2() {
   return <html>
      <button onclick=~{
         ${dummy}( { b: 22 } )
            .post( function( r ) {
               document.body.appendChild(
                  <table>
                    ${r.map( function( e ) {
                       return <tr><th>${ e.head }</th><td>${ e.data }</td></tr>
                    } )}
                  </table>
               ) } ) }>
         add
       </button>
   </html>;
}

service dummy();

console.log( "Go to \"http://%s:%d/hop/svc2\"", hop.hostname, hop.port );

svc2/extern.js

service implementation( o ) {
   var a = o && "a" in o ? o.a : 10;
   var b = o && "a" in o ? o.b : 11;
   return [ { head: a, data: b }, { head: b, data: a } ];
}

implementation.path = "/hop/dummy";

Service Frames

Invoking a service returns a HopFrame object that can later spawn the execution of the service body.

Example:

var frame = svc2( { lname: "durant" } );
typeof( frame );           // "object"
frame instanceof HopFrame; // true
frame.toString();          // /hop/svc2?hop-encoding=hop&vals=c%01%02(%01%0...

When a service is used as a method of a server object, the returned frame is bound to that server.

var srv = new hop.Server( "cloud.net", 8888 );
srv.svc2 = service catalog();
var frame = srv.svc2( "music" );
typeof( frame );           // "object"
frame instanceof HopFrame; // true
frame.toString();          // http://cloud.net:8888/hop/catalog?hop-encoding=hop&vals=...

When a service is used as a method of a websocket object, the returned frame is bound to that websocket. In that case, the invokation (argument passing and result return) use the websocket instead of creating a new HTTP connection.

var ws = new WebSocket( "ws://cloud.net:" + port + "/hop/serv" );
var frame = svc2.call( ws, "music" );
typeof( frame );           // "object"
frame instanceof HopFrame; // true
frame.post()
   .then( result => doit( result ) )
   .catch( reason => handle( reason ) )

A HopFrame implements the methods described in the section.

frame.post( [ success [, fail ] ] )

Invokes asynchronously the service. The optional success argument, when provided, must be a function of one argument. The argument is the value returned by the service.

Example:

svc2( { name: "dupond" } )
   .post( function( r ) { console.log( r ); } );

If the optional argument fail is a procedure, it is invoked if an error occurs while invoking the service.

When no argument are passed, post returns a promise that resolves on successful completion and that otherwise rejects.

Example:

var srv = new hop.server( "remote.org", 443, true );
var config = {
  header: { "X-encoding", "my-encoding" }
};

svc2( { name: "dupond" } )
   .post( function( r ) { console.log( r ); }, srv, config );

frame.postSync()

The synchronous version of post. Returns the value returned by the service. Since postSyncblocks the execution of the client process until the service returns a value, it is strongly advised to use the asynchronous version of postinstead.

frame.call( req )

Invokes the function associated with service, with req as the this. This method can only be invoked from the server-side code that defines the service.

frame.setHeaders( obj )

Returns the frame object. Set header attributes to the frame.

service svc();

svc( "jean", "dupont" )
  .setHeaders( { "X-Version: "v1.0" } )
  .post( v => console.log );

frame.setOptions( obj )

Returns the frame object. Set options to the frame, the attributes of obj can be:

service svc();

svc( "jean", "dupont" )
  .setOptions( { password: "nopass" } )
  .post( v => console.log );

HopFrame as URLs

HopFrame can be everywhere a URL is expected, in particular, in HTML nodes. For instance, the src attribute of an image can be filled with an HopFrame. In that case, the content of the image will be the result of the service invocation.

Example:

service getImg( file ) {
  if( !file ) {
     return hop.HTTPResponseFile( DEFAULT_IMG );
  } else {
     return hop.HTTPResponseFile( ROOT + "/" + file );
  }
}

service page() {
   return <html>
      <img src=${getImg( false )}.toString()/>
      <img src=${getImg( "monalisa.jpg" )}.toString()/>
   </html>
}

Service methods & attributes

service.name

The name of the associated service, which the the service.path without the /hop prefix.

service.path

The path (i.e., the absolute path of the URL) of the associated service. This string must be prefixed by /hop/.

Example

svc2.path = "/hop/dummy";

When a named service is declared, the default value for service.pathis /hop/<service-name>. Anonymous services get a unique path built by hop, prefixed by /hop/public/. Changing the service path can be done at any time. A path value which is currently assigned to a service cannot be assigned to another service.

Note: Services are global resources of a hop.js server. Services declared in a worker cannot use an already assigned path. This is the cost to pay to benefit from automatic routing of service invocations to the proper worker thread.

service.resource( file )

Create the absolute path relatively to the file defining the service. For instance, this can be used to obtained the absolute path of a CSS file or an image whose name is known relatively to the source file defining the service.

Example

This example shows service definitions invocations. The example creates two services. The first one, svc, accepts no parameter. The second one, svc1, accepts optional named arguments. Each optional arguments holds a default value that is used if the argument is not provided.

svc/svc.js

var hop = require( "hop" );

service svc() {
   var conn = <div/>;
   return <html>
     <button onclick=~{
        ${svc1}().post( function( r ) { document.body.appendChild( r ) } ) }>
       add "10, 11, 12"
     </button>
     <button onclick=~{
        ${svc1}( {c: "c", b: "b", a: "a"} )
           .post( function( r ) { document.body.appendChild( r ) } ) }>
       add "a, b, c"
     </button>
     <button onclick=~{
        ${svc1( {c: 6, b: 5, a: 4} )}
           .post( function( r ) { document.body.appendChild( r ) } ) }>
       add "4, 5, 6"
     </button>
     <button onclick=~{
        ${svc2}( "A", "B", "C" )
           .post( function( r ) { document.body.appendChild( r ) } ) }>
       add "A, B, C"
     </button>
     <button onclick=~{
        ${svc2( 100, 200, 300 ) }
           .post( function( r ) { document.body.appendChild( r ) } ) }>
       add "100, 200, 300"
     </button>
     <button onclick=~{
        document.body.appendChild( <div>${${svc1}.resource( "svc.js" )}</div> );
        document.body.appendChild( <div>${${svc1.resource( "svc.js" )}}</div> );
     }>
       add source path twice
     </button>
     ${conn}
   </html>;
}

service svc1( o ) {
   var a = o && "a" in o ? o.a : 10;
   var b = o && "a" in o ? o.b : 11;
   var c = o && "c" in o ? o.c : 12;
   return <div> 
     <span>${a}</span>, 
     <span>${b}</span>, 
     <span>${c}</span>
   </div>;
}

service svc2( a, b, c ) {
   return <div> 
     <span>${a}</span>, 
     <span>${b}</span>, 
     <span>${c}</span>
   </div>;
}

console.log( "Go to \"http://%s:%d/hop/svc\"", hop.hostname, hop.port );

service.timeout

The number of seconds the service is live. Negative values means infinite timeout.

Example

console.log( svc2.timeout );

service.ttl

The number of time the service can be invoked. Negative values mean infinite time-to-live.

Example

svc2.ttl = 5;

service.unregister()

Unregister a service from the Hop.js server. Once unregistered services can no longer be invoked in response to client requests.

service.addURL( url )

Adds another public URL to the service. This URL is not required to be prefixed with /hop as public URL automatically associated with services are.

An additional URL can be added to a service under the following conditions.

  1. It has been previously added to the list of the alias URLs via the method Service.allowURL. This method can only be invoked from with the hoprc.js file.
  2. The URL is not already associated with another service.

Unless these two conditions hold, a runtime error is raised.

Examples:

service mySvc( o ) {
   console.log( "o=", o );
   return <html>
     v=${o.v}
   </html>
}

mySvc.addURL( "/" );
mySvc.addURL( "/bar" );

service.removeURL( url )

Remove an alias URL from service. The automatic URL cannot be removed.

service.getURLs()

Returns the vector of the current alias URL.

Interoperable WebServices

Services may be invoked from third party clients, allowing the Hop server to deliver WebServices to these clients. To do so, a few interoperability rules must be satisfied:

Server Example

service getRecord( o ) {
   var name = "name" in o ? o.name : "main";
   var record;
   switch (name) {
     case 'main': record = { host: 'hop.inria.fr', port : 80 };
        break;
     case 'game': record = { host: 'game.inria.fr', port: 8080 };
        break;
     default: record = {};
   };
   return JSON.stringify( record );
}

Client Side, service invocation

getRecord( { name: 'game' } ).post( function( result ) {
   var record = JSON.parse( result );
   console.log( 'http://%s:%s', record.host, record.port );
});

Client side, Hop.js WebService API

var util = require( 'util' );
var serverURL = util.format( 'http://%s:%s/hop/getRecord', hop.hostname, hop.port );
var webService = hop.webService( serverURL );
var frame = webService( { name : 'game' } );
frame.post( function( result ) {
   var record = JSON.parse( result );
   console.log( 'http://%s:%s', record.host, record.port );
});

Asynchronous Services

Hop services can either be synchronous or asynchronous. A synchronous service directly returns its response to its client, using a normal return statement. Asynchronous services postpone their responses. For that, instead of returning a value, they simply return a JavaScript promise. When this promise resolves, its resolution is sent to the client as response of the service. If the promise reject, the error object is propagated to the client.

Example

This example shows how to use asynchronous responses. Asynchronous responses are needed, when a service cannot respond instantly to a request. This happens when the service execution relies on a asynchronous computation, which can either results from a remote service invocation or an asynchronous function call.

This example uses the standard fs.readFile function which is asynchronous. It returns instantly a registers a callback function that is to be called when the characters of the file are all read.

Asynchronous responses are implemented by ECMAScript 6 promises. That is, when a service returns a promise, this promise is treated by the server as an asynchronous response. When executor of the promise resolves, the resolved value is transmitted to the client.

In the example, once the characters are read, they are fontified using the hop hop.fontifier module. Then an Html document is built, which is eventually shipped to the client using the resolve function.

asvc/asvc.js

var fs = require( "fs" );
var fontifier = require( hop.fontifier );

service asvc() {
   return new Promise( function( resolve, reject ) {
      fs.readFile( asvc.resource( "asvc.js" ), "ascii",
                   function( err, data ) {
                      resolve( <html>
                        <head css=${fontifier.css}/>
                        <pre class="fontifier-prog">
${fontifier.hopscript( data )}
                        </pre>
                      </html> ) } );
      } );
}

console.log( 'Go to "http://%s:%d/hop/asvc"', hop.hostname, hop.port );

Examples

Simple invocations

This example shows service definitions invocations. The example creates two services. The first one, svc, accepts no parameter. The second one, svc1, accepts optional named arguments. Each optional arguments holds a default value that is used if the argument is not provided.

svc/svc.js

var hop = require( "hop" );

service svc() {
   var conn = <div/>;
   return <html>
     <button onclick=~{
        ${svc1}().post( function( r ) { document.body.appendChild( r ) } ) }>
       add "10, 11, 12"
     </button>
     <button onclick=~{
        ${svc1}( {c: "c", b: "b", a: "a"} )
           .post( function( r ) { document.body.appendChild( r ) } ) }>
       add "a, b, c"
     </button>
     <button onclick=~{
        ${svc1( {c: 6, b: 5, a: 4} )}
           .post( function( r ) { document.body.appendChild( r ) } ) }>
       add "4, 5, 6"
     </button>
     <button onclick=~{
        ${svc2}( "A", "B", "C" )
           .post( function( r ) { document.body.appendChild( r ) } ) }>
       add "A, B, C"
     </button>
     <button onclick=~{
        ${svc2( 100, 200, 300 ) }
           .post( function( r ) { document.body.appendChild( r ) } ) }>
       add "100, 200, 300"
     </button>
     <button onclick=~{
        document.body.appendChild( <div>${${svc1}.resource( "svc.js" )}</div> );
        document.body.appendChild( <div>${${svc1.resource( "svc.js" )}}</div> );
     }>
       add source path twice
     </button>
     ${conn}
   </html>;
}

service svc1( o ) {
   var a = o && "a" in o ? o.a : 10;
   var b = o && "a" in o ? o.b : 11;
   var c = o && "c" in o ? o.c : 12;
   return <div> 
     <span>${a}</span>, 
     <span>${b}</span>, 
     <span>${c}</span>
   </div>;
}

service svc2( a, b, c ) {
   return <div> 
     <span>${a}</span>, 
     <span>${b}</span>, 
     <span>${c}</span>
   </div>;
}

console.log( "Go to \"http://%s:%d/hop/svc\"", hop.hostname, hop.port );

WebSocket invocations

This example shows how to invoke services using websockets. Websocket connections trade space for efficiency. They are faster than HTTP connections as they avoid creating fresh sockets per call but they require permanent server links.

wspost/wsserver.js

var serv = new WebSocketServer( { path: "serv", protocol: "foo" } );

service obj( o ) {
   o.a++;
   o.b = "obj ok";
   return o;
}

service str( o ) {
   if( o.a > 10 ) {
      return "ok strict";
   } else {
      return hop.HTTPResponseString( "error string",
                                     { startLine: "HTTP/1.0 404 File not found" } );
   }
}

service asyn( o ) {
   o.a++;
   o.b = "asyn ok";
   return new Promise( function( resolve, reject ) {
      setTimeout( function( e ) { resolve( o ) }, 1000 );
   } );
}

console.log( "wsserver ready" );

wspost/wsclient.js

var port = parseInt( process.argv[ process.argv.length - 1 ] );
var ws = new WebSocket( "ws://localhost:" + port + "/hop/serv" );

ws.obj = service obj();
ws.asyn = service asyn();
ws.str = service str();

var f1 = ws.obj( { a: 1 } );
var f2 = ws.asyn( { a: 2 } );
var f3 = ws.str( { a: 20 } );
var f4 = ws.str( { a: 2 } );
var f5 = ws.obj( { a: 3 } );

f1.post()
   .then( result => console.log( "obj result=", result ) )
   .catch( reason => console.log( "obj reason=", reason ) );

f2.post()
   .then( result => console.log( "asyn result=", result ) )
   .catch( reason => console.log( "asyn reason=", reason ) );

f3.post()
   .then( result => console.log( "str3 result=", result ) )
   .catch( reason => console.log( "str3 reason=", reason ) );

f4.post()
   .then( result => console.log( "str4 result=", result ) )
   .catch( reason => console.log( "str4 reason (expected)=", reason ) );