(( Plexer.js ))
Bootstrapping Web Workers for the Real World

What it is

Concurrency in JavaScript and Web Workers are a fine thing, but not available with every browser in the real world.
«Plexer.js» is an attempt to abstract Web Workers by bootstrapping normal functions into workers. In case a browser would not support Web Workers, «Plexer.js» creates a traditional instance of the function object. In both cases «Plexer.js» provides a single interface via a proxy-object. Also, any messaging to and from the worker is abstracted by this proxy-object, so you may just call any method of the function without bothering with communication issues.

All you have to do is to provide a function that can be called as a constructor and is exposing any methods to be called as its properties.
Whether or not a browser has support for Web Workers, the proxy-object will behave the same. There is no need anymore to provide separate solutions for various browsers or to target a specific subset of browsers only.

A Quick Example

<!DOCTYPE html>
<html>
  <head>
    <title>Plexer.js: Example 1</title>
    <script type="text/javascript" src="plexer.js"></script>
  
    <script type="text/javascript">
  
    function myFunction() {                          // this will be used as a template for the worker

      function add(a, b) {
        return ['sum', a, b, a + b];
      }

      function mult(a, b) {
        return ['product', a, b, a * b];
      }

      this.add = add;
      this.mult = mult;

    }

    function myCallback( kindOfResult, a, b, r ) {   // this will handle the results
      alert( "The " + kindOfResult + " of "
             + a + " and " + b
             + " is " + r );
    }
    
    var myPlex = new Plexer( myFunction );           // construct a Plexer proxy-object
    myPlex.run( "add", [2, 3], myCallback );         // will alert "The sum of 2 and 3 is 5"
    myPlex.run( "mult", [4, 5], myCallback );        // will alert "The product of 4 and 5 is 20"

    </script>
  </head>
  <body>
    Just an example ...
  </body>
</html>

It's just as easy. You've just spawned a Web Worker and had two jobs done by it.
(And in case the browser wouldn't support them, the results and the flow would just remain the same — 100% compatibility, no overhead.)

Essentials:
The function may be located anywhere in the same script, or in an other script, or may even be an anonymous function reference.
Just provide the method's name, and the arguments to the method as an array, and a reference to a callback-function to handle any results.
In case the worker's method returns an array, this will be applied as the arguments-array to the callback (by default, there is an option to override this behavior).

Please note that results will be provided asynchroniously as this is how workers work.
Even, if the function will be run as a traditional instance, the callback will be called asynchroniously in order to provide an equevalent behavior.

How it Works

«Plexer.js» follows the following outline when called as a constructor:

API

var plexer = new Plexer( <function-reference> [, <force-instance> [, <rest>* ]] );

Creates a Plexer-object.

<function-reference>:
Function: reference to the function to be "bootstrapped"
<force-instance>:
Boolean (optional, default: false): force the creation as an instance (for testing)
<rest>:
Function(s): any number of external functions to be included to the worker
returns:
object Object

V.1.1: You may list any number of additional functions to be added to the worker (besides the core function, which will receive the messages) as external helpers after the second argument. This will be without effect in instance-mode, but allows to resolve some dependencies of an otherwise self-contained function-constructor. Please mind that this will work with named function only.

 

plexer.run( <method-name> [, <arguments> [, <callback>
            [, <transferables-up> [, <transferables-down> [, <preserve-arrays>]]]]] );

Calls a worker's or instances public method and returns results assynchroniously to the callback.

<method-name>:
String: Name of the method to be called.
<arguments>:
Array: List of arguments to be passed to the method.
May be null or undefined (default: []).
<callback>:
Function: Reference to the function to be called with the method's result value(s) as argument(s).
<transferables-up>:
Number: Treat the first n arguments as transferable objects, when posting them to the worker (see below).
Without effect, if transferable objects are not available or the the run-mode is "instance". (default: 0).
<transferables-down>:
Number: Treat the first n result values from the worker as transferable objects, when posting them back from the worker.
Without effect, if transferable objects are not available or the the run-mode is "instance". (default: 0).
<preserve-arrays>:
Boolean: false = if the result is an array, apply it as the argument-list of the callback (default),
true: rather apply the array-reference as the single argument to the callback.
Example: By default the result [1, 2, 3] will be received by a callback function( a, b, c ) { ... } as the 1st, 2nd, and 3rd argument. If a value evaluating to true is provided for <preserve-arrays>, the array will be applied as the single argument (here "a") of the callback (while "b" and "c" will be undefined).
returns:
void (nothing)

Normally, you would wish to provide at least the method-name, an argument-list, and a callback-function, because you would want to have things done by the worker(or instance). For more on values, how they are passed, transferable objects, and an other example, see the section below.

 

plexer.terminate();

Terminates (kills) the worker or removes the instance. Also removes any references and any handlers attached to it.
This method is called internally as a clean-up procedure on the "unload" event, but may also be called manually to free resources.
If the "run()" method is called after a call to "terminate()", the worker/instance is automatically re-created again.

returns:
void (nothing)

 

plexer.getRunmode();

Returns the run-mode of a Plexer-instance.

returns:
String: "worker" or "instance"

 

plexer.isTerminated();

Returns the run-state of a Plexer-instance.

returns:
Boolean: true = terminated, false = not terminated (still alive)

Passing Values

Since concurrency adds the problem of more than a single agent acting on a given set of data and would open an opportunity for race-conditions, values are normally copied, when posted to or from a Web Worker. For this a method called "structured cloning" is applied to the values to be passed.

Modern browsers also support "transferable objects", meaning that the object is directly passed instead of being copied. In this case, the ownership of the object is also transfered (therefor the name) and the object is no longer accessible for the previous owner. Please note that not all data types are transferable: These are only ArrayBuffer, Canvas-data, and JSON-objects.

Trying to pass an unsupported value by cloning will trigger a DataCloneError exception, trying to pass an unsupported type as a transferable object will throw a TypeError.

Using Transferable Objects

«Plexer.js» provides a simple way to specify arguments or results as transferables: Just tell the "run() method how many (from left to right) of the arguments-list's or the results-list's values should be handled as transferable objects. «Plexer.js» will do the job for you and will generate the required "transfer map", in case the browser supports transferable objects. If the browser does not, these options will remain without effect.

Example 2

/* to be spawned as worker */

function WavFactory() {
  // just for fun: detect, if we are running as a worker
  var isAWorker = (!self.document);

  function mix( wav1, wav2, echo, delay ) {
    // argument-types: ArrayBuffer, ArrayBuffer, Number, Number
    // mixes two wav-files and optionally adds an echo effect
    // returns mixed wav as ArrayBuffer and some stats (as object)
    var stats = {};
    var result = new Uint8Array();
    var stream1 = new Uint8Array(wav1);
    var stream2 = new Uint8Array(wav2);
    // ... business logic goes here ...

    return [ result.buffer, stats ];
  }
  
  this.mix = mix;
}

/* main */

function mixReceiver( wavBuffer, stats ) {
   // receives results: ArrayBuffer and stats-object
   // do anything with these ...
}

var wavFile1 = new Uint8Array();        // contains first wav-stream
var wavFile2 = new ArrayBuffer(28000);  // just to make difference

// now call "mix()" and post the first 2 arguments as transferable objects,
// and receive the first result element as a transferable object

var factory = new Plexer( WavFactory );
factory.run( "mix", [ wavFile1.buffer, wavFile2, 0.2, 60 ], mixReceiver, 2, 1 );

// test if ownership was transfered
if ( wavFile2.byteLength == 0 )
    console.log( "Transferable objects, ownership transfered." );

Adding External Functions

There might be some cases, where a self-contained function would not do for the worker alone, or would mean at least a duplication of code maintained elsewhere. For this, you may add any number of references to (named) functions to be added to the worker's script as the argument-list's rest (i.e. after the second argument). These functions will be on the global scope and external to the constructor of the worker's main-function.

Example 3

/* to be spawned as worker */

function Grader() {

  this.mill = function( data ) {
    var out = [];
    // some business logic goes here ...

    // sort output, use an external numeric sort function
    out.sort( numericSort );
    // use yet another external helper
    var obj = arrayToObject( out, 'a_' );

    // finally return the result
    return [ obj ];
  }

}

/* some helper functions, used by worker and main */

function numericSort( a, b ) {
  return a - b;
}

function arrayToObject( a, prefix ) {
  var obj = {};
  for (var i = 0; i < a.length; i++) obj[ prefix+i ] = a[i];
  return obj;
}

/* main */

var statistics = {} // any content for this

// construct a Plexer-object with dependencies and call it
var myGrader = new Plexer( Grader, false, numericSort, arrayToObject );
myGrader.run( 'mill', [ statistics ], graderCallback );

// this will receive the results
function graderCallback( result ) {
  // do something ...
}

See the Source

Source of file plexer.js.

Download

Get plexer.zip (v.1.1, full-version and minified).

License

«Plexer.js» is free and provided under the M.I.T.-license.

Disclaimer: This software is distributed AS IS and in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. The entire risk as to the quality and performance of the product is borne by the user.

Trivia

«Plexer.js» was originally written to let Emscripten-generated code run transparently in the background (as this is essentially a headless environment) while providing compatibility at the same time. It was hard to decide, whether this bootstrapping of inlined code to a potential second runtime environment from a single source was multiplexing, singleplexing, or even uniplexing. It depends on the angle of view. So it's just a «Plexer».

Project Page

Project page (this): http://www.masswerk.at/plexer.

Author

Norbert Landsteiner, mass:werk – media environments, www.masswerk.at,
Vienna, 2013-07-22

Updated (v.1.1): 2013-07-25

 

Appendix: Loading & Modelling Data in the Background

Here is a scheme to load and model data concurrently in the background:

Example 4

(function() {

  var modeller, modelData = null;

  /* to be spawned as a worker */

  function DataModeller() {
    var rawDataSet, currentViewSet;
    
    this.loadData = function( dataId ) {
      // load and process data
      // (since we are in the background and/or require this data, we could have this blocking anyway)
      var xhr = new XMLHttpRequest();
      xhr.open( 'GET', '/dataservice?set=' + dataId , false);
      var response = xhr.send( '' );
      if (response.status != 200)
      	  return ['initial', {
      	    'status': 'ERROR',
            'message': 'Network error (' + response.status + '.)',
      	    'datapoints': []
      	  }];
      // save raw data locally in rawDataSet
      rawDataSet = JSON.parse( response.responseText );
      // now do anything (...)
      // process to initial viev-data (currentViewSet)
      // and return only the data required by the view
      return ['initial', currentViewSet];
    }
    
    this.remodel = function( dataPoint ) {
      // remodel the view on given data-point
      // and return new view-data
      return ['changed', currentViewSet];
    }
  }

  /* main */

  function init() {
    // listen for DOMContentLoaded
    document.addEventListener( 'DOMContentLoaded', documentReadyHandler, false );

    // setup a plexer
    modeller = new Plexer( DataModeller );

    // match pathname for dataset-ID (like in 'www.exmple.com/myapp/dataset/a64b45689c23/')
    var idMatches = RegExp(/\/dataset\/(\w+)\/?$/).exec(self.location.pathname);
    if (idMatches) {
      modeller.run( 'loadData', [idMatches[1]], dataHandler );
    }
    else {
      modelData = {
        'status': 'ERROR',
        'message': 'No data-ID.',
        'datapoints': []
      };
    }
  }

  function documentReadyHandler() {
    if (modelData) {
      renderData();
      // add DOM-handlers for interactive redraw
      document.querySelector('#dataview').addEventListener( 'click', viewResponder, false );
    }
    else {
      // wait ...
      setTimeout( documentReadyHandler, 100 );
    }
  }

  function dataHandler( status, dataset ) {
    switch (status) {
      case 'initial':  // just save the data
        modelData = dataset;
        break;
      case 'changed':  // save and redraw
        modelData = dataset;
        renderData();
        break;
      default:         // no change
        // actually nothing to do (reset UI?)
    }
  }

  function renderData() {
    // render modelData to the DOM
  }

  function viewResponder( event ) {
    // have the view-model recalculated in the background to keep the UI responsive
    // (data co-ordinates would be a bit more abstracted in a real case)
    var el = document.querySelector('#dataview'),
      x = event.pageX - el.offsetLeft,
      y = event.pageY - el.offsetTop;
    modeller.run( 'remodel', [{ 'x': x, 'y': y}], dataHandler );
  }

  init();

})();

Please note that this example is lacking MSIE-specific code (for the XHR and event-handling), which would have to be added for real use.
Anyway, this example is meant to illustrate, how a script could start loading and modelling data in parallel, while other assets of the page are still loading, and by doing so, potentially shortening the response time of a web application significantly. (As the script would have been probably cached, only the data-set would have to load on page-entry.) As a bonus, recalculating any view-data on user interaction concurrently in the background would keep the UI responsive by freeing the UI-thread from an essentially headless task.

 

<www.masswerk.at>