1.0.5 • Published 1 year ago

@cdot/cbor v1.0.5

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

CBOR

CBOR library with pluggable tag handlers.

There are several CBOR implementations available from npm and github. This is not intended to replace them, it was written when experiments with the existing solutions failed, due to complexity, functionality, bugs, or poor documentation.

This implementation doesn't claim to be the best; it is intended to write and read complex ES6+ Javascript structures with small output, be lightweight, well documented, and easy to extend. It has the capability to read CBOR it didn't generate itself, but that's not the primary intention. It's a lot lighter than cbor-x (cbor-x is recommended if you want a general CBOR solution).

  • Zero production dependencies
  • Works in browser and node.js
  • Well documented, clearly structured Javascript

Installation

Install it using:

$ npm install @cdot/cbor

Usage

If you are just using it to save/restore simple data structures, with no requirement for data size optimisation, use it as follows.

To encode some data using CBOR:

const frozen = CBOR.Encoder.encode(data);

frozen is a Uint8Array, the buffer of which can be written e.g. to a file using node.js fs, or to a network partner.

To decode incoming data:

const decoded = CBOR.Decoder.decode(frozen);

frozen can be a DataView, a TypedArray, or an ArrayBuffer.

ESM

import { Encoder, Decoder } from "@cdot/cbor";
const frozen = Encoder.encode(data);

CommonJS

const { Encoder, Decoder } = require("@cdot/cbor");
const frozen = Encoder.encode(data);

AMD

require(["@cdot/cbor"], CBOR => {
   const frozen = CBOR.Encoder.encode(data);
});

Browser

<script type="text/javascript" src="./node_modules/@cdot/cbor/dist/index.js"></script>
    <script>
      const frozen = CBOR.Encoder.encode(data);
    </script>

Tags

CBOR uses the concept of "tags", user-defined extension points to the protocol. Included are a number of handlers that define some useful behaviurs.

We use subclass factories to implement handlers as mixins, as described here. This lets you easily use multiple tag handlers. For example:

const handler =
   new (IDRefHandler(TypeMapHandler(TagHandler)))();

Generally if you use any of the handlers described below, you should pass the same parameters to the handlers used to encode and decode the data (though there are exceptions to this).

IDREFHandler

Optimises the output by never saving the same structure twice. For example, you might have the following:

const simple = { ... };
const complex = { once: simple, twice: simple };

The basic encoder will write two copies of simple to the output. If instead we use the IDREF handler:

const handler = new IDREFHandler(TagHandler)();
const frozen = CBOR.Encoder.encode(complex, handler);
...
const decoded = CBOR.Decoder.decode(frozen, handler);

The handler will now spot the double-reference to simple and only write it once, and the decoder will restore the original data structure. The handler also supports self-referential structures.

TypeMapHandler

Lets you record complex types (classes) in the output data. For example:

class Thing {
   field = 99.99;
}
const data = new Thing();

The basic encoder will write this data structure, but when decoded you will get back:

{ field: 99.99 }

i.e. the prototype Thing will be lost. If instead we write:

const handler = new TypeMapHandler(TagHandler)({
    typeMap: { Thing: Thing }
});
const frozen = CBOR.Encoder.encode(data, handler);
...
const decoded = CBOR.Decoder.decode(frozen, handler);

when this is decoded using the same tag handler, the original prototype will be restored.

Note that instances of subclasses of a class mentioned in the type map will be regenerated according to the type map used with the decoder. So it's possible to change the class of an object thus:

class Thing { ... }
class subThing extends Thing { ... }
const data = new subThing();
const outhandler = new TypeMapHandler(TagHandler)({
    // Save all subclasses of 'Thing' as class 'Thing'
    typeMap: { Thing: Thing }
});
const frozen = CBOR.Encoder.encode(data, outhandler);
...
class newThing extends Thing { ... }
const inhandler = new TypeMapHandler(TagHandler)({
    // Map all saved 'Thing's to 'newThing'
    typeMap: { Thing: newThing }
});
const decoded = CBOR.Decoder.decode(frozen, inhandler);

decoded will now be an instance of class newThing with all the same attributes as the original data (including any additional attributes added when it was a subThing).

Note that if you want to use both the IDREFHandler and the TypeMapHandler then you MUST include the IDREFHandler in the prototype chain first, thus:

const inhandler = new IDREFHandler(TypeMapHandler(TagHandler)(...));

This will NOT work:

const inhandler = new TypeMapHandler(IDREFHandler(TagHandler)(...));

KeyDictionaryHandler

If you are saving lots of data structures that are the same, then the basic encoder will save all the attribute names for each instance saved. So if you have a structure like this:

class Location {
  latitude = 0;
  longitude = 0;
}
const data = [ ... array of 10,000 Location ... ]

then there will be 10,000 copies of the word latitude and 10,000 copies of the word longitude in the output. To optimise this, the KeyDictionaryHandler creates a lookup table of key strings and replaces the keys with a simple integer ID. When the data is decoded, the key dictionary is used to recreate the original attribute names.

The handler can be used with an known list of keys e.g.

const handler = new (KeyDictionaryHandler(TagHandler))({
  keys: [ "latitude", "longitude" ]
});

You can also provide a partial key list:

const handler = new (KeyDictionaryHandler(TagHandler))({
  keys: [ "latitude" ]
});

In this case the longitude key will be added to the dictionary during encoding. The dictionary written to the output will only contain the additional keys added during encoding.

If you don't provide any keys, or provide an empty keys array, then all keys will be saved.

Other Tag Handlers

You can define your own tags and tag handlers. Simply follow the pattern of one of the existing tag handlers. If you generate a pull request for your new handler, please ensure you provide a mocha test for it and test the interaction with the other handlers.

Streams

You can also use the encoder and decoder on streams. See the code documentation for details.