NAV
Type-R 3.0
universal state management
javascript typescript

Getting started

Overview

Type-R is the TypeScript and JavaScript model framework helping to define and manage the complex application state as a combination of reusable parts. Type-R cover the needs of business logic and data layers in 3-tier application architecture, providing the presentation layer with the unified technique to handle the UI and domain state. Type-R data structures look and feel (and, in some aspects, behaves) more like classes in the statically typed languages.

Type-R in unopinionated on the way how an application state should be managed ("single source of truth" or "distributed state"). It can support all approaches equally well being not dependent on singletons and having powerful capabilities for state synchronization.

overview

A state is defined as a superposition of typed records and collections. A record is a class with a known set of attributes of predefined types possibly holding other records and collections in its attributes, describing the data structure of arbitrary complexity. Record with its attributes forms an aggregation tree with deeply observable attributes changes. Attribute types are checked on assignments and invalid changes are being rejected, therefore it is guaranteed that the application state will preserve the valid shape.

Application state defined with Type-R is serializable to JSON by default. Aggregation tree of records and collections is mapped in JSON as a tree of plain objects and arrays. Normalized data represented as a set of collections of records cross-referencing each other are supported as first-class serialization scenario.

A record may have an associated IOEndpont representing the I/O protocol for CRUD and collection fetch operations which enables the persistence API for the particular record/collection class pair. Some useful endpoints (restfulIO, localStorageIO, etc) are provided by type-r/endpoints/* packages, and developers can define their own I/O endpoints implementing any particular persistence transport or API.

Record attributes may have custom validation rules attached to them. Validation is being triggered transparently on demand and its result is cached across the record/collection aggregation tree, making subsequent calls to the validation API extremely cheap.

All aspects of record behavior including serialization and validation can be controlled on attribute level with declarative definitions combining attribute types with metadata. Attribute definitions ("metatypes") can be reused across different models forming the domain-specific language of model declarations. Some useful attribute metatypes (Email, Url, MicrosoftDate, etc) are provided by type-r/ext-types package.

How Type-R compares to X?

Type-R (former "NestedTypes") project was started in 2014 in Volicon as a modern successor to BackboneJS models, which would match Ember Data in its capabilities to work with a complex state while retaining the BackboneJS simplicity, modularity, and some degree of backward API compatibility. It replaced BackboneJS in the model layer of Volicon products, and it became the key technology in Volicon's strategy to gradually move from BackboneJS Views to React in the view layer.

Ember Data is the closest thing to Type-R by its capabilities, with BackboneJS models and collections being the closest thing by the API, and mobx being pretty close in the way how the UI state is managed.

Type-R, however, takes a very different approach to all of them:

Feature Type-R Backbone Models Ember Data mobx
Observable changes in object graph - -
JSON Serialization -
Validation -
Dynamic Type Safety - for serialization only -
Aggregation - - -
Relations by id - -
Generalized I/O sync function -

Features by example

Here's the brief overview of features groped by application purpose.

Persistent domain state

The basic building block is the Record class. To fetch data from the server, a developer creates the subclass of the Record describing its attribute types and attaches the restfulIO endpoint. It enables the persistence API allowing the developer to fetch the collection from the server. restfulIO expects the server to implement the standard RESTful API expected by BackboneJS models.

Record and collection are serializable to and can be parsed from JSON with no additional effort. A mapping to JSON can be customized for collections, records, and individual attributes. The Record validates all updates casting attribute values to declared attribute types to protect the state structure from the protocol incompatibilities and improper assignments.

@define User extends Record {
    static endpoint = restfulIO( '/api/users' );
    static attributes = {
        name : String,
        email : String,
        createdAt : Date
    }
}

const users = new User.Collection();
await users.fetch();

expect( users.first().createdAt ).toBeInstanceOf( Date );
expect( typeof users.toJSON()[ 0 ].createdAt ).toBe( "string" );
@define User extends Record {
    static endpoint = restfulIO( '/api/users' );

    // Type-R can infer attribute types from TypeScript type annotations.
    @auto name : string
    @auto email : string
    @auto createdAt : Date
}

const users : Collection<User> = new User.Collection();
await users.fetch();

expect( users.first().createdAt ).toBeInstanceOf( Date );
expect( typeof users.toJSON()[ 0 ].createdAt ).toBe( "string" );

UI state and observable changes

Type-R provides the universal technique to working with the UI and domain state. To define the UI state, a developer creates the subclass of the Record with attributes holding all the necessary state data possibly along with the persistent data which can become the part of the same local UI state. The UI state itself can be a part of some particular view or UI component, it can be managed as a singleton ("single source of truth"), or both at the same time. Type-R is unopinionated on the application state structure leaving this decision to the developer.

Records and collections form an aggregation tree with deeply observable changes, so it's enough to subscribe to the single change event from the UIState to get updates on both data arrival and local changes of the state attributes. Records and collections can be indefinitely nested to describe a state of arbitrary complexity. The developer can attach reactions on changes to the records, their individual attributes, and collections. Additional changes made in reactions will be executed in the scope of the same "change transaction" and won't trigger additional change events.

@define UIState extends Record {
    static attributes = {
        users : User.Collection,
        selectedUser : memberOf( 'users' )
    }
}

const uiState = new UIState();

uiState.on( 'change', () => {
    console.log( 'Something is changed' );
    updateUI();
});

uiState.users.fetch();
@define UIState extends Record {
    // For collections and more complex types attribute type must be provided explicitly
    @type( User.Collection ).as users : Collection<User>

    @memberOf( 'users' ).as selectedUser : User
}

const uiState = new UIState();

uiState.on( 'change', () => {
    console.log( 'Something is changed' );
    updateUI();
});

uiState.users.fetch();

Validation

Type-R supports validation as attribute-level checks attached to attribute definitions as metadata. Attribute type together with checks forms an "attribute metatype", which can be defined separately and reused across multiple record definitions.

Validation rules are evaluated recursively on the aggregation tree on first access to the validation API, and validations results are cached in records and collections across the tree till the next update. The validation is automatic, subsequent calls to the validation API are cheap, and the developer doesn't need to manually trigger the validation on data changes.

The majority of checks in a real application will be a part of attribute "metatypes", while the custom validation can be also defined on the Record and Collection level to check data integrity and cross-attributes dependencies.

const Email = type( String )
    .check( x => !x || x.indexOf( '@' ) >= 0, "Doesn't look like an email" );

@define User extends Record {
    static endpoint = restfulIO( '/api/users' );
    static attributes = {
        name : type( String ).required,
        email : type( Email ).required,
        createdAt : type( Date ).check( x => x.getTime() <= Date.now() )
    }
}

const users = new User.Collection();
users.add({ email : 'john' });
expect( users.isValid() ).toBe( false );
expect( users.first().isValid() ).toBe( false );

users.first().name = "John";
users.first().email = "john@ny.com";
expect( users.isValid() ).toBe( true );
const Email = type( String )
    .check( x => !x || x.indexOf( '@' ) >= 0, "Doesn't look like an email" );

@define User extends Record {
    static endpoint = restfulIO( '/api/users' );

    // @type(...).as converts Type-R attribute type definition to the TypeScript decorator.
    @type( String ).required.as
        name : string

    @type( Email ).required.as
        email : string

    @type( Date ).check( x => x.getTime() <= Date.now() ).as
        createdAt : Date
}

const users = new User.Collection();
users.add({ email : 'john' });
expect( users.isValid() ).toBe( false );
expect( users.first().isValid() ).toBe( false );

users.first().name = "John";
users.first().email = "john@ny.com";
expect( users.isValid() ).toBe( true );

Installation and requirements

Is packed as UMD and ES6 module. No peer dependencies are required.

npm install type-r --save-dev

ReactJS bindings

React-MVx is a glue framework which uses Type-R to manage the UI state in React and the NestedLink library to implement two-way data binding. React-MVx provides the complete MVVM solution on top of ReactJS, featuring:

Usage with NodeJS

Type-R can be used at the server side to build the business logic layer by defining the custom I/O endpoints to store data in a database. Type-R dynamic type safety features are particularly advantageous when schema-less JSON databases (like Couchbase) are being used.

server

Record

Record is an optionally persistent class having the predefined set of attributes. Each attribute is the property of known type which is protected from improper assigments at run-time, is serializable to JSON by default, has deeply observable changes, and may have custom validation rules attached.

Records may have other records and collections of records stored in its attributes describing an application state of an arbitrary complexity. These nested records and collections are considered to be an integral part of the parent record forming an aggregation tree which can be serialized to JSON, cloned, and disposed of as a whole.

All aspects of an attribute behavior are controlled with attribute metadata, which (taken together with its type) is called attribite metatype. Metatypes can be declared separately and reused across multiple records definitions.

import { define, type, Record } from 'type-r'

// ⤹ required to make magic work  
@define class User extends Record {
    // ⤹ attribute's declaration
    static attributes = {
        firstName : '', // ⟵ String type is inferred from the default value
        lastName  : String, // ⟵ Or you can just mention its constructor
        email     : type(String).value(null), //⟵ Or you can provide both
        createdAt : Date, // ⟵ And it works for any constructor.
        // And you can attach ⤹ metadata to fine-tune attribute's behavior
        lastLogin : type(Date).value(null).toJSON(false) // ⟵ not serializable
    }
}

const user = new User();
console.log( user.createdAt ); // ⟵ this is an instance of Date created for you.

const users = new User.Collection(); // ⟵ Collections are defined automatically.
users.on( 'changes', () => updateUI( users ) ); // ⟵ listen to the changes.

users.set( json, { parse : true } ); // ⟵ parse raw JSON from the server.
users.updateEach( user => user.firstName = '' ); // ⟵ bulk update triggering 'changes' once
import { define, attr, type, Record } from 'type-r'
import "reflect-metadata" // Required for @auto without arguments

// ⤹ required to make the magic work  
@define class User extends Record {
    // ⤹ attribute's declaration
    // IMPORTANT: attributes will be initialized even if no default value is provided.
    @auto lastName  : string // ⟵ @auto decorator extracts type from the Reflect metadata
    @auto createdAt : Date // ⟵ It works for any constructor.
    @auto('somestring') firstName : string //⟵ The custom default value must be passed to @auto decorator.
    @auto(null) updatedAt : Date 

    // You have to pass the type explicitly if reflect-metadata is not used.
    @type(String).as email : string

    // Or, you can tell Type-R to infer type from the default value.
    @value('').as email2 : string

    // Type cannot be inferred from null default values, and needs to be specified explicitly
    @type(String).value(null).as email3 : string 

    // You can attach ⤹ metadata to fine-tune attribute's behavior
    @type(Date).toJSON(false).as
        lastLogin : Date// ⟵ not serializable
}

const user = new User();
console.log(user.createdAt); // ⟵ this is an instance of Date created for you.

const users : Collection<User> = new User.Collection(); // ⟵ Collections are defined automatically.
users.on('changes', () => updateUI(users)); // ⟵ listen to the changes.

users.set(json, { parse : true }); // ⟵ parse raw JSON from the server.
users.updateEach( user => user.firstName = '' ); // ⟵ bulk update triggering 'changes' once

Definition

Record definition is ES6 class extending Record preceeded by @define class decorator.

Unlike in the majority of the JS state management framework, Record is not the key-value hash. Record has typed attributes with metadata controlling different aspects of attribute beavior. Therefore, developer needs to create the Record subclass to describe the data structure of specific shape, in a similar way as it's done in statically typed languages. The combination of an attribute type and metadata is called metatype and can be reused across record definitions.

The minimal record definition looks like this:

@define class MyRecord extends Record {
    static attributes = {
        name : ''
    }
}
@define class MyRecord extends Record {
    @auto name : string
}

static attributes = { name : attrDef, ... }

Record's attributes definition. Lists attribute names along with their types, default values, and metadata controlling different aspects of attribute behavior.

@define class User extends Record {
    static attributes = {
        name    : type( String ).value( 'John Dow' ),
        email   : 'john.dow@mail.com', // Same as type( String ).value( 'john.dow@mail.com' )
        address : String, // Same as type( String ).value( '' )
    }
}
// You should not use `static attributes` in TypeScript. Use decorators instead.
@define class User extends Record {
    // Complete form of an attribute definition.
    @type( String ).value( 'John Dow' ).as name : string,

    // Attribute type is inferred from the default value.
    @value( 'john.dow@mail.com' ).as email : string , // Same as @type( String ).value( 'john.dow@mail.com' ).as

    // Attribute type is inferred from the TypeScript type declaration.
    @auto address : string, // Same as @type( String ).value( '' )

    // Same as above, but with a custom default value.
    @auto( 'john.dow@mail.com' ) email2 : string // Same as @value( 'john.dow@mail.com' ).as
}

The Record guarantee that every attribute will retain the value of the declared type. Whenever an attribute is being assigned with the value which is not compatible with its declared type, the type is being converted with an invocation of the constructor: new Type( value ) (primitive types are treated specially).

static idAttribute = 'attrName'

A record's unique identifier is stored under the pre-defined id attribute. If you're directly communicating with a backend (CouchDB, MongoDB) that uses a different unique key, you may set a Record's idAttribute to transparently map from that key to id.

Record's id property will still be linked to Record's id, no matter which value idAttribute has.

@define class Meal extends Record {
  static idAttribute =  "_id";
  static attributes = {
      _id : Number,
      name : ''
  }
}

const cake = new Meal({ _id: 1, name: "Cake" });
alert("Cake id: " + cake.id);
@define class Meal extends Record {
  static idAttribute =  "_id";

  @auto _id : number
  @auto name : string
}

const cake = new Meal({ _id: 1, name: "Cake" });
alert("Cake id: " + cake.id);

attrDef : Constructor

Constructor function is the simplest form of attribute definition. Any constructor function which behaves as converting constructor (like new Date( msecs )) may be used as an attribute type.

@define class Person extends Record {
    static attributes = {
        name : String, // String attribute which is "" by default.
        createdAt : Date, // Date attribute
        ...
    }
}
// In typescript, @auto decorator will extract constructor function from the TypeScript type
@define class Person extends Record {
    @auto name : string // String attribute which is "" by default.
    @auto createdAt : Date // Date attribute

    // Or, it can be specified explicitly with @type decorator.
    @type( Date ).as updatedAt : Date // Date attribute
    ...
}

attrDef : defaultValue

Any non-function value used as attribute definition is treated as an attribute's default value. Attribute's type is being inferred from the value.

Type cannot be properly inferred from the null values and functions. Use the general form of attribute definition in such cases: value( theFunction ), type( Boolean ).value( null ).

@define class GridColumn extends Record {
    static attributes = {
        name : '', // String attribute which is '' by default.
        render : value( x => x ), // Infer Function type from the default value.
        ...
    }
}
// In typescript, @value decorator will extract constructor function from the default value.
@define class GridColumn extends Record {
    @value( '' ).as name : string // String attribute which is '' by default.
    @value( x => x ).as render : Function
    ...
}

attrDef : type(Constructor).value(defaultValue)

Declare an attribute with type T having the custom defaultValue.

@define class Person extends Record {
    static attributes = {
        phone : type( String ).value( null ) // String attribute which is null by default.
        ...
    }
}
@define class Person extends Record {
    @type( String ).value( null ).as phone : string // String attribute which is null by default.

    // There's an easy way of doing that in TypeScript.
    @auto( null ).as phone : string
    ...
}

If record needs to reference itself in its attributes definition, @predefine decorator with subsequent MyRecord.define() needs to be used.

attrDef : Date

Date attribute initialized as new Date(), and represented in JSON as UTC ISO string.

There are other popular Date serialization options available in type-r/ext-types package.

@define class Person extends Record {
    @auto justDate : Date
    // MicrosoftDate is an attribute metatype, not a real type, so you must pass it explictly.
    @type( Timestamp ).as createdAt : Date
    ...
}

static Collection

The default record's collection class automatically defined for every Record subclass. Can be referenced as Record.Collection.

May be explicitly assigned in record's definition with custom collection class.

// Declare the collection class.
@define class Comments extends Record.Collection {}

@define class Comment extends Record{
    static Collection = Comments; // Make it the default Comment collection.

    static attributes = {
        text : String,
        replies : Comments
    }
}
// Declare the collection class.
@define class Comments extends Collection<Comment> {}

@define class Comment extends Record{
    static Collection = Comments; // Make it the default Comment collection.

    @auto text : String
    @auto replies : Comments
}

attrDef type(Type)

Attribute definition can have different metadata attached which affects various aspects of attribute's behavior. Metadata is attached with a chain of calls after the type( Ctor ) call. Attribute's default value is the most common example of such a metadata and is the single option which can be applied to the constructor function directly.

import { define, type, Record }

@define class Dummy extends Record {
    static attributes = {
        a : type( String ).value( "a" )
    }
}
import { define, type, Record }

@define class Dummy extends Record {
    @type( String ).value( "a" ).as a : string
}

Definitions in TypeScript

Type-R supports several options to define record attributes.

decorator @auto

Turns TypeScript class property definition to the record's attribute, automatically extracting attribute type from the TypeScript type annotation. Requires reflect-metadata npm package and emitDecoratorMetadata option set to true in the tsconfig.json.

@auto may take a single parameter as an attribute default value. No other attribute metadata can be attached.

import { define, auto, Record } from 'type-r'

@define class User extends Record {
    @auto name : string
    @auto( "john@verizon.com" ) email : string
    @auto( null ) updatedAt : Date
}

decorator @attrDef.as

Attribute definition creates the TypeScript property decorator when being appended with .as suffix. It's an alternative syntax to @auto.

import { define, type, Record } from 'type-r'

@define class User extends Record {
    @value( "5" ).as name : string
    @type( String ).toJSON( false ).as email : string
}

Create and dispose

Record behaves as regular ES6 class with attributes accessible as properties.

new Record()

Create an instance of the record with default attribute values taken from the attributes definition.

When no default value is explicitly provided for an attribute, it's initialized as new Type() (just Type() for primitives). When the default value is provided and it's not compatible with the attribute type, it's converted with new Type( defaultValue ) call.

new Record({ attrName : value, ... }, options?)

When creating an instance of a record, you can pass in the initial attribute values to override the defaults.

If {parse: true} option is used, attrs is assumed to be the JSON.

If the value of the particular attribute is not compatible with its type, it's converted to the declared type invoking the constructor new Type( value ) (just Type( value ) for primitives).

@define class Book extends Record {
    static attributes = {
        title  : '',
        author : ''
    }
}

const book = new Book({
  title: "One Thousand and One Nights",
  author: "Scheherazade"
});
@define class Book extends Record {
    @auto title : string
    @auto author : string
}

const book = new Book({
  title: "One Thousand and One Nights",
  author: "Scheherazade"
});

record.clone()

Create the deep copy of the aggregation tree, recursively cloning all aggregated records and collections. References to shared members will be copied, but not shared members themselves.

callback record.initialize(attrs?, options?)

Called at the end of the Record constructor when all attributes are assigned and the record's inner state is properly initialized. Takes the same arguments as a constructor.

record.dispose()

Recursively dispose the record and its aggregated members. "Dispose" means that elements of the aggregation tree will unsubscribe from all event sources. It's crucial to prevent memory leaks in SPA.

The whole aggregation tree will be recursively disposed, shared members won't.

Read and Update

record.cid

Read-only client-side record's identifier. Generated upon creation of the record and is unique for every record's instance. Cloned records will have different cid.

record.id

Predefined record's attribute, the id is an arbitrary string (integer id or UUID). id is typically generated by the server. It is used in JSON for id-references.

Records can be retrieved by id from collections, and there can be just one instance of the record with the same id in the particular collection.

record.isNew()

Has this record been saved to the server yet? If the record does not yet have an id, it is considered to be new.

record.attrName

Record's attributes may be directly accessed as record.name.

@define class Account extends Record {
    static attributes = {
        name : String,
        balance : Number
    }
}

const myAccount = new Account({ name : 'mine' });
myAccount.balance += 1000000; // That works. Good, eh?

record.attrName = value

Assign the record's attribute. If the value is not compatible with attribute's type from the declaration, it is converted:

Record triggers events on changes:

@define class Book extends Record {
    static attributes = {
        title : String,
        author : String
        price : Number,
        publishedAt : Date,
        available : Boolean
    }
}

const myBook = new Book({ title : "State management with Type-R" });
myBook.author = 'Vlad'; // That works.
myBook.price = 'Too much'; // Converted with Number( 'Too much' ), resulting in NaN.
myBook.price = '123'; // = Number( '123' ).
myBook.publishedAt = new Date(); // Type is compatible, no conversion.
myBook.publishedAt = '1678-10-15 12:00'; // new Date( '1678-10-15 12:00' )
myBook.available = some && weird || condition; // Will always be Boolean. Or null.

record.set({ attrName : value, ... }, options? : options)

Bulk assign record's attributes, possibly taking options.

If the value is not compatible with attribute's type from the declaration, it is converted:

Record triggers events after all changes are applied:

  1. change:attrName ( record, val, options ) for any changed attribute.
  2. change (record, options), if there were changed attributes.

RecordClass.from(attrs, options?)

Create RecordClass from attributes. Similar to direct record creation, but supports additional option for strict data validation. If { strict : true } option is passed the validation will be performed and an exception will be thrown in case of an error.

Please note, that Type-R always perform type checks on assignments, convert types, and reject improper updates reporting it as error. It won't, however, execute custom validation rules on every updates as they are evaluated lazily. strict option will invoke custom validators and will throw on every error or warning instead of reporting them and continue.

// Fetch record with a given id.
const book = await Book.from({ id : 5 }).fetch();

// Validate the body of an incoming HTTP request.
// Throw an exception if validation fails.
const body = MyRequestBody.from( ctx.request.body, { parse : true, strict : true });
// Fetch record with a given id.
const book = await Book.from({ id : 5 }).fetch();

// Validate the body of an incoming HTTP request.
// Throw an exception if validation fails.
const body = MyRequestBody.from( ctx.request.body, { parse : true, strict : true });

record.assignFrom(otherRecord)

Makes an existing record to be the full clone of otherRecord, recursively assigning all attributes. In contracts to record.clone(), the record is updated in place.

// Another way of doing the bestSeller.clone()
const book = new Book();
book.assignFrom(bestSeller);

record.transaction(fun)

Execute the all changes made to the record in fun as single transaction triggering the single change event.

All record updates occurs in the scope of transactions. Transaction is the sequence of changes which results in a single change event. Transaction can be opened either manually or implicitly with calling set() or assigning an attribute. Any additional changes made to the record in change:attr event handler will be executed in the scope of the original transaction, and won't trigger additional change events.

some.record.transaction( record => {
    record.a = 1; // `change:a` event is triggered.
    record.b = 2; // `change:b` event is triggered.
}); // `change` event is triggered.

Manual transactions with attribute assignments are superior to record.set() in terms of both performance and flexibility.

attrDef : type(Type).get(hook)

Attach get hook to the record's attribute. hook is the function of signature ( value, attr ) => value which is used to transform the attribute's value before it will be read. Hook is executed in the context of the record.

attrDef : type(Type).set(hook)

Attach the set hook to the record's attribute. hook is the function of signature ( value, attr ) => value which is used to transform the attribute's value before it will be assigned. Hook is executed in the context of the record.

If set hook will return undefined, it will cancel attribute update.

Nested records and collections

Record's attributes can hold other Records and Collections, forming indefinitely nested data structures of arbitrary complexity. To create nested record or collection you should just mention its constructor function in attribute's definition.

import { Record } from 'type-r'

@define class User extends Record {
    static attributes = {
        name : String,
        email : String,
        isActive : true
    }
}

@define class UsersListState extends Record {
    static attributes = {
        users : User.Collection
    }
}

All nested records and collections are aggregated by default and behave as integral parts of the containing record. Aggregated attributes are exclusively owned by the record, and taken with it together form an ownership tree. Many operations are performed recursively on aggregated elements:

The nature of aggregation relationship in OO is explained in this article.

attrDef : RecordOrCollection

Aggregated record or collection. Represented as nested object or array in record's JSON. Aggregated members are owned by the record and treated as its integral part (recursively created, cloned, serialized, validated, and disposed). One object can have single owner. The record with its aggregated attributes forms an aggregation tree.

All changes in aggregated record or collections are detected and cause change events on the containing record.

record.getOwner()

Return the record which is an owner of the current record, or null there are no one.

Due to the nature of aggregation, an object may have one and only one owner.

record.collection

Return the collection which aggregates the record, or null if there are no one.

attrDef : shared(RecordOrCollection)

Non-serializable reference to the record or collection possibly from the different aggregation tree. Initialized with null. Is not recursively cloned, serialized, validated, or disposed.

All changes in shared records or collections are detected and cause change events of the containing record.

@define class UsersListState extends Record {
    static attributes = {
        users : User.Collection,
        selected : shared( User ) // Can be assigned with the user from this.users
    }
}
@define class UsersListState extends Record {
    @type( User.Collection ).as users : Collection<User>,
    @shared( User ).as selected : User // Can be assigned with the user from this.users
}

attrDef : Collection.Refs

Non-aggregating collection. Collection of references to shared records which itself is aggregated by the record, but does not aggregate its elements. In contrast to the shared( Collection ), Collection.Refs is an actual constructor and creates an instance of collection which is the part the parent record.

The collection itself is recursively created and cloned. However, its records are not aggregated by the collection thus they are not recursively cloned, validated, serialized, or disposed.

All changes in the collection and its elements are detected and cause change events of the containing record.

    @define class MyRecord extends Record {
        static attributes = {
            notCloned : shared( SomeCollection ), // Reference to the _shared collection_ object.
            cloned : SomeCollection.Refs // _Aggregated_ collection of references to the _shared records_.
        }
    }
    @define class MyRecord extends Record {
        // Reference to the _shared collection_ object.
        @shared( SomeCollection ).as notCloned : Collection<Some>

        // _Aggregated_ collection of references to the _shared records_.
        @type( SomeCollection.Refs ).as cloned : SomeCollection
    }

decorator @predefine

Make forward declaration for the record to define its attributes later with RecordClass.define(). Used instead of @define for recursive record definitions.

Creates the default RecordClass.Collection type which can be referenced in attribute definitions.

static define({ attributes : { name : attrDef, ... }})

May be called to define attributes in conjunction with @predefine decorator to make recursive record definitions.

@predefine class Comment extends Record{}

Comment.define({
    attributes : {
        text : String,
        replies : Comment.Collection
    }
});

Collection

Collections are ordered sets of records. The collection is an array-like object exposing ES6 Array and BackboneJS Collection interface. It encapsulates JS Array of records (collection.models) and a hashmap for a fast O(1) access by the record id and cid (collection.get( id )).

Collactions are deeply observable. You can bind "changes" events to be notified when the collection has been modified, listen for the record "add", "remove", and "change" events.

Every Record class has an implicitly defined Collection accessible as a static member of a record's constructor. In a most cases, you don't need to define the custom collection class.

@define class Book extends Record {
    static attributes = {
        title : String
        author : Author
    }
}

// Implicitly defined collection.
const books = new Book.Collection();
@define class Book extends Record {
    @auto title : string
    @auto author : Author

    // Tell TypeScript the proper type.
    static Collection : CollectionConstructor<Book>
}

const books = new Book.Collection();

You can define custom collection classes extending Record.Collection or any other collection class. It can either replace the default Collection type, or

// Define custom collection class.
@define class Library extends Record.Collection {
    doSomething(){ ... }
}

@define class Book extends Record {
    // Override the default collection.
    static Collection = Library;
}

// Define another custom collection class.
@define class OtherLibrary extends Record.Collection {
    // Specify the record so the collection will be able to restore itself from JSON.
    static model = Book; 
}
// Define custom collection class.
@define class Library extends Collection<Book> {
    doSomething(){ ... }
}

@define class Book extends Record {
    // Override the default collection.
    static Collection = Library;
}

// Define another custom collection class.
@define class OtherLibrary extends Collection<Book> {
    // Specify the record so the collection will be able to restore itself from JSON.
    static model = Book;
}

// An alternative way of overriding the default collection class in TypeScript.
namespace Book {
    @define class Collection extends Collection<Book> {
        static model = Book;
    }
}

Collection types

constructor CollectionClass( records?, options? )

The most common collection type is an aggregating serializable collection. By default, collection aggregates its elements which are treated as an integral part of the collection (serialized, cloned, disposed, and validated recursively). An aggregation means the single owner, as the single object cannot be an integral part of two distinct things. The collection will take ownership on its records and will put an error in the console if it can't.

When creating a Collection, you may choose to pass in the initial array of records.

@define class Role extends Record {
    static attributes = {
        name : String
    }
}

const roles = new Role.Collection( json, { parse : true } );
@define class Role extends Record {
    // In typescript, you have to specify record's Collection type expicitly.
    static Collection : CollectionConstructor<Role>

    @auto name : string
}

@define class User extends Record {
    @auto name : string

    // Type-R cannot infer a Collection metatype from the TypeScript type automatically.
    // Full attribute type annotation is required.
    @type( Role.Collection ).as roles : Collection<User>
}

constructor CollectionClass.Refs( records?, options? )

Collection of record references is a non-aggregating non-serializable collection. Collection.Refs doesn't aggregate its elements, which means that containing records are not considered as an integral part of the enclosing collection and not being validated, cloned, disposed, and serialized recursively.

It is useful for a local non-persistent application state.

attrDef subsetOf(masterRef, CollectionClass?)

The subset of other collections are non-aggregating serializable collection. Subset-of collection is serialized as an array of record ids and used to model many-to-many relationships. The collection object itself is recursively created and cloned, however, its records are not aggregated by the collection thus they are not recursively cloned, validated, or disposed. CollectionClass argument may be omitted unless you need the record's attribute to be an instance of the particular collection class.

Must have a reference to the master collection which is used to resolve record ids to records. masterRef may be:

@define class Role extends Record {
    static attributes = {
        name : String,
        ...
    }
}

@define class User extends Record {
    static attributes = {
        name : String,
        roles : subsetOf( 'owner.roles', Role.Collection )
    }
}

@define class UsersDirectory extends Store {
    static attributes = {
        roles : Role.Collection,
        users : User.Collection // `~roles` references will be resolved against this.roles
    }
}
@define class Role extends Record {
    static Collection : CollectionConstructor<Role>

    @auto name : string
    ...
}

@define class User extends Record {
    static Collection : CollectionConstructor<User>

    @auto name : string
    @subsetOf('store.roles').as roles : Collection<Role>
}

@define class UsersDirectory extends Store {
    @type(Role.Collection).as roles : Collection<Role>,
    @type(User.Collection).as users : Collection<User> // <- `store.roles` references will be resolved against this.roles
}

Array API

A collection class is an array-like object implementing ES6 Array methods and properties.

collection.length

Like an array, a Collection maintains a length property, counting the number of records it contains.

collection.slice( begin, end )

Return a shallow copy of the collection.models, using the same options as native Array#slice.

collection.indexOf( recordOrId : any ) : number

Return an index of the record in the collection, and -1 if there is no such a record in the collection.

Can take the record itself as an argument, id, or cid of the record.

collection.forEach( iteratee : ( val : Record, index ) => void, context? )

Iterate through the elements of the collection.

collection.map( iteratee : ( val : Record, index ) => T, context? )

Map elements of the collection. Similar to Array.map.

collection.filter( iteratee : Predicate, context? )

Return the filtered array of records matching the predicate.

The predicate is either the iteratee function returning boolean, or an object with attribute values used to match with record's attributes.

collection.every( iteratee : Predicate, context? ) : boolean

Return true if all records match the predicate.

collection.some( iteratee : Predicate, context? ) : boolean

Return true if at least one record matches the predicated.

collection.push( record, options? )

Add a record at the end of a collection. Takes the same options as add().

collection.pop( options? )

Remove and return the last record from a collection. Takes the same options as remove().

collection.unshift( record, options? )

Add a record at the beginning of a collection. Takes the same options as add().

collection.shift( options? )

Remove and return the first record from a collection. Takes the same options as remove().

Backbone API

Common options used by Backbone API methods:

callback collection.initialize( records?, options? )

Initialization function which is called at the end of the constructor.

collection.clone()

Clone the collection. An aggregating collection will be recursively cloned, non-aggregated collections will be shallow cloned.

collection.models

Raw access to the JavaScript array of records inside of the collection. Usually, you'll want to use get, at, or the other methods to access record objects, but occasionally a direct reference to the array is desired.

collection.get( id )

Get a record from a collection, specified by an id, a cid, or by passing in a record.

const book = library.get(110);

collection.at( index )

Get a record from a collection, specified by index. Useful if your collection is sorted, and if your collection isn't sorted, at will still retrieve records in insertion order. When passed a negative index, it will retrieve the record from the back of the collection.

collection.add( records, options? )

Add a record (or an array of records) to the collection. If this is the Record.Collection, you may also pass raw attributes objects, and have them be vivified as instances of the Record. Returns the added (or preexisting, if duplicate) records.

Pass {at: index} to splice the record into the collection at the specified index. If you're adding records to the collection that are already in the collection, they'll be ignored, unless you pass {merge: true}, in which case their attributes will be merged into the corresponding records.

  1. Trigger the one event per record:
    • add(record, collection, options) for each record added.
    • change(record, options) for each record changed (if the {merge: true} option is passed).
  2. Trigger the single event:
    • update(collection, options) if any records were added.
    • sort(collection, options) if an order of records was changed.
  3. Trigger changes event in case if any changes were made to the collection and objects inside.

collection.remove( records, options? )

Remove a record (or an array of records) from the collection, and return them. Each record can be a record instance, an id string or a JS object, any value acceptable as the id argument of collection.get.

  1. Trigger remove(record, collection, options) for each record removed.
  2. If any records were removed, trigger:
    • update(collection, options)
    • changes(collection, options).

collection.set( records, options? )

The set method performs a "smart" update of the collection with the passed list of records. If a record in the list isn't yet in the collection it will be added; if the record is already in the collection its attributes will be merged; and if the collection contains any records that aren't present in the list, they'll be removed. All of the appropriate "add", "remove", and "change" events are fired as this happens. Returns the touched records in the collection. If you'd like to customize the behavior, you can disable it with options: {remove: false}, or {merge: false}.

Events

  1. Trigger the one event per record:
    • add(record, collection, options) for each record added.
    • remove(record, collection, options) for each record removed.
    • change(record, options) for each record changed.
  2. Trigger the single event:
    • update(collection, options) if any records were added.
    • sort(collection, options) if an order of records was changed.
  3. Trigger changes event in case if any changes were made to the collection and objects inside.
const vanHalen = new Man.Collection([ eddie, alex, stone, roth ]);

vanHalen.set([ eddie, alex, stone, hagar ]);

// Fires a "remove" event for roth, and an "add" event for hagar.
// Updates any of stone, alex, and eddie's attributes that may have
// changed over the years.

collection.reset(records, options?)

Replace the collection's content with the new records. More efficient than collection.set, but does not send record-level events.

Calling collection.reset() without passing any records as arguments will empty the entire collection.

  1. Trigger event reset(collection, options).
  2. Trigger event changes(collection, options).

collection.pluck(attribute)

Pluck an attribute from each model in the collection. Equivalent to calling map and returning a single attribute from the iterator.

const users = new UserCollection([
  {name: "Curly"},
  {name: "Larry"},
  {name: "Moe"}
]);

const names = users.pluck("name");

alert(JSON.stringify(names));

Sorting

Type-R implements BackboneJS Collection sorting API with some extensions.

collection.sort(options?)

Force a collection to re-sort itself. You don't need to call this under normal circumstances, as a collection with a comparator will sort itself whenever a record is added. To disable sorting when adding a record, pass {sort: false} to add. Calling sort triggers a "sort" event on the collection.

By default, there is no comparator for a collection. If you define a comparator, it will be used to maintain the collection in sorted order. This means that as records are added, they are inserted at the correct index in collection.models.

Note that Type-R depends on the arity of your comparator function to determine between the two styles, so be careful if your comparator function is bound.

Collections with a comparator will not automatically re-sort if you later change record attributes, so you may wish to call sort after changing record attributes that would affect the order.

static comparator = 'attrName'

Maintain the collection in sorted order by the given record's attribute.

static comparator = x => number | string

Maintain the collection in sorted order according to the "sortBy" comparator function.

"sortBy" comparator functions take a record and return a numeric or string value by which the record should be ordered relative to others.

static comparator = (x, y) => -1 | 0 | 1

Maintain the collection in sorted order according to the "sort" comparator function.

"sort" comparator functions take two records and return -1 if the first record should come before the second, 0 if they are of the same rank and 1 if the first record should come after.

Note how even though all of the chapters in this example are added backward, they come out in the proper order:

@define class Chapter extends Record {
    static attributes = {
        page : Number,
        title : String
    }
}

var chapters = new Chapter.Collection();

chapters.comparator = 'page';

chapters.add({page: 9, title: "The End"});
chapters.add({page: 5, title: "The Middle"});
chapters.add({page: 1, title: "The Beginning"});

alert(chapters.map( x => x.title ));

Other methods

CollectionClass.from( models, options? )

Create CollectionClass from the array of models. Similar to direct collection creation, but supports additional option for strict data validation. If { strict : true } option is passed the validation will be performed and an exception will be thrown in case of an error.

Please note, that Type-R always performs type checks on assignments, convert types, and reject improper updates reporting it as an error. It won't, however, execute custom validation rules on every update as they are evaluated lazily. strict option will invoke custom validators and will throw on every error or warning instead of reporting them and continue.

// Validate the body of an incoming HTTP request.
// Throw an exception if validation fails.
const body = MyRequestBody.from( ctx.request.body, { parse : true, strict : true });
// Validate the body of an incoming HTTP request.
// Throw an exception if validation fails.
const body = MyRequestBody.from( ctx.request.body, { parse : true, strict : true });

collection.createSubset( records?, options? )

Create the collection which is a subset of a source collection serializable as an array of record ids. Takes the same arguments as the collection's constructor.

The created collection is an instance of subsetOf( sourceCollection, CollectionCtor ) attribute type (non-aggregating serializable collection).

collection.assignFrom( otherCollection )

Synchronize the state of the collection and its aggregation tree with other collection of the same type. Updates existing objects in place. Record in the collection is considered to be "existing" if it has the same id.

Equivalent to collection.set( otherCollection.models, { merge : true } ) and triggers similar events on change.

collection.dispose()

Dispose of the collection. An aggregating collection will recursively dispose of its records.

Observable Changes

Overview

Type-R implements deeply observable changes on the object graph constructed of records and collection.

All of the record and collection updates happens in a scope of the transaction followed by the change event. Every record or collection update operation opens implicit transaction. Several update operations can be groped to the single explicit transaction if executed in the scope of the obj.transaction() or col.updateEach() call.

@define class Author extends Record {
    static attributes = {
        name : ''
    }
}

@define class Book extends Record {
    static attributes = {
        name : '',
        datePublished : Date,
        author : Author
    }
}

const book = new Book();
book.on( 'change', () => console.log( 'Book is changed') );

// Implicit transaction, prints to the console
book.author.name = 'John Smith';

Record

Events mixin methods (7)

Record implements Events mixin.

event "change" ( record )

Triggered by the record at the end of the attributes update transaction in case if there were any changes applied.

event "change:attrName" ( record, value )

Triggered by the record during the attributes update transaction for every changed attribute.

attrDef : type( Type ).watcher( watcher )

Attach change:attr event listener to the particular record's attribute. watcher can either be the record's method name or the function ( newValue, attr ) => void. Watcher is always executed in the context of the record.

@define class User extends Record {
    static attributes = {
        name : type( String ).watcher( 'onNameChange' ),
        isAdmin : Boolean,
    }

    onNameChange(){
        // Cruel. But we need it for the purpose of the example.
        this.isAdmin = this.name.indexOf( 'Admin' ) >= 0;
    }
}

attrDef : type( Type ).changeEvents( false )

Turn off changes observation for nested records or collections.

Record automatically listens to change events of all nested records and collections, triggering appropriate change events for its attributes. This declaration turns it off for the specific attribute.

attrDef : type( Type ).events({ eventName : handler, ... })

Automatically manage custom event subscription for the attribute. handler is either the method name or the handler function.

record.changed

The changed property is the internal hash containing all the attributes that have changed during its last transaction. Please do not update changed directly since its state is internally maintained by set(). A copy of changed can be acquired from changedAttributes().

record.changedAttributes( attrs? )

Retrieve a hash of only the record's attributes that have changed during the last transaction, or false if there are none. Optionally, an external attributes hash can be passed in, returning the attributes in that hash which differ from the record. This can be used to figure out which portions of a view should be updated, or what calls need to be made to sync the changes to the server.

record.previous( attr )

During a "change" event, this method can be used to get the previous value of a changed attribute.

@define class Person extends Record{
    static attributes = {
        name: ''
    }
}

const bill = new Person({
  name: "Bill Smith"
});

bill.on("change:name", ( record, name ) => {
  alert( `Changed name from ${ bill.previous('name') } to ${ name }`);
});

bill.name = "Bill Jones";

record.previousAttributes()

Return a copy of the record's previous attributes. Useful for getting a diff between versions of a record, or getting back to a valid state after an error occurs.

Collection

All changes in the records cause change events in the collections they are contained in.

Subset collections is an exception; they don't observe changes of its elements by default.

Events mixin methods (7)

Collection implements Events mixin.

collection.transaction( fun )

Execute the sequence of updates in fun function in the scope of the transaction.

All collection updates occurs in the scope of transactions. Transaction is the sequence of changes which results in a single changes event.

Transaction can be opened either manually or implicitly with calling any of collection update methods. Any additional changes made to the collection or its items in event handlers will be executed in the scope of the original transaction, and won't trigger an additional changes events.

collection.updateEach( iteratee : ( val : Record, index ) => void, context? )

Similar to the collection.each, but wraps an iteration in a transaction. The single changes event will be emitted for the group of changes to the records made in updateEach.

static itemEvents = { eventName : handler, ... }

Subscribe for events from records. The hander is either the collection's method name, the handler function, or true.

When true is passed as a handler, the corresponding event will be triggered on the collection.

event "changes" (collection, options)

When collection has changed. Single event triggered when the collection has been changed.

event "reset" (collection, options)

When the collection's entire contents have been reset (reset() method was called).

event "update" (collection, options)

Single event triggered after any number of records have been added or removed from a collection.

event "sort" (collection, options)

When the collection has been re-sorted.

event "add" (record, collection, options)

When a record is added to a collection.

event "remove" (record, collection, options)

When a record is removed from a collection.

event "change" (record, options)

When a record inside of the collection is changed.

Events mixin

Type-R uses an efficient synchronous events implementation which is backward compatible with Backbone 1.1 Events API but is about twice faster in all major browsers. It comes in form of Events mixin and the Messenger base class.

Events is a mixin giving the object the ability to bind and trigger custom named events. Events do not have to be declared before they are bound, and may take passed arguments.

Both source and listener mentioned in method signatures must implement Events methods.

import { mixins, Events } from 'type-r'

@mixins( Events )
class EventfulClass {
    ...
}

source.trigger(event, arg1, arg2, ... )

Trigger callbacks for the given event, or space-delimited list of events. Subsequent arguments to trigger will be passed along to the event callbacks.

listener.listenTo(source, event, callback)

Tell an object to listen to a particular event on an other object. The advantage of using this form, instead of other.on(event, callback, object), is that listenTo allows the object to keep track of the events, and they can be removed all at once later on. The callback will always be called with object as context.

    view.listenTo(record, 'change', view.render );

listener.stopListening([source], [event], [callback])

Tell an object to stop listening to events. Either call stopListening with no arguments to have the object remove all of its registered callbacks ... or be more precise by telling it to remove just the events it's listening to on a specific object, or a specific event, or just a specific callback.

    view.stopListening(); // Unsubscribe from all events

    view.stopListening(record); // Unsubscribe from all events from the record

listener.listenToOnce(source, event, callback)

Just like listenTo(), but causes the bound callback to fire only once before being automatically removed.

source.on(event, callback, [context])

Bind a callback function to an object. The callback will be invoked whenever the event is fired. If you have a large number of different events on a page, the convention is to use colons to namespace them: poll:start, or change:selection. The event string may also be a space-delimited list of several events...

    book.on("change:title change:author", ...);

Callbacks bound to the special "all" event will be triggered when any event occurs, and are passed the name of the event as the first argument. For example, to proxy all events from one object to another:

    proxy.on("all", function(eventName) {
        object.trigger(eventName);
    });

All event methods also support an event map syntax, as an alternative to positional arguments:

    book.on({
        "change:author": authorPane.update,
        "change:title change:subtitle": titleView.update,
        "destroy": bookView.remove
    });

To supply a context value for this when the callback is invoked, pass the optional last argument: record.on('change', this.render, this) or record.on({change: this.render}, this).

source.off([event], [callback], [context])

Remove a previously bound callback function from an object. If no context is specified, all of the versions of the callback with different contexts will be removed. If no callback is specified, all callbacks for the event will be removed. If no event is specified, callbacks for all events will be removed.

    // Removes just the `onChange` callback.
    object.off("change", onChange);

    // Removes all "change" callbacks.
    object.off("change");

    // Removes the `onChange` callback for all events.
    object.off(null, onChange);

    // Removes all callbacks for `context` for all events.
    object.off(null, null, context);

    // Removes all callbacks on `object`.
    object.off();

Note that calling record.off(), for example, will indeed remove all events on the record — including events that Backbone uses for internal bookkeeping.

source.once(event, callback, [context])

Just like on(), but causes the bound callback to fire only once before being removed. Handy for saying "the next time that X happens, do this". When multiple events are passed in using the space separated syntax, the event will fire once for every event you passed in, not once for a combination of all events

Built-in events

All Type-R objects implement Events mixin and use events to notify listeners on changes.

Record and Store change events:

Event name Handler arguments When triggered
change (record, options) At the end of any changes.
change:attrName (record, value, options) The record's attribute has been changed.

Collection change events:

Event name Handler arguments When triggered
changes (collection, options) At the end of any changes.
reset (collection, options) reset() method was called.
update (collection, options) Any records added or removed.
sort (collection, options) Order of records is changed.
add (record, collection, options) The record is added to a collection.
remove (record, collection, options) The record is removed from a collection.
change (record, options) The record is changed inside of collection.

Messenger class

Messenger is an abstract base class implementing Events mixin and some convenience methods.

import { define, Messenger } from 'type-r'

class MyMessenger extends Messenger {

}

Events mixin methods (7)

Messenger implements Events mixin.

messenger.cid

Unique run-time only messenger instance id (string).

callback messenger.initialize()

Callback which is called at the end of the constructor.

messenger.dispose()

Executes messenger.stopListening() and messenger.off().

Objects must be disposed to prevent memory leaks caused by subscribing for events from singletons.

Type Safety and Validation

Type-R records and collections are dynamically type safe. It's guaranteed that Type-R data structures will always conform to the declared shape. Records and collections convert values to the declared types on assignment, and reject an update (logging an error in a console) if it cannot be done.

In addition to that, Type-R supports validation API allowing developer to attach custom validation rules to attributes, records, and collections. Type-R validation mechanics based on following principles:

Attribute-level checks

attrDef : type( Type ).check( predicate, errorMsg? )

Attribute-level validator.

If errorMsg is omitted, error message will be taken from predicate.error. It makes possible to define reusable validation functions.

function isAge( years ){
    return years >= 0 && years < 200;
}

isAge.error = "Age must be between 0 and 200";

Attribute may have any number of checks attached which are being executed in a sequence. Validation stops when first check in sequence fails. It can be used to define reusable attribute types as demonstrated below:

// Define new attribute metatypes encapsulating validation checks.
const Age = type( Number )
                .check( x => x == null || x >= 0, 'I guess you are a bit older' )
                .check( x => x == null || x < 200, 'No way man can be that old' );

const Word = type( String ).check( x => indexOf( ' ' ) < 0, 'No spaces allowed' );

@define class Person extends Record {
    static attributes = {
        firstName : Word,
        lastName : Word,
        age : Age
    }
}

attrDef : type( Type ).required

The special case of attribute-level check cutting out empty values. Attribute value must be truthy to pass, "Required" is used as validation error.

isRequired is the first validator to check, no matter in which order validators were attached.

Record

rec.isValid( attrName )

Returns true if the specified record's attribute is valid.

rec.getValidationError( attrName )

Return the validation error for the given attribute or null if it's valid.

Record and Collection

Record and Collection share the same validation API. key is the attribute name for the record and record's id/cid for the collection.

callback obj.validate()

Override this method in subclass to define object-level validation rules. Whatever is returned from validate() is treated as validation error.

obj.isValid()

Returns true if the object is valid. Has same effect as !object.validationError.

obj.isValid( key )

Returns true if the specified record's attribute or collection element is valid. key is an attribute's name for the record or record's id/cid for the collection.

obj.validationError

null if an object is valid, or the the ValidationError object with detailed information on validation results.

ValidationError object has following shape:

{
    error : /* as returned from collection.validate() */,

    // Members validation errors.
    nested : {
        // key is an attrName for the record, and record.cid for the collcation
        key : validationError,
        ...
    }
}

obj.getValidationError( key )

Return the validation error for the given attribute or collection's item. key is an attribute's name for the record or record's id/cid for the collection.

obj.eachValidationError( iteratee : ( error, key, obj ) => void )

Recursively traverse aggregation tree validation errors. key is null for the object-level validation error returned by obj.validate(). obj is the reference to the current object.

I/O and Serialization

Overview

Type-R implements generalized IO on top of the IOEndpoint interface, with JSON serialization handled by Record and Collection classes.

IOEndpoint defines the set of CRUD + list methods operating on raw JSON. Attachment of an endpoint to the record or collection enables I/O API. There are few endpoints bundled with Type-R, for instance memoryIO() which can be used for mock testing.

@define class User extends Record {
    static endpoint = memoryIO();

    static attributes = {
        name : '',
        email : ''
    }
}

const users = new User.Collection();
users
    .add({ name : 'John' })
    .save()
    .then( () => console.log( user.id );

I/O API

static endpoint

I/O endpoint declaration which should be used in Record or Collection definition to enable I/O API.

If an endpoint is defined for the MyRecord, it's automatically defined for the corresponding MyRecord.Collection as well.

attrDef : type( Type ).endpoint( endpoint )

Override or define an I/O endpoint for the specific record's attribute.

obj.getEndpoint()

Returns an object's IO endpoint. Normally, this is an endpoint which is defined in object's static endpoint = ... declaration, but it might be overridden by the parent's record using type( Type ).endpoint( ... ) attribute declaration.

@define class User extends Record {
    static endpoint = restfulIO( '/api/users' );
    ...
}

@define class UserRole extends Record {
    static endpoint = restfulIO( '/api/roles' );
    static attributes = {
        // Use the relative path '/api/roles/:id/users'
        users : type( User.Collection ).endpoint( restfulIO( './users' ) ),
        ...
    }
}

record.fetch( options? )

Asynchronously fetch the record using endpoint.read() method. Returns an abortable ES6 promise.

An endpoint must be defined for the record in order to use that method.

record.save( options? )

Asynchronously save the record using endpoint.create() (if there are no id) or endpoint.update() (if id is present) method. Returns an abortable ES6 promise.

An endpoint must be defined for the record in order to use that method.

record.destroy( options? )

Asynchronously destroy the record using endpoint.destroy() method. Returns an abortable ES6 promise. The record is removed from the aggregating collection upon the completion of the I/O request.

An endpoint must be defined for the record in order to use that method.

collection.fetch( options? )

Fetch the collection. Returns an abortable promise.

options accepts an optional liveUpdates parameter. When true, collection subscribes for the live updates when I/O is finished.

collection.liveUpdates( true | false )

Subscribe for the live data updates if an I/O endpoint supports it (subscribe()/unsubscribe() IOEndpoint methods).

obj.hasPendingIO()

Returns an abortable promise if there's any I/O pending with the object, or null otherwise.

Can be used to check for active I/O in progress or to abort pending I/O operation. Please note, that all pending I/O is aborted automatically when new I/O operation is started or an object is disposed. When I/O is aborted, the promise is rejected.

const promise = users.hasPendingIO();
if( promise && promise.abort ) promise.abort();

I/O endpoints

restfulIO( url, options? )

HTTP REST client endpoint. Requires window.fetch available natively or through the polyfill. Implements standard BackboneJS REST semantic.

All I/O methods append an optional options.params object to the URL parameters translating them to string with JSON.stringify().

Supports URI relative to owner (./relative/url resolves as /owner/:id/relative/url/:id ).

import { restfulIO } from 'type-r/endpoints/restful'

@define class Role extends Record {
    static endpoint = restfulIO( '/api/roles' );
    ...
}

@define class User extends Record {
    static endpoint = restfulIO( '/api/users' );

    static attributes = {
        // Roles collection here has relative url /api/users/:user_id/roles/
        roles : type( Role.Collection ).endpoint( restfulIO( './roles' ) ), 
        ...
    }
}

memoryIO( mockData?, delay? )

Endpoint for mock testing. Takes optional array with mock data, and optional delay parameter which is the simulated I/O delay in milliseconds.

import { memoryIO } from 'type-r/endpoints/memory'

@define class User extends Record {
    static endpoint = memoryIO();
    ...
}

localStorageIO( key )

Endpoint for localStorage. Takes key parameter which must be unique for the persistent record's collection.

import { localStorageIO } from 'type-r/endpoints/localStorage'

@define class User extends Record {
    static endpoint = localStorageIO( '/users' );
    ...
}

attributesIO()

Endpoint for I/O composition. Redirects record's fetch() request to its attributes and returns the combined abortable promise. Does not enable any other I/O methods and can be used with record.fetch() only.

It's common pattern to use attributesIO endpoint in conjunction with Store to fetch all the data required by SPA page.

import { localStorageIO } from 'type-r/endpoints/attributes'

@define class PageStore extends Store {
    static endpoint = attributesIO();
    static attributes = {
        users : User.Collection,
        roles : UserRole.Collection,
    }
}
...
const store = new PageStore();
store.fetch().then( () => renderUI() );

proxyIO( RecordCtor )

Create IO endpoint from the Record class. This endpoint is designed for use on the server side with a data layer managed by Type-R.

Assuming that you have Type-R records with endpoints working with the database, you can create an endpoint which will use an existing Record subclass as a transport. This endpoint can be connected to the RESTful endpoint API on the server side which will serve JSON to the restfulIO endpoint on the client.

An advantage of this approach is that JSON schema will be transparently validated on the server side by the Type-R.

    import { proxyIO } from 'type-r/endpoint/proxy'

    ...

    const usersIO = proxyIO( User );

IOEndpoint Interface

An IO endpoint is an "plug-in" abstraction representing the persistent collection of JSON objects, which is required to enable records and collections I/O API. There are several pre-defined endpoints included in Type-R package which can be used for HTTP REST I/O, mock testing, working with localStorage, and IO composition.

You will need to define custom endpoint if you would like to implement or customize serialization transport for Type-R objects. Use built-in endpoints as an example and the starting boilerplate.

All IOEndpoint methods might return standard Promises or abortable promises (created with createIOPromise()). An IOEndpoint instance is shared by all of the class instances it's attached to and therefore it's normally must be stateless.

endpoint.read( id, options, record )

Reads an object with a given id. Used by record.fetch() method. Must return JSON wrapped in abortable promise.

endpoint.update( id, json, options, record )

Updates or creates an object with a given id. Used by record.save() method when record already has an id. Must return abortable promise.

endpoint.create( json, options, record )

Creates an object. Used by record.save() method when record does not have an id. Must return abortable promise.

endpoint.destroy( id, options, record )

Destroys the object with the given id. Used by record.destroy() method. Must return abortable promise.

endpoint.list( options, collection )

Fetch an array of objects. Used by collection.fetch() method. Must returns abortable promise.

endpoint.subscribe( callbacks, collection )

Optional method to enable the live updates subscription. Used by collection.liveUpdates( true ) method. Must returns abortable promise.

Method callbacks argument is an object of the following shape:

{
    // Endpoint must call it when an object is created or updated.
    updated( json ){}

    // Endpoint must call it when an object is removed.
    removed( json ){}
}

endpoint.unsubscribe( callbacks, collection )

Unsubscribe from the live updates. Used by collection.liveUpdates( false ) method. Takes the same callbacks object as subscribe().

createIOPromise( init )

Service function to create an abortable version of ES6 promise (with promise.abort() which meant to stop pending I/O and reject the promise).

init function takes the third onAbort argument to register an optional abort handler. If no handler is registered, the default implementation of promise.abort() will just reject the promise.

import { createIOPromise } from 'type-r'

const abortablePromise = createIOPromise( ( resolve, reject, onAbort ) =>{
    ...
    onAbort( () => {
        reject( 'I/O Aborted' );
    });
});

Serialization

Record and Collection has a portion of common API related to the I/O and serialization.

obj.toJSON( options? )

Serialize record or collection to JSON. Used internally by save() I/O method (options.ioMethod === 'save' when called from within save()). Can be overridden to customize serialization.

Produces the JSON for the given record or collection and its aggregated members. Aggregation tree is serialized as nested JSON. Record corresponds to an object in JSON, while the collection is represented as an array of objects.

If you override toJSON(), it usually means that you must override parse() as well, and vice versa.

@define class Comment extends Record {
    static attributes = {
        body : ''
    }
}

@define class BlogPost extends Record {
    static attributes = {
        title : '',
        body : '',
        comments : Comment.Collection
    }
}

const post = new BlogPost({
    title: "Type-R is cool!",
    comments : [ { body : "Agree" }]
});

const rawJSON = post.toJSON()
// { title : "Type-R is cool!", body : "", comments : [{ body : "Agree" }] }

option { parse : true }

obj.set() and constructor's option to force parsing of the raw JSON. Is used internally by I/O methods to parse the data received from the server.

// Another way of doing the bestSeller.clone()
// Amazingly, this is guaranteed to work by default.
const book = new Book();
book.set( bestSeller.toJSON(), { parse : true } );

callback obj.parse( json, options? )

Optional hook called to transform the JSON when it's passes to the record or collection with set( json, { parse : true }) call. Used internally by I/O methods (options.ioMethod is either "save" or "fetch" when called from I/O method).

If you override toJSON(), it usually means that you must override parse() as well, and vice versa.

attrDef : type( Type ).toJSON( false )

Do not serialize the specific attribute.

attrDef : type( Type ).toJSON( ( value, name, options ) => json )

Override the default serialization for the specific record's attribute.

Attribute is not serialized when the function return undefined.

attrDef : type( Type ).parse( ( json, name ) => value )

Transform the data before it will be assigned to the record's attribute.

Invoked when the { parse : true } option is set.

// Define custom boolean attribute type which is serialized as 0 or 1.
const MyWeirdBool = type( Boolean )
                      .parse( x => x === 1 )
                      .toJSON( x => x ? 1 : 0 );

static create( attrs, options )

Static factory function used internally by Type-R to create instances of the record.

May be redefined in the abstract Record base class to make it serializable type.

@define class Widget extends Record {
    static attributes = {
        type : String
    }

    static create( attrs, options ){
        switch( attrs.type ){
            case "typeA" : return new TypeA( attrs, options );
            case "typeB" : return new TypeB( attrs, options );
        }
    }
}

@define class TypeA extends Widget {
    static attributes = {
        type : "typeA",
        ...
    }
}

@define class TypeB extends Widget {
    static attributes = {
        type : "typeB",
        ...
    }
}

Normalized data

Type-R has first-class support for working with normalized data represented as a set of collections with cross-references by record id. References are represented as record ids in JSON, and being transparently resolved to record instances on the first access.

Store class is the special record class which serves as a placeholder for the set of interlinked collections of normalized records. Id-references are defined as record attributes of the special type representing the serializable reference to the records from the specified master collection.

attrDef : memberOf( sourceCollection )

Serializable reference to the record from the particular collection. Initialized as null and serialized as record.id. Is not recursively cloned, validated, or disposed. Used to model one-to-many relationships.

Changes in shared record are not detected.

sourceCollection may be:

    @define class State extends Record {
        static attributes = {
            items : Item.Collection,
            selected : memberOf( 'items' ) // Will resolve to `this.items`
        }
    }
    @define class State extends Record {
        @type( Item.Collection ).as items : Collection<Item>;
        @memberOf( 'items' ).as selected : Item
    }

attrDef : subsetOf( sourceCollection, CollectionCtor? )

Serializable non-aggregating collection which is the subset of the existing collection. Serialized as an array of record ids. Used to model many-to-many relationships. CollectionCtor argument may be omitted unless you need it to be a sublass of the particular collection type.

The collection object itself is recursively created and cloned. However, its records are not aggregated by the collection thus they are not recursively cloned, validated, or disposed.

sourceCollection is the same reference as used by memberOf( sourceCollection ).

@define class Role extends Record {
    static attributes = {
        name : String,
        ...
    }
}

@define class User extends Record {
    static attributes = {
        name : String,
        roles : subsetOf( '~roles', Role.Collection )
    }
}

@define class UsersDirectory extends Store {
    static attributes = {
        roles : Role.Collection,
        users : User.Collection // `~roles` references will be resolved against this.roles
    }
}

sourceCollection.createSubset( records?, options? )

Create an instance of subsetOf( sourceCollection, CollectionCtor ) type (non-aggregating serializable collection) which is the subset of the given collection. Takes the same arguments as the collection's constructor.

class Store

Store is the special kind of record which serves as a root for id references.

For all records inside of the store's aggregation tree ~attrName will resolve to the attribute of the store class found with record.getStore() method. If there are no such an attribute in the store, the next available store upper in aggregation tree will be used (as regular records stores can be nested), or the default store if there are no one.

Store is the subclass of the Record. It's defined extending the Store abstract base class. It behaves as a regular record in most aspects.

store._defaultStore

Reference to the master store used for lookups if the current store doesn't have the required attribute and there are no other store found upper in the ownership chain.

Defaults to the Store.global. May be explicitly defined to create custom store lookup chains across the ownership hierarchy.

static Store.global

The default singleton store class. Is always the last store to lookup when resolving ~reference.

Use the default store for the globally shared data only. Each application page must have its local store.

@define class MyStore extends Store {
    static attributes = {
        users : User.Collection,
        roles : Role.Collection
    }
}

Store.global = new MyStore();

// Now the reference '~users` will point to users collection from the MyStore.

recordOrCollection.getStore()

Return the closest store. Used internally to resolve symbolic ~reference relative to the store.

Method looks for the Store subclass traversing the ownership chain of current aggregation tree upwards. If there are no store found this way, default Store from Store.global is returned.

recordOrCollection.clone({ pinStore : true })

Make the cloned object to preserve the reference to its original store.

Cloned objects don't have an owner by default, thus they loose the reference to their store as no ownership chain can be traversed. pinStore option should be used in such a cases.

Tools

Logging

Type-r doesn't attempt to manage logs. Instead, it treat logs as an event stream and uses the logger singleton as a log router.

By default, the logger has the default listener writing events to the console.

log( level, topic, msg, props? )

Method used to trigger the log event. Same as logger.trigger( level, topic, msg, props? ).

The level corresponds to the logging methods of the console object: error, warn, info, log, debug.

topic is the short string used to denote the log source source and functional area. Type-R topics are prefixed with TR, and looks like TR:TypeError. If you want to use Type-R

import { log } from 'type-r'

log( 'error', 'client-api:users', 'No user with the given id', { user } );

logger.off()

import { logger } from 'type-r'

// Remove all the listeners
logger.off();

// Remove specific log level listeners (corresponds to the console methods, like console.log, console.warn, etc)
logger.off( 'warn' );

logger.throwOn( level )

Sometimes (for instance, in a test suite) developer would like Type-R to throw exceptions on type errors instead of the console warnings.

import { logger } from 'type-r'

logger.off().throwOn( 'error' ).throwOn( 'warn' );

Or, there might be a need to throw exceptions on error in the specific situation (e.g. throw if the incoming HTTP request is not valid to respond with 500 HTTP code).

import { Logger } from 'type-r'

async function processRequest( ... ){
    // Create an empty logger
    const logger = new Logger();

    // Tell it to throw exceptions.
    logger.throwOn( 'error' ).throwOn( 'warn' );

    // Override the default logger with option. Constructor will throw on error or warning.
    const request = new RequestBody( json, { parse : true, logger });
    ...
}

logger.on( level, handler )

Type-R log message is the regular event. It's easy to attach custom listeners to integrate third-party log management libraries.

import { logger } from 'type-r'

logger
    .off()
    .on( 'error', ( topic, msg, props ) => {
        // Log errors with bunyan
    } );

Class Definitions

Type-R mechanic is based on class transformations at the moment of module load. These transformations are controlled by definitions in static class members.

decorator @definitions({ propName : rule, ... })

Treat specified static class members as definitions. When @define decorator is being called, definitions are extracted from static class members and mixins and passed as an argument to the Class.onDefine( definition ).

Class definitions are intended to use in the abstract base classes and they are inherited by subclasses. You don't need to add any new definitions to existing Type-R classes unless you want to extend the library, which you're welcome to do.

rule mixinRules.value

Merge rule used to mark class definitions. The same rule is also applied to all mixin members if other rule is not specified.

@define
@definitions({
    urlRoot : mixinRules.value
})
class X {
    static urlRoot = '/api';

    static onDefine( definition ){
        this.prototype.urlRoot = definition.urlRoot;
    }
}

rule mixinRules.protoValue

Same as mixinRules.value, but the value is being assigned to the class prototype.

@define
@definitions({
    urlRoot : mixinRules.protoValue
})
class X {
    static urlRoot = '/api';
}

assert( X.prototype.urlRoot === '/api' );

rule mixinRules.merge

Assume the property to be the key-value hash. Properties with the same name from mixins are merged.

const M = {
    attributes : {
        b : 1
    }
};

@define
@mixins( M )
@definitions({
    attributes : mixinRules.merge
})
class X {
    static attributes = {
        a : 1
    };

    onDefine( definitions ){
        const { attributes } = definitions;
        assert( attributes.a === attributes.b === 1 );
    }
}

decorator @define

Extract class definitions, call class definition hooks, and apply mixin merge rules to inherited class members.

  1. Call static onExtend( BaseClass ) hook.
  2. Extract definitions from static class members and all the mixins applied, and pass them to onDefine( definitions, BaseClass ) hook.
  3. Apply merge rules for overriden class methods.

All Type-R class definitions must be precedeed with the @define (or @predefine) decorator.

@define
@definitions({
    attributes : mixinRules.merge
})
class Record {
    static onDefine( definitions, BaseClass ){
        definitions.attributes && console.log( JSON.stringify( definitions.attributes ) );
    }
}

// Will print "{ "a" : 1 }"
@define class A extends Record {
    static attributes = {
        a : 1
    }
}

// Will print "{ "b" : 1 }"
@define class B extends Record {
    static attributes = {
        b : 1
    }
}

decorator @define( mixin )

When called with an argument, @define decorator applies the given mixin as if it would be the first mixin applied. In other aspects, it behaves the same as the @default decorator without argument.

static Class.onExtend( BaseClass )

Called from the @predefine or as the first action of the @define. Takes base class constructor as an argument.

static Class.onDefine( definition, BaseClass )

Called from the @define or Class.define() method. Takes class definition (see the @definitions decorator) as the first argument.

decorator @predefine

The sequence of @predefine with the following Class.define() call is equivalent to @define decorator. It should be used in the case if the class definition must reference itself, or multiple definitions contain circular dependency.

It calls static onExtend( BaseClass ) function if it's defined. It assumes that the Class.define( definitions ) method will be called later, and attaches Class.define method to the class if it was not defined.

static Class.define( definitions? )

Finalized the class definition started with @predefine decorator. Has the same effect as the @define decorator excepts it assumes that Class.onExtend() static function was called already.

Mixins

decorator @mixins( mixinA, mixinB, ... ) class X ...

Merge specified mixins to the class definition. Both plain JS object and class constructor may be used as mixin. In the case of the class constructor, missing static members will copied over as well.

    import { mixins, Events } from 'type-r'
    ...

    @define
    @mixins( Events, plainObject, MyClass, ... )
    class X {
        ...
    }

static Class.mixins

Class member holding the state of the class mixins.

Merge rules

decorator @mixinRules({ propName : rule, ... })

The rule is the reducer function which is applied when there are several values for the particular class members are defined in different mixins or the class, or if the class member is overriden by the subclass.

rule mixinRules.classFirst

Assume the property to be the function. Call functions from mixins in sequence: f1.apply( this, arguments ); f2.apply( this, arguments );...

rule mixinRules.classLast

Same as sequence, but functions are called in the reverse sequence.

@define
@mixinRules({
    componentWillMount : mixinRules.classLast
})
class Component {
    componentWillMount(){
        console.log( 1 );
    }
}

const M = {
    componentWillMount(){
        console.log( 2 );
    }
}

@define
@mixins( M )
class X extends Component {
    componentWillMount(){
        console.log( 3 );
    }
}

const x = new X();
x.componentWillMount();
// Will print 1, 2, 3

rule mixinRules.pipe

Assume the property to be the function with a signature ( x : T ) => T. Join functions from mixins in a pipe: f1( f2( f3( x ) ) ).

rule mixinRules.defaults

Assume the property to be the function returning object. Merge objects returned by functions from mixins, executing them in sequence.

rule mixinRules.every

Assume property to be the function returning boolean. Return true if all functions from mixins return truthy values.

rule mixinRules.some

Same as every, but return true when at least one function from mixins returns true.

Release Notes

3.0.0

Breaking changes

Changed semantic which needs to be refactored:

2.x 3.x
Typeless attribute value(x) type(null).value(x)
Infer type from the value x (except functions) value(x), or x (except functions)
record.parse() override record._parse(json) no such a method, remove it
record attributes iteration record.forEachAttr(obj, iteratee) record.forEach(iteratee)
Shared object User.shared shared( User )
one-to-many relationship RecordClass.from( ref ) memberOf( ref )
many-to-many relationship CollectionClass.from( ref ) subsetOf( ref, CollectionClass? )
construct from object/array - RecordOrCollectionClass.from( json, options? )

New attribute definition notation

Starting from version 3.X, Type-R does not modify built-in global JS objects. New type(T) attribute definition notation is introduced to replace T.has.

There's type-r/globals package for compatibility with version 2.x which must be imported once with import 'type-r/globals'. If this package is not used, the code must be refactored according to the rules below.

2.x 3.x
UNIX Timestamp Date.timestamp import { Timestamp } from 'type-r/ext-types'
Microsoft date Date.microsoft import { MicrosoftDate } from 'type-r/ext-types'
Integer Integer and Number.integer import { Integer } from 'type-r/ext-types'
Create metatype from constructor Ctor.has type(Ctor)
Typed attribute with default value Ctor.value(default) type(Ctor).value(default)
Attribute "Required" check Ctor.isRequired type(Ctor).required

First-class TypeScript support

TypeScript attributes definitions:

2.x 3.x
Extract Type-R type with Reflect.metadata @attr name : T @auto name : T
Extract Type-R type & specify the default value not possible @auto(default) name : T
Explicitly specify the type @attr(T) name : T @type(T).as name : T
Infer Type-R type from default value @attr(default) name : T @value(default).as name : T
Specify type and default value @attr(T.value(default)) name : T @type(T).value(default).as name : T

Other improvements

@define class User extends Record {
    // There's an HTTP REST enpoint for users.
    static endpoint = restfulIO( '/api/users' );

    @auto name : string

    // Collection of Role records represented as an array of role.id in JSON.
    // When the "roles" attribute will be accessed for the first time,
    // User will look-up for a 'roles' attribute of the nearest store to resolve ids to actual Users.
    @subsetOf( '~roles' ).as roles : Collection<Role>
}

@define class Role extends Record {
    static endpoint = restfulIO( '/api/roles' );
    @auto name : string
}

// Store is the regular Record, nothing special.
@define class UsersDirectory extends Store {
    // When this record is fetched, fetch all the attributes instead.
    static endpoint = attributesIO();

    // '~roles' references from all aggregated collections
    // will point to here, because this is the nearest store.
    @type( User.Collection ).as users : Collection<User>
    @type( Role.Collection ).as roles : Collection<Role>
}

const directory = new UsersDirectory();
await directory.fetch();

for( let user of directory.users ){
    assert( user.roles.first().users.first() instanceOf User );
}

2.1.0

This release adds long-awaited HTTP REST endpoint.

2.0.0

This release brings new features which fixes problems with component's inheritance in React bindings and implements long-awaited generic IO implementation based on ES6 promises.

There shouldn't be breaking changes unless you're using custom logger or React bindings (formerly known as React-MVx, with a name changed to React-R in new release).

Generic IO support

New IOEndpoint concept is introduced, making it easy to create IO abstractions. To enable Record and Collection IO API, you need to assign IO endpoint in the class definition.

Endpoint is the class defining CRUD and list operations on JSON data, as well as the methods to subscribe for the data changes. There are two endpoints included with 2.0 release, memoryIO which is suitable for mock testing and localStorageIO which could be used in demos and prototypes. They can be used as a references as starting points to define your own IO endpoints.

@define class User extends Record {
    static endpoint = memoryIO();
    static attributes = {
        name : String,
        ...
    }
}

There are three Record IO methods (save(), fetch(), and destroy()) and two collection IO method (fetch() and liveUpdates()) ). All IO methods returns ES6 promises, so you either must have the runtime supporting ES6 or use the ES6 promise polyfill. The promises are modified to be abortable (all of them have abort() method).

const user = new User({ name : 'John' });
user.save().then( () => {
    console.log( `new user is added ${ user.id }` )
});

There's the special attributesIO() endpoint to fetch all of attributes independently and return the combined promise. This is the recommended way of fetching the data required by SPA page.

@define class PageStore extends Store {
    static endpoint = attributesIO();
    static attributes = {
        users : User.Collection,
        roles : UserRole.Collection,
        ...
    }
}

const store = new PageStore();
store.fetch().then( () =>{
    // render your page
});

It's possible to define or override the defined endpoint for the nested model or collection using type().endpoint() type-R attribute annotation.

@define class PageStore extends Store {
    static endpoint = attributesIO();
    static attributes = {
        users : type( User.Collection ).endpoint( restful( '/api/users' ) ),
        roles : type( UserRole.Collection ).endpoint( restful( '/api/userroles' ) ),
        ...
    }
}

New mixins engine

Type-R metaprogramming system built on powerful mixins composition with configurable member merge rules. In 2.0 release, mixins engine was rewritten to properly apply merge rules on inheritance. This feature is heavily used in Type-R React's bindings and is crucial to prevent errors when extending the React.Component subclasses.

An example illustrating the principle:

@define
// Define the class with 
@mixinRules({
    componentWillMount : mixinRules.classLast,
    componentWillUnmount : mixinRules.classFirst
})
class Component {
    componentWillMount(){
        console.log( 1 );
    }

    componentWillUnmount(){
        console.log( 3 );
    }
}

@define
@mixins({
    componentWillMount(){
        console.log( 2 );
    },

    componentWillUnmount(){
        console.log( 2 );
    }
})
class MyBaseComponent extends Component {
    componentWillMount(){
        console.log( 3 );
    }

    componentWillUnmount(){
        console.log( 1 );
    }
}

In this example, all of the methods defined in the mixin, base class, and subclass will be called in the order specified in the console.log.

Other changes