Table of Contents
- HopScript Service
- Service Declarations
- Service Constructor
- new hop.Service( [ fun-or-name [, name ] ] )
- Service.exists( name )
- Service.getService( name )
- Service.getServiceFromPath( name )
- Service.allowURL( url )
- Importing Services
- Service Frames
- frame.post( [ success [, fail ] ] )
- frame.postSync()
- frame.call( req )
- frame.setHeaders( obj )
- frame.setOptions( obj )
- HopFrame as URLs
- Service methods & attributes
- service.name
- service.path
- service.resource( file )
- service.timeout
- service.ttl
- service.unregister()
- service.addURL( url )
- service.removeURL( url )
- service.getURLs()
- Interoperable WebServices
- Asynchronous Services
- Examples
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 arguments
object 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:
header
is a JavaScript object containing the properties that the client has put in the request header:host
: the server name and port (for example: `hop.inria.fr:8080` ,)connection
: whether to close the connectionclose
or keep it alivekeep-alive
,clients may add custom header properties in the options argument of the service invocation, for example specifying
{header: { foo: 'foo property value' }}
in thepost
options defines the propertyfoo
that can be retrieved inheader.foo
on the server,
host
is the hostname of the server (as set by the client),port
is the tcp port the request was sent to (as set by the client),path
is the service path (for example/hop/foo
),abspath
,seconds
is the date of arrival of the request,connection
is a shortcut toheader.connection
,"connection-timeout"
is the timeout for keepalive connections,http
is the protocol version requested by the client,scheme
is the protocol scheme requested by the client,method
is the HTTP method used in the request,
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:
- if the response is a string, Hop constructs a
hop.HTTPResponseString
object. - if the response is a XML document, a
hop.HTTPResponseXML
object is constructed. - If the response is a promise, a
hop.HTTPResponseAsync
object is built. - if the response is a JavaScript object.
- if that object has a
toResponse
property which is a function, the result of invoking this function is used as a response.
- if that object has a
- Otherwise, a
hop.HTTPResponseHop
is constructed. This will have the effect of serializing the JavaScript object and re-creating it on the client.
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 ] ] )
fun-or-name
, is either a function or a string. When it is a:- function: this is the function implementing the service. When invoked,
this
is bound to the request object. - string: this is the name of an imported service.
- function: this is the function implementing the service. When invoked,
name
, is an optional string argument, which is the name of the service.
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.
server
, On the server code, this optional argument can be passed a server object that designates the host running the invoked service.fail
, a failure procedure. service. Defaults tofalse
.header
, a JavaScript object to add properties to the HTTP header of the request.
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 postSync
blocks the execution of the client process
until the service returns a value, it is strongly advised to use the
asynchronous version of post
instead.
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:
user
, a user identity on behalf of who the service is invoked.password
, the user password.anim
, a boolean that controls that enables/disables a GUI animation during service execution.
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.path
is /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.
- 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 thehoprc.js
file. - 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:
the service must be declared with named arguments or no arguments, which makes the service compliant to RFC3986. Services with unnamed arguments cannot be invoked from a third party client.
The service should not respond with
hop.HTTPResponseHop
that would not be understood by the client. Others Response constructors deliver contents that are generally handled by most clients. Take care to stringify objects before sending them to the client, and note that string values are received on the client side as[text/plain]
, HTML values are received as[text/html]
.The client should use
GET
orPOST
methods to invoke services. Both theapplication/x-www-form-urlencoded
andmultipart/form-data
Content-Type
options are supported.
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 ) );