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) {
// }
// }
// }
})
;