diff --git a/package.json b/package.json index 2b0e33e..71f2b23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frigid", - "version": "1.0.4", + "version": "1.0.5", "main": "out/index.js", "types": "out/index.d.ts", "license": "MIT", @@ -13,6 +13,7 @@ }, "scripts": { "prepublish": "yarn build", - "build": "tsc" + "build": "tsc", + "test": "yarn build && node --enable-source-maps test/index.js" } } diff --git a/src/Serializable.ts b/src/Serializable.ts index 244e2fb..cfc9ebf 100644 --- a/src/Serializable.ts +++ b/src/Serializable.ts @@ -11,6 +11,8 @@ export default class Serializable { // format should be a bit more complex, to // avoid this but... simplicity for now... static CLASS_REFERENCE = '$$CLASS_NAME'; + static INSTANCE_DECLARATION = '$$INSTANCE_ID'; + static INSTANCE_REFERENCE = '$$INSTANCE_REF'; // things that need to be stored only at runtime // are keyed with symbols to not interfere with @@ -29,7 +31,11 @@ export default class Serializable { return this.fromSerializableObject(JSON.parse(str)); } + // thisdoesnt operate recursively, it doesnt need to, because dependency + // resoltion isnt required. we simply declare the dependencies. + // so we never touch static serializationDependencies! toSerializableObject() { + const instances: Map = new Map(); const transformValue = (val: any): any => { if(Array.isArray(val)) { @@ -44,15 +50,26 @@ export default class Serializable { } const transformObject = (obj: any): any => { + + // is this a circular reference, or reference to a previously + // known object... + const duplicateObjectLink = reverseLookup(instances, obj); + if(duplicateObjectLink !== null) return { [Serializable.INSTANCE_REFERENCE]: duplicateObjectLink }; + const clone: any = {}; + const newId = instances.size; + clone[Serializable.INSTANCE_DECLARATION] = newId; + instances.set(newId, obj); + for(const prop of Object.keys(obj)) { if(prop.startsWith('_')) continue; + else clone[prop] = transformValue(obj[prop]); + } + + if(obj instanceof Serializable) clone[Serializable.CLASS_REFERENCE] = obj.constructor.name; + + // console.log('recorded instance', newId, obj, instances); - clone[prop] = transformValue(obj[prop]); - } - if(obj instanceof Serializable) { - clone[Serializable.CLASS_REFERENCE] = obj.constructor.name; - } return clone; } @@ -67,7 +84,8 @@ export default class Serializable { return transformObject(this); } - static fromSerializableObject(obj: any) { + static fromSerializableObject(obj: any, instances: Map = new Map()) { + // console.log('deserializing', obj); if(obj[Serializable.CLASS_REFERENCE] !== this.name) return null; const transformValue = (val: any): any => { @@ -79,12 +97,13 @@ export default class Serializable { if(Serializable.CLASS_REFERENCE in val) { const classes = this.serializationDependencies(); const matchingClasses = classes.filter((classObject) => { - classObject.name === val[Serializable.CLASS_REFERENCE] + return classObject.name === val[Serializable.CLASS_REFERENCE] }); if(matchingClasses.length === 1) { - return matchingClasses[0].fromSerializableObject(val); + return matchingClasses[0].fromSerializableObject(val, instances); } else { - return transformObject(val); + throw new Error('Unknown class ' + val[Serializable.CLASS_REFERENCE] + '!\n' + + 'Did you forget to add ' + val[Serializable.CLASS_REFERENCE] + ' to static serializationDependencies?'); } } return transformObject(val); @@ -94,13 +113,23 @@ export default class Serializable { } const transformObject = (obj: any): any => { + let constructedObject = null; + const clone: any = {}; for(const prop of Object.keys(obj)) { if(prop.startsWith('_')) continue; + // if(prop.startsWith('$$')) continue; clone[prop] = transformValue(obj[prop]); } - return clone; + constructedObject = clone; + + if(Serializable.INSTANCE_DECLARATION in obj) { + // console.log('recording instance', obj[Serializable.INSTANCE_DECLARATION], constructedObject); + instances.set(obj[Serializable.INSTANCE_DECLARATION], constructedObject); + } + + return constructedObject; } const transformArray = (arr: any[]): any[] => { @@ -115,9 +144,29 @@ export default class Serializable { if(Serializable.CLASS_REFERENCE in obj) clone.__proto__ = this.prototype; - clone.restore(); + const secondPass = (obj) => { + for(const key of Object.keys(obj)) { + if(key === Serializable.INSTANCE_DECLARATION) delete obj[key]; + if(key === Serializable.CLASS_REFERENCE) delete obj[key]; + const val = obj[key]; + if(typeof val === 'object') { + if(Serializable.INSTANCE_REFERENCE in val) { + const refId = val[Serializable.INSTANCE_REFERENCE]; + if(instances.has(refId)) { + obj[key] = instances.get(refId); + } + } + else obj[key] = secondPass(val); + } + } + return obj; + } - return clone; + const parse = secondPass(clone); + + // clone.restore?.(); + + return parse; } serialize({ @@ -188,4 +237,17 @@ export default class Serializable { function createFilepath(path: string) { return `data/${path}`; +} + + +function reverseLookup(map: Map, value: V): K { + // console.log('searching for', value, 'in', map); + for(const [k, v] of map) { + if(v === value) { + // console.log('found in key', k); + return k; + } + } + // console.log(value, 'not found') + return null; } \ No newline at end of file diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..b859c66 --- /dev/null +++ b/test/index.js @@ -0,0 +1,57 @@ +import { Serializable } from '../out/index.js' + +class Sub extends Serializable { + otherData = sharedObject; + root; + + static serializationDependencies() { + return [Root]; + } +} + + + +class Root extends Serializable{ + stuff = sharedObject; + child; + + static serializationDependencies() { + return [Sub]; + } + + test() { + return { + circular: this.child.root === this, + shared: this.stuff === this.child.otherData + } + } +} + +const sharedObject = {shared: 'data'} + +const root = new Root(); +const sub = new Sub(); + +root.child = sub; +sub.root = root; + +console.clear(); +console.log('#'.repeat(process.stdout.columns)); + + + +console.log(root); +const json = root.toJson(); +console.log(json); +const obj = Root.fromJson(json); +console.log(obj); +const tests = obj.test(); +console.log(tests); + +const passing = Object.values(tests).reduce((v, acc) => v && acc, true); +if(!passing) { + console.log('Some tests failed!'); + process.exit(1); +} else { + console.log('All tests Passed!'); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index ef4f823..ac71f25 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "moduleResolution": "Node", "outDir": "out", "declaration": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "sourceMap": true }, "include": [ "src/**/*.ts"