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

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

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.

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

Invoking the post or postSync methods of a service frame triggers the remote invocation of the service. That is, the arguments serialized in the service frame are transmitted to the remote host and the service body is executed.

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.

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";

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 );
});