transactional
Version:
Reactive objects with transactional updates and automatic serialization
297 lines (236 loc) • 10.4 kB
text/typescript
import { Messenger, trigger2, trigger3, assign, define } from './toolkit/index.ts'
import { ValidationError, Validatable, ChildrenErrors } from './validation.ts'
import { Traversable, resolveReference } from './references.ts'
/***
* Abstract class implementing ownership tree, tho-phase transactions, and validation.
* 1. createTransaction() - apply changes to an object tree, and if there are some events to send, transaction object is created.
* 2. transaction.commit() - send and process all change events, and close transaction.
*/
// Transactional object interface
export abstract class Transactional extends Messenger implements Validatable, Traversable {
// Unique version token replaced on change
_changeToken : {} = {}
// true while inside of the transaction
_transaction : boolean = false;
// true, when in the middle of transaction and there're changes but is an unsent change event
_isDirty : boolean = false;
// Backreference set by owner (Record, Collection, or other object)
_owner : Owner
// Key supplied by owner. Used by record to identify attribute key.
// Only collections doesn't set the key, which is used to distinguish collections.
_ownerKey : string
// Name of the change event
_changeEventName : string
constructor( cid : string | number, owner? : Owner, ownerKey? : string ){
super( cid );
this._owner = owner;
this._ownerKey = ownerKey;
}
// Deeply clone ownership subtree, optionally setting the new owner
// (TODO: Do we really need it? Record must ignore events with empty keys)
// 'Pin store' shall assign this._defaultStore = this.getStore();
abstract clone( options? : { owner? : Owner, key? : string, pinStore? : boolean }) : this
// Execute given function in the scope of ad-hoc transaction.
transaction( fun : ( self : this ) => void, options? : TransactionOptions ) : void{
const isRoot = begin( this );
fun( this );
isRoot && commit( this, options );
}
// Loop through the members in the scope of transaction.
// Transactional version of each()
updateEach( iteratee : ( val : any, key : string ) => void, options? : TransactionOptions ){
const isRoot = begin( this );
this.each( iteratee );
isRoot && commit( this, options );
}
// Apply bulk in-place object update in scope of ad-hoc transaction
set( values : any, options? : TransactionOptions ) : this {
if( values ){
const transaction = this._createTransaction( values, options );
transaction && transaction.commit( options, true );
}
return this;
}
// Apply bulk object update without any notifications, and return open transaction.
// Used internally to implement two-phase commit.
// Returns null if there are no any changes.
abstract _createTransaction( values : any, options? : TransactionOptions ) : Transaction
// Parse function applied when 'parse' option is set for transaction.
parse( data : any ) : any { return data }
// Convert object to the serializable JSON structure
abstract toJSON() : {}
/*******************
* Traversals and member access
*/
// Get object member by its key.
abstract get( key : string ) : any;
// Get object member by symbolic reference.
deepGet( reference : string ) : any {
return resolveReference( this, reference, ( object, key ) => object.get( key ) );
}
//_isCollection : boolean
// Return owner skipping collections.
getOwner() : Owner {
return this._owner;
}
// Store used when owner chain store lookup failed. Static value in the prototype.
_defaultStore : Transactional
// Locate the closest store. Store object stops traversal by overriding this method.
getStore() : Transactional {
const { _owner } = this;
return _owner ? <Transactional> _owner.getStore() : this._defaultStore;
}
/***************************************************
* Iteration API
*/
// Loop through the members. Must be efficiently implemented in container class.
abstract each( iteratee : ( val : any, key : string | number ) => void, context? : any )
// Map members to an array
map<T>( iteratee : ( val : any, key : string ) => T, context? : any ) : T[]{
const arr : T[] = [],
fun = arguments.length === 2 ? ( v, k ) => iteratee.call( context, v, k ) : iteratee;
this.each( ( val, key ) => {
const result = fun( val, key );
if( result !== void 0 ) arr.push( result );
} );
return arr;
}
// Map members to an object
mapObject<T>( iteratee : ( val : any, key : string | number ) => T, context? : any ) : { [ key : string ] : T }{
const obj : { [ key : string ] : T } = {},
fun = arguments.length === 2 ? ( v, k ) => iteratee.call( context, v, k ) : iteratee;
this.each( ( val, key ) => {
const result = iteratee( val, key );
if( result !== void 0 ) obj[ key ] = result;
} );
return obj;
}
// Get array of attribute keys (Record) or record ids (Collection)
keys() : string[] {
return this.map( ( value, key ) => {
if( value !== void 0 ) return key;
});
}
// Get array of attribute values (Record) or records (Collection)
values() : any[] {
return this.map( value => value );
}
/*********************************
* Validation API
*/
// Lazily evaluated validation error
_validationError : ValidationError = void 0
// Validate ownership tree and return valudation error
get validationError() : ValidationError {
return this._validationError || ( this._validationError = new ValidationError( this ) );
}
// Validate nested members. Returns errors count.
abstract _validateNested( errors : ChildrenErrors ) : number;
// Object-level validator. Returns validation error.
validate( obj? : Transactional ) : any {}
// Return validation error (or undefined) for nested object with the given key.
getValidationError( key : string ) : any {
var error = this.validationError;
return ( key ? error && error.nested[ key ] : error ) || null;
}
// Get validation error for the given symbolic reference.
deepValidationError( reference : string ) : any {
return resolveReference( this, reference, ( object, key ) => object.getValidationError( key ) );
}
// Iterate through all validation errors across the ownership tree.
eachValidationError( iteratee : ( error : any, key : string, object : Transactional ) => void ) : void {
const { validationError } = this;
validationError && validationError.eachError( iteratee, this );
}
// Check whenever member with a given key is valid.
isValid( key : string ) : boolean {
return !this.getValidationError( key );
}
}
// Owner must accept children update events. It's an only way children communicates with an owner.
export interface Owner extends Traversable {
_onChildrenChange( child : Transactional, options : TransactionOptions );
getOwner() : Owner
getStore() : Transactional
}
// Transaction object used for two-phase commit protocol.
// Must be implemented by subclasses.
export interface Transaction {
// Object transaction is being made on.
object : Transactional
// Send out change events, process update triggers, and close transaction.
// Nested transactions must be marked with isNested flag (it suppress owner notification).
commit( options? : TransactionOptions, isNested? : boolean )
}
// Options for distributed transaction
export interface TransactionOptions {
// Invoke parsing
parse? : boolean
// Suppress change notifications and update triggers
silent? : boolean
// Update existing transactional members in place, or skip the update (ignored by models)
merge? : boolean // =true
// Should collections remove elements in set (ignored by models)
remove? : boolean // =true
// Always replace enclosed objects with new instances
reset? : boolean // = false
validate? : boolean
}
/**
* Low-level transactions API. Must be used like this:
* const isRoot = begin( record );
* ...
* isRoot && commit( record, options );
*
* When committing nested transaction, the flag must be set to true.
* commit( object, options, isNested )
*/
// Start transaction. Return true if it's the root one.
export function begin( object : Transactional ) : boolean {
return object._transaction ? false : ( object._transaction = true );
}
// Mark transactional object as dirty, so change event will be sent on commit.
export function markAsDirty( object : Transactional ){
object._isDirty = true;
object._changeToken = {};
object._validationError = void 0;
}
// Commit transaction. Send out change event and notify owner. Returns true if there were changes.
// Should be executed for the root transaction only.
export function commit( object : Transactional, options : TransactionOptions, isNested? : boolean ){
const wasDirty = object._isDirty;
if( options.silent ){
object._isDirty = false;
}
else{
while( object._isDirty ){
object._isDirty = false;
trigger2( object, object._changeEventName, object, options );
}
}
object._transaction = false;
// Don't notify owner for the case of nested transaction, it already knows if there were changes
if( !isNested && wasDirty && object._owner ){
object._owner._onChildrenChange( object, options );
}
}
/************************************
* Ownership management
*/
// Add reference to the record.
export function aquire( owner : Owner, child : Transactional, key? : string ) : void {
if( !child._owner ){
child._owner = owner;
child._ownerKey = key;
}
}
// Remove reference to the record.
export function free( owner : Owner, child : Transactional ) : void {
if( owner === child._owner ){
child._owner = void 0;
child._ownerKey = void 0;
}
}