This tutorial walks you through how to create a simple multi-user chat application using Conductance.

If you want to see what you're in for, you can try out the finished version.

Our final app will consist of only two short files: chat.app executing on the client side, and chat.api executing on the server.

chat.app
= require(['mho:std', 'mho:app']);
function runChat(api) {
  var messages = api.getMessages();
  var renderMessages = messageList -> messageList.map(msg -> `<li>$msg</li>`);
  var messageView = messages .. @transform(renderMessages);
  var input = @TextInput() .. @Mechanism(function(elem) {
    elem.focus();
    elem .. @events('keydown') .. @each {
      |event|
      if (event.keyCode !== 13 || !elem.value.length) continue;
      api.addMessage(elem.value);
      elem.value = '';
    }
  });
  @mainContent .. @appendContent(`
    <h1>Oni Labs Chat Demo</h1>
    <ul>$messageView</ul>
    <hr>
    <div>Say something: $input</div>`
  ) { || hold() }
}
@withAPI('./chat.api', runChat);
chat.api
= require('mho:std');
var messages = @ObservableVar([
  "Welcome to multi-user chat!"
]);
var readOnlyMessages = messages .. @transform(@fn.identity);
exports.getMessages = -> readOnlyMessages;
exports.addMessage = function(msg) {
  messages.modify(current -> current.concat([msg]).slice(-40));
};

As you can see, Conductance code introduces a few new pieces of syntax (like @, `...`, or { || ... }) that are not available in plain JavaScript and other frameworks. It is these enhancements to JavaScript that make programming web applications in Conductance so powerful and succinct.

These features aren't just skin-deep syntactic sugar; they are fundamentally integrated into the language and allow you to write asynchronous web applications in a natural, sequential style.

As with all new things, there is a learning curve, but the great thing is that once you get used to Conductance's abstractions, you can use them everywhere - on the client, on the server, for defining UIs with advanced data-binding (like AngularJS or Ember), or for plain business logic. Conductance removes the need to deal with separate languages for the different layers of a web app.

In the following sections we will build up the chat application step-by-step and introduce Conductance's unique features as we go along. Hopefully by the end you should have a feel for Conductance's programming model.

Step 1: Simple Display

We'll start by creating a simple client-side-only application that dynamically constructs some HTML content and shows how we can embed live variables into HTML.

If you haven't already done so, start by installing Conductance (see the instructions on the Introduction page). Then create a directory called chat-demo/ and navigate to it (it doesn't matter where you put this folder - on your Desktop is fine).

File: chat.app

Instead of starting with a HTML file or some content template, in Conductance you usually begin by writing an .app file.

.app is Conductance's extension for the main client-side file of a web application. Conductance serves it up with some HTML boilerplate so you can just write code and have it run in the browser.

Let's start with a simple UI. Paste the following into a new file called chat.app:

chat.app
var observable = require('sjs:observable');
var surface    = require('mho:surface');
var app        = require('mho:app');
var messages = observable.ObservableVar([]);
function push(arrayelem) {
  // Append an element to the end
  // of an observable array.
  array.modify(function(current) {
    return current.concat([elem]);
  });
}
surface.appendContent(app.mainContent, `
  <h1>Oni Labs Chat Demo</h1>
  $messages
`);
push(messages, 'hey');
hold(1000);
push(messages, 'how are you?');

To serve this .app file, open a terminal and navigate to the chat-demo/ directory you created. From this directory, run:

conductance serve

This will serve the current directory using the default configuration. You should now be able to navigate to http://localhost:7075/chat.app to run your app. If it doesn't seem to be working, check the terminal window and your browser's javascript console for errors.

You can also run this version online.

What happened?

Let's go over the code we wrote, and explain what's new to Conductance that you may not have seen before:

Modules

Conductance includes a module system that works the same in the browser and on the server. Modules are loaded using the require() function.

Conductance includes two builtin collections of modules. They can be addressed via "hub" names, which are just short names for a URL prefix:

E.g. the code var seq = require('sjs:sequence') loads the sequence module from the SJS module library and assigns its exported symbols to the seq variable.

Read more ...

Conductance is built on StratifiedJS (SJS), which adds a number of powerful features on top of the JavaScript language, including a module system. We're importing the following modules:

// observable objects
var observable = require('sjs:observable');
// the main Conductance UI module:
var surface    = require('mho:surface');
// helpers for .app modules:
var app        = require('mho:app');

The "mho:" prefix is used for all Conductance modules. You'll also see the "sjs:" prefix often, as that's the prefix for the StratifedJS Standard Library.

require() loads a module asynchronously (so you can do other things while it's loading), but the SJS runtime allows us to simply wait for the result of this operation rather than requiring us to write the rest of our program inside a callback function. (Observant readers may notice that we don't actually need to wait for the observable module to load before we fetch surface - we could load both of them at the same time. As most browsers are able to perform several concurrent requests to a server, this would cut down on latency. This is something we'll fix later.)

Storing messages

Next, we create an ObservableVar which we'll use to store chat messages.

var messages = observable.ObservableVar([]);

ObservableVar objects are simple containers for changeable values - they can be get and set, and they implement an API for tracking their changing value. For our array, all we'll be doing is adding an item onto the end of the array, so we write a small helper function to do that:

function push(arrayelem) {
  // Append an element to the end
  // of an observable array.
  array.modify(function(current) {
    return current.concat([elem]);
  });
}

We could have used current.push() rather that constructing a new array, but we want to avoid modifying the array in-place. We'll see why that's important later, when we move the messages object to the server.

Quasi-quotes

SJS adds quasi-quote syntax, which is like a string but surrounded by backticks, `like so`. Values can be embedded into these strings with the ${expression} syntax (or $variable for single variables).

Read more ...

Next, we add some content to the current document body:

surface.appendContent(app.mainContent, `
  <h1>Oni Labs Chat Demo</h1>
   $messages
`);

Although the above code looks like string interpolation in other languages, quasi-quotes (surrounded by backticks) don't immediately collapse everything into a string - they preserve the original values. This allows the receiver (appendContent in this case) to treat embedded values intelligently, such as escaping any special HTML characters.

Keeping viewers informed

More than just escaping values, appendContent has automatically added a mechanism to keep the displayed value of messages updated whenever the underlying variable changes, which it does for any Stream of values. So we can add messages to the display simply by modifying the messages value, using the push function we defined earlier:

push(messages, 'hey');
hold(1000);
push(messages, 'how are you?');

We also use the global hold function, which suspends an expression for (in this case) 1000 milliseconds. "how are you?" is added one second after the first message, without having to resort to callbacks.

You might be surprised that we can just pause execution like this. In normal JavaScript we would have to code this with a callback, e.g.:

push(messages, 'hey');
window.setTimeout(function() { push(messages, 'how are you?'); }, 1000);

Behind the scenes this is similar to how our code is being executed, but rather than having to manually chain up asynchronous logic in this way, Conductance's Stratified JavaScript engine performs the translation automatically. It enables us to write asychronous code in a straightforward sequential style.

Styling

You'll have noticed that our application is already styled with different fonts and margins than a normal bare-bones HTML document. By default, .app files are styled with Twitter Bootstrap, which is included in Conductance's "surface" UI framework. While this gives you a good starting point for styling an application, this is completely optional and can be customized using the @template metadata directive. E.g. specifying

/**
   @template app-plain
 */

at the head of chat.app would cause our application to be unstyled.

Our choice of application template also influences the symbols exported by the mho:app module that we're importing into chat.app. E.g. for the 'app-default' (bootstrap) template, app.mainContent will point to a <div class='container'> element which the template automatically adds to the document body. For the 'app-plain' template, app.mainContent points to the document body itself.

Next steps

Well, that was a lot to take in for a first step, but on the other hand we've packed a fair bit of functionality into a pretty minimal .app.

Conductance brings a lot of new features that can take some getting used to, but we're sure you'll agree they're worth it once you get up to speed. So let's get on with that:

Step 2: Derived Values

Splicing an array directly into HTML isn't very pretty - we don't even get line breaks. But we still want the UI to update when the messages variable changes.

An incredibly useful part of StratifiedJS is the sequence module. Not only does it provide a lot of higher-order functions for operating over arrays, it also supports the Stream type, which is similar to a generator or iterator in other languages.

Because Streams are so useful, ObservableVar objects are also streams. You can think of them as an infinite sequence of values - each item is the value of that observable at a certain point in time.

So if we want to create a derived value from a Stream, we can use the transform function to apply a transformation to each value of the ObservableVar, as soon as it appears.

Update chat.app as shown (the highlighted lines are the only ones modified since the previous step):

chat.app
var observable = require('sjs:observable');
var surface    = require('mho:surface');
var app        = require('mho:app');
var seq        = require('sjs:sequence');
var messages = observable.ObservableVar([]);
function push(arrayelem) {
  // Append an element to the end
  // of an observable array.
  array.modify(function(current) {
    return current.concat([elem]);
  });
}
var renderMessages = function(messageList) {
  // convert an array of messages into an array
  // of <li> elements
  return seq.map(messageList, function(msg) {
    return `<li>$msg</li>`;
  });
};
var messageView = seq.transform(messages, renderMessages);
surface.appendContent(app.mainContent, `
  <h1>Oni Labs Chat Demo</h1>
  <ul>$messageView</ul>
`);
push(messages, 'hey');
hold(1000);
push(messages, 'how are you?');

Now, the messages display as list items - but because messageView is still a Stream of values, the UI updates whenever we modify the underlying messages observable. And since each element of messageView is a quasi-quote, it is merged into the display as HTML (if it were a plain string, it would instead be escaped).

Run it

Refresh your browser after modifying chat.app, or run this version online.

Step 3: Advanced Syntax

Currently, the code we've written looks a lot like JavaScript. Because JavaScript syntax can be quite limiting, StratifiedJS adds some features to make code more readable and concise.

This step takes a bit of a diversion to introduce the reasoning behind some features and show you how to put them to good use.

Using functions like methods

StratifiedJS plays nicely with different JavaScript runtimes. It adds very few global objects, but provides access to a wealth of functionality via individual modules on demand.

Similarly, we don't monkey-patch or extend builtin classes, because that is a sure way to cause conflicts with other code. Instead, StratifiedJS tends to provide functional equivalents for things that might be methods in other languages. For example, the map function is used as map(array, fn), whereas some javascript runtimes provide similar functionality via a method: array.map(fn).

As a side benefit, the functional approach allows our version of map to work in places where methods can't - e.g it works the same whether you pass it an Array or an arguments objects.

An unfortunate effect of functional style is that chaining operations becomes visually confusing. While you might chain methods like:

[1,2,3,4,5].filter(isEven).each(console.log);

the functional equivalent is not so easy to follow:

Double-dots

SJS adds the .. syntax as a way to use functional APIs while maintaining the left-to-right order and chainaing ability of object methods.

e.g to double the elements in an array, you can do:

arr .. map(i -> i * 2)

Which is equivalent to:

map(arr, i -> i * 2)

This allows us to use functions as extension methods on builtin types without modifying global prototypes (which is a risky practice).

Read more ...

each(filter([1,2,3,4,5], isEven), console.log);

To help readabaility when using these sorts of functions, we can use StratifiedJS's "double-dot" syntax. It's a simple transformation that allows us to write:

[1,2,3,4,5] .. filter(isEven) .. each(console.log);

and have it execute as if we wrote:

each(filter([1,2,3,4,5], isEven), console.log);

The Alternate Namespace

Most apps will use a handful of common modules - object, sequence, string, regexp, array, etc. What's more, for brevity you'll often want to make symbols from another module available as local variables.

So instead of this:

var object = require('sjs:object');
var seq = require('sjs:sequence');

You might write something like:

Destructuring

Destructuring assignment allows you to write code like:

var { a, b, c } = someObject;

As a shortcut for:

var a = someObject.a,
    b = someObject.b,
    c = someObject.c;

Read more ...

var { clone, merge, ownPropertyPairs } = require('sjs:object');
var { each, map, toArray, find } = require('sjs:sequence');

Which makes local variables each, map, clone, etc referencing the functions of the same name from the sequence and object modules.

We noted above that this line-by-line importing actually loads the modules one at a time - sequence only starts loading once object is complete.

Sometimes this isn't a big deal on the server, modules load really fast. And even in a browser, you can preload all the modules you need to make sure that the require() call is instant. But if the modules aren't preloaded, each will be fetched from the server. In order to ensure we don't wait longer than we need to, we can use StratifiedJS' waitfor/and syntax to load both modules in parallel:

// start loading `object` and `sequence` in parallel:
waitfor {
  var { clone, merge, ownPropertyPairs } = require('sjs:object');
} and {
  var { each, map, toArray, find } = require('sjs:sequence');
}
// code below here will only run once both of the above blocks are complete

This is quite useful, but doing this for each new module you import can be verbose. Even worse, it's error-prone - if you forget to include find in the list, you may wind up accidentally using the global find function which is a recipe for hard-to-debug errors.

Some languages solve this by supporting a wildcard import syntax - e.g. python's from modulename import *. Aside from this being very difficult to implement in JavaScript, it's also often advised against - it means that any symbols in module are on equal footing with (and can conflict with) regular local variables.

@altns

"@altns", or the "alternate namespace" is a special module-local variable where (by convention) you place symbols imported from other modules.

Properties on the @ object require no dot-accessor - e.g @.each can be written as just @each.

Read more ...

But local and imported symbols are not the same thing, and it's often useful to know that something is be provided by another module. StratifiedJS adds the "alternate namespace" syntax, which marries the brevity of local variables with an obvious visual indicator (and scope) for imported symbols.

So instead of the above code, you can load these modules in parallel, in one line:

= require(['sjs:object', 'sjs:sequence']);

Passing an array to require loads the given modules in parallel, and returns them merged into a single object, which we assign to the special @ variable.

From then on, you can access symbols defined in the object or sequence modules by prefixing them with @ - e.g @map, @each, @clone, etc.

And since this is such a common thing to do, we provide a convenience module called mho:std which combines a number of the most commonly-used modules for you. So all you really need to start an app with a fairly full array of builtins at your disposal is:

= require('mho:std');

Cleaning up

Arrow notation

SJS allows a shorthand "arrow function syntax". This allows you to write functions like:

var add = (x, y) -> x + y;

Which is equivalent to:

var add = function(x, y) {
  return x + y;
};

If an arrow function only takes a single argument, the brackets around the argument list are optional.

Read more ...

So, let's apply these StratifiedJS features (as well as the "arrow function syntax" explained on the sidebar on the right) to our existing code, to simplify and make it more readable:

chat.app
= require(['mho:std', 'mho:app']);
var messages = @ObservableVar([]);
function push(arrayelem) {
  array.modify(current -> current.concat([elem]));
}
var renderMessages = messageList -> messageList.map(msg -> `<li>$msg</li>`);
var messageView = messages .. @transform(renderMessages);
@mainContent .. @appendContent(`
  <h1>Oni Labs Chat Demo</h1>
  <ul>$messageView</ul>
`);
messages .. push('hey');
hold(1000);
messages .. push('how are you?');

After these modifications, the functionality won't have changed, but it's gotten shorter. And rather than just being terse, we think you'll agree that the resulting code is actually easier to read and follow, once you get used to the syntax.

Run it

Refresh your browser after modifying chat.app, or run this version online.

Step 4: User Input

Let's be honest. There hasn't been all that much chatting going on so far. Let's add an input box so you can talk back to the computer.

Mechanism

A mechanism is a function attached to a HTML widget, using the Mechanism function. When a widget is added to the document, any attached mechanisms are called. When the widget is removed from the document, the relevent functions are aborted (if they haven't already completed).

Mechanisms encourage self-contained, modular widgets - you can add a widget to a page multiple times, and the attached mechanism will be called independently for each instance, each being passed a different elem argument.

Read more ...

Blocklambda

SJS adds a "blocklambda" syntax, which is similar to blocks in the ruby language. Blocklambdas are appended to the existing arguments in a function call, so:

fn(arg) { |x, y| /* body */ }

Is similar to:

fn(arg, function(x, y) { /* body */ })

The above code is not quite equivalent - blocklambdas behave differently to regular functions when they include continue, break or return statements, as well as retaining the same this reference inside the block.

Read more ...

chat.app
= require(['mho:std', 'mho:app']);
var messages = @ObservableVar([]);
function push(arrayelem) {
  array.modify(current -> current.concat([elem]));
}
var renderMessages = messageList -> messageList.map(msg -> `<li>$msg</li>`);
var messageView = messages .. @transform(renderMessages);
var input = @TextInput() .. @Mechanism(function(elem) {
  elem.focus();
  elem .. @events('keydown') .. @each {
    |event|
    if (event.keyCode !== 13 || !elem.value.length) continue;
    messages .. push(elem.value);
    elem.value = '';
  }
});
@mainContent .. @appendContent(`
  <h1>Oni Labs Chat Demo</h1>
  <ul>$messageView</ul>
  <hr>
  <div>Say something: $input</div>`
);
messages .. push('hey');
hold(1000);
messages .. push('how are you?');

Here we've created a simple <input type='text'> element (using the TextInput function defined by Conductance's "surface" UI system), and attached a Mechanism which will run whenever the widget is added to the document (so if you wanted to, you could add input to the document multiple times, and it would run the mechanism once for each instance).

Unlike typical event-based code in JavaScript, this is an actual loop - the @events .. @each construct (from the event and sequence modules) waits for the named event, runs the provided block, and repeats indefinitely. @eachs argument can be a function, or, as we're using here, a blocklamda.

The blocklambda makes @events(...) .. @each { |...| ... } almost behave like a normal JavaScript loop - such as e.g. while(...) { ... } - in that we can use continue, break and return statements to influence the control flow. E.g. here we're using continue to go round the loop again immediately if the key associated with the event is not "return", or if the value of the input field is empty. A break would bail out of the @events(...) .. @each(...) { |...| .... }, whereas a return would directly end the enclosing function.

This might not seem usefully different from typical event systems using something like addListener, which registers a function to be called each time an event happens. The difference is that running code is a very useful form of composition and concurrency, compared to managing individual code fragments in some data structure.

Partly, this is so that we can use control flow statements like break, continue and return, but it is also important for error handling - if some error is thrown from the @each block (perhaps push has a bug in it), the whole mechanism will end, because the exception will bubble up through the @each and @events functions.

Error handling is one (big) benefit, but there's another: retraction. StratifiedJS supports the notion of cancelling or retracting a computation, which is useful in many scenarios. Frequently, you'll want to do multiple things concurrently, but stop all of them as soon as any of them completes. Here's a concrete example, a dialog widget that can be dismissed via OK or Cancel buttons, or by pressing esc:

var dialog = showDialog();
try {
  waitfor {
    dialog.okButton .. @wait('click');
    return true;
  } or {
    dialog.cancelButton .. @wait('click');
  } or {
    document.body .. @wait('keydown', {filter: isEscapeKey});
  }
} finally {
  dialog.remove();
}

wait is implemented so that whatever happens, the event listener it adds is always removed - if the event is triggered, or an exception occurs, or even if the call to wait() is retracted (cancelled) because some other code branch ended first in a waitfor/or block.

In an event-based system, you would have to register different callbacks to handle each event (ok button, cancel button, and escape key). In all of these callbacks, you would have to ensure that some common cleanup code (i.e dialog.remove()) was called.

Critically, you're relying on every callback to be well-written and bug-free, or else your application may end up in an invalid state - the dialog may stick around, unconnected to the application logic. Or if the esc event listener doesn't get removed properly, it may fire some time later and do something completely illogical because it assumed the dialog was currently being displayed.

By allowing standard control-flow constructs like try/finally to be used with concurrent, event-based code, it becomes very easy to construct a component that gracefully handles failures elsewhere in the application, and doesn't end up in an inconsistent state when something goes wrong.

One more thing: we mentioned above that the mechanism attached to a widget will run whenever it gets inserted into the document. But when an element is removed from the document, all corresponding mechanisms are aborted too (if they're still running). So if you were to remove the input widget from the document, the @events .. @each loop would automatically be cancelled and all event listeners removed without you needing to do anything extra.

Run it

Refresh your browser after modifying chat.app, or run this version online.

Step 5: Multiple Users

It's time to invite the rest of the world in. Rather than an array in your browser, we need to store messages on the server, so that multiple clients can chat to each other. On the client side, this is pretty straightforward:

chat.app
= require(['mho:std', 'mho:app']);
function push(arrayelem) {
  array.modify(current -> current.concat([elem]));
}
function runChat(api) {
  var messages = api.getMessages();
  var renderMessages = messageList -> messageList.map(msg -> `<li>$msg</li>`);
  var messageView = messages .. @transform(renderMessages);
  
  var input = @TextInput() .. @Mechanism(function(elem) {
    elem.focus();
    elem .. @events('keydown') .. @each {
      |event|
      if (event.keyCode !== 13 || !elem.value.length) continue;
      messages .. push(elem.value);
      elem.value = '';
    }
  });
  
  @mainContent .. @appendContent(`
    <h1>Oni Labs Chat Demo</h1>
    <ul>$messageView</ul>
    <hr>
    <div>Say something: $input</div>`
  ) { || hold() }
}
require('./chat.api').connect(runChat);

We have moved the majority of our code into a runChat function which takes an api argument. Rather than referring to a local messages variable, we're now retrieving it like so:

var messages = api.getMessages();

So where does this api come from? It's being passed to runChat by the call

require('./chat.api').connect(runChat);

Similar to how .app files are files that execute on the client, .api files execute on the server. When you require(.) an .api file from the client-side, it exposes a single connect symbol. connect(f) connects to the server, retrieves the given API exports and calls f with them.

Our api will expose a getMessages() function which returns a proxy to a server-side ObservableVar array. Because it lives on the server, modificiations to this array will cause the the UI on every connected client to be updated.

We'll show in a minute how the .api file is written (it is very easy!), but first note how we're now calling appendContent with an extra blocklambda argument { || hold() }. This blocklambda manages the lifetime of our appended content: when the block finishes (by returning, throwing an error, or being retracted), the content is automatically and cleanly removed from the document.

Previously, we left out the final argument to appendContent. This is the "fire-and-forget" mode - the content is added to the document, but not managed afterwards. Now we call hold(), which means "wait forever" (or until retracted from the outside). This serves two purposes:

  1. connect(runChat) will shut down the API when runChat returns. The hold() prevents runChat from returning by itself, so we will keep open the API indefinitely.

  2. When there is a connection error (e.g. the internet connection drops out), connect(runChat) will automatically abort runChat. This will cause our hold() to be retracted, and, because it manages the lifetime of our appended content, will also cause the content to be cleanly removed from the document. That way, we won't end up with "ghost" elements which are visible but no longer connected to the application logic.

We also took out the calls to push() at the bottom of the file, since we don't want to spam the server with messages every time we load the page.

So now let's get to the implementation of the .api module.

chat.api
= require('mho:std');
var messages = @ObservableVar([
  "Welcome to multi-user chat!"
]);
exports.getMessages = -> messages;

This shows how easy it is to set up bidirectional communication in Conductance. We've moved the ObservableVar onto the server, but the client doesn't have to care - all the object's methods methods work as expected, they're just a little slower now because they are happening over the network.

Run it

Refresh your browser after restarting conductance, or run this version online.

Aside: we cheated a little in this step - the live version contains extra code to drop old messages after a limit is reached. We'll deal with this properly in the next step.

Now that the messages are stored on the server, you can open multiple browser windows / tabs of the same page - messages sent to the server will appear in every tab.

Of course, adding server-side communication means that a connection error can now cause an error in any function that is implemented on the server, at any time. Remember how we added a call to hold() in chat.app, so that the block never ends? That's not technically true - an API connection error will throw an exception, which causes the block to be aborted. We'll show you how to deal with errors like this in step 7.

Step 6: Conflicts

There's a catch to moving the messages object onto the server, though. Remember the push function we wrote? It uses the modify method on the messages object. But what happens if multiple people call this method at the same time? The messages object is defined in the .api module, so it lives and executes on the server.

The answer is: if multiple people try to modify messages at the same time, one of them will fail (modify will throw a ConflictError). This is because the function we pass to modify lives on each client, so in the time it takes for the server to call our function, another client might have also changed messages.

To deal with this, we can move the push method to the server. We know that the implementation of the push method is synchronous (because Array.concat is), it's only possible to have a conflict because the modify method is currently being called over the bridge.

So let's remove the client's push function, and replace it with an addMessage function on the API module:

chat.app
= require(['mho:std', 'mho:app']);
function runChat(api) {
  var messages = api.getMessages();
  var renderMessages = messageList -> messageList.map(msg -> `<li>$msg</li>`);
  var messageView = messages .. @transform(renderMessages);
  
  var input = @TextInput() .. @Mechanism(function(elem) {
    elem.focus();
    elem .. @events('keydown') .. @each {
      |event|
      if (event.keyCode !== 13 || !elem.value.length) continue;
      api.addMessage(elem.value);
      elem.value = '';
    }
  });
  
  @mainContent .. @appendContent(`
    <h1>Oni Labs Chat Demo</h1>
    <ul>$messageView</ul>
    <hr>
    <div>Say something: $input</div>`
  ) { || hold() }
}
require('./chat.api').connect(runChat);

While we're at it, we'll add a few features to improve reliability.

The first change we make is to turn messages into a read-only version. We can use a trivial transform for this - it returns a Stream rather than the original ObservableVar value, which leaves the client without any way of modifying the array other than the addMessage function we provide. We don't actually need to modify the value in the transform, so we just pass in the identity function.

Secondly, we don't want the array of messages to grow forever, as that would eventually exhaust the server's memory given enough messages. Instead, we'll limit the list of messages to the 40 newest messages whenever we add a new message.

chat.api
= require('mho:std');
var messages = @ObservableVar([
  "Welcome to multi-user chat!"
]);
var readOnlyMessages = messages .. @transform(@fn.identity);
exports.getMessages = -> readOnlyMessages;
exports.addMessage = function(msg) {
  messages.modify(current -> current.concat([msg]).slice(-40));
};

Now, the addMessage function executes entirely on the server. Since all of the data it uses is local and all of the functions it calls are atomic (i.e they don't suspend), we can be confident that there will be no conflicts updating the messages observable.

If you're building an app where it isn't possible to consolidate updates on the server like this, the modify method still makes this possible - all you need to do is to catch the ConflictError thrown by modify, and then to retry using the latest data. Since this might require merging the data you have and the latest version from the server, it's the responsibility of your application to determine how this situation should be handled.

Run it

Refresh your browser after restarting conductance, or run this version online.

Step 7: Error Handling

If your network dropped out (or the server stopped) when running the previous example, you wouldn't have a very good experience. You'll see the default .app error handling behaviour, which is to remove all content and show a simple error message when an unhandled error occurs, telling the user they can reload the page to try again.

Of course, that's not good enough for an app that needs to handle transient network failures. Fortunately, Conductance makes it easy to handle connection issues when using API modules:

chat.app
= require(['mho:std', 'mho:app']);
function runChat(api) {
  var messages = api.getMessages();
  var renderMessages = messageList -> messageList.map(msg -> `<li>$msg</li>`);
  var messageView = messages .. @transform(renderMessages);
  
  var input = @TextInput() .. @Mechanism(function(elem) {
    elem.focus();
    elem .. @events('keydown') .. @each {
      |event|
      if (event.keyCode !== 13 || !elem.value.length) continue;
      api.addMessage(elem.value);
      elem.value = '';
    }
  });
  
  @mainContent .. @appendContent(`
    <h1>Oni Labs Chat Demo</h1>
    <ul>$messageView</ul>
    <hr>
    <div>Say something: $input</div>`
  ) { || hold() }
}
@withAPI('./chat.api', runChat);

Swapping the API object's connect method for withAPI is very useful, but it could reasonably be considered cheating for the purpose of a tutorial. So in the interest of actually learning about error handling, we'll show you through a (simplified but entirely functional) version of the builtin withAPI utility.

We'll place it in its own module (file api-connection.sjs), as it is a self-contained and reusable piece of code.

If we want to use this version of the file, we could either override the @withAPI function for our main .app module by adding (sometime after the first line):

var { @withAPI } = require('./api-connection');

Or, since we're just using it once, we could replace the call to @withAPI with:

require('api-connection').withAPI('./chat.api', runChat);

File: api-connection.sjs

.sjs is the extension for general StratifiedJS modules. These are just code, and can be loaded via require() on the server and in a browser - if your module doesn't use server or browser-specific functionality, it can run anywhere without modification.

In our case, this module does use browser-specific functionality - the single exported function makes use of the surface library to display notices to the user. There's quite a bit of code here, but it's all making use of concepts we've already encountered:

api-connection.sjs
// selectively import the modules we need
= require(['mho:surface', 'mho:rpc/bridge', 'sjs:event']);
var { @Notice } = require('mho:surface/bootstrap/notice');
var { @Countdown } = require('mho:surface/widget/countdown');
var connectingNotice = @Notice('Connecting...', {'class':'alert-warning'});
var reconnectNotice = function(delay) {
  var secondsRemaining = @Countdown(Math.floor(delay/1000));
  return @Notice(`
    Not connected.
    Reconnect in ${secondsRemaining}s.
    ${@Element('a', "Try Now"{href:'#'})}
  `, {'class':'alert-warning'});
};
exports.withAPI = function(apiblock) {
  var initialDelay = 1000;
  var delay = initialDelay;
  var settings = {
    connectionMonitor: function() {
      document.body .. @appendContent(connectingNotice, -> hold());
    }
  };
  while (true) {
    try {
      @connect(api, settings) { |connection|
        // we're connected; reset connection delay
        delay = initialDelay;
        block(connection.api);
      }
      break;
    } catch(e) {
      // only catch transport errors
      if (! @isTransportError(e)) throw e;
      // display after a short pause (e.g we don't want to
      // display an error if the transport dropped because
      // of page navigation)
      hold(300);
      document.body .. @appendContent(reconnectNotice(delay)) { |elem|
        waitfor {
          hold(delay);
          delay *= 1.5;
        } or {
          // wait for a click on the "try now" link
          elem.querySelector('a') .. @wait('click', {handle:@preventDefault});
        }
      }
    }
  }
}

If we didn't include the UI-related code, it's worth noting how similar it would look to a simple retry-on-error block in most synchronous languages (without making use of events or threads):

  // (psuedo-code)
  function (api, block):
    while (true):
      try:
        connect(api, block)
        break
      catch TransportError:
        // wait for a delay,
        // then retry

Of course, the block we pass is event based under the hood - but StratifiedJS allows us to treat it as a normal, self-contained function call, which makes our code a lot easier to write, read, and reason about.

Importantly, we really don't have to maintain any state at all, aside from resetting to the initial delay when we successfully connect (each failed attempt causes the delay between attempts to grow, so we don't keep frantically trying to reconnect). Because the block we pass to withAPI runs until it ends (rather than setting up a web of callbacks), we can rely on the automatic retraction of StratifiedJS to cleanly abort any code that was executing (or awaiting results) when the transport error occurred.

Run it

Refresh your browser after restarting conductance, or run this version online.

Next steps

Now that you've completed the chat tutorial, it's time for you to explore Conductance. You're not on your own though - the online documentation contains sections explaining Conductance features and StratifiedJS syntax alongside the comprehensive API documentation:

There's also the mailing list, stackoverflow, github and blog links over on the Community page - you should use these if you get stuck on something, or if you just have questions about Conductance / StratifiedJS.