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

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

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

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