/* eslint-disable no-unsafe-finally */ /** * ############# PURE EVIL ################### * Overwrites JSON.parse and JSON.stringify to * encode object class information. * * We make a best attempt to support all JSON features * and be as unobtrusive as possible, but still, monkey * patching is dangerous. * * User beware. Here be dragons. */ JSON.debug = {}; // Keep the old versions around JSON.debug.parse = JSON.parse.bind(JSON); JSON.debug.stringify = JSON.stringify.bind(JSON); // We use this to prevent us from spamming the log if a // specific class isn't able to be found in the serialiser // Also contains a count of the number of non-serialised object // for a given classname/path JSON.debug.stringifyMissingClasses = {}; JSON.debug.stringifyClasses = {}; JSON.debug.parseMissingClasses = {}; JSON.debug.parseClasses = {}; JSON.proto = Object.freeze({ key: "__json_class__", path: "__json_class_path__", reviver: "__json_class_reviver__", }); JSON.stringify = function (o, userReplacer, ...args) { return JSON.debug.stringify.call( this, o, (key, val) => { let nextVal = val; try { if (val && typeof val === "object" && val.constructor !== Object && !Array.isArray(val)) { const classCtor = Object.getPrototypeOf(val).constructor; const classPath = classCtor.prototype[JSON.proto.path] || classCtor.name; if (JSON.debug.getFromPath(classPath)) { nextVal = { [JSON.proto.key]: classPath, ...val, }; if (!JSON.debug.stringifyClasses[classPath]) { console.log(`Serializing object with class ${classPath}`); JSON.debug.stringifyClasses[classPath] = 0; } JSON.debug.stringifyClasses[classPath]++; } else { if (!JSON.debug.stringifyMissingClasses[classPath]) { console.warn(`Serializing object with class ${classPath} failed. Skipping objects like`, val); JSON.debug.stringifyMissingClasses[classPath] = 0; } JSON.debug.stringifyMissingClasses[classPath]++; } } } catch (e) { console.error(`Critical error occurred in JSON.parse. Attempting to continue`, e); } finally { return userReplacer ? userReplacer(key, nextVal) : nextVal; } }, ...args ); }; JSON.debug.getFromPath = function getFromPath(path) { return (path || "").split(".").reduce((nextObj, nextPathPart) => { return (nextObj || {})[nextPathPart]; }, window); }; JSON.parse = function (string, userReviver, ...args) { return JSON.debug.parse.call( this, string, (key, val) => { let nextVal = val; const className = val[JSON.proto.key]; if (className) { const maybeClass = JSON.debug.getFromPath(className); if (maybeClass && (typeof maybeClass === "object" || typeof maybeClass === "function")) { nextVal = JSON.revive(maybeClass, val); if (!JSON.debug.parseClasses[className]) { console.log(`Revived object with class ${className}`, maybeClass); JSON.debug.parseClasses[className] = 0; } JSON.debug.parseClasses[className]++; } else { if (!JSON.debug.parseMissingClasses[className]) { console.warn(`Parsing instance of ${className} failed. Skipping objects like`, val, maybeClass); JSON.debug.parseMissingClasses[className] = 0; } JSON.debug.parseMissingClasses[className]++; } } delete nextVal[JSON.proto.key]; return userReviver ? userReviver(key, nextVal) : nextVal; }, ...args ); }; JSON.revive = function (reviverClass, data) { let nextVal = Object.create( reviverClass.prototype, Object.entries(data).reduce((acc, [key, val]) => { acc[key] = { value: val, configurable: true, writeable: true, enumerable: true }; return acc; }, {}) ); if (reviverClass[JSON.proto.reviver]) { try { nextVal = reviverClass[JSON.proto.reviver].call(nextVal); } catch (e) { console.error(`Failed to revive custom class ${reviverClass.name}`, e); } } return nextVal; };