StratifiedJS

JavaScript + structured concurrency.
Bring structure to your JavaScript based asynchronous applications.
A small but powerful extension to the language of the web.

Contents

SJS Constructs

Introduction

StratifiedJS extends the JavaScript language with a small number of constructs for concurrent programming. It allows you to express asynchronous control flow in a straightforward sequential style:

var news;
waitfor {
  news = http.get("http://news.bbc.co.uk");
}
or {
  hold(1000);
  news = http.get("http://news.cnn.com");
}
or {
  hold(1000*60);
  throw "sorry, no news. timeout";
}
show(news);

This snippet

  • tries to retrieve the news from the BBC.
  • concurrently tries CNN if news not received after 1 second.
  • displays the first news that comes in, and cancels any request that might still be pending.
  • times out after 1 minute if no news received, automatically cancelling the pending requests.

For more information about the rationale and basic usage of StratifiedJS, please see one of these interactive presentations from recent talks:


Implementations

The reference implementation of StratifiedJS is Oni Apollo. It is an open-source implementation for use in browsers (including IE6+ and mobile) and on the server (based on node.js). Apollo is written in JavaScript and hence requires no browser extension or plugins.

We're also investigating extending Google's V8 JavaScript engine with native StratifiedJS support.

Language Overview

StratifiedJS executes code in logical units we call strata. Unlike normal JS code, strata are allowed to "block", i.e. the control flow is allowed to stop at a particular point to be picked up again later at the same point where it left off. An example would be the hold statement which blocks for a given period of time.

While one stratum is blocked, other strata can execute. Strata are a bit like threads, but they are much more deterministic: Only one stratum is running at any one time and it is executed atomically up until the point where it either finishes or suspends. At that point the next pending stratum is executed until it finishes or suspends, and so forth.

StratifiedJS contains constructs for casting asynchronous logic into blocking form (waitfor()) and constructs for combining multiple strata (waitfor/and, waitfor/or, spawn) in a structured way.

Pausing execution

StratifiedJS's hold function can be called with a number argument t to suspend the current stratum for ~t milliseconds:

...
hold(1000);
// the next code will be executed after around 1s
...

Note that hold only suspends the stratum that it appears in; it doesn't block the whole program and it doesn't 'busy-wait'. Other concurrent strata can continue to execute during this time. If you are running SJS in a browser, the UI will stay fully responsive during periods of suspension.

Calling hold with an undefined argument suspends the current stratum indefinitely.

Creating new strata

StratifiedJS offers several different constructs for running multiple concurrent strata. One of the most straightforward ways is to use StratifiedJS's spawn operator to execute some code in the background, similar to how you would "spawn a thread" in other programming languages:

spawn subexpression;

This spawn expression executes subexpression synchronously until subexpression suspends or finishes, and then returns a stratum object:

Class Stratum
abort Function that aborts the spawned stratum (if it is not finished yet).
waitforValue Function that returns the value of the spawned stratum expression. If the stratum isn't finished yet, blocks until it is. If the stratum threw an exception, waitforValue throws this exception. If the stratum was aborted (through a call to Stratum.abort), waitforValue throws an exception too.
waiting Function that returns the number of strata currently waiting for the spawned stratum to finish by being blocked in waitforValue (requires Apollo >=0.12).

Further execution of subexpression proceeds asynchronously until it finishes or is aborted through a call to Stratum.abort.

Instead of spawning a stratum it is often a better idea to use one of the structured concurrency constructs waitfor/and and waitfor/or - see below.

Note: The section above describes the spawn construct as it is implemented in recent versions of StratifiedJS. In older versions (Apollo < 0.11), spawn took a function argument instead of an expression.

Parallel composition

Parallel composition or 'fork-join' composition is expressed with SJS's waitfor/and construct:

waitfor {
  ... some code ...
}
and {
  ... some other code ...
}
... next code ...

This code executes some code until it finishes or suspends. It then executes some other code until the latter finishes or suspends. Only when both some code and some other code have finished will the execution proceed with next code.

waitfor/and can take more than 2 clauses, as well as optional catch/retract/finally clauses (like try). The full syntax is:

waitfor block1 and block2 [ and block3 [ and ... ] ] 
[ catch(e) catch_block ]
[ retract retract_block ]
[ finally finally_block ]

Alt composition

Alt ('alternatives') composition explores a set of alternatives in parallel. It is expressed with SJS's waitfor/or construct:

waitfor {
  ... some code ...
}
or {
  ... some other code ...
}
... next code ...

This code first executes some code. If some code finishes, then some other code will be skipped, and execution proceeds directly with next code. If, instead, some code suspends, some other code will be executed until it either suspends or finishes. When either some code or some other code finishes, while the other is suspends, the suspended code will be cancelled.

One scenario where alt composition is helpful is to add timeouts:

waitfor {
  var rv = do_request_to_database_server();
}
or {
  hold(1000);
  throw ("timeout in database server communication");
}

Here, we're timing out our database request after 1s. Note how we can treat do_request_to_database_server as a complete black box. SJS is modular!

waitfor/or can take more than 2 clauses, as well as optional catch/retract/finally clauses (like try). The full syntax is:

waitfor block1 or block2 [ or block3 [ or ... ] ] 
[ catch(e) catch_block ]
[ retract retract_block ]
[ finally finally_block ]

Collapsing alternatives

The collapse keyword (requires Apollo >=0.13) can be used to prematurely collapse the strata in a waitfor/or construct:

waitfor {
  console.log('a1');
  hold(1000);
  console.log('a2');
}
or {
  console.log('b1');
  collapse; // this aborts all other waitfor..or clauses
  hold(2000);
  console.log('b2');
}
// This code prints a1, b1, b2.

Suspending execution

Using SJS's waitfor() construct, called the suspending waitfor (not to be confused with the composition constructs waitfor/and/or), execution of a stratum can be suspended until explictly resumed:

waitfor() {
  ... "suspending code block" ...
}
... next code ...

Here, the suspending code block (the code inside the curly brackets) will be executed and the stratum will suspend. next code will not be executed until the stratum is explicitly resumed. For this purpose, the waitfor() construct defines a function resume inside its suspending code block. Calling resume() will resume the corresponding suspended stratum at the point of next code. Only the first call to resume will have an effect (and only if the suspended waitfor has not been cancelled). Subsequent calls to resume() will be ignored.

Example: In a web browser we can use waitfor() with window.setTimeout to make a 'pause' function (similar to hold(t)):

function pause(t) {
  waitfor() {
    window.setTimeout(resume, t);
  }
}

Parameters passed into the call to resume() can be captured by putting them into the waitfor()'s variable list:

waitfor(var a, b, c) {
  window.r = resume;
}
console.log("a,b,c = "+a+","+b+","+c);


//code running in another stratum:
... 
window.r(1,2,3);
...

This code will log "a,b,c=1,2,3".

The full syntax of the suspending waitfor construct is:

waitfor ( [return_var_decls] ) block
[ catch(e) catch_block ]
[ retract retract_block ]
[ finally finally_block ]

Cancellation

There are a number of situations under which suspended code gets cancelled, e.g. when another branch in a waitfor/or construct finishes first, or when a branch in a waitfor/and returns early:

function foo() {
  waitfor {
    ...some suspending code...
  }
  and {
    ...
    return;
  }
}

Here, if some suspending code is still suspended by the time of the return call in the other branch, it will be cancelled.

Cancellation means that a suspended waitfor()-construct will not wait for a call to its corresponding resume any more. Also, a hold(t) will not wake up any more. Cancellation can be "caught" much like exceptions using try/retract:

try {
  ... some suspending code ...
}
retract {
  console.log("'some suspending code' has been cancelled!");
}

Cancellation is also honoured by try/finally; i.e. a finally clause will be executed whether a try block is left normally, by exception or by cancellation.

As a shorthand, catch, retract, and finally blocks can be appended directly to a waitfor(), waitfor/and, or waitfor/or, without having to wrap it into a try-clause.

Example: Here is a function that waits for a DOM event in the webbrowser:

function waitforEvent(elem, event) {
  waitfor(var rv) {
    elem.addEventListener(event, resume, false);
  }
  finally {
    console.log("cleanup on "+elem.id+"...");
    elem.removeEventListener(event, resume, false);
  }
  return rv;
}

Factoring out try/finally

Similar to Python's with statement (PEP-0343), SJS has a using statement for factoring out standard uses of try/finally:

using (context_manager) {
  ... some code ...
}

This code executes the code block some code. When some code is exited (either normally, by exception or by cancellation), context_manager.__finally__() will be called, if this function exists.

I.e. the above code is equivalent to something like:

try {
  ... some code ...
}
finally { 
  if (context_manager && 
      typeof context_manager.__finally__ == "function")
    context_manager.__finally__();
}

Example of an event loop in Oni Apollo:

using (var q = require("apollo:dom").eventQueue(window, "keydown")) {
  for (;;) {
    switch (q.get().keyCode) { 
      ...
    }
  }
}

Base Language

StratifiedJS is based on the JavaScript language as defined in ECMA-262 Edition 3. In addition, StratifiedJS supports several functions defined in ECMA-262 Edition 5:

Depending on the particular browser or server environment, additional features from recent ECMA editions (such as e.g. Object.create, Object.seal, etc) might be available.

StratifiedJS also supports some of the syntactic sugar proposed for ECMA-262 Edition 6:

Destructuring Support

'Destructuring' is a convenient syntax for picking apart structured data, mirroring the way array and object literals are constructed.

E.g. if you have a function that returns an array, you can directly extract this to individual variables using the following destructuring assignment:

function foo() { return ['x', 'y', 'z']; }
var a,b,c;
[a,b,c] = foo();
// we'll now have a=='x', b=='y' and x=='z'

Similarly, objects can be picked apart like this:

function bar() { return {x:1, y:2, z:3; } }
var a,b,c;
({x:a,y:b,z:c}) = bar();
// we'll now have a==1, b==2 and x==3

Note that object patterns cannot appear at the start of a statement, hence the parenthesization of the object pattern in ({x:a,y:b,z:c}) = bar();.

Destructuring works with nested array and object data, too. Here's an example of extracting the third Google search result for "js destructuring": [Run example in sandbox]

var title, url;
({responseData:  
   { 
     results: [ , , {title, url}]
   }
 }) = require("apollo:google").search("js destructuring");
 console.log("3rd Google result: "+ title + " --- " + url);

This pattern makes use of a DRY shorthand whereby the pattern {title, url} is equivalent to {title:title, url:url}.

The two empty elements in the pattern [ , , {title, url}] cause the assignment to skip the first two results returned by the google search, and bind title and url to the third result.

Please see the destructuring docs at ecmascript.org for full syntax details. Note that destructuring support as implemented in Apollo is currently limited to plain assignment expressions. Destructuring in var definitions, for/in loops, catch clauses or parameter lists is not supported yet.