UNPKG

ucsc-xena-client

Version:

UCSC Xena Client. Functional genomics visualizations.

156 lines (136 loc) 5.25 kB
// 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). 'use strict'; 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 };