commons

0.0.1-pre • Public • Published

The Javascript Commons

This is a simple library for bootstrapping compatibility into the many nearly compatible implementations that exist. It provides shims to make it possible to interoperate with code not designed for compatibility and some modules to make getting started as quick and painless as possible. We should all be able to take advantage of the great work poured into the various javascript runtimes and well thought out inside them.

The Javascript Commons is similar in nature to the Apache Commons -- just not quite as huge. You could also think of the commons package as like CommonJS, just without the J -- after all, the J stands for Java, and that's not really our style.

Introduction

This project started as a simple experiment to see if it was feasible to shim the native binary types of as many CommonJS platforms as possible to squeeze out some small semblance of interoperability. This turns out to be relatively easy if you pare back the interfaces you require to a bare minimum. Once you have binary-level compatibility it becomes that much easier to coerce various modules into interoperability. So that's what we did.

First we ported Kris Zyp's excellent promised-io (TODO link) package to act the base for our file system operations. Promises allow sync and async operations maintain the same function signatures, which is very nice for some. Others much prefer a callback-style signature for any number of reasons: some claim it doesn't obscure the asynchronous nature of the operation being called, others just think that way and much prefer it. This library will attempt to bridge the gap, exposing one unified API for all of these styles.

But isn't this a hopelessly insurmountable religious divide? No. At least, we don't think so. Promises get us halfway there -- they can be used to unify synchronous and asynchronous signatures. The node-style callback-as-last-arg can get us all the way.

We've defined two different styles of streams: evented streams (like what you'd see in node) and forEachable streams (like what you'd see in JSGI). If a function returns a stream then you will get one or the other, depending on whether you passed a callback. We will have converters to switch between either as well as helpers to let you do stream manipulation completely neutrally.

NOTE callback-style and evented streams are NYI

TODO still an open issue on how to handle node *Sync APIs -- as it stands what would get returned are promises. One solution could be to return sync if no callback is provided before the program enters its event loop but then switch to returning promises after (or punching the developer in the face -- we could make that optional as well).

Conveniently, bridging the various native binary implementations also allows us to roll in a big chunk of narwhal-lib (TODO link), a package that is chock full of useful utilities but was difficult to use on node.js because of its lack of engine overlays. Often only very simple binary operations were required so we can all use this code now.

Supported Platforms

  • node.js
  • narwhal
  • GPSEE
  • ringo.js
  • Flusspferd

We need some help for supporting these platforms:

  • Browsers, modern and legacy
  • v8cgi
  • ejscript
  • MonkeyScript

And any others we are missing...

Usage

To shim your environment for full compatibility:

require("commons");

This will first upgrade your runtime with as much es5 as possible that can be shimmed into es3 (in most cases nothing will need to be added as the javascript engine vendors have picked off most of this low-hanging fruit). Then it will load all available module shims.

Or you can be selective with what you want loaded:

require("commons/binary").shim();
require("commons/fs").shim();

The key here is we're simply enhancing the native binary code with a few well- defined helpers. We're not forcing you to use our binary module, which means can use modules that are completely oblivious to our newly defined API without issue. We don't all have to agree what the right binary module is or where to load it from. This is a critical win.

Of course if you prefer to use the binary classes exposed by the library you can do that too. Binary objects you create will remain compatible with your host runtime's binaries, so you can intermix libraries that are completely unaware of CommonJS binaries. The binaries exported inherit from the prototype of the host binary object (Buffer in Binary/F and node, and Binary in the Binary/B spec).

var binary = require("commons/binary");
var assert = require("commons/assert");

var buf = binary.Buffer([97, 98, 99]);
assert.equal(buf.toString("ascii"), "abc");
assert.ok(typeof buf.set === "function");
assert.ok(buf instanceof binary.NativeBinary);
assert.ok(binary.isBinary(buf));

var bs = binary.ByteString("abc");
assert.equal(bs.toString("ascii"), "abc");
assert.ok(typeof bs.set === "undefined");
assert.ok(bs instanceof binary.NativeBinary);
assert.ok(binary.isBinary(bs));

var runtime = require("commons/process").runtime;
if (runtime == "node") {
    assert.ok(buf instanceof Buffer);
    assert.ok(bs instanceof Buffer);
}
else if (runtime == "narwhal") {
    assert.ok(buf instanceof require("binary").Binary);
    assert.ok(bs instanceof require("binary").Binary);
}

NOTE: this probably won't completely work yet

Running the Tests

From the commons package root:

<runtime> lib/commons.js

Or for just the binary tests:

<runtime> lib/commons/binary.js

The Process Module

We sniff the runtime and javascript engine and expose it in the process module:

require("commons/process").runtime;

To find out what javascript engine your on:

require("commons/process").engine;

Bear in mind: forking based on the underlying runtime or engine should be avoided at all costs -- it's functionally equivalent to user-agent sniffing and is probably just as bad.

TODO right now if you require("commons") we adding process as a global for node compatibility -- we should probably have more switches for shimming, maybe require("commons/runtime/node").shim() or some such would be best.

Philosophy

We have been suffering with the false-chioce of lowest common denominator APIs and more useful but runtime-specific classes for too long. We can have both and still enjoy some level of interoperability.

TODO: describe binary, stream, and fs libs and how fs (and other io libs) can be used interchangeably in both a callback/evented-stream context or a synchronous/promised/forEach-stream context

Rationale

To date most efforts have been focused on creating specs for the various runtime implementations to follow. This has been met with some success in that there are quite a few runtimes which implement one version or another of some key specs. None the less it is still almost impossible to write code that works across even a few runtimes. This library seeks to change that.

Rather than focusing on the underlying classes that implement specifications, the goal of this library is to take what is implemented by the various runtimes and enhance where needed to implement base interfaces. The binary interfaces are a good example.

There are two CommonJS binary specifications that have seen the most implementation: Binary/B and Binary/F. Binary/B defines ByteString and ByteArray and is implemented in most CommonJS runtimes. Binary/F talks in terms of Buffer, and is the binary representation node uses. While these two APIs seemingly differ dramatically they share enough in common for us to be able to do useful things with binary data regardless of the underlying form.

One key lesson that users of dynamic languages have learned over and over, yet bears repeating: instanceof is insufficient. Feature testing is the only way forward for interoperability. In this light, we can define a very light API that can be shimmed onto any of the existing native binary classes to at least let us read binary data in a "common" way:

String.prototype.getBytes: decodes string, returns Binary object
Binary.prototype.get: returns Number byte
Binary.prototype.toString: encodes string, returns String

It's important to note that Binary need not be an actual class, just an idea. The underlying class can be anything that is suitable in the hosting runtime.

Here's the layout of the various Binary feature APIs:

Binary (no mutable APIs defined, just `prototype.get` and friends)
    
    MutableBinary (defines `prototype.set`)
        
        MutableLengthBinary (defines `prototype.length [[Put]]`)
            
            ByteArray (adds Array.prototype conventions, from Binary/B)
            
        SubsettableBinary (adds `prototype.subset`)
            
            Buffer (SubsettableBinary plus node and Binary/F extras)
    
    ByteString (adds String.prototype conventions, from Binary/B)

Elaborating on the hierarchy a bit, the next level of specificity is mutability: being able to change the bytes:

MutableBinary.prototype.set: sets byte at index, returns Number byte

There are certainly uses for more optimized interaction paradigms, like copying bytes from some Binary to a MutableBinary. We can specify these with additional higher-level interfaces but fundamentally all MutableBinary needs is Binary and a set method.

MutableBinary says nothing about the length semantics of a Binary object -- nowhere in the interface is there a means to even alter the length. But some binary paradigms, like byte arrays, allow for mutable lengths. Thus another type of interface is born: MutableLengthBinary.

MutableLengthBinary.prototype.length[[Put]]: sets length to provided Number

Finally, there is another type of interface that extends MutableBinary (but not MutableLengthBinary) -- the ability to get a range -- or subset -- of a binary object. SubsettableBinary requires just one method:

SubsettableBinary.prototype.subset: returns the subsetted SubsettableBinary

It's a relatively simple hierarchy. This hasn't been proposed as a standard but if met with optimism from the CommonJS and node.js communities it will be.

There are no doubt more interesting or useful interfaces we may want to build on top of these base specs. For instance we should probably specify methods which can be optimized by the runtime (such as Binary.prototype.slice). But what is outlined above out to be enough to model the interaction of pretty much every binary class in all of javascriptland.

So while this prototypal hierarchy will be made available in this library, it is really nothing more than javascript engineering porn. As mentioned above: we should not need instanceof and friends to do useful things with binary data. We should be able to work with binary data on all runtimes -- we should be able to do this today.

License

The vast majority of this code was lifted from the Persevere's promised-io package. As such it is licensed under the AFL or BSD license.

It is as yet undetermined whether this will be a Persevere project or even a separate Dojo foundation project, but as this is possible all contributions will require a Dojo CLA.

Readme

Keywords

none

Package Sidebar

Install

npm i commons

Weekly Downloads

647

Version

0.0.1-pre

License

AFL/BSD

Last publish

Collaborators

  • deanlandolt