ucsc-xena-client
Version:
UCSC Xena Client. Functional genomics visualizations.
156 lines (136 loc) • 5.25 kB
JavaScript
// Compact encoding for structures with multiple references to the same
// objects, e.g. the type of structures we get by doing a naive path copy in
// ehmutable.js.
//
// Such structures waste a lot of space when JSON stringified, because each
// reference is stringified independently. The routines here will instead
// insert a JSON pointer if the reference has already been stringified. When
// parsing, we resolve any pointers.
//
// One drawback of this technique is it prevents using JSON pointers in our
// structures. If this becomes a requirement, we could use a private encoding
// for pointers, e.g. `##ref#${path.join('/')}`, or etc.
//
// To identify references which have already been stringified, we use an es6
// Map, which gives us O(1) performance, but uses reference semantics. Note
// this means two equal objects will be stringified independently if they are
// not the same reference object. If value semantics are required we would need
// to do a linear search with _.isEqual, or use a hashing method, or use better
// underlying data structures (i.e. not plain js).
;
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
var _ = require('./underscore_ext');
var isRef = function isRef(x) {
return _.has(x, '$ref');
};
var type = function type(x) {
return isRef(x) ? 'ref' : _.isUndefined(x) ? 'undefined' : _.isArray(x) ? 'array' : _.isObject(x) ? 'object' : 'primitive';
};
var m = function m(x, methods) {
return methods[type(x)](x);
};
// escape slash so we can join/split on it.
var escSlash = function escSlash(s) {
return s.toString().replace(/\//g, '~1');
};
var unescSlash = function unescSlash(s) {
return s.toString().replace(/~1/g, '/');
};
var escQuote = function escQuote(s) {
return s.toString().replace(/"/g, '\\"');
};
// for objects: filter props that shouldn't be JSON encoded.
var validKeys = function validKeys(x) {
return _.pick(x, function (v) {
return !_.isUndefined(v) && !_.isFunction(v);
});
};
var mapPairs = function mapPairs(x, fn) {
return _.pairs(x).map(fn);
};
var kvStr = function kvStr(k, v) {
return '"' + escQuote(k) + '":' + v;
};
function stringify(x) {
var cache = new Map();
// make fn that caches references to objects
var addToCache = function addToCache(path, fn) {
return function (x) {
cache.set(x, JSON.stringify({ '$ref': '#/' + path.map(escSlash).join('/') }));
return fn(x);
};
};
var stringifyCached = function stringifyCached(y, path) {
return cache.has(y) ? cache.get(y) : m(y, {
ref: function ref() {
throw new Error('Can\'t stringify JSON pointers');
},
array: addToCache(path, function (x) {
return '[' + x.map(function (v, i) {
return stringifyCached(v, [].concat(_toConsumableArray(path), [i]));
}).join(',') + ']';
}),
object: addToCache(path, function (x) {
return '{' + mapPairs(validKeys(x), function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
k = _ref2[0],
v = _ref2[1];
return kvStr(k, stringifyCached(v, [].concat(_toConsumableArray(path), [k])));
}).join(',') + '}';
}),
primitive: function primitive(x) {
return JSON.stringify(x);
},
'undefined': function undefined() {
return 'null';
}
});
};
return stringifyCached(x, []);
}
var refToPath = function refToPath(s) {
return s.$ref.slice(2).split('/').map(unescSlash);
};
var getRefIn = function getRefIn(x, ref) {
return _.getIn(x, refToPath(ref));
};
// After running fn on coll, if the values in the result are identical to the
// values in the input, return the input (to preserve reference equality).
var preserveId = function preserveId(fn) {
return function (coll) {
var calc = fn(coll);
for (var i in calc) {
if (calc[i] !== coll[i]) {
return calc;
}
}
return coll;
};
};
function parse(x) {
var refd = JSON.parse(x),
cache = new Map();
var findRef = function findRef(ref) {
if (!cache.has(ref.$ref)) {
cache.set(ref.$ref, resolve(getRefIn(refd, ref))); //eslint-disable-line no-use-before-define
}
return cache.get(ref.$ref);
};
var resolve = function resolve(y) {
return m(y, {
ref: findRef,
array: preserveId(function (x) {
return x.map(resolve);
}),
object: preserveId(function (x) {
return _.mapObject(x, resolve);
}),
primitive: function primitive(x) {
return x;
}
});
};
return resolve(refd);
}
module.exports = { stringify: stringify, parse: parse };