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:

Slice
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:

JavaScript
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:

  1. 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 protocol (TCP/IP) at port 10000. The object identity of the root directory is known to be RootDir.
  2. The client down-casts the proxy to DirectoryPrx and passes that proxy to listRecursive, which prints the contents of the file system.

Here is a snippet of HTML code for the page that the client runs in.

HTML
...
<!-- 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:

JavaScript
(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:

  1. The client updates the state of the page to reflect that it's busy.
  2. 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 protocol (TCP/IP) at port 10000. The object identity of the root directory is known to be RootDir.
  3. The client down-casts the proxy to DirectoryPrx and passes that proxy to listRecursive, which prints the contents of the file system.
  4. 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:

  1. The code uses checkedCast to narrow the Node proxy to a Directory proxy, and uses uncheckedCast to narrow the Node proxy to a File proxy. Exactly one of those casts will succeed, so there is no need to call checkedCast twice: if the Node is-a Directory, the code uses the proxy returned by checkedCast; if checkedCast fails, we know that the Node is-a File and, therefore, uncheckedCast is sufficient to get a File proxy. 
    In general, if you know that a down-cast to a specific type will succeed, it is preferable to use uncheckedCast instead of checkedCast because uncheckedCast does not incur any network traffic.
  2. The code prints the name of the file or directory and then, depending on which cast succeeded, prints "(directory)" or "(file)" following the name.
  3. 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 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.

See Also