serializer/serializer.ts

270 lines
6.5 KiB
TypeScript

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;
}