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) {
// }
// }
// }
})
;
Class('KiokuJS.Backend.Hash', {
isa : 'KiokuJS.Backend',
has : {
docs : Joose.I.Object
},
does : [ 'KiokuJS.Backend.Feature.Overwrite', 'KiokuJS.Backend.Feature.Update' ],
use : [ 'KiokuJS.Exception.Overwrite', 'KiokuJS.Exception.Update' ],
continued : {
methods : {
get : function (idsToGet, mode) {
var docs = this.docs
var strings = Joose.A.map(idsToGet, function (id) {
if (docs[ id ]) return docs[ id ]
throw new KiokuJS.Exception.LookUp({ id : id, backendName : this.meta.name })
}, this)
var entries = Joose.A.map(strings, this.deserialize, this)
var CONTINUE = this.getCONTINUE()
setTimeout(function () {
CONTINUE(entries)
}, 0)
},
insert : function (entries, mode) {
var docs = this.docs
var strings = Joose.A.map(entries, this.serialize, this)
Joose.A.each(entries, function (entry, index) {
var ID = entry.ID
if (mode == 'insert' && docs[ ID ])
throw new KiokuJS.Exception.Overwrite({
id : ID,
oldValue : docs[ ID ],
newValue : strings[ index ]
})
if (mode == 'update' && !docs[ ID ])
throw new KiokuJS.Exception.Update({
message : "Attempt to update entry with ID = [" + ID + "], value = [" + strings[ index ] + "], but no such entry in storage"
})
docs[ ID ] = strings[ index ]
return ID
})
var CONTINUE = this.getCONTINUE()
setTimeout(function () {
CONTINUE(entries)
}, 0)
},
remove : function (idsOrEntriesToRemove) {
var docs = this.docs
Joose.A.each(idsOrEntriesToRemove, function (id) {
// if `id` is an entry
if (id === Object(id)) id = id.ID
delete docs[ id ]
})
setTimeout(this.getCONTINUE(), 0)
},
exists : function (idsToCheck) {
var docs = this.docs
var checks = Joose.A.map(idsToCheck, function (id) {
return docs[ id ] != null
})
var CONTINUE = this.getCONTINUE()
setTimeout(function () {
CONTINUE(checks)
}, 0)
}
}
}
})
;
Class('KiokuJS.Test.Person', {
has : {
name : null,
self : null,
spouse : {
is : 'rwc'
},
father : null,
mother : null,
children : Joose.I.Array,
mood : {
is : 'rw'
},
age : 0,
task : null
},
methods : {
initialize : function () {
this.self = this
}
}
})
;
Role('KiokuJS.Test.Person.Hobby', {
requires : [ 'setMood' ],
has : {
hobbyName : null
},
methods : {
doHobby : function () {
this.setMood('good')
}
}
})
;
Class('KiokuJS.Test.Vertex', {
has : {
ID : null,
checkSum : Joose.I.Array,
ref0 : null,
ref1 : null,
ref2 : null,
ref3 : null,
ref4 : null,
ref5 : null,
ref6 : null,
ref7 : null,
ref8 : null,
ref9 : null
},
methods : {
initialize : function () {
this.ID = this.my.ID++
},
addRef : function (object) {
var index = this.checkSum.length
this[ 'ref' + index ] = object
this.checkSum[ index ] = object.ID
},
verifyIntegrity : function () {
var ok = true
Joose.A.each(this.checkSum, function (ID, index) {
if (this[ 'ref' + index ].ID != ID) {
ok = false
return false
}
}, this)
return ok
}
},
my : {
has : {
ID : 1,
HOST : null
},
methods : {
createVertex : function (level, maxRefs) {
var vertex = new this.HOST()
if (level) {
var refsNum = 1 + Math.floor(Math.random() * maxRefs)
for (var i = 1; i <= refsNum; i++) vertex.addRef(this.createVertex(level - 1, maxRefs))
}
return vertex
},
createGeneration : function (levels, maxRefs) {
return this.createVertex(levels, maxRefs)
}
}
}
})
;
Class('KiokuJS.Test.Fixture', {
use : 'KiokuJS.Backend.Role.SkipFixture',
trait : 'JooseX.CPS',
has : {
connect : { required : true },
cleanup : null,
skipCleanup : false,
t : { required : true },
requiredBackendRoles : Joose.I.Array,
sort : 1
},
methods : {
checkSkipReason : function (handle) {
var backend = handle
if (backend.meta.does(KiokuJS.Backend.Role.SkipFixture)) {
var shouldSkip = backend.skipFixtures()
if (Joose.A.exists(shouldSkip, this.meta.name)) return "Backend [" + backend + "] doesn't support functionality from the [" + this + "]"
}
var notImplementedRole
Joose.A.each(this.requiredBackendRoles, function (requiredRole) {
requiredRole = eval(requiredRole)
if (typeof requiredRole != 'function' || !requiredRole.meta) throw "Unknown value used for required backend role: [" + requiredRole + "], fixture [" + this + "]"
if (!backend.meta.does(requiredRole)) {
notImplementedRole = requiredRole
return false
}
}, this)
if (notImplementedRole) return "Backend [" + backend + "] don't implement the role [" + notImplementedRole.meta.name + "], required for fixture [" + this + "]"
}
},
continued : {
methods : {
run : function () {
this.createHandler().andThen(function (handle) {
var t = this.t
this.TRY(function () {
var skipReason = this.checkSkipReason(handle)
if (skipReason) {
t.skip(skipReason)
this.cleanupHandler(handle, t).now()
return
}
this.populate(handle, t)
this.verify(handle, t)
this.cleanupHandler(handle, t).now()
}).CATCH(function (e) {
t.fail('Exception [' + e + '] caught when running fixture: [' + this + ']')
this.CONTINUE()
}).now()
})
},
populate : function (handle, t) {
this.CONTINUE()
},
verify : function (handle, t) {
this.CONTINUE()
},
createHandler : function () {
var connect = this.connect
this.TRY(connect, this).now()
},
cleanupHandler : function (handle, t) {
var cleanup = this.cleanup
if (cleanup && !this.skipCleanup)
this.TRY(cleanup, this, [ handle, t ]).now()
else
this.CONTINUE()
}
}
}
})
;
Class('KiokuJS.Test.Fixture.ObjectGraph', {
isa : 'KiokuJS.Test.Fixture',
use : 'KiokuJS.Test.Person',
has : {
sort : 10,
originalHomer : null,
homerID : null
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.ObjectGraph - Sanity')
t.ok(KiokuJS.Test.Person, "'KiokuJS.Test.Person' is here")
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var Homer = this.originalHomer = new KiokuJS.Test.Person({
name : 'Homer Simpson',
task : function () { return "Beer" }
})
var margesTask = function () { return "Children" }
var Marge = new KiokuJS.Test.Person({
name : 'Marge Simpson',
task : margesTask
})
var Bart = new KiokuJS.Test.Person({
name : 'Bart Simpson'
})
var Lisa = new KiokuJS.Test.Person({
name : 'Lisa Simpson'
})
t.ok(Homer.self == Homer, 'Self-reference established')
Homer.spouse(Marge)
Marge.spouse(Homer)
Bart.father = Lisa.father = Homer
Bart.mother = Lisa.mother = Marge
var kids = [ Bart, Lisa ]
Homer.children = Marge.children = margesTask.children = kids
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.store(Homer).andThen(function (homerID) {
this.homerID = homerID
//======================================================================================================================================================================================================================================================
t.diag('Retrieving live object from the same scope')
scope.lookUp(homerID).andThen(function (homer2) {
t.ok(homer2 === Homer, 'Retrieved the Homer object from live objects')
this.CONTINUE()
})
}, this)
},
verify : function (handle, t) {
var newScope = handle.newScope()
newScope.lookUp(this.homerID).andThen(function (homer3) {
//======================================================================================================================================================================================================================================================
t.diag('Retrieving from backend')
t.ok(homer3 !== this.originalHomer, 'Retrieved Homer is another instance this time')
t.ok(homer3.name == 'Homer Simpson', 'But it has a correct name')
t.ok(homer3.self === homer3, 'Self-reference was reflected correctly #1')
t.ok(homer3.task() == 'Beer', 'Correctly de-serialized function #1')
var marge3 = homer3.spouse()
t.ok(marge3.self = marge3, 'Self-reference was reflected correctly #2')
t.ok(marge3 instanceof KiokuJS.Test.Person, 'Marge2 isa Person')
t.ok(marge3.name == 'Marge Simpson', 'Marge has a correct name')
t.ok(marge3.task() == 'Children', 'Correctly de-serialized function #2')
t.ok(marge3.spouse() === homer3, 'Marge2&Homer2 are spouses')
t.ok(marge3.children === homer3.children, 'Marge2&Homer2 have correct kids')
t.ok(marge3.children === marge3.task.children, 'Marge2 also has children array attached to its `task`')
var kids = marge3.children
t.ok(kids.length == 2, 'we forgot Maggy..')
t.ok((kids[0] instanceof KiokuJS.Test.Person) && (kids[1] instanceof KiokuJS.Test.Person), 'Both kids are Persons')
var bart3 = kids[0]
var lisa3 = kids[1]
t.ok(bart3.name == 'Bart Simpson', 'First kid in array is Bart')
t.ok(lisa3.name == 'Lisa Simpson', 'Second kid in array is Lisa')
t.ok(bart3.father == homer3 && bart3.mother == marge3, 'Bart3 has correct parents')
t.ok(lisa3.father == homer3 && lisa3.mother == marge3, 'Lisa3 has correct parents')
//======================================================================================================================================================================================================================================================
t.diag('Examining scope')
t.ok(newScope.objectPinned(homer3), 'Homer is in scope')
t.ok(newScope.objectPinned(marge3), 'Marge is in scope')
t.ok(newScope.objectPinned(marge3.children), 'Kids are in scope')
t.ok(newScope.objectPinned(bart3), 'Bart is in scope')
t.ok(newScope.objectPinned(lisa3), 'Lisa is in scope')
//======================================================================================================================================================================================================================================================
t.diag('Retrieving unknown key')
var thrown = false
newScope.lookUp('foobar').except(function (e) {
thrown = true
t.isaOk(e, KiokuJS.Exception.LookUp, 'Correct exception thrown')
this.CONTINUE()
}).andThen(function () {
t.ok(thrown, 'Exception thrown')
this.CONTINUE()
})
}, this)
}
}
}
})
;
Class('KiokuJS.Test.Fixture.Update', {
isa : 'KiokuJS.Test.Fixture',
use : 'KiokuJS.Test.Person',
has : {
sort : 10,
homerID : null,
requiredBackendRoles : {
init : [ 'KiokuJS.Backend.Feature.Update' ]
}
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.Update - Sanity')
t.ok(KiokuJS.Test.Person, "'KiokuJS.Test.Person' is here")
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var Homer = new KiokuJS.Test.Person({
name : 'Homer Simpson'
})
var Marge = new KiokuJS.Test.Person({
name : 'Marge Simpson'
})
var Bart = new KiokuJS.Test.Person({
name : 'Bart Simpson'
})
var Lisa = new KiokuJS.Test.Person({
name : 'Lisa Simpson'
})
t.ok(Homer.self == Homer, 'Self-reference established')
Homer.spouse(Marge)
Marge.spouse(Homer)
Bart.father = Lisa.father = Homer
Bart.mother = Lisa.mother = Marge
var kids = [ Bart, Lisa ]
Homer.children = Marge.children = kids
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.store(Homer).andThen(function (homerID) {
this.homerID = homerID
this.CONTINUE()
}, this)
},
verify : function (handle, t) {
var homerID = this.homerID
var newScope = handle.newScope()
newScope.lookUp(homerID).andThen(function (homer3) {
//======================================================================================================================================================================================================================================================
t.diag('Retrieving from backend')
var marge3 = homer3.spouse()
homer3.name = 'Homer Simpson the 3rd'
marge3.name = 'Marge Simpson the 3rd'
newScope.update(homer3).andThen(function () {
var cleanScope = handle.newScope()
cleanScope.lookUp(homerID).andThen(function (homer4) {
//======================================================================================================================================================================================================================================================
t.diag('Examining changes')
t.ok(homer4.name == 'Homer Simpson the 3rd', 'Homer was updated')
var marge4 = homer4.spouse()
t.ok(marge4.name == 'Marge Simpson', 'But Marge not, as it was not a deep update')
//======================================================================================================================================================================================================================================================
t.diag('Deep update')
homer4.name = 'Homer Simpson the 4th'
marge4.name = 'Marge Simpson the 4th'
cleanScope.deepUpdate(homer4).andThen(function () {
var cleanScope2 = handle.newScope()
var CONTINUE = this.getCONTINUE()
// XXX this prevents a very weird behavior of FF 3.6.8 for Ubuntu
// when 2 GET requests are somehow skipped
// all other browsers (even FF 3.6.8 for Windows) aren't affected
setTimeout(function () {
cleanScope2.lookUp(homerID).andThen(function (homer5) {
//======================================================================================================================================================================================================================================================
t.diag('Examining changes')
t.ok(homer5.name == 'Homer Simpson the 4th', 'Homer was updated')
var marge5 = homer5.spouse()
t.ok(marge5.name == 'Marge Simpson the 4th', 'And Marge also, as it was a deep update')
CONTINUE()
})
}, 100)
})
})
})
})
}
}
}
})
;
Class('KiokuJS.Test.Fixture.Remove', {
isa : 'KiokuJS.Test.Fixture',
use : 'KiokuJS.Test.Person',
has : {
sort : 10,
originalHomer : null,
homerID : null
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.Remove - Sanity')
t.ok(KiokuJS.Test.Person, "'KiokuJS.Test.Person' is here")
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var Homer = this.originalHomer = new KiokuJS.Test.Person({
name : 'Homer Simpson'
})
var Marge = new KiokuJS.Test.Person({
name : 'Marge Simpson'
})
var Bart = new KiokuJS.Test.Person({
name : 'Bart Simpson'
})
var Lisa = new KiokuJS.Test.Person({
name : 'Lisa Simpson'
})
t.ok(Homer.self == Homer, 'Self-reference established')
Homer.spouse(Marge)
Marge.spouse(Homer)
Bart.father = Lisa.father = Homer
Bart.mother = Lisa.mother = Marge
var kids = [ Bart, Lisa ]
Homer.children = Marge.children = kids
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.store(Homer).andThen(function (homerID) {
//======================================================================================================================================================================================================================================================
t.diag('Removing')
scope.remove(homerID, Marge).andThen(function () {
//======================================================================================================================================================================================================================================================
t.diag('Examining scope')
t.ok(!scope.objectPinned(Homer), 'Homer is not in scope now')
t.ok(!scope.objectPinned(Marge), 'Marge is not in scope now')
t.ok(scope.objectPinned(Marge.children), 'Kids are in scope')
t.ok(scope.objectPinned(Bart), 'Bart is in scope')
t.ok(scope.objectPinned(Lisa), 'Lisa is in scope')
//======================================================================================================================================================================================================================================================
t.diag('Retrieving removed key')
var thrown = false
handle.newScope().lookUp(homerID).except(function (e) {
thrown = true
t.isaOk(e, KiokuJS.Exception.LookUp, 'Correct exception thrown')
this.CONTINUE()
}).andThen(function () {
t.ok(thrown, 'Exception thrown')
this.CONTINUE()
})
})
})
}
}
}
})
;
Class('KiokuJS.Test.Fixture.Refresh', {
isa : 'KiokuJS.Test.Fixture',
use : 'KiokuJS.Test.Person',
has : {
sort : 150
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.Refresh - Sanity')
t.ok(KiokuJS.Test.Person, "'KiokuJS.Test.Person' is here")
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var Homer = this.originalHomer = new KiokuJS.Test.Person({
name : 'Homer Simpson',
task : function () { return 'Beer' }
})
var Marge = new KiokuJS.Test.Person({
name : 'Marge Simpson'
})
var Bart = new KiokuJS.Test.Person({
name : 'Bart Simpson'
})
var Lisa = new KiokuJS.Test.Person({
name : 'Lisa Simpson'
})
t.ok(Homer.self == Homer, 'Self-reference established')
Homer.spouse(Marge)
Marge.spouse(Homer)
Bart.father = Lisa.father = Homer
Bart.mother = Lisa.mother = Marge
var kids = [ Bart, Lisa ]
Homer.children = Marge.children = kids
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope1 = handle.newScope()
var scope2 = handle.newScope()
scope1.store(Homer).andThen(function (homerID) {
scope2.lookUp(homerID).andThen(function (homer2) {
t.ok(homer2 != Homer, 'Retrieved another copy of Homer')
//======================================================================================================================================================================================================================================================
t.diag('Changing values deeply in graph')
homer2.name = 'Homer Simpson the 2nd'
homer2.task = function () { return 'Bible' }
homer2.spouse().name = 'Marge Simpson the 2nd'
homer2.children[0].name = 'Bart Simpson the 2nd'
homer2.children[1].name = 'Lisa Simpson the 2nd'
scope2.store(homer2).andThen(function () {
scope1.refresh(Homer).andThen(function () {
t.ok(Homer.name == 'Homer Simpson the 2nd', 'Correct name for refreshed Homer')
t.ok(Homer.task() == 'Bible', 'Homer goes religious')
t.ok(Marge.name == 'Marge Simpson', 'But Marge name is still the same, as it was not deep update')
scope1.deepRefresh(Homer).andThen(function () {
t.ok(Homer.name == 'Homer Simpson the 2nd', 'Correct name for refreshed Homer')
t.ok(Marge.name == 'Marge Simpson the 2nd', 'Marge name is now also refreshed')
t.ok(Homer.children[0].name == 'Bart Simpson the 2nd', '... as well as Bart')
t.ok(Homer.children[1].name == 'Lisa Simpson the 2nd', '... as well as Lisa')
this.CONTINUE()
})
})
})
})
})
}
}
}
})
;
Class('KiokuJS.Test.Fixture.Traits', {
isa : 'KiokuJS.Test.Fixture',
use : 'KiokuJS.Test.Person',
has : {
sort : 10,
homerID : null
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.Traits - Sanity')
t.ok(KiokuJS.Test.Person, "'KiokuJS.Test.Person' is here")
t.ok(KiokuJS.Test.Person.Hobby, "'KiokuJS.Test.Person.Hobby' is here")
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var Homer = new KiokuJS.Test.Person({
name : 'Homer Simpson',
trait : KiokuJS.Test.Person.Hobby
})
var Marge = new KiokuJS.Test.Person({
name : 'Marge Simpson'
})
var Bart = new KiokuJS.Test.Person({
name : 'Bart Simpson'
})
var Lisa = new KiokuJS.Test.Person({
name : 'Lisa Simpson',
trait : KiokuJS.Test.Person.Hobby
})
t.ok(Homer.self == Homer, 'Self-reference established')
Homer.spouse(Marge)
Marge.spouse(Homer)
Bart.father = Lisa.father = Homer
Bart.mother = Lisa.mother = Marge
var kids = [ Bart, Lisa ]
Homer.children = Marge.children = kids
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.store(Homer).andThen(function (homerID) {
this.homerID = homerID
this.CONTINUE()
}, this)
},
verify : function (handle, t) {
var newScope = handle.newScope()
newScope.lookUp(this.homerID).andThen(function (homer3) {
//======================================================================================================================================================================================================================================================
t.diag('Retrieving from backend')
t.ok(homer3.meta.isDetached, 'Retrieved Homer has traits')
t.ok(homer3.meta.does(KiokuJS.Test.Person.Hobby), 'Retrieved Homer has hobby')
homer3.doHobby()
t.is(homer3.getMood(), 'good', 'Doing hobby is fun #1')
var lisa3 = homer3.children[1]
t.ok(lisa3.meta.isDetached, 'Retrieved Lisa has traits')
t.ok(lisa3.meta.does(KiokuJS.Test.Person.Hobby), 'Retrieved Lisa has hobby')
lisa3.doHobby()
t.is(lisa3.getMood(), 'good', 'Doing hobby is fun #2')
this.CONTINUE()
}, this)
}
}
}
})
;
Class('KiokuJS.Test.Fixture.Intrinsic', {
isa : 'KiokuJS.Test.Fixture',
has : {
sort : 10
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.Intrinsic - Sanity')
t.ok(KiokuJS.Feature.Class.Intrinsic, 'KiokuJS.Feature.Class.Intrinsic is here')
t.ok(KiokuJS.Feature.Attribute.Intrinsic, 'KiokuJS.Feature.Attribute.Intrinsic is here')
Class('Test.Wrapper', {
has : {
slot : null,
intrinsicSlot : {
trait : KiokuJS.Feature.Attribute.Intrinsic
}
}
})
Class('Test.Value.Intrinsic', {
does : KiokuJS.Feature.Class.Intrinsic,
has : {
value : null
}
})
t.ok(Test.Wrapper, 'Test.Wrapper is here')
t.ok(Test.Value.Intrinsic, 'Test.Value.Intrinsic is here')
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var sharedValue1 = new Test.Value.Intrinsic({ value : 'value' })
var sharedValue2 = [ 'foo' ]
var wrapper1 = new Test.Wrapper({ slot : sharedValue1, intrinsicSlot : sharedValue2 })
var wrapper2 = new Test.Wrapper({ slot : sharedValue1, intrinsicSlot : sharedValue2 })
var wrapper3 = new Test.Wrapper({ slot : sharedValue1, intrinsicSlot : sharedValue2 })
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.storeAs({
wrapper1 : wrapper1,
wrapper2 : wrapper2,
wrapper3 : wrapper3
}).now()
},
verify : function (handle, t) {
var newScope = handle.newScope()
newScope.lookUp('wrapper1', 'wrapper2', 'wrapper3').andThen(function (wrapper1, wrapper2, wrapper3) {
t.ok(wrapper1.slot != wrapper2.slot && wrapper2.slot != wrapper3.slot && wrapper1.slot != wrapper3.slot, 'All `slot` attributes are now different object')
t.ok(wrapper1.intrinsicSlot != wrapper2.intrinsicSlot && wrapper2.intrinsicSlot != wrapper3.intrinsicSlot && wrapper1.intrinsicSlot != wrapper3.intrinsicSlot, 'All `intrinsicSlot` attributes are now different object')
Joose.A.each(arguments, function (wrapper) {
t.ok(wrapper.slot.value == 'value', 'All `slot` has correct values though')
t.ok(wrapper.intrinsicSlot[ 0 ] == 'foo', 'All `intrinsicSlot` has correct values though')
})
this.CONTINUE()
}, this)
}
}
}
})
;
Class('KiokuJS.Test.Fixture.Immutable', {
isa : 'KiokuJS.Test.Fixture',
has : {
sort : 10
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.Immutable - Sanity')
t.ok(KiokuJS.Feature.Class.Immutable, 'KiokuJS.Feature.Class.Immutable is here')
Class('Test.Immutable', {
does : KiokuJS.Feature.Class.Immutable,
has : {
slot1 : null,
slot2 : null
}
})
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var mutable = [ 'foo' ]
var immutable = new Test.Immutable({
slot1 : 'foo1',
slot2 : mutable
})
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.storeAs({
immutable : immutable
}).now()
},
verify : function (handle, t) {
var newScope = handle.newScope()
newScope.lookUp('immutable').andThen(function (immutable) {
t.ok(immutable.slot1 == 'foo1', 'Correct value for `slot1` of `immutable`')
t.ok(immutable.slot2[ 0 ] == 'foo' && immutable.slot2.length == 1, 'Correct value for `slot2` of `immutable`')
immutable.slot2.push('baz')
newScope.store(immutable).andThen(function (immutableID) {
t.ok(immutableID == 'immutable', 'Correct ID passed after `store`')
var cleanScope1 = handle.newScope()
cleanScope1.lookUp('immutable').andThen(function (immutable) {
t.ok(immutable.slot1 == 'foo1', 'Correct value for `slot1` of `immutable`')
t.ok(immutable.slot2[ 0 ] == 'foo' && immutable.slot2.length == 1, 'Immutable object does not update referenced objects when stored 2nd time')
cleanScope1.insertAs({ immutable : immutable }).except(function (e) {
t.fail('Exception during repeated storage of immutable object')
this.CONTINUE()
}).now()
})
})
})
}
}
}
})
;
Class('KiokuJS.Test.Fixture.Proxy', {
isa : 'KiokuJS.Test.Fixture',
has : {
ID : null,
sort : 10
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.Proxy - Sanity')
Class('Proxy2', {
does : KiokuJS.Feature.Class.OwnID,
methods : {
acquireID : function () {
return 'proxy2'
}
}
})
Class('With.Proxy', {
has : {
outerProxy : null,
attr : null,
proxy2 : null
}
})
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var proxy = { 'bar' : 'baz' }
var proxy2 = new Proxy2()
var withproxy = new With.Proxy({
outerProxy : proxy,
attr : [ 'foo', proxy ],
proxy2 : proxy2
})
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.registerProxy(proxy, 'proxy')
scope.registerProxy(proxy2)
scope.store(withproxy).andThen(function (ID) {
this.ID = ID
this.CONTINUE()
}, this)
},
verify : function (handle, t) {
var newScope = handle.newScope()
var proxy = {}
var proxy2 = {}
newScope.registerProxy(proxy, 'proxy')
newScope.registerProxy(proxy2, 'proxy2')
newScope.lookUp(this.ID).andThen(function (withproxy) {
t.ok(withproxy.attr[0] == 'foo', '`withproxy` has been correctly restored from DB')
t.ok(withproxy.attr[1] == proxy, 'Proxy has been correctly set from the scope #1')
t.ok(withproxy.outerProxy == proxy, 'Proxy has been correctly set from the scope #2')
t.ok(withproxy.proxy2 == proxy2, 'Proxy2 has been correctly set from the scope')
this.CONTINUE()
})
}
}
}
})
;
Class('KiokuJS.Test.Fixture.AnimatePacket', {
isa : 'KiokuJS.Test.Fixture',
has : {
test1 : null,
packet : null
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.AnimatePacket - Sanity')
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
Class('Test.Class', {
has : {
attr1 : null,
attr2 : null
}
})
var test1 = new Test.Class({
attr1 : 'foo',
attr2 : 'bar'
})
this.test1 = test1
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.store(test1).andThen(function (test1ID) {
var test2 = new Test.Class({
attr1 : test1
})
var test3 = new Test.Class({
attr1 : test1,
attr2 : test2
})
this.packet = scope.includeNewObjects({ test3 : test3 }, [ test2 ])
this.CONTINUE()
}, this)
},
verify : function (handle, t) {
var newScope = handle.newScope()
//======================================================================================================================================================================================================================================================
t.diag('Animating packet')
newScope.animatePacket(this.packet).andThen(function (customIDs, IDs) {
var test3 = customIDs.test3
t.ok(test3, 'Something animated as the test3')
t.ok(IDs.length == 1, 'Something animated as the object w/o ids')
var test21 = IDs[0]
var test22 = test3.attr2
t.ok(test21 == test22, 'Correct relationships in animated graph #1')
t.ok(test21.attr1 == test3.attr1, 'Correct relationships in animated graph #2')
var test1 = test21.attr1
t.ok(test1.attr1 == 'foo' && test1.attr2 == 'bar', 'Correctly fetched `test1` instance')
t.ok(test1 != this.test1, 'Test1 is copy, not the same object')
this.CONTINUE()
}, this)
}
}
}
})
;
Class('KiokuJS.Test.Fixture.Lazy', {
isa : 'KiokuJS.Test.Fixture',
has : {
sort : 10
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.Lazy - Sanity')
t.ok(KiokuJS.Feature.Attribute.Lazy, 'KiokuJS.Feature.Attribute.Lazy is here')
Class('TestClass', {
has : {
lazyAttr1 : {
trait : KiokuJS.Feature.Attribute.Lazy
},
lazyAttr2 : {
trait : KiokuJS.Feature.Attribute.Lazy
},
lazyAttr3 : {
trait : KiokuJS.Feature.Attribute.Lazy
},
usualAttr : 'foo'
}
})
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var bigArray = [ 1, 2, 3 ]
var instance = new TestClass({
lazyAttr1 : bigArray,
lazyAttr2 : bigArray,
lazyAttr3 : 'yo'
})
t.ok(instance, 'Instance with lazy attributes has been instantiated successfully')
t.ok(instance.getLazyAttr1() instanceof JooseX.CPS.Continuation, 'Correct result from getter #1')
instance.getLazyAttr1().andThen(function (value) {
t.ok(value == bigArray, 'Correct result from getter #2')
t.ok(this == instance, 'Scope of getter is the instance itself #1')
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.storeAs({ instance : instance }).now()
})
},
verify : function (handle, t) {
var newScope = handle.newScope()
newScope.lookUp('instance').andThen(function (instance) {
//======================================================================================================================================================================================================================================================
t.diag('Retrieving lazy attributes')
t.ok(instance && instance.usualAttr == 'foo', 'Instance seems to be restored correctly #1')
instance.getLazyAttr3().andThen(function (value) {
t.ok(value == 'yo', 'Instance seems to be restored correctly #2')
t.ok(this == instance, 'Scope of getter is the instance itself #2')
instance.getLazyAttr1(newScope).andThen(function (value1) {
t.isDeeply(value1, [ 1, 2, 3 ], 'Value of `lazyAttr1` is correct')
instance.getLazyAttr2(newScope).andThen(function (value2) {
t.isDeeply(value2, [ 1, 2, 3 ], 'Value of `lazyAttr2` is correct')
t.ok(value1 == value2, 'Lazy attribute has been correctly pulled from scope')
this.CONT.CONTINUE()
})
})
})
})
}
}
}
})
;
Class('KiokuJS.Test.Fixture.BackendFeature.Overwrite', {
isa : 'KiokuJS.Test.Fixture',
use : [ 'KiokuJS.Test.Person', 'KiokuJS.Backend.Feature.Overwrite' ],
has : {
sort : 20,
requiredBackendRoles : {
init : [ 'KiokuJS.Backend.Feature.Overwrite' ]
}
},
continued : {
methods : {
// XXX add tests for content-addressed objects
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.BackendFeature.Overwrite - Sanity')
t.ok(KiokuJS.Test.Person, "'KiokuJS.Test.Person' is here")
//======================================================================================================================================================================================================================================================
t.diag('Graph setup')
var Homer = new KiokuJS.Test.Person({
name : 'Homer Simpson'
})
//======================================================================================================================================================================================================================================================
t.diag('Populating')
var scope = handle.newScope()
scope.insertAs({ 'homer' : Homer }).now()
},
verify : function (handle, t) {
var exceptionThrown = false
var newScope = handle.newScope()
newScope.insertAs({ 'homer' : {} }).except(function (ex) {
t.isaOk(ex, KiokuJS.Exception.Overwrite, 'Correct exception thrown')
exceptionThrown = true
this.CONTINUE()
}).andThen(function () {
t.ok(exceptionThrown, 'Exception thrown on overwrite attempt')
this.CONTINUE()
})
}
}
}
})
;
Class('KiokuJS.Test.Fixture.StressLoad.Tree', {
isa : 'KiokuJS.Test.Fixture',
use : 'KiokuJS.Test.Vertex',
has : {
sort : 100
},
continued : {
methods : {
populate : function (handle, t) {
//======================================================================================================================================================================================================================================================
t.diag('KiokuJS.Test.Fixture.StressLoad.Tree - Sanity')
t.ok(KiokuJS.Test.Vertex, 'KiokuJS.Test.Vertex is here')
var CONT = this.CONT
var scope = handle.newScope()
for (var i = 1; i <= 10; i++) CONT.AND(function () {
var CONTINUE = this.getCONTINUE()
setTimeout(function () {
scope.store(KiokuJS.Test.Vertex.createGeneration(3, 3)).andThen(function (genID) {
var cleanScope = handle.newScope()
cleanScope.lookUp(genID).andThen(function (vertex) {
t.ok(vertex.verifyIntegrity(), 'Generation integrity is ok')
CONTINUE()
})
})
}, Math.floor(Math.random() * 3000))
})
CONT.now()
}
}
// eof methods
}
// eof continued
})
;
Class('KiokuJS.Test', {
trait : 'JooseX.CPS',
has : {
t : { required : true },
connect : { required : true },
cleanup : null,
skipCleanup : false,
fixtures : {
init : [
'ObjectGraph',
'Refresh',
'Update',
'Remove',
'Traits',
'Intrinsic',
'Immutable',
'Proxy',
'AnimatePacket',
'Lazy',
'BackendFeature.Overwrite',
'StressLoad.Tree'
]
}
},
methods : {
expandFixturesNames : function () {
return Joose.A.map(this.fixtures, function (fixture) {
if (!/^=/.test(fixture))
fixture = 'KiokuJS.Test.Fixture.' + fixture
else
fixture = fixture.replace(/^=/, '')
return fixture
})
}
},
continued : {
methods : {
runAllFixtures : function () {
this.loadFixtures().andThen(function () {
var fixtures = Joose.A.map(this.expandFixturesNames(), function (fixtureName) {
var constructor = eval(fixtureName)
return new constructor({
t : this.t,
connect : this.connect,
cleanup : this.cleanup,
skipCleanup : this.skipCleanup
})
}, this)
fixtures.sort(function (a, b) {
return a.sort - b.sort
})
this.runFixtures(fixtures).now()
})
},
runFixtures : function (fixtures) {
var me = this
Joose.A.each(fixtures, function (fixture) {
me.THEN(function () {
fixture.run().now()
})
})
if (fixtures.length)
this.NOW()
else
this.CONTINUE()
},
loadFixtures : function () {
use(this.expandFixturesNames(), this.getCONTINUE())
}
}
}
})
;