= yaml = title: “Fortune in JS” fullTitle: “Fortune in JavaScript” layout: tutorial wherein: you build a fortune teller service and a client to talk to it. sort: 4 toc: true prerequisites: {completer: js-fortune, scenario: e, also: {jsSpecial: true, chrome: true}} = yaml =
This tutorial creates a fortune teller example in order to go more deeply into some Vanadium concepts than Hello Peer. In addition, this tutorial demonstrates how to connect the application with a compatible Go-language fortune application.
JavaScript Vanadium applications are based around the same fundamental structure as Go Vanadium applications. Because of the similarities, this tutorial focuses on the differences between building Vanadium applications in Go and JavaScript.
In the Hello Peer tutorial, VDL (Vanadium Definition Language) was not used. It is possible to communicate without VDL for simple applications, but it is strongly recommended to use VDL for cross-platform, cross-language, or more complex applications. VDL provides a clear, strongly-typed definition of the protocols that your clients and servers will use to communicate with each other.
When not using VDL, different programming languages may have different representations of values that may not be coerced to compatible types. In particular, a special representation is used for JavaScript values when VDL is not being used that may be incompatible with Go values.
We will reuse the same interface definition from the Client/Server Basics tutorial. $V_TUT/src/fortune/ifc/fortune.vdl
defines a fortune teller service, which allows clients to Get
and Add
fortunes. The relevant code is reproduced below:
{{# helpers.code }} package fortune
type Fortune interface { // Returns a random fortune. Get() (wisdom string | error) // Adds a fortune to the set used by Get(). Add(wisdom string) error } {{/ helpers.code }}
The vdl
tool generates a JavaScript file from this protocol definition.
mkdir -p $V_TUT/src/fortune VDLROOT=$V23_RELEASE/src/v.io/v23/vdlroot \ VDLPATH=$V_TUT/src \ $V_BIN/vdl generate -lang=javascript -js-out-dir=$V_TUT/src \ $V_TUT/src/fortune/ifc
When generating JavaScript, the flag -js-out-dir
is used to specify where generated files should appear. After running this command, $V_TUT/src/fortune/ifc/index.js
should have been created.
The code below implements the Fortune service. It defines the Get
and Add
methods and attaches the VDL-generated interface description to the service's prototype chain.
Create $V_TUT/src/fortune/service/index.js
.
mkdir -p $V_TUT/src/fortune/service cat - <<EOF >$V_TUT/src/fortune/service/index.js var vdlFortune = require('../ifc'); module.exports = FortuneService; // Define the fortune service. function FortuneService() { this.fortunes = [ 'You will reach the heights of success.', 'Conquer your fears or they will conquer you.', 'Today is your lucky day!', ]; this.numFortunesServed = 0; } // Add VDL service metadata and type information. FortuneService.prototype = new vdlFortune.Fortune(); // Define the FortuneServiceMethod bodies. FortuneService.prototype.add = function(ctx, serverCall, wisdom) { this.fortunes.push(wisdom); } FortuneService.prototype.get = function(ctx, serverCall) { this.numFortunesServed++; var fortuneIndex = Math.floor(Math.random() * this.fortunes.length); return this.fortunes[fortuneIndex]; }; EOF
This service implementation is analogous to Go's. It also exposes two fields, fortunes
and numFortunesServed
, which will be used to make the fortune teller more interactive.
Two files are used to serve the Fortune service: the JavaScript file that serves the service and an HTML page to display the server status.
Use a Vanadium server to serve the Fortune service.
mkdir -p $V_TUT/src/fortune/server cat - <<EOF >$V_TUT/src/fortune/server/index.js var vanadium = require('vanadium'); var FortuneService = require('../service'); // Define the Vanadium configuration for this app. var config = { logLevel: vanadium.vlog.levels.INFO, appName: 'Fortune Server' }; // Setup Vanadium and serve the Fortune service. vanadium.init(config, function(err, runtime) { if (err) { return displayError(err); } runtime.on('crash', displayError); // Create and serve the Fortune service. var service = new FortuneService(); var serviceName = getDefaultServiceName(runtime.accountName); runtime.newServer(serviceName, service, function(err) { if (err) { displayError(err); } }); // Initialize the UI (see fortune-server.html). uiInit(service, serviceName); }); function getDefaultServiceName(accountName) { var homeDir = accountName.replace(/^dev.v.io:u:/, 'users/').replace(vanadium.security.ChainSeparator.val, '/'); return homeDir + '/tutorial/fortune'; } EOF
The Fortune server works similarly to the one in the Client/Server Basics tutorial. The service implementation:
The following sections describe some key differences between the JavaScript and Go APIs.
A configuration can optionally be specified when initializing Vanadium, for example:
{{# helpers.code }} var config = { logLevel: vanadium.vlog.levels.INFO, appName: ‘Fortune Server’ }; {{/ helpers.code }}
This sets the output level for Vanadium log messages to INFO
(WARN
is the default) and sets the name of the application, which is primarily used to identify the application in error messages.
Similar parameters are typically specified through command line flags with Go.
The Fortune service was served using a Vanadium name. To learn more about names and name resolution, read the Naming Concepts page or go through the Naming tutorial.
{{# helpers.code }} // Serve the service at a Vanadium name. server.serve(name, service, callback); {{/ helpers.code }}
In the Client/Server Basics tutorial, the Fortune client connected to the server directly using a name rooted at its endpoint address. In contrast, the server in this tutorial uses a name that does not include an endpoint address and is resolved against a mount table.
Create the HTML page. This page displays:
The service will manipulate the page as its data is updated.
cat - <<EOF >$V_TUT/fortune-server.html <!DOCTYPE html> <html> <head> <title>Fortune Teller - Server</title> <script> // Helpers to display status information on the page. function displayError(err) { displayNumFortunesServed('Error: ' + err.toString()); } function displayNumFortunesServed(count) { document.getElementById('fortune-count').innerHTML = count; } function displayFortunes(fortunes) { var fortuneList = document.getElementById('fortune-list'); // Assume that only new fortunes can be added to the end list. for (var i = fortuneList.childNodes.length; i < fortunes.length; i++) { var bullet = document.createElement('li'); bullet.textContent = fortunes[i]; fortuneList.appendChild(bullet); } } function setServiceName(serviceName) { return document.getElementById('service-name').textContent = serviceName; } function uiInit(service, serviceName) { setServiceName(serviceName); setInterval(function() { displayNumFortunesServed(service.numFortunesServed); displayFortunes(service.fortunes); }, 250); } </script> </head> <body> <h1>Server</h1> <p> <span>Name of service to provide to clients: </span> <span id="service-name"></span> </p> <p> List of fortunes: <br><ol id="fortune-list"></ol></br> </p> <p> Total Fortunes Sent: <span id="fortune-count">0</span> </p> <script src="browser/fortune-server.js"></script> </body> </html> EOF
As with the server, the client consists of two files: a JavaScript file that contains the application logic and an HTML page for the user interface.
The client code starts Vanadium and waits for the user to act.
Add
button, the client makes an Add
RPC request.Get
button, the client makes a Get
RPC request.Create $V_TUT/src/fortune/client/index.js
.
mkdir -p $V_TUT/src/fortune/client cat - <<EOF >$V_TUT/src/fortune/client/index.js var vanadium = require('vanadium'); // Define the Vanadium configuration for this app. var config = { logLevel: vanadium.vlog.levels.INFO, appName: 'Fortune Client' }; vanadium.init(config, function(err, runtime) { if (err) { displayError(err); return; } // Get runtime context and client. var context = runtime.getContext(); var client = runtime.getClient(); // Set default service name. var defaultName = getDefaultServiceName(runtime.accountName); setServiceName(defaultName); // Listen for button presses. document.getElementById('get-button').addEventListener('click', getFortune); document.getElementById('add-button').addEventListener('click', addFortune); // Adds a fortune to the fortune teller. function addFortune() { updateStatus('Adding ' + getEnteredFortune() + '...'); client.bindTo(context, getServiceName(), function(err, s) { if (err) { displayError(err); return; } s.add(context, getEnteredFortune(), function(err) { if (err) { displayError(err); return; } updateStatus('Done!'); }); }); } // Gets a random fortune from the fortune teller. function getFortune() { updateStatus('Getting random fortune...'); client.bindTo(context, getServiceName(), function(err, s) { if (err) { displayError(err); return; } s.get(context, function(err, randomFortune) { if (err) { displayError(err); return; } displayFortune(randomFortune); updateStatus('Done!'); }); }); } }); function getDefaultServiceName(accountName) { var homeDir = accountName.replace(/^dev.v.io:u:/, 'users/').replace(vanadium.security.ChainSeparator.val, '/'); return homeDir + '/tutorial/fortune'; } EOF
Create the client's HTML page. This page contains:
cat - <<EOF >$V_TUT/fortune-client.html <!DOCTYPE html> <html> <head> <title>Fortune Teller - Client</title> <script> // Helpers to update and introspect the HTML page. function getServiceName() { return document.getElementById('service-name').value; } function setServiceName(serviceName) { return document.getElementById('service-name').value = serviceName; } function getEnteredFortune() { return document.getElementById('add-text').value; } function displayFortune(fortune) { var fortuneNode = document.createElement('li'); fortuneNode.textContent = fortune; document.getElementById('fortune-list').appendChild(fortuneNode); } function displayError(err) { updateStatus(err.toString()); } function updateStatus(status) { document.getElementById('status').innerHTML = status; } </script> </head> <body> <h1>Client</h1> <p>Service to connect to: <input id="service-name" type="text" placeholder="Enter a service name" size="60" /></p> <p> Fortune to add: <input type="text" id="add-text" placeholder="write a custom fortune" size="60"/> <button id="add-button">Add Fortune</button> </p> <p><button id="get-button">Get Fortune</button></p> <h2>Status: <span id="status">Ready</span></h2> <p>Received fortunes: <ol id="fortune-list"></ol></p> <script src="browser/fortune-client.js"></script> </body> </html> EOF
The example above uses bindTo
to retrieve a service stub. This step does not exist when using Go.
{{# helpers.code }} client.bindTo(context, name, callback); {{/ helpers.code }}
bindTo
retrieves the service definition from the remote server. This allows the client to generate a service stub without needing a local VDL definition. In contrast, Go clients typically get the service definition from the generated VDL code because types must be known at compile-time.
If the JavaScript client already has access to the service signature, it can skip the retrieval step by creating the stub directly using bindWithSignature
:
{{# helpers.code }} client.bindWithSignature(name, signature); {{/ helpers.code }}
The context
or ctx
variable has appeared in a few places in this tutorial.
{{# helpers.code }} // Client connecting to a service. client.bindTo(context, serviceName, callback);
// Fortune service method definition. FortuneService.prototype.get = function(ctx, serverCall) { ... }; {{/ helpers.code }}
Contexts are used to:
When a service method is invoked, the context it receives should generally be used for any ensuing outgoing RPCs so that the full sequence of calls can be traced.
Use browserify
to build the browser-targeted JavaScript files, which integrate the Vanadium libraries with the server and client code.
NODE_PATH=$V_TUT $V_TUT/node_modules/.bin/browserify \ $V_TUT/src/fortune/client/index.js -o $V_TUT/browser/fortune-client.js NODE_PATH=$V_TUT $V_TUT/node_modules/.bin/browserify \ $V_TUT/src/fortune/server/index.js -o $V_TUT/browser/fortune-server.js
For demonstration purposes, the Fortune client and server are shown on the same page.
cat - <<EOF >$V_TUT/fortune.html <!DOCTYPE html> <html> <head> <title>Fortune Teller</title> </head> <body style="background: #000000;"> <div style="position:fixed;top:0px;left:0px;bottom:0;width:48%; background: #ffffff;"> <iframe id="client" src="fortune-client.html" style="width:100%; height:100%;" frameBorder="0"></iframe> </div> <div style="position:fixed;top:0px;right:0px;bottom:0;width:48%; background: #ffffff;"> <iframe id="server" src="fortune-server.html" style="width:100%; height:100%;" frameBorder="0"></iframe> </div> </body> </html> EOF
You are ready to serve the web pages on a local server. Run the web server on port 8989.
Using node-static, the command is:
$V_TUT/node_modules/.bin/static $V_TUT -p 8989 > /dev/null & TUT_PID_HTTPD=$!
This static server runs in the background and is stopped in the Cleanup section.
Go to http://127.0.0.1:8989/fortune.html to view the examples.
{{# helpers.hidden }} Check that curling the fortune page doesn't fail. Since the user presumably visits the link above, this ensures that the URL functions properly.
curl -f http://127.0.0.1:8989/fortune.html &> /dev/null
Set the workspace, if necessary. Check other required environment variables.
# Set WORKSPACE, if not chosen. [ -z "$WORKSPACE" ] && export WORKSPACE=${JIRI_ROOT}/www # Check that the environment variables exist. echo ${CHROME_WEBDRIVER?} > /dev/null echo ${GOOGLE_BOT_USERNAME?} > /dev/null echo ${GOOGLE_BOT_PASSWORD?} > /dev/null
Then, run the WebDriver test with maven.
# Run the maven test. TMPDIR=/tmp xvfb-run -s '-ac -screen 0 1024x768x24' \ mvn test \ -f=$JIRI_ROOT/website/test/ui/pom.xml \ -Dtest=FortuneUITest \ -DchromeDriverBin=$CHROME_WEBDRIVER \ -DhtmlReportsRelativePath=htmlReports \ -DgoogleBotUsername=$GOOGLE_BOT_USERNAME \ -DgoogleBotPassword=$GOOGLE_BOT_PASSWORD \ -DprincipalBin=$V_BIN/principal \ -DtutDir=$V_TUT
{{/ helpers.hidden }}
The client and server defined here can communicate with the client and server that was defined in Go.
For simplicity, this example uses the default authorizer. To pass the authorization check, it is possible to create a principal with the proper blessings for communication with the browser. To read more, go to the Security Concepts page or run through the Principals and blessings tutorial.
Use the principal
tool, as follows:
$V_BIN/principal --v23.credentials $V_TUT/cred/basics \ seekblessings
A new tab will appear in the browser. Click the Bless
button. The Go code will now have access to an OAuth-authenticated blessing that matches the browser's.
In general, it is not necessary to generate an identical set of blessings; other authorizers can be used to grant access with different rules than the default authorizer.
In order for the Go client to contact the JavaScript server, it needs the Vanadium name for the server.
$JS_FORTUNE_NAME
will hold the name of the JavaScript server. The following command parses the principal and computes the name of the fortune server. It should match the service name listed on the Fortune page.
export JS_FORTUNE_NAME=$( $V23_RELEASE/bin/principal get \ --v23.credentials $V_TUT/cred/basics default \ | $V_BIN/principal dumpblessings - \ | awk -F/ '/Blessings/ {print "users/" $3 "/chrome/tutorial/fortune"}' )
Use the Go fortune client to request a fortune from the JS server.
$V_TUT/bin/client --v23.credentials $V_TUT/cred/basics \ --server $JS_FORTUNE_NAME
It is also possible to add a fortune:
$V_TUT/bin/client --v23.credentials $V_TUT/cred/basics \ --server $JS_FORTUNE_NAME -add 'Fortune favors the bold.'
To use a Go fortune server with a JS client, first run the server.
kill_tut_process TUT_PID_SERVER $V_TUT/bin/server --v23.credentials $V_TUT/cred/basics \ --endpoint-file-name=$V_TUT/server.txt & TUT_PID_SERVER=$!
The server's endpoint will be used to identify the fortune service. To get the endpoint, run the following command.
cat $V_TUT/server.txt # The go server's endpoint address
The Go server‘s endpoint should be printed out to the console. **Copy this endpoint and paste it into the client page’s Server field.**
The JS client will now be able to call Get
and Add
on the Go server.
Once finished, stop the HTTP server and the Go fortune server.
kill $TUT_PID_HTTPD kill $TUT_PID_SERVER
Congratulations! You have successfully run the Go-JS fortune example.
You have: