npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mousse

v0.3.1

Published

Deserialization of JavaScript objects

Downloads

225

Readme

Mousse

A serialization library that serializes graphs of JavaScript objects.

Its main purpose is to provide the features that are missing in JSON and a mechanism to easily extend the serialization format with new types or custom JavaScript objects.

This is the library used by the Montage framework to manage the serialization of objects in its templates.

What it does that JSON doesn't

  • Named objects
  • References and circular references (no more TypeError: Converting circular structure to JSON)
  • Regular Expressions
  • Custom Types
  • Asynchronous revivers

API Reference

Serializer()

  • serializeObject(object)

    Serializes an object into a string.

  • serialize(objects)

    Serializes objects into a string, each object is passed with a label associated with it. Objects are passed in an object literal {label1: object1, label2: object2, ..., labelN: objectN}.

  • setSerializationIndentation(indentation)

    Set the indentation level of the serialization string (in number of spaces).

Deserializer(serializationString)

  • constructor

    Creates a deserialization object to deserialize the objects serialized in serializationString.

  • deserializeObject()

    Returns a promise for an object that was serialized with serializeObject.

  • deserialize(instances)

    Returns a promise for the objects that were serialized with serialize. This result is an object literal with the deserialized objects and their respective labels: {label1: object1, label2: object2, ..., labelN: objectN}.

    The instances parameter allows to override the deserialization of specific objects by using the instance passed instead, they are passed in an object literal: {label1: object1, label2: object2, ..., labelN: objectN}.

Serialization of JavaScript objects

Multiple Objects

var Serializer = require("mousse").Serializer;

var object = {
    x: 2,
    y: 4
};

var array = [1, 2, 3];

var serializationString = new Serializer().serialize({foo: object, bar: array});

Single Object

When serializing a single object there's no need to provide a label:

var Serializer = require("mousse").Serializer;

var object = {
    x: 2,
    y: 4
};

var serializationString = new Serializer().serializeObject(object);

There's also a shorthand function to serialize a single object:

var serialize = require("mousse").serialize;

var object = {
    x: 2,
    y: 4
};

var serializationString = serialize(object);

Deserialization of JavaScript objects

Multiple Objects

var Deserializer = require("mousse").Deserializer,
    deserializer = new Deserializer(serializationString);

deserializer.deserialize()
.then(function(objects) {
    // deserialized objects are in objects
});

Single Object

var Deserializer = require("mousse").Deserializer,
    deserializer = new Deserializer(serializationString);

deserializer.deserializeObject()
.then(function(object) {
    //
});

Again, like for serialization, there's a shorthand function to deserialize a single object:

var Deserializer = require("mousse").deserialize;

deserialize(serializationString)
.then(function(object) {
    //
});

Consecutive calls to deserializer.deserialize() will create a new set of objects from the serialization.

Serialization Format

The serialization format is inspired by JSON and it may even be considered as an extension. By itself the format is a JSON valid object.

Instead of only serializing a single object, Mousse is able to serialize several independent objects by providing a label for each one. We can look at it as a dictionary.

The base format of the serialization is thus an object with as many entries as labeled objects:

{
    "label1": {
        "value": <object1 serialization>
    },

    "label2": {
        "value": <object2 serialization>
    },

    ...,

    "labelN": {
        "value": <objectN serialization>
    }
}

The following JavaScript objects are supported:

* string
* number
* boolean
* null
* array
* object literal
* regular expression
* references

Native JavaScript objects are stored just like their JSON representation with the exception of regular expressions (which are not supported by JSON).

String

serialize({string: "a string"})

{
    "string": {
        "value": "a string"
    }
}

Number

serialize({number: 42})

{
    "number": {
        "value": 42
    }
}

Boolean

serialize({bool: true})

{
    "bool": {
        "value": true
    }
}

Null

serialize({nil: null})

{
    "nil": {
        "value": null
    }
}

Array

serialize({array: [1, 2, 3]})

{
    "array": {
        "value": [1, 2, 3]
    }
}

Object Literal

serialize({object: {x: 2, y: 4}})

{
    "object": {
        "value": {
            "x": 2,
            "y": 4
        }
    }
}

Regular Expression

serialize({regexp: /regexp/gi})

{
    "regexp": {
        "value": {"/": {
            "source": "regexp",
            "flags": "gi"
        }}
    }
}

References

Since objects have labels it is possible to serialize a reference to an object instead of serializing the entire object again as it happens in JSON:

var manager = {
    name: "Foo"
}

var employee = {
    name: "Bar",
    manager: manager
}

serialize({manager: manager, employee: employee})

{
    "manager": {
        "value": {
            "name": "Foo"
        }
    },

    "employee": {
        "value": {
            "name": "Bar",
            "manager": {"@": "manager"}
        }
    }
}

References also solves cycles in an object graph:

var object = {};
object.self = object;

serialize({object: object});

{
    "object": {
        "value": {
            "self": {"@": "object"}
        }
    }
}

When an object is referred more than once it will automatically be assigned a label and only references will be used to refer to it.

var array = [1, 2, 3];
var object = {
    foo: array,
    bar: array
}

serialize({object: object});

{
    "object": {
        "value": {
            "foo": {"@": "array"},
            "bar": {"@": "array"}
        }
    },

    "array": {
        "value": [1, 2, 3]
    }
}

Custom Objects

Root objects with the value property represent JavaScript objects. It is possible to define other types of objects by adding the necessary logic to recognize them and create them.

Serialization

The logic to serialize a custom object is handled by the Visitor object. This is the object that visits the graph of objects traversed during serialization and knows what data to store for each type. The interface expects a getTypeOf function that returns the type of the object and a visit<Type> function that knows what data to store from the object. Multiple visit<Type> can be defined as long as getTypeOf returns different types.

Visitor.addCustomObjectVisitor({
    getTypeOf: function(value) {
        if (value instanceof Map) {
            return "Map";
        }
    },

    visitMap: function(malker, visitor, object, name) {
        var map = visitor.builder.createCustomObject();
            mapData = object.toObject();

        malker.visit("map", "type");
        malker.visit(mapData, "object");

        visitor.storeValue(map, object, name);
    }
});

The result of the serialization is:

{
    "root": {
        "type": "map",
        "object": {/* map data */}
    }
}

Deserialization

The logic to deserialize a custom object is handled by the Reviver object. This is the object that knows how to revive objects. The interface expects a getTypeOf function that returns the type of the object and a revive<Type> function that knows how to revive the object. Multiple revive<Type> can be defined as long as getTypeOf returns different types.

Example to deserialize:

{
    "root": {
        "type": "map",
        "object": {/* map data */}
    }
}
Reviver.addCustomObjectReviver({
    getTypeOf: function(value) {
        if (value.type === "map") {
            return "Map";
        }
    },

    reviveMap: function(value, context, label) {
        var map = new Map(value.object);

        if (label) {
            context.setObjectLabel(map, label);
        }

        return map;
    }
});

The result of the deserialization is:

{
    root: <Map Object>
}

Reviver functions can be asynchronous by returning a promise to the revived value.

Context

The context object is given as the second parameter to all revive* functions and it is used to set labels on deserialized objects, so they can be accessed after the deserialization, and to get objects that were serialized under a specific label.

  • setObjectLabel(object, label) - Defines the label of object.
  • getObject(label) - Returns the object with label label.

Extending the Serialization Format

The Mousse serialization format can be extended by extending the Builder and Visitor objects.

Serialization

During serialization an AST-like object is created that holds all the data needed to be serialized. It is the role of the Visitor to create this AST.

When the AST is finished it is the role of the Builder to generate an output format by reading the AST. The Builder presented in Mousse generates JSON but it should be possible to create a Builder that generates another format, for instance XML.

The Builder provides the necessary methods to create the AST Nodes:

  • createObjectLiteral()
  • createArray()
  • createObjectReference()
  • createRegExp(regexp)
  • createString(string)
  • createNumber(number)
  • createBoolean(value)
  • createNull()
  • createCustomObject()

These are the JavaScript objects that Mousse supports. In order to create new ones it is necessary to extend the Builder and the Visitor.

To extend the serialization format to know about DOM elements and to serialize them into {"#" "<element id>"} we need to:

Create the AST node

function ElementReference(root, id) {
    Value.call(this, root, id);
}

ElementReference.prototype = Object.create(Value.prototype, {
    constructor: {value: ElementReference},

    _getSerializationValue: {
        value: function() {
            return {"#": this.value};
        }
    }
});

Extend the Builder object

function ExtendedBuilder() {
    Builder.call(this);
}

ExtendedBuilder.prototype = Object.create(Builder.prototype, {
    constructor: {value: ExtendedBuilder},

    createElementReference: {
        value: function(id) {
            return new ElementReference(this._root, id);
        }
    }
});

Extend the Visitor object

function ExtendedVisitor(builder, labeler) {
    Visitor.call(this, builder, labeler);
}

ExtendedVisitor.prototype = Object.create(Visitor.prototype, {
    constructor: {value: ExtendedVisitor},

    getTypeOf: {
        value: function(object) {
            if (!!(object && 1 === object.nodeType)) {
                return "ElementReference";
            }
        }
    },

    visitElementReference: {
        value: function(malker, object, name) {
            var elementReference,
                id = object.id;

            elementReference = this.builder.createElementReference(id);
            this.storeValue(elementReference, object, name);
        }
    }
});

Deserialization

Deserialization is handled by the Reviver and as such this object needs to be extended to understand the new syntax added to the serialization ({"#": "<element id>"}).

ExtendedReviver.prototype = Object.create(Reviver.prototype, {
    constructor: {value: ExtendedReviver},

    getTypeOf: {
        value: function(value) {
            if (value !== null && typeof value === "object"
                && Object.keys(value).length === 1 && "#" in value) {
                return "ElementReference";
            } else {
                return Reviver.prototype.getTypeOf.call(this, value);
            }
        }
    },

    reviveElementReference: {
        value: function(value, context, label) {
            var elementId = value["#"],
                element = document.getElementById(elementId);

            if (label) {
                context.setObjectLabel(element, label);
            }

            return element;
        }
    }
});

Known Issues

Not possible to serialize literal objects that can be mistaken as a reference or a regexp - {"@": "label"} and {"/": {"source": "regexp"}}).