UNPKG

@type-r/models

Version:

The serializable type system for JS and TypeScript

233 lines (185 loc) 8.35 kB
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 ); } }