Example of a File System Client in JavaScript
This page presents a very simple client to access a server that implements the file system we developed in Slice for a Simple File System. The JavaScript code shown here hardly differs from the code you would write for an ordinary JavaScript program. This is one of the biggest advantages of using Ice: accessing a remote object is as easy as accessing an ordinary, local JavaScript object. This allows you to put your effort where you should, namely, into developing your application logic instead of having to struggle with arcane networking APIs.
We now have seen enough of the client-side JavaScript mapping to develop a complete client to access our remote file system. Even though the language mapping is the same whether you're writing applications for NodeJS or a browser, the code style is different enough that here we'll handle the two platforms separately. For reference, here is the Slice definition once more:
module Filesystem { interface Node { idempotent string name(); } exception GenericError { string reason; } sequence<string> Lines; interface File extends Node { idempotent Lines read(); idempotent void write(Lines text) throws GenericError; } sequence<Node*> NodeSeq; interface Directory extends Node { idempotent NodeSeq list(); } }
To exercise the file system, the client does a recursive listing of the file system, starting at the root directory. For each node in the file system, the client shows the name of the node and whether that node is a file or directory. If the node is a file, the client retrieves the contents of the file and prints them.
The body of the client code looks as follows:
const Ice = require("ice").Ice; const Filesystem = require("./generated/Filesystem").Filesystem; (async function() { async function listRecursive(dir, depth) { const indent = "\t".repeat(++depth); const contents = await dir.list(); for(let node of contents) { const subdir = await Filesystem.DirectoryPrx.checkedCast(node); console.log(indent + (await node.name()) + (subdir? " (directory):" : " (file):")); if(subdir) { await listRecursive(subdir, depth); } else { const file = Filesystem.FilePrx.uncheckedCast(node); const text = await file.read(); for(let line of text) { console.log(`${indent}\t${line}`); } } } } let communicator; try { communicator = Ice.initialize(process.argv); const base = communicator.stringToProxy("RootDir:default -h localhost -p 10000"); const rootDir = await Filesystem.DirectoryPrx.checkedCast(base); if(!rootDir) { throw new Error("Invalid Proxy"); } console.log("Contents of root directory:"); await listRecursive(rootDir, 0); } catch(ex) { console.log(ex.toString()); process.exitCode = 1; } finally { if(communicator) { await communicator.destroy(); } } }());
After importing code generated from the slice definitions, the program defines a helper fuction: listRecursive, which recursively prints the contents of the file system, before starting the main program. Let us look at the main program first:
- The client first initializes the run time and creates a proxy to the root directory of the file system. For this example, we assume that the server runs on the local host and listens using the default transport protocol (TCP/IP) at port 10000. The object identity of the root directory is known to be
RootDir
. - The client down-casts the proxy to
DirectoryPrx
and passes that proxy tolistRecursive
, which prints the contents of the file system.
Here is a snippet of HTML code for the page that the client runs in.
... <!-- Main section that contains the user interface --> <section role="main" id="body"> <div class="row"> <div class="large-12 medium-12 columns"> <form> <div class="row"> <div class="small-12 columns"> <a href="#" class="button small" id="run">Run</a> </div> </div> <div class="row"> <div class="small-12 columns"> <textarea id="output" readonly></textarea> </div> </div> <div id="progress" class="row hide"> <div class="small-12 columns left"> <div class="inline left icon"></div> <div class="text">Sending Request...</div> </div> </div> </form> </div> </div> </section> ...
It creates a button labeled "Run" that will trigger the demo to run, a textarea for displaying output from the demo, and a progress pane for displaying the state of the client.
The body of the client code looks as follows:
(function(){ const State = { Idle: 0, Busy: 1 }; const communicator = Ice.initialize(); async function listRecursive(dir, depth) { const indent = "\t".repeat(++depth); const contents = await dir.list(); for(let node of contents) { let subdir = await Filesystem.DirectoryPrx.checkedCast(node); $("#output").val($("#output").val() + indent + (await node.name()) + (subdir? " (directory):" : " (file):") + "\n"); if(subdir) { await listRecursive(subdir, depth); } else { const file = Filesystem.FilePrx.uncheckedCast(node); const text = await file.read(); for(let line of text) { $("#output").val($("#output").val() + indent + "\t" + line + "\n"); } } } } async function runDemo() { try { setState(State.Busy); const hostname = document.location.hostname || "localhost"; const proxy = communicator.stringToProxy(`RootDir:ws -h ${hostname} -p 10000`); const rootDir = await Filesystem.DirectoryPrx.checkedCast(proxy); await listRecursive(rootDir, 0); } catch(ex) { $("#output").val(ex.toString()); } finally { setState(State.Idle); } } function setState(newState) { switch(newState) { case State.Idle: { $("#progress").hide(); $("body").removeClass("waiting"); $("#run").removeClass("disabled").click(runDemo); break; } case State.Busy: { $("#output").val(""); $("#run").addClass("disabled").off("click"); $("#progress").show(); $("body").addClass("waiting"); break; } } } setState(State.Idle); }());
After creating an object for storing the state of the demo, the program defines 3 functions, listRecursive, a helper function which recursively prints the contents of the file system, runDemo, which runs the main program, and setState, which updates the page depending on whether the client is busy or idle. Let us look at the main program first:
- The client updates the state of the page to reflect that it's busy.
- The client initializes the run time and creates a proxy to the root directory of the file system. For this example, we assume that the server runs on the local host and listens using the default transport protocol (TCP/IP) at port 10000. The object identity of the root directory is known to be
RootDir
. - The client down-casts the proxy to
DirectoryPrx
and passes that proxy tolistRecursive
, which prints the contents of the file system. - The client ensures the browser page is reset to it's idle state.
SetState is responsible for updating the browser page depending on whether the client is busy or idle by displaying/hiding the progress pane and indicator message. In addition it also disables and enables the "Run" button, which lets the user start the demo.
Most of the work happens in listRecursive
. The function is passed a proxy to a directory to list, and an indent level. (The indent level increments with each recursive call and allows the code to print the name of each node at an indent level that corresponds to the depth of the tree at that node.) listRecursive
calls the list
operation on the directory and iterates over the returned sequence of nodes:
- The code uses
checkedCast
to narrow theNode
proxy to aDirectory
proxy, and usesuncheckedCast
to narrow theNode
proxy to aFile
proxy. Exactly one of those casts will succeed, so there is no need to callcheckedCast
twice: if theNode
is-aDirectory
, the code uses the proxy returned bycheckedCast
; ifcheckedCast
fails, we know that the Node is-a File and, therefore,uncheckedCast
is sufficient to get aFile
proxy.
In general, if you know that a down-cast to a specific type will succeed, it is preferable to useuncheckedCast
instead ofcheckedCast
becauseuncheckedCast
does not incur any network traffic. - The code prints the name of the file or directory and then, depending on which cast succeeded, prints
"(directory)"
or"(file)"
following the name. - The code checks the type of the node:
- If it is a directory, the code recurses, incrementing the indent level.
- If it is a file, the code calls the
read
operation on the file to retrieve the file contents and then iterates over the returned sequence of lines, printing each line.
Assume that we have a small file system consisting of a two files and a a directory as follows:
A small file system.
The output produced by the client for this file system is:
Contents of root directory: README (file): This file system contains a collection of poetry. Coleridge (directory): Kubla_Khan (file): In Xanadu did Kubla Khan A stately pleasure-dome decree: Where Alph, the sacred river, ran Through caverns measureless to man Down to a sunless sea.
Note that, so far, our client is not very sophisticated:
- The transport protocol and address information are hard-wired into the code.
- The client makes more remote procedure calls than strictly necessary; with minor redesign of the Slice definitions, many of these calls can be avoided.
We will see how to address these shortcomings in our discussions of IceGrid and object life cycle.