Skip to content
David Nolen edited this page Mar 12, 2015 · 38 revisions

Reading

In order to read Transit encoded JSON you need to construct a reader:

var reader = transit.reader("json");

Currently "json" is the only type of reader available. The above will construct a reader instance which has only one method, read:

var anArray = reader.read("[1,2,3]");

Most JSON values will read as you expect but remember that the map encoding returns ES6-like Maps not JavaScript objects. The primary difference with ES6 Maps is that lookup is based on value not reference:

var aMap = reader.read('{"foo":"bar"}');
aMap.get("foo");

Writing

Like reading, writing is fairly straightforward. Constructing a writer looks very much like constructing a reader:

var writer = transit.writer("json");

Writers only have a single method, write:

writer.write([1,2,3]);

This will return the string '[1,2,3]' as expected.

Compare to writing map-like values:

writer.write({foo:"bar"});

This will return the string'["^ ","foo","bar"]'. Maps get written out as arrays as this form is more efficient for decoding. For debugging purposes it's useful to construct a verbose writer:

var vwriter = transit.writer("json-verbose");

And now the result of writing map-like values is easier to read:

vwriter.write({foo:"bar"});

This will return the string '{"foo":"bar"}'.

Writing Custom Values

Being able to easily write out graphs of JavaScript objects is one the big benefits of transit-js. transit-js will recursively encode graphs of objects and Transit ground values like integers and dates need no special treatment.

To demonstrate this lets define some simple geometry primitives:

var Rect = function(origin, size) {
  this.origin = origin;
  this.size = size;
};

var Point = function(x, y) {
  this.x = x;
  this.y = y;
};

var Size = function(width, height) {
  this.width = width;
  this.height = height;
};

var aRect = new Rect(new Point(0, 0), new Size(150,150));

In order to write out aRect we need write handlers for all of the types involved. First let's write a handler for Point:

var PointHandler = transit.makeWriteHandler({
  tag: function(v, h) { return "point" },
  rep: function(v, h) { return [v.x, v.y]; },
  stringRep: function(v, h) { return null; }
});

Write handlers are constructed with transit.makeWriteHandler. Custom types always become tagged values on the wire and the handler methods specify how your instance will become a Transit tagged value. Write handlers must supply at least the first two of the three methods: tag, rep, and stringRep. Each handler method recieves the instance v as the first argument, and the handler h itself as the second argument.

tag should be a function that will take the instance and return a string based tag. You can of course use the instance argument v to write out different tags if you like but we're going to keep it simple here.

rep is the representation to use for the tagged value. In this case we simply return an array containing the x and y properties. These properties are numbers, a ground type, so there's nothing more for us to do. It's important that the result of rep be something that transit-js already knows how to encode either via a built-in or provided custom handler.

stringRep is for tagged values that have a sensible representation as JSON object keys (strings). For the most part you can omit this method but we've left it here for completeness.

Now we can construct the following verbose writer and write Point instances:

var vwriter = transit.writer("json-verbose", {
  "handlers": transit.map([
     Point, PointHandler
  ])
});
vwriter.write(new Point(1.5,2.5));

This will return '{"~#point:[1.5,2.5]"}'. Notice that we had to pass the handlers as a transit.map. This is because JavaScript objects only support string keys.

Now let's write the handlers for Size and Rect:

var SizeHandler = transit.makeWriteHandler({
  tag: function(v, h) { return "size"; },
  rep: function(v, h) { return [v.width, v.height]; },
  stringRep: function(v, h) { return null; }
});

var RectHandler = transit.makeWriteHandler({
  tag: function(v, h) { return "rect"; },
  rep: function(v, h) { return [v.origin, v.size]; },
  stringRep: function(v, h) { return null; }
});

That's it, we can now write out Rect instances!

var vwriter = transit.writer("json-verbose", {
  "handlers": transit.map([
     Point, PointHandler,
     Size, SizeHandler,
     Rect, RectHandler
  ])
});
vwriter.write(aRect);

This will return '{"~#rect":[{"~#point":[0,0]},{"~#size":[150,150]}]}'. As we said earlier transit-js will recurse through the values returned by the rep methods and encode as needed.

Reading Custom Types

Now that we can write custom types we will want to be able read them.

var reader = transit.reader("json", {
  "handlers": {
    "point": function(rep) { return new Point(rep[0], rep[1]); },
    "size": function(rep) { return new Size(rep[0], rep[1]); },
    "rect": function(rep) { return new Rect(rep[0], rep[1]); }
  }
});

Reading is considerably simpler. When a tagged value is encountered the corresponding handler is invoked with the representaiton that was written on the wire - in our case we just used arrays (we could have used maps).

Notice that the Rect handler doesn't need to instantiate Point or Size. Again transit-js is recursive and these will have already been instantiated for you.

Avoiding Duplication

In real applications you may find yourself with many different types that share most of their representation (possibly through prototypal inheritance). In this case it may be cumbersome to define a large set of handlers. Via transitTag you can easily reap the benefits of inheritance.

For example you can in your base prototype declare a transitTag and give it whatever string value you want.

var BaseClass = function() {};
BaseClass.prototype.transitTag = "base";

Now when you create your handler you can leverage inheritance:

var MyWriteHandler = transit.makeWriteHandler({
  tag: function(v) { return v.tag(); },
  rep: function(v) { return v.rep(); }
});

var writer = transit.writer("json", {
  "handlers": transit.map([
    "base", MyWriteHandler
  ])
});

Instances are now completely in control of the tag string and the rep value - implementations can be shared in whatever way best fits your application.

On the read side because read handlers are generally very simple it's easy to dynamically generate the reader handlers option.

Customizing Map & Array Decoding

It is sometimes useful to be able to control the the decoding of Transit maps and arrays. For this purpose transit-js supplies two low-level options when constructing readers: mapBuilder and arrayBuilder. Builders must supply at a minimum three methods: init, add, and finalize. Builders may also supply an optional fromArray parameter in the case that efficient construction is possible from an array representation.

In the following example we construct a reader that returns immutable-js's Map and List instances instead of transit-js maps and JavaScript arrays:

var rdr = transit.reader("json", {
  arrayBuilder: {
    init: function(node) { return Immutable.List().asMutable(); },
    add: function(ret, val, node) { return ret.push(val); },
    finalize: function(ret, node) { return ret.asImmutable(); },
    fromArray: function(arr, node) {
      return Immutable.List.from(arr);
    }
  },
  mapBuilder: {
    init: function(node) { return Immutable.Map().asMutable(); },
    add: function(ret, key, val, node) { return ret.set(key, val); },
    finalize: function(ret, node) { return ret.asImmutable(); }
  }
});

Customizing Map & Array Encoding

It's also possible to encode values as Transit maps and arrays. We can easily write out immutable-js Map and List instances as Transit maps and arrays respectively:

var ListHandler = transit.makeWriteHandler({
  tag: function(v) { return "array"; },
  rep: function(v) { return v; },
  stringRep: function(v) { return null; }
});

var MapHandler = transit.makeWriteHandler({
  tag: function(v) { return "map"; },
  rep: function(v) { return v; },
  stringRep: function(v) { return null; }
});

var wrtr = transit.writer("json-verbose", {
  handlers: transit.map([
    Immutable.List, ListHandler,
    Immutable.Map, MapHandler
  ]) 
});

The "array" and "map" tags are treated specially. Notice that the rep handlers can just return the instance. This is because immutable-js collections implement forEach. Without a forEach implementation transit-js cannot traverse your custom map/array-like collections and recursively encode their contents.

Default Write Handlers

In some cases supplying transitTag may be undesirable or unsuitable. You can also supply a default write handler. This handler will always be invoked if no other handlers can be found:

var w = transit.writer("json", {
  "handlers": transit.map([
     "default", DefaultHandler
  ])
});

Foreign Values

In some situations foreign JavaScript values from other JavaScript Contexts may leak into your program. However you still want to map these values to handlers you have in hand. As of 0.8.751 writers support a new option handlerForForeign. This function should return the correct handler for the value.

Here is a complete Node.js example:

var vm   = require("vm"),
    ctxt = vm.createContext(),
    arr  = vm.runInContext("(function(){return [];})()", ctxt, "test");

var w = transit.writer("json", {
    handlerForForeign: function(x, handlers) {
        if(Array.isArray(x)) {
            return handlers.get(Array);
        } else if(typeof x == "object") {
            return handlers.get(Object);
        }
    }
});

w.write(arr); // "[]"