// Const var TRANSFORMED_TYPE_KEY = '@t'; var CIRCULAR_REF_KEY = '@r'; var KEY_REQUIRE_ESCAPING_RE = /^#*@(t|r)$/; var GLOBAL = (function getGlobal () { // NOTE: see http://www.ecma-international.org/ecma-262/6.0/index.html#sec-performeval step 10 var savedEval = eval; return savedEval('this'); })(); var TYPED_ARRAY_CTORS = { 'Int8Array': typeof Int8Array === 'function' ? Int8Array : void 0, 'Uint8Array': typeof Uint8Array === 'function' ? Uint8Array : void 0, 'Uint8ClampedArray': typeof Uint8ClampedArray === 'function' ? Uint8ClampedArray : void 0, 'Int16Array': typeof Int16Array === 'function' ? Int16Array : void 0, 'Uint16Array': typeof Uint16Array === 'function' ? Uint16Array : void 0, 'Int32Array': typeof Int32Array === 'function' ? Int32Array : void 0, 'Uint32Array': typeof Uint32Array === 'function' ? Uint32Array : void 0, 'Float32Array': typeof Float32Array === 'function' ? Float32Array : void 0, 'Float64Array': typeof Float64Array === 'function' ? Float64Array : void 0 }; var ARRAY_BUFFER_SUPPORTED = typeof ArrayBuffer === 'function'; var MAP_SUPPORTED = typeof Map === 'function'; var SET_SUPPORTED = typeof Set === 'function'; var BUFFER_FROM_SUPPORTED = typeof Buffer === 'function'; var TYPED_ARRAY_SUPPORTED = function (typeName) { return !!TYPED_ARRAY_CTORS[typeName]; }; // Saved proto functions var arrSlice = Array.prototype.slice; // Default serializer var JSONSerializer = { serialize: function (val) { return JSON.stringify(val); }, deserialize: function (val) { return JSON.parse(val); } }; // EncodingTransformer var EncodingTransformer = function (val, transforms) { this.references = val; this.transforms = transforms; this.circularCandidates = []; this.circularCandidatesDescrs = []; this.circularRefCount = 0; }; EncodingTransformer._createRefMark = function (idx) { var obj = Object.create(null); obj[CIRCULAR_REF_KEY] = idx; return obj; }; EncodingTransformer.prototype._createCircularCandidate = function (val, parent, key) { this.circularCandidates.push(val); this.circularCandidatesDescrs.push({ parent: parent, key: key, refIdx: -1 }); }; EncodingTransformer.prototype._applyTransform = function (val, parent, key, transform) { var result = Object.create(null); var serializableVal = transform.toSerializable(val); if (typeof serializableVal === 'object') this._createCircularCandidate(val, parent, key); result[TRANSFORMED_TYPE_KEY] = transform.type; result.data = this._handleValue(serializableVal, parent, key); return result; }; EncodingTransformer.prototype._handleArray = function (arr) { var result = []; for (var i = 0; i < arr.length; i++) result[i] = this._handleValue(arr[i], result, i); return result; }; EncodingTransformer.prototype._handlePlainObject = function (obj) { var replicator = this; var result = Object.create(null); var ownPropertyNames = Object.getOwnPropertyNames(obj); ownPropertyNames.forEach(function (key) { var resultKey = KEY_REQUIRE_ESCAPING_RE.test(key) ? '#' + key : key; result[resultKey] = replicator._handleValue(obj[key], result, resultKey); }); return result; }; EncodingTransformer.prototype._handleObject = function (obj, parent, key) { this._createCircularCandidate(obj, parent, key); return Array.isArray(obj) ? this._handleArray(obj) : this._handlePlainObject(obj); }; EncodingTransformer.prototype._ensureCircularReference = function (obj) { var circularCandidateIdx = this.circularCandidates.indexOf(obj); if (circularCandidateIdx > -1) { var descr = this.circularCandidatesDescrs[circularCandidateIdx]; if (descr.refIdx === -1) descr.refIdx = descr.parent ? ++this.circularRefCount : 0; return EncodingTransformer._createRefMark(descr.refIdx); } return null; }; EncodingTransformer.prototype._handleValue = function (val, parent, key) { var type = typeof val; var isObject = type === 'object' && val !== null; if (isObject) { var refMark = this._ensureCircularReference(val); if (refMark) return refMark; } for (var i = 0; i < this.transforms.length; i++) { var transform = this.transforms[i]; if (transform.shouldTransform(type, val)) return this._applyTransform(val, parent, key, transform); } if (isObject) return this._handleObject(val, parent, key); return val; }; EncodingTransformer.prototype.transform = function () { var references = [this._handleValue(this.references, null, null)]; for (var i = 0; i < this.circularCandidatesDescrs.length; i++) { var descr = this.circularCandidatesDescrs[i]; if (descr.refIdx > 0) { references[descr.refIdx] = descr.parent[descr.key]; descr.parent[descr.key] = EncodingTransformer._createRefMark(descr.refIdx); } } return references; }; // DecodingTransform var DecodingTransformer = function (references, transformsMap) { this.references = references; this.transformMap = transformsMap; this.activeTransformsStack = []; this.visitedRefs = Object.create(null); }; DecodingTransformer.prototype._handlePlainObject = function (obj) { var replicator = this; var unescaped = Object.create(null); var ownPropertyNames = Object.getOwnPropertyNames(obj); ownPropertyNames.forEach(function (key) { replicator._handleValue(obj[key], obj, key); if (KEY_REQUIRE_ESCAPING_RE.test(key)) { // NOTE: use intermediate object to avoid unescaped and escaped keys interference // E.g. unescaped "##@t" will be "#@t" which can overwrite escaped "#@t". unescaped[key.substring(1)] = obj[key]; delete obj[key]; } }); for (var unsecapedKey in unescaped) obj[unsecapedKey] = unescaped[unsecapedKey]; }; DecodingTransformer.prototype._handleTransformedObject = function (obj, parent, key) { var transformType = obj[TRANSFORMED_TYPE_KEY]; var transform = this.transformMap[transformType]; if (!transform) throw new Error('Can\'t find transform for "' + transformType + '" type.'); this.activeTransformsStack.push(obj); this._handleValue(obj.data, obj, 'data'); this.activeTransformsStack.pop(); parent[key] = transform.fromSerializable(obj.data); }; DecodingTransformer.prototype._handleCircularSelfRefDuringTransform = function (refIdx, parent, key) { // NOTE: we've hit a hard case: object reference itself during transformation. // We can't dereference it since we don't have resulting object yet. And we'll // not be able to restore reference lately because we will need to traverse // transformed object again and reference might be unreachable or new object contain // new circular references. As a workaround we create getter, so once transformation // complete, dereferenced property will point to correct transformed object. var references = this.references; var val = void 0; Object.defineProperty(parent, key, { configurable: true, enumerable: true, get: function () { if (val === void 0) val = references[refIdx]; return val; }, set: function (value) { val = value; return val; } }); }; DecodingTransformer.prototype._handleCircularRef = function (refIdx, parent, key) { if (this.activeTransformsStack.indexOf(this.references[refIdx]) > -1) this._handleCircularSelfRefDuringTransform(refIdx, parent, key); else { if (!this.visitedRefs[refIdx]) { this.visitedRefs[refIdx] = true; this._handleValue(this.references[refIdx], this.references, refIdx); } parent[key] = this.references[refIdx]; } }; DecodingTransformer.prototype._handleValue = function (val, parent, key) { if (typeof val !== 'object' || val === null) return; var refIdx = val[CIRCULAR_REF_KEY]; if (refIdx !== void 0) this._handleCircularRef(refIdx, parent, key); else if (val[TRANSFORMED_TYPE_KEY]) this._handleTransformedObject(val, parent, key); else if (Array.isArray(val)) { for (var i = 0; i < val.length; i++) this._handleValue(val[i], val, i); } else this._handlePlainObject(val); }; DecodingTransformer.prototype.transform = function () { this.visitedRefs[0] = true; this._handleValue(this.references[0], this.references, 0); return this.references[0]; }; // Transforms var builtInTransforms = [ { type: '[[NaN]]', shouldTransform: function (type, val) { return type === 'number' && isNaN(val); }, toSerializable: function () { return ''; }, fromSerializable: function () { return NaN; } }, { type: '[[undefined]]', shouldTransform: function (type) { return type === 'undefined'; }, toSerializable: function () { return ''; }, fromSerializable: function () { return void 0; } }, { type: '[[Date]]', shouldTransform: function (type, val) { return val instanceof Date; }, toSerializable: function (date) { return date.getTime(); }, fromSerializable: function (val) { var date = new Date(); date.setTime(val); return date; } }, { type: '[[RegExp]]', shouldTransform: function (type, val) { return val instanceof RegExp; }, toSerializable: function (re) { var result = { src: re.source, flags: '' }; if (re.global) result.flags += 'g'; if (re.ignoreCase) result.flags += 'i'; if (re.multiline) result.flags += 'm'; return result; }, fromSerializable: function (val) { return new RegExp(val.src, val.flags); } }, { type: '[[Error]]', shouldTransform: function (type, val) { return val instanceof Error; }, toSerializable: function (err) { return { name: err.name, message: err.message, stack: err.stack }; }, fromSerializable: function (val) { var Ctor = GLOBAL[val.name] || Error; var err = new Ctor(val.message); err.stack = val.stack; return err; } }, { type: '[[ArrayBuffer]]', shouldTransform: function (type, val) { return ARRAY_BUFFER_SUPPORTED && val instanceof ArrayBuffer; }, toSerializable: function (buffer) { var view = new Int8Array(buffer); return arrSlice.call(view); }, fromSerializable: function (val) { if (ARRAY_BUFFER_SUPPORTED) { var buffer = new ArrayBuffer(val.length); var view = new Int8Array(buffer); view.set(val); return buffer; } return val; } }, { type: '[[Buffer]]', shouldTransform: function (type, val) { return BUFFER_FROM_SUPPORTED && val instanceof Buffer; }, toSerializable: function (buffer) { return arrSlice.call(buffer); }, fromSerializable: function (val) { if (BUFFER_FROM_SUPPORTED) return Buffer.from(val); return val; } }, { type: '[[TypedArray]]', shouldTransform: function (type, val) { return Object.keys(TYPED_ARRAY_CTORS).some(function (ctorName) { return TYPED_ARRAY_SUPPORTED(ctorName) && val instanceof TYPED_ARRAY_CTORS[ctorName]; }); }, toSerializable: function (arr) { return { ctorName: arr.constructor.name, arr: arrSlice.call(arr) }; }, fromSerializable: function (val) { return TYPED_ARRAY_SUPPORTED(val.ctorName) ? new TYPED_ARRAY_CTORS[val.ctorName](val.arr) : val.arr; } }, { type: '[[Map]]', shouldTransform: function (type, val) { return MAP_SUPPORTED && val instanceof Map; }, toSerializable: function (map) { var flattenedKVArr = []; map.forEach(function (val, key) { flattenedKVArr.push(key); flattenedKVArr.push(val); }); return flattenedKVArr; }, fromSerializable: function (val) { if (MAP_SUPPORTED) { // NOTE: new Map(iterable) is not supported by all browsers var map = new Map(); for (var i = 0; i < val.length; i += 2) map.set(val[i], val[i + 1]); return map; } var kvArr = []; for (var j = 0; j < val.length; j += 2) kvArr.push([val[i], val[i + 1]]); return kvArr; } }, { type: '[[Set]]', shouldTransform: function (type, val) { return SET_SUPPORTED && val instanceof Set; }, toSerializable: function (set) { var arr = []; set.forEach(function (val) { arr.push(val); }); return arr; }, fromSerializable: function (val) { if (SET_SUPPORTED) { // NOTE: new Set(iterable) is not supported by all browsers var set = new Set(); for (var i = 0; i < val.length; i++) set.add(val[i]); return set; } return val; } } ]; // Replicator var Replicator = module.exports = function (serializer) { this.transforms = []; this.transformsMap = Object.create(null); this.serializer = serializer || JSONSerializer; this.addTransforms(builtInTransforms); }; // Manage transforms Replicator.prototype.addTransforms = function (transforms) { transforms = Array.isArray(transforms) ? transforms : [transforms]; for (var i = 0; i < transforms.length; i++) { var transform = transforms[i]; if (this.transformsMap[transform.type]) throw new Error('Transform with type "' + transform.type + '" was already added.'); this.transforms.push(transform); this.transformsMap[transform.type] = transform; } return this; }; Replicator.prototype.removeTransforms = function (transforms) { transforms = Array.isArray(transforms) ? transforms : [transforms]; for (var i = 0; i < transforms.length; i++) { var transform = transforms[i]; var idx = this.transforms.indexOf(transform); if (idx > -1) this.transforms.splice(idx, 1); delete this.transformsMap[transform.type]; } return this; }; Replicator.prototype.encode = function (val) { var transformer = new EncodingTransformer(val, this.transforms); var references = transformer.transform(); return this.serializer.serialize(references); }; Replicator.prototype.decode = function (val) { var references = this.serializer.deserialize(val); var transformer = new DecodingTransformer(references, this.transformsMap); return transformer.transform(); };