@type-r/models
Version:
The serializable type system for JS and TypeScript
233 lines (185 loc) • 8.35 kB
text/typescript
import { eventsApi } from '@type-r/mixture';
import { Owner, Transaction, Transactional, transactionApi, TransactionOptions } from "../transactions";
const { begin : _begin, markAsDirty : _markAsDirty, commit } = transactionApi;
const { trigger3 } = eventsApi;
export interface ConstructorsMixin {
Attributes : AttributesConstructor
AttributesCopy : AttributesCopyConstructor
}
export interface ConstructorOptions extends TransactionOptions{
clone? : boolean
}
export type AttributesConstructor = new ( record : AttributesContainer, values : object, options : TransactionOptions ) => AttributesValues;
export type AttributesCopyConstructor = new ( values : object ) => AttributesValues;
export interface AttributesContainer extends Transactional, Owner, ConstructorsMixin {
// Attribute descriptors.
/** @internal */
_attributes : AttributesDescriptors
// Attribute values.
attributes : AttributesValues
// Previous attribute values.
/** @internal */
_previousAttributes : AttributesValues
// Changed attributes cache.
/** @internal */
_changedAttributes : AttributesValues
}
export interface AttributesValues {
[ name : string ] : any
}
export interface AttributesDescriptors {
[ name : string ] : AttributeUpdatePipeline
}
export interface AttributeUpdatePipeline{
doUpdate( value, record : AttributesContainer, options : TransactionOptions, nested? : Transaction[] ) : boolean
}
// Optimized single attribute transactional update. To be called from attributes setters
// options.silent === false, parse === false.
export function setAttribute( record : AttributesContainer, name : string, value : any ) : void {
// Open the transaction.
const isRoot = begin( record ),
options = {};
// Update attribute.
if( record._attributes[ name ].doUpdate( value, record, options ) ){
// Notify listeners on changes.
markAsDirty( record, options );
trigger3( record, 'change:' + name, record, record.attributes[ name ], options );
}
// Close the transaction.
isRoot && commit( record );
}
function begin( record : AttributesContainer ){
if( _begin( record ) ){
record._previousAttributes = new record.AttributesCopy( record.attributes );
record._changedAttributes = null;
return true;
}
return false;
}
function markAsDirty( record : AttributesContainer, options : TransactionOptions ){
// Need to recalculate changed attributes, when we have nested set in change:attr handler
if( record._changedAttributes ){
record._changedAttributes = null;
}
return _markAsDirty( record, options );
}
/**
* TODO: There's an opportunity to create an optimized pipeline for primitive types and Date, which makes the majority
* of attributes. It might create the major speedup.
*
* Create the dedicated pipeline for owned and shared attributes as well.
*
* Three elements of the pipeline:
* - from constructor
* - from assignment
* - from `set`
*/
export const UpdateModelMixin = {
// Need to override it here, since begin/end transaction brackets are overriden.
transaction( this : AttributesContainer, fun : ( self : AttributesContainer ) => void, options : TransactionOptions = {} ) : void{
const isRoot = begin( this );
fun.call( this, this );
isRoot && commit( this );
},
// Handle nested changes. TODO: propagateChanges == false, same in transaction.
_onChildrenChange( child : Transactional, options : TransactionOptions ) : void {
const { _ownerKey } = child,
attribute = this._attributes[ _ownerKey ];
if( !attribute /* TODO: Must be an opposite, likely the bug */ || attribute.propagateChanges ) this.forceAttributeChange( _ownerKey, options );
},
// Simulate attribute change
forceAttributeChange( key : string, options : TransactionOptions = {} ){
// Touch an attribute in bounds of transaction
const isRoot = begin( this );
if( markAsDirty( this, options ) ){
trigger3( this, 'change:' + key, this, this.attributes[ key ], options );
}
isRoot && commit( this );
},
_createTransaction( this : AttributesContainer, a_values : {}, options : TransactionOptions = {} ) : Transaction {
const isRoot = begin( this ),
changes : string[] = [],
nested : ModelTransaction[]= [],
{ _attributes } = this,
values = options.parse ? this.parse( a_values, options ) : a_values;
let unknown;
if( shouldBeAnObject( this, values, options ) ){
for( let name in values ){
const spec = _attributes[ name ];
if( spec ){
if( spec.doUpdate( values[ name ], this, options, nested ) ){
changes.push( name );
}
}
else{
unknown || ( unknown = [] );
unknown.push( `'${ name }'` );
}
}
if( unknown ){
unknownAttrsWarning( this, unknown, { values }, options );
}
}
if( changes.length && markAsDirty( this, options ) ){
return new ModelTransaction( this, isRoot, nested, changes );
}
// No changes, but there might be silent attributes with open transactions.
for( let pendingTransaction of nested ){
pendingTransaction.commit( this );
}
isRoot && commit( this );
}
};
export function unknownAttrsWarning( record : AttributesContainer, unknown : string[], props, options ){
record._log( 'warn', 'Type-R:UnknownAttrs', `undefined attributes ${ unknown.join(', ')} are ignored.`, props, options.logger );
}
// One of the main performance tricks of Type-R.
// Create loop unrolled constructors for internal attribute hash,
// so the hidden class JIT optimization will be engaged and they will become static structs.
// It dramatically improves record performance.
export function constructorsMixin( attrDefs : AttributesDescriptors ) : ConstructorsMixin {
const attrs = Object.keys( attrDefs );
const AttributesCopy : AttributesCopyConstructor = new Function( 'values', `
${ attrs.map( attr =>`
this.${ attr } = values.${ attr };
`).join( '' ) }
`) as any;
AttributesCopy.prototype = Object.prototype;
const Attributes : AttributesConstructor = new Function( 'record', 'values', 'options', `
var _attrs = record._attributes;
${ attrs.map( attr =>`
this.${ attr } = _attrs.${ attr }.doInit( values.${ attr }, record, options );
`).join( '' ) }
`) as any;
Attributes.prototype = Object.prototype;
return { Attributes, AttributesCopy };
}
export function shouldBeAnObject( record : AttributesContainer, values : object, options ){
if( values && values.constructor === Object ) return true;
record._log( 'error', 'Type-R:InvalidObject', 'update with non-object is ignored!', { values }, options.logger );
return false;
}
// Transaction class. Implements two-phase transactions on object's tree.
// Transaction must be created if there are actual changes and when markIsDirty returns true.
export class ModelTransaction implements Transaction {
// open transaction
constructor( public object : AttributesContainer,
public isRoot : boolean,
public nested : Transaction[],
public changes : string[] ){}
// commit transaction
commit( initiator? : AttributesContainer ) : void {
const { nested, object, changes } = this;
// Commit all pending nested transactions...
for( let transaction of nested ){
transaction.commit( object );
}
// Notify listeners on attribute changes...
// Transaction is never created when silent option is set, so just send events out.
const { attributes, _isDirty } = object;
for( let key of changes ){
trigger3( object, 'change:' + key, object, attributes[ key ], _isDirty );
}
this.isRoot && commit( object, initiator );
}
}