const STOP_CRAWLING = {}; type SerializerNamespace = { [name: string]: { new (): any } }; /** * A deep serializer of javascript objects. */ export class Serializer { private namespace: SerializerNamespace; private namespace_rev: { [classname: string]: string }; private ignored: string[] = []; private stats: any = {}; constructor(namespace: any) { if (Array.isArray(namespace)) { this.namespace = {}; for (let construct of namespace) { this.namespace[construct.name] = construct; } } else { this.namespace = namespace; } this.namespace_rev = {}; for (let [key, value] of Object.entries(this.namespace)) { this.namespace_rev[value.name] = key; } } /** * Add a class to the ignore list */ addIgnoredClass(cl: string | { new (): any }) { this.ignored.push(typeof cl === "string" ? cl : cl.name); } /** * Serialize an object to a string */ serialize(obj: any): string { // Collect objects var objects: Object[] = []; var stats: any = {}; function add(value: any): void { if (objects.indexOf(value) < 0) { objects.push(value); } } crawl(obj, (value) => { if (isObject(value)) { var vtype = classname(value); if (vtype != "" && this.ignored.indexOf(vtype) < 0) { stats[vtype] = (stats[vtype] || 0) + 1; add(value); return value; } else { return STOP_CRAWLING; } } else if (typeof value == "function") { const found = Object.entries(this.namespace).find(([_, cons]) => cons === value ); if (found && this.ignored.indexOf(found[0]) < 0) { // Keep references to constructors in the namespace add(value); } return STOP_CRAWLING; } else { return value; } }); this.stats = stats; // Serialize objects list, transforming deeper objects to links var fobjects = objects.map((value) => this.encodeObject(value)); return JSON.stringify(fobjects, (key, value) => { if ( key != "$f" && (isObject(value) || typeof value == "function") && !value.hasOwnProperty("$c") && !value.hasOwnProperty("$i") ) { return { $i: objects.indexOf(value) }; } else { return value; } }); } /** * Unserialize a string to an object */ unserialize(data: string): any { // Unserialize objects list var fobjects = JSON.parse(data); // Reconstruct objects var objects = fobjects.map((objdata: any) => this.decodeObject(objdata)); // Reconnect links crawl(objects, (value) => { if (value instanceof Object && value.hasOwnProperty("$i")) { return objects[value["$i"]]; } else { return value; } }, true); // Post unserialize hooks crawl(objects[0], (value) => { if ( value instanceof Object && typeof value.postUnserialize === "function" ) { value.postUnserialize(); } }); // First object was the root return objects[0]; } /** * Construct an object from a constructor name */ private constructObject(ctype: string): Object { if (ctype == "Object") { return {}; } else { let cl = this.namespace[ctype]; if (cl) { return Object.create(cl.prototype); } else { console.error("Can't find class", ctype); return {}; } } } /** * Transform an object into encoded representation */ private encodeObject(obj: Object) { const ctype = classname(obj); if (obj instanceof Set || obj instanceof Map) { return { $c: ctype, $f: Array.from(obj), }; } else if (typeof obj == "function") { return { $c: obj.name, }; } else { return { $c: this.namespace_rev[ctype] || ctype, $f: Object.assign({}, obj), }; } } /** * Decode back an encoded representation to a full object */ private decodeObject(objdata: any) { const ctype = objdata.$c; if (ctype == "Set") { return new Set(objdata.$f); } else if (ctype == "Map") { return new Map(objdata.$f); } else if (objdata.$f) { return Object.assign(this.constructObject(ctype), objdata.$f); } else { return this.namespace[ctype]; } } } /** * Check if the argument is an instance of a class */ function isObject(value: any): boolean { return typeof value == "object" && value instanceof Object && !Array.isArray(value); } /** * Recursively crawl through an object, yielding any defined value found along the way * * If *replace* is set to true, the current object is replaced (in array or object attribute) by the result of the callback * * *memo* is used to prevent circular references to be traversed */ function crawl( obj: any, callback: (item: any) => any, replace = false, memo: any[] = [], ): any { if (isObject(obj)) { if (memo.indexOf(obj) >= 0) { return obj; } else { memo.push(obj); } } if (obj !== undefined && obj !== null) { const result = callback(obj); if (result === STOP_CRAWLING) { return undefined; } if (Array.isArray(obj)) { const subresult = obj.map((value) => crawl(value, callback, replace, memo) ); if (replace) { subresult.forEach((value, index) => { obj[index] = value; }); } } else if (obj instanceof Set) { const subresult = new Set(); for (const item of obj) { subresult.add(crawl(item, callback, replace, memo)); } if (replace) { obj.clear(); for (const item of subresult) { obj.add(item); } } } else if (obj instanceof Map) { const subresult = new Map(); for (const [key, item] of obj.entries()) { subresult.set( crawl(key, callback, replace, memo), crawl(item, callback, replace, memo), ); } if (replace) { obj.clear(); for (const [key, item] of subresult.entries()) { obj.set(key, item); } } } else if (obj instanceof Object) { const subresult: any = {}; for (const key in obj) { subresult[key] = crawl(obj[key], callback, replace, memo); } if (replace) { Object.assign(obj, subresult); } } return result; } else { return obj; } } /** * Get the class name of an object. */ function classname(obj: Object): string { return obj.constructor.name; }