Nickolay - KiokuJS-0.01

Documentation | Source
Class('KiokuJS.Reference', {
    
    has : {
        ID          : { required : true },
        
        type        : null
    },
    
    
    methods : {
        toString : function () {
            return '{"$ref":' + this.ID + '}'
        }
    }
    
});
Class('KiokuJS.Exception', {
    
    has : {
        nativeEx        : null,
        
        message         : { is : 'rw' },
        description     : 'Unknown exception'
    },
    
    
    methods : {
        
        toString : function () {
            return this.meta.name + ': ' + this.description + ', ' + this.getMessage()
        }
    }
})
;
Class('KiokuJS.Exception.Network', {
    
    isa     : 'KiokuJS.Exception',
    
    has : {
        description : 'Network failure'
    }
})
;
Class('KiokuJS.Exception.Format', {
    
    isa     : 'KiokuJS.Exception',
    
    has : {
        description : 'Wrong serialization format'
    }
})
;
Class('KiokuJS.Exception.Overwrite', {
    
    isa     : 'KiokuJS.Exception',
    
    has : {
        id          : { required : true },
        oldValue    : '',
        newValue    : '',
        
        description : 'Overwrite attempt occured'
    },
    
    methods : {
        
        getMesage : function () {
            return "Attempt to overwrite entry with ID = [" + this.id + "], value = [" + this.oldValue + "], with [" + this.newValue + "]"
        }
    }
})
;
Class('KiokuJS.Exception.Update', {
    
    isa     : 'KiokuJS.Exception',
    
    has : {
        description : 'The entry being updated missed in the storage'
    }
})
;
Class('KiokuJS.Exception.Remove', {
    
    isa     : 'KiokuJS.Exception',
    
    has : {
        description : 'Failed `remove` operation'
    }
})
;
Class('KiokuJS.Exception.LookUp', {
    
    isa     : 'KiokuJS.Exception',
    
    has : {
        id          : { required : true },
        backendName : { required : true },
        
        description : 'Failed lookup attempt'
    },
    
    methods : {
        
        getMesage : function () {
            return 'ID [' + this.id + '] not found in the backend [' + this.backendName + ']'
        }
    }
})
;
Class('KiokuJS.Exception.Conflict', {
    
    isa     : 'KiokuJS.Exception',
    
    has : {
        description : 'Revisioning or consistency conflict'
    }
})
;
Role('KiokuJS.Aspect.AfterCollapse', {
    
    
    methods : {
        
        afterCollapse : function () {
        }
    }
    
})
;
Role('KiokuJS.Aspect.AfterExpand', {
    
    
    methods : {
        
        afterExpand : function () {
        }
    }
    
})
;
Role('KiokuJS.Feature.Attribute.Intrinsic', {
    
    does    : 'KiokuJS.Aspect.AfterCollapse',
    
    
    
    after : {
        
        
        afterCollapse : function (instance, value) {
            
            if (value instanceof KiokuJS.Node) value.intrinsic = true
        }
    }
    
})
;
Role('KiokuJS.Feature.Attribute.Extrinsic', {
    
    does    : 'KiokuJS.Aspect.AfterCollapse',
    
    
    
    after : {
        
        
        afterCollapse : function (instance, value) {
            
            if (value instanceof KiokuJS.Node) value.extrinsic = true
        }
    }
    
})
;
Role('KiokuJS.Feature.Attribute.Skip')
;
// XXX implement auto-registration of such attributes
// XXX proxy will currently only work, when linking with the `shallowLevel` == 0

Role('KiokuJS.Feature.Attribute.Proxy')
;
Role('KiokuJS.Feature.Attribute.Lazy', {
    
    does    : 'KiokuJS.Aspect.AfterCollapse',
    
    
    use     : 'JooseX.CPS',
    
    
    after : {
        
        afterCollapse : function (instance, value) {
            
            if (value instanceof KiokuJS.Node) value.lazy = true
        },
        
        
        initialize : function () {
            this.readable = this.hasGetter = true
        }
    },
    
    
    override : {
        
        getGetter : function () {
            var original    = this.SUPER()
            
            var me          = this
            
            return function (scope) {
                var value   = original.call(this)
                var self    = this
                
                var cont = Joose.top.__GLOBAL_CNT__ || new JooseX.CPS.Continuation()
                
                if (value instanceof KiokuJS.Reference && value.type == 'lazy') {
                    
                    var ID = value.ID
                    
                    if (!scope) throw "No scope provided to fetch the lazy reference in. Reference ID [" + ID + "]"
                    
                    return cont.TRY(function () {
                        
                        scope.lookUp(ID).andThen(function (obj) {
                            
                            me.setRawValueTo(self, obj)
                            
                            this.CONT.CONTINUE(obj)
                        }, self)
                    }, self)
                }
                
                return cont.TRY(function () { 
                    this.CONT.CONTINUE(value) 
                }, self)    
            }
        }
    }
})
;
Role('KiokuJS.Feature.Class.Intrinsic', {
    
    does    : 'KiokuJS.Aspect.AfterCollapse',
    
    
    
    after : {
        
        
        afterCollapse : function (node) {
            node.intrinsic = true
        }
    }
    
})
;
Role('KiokuJS.Feature.Class.OwnID', {
    
    requires : [ 'acquireID' ]

})
;
Role('KiokuJS.Feature.Class.OwnUUID', {
    
    use         : 'Data.UUID',
    
    does        : 'KiokuJS.Feature.Class.OwnID',
    
    
    has : {
        uuid    : {
            is      : 'rw',
            init    : function () { return Data.UUID.uuid() }
        }
    },
    
    
    methods : {
        
        acquireID : function () {
            return this.getUuid()
        }
    }
})
;
Role('KiokuJS.Feature.Class.Immutable')
;
Role('KiokuJS.Role.Serializer', {
    
    my : {
        requires : [ 'serialize', 'deserialize' ]
    }
})

;
Class('KiokuJS.Serializer.JSON', {
    
    does        : 'KiokuJS.Role.Serializer',
    
    use         : [ 'JSON2', 'KiokuJS.Exception.Format' ],
    
    my : {
        
        methods : {
            
            serialize   : function (data) {
                try {
                    return JSON2.stringify(data)
                } catch (e) {
                    throw new KiokuJS.Exception.Format({ message : 'Invalid JSON: ' + data })
                }
            },
            
            
            deserialize : function (string) {
                try {
                    return JSON2.parse(string)
                } catch (e) {
                    
                    throw new KiokuJS.Exception.Format({ message : 'Invalid JSON: ' + string })
                }
            }
        } 
    }
})
;
Role('KiokuJS.TypeMap.Role.NoDeps', {
        
    methods : {
        
        getRequiredClasses : function () {
            return []
        }
    }
})
;
Class('KiokuJS.TypeMap', {
    
    use     : 'Data.UUID',
    
    
    has : {
        inherit     : false,
        
        intrinsic   : false,

        // this flag will be set for native ( [], {} ) data structures which can be passed through w/o own entries 
        passThrough : false,      
        
        forClass    : {
            required    : true
        },
        
        classVersion    : null,
        isVersionExact  : true 
    },
    
    
    methods : {
        
        getRequiredClasses : function () {
            if (this.classVersion) {
                var obj = {}
                
                obj[ this.forClass ] = this.classVersion
                
                return [ obj ]
            }
            
            return [ this.forClass ]
        },
        
        
        // XXX add versions check
        canHandle : function (className) {
            if (className == this.forClass) return true
            
            if (this.inherit) {
                
                var classConstructor        = eval(className)
                var forClass                = eval(this.forClass)
                
                if (classConstructor.meta) return classConstructor.meta.isa(forClass)
            }
            
            return false
        },
        
        
        acquireID : function (node, desiredId) {
            return desiredId != null ? desiredId : Data.UUID.uuid()
        },
        
        
        collapse : function (node, collapser) {
            throw "Abstract method 'collapse' called for " + this
        },
        
        
        clearInstance : function (node) {
            throw "Abstract method 'clear' called for " + this
        },
        
        
        createEmptyInstance : function (node) {
            throw "Abstract method 'createEmptyInstance' called for " + this
        },
        
        
        populate : function (node, expander) {
            throw "Abstract method 'expand' called for " + this
        }
    }
})
;
Class('KiokuJS.TypeMap.Date', {
    
    isa     : 'KiokuJS.TypeMap',
    
    does    : 'KiokuJS.TypeMap.Role.NoDeps',
    
    
    has : {
        forClass    : 'Date'
    },
    
        
    methods : {
        
        canHandle : function (className) {
            return className == 'Date'
        },
        
        
        collapse : function (node, collapser) {
            return node.object.getTime()
        },
        
        
        clearInstance : function (node) {
        },
        
        
        createEmptyInstance : function (node) {
            return new Date()
        },
        
        
        populate : function (node, expander) {
            var instance = node.object
            
            instance.setTime(node.data)
        }
    }

})
;
Class('KiokuJS.TypeMap.Function', {
    
    isa     : 'KiokuJS.TypeMap',
    
    does    : 'KiokuJS.TypeMap.Role.NoDeps',
    
    
    has : {
        forClass    : 'Function'
    },
    
        
    methods : {
        
        canHandle : function (className) {
            return className == 'Function'
        },
        
        
        collapse : function (node, collapser) {
            var props = {}
            
            Joose.O.eachOwn(node.object, function (value, name) {
                props[ name ] = collapser.visit(value)
            })
            
            return {
                source  : Function.prototype.toString.call(node.object),
                
                props   : props
            }
        },
        
        
        clearInstance : function (node) {
            var func = node.object
            
            Joose.O.eachOwn(func, function (value, name) {
                
                delete func[ name ]
            })
            
            delete node.objectData.action
        },
        
        
        createEmptyInstance : function (node) {
            var closure = node.objectData = {
                action : null
            }
            
            return function () {
                return closure.action.apply(this, arguments)
            }
        },
        
        
        populate : function (node, expander) {
            
            var func = node.object
            
            Joose.O.each(node.data.props, function (value, name) {
                
                func[ name ] = expander.visit(value)
            })
            
            node.objectData.action = eval('(' + node.data.source + ')')
        }
    }

})
;
Class('KiokuJS.TypeMap.Array', {
    
    isa     : 'KiokuJS.TypeMap',
    
    does    : 'KiokuJS.TypeMap.Role.NoDeps',
    
    
    has : {
        forClass    : 'Array',
        passThrough : true
    },
    
        
    methods : {
        
        canHandle : function (className) {
            return className == 'Array'
        },
        
        
        collapse : function (node, collapser) {
            
            return Joose.A.map(node.object, function (value) {
                return collapser.visit(value)
            })
        },

        
        clearInstance : function (node) {
            var instance = node.object
            
            if (instance.length) instance.splice(0, instance.length)
        },
        
        
        createEmptyInstance : function (node) {
            return []
        },
        
        
        populate : function (node, expander) {
            var instance = node.object
            
            Joose.A.map(node.data, function (value) {
                
                instance.push(expander.visit(value))
            })
        }
    }

})
;
Class('KiokuJS.TypeMap.Object', {
    
    isa     : 'KiokuJS.TypeMap',
    
    does    : 'KiokuJS.TypeMap.Role.NoDeps',
    
    
    has : {
        forClass    : 'Object',
        passThrough : true
    },
    
        
    methods : {
        
        canHandle : function (className) {
            return className == 'Object'
        },
        
        
        collapse : function (node, collapser) {
            var data = {}
            
            Joose.O.eachOwn(node.object, function (value, name) {
                data[ name ] = collapser.visit(value)
            })
            
            return data
        },
        
        
        clearInstance : function (node) {
            
            Joose.O.eachOwn(node.object, function (value, name) {
                delete instance[ name ]
            })
        },
        
        
        createEmptyInstance : function (node) {
            return {}
        },
        
        
        populate : function (node, expander) {
            var instance = node.object
            
            Joose.O.each(node.data, function (value, name) {
                
                instance[ name ] = expander.visit(value)
            })
        }
    }

})
;
Class('KiokuJS.TypeMap.Joose', {
    
    isa     : 'KiokuJS.TypeMap',
    
    use     : [ 
        'KiokuJS.Aspect.AfterCollapse', 
        'KiokuJS.Feature.Attribute.Skip',
        'KiokuJS.Feature.Class.Immutable'
    ],
    
    
    has : {
        forClass    : 'Joose.Proto.Object',
        inherit     : false
    },
    
        
    methods : {
        
        acquireID : function (node, desiredId) {
            var instance        = node.object
            
            if (instance.meta.does('KiokuJS.Feature.Class.OwnID')) return instance.acquireID(desiredId, node)
            
            return this.SUPER(node, desiredId)
        },
        
        
        eachAttribute : function (instance, func, scope) {
            var meta            = instance.meta
            
            // XXX only store Joose.Managed.Attribute and Joose.Managed.Class?
            var scanAttribute = function (attribute, name) {
                var attributeLevel = attribute instanceof Joose.Managed.Attribute ? 2 : attribute instanceof Joose.Managed.Property.Attribute ? 1 : 0
                
                if (attributeLevel == 2 && attribute.meta.does(KiokuJS.Feature.Attribute.Skip)) return
                
                func.call(scope || null, attribute, name, attributeLevel)
            }
            
            if (meta instanceof Joose.Managed.Class)
                meta.getAttributes().each(scanAttribute)
            else
                Joose.O.each(meta.attributes, scanAttribute)
        },
        
        
        collapse : function (node, collapser) {
            var instance        = node.object
            var meta            = instance.meta
            var isManagedClass  = meta instanceof Joose.Managed.Class
            var data            = {}
            
            // if node already has `data`, then either it is being collapsed for the 2nd time or were loaded from the backend
            // in both cases, if node represents an instance of immutable class, it becomes also immutable
            // and will be skipped from all `store/update/insert` commands 
            if (node.data && isManagedClass && meta.does(KiokuJS.Feature.Class.Immutable)) {
                node.immutable = true
                
                return node.data
            }
            
            this.eachAttribute(instance, function (attribute, name, attributeLevel) {
                
                if (attribute.hasValue(instance))
                    if (attributeLevel) {
                        data[ name ] = collapser.visit(attribute.getRawValueFrom(instance))
                        
                        if (attributeLevel == 2 && attribute.meta.does(KiokuJS.Aspect.AfterCollapse)) attribute.afterCollapse(instance, data[ name ], node, collapser, attribute)
                        
                    } else
                        // Joose.Proto.Class attributes - just raw values
                        data[ name ] = collapser.visit(instance[ name ])
            })
            
            
            // instance has traits
            if (meta.isDetached) node.objTraits = Joose.A.map(meta.getRoles(), function (trait) {
                var traitName = trait.meta.name
                
                if (!traitName) throw "Can't serialize instance [" + instance + "] - it contains an anonymous trait"
                
                return trait.meta.VERSION ? {
                    type    : 'joose',
                    token   : traitName,
                    version : trait.meta.VERSION
                } : traitName
            })
            
            node.classVersion = meta.VERSION
            
            if (isManagedClass && meta.does(KiokuJS.Aspect.AfterCollapse)) instance.afterCollapse(node, collapser)
            
            return data
        },
        
        
        clearInstance : function (node) {
            var instance        = node.object
            
            this.eachAttribute(instance, function (attribute, name, attributeLevel) {
                
                if (attributeLevel)
                    delete instance[ attribute.slot ]
                else
                    // Joose.Proto.Class attributes - just raw values
                    delete instance[ attribute.name ]
            })
        },
        
        
        createEmptyInstance : function (node) {
            var constructor     = node.getClass()
            
            var classVersion = constructor.meta.VERSION
            
            if (this.isVersionExact && classVersion && classVersion != node.classVersion) 
                throw "Typemap [" + this + "] handles only exact version [" + classVersion + "] of class [" + node.className + ']'
            
            if (node.objTraits) {
                var traits = Joose.A.map(node.objTraits, function (traitOrDesc) {
                    if (typeof traitOrDesc == 'string') return eval(traitOrDesc)
                    
                    return eval(traitOrDesc.token)
                })
                
                constructor = constructor.meta.subClass({
                    does : traits 
                }, node.className)
                
                constructor.meta.isDetached = true
            }
            
            var f               = function () {}
            f.prototype         = constructor.prototype
            
            return new f()
        },
        
        
        populate : function (node, expander) {
            var instance        = node.object
            var data            = node.data
            
            // now that instance for `node.ID` is already pinned and we can assign its attributes (which can contain 
            // self-references for example)
            this.eachAttribute(instance, function (attribute, name, attributeLevel) {
                
                if (data.hasOwnProperty(name))
                    if (attributeLevel)
                        attribute.setRawValueTo(instance, expander.visit(data[ name ]))
                    else
                        // Joose.Proto.Class attributes - just raw values
                        instance[ attribute.name ] = expander.visit(data[ name ])
            })
        }
    }

})
;
Class('KiokuJS.Resolver', {
    
    trait   : 'JooseX.CPS',
    
    
    has : {
        entries             : Joose.I.Array,
        
        parent              : null,
        
        cache               : Joose.I.Object,
        
        classesFetched      : false
    },
    
    
    methods : {
        
        BUILD   : function (param) {
            if (param instanceof Array) return {
                entries : param
            }
            
            return this.SUPERARG(arguments)
        },
        
        
        initialize : function () {
            
            var entries     = this.entries
            
            Joose.A.each(entries, function (entry, index) {
                    
                entries[ index ] = this.prepareEntry(entry)
                
            }, this)
        },
        
        
        prepareEntry : function (entry) {
            if (!entry) throw "Can't add empty entry to resolver : " + this
            
            if (!Joose.O.isInstance(entry)) {
                var entryClass = eval(entry.meta)
                delete entry.meta
                
                entry = new entryClass(entry)
            }
            
            if (entry instanceof KiokuJS.Resolver) entry.parent = this 
            
            return entry
        },
        

        //XXX implement full CRUD for entries
        addEntry : function (entry) {
            this.entries.push(this.prepareEntry(entry))
            
            this.discardCache()
        },
        
        
        getEntryAt : function (index) {
            return this.entries[ index ]
        },
        
        
        discardCache : function () {
            this.cache              = {}
            this.classesFetched     = false
            
            if (this.parent) this.parent.discardCache()
        },
        
        
        each : function (func, scope) {
            scope = scope || this
            
            return Joose.A.each(this.entries, function (entry) {
                
                if (entry instanceof KiokuJS.Resolver) 
                    return entry.each(func, scope)
                else 
                    return func.call(scope, entry)
            })
        },
        
        
        resolveSingle : function (className) {
            var cache = this.cache
            
            if (cache[ className ]) return cache[ className ]
            
            var typeMap
            
            this.each(function (entry) {
                
                if (entry instanceof KiokuJS.Resolver) { 
                    typeMap = entry.resolveSingle(className)
                    
                    if (typeMap) return false
                } else 
                    if (entry instanceof KiokuJS.TypeMap) { 
                        
                        if (entry.canHandle(className)) {
                            typeMap = entry
                            
                            return false
                        }
                    } else
                        throw "Invalid entry [" + entry + "] in resolver + [" + this + "]"
            })
            
            if (typeMap) return cache[ className ] = typeMap
        },
        
        
        resolveMulti : function (classNames) {
            return Joose.A.map(classNames, this.resolveSingle, this)
        }
        
    },
    
    
    continued : {
        
        methods : {
        

            fetchClasses : function () {
                if (this.classesFetched) {
                    this.CONTINUE()
                    
                    return
                }
                
                
                var classes = []
                
                this.each(function (entry) {
                    classes.push.apply(classes, entry.getRequiredClasses())
                })

                
                var me      = this
                var CONT    = this.CONT
                
                use(classes, function () {
                    me.classesFetched = true
                    
                    CONT.CONTINUE()
                })
            },

            
            resolve : function (classNames) {
                (this.classesFetched ? this : this.fetchClasses()).andTHEN(function () {
                    
                    this.CONTINUE(this.resolveMulti(classNames))
                })
            }
        }
    }

})
;
Class('KiokuJS.Resolver.Standard', {
    
    isa         : 'KiokuJS.Resolver',
    
    
    use         : [
        'KiokuJS.TypeMap.Role.NoDeps',
    
        'KiokuJS.TypeMap.Joose',
        
        'KiokuJS.TypeMap.Object',
        'KiokuJS.TypeMap.Array',
        
        'KiokuJS.TypeMap.Function',
        'KiokuJS.TypeMap.Date'
    ],
    

    
    after : {
        
        initialize : function () {
            
            // the order matter
            
            this.addEntry(new KiokuJS.TypeMap.Joose({
                trait   : KiokuJS.TypeMap.Role.NoDeps,
                
                inherit : true
            }))
            
            this.addEntry(new KiokuJS.TypeMap.Object())
            
            this.addEntry(new KiokuJS.TypeMap.Array())
            
            this.addEntry(new KiokuJS.TypeMap.Function())
            
            this.addEntry(new KiokuJS.TypeMap.Date())
        }
    }

})
;
Role('KiokuJS.Role.Resolvable', {
    
    requires : [ 'getResolver' ],
    
        
    methods : {
        
        getClassNameFor : function (object) {
            if (Joose.O.isInstance(object))      return object.meta.name
            
            return Object.prototype.toString.call(object).replace(/^\[object /, '').replace(/\]$/, '')
        },
        
        
        getTypeMapFor : function (className) {
            if (typeof className != 'string') className = this.getClassNameFor(className)
            
            var typeMap = this.getResolver().resolveSingle(className)
            
            if (!typeMap) throw "Can't find TypeMap entry for className = [" + className + "]"
            
            return typeMap
        }
    }
})
;
Class('KiokuJS.Node', {
    
    use     : 'Data.Visitor',
    
    does    : 'KiokuJS.Role.Resolvable',
    
    
    has : {
        // stored attributes
        ID              : null,
        
        className       : undefined,
        classVersion    : undefined,
        objTraits       : undefined,
        
        isRoot          : undefined,
        data            : null,
        
        // run-time attributes
        object      : {
            is  : 'rw'
        },
        
        // arbitrary data about object
        objectData  : null,
        
        typeMap     : {
            is      : 'ro',
            lazy    : function () { return this.getTypeMapFor(this.className) }
        },
        
        resolver    : {
            is          : 'rw',
            required    : true
        },
        
        entry       : {
            is      : 'ro',
            lazy    : 'this.buildEntry'
        },
        
        
        selfReference   : {
            is      : 'ro',
            lazy    : 'this.buildSelfReference'
        },
        
        // node will produce separate first-class (extrinisic) entry unless it (or its typemap) has `intrinsic` flag set
        extrinsic   : false,
        
        // node will produce intrinsic entries
        intrinsic   : false,
        
        // node will be skipped from the collapse result (effectively from all store operations)
        immutable   : false,
        
        // node will contain a lazy reference
        lazy        : false
    },
    

    methods : {
        
        initialize : function () {
            var object      = this.object
            var className   = this.className
            
            if (!className && !object) throw "Either `object` or `className` with `data` must be supplied during instantiation of node [" + this + "]"
            
            if (!className) this.className = this.getClassNameFor(object)
        },
        
        
        isLive : function () {
            return this.object != null 
        },
        
        
        isFirstClass : function () {
            return this.ID != null
        },
        
        
        isIntrinsic : function () {
            return this.intrinsic || this.getTypeMap().intrinsic
        },
        
        
        getClass : function () {
            return eval(this.className)
        },
        
        
        acquireID : function (desiredId) {
            var ID      = this.ID
            
            if (ID) {
                if (desiredId != null && ID != desiredId) throw "Attempt to redefine the ID of node [" + this + "] from [" + ID + "] to [" + desiredId + "]"
                
                return
            }
            
            this.ID = this.getTypeMap().acquireID(this, desiredId)
        },
        
        
        // XXX implement clear/predicate for attribute
        clearEntry : function () {
            delete this.entry
        },
        
        
        clearInstance : function () {
            if (!this.object) throw "Node [" + this + "] doesn't contain an object instance to clear"
            
            this.getTypeMap().clearInstance(this)
        },
        
        
        buildEntry   : function () {
            
            var entry = {
                className       : this.className,
                classVersion    : this.classVersion,
                objTraits       : this.objTraits,
                
                isRoot          : this.isRoot,
                data            : this.data
            }
            
            if (this.ID != null) entry.ID = this.ID
            
            return entry
        },
        
        
        buildSelfReference : function () {
            var ref = {
                $ref    : this.ID
            }
            
            if (this.lazy) ref.type = 'lazy'
            
            return ref
        },
        

        collapse : function (collapser) {
            this.data = this.getTypeMap().collapse(this, collapser)
        },
        
        
        createEmptyInstance : function () {
            if (this.object) throw "Node [" + this + "] already contain an object instance"
            
            return this.object = this.getTypeMap().createEmptyInstance(this)
        },
        
        
        populate : function (expander) {
            if (!this.object) throw "Node [" + this + "] doesn't contain the object - can't be expanded"
            
            this.getTypeMap().populate(this, expander)
            
            return this.object
        },
        
        
        consumeOldNode : function (oldNode) {
            this.object = oldNode.object
            
            // this.isRoot = oldNode.isRoot seems this will be returned from DB in entry anyway
            
            // XXX also copy `intrinsic, extrinsic, immutable and lazy` ?
            // seems it will be recalculated during collapsing anyway?
        },
        
        
        consumeEntry : function (entry) {
            this.clearEntry()
        }
        
    },
    
    
    my : {
        
        has : {
            HOST    : null
        },
        
        
        methods : {
        
            newFromEntry : function (entry, resolver) {
                entry.resolver   = resolver
                
                return new this.HOST(entry)
            },
            
            
            newFromObject : function (object, resolver) {
                
                Data.Visitor.assignRefAdrTo(object)
                
                return new this.HOST({
                    object      : object,
                    
                    resolver    : resolver
                })
            }
        }
    }
});
Class('KiokuJS.Collapser.Encoder', {
    
    isa         : 'Data.Visitor',
    
    
    use         : [ 'KiokuJS.Reference', 'KiokuJS.Node' ],
    

    has         : {
        reservedKeys : /^\$ref$|^\$entry$/
    },
    
    
    methods : {
        
        encodeEntry : function (entry, node) {
            
            if (node.isFirstClass() || !node.getTypeMap().passThrough) {
                entry = this.visit(entry)
                
                entry.$entry = true
                 
                return entry
            }

            //passthrough the entries from non-firstclass nodes with native typemaps: [], {} 
            return this.visit(entry.data)
        },
        
        
        visitNode : function (node, needEntry) {
            if (node.isFirstClass() && !needEntry) return node.getSelfReference()
            
            return this.encodeEntry(node.getEntry(), node)
        },
        
        
        visitJooseInstance : function (node, className) {
            if (node instanceof KiokuJS.Node) return this.visitNode(node)

            throw "Invalid Joose instance [" + node + "] encountered during inlining - only `KiokuJS.Node` allowed"
        },
        
        
        // a bit faster visiting of array
        visitArray  : function (array, className) {
            return Joose.A.map(array, function (value) {
                
                return this.visit(value)
                
            }, this)
        },
        
        
        visitObject : function (object, className) {
            var res = {}
            
            Joose.O.eachOwn(object, function (value, key) {
                
                if (this.reservedKeys.test(key)) key = 'public:' + key
                
                res[ key ] = this.visit(value)
            }, this)
            
            return res
        }
    },
    
    
    my : {

        methods : {
            
            encodeNodes : function (nodes) {
                
                var instance = new this.HOST()
                
                return Joose.A.map(nodes, function (node) {
                    
                    return instance.visitNode(node, true)
                })
            }
        }                    
    }
})
;
Class('KiokuJS.Linker.Decoder', {
    
    isa         : 'Data.Visitor',
    
    use         : 'KiokuJS.Reference',
    

    has         : {
        backend         : { required : true },
        
        reservedKeys    : /^\$ref$|^\$entry$/
    },
    
    
    methods : {
        
        visitJooseInstance : function (node, className) {
            throw "Joose instance [" + node + "] encountered during decoding - data should contain only native structures"
        },
        
        
        // a bit faster visiting of array
        visitArray  : function (array, className) {
            return Joose.A.map(array, function (value) {
                
                return this.visit(value)
                
            }, this)
        },
        
        
        visitObject : function (object, className) {
            var refID = object.$ref
            
            if (refID)
                return new KiokuJS.Reference({
                    ID      : refID,
                    type    : object.type
                })
                
            var decodedObject = this.visitNativeObject(object, className)
            
            if (object.$entry) return this.backend.createNodeFromEntry(decodedObject)
            
            return decodedObject
        },
        
        
        visitNativeObject : function (object, className) {
            var res = {}
            
            Joose.O.eachOwn(object, function (value, key) {
                
                // ignore `$entry` mark from entries
                if (key == '$entry') return
                
                if (/^public:/.test(key)) {
                    var reservedKey = key.replace(/^public:/, '')
                    
                    if (this.reservedKeys.test(reservedKey)) key = reservedKey
                }
                
                res[ key ] = this.visit(value)
                
            }, this)
            
            return res
        }
    },
    
    
    my : {

        methods : {
            
            decodeEntries : function (entries, backend) {
                var instance = new this.HOST({
                    backend : backend
                })
                
                return instance.visit(entries)
            }
        }                    
    }
})
;
Class('KiokuJS.Linker.RefGatherer', {
    
    isa         : 'Data.Visitor',
    
    use         : 'KiokuJS.Reference',
    
    
    has : {
        references      : Joose.I.Object
    },    
    
    
    methods : {
        
        visitObject : function (object, className) {
            if (object.$ref && object.type != 'lazy') this.references[ object.$ref ] = true
            
            return this.SUPERARG(arguments)
        }
    },
    
    
    my : {
        
        methods : {
            
            gatherReferences   : function (data) {
                
                var instance = new this.HOST()
                
                instance.visit(data)
                
                var uniqueRefs = []
                
                Joose.O.each(instance.references, function (value, ref) {
                    uniqueRefs.push(ref)
                })
                
                return uniqueRefs
            }
        }                    
    }
})
;
Class('KiokuJS.Linker.Expander', {
    
    isa         : 'Data.Visitor',
    
    use         : 'KiokuJS.Reference',
    
    
    has         : {
        scope           : { required : true },
        nodes           : { required : true }
    },
    
    
    methods : {
        
        visitNode : function (node) {
            var scope   = this.scope
            
            var oldNode = scope.idToNode(node.ID)
            
            if (oldNode) node.consumeOldNode(oldNode)
            
            if (node.isLive())
                node.clearInstance()
            else 
                // newly created instance need to has the __REF_ADR__ as this property
                // is being extensively used internally
                this.assignRefAdrTo(node.createEmptyInstance())
            
            return node.populate(this)
        },
        
        
        visitReference : function (reference) {
            
            var refID   = reference.ID
            
            var refNode = this.nodes[ refID ] || this.scope.idToNode(refID)
            
            if (!refNode) 
                if (reference.type != 'lazy') 
                    throw new KiokuJS.Exception.LookUp({ id : refID, backendName : "Expander working set" })
                else
                    return reference
            
            
            if (refNode.isLive()) {
                var instance = refNode.getObject()
                
                this.assignRefAdrTo(instance)
                
                return instance
            }
            
            // `visit` and not(!) `visitNode` to utilize the `seen` cache for already processed nodes
            return this.visit(refNode)
        },
        
        
        visitJooseInstance : function (node, className) {
            if (node instanceof KiokuJS.Node) return this.visitNode(node)

            if (node instanceof KiokuJS.Reference) return this.visitReference(node)

            throw "Invalid Joose instance [" + node + "] encountered during inlining - only `KiokuJS.Node` and `KiokuJS.Reference` allowed"
        },
        
        
        visitArray  : function (array, className) {
            return Joose.A.map(array, function (value, index) {
                
                return this.visit(value)
            }, this)
        },
        
        
        visitObject : function (object, className) {
            var res = {}
            
            Joose.O.eachOwn(object, function (value, key) {
                
                res[ key ] = this.visit(value)
            }, this)
            
            return res
        }
    },
    
    
    my : {
        
        methods : {
            
            expandNodes   : function (nodes, scope) {
                
                var instance = new this.HOST({
                    scope       : scope,
                    nodes       : nodes
                })
                
                return instance.visit(nodes)
            }
        }                    
    }
})
;
Role('KiokuJS.Backend.Role.SkipFixture', {
    
    requires : [ 'skipFixtures' ]

})

;
// Backend can detect overwrite attempts

Role('KiokuJS.Backend.Feature.Overwrite')
;
// Backend can detect incorrect updates (w/o corresponding entry in the storage)

Role('KiokuJS.Backend.Feature.Update')
;
Class('KiokuJS.Backend', {
    
    trait   : 'JooseX.CPS',
    
    use     : [
        'KiokuJS.Resolver', 
        'KiokuJS.Resolver.Standard', 
    
        'KiokuJS.Serializer.JSON',
        
        'KiokuJS.Scope',
        'KiokuJS.Node' 
    ],
    
    
    has : {
        nodeClass       : Joose.I.FutureClass('KiokuJS.Node'),
        scopeClass      : Joose.I.FutureClass('KiokuJS.Scope'),
        
        resolver        : null,
        
        serializer      : {
            handles     : [ 'serialize', 'deserialize' ],
            init        : Joose.I.FutureClass('KiokuJS.Serializer.JSON')
        }
    },
    
        
    methods : {
        
        initialize : function () {
            var resolver    = this.resolver
            
            // wrapping the possibly passed resolver with another one, containig the standard resolver as the lowest-priority
            this.resolver = new KiokuJS.Resolver( 
                (resolver ? [ resolver ] : []).concat( new KiokuJS.Resolver.Standard() )
            )
        },
        
        
        newScope    : function (options) {
            return new this.scopeClass(Joose.O.copy({
                backend     : this,
                resolver    : this.resolver
            }, options))
        },
        
        
        createNodeFromEntry : function (entry) {
            return this.nodeClass.newFromEntry(entry, this.resolver)
        },
        
        
        createNodeFromObject : function (object) {
            return this.nodeClass.newFromObject(object, this.resolver)
        },
        
        
        decodePacket : function (packet) {
            var scope = this.newScope()
            
            var linker = new KiokuJS.Linker({
                scope       : scope,
                
                entries     : packet.entries
            })
            
            linker.animateNodes()
            
            var objects = {}
            
            Joose.A.each(packet.customIDs, function (id) { 
                objects[ id ] = scope.idToObject(id)
            })
                
            return [ objects, Joose.A.map(packet.IDs, scope.idToObject, scope) ]
        },
        
        
        encodePacket : function (wIDs, woIDs) {
            var scope = this.newScope()
            
            return scope.includeNewObjects(wIDs, woIDs)
        }
    },
    
    
    continued : {
        
        methods : {
            
            get     : function (idsToGet, mode) {
                throw "Abstract method 'get' called for " + this
            },
            
            
            insert  : function (entriesToInsert, mode) {
                throw "Abstract method 'insert' called for " + this
            },
            
            
            remove  : function (idsOrEntriesToRemove) {
                throw "Abstract method 'remove' called for " + this
            },
            
            
            exists  : function (idsToCheck) {
                throw "Abstract method 'exists' called for " + this
            },
            
            
            search : function (scope, arguments) {
                throw "Abstract method 'search' called for " + this
            }
        }
    }

})

// placing this override here, since backend is always required

// XXX need to keep in sync with original `Joose.O.each`
Joose.O.each = function (object, func, scope) {
    scope = scope || this
    
    for (var i in object) 
        if (i != '__REFADR__')
            if (func.call(scope, object[i], i) === false) return false
    
    if (Joose.is_IE) 
        return Joose.A.each([ 'toString', 'constructor', 'hasOwnProperty' ], function (el) {
            
            if (object.hasOwnProperty(el)) return func.call(scope, object[el], el)
        })
}


Joose.O.isEmpty = function (object) {
    for (var i in object) if (object.hasOwnProperty(i) && i != '__REFADR__') return false
    
    return true
}
;
Class('KiokuJS.Collapser', {
    
    isa         : 'Data.Visitor',
    
    
    has : {
        refCounts           : Joose.I.Object,
        nodes               : Joose.I.Object, //vertexes, addressed by __REFADR__
        
        backend             : null,
        scope               : { required : true },
        
        isShallow           : false,
        setRoot             : true,
        
        rootObjects         : Joose.I.Object
    },
    
        
    
    before : {
        
        markSeenAs    : function (object) {
            this.refCounts[ object.__REFADR__ ] = 1
        }
        
    },
    
    
    methods : {
        
        initialize : function () {
            this.backend = this.scope.getBackend()
        },
        
        
        visitSeen : function (object, seen) {
            var refAdr = object.__REFADR__
            
            this.refCounts[ refAdr ]++
            
            // return either the node from `this.nodes` - for nodes being collapsed
            // or "seen" node - for nodes which are skipped during shallow collapsing
            return this.nodes[ refAdr ] || seen
        },
        
        
        // calls `func` with (object, desiredID) signature for each passed argument
        eachArgument : function (wIDs, woIDs, func, scope) {
            scope = scope || this
            
            Joose.O.each(wIDs, func, scope)
            
            Joose.A.each(woIDs, function (argument) {
                func.call(scope, argument)
            })
        },
        
        
        collapse : function (wIDs, woIDs) {
            
            // sanity checks
            
            this.eachArgument(wIDs, woIDs, function (argument) {
                if (argument == null || (typeof argument != 'object' && typeof argument != 'function') ) throw "Invalid argument [" + argument + "]  to 'collapse'. Can only collapse objects, not values."
                
                var refAdr = this.assignRefAdrTo(argument)
                
                this.rootObjects[ refAdr ] = true
            })
            
            
            // recurse through the graph, accumulating nodes and counting refs
            
            this.visit(wIDs, woIDs)
            

            // all objects are from root set, so we need to acquire IDs for them
            
            var nodes               = this.nodes
            
            this.eachArgument(wIDs, woIDs, function (argument, desiredId) {
                var node = nodes[ argument.__REFADR__ ]
                
                node.acquireID(desiredId)
                if (this.setRoot) node.isRoot = true
            })


            // also marks shared nodes (refCount > 1) and Joose instances (unless intrinsic) as first class 
            
            var refCounts           = this.refCounts
            var firstClassNodes     = []
            var me                  = this
            
            Joose.O.each(nodes, function (node, refadr) {
                
                var object      = node.object
                var isExtrinsic = (refCounts[ refadr ] > 1 || Joose.O.isInstance(object) || node.extrinsic || node.lazy) && !node.isIntrinsic()
                
                if ( me.belongsToRootSet(object) || isExtrinsic) {
                    // this makes this node `firstClass` (but not root)
                    if (!node.isFirstClass()) node.acquireID()
                    
                    if (!node.immutable) firstClassNodes.push(node)
                }
            })
            
            
            return firstClassNodes
        },
     
        
        visitArray : function (instance, className) {
            return this.visitObject(instance, className)
        },
        
        
        belongsToRootSet : function (object) {
            return this.rootObjects[ object.__REFADR__ ]
        },
        
        
        visitObject : function (instance, className) {
            var scope           = this.scope
            var nodes           = this.nodes
            
            var node            = scope.objectToNode(instance)
            var refAdr          = instance.__REFADR__
            
            // if this is a shallow collapsing, and we found the instance which already has the node
            // then stop collapsing at this point and do not recurse
            // also do not add the node into `this.nodes` to prevent it from returning as a result of `collapse`
            if (node && (this.isShallow && !this.belongsToRootSet(instance) || node.immutable)) return node
                
            if (node) {
                node.clearEntry()
                
                nodes[ refAdr ] = node
            } else
                // need to create the node before recursing through the instance's data, to handle circular-references correctly
                nodes[ refAdr ] = node = this.backend.createNodeFromObject(instance)
            
            // now that node is already in the `this.nodes` we can collect the data
            node.collapse(this)
            
            return node
        }
    }

})


;
Class('KiokuJS.Linker', {
    
    trait   : 'JooseX.CPS',
    
    
    use     : 'KiokuJS.Linker.Expander',
    
    
    has : {
        scope           : { 
            required        : true,
            handles         : 'decodeEntries'
        },
        
        entries         : Joose.I.Object,
        
        nodes           : {
            lazy    : function () {
                return this.decodeEntries(this.entries)
            }
        }
    },
    

    methods : {
        
        BUILD : function (config) {
            var entries = config.entries
            
            if (entries instanceof Array) {
                var entriesByID = {}
                
                Joose.A.each(entries, function (entry) {
                    entriesByID[ entry.ID ] = entry
                })
                
                config.entries = entriesByID
            }
            
            return config
        },
        
        
        animateNodes : function () {
            var scope   = this.scope
            var nodes   = this.getNodes()
            
            KiokuJS.Linker.Expander.expandNodes(nodes, this.scope)
            
            Joose.O.each(nodes, function (node, id) {
                
                if (node.isFirstClass()) scope.pinNode(node)
            })
        }
    },
    
    
    continued : {
        
        methods : {
            
            materialize : function (ids, shallowLevel) {
                var me          = this
                var scope       = this.scope
                var backend     = scope.getBackend()
                
                var idsToFetch  = []
                
                Joose.A.each(ids, function (id) {
                    if (!scope.idPinned(id) || shallowLevel > 0) idsToFetch.push(id)
                })
                

                backend.get(idsToFetch, scope).andThen(function (entries) {
                    
                    var newEntries        = []
                    
                    // filter the entries returned from backend to only the new ones
                    // (which don't already have corresponding object in the scope)
                    // this should allow backends to pre-fetch references (potentially
                    // with extra entries)
                    Joose.A.each(entries, function (entry) {
                        
                        var entryID      = entry.ID
                        
                        // some entry was returned repeatedly
                        if (me.entries[ entryID ]) return
                        
                        if (!scope.idPinned(entryID) || shallowLevel > 0) {

                            me.entries[ entryID ] = entry
                            
                            newEntries.push(entry)
                        }
                    })
                    
                    var notFetchedIds   = []
                    
                    Joose.A.each(scope.gatherReferences(newEntries), function (refID) {
                        if (me.entries[ refID ]) return
                        
                        if (!scope.idPinned(refID) || shallowLevel == 2) notFetchedIds.push(refID)
                    })
                    
                    if (notFetchedIds.length)
                        me.materialize(notFetchedIds, shallowLevel == 2 ? 2 : 0).now()
                    else 
                        me.prefetchClasses(me.getNodes()).andThen(function () {
                            
                            me.animateNodes()
                            
                            this.CONTINUE()
                        })
                })
            },
            
            
            prefetchClasses : function (nodes) {
                // gathering classes of the nodes, which needs to be loaded
                var classDescriptors = []
                
                //XXX extract required classes from typemap instead of directly from node (node.getRequiredClasses())
                Joose.O.each(nodes, function (node, id) {
                    
                    var className = node.className
                    
                    if (className == 'Object' || className == 'Array') return
                    
                    if (node.classVersion) 
                        classDescriptors.push({ type : 'joose', token : className, version : node.classVersion })
                    else
                        classDescriptors.push(className)
                        
                    if (node.objTraits) classDescriptors.push.apply(classDescriptors, node.objTraits)
                })
                
                use(classDescriptors, this.getCONTINUE())
            },
            
            
            link : function (ids, shallowLevel) {
                // fetching resolver's classes (in case it has been mutated)
                this.scope.getResolver().fetchClasses().andThen(function () {
                    
                    this.materialize(ids, shallowLevel).now()
                    
                }, this)
            }
        }
    }
    

})


;
Class('KiokuJS.Scope', {
    
    trait   : 'JooseX.CPS',
    
    use     : [ 
        'KiokuJS.Collapser', 
        'KiokuJS.Exception.LookUp',
        
        'KiokuJS.Collapser.Encoder', 
        'KiokuJS.Linker.Decoder', 
        'KiokuJS.Linker.RefGatherer' 
    ],
    
    
    has : {
        backend         : {
            is          : 'ro',
            required    : true
        },
        
//        parent          : null,        
        
        // only store the first class nodes (with ID) & live ones
        nodesByREFADR   : Joose.I.Object,
        nodesByID       : Joose.I.Object,
        
        encoder         : {
            handles     : 'encodeNodes',
            init        : Joose.I.FutureClass('KiokuJS.Collapser.Encoder')
        },
        decoder         : Joose.I.FutureClass('KiokuJS.Linker.Decoder'),
        
        gatherer        : {
            handles     : 'gatherReferences',
            init        : Joose.I.FutureClass('KiokuJS.Linker.RefGatherer')
        }
    },
    
        
    methods : {
        
        getResolver : function () {
            return this.backend.resolver
        },
        
        
        registerProxy : function (object, ID) {
            
            var node = this.backend.createNodeFromObject(object)
            
            // XXX proxy will currently only work, when linking with the `shallowLevel` == 0
            node.immutable  = true
            
            if (!node.isFirstClass()) node.acquireID(ID)
            
            this.pinNode(node)
        },
        
        
//        deriveChild : function (options) {
//            return new this.constructor(Joose.O.copy({
//                parent          : this,
//                
//                nodesByREFADR   : Joose.O.getMutableCopy(this.nodesByREFADR),
//                nodesByID       : Joose.O.getMutableCopy(this.nodesByID)
//            }, options))
//        },        
        
        
        // node *must* be live
        pinNode : function (node) {
            var nodeID = node.ID
            
//            if (!this.hasID(nodeID) || this.hasOwnID(nodeID)) {
                
                if (!node.isLive()) throw "Can pin only live nodes"
                
                this.nodesByID[ nodeID ] = node
                
                var object  = node.getObject()
                
                this.nodesByREFADR[ object.__REFADR__ ] = node
//            } else
//                // XXX no proto inheritance already
//                this.parent.pinNode(node)
        },
        
        
        unpinNode : function (node) {
            var nodeID = node.ID
            
//            if (this.hasOwnID( nodeID )) {
            
                if (!node.isLive()) throw "Can unpin only live node"
                
                delete this.nodesByID[ nodeID ]
                
                var REFADR  = node.getObject().__REFADR__
                
                delete this.nodesByREFADR[ REFADR ]
//            } else
//                // XXX no proto inheritance already
//                this.parent.unpinNode(node)
        },
        

        unpinID : function (id) {
            var node = this.idToNode(id)
            
            if (node) 
                this.unpinNode(node)
            else
                throw "ID [" + id + "] is not in scope - can't unpin it"
        },
        
        
        objectToNode : function (obj) {
            return this.nodesByREFADR[ obj.__REFADR__ ]
        },
        
        
        nodeToObject : function (node) {
            var ownNode     = this.nodesByID[ node.ID ]
            
            return ownNode && ownNode.getObject()
        },
        
        
        objectToId : function (obj) {
            return obj.__REFADR__ && this.nodesByREFADR[ obj.__REFADR__ ].ID || null
        },
        
        
        idToObject : function (id) {
            var node = this.nodesByID[ id ]
            
            return node && node.getObject()
        },
        
        
        idToNode : function (id) {
            return this.nodesByID[ id ]
        },
        
        
        idPinned : function (id) {
            return this.nodesByID[ id ] != null
        },
        
        
        nodePinned : function (node) {
            return node.ID && this.nodesByID[ node.ID ] != null
        },
        
        
        objectPinned : function (object) {
            return object.__REFADR__ && this.nodesByREFADR[ object.__REFADR__ ] != null || false
        },
        
        
//        getOwnNodes : function () {
//            return Joose.O.copyOwn(this.nodesByID)
//        },
//        
//        
//        
//        hasID : function (id) {
//            return this.nodesByID[ id ] != null
//        },
//        
//        
//        hasOwnID : function (id) {
//            return this.nodesByID.hasOwnProperty( id )
//        },
        
        
        collapse : function (wIDs, woIDs, options) {
            options             = options || {}
            
            options.backend     = this.getBackend()
            options.scope       = this
            
            return new KiokuJS.Collapser(options).collapse(wIDs, woIDs)
        },
        
        
        encodeNode : function (node) {
            return this.encoder.encodeNodes([ node ])[ 0 ]
        },
        
        
        decodeEntry : function (entry) {
            return this.decoder.decodeEntries([ entry ], this.getBackend())[ 0 ]
        },
        
        
        decodeEntries : function (entries) {
            return this.decoder.decodeEntries(entries, this.getBackend())
        },
        
        
        includeNewObjects : function (wIDs, woIDs) {
            var nodes   = this.collapse(wIDs, woIDs, { isShallow : true })
            
            Joose.A.each(nodes, this.pinNode, this)
            
            var customIDs = []
            
            Joose.O.each(wIDs, function (object, id) {
                customIDs.push(id)
            })
            
            return {
                entries     : this.encodeNodes(nodes),
                customIDs   : customIDs,
                IDs         : Joose.A.map(woIDs, this.objectToId, this)
            }
        }
    },
    
    
    continued : {
        
        methods : {
            
            store : function () {
                this.storeObjects({
                    
                    wIDs        : {},
                    woIDs       : Array.prototype.slice.call(arguments),
                    
                    mode        : 'store',
                    shallow     : false
                    
                }).now()
            },
            
            
            storeAs : function () {
                var woIDs = Array.prototype.slice.call(arguments)
                
                this.storeObjects({
                    
                    wIDs        : woIDs.shift(),
                    woIDs       : woIDs,
                    
                    mode        : 'store',
                    shallow     : false
                    
                }).now()
            },
            
            
            update : function () {
                this.storeObjects({
                    
                    wIDs        : {},
                    woIDs       : Array.prototype.slice.call(arguments),
                    
                    mode        : 'update',
                    shallow     : true
                    
                }).now()
            },
            
            
            deepUpdate : function () {
                this.storeObjects({
                    
                    wIDs        : {},
                    woIDs       : Array.prototype.slice.call(arguments),
                    
                    mode        : 'update',
                    shallow     : false
                    
                }).now()
            },
            
            
            insert : function () {
                this.storeObjects({
                    
                    wIDs        : {},
                    woIDs       : Array.prototype.slice.call(arguments),
                    
                    mode        : 'insert',
                    shallow     : true
                    
                }).now()
            },
            
            
            insertAs : function () {
                var woIDs = Array.prototype.slice.call(arguments)
                
                this.storeObjects({
                    
                    wIDs        : woIDs.shift(),
                    woIDs       : woIDs,
                    
                    mode        : 'insert',
                    shallow     : true
                    
                }).now()
            },
            
            
            
            storeObjects : function (args) {
                var wIDs        = args.wIDs     || {}
                var woIDs       = args.woIDs    || []
                var mode        = args.mode     || 'store'    
                var shallow     = args.shallow  || false
                var setRoot     = args.setRoot
                
                
                var resolver    = this.getResolver()
                var backend     = this.getBackend()
                var self        = this
                
                resolver.fetchClasses().andThen(function () {
                    
                    var firstClassNodes = self.collapse(wIDs, woIDs, {
                        isShallow           : shallow,
                        setRoot             : setRoot != null ? setRoot : true
                    })
                    
                    // saving only first-class nodes - by design they'll contain a description of the whole graph
                    backend.insert(self.encodeNodes(firstClassNodes), mode).andThen(function (entries) {
                        
                        // pin nodes only after successfull insert and only those not pinned yet 
                        Joose.A.each(firstClassNodes, function (node, index) {
                            
                            node.consumeEntry(entries[ index ])
                            
                            if (!self.nodePinned(node)) self.pinNode(node)
                        })
                        
                        this.CONTINUE.apply(this, Joose.A.map(woIDs, self.objectToId, self))
                    })
                })
            },
            
            
            remove : function () {
                var me          = this
                
                var entriesOrIds  = Joose.A.map(arguments, function (arg) {
                    // id
                    // trying to replace an ID with entry where possible, as it has more information attached
                    if (typeof arg == 'string') return me.idPinned(arg) ? me.idToNode(arg).getEntry() : arg
                    
                    // object
                    if (!me.objectPinned(arg)) 
                        throw new KiokuJS.Exception.Remove({
                            message : "Can't remove object [" + arg + "] - its not in the scope"
                        })
                    
                    return me.objectToNode(arg).getEntry()
                })
                
                
                var backend  = this.getBackend()
                
                backend.remove(entriesOrIds).andThen(function () {
                    
                    Joose.A.each(entriesOrIds, function (entryOrId) {
                        
                        me.unpinID(typeof entryOrId == 'string' ? entryOrId : entryOrId.ID)
                    })
                    
                    this.CONTINUE()
                })
            },
            
            
            animatePacket : function (packet) {
                var me          = this
                
                var linker = new KiokuJS.Linker({
                    scope       : this,
                    
                    entries     : packet.entries // entries will be converted from Array to Object (by ID)
                })
                
                var entries         = linker.entries
                var notFetchedRefs  = []
                
                Joose.A.each(this.gatherReferences(entries), function (refID) {
                    if (entries[ refID ]) return
                    
                    if (!me.idPinned(refID)) notFetchedRefs.push(refID)
                })
                
                
                linker.link(notFetchedRefs, 0).andThen(function () {
                    
                    var objects = {}
                    
                    Joose.A.each(packet.customIDs, function (id) { 
                        objects[ id ] = me.idToObject(id)
                    })
                    
                    this.CONTINUE.apply(this, [ objects, Joose.A.map(packet.IDs, me.idToObject, me) ])
                })
            },            
            
            
            // `idsToFetch` - array of ids to fetch and materialize from backend 
            // `shallowLevel == 0` - means stop fetching at the nodes already in scope 
            // `shallowLevel == 1` - means fetching the passed `idsToFetch` anyway, but stop on further references 
            // `shallowLevel == 2` - full deep refresh 
            fetch : function (idsToFetch, shallowLevel) {
                var me          = this
                
                // linker will have an access to backend through the scope
                var linker = new KiokuJS.Linker({
                    scope       : this
                })
                
                linker.link(idsToFetch, shallowLevel).andThen(function () {
                    
                    this.CONTINUE.apply(this, Joose.A.map(idsToFetch, me.idToObject, me))
                })
            },
            
            
            lookUp : function () {
                this.fetch(Array.prototype.slice.call(arguments), 0).now()
            },
            
            
            refresh : function () {
                var me              = this
                var idsToFetch      = []
                
                Joose.A.each(arguments, function (object) {
                    if (!me.objectPinned(object)) throw "Can only refresh objects in scope" 
                    
                    idsToFetch.push(me.objectToId(object))
                })
                
                this.fetch(idsToFetch, 1).now()
            },
            
            
            deepRefresh : function () {
                var me              = this
                var idsToFetch      = []
                
                Joose.A.each(arguments, function (object) {
                    if (!me.objectPinned(object)) throw "Can only refresh objects in scope" 
                    
                    idsToFetch.push(me.objectToId(object))
                })
                
                this.fetch(idsToFetch, 2).now()
            },
            
            
            search : function () {
                var backend = this.getBackend()
                
                backend.search(this, arguments).now()
            }
        }
    }

})


;
Class('KiokuJS', {
    
//    my : {
//        
//        methods : {
//        
//            connect : function (config) {
//            }
//        }
//    }

})
;