From 6a77514d799e4a5e380865ff077baff60743c816 Mon Sep 17 00:00:00 2001 From: Philip Diffenderfer Date: Mon, 19 Sep 2016 23:25:44 -0400 Subject: [PATCH] Finish Test Scenarios (resolves #1) --- bower.json | 2 +- build/rekord-session.js | 561 ++++++++++++++---- build/rekord-session.min.js | 4 +- build/rekord-session.min.js.map | 2 +- package.json | 2 +- src/header.js | 4 + src/lib/Listeners.js | 26 +- src/lib/Model.js | 9 +- src/lib/Session.js | 314 +++++++--- src/lib/SessionWatch.js | 165 +++++- src/lib/search.js | 40 ++ test/base.js | 72 ++- test/cases/rekord-session.js | 992 +++++++++++++++++++++++++++++++- 13 files changed, 1964 insertions(+), 229 deletions(-) create mode 100644 src/lib/search.js diff --git a/bower.json b/bower.json index 6f7df31..9e63fd3 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "rekord-session", - "version": "1.4.0", + "version": "1.4.1", "homepage": "https://github.com/Rekord/rekord-session", "authors": [ "Philip Diffenderfer " diff --git a/build/rekord-session.js b/build/rekord-session.js index 130a660..8cdd3c1 100644 --- a/build/rekord-session.js +++ b/build/rekord-session.js @@ -1,4 +1,4 @@ -/* rekord-session 1.4.0 - Adds mass changes & discards to Rekord by Philip Diffenderfer */ +/* rekord-session 1.4.1 - Adds mass changes & discards to Rekord by Philip Diffenderfer */ (function(global, Rekord, undefined) { var Map = Rekord.Map; @@ -9,8 +9,10 @@ var ModelCollection = Rekord.ModelCollection; var RelationHasOne = Rekord.Relations.hasOne; var RelationBelongsTo = Rekord.Relations.belongsTo; + var Cascade = Rekord.Cascade; var isObject = Rekord.isObject; + var isNumber = Rekord.isNumber; var uuid = Rekord.uuid; var equals = Rekord.equals; var noop = Rekord.noop; @@ -18,28 +20,34 @@ var addMethods = Rekord.addMethods; var replaceMethod = Rekord.replaceMethod; + var keyParser = Rekord.createParser('$key()'); + var Listeners = { RelationUpdate: function(session, watcher, parent, related, property) { - return function(relator, relation) + return function onRelationUpdate(relator, relation) { if ( session.isDestroyed() ) { return; } - if ( session.isWatching( relation.lastRelated ) && !session.isWatching( session.related ) ) + if ( relation.lastRelated && session.isWatching( relation.lastRelated ) ) { session.unwatch( relation.lastRelated ); - session.watch( session.related, watcher.relations[ property ], watcher ); + } + + if ( relation.related && !session.isWatching( relation.related ) ) + { + session.watch( relation.related, watcher.relations[ property ], watcher ); } }; }, CollectionAdd: function(session, watcher) { - return function (collection, added) + return function onAdd(collection, added) { if ( session.isDestroyed() ) { @@ -52,7 +60,7 @@ var Listeners = { CollectionAdds: function(session, watcher) { - return function (collection, added) + return function onAdds(collection, added) { if ( session.isDestroyed() ) { @@ -68,7 +76,7 @@ var Listeners = { CollectionRemove: function(session, watcher) { - return function (collection, removed) + return function onRemove(collection, removed) { if ( session.isDestroyed() ) { @@ -81,7 +89,7 @@ var Listeners = { CollectionRemoves: function(session, watcher) { - return function (collection, removed) + return function onRemoves(collection, removed) { if ( session.isDestroyed() ) { @@ -97,14 +105,14 @@ var Listeners = { CollectionReset: function(session, watcher) { - return function (collection) + return function onReset(collection) { if ( session.isDestroyed() ) { return; } - watcher.destroyChildren(); + watcher.moveChildren( session.unwatched ); for (var i = 0; i < collection.length; i++) { @@ -115,14 +123,14 @@ var Listeners = { CollectionCleared: function(session, watcher) { - return function (collection) + return function onCleared(collection) { if ( session.isDestroyed() ) { return; } - watcher.destroyChildren(); + watcher.moveChildren( session.unwatched ); }; } @@ -144,9 +152,14 @@ replaceMethod( Model.prototype, '$save', function($save) if ( fakeIt ) { + var cascade = + (arguments.length === 3 ? cascade : + (arguments.length === 2 && isObject( setProperties ) && isNumber( setValue ) ? setValue : + (arguments.length === 1 && isNumber( setProperties ) ? setProperties : this.$db.cascade ) ) ); + this.$set( setProperties, setValue ); - this.$session.saveModel( this ); + this.$session.saveModel( this, cascade ); return Promise.resolve( this ); } @@ -169,7 +182,7 @@ replaceMethod( Model.prototype, '$remove', function($remove) if ( fakeIt ) { - this.$session.removeModel( this ); + this.$session.removeModel( this, cascade ); return Promise.resolve( this ); } @@ -185,7 +198,8 @@ function Session() { this.status = Session.Status.Active; this.watching = new Map(); - this.removing = new Collection(); + this.removing = new Map(); + this.unwatched = new Map(); this.validationRequired = false; } @@ -205,33 +219,58 @@ addMethods( Session.prototype, hasChanges: function(checkSavedOnly) { - if (this.removing.length > 0) + if (this.removing.size() > 0) { return true; } - return this.searchModels( false, function(model, watcher) + var unwatchedChanges = searchModels( this.unwatched, false, function(model, watcher) { if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() ) { return true; } }); + + if ( unwatchedChanges ) + { + return true; + } + + var watchedChanges = searchModels( this.watching, false, function(model, watcher) + { + if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() ) + { + return true; + } + }); + + return watchedChanges; }, getChanged: function(checkSavedOnly, out) { var target = out || new Collection(); - target.push.apply( target, this.removing ); + target.push.apply( target, this.removing.values ); - return this.searchModels( target, function(model, watcher) + searchModels( this.watching, null, function(model, watcher) { if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() ) { target.push( model ); } }); + + searchModels( this.unwatched, null, function(model, watcher) + { + if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() ) + { + target.push( model ); + } + }); + + return target; }, validate: function(stopAtInvalid) @@ -240,7 +279,7 @@ addMethods( Session.prototype, if ( Rekord.Validation ) { - this.searchModels( true, function(model, watcher) + searchModels( this.watching, true, function(model, watcher) { if ( model.$validate && !model.$validate() ) { @@ -274,16 +313,26 @@ addMethods( Session.prototype, return Promise.reject( this ); } - return Promise.singularity( Promise.resolve( this ), this, this.handleSave ); + var sessionPromise = new Promise(); + + var savePromise = Promise.singularity( sessionPromise, this, this.handleSave ); + + sessionPromise.resolve( this ); + + savePromise.success( this.onSaveSuccess, this ); + + return savePromise; }, - handleSave: function() + handleSave: function(singularity) { this.status = Session.Status.Saving; - this.searchModels( true, this.executeSave ); + searchModels( this.watching, true, this.executeSave, this ); - this.removing.each( this.executeRemove, this ); + searchModels( this.removing, true, this.executeRemove, this ); + + searchAny( this.unwatched, true, this.executeUnwatchedSave, this ); this.status = Session.Status.Active; }, @@ -298,11 +347,11 @@ addMethods( Session.prototype, model.$db.models.remove( model.$key() ); } - model.$save().success( this.afterSave( watcher, model) ); + model.$save( watcher.cascade ).success( this.afterSave( watcher ) ); } }, - executeRemove: function(model) + executeRemove: function(model, watcher) { if ( model.$status === Model.Status.RemovePending ) { @@ -310,33 +359,72 @@ addMethods( Session.prototype, model.$status = Model.Status.Synced; model.$db.models.put( model.$key(), model ); - model.$remove().success( this.afterRemove( this, model ) ); + model.$remove( watcher.cascade ).success( this.afterRemove( watcher, this ) ); + } + }, + + executeUnwatchedSave: function(model, watcher) + { + if ( watcher.save ) + { + if ( !model.$isSaved() ) + { + model.$db.models.remove( model.$key() ); + } + + model.$save( watcher.cascade ).success( this.afterUnwatchSave( watcher ) ); } }, - afterSave: function(watcher, model) + afterSave: function(watcher) { return function onSave() { - model.$push(); + watcher.saveState( true ); watcher.save = false; }; }, - afterRemove: function(session, model) + afterRemove: function(watcher, session) { return function onRemove() { - session.removing.remove( model ); + session.removing.remove( watcher.key ); + watcher.destroyReferences(); }; }, + afterUnwatchSave: function(watcher) + { + return function onSave() + { + watcher.destroy(); + }; + }, + + onSaveSuccess: function() + { + searchAny( this.unwatched, true, this.onSaveSuccessUnwatched, this ); + + this.removing.reset(); + this.unwatched.reset(); + }, + + onSaveSuccessUnwatched: function(model, watcher) + { + watcher.destroy(); + }, + discard: function() { - this.searchModels( true, this.discardSave ); + searchModels( this.removing, true, this.discardRemove, this ); - this.removing.each( this.discardRemove ); - this.removing.clear(); + searchAny( this.unwatched, true, this.discardUnwatched, this ); + + searchModels( this.watching, true, this.discardSave, this ); + + this.removing.reset(); + this.unwatched.reset(); return this; }, @@ -345,30 +433,35 @@ addMethods( Session.prototype, { if ( watcher.save ) { + watcher.save = false; + if ( !model.$isSaved() ) { model.$db.removeFromModels( model ); } - else - { - model.$pop(); - } - - watcher.save = false; } + + watcher.restoreState(); + + model.$updated(); }, - discardRemove: function(model) + discardRemove: function(model, watcher) { if ( model.$status === Model.Status.RemovePending ) { - model.$pop(); model.$status = Model.Status.Synced; model.$db.models.put( model.$key(), model ); - model.$updated(); + + watcher.reattach(); } }, + discardUnwatched: function(model, watcher) + { + watcher.reattach(); + }, + disable: function() { if ( this.status === Session.Status.Active ) @@ -411,29 +504,6 @@ addMethods( Session.prototype, return this.status === Session.Status.Destroyed; }, - searchModels: function(defaultResult, callback, context) - { - var callbackContext = context || this; - var watches = this.watching.values; - - for (var i = 0; i < watches.length; i++) - { - var watcher = watches[ i ]; - - if ( watcher.object instanceof Model ) - { - var result = callback.call( callbackContext, watcher.object, watcher ); - - if ( result !== undefined ) - { - return result; - } - } - } - - return defaultResult; - }, - destroy: function() { if ( this.status !== Session.Status.Destroyed ) @@ -446,12 +516,21 @@ addMethods( Session.prototype, if ( !watcher.parent ) { - watcher.destroy( this ); + watcher.destroy(); } } + var removing = this.removing.values; + + for (var i = 0; i < removing.length; i++) + { + var remover = removing[ i ]; + + remover.destroyReferences(); + } + this.watching.reset(); - this.removing.clear(); + this.removing.reset(); this.status = Session.Status.Destroyed; } @@ -472,7 +551,7 @@ addMethods( Session.prototype, return object.$sessionKey; } - else + else if ( create ) { throw 'The object provided cannot be watched by session.'; } @@ -486,6 +565,18 @@ addMethods( Session.prototype, { var watch = this.watching.get( key ); + if ( !watch && create ) + { + watch = this.unwatched.get( key ); + + this.unwatched.remove( key ); + + if ( watch ) + { + this.watching.put( key, watch ); + } + } + if ( !watch && create ) { watch = new SessionWatch( key, object ); @@ -497,11 +588,72 @@ addMethods( Session.prototype, } }, + getAnyWatch: function(object) + { + var key = this.getSessionKey( object ); + var watcher = null; + + if ( key ) + { + watcher = this.watching.get( key ); + + if ( !watcher ) + { + watcher = this.unwatched.get( key ); + } + } + + return watcher; + }, + + getRemoveWatch: function(object) + { + var key = this.getSessionKey( object ); + + if ( key ) + { + return this.removing.get( key ); + } + }, + isWatching: function(object) { var key = this.getSessionKey( object ); - return key !== false && this.watching.has( key ); + return key && this.watching.has( key ); + }, + + isUnwatched: function(object) + { + var key = this.getSessionKey( object ); + + return key && this.unwatched.has( key ); + }, + + isRemoved: function(object) + { + var key = this.getSessionKey( object ); + + return key && this.removing.has( key ); + }, + + hasWatched: function(object) + { + var key = this.getSessionKey( object ); + + return key && ( this.watching.has( key ) || this.removing.has( key ) || this.unwatched.has( key ) ); + }, + + watchMany: function(models, relations) + { + var watchers = []; + + for (var i = 0; i < models.length; i++) + { + watchers.push( this.watch( models[ i ], relations ) ); + } + + return watchers; }, watch: function(model, relations, parent) @@ -511,8 +663,7 @@ addMethods( Session.prototype, watcher.setRelations( relations ); watcher.setSession( this ); watcher.setParent( parent ); - - model.$push(); + watcher.saveState(); if ( isObject( relations ) ) { @@ -573,52 +724,64 @@ addMethods( Session.prototype, if ( watcher ) { - watcher.destroy( this ); + watcher.moveTo( this.unwatched ); } } }, - saveModel: function(model) + saveModel: function(model, cascade) { - var watcher = this.getSessionWatch( model ); + var watcher = this.getAnyWatch( model ); - if ( watcher && !watcher.save ) + if ( watcher ) { - var key = model.$key(); - var db = model.$db; + watcher.addCascade( cascade ); - if ( db.models.has( key ) ) + if ( !watcher.save ) { - db.trigger( Database.Events.ModelUpdated, [model] ); + var key = model.$key(); + var db = model.$db; - model.$trigger( Model.Events.UpdateAndSave ); - } - else - { - db.models.put( key, model ); - db.trigger( Database.Events.ModelAdded, [model] ); - db.updated(); + if ( db.models.has( key ) ) + { + db.trigger( Database.Events.ModelUpdated, [model] ); - model.$trigger( Model.Events.CreateAndSave ); - } + model.$trigger( Model.Events.UpdateAndSave ); + } + else + { + db.models.put( key, model ); + db.trigger( Database.Events.ModelAdded, [model] ); + db.updated(); + + model.$trigger( Model.Events.CreateAndSave ); + } - watcher.save = true; + watcher.save = true; + } } }, - removeModel: function(model) + removeModel: function(model, cascade) { - var watcher = this.getSessionWatch( model ); + var watcher = this.getAnyWatch( model ); if ( watcher ) { - model.$push(); + watcher.addCascade( cascade ); + watcher.moveTo( this.removing ); + model.$status = Model.Status.RemovePending; model.$db.removeFromModels( model ); + } + else + { + var removed = this.getRemoveWatch( model ); - this.removing.add( model ); - - watcher.destroy( this ); + if ( removed ) + { + removed.addCascade( cascade ); + } } } @@ -629,11 +792,14 @@ function SessionWatch( key, object ) { this.key = key; this.object = object; + this.state = null; this.relations = false; this.parent = false; this.children = {}; this.offs = []; this.save = false; + this.cascade = undefined; + this.state = null; } addMethods( SessionWatch.prototype, @@ -692,42 +858,227 @@ addMethods( SessionWatch.prototype, this.offs.push( off ); }, - destroy: function(session) + addCascade: function(cascade) + { + if ( isNumber( cascade ) ) + { + if ( this.cascade === undefined ) + { + this.cascade = 0; + } + + this.cascade = this.cascade | cascade; + } + }, + + saveState: function(override) + { + if ( this.state && !override ) + { + return; + } + + var model = this.object; + var oldState = model.$savedState; + + model.$push(); + + var relations = this.relations; + var state = model.$savedState; + + if ( isObject( relations ) ) + { + for (var relationName in relations) + { + var value = model[ relationName ]; + + if ( value instanceof Model ) + { + state[ relationName ] = value.$key(); + } + else if ( value instanceof ModelCollection ) + { + state[ relationName ] = value.pluck( keyParser ); + } + else + { + state[ relationName ] = null; + } + } + } + + this.state = state; + + model.$savedState = oldState; + }, + + restoreState: function() + { + var model = this.object; + var state = this.state; + + if ( isObject( state ) ) + { + var relations = model.$db.relations; + var relationsWatched = this.relations; + var relationsSnapshot = {}; + + for (var relationName in relationsWatched) + { + var relation = relations[ relationName ]; + + relationsSnapshot[ relationName ] = { + clearKey: relation.clearKey, + cascadeRemove: relation.cascadeRemove, + cascade: relation.cascade + }; + + relation.clearKey = false; + relation.cascadeRemove = Cascade.None; + relation.cascade = Cascade.None; + } + + model.$set( state, undefined, true, true ); + model.$decode(); + + for (var relationName in relationsWatched) + { + var relation = relations[ relationName ]; + var snapshot = relationsSnapshot[ relationName ]; + + relation.clearKey = snapshot.clearKey; + relation.cascadeRemove = snapshot.cascadeRemove; + relation.cascade = snapshot.cascade; + } + } + }, + + removeListeners: function() { var offs = this.offs; - var object = this.object; for (var i = 0; i < offs.length; i++) { offs[ i ](); } + offs.length = 0; + }, + + moveTo: function(target) + { + var session = this.object.$session; + + this.removeListeners(); + this.moveChildren( target ); + + if ( this.parent ) + { + delete this.parent.children[ this.key ]; + } + session.watching.remove( this.key ); + session.unwatched.remove( this.key ); + session.removing.remove( this.key ); + + target.put( this.key, this ); + }, + + reattach: function() + { + var session = this.object.$session; + + session.removing.remove( this.key ); + session.unwatched.remove( this.key ); + session.watching.put( this.key, this ); + + session.watch( this.object, this.relations, this.parent ); + }, - this.destroyChildren( session ); + destroy: function() + { + var children = this.children; - object.$session = null; + this.children = {}; + this.removeListeners(); + this.destroyReferences(); + + for (var childKey in children) + { + children[ childKey ].destroy(); + } + }, + + destroyReferences: function() + { + var session = this.object.$session; + + session.watching.remove( this.key ); + session.removing.remove( this.key ); + session.unwatched.remove( this.key ); + + this.object.$session = null; + this.state = null; this.parent = null; - this.offs.length = 0; this.save = false; + this.cascade = undefined; }, - destroyChildren: function(session) + moveChildren: function(target) { var children = this.children; for (var childKey in children) { - children[ childKey ].destroy( session ); + children[ childKey ].moveTo( target ); } - - this.children = {}; } }); +function searchAny(map, defaultResult, callback, context) +{ + var watchers = map.values; + + for (var i = watchers.length - 1; i >= 0; i--) + { + var watcher = watchers[ i ]; + var result = callback.call( context, watcher.object, watcher ); + + if ( result !== undefined ) + { + return result; + } + } + + return defaultResult; +} + +function searchModels(map, defaultResult, callback, context) +{ + var watchers = map.values; + + for (var i = watchers.length - 1; i >= 0; i--) + { + var watcher = watchers[ i ]; + + if ( watcher.object instanceof Model ) + { + var result = callback.call( context, watcher.object, watcher ); + + if ( result !== undefined ) + { + return result; + } + } + } + + return defaultResult; +} + + Rekord.Session = Session; Rekord.SessionWatch = SessionWatch; diff --git a/build/rekord-session.min.js b/build/rekord-session.min.js index 7a932b9..1af2062 100644 --- a/build/rekord-session.min.js +++ b/build/rekord-session.min.js @@ -1,3 +1,3 @@ -/* rekord-session 1.4.0 - Adds mass changes & discards to Rekord by Philip Diffenderfer */ -!function(t,e,s){function i(){this.status=i.Status.Active,this.watching=new o,this.removing=new u,this.validationRequired=!1}function n(t,e){this.key=t,this.object=e,this.relations=!1,this.parent=!1,this.children={},this.offs=[],this.save=!1}var o=e.Map,a=e.Model,r=e.Promise,h=e.Database,u=e.Collection,c=e.ModelCollection,d=e.Relations.hasOne,l=e.Relations.belongsTo,v=e.isObject,f=e.uuid,g=e.equals,S=e.noop,$=e.addMethods,y=e.replaceMethod,m={RelationUpdate:function(t,e,s,i,n){return function(s,i){t.isDestroyed()||t.isWatching(i.lastRelated)&&!t.isWatching(t.related)&&(t.unwatch(i.lastRelated),t.watch(t.related,e.relations[n],e))}},CollectionAdd:function(t,e){return function(s,i){t.isDestroyed()||t.watch(i,e.relations,e)}},CollectionAdds:function(t,e){return function(s,i){if(!t.isDestroyed())for(var n=0;n0?!0:this.searchModels(!1,function(e,s){return t&&!s.save||!e.$hasChanges()?void 0:!0})},getChanged:function(t,e){var s=e||new u;return s.push.apply(s,this.removing),this.searchModels(s,function(e,i){t&&!i.save||!e.$hasChanges()||s.push(e)})},validate:function(t){var s=!0;return e.Validation&&this.searchModels(!0,function(e,i){return e.$validate&&!e.$validate()&&(s=!1,t)?!1:void 0}),s},setValidationRequired:function(t){this.validationRequired=t},save:function(t){return this.status!==i.Status.Active?r.reject(this):this.validationRequired&&!this.validate(!t)?r.reject(this):r.singularity(r.resolve(this),this,this.handleSave)},handleSave:function(){this.status=i.Status.Saving,this.searchModels(!0,this.executeSave),this.removing.each(this.executeRemove,this),this.status=i.Status.Active},executeSave:function(t,e){e.save&&(t.$isSaved()||t.$db.models.remove(t.$key()),t.$save().success(this.afterSave(e,t)))},executeRemove:function(t){t.$status===a.Status.RemovePending&&(t.$status=a.Status.Synced,t.$db.models.put(t.$key(),t),t.$remove().success(this.afterRemove(this,t)))},afterSave:function(t,e){return function(){e.$push(),t.save=!1}},afterRemove:function(t,e){return function(){t.removing.remove(e)}},discard:function(){return this.searchModels(!0,this.discardSave),this.removing.each(this.discardRemove),this.removing.clear(),this},discardSave:function(t,e){e.save&&(t.$isSaved()?t.$pop():t.$db.removeFromModels(t),e.save=!1)},discardRemove:function(t){t.$status===a.Status.RemovePending&&(t.$pop(),t.$status=a.Status.Synced,t.$db.models.put(t.$key(),t),t.$updated())},disable:function(){this.status===i.Status.Active&&(this.status=i.Status.Disabled)},enable:function(){this.status===i.Status.Disabled&&(this.status=i.Status.Active)},isEnabled:function(){return this.status!==i.Status.Disabled&&this.status!==i.Status.Destroyed},isActive:function(){return this.status===i.Status.Active},isDisabled:function(){return this.status===i.Status.Disabled},isSaving:function(){return this.status===i.Status.Saving},isDestroyed:function(){return this.status===i.Status.Destroyed},searchModels:function(t,e,i){for(var n=i||this,o=this.watching.values,r=0;r=0;o--){var r=a[o],h=i.call(n,r.object,r);if(h!==s)return h}return e}function o(t,e,i,n){for(var a=t.values,o=a.length-1;o>=0;o--){var r=a[o];if(r.object instanceof h){var c=i.call(n,r.object,r);if(c!==s)return c}}return e}var r=e.Map,h=e.Model,c=e.Promise,u=e.Database,d=e.Collection,v=e.ModelCollection,l=e.Relations.hasOne,f=e.Relations.belongsTo,g=e.Cascade,m=e.isObject,y=e.isNumber,S=e.uuid,w=e.equals,$=e.noop,R=e.addMethods,p=e.replaceMethod,b=e.createParser("$key()"),C={RelationUpdate:function(t,e,s,i,n){return function(s,i){t.isDestroyed()||(i.lastRelated&&t.isWatching(i.lastRelated)&&t.unwatch(i.lastRelated),i.related&&!t.isWatching(i.related)&&t.watch(i.related,e.relations[n],e))}},CollectionAdd:function(t,e){return function(s,i){t.isDestroyed()||t.watch(i,e.relations,e)}},CollectionAdds:function(t,e){return function(s,i){if(!t.isDestroyed())for(var n=0;n0)return!0;var e=o(this.unwatched,!1,function(e,s){return t&&!s.save||!e.$hasChanges()?void 0:!0});if(e)return!0;var s=o(this.watching,!1,function(e,s){return t&&!s.save||!e.$hasChanges()?void 0:!0});return s},getChanged:function(t,e){var s=e||new d;return s.push.apply(s,this.removing.values),o(this.watching,null,function(e,i){t&&!i.save||!e.$hasChanges()||s.push(e)}),o(this.unwatched,null,function(e,i){t&&!i.save||!e.$hasChanges()||s.push(e)}),s},validate:function(t){var s=!0;return e.Validation&&o(this.watching,!0,function(e,i){return e.$validate&&!e.$validate()&&(s=!1,t)?!1:void 0}),s},setValidationRequired:function(t){this.validationRequired=t},save:function(t){if(this.status!==i.Status.Active)return c.reject(this);if(this.validationRequired&&!this.validate(!t))return c.reject(this);var e=new c,s=c.singularity(e,this,this.handleSave);return e.resolve(this),s.success(this.onSaveSuccess,this),s},handleSave:function(t){this.status=i.Status.Saving,o(this.watching,!0,this.executeSave,this),o(this.removing,!0,this.executeRemove,this),a(this.unwatched,!0,this.executeUnwatchedSave,this),this.status=i.Status.Active},executeSave:function(t,e){e.save&&(t.$isSaved()||t.$db.models.remove(t.$key()),t.$save(e.cascade).success(this.afterSave(e)))},executeRemove:function(t,e){t.$status===h.Status.RemovePending&&(t.$status=h.Status.Synced,t.$db.models.put(t.$key(),t),t.$remove(e.cascade).success(this.afterRemove(e,this)))},executeUnwatchedSave:function(t,e){e.save&&(t.$isSaved()||t.$db.models.remove(t.$key()),t.$save(e.cascade).success(this.afterUnwatchSave(e)))},afterSave:function(t){return function(){t.saveState(!0),t.save=!1}},afterRemove:function(t,e){return function(){e.removing.remove(t.key),t.destroyReferences()}},afterUnwatchSave:function(t){return function(){t.destroy()}},onSaveSuccess:function(){a(this.unwatched,!0,this.onSaveSuccessUnwatched,this),this.removing.reset(),this.unwatched.reset()},onSaveSuccessUnwatched:function(t,e){e.destroy()},discard:function(){return o(this.removing,!0,this.discardRemove,this),a(this.unwatched,!0,this.discardUnwatched,this),o(this.watching,!0,this.discardSave,this),this.removing.reset(),this.unwatched.reset(),this},discardSave:function(t,e){e.save&&(e.save=!1,t.$isSaved()||t.$db.removeFromModels(t)),e.restoreState(),t.$updated()},discardRemove:function(t,e){t.$status===h.Status.RemovePending&&(t.$status=h.Status.Synced,t.$db.models.put(t.$key(),t),e.reattach())},discardUnwatched:function(t,e){e.reattach()},disable:function(){this.status===i.Status.Active&&(this.status=i.Status.Disabled)},enable:function(){this.status===i.Status.Disabled&&(this.status=i.Status.Active)},isEnabled:function(){return this.status!==i.Status.Disabled&&this.status!==i.Status.Destroyed},isActive:function(){return this.status===i.Status.Active},isDisabled:function(){return this.status===i.Status.Disabled},isSaving:function(){return this.status===i.Status.Saving},isDestroyed:function(){return this.status===i.Status.Destroyed},destroy:function(){if(this.status!==i.Status.Destroyed){for(var t=this.watching.values,e=0;e 0)\n {\n return true;\n }\n\n return this.searchModels( false, function(model, watcher)\n {\n if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() )\n {\n return true;\n }\n });\n },\n\n getChanged: function(checkSavedOnly, out)\n {\n var target = out || new Collection();\n\n target.push.apply( target, this.removing );\n\n return this.searchModels( target, function(model, watcher)\n {\n if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() )\n {\n target.push( model );\n }\n });\n },\n\n validate: function(stopAtInvalid)\n {\n var valid = true;\n\n if ( Rekord.Validation )\n {\n this.searchModels( true, function(model, watcher)\n {\n if ( model.$validate && !model.$validate() )\n {\n valid = false;\n\n if ( stopAtInvalid )\n {\n return false;\n }\n }\n });\n }\n\n return valid;\n },\n\n setValidationRequired: function(required)\n {\n this.validationRequired = required;\n },\n\n save: function(fullValidate)\n {\n if ( this.status !== Session.Status.Active )\n {\n return Promise.reject( this );\n }\n\n if ( this.validationRequired && !this.validate( !fullValidate ) )\n {\n return Promise.reject( this );\n }\n\n return Promise.singularity( Promise.resolve( this ), this, this.handleSave );\n },\n\n handleSave: function()\n {\n this.status = Session.Status.Saving;\n\n this.searchModels( true, this.executeSave );\n\n this.removing.each( this.executeRemove, this );\n\n this.status = Session.Status.Active;\n },\n\n executeSave: function(model, watcher)\n {\n if ( watcher.save )\n {\n // Remove it so $save processes normally\n if ( !model.$isSaved() )\n {\n model.$db.models.remove( model.$key() );\n }\n\n model.$save().success( this.afterSave( watcher, model) );\n }\n },\n\n executeRemove: function(model)\n {\n if ( model.$status === Model.Status.RemovePending )\n {\n // Add it back so $remove processes normally\n model.$status = Model.Status.Synced;\n model.$db.models.put( model.$key(), model );\n\n model.$remove().success( this.afterRemove( this, model ) );\n }\n },\n\n afterSave: function(watcher, model)\n {\n return function onSave()\n {\n model.$push();\n watcher.save = false;\n };\n },\n\n afterRemove: function(session, model)\n {\n return function onRemove()\n {\n session.removing.remove( model );\n };\n },\n\n discard: function()\n {\n this.searchModels( true, this.discardSave );\n\n this.removing.each( this.discardRemove );\n this.removing.clear();\n\n return this;\n },\n\n discardSave: function(model, watcher)\n {\n if ( watcher.save )\n {\n if ( !model.$isSaved() )\n {\n model.$db.removeFromModels( model );\n }\n else\n {\n model.$pop();\n }\n\n watcher.save = false;\n }\n },\n\n discardRemove: function(model)\n {\n if ( model.$status === Model.Status.RemovePending )\n {\n model.$pop();\n model.$status = Model.Status.Synced;\n model.$db.models.put( model.$key(), model );\n model.$updated();\n }\n },\n\n disable: function()\n {\n if ( this.status === Session.Status.Active )\n {\n this.status = Session.Status.Disabled;\n }\n },\n\n enable: function()\n {\n if ( this.status === Session.Status.Disabled )\n {\n this.status = Session.Status.Active;\n }\n },\n\n isEnabled: function()\n {\n return this.status !== Session.Status.Disabled &&\n this.status !== Session.Status.Destroyed;\n },\n\n isActive: function()\n {\n return this.status === Session.Status.Active;\n },\n\n isDisabled: function()\n {\n return this.status === Session.Status.Disabled;\n },\n\n isSaving: function()\n {\n return this.status === Session.Status.Saving;\n },\n\n isDestroyed: function()\n {\n return this.status === Session.Status.Destroyed;\n },\n\n searchModels: function(defaultResult, callback, context)\n {\n var callbackContext = context || this;\n var watches = this.watching.values;\n\n for (var i = 0; i < watches.length; i++)\n {\n var watcher = watches[ i ];\n\n if ( watcher.object instanceof Model )\n {\n var result = callback.call( callbackContext, watcher.object, watcher );\n\n if ( result !== undefined )\n {\n return result;\n }\n }\n }\n\n return defaultResult;\n },\n\n destroy: function()\n {\n if ( this.status !== Session.Status.Destroyed )\n {\n var watches = this.watching.values;\n\n for (var i = 0; i < watches.length; i++)\n {\n var watcher = watches[ i ];\n\n if ( !watcher.parent )\n {\n watcher.destroy( this );\n }\n }\n\n this.watching.reset();\n this.removing.clear();\n\n this.status = Session.Status.Destroyed;\n }\n },\n\n getSessionKey: function(object, create)\n {\n if ( object instanceof Model )\n {\n return object.$uid();\n }\n else if ( object instanceof ModelCollection )\n {\n if ( !object.$sessionKey && create )\n {\n object.$sessionKey = uuid();\n }\n\n return object.$sessionKey;\n }\n else\n {\n throw 'The object provided cannot be watched by session.';\n }\n },\n\n getSessionWatch: function(object, create)\n {\n var key = this.getSessionKey( object, create );\n\n if ( key )\n {\n var watch = this.watching.get( key );\n\n if ( !watch && create )\n {\n watch = new SessionWatch( key, object );\n\n this.watching.put( key, watch );\n }\n\n return watch;\n }\n },\n\n isWatching: function(object)\n {\n var key = this.getSessionKey( object );\n\n return key !== false && this.watching.has( key );\n },\n\n watch: function(model, relations, parent)\n {\n var watcher = this.getSessionWatch( model, true );\n\n watcher.setRelations( relations );\n watcher.setSession( this );\n watcher.setParent( parent );\n\n model.$push();\n\n if ( isObject( relations ) )\n {\n for (var property in relations)\n {\n var value = model[ property ];\n\n if ( value instanceof Model )\n {\n this.watch( value, relations[ property ], watcher );\n }\n else if ( value instanceof ModelCollection )\n {\n this.watchCollection( value, relations[ property ], watcher );\n }\n\n var relation = model.$getRelation( property );\n\n if ( relation instanceof RelationHasOne || relation instanceof RelationBelongsTo )\n {\n watcher.addListener( Model.Events.RelationUpdate, Listeners.RelationUpdate( this, watcher, model, value, property ) );\n }\n }\n }\n\n return watcher;\n },\n\n watchCollection: function(collection, relations, parent)\n {\n var watcher = this.getSessionWatch( collection, true );\n\n watcher.setRelations( relations );\n watcher.setSession( this );\n watcher.setParent( parent );\n\n collection.each(function(model)\n {\n this.watch( model, relations, watcher );\n\n }, this );\n\n watcher.addListener( Collection.Events.Add, Listeners.CollectionAdd( this, watcher ) );\n watcher.addListener( Collection.Events.Adds, Listeners.CollectionAdds( this, watcher ) );\n watcher.addListener( Collection.Events.Reset, Listeners.CollectionReset( this, watcher ) );\n watcher.addListener( Collection.Events.Remove, Listeners.CollectionRemove( this, watcher ) );\n watcher.addListener( Collection.Events.Removes, Listeners.CollectionRemoves( this, watcher ) );\n watcher.addListener( Collection.Events.Cleared, Listeners.CollectionCleared( this, watcher ) );\n\n return watcher;\n },\n\n unwatch: function(object)\n {\n if ( object )\n {\n var watcher = this.getSessionWatch( object );\n\n if ( watcher )\n {\n watcher.destroy( this );\n }\n }\n },\n\n saveModel: function(model)\n {\n var watcher = this.getSessionWatch( model );\n\n if ( watcher && !watcher.save )\n {\n var key = model.$key();\n var db = model.$db;\n\n if ( db.models.has( key ) )\n {\n db.trigger( Database.Events.ModelUpdated, [model] );\n\n model.$trigger( Model.Events.UpdateAndSave );\n }\n else\n {\n db.models.put( key, model );\n db.trigger( Database.Events.ModelAdded, [model] );\n db.updated();\n\n model.$trigger( Model.Events.CreateAndSave );\n }\n\n watcher.save = true;\n }\n },\n\n removeModel: function(model)\n {\n var watcher = this.getSessionWatch( model );\n\n if ( watcher )\n {\n model.$push();\n model.$status = Model.Status.RemovePending;\n model.$db.removeFromModels( model );\n\n this.removing.add( model );\n\n watcher.destroy( this );\n }\n }\n\n});\n\n\nfunction SessionWatch( key, object )\n{\n this.key = key;\n this.object = object;\n this.relations = false;\n this.parent = false;\n this.children = {};\n this.offs = [];\n this.save = false;\n}\n\naddMethods( SessionWatch.prototype,\n{\n\n setRelations: function(relations)\n {\n if ( isObject( relations ) )\n {\n if ( this.relations && !equals( this.relations, relations ) )\n {\n throw 'Changing already watched relations is not allowed.';\n }\n\n this.relations = relations;\n }\n },\n\n setSession: function(session)\n {\n var object = this.object;\n var objectSession = object.$session;\n\n if ( objectSession && objectSession !== session && !objectSession.isDestroyed() )\n {\n throw 'An object can only be watched by one live session at a time.';\n }\n\n object.$session = session;\n },\n\n setParent: function(parent)\n {\n this.parent = parent;\n\n if ( parent )\n {\n parent.children[ this.key ] = this;\n }\n },\n\n addListener: function(eventName, listener)\n {\n var object = this.object;\n var off = noop;\n\n if ( object.$on )\n {\n off = object.$on( eventName, listener );\n }\n else if ( object.on )\n {\n off = object.on( eventName, listener );\n }\n\n this.offs.push( off );\n },\n\n destroy: function(session)\n {\n var offs = this.offs;\n var object = this.object;\n\n for (var i = 0; i < offs.length; i++)\n {\n offs[ i ]();\n }\n\n session.watching.remove( this.key );\n\n this.destroyChildren( session );\n\n object.$session = null;\n\n this.parent = null;\n this.offs.length = 0;\n this.save = false;\n },\n\n destroyChildren: function(session)\n {\n var children = this.children;\n\n for (var childKey in children)\n {\n children[ childKey ].destroy( session );\n }\n\n this.children = {};\n }\n\n});\n\n\n Rekord.Session = Session;\n Rekord.SessionWatch = SessionWatch;\n\n})(this, this.Rekord);\n"],"sourceRoot":"/source/"} \ No newline at end of file +{"version":3,"sources":["rekord-session.min.js"],"names":["global","Rekord","undefined","Session","this","status","Status","Active","watching","Map","removing","unwatched","validationRequired","SessionWatch","key","object","state","relations","parent","children","offs","save","cascade","searchAny","map","defaultResult","callback","context","watchers","values","i","length","watcher","result","call","searchModels","Model","Promise","Database","Collection","ModelCollection","RelationHasOne","Relations","hasOne","RelationBelongsTo","belongsTo","Cascade","isObject","isNumber","uuid","equals","noop","addMethods","replaceMethod","keyParser","createParser","Listeners","RelationUpdate","session","related","property","relator","relation","isDestroyed","lastRelated","isWatching","unwatch","watch","CollectionAdd","collection","added","CollectionAdds","CollectionRemove","removed","CollectionRemoves","CollectionReset","moveChildren","CollectionCleared","prototype","$save","setProperties","setValue","fakeIt","$session","isActive","$isDeleted","debug","Debugs","SAVE_DELETED","$db","resolve","arguments","$set","saveModel","apply","$remove","ignoreExists","isSaving","$exists","removeModel","Saving","Disabled","Destroyed","hasChanges","checkSavedOnly","size","unwatchedChanges","model","$hasChanges","watchedChanges","getChanged","out","target","push","validate","stopAtInvalid","valid","Validation","$validate","setValidationRequired","required","fullValidate","reject","sessionPromise","savePromise","singularity","handleSave","success","onSaveSuccess","executeSave","executeRemove","executeUnwatchedSave","$isSaved","models","remove","$key","afterSave","$status","RemovePending","Synced","put","afterRemove","afterUnwatchSave","saveState","destroyReferences","destroy","onSaveSuccessUnwatched","reset","discard","discardRemove","discardUnwatched","discardSave","removeFromModels","restoreState","$updated","reattach","disable","enable","isEnabled","isDisabled","watches","remover","getSessionKey","create","$uid","$sessionKey","getSessionWatch","get","getAnyWatch","getRemoveWatch","has","isUnwatched","isRemoved","hasWatched","watchMany","setRelations","setSession","setParent","value","watchCollection","$getRelation","addListener","Events","each","Add","Adds","Reset","Remove","Removes","Cleared","moveTo","addCascade","db","trigger","ModelUpdated","$trigger","UpdateAndSave","ModelAdded","updated","CreateAndSave","objectSession","eventName","listener","off","$on","on","override","oldState","$savedState","$push","relationName","pluck","relationsWatched","relationsSnapshot","clearKey","cascadeRemove","None","$decode","snapshot","removeListeners","childKey"],"mappings":"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","file":"rekord-session.min.js","sourcesContent":["/* rekord-session 1.4.1 - Adds mass changes & discards to Rekord by Philip Diffenderfer */\n(function(global, Rekord, undefined)\n{\n var Map = Rekord.Map;\n var Model = Rekord.Model;\n var Promise = Rekord.Promise;\n var Database = Rekord.Database;\n var Collection = Rekord.Collection;\n var ModelCollection = Rekord.ModelCollection;\n var RelationHasOne = Rekord.Relations.hasOne;\n var RelationBelongsTo = Rekord.Relations.belongsTo;\n var Cascade = Rekord.Cascade;\n\n var isObject = Rekord.isObject;\n var isNumber = Rekord.isNumber;\n var uuid = Rekord.uuid;\n var equals = Rekord.equals;\n var noop = Rekord.noop;\n\n var addMethods = Rekord.addMethods;\n var replaceMethod = Rekord.replaceMethod;\n\n var keyParser = Rekord.createParser('$key()');\n\nvar Listeners = {\n\n RelationUpdate: function(session, watcher, parent, related, property)\n {\n return function onRelationUpdate(relator, relation)\n {\n if ( session.isDestroyed() )\n {\n return;\n }\n\n if ( relation.lastRelated && session.isWatching( relation.lastRelated ) )\n {\n session.unwatch( relation.lastRelated );\n }\n\n if ( relation.related && !session.isWatching( relation.related ) )\n {\n session.watch( relation.related, watcher.relations[ property ], watcher );\n }\n };\n },\n\n CollectionAdd: function(session, watcher)\n {\n return function onAdd(collection, added)\n {\n if ( session.isDestroyed() )\n {\n return;\n }\n\n session.watch( added, watcher.relations, watcher );\n };\n },\n\n CollectionAdds: function(session, watcher)\n {\n return function onAdds(collection, added)\n {\n if ( session.isDestroyed() )\n {\n return;\n }\n\n for (var i = 0; i < added.length; i++)\n {\n session.watch( added[ i ], watcher.relations, watcher );\n }\n };\n },\n\n CollectionRemove: function(session, watcher)\n {\n return function onRemove(collection, removed)\n {\n if ( session.isDestroyed() )\n {\n return;\n }\n\n session.unwatch( removed );\n };\n },\n\n CollectionRemoves: function(session, watcher)\n {\n return function onRemoves(collection, removed)\n {\n if ( session.isDestroyed() )\n {\n return;\n }\n\n for (var i = 0; i < removed.length; i++)\n {\n session.unwatch( removed[ i ] );\n }\n };\n },\n\n CollectionReset: function(session, watcher)\n {\n return function onReset(collection)\n {\n if ( session.isDestroyed() )\n {\n return;\n }\n\n watcher.moveChildren( session.unwatched );\n\n for (var i = 0; i < collection.length; i++)\n {\n session.watch( collection[ i ], watcher.relations, watcher );\n }\n };\n },\n\n CollectionCleared: function(session, watcher)\n {\n return function onCleared(collection)\n {\n if ( session.isDestroyed() )\n {\n return;\n }\n\n watcher.moveChildren( session.unwatched );\n };\n }\n\n};\n\n\nreplaceMethod( Model.prototype, '$save', function($save)\n{\n return function(setProperties, setValue, cascade)\n {\n var fakeIt = this.$session && this.$session.isActive();\n\n if ( this.$isDeleted() )\n {\n Rekord.debug( Rekord.Debugs.SAVE_DELETED, this.$db, this );\n\n return Promise.resolve( this );\n }\n\n if ( fakeIt )\n {\n var cascade =\n (arguments.length === 3 ? cascade :\n (arguments.length === 2 && isObject( setProperties ) && isNumber( setValue ) ? setValue :\n (arguments.length === 1 && isNumber( setProperties ) ? setProperties : this.$db.cascade ) ) );\n\n this.$set( setProperties, setValue );\n\n this.$session.saveModel( this, cascade );\n\n return Promise.resolve( this );\n }\n\n return $save.apply( this, arguments );\n };\n});\n\nreplaceMethod( Model.prototype, '$remove', function($remove)\n{\n return function(cascade)\n {\n var ignoreExists = this.$session && this.$session.isSaving();\n var fakeIt = this.$session && this.$session.isActive();\n\n if ( !this.$exists() && !ignoreExists )\n {\n return Promise.resolve( this );\n }\n\n if ( fakeIt )\n {\n this.$session.removeModel( this, cascade );\n\n return Promise.resolve( this );\n }\n\n return $remove.apply( this, arguments );\n };\n});\n\n\n\n\nfunction Session()\n{\n this.status = Session.Status.Active;\n this.watching = new Map();\n this.removing = new Map();\n this.unwatched = new Map();\n this.validationRequired = false;\n}\n\nSession.Status =\n{\n Active: 'active',\n\n Saving: 'saving',\n\n Disabled: 'disabled',\n\n Destroyed: 'destroyed'\n};\n\naddMethods( Session.prototype,\n{\n\n hasChanges: function(checkSavedOnly)\n {\n if (this.removing.size() > 0)\n {\n return true;\n }\n\n var unwatchedChanges = searchModels( this.unwatched, false, function(model, watcher)\n {\n if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() )\n {\n return true;\n }\n });\n\n if ( unwatchedChanges )\n {\n return true;\n }\n\n var watchedChanges = searchModels( this.watching, false, function(model, watcher)\n {\n if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() )\n {\n return true;\n }\n });\n\n return watchedChanges;\n },\n\n getChanged: function(checkSavedOnly, out)\n {\n var target = out || new Collection();\n\n target.push.apply( target, this.removing.values );\n\n searchModels( this.watching, null, function(model, watcher)\n {\n if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() )\n {\n target.push( model );\n }\n });\n\n searchModels( this.unwatched, null, function(model, watcher)\n {\n if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() )\n {\n target.push( model );\n }\n });\n\n return target;\n },\n\n validate: function(stopAtInvalid)\n {\n var valid = true;\n\n if ( Rekord.Validation )\n {\n searchModels( this.watching, true, function(model, watcher)\n {\n if ( model.$validate && !model.$validate() )\n {\n valid = false;\n\n if ( stopAtInvalid )\n {\n return false;\n }\n }\n });\n }\n\n return valid;\n },\n\n setValidationRequired: function(required)\n {\n this.validationRequired = required;\n },\n\n save: function(fullValidate)\n {\n if ( this.status !== Session.Status.Active )\n {\n return Promise.reject( this );\n }\n\n if ( this.validationRequired && !this.validate( !fullValidate ) )\n {\n return Promise.reject( this );\n }\n\n var sessionPromise = new Promise();\n\n var savePromise = Promise.singularity( sessionPromise, this, this.handleSave );\n\n sessionPromise.resolve( this );\n\n savePromise.success( this.onSaveSuccess, this );\n\n return savePromise;\n },\n\n handleSave: function(singularity)\n {\n this.status = Session.Status.Saving;\n\n searchModels( this.watching, true, this.executeSave, this );\n\n searchModels( this.removing, true, this.executeRemove, this );\n\n searchAny( this.unwatched, true, this.executeUnwatchedSave, this );\n\n this.status = Session.Status.Active;\n },\n\n executeSave: function(model, watcher)\n {\n if ( watcher.save )\n {\n // Remove it so $save processes normally\n if ( !model.$isSaved() )\n {\n model.$db.models.remove( model.$key() );\n }\n\n model.$save( watcher.cascade ).success( this.afterSave( watcher ) );\n }\n },\n\n executeRemove: function(model, watcher)\n {\n if ( model.$status === Model.Status.RemovePending )\n {\n // Add it back so $remove processes normally\n model.$status = Model.Status.Synced;\n model.$db.models.put( model.$key(), model );\n\n model.$remove( watcher.cascade ).success( this.afterRemove( watcher, this ) );\n }\n },\n\n executeUnwatchedSave: function(model, watcher)\n {\n if ( watcher.save )\n {\n if ( !model.$isSaved() )\n {\n model.$db.models.remove( model.$key() );\n }\n\n model.$save( watcher.cascade ).success( this.afterUnwatchSave( watcher ) );\n }\n },\n\n afterSave: function(watcher)\n {\n return function onSave()\n {\n watcher.saveState( true );\n watcher.save = false;\n };\n },\n\n afterRemove: function(watcher, session)\n {\n return function onRemove()\n {\n session.removing.remove( watcher.key );\n watcher.destroyReferences();\n };\n },\n\n afterUnwatchSave: function(watcher)\n {\n return function onSave()\n {\n watcher.destroy();\n };\n },\n\n onSaveSuccess: function()\n {\n searchAny( this.unwatched, true, this.onSaveSuccessUnwatched, this );\n\n this.removing.reset();\n this.unwatched.reset();\n },\n\n onSaveSuccessUnwatched: function(model, watcher)\n {\n watcher.destroy();\n },\n\n discard: function()\n {\n searchModels( this.removing, true, this.discardRemove, this );\n\n searchAny( this.unwatched, true, this.discardUnwatched, this );\n\n searchModels( this.watching, true, this.discardSave, this );\n\n this.removing.reset();\n this.unwatched.reset();\n\n return this;\n },\n\n discardSave: function(model, watcher)\n {\n if ( watcher.save )\n {\n watcher.save = false;\n\n if ( !model.$isSaved() )\n {\n model.$db.removeFromModels( model );\n }\n }\n\n watcher.restoreState();\n\n model.$updated();\n },\n\n discardRemove: function(model, watcher)\n {\n if ( model.$status === Model.Status.RemovePending )\n {\n model.$status = Model.Status.Synced;\n model.$db.models.put( model.$key(), model );\n\n watcher.reattach();\n }\n },\n\n discardUnwatched: function(model, watcher)\n {\n watcher.reattach();\n },\n\n disable: function()\n {\n if ( this.status === Session.Status.Active )\n {\n this.status = Session.Status.Disabled;\n }\n },\n\n enable: function()\n {\n if ( this.status === Session.Status.Disabled )\n {\n this.status = Session.Status.Active;\n }\n },\n\n isEnabled: function()\n {\n return this.status !== Session.Status.Disabled &&\n this.status !== Session.Status.Destroyed;\n },\n\n isActive: function()\n {\n return this.status === Session.Status.Active;\n },\n\n isDisabled: function()\n {\n return this.status === Session.Status.Disabled;\n },\n\n isSaving: function()\n {\n return this.status === Session.Status.Saving;\n },\n\n isDestroyed: function()\n {\n return this.status === Session.Status.Destroyed;\n },\n\n destroy: function()\n {\n if ( this.status !== Session.Status.Destroyed )\n {\n var watches = this.watching.values;\n\n for (var i = 0; i < watches.length; i++)\n {\n var watcher = watches[ i ];\n\n if ( !watcher.parent )\n {\n watcher.destroy();\n }\n }\n\n var removing = this.removing.values;\n\n for (var i = 0; i < removing.length; i++)\n {\n var remover = removing[ i ];\n\n remover.destroyReferences();\n }\n\n this.watching.reset();\n this.removing.reset();\n\n this.status = Session.Status.Destroyed;\n }\n },\n\n getSessionKey: function(object, create)\n {\n if ( object instanceof Model )\n {\n return object.$uid();\n }\n else if ( object instanceof ModelCollection )\n {\n if ( !object.$sessionKey && create )\n {\n object.$sessionKey = uuid();\n }\n\n return object.$sessionKey;\n }\n else if ( create )\n {\n throw 'The object provided cannot be watched by session.';\n }\n },\n\n getSessionWatch: function(object, create)\n {\n var key = this.getSessionKey( object, create );\n\n if ( key )\n {\n var watch = this.watching.get( key );\n\n if ( !watch && create )\n {\n watch = this.unwatched.get( key );\n\n this.unwatched.remove( key );\n\n if ( watch )\n {\n this.watching.put( key, watch );\n }\n }\n\n if ( !watch && create )\n {\n watch = new SessionWatch( key, object );\n\n this.watching.put( key, watch );\n }\n\n return watch;\n }\n },\n\n getAnyWatch: function(object)\n {\n var key = this.getSessionKey( object );\n var watcher = null;\n\n if ( key )\n {\n watcher = this.watching.get( key );\n\n if ( !watcher )\n {\n watcher = this.unwatched.get( key );\n }\n }\n\n return watcher;\n },\n\n getRemoveWatch: function(object)\n {\n var key = this.getSessionKey( object );\n\n if ( key )\n {\n return this.removing.get( key );\n }\n },\n\n isWatching: function(object)\n {\n var key = this.getSessionKey( object );\n\n return key && this.watching.has( key );\n },\n\n isUnwatched: function(object)\n {\n var key = this.getSessionKey( object );\n\n return key && this.unwatched.has( key );\n },\n\n isRemoved: function(object)\n {\n var key = this.getSessionKey( object );\n\n return key && this.removing.has( key );\n },\n\n hasWatched: function(object)\n {\n var key = this.getSessionKey( object );\n\n return key && ( this.watching.has( key ) || this.removing.has( key ) || this.unwatched.has( key ) );\n },\n\n watchMany: function(models, relations)\n {\n var watchers = [];\n\n for (var i = 0; i < models.length; i++)\n {\n watchers.push( this.watch( models[ i ], relations ) );\n }\n\n return watchers;\n },\n\n watch: function(model, relations, parent)\n {\n var watcher = this.getSessionWatch( model, true );\n\n watcher.setRelations( relations );\n watcher.setSession( this );\n watcher.setParent( parent );\n watcher.saveState();\n\n if ( isObject( relations ) )\n {\n for (var property in relations)\n {\n var value = model[ property ];\n\n if ( value instanceof Model )\n {\n this.watch( value, relations[ property ], watcher );\n }\n else if ( value instanceof ModelCollection )\n {\n this.watchCollection( value, relations[ property ], watcher );\n }\n\n var relation = model.$getRelation( property );\n\n if ( relation instanceof RelationHasOne || relation instanceof RelationBelongsTo )\n {\n watcher.addListener( Model.Events.RelationUpdate, Listeners.RelationUpdate( this, watcher, model, value, property ) );\n }\n }\n }\n\n return watcher;\n },\n\n watchCollection: function(collection, relations, parent)\n {\n var watcher = this.getSessionWatch( collection, true );\n\n watcher.setRelations( relations );\n watcher.setSession( this );\n watcher.setParent( parent );\n\n collection.each(function(model)\n {\n this.watch( model, relations, watcher );\n\n }, this );\n\n watcher.addListener( Collection.Events.Add, Listeners.CollectionAdd( this, watcher ) );\n watcher.addListener( Collection.Events.Adds, Listeners.CollectionAdds( this, watcher ) );\n watcher.addListener( Collection.Events.Reset, Listeners.CollectionReset( this, watcher ) );\n watcher.addListener( Collection.Events.Remove, Listeners.CollectionRemove( this, watcher ) );\n watcher.addListener( Collection.Events.Removes, Listeners.CollectionRemoves( this, watcher ) );\n watcher.addListener( Collection.Events.Cleared, Listeners.CollectionCleared( this, watcher ) );\n\n return watcher;\n },\n\n unwatch: function(object)\n {\n if ( object )\n {\n var watcher = this.getSessionWatch( object );\n\n if ( watcher )\n {\n watcher.moveTo( this.unwatched );\n }\n }\n },\n\n saveModel: function(model, cascade)\n {\n var watcher = this.getAnyWatch( model );\n\n if ( watcher )\n {\n watcher.addCascade( cascade );\n\n if ( !watcher.save )\n {\n var key = model.$key();\n var db = model.$db;\n\n if ( db.models.has( key ) )\n {\n db.trigger( Database.Events.ModelUpdated, [model] );\n\n model.$trigger( Model.Events.UpdateAndSave );\n }\n else\n {\n db.models.put( key, model );\n db.trigger( Database.Events.ModelAdded, [model] );\n db.updated();\n\n model.$trigger( Model.Events.CreateAndSave );\n }\n\n watcher.save = true;\n }\n }\n },\n\n removeModel: function(model, cascade)\n {\n var watcher = this.getAnyWatch( model );\n\n if ( watcher )\n {\n watcher.addCascade( cascade );\n watcher.moveTo( this.removing );\n\n model.$status = Model.Status.RemovePending;\n model.$db.removeFromModels( model );\n }\n else\n {\n var removed = this.getRemoveWatch( model );\n\n if ( removed )\n {\n removed.addCascade( cascade );\n }\n }\n }\n\n});\n\n\nfunction SessionWatch( key, object )\n{\n this.key = key;\n this.object = object;\n this.state = null;\n this.relations = false;\n this.parent = false;\n this.children = {};\n this.offs = [];\n this.save = false;\n this.cascade = undefined;\n this.state = null;\n}\n\naddMethods( SessionWatch.prototype,\n{\n\n setRelations: function(relations)\n {\n if ( isObject( relations ) )\n {\n if ( this.relations && !equals( this.relations, relations ) )\n {\n throw 'Changing already watched relations is not allowed.';\n }\n\n this.relations = relations;\n }\n },\n\n setSession: function(session)\n {\n var object = this.object;\n var objectSession = object.$session;\n\n if ( objectSession && objectSession !== session && !objectSession.isDestroyed() )\n {\n throw 'An object can only be watched by one live session at a time.';\n }\n\n object.$session = session;\n },\n\n setParent: function(parent)\n {\n this.parent = parent;\n\n if ( parent )\n {\n parent.children[ this.key ] = this;\n }\n },\n\n addListener: function(eventName, listener)\n {\n var object = this.object;\n var off = noop;\n\n if ( object.$on )\n {\n off = object.$on( eventName, listener );\n }\n else if ( object.on )\n {\n off = object.on( eventName, listener );\n }\n\n this.offs.push( off );\n },\n\n addCascade: function(cascade)\n {\n if ( isNumber( cascade ) )\n {\n if ( this.cascade === undefined )\n {\n this.cascade = 0;\n }\n\n this.cascade = this.cascade | cascade;\n }\n },\n\n saveState: function(override)\n {\n if ( this.state && !override )\n {\n return;\n }\n\n var model = this.object;\n var oldState = model.$savedState;\n\n model.$push();\n\n var relations = this.relations;\n var state = model.$savedState;\n\n if ( isObject( relations ) )\n {\n for (var relationName in relations)\n {\n var value = model[ relationName ];\n\n if ( value instanceof Model )\n {\n state[ relationName ] = value.$key();\n }\n else if ( value instanceof ModelCollection )\n {\n state[ relationName ] = value.pluck( keyParser );\n }\n else\n {\n state[ relationName ] = null;\n }\n }\n }\n\n this.state = state;\n\n model.$savedState = oldState;\n },\n\n restoreState: function()\n {\n var model = this.object;\n var state = this.state;\n\n if ( isObject( state ) )\n {\n var relations = model.$db.relations;\n var relationsWatched = this.relations;\n var relationsSnapshot = {};\n\n for (var relationName in relationsWatched)\n {\n var relation = relations[ relationName ];\n\n relationsSnapshot[ relationName ] = {\n clearKey: relation.clearKey,\n cascadeRemove: relation.cascadeRemove,\n cascade: relation.cascade\n };\n\n relation.clearKey = false;\n relation.cascadeRemove = Cascade.None;\n relation.cascade = Cascade.None;\n }\n\n model.$set( state, undefined, true, true );\n model.$decode();\n\n for (var relationName in relationsWatched)\n {\n var relation = relations[ relationName ];\n var snapshot = relationsSnapshot[ relationName ];\n\n relation.clearKey = snapshot.clearKey;\n relation.cascadeRemove = snapshot.cascadeRemove;\n relation.cascade = snapshot.cascade;\n }\n }\n },\n\n removeListeners: function()\n {\n var offs = this.offs;\n\n for (var i = 0; i < offs.length; i++)\n {\n offs[ i ]();\n }\n\n offs.length = 0;\n },\n\n moveTo: function(target)\n {\n var session = this.object.$session;\n\n this.removeListeners();\n this.moveChildren( target );\n\n if ( this.parent )\n {\n delete this.parent.children[ this.key ];\n }\n\n session.watching.remove( this.key );\n session.unwatched.remove( this.key );\n session.removing.remove( this.key );\n\n target.put( this.key, this );\n },\n\n reattach: function()\n {\n var session = this.object.$session;\n\n session.removing.remove( this.key );\n session.unwatched.remove( this.key );\n session.watching.put( this.key, this );\n\n session.watch( this.object, this.relations, this.parent );\n },\n\n destroy: function()\n {\n var children = this.children;\n\n this.children = {};\n this.removeListeners();\n this.destroyReferences();\n\n for (var childKey in children)\n {\n children[ childKey ].destroy();\n }\n },\n\n destroyReferences: function()\n {\n var session = this.object.$session;\n\n session.watching.remove( this.key );\n session.removing.remove( this.key );\n session.unwatched.remove( this.key );\n\n this.object.$session = null;\n this.state = null;\n\n this.parent = null;\n this.save = false;\n this.cascade = undefined;\n },\n\n moveChildren: function(target)\n {\n var children = this.children;\n\n for (var childKey in children)\n {\n children[ childKey ].moveTo( target );\n }\n }\n\n});\n\n\nfunction searchAny(map, defaultResult, callback, context)\n{\n var watchers = map.values;\n\n for (var i = watchers.length - 1; i >= 0; i--)\n {\n var watcher = watchers[ i ];\n var result = callback.call( context, watcher.object, watcher );\n\n if ( result !== undefined )\n {\n return result;\n }\n }\n\n return defaultResult;\n}\n\nfunction searchModels(map, defaultResult, callback, context)\n{\n var watchers = map.values;\n\n for (var i = watchers.length - 1; i >= 0; i--)\n {\n var watcher = watchers[ i ];\n\n if ( watcher.object instanceof Model )\n {\n var result = callback.call( context, watcher.object, watcher );\n\n if ( result !== undefined )\n {\n return result;\n }\n }\n }\n\n return defaultResult;\n}\n\n\n Rekord.Session = Session;\n Rekord.SessionWatch = SessionWatch;\n\n})(this, this.Rekord);\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/package.json b/package.json index c9858cd..d02938d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rekord-session", - "version": "1.4.0", + "version": "1.4.1", "description": "Adds mass changes & discards to Rekord", "author": "Philip Diffenderfer", "license": "MIT", diff --git a/src/header.js b/src/header.js index a68d4d7..84c7e5b 100644 --- a/src/header.js +++ b/src/header.js @@ -8,11 +8,15 @@ var ModelCollection = Rekord.ModelCollection; var RelationHasOne = Rekord.Relations.hasOne; var RelationBelongsTo = Rekord.Relations.belongsTo; + var Cascade = Rekord.Cascade; var isObject = Rekord.isObject; + var isNumber = Rekord.isNumber; var uuid = Rekord.uuid; var equals = Rekord.equals; var noop = Rekord.noop; var addMethods = Rekord.addMethods; var replaceMethod = Rekord.replaceMethod; + + var keyParser = Rekord.createParser('$key()'); diff --git a/src/lib/Listeners.js b/src/lib/Listeners.js index e898e49..fccc28d 100644 --- a/src/lib/Listeners.js +++ b/src/lib/Listeners.js @@ -2,24 +2,28 @@ var Listeners = { RelationUpdate: function(session, watcher, parent, related, property) { - return function(relator, relation) + return function onRelationUpdate(relator, relation) { if ( session.isDestroyed() ) { return; } - if ( session.isWatching( relation.lastRelated ) && !session.isWatching( session.related ) ) + if ( relation.lastRelated && session.isWatching( relation.lastRelated ) ) { session.unwatch( relation.lastRelated ); - session.watch( session.related, watcher.relations[ property ], watcher ); + } + + if ( relation.related && !session.isWatching( relation.related ) ) + { + session.watch( relation.related, watcher.relations[ property ], watcher ); } }; }, CollectionAdd: function(session, watcher) { - return function (collection, added) + return function onAdd(collection, added) { if ( session.isDestroyed() ) { @@ -32,7 +36,7 @@ var Listeners = { CollectionAdds: function(session, watcher) { - return function (collection, added) + return function onAdds(collection, added) { if ( session.isDestroyed() ) { @@ -48,7 +52,7 @@ var Listeners = { CollectionRemove: function(session, watcher) { - return function (collection, removed) + return function onRemove(collection, removed) { if ( session.isDestroyed() ) { @@ -61,7 +65,7 @@ var Listeners = { CollectionRemoves: function(session, watcher) { - return function (collection, removed) + return function onRemoves(collection, removed) { if ( session.isDestroyed() ) { @@ -77,14 +81,14 @@ var Listeners = { CollectionReset: function(session, watcher) { - return function (collection) + return function onReset(collection) { if ( session.isDestroyed() ) { return; } - watcher.destroyChildren(); + watcher.moveChildren( session.unwatched ); for (var i = 0; i < collection.length; i++) { @@ -95,14 +99,14 @@ var Listeners = { CollectionCleared: function(session, watcher) { - return function (collection) + return function onCleared(collection) { if ( session.isDestroyed() ) { return; } - watcher.destroyChildren(); + watcher.moveChildren( session.unwatched ); }; } diff --git a/src/lib/Model.js b/src/lib/Model.js index 9d4789d..0d6f2cd 100644 --- a/src/lib/Model.js +++ b/src/lib/Model.js @@ -14,9 +14,14 @@ replaceMethod( Model.prototype, '$save', function($save) if ( fakeIt ) { + var cascade = + (arguments.length === 3 ? cascade : + (arguments.length === 2 && isObject( setProperties ) && isNumber( setValue ) ? setValue : + (arguments.length === 1 && isNumber( setProperties ) ? setProperties : this.$db.cascade ) ) ); + this.$set( setProperties, setValue ); - this.$session.saveModel( this ); + this.$session.saveModel( this, cascade ); return Promise.resolve( this ); } @@ -39,7 +44,7 @@ replaceMethod( Model.prototype, '$remove', function($remove) if ( fakeIt ) { - this.$session.removeModel( this ); + this.$session.removeModel( this, cascade ); return Promise.resolve( this ); } diff --git a/src/lib/Session.js b/src/lib/Session.js index 1e71f62..8ab29ce 100644 --- a/src/lib/Session.js +++ b/src/lib/Session.js @@ -5,7 +5,8 @@ function Session() { this.status = Session.Status.Active; this.watching = new Map(); - this.removing = new Collection(); + this.removing = new Map(); + this.unwatched = new Map(); this.validationRequired = false; } @@ -25,33 +26,58 @@ addMethods( Session.prototype, hasChanges: function(checkSavedOnly) { - if (this.removing.length > 0) + if (this.removing.size() > 0) { return true; } - return this.searchModels( false, function(model, watcher) + var unwatchedChanges = searchModels( this.unwatched, false, function(model, watcher) { if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() ) { return true; } }); + + if ( unwatchedChanges ) + { + return true; + } + + var watchedChanges = searchModels( this.watching, false, function(model, watcher) + { + if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() ) + { + return true; + } + }); + + return watchedChanges; }, getChanged: function(checkSavedOnly, out) { var target = out || new Collection(); - target.push.apply( target, this.removing ); + target.push.apply( target, this.removing.values ); + + searchModels( this.watching, null, function(model, watcher) + { + if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() ) + { + target.push( model ); + } + }); - return this.searchModels( target, function(model, watcher) + searchModels( this.unwatched, null, function(model, watcher) { if ( (!checkSavedOnly || watcher.save) && model.$hasChanges() ) { target.push( model ); } }); + + return target; }, validate: function(stopAtInvalid) @@ -60,7 +86,7 @@ addMethods( Session.prototype, if ( Rekord.Validation ) { - this.searchModels( true, function(model, watcher) + searchModels( this.watching, true, function(model, watcher) { if ( model.$validate && !model.$validate() ) { @@ -94,16 +120,26 @@ addMethods( Session.prototype, return Promise.reject( this ); } - return Promise.singularity( Promise.resolve( this ), this, this.handleSave ); + var sessionPromise = new Promise(); + + var savePromise = Promise.singularity( sessionPromise, this, this.handleSave ); + + sessionPromise.resolve( this ); + + savePromise.success( this.onSaveSuccess, this ); + + return savePromise; }, - handleSave: function() + handleSave: function(singularity) { this.status = Session.Status.Saving; - this.searchModels( true, this.executeSave ); + searchModels( this.watching, true, this.executeSave, this ); - this.removing.each( this.executeRemove, this ); + searchModels( this.removing, true, this.executeRemove, this ); + + searchAny( this.unwatched, true, this.executeUnwatchedSave, this ); this.status = Session.Status.Active; }, @@ -118,11 +154,11 @@ addMethods( Session.prototype, model.$db.models.remove( model.$key() ); } - model.$save().success( this.afterSave( watcher, model) ); + model.$save( watcher.cascade ).success( this.afterSave( watcher ) ); } }, - executeRemove: function(model) + executeRemove: function(model, watcher) { if ( model.$status === Model.Status.RemovePending ) { @@ -130,33 +166,72 @@ addMethods( Session.prototype, model.$status = Model.Status.Synced; model.$db.models.put( model.$key(), model ); - model.$remove().success( this.afterRemove( this, model ) ); + model.$remove( watcher.cascade ).success( this.afterRemove( watcher, this ) ); } }, - afterSave: function(watcher, model) + executeUnwatchedSave: function(model, watcher) + { + if ( watcher.save ) + { + if ( !model.$isSaved() ) + { + model.$db.models.remove( model.$key() ); + } + + model.$save( watcher.cascade ).success( this.afterUnwatchSave( watcher ) ); + } + }, + + afterSave: function(watcher) { return function onSave() { - model.$push(); + watcher.saveState( true ); watcher.save = false; }; }, - afterRemove: function(session, model) + afterRemove: function(watcher, session) { return function onRemove() { - session.removing.remove( model ); + session.removing.remove( watcher.key ); + watcher.destroyReferences(); + }; + }, + + afterUnwatchSave: function(watcher) + { + return function onSave() + { + watcher.destroy(); }; }, + onSaveSuccess: function() + { + searchAny( this.unwatched, true, this.onSaveSuccessUnwatched, this ); + + this.removing.reset(); + this.unwatched.reset(); + }, + + onSaveSuccessUnwatched: function(model, watcher) + { + watcher.destroy(); + }, + discard: function() { - this.searchModels( true, this.discardSave ); + searchModels( this.removing, true, this.discardRemove, this ); + + searchAny( this.unwatched, true, this.discardUnwatched, this ); + + searchModels( this.watching, true, this.discardSave, this ); - this.removing.each( this.discardRemove ); - this.removing.clear(); + this.removing.reset(); + this.unwatched.reset(); return this; }, @@ -165,30 +240,35 @@ addMethods( Session.prototype, { if ( watcher.save ) { + watcher.save = false; + if ( !model.$isSaved() ) { model.$db.removeFromModels( model ); } - else - { - model.$pop(); - } - - watcher.save = false; } + + watcher.restoreState(); + + model.$updated(); }, - discardRemove: function(model) + discardRemove: function(model, watcher) { if ( model.$status === Model.Status.RemovePending ) { - model.$pop(); model.$status = Model.Status.Synced; model.$db.models.put( model.$key(), model ); - model.$updated(); + + watcher.reattach(); } }, + discardUnwatched: function(model, watcher) + { + watcher.reattach(); + }, + disable: function() { if ( this.status === Session.Status.Active ) @@ -231,29 +311,6 @@ addMethods( Session.prototype, return this.status === Session.Status.Destroyed; }, - searchModels: function(defaultResult, callback, context) - { - var callbackContext = context || this; - var watches = this.watching.values; - - for (var i = 0; i < watches.length; i++) - { - var watcher = watches[ i ]; - - if ( watcher.object instanceof Model ) - { - var result = callback.call( callbackContext, watcher.object, watcher ); - - if ( result !== undefined ) - { - return result; - } - } - } - - return defaultResult; - }, - destroy: function() { if ( this.status !== Session.Status.Destroyed ) @@ -266,12 +323,21 @@ addMethods( Session.prototype, if ( !watcher.parent ) { - watcher.destroy( this ); + watcher.destroy(); } } + var removing = this.removing.values; + + for (var i = 0; i < removing.length; i++) + { + var remover = removing[ i ]; + + remover.destroyReferences(); + } + this.watching.reset(); - this.removing.clear(); + this.removing.reset(); this.status = Session.Status.Destroyed; } @@ -292,7 +358,7 @@ addMethods( Session.prototype, return object.$sessionKey; } - else + else if ( create ) { throw 'The object provided cannot be watched by session.'; } @@ -306,6 +372,18 @@ addMethods( Session.prototype, { var watch = this.watching.get( key ); + if ( !watch && create ) + { + watch = this.unwatched.get( key ); + + this.unwatched.remove( key ); + + if ( watch ) + { + this.watching.put( key, watch ); + } + } + if ( !watch && create ) { watch = new SessionWatch( key, object ); @@ -317,11 +395,72 @@ addMethods( Session.prototype, } }, + getAnyWatch: function(object) + { + var key = this.getSessionKey( object ); + var watcher = null; + + if ( key ) + { + watcher = this.watching.get( key ); + + if ( !watcher ) + { + watcher = this.unwatched.get( key ); + } + } + + return watcher; + }, + + getRemoveWatch: function(object) + { + var key = this.getSessionKey( object ); + + if ( key ) + { + return this.removing.get( key ); + } + }, + isWatching: function(object) { var key = this.getSessionKey( object ); - return key !== false && this.watching.has( key ); + return key && this.watching.has( key ); + }, + + isUnwatched: function(object) + { + var key = this.getSessionKey( object ); + + return key && this.unwatched.has( key ); + }, + + isRemoved: function(object) + { + var key = this.getSessionKey( object ); + + return key && this.removing.has( key ); + }, + + hasWatched: function(object) + { + var key = this.getSessionKey( object ); + + return key && ( this.watching.has( key ) || this.removing.has( key ) || this.unwatched.has( key ) ); + }, + + watchMany: function(models, relations) + { + var watchers = []; + + for (var i = 0; i < models.length; i++) + { + watchers.push( this.watch( models[ i ], relations ) ); + } + + return watchers; }, watch: function(model, relations, parent) @@ -331,8 +470,7 @@ addMethods( Session.prototype, watcher.setRelations( relations ); watcher.setSession( this ); watcher.setParent( parent ); - - model.$push(); + watcher.saveState(); if ( isObject( relations ) ) { @@ -393,52 +531,64 @@ addMethods( Session.prototype, if ( watcher ) { - watcher.destroy( this ); + watcher.moveTo( this.unwatched ); } } }, - saveModel: function(model) + saveModel: function(model, cascade) { - var watcher = this.getSessionWatch( model ); + var watcher = this.getAnyWatch( model ); - if ( watcher && !watcher.save ) + if ( watcher ) { - var key = model.$key(); - var db = model.$db; + watcher.addCascade( cascade ); - if ( db.models.has( key ) ) + if ( !watcher.save ) { - db.trigger( Database.Events.ModelUpdated, [model] ); + var key = model.$key(); + var db = model.$db; - model.$trigger( Model.Events.UpdateAndSave ); - } - else - { - db.models.put( key, model ); - db.trigger( Database.Events.ModelAdded, [model] ); - db.updated(); + if ( db.models.has( key ) ) + { + db.trigger( Database.Events.ModelUpdated, [model] ); - model.$trigger( Model.Events.CreateAndSave ); - } + model.$trigger( Model.Events.UpdateAndSave ); + } + else + { + db.models.put( key, model ); + db.trigger( Database.Events.ModelAdded, [model] ); + db.updated(); + + model.$trigger( Model.Events.CreateAndSave ); + } - watcher.save = true; + watcher.save = true; + } } }, - removeModel: function(model) + removeModel: function(model, cascade) { - var watcher = this.getSessionWatch( model ); + var watcher = this.getAnyWatch( model ); if ( watcher ) { - model.$push(); + watcher.addCascade( cascade ); + watcher.moveTo( this.removing ); + model.$status = Model.Status.RemovePending; model.$db.removeFromModels( model ); + } + else + { + var removed = this.getRemoveWatch( model ); - this.removing.add( model ); - - watcher.destroy( this ); + if ( removed ) + { + removed.addCascade( cascade ); + } } } diff --git a/src/lib/SessionWatch.js b/src/lib/SessionWatch.js index d594229..b79dbf6 100644 --- a/src/lib/SessionWatch.js +++ b/src/lib/SessionWatch.js @@ -3,11 +3,14 @@ function SessionWatch( key, object ) { this.key = key; this.object = object; + this.state = null; this.relations = false; this.parent = false; this.children = {}; this.offs = []; this.save = false; + this.cascade = undefined; + this.state = null; } addMethods( SessionWatch.prototype, @@ -66,37 +69,181 @@ addMethods( SessionWatch.prototype, this.offs.push( off ); }, - destroy: function(session) + addCascade: function(cascade) + { + if ( isNumber( cascade ) ) + { + if ( this.cascade === undefined ) + { + this.cascade = 0; + } + + this.cascade = this.cascade | cascade; + } + }, + + saveState: function(override) + { + if ( this.state && !override ) + { + return; + } + + var model = this.object; + var oldState = model.$savedState; + + model.$push(); + + var relations = this.relations; + var state = model.$savedState; + + if ( isObject( relations ) ) + { + for (var relationName in relations) + { + var value = model[ relationName ]; + + if ( value instanceof Model ) + { + state[ relationName ] = value.$key(); + } + else if ( value instanceof ModelCollection ) + { + state[ relationName ] = value.pluck( keyParser ); + } + else + { + state[ relationName ] = null; + } + } + } + + this.state = state; + + model.$savedState = oldState; + }, + + restoreState: function() + { + var model = this.object; + var state = this.state; + + if ( isObject( state ) ) + { + var relations = model.$db.relations; + var relationsWatched = this.relations; + var relationsSnapshot = {}; + + for (var relationName in relationsWatched) + { + var relation = relations[ relationName ]; + + relationsSnapshot[ relationName ] = { + clearKey: relation.clearKey, + cascadeRemove: relation.cascadeRemove, + cascade: relation.cascade + }; + + relation.clearKey = false; + relation.cascadeRemove = Cascade.None; + relation.cascade = Cascade.None; + } + + model.$set( state, undefined, true, true ); + model.$decode(); + + for (var relationName in relationsWatched) + { + var relation = relations[ relationName ]; + var snapshot = relationsSnapshot[ relationName ]; + + relation.clearKey = snapshot.clearKey; + relation.cascadeRemove = snapshot.cascadeRemove; + relation.cascade = snapshot.cascade; + } + } + }, + + removeListeners: function() { var offs = this.offs; - var object = this.object; for (var i = 0; i < offs.length; i++) { offs[ i ](); } + offs.length = 0; + }, + + moveTo: function(target) + { + var session = this.object.$session; + + this.removeListeners(); + this.moveChildren( target ); + + if ( this.parent ) + { + delete this.parent.children[ this.key ]; + } + session.watching.remove( this.key ); + session.unwatched.remove( this.key ); + session.removing.remove( this.key ); - this.destroyChildren( session ); + target.put( this.key, this ); + }, - object.$session = null; + reattach: function() + { + var session = this.object.$session; + + session.removing.remove( this.key ); + session.unwatched.remove( this.key ); + session.watching.put( this.key, this ); + + session.watch( this.object, this.relations, this.parent ); + }, + + destroy: function() + { + var children = this.children; + + this.children = {}; + this.removeListeners(); + this.destroyReferences(); + + for (var childKey in children) + { + children[ childKey ].destroy(); + } + }, + + destroyReferences: function() + { + var session = this.object.$session; + + session.watching.remove( this.key ); + session.removing.remove( this.key ); + session.unwatched.remove( this.key ); + + this.object.$session = null; + this.state = null; this.parent = null; - this.offs.length = 0; this.save = false; + this.cascade = undefined; }, - destroyChildren: function(session) + moveChildren: function(target) { var children = this.children; for (var childKey in children) { - children[ childKey ].destroy( session ); + children[ childKey ].moveTo( target ); } - - this.children = {}; } }); diff --git a/src/lib/search.js b/src/lib/search.js new file mode 100644 index 0000000..b5f06f9 --- /dev/null +++ b/src/lib/search.js @@ -0,0 +1,40 @@ + +function searchAny(map, defaultResult, callback, context) +{ + var watchers = map.values; + + for (var i = watchers.length - 1; i >= 0; i--) + { + var watcher = watchers[ i ]; + var result = callback.call( context, watcher.object, watcher ); + + if ( result !== undefined ) + { + return result; + } + } + + return defaultResult; +} + +function searchModels(map, defaultResult, callback, context) +{ + var watchers = map.values; + + for (var i = watchers.length - 1; i >= 0; i--) + { + var watcher = watchers[ i ]; + + if ( watcher.object instanceof Model ) + { + var result = callback.call( context, watcher.object, watcher ); + + if ( result !== undefined ) + { + return result; + } + } + } + + return defaultResult; +} diff --git a/test/base.js b/test/base.js index 3642ec2..91ef946 100644 --- a/test/base.js +++ b/test/base.js @@ -8,6 +8,77 @@ QUnit.config.reorder = false; // Extra Assertions +function assertRemoved(model) +{ + ok( model.$isDeleted(), model.$uid() + ' is deleted' ); + notOk( model.$exists(), model.$uid() + ' does not exist' ); + notOk( model.$db.models.has( model.$key() ), model.$uid() + ' is not in the database' ); + notOk( model.$db.store.map.has( model.$key() ), model.$uid() + ' is not in the store' ); + notOk( model.$db.rest.map.has( model.$key() ), model.$uid() + ' is not in the rest' ); +} + +function assertRemovedLocally(model) +{ + ok( model.$isDeleted(), model.$uid() + ' is deleted' ); + notOk( model.$exists(), model.$uid() + ' does not exist' ); + notOk( model.$db.models.has( model.$key() ), model.$uid() + ' is not in the database' ); + notOk( model.$db.store.map.has( model.$key() ), model.$uid() + ' is not in the store' ); + ok( model.$db.rest.map.has( model.$key() ), model.$uid() + ' is in the rest' ); +} + +function assertSaved(model, record) +{ + ok( model.$isSaved(), model.$uid() + ' is saved remotely' ); + ok( model.$isSavedLocally(), model.$uid() + ' is saved locally' ); + ok( model.$exists(), model.$uid() + ' exists' ); + ok( model.$db.models.has( model.$key() ), model.$uid() + ' is in the database' ); + notOk( model.$isDeleted(), model.$uid() + ' is not removed' ); + ok( model.$db.store.map.has( model.$key() ), model.$uid() + ' is in the store' ); + ok( model.$db.rest.map.has( model.$key() ), model.$uid() + ' is in the rest' ); +} + +function assertSavedLocally(model) +{ + notOk( model.$isSaved(), model.$uid() + ' is not saved remotely' ); + ok( model.$isSavedLocally(), model.$uid() + ' is saved locally' ); + ok( model.$exists(), model.$uid() + ' exists' ); + ok( model.$db.models.has( model.$key() ), model.$uid() + ' is in the database' ); + notOk( model.$isDeleted(), model.$uid() + ' is not removed' ); + ok( model.$db.store.map.has( model.$key() ), model.$uid() + ' is in the store' ); + notOk( model.$db.rest.map.has( model.$key() ), model.$uid() + ' is not in the rest' ); +} + +function assertNew(model) +{ + notOk( model.$isSaved(), model.$uid() + ' is not saved remotely' ); + notOk( model.$isSavedLocally(), model.$uid() + ' is not saved locally' ); + notOk( model.$exists(), model.$uid() + ' does not exist' ); + notOk( model.$db.models.has( model.$key() ), model.$uid() + ' is not in the database' ); + notOk( model.$isDeleted(), model.$uid() + ' is not removed' ); + notOk( model.$db.store.map.has( model.$key() ), model.$uid() + ' is not in the store' ); + notOk( model.$db.rest.map.has( model.$key() ), model.$uid() + ' is not in the rest' ); +} + +function assertStored(model, data) +{ + var stored = model.$db.store.map.get( model.$key() ); + + for (var prop in data) + { + deepEqual( data[ prop ], stored[ prop ], model.$uid() + ' ' + prop + ' stored' ); + } +} + +function assertRest(model, data) +{ + var saved = model.$db.rest.map.get( model.$key() ); + + for (var prop in data) + { + deepEqual( data[ prop ], saved[ prop ], model.$uid() + ' ' + prop + ' saved' ); + } +} + function isInstance(model, Class, message) { ok( model instanceof Class, message ); @@ -420,7 +491,6 @@ TestRest.prototype = wait( rest.delay, function() { rest.finish( success, failure, returnValue ); - }); } else diff --git a/test/cases/rekord-session.js b/test/cases/rekord-session.js index ce94f0d..411a2e6 100644 --- a/test/cases/rekord-session.js +++ b/test/cases/rekord-session.js @@ -579,8 +579,12 @@ test( 'complex discard', function(assert) } }); + ok( sess.isWatching( c0 ) ); + c0.$remove(); + notOk( sess.isWatching( c0 ) ); + strictEqual( ps.rules[0].group.conditions.length, 0 ); var c4 = Condition.create({ @@ -589,6 +593,7 @@ test( 'complex discard', function(assert) group_id: ps.rules[0].group.id }) + ok( sess.isWatching( c4 ) ); notOk( c4.$saved ); ok( c4, 'condition 3' ); strictEqual( ps.rules[0].group.conditions.length, 1 ); @@ -605,6 +610,7 @@ test( 'complex discard', function(assert) notOk( c4.$isSaved() ); notOk( c4.$saved ); notOk( sess.hasChanges() ); + notOk( sess.isWatching( c4 ) ); strictEqual( Condition.Database.rest.lastModel, null ); notStrictEqual( g0.$saved.name, 'group 1 a' ); strictEqual( g0.name, 'group 1'); @@ -656,18 +662,976 @@ test( 'complex discard', function(assert) deepEqual( result, expected ) }); -// TODO tests - -// relation update (hasOne & belongsTo) -// unrelate -// relate -// collection add -// collection adds -// collection remove -// collection removes -// collection reset -// collection clear +test( 'relation update belongsTo', function(assert) +{ + var prefix = 'relation_update_belongsTo_'; + + var TaskList = Rekord({ + name: prefix + 'list', + fields: ['name'] + }); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['list_id', 'name', 'done'], + belongsTo: { + list: { + model: TaskList, + local: 'list_id' + } + } + }); + + var l0 = TaskList.boot({id: 1, name: 'l0'}); + var l1 = TaskList.boot({id: 2, name: 'l0'}); + var t0 = Task.boot({id: 3, list_id: 1, name: 't0', done: false}); + + var sess = new Session(); + + sess.watch( t0, { list: true } ); + + ok( sess.isWatching( l0 ) ); + notOk( sess.isWatching( l1 ) ); + ok( l0.$isSaved() ); + ok( l1.$isSaved() ); + + t0.$save( 'list', l1 ); + + strictEqual( t0.list, l1 ); + strictEqual( t0.list_id, l1.id ); + notOk( sess.isWatching( l0 ) ); + ok( sess.isWatching( l1 ) ); + + sess.save(); + + ok( l0.$isSaved() ); + ok( l1.$isSaved() ); + strictEqual( t0.list, l1 ); + strictEqual( t0.list_id, l1.id ); + notOk( sess.isWatching( l0 ) ); + ok( sess.isWatching( l1 ) ); +}); + + +test( 'relation update belongsTo discard', function(assert) +{ + var prefix = 'relation_update_belongsTo_discard_'; + + var TaskList = Rekord({ + name: prefix + 'list', + fields: ['name'] + }); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['list_id', 'name', 'done'], + belongsTo: { + list: { + model: TaskList, + local: 'list_id' + } + } + }); + + var l0 = TaskList.boot({id: 1, name: 'l0'}); + var l1 = TaskList.boot({id: 2, name: 'l0'}); + var t0 = Task.boot({id: 3, list_id: 1, name: 't0', done: false}); + + var sess = new Session(); + + sess.watch( t0, { list: true } ); + + ok( sess.isWatching( l0 ) ); + notOk( sess.isWatching( l1 ) ); + ok( l0.$isSaved() ); + ok( l1.$isSaved() ); + + t0.$save( 'list', l1 ); + + strictEqual( t0.list, l1 ); + strictEqual( t0.list_id, l1.id ); + notOk( sess.isWatching( l0 ) ); + ok( sess.isWatching( l1 ) ); + + sess.discard(); + + ok( l0.$isSaved() ); + ok( l1.$isSaved() ); + strictEqual( t0.list, l0 ); + strictEqual( t0.list_id, l0.id ); + ok( sess.isWatching( l0 ) ); + notOk( sess.isWatching( l1 ) ); +}); + + +test( 'relation update hasOne', function(assert) +{ + var prefix = 'relation_update_hasOne_'; + + var Permission = Rekord({ + name: prefix + 'permission', + fields: ['rights'] + }); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'permission_id'], + hasOne: { + permission: { + model: Permission, + local: 'permission_id' + } + } + }); + + var p0 = Permission.boot({id: 1, rights: 'all'}); + var p1 = Permission.boot({id: 2, rights: 'none'}); + var t0 = Task.boot({id: 3, permission_id: 1, name: 't0', done: false}); + + var sess = new Session(); + + sess.watch( t0, { permission: true } ); + + ok( sess.isWatching( p0 ) ); + notOk( sess.isWatching( p1 ) ); + ok( p0.$isSaved() ); + ok( p1.$isSaved() ); + + t0.$save( 'permission', p1 ); + + strictEqual( t0.permission, p1 ); + strictEqual( t0.permission_id, p1.id ); + notOk( sess.isWatching( p0 ) ); + ok( sess.isWatching( p1 ) ); + + sess.save(); + + notOk( p0.$isSaved(), 'permission removed when hasOne' ); + ok( p1.$isSaved() ); + strictEqual( t0.permission, p1 ); + strictEqual( t0.permission_id, p1.id ); + notOk( sess.isWatching( p0 ) ); + ok( sess.isWatching( p1 ) ); +}); + + +test( 'relation update belongsTo discard', function(assert) +{ + var prefix = 'relation_update_hasOne_discard_'; + + var Permission = Rekord({ + name: prefix + 'permission', + fields: ['rights'] + }); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'permission_id'], + hasOne: { + permission: { + model: Permission, + local: 'permission_id' + } + } + }); + + var p0 = Permission.boot({id: 1, rights: 'all'}); + var p1 = Permission.boot({id: 2, rights: 'none'}); + var t0 = Task.boot({id: 3, permission_id: 1, name: 't0', done: false}); + + var sess = new Session(); + + sess.watch( t0, { permission: true } ); + + ok( sess.isWatching( p0 ) ); + notOk( sess.isWatching( p1 ) ); + ok( p0.$isSaved() ); + ok( p1.$isSaved() ); + + t0.$save( 'permission', p1 ); + + strictEqual( t0.permission, p1 ); + strictEqual( t0.permission_id, p1.id ); + notOk( sess.isWatching( p0 ) ); + ok( sess.isWatching( p1 ) ); + + sess.discard(); + + ok( p0.$isSaved() ); + ok( p1.$isSaved() ); + strictEqual( t0.permission, p0 ); + strictEqual( t0.permission_id, p0.id ); + ok( sess.isWatching( p0 ) ); + notOk( sess.isWatching( p1 ) ); +}); + +test( 'cascade save', function() +{ + var prefix = 'cascade_save_'; + var TaskName = prefix + 'task'; + + var _t0 = {id: 1, name: 't0', done: 1}; + var _t1 = {id: 2, name: 't1', done: 0}; + + var Task_rest = Rekord.rest[ TaskName ] = new TestRest(); + Task_rest.map.put( _t0.id, _t0 ); + Task_rest.map.put( _t1.id, _t1 ); + + var Task = Rekord({ + name: TaskName, + fields: ['name', 'done'] + }); + + var t0 = Task.get(1); + var t1 = Task.get(2); + var t2 = new Task({id: 3, name: 't2', done: 1}); + + var sess = new Session(); + + sess.watchMany( [t0, t1, t2] ); + + Task.Database.store.lastKey = null; + Task.Database.rest.lastModel = null; + + assertSaved( t0 ); + assertSaved( t1 ); + assertNew( t2 ); + + t0.name = 't0a'; + + t0.$save( Rekord.Cascade.Local ); + t1.$remove( Rekord.Cascade.Local ); + t2.$save( Rekord.Cascade.Local ); + + strictEqual( Task.Database.store.lastKey, null ); + strictEqual( Task.Database.rest.lastModel, null ); + + strictEqual( sess.getSessionWatch( t0 ).cascade, Rekord.Cascade.Local ); + strictEqual( sess.getRemoveWatch( t1 ).cascade, Rekord.Cascade.Local ); + strictEqual( sess.getSessionWatch( t2 ).cascade, Rekord.Cascade.Local ); + + ok( t0.$isSaved() ); + ok( t0.$isSavedLocally() ); + ok( t1.$isDeleted() ); + notOk( t2.$isSaved() ); + notOk( t2.$isSavedLocally() ); + + sess.save(); + + assertSaved( t0 ); + assertStored( t0, {name: 't0a'} ); + assertRest( t0, {name: 't0'} ); + assertRemovedLocally( t1 ); + assertSavedLocally( t2 ); + assertStored( t2, {name: 't2'} ); +}); + +test( 'cascade discard', function() +{ + var prefix = 'cascade_discard_'; + var TaskName = prefix + 'task'; + + var _t0 = {id: 1, name: 't0', done: 1}; + var _t1 = {id: 2, name: 't1', done: 0}; + + var Task_rest = Rekord.rest[ TaskName ] = new TestRest(); + Task_rest.map.put( _t0.id, _t0 ); + Task_rest.map.put( _t1.id, _t1 ); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done'] + }); + + var t0 = Task.get(1); + var t1 = Task.get(2); + var t2 = new Task({id: 3, name: 't2', done: 1}); + + var sess = new Session(); + + sess.watchMany( [t0, t1, t2] ); + + Task.Database.store.lastKey = null; + Task.Database.rest.lastModel = null; + + assertSaved( t0 ); + assertSaved( t1 ); + assertNew( t2 ); + + t0.name = 't0a'; + + t0.$save( Rekord.Cascade.Local ); + t1.$remove( Rekord.Cascade.Local ); + t2.$save( Rekord.Cascade.Local ); + + strictEqual( Task.Database.store.lastKey, null ); + strictEqual( Task.Database.rest.lastModel, null ); + + strictEqual( sess.getSessionWatch( t0 ).cascade, Rekord.Cascade.Local ); + strictEqual( sess.getRemoveWatch( t1 ).cascade, Rekord.Cascade.Local ); + strictEqual( sess.getSessionWatch( t2 ).cascade, Rekord.Cascade.Local ); + + ok( t0.$isSaved() ); + ok( t0.$isSavedLocally() ); + ok( t1.$isDeleted() ); + notOk( t2.$isSaved() ); + notOk( t2.$isSavedLocally() ); + + sess.discard(); + + assertSaved( t0 ); + assertSaved( t1 ); + assertNew( t2 ); +}); + +test( 'tree', function() +{ + var prefix = 'tree_'; + var TaskName = prefix + 'task'; + + var Task = Rekord({ + name: TaskName, + fields: ['name', 'done', 'parent_id'], + hasMany: { + children: { + model: TaskName, + foreign: 'parent_id' + } + } + }); + + var t0 = Task.boot({ + id: 1, + name: 't0', + done: false, + children: [ + { + id: 2, + name: 't1', + done: true + }, + { + id: 3, + name: 't2', + done: true, + children: [ + { + id: 4, + name: 't3', + done: false + } + ] + } + ] + }); + + var t1 = Task.get(2); + var t2 = Task.get(3); + var t3 = Task.get(4); + + var sess = new Session(); + + var relations = {}; + relations.children = relations; + + sess.watch( t0, relations ); + + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + ok( sess.isWatching( t2 ) ); + ok( sess.isWatching( t3 ) ); + + t2.$remove(); + + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + notOk( sess.isWatching( t2 ) ); + notOk( sess.isWatching( t3 ) ); + + sess.discard(); + + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + ok( sess.isWatching( t2 ) ); + ok( sess.isWatching( t3 ) ); +}); + +test( 'delayed remove', function(assert) +{ + var prefix = 'delayed_remove_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done'] + }); + + var t0 = Task.boot({id: 1, name: 't0', done: false}); + + var sess = new Session(); + + sess.watch( t0 ); + + ok( sess.isWatching( t0 ) ); + notOk( sess.isRemoved( t0 ) ); + notOk( t0.$isDeleted() ); + + t0.$remove(); + + notOk( sess.isWatching( t0 ) ); + ok( sess.isRemoved( t0 ) ); + ok( t0.$isDeleted() ); + + sess.discard(); + + ok( sess.isWatching( t0 ) ); + notOk( sess.isRemoved( t0 ) ); + notOk( t0.$isDeleted() ); + + t0.$remove(); + + notOk( sess.isWatching( t0 ) ); + ok( sess.isRemoved( t0 ) ); + ok( t0.$isDeleted() ); + + sess.save(); + + notOk( sess.isWatching( t0 ) ); + notOk( sess.isRemoved( t0 ) ); // it's removed for good + + assertRemoved( t0 ); +}); + +test( 'remove save remove', function(assert) +{ + var prefix = 'remove_save_remove_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done'] + }); + + var t0 = Task.boot({id: 1, name: 't0', done: false}); + + var sess = new Session(); + + sess.watch( t0 ); + + ok( sess.isWatching( t0 ) ); + notOk( sess.isRemoved( t0 ) ); + notOk( t0.$isDeleted() ); + + t0.$remove(); + + notOk( sess.isWatching( t0 ) ); + ok( sess.isRemoved( t0 ) ); + ok( t0.$isDeleted() ); + + sess.discard(); + + ok( sess.isWatching( t0 ) ); + notOk( sess.isRemoved( t0 ) ); + notOk( t0.$isDeleted() ); + + t0.name = 't0a'; + t0.$save(); + + ok( sess.isWatching( t0 ) ); + ok( sess.getSessionWatch( t0 ).save ); + notOk( sess.isRemoved( t0 ) ); + notOk( t0.$isDeleted() ); + + sess.discard(); + + ok( sess.isWatching( t0 ) ); + notOk( sess.getSessionWatch( t0 ).save ); + notOk( sess.isRemoved( t0 ) ); + notOk( t0.$isDeleted() ); + + strictEqual( t0.name, 't0' ); + + t0.$remove(); + + notOk( sess.isWatching( t0 ) ); + ok( sess.isRemoved( t0 ) ); + ok( t0.$isDeleted() ); + + sess.save(); + + notOk( sess.isWatching( t0 ) ); + notOk( sess.isRemoved( t0 ) ); // it's removed for good + + assertRemoved( t0 ); +}); + +test( 'relation move', function(assert) +{ + var prefix = 'relation_remove_'; + var TaskName = prefix + 'task'; + + var Task = Rekord({ + name: TaskName, + fields: ['name', 'done', 'parent_id'], + hasMany: { + children: { + model: TaskName, + foreign: 'parent_id', + cascadeRemove: Rekord.Cascade.None + } + } + }); + + var t0 = Task.boot({id: 1, name: 't0', done: false}); + var t1 = Task.boot({id: 2, name: 't1', done: true}); + var t2 = Task.boot({id: 3, name: 't2', done: false, children:[t1]}); + + strictEqual( t1.parent_id, t2.id ); + + var sess = new Session(); + + var relations = {}; + relations.children = relations; + + sess.watch( t0, relations ); + sess.watch( t2, relations ); + + strictEqual( t1.parent_id, t2.id ); + strictEqual( t0.children[0], undefined ); + strictEqual( t2.children[0], t1 ); + + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + ok( sess.isWatching( t2 ) ); + + t2.children.unrelate( t1 ); + + strictEqual( t1.parent_id, null ); + strictEqual( t0.children[0], undefined ); + strictEqual( t2.children[0], undefined ); + + ok( sess.isWatching( t0 ) ); + notOk( sess.isWatching( t1 ) ); + ok( sess.isWatching( t2 ) ); + + t0.children.relate( t1 ); + + strictEqual( t1.parent_id, t0.id ); + strictEqual( t0.children[0], t1 ); + strictEqual( t2.children[0], undefined ); + + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + ok( sess.isWatching( t2 ) ); + + sess.discard(); + + strictEqual( t1.parent_id, t2.id ); + strictEqual( t0.children[0], undefined ); + strictEqual( t2.children[0], t1 ); +}); + +test( 'unrelate no cascade', function(assert) +{ + var prefix = 'unrelate_no_cascade_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'list', + fields: ['name'], + hasMany: { + tasks: { + model: Task, + foreign: 'list_id', + comparator: 'id', + cascadeRemove: Rekord.Cascade.None + } + } + }); + + var l0 = TaskList.boot({id: 1, name: 'l0'}); + var t0 = Task.boot({id: 2, list_id: 1, name: 't0', done: false}); + var t1 = Task.boot({id: 3, list_id: 1, name: 't1', done: true}); + + var sess = new Session(); + + sess.watch( l0, { tasks: true } ); + + ok( sess.isWatching( l0 ) ); + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + + l0.tasks.unrelate( t0 ); + + ok( sess.isUnwatched( t0 ) ); + ok( sess.hasChanges() ); + + strictEqual( t0.list_id, null ); + deepEqual( l0.tasks.toArray(), [t1], 'tasks updated' ); + + sess.discard(); + + notOk( sess.hasChanges(), 'no changes as expected' ); + strictEqual( t0.list_id, l0.id, 'fk restored' ); + deepEqual( l0.tasks.toArray(), [t0, t1], 'tasks restored' ); + + l0.tasks.unrelate( t0 ); + + strictEqual( t0.list_id, null ); + deepEqual( l0.tasks.toArray(), [t1], 'tasks updated' ); + + sess.save(); + + strictEqual( t0.list_id, null ); + deepEqual( l0.tasks.toArray(), [t1], 'tasks updated' ); +}); + +test( 'unrelate cascade', function(assert) +{ + var prefix = 'unrelate_cascade_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'list', + fields: ['name'], + hasMany: { + tasks: { + model: Task, + foreign: 'list_id', + comparator: 'id', + cascadeRemove: Rekord.Cascade.All + } + } + }); + + var l0 = TaskList.boot({id: 1, name: 'l0'}); + var t0 = Task.boot({id: 2, list_id: 1, name: 't0', done: false}); + var t1 = Task.boot({id: 3, list_id: 1, name: 't1', done: true}); + + var sess = new Session(); + + sess.watch( l0, { tasks: true } ); + + ok( sess.isWatching( l0 ) ); + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + + l0.tasks.unrelate( t0 ); + + ok( sess.isRemoved( t0 ) ); + ok( sess.hasChanges() ); + + strictEqual( t0.list_id, null ); + deepEqual( l0.tasks.toArray(), [t1], 'tasks updated' ); + + sess.discard(); + + notOk( sess.hasChanges(), 'no changes as expected' ); + strictEqual( t0.list_id, l0.id, 'fk restored' ); + deepEqual( l0.tasks.toArray(), [t0, t1], 'tasks restored' ); + + l0.tasks.unrelate( t0 ); + + strictEqual( t0.list_id, null ); + deepEqual( l0.tasks.toArray(), [t1], 'tasks updated' ); + + sess.save(); + + assertRemoved( t0 ); + strictEqual( t0.list_id, null ); + deepEqual( l0.tasks.toArray(), [t1], 'tasks updated' ); +}); + +test( 'relate', function(assert) +{ + var prefix = 'relate_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'list', + fields: ['name'], + hasMany: { + tasks: { + model: Task, + foreign: 'list_id', + comparator: 'id' + } + } + }); + + var l0 = TaskList.boot({id: 1, name: 'l0'}); + var l1 = TaskList.boot({id: 2, name: 'l1'}); + var t0 = Task.boot({id: 3, list_id: 1, name: 't0', done: false}); + var t1 = Task.boot({id: 4, list_id: 1, name: 't1', done: true}); + var t2 = Task.boot({id: 5, list_id: 2, name: 't2', done: true}); + var t3 = new Task({id: 6, name: 't3', done: true}); + + var sess = new Session(); + + sess.watch( l0, { tasks: true } ); + sess.watch( l1, { tasks: true } ); + + ok( sess.isWatching( l0 ) ); + ok( sess.isWatching( l1 ) ); + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + ok( sess.isWatching( t2 ) ); + notOk( sess.isWatching( t3 ) ); + + assertNew( t3 ); + + l0.tasks.relate( t3 ); + + strictEqual( t3.list_id, l0.id ); + strictEqual( l0.tasks.length, 3 ); + strictEqual( l1.tasks.length, 1 ); + + l0.tasks.relate( t2 ); + + strictEqual( t2.list_id, l0.id ); + strictEqual( l0.tasks.length, 4 ); + strictEqual( l1.tasks.length, 0 ); +}); + +test( 'unrelate all no cascade', function(assert) +{ + var prefix = 'unrelate_all_no_cascade_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'list', + fields: ['name'], + hasMany: { + tasks: { + model: Task, + foreign: 'list_id', + comparator: 'id', + cascadeRemove: Rekord.Cascade.None + } + } + }); + + var l0 = TaskList.boot({id: 1, name: 'l0'}); + var t0 = Task.boot({id: 2, list_id: 1, name: 't0', done: false}); + var t1 = Task.boot({id: 3, list_id: 1, name: 't1', done: true}); + + var sess = new Session(); + + sess.watch( l0, { tasks: true } ); + + ok( sess.isWatching( l0 ) ); + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + + l0.tasks.unrelate(); + + ok( sess.isUnwatched( t0 ) ); + ok( sess.isUnwatched( t1 ) ); + ok( sess.hasChanges() ); + + strictEqual( t0.list_id, null ); + strictEqual( t1.list_id, null ); + deepEqual( l0.tasks.toArray(), [], 'tasks updated' ); + + sess.discard(); + + notOk( sess.hasChanges(), 'no changes as expected' ); + strictEqual( t0.list_id, l0.id, 'fk restored' ); + strictEqual( t1.list_id, l0.id, 'fk restored' ); + deepEqual( l0.tasks.toArray(), [t0, t1], 'tasks restored' ); + + l0.tasks.unrelate(); + + strictEqual( t0.list_id, null ); + strictEqual( t1.list_id, null ); + deepEqual( l0.tasks.toArray(), [], 'tasks updated' ); + + sess.save(); + + strictEqual( t0.list_id, null ); + strictEqual( t1.list_id, null ); + deepEqual( l0.tasks.toArray(), [], 'tasks updated' ); +}); + +test( 'unrelate all cascade', function(assert) +{ + var prefix = 'unrelate_all_cascade_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'list', + fields: ['name'], + hasMany: { + tasks: { + model: Task, + foreign: 'list_id', + comparator: 'id', + cascadeRemove: Rekord.Cascade.All + } + } + }); + + var l0 = TaskList.boot({id: 1, name: 'l0'}); + var t0 = Task.boot({id: 2, list_id: 1, name: 't0', done: false}); + var t1 = Task.boot({id: 3, list_id: 1, name: 't1', done: true}); + + var sess = new Session(); + + sess.watch( l0, { tasks: true } ); + + ok( sess.isWatching( l0 ) ); + ok( sess.isWatching( t0 ) ); + ok( sess.isWatching( t1 ) ); + + l0.tasks.unrelate(); + + ok( sess.isRemoved( t0 ) ); + ok( sess.isRemoved( t1 ) ); + ok( sess.hasChanges() ); + + strictEqual( t0.list_id, null ); + strictEqual( t1.list_id, null ); + deepEqual( l0.tasks.toArray(), [], 'tasks updated' ); + + sess.discard(); + + notOk( sess.hasChanges(), 'no changes as expected' ); + strictEqual( t0.list_id, l0.id, 'fk restored' ); + strictEqual( t1.list_id, l0.id, 'fk restored' ); + deepEqual( l0.tasks.toArray(), [t0, t1], 'tasks restored' ); + + l0.tasks.unrelate(); + + strictEqual( t0.list_id, null ); + strictEqual( t1.list_id, null ); + deepEqual( l0.tasks.toArray(), [], 'tasks updated' ); + + sess.save(); + + assertRemoved( t0 ); + strictEqual( t0.list_id, null ); + strictEqual( t1.list_id, null ); + deepEqual( l0.tasks.toArray(), [], 'tasks updated' ); +}); + +test( 'promise success', function(assert) +{ + var prefix = 'promise_success_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'list', + fields: ['name'], + hasMany: { + tasks: { + model: Task, + foreign: 'list_id', + comparator: 'id', + cascadeRemove: Rekord.Cascade.All + } + } + }); + + var l0 = TaskList.boot({id: 1, name: 'l0'}); + var t0 = Task.boot({id: 2, list_id: 1, name: 't0', done: false}); + var t1 = Task.boot({id: 3, list_id: 1, name: 't1', done: true}); + + var sess = new Session(); + + sess.watch( l0, { tasks: true } ); + + t0.done = true; + t0.$save(); + + expect( 2 ); + + ok( sess.hasChanges() ); + + var promise = sess.save(); + + promise.success(function() + { + notOk( t0.$hasChanges() ); + }); + + promise.failure(function() + { + ok(); + }); +}); + +test( 'promise failure', function(assert) +{ + var prefix = 'promise_failure_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'list', + fields: ['name'], + hasMany: { + tasks: { + model: Task, + foreign: 'list_id', + comparator: 'id', + cascadeRemove: Rekord.Cascade.All + } + } + }); + + var l0 = TaskList.boot({id: 1, name: 'l0'}); + var t0 = Task.boot({id: 2, list_id: 1, name: 't0', done: false}); + var t1 = Task.boot({id: 3, list_id: 1, name: 't1', done: true}); + + var sess = new Session(); + + sess.watch( l0, { tasks: true } ); + + t0.done = true; + t0.$save(); + + expect( 2 ); + + ok( sess.hasChanges() ); + + Task.Database.rest.status = 500; + + var promise = sess.save(); + + promise.success(function() + { + ok(); + }); + + promise.failure(function() + { + ok( t0.$hasChanges() ); + }); +}); + // one of the rest calls fails during save (promise must fail with error, calling save again should finish everything) -// moving an object from one relation to another (must watch two objects) -// model.remove -> discard -> model.remove -> save -// model.remove -> discard -> model.save -> discard -> model.remove -> save