transactional
Version:
Reactive objects with transactional updates and automatic serialization
248 lines (196 loc) • 5.74 kB
JavaScript
import {Class, defaults} from './class+'
/*
Core model API.
1)
@define({
attributes : {
...
}
})
class MyModel extends Model {}
2)
class MyModel extends Model {}
MyModel.define({
attributes : {
...
}
})
3)
class MyModel extends Model {}
MyModel.Collection = class extends Model.Collection {}
MyModel.define({
attributes : {
...
}
})
*/
/**
* Core model transactions
*
* Generic Ownerwhip
* Deep changes
*
* Scenarios:
* Implicit transactions:
* - model.x = 5;
* - model.nested = { some: 'thing };
* - model.set({ some : 'thing' });
*
* Things to consider:
* - listeners to 'change:attr', chained change including deep.
* - listeners to 'change', chained chane including deep
* All listeners must be executed inside of current transaction.
* Must be optimized for object sync (full updates).
* Major use case - I/O transactions.
*
* Explicit transactions:
* model.transaction
* - model.x = 5;
* - model.nested = { some: 'thing };
* - model.set({ some : 'thing' });
* - nested transaction statement.
* Things to consider:
* - change:attr must happen instantly in this case.
* Major use case - data manipulations.
*
* (!) Note: Object sync
* - normally will touch all subtree, so it's okay to traverse complete subtree.
*
* (!) In transactional mode, change:attr must be immediate.
*
* Methods:
* set( { attrs } )
* transaction
*
* @attributes({
*
* })
* class
*
*
*
*/
function setOwner( owner, object, key ){
object._owner = owner;
object._ownerKey = key;
}
function notifyOwnerOnChange( object ){
const { _owner } = object;
if( _owner ){
_owner._onNestedChange( object );
}
}
function setSingleAttr( model, key, value ){
const isRoot = begin( model );
var options = {},
prevValue = current[ key ],
val = attrSpec.transform( value, options, model, key );
current[ key ] = val;
if( attrSpec.isChanged( prevValue, val ) ){
model._pending = options;
trigger3( model, 'change:' + key, model, val, options );
}
isRoot && commit( model, {} );
}
class Model {
constructor( attrs, options ){
this.attributes = this.defaults();
}
get collection(){
return this._ownerKey ? this._owner : null;
}
_onNestedChange( object ){
if( this._changing ){
// inside of transaction it's enough, if it's guaranteed that there will be single callback per .set
this._changes.push( key );
this._pending = true;
}
else{
// make transaction, touch single attribute
}
}
}
function begin( model ){
const isRoot = !model._changing;
if( isRoot ){
model._changing = true;
model._pending = false;
model._previousAttributes = new model.Attributes( model.attributes );
model.changed = new model.Attributes( {} );
}
return isRoot;
}
function commit( model, isRoot, options ){
// Trigger all relevant attribute changes.
if( !options.silent ){
let changes = model._changes;
if( changes.length ) this._pending = true;
for( var i = 0; i < changes.length; i++ ){
this.trigger( 'change:' + changes[ i ], this, current[ changes[ i ] ], options );
}
}
if( isRoot && !options.silent ){
while( model._pending ){
model._pending = false;
model.trigger( 'change', model, options );
}
}
model._changing = false;
}
function attrHasChanges( model, key, value ){
model._pending = true;
model.changed[ key ] = model.attributes[ key ] = value;
model.trigger( 'change:' + key, model, value, this.options );
}
function touchAttr( model, key ){
const isRoot = begin( model );
attrHasChanges( model, key, model.attributes[ key ] );
isRoot && commit( model );
}
function _onNestedChange( object, key ){
// if the part of object sync...
if( this._changed ){
this._changes.push( key );
this._pending = true;
}
else if( this._changing ){
// inside of transaction it's enough, if it's guaranteed that there will be single callback per .set
}
else{
// make transaction, touch single attribute
}
}
var isChanged = attrSpec.assign( value, options, model, key );
function assign( value, options, model, key ){ //<- todo: too many params. Introduce transaction context? model + options + attrs...
///================
const compatibleType = value == null || value instanceof this.type,
existing = model.attributes[ key ];
if( existing && !compatibleType ){
return existing.setState( value, options );
} // <- is not the part of regular pipeline. It's not cast which can be exected after hook.
// todo: move this out of the pipeline.
return this.pipeline( compatibleType ? value : this.create( value, options ), existing, model, key );
}
// Guaranteed that here's compatible type
function pipeline( next, prev, model, key ){
////================
// hook... Optional pipeline stage
///========================================
// Check for changes...
if( this.isChanged( next ) ){
// Ownership... Optional pipeline stage
// Events... Optional pipeline stage
// Assignment
model.attributes[ key ] = next;
return true;
}
return true;
}
/**
primitive type assign
*/
function assign( value, options, model, key ){
///================
const compatibleType = value == null || primitives[ typeof value ];
return this.pipeline( compatibleType ? value : this.create( value ), existing, model, key );
}