Skip to content

Commit

Permalink
Merge pull request #1 from Volicon/f/local-options
Browse files Browse the repository at this point in the history
Subset collection
  • Loading branch information
Vlad Balin authored Aug 16, 2016
2 parents 48c3345 + aa2ac52 commit 27779c6
Show file tree
Hide file tree
Showing 17 changed files with 332 additions and 171 deletions.
145 changes: 75 additions & 70 deletions index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion index.js.map

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions notes/ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Onwership Scheme in Type-R

`Type-R` and `NestedTypes 2.0` enforces strict ownerhip policy.

*Ownership tree* - is the tree formed by nested records and collections, which is:
- serialized as nested JSON
- can be updated _in place_ from JSON, preserving all the references to its parts.
- all changes to the members are tracked across the tree, resulting in root change event.
- disposed when the root is disposed.

One object cannot have more than one owner, attempt to violate this rule results
in immediate run-time error.

Default Record/Collection type annotations denotes aggregation, and forms ownership tree.
Thus, by default, all compound Record types you define are serializable.

## Owned (aggregated) attributes

`attr : Record` and `attr : Record.Collection`

The default ownership policy for a record's attributes.
Used when attribute is an integral part of the Record - when Record is disposed, aggregated attributes
are disposed too.

- Have one and only one owner.
- Default value - `new Record` and `new Record.Collection`
- Internal changes are tracked and cause owner 'change' event.
- Can be updated in place (`merge` global transaction's setting).
- Type is automatically converted on assignment (with `new Record( value )`).
- Are serialized as nested JSON by default.

## Shared attributes

`attr : Record.shared` and `attr : Record.Collection.shared`

Attribute has 'shared' ownership policy when `shared` modifyer is added to the contructor type.
Used when attribute is the reference to some exiting record or collection.

- Record don't attempt to take ownership on shared attributes.
- Default value is `null`.
- Internal changes are tracked and cause owner 'change' event.
- Never is updated in place.
- No type convertions allowed - attribute must be assigned with a valid subtype.
- Don't participate in serialization.

## Collection subsets

`attr : Collection.Subset`

Is a subclass of `Collection` (created when referenced for the first time).
Used when collection needs to be populated with records wich are the part of different ownership tree.

- Have one and only one owner. But don't attempt to take ownership on its members.
- Default value is an empty collection (`new Collection.Subset`).
- Only collection (not enclosed record) changes are tracked and cause owner 'change' event.
- Can be updated in place. But never update its members in place (`merge : false` transaction option).
- Type is automatically converted on assignment (with `new Record( value )`).
- Don't participate in serialization.

`attr : Collection.Subset.of( 'path.to.collection.relative.to.this' )`

Serializable version of `Subset`.

- Type is automatically converted on assignment. id arrays and record arrays are resolved against the given collection.
- Serialized as an array of record ids.

## Serializable shared records

`attr : Record.from( 'path.to.collection.relative.to.this' )`

Serializable reference to the record, which is a part of existing collection.

- Record don't attempt to take ownership on this attribute.
- Default value is `null`.
- Internal changes are not tracked.
- Never is updated in place.
- Type is automatically converted on assignment. Record id is resolved against the given collection.
- Serialized as Record id.
11 changes: 5 additions & 6 deletions src/collection/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ interface AddOptions extends CollectionOptions {
}

/** @private */
export function addTransaction( collection : CollectionCore, items, options : AddOptions ){
export function addTransaction( collection : CollectionCore, items, options : AddOptions, merge? : boolean ){
const isRoot = begin( collection ),
nested = [];

var added = appendElements( collection, items, nested, options );
var added = appendElements( collection, items, nested, options, merge );

if( added.length || nested.length ){
let needSort = sortOrMoveElements( collection, added, options );
Expand Down Expand Up @@ -65,10 +65,9 @@ function moveElements( source : any[], at : number, added : any[] ) : void {

// append data to model and index
/** @private */
function appendElements( collection : CollectionCore, a_items, nested : Transaction[], a_options ){
var models = collection.models,
_byId = collection._byId,
merge = a_options.merge,
function appendElements( collection : CollectionCore, a_items, nested : Transaction[], a_options, forceMerge : boolean ){
var { _byId, models } = collection,
merge = ( forceMerge || a_options.merge ) && collection._aggregates,
parse = a_options.parse,
idAttribute = collection.model.prototype.idAttribute,
prevLength = models.length;
Expand Down
20 changes: 7 additions & 13 deletions src/collection/commons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Owner, Transaction,

import { eventsApi, tools } from '../object-plus'

const { EventMap, trigger2, trigger3 } = eventsApi,
const { EventMap, trigger2, trigger3, on, off } = eventsApi,
{ commit, markAsDirty } = transactionApi,
_aquire = transactionApi.aquire, _free = transactionApi.free;

Expand Down Expand Up @@ -43,18 +43,12 @@ export function dispose( collection : CollectionCore ) : Record[]{

/** @private */
export function convertAndAquire( collection : CollectionCore, attrs : {} | Record, options ){
const { model } = collection;
let record : Record;

if( attrs instanceof model ){
record = attrs;
if( collection._aggregates && !_aquire( collection, record ) ){
const errors = collection._aggregationError || ( collection._aggregationError = [] );
errors.push( record );
}
}
else{
record = <Record> model.create( attrs, options, collection );
const { model } = collection,
record : Record = attrs instanceof model ? attrs : <Record>model.create( attrs, options );

if( collection._aggregates && !_aquire( collection, record ) ){
const errors = collection._aggregationError || ( collection._aggregationError = [] );
errors.push( record );
}

// Subscribe for events...
Expand Down
89 changes: 61 additions & 28 deletions src/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,46 @@ interface CollectionDefinition extends TransactionalDefinition {
_itemEvents? : EventMap
}


@define({
// Default client id prefix
cidPrefix : 'c',
model : Record,
_changeEventName : 'changes',
_aggregates : true,
_aggregationError : null
})
export class Collection extends Transactional implements CollectionCore {
_aggregates : boolean
_aggregationError : Record[]

static Subset : typeof Collection
static _SubsetOf : typeof Collection

createSubset( models, options ){
var SubsetOf = (<any>this.constructor).subsetOf( this ).options.type;
var subset = new SubsetOf( models, options );
const SubsetOf = (<any>this.constructor).subsetOf( this ).options.type,
subset = new SubsetOf( models, options );

subset.resolve( this );
return subset;
}

static predefine() : any {
// Cached subset collection must not be inherited.
const Ctor = this;
this._SubsetOf = null;

function Subset( a, b ){
Ctor.call( this, a, b, true );
}

Subset.prototype = this.prototype;
Subset._attribute = TransactionalType;
Subset[ 'of' ] = function( path ){
return Ctor.subsetOf( path );
}

this.Subset = <any>Subset;

Transactional.predefine.call( this );
createSharedTypeSpec( this );
return this;
Expand Down Expand Up @@ -128,26 +145,23 @@ export class Collection extends Transactional implements CollectionCore {
_comparator : ( a : Record, b : Record ) => number

_onChildrenChange( record : Record, options : TransactionOptions = {} ){
const { _byId } = this;
// Updates in initialize cause troubles, especially id change. TODO: check the same thing for the model.
if( _byId[ record.cid ] ){
const isRoot = begin( this ),
{ idAttribute } = this;
const isRoot = begin( this ),
{ idAttribute } = this;

if( record.hasChanged( idAttribute ) ){
delete _byId[ record.previous( idAttribute ) ];
if( record.hasChanged( idAttribute ) ){
const { _byId } = this;
delete _byId[ record.previous( idAttribute ) ];

const { id } = record;
id == null || ( _byId[ id ] = record );
}
const { id } = record;
id == null || ( _byId[ id ] = record );
}

if( markAsDirty( this, options ) ){
// Forward change event from the record.
trigger2( this, 'change', record, options )
}
if( markAsDirty( this, options ) ){
// Forward change event from the record.
trigger2( this, 'change', record, options )
}

isRoot && commit( this );
}
isRoot && commit( this );
}

get( objOrId : string | Record | Object ) : Record {
Expand All @@ -172,6 +186,9 @@ export class Collection extends Transactional implements CollectionCore {
}

_validateNested( errors : {} ) : number {
// Don't validate if not aggregated.
if( !this._aggregates ) return 0;

let count = 0;

this.each( record => {
Expand All @@ -190,24 +207,36 @@ export class Collection extends Transactional implements CollectionCore {
// idAttribute extracted from the model type.
idAttribute : string


constructor( records? : ( Record | {} )[], options : CollectionOptions = {} ){
constructor( records? : ( Record | {} )[], options : CollectionOptions = {}, shared? : boolean ){
super( _count++ );
this.models = [];
this._byId = {};
this.model = options.model || this.model;
this.idAttribute = this.model.prototype.idAttribute;

this.comparator = this.comparator;

if( options.comparator !== void 0 ){
this.comparator = options.comparator;
options.comparator = void 0;
}

this.model = this.model;

if( options.model ){
this.model = options.model;
options.model = void 0;
}

this.idAttribute = this.model.prototype.idAttribute; //TODO: Remove?

this._aggregates = !shared;

if( records ){
const elements = toElements( this, records, options );
emptySetTransaction( this, elements, options, true );
}

this.initialize.apply( this, arguments );

if( this._localEvents ) this._localEvents.subscribe( this, this );
}

Expand All @@ -223,15 +252,19 @@ export class Collection extends Transactional implements CollectionCore {

// Deeply clone collection, optionally setting new owner.
clone( options : CloneOptions = {} ) : this {
var models = this.map( model => model.clone() );
const copy : this = new (<any>this.constructor)( models, { model : this.model, comparator : this.comparator }, options.owner );
if( options.key ) copy._ownerKey = options.key;
const models = this.map( model => model.clone() ),
copy : this = new (<any>this.constructor)( models, { model : this.model, comparator : this.comparator } );

if( options.pinStore ) copy._defaultStore = this.getStore();

return copy;
}

toJSON() : Object[] {
return this.models.map( model => model.toJSON() );
// Don't serialize when not aggregated
if( this._aggregates ){
return this.models.map( model => model.toJSON() );
}
}

// Apply bulk in-place object update in scope of ad-hoc transaction
Expand Down Expand Up @@ -300,7 +333,7 @@ export class Collection extends Transactional implements CollectionCore {

if( this.models.length ){
return options.remove === false ?
addTransaction( this, elements, options ) :
addTransaction( this, elements, options, true ) :
setTransaction( this, elements, options );
}
else{
Expand Down
2 changes: 1 addition & 1 deletion src/collection/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function _garbageCollect( collection : CollectionCore, previous : Record[] ) : R
function _reallocate( collection : CollectionCore, source : any[], nested : Transaction[], options ){
var models = Array( source.length ),
_byId : IdIndex = {},
merge = options.merge == null ? true : options.merge,
merge = ( options.merge == null ? true : options.merge ) && collection._aggregates,
_prevById = collection._byId,
prevModels = collection.models,
idAttribute = collection.model.prototype.idAttribute,
Expand Down
7 changes: 3 additions & 4 deletions src/object-plus/mixins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface Constructor< T >{
*/
export interface MixableConstructor< T > extends Constructor< T >{
prototype : Mixable
create( a : any, b? : any, c? : any ) : Mixable
create( a : any, b? : any ) : Mixable
mixins( ...mixins : ( Constructor<any> | {} )[] ) : MixableConstructor< T >
mixinRules( mixinRules : MixinRules ) : MixableConstructor< T >
mixTo( ...args : Constructor<any>[] ) : MixableConstructor< T >
Expand All @@ -58,10 +58,9 @@ export interface MixableConstructor< T > extends Constructor< T >{
* Supports mixins, and Class.define metaprogramming method.
*/
export class Mixable {

// Generic class factory. May be overridden for abstract classes. Not inherited.
static create( a : any, b? : any, c? : any ) : Mixable {
return new (<any>this)( a, b, c );
static create( a : any, b? : any ) : Mixable {
return new (<any>this)( a, b );
}

protected static _mixinRules : MixinRules = { properties : 'merge' };
Expand Down
6 changes: 3 additions & 3 deletions src/record/attributes/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ export class GenericAttribute implements Attribute {
/**
* Stage 1. Transform stage
*/
transform( value, options, prev, model ) { return value; }
transform( value, options, prev, model : Record ) { return value; }

// convert attribute type to `this.type`
convert( value, options, model ) { return value; }
convert( value, options, model : Record ) { return value; }

/**
* Stage 2. Check if attr value is changed
Expand All @@ -59,7 +59,7 @@ export class GenericAttribute implements Attribute {
/**
* Stage 3. Handle attribute change
*/
handleChange( next, prev, model ) {}
handleChange( next, prev, model : Record ) {}

/**
* End update pipeline definitions.
Expand Down
2 changes: 1 addition & 1 deletion src/record/attributes/owned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class TransactionalType extends GenericAttribute {

convert( value : any, options : TransactionOptions, record : Record ) : Transactional {
// Invoke class factory to handle abstract classes
return value == null || value instanceof this.type ? value : this.type.create( value, options, record );
return value == null || value instanceof this.type ? value : this.type.create( value, options );
}

validate( record : Record, value : Transactional ){
Expand Down
3 changes: 2 additions & 1 deletion src/record/attributes/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export class SharedType extends GenericAttribute {
convert( value : any, options : TransactionOptions, record : Record ) : Transactional {
if( value == null || value instanceof this.type ) return value;

tools.log.error( `[Shared Attribute] Cannot assign value of incompatible type.`, value, record );
// TODO: May allow conversion here - unnecessary restriction. We can do it. Or error is better?
tools.log.error( `[Record] Cannot assign value of incompatible type to shared attribute.`, value, record._attributes );

return null;
}
Expand Down
Loading

0 comments on commit 27779c6

Please sign in to comment.